@skillcdn/console 0.4.4 → 0.4.5
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 +3 -3
- package/dist/api.d.ts +3 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +3 -3
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +162 -15
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +16 -3
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +68 -19
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +26 -5
- package/dist/cli/help.js.map +1 -1
- package/dist/client.d.ts +28 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +12 -2
- package/dist/client.js.map +1 -1
- package/dist/compat.js +1 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +46 -15
- package/dist/components/board.js.map +1 -1
- package/dist/components/decision-form.d.ts +0 -3
- package/dist/components/decision-form.d.ts.map +1 -1
- package/dist/components/decision-form.js +3 -15
- package/dist/components/decision-form.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +30 -10
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/my-decisions.d.ts +20 -3
- package/dist/components/my-decisions.d.ts.map +1 -1
- package/dist/components/my-decisions.js +14 -1
- package/dist/components/my-decisions.js.map +1 -1
- package/dist/components/people.d.ts +43 -1
- package/dist/components/people.d.ts.map +1 -1
- package/dist/components/people.js +117 -9
- package/dist/components/people.js.map +1 -1
- package/dist/components/picker.d.ts +8 -0
- package/dist/components/picker.d.ts.map +1 -1
- package/dist/components/picker.js +2 -2
- package/dist/components/picker.js.map +1 -1
- package/dist/components/plugin-frame.d.ts +0 -1
- package/dist/components/plugin-frame.d.ts.map +1 -1
- package/dist/components/plugin-frame.js +22 -1
- package/dist/components/plugin-frame.js.map +1 -1
- package/dist/components/plugin-settings.d.ts +5 -1
- package/dist/components/plugin-settings.d.ts.map +1 -1
- package/dist/components/plugin-settings.js +143 -30
- package/dist/components/plugin-settings.js.map +1 -1
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +3 -1
- package/dist/components/projects.js.map +1 -1
- package/dist/components/searches.d.ts +11 -0
- package/dist/components/searches.d.ts.map +1 -0
- package/dist/components/searches.js +30 -0
- package/dist/components/searches.js.map +1 -0
- package/dist/components/task-form.d.ts.map +1 -1
- package/dist/components/task-form.js +6 -5
- package/dist/components/task-form.js.map +1 -1
- package/dist/components/ui.d.ts +16 -0
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +10 -0
- package/dist/components/ui.js.map +1 -1
- package/dist/console.css +362 -67
- package/dist/console.d.ts +2 -1
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +90 -14
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +14 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +36 -1
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +89 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +87 -2
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +87 -3
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +6 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +6 -0
- package/dist/limits.js.map +1 -1
- package/dist/plugin/args.d.ts +6 -1
- package/dist/plugin/args.d.ts.map +1 -1
- package/dist/plugin/args.js +16 -11
- package/dist/plugin/args.js.map +1 -1
- package/dist/plugin/bridge.d.ts +20 -1
- package/dist/plugin/bridge.d.ts.map +1 -1
- package/dist/plugin/bridge.js +28 -0
- package/dist/plugin/bridge.js.map +1 -1
- package/dist/plugin/capabilities.d.ts +1 -1
- package/dist/plugin/capabilities.d.ts.map +1 -1
- package/dist/plugin/capabilities.js +10 -0
- package/dist/plugin/capabilities.js.map +1 -1
- package/dist/plugin/context.d.ts +158 -13
- package/dist/plugin/context.d.ts.map +1 -1
- package/dist/plugin/define.d.ts +13 -1
- package/dist/plugin/define.d.ts.map +1 -1
- package/dist/plugin/define.js +17 -1
- package/dist/plugin/define.js.map +1 -1
- package/dist/plugin/limits.d.ts +4 -0
- package/dist/plugin/limits.d.ts.map +1 -1
- package/dist/plugin/limits.js +4 -0
- package/dist/plugin/limits.js.map +1 -1
- package/dist/plugin/manifest.d.ts +15 -0
- package/dist/plugin/manifest.d.ts.map +1 -1
- package/dist/plugin/manifest.js +61 -4
- package/dist/plugin/manifest.js.map +1 -1
- package/dist/plugin/rest-schemas.d.ts +44 -0
- package/dist/plugin/rest-schemas.d.ts.map +1 -1
- package/dist/plugin/rest-schemas.js +55 -3
- package/dist/plugin/rest-schemas.js.map +1 -1
- package/dist/plugin/testing.d.ts +15 -1
- package/dist/plugin/testing.d.ts.map +1 -1
- package/dist/plugin/testing.js +40 -4
- package/dist/plugin/testing.js.map +1 -1
- package/dist/plugin/web.d.ts +6 -0
- package/dist/plugin/web.d.ts.map +1 -1
- package/dist/plugin/web.js +10 -1
- package/dist/plugin/web.js.map +1 -1
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +1 -1
- package/dist/plugin.js.map +1 -1
- package/dist/router.d.ts +19 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +18 -0
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +11 -2
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +14 -1
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +367 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +62 -2
- 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 +6 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +7 -4
- package/docs/specs/plugins.md +47 -39
- package/docs/specs/rest.md +22 -5
- package/package.json +1 -1
- package/skills/building-a-plugin/SKILL.md +8 -6
- package/skills/working-the-board/SKILL.md +1 -1
- package/src/api.ts +17 -0
- package/src/cli/cli.ts +200 -12
- package/src/cli/format.ts +82 -19
- package/src/cli/help.ts +30 -5
- package/src/client.ts +70 -0
- package/src/compat.ts +1 -1
- package/src/components/board.tsx +83 -42
- package/src/components/decision-form.tsx +13 -32
- package/src/components/event-feed.tsx +36 -9
- package/src/components/my-decisions.tsx +50 -5
- package/src/components/people.tsx +394 -37
- package/src/components/picker.tsx +11 -2
- package/src/components/plugin-frame.tsx +23 -0
- package/src/components/plugin-settings.tsx +346 -137
- package/src/components/projects.tsx +14 -14
- package/src/components/searches.ts +47 -0
- package/src/components/task-form.tsx +23 -28
- package/src/components/ui.tsx +36 -0
- package/src/console.tsx +229 -40
- package/src/data.ts +53 -1
- package/src/i18n/en.ts +98 -2
- package/src/i18n/ko.ts +95 -3
- package/src/index.ts +11 -0
- package/src/limits.ts +6 -0
- package/src/plugin/args.ts +16 -11
- package/src/plugin/bridge.ts +45 -0
- package/src/plugin/capabilities.ts +11 -0
- package/src/plugin/context.ts +170 -10
- package/src/plugin/define.ts +34 -0
- package/src/plugin/limits.ts +4 -0
- package/src/plugin/manifest.ts +74 -3
- package/src/plugin/rest-schemas.ts +62 -5
- package/src/plugin/testing.ts +61 -5
- package/src/plugin/web.ts +17 -0
- package/src/plugin.ts +1 -0
- package/src/router.ts +25 -0
- package/src/routes.ts +17 -2
- package/src/schemas.ts +81 -2
- package/src/styles/console.css +362 -67
- package/src/vocabulary.ts +6 -0
|
@@ -66,7 +66,7 @@ The work happens where it happens: in the repository, the document, the tool. At
|
|
|
66
66
|
|
|
67
67
|
### Phase 4: Ask when a person must decide
|
|
68
68
|
|
|
69
|
-
A choice the body does not settle, a trade-off, a cost, a change of scope: `console ask "<question>" --option "<one>" --option "<another>" --body "<what they need to know>"`, with `--attach <file>` for a picture of what is to be chosen between, a screenshot of what was made; a picture the body names as `` is shown there ([ADR-0055](/docs/adr/0055-a-decision-carries-the-files-put-on-it-when-it-is-raised.md)). The decision is for the person you work for, who finds it among their decisions; `--to <login>` addresses it to another person of the project when the task or the rules name who decides such things ([ADR-0042](/docs/adr/0042-a-decision-is-addressed-to-a-person-its-raiser-unless-another-is-named.md)), and `--to` given again to several of them, any of whom may answer, the first answer ending it ([ADR-0057](/docs/adr/0057-a-decision-may-be-addressed-to-several-people-and-the-first-answer-ends-it.md)). The run waits for the answer; the command waits a minute and exits with `3` while it is still to come. Keep waiting with `console decision <id> --wait 100` (seconds), as often as needed, and meanwhile do only what does not depend on the answer. Act on the answer as given, which may be the person's own words rather than one of the options, marked as theirs; what was answered is not asked again. An answer that reaches you anywhere but the board is written on the decision in the person's name first (`console answer`, above) and acted on from there; a question the work overtook is answered the same way, with the person's word on it, however short. Once acted on, `console decision <id> --outcome "<what followed>"` makes the decision read as a record.
|
|
69
|
+
A choice the body does not settle, a trade-off, a cost, a change of scope: `console ask "<question>" --option "<one>" --option "<another>" --body "<what they need to know>"`, with `--attach <file>` for a picture of what is to be chosen between, a screenshot of what was made; a picture the body names as `` is shown there ([ADR-0055](/docs/adr/0055-a-decision-carries-the-files-put-on-it-when-it-is-raised.md)). The decision is for the person you work for, who finds it among their decisions; `--to <login>` addresses it to another person of the project when the task or the rules name who decides such things ([ADR-0042](/docs/adr/0042-a-decision-is-addressed-to-a-person-its-raiser-unless-another-is-named.md)), and `--to` given again to several of them, any of whom may answer, the first answer ending it ([ADR-0057](/docs/adr/0057-a-decision-may-be-addressed-to-several-people-and-the-first-answer-ends-it.md)). The run waits for the answer; the command waits a minute and exits with `3` while it is still to come. Keep waiting with `console decision <id> --wait 100` (seconds), as often as needed, and meanwhile do only what does not depend on the answer; the command finds a decision wherever it is, in the project it works in or, for a plugin's decision that waits in the plugin's own project, there ([ADR-0068](/docs/adr/0068-a-decision-reaches-whom-it-is-addressed-to-wherever-it-is-and-a-plugins-reaches-the-managers-above-them.md)). Act on the answer as given, which may be the person's own words rather than one of the options, marked as theirs; what was answered is not asked again. An answer that reaches you anywhere but the board is written on the decision in the person's name first (`console answer`, above) and acted on from there; a question the work overtook is answered the same way, with the person's word on it, however short. Once acted on, `console decision <id> --outcome "<what followed>"` makes the decision read as a record.
|
|
70
70
|
|
|
71
71
|
### Phase 5: Hand in
|
|
72
72
|
|
package/src/api.ts
CHANGED
|
@@ -108,6 +108,9 @@ export {
|
|
|
108
108
|
MAX_STREAMS_PER_PERSON,
|
|
109
109
|
MAX_SUMMARY_LENGTH,
|
|
110
110
|
MAX_TASKS_PER_PROJECT,
|
|
111
|
+
MAX_TEAM_KEY_LENGTH,
|
|
112
|
+
MAX_TEAM_NAME_LENGTH,
|
|
113
|
+
MAX_TEAMS,
|
|
111
114
|
MAX_TITLE_LENGTH,
|
|
112
115
|
MAX_TOKEN_DAYS,
|
|
113
116
|
MAX_TOKEN_NAME_LENGTH,
|
|
@@ -124,6 +127,8 @@ export {
|
|
|
124
127
|
CLAIM_PATH,
|
|
125
128
|
categoryPath,
|
|
126
129
|
connectPath,
|
|
130
|
+
decisionFilePath,
|
|
131
|
+
decisionPath,
|
|
127
132
|
isSignInFailure,
|
|
128
133
|
loginPath,
|
|
129
134
|
PROJECT_COLLECTIONS,
|
|
@@ -224,6 +229,7 @@ export {
|
|
|
224
229
|
type RestProjectRules,
|
|
225
230
|
type RestProjects,
|
|
226
231
|
type RestProvider,
|
|
232
|
+
type RestReachedDecision,
|
|
227
233
|
type RestReport,
|
|
228
234
|
type RestReportInput,
|
|
229
235
|
type RestRules,
|
|
@@ -245,6 +251,11 @@ export {
|
|
|
245
251
|
type RestTaskLink,
|
|
246
252
|
type RestTaskPatch,
|
|
247
253
|
type RestTasks,
|
|
254
|
+
type RestTeam,
|
|
255
|
+
type RestTeamInput,
|
|
256
|
+
type RestTeamMemberInput,
|
|
257
|
+
type RestTeamPatch,
|
|
258
|
+
type RestTeams,
|
|
248
259
|
type RestToken,
|
|
249
260
|
type RestTokenCreated,
|
|
250
261
|
type RestTokenInput,
|
|
@@ -324,6 +335,7 @@ export {
|
|
|
324
335
|
restProjectSchema,
|
|
325
336
|
restProjectsSchema,
|
|
326
337
|
restProviderSchema,
|
|
338
|
+
restReachedDecisionSchema,
|
|
327
339
|
restReportInputSchema,
|
|
328
340
|
restReportSchema,
|
|
329
341
|
restRulesInputSchema,
|
|
@@ -345,6 +357,11 @@ export {
|
|
|
345
357
|
restTaskPatchSchema,
|
|
346
358
|
restTaskSchema,
|
|
347
359
|
restTasksSchema,
|
|
360
|
+
restTeamInputSchema,
|
|
361
|
+
restTeamMemberInputSchema,
|
|
362
|
+
restTeamPatchSchema,
|
|
363
|
+
restTeamSchema,
|
|
364
|
+
restTeamsSchema,
|
|
348
365
|
restTokenCreatedSchema,
|
|
349
366
|
restTokenInputSchema,
|
|
350
367
|
restTokenSchema,
|
package/src/cli/cli.ts
CHANGED
|
@@ -33,6 +33,7 @@ import { PLUGIN_LIST_LIMIT } from "../plugin/limits.js";
|
|
|
33
33
|
import { fillRules, RULES_PATH, RULES_TITLE } from "../rules.js";
|
|
34
34
|
import {
|
|
35
35
|
hostOfLogin,
|
|
36
|
+
type RestAnswerInput,
|
|
36
37
|
type RestClaim,
|
|
37
38
|
type RestDecision,
|
|
38
39
|
type RestMember,
|
|
@@ -101,6 +102,7 @@ import {
|
|
|
101
102
|
formatPersonLine,
|
|
102
103
|
formatPersonWork,
|
|
103
104
|
formatPlugin,
|
|
105
|
+
formatPluginAsks,
|
|
104
106
|
formatPluginGrantLine,
|
|
105
107
|
formatPluginHelp,
|
|
106
108
|
formatPluginRecord,
|
|
@@ -115,6 +117,7 @@ import {
|
|
|
115
117
|
formatRunLine,
|
|
116
118
|
formatTask,
|
|
117
119
|
formatTaskLine,
|
|
120
|
+
formatTeamLine,
|
|
118
121
|
formatVersion,
|
|
119
122
|
formatVersionLine,
|
|
120
123
|
formatWork,
|
|
@@ -632,9 +635,14 @@ function theTwo(positionals: readonly string[], ask: string): readonly [string,
|
|
|
632
635
|
* The decision once answered, or as it stands when the wait is over. Each request asks the
|
|
633
636
|
* console to hold it for a while, so the answer arrives as soon as a person gives it.
|
|
634
637
|
*/
|
|
638
|
+
/** Whatever waits for a decision's answer: the project's client, or the decision wherever it is (ADR-0068). */
|
|
639
|
+
interface Waiter {
|
|
640
|
+
awaitDecision(id: string, waitSeconds: number): Promise<RestDecision>;
|
|
641
|
+
}
|
|
642
|
+
|
|
635
643
|
async function waitForAnswer(
|
|
636
644
|
io: CliIo,
|
|
637
|
-
project:
|
|
645
|
+
project: Waiter,
|
|
638
646
|
decision: RestDecision,
|
|
639
647
|
seconds: number,
|
|
640
648
|
): Promise<RestDecision> {
|
|
@@ -665,7 +673,7 @@ const stillWaiting = (id: string): string =>
|
|
|
665
673
|
async function settle(
|
|
666
674
|
io: CliIo,
|
|
667
675
|
session: Session,
|
|
668
|
-
project:
|
|
676
|
+
project: Waiter,
|
|
669
677
|
decision: RestDecision,
|
|
670
678
|
seconds: number,
|
|
671
679
|
shown = false,
|
|
@@ -1919,12 +1927,18 @@ async function pluginEnable(
|
|
|
1919
1927
|
answer(io, session, enabled, () =>
|
|
1920
1928
|
[
|
|
1921
1929
|
`Enabled ${enabled.key}${enabled.board === null ? "" : `, with the board project ${enabled.board.key}`}; on ${enabled.projects === "all" || enabled.projects === null ? "every project" : enabled.projects.join(", ") || "no project"}.`,
|
|
1930
|
+
...(enabled.needsTimeZone === true
|
|
1931
|
+
? [
|
|
1932
|
+
"The workspace has no time zone: the plugin's times of day run on UTC until an administrator sets one, with console workspace edit --time-zone <zone>.",
|
|
1933
|
+
]
|
|
1934
|
+
: []),
|
|
1922
1935
|
...(enabled.settings.length === 0
|
|
1923
1936
|
? []
|
|
1924
1937
|
: [
|
|
1925
1938
|
"Settings:",
|
|
1926
1939
|
...enabled.settings.map((setting) => formatPluginSettingLine(setting, me.language)),
|
|
1927
1940
|
]),
|
|
1941
|
+
...formatPluginAsks(enabled, me.language),
|
|
1928
1942
|
].join("\n"),
|
|
1929
1943
|
);
|
|
1930
1944
|
return EXIT.ok;
|
|
@@ -1999,7 +2013,10 @@ async function pluginSecret(
|
|
|
1999
2013
|
return EXIT.ok;
|
|
2000
2014
|
}
|
|
2001
2015
|
|
|
2002
|
-
/**
|
|
2016
|
+
/**
|
|
2017
|
+
* Grants a role of the plugin to a person, to the members of a project, to another plugin, to the
|
|
2018
|
+
* members of a team, or to everyone; an administrator's.
|
|
2019
|
+
*/
|
|
2003
2020
|
async function pluginGrant(
|
|
2004
2021
|
io: CliIo,
|
|
2005
2022
|
session: Session,
|
|
@@ -2009,14 +2026,26 @@ async function pluginGrant(
|
|
|
2009
2026
|
): Promise<number> {
|
|
2010
2027
|
const { values, positionals } = parse(rest, {
|
|
2011
2028
|
person: { type: "string" },
|
|
2029
|
+
plugin: { type: "string" },
|
|
2030
|
+
team: { type: "string" },
|
|
2012
2031
|
everyone: { type: "boolean" },
|
|
2013
2032
|
});
|
|
2014
|
-
const usage = `console plugin ${client.key} grant <role> --person <login> | --project <key> | --everyone`;
|
|
2033
|
+
const usage = `console plugin ${client.key} grant <role> --person <login> | --project <key> | --plugin <key> | --team <key> | --everyone`;
|
|
2015
2034
|
const role = theOne(positionals, `Say which role: ${usage}`);
|
|
2016
2035
|
const login = text(values, "person");
|
|
2036
|
+
const other = text(values, "plugin");
|
|
2037
|
+
const teamKey = text(values, "team");
|
|
2017
2038
|
const everyone = on(values, "everyone");
|
|
2018
|
-
if (
|
|
2019
|
-
|
|
2039
|
+
if (
|
|
2040
|
+
[
|
|
2041
|
+
login !== undefined,
|
|
2042
|
+
projectKey !== undefined,
|
|
2043
|
+
other !== undefined,
|
|
2044
|
+
teamKey !== undefined,
|
|
2045
|
+
everyone,
|
|
2046
|
+
].filter(Boolean).length !== 1
|
|
2047
|
+
) {
|
|
2048
|
+
throw misuse(`Say whom, one of the five: ${usage}`);
|
|
2020
2049
|
}
|
|
2021
2050
|
let input: RestPluginGrantInput;
|
|
2022
2051
|
if (login !== undefined) {
|
|
@@ -2027,6 +2056,10 @@ async function pluginGrant(
|
|
|
2027
2056
|
input = { role, person: person.id };
|
|
2028
2057
|
} else if (projectKey !== undefined) {
|
|
2029
2058
|
input = { role, project: projectKey };
|
|
2059
|
+
} else if (other !== undefined) {
|
|
2060
|
+
input = { role, plugin: other };
|
|
2061
|
+
} else if (teamKey !== undefined) {
|
|
2062
|
+
input = { role, team: teamKey };
|
|
2030
2063
|
} else {
|
|
2031
2064
|
input = { role, everyone: true };
|
|
2032
2065
|
}
|
|
@@ -2679,8 +2712,7 @@ const answering: Command = async (args, io) => {
|
|
|
2679
2712
|
}
|
|
2680
2713
|
const note = await bodyOf(io, values, text(values, "note"));
|
|
2681
2714
|
const session = await open(io, values);
|
|
2682
|
-
const
|
|
2683
|
-
const answered = await project.answerDecision(id, {
|
|
2715
|
+
const answered = await answerReached(session, id, {
|
|
2684
2716
|
...(own === undefined ? { option } : { own }),
|
|
2685
2717
|
...(note === undefined ? {} : { note }),
|
|
2686
2718
|
});
|
|
@@ -2688,6 +2720,75 @@ const answering: Command = async (args, io) => {
|
|
|
2688
2720
|
return EXIT.ok;
|
|
2689
2721
|
};
|
|
2690
2722
|
|
|
2723
|
+
/** Whether a refusal says the decision is not in the project the command works in, or the project is not the person's. */
|
|
2724
|
+
const notInProject = (error: unknown): boolean =>
|
|
2725
|
+
error instanceof ApiError &&
|
|
2726
|
+
(error.code === "decision.not_found" || error.code === "project.not_found");
|
|
2727
|
+
|
|
2728
|
+
/**
|
|
2729
|
+
* One decision as the command reaches it (ADR-0068): in the project the command works in, as
|
|
2730
|
+
* before; else wherever it is, addressed to the person, raised by them, or a plugin's addressed
|
|
2731
|
+
* to someone they manage, through the console's own route for one decision. A plugin's decision
|
|
2732
|
+
* waits in the plugin's board project, which a checkout bound to another project does not name,
|
|
2733
|
+
* and which the person may not be in at all. A console from before that route answers as before.
|
|
2734
|
+
*/
|
|
2735
|
+
async function reachDecision(
|
|
2736
|
+
session: Session,
|
|
2737
|
+
id: string,
|
|
2738
|
+
): Promise<{ readonly decision: RestDecision; readonly waiter: Waiter }> {
|
|
2739
|
+
if ((await session.projectKey()) !== undefined) {
|
|
2740
|
+
const project = await session.project();
|
|
2741
|
+
try {
|
|
2742
|
+
return { decision: await project.decision(id), waiter: project };
|
|
2743
|
+
} catch (error) {
|
|
2744
|
+
if (!notInProject(error)) {
|
|
2745
|
+
throw error;
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
const anywhere = session.client;
|
|
2750
|
+
try {
|
|
2751
|
+
const reached = await anywhere.decision(id);
|
|
2752
|
+
return {
|
|
2753
|
+
decision: reached.decision,
|
|
2754
|
+
waiter: {
|
|
2755
|
+
awaitDecision: async (decisionId, seconds) =>
|
|
2756
|
+
(await anywhere.awaitDecision(decisionId, seconds)).decision,
|
|
2757
|
+
},
|
|
2758
|
+
};
|
|
2759
|
+
} catch (error) {
|
|
2760
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
2761
|
+
throw new ApiError(404, "decision.not_found", "The decision was not found.");
|
|
2762
|
+
}
|
|
2763
|
+
throw error;
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
/** Writes an answer on a decision as the command reaches it (ADR-0068): in the project, else wherever it is. */
|
|
2768
|
+
async function answerReached(
|
|
2769
|
+
session: Session,
|
|
2770
|
+
id: string,
|
|
2771
|
+
input: RestAnswerInput,
|
|
2772
|
+
): Promise<RestDecision> {
|
|
2773
|
+
if ((await session.projectKey()) !== undefined) {
|
|
2774
|
+
try {
|
|
2775
|
+
return await (await session.project()).answerDecision(id, input);
|
|
2776
|
+
} catch (error) {
|
|
2777
|
+
if (!notInProject(error)) {
|
|
2778
|
+
throw error;
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
try {
|
|
2783
|
+
return await session.client.answerDecision(id, input);
|
|
2784
|
+
} catch (error) {
|
|
2785
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
2786
|
+
throw new ApiError(404, "decision.not_found", "The decision was not found.");
|
|
2787
|
+
}
|
|
2788
|
+
throw error;
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2691
2792
|
const decision: Command = async (args, io) => {
|
|
2692
2793
|
const { values, positionals } = parse(args, {
|
|
2693
2794
|
wait: { type: "string" },
|
|
@@ -2701,19 +2802,19 @@ const decision: Command = async (args, io) => {
|
|
|
2701
2802
|
const seconds = secondsOf(text(values, "wait"), 0);
|
|
2702
2803
|
const outcome = await bodyOf(io, values, text(values, "outcome"));
|
|
2703
2804
|
const session = await open(io, values);
|
|
2704
|
-
const project = await session.project();
|
|
2705
2805
|
if (outcome !== undefined) {
|
|
2706
2806
|
// What followed the decision: the record grows, and is printed as it stands.
|
|
2807
|
+
const project = await session.project();
|
|
2707
2808
|
const grown = await project.updateDecision(id, { outcome });
|
|
2708
2809
|
answer(io, session, grown, () => formatDecision(grown));
|
|
2709
2810
|
return EXIT.ok;
|
|
2710
2811
|
}
|
|
2711
|
-
const found = await
|
|
2812
|
+
const { decision: found, waiter } = await reachDecision(session, id);
|
|
2712
2813
|
if (seconds === 0) {
|
|
2713
2814
|
answer(io, session, found, () => formatDecision(found));
|
|
2714
2815
|
return EXIT.ok;
|
|
2715
2816
|
}
|
|
2716
|
-
return settle(io, session,
|
|
2817
|
+
return settle(io, session, waiter, found, seconds);
|
|
2717
2818
|
};
|
|
2718
2819
|
|
|
2719
2820
|
const docs: Command = async (args, io) => {
|
|
@@ -3215,6 +3316,77 @@ const people: Command = async (args, io) => {
|
|
|
3215
3316
|
return EXIT.ok;
|
|
3216
3317
|
};
|
|
3217
3318
|
|
|
3319
|
+
/** The teams of the workspace (ADR-0073): each with who is in it, by login. */
|
|
3320
|
+
const teams: Command = async (args, io) => {
|
|
3321
|
+
const { values } = parse(args, {}, false);
|
|
3322
|
+
const session = await open(io, values);
|
|
3323
|
+
const [found, everyone] = await Promise.all([session.client.teams(), session.client.people()]);
|
|
3324
|
+
const loginOf = new Map(everyone.items.map((person) => [person.id, person.login]));
|
|
3325
|
+
answer(io, session, found, () =>
|
|
3326
|
+
found.items.length === 0
|
|
3327
|
+
? "No teams yet. console team new <key> <name> makes one."
|
|
3328
|
+
: found.items.map((team) => formatTeamLine(team, loginOf)).join("\n"),
|
|
3329
|
+
);
|
|
3330
|
+
return EXIT.ok;
|
|
3331
|
+
};
|
|
3332
|
+
|
|
3333
|
+
const TEAM_USAGE =
|
|
3334
|
+
'Say what to do: console team new <key> "<name>" | team rename <key> "<new name>" | team remove <key> | team add <key> <login> | team drop <key> <login>';
|
|
3335
|
+
|
|
3336
|
+
// A team is called by its key (ADR-0074), which never changes; its name is what people read.
|
|
3337
|
+
const team: Command = async (args, io) => {
|
|
3338
|
+
const what = args[0];
|
|
3339
|
+
if (what === "new") {
|
|
3340
|
+
const { values, positionals } = parse(args.slice(1), {});
|
|
3341
|
+
const [key, name] = theTwo(
|
|
3342
|
+
positionals,
|
|
3343
|
+
'Say the key and the name: console team new <key> "<name>"',
|
|
3344
|
+
);
|
|
3345
|
+
const session = await open(io, values);
|
|
3346
|
+
const made = await session.client.createTeam({ key, name });
|
|
3347
|
+
answer(io, session, made, () => formatTeamLine(made, new Map()));
|
|
3348
|
+
return EXIT.ok;
|
|
3349
|
+
}
|
|
3350
|
+
if (what === "rename") {
|
|
3351
|
+
const { values, positionals } = parse(args.slice(1), {});
|
|
3352
|
+
const [key, next] = theTwo(
|
|
3353
|
+
positionals,
|
|
3354
|
+
'Say which team and its new name: console team rename <key> "<new name>"',
|
|
3355
|
+
);
|
|
3356
|
+
const session = await open(io, values);
|
|
3357
|
+
const changed = await session.client.renameTeam(key, { name: next });
|
|
3358
|
+
answer(io, session, changed, () => formatTeamLine(changed, new Map()));
|
|
3359
|
+
return EXIT.ok;
|
|
3360
|
+
}
|
|
3361
|
+
if (what === "remove") {
|
|
3362
|
+
const { values, positionals } = parse(args.slice(1), {});
|
|
3363
|
+
const key = theOne(positionals, "Say which team: console team remove <key>");
|
|
3364
|
+
const session = await open(io, values);
|
|
3365
|
+
await session.client.removeTeam(key);
|
|
3366
|
+
answer(io, session, { removed: key }, () => `The team ${key} is no more.`);
|
|
3367
|
+
return EXIT.ok;
|
|
3368
|
+
}
|
|
3369
|
+
if (what === "add" || what === "drop") {
|
|
3370
|
+
const { values, positionals } = parse(args.slice(1), {});
|
|
3371
|
+
const [key, login] = theTwo(
|
|
3372
|
+
positionals,
|
|
3373
|
+
`Say which team and whom: console team ${what} <key> <login>`,
|
|
3374
|
+
);
|
|
3375
|
+
const session = await open(io, values);
|
|
3376
|
+
const person = await personOf(session, login);
|
|
3377
|
+
const changed =
|
|
3378
|
+
what === "add"
|
|
3379
|
+
? await session.client.addTeamMember(key, person.id)
|
|
3380
|
+
: await session.client.removeTeamMember(key, person.id);
|
|
3381
|
+
const everyone = await session.client.people();
|
|
3382
|
+
answer(io, session, changed, () =>
|
|
3383
|
+
formatTeamLine(changed, new Map(everyone.items.map((one) => [one.id, one.login]))),
|
|
3384
|
+
);
|
|
3385
|
+
return EXIT.ok;
|
|
3386
|
+
}
|
|
3387
|
+
throw misuse(TEAM_USAGE);
|
|
3388
|
+
};
|
|
3389
|
+
|
|
3218
3390
|
// The workspace's settings (ADR-0035): read by everyone, changed by an administrator, themselves
|
|
3219
3391
|
// on the Settings page or through their agent here (ADR-0021).
|
|
3220
3392
|
const WORKSPACE_FLAGS =
|
|
@@ -3291,7 +3463,7 @@ const workspace: Command = async (args, io) => {
|
|
|
3291
3463
|
};
|
|
3292
3464
|
|
|
3293
3465
|
const PERSON_USAGE =
|
|
3294
|
-
"Say what to do: console person role <login> <admin|member> | person manager <login> <manager login|none> | person add <login> [--host gh|google] | person remove <login> | person deactivate <login> | person reactivate <login>";
|
|
3466
|
+
"Say what to do: console person role <login> <admin|member> | person manager <login> <manager login|none> | person email <login> <address|none> | person add <login> [--host gh|google] | person remove <login> | person deactivate <login> | person reactivate <login>";
|
|
3295
3467
|
|
|
3296
3468
|
const person: Command = async (args, io) => {
|
|
3297
3469
|
const what = args[0];
|
|
@@ -3308,6 +3480,20 @@ const person: Command = async (args, io) => {
|
|
|
3308
3480
|
answer(io, session, changed, () => formatPersonLine(changed));
|
|
3309
3481
|
return EXIT.ok;
|
|
3310
3482
|
}
|
|
3483
|
+
if (what === "email") {
|
|
3484
|
+
const { values, positionals } = parse(args.slice(1), {});
|
|
3485
|
+
const [login, address] = theTwo(
|
|
3486
|
+
positionals,
|
|
3487
|
+
"Say whom and their address: console person email <login> <address|none>",
|
|
3488
|
+
);
|
|
3489
|
+
const session = await open(io, values);
|
|
3490
|
+
const found = await personOf(session, login);
|
|
3491
|
+
const changed = await session.client.updatePerson(found.id, {
|
|
3492
|
+
email: address === "none" ? null : address,
|
|
3493
|
+
});
|
|
3494
|
+
answer(io, session, changed, () => formatPersonLine(changed));
|
|
3495
|
+
return EXIT.ok;
|
|
3496
|
+
}
|
|
3311
3497
|
if (what === "manager") {
|
|
3312
3498
|
const { values, positionals } = parse(args.slice(1), {});
|
|
3313
3499
|
const [login, manager] = theTwo(
|
|
@@ -3769,6 +3955,8 @@ const COMMANDS: Readonly<Record<string, Command>> = {
|
|
|
3769
3955
|
plugin,
|
|
3770
3956
|
people,
|
|
3771
3957
|
person,
|
|
3958
|
+
teams,
|
|
3959
|
+
team,
|
|
3772
3960
|
workspace,
|
|
3773
3961
|
project,
|
|
3774
3962
|
members,
|
package/src/cli/format.ts
CHANGED
|
@@ -30,6 +30,7 @@ import type {
|
|
|
30
30
|
RestRulesPlugin,
|
|
31
31
|
RestRun,
|
|
32
32
|
RestTask,
|
|
33
|
+
RestTeam,
|
|
33
34
|
RestVersion,
|
|
34
35
|
RestVersionSummary,
|
|
35
36
|
RestWork,
|
|
@@ -123,9 +124,11 @@ export function formatProject(project: RestProject): string {
|
|
|
123
124
|
export function formatPersonLine(person: RestPerson, reportsTo?: string | undefined): string {
|
|
124
125
|
const standing = person.status === "active" ? person.role : `${person.role}, deactivated`;
|
|
125
126
|
const line = reportsTo === undefined ? "" : `, reports to ${reportsTo}`;
|
|
127
|
+
// The work email where the console answers one: to an administrator, and for oneself.
|
|
128
|
+
const email = typeof person.email === "string" ? ` <${person.email}>` : "";
|
|
126
129
|
return person.name === null
|
|
127
|
-
? `${person.login} (${standing}${line})`
|
|
128
|
-
: `${person.login} ${person.name} (${standing}${line})`;
|
|
130
|
+
? `${person.login}${email} (${standing}${line})`
|
|
131
|
+
: `${person.login} ${person.name}${email} (${standing}${line})`;
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
/** The provider a listing names, as people call it. */
|
|
@@ -637,12 +640,22 @@ export function formatPlugins(plugins: RestPlugins, language: string | null | un
|
|
|
637
640
|
].join("\n");
|
|
638
641
|
}
|
|
639
642
|
|
|
640
|
-
/**
|
|
641
|
-
|
|
643
|
+
/**
|
|
644
|
+
* How a command or a query of a plugin is given, as help prints it: the positionals, then the
|
|
645
|
+
* flags. In brief, for a listing that an agent reads whole at every first use, the positionals
|
|
646
|
+
* and the required flags alone, the rest as `[options]`, since `help <command>` has them all.
|
|
647
|
+
*/
|
|
648
|
+
export function formatOperationUsage(
|
|
649
|
+
key: string,
|
|
650
|
+
operation: RestPluginOperation,
|
|
651
|
+
{ brief = false }: { readonly brief?: boolean } = {},
|
|
652
|
+
): string {
|
|
642
653
|
const entries = Object.entries(operation.args);
|
|
654
|
+
const shown = brief ? entries.filter(([, arg]) => arg.positional || !arg.optional) : entries;
|
|
643
655
|
const usage = [
|
|
644
|
-
...
|
|
645
|
-
...
|
|
656
|
+
...shown.filter(([, arg]) => arg.positional).map(([name, arg]) => argUsage(name, arg)),
|
|
657
|
+
...shown.filter(([, arg]) => !arg.positional).map(([name, arg]) => argUsage(name, arg)),
|
|
658
|
+
...(brief && shown.length < entries.length ? ["[options]"] : []),
|
|
646
659
|
];
|
|
647
660
|
return `console plugin ${key} ${operation.name}${usage.map((part) => ` ${part}`).join("")}`;
|
|
648
661
|
}
|
|
@@ -703,11 +716,11 @@ export function formatPluginHelp(detail: RestPluginDetail, name: string | undefi
|
|
|
703
716
|
}
|
|
704
717
|
return [
|
|
705
718
|
...detail.operations.flatMap((operation) => [
|
|
706
|
-
formatOperationUsage(detail.key, operation),
|
|
719
|
+
formatOperationUsage(detail.key, operation, { brief: true }),
|
|
707
720
|
` ${operation.kind === "query" ? "[query] " : ""}${operation.about}`,
|
|
708
721
|
]),
|
|
709
722
|
"",
|
|
710
|
-
`One in full: console plugin ${detail.key} help <command>.`,
|
|
723
|
+
`One in full, with every option: console plugin ${detail.key} help <command>.`,
|
|
711
724
|
].join("\n");
|
|
712
725
|
}
|
|
713
726
|
const operation = detail.operations.find((candidate) => candidate.name === name);
|
|
@@ -750,7 +763,13 @@ export function formatPluginSettingLine(
|
|
|
750
763
|
const holds =
|
|
751
764
|
setting.type === "secret"
|
|
752
765
|
? setting.set
|
|
753
|
-
? `set${setting.setAt === null ? "" : ` ${moment(setting.setAt)}`}
|
|
766
|
+
? `set${setting.setAt === null ? "" : ` ${moment(setting.setAt)}`}${
|
|
767
|
+
setting.source === null ||
|
|
768
|
+
setting.source === undefined ||
|
|
769
|
+
setting.source === "administrator"
|
|
770
|
+
? ""
|
|
771
|
+
: ` from ${setting.source}`
|
|
772
|
+
}${setting.expiresAt === null || setting.expiresAt === undefined ? "" : `, expires ${moment(setting.expiresAt)}`}`
|
|
754
773
|
: "not set"
|
|
755
774
|
: setting.set
|
|
756
775
|
? setting.value === null
|
|
@@ -765,14 +784,25 @@ export function formatPluginSettingLine(
|
|
|
765
784
|
/** A grant in a line: its id, the role, whom it is to, since when. */
|
|
766
785
|
export function formatPluginGrantLine(grant: RestPluginGrant): string {
|
|
767
786
|
const whom =
|
|
768
|
-
grant.
|
|
769
|
-
? `
|
|
770
|
-
: grant.
|
|
771
|
-
? `the
|
|
772
|
-
:
|
|
787
|
+
grant.plugin !== null && grant.plugin !== undefined
|
|
788
|
+
? `the plugin ${grant.plugin}`
|
|
789
|
+
: grant.team !== null && grant.team !== undefined
|
|
790
|
+
? `the team ${grant.team.key}`
|
|
791
|
+
: grant.person !== null
|
|
792
|
+
? `person ${grant.person.login}`
|
|
793
|
+
: grant.project !== null
|
|
794
|
+
? `the members of ${grant.project.key}`
|
|
795
|
+
: "everyone";
|
|
773
796
|
return `${grant.id} ${grant.role} to ${whom} since ${moment(grant.createdAt)}`;
|
|
774
797
|
}
|
|
775
798
|
|
|
799
|
+
/** A team in a line (ADR-0073): its key (ADR-0074), its name, how many are in it, and their logins. */
|
|
800
|
+
export function formatTeamLine(team: RestTeam, loginOf: ReadonlyMap<string, string>): string {
|
|
801
|
+
const logins = team.memberIds.map((id) => loginOf.get(id) ?? id);
|
|
802
|
+
const count = team.memberIds.length === 1 ? "1 member" : `${team.memberIds.length} members`;
|
|
803
|
+
return `${team.key} ${team.name} ${count}${logins.length === 0 ? "" : `: ${logins.join(", ")}`}`;
|
|
804
|
+
}
|
|
805
|
+
|
|
776
806
|
/** One plugin in full: what it is, how it stands, what one may run, its skills and its settings. */
|
|
777
807
|
export function formatPlugin(
|
|
778
808
|
detail: RestPluginDetail,
|
|
@@ -811,11 +841,11 @@ export function formatPlugin(
|
|
|
811
841
|
lines.push("Commands and queries:");
|
|
812
842
|
for (const operation of detail.operations) {
|
|
813
843
|
lines.push(
|
|
814
|
-
` ${formatOperationUsage(detail.key, operation)}`,
|
|
844
|
+
` ${formatOperationUsage(detail.key, operation, { brief: true })}`,
|
|
815
845
|
` ${operation.kind === "query" ? "[query] " : ""}${operation.about}`,
|
|
816
846
|
);
|
|
817
847
|
}
|
|
818
|
-
lines.push(`One in full: console plugin ${detail.key} help <command>.`);
|
|
848
|
+
lines.push(`One in full, with every option: console plugin ${detail.key} help <command>.`);
|
|
819
849
|
}
|
|
820
850
|
if (detail.skills !== null) {
|
|
821
851
|
lines.push(
|
|
@@ -839,18 +869,51 @@ export function formatPlugin(
|
|
|
839
869
|
...detail.grants.map((grant) => ` ${formatPluginGrantLine(grant)}`),
|
|
840
870
|
);
|
|
841
871
|
}
|
|
872
|
+
lines.push(...formatPluginAsks(detail, language));
|
|
842
873
|
return lines.join("\n");
|
|
843
874
|
}
|
|
844
875
|
|
|
876
|
+
/**
|
|
877
|
+
* The roles the plugin asks of other plugins (ADR-0072), each with how it stands and, where not
|
|
878
|
+
* granted, the command that grants it; nothing where it asks none.
|
|
879
|
+
*/
|
|
880
|
+
export function formatPluginAsks(
|
|
881
|
+
detail: RestPluginDetail,
|
|
882
|
+
language: string | null | undefined,
|
|
883
|
+
): string[] {
|
|
884
|
+
const asks = detail.asks ?? [];
|
|
885
|
+
if (asks.length === 0) {
|
|
886
|
+
return [];
|
|
887
|
+
}
|
|
888
|
+
return [
|
|
889
|
+
"",
|
|
890
|
+
"Asks of other plugins:",
|
|
891
|
+
...asks.map((ask) => {
|
|
892
|
+
const standing =
|
|
893
|
+
ask.state === "granted"
|
|
894
|
+
? "granted"
|
|
895
|
+
: ask.state === "unavailable"
|
|
896
|
+
? `${ask.plugin} is not enabled`
|
|
897
|
+
: `not granted; console plugin ${ask.plugin} grant ${ask.role} --plugin ${detail.key}`;
|
|
898
|
+
return ` ${ask.role} of ${ask.plugin}: ${standing} ${localized(ask.why, language)}`;
|
|
899
|
+
}),
|
|
900
|
+
];
|
|
901
|
+
}
|
|
902
|
+
|
|
845
903
|
/** What a command or a query answered, as the command prints it. */
|
|
846
904
|
export function formatInvocation(result: RestPluginInvocation): string {
|
|
847
905
|
if (result.kind === "query") {
|
|
848
906
|
return result.data === undefined ? "null" : JSON.stringify(result.data, null, 2);
|
|
849
907
|
}
|
|
850
908
|
const text = result.text.length === 0 ? "Done." : result.text;
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
909
|
+
if (result.waiting === null) {
|
|
910
|
+
return text;
|
|
911
|
+
}
|
|
912
|
+
const where =
|
|
913
|
+
result.project === null || result.project === undefined
|
|
914
|
+
? ""
|
|
915
|
+
: ` in the project ${result.project}`;
|
|
916
|
+
return `${text}\ndecision id: ${result.waiting}\nThe decision waits for a person${where}: console decision ${result.waiting} --wait 100`;
|
|
854
917
|
}
|
|
855
918
|
|
|
856
919
|
/** A record's data on one line, cut where it runs long. */
|