@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.
Files changed (192) hide show
  1. package/README.md +3 -3
  2. package/dist/api.d.ts +3 -3
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +3 -3
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts.map +1 -1
  7. package/dist/cli/cli.js +162 -15
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts +16 -3
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +68 -19
  12. package/dist/cli/format.js.map +1 -1
  13. package/dist/cli/help.d.ts.map +1 -1
  14. package/dist/cli/help.js +26 -5
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/client.d.ts +28 -1
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +12 -2
  19. package/dist/client.js.map +1 -1
  20. package/dist/compat.js +1 -1
  21. package/dist/components/board.d.ts.map +1 -1
  22. package/dist/components/board.js +46 -15
  23. package/dist/components/board.js.map +1 -1
  24. package/dist/components/decision-form.d.ts +0 -3
  25. package/dist/components/decision-form.d.ts.map +1 -1
  26. package/dist/components/decision-form.js +3 -15
  27. package/dist/components/decision-form.js.map +1 -1
  28. package/dist/components/event-feed.d.ts.map +1 -1
  29. package/dist/components/event-feed.js +30 -10
  30. package/dist/components/event-feed.js.map +1 -1
  31. package/dist/components/my-decisions.d.ts +20 -3
  32. package/dist/components/my-decisions.d.ts.map +1 -1
  33. package/dist/components/my-decisions.js +14 -1
  34. package/dist/components/my-decisions.js.map +1 -1
  35. package/dist/components/people.d.ts +43 -1
  36. package/dist/components/people.d.ts.map +1 -1
  37. package/dist/components/people.js +117 -9
  38. package/dist/components/people.js.map +1 -1
  39. package/dist/components/picker.d.ts +8 -0
  40. package/dist/components/picker.d.ts.map +1 -1
  41. package/dist/components/picker.js +2 -2
  42. package/dist/components/picker.js.map +1 -1
  43. package/dist/components/plugin-frame.d.ts +0 -1
  44. package/dist/components/plugin-frame.d.ts.map +1 -1
  45. package/dist/components/plugin-frame.js +22 -1
  46. package/dist/components/plugin-frame.js.map +1 -1
  47. package/dist/components/plugin-settings.d.ts +5 -1
  48. package/dist/components/plugin-settings.d.ts.map +1 -1
  49. package/dist/components/plugin-settings.js +143 -30
  50. package/dist/components/plugin-settings.js.map +1 -1
  51. package/dist/components/projects.d.ts.map +1 -1
  52. package/dist/components/projects.js +3 -1
  53. package/dist/components/projects.js.map +1 -1
  54. package/dist/components/searches.d.ts +11 -0
  55. package/dist/components/searches.d.ts.map +1 -0
  56. package/dist/components/searches.js +30 -0
  57. package/dist/components/searches.js.map +1 -0
  58. package/dist/components/task-form.d.ts.map +1 -1
  59. package/dist/components/task-form.js +6 -5
  60. package/dist/components/task-form.js.map +1 -1
  61. package/dist/components/ui.d.ts +16 -0
  62. package/dist/components/ui.d.ts.map +1 -1
  63. package/dist/components/ui.js +10 -0
  64. package/dist/components/ui.js.map +1 -1
  65. package/dist/console.css +362 -67
  66. package/dist/console.d.ts +2 -1
  67. package/dist/console.d.ts.map +1 -1
  68. package/dist/console.js +90 -14
  69. package/dist/console.js.map +1 -1
  70. package/dist/data.d.ts +14 -1
  71. package/dist/data.d.ts.map +1 -1
  72. package/dist/data.js +36 -1
  73. package/dist/data.js.map +1 -1
  74. package/dist/i18n/en.d.ts +89 -0
  75. package/dist/i18n/en.d.ts.map +1 -1
  76. package/dist/i18n/en.js +87 -2
  77. package/dist/i18n/en.js.map +1 -1
  78. package/dist/i18n/ko.d.ts.map +1 -1
  79. package/dist/i18n/ko.js +87 -3
  80. package/dist/i18n/ko.js.map +1 -1
  81. package/dist/index.d.ts +3 -2
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +3 -2
  84. package/dist/index.js.map +1 -1
  85. package/dist/limits.d.ts +6 -0
  86. package/dist/limits.d.ts.map +1 -1
  87. package/dist/limits.js +6 -0
  88. package/dist/limits.js.map +1 -1
  89. package/dist/plugin/args.d.ts +6 -1
  90. package/dist/plugin/args.d.ts.map +1 -1
  91. package/dist/plugin/args.js +16 -11
  92. package/dist/plugin/args.js.map +1 -1
  93. package/dist/plugin/bridge.d.ts +20 -1
  94. package/dist/plugin/bridge.d.ts.map +1 -1
  95. package/dist/plugin/bridge.js +28 -0
  96. package/dist/plugin/bridge.js.map +1 -1
  97. package/dist/plugin/capabilities.d.ts +1 -1
  98. package/dist/plugin/capabilities.d.ts.map +1 -1
  99. package/dist/plugin/capabilities.js +10 -0
  100. package/dist/plugin/capabilities.js.map +1 -1
  101. package/dist/plugin/context.d.ts +158 -13
  102. package/dist/plugin/context.d.ts.map +1 -1
  103. package/dist/plugin/define.d.ts +13 -1
  104. package/dist/plugin/define.d.ts.map +1 -1
  105. package/dist/plugin/define.js +17 -1
  106. package/dist/plugin/define.js.map +1 -1
  107. package/dist/plugin/limits.d.ts +4 -0
  108. package/dist/plugin/limits.d.ts.map +1 -1
  109. package/dist/plugin/limits.js +4 -0
  110. package/dist/plugin/limits.js.map +1 -1
  111. package/dist/plugin/manifest.d.ts +15 -0
  112. package/dist/plugin/manifest.d.ts.map +1 -1
  113. package/dist/plugin/manifest.js +61 -4
  114. package/dist/plugin/manifest.js.map +1 -1
  115. package/dist/plugin/rest-schemas.d.ts +44 -0
  116. package/dist/plugin/rest-schemas.d.ts.map +1 -1
  117. package/dist/plugin/rest-schemas.js +55 -3
  118. package/dist/plugin/rest-schemas.js.map +1 -1
  119. package/dist/plugin/testing.d.ts +15 -1
  120. package/dist/plugin/testing.d.ts.map +1 -1
  121. package/dist/plugin/testing.js +40 -4
  122. package/dist/plugin/testing.js.map +1 -1
  123. package/dist/plugin/web.d.ts +6 -0
  124. package/dist/plugin/web.d.ts.map +1 -1
  125. package/dist/plugin/web.js +10 -1
  126. package/dist/plugin/web.js.map +1 -1
  127. package/dist/plugin.d.ts +1 -1
  128. package/dist/plugin.d.ts.map +1 -1
  129. package/dist/plugin.js +1 -1
  130. package/dist/plugin.js.map +1 -1
  131. package/dist/router.d.ts +19 -0
  132. package/dist/router.d.ts.map +1 -1
  133. package/dist/router.js +18 -0
  134. package/dist/router.js.map +1 -1
  135. package/dist/routes.d.ts +11 -2
  136. package/dist/routes.d.ts.map +1 -1
  137. package/dist/routes.js +14 -1
  138. package/dist/routes.js.map +1 -1
  139. package/dist/schemas.d.ts +367 -0
  140. package/dist/schemas.d.ts.map +1 -1
  141. package/dist/schemas.js +62 -2
  142. package/dist/schemas.js.map +1 -1
  143. package/dist/vocabulary.d.ts +1 -1
  144. package/dist/vocabulary.d.ts.map +1 -1
  145. package/dist/vocabulary.js +6 -0
  146. package/dist/vocabulary.js.map +1 -1
  147. package/docs/specs/cli.md +7 -4
  148. package/docs/specs/plugins.md +47 -39
  149. package/docs/specs/rest.md +22 -5
  150. package/package.json +1 -1
  151. package/skills/building-a-plugin/SKILL.md +8 -6
  152. package/skills/working-the-board/SKILL.md +1 -1
  153. package/src/api.ts +17 -0
  154. package/src/cli/cli.ts +200 -12
  155. package/src/cli/format.ts +82 -19
  156. package/src/cli/help.ts +30 -5
  157. package/src/client.ts +70 -0
  158. package/src/compat.ts +1 -1
  159. package/src/components/board.tsx +83 -42
  160. package/src/components/decision-form.tsx +13 -32
  161. package/src/components/event-feed.tsx +36 -9
  162. package/src/components/my-decisions.tsx +50 -5
  163. package/src/components/people.tsx +394 -37
  164. package/src/components/picker.tsx +11 -2
  165. package/src/components/plugin-frame.tsx +23 -0
  166. package/src/components/plugin-settings.tsx +346 -137
  167. package/src/components/projects.tsx +14 -14
  168. package/src/components/searches.ts +47 -0
  169. package/src/components/task-form.tsx +23 -28
  170. package/src/components/ui.tsx +36 -0
  171. package/src/console.tsx +229 -40
  172. package/src/data.ts +53 -1
  173. package/src/i18n/en.ts +98 -2
  174. package/src/i18n/ko.ts +95 -3
  175. package/src/index.ts +11 -0
  176. package/src/limits.ts +6 -0
  177. package/src/plugin/args.ts +16 -11
  178. package/src/plugin/bridge.ts +45 -0
  179. package/src/plugin/capabilities.ts +11 -0
  180. package/src/plugin/context.ts +170 -10
  181. package/src/plugin/define.ts +34 -0
  182. package/src/plugin/limits.ts +4 -0
  183. package/src/plugin/manifest.ts +74 -3
  184. package/src/plugin/rest-schemas.ts +62 -5
  185. package/src/plugin/testing.ts +61 -5
  186. package/src/plugin/web.ts +17 -0
  187. package/src/plugin.ts +1 -0
  188. package/src/router.ts +25 -0
  189. package/src/routes.ts +17 -2
  190. package/src/schemas.ts +81 -2
  191. package/src/styles/console.css +362 -67
  192. 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 `![words](its name)` 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 `![words](its name)` 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: ProjectClient,
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: ProjectClient,
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
- /** Grants a role of the plugin to a person, to the members of a project, or to everyone; an administrator's. */
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 ([login !== undefined, projectKey !== undefined, everyone].filter(Boolean).length !== 1) {
2019
- throw misuse(`Say whom, one of the three: ${usage}`);
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 project = await session.project();
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 project.decision(id);
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, project, found, seconds);
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
- /** How a command or a query of a plugin is given, as help prints it: the positionals, then the flags. */
641
- export function formatOperationUsage(key: string, operation: RestPluginOperation): string {
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
- ...entries.filter(([, arg]) => arg.positional).map(([name, arg]) => argUsage(name, arg)),
645
- ...entries.filter(([, arg]) => !arg.positional).map(([name, arg]) => argUsage(name, arg)),
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.person !== null
769
- ? `person ${grant.person.login}`
770
- : grant.project !== null
771
- ? `the members of ${grant.project.key}`
772
- : "everyone";
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
- return result.waiting === null
852
- ? text
853
- : `${text}\ndecision id: ${result.waiting}\nThe decision waits for a person: console decision ${result.waiting} --wait 100`;
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. */