@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/dist/cli/cli.js CHANGED
@@ -3,7 +3,7 @@ import { parseArgs } from "node:util";
3
3
  import { ApiError, createClient, } from "../client.js";
4
4
  import { CLIENT_RANGE_HEADER, CONSOLE_AT_LEAST, consoleRange, formatRange, judgeClient, judgeConsole, parseVersion, VERSION_HEADER, formatVersion as versionText, } from "../compat.js";
5
5
  import { addDays, dayOf as dayOfDate, firstDayOfWeek, isDay, weekOf } from "../dates.js";
6
- import { DEFAULT_PAGE_LIMIT, LIST_LIMIT, MAX_OPTIONS, MIN_OPTIONS } from "../limits.js";
6
+ import { DEFAULT_PAGE_LIMIT, LIST_LIMIT, MAX_DECIDERS, MAX_OPTIONS, MIN_OPTIONS, } from "../limits.js";
7
7
  import { capabilityWords } from "../plugin/capabilities.js";
8
8
  import { PLUGIN_LIST_LIMIT } from "../plugin/limits.js";
9
9
  import { fillRules, RULES_PATH, RULES_TITLE } from "../rules.js";
@@ -12,7 +12,7 @@ import { COLOR_NAMES, GOAL_STATES, PEOPLE_VISIBILITIES, PERSON_ROLES, PROJECT_RO
12
12
  import { loadWebRoot, WebRootError } from "../web.js";
13
13
  import { CredentialsError, EMPTY_CREDENTIALS, loginsAt, resolveLogin, withLogin, withoutLogin, } from "./credentials.js";
14
14
  import { THE_SENTENCE } from "./directory.js";
15
- import { formatAnswer, formatCategoryLine, formatComment, formatDecision, formatDecisionLine, formatDocument, formatDocuments, formatGoal, formatGoalLine, formatInvocation, formatListingLine, formatMemberLine, formatMore, formatOptions, formatPersonLine, formatPersonWork, formatPlugin, formatPluginGrantLine, formatPluginHelp, formatPluginRecord, formatPluginRecords, formatPluginRecordVersions, formatPluginSettingLine, formatPlugins, formatProject, formatProjectLine, formatRules, formatRun, formatRunLine, formatTask, formatTaskLine, formatVersion, formatVersionLine, formatWork, formatWorkspace, localized, moment, setMomentZone, } from "./format.js";
15
+ import { deciderLogins, formatAnswer, formatCategoryLine, formatComment, formatDecision, formatDecisionLine, formatDocument, formatDocuments, formatGoal, formatGoalLine, formatInvocation, formatListingLine, formatMemberLine, formatMore, formatOptions, formatPersonLine, formatPersonWork, formatPlugin, formatPluginAsks, formatPluginGrantLine, formatPluginHelp, formatPluginRecord, formatPluginRecords, formatPluginRecordVersions, formatPluginSettingLine, formatPlugins, formatProject, formatProjectLine, formatRules, formatRun, formatRunLine, formatTask, formatTaskLine, formatTeamLine, formatVersion, formatVersionLine, formatWork, formatWorkspace, localized, moment, setMomentZone, } from "./format.js";
16
16
  import { COMMAND_HELP, helpFor, ownSkillPath, usage } from "./help.js";
17
17
  import { describeInstall, updateCommandFor } from "./install.js";
18
18
  import { bodyOfFile, bodyOfFolder, bodyOfSkill, formatSkillBrowse, formatSkillFile, formatSkillFind, formatSkillLoad, formatSkills, joinFile, joinFolder, joinSkill, skillPathIn, summaryOf, } from "./pack.js";
@@ -373,10 +373,6 @@ function theTwo(positionals, ask) {
373
373
  }
374
374
  return [first, second];
375
375
  }
376
- /**
377
- * The decision once answered, or as it stands when the wait is over. Each request asks the
378
- * console to hold it for a while, so the answer arrives as soon as a person gives it.
379
- */
380
376
  async function waitForAnswer(io, project, decision, seconds) {
381
377
  const deadline = io.now() + seconds * 1000;
382
378
  let current = decision;
@@ -1077,6 +1073,8 @@ const plugin = async (args, io) => {
1077
1073
  answer(io, session, detail, () => `Disabled ${detail.key}: its commands are refused until it is enabled again; what the console kept of it stays.`);
1078
1074
  return EXIT.ok;
1079
1075
  }
1076
+ case "capabilities":
1077
+ return pluginCapabilities(io, session, client, rest);
1080
1078
  case "settings":
1081
1079
  return pluginSettings(io, session, client, rest);
1082
1080
  case "secret":
@@ -1307,7 +1305,49 @@ function capabilityLines(capabilities) {
1307
1305
  : ` ${capability}: opens ${words.opens}; a malicious plugin reaches ${words.reach}`;
1308
1306
  });
1309
1307
  }
1310
- /** Enables a plugin, with consent to every capability it asks, named again; an administrator's. */
1308
+ /** The capabilities `--consent` names, given again or with commas between. */
1309
+ function capabilitiesNamed(values) {
1310
+ return texts(values, "consent")
1311
+ .flatMap((value) => value.split(","))
1312
+ .map((capability) => capability.trim())
1313
+ .filter((capability) => capability.length > 0);
1314
+ }
1315
+ /** What a plugin requires, and which of what it can go without it holds, as `capabilities` prints it. */
1316
+ function capabilitiesHeld(detail) {
1317
+ const held = detail.consent ?? [];
1318
+ const optional = detail.optionalCapabilities ?? [];
1319
+ const named = (capabilities) => capabilities.length === 0 ? "none" : capabilities.join(", ");
1320
+ return [
1321
+ `Capabilities of ${detail.key}:`,
1322
+ ` required: ${named(detail.capabilities)}`,
1323
+ ` optional, on: ${named(optional.filter((capability) => held.includes(capability)))}`,
1324
+ ` optional, off: ${named(optional.filter((capability) => !held.includes(capability)))}`,
1325
+ ].join("\n");
1326
+ }
1327
+ /**
1328
+ * What a plugin holds; with `--consent`, which of the optional capabilities it holds from now on,
1329
+ * `none` for none of them. An administrator's.
1330
+ */
1331
+ async function pluginCapabilities(io, session, client, rest) {
1332
+ const { values, positionals } = parse(rest, { consent: { type: "string", multiple: true } });
1333
+ if (positionals.length > 0) {
1334
+ throw misuse(`Nothing follows capabilities but --consent: console plugin ${client.key} capabilities [--consent <capability>,...|none]`);
1335
+ }
1336
+ const detail = await client.get();
1337
+ const named = capabilitiesNamed(values);
1338
+ if (named.length === 0) {
1339
+ answer(io, session, detail, () => capabilitiesHeld(detail));
1340
+ return EXIT.ok;
1341
+ }
1342
+ const chosen = named.filter((capability) => capability !== "none");
1343
+ const changed = await client.updateConsent([
1344
+ ...detail.capabilities,
1345
+ ...chosen.filter((capability) => !detail.capabilities.includes(capability)),
1346
+ ]);
1347
+ answer(io, session, changed, () => capabilitiesHeld(changed));
1348
+ return EXIT.ok;
1349
+ }
1350
+ /** Enables a plugin, with consent to every capability it requires, named again; an administrator's. */
1311
1351
  async function pluginEnable(io, session, client, rest) {
1312
1352
  const { values, positionals } = parse(rest, {
1313
1353
  consent: { type: "string", multiple: true },
@@ -1320,16 +1360,17 @@ async function pluginEnable(io, session, client, rest) {
1320
1360
  }
1321
1361
  const [detail, me] = await Promise.all([client.get(), session.client.me()]);
1322
1362
  const asked = detail.capabilities;
1323
- const consent = texts(values, "consent")
1324
- .flatMap((value) => value.split(","))
1325
- .map((capability) => capability.trim())
1326
- .filter((capability) => capability.length > 0);
1363
+ const optional = detail.optionalCapabilities ?? [];
1364
+ const consent = capabilitiesNamed(values);
1327
1365
  if (asked.length > 0 && !session.json) {
1328
- io.stdout(`${detail.key} asks ${asked.length === 1 ? "one capability" : `${asked.length} capabilities`}:\n${capabilityLines(asked).join("\n")}\n`);
1366
+ io.stdout(`${detail.key} requires ${asked.length === 1 ? "one capability" : `${asked.length} capabilities`}:\n${capabilityLines(asked).join("\n")}\n`);
1367
+ }
1368
+ if (optional.length > 0 && !session.json) {
1369
+ io.stdout(`It can go without these, which it holds if --consent names them too:\n${capabilityLines(optional).join("\n")}\n`);
1329
1370
  }
1330
1371
  const missing = asked.filter((capability) => !consent.includes(capability));
1331
1372
  if (missing.length > 0) {
1332
- throw misuse(`Enabling ${detail.key} needs consent to every capability it asks, named again: --consent ${asked.join(",")}. Not named: ${missing.join(", ")}.`);
1373
+ throw misuse(`Enabling ${detail.key} needs consent to every capability it requires, named again: --consent ${asked.join(",")}. Not named: ${missing.join(", ")}.`);
1333
1374
  }
1334
1375
  const boardFlag = text(values, "board");
1335
1376
  let board = null;
@@ -1361,12 +1402,18 @@ async function pluginEnable(io, session, client, rest) {
1361
1402
  });
1362
1403
  answer(io, session, enabled, () => [
1363
1404
  `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"}.`,
1405
+ ...(enabled.needsTimeZone === true
1406
+ ? [
1407
+ "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>.",
1408
+ ]
1409
+ : []),
1364
1410
  ...(enabled.settings.length === 0
1365
1411
  ? []
1366
1412
  : [
1367
1413
  "Settings:",
1368
1414
  ...enabled.settings.map((setting) => formatPluginSettingLine(setting, me.language)),
1369
1415
  ]),
1416
+ ...formatPluginAsks(enabled, me.language),
1370
1417
  ].join("\n"));
1371
1418
  return EXIT.ok;
1372
1419
  }
@@ -1411,18 +1458,31 @@ async function pluginSecret(io, session, client, rest) {
1411
1458
  answer(io, session, { name, set: true }, () => `Set the secret ${name} of ${client.key}: sealed, and shown by nothing from now on.`);
1412
1459
  return EXIT.ok;
1413
1460
  }
1414
- /** Grants a role of the plugin to a person, to the members of a project, or to everyone; an administrator's. */
1461
+ /**
1462
+ * Grants a role of the plugin to a person, to the members of a project, to another plugin, to the
1463
+ * members of a team, or to everyone; an administrator's.
1464
+ */
1415
1465
  async function pluginGrant(io, session, client, rest, projectKey) {
1416
1466
  const { values, positionals } = parse(rest, {
1417
1467
  person: { type: "string" },
1468
+ plugin: { type: "string" },
1469
+ team: { type: "string" },
1418
1470
  everyone: { type: "boolean" },
1419
1471
  });
1420
- const usage = `console plugin ${client.key} grant <role> --person <login> | --project <key> | --everyone`;
1472
+ const usage = `console plugin ${client.key} grant <role> --person <login> | --project <key> | --plugin <key> | --team <key> | --everyone`;
1421
1473
  const role = theOne(positionals, `Say which role: ${usage}`);
1422
1474
  const login = text(values, "person");
1475
+ const other = text(values, "plugin");
1476
+ const teamKey = text(values, "team");
1423
1477
  const everyone = on(values, "everyone");
1424
- if ([login !== undefined, projectKey !== undefined, everyone].filter(Boolean).length !== 1) {
1425
- throw misuse(`Say whom, one of the three: ${usage}`);
1478
+ if ([
1479
+ login !== undefined,
1480
+ projectKey !== undefined,
1481
+ other !== undefined,
1482
+ teamKey !== undefined,
1483
+ everyone,
1484
+ ].filter(Boolean).length !== 1) {
1485
+ throw misuse(`Say whom, one of the five: ${usage}`);
1426
1486
  }
1427
1487
  let input;
1428
1488
  if (login !== undefined) {
@@ -1435,6 +1495,12 @@ async function pluginGrant(io, session, client, rest, projectKey) {
1435
1495
  else if (projectKey !== undefined) {
1436
1496
  input = { role, project: projectKey };
1437
1497
  }
1498
+ else if (other !== undefined) {
1499
+ input = { role, plugin: other };
1500
+ }
1501
+ else if (teamKey !== undefined) {
1502
+ input = { role, team: teamKey };
1503
+ }
1438
1504
  else {
1439
1505
  input = { role, everyone: true };
1440
1506
  }
@@ -1531,7 +1597,7 @@ const rules = async (args, io) => {
1531
1597
  };
1532
1598
  /**
1533
1599
  * The rules as they read for the person whose token the command holds: `{language}` filled
1534
- * with the language they chose for the pages, or with the task's when they chose none.
1600
+ * with the language they read in, or with the task's while they have none.
1535
1601
  */
1536
1602
  function forPerson(half, language) {
1537
1603
  return half.body === null ? half : { ...half, body: fillRules(half.body, language) };
@@ -1909,13 +1975,16 @@ const ask = async (args, io) => {
1909
1975
  file: { type: "string" },
1910
1976
  run: { type: "string" },
1911
1977
  wait: { type: "string" },
1912
- to: { type: "string" },
1978
+ to: { type: "string", multiple: true },
1913
1979
  attach: { type: "string", multiple: true },
1914
1980
  });
1915
1981
  const question = theOne(positionals, 'Say the question: console ask "<question>" --option "<label>" --option "<label>"');
1916
- const to = text(values, "to");
1917
- if (to === "none") {
1918
- throw misuse("Say whom the decision is for: --to <login|me>.");
1982
+ const tos = texts(values, "to");
1983
+ if (tos.includes("none")) {
1984
+ throw misuse("Say whom the decision is for: --to <login|me>, given again for several.");
1985
+ }
1986
+ if (tos.length > MAX_DECIDERS) {
1987
+ throw misuse(`Name up to ${MAX_DECIDERS} people with --to.`);
1919
1988
  }
1920
1989
  const options = texts(values, "option");
1921
1990
  if (options.length < MIN_OPTIONS || options.length > MAX_OPTIONS) {
@@ -1928,20 +1997,31 @@ const ask = async (args, io) => {
1928
1997
  const session = await open(io, values);
1929
1998
  const project = await session.project();
1930
1999
  const runId = await currentRun(project, values);
1931
- // Whom the decision is for (ADR-0042): the person the agent works for, unless --to names another.
1932
- const deciderId = await personIdOf(session, to);
2000
+ // Whom the decision is for (ADR-0042, ADR-0057): the person the agent works for, unless --to
2001
+ // names others of the project, each once; several, and any of them may answer.
2002
+ const deciderIds = [];
2003
+ for (const to of tos) {
2004
+ const id = await personIdOf(session, to);
2005
+ if (typeof id === "string" && !deciderIds.includes(id)) {
2006
+ deciderIds.push(id);
2007
+ }
2008
+ }
1933
2009
  const raised = await project.raiseDecision({
1934
2010
  question,
1935
2011
  options: options.map((label) => ({ label })),
1936
2012
  runId,
1937
2013
  ...(body === undefined ? {} : { body }),
1938
- ...(typeof deciderId === "string" ? { deciderId } : {}),
2014
+ ...(deciderIds.length === 0
2015
+ ? {}
2016
+ : deciderIds.length === 1
2017
+ ? { deciderId: deciderIds[0] }
2018
+ : { deciderId: deciderIds[0], deciderIds }),
1939
2019
  }, files);
1940
2020
  const withFiles = files.length === 0 ? "" : `, with ${files.length} file${files.length === 1 ? "" : "s"}`;
1941
2021
  // The id is printed before any waiting, so that a wait cut short loses nothing.
1942
2022
  io.stdout(session.json
1943
2023
  ? `${JSON.stringify(raised, null, 2)}\n`
1944
- : `Decision ${raised.id} raised on run ${runId}${withFiles}; the run waits for ${raised.decider.login}.\ndecision id: ${raised.id}\n${formatOptions(raised)}\n`);
2024
+ : `Decision ${raised.id} raised on run ${runId}${withFiles}; the run waits for ${deciderLogins(raised)}.\ndecision id: ${raised.id}\n${formatOptions(raised)}\n`);
1945
2025
  return settle(io, session, project, raised, seconds, true);
1946
2026
  };
1947
2027
  /**
@@ -1964,14 +2044,74 @@ const answering = async (args, io) => {
1964
2044
  }
1965
2045
  const note = await bodyOf(io, values, text(values, "note"));
1966
2046
  const session = await open(io, values);
1967
- const project = await session.project();
1968
- const answered = await project.answerDecision(id, {
2047
+ const answered = await answerReached(session, id, {
1969
2048
  ...(own === undefined ? { option } : { own }),
1970
2049
  ...(note === undefined ? {} : { note }),
1971
2050
  });
1972
2051
  answer(io, session, answered, () => formatDecision(answered));
1973
2052
  return EXIT.ok;
1974
2053
  };
2054
+ /** Whether a refusal says the decision is not in the project the command works in, or the project is not the person's. */
2055
+ const notInProject = (error) => error instanceof ApiError &&
2056
+ (error.code === "decision.not_found" || error.code === "project.not_found");
2057
+ /**
2058
+ * One decision as the command reaches it (ADR-0068): in the project the command works in, as
2059
+ * before; else wherever it is, addressed to the person, raised by them, or a plugin's addressed
2060
+ * to someone they manage, through the console's own route for one decision. A plugin's decision
2061
+ * waits in the plugin's board project, which a checkout bound to another project does not name,
2062
+ * and which the person may not be in at all. A console from before that route answers as before.
2063
+ */
2064
+ async function reachDecision(session, id) {
2065
+ if ((await session.projectKey()) !== undefined) {
2066
+ const project = await session.project();
2067
+ try {
2068
+ return { decision: await project.decision(id), waiter: project };
2069
+ }
2070
+ catch (error) {
2071
+ if (!notInProject(error)) {
2072
+ throw error;
2073
+ }
2074
+ }
2075
+ }
2076
+ const anywhere = session.client;
2077
+ try {
2078
+ const reached = await anywhere.decision(id);
2079
+ return {
2080
+ decision: reached.decision,
2081
+ waiter: {
2082
+ awaitDecision: async (decisionId, seconds) => (await anywhere.awaitDecision(decisionId, seconds)).decision,
2083
+ },
2084
+ };
2085
+ }
2086
+ catch (error) {
2087
+ if (error instanceof ApiError && error.status === 404) {
2088
+ throw new ApiError(404, "decision.not_found", "The decision was not found.");
2089
+ }
2090
+ throw error;
2091
+ }
2092
+ }
2093
+ /** Writes an answer on a decision as the command reaches it (ADR-0068): in the project, else wherever it is. */
2094
+ async function answerReached(session, id, input) {
2095
+ if ((await session.projectKey()) !== undefined) {
2096
+ try {
2097
+ return await (await session.project()).answerDecision(id, input);
2098
+ }
2099
+ catch (error) {
2100
+ if (!notInProject(error)) {
2101
+ throw error;
2102
+ }
2103
+ }
2104
+ }
2105
+ try {
2106
+ return await session.client.answerDecision(id, input);
2107
+ }
2108
+ catch (error) {
2109
+ if (error instanceof ApiError && error.status === 404) {
2110
+ throw new ApiError(404, "decision.not_found", "The decision was not found.");
2111
+ }
2112
+ throw error;
2113
+ }
2114
+ }
1975
2115
  const decision = async (args, io) => {
1976
2116
  const { values, positionals } = parse(args, {
1977
2117
  wait: { type: "string" },
@@ -1982,19 +2122,19 @@ const decision = async (args, io) => {
1982
2122
  const seconds = secondsOf(text(values, "wait"), 0);
1983
2123
  const outcome = await bodyOf(io, values, text(values, "outcome"));
1984
2124
  const session = await open(io, values);
1985
- const project = await session.project();
1986
2125
  if (outcome !== undefined) {
1987
2126
  // What followed the decision: the record grows, and is printed as it stands.
2127
+ const project = await session.project();
1988
2128
  const grown = await project.updateDecision(id, { outcome });
1989
2129
  answer(io, session, grown, () => formatDecision(grown));
1990
2130
  return EXIT.ok;
1991
2131
  }
1992
- const found = await project.decision(id);
2132
+ const { decision: found, waiter } = await reachDecision(session, id);
1993
2133
  if (seconds === 0) {
1994
2134
  answer(io, session, found, () => formatDecision(found));
1995
2135
  return EXIT.ok;
1996
2136
  }
1997
- return settle(io, session, project, found, seconds);
2137
+ return settle(io, session, waiter, found, seconds);
1998
2138
  };
1999
2139
  const docs = async (args, io) => {
2000
2140
  const { values, positionals } = parse(args, {
@@ -2396,9 +2536,68 @@ const people = async (args, io) => {
2396
2536
  const { values } = parse(args, {}, false);
2397
2537
  const session = await open(io, values);
2398
2538
  const found = await session.client.people();
2399
- answer(io, session, found, () => [...found.items.map(formatPersonLine), ...found.listed.map(formatListingLine)].join("\n"));
2539
+ const loginOf = new Map(found.items.map((person) => [person.id, person.login]));
2540
+ answer(io, session, found, () => [
2541
+ ...found.items.map((person) => formatPersonLine(person, person.managerId === null || person.managerId === undefined
2542
+ ? undefined
2543
+ : (loginOf.get(person.managerId) ?? person.managerId))),
2544
+ ...found.listed.map(formatListingLine),
2545
+ ].join("\n"));
2400
2546
  return EXIT.ok;
2401
2547
  };
2548
+ /** The teams of the workspace (ADR-0073): each with who is in it, by login. */
2549
+ const teams = async (args, io) => {
2550
+ const { values } = parse(args, {}, false);
2551
+ const session = await open(io, values);
2552
+ const [found, everyone] = await Promise.all([session.client.teams(), session.client.people()]);
2553
+ const loginOf = new Map(everyone.items.map((person) => [person.id, person.login]));
2554
+ answer(io, session, found, () => found.items.length === 0
2555
+ ? "No teams yet. console team new <key> <name> makes one."
2556
+ : found.items.map((team) => formatTeamLine(team, loginOf)).join("\n"));
2557
+ return EXIT.ok;
2558
+ };
2559
+ const TEAM_USAGE = '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>';
2560
+ // A team is called by its key (ADR-0074), which never changes; its name is what people read.
2561
+ const team = async (args, io) => {
2562
+ const what = args[0];
2563
+ if (what === "new") {
2564
+ const { values, positionals } = parse(args.slice(1), {});
2565
+ const [key, name] = theTwo(positionals, 'Say the key and the name: console team new <key> "<name>"');
2566
+ const session = await open(io, values);
2567
+ const made = await session.client.createTeam({ key, name });
2568
+ answer(io, session, made, () => formatTeamLine(made, new Map()));
2569
+ return EXIT.ok;
2570
+ }
2571
+ if (what === "rename") {
2572
+ const { values, positionals } = parse(args.slice(1), {});
2573
+ const [key, next] = theTwo(positionals, 'Say which team and its new name: console team rename <key> "<new name>"');
2574
+ const session = await open(io, values);
2575
+ const changed = await session.client.renameTeam(key, { name: next });
2576
+ answer(io, session, changed, () => formatTeamLine(changed, new Map()));
2577
+ return EXIT.ok;
2578
+ }
2579
+ if (what === "remove") {
2580
+ const { values, positionals } = parse(args.slice(1), {});
2581
+ const key = theOne(positionals, "Say which team: console team remove <key>");
2582
+ const session = await open(io, values);
2583
+ await session.client.removeTeam(key);
2584
+ answer(io, session, { removed: key }, () => `The team ${key} is no more.`);
2585
+ return EXIT.ok;
2586
+ }
2587
+ if (what === "add" || what === "drop") {
2588
+ const { values, positionals } = parse(args.slice(1), {});
2589
+ const [key, login] = theTwo(positionals, `Say which team and whom: console team ${what} <key> <login>`);
2590
+ const session = await open(io, values);
2591
+ const person = await personOf(session, login);
2592
+ const changed = what === "add"
2593
+ ? await session.client.addTeamMember(key, person.id)
2594
+ : await session.client.removeTeamMember(key, person.id);
2595
+ const everyone = await session.client.people();
2596
+ answer(io, session, changed, () => formatTeamLine(changed, new Map(everyone.items.map((one) => [one.id, one.login]))));
2597
+ return EXIT.ok;
2598
+ }
2599
+ throw misuse(TEAM_USAGE);
2600
+ };
2402
2601
  // The workspace's settings (ADR-0035): read by everyone, changed by an administrator, themselves
2403
2602
  // on the Settings page or through their agent here (ADR-0021).
2404
2603
  const WORKSPACE_FLAGS = "--people-visibility <everyone|shared-projects>, --week-start <auto|monday|sunday>, --time-zone <zone|none>";
@@ -2459,7 +2658,7 @@ const workspace = async (args, io) => {
2459
2658
  }
2460
2659
  throw misuse(WORKSPACE_USAGE);
2461
2660
  };
2462
- const PERSON_USAGE = "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>";
2661
+ const PERSON_USAGE = "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>";
2463
2662
  const person = async (args, io) => {
2464
2663
  const what = args[0];
2465
2664
  if (what === "role") {
@@ -2472,6 +2671,27 @@ const person = async (args, io) => {
2472
2671
  answer(io, session, changed, () => formatPersonLine(changed));
2473
2672
  return EXIT.ok;
2474
2673
  }
2674
+ if (what === "email") {
2675
+ const { values, positionals } = parse(args.slice(1), {});
2676
+ const [login, address] = theTwo(positionals, "Say whom and their address: console person email <login> <address|none>");
2677
+ const session = await open(io, values);
2678
+ const found = await personOf(session, login);
2679
+ const changed = await session.client.updatePerson(found.id, {
2680
+ email: address === "none" ? null : address,
2681
+ });
2682
+ answer(io, session, changed, () => formatPersonLine(changed));
2683
+ return EXIT.ok;
2684
+ }
2685
+ if (what === "manager") {
2686
+ const { values, positionals } = parse(args.slice(1), {});
2687
+ const [login, manager] = theTwo(positionals, "Say whom and whom they report to: console person manager <login> <manager login|none>");
2688
+ const session = await open(io, values);
2689
+ const found = await personOf(session, login);
2690
+ const managerId = manager === "none" ? null : (await personOf(session, manager)).id;
2691
+ const changed = await session.client.updatePerson(found.id, { managerId });
2692
+ answer(io, session, changed, () => formatPersonLine(changed, managerId === null ? undefined : manager));
2693
+ return EXIT.ok;
2694
+ }
2475
2695
  if (what === "add") {
2476
2696
  const { values, positionals } = parse(args.slice(1), { host: { type: "string" } });
2477
2697
  const login = theOne(positionals, "Say whom: console person add <login> [--host gh|google]");
@@ -2817,6 +3037,8 @@ const COMMANDS = {
2817
3037
  plugin,
2818
3038
  people,
2819
3039
  person,
3040
+ teams,
3041
+ team,
2820
3042
  workspace,
2821
3043
  project,
2822
3044
  members,