@skillcdn/console 0.4.3 → 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 (268) hide show
  1. package/README.md +6 -6
  2. package/dist/api.d.ts +4 -4
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +4 -4
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts.map +1 -1
  7. package/dist/cli/cli.js +255 -33
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts +20 -5
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +98 -26
  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 +39 -15
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/cli/plugin-check.d.ts +5 -1
  17. package/dist/cli/plugin-check.d.ts.map +1 -1
  18. package/dist/cli/plugin-check.js +23 -4
  19. package/dist/cli/plugin-check.js.map +1 -1
  20. package/dist/cli/plugin-cli.d.ts.map +1 -1
  21. package/dist/cli/plugin-cli.js +10 -1
  22. package/dist/cli/plugin-cli.js.map +1 -1
  23. package/dist/cli/plugin-init.d.ts +2 -0
  24. package/dist/cli/plugin-init.d.ts.map +1 -1
  25. package/dist/cli/plugin-init.js +6 -0
  26. package/dist/cli/plugin-init.js.map +1 -1
  27. package/dist/client.d.ts +32 -1
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +14 -2
  30. package/dist/client.js.map +1 -1
  31. package/dist/compat.js +1 -1
  32. package/dist/components/board.d.ts +8 -0
  33. package/dist/components/board.d.ts.map +1 -1
  34. package/dist/components/board.js +49 -18
  35. package/dist/components/board.js.map +1 -1
  36. package/dist/components/decision-form.d.ts +6 -3
  37. package/dist/components/decision-form.d.ts.map +1 -1
  38. package/dist/components/decision-form.js +24 -18
  39. package/dist/components/decision-form.js.map +1 -1
  40. package/dist/components/decision-list.d.ts +5 -1
  41. package/dist/components/decision-list.d.ts.map +1 -1
  42. package/dist/components/decision-list.js +25 -5
  43. package/dist/components/decision-list.js.map +1 -1
  44. package/dist/components/event-feed.d.ts.map +1 -1
  45. package/dist/components/event-feed.js +34 -8
  46. package/dist/components/event-feed.js.map +1 -1
  47. package/dist/components/goals.d.ts.map +1 -1
  48. package/dist/components/goals.js +2 -2
  49. package/dist/components/goals.js.map +1 -1
  50. package/dist/components/home.d.ts +35 -0
  51. package/dist/components/home.d.ts.map +1 -0
  52. package/dist/components/home.js +326 -0
  53. package/dist/components/home.js.map +1 -0
  54. package/dist/components/my-decisions.d.ts +20 -3
  55. package/dist/components/my-decisions.d.ts.map +1 -1
  56. package/dist/components/my-decisions.js +14 -1
  57. package/dist/components/my-decisions.js.map +1 -1
  58. package/dist/components/pack-connect.d.ts.map +1 -1
  59. package/dist/components/pack-connect.js +5 -4
  60. package/dist/components/pack-connect.js.map +1 -1
  61. package/dist/components/people.d.ts +45 -1
  62. package/dist/components/people.d.ts.map +1 -1
  63. package/dist/components/people.js +125 -4
  64. package/dist/components/people.js.map +1 -1
  65. package/dist/components/picker.d.ts +8 -0
  66. package/dist/components/picker.d.ts.map +1 -1
  67. package/dist/components/picker.js +2 -2
  68. package/dist/components/picker.js.map +1 -1
  69. package/dist/components/plugin-frame.d.ts +3 -13
  70. package/dist/components/plugin-frame.d.ts.map +1 -1
  71. package/dist/components/plugin-frame.js +75 -21
  72. package/dist/components/plugin-frame.js.map +1 -1
  73. package/dist/components/plugin-settings.d.ts +5 -1
  74. package/dist/components/plugin-settings.d.ts.map +1 -1
  75. package/dist/components/plugin-settings.js +283 -45
  76. package/dist/components/plugin-settings.js.map +1 -1
  77. package/dist/components/projects.d.ts +0 -8
  78. package/dist/components/projects.d.ts.map +1 -1
  79. package/dist/components/projects.js +4 -14
  80. package/dist/components/projects.js.map +1 -1
  81. package/dist/components/searches.d.ts +11 -0
  82. package/dist/components/searches.d.ts.map +1 -0
  83. package/dist/components/searches.js +30 -0
  84. package/dist/components/searches.js.map +1 -0
  85. package/dist/components/task-form.d.ts.map +1 -1
  86. package/dist/components/task-form.js +6 -5
  87. package/dist/components/task-form.js.map +1 -1
  88. package/dist/components/task-view.d.ts.map +1 -1
  89. package/dist/components/task-view.js +2 -1
  90. package/dist/components/task-view.js.map +1 -1
  91. package/dist/components/ui.d.ts +16 -0
  92. package/dist/components/ui.d.ts.map +1 -1
  93. package/dist/components/ui.js +15 -2
  94. package/dist/components/ui.js.map +1 -1
  95. package/dist/connect-page.d.ts.map +1 -1
  96. package/dist/connect-page.js +5 -6
  97. package/dist/connect-page.js.map +1 -1
  98. package/dist/console.css +1123 -258
  99. package/dist/console.d.ts +12 -4
  100. package/dist/console.d.ts.map +1 -1
  101. package/dist/console.js +123 -27
  102. package/dist/console.js.map +1 -1
  103. package/dist/data.d.ts +18 -3
  104. package/dist/data.d.ts.map +1 -1
  105. package/dist/data.js +41 -1
  106. package/dist/data.js.map +1 -1
  107. package/dist/docs-page.d.ts.map +1 -1
  108. package/dist/docs-page.js +6 -5
  109. package/dist/docs-page.js.map +1 -1
  110. package/dist/home-tiles.d.ts +92 -0
  111. package/dist/home-tiles.d.ts.map +1 -0
  112. package/dist/home-tiles.js +294 -0
  113. package/dist/home-tiles.js.map +1 -0
  114. package/dist/i18n/en.d.ts +264 -1
  115. package/dist/i18n/en.d.ts.map +1 -1
  116. package/dist/i18n/en.js +260 -1
  117. package/dist/i18n/en.js.map +1 -1
  118. package/dist/i18n/index.d.ts +4 -2
  119. package/dist/i18n/index.d.ts.map +1 -1
  120. package/dist/i18n/index.js +10 -11
  121. package/dist/i18n/index.js.map +1 -1
  122. package/dist/i18n/ko.d.ts.map +1 -1
  123. package/dist/i18n/ko.js +255 -4
  124. package/dist/i18n/ko.js.map +1 -1
  125. package/dist/index.d.ts +7 -5
  126. package/dist/index.d.ts.map +1 -1
  127. package/dist/index.js +7 -5
  128. package/dist/index.js.map +1 -1
  129. package/dist/limits.d.ts +12 -0
  130. package/dist/limits.d.ts.map +1 -1
  131. package/dist/limits.js +12 -0
  132. package/dist/limits.js.map +1 -1
  133. package/dist/plugin/args.d.ts +6 -1
  134. package/dist/plugin/args.d.ts.map +1 -1
  135. package/dist/plugin/args.js +16 -11
  136. package/dist/plugin/args.js.map +1 -1
  137. package/dist/plugin/bridge.d.ts +63 -3
  138. package/dist/plugin/bridge.d.ts.map +1 -1
  139. package/dist/plugin/bridge.js +54 -0
  140. package/dist/plugin/bridge.js.map +1 -1
  141. package/dist/plugin/capabilities.d.ts +7 -1
  142. package/dist/plugin/capabilities.d.ts.map +1 -1
  143. package/dist/plugin/capabilities.js +24 -1
  144. package/dist/plugin/capabilities.js.map +1 -1
  145. package/dist/plugin/context.d.ts +174 -12
  146. package/dist/plugin/context.d.ts.map +1 -1
  147. package/dist/plugin/define.d.ts +14 -1
  148. package/dist/plugin/define.d.ts.map +1 -1
  149. package/dist/plugin/define.js +18 -2
  150. package/dist/plugin/define.js.map +1 -1
  151. package/dist/plugin/limits.d.ts +9 -2
  152. package/dist/plugin/limits.d.ts.map +1 -1
  153. package/dist/plugin/limits.js +9 -2
  154. package/dist/plugin/limits.js.map +1 -1
  155. package/dist/plugin/manifest.d.ts +98 -6
  156. package/dist/plugin/manifest.d.ts.map +1 -1
  157. package/dist/plugin/manifest.js +171 -10
  158. package/dist/plugin/manifest.js.map +1 -1
  159. package/dist/plugin/rest-schemas.d.ts +120 -0
  160. package/dist/plugin/rest-schemas.d.ts.map +1 -1
  161. package/dist/plugin/rest-schemas.js +78 -5
  162. package/dist/plugin/rest-schemas.js.map +1 -1
  163. package/dist/plugin/sandbox.d.ts +4 -3
  164. package/dist/plugin/sandbox.d.ts.map +1 -1
  165. package/dist/plugin/sandbox.js +33 -6
  166. package/dist/plugin/sandbox.js.map +1 -1
  167. package/dist/plugin/semver.d.ts +26 -0
  168. package/dist/plugin/semver.d.ts.map +1 -0
  169. package/dist/plugin/semver.js +76 -0
  170. package/dist/plugin/semver.js.map +1 -0
  171. package/dist/plugin/testing.d.ts +15 -1
  172. package/dist/plugin/testing.d.ts.map +1 -1
  173. package/dist/plugin/testing.js +42 -5
  174. package/dist/plugin/testing.js.map +1 -1
  175. package/dist/plugin/web.d.ts +16 -1
  176. package/dist/plugin/web.d.ts.map +1 -1
  177. package/dist/plugin/web.js +44 -1
  178. package/dist/plugin/web.js.map +1 -1
  179. package/dist/plugin.d.ts +3 -2
  180. package/dist/plugin.d.ts.map +1 -1
  181. package/dist/plugin.js +3 -2
  182. package/dist/plugin.js.map +1 -1
  183. package/dist/router.d.ts +19 -0
  184. package/dist/router.d.ts.map +1 -1
  185. package/dist/router.js +18 -0
  186. package/dist/router.js.map +1 -1
  187. package/dist/routes.d.ts +11 -2
  188. package/dist/routes.d.ts.map +1 -1
  189. package/dist/routes.js +14 -1
  190. package/dist/routes.js.map +1 -1
  191. package/dist/rules.d.ts +2 -2
  192. package/dist/rules.js +3 -3
  193. package/dist/schemas.d.ts +864 -2
  194. package/dist/schemas.d.ts.map +1 -1
  195. package/dist/schemas.js +156 -9
  196. package/dist/schemas.js.map +1 -1
  197. package/dist/use-action.d.ts +8 -2
  198. package/dist/use-action.d.ts.map +1 -1
  199. package/dist/use-action.js +13 -6
  200. package/dist/use-action.js.map +1 -1
  201. package/dist/vocabulary.d.ts +3 -1
  202. package/dist/vocabulary.d.ts.map +1 -1
  203. package/dist/vocabulary.js +9 -0
  204. package/dist/vocabulary.js.map +1 -1
  205. package/docs/specs/cli.md +10 -6
  206. package/docs/specs/plugins.md +83 -62
  207. package/docs/specs/rest.md +34 -16
  208. package/package.json +1 -1
  209. package/skills/building-a-plugin/SKILL.md +15 -12
  210. package/skills/planning-the-board/references/setting-up.md +2 -1
  211. package/skills/working-the-board/SKILL.md +3 -3
  212. package/src/api.ts +23 -0
  213. package/src/cli/cli.ts +323 -30
  214. package/src/cli/format.ts +122 -26
  215. package/src/cli/help.ts +43 -14
  216. package/src/cli/plugin-check.ts +41 -2
  217. package/src/cli/plugin-cli.ts +10 -1
  218. package/src/cli/plugin-init.ts +8 -0
  219. package/src/client.ts +78 -0
  220. package/src/compat.ts +1 -1
  221. package/src/components/board.tsx +86 -45
  222. package/src/components/decision-form.tsx +73 -36
  223. package/src/components/decision-list.tsx +45 -13
  224. package/src/components/event-feed.tsx +40 -7
  225. package/src/components/goals.tsx +4 -1
  226. package/src/components/home.tsx +615 -0
  227. package/src/components/my-decisions.tsx +50 -5
  228. package/src/components/pack-connect.tsx +5 -4
  229. package/src/components/people.tsx +500 -61
  230. package/src/components/picker.tsx +11 -2
  231. package/src/components/plugin-frame.tsx +85 -63
  232. package/src/components/plugin-settings.tsx +735 -185
  233. package/src/components/projects.tsx +14 -69
  234. package/src/components/searches.ts +47 -0
  235. package/src/components/task-form.tsx +23 -28
  236. package/src/components/task-view.tsx +7 -0
  237. package/src/components/ui.tsx +42 -2
  238. package/src/connect-page.tsx +5 -6
  239. package/src/console.tsx +303 -64
  240. package/src/data.ts +62 -3
  241. package/src/docs-page.tsx +6 -5
  242. package/src/home-tiles.ts +438 -0
  243. package/src/i18n/en.ts +274 -0
  244. package/src/i18n/index.ts +17 -14
  245. package/src/i18n/ko.ts +267 -4
  246. package/src/index.ts +18 -5
  247. package/src/limits.ts +12 -0
  248. package/src/plugin/args.ts +16 -11
  249. package/src/plugin/bridge.ts +122 -1
  250. package/src/plugin/capabilities.ts +28 -1
  251. package/src/plugin/context.ts +186 -9
  252. package/src/plugin/define.ts +38 -1
  253. package/src/plugin/limits.ts +9 -2
  254. package/src/plugin/manifest.ts +228 -13
  255. package/src/plugin/rest-schemas.ts +89 -6
  256. package/src/plugin/sandbox.ts +36 -8
  257. package/src/plugin/semver.ts +89 -0
  258. package/src/plugin/testing.ts +63 -6
  259. package/src/plugin/web.ts +66 -0
  260. package/src/plugin.ts +19 -0
  261. package/src/router.ts +25 -0
  262. package/src/routes.ts +17 -2
  263. package/src/rules.ts +3 -3
  264. package/src/schemas.ts +197 -10
  265. package/src/styles/console.css +1123 -258
  266. package/src/use-action.ts +16 -7
  267. package/src/vocabulary.ts +10 -0
  268. package/template/plugin/console-plugin.json +3 -1
package/src/cli/cli.ts CHANGED
@@ -21,12 +21,19 @@ import {
21
21
  formatVersion as versionText,
22
22
  } from "../compat.js";
23
23
  import { addDays, dayOf as dayOfDate, firstDayOfWeek, isDay, weekOf } from "../dates.js";
24
- import { DEFAULT_PAGE_LIMIT, LIST_LIMIT, MAX_OPTIONS, MIN_OPTIONS } from "../limits.js";
24
+ import {
25
+ DEFAULT_PAGE_LIMIT,
26
+ LIST_LIMIT,
27
+ MAX_DECIDERS,
28
+ MAX_OPTIONS,
29
+ MIN_OPTIONS,
30
+ } from "../limits.js";
25
31
  import { capabilityWords } from "../plugin/capabilities.js";
26
32
  import { PLUGIN_LIST_LIMIT } from "../plugin/limits.js";
27
33
  import { fillRules, RULES_PATH, RULES_TITLE } from "../rules.js";
28
34
  import {
29
35
  hostOfLogin,
36
+ type RestAnswerInput,
30
37
  type RestClaim,
31
38
  type RestDecision,
32
39
  type RestMember,
@@ -77,6 +84,7 @@ import {
77
84
  } from "./credentials.js";
78
85
  import { type DirectorySettings, type DirectoryStore, THE_SENTENCE } from "./directory.js";
79
86
  import {
87
+ deciderLogins,
80
88
  formatAnswer,
81
89
  formatCategoryLine,
82
90
  formatComment,
@@ -94,6 +102,7 @@ import {
94
102
  formatPersonLine,
95
103
  formatPersonWork,
96
104
  formatPlugin,
105
+ formatPluginAsks,
97
106
  formatPluginGrantLine,
98
107
  formatPluginHelp,
99
108
  formatPluginRecord,
@@ -108,6 +117,7 @@ import {
108
117
  formatRunLine,
109
118
  formatTask,
110
119
  formatTaskLine,
120
+ formatTeamLine,
111
121
  formatVersion,
112
122
  formatVersionLine,
113
123
  formatWork,
@@ -625,9 +635,14 @@ function theTwo(positionals: readonly string[], ask: string): readonly [string,
625
635
  * The decision once answered, or as it stands when the wait is over. Each request asks the
626
636
  * console to hold it for a while, so the answer arrives as soon as a person gives it.
627
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
+
628
643
  async function waitForAnswer(
629
644
  io: CliIo,
630
- project: ProjectClient,
645
+ project: Waiter,
631
646
  decision: RestDecision,
632
647
  seconds: number,
633
648
  ): Promise<RestDecision> {
@@ -658,7 +673,7 @@ const stillWaiting = (id: string): string =>
658
673
  async function settle(
659
674
  io: CliIo,
660
675
  session: Session,
661
- project: ProjectClient,
676
+ project: Waiter,
662
677
  decision: RestDecision,
663
678
  seconds: number,
664
679
  shown = false,
@@ -1526,6 +1541,8 @@ const plugin: Command = async (args, io) => {
1526
1541
  );
1527
1542
  return EXIT.ok;
1528
1543
  }
1544
+ case "capabilities":
1545
+ return pluginCapabilities(io, session, client, rest);
1529
1546
  case "settings":
1530
1547
  return pluginSettings(io, session, client, rest);
1531
1548
  case "secret":
@@ -1787,7 +1804,60 @@ function capabilityLines(capabilities: readonly string[]): string[] {
1787
1804
  });
1788
1805
  }
1789
1806
 
1790
- /** Enables a plugin, with consent to every capability it asks, named again; an administrator's. */
1807
+ /** The capabilities `--consent` names, given again or with commas between. */
1808
+ function capabilitiesNamed(values: Values): string[] {
1809
+ return texts(values, "consent")
1810
+ .flatMap((value) => value.split(","))
1811
+ .map((capability) => capability.trim())
1812
+ .filter((capability) => capability.length > 0);
1813
+ }
1814
+
1815
+ /** What a plugin requires, and which of what it can go without it holds, as `capabilities` prints it. */
1816
+ function capabilitiesHeld(detail: RestPluginDetail): string {
1817
+ const held = detail.consent ?? [];
1818
+ const optional = detail.optionalCapabilities ?? [];
1819
+ const named = (capabilities: readonly string[]) =>
1820
+ capabilities.length === 0 ? "none" : capabilities.join(", ");
1821
+ return [
1822
+ `Capabilities of ${detail.key}:`,
1823
+ ` required: ${named(detail.capabilities)}`,
1824
+ ` optional, on: ${named(optional.filter((capability) => held.includes(capability)))}`,
1825
+ ` optional, off: ${named(optional.filter((capability) => !held.includes(capability)))}`,
1826
+ ].join("\n");
1827
+ }
1828
+
1829
+ /**
1830
+ * What a plugin holds; with `--consent`, which of the optional capabilities it holds from now on,
1831
+ * `none` for none of them. An administrator's.
1832
+ */
1833
+ async function pluginCapabilities(
1834
+ io: CliIo,
1835
+ session: Session,
1836
+ client: PluginClient,
1837
+ rest: readonly string[],
1838
+ ): Promise<number> {
1839
+ const { values, positionals } = parse(rest, { consent: { type: "string", multiple: true } });
1840
+ if (positionals.length > 0) {
1841
+ throw misuse(
1842
+ `Nothing follows capabilities but --consent: console plugin ${client.key} capabilities [--consent <capability>,...|none]`,
1843
+ );
1844
+ }
1845
+ const detail = await client.get();
1846
+ const named = capabilitiesNamed(values);
1847
+ if (named.length === 0) {
1848
+ answer(io, session, detail, () => capabilitiesHeld(detail));
1849
+ return EXIT.ok;
1850
+ }
1851
+ const chosen = named.filter((capability) => capability !== "none");
1852
+ const changed = await client.updateConsent([
1853
+ ...detail.capabilities,
1854
+ ...chosen.filter((capability) => !detail.capabilities.includes(capability)),
1855
+ ]);
1856
+ answer(io, session, changed, () => capabilitiesHeld(changed));
1857
+ return EXIT.ok;
1858
+ }
1859
+
1860
+ /** Enables a plugin, with consent to every capability it requires, named again; an administrator's. */
1791
1861
  async function pluginEnable(
1792
1862
  io: CliIo,
1793
1863
  session: Session,
@@ -1807,19 +1877,22 @@ async function pluginEnable(
1807
1877
  }
1808
1878
  const [detail, me] = await Promise.all([client.get(), session.client.me()]);
1809
1879
  const asked = detail.capabilities;
1810
- const consent = texts(values, "consent")
1811
- .flatMap((value) => value.split(","))
1812
- .map((capability) => capability.trim())
1813
- .filter((capability) => capability.length > 0);
1880
+ const optional = detail.optionalCapabilities ?? [];
1881
+ const consent = capabilitiesNamed(values);
1814
1882
  if (asked.length > 0 && !session.json) {
1815
1883
  io.stdout(
1816
- `${detail.key} asks ${asked.length === 1 ? "one capability" : `${asked.length} capabilities`}:\n${capabilityLines(asked).join("\n")}\n`,
1884
+ `${detail.key} requires ${asked.length === 1 ? "one capability" : `${asked.length} capabilities`}:\n${capabilityLines(asked).join("\n")}\n`,
1885
+ );
1886
+ }
1887
+ if (optional.length > 0 && !session.json) {
1888
+ io.stdout(
1889
+ `It can go without these, which it holds if --consent names them too:\n${capabilityLines(optional).join("\n")}\n`,
1817
1890
  );
1818
1891
  }
1819
1892
  const missing = asked.filter((capability) => !consent.includes(capability));
1820
1893
  if (missing.length > 0) {
1821
1894
  throw misuse(
1822
- `Enabling ${detail.key} needs consent to every capability it asks, named again: --consent ${asked.join(",")}. Not named: ${missing.join(", ")}.`,
1895
+ `Enabling ${detail.key} needs consent to every capability it requires, named again: --consent ${asked.join(",")}. Not named: ${missing.join(", ")}.`,
1823
1896
  );
1824
1897
  }
1825
1898
  const boardFlag = text(values, "board");
@@ -1854,12 +1927,18 @@ async function pluginEnable(
1854
1927
  answer(io, session, enabled, () =>
1855
1928
  [
1856
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
+ : []),
1857
1935
  ...(enabled.settings.length === 0
1858
1936
  ? []
1859
1937
  : [
1860
1938
  "Settings:",
1861
1939
  ...enabled.settings.map((setting) => formatPluginSettingLine(setting, me.language)),
1862
1940
  ]),
1941
+ ...formatPluginAsks(enabled, me.language),
1863
1942
  ].join("\n"),
1864
1943
  );
1865
1944
  return EXIT.ok;
@@ -1934,7 +2013,10 @@ async function pluginSecret(
1934
2013
  return EXIT.ok;
1935
2014
  }
1936
2015
 
1937
- /** 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
+ */
1938
2020
  async function pluginGrant(
1939
2021
  io: CliIo,
1940
2022
  session: Session,
@@ -1944,14 +2026,26 @@ async function pluginGrant(
1944
2026
  ): Promise<number> {
1945
2027
  const { values, positionals } = parse(rest, {
1946
2028
  person: { type: "string" },
2029
+ plugin: { type: "string" },
2030
+ team: { type: "string" },
1947
2031
  everyone: { type: "boolean" },
1948
2032
  });
1949
- 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`;
1950
2034
  const role = theOne(positionals, `Say which role: ${usage}`);
1951
2035
  const login = text(values, "person");
2036
+ const other = text(values, "plugin");
2037
+ const teamKey = text(values, "team");
1952
2038
  const everyone = on(values, "everyone");
1953
- if ([login !== undefined, projectKey !== undefined, everyone].filter(Boolean).length !== 1) {
1954
- 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}`);
1955
2049
  }
1956
2050
  let input: RestPluginGrantInput;
1957
2051
  if (login !== undefined) {
@@ -1962,6 +2056,10 @@ async function pluginGrant(
1962
2056
  input = { role, person: person.id };
1963
2057
  } else if (projectKey !== undefined) {
1964
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 };
1965
2063
  } else {
1966
2064
  input = { role, everyone: true };
1967
2065
  }
@@ -2070,7 +2168,7 @@ const rules: Command = async (args, io) => {
2070
2168
 
2071
2169
  /**
2072
2170
  * The rules as they read for the person whose token the command holds: `{language}` filled
2073
- * with the language they chose for the pages, or with the task's when they chose none.
2171
+ * with the language they read in, or with the task's while they have none.
2074
2172
  */
2075
2173
  function forPerson<T extends { readonly body: string | null }>(
2076
2174
  half: T,
@@ -2530,16 +2628,19 @@ const ask: Command = async (args, io) => {
2530
2628
  file: { type: "string" },
2531
2629
  run: { type: "string" },
2532
2630
  wait: { type: "string" },
2533
- to: { type: "string" },
2631
+ to: { type: "string", multiple: true },
2534
2632
  attach: { type: "string", multiple: true },
2535
2633
  });
2536
2634
  const question = theOne(
2537
2635
  positionals,
2538
2636
  'Say the question: console ask "<question>" --option "<label>" --option "<label>"',
2539
2637
  );
2540
- const to = text(values, "to");
2541
- if (to === "none") {
2542
- throw misuse("Say whom the decision is for: --to <login|me>.");
2638
+ const tos = texts(values, "to");
2639
+ if (tos.includes("none")) {
2640
+ throw misuse("Say whom the decision is for: --to <login|me>, given again for several.");
2641
+ }
2642
+ if (tos.length > MAX_DECIDERS) {
2643
+ throw misuse(`Name up to ${MAX_DECIDERS} people with --to.`);
2543
2644
  }
2544
2645
  const options = texts(values, "option");
2545
2646
  if (options.length < MIN_OPTIONS || options.length > MAX_OPTIONS) {
@@ -2552,15 +2653,26 @@ const ask: Command = async (args, io) => {
2552
2653
  const session = await open(io, values);
2553
2654
  const project = await session.project();
2554
2655
  const runId = await currentRun(project, values);
2555
- // Whom the decision is for (ADR-0042): the person the agent works for, unless --to names another.
2556
- const deciderId = await personIdOf(session, to);
2656
+ // Whom the decision is for (ADR-0042, ADR-0057): the person the agent works for, unless --to
2657
+ // names others of the project, each once; several, and any of them may answer.
2658
+ const deciderIds: string[] = [];
2659
+ for (const to of tos) {
2660
+ const id = await personIdOf(session, to);
2661
+ if (typeof id === "string" && !deciderIds.includes(id)) {
2662
+ deciderIds.push(id);
2663
+ }
2664
+ }
2557
2665
  const raised = await project.raiseDecision(
2558
2666
  {
2559
2667
  question,
2560
2668
  options: options.map((label) => ({ label })),
2561
2669
  runId,
2562
2670
  ...(body === undefined ? {} : { body }),
2563
- ...(typeof deciderId === "string" ? { deciderId } : {}),
2671
+ ...(deciderIds.length === 0
2672
+ ? {}
2673
+ : deciderIds.length === 1
2674
+ ? { deciderId: deciderIds[0] }
2675
+ : { deciderId: deciderIds[0], deciderIds }),
2564
2676
  },
2565
2677
  files,
2566
2678
  );
@@ -2570,7 +2682,7 @@ const ask: Command = async (args, io) => {
2570
2682
  io.stdout(
2571
2683
  session.json
2572
2684
  ? `${JSON.stringify(raised, null, 2)}\n`
2573
- : `Decision ${raised.id} raised on run ${runId}${withFiles}; the run waits for ${raised.decider.login}.\ndecision id: ${raised.id}\n${formatOptions(raised)}\n`,
2685
+ : `Decision ${raised.id} raised on run ${runId}${withFiles}; the run waits for ${deciderLogins(raised)}.\ndecision id: ${raised.id}\n${formatOptions(raised)}\n`,
2574
2686
  );
2575
2687
  return settle(io, session, project, raised, seconds, true);
2576
2688
  };
@@ -2600,8 +2712,7 @@ const answering: Command = async (args, io) => {
2600
2712
  }
2601
2713
  const note = await bodyOf(io, values, text(values, "note"));
2602
2714
  const session = await open(io, values);
2603
- const project = await session.project();
2604
- const answered = await project.answerDecision(id, {
2715
+ const answered = await answerReached(session, id, {
2605
2716
  ...(own === undefined ? { option } : { own }),
2606
2717
  ...(note === undefined ? {} : { note }),
2607
2718
  });
@@ -2609,6 +2720,75 @@ const answering: Command = async (args, io) => {
2609
2720
  return EXIT.ok;
2610
2721
  };
2611
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
+
2612
2792
  const decision: Command = async (args, io) => {
2613
2793
  const { values, positionals } = parse(args, {
2614
2794
  wait: { type: "string" },
@@ -2622,19 +2802,19 @@ const decision: Command = async (args, io) => {
2622
2802
  const seconds = secondsOf(text(values, "wait"), 0);
2623
2803
  const outcome = await bodyOf(io, values, text(values, "outcome"));
2624
2804
  const session = await open(io, values);
2625
- const project = await session.project();
2626
2805
  if (outcome !== undefined) {
2627
2806
  // What followed the decision: the record grows, and is printed as it stands.
2807
+ const project = await session.project();
2628
2808
  const grown = await project.updateDecision(id, { outcome });
2629
2809
  answer(io, session, grown, () => formatDecision(grown));
2630
2810
  return EXIT.ok;
2631
2811
  }
2632
- const found = await project.decision(id);
2812
+ const { decision: found, waiter } = await reachDecision(session, id);
2633
2813
  if (seconds === 0) {
2634
2814
  answer(io, session, found, () => formatDecision(found));
2635
2815
  return EXIT.ok;
2636
2816
  }
2637
- return settle(io, session, project, found, seconds);
2817
+ return settle(io, session, waiter, found, seconds);
2638
2818
  };
2639
2819
 
2640
2820
  const docs: Command = async (args, io) => {
@@ -3119,12 +3299,94 @@ const people: Command = async (args, io) => {
3119
3299
  const { values } = parse(args, {}, false);
3120
3300
  const session = await open(io, values);
3121
3301
  const found = await session.client.people();
3302
+ const loginOf = new Map(found.items.map((person) => [person.id, person.login]));
3303
+ answer(io, session, found, () =>
3304
+ [
3305
+ ...found.items.map((person) =>
3306
+ formatPersonLine(
3307
+ person,
3308
+ person.managerId === null || person.managerId === undefined
3309
+ ? undefined
3310
+ : (loginOf.get(person.managerId) ?? person.managerId),
3311
+ ),
3312
+ ),
3313
+ ...found.listed.map(formatListingLine),
3314
+ ].join("\n"),
3315
+ );
3316
+ return EXIT.ok;
3317
+ };
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]));
3122
3325
  answer(io, session, found, () =>
3123
- [...found.items.map(formatPersonLine), ...found.listed.map(formatListingLine)].join("\n"),
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"),
3124
3329
  );
3125
3330
  return EXIT.ok;
3126
3331
  };
3127
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
+
3128
3390
  // The workspace's settings (ADR-0035): read by everyone, changed by an administrator, themselves
3129
3391
  // on the Settings page or through their agent here (ADR-0021).
3130
3392
  const WORKSPACE_FLAGS =
@@ -3201,7 +3463,7 @@ const workspace: Command = async (args, io) => {
3201
3463
  };
3202
3464
 
3203
3465
  const PERSON_USAGE =
3204
- "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>";
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>";
3205
3467
 
3206
3468
  const person: Command = async (args, io) => {
3207
3469
  const what = args[0];
@@ -3218,6 +3480,35 @@ const person: Command = async (args, io) => {
3218
3480
  answer(io, session, changed, () => formatPersonLine(changed));
3219
3481
  return EXIT.ok;
3220
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
+ }
3497
+ if (what === "manager") {
3498
+ const { values, positionals } = parse(args.slice(1), {});
3499
+ const [login, manager] = theTwo(
3500
+ positionals,
3501
+ "Say whom and whom they report to: console person manager <login> <manager login|none>",
3502
+ );
3503
+ const session = await open(io, values);
3504
+ const found = await personOf(session, login);
3505
+ const managerId = manager === "none" ? null : (await personOf(session, manager)).id;
3506
+ const changed = await session.client.updatePerson(found.id, { managerId });
3507
+ answer(io, session, changed, () =>
3508
+ formatPersonLine(changed, managerId === null ? undefined : manager),
3509
+ );
3510
+ return EXIT.ok;
3511
+ }
3221
3512
  if (what === "add") {
3222
3513
  const { values, positionals } = parse(args.slice(1), { host: { type: "string" } });
3223
3514
  const login = theOne(positionals, "Say whom: console person add <login> [--host gh|google]");
@@ -3664,6 +3955,8 @@ const COMMANDS: Readonly<Record<string, Command>> = {
3664
3955
  plugin,
3665
3956
  people,
3666
3957
  person,
3958
+ teams,
3959
+ team,
3667
3960
  workspace,
3668
3961
  project,
3669
3962
  members,