@skillcdn/console 0.4.0 → 0.4.2

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 (308) hide show
  1. package/README.md +20 -2
  2. package/SKILLCDN.md +7 -5
  3. package/dist/api.d.ts +5 -4
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/api.js +5 -4
  6. package/dist/api.js.map +1 -1
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +579 -45
  9. package/dist/cli/cli.js.map +1 -1
  10. package/dist/cli/format.d.ts +34 -3
  11. package/dist/cli/format.d.ts.map +1 -1
  12. package/dist/cli/format.js +277 -6
  13. package/dist/cli/format.js.map +1 -1
  14. package/dist/cli/help.d.ts.map +1 -1
  15. package/dist/cli/help.js +60 -7
  16. package/dist/cli/help.js.map +1 -1
  17. package/dist/cli/plugin-check.d.ts +21 -0
  18. package/dist/cli/plugin-check.d.ts.map +1 -0
  19. package/dist/cli/plugin-check.js +374 -0
  20. package/dist/cli/plugin-check.js.map +1 -0
  21. package/dist/cli/plugin-cli.d.ts +20 -0
  22. package/dist/cli/plugin-cli.d.ts.map +1 -0
  23. package/dist/cli/plugin-cli.js +123 -0
  24. package/dist/cli/plugin-cli.js.map +1 -0
  25. package/dist/cli/plugin-init.d.ts +20 -0
  26. package/dist/cli/plugin-init.d.ts.map +1 -0
  27. package/dist/cli/plugin-init.js +74 -0
  28. package/dist/cli/plugin-init.js.map +1 -0
  29. package/dist/cli/plugin-main.d.ts +3 -0
  30. package/dist/cli/plugin-main.d.ts.map +1 -0
  31. package/dist/cli/plugin-main.js +28 -0
  32. package/dist/cli/plugin-main.js.map +1 -0
  33. package/dist/client.d.ts +109 -2
  34. package/dist/client.d.ts.map +1 -1
  35. package/dist/client.js +67 -2
  36. package/dist/client.js.map +1 -1
  37. package/dist/compat.d.ts +1 -1
  38. package/dist/compat.js +2 -2
  39. package/dist/components/board.d.ts +8 -0
  40. package/dist/components/board.d.ts.map +1 -1
  41. package/dist/components/board.js +59 -15
  42. package/dist/components/board.js.map +1 -1
  43. package/dist/components/event-feed.d.ts +1 -1
  44. package/dist/components/event-feed.d.ts.map +1 -1
  45. package/dist/components/event-feed.js +33 -1
  46. package/dist/components/event-feed.js.map +1 -1
  47. package/dist/components/markdown.d.ts.map +1 -1
  48. package/dist/components/markdown.js +6 -3
  49. package/dist/components/markdown.js.map +1 -1
  50. package/dist/components/pack-connect.d.ts +3 -1
  51. package/dist/components/pack-connect.d.ts.map +1 -1
  52. package/dist/components/pack-connect.js +4 -4
  53. package/dist/components/pack-connect.js.map +1 -1
  54. package/dist/components/pack-token.d.ts +3 -1
  55. package/dist/components/pack-token.d.ts.map +1 -1
  56. package/dist/components/pack-token.js +1 -1
  57. package/dist/components/pack-token.js.map +1 -1
  58. package/dist/components/people.d.ts.map +1 -1
  59. package/dist/components/people.js +14 -3
  60. package/dist/components/people.js.map +1 -1
  61. package/dist/components/plugin-frame.d.ts +52 -0
  62. package/dist/components/plugin-frame.d.ts.map +1 -0
  63. package/dist/components/plugin-frame.js +314 -0
  64. package/dist/components/plugin-frame.js.map +1 -0
  65. package/dist/components/plugin-settings.d.ts +38 -0
  66. package/dist/components/plugin-settings.d.ts.map +1 -0
  67. package/dist/components/plugin-settings.js +348 -0
  68. package/dist/components/plugin-settings.js.map +1 -0
  69. package/dist/components/remote-pictures.d.ts +6 -0
  70. package/dist/components/remote-pictures.d.ts.map +1 -0
  71. package/dist/components/remote-pictures.js +8 -0
  72. package/dist/components/remote-pictures.js.map +1 -0
  73. package/dist/components/shell.d.ts +2 -0
  74. package/dist/components/shell.d.ts.map +1 -1
  75. package/dist/components/shell.js +26 -1
  76. package/dist/components/shell.js.map +1 -1
  77. package/dist/components/sign-in.d.ts.map +1 -1
  78. package/dist/components/sign-in.js +49 -2
  79. package/dist/components/sign-in.js.map +1 -1
  80. package/dist/components/ui.d.ts +3 -2
  81. package/dist/components/ui.d.ts.map +1 -1
  82. package/dist/components/ui.js +6 -3
  83. package/dist/components/ui.js.map +1 -1
  84. package/dist/components/workspace.d.ts +7 -1
  85. package/dist/components/workspace.d.ts.map +1 -1
  86. package/dist/components/workspace.js +1 -1
  87. package/dist/components/workspace.js.map +1 -1
  88. package/dist/console.css +384 -5
  89. package/dist/console.d.ts.map +1 -1
  90. package/dist/console.js +146 -29
  91. package/dist/console.js.map +1 -1
  92. package/dist/data.d.ts +11 -2
  93. package/dist/data.d.ts.map +1 -1
  94. package/dist/data.js +35 -21
  95. package/dist/data.js.map +1 -1
  96. package/dist/i18n/en.d.ts +163 -1
  97. package/dist/i18n/en.d.ts.map +1 -1
  98. package/dist/i18n/en.js +174 -1
  99. package/dist/i18n/en.js.map +1 -1
  100. package/dist/i18n/ko.d.ts.map +1 -1
  101. package/dist/i18n/ko.js +154 -1
  102. package/dist/i18n/ko.js.map +1 -1
  103. package/dist/index.d.ts +3 -1
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +3 -1
  106. package/dist/index.js.map +1 -1
  107. package/dist/limits.d.ts +14 -0
  108. package/dist/limits.d.ts.map +1 -1
  109. package/dist/limits.js +14 -0
  110. package/dist/limits.js.map +1 -1
  111. package/dist/plugin/args.d.ts +148 -0
  112. package/dist/plugin/args.d.ts.map +1 -0
  113. package/dist/plugin/args.js +369 -0
  114. package/dist/plugin/args.js.map +1 -0
  115. package/dist/plugin/bridge.d.ts +146 -0
  116. package/dist/plugin/bridge.d.ts.map +1 -0
  117. package/dist/plugin/bridge.js +125 -0
  118. package/dist/plugin/bridge.js.map +1 -0
  119. package/dist/plugin/capabilities.d.ts +20 -0
  120. package/dist/plugin/capabilities.d.ts.map +1 -0
  121. package/dist/plugin/capabilities.js +133 -0
  122. package/dist/plugin/capabilities.js.map +1 -0
  123. package/dist/plugin/context.d.ts +374 -0
  124. package/dist/plugin/context.d.ts.map +1 -0
  125. package/dist/plugin/context.js +2 -0
  126. package/dist/plugin/context.js.map +1 -0
  127. package/dist/plugin/define.d.ts +208 -0
  128. package/dist/plugin/define.d.ts.map +1 -0
  129. package/dist/plugin/define.js +482 -0
  130. package/dist/plugin/define.js.map +1 -0
  131. package/dist/plugin/definition-error.d.ts +11 -0
  132. package/dist/plugin/definition-error.d.ts.map +1 -0
  133. package/dist/plugin/definition-error.js +17 -0
  134. package/dist/plugin/definition-error.js.map +1 -0
  135. package/dist/plugin/limits.d.ts +67 -0
  136. package/dist/plugin/limits.d.ts.map +1 -0
  137. package/dist/plugin/limits.js +70 -0
  138. package/dist/plugin/limits.js.map +1 -0
  139. package/dist/plugin/load-worker.d.ts +7 -0
  140. package/dist/plugin/load-worker.d.ts.map +1 -0
  141. package/dist/plugin/load-worker.js +41 -0
  142. package/dist/plugin/load-worker.js.map +1 -0
  143. package/dist/plugin/load.d.ts +18 -0
  144. package/dist/plugin/load.d.ts.map +1 -0
  145. package/dist/plugin/load.js +62 -0
  146. package/dist/plugin/load.js.map +1 -0
  147. package/dist/plugin/manifest.d.ts +237 -0
  148. package/dist/plugin/manifest.d.ts.map +1 -0
  149. package/dist/plugin/manifest.js +411 -0
  150. package/dist/plugin/manifest.js.map +1 -0
  151. package/dist/plugin/refusals.d.ts +49 -0
  152. package/dist/plugin/refusals.d.ts.map +1 -0
  153. package/dist/plugin/refusals.js +79 -0
  154. package/dist/plugin/refusals.js.map +1 -0
  155. package/dist/plugin/rest-schemas.d.ts +556 -0
  156. package/dist/plugin/rest-schemas.d.ts.map +1 -0
  157. package/dist/plugin/rest-schemas.js +262 -0
  158. package/dist/plugin/rest-schemas.js.map +1 -0
  159. package/dist/plugin/sandbox.d.ts +23 -0
  160. package/dist/plugin/sandbox.d.ts.map +1 -0
  161. package/dist/plugin/sandbox.js +99 -0
  162. package/dist/plugin/sandbox.js.map +1 -0
  163. package/dist/plugin/testing.d.ts +45 -0
  164. package/dist/plugin/testing.d.ts.map +1 -0
  165. package/dist/plugin/testing.js +420 -0
  166. package/dist/plugin/testing.js.map +1 -0
  167. package/dist/plugin/web-react.d.ts +11 -0
  168. package/dist/plugin/web-react.d.ts.map +1 -0
  169. package/dist/plugin/web-react.js +34 -0
  170. package/dist/plugin/web-react.js.map +1 -0
  171. package/dist/plugin/web.d.ts +95 -0
  172. package/dist/plugin/web.d.ts.map +1 -0
  173. package/dist/plugin/web.js +245 -0
  174. package/dist/plugin/web.js.map +1 -0
  175. package/dist/plugin-nav.d.ts +29 -0
  176. package/dist/plugin-nav.d.ts.map +1 -0
  177. package/dist/plugin-nav.js +92 -0
  178. package/dist/plugin-nav.js.map +1 -0
  179. package/dist/plugin-testing.d.ts +2 -0
  180. package/dist/plugin-testing.d.ts.map +1 -0
  181. package/dist/plugin-testing.js +4 -0
  182. package/dist/plugin-testing.js.map +1 -0
  183. package/dist/plugin-web-react.d.ts +3 -0
  184. package/dist/plugin-web-react.d.ts.map +1 -0
  185. package/dist/plugin-web-react.js +3 -0
  186. package/dist/plugin-web-react.js.map +1 -0
  187. package/dist/plugin-web.d.ts +4 -0
  188. package/dist/plugin-web.d.ts.map +1 -0
  189. package/dist/plugin-web.js +5 -0
  190. package/dist/plugin-web.js.map +1 -0
  191. package/dist/plugin.d.ts +11 -0
  192. package/dist/plugin.d.ts.map +1 -0
  193. package/dist/plugin.js +15 -0
  194. package/dist/plugin.js.map +1 -0
  195. package/dist/router.d.ts +28 -0
  196. package/dist/router.d.ts.map +1 -1
  197. package/dist/router.js +47 -0
  198. package/dist/router.js.map +1 -1
  199. package/dist/routes.d.ts +6 -0
  200. package/dist/routes.d.ts.map +1 -1
  201. package/dist/routes.js +8 -0
  202. package/dist/routes.js.map +1 -1
  203. package/dist/schemas.d.ts +147 -1
  204. package/dist/schemas.d.ts.map +1 -1
  205. package/dist/schemas.js +55 -5
  206. package/dist/schemas.js.map +1 -1
  207. package/dist/tokens.d.ts +2 -0
  208. package/dist/tokens.d.ts.map +1 -0
  209. package/dist/tokens.js +101 -0
  210. package/dist/tokens.js.map +1 -0
  211. package/dist/vocabulary.d.ts +11 -1
  212. package/dist/vocabulary.d.ts.map +1 -1
  213. package/dist/vocabulary.js +24 -0
  214. package/dist/vocabulary.js.map +1 -1
  215. package/dist/web.d.ts.map +1 -1
  216. package/dist/web.js +5 -2
  217. package/dist/web.js.map +1 -1
  218. package/docs/specs/cli.md +16 -9
  219. package/docs/specs/plugins.md +487 -0
  220. package/docs/specs/rest.md +25 -13
  221. package/package.json +21 -3
  222. package/skills/building-a-console/SKILL.md +25 -26
  223. package/skills/building-a-console/references/configuring.md +40 -0
  224. package/skills/building-a-console/references/the-default-console.md +1 -1
  225. package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
  226. package/skills/building-a-plugin/SKILL.md +137 -0
  227. package/skills/deploying-a-console/SKILL.md +24 -26
  228. package/skills/deploying-a-console/references/access.md +13 -9
  229. package/skills/deploying-a-console/references/resources.md +25 -10
  230. package/skills/deploying-a-console/references/running-it-again.md +15 -0
  231. package/skills/deploying-a-console/references/sizing.md +2 -0
  232. package/skills/planning-the-board/SKILL.md +96 -0
  233. package/skills/planning-the-board/references/a-task.md +36 -0
  234. package/skills/planning-the-board/references/setting-up.md +44 -0
  235. package/skills/working-the-board/SKILL.md +43 -34
  236. package/skills/working-the-board/references/the-pack.md +19 -0
  237. package/src/api.ts +16 -0
  238. package/src/cli/cli.ts +736 -45
  239. package/src/cli/format.ts +356 -9
  240. package/src/cli/help.ts +71 -6
  241. package/src/cli/plugin-check.ts +470 -0
  242. package/src/cli/plugin-cli.ts +148 -0
  243. package/src/cli/plugin-init.ts +90 -0
  244. package/src/cli/plugin-main.ts +30 -0
  245. package/src/client.ts +324 -1
  246. package/src/compat.ts +2 -2
  247. package/src/components/board.tsx +62 -14
  248. package/src/components/event-feed.tsx +42 -1
  249. package/src/components/markdown.tsx +6 -3
  250. package/src/components/pack-connect.tsx +10 -7
  251. package/src/components/pack-token.tsx +4 -1
  252. package/src/components/people.tsx +17 -7
  253. package/src/components/plugin-frame.tsx +437 -0
  254. package/src/components/plugin-settings.tsx +1111 -0
  255. package/src/components/remote-pictures.ts +19 -0
  256. package/src/components/shell.tsx +82 -14
  257. package/src/components/sign-in.tsx +114 -13
  258. package/src/components/ui.tsx +6 -3
  259. package/src/components/workspace.tsx +18 -0
  260. package/src/console.tsx +309 -23
  261. package/src/data.ts +47 -22
  262. package/src/i18n/en.ts +183 -1
  263. package/src/i18n/ko.ts +159 -1
  264. package/src/index.ts +12 -0
  265. package/src/limits.ts +14 -0
  266. package/src/plugin/args.ts +526 -0
  267. package/src/plugin/bridge.ts +314 -0
  268. package/src/plugin/capabilities.ts +150 -0
  269. package/src/plugin/context.ts +416 -0
  270. package/src/plugin/define.ts +792 -0
  271. package/src/plugin/definition-error.ts +19 -0
  272. package/src/plugin/limits.ts +70 -0
  273. package/src/plugin/load-worker.ts +50 -0
  274. package/src/plugin/load.ts +82 -0
  275. package/src/plugin/manifest.ts +561 -0
  276. package/src/plugin/refusals.ts +94 -0
  277. package/src/plugin/rest-schemas.ts +354 -0
  278. package/src/plugin/sandbox.ts +123 -0
  279. package/src/plugin/testing.ts +612 -0
  280. package/src/plugin/web-react.ts +45 -0
  281. package/src/plugin/web.ts +403 -0
  282. package/src/plugin-nav.ts +132 -0
  283. package/src/plugin-testing.ts +10 -0
  284. package/src/plugin-web-react.ts +4 -0
  285. package/src/plugin-web.ts +35 -0
  286. package/src/plugin.ts +126 -0
  287. package/src/router.ts +73 -0
  288. package/src/routes.ts +9 -0
  289. package/src/schemas.ts +60 -4
  290. package/src/styles/console.css +384 -5
  291. package/src/tokens.ts +101 -0
  292. package/src/vocabulary.ts +30 -0
  293. package/src/web.ts +5 -2
  294. package/template/plugin/.gitignore +2 -0
  295. package/template/plugin/README.md +26 -0
  296. package/template/plugin/SKILLCDN.md +13 -0
  297. package/template/plugin/build.mjs +40 -0
  298. package/template/plugin/console-plugin.json +50 -0
  299. package/template/plugin/package.json +27 -0
  300. package/template/plugin/skills/__key__/SKILL.md +23 -0
  301. package/template/plugin/src/check.test.ts +34 -0
  302. package/template/plugin/src/server/index.test.ts +41 -0
  303. package/template/plugin/src/server/index.ts +72 -0
  304. package/template/plugin/src/web/index.css +51 -0
  305. package/template/plugin/src/web/index.ts +115 -0
  306. package/template/plugin/tsconfig.json +17 -0
  307. package/skills/working-the-board/references/setting-up.md +0 -3
  308. /package/skills/{working-the-board → planning-the-board}/references/project-rules.md +0 -0
package/dist/cli/cli.js CHANGED
@@ -4,13 +4,15 @@ 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
6
  import { DEFAULT_PAGE_LIMIT, LIST_LIMIT, MAX_OPTIONS, MIN_OPTIONS } from "../limits.js";
7
+ import { capabilityWords } from "../plugin/capabilities.js";
8
+ import { PLUGIN_LIST_LIMIT } from "../plugin/limits.js";
7
9
  import { fillRules, RULES_PATH, RULES_TITLE } from "../rules.js";
8
10
  import { hostOfLogin, } from "../schemas.js";
9
11
  import { COLOR_NAMES, GOAL_STATES, PEOPLE_VISIBILITIES, PERSON_ROLES, PROJECT_ROLES, PROJECT_VISIBILITIES, PROVIDER_KEYS, TASK_PRIORITIES, TASK_STATES, WEEK_STARTS, } from "../vocabulary.js";
10
12
  import { loadWebRoot, WebRootError } from "../web.js";
11
13
  import { CredentialsError, EMPTY_CREDENTIALS, loginsAt, resolveLogin, withLogin, withoutLogin, } from "./credentials.js";
12
14
  import { THE_SENTENCE } from "./directory.js";
13
- import { formatAnswer, formatCategoryLine, formatComment, formatDecision, formatDecisionLine, formatDocument, formatDocuments, formatGoal, formatGoalLine, formatListingLine, formatMemberLine, formatMore, formatOptions, formatPersonLine, formatPersonWork, formatProject, formatProjectLine, formatRules, formatRun, formatRunLine, formatTask, formatTaskLine, formatVersion, formatVersionLine, formatWork, formatWorkspace, moment, setMomentZone, } from "./format.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";
14
16
  import { COMMAND_HELP, helpFor, ownSkillPath, usage } from "./help.js";
15
17
  import { describeInstall, updateCommandFor } from "./install.js";
16
18
  import { bodyOfFile, bodyOfFolder, bodyOfSkill, formatSkillBrowse, formatSkillFile, formatSkillFind, formatSkillLoad, formatSkills, joinFile, joinFolder, joinSkill, skillPathIn, summaryOf, } from "./pack.js";
@@ -518,14 +520,19 @@ const logout = async (args, io) => {
518
520
  }
519
521
  // The token is given up at the console first, so that it is good for nothing from now on;
520
522
  // one the console no longer knows is gone already. A console out of reach leaves it to the
521
- // Agents page, and the person is told so.
522
- let gone = true;
523
+ // Agents page, and the person is told so. A console that keeps it because the person is out
524
+ // (ADR-0044) is told so too: the token is theirs again when they are back in.
525
+ let outcome = "gone";
523
526
  try {
524
527
  await clientOf(io, url, login.token).giveUpToken();
525
528
  }
526
529
  catch (error) {
527
- if (!(error instanceof ApiError && error.code === "auth.required")) {
528
- gone = false;
530
+ if (error instanceof ApiError &&
531
+ (error.code === "auth.deactivated" || error.code === "auth.not_admitted")) {
532
+ outcome = "kept";
533
+ }
534
+ else if (!(error instanceof ApiError && error.code === "auth.required")) {
535
+ outcome = "left";
529
536
  }
530
537
  }
531
538
  const next = withoutLogin(credentials, url, login.account);
@@ -536,9 +543,11 @@ const logout = async (args, io) => {
536
543
  await io.store.save(next);
537
544
  }
538
545
  const whom = login.account === undefined ? "" : ` as ${login.account}`;
539
- io.stdout(gone
546
+ io.stdout(outcome === "gone"
540
547
  ? `Signed out of ${url}${whom}: the token is given up at the console and forgotten here.\n`
541
- : `Signed out of ${url}${whom} here, but the console could not be reached to give the token up: a person removes it on the Agents page.\n`);
548
+ : outcome === "kept"
549
+ ? `Signed out of ${url}${whom} here: the console keeps the token while the person is out, for when they are back in.\n`
550
+ : `Signed out of ${url}${whom} here, but the console could not be reached to give the token up: a person removes it on the Agents page.\n`);
542
551
  envNote();
543
552
  return EXIT.ok;
544
553
  };
@@ -765,18 +774,29 @@ const MAX_SKILL_PAGES = 64;
765
774
  /** How many results a search answers at most: what the deployment allows on one page. */
766
775
  const MAX_FIND_LIMIT = 25;
767
776
  /** The pack of the project as the console lists it, and what its reads are rendered at; none when the pack did not answer. */
768
- async function packOf(project) {
769
- const listing = await project.skills();
777
+ async function packOf(reader) {
778
+ const listing = await reader.skills();
770
779
  return { listing, summary: summaryOf(listing) };
771
780
  }
772
- const skills = async (args, io) => {
773
- const { values, positionals } = parse(args, {
774
- search: { type: "string" },
775
- limit: { type: "string" },
776
- cursor: { type: "string" },
777
- });
781
+ const SKILLS_OPTIONS = {
782
+ search: { type: "string" },
783
+ limit: { type: "string" },
784
+ cursor: { type: "string" },
785
+ };
786
+ const SKILL_OPTIONS = {
787
+ file: { type: "string" },
788
+ offset: { type: "string" },
789
+ limit: { type: "string" },
790
+ };
791
+ /** The line `console skills` ends with: where the plugins' own skills are. */
792
+ const PLUGIN_SKILLS_LINE = "The plugins of this console have skills of their own: console plugins lists the plugins, console plugin <key> skills the skills of one.";
793
+ /**
794
+ * The pack as the words say: whole, a folder of it, a file's outline, or what words find; for
795
+ * the project's pack as `console skills`, and for a plugin's as `console plugin <key> skills`.
796
+ */
797
+ async function readSkills(io, session, reader, values, positionals, extras) {
778
798
  if (positionals.length > 1) {
779
- throw misuse("Say one folder: console skills [<folder>] [--search <words>]");
799
+ throw misuse(`Say one folder: ${extras.command} [<folder>] [--search <words>]`);
780
800
  }
781
801
  // The root of the pack is the mounted folder, which the deployment reads as no path at all.
782
802
  const given = positionals[0];
@@ -785,24 +805,24 @@ const skills = async (args, io) => {
785
805
  const limit = text(values, "limit");
786
806
  const cursor = text(values, "cursor");
787
807
  if (words === undefined && (limit !== undefined || cursor !== undefined)) {
788
- throw misuse("--limit and --cursor go with --search: console skills --search <words> [--limit <n>] [--cursor <cursor>]");
808
+ throw misuse(`--limit and --cursor go with --search: ${extras.command} --search <words> [--limit <n>] [--cursor <cursor>]`);
789
809
  }
790
810
  const atMost = limit === undefined ? undefined : Number(limit);
791
811
  if (atMost !== undefined &&
792
812
  (!Number.isInteger(atMost) || atMost < 1 || atMost > MAX_FIND_LIMIT)) {
793
813
  throw misuse(`Not a number of results, 1 to ${MAX_FIND_LIMIT}: ${limit}`);
794
814
  }
795
- const session = await open(io, values);
796
- const project = await session.project();
797
- const { listing, summary } = await packOf(project);
815
+ const { listing, summary } = await packOf(reader);
798
816
  // The pack as it stands; or why it does not answer, which a folder or a search would meet too.
799
817
  if (summary === undefined || (words === undefined && given === undefined)) {
800
- answer(io, session, listing, () => formatSkills(listing, io.skillsDir));
818
+ answer(io, session, listing, () => [formatSkills(listing, extras.own), extras.afterwards]
819
+ .filter((part) => part !== undefined)
820
+ .join("\n"));
801
821
  return EXIT.ok;
802
822
  }
803
823
  if (words !== undefined) {
804
824
  // One page of what the words find; more is said, and asked for with --cursor.
805
- const found = await project.skillFind(words, { path: folder, limit: atMost, cursor });
825
+ const found = await reader.skillFind(words, { path: folder, limit: atMost, cursor });
806
826
  answer(io, session, found, () => formatSkillFind(found, summary));
807
827
  return EXIT.ok;
808
828
  }
@@ -811,7 +831,7 @@ const skills = async (args, io) => {
811
831
  let next;
812
832
  let last;
813
833
  for (let n = 0; n < MAX_SKILL_PAGES; n += 1) {
814
- last = await project.skillBrowse({ path: folder, cursor: next });
834
+ last = await reader.skillBrowse({ path: folder, cursor: next });
815
835
  if (last.read?.status !== "ready") {
816
836
  break;
817
837
  }
@@ -828,6 +848,16 @@ const skills = async (args, io) => {
828
848
  const whole = joinFolder(pages, summary);
829
849
  answer(io, session, { ...browsed, read: whole === undefined ? null : bodyOfFolder(whole) }, () => formatSkillBrowse(browsed, whole));
830
850
  return EXIT.ok;
851
+ }
852
+ const skills = async (args, io) => {
853
+ const { values, positionals } = parse(args, SKILLS_OPTIONS);
854
+ const session = await open(io, values);
855
+ const project = await session.project();
856
+ return readSkills(io, session, project, values, positionals, {
857
+ command: "console skills",
858
+ own: io.skillsDir,
859
+ afterwards: PLUGIN_SKILLS_LINE,
860
+ });
831
861
  };
832
862
  /** The most characters one read of a file asks for: what the console's API allows on one page. */
833
863
  const MAX_FILE_LIMIT = 100_000;
@@ -845,28 +875,25 @@ function countOf(given, name, bounds) {
845
875
  }
846
876
  return count;
847
877
  }
848
- const skill = async (args, io) => {
849
- const { values, positionals } = parse(args, {
850
- file: { type: "string" },
851
- offset: { type: "string" },
852
- limit: { type: "string" },
853
- });
878
+ /**
879
+ * One skill of the pack whole, a file of it, or one page of a file; for the project's pack as
880
+ * `console skill`, and for a plugin's as `console plugin <key> skill`.
881
+ */
882
+ async function readSkill(io, session, reader, values, positionals, command) {
854
883
  const file = text(values, "file");
855
884
  // A file is read by its own path from the repository's root; a skill's path beside it would be
856
885
  // ignored, which reads as if it mattered, so the two together are not understood.
857
886
  if (file !== undefined && positionals.length > 0) {
858
- throw misuse("Say one: console skill <path of its SKILL.md> for a skill, or console skill --file <path> for a file of the pack.");
887
+ throw misuse(`Say one: ${command} <path of its SKILL.md> for a skill, or ${command} --file <path> for a file of the pack.`);
859
888
  }
860
889
  // A section is a page of a file read as asked (SkillCDN 0.4): the flags mean nothing without one.
861
890
  if (file === undefined &&
862
891
  (text(values, "offset") !== undefined || text(values, "limit") !== undefined)) {
863
- throw misuse("--offset and --limit go with --file: console skill --file <path> [--offset <n>] [--limit <n>]");
892
+ throw misuse(`--offset and --limit go with --file: ${command} --file <path> [--offset <n>] [--limit <n>]`);
864
893
  }
865
894
  const offset = countOf(text(values, "offset"), "offset", { least: 0 });
866
895
  const limit = countOf(text(values, "limit"), "limit", { least: 1, most: MAX_FILE_LIMIT });
867
- const session = await open(io, values);
868
- const project = await session.project();
869
- const { listing, summary } = await packOf(project);
896
+ const { listing, summary } = await packOf(reader);
870
897
  if (summary === undefined) {
871
898
  answer(io, session, listing, () => formatSkills(listing, io.skillsDir));
872
899
  return EXIT.ok;
@@ -874,7 +901,7 @@ const skill = async (args, io) => {
874
901
  if (file !== undefined && (offset !== undefined || limit !== undefined)) {
875
902
  // One page as asked, the section a search or an outline named; the result says where the
876
903
  // file continues, and asks for nothing.
877
- const page = await project.skillFile(file, { offset, limit });
904
+ const page = await reader.skillFile(file, { offset, limit });
878
905
  const section = page.read === null ? undefined : joinFile([page], summary, limit);
879
906
  answer(io, session, { ...page, read: section === undefined ? null : bodyOfFile(section) }, () => formatSkillFile(page, section));
880
907
  return EXIT.ok;
@@ -885,7 +912,7 @@ const skill = async (args, io) => {
885
912
  let offset;
886
913
  let last;
887
914
  for (let n = 0; n < MAX_SKILL_PAGES; n += 1) {
888
- last = await project.skillFile(file, offset === undefined ? {} : { offset });
915
+ last = await reader.skillFile(file, offset === undefined ? {} : { offset });
889
916
  if (last.read === null) {
890
917
  break;
891
918
  }
@@ -903,7 +930,7 @@ const skill = async (args, io) => {
903
930
  answer(io, session, { ...fileRead, read: whole === undefined ? null : bodyOfFile(whole) }, () => formatSkillFile(fileRead, whole));
904
931
  return EXIT.ok;
905
932
  }
906
- const wanted = skillPathIn(theOne(positionals, "Say which skill: console skill <path of its SKILL.md>, or console skill --file <path> for a file of the pack"), listing.address);
933
+ const wanted = skillPathIn(theOne(positionals, `Say which skill: ${command} <path of its SKILL.md>, or ${command} --file <path> for a file of the pack`), listing.address);
907
934
  if ("error" in wanted) {
908
935
  throw misuse(wanted.error);
909
936
  }
@@ -912,7 +939,7 @@ const skill = async (args, io) => {
912
939
  let cursor;
913
940
  let last;
914
941
  for (let n = 0; n < MAX_SKILL_PAGES; n += 1) {
915
- last = await project.skill(wanted.path, cursor);
942
+ last = await reader.skill(wanted.path, cursor);
916
943
  if (last.read?.status !== "ready") {
917
944
  break;
918
945
  }
@@ -929,7 +956,492 @@ const skill = async (args, io) => {
929
956
  const whole = joinSkill(pages, summary);
930
957
  answer(io, session, { ...loaded, read: whole === undefined ? null : bodyOfSkill(whole) }, () => formatSkillLoad(loaded, whole));
931
958
  return EXIT.ok;
959
+ }
960
+ const skill = async (args, io) => {
961
+ const { values, positionals } = parse(args, SKILL_OPTIONS);
962
+ const session = await open(io, values);
963
+ const project = await session.project();
964
+ return readSkill(io, session, project, values, positionals, "console skill");
965
+ };
966
+ // ---- The plugins (docs/specs/plugins.md, "The command") ----
967
+ // `console plugins` and `console plugin <key> ...` are the two entries: the command sends the
968
+ // words as typed and renders the console's generic shapes, and knows nothing of any plugin.
969
+ const plugins = async (args, io) => {
970
+ const { values } = parse(args, {}, false);
971
+ const session = await open(io, values);
972
+ const [listed, me] = await Promise.all([session.client.plugins(), session.client.me()]);
973
+ answer(io, session, listed, () => formatPlugins(listed, me.language));
974
+ return EXIT.ok;
975
+ };
976
+ /** The flags of the command's own on `console plugin ...`, wherever they stand among the words. */
977
+ const PLUGIN_GLOBAL_FLAGS = {
978
+ json: "boolean",
979
+ url: "string",
980
+ account: "string",
981
+ project: "string",
982
+ };
983
+ /** The flags of the command's own on a plugin's command: the file it uploads first, the run it is for. */
984
+ const PLUGIN_COMMAND_FLAGS = {
985
+ file: "string",
986
+ run: "string",
987
+ };
988
+ /**
989
+ * The words with the flags named in `own` taken out, wherever they stand: the rest is the
990
+ * plugin's, and goes to the console as typed. `--` ends the flags, and stays among the words.
991
+ */
992
+ function splitWords(args, own) {
993
+ const values = {};
994
+ const words = [];
995
+ let ended = false;
996
+ for (let index = 0; index < args.length; index += 1) {
997
+ const word = args[index] ?? "";
998
+ if (ended || !word.startsWith("--") || word.length === 2) {
999
+ ended = ended || word === "--";
1000
+ words.push(word);
1001
+ continue;
1002
+ }
1003
+ const equals = word.indexOf("=");
1004
+ const name = equals < 0 ? word.slice(2) : word.slice(2, equals);
1005
+ const type = Object.hasOwn(own, name) ? own[name] : undefined;
1006
+ if (type === undefined) {
1007
+ words.push(word);
1008
+ continue;
1009
+ }
1010
+ if (type === "boolean") {
1011
+ if (equals >= 0) {
1012
+ throw misuse(`--${name} takes no value`);
1013
+ }
1014
+ values[name] = true;
1015
+ continue;
1016
+ }
1017
+ if (equals >= 0) {
1018
+ values[name] = word.slice(equals + 1);
1019
+ continue;
1020
+ }
1021
+ const next = args[index + 1];
1022
+ if (next === undefined) {
1023
+ throw misuse(`--${name} needs a value`);
1024
+ }
1025
+ values[name] = next;
1026
+ index += 1;
1027
+ }
1028
+ return { values, words };
1029
+ }
1030
+ const PLUGIN_USAGE = "console plugin <key> [help [<command>] | <command> [args] | records <collection> | skills | skill <path> | enable | disable | settings | secret <name> | grant <role> | revoke <id>]. console plugins lists the plugins.";
1031
+ const plugin = async (args, io) => {
1032
+ const { values, words } = splitWords(args, PLUGIN_GLOBAL_FLAGS);
1033
+ const [key, verb, ...rest] = words;
1034
+ if (key === undefined || key.startsWith("-")) {
1035
+ throw misuse(`Say which plugin: ${PLUGIN_USAGE}`);
1036
+ }
1037
+ const session = await open(io, values);
1038
+ const client = session.client.plugin(key);
1039
+ switch (verb) {
1040
+ case undefined: {
1041
+ const [detail, me] = await Promise.all([client.get(), session.client.me()]);
1042
+ answer(io, session, detail, () => formatPlugin(detail, me.language));
1043
+ return EXIT.ok;
1044
+ }
1045
+ case "help": {
1046
+ if (rest.length > 1) {
1047
+ throw misuse(`Say one command: console plugin ${key} help [<command>]`);
1048
+ }
1049
+ const name = rest[0];
1050
+ const detail = await client.get();
1051
+ const operation = name === undefined
1052
+ ? undefined
1053
+ : detail.operations.find((candidate) => candidate.name === name);
1054
+ if (name !== undefined && operation === undefined) {
1055
+ throw misuse(`Not a command or a query of ${key} you may run: ${name}. console plugin ${key} lists them.`);
1056
+ }
1057
+ answer(io, session, operation ?? detail.operations, () => formatPluginHelp(detail, name));
1058
+ return EXIT.ok;
1059
+ }
1060
+ case "records":
1061
+ return pluginRecords(io, session, client, rest, text(values, "project"));
1062
+ case "skills": {
1063
+ const parsed = parse(rest, SKILLS_OPTIONS);
1064
+ return readSkills(io, session, client, parsed.values, parsed.positionals, {
1065
+ command: `console plugin ${key} skills`,
1066
+ });
1067
+ }
1068
+ case "skill": {
1069
+ const parsed = parse(rest, SKILL_OPTIONS);
1070
+ return readSkill(io, session, client, parsed.values, parsed.positionals, `console plugin ${key} skill`);
1071
+ }
1072
+ case "enable":
1073
+ return pluginEnable(io, session, client, rest);
1074
+ case "disable": {
1075
+ parse(rest, {}, false);
1076
+ const detail = await client.disable();
1077
+ answer(io, session, detail, () => `Disabled ${detail.key}: its commands are refused until it is enabled again; what the console kept of it stays.`);
1078
+ return EXIT.ok;
1079
+ }
1080
+ case "settings":
1081
+ return pluginSettings(io, session, client, rest);
1082
+ case "secret":
1083
+ return pluginSecret(io, session, client, rest);
1084
+ case "grant":
1085
+ return pluginGrant(io, session, client, rest, text(values, "project"));
1086
+ case "revoke": {
1087
+ const { positionals } = parse(rest, {});
1088
+ const id = theOne(positionals, `Say which grant: console plugin ${key} revoke <grant id>`);
1089
+ const revoked = await client.revoke(id);
1090
+ answer(io, session, revoked, () => `Revoked: ${formatPluginGrantLine(revoked)}`);
1091
+ return EXIT.ok;
1092
+ }
1093
+ default:
1094
+ return pluginInvoke(io, session, client, verb, rest);
1095
+ }
932
1096
  };
1097
+ /** A query's answer that is a file, as the console carries one: its name, its type, its bytes as base64. */
1098
+ function queryFileOf(result) {
1099
+ if (result.kind !== "query" || typeof result.data !== "object" || result.data === null) {
1100
+ return undefined;
1101
+ }
1102
+ const file = result.data.file;
1103
+ if (typeof file !== "object" ||
1104
+ file === null ||
1105
+ typeof file.name !== "string" ||
1106
+ typeof file.type !== "string" ||
1107
+ typeof file.base64 !== "string") {
1108
+ return undefined;
1109
+ }
1110
+ const named = file;
1111
+ return {
1112
+ name: basename(named.name) || "answer",
1113
+ type: named.type,
1114
+ bytes: Uint8Array.from(Buffer.from(named.base64, "base64")),
1115
+ };
1116
+ }
1117
+ /**
1118
+ * A command or a query of the plugin, by the words as typed: a file named with --file, or read
1119
+ * from standard input for -, is uploaded first and goes as a handle; the console parses the rest.
1120
+ */
1121
+ async function pluginInvoke(io, session, client, name, rest) {
1122
+ const { values, words } = splitWords(rest, PLUGIN_COMMAND_FLAGS);
1123
+ const stdin = words.includes("-");
1124
+ const filePath = text(values, "file");
1125
+ if (filePath !== undefined && stdin) {
1126
+ throw misuse("Say one file: --file <path>, or - for standard input.");
1127
+ }
1128
+ const files = [];
1129
+ if (filePath !== undefined) {
1130
+ let bytes;
1131
+ try {
1132
+ bytes = await io.readBytes(filePath);
1133
+ }
1134
+ catch {
1135
+ throw misuse(`The file could not be read: ${filePath}`);
1136
+ }
1137
+ const fileName = basename(filePath);
1138
+ const uploaded = await client.upload({
1139
+ name: fileName,
1140
+ bytes,
1141
+ contentType: CONTENT_TYPES[extname(fileName).toLowerCase()] ?? "application/octet-stream",
1142
+ });
1143
+ files.push(uploaded.handle);
1144
+ }
1145
+ else if (stdin) {
1146
+ const uploaded = await client.upload({
1147
+ name: "stdin.txt",
1148
+ bytes: new TextEncoder().encode(await io.readStdin()),
1149
+ contentType: "text/plain",
1150
+ });
1151
+ files.push(uploaded.handle);
1152
+ }
1153
+ const project = await session.projectKey();
1154
+ const run = text(values, "run");
1155
+ const result = await client.invoke({
1156
+ argv: [name, ...words.filter((word) => word !== "-")],
1157
+ ...(files.length === 0 ? {} : { files }),
1158
+ ...(project === undefined ? {} : { project }),
1159
+ ...(run === undefined ? {} : { run }),
1160
+ });
1161
+ // A query that answers a file: saved under its name in the working directory, as console file saves one.
1162
+ const file = queryFileOf(result);
1163
+ let saved;
1164
+ if (file !== undefined && !session.json) {
1165
+ try {
1166
+ saved = await io.writeBytes(file.name, file.bytes, file.name);
1167
+ }
1168
+ catch {
1169
+ throw failed(`The file could not be written at ${file.name}.`);
1170
+ }
1171
+ }
1172
+ answer(io, session, result, () => file !== undefined && saved !== undefined
1173
+ ? `Saved ${file.name} (${file.bytes.byteLength} bytes, ${file.type}) at ${saved}`
1174
+ : formatInvocation(result));
1175
+ return result.waiting === null ? EXIT.ok : EXIT.waiting;
1176
+ }
1177
+ /** A condition's value as typed: a number, true, false and null read as such; JSON where it begins as JSON; text otherwise. */
1178
+ function whereValueOf(word) {
1179
+ if (word === "true") {
1180
+ return true;
1181
+ }
1182
+ if (word === "false") {
1183
+ return false;
1184
+ }
1185
+ if (word === "null") {
1186
+ return null;
1187
+ }
1188
+ if (/^-?\d+(?:\.\d+)?$/.test(word)) {
1189
+ return Number(word);
1190
+ }
1191
+ if (word.startsWith("{") || word.startsWith("[")) {
1192
+ try {
1193
+ return JSON.parse(word);
1194
+ }
1195
+ catch {
1196
+ return word;
1197
+ }
1198
+ }
1199
+ return word;
1200
+ }
1201
+ /** The records of a collection one may read, a page of them; or one record, or its versions. */
1202
+ async function pluginRecords(io, session, client, rest, project) {
1203
+ const { values, positionals } = parse(rest, {
1204
+ where: { type: "string", multiple: true },
1205
+ search: { type: "string" },
1206
+ order: { type: "string" },
1207
+ limit: { type: "string" },
1208
+ after: { type: "string" },
1209
+ archived: { type: "boolean" },
1210
+ versions: { type: "boolean" },
1211
+ });
1212
+ const [collection, id, ...more] = positionals;
1213
+ if (collection === undefined || more.length > 0) {
1214
+ throw misuse(`Say which collection: console plugin ${client.key} records <collection> [<id>] [--where <field>=<value>]... [--search <words>] [--order <field>[:desc]] [--limit <n>] [--after <cursor>] [--archived] [--versions]`);
1215
+ }
1216
+ if (id !== undefined) {
1217
+ if (on(values, "versions")) {
1218
+ const versions = await client.recordVersions(collection, id);
1219
+ answer(io, session, versions, () => formatPluginRecordVersions(versions));
1220
+ return EXIT.ok;
1221
+ }
1222
+ const record = await client.record(collection, id);
1223
+ answer(io, session, record, () => formatPluginRecord(record));
1224
+ return EXIT.ok;
1225
+ }
1226
+ if (on(values, "versions")) {
1227
+ throw misuse("--versions goes with a record's id: console plugin <key> records <collection> <id> --versions");
1228
+ }
1229
+ const where = {};
1230
+ for (const pair of texts(values, "where")) {
1231
+ const equals = pair.indexOf("=");
1232
+ if (equals <= 0) {
1233
+ throw misuse(`Not a condition, as <field>=<value>: ${pair}`);
1234
+ }
1235
+ where[pair.slice(0, equals)] = whereValueOf(pair.slice(equals + 1));
1236
+ }
1237
+ const order = text(values, "order");
1238
+ const page = await client.records(collection, {
1239
+ ...(Object.keys(where).length === 0 ? {} : { where }),
1240
+ q: text(values, "search"),
1241
+ order: order === undefined ? undefined : order.endsWith(":desc") ? `-${order.slice(0, -5)}` : order,
1242
+ limit: countOf(text(values, "limit"), "limit", { least: 1, most: PLUGIN_LIST_LIMIT }),
1243
+ after: text(values, "after"),
1244
+ project,
1245
+ archived: on(values, "archived") ? true : undefined,
1246
+ });
1247
+ answer(io, session, page, () => formatPluginRecords(page));
1248
+ return EXIT.ok;
1249
+ }
1250
+ /** The values `--set <key>=<value>` give, each read as its setting's type; an empty value clears one. */
1251
+ function settingValuesOf(pairs, detail) {
1252
+ const values = {};
1253
+ const read = (type, word, key) => {
1254
+ switch (type) {
1255
+ case "number": {
1256
+ const value = Number(word);
1257
+ if (!Number.isFinite(value) || word.trim().length === 0) {
1258
+ throw misuse(`Not a number for the setting ${key}: ${word}`);
1259
+ }
1260
+ return value;
1261
+ }
1262
+ case "boolean":
1263
+ if (word !== "true" && word !== "false") {
1264
+ throw misuse(`The setting ${key} is true or false: ${word}`);
1265
+ }
1266
+ return word === "true";
1267
+ default:
1268
+ return word;
1269
+ }
1270
+ };
1271
+ for (const pair of pairs) {
1272
+ const equals = pair.indexOf("=");
1273
+ if (equals <= 0) {
1274
+ throw misuse(`Not a setting, as <key>=<value>: ${pair}`);
1275
+ }
1276
+ const key = pair.slice(0, equals);
1277
+ const word = pair.slice(equals + 1);
1278
+ const setting = detail.settings.find((candidate) => candidate.key === key);
1279
+ if (setting === undefined) {
1280
+ throw misuse(`Not a setting of ${detail.key}: ${key}. Its settings: ${detail.settings.map((candidate) => candidate.key).join(", ") || "none"}`);
1281
+ }
1282
+ if (setting.type === "secret") {
1283
+ throw misuse(`${key} is a secret: console plugin ${detail.key} secret ${key} reads it from standard input.`);
1284
+ }
1285
+ if (word.length === 0) {
1286
+ values[key] = null;
1287
+ }
1288
+ else if (setting.type === "list") {
1289
+ values[key] = word
1290
+ .split(",")
1291
+ .map((item) => item.trim())
1292
+ .filter((item) => item.length > 0)
1293
+ .map((item) => read(setting.of ?? "text", item, key));
1294
+ }
1295
+ else {
1296
+ values[key] = read(setting.type, word, key);
1297
+ }
1298
+ }
1299
+ return values;
1300
+ }
1301
+ /** The capabilities a plugin asks, each with what it opens and what it risks, for the administrator to read before consenting. */
1302
+ function capabilityLines(capabilities) {
1303
+ return capabilities.map((capability) => {
1304
+ const words = capabilityWords(capability);
1305
+ return words === undefined
1306
+ ? ` ${capability}: a capability this command does not know`
1307
+ : ` ${capability}: opens ${words.opens}; a malicious plugin reaches ${words.reach}`;
1308
+ });
1309
+ }
1310
+ /** Enables a plugin, with consent to every capability it asks, named again; an administrator's. */
1311
+ async function pluginEnable(io, session, client, rest) {
1312
+ const { values, positionals } = parse(rest, {
1313
+ consent: { type: "string", multiple: true },
1314
+ board: { type: "string" },
1315
+ projects: { type: "string", multiple: true },
1316
+ set: { type: "string", multiple: true },
1317
+ });
1318
+ if (positionals.length > 0) {
1319
+ throw misuse(`Nothing follows enable but its flags: console plugin ${client.key} enable [--consent <capability>,...] [--board new|<key>] [--projects all|<key>,...] [--set <key>=<value>]...`);
1320
+ }
1321
+ const [detail, me] = await Promise.all([client.get(), session.client.me()]);
1322
+ 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);
1327
+ 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`);
1329
+ }
1330
+ const missing = asked.filter((capability) => !consent.includes(capability));
1331
+ 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(", ")}.`);
1333
+ }
1334
+ const boardFlag = text(values, "board");
1335
+ let board = null;
1336
+ if (boardFlag === "new") {
1337
+ if (detail.boardDefaults === null) {
1338
+ throw misuse(`${detail.key} names no defaults for a board project: choose one with --board <key>.`);
1339
+ }
1340
+ board = {
1341
+ create: {
1342
+ key: detail.boardDefaults.key,
1343
+ name: localized(detail.boardDefaults.name, me.language),
1344
+ description: localized(detail.boardDefaults.description, me.language),
1345
+ },
1346
+ };
1347
+ }
1348
+ else if (boardFlag !== undefined) {
1349
+ board = { project: boardFlag };
1350
+ }
1351
+ const named = texts(values, "projects")
1352
+ .flatMap((value) => value.split(","))
1353
+ .map((key) => key.trim())
1354
+ .filter((key) => key.length > 0);
1355
+ const settings = settingValuesOf(texts(values, "set"), detail);
1356
+ const enabled = await client.enable({
1357
+ consent,
1358
+ board,
1359
+ projects: named.length === 0 || named.includes("all") ? "all" : named,
1360
+ ...(Object.keys(settings).length === 0 ? {} : { settings }),
1361
+ });
1362
+ answer(io, session, enabled, () => [
1363
+ `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"}.`,
1364
+ ...(enabled.settings.length === 0
1365
+ ? []
1366
+ : [
1367
+ "Settings:",
1368
+ ...enabled.settings.map((setting) => formatPluginSettingLine(setting, me.language)),
1369
+ ]),
1370
+ ].join("\n"));
1371
+ return EXIT.ok;
1372
+ }
1373
+ /** The settings as they stand, or changed as `--set` says; an administrator's. */
1374
+ async function pluginSettings(io, session, client, rest) {
1375
+ const { values, positionals } = parse(rest, { set: { type: "string", multiple: true } });
1376
+ if (positionals.length > 0) {
1377
+ throw misuse(`Nothing follows settings but --set: console plugin ${client.key} settings [--set <key>=<value>]...`);
1378
+ }
1379
+ const [detail, me] = await Promise.all([client.get(), session.client.me()]);
1380
+ const sets = texts(values, "set");
1381
+ if (sets.length === 0) {
1382
+ answer(io, session, detail.settings, () => detail.settings.length === 0
1383
+ ? `${detail.key} has no settings.`
1384
+ : [
1385
+ `Settings of ${detail.key}:`,
1386
+ ...detail.settings.map((setting) => formatPluginSettingLine(setting, me.language)),
1387
+ ].join("\n"));
1388
+ return EXIT.ok;
1389
+ }
1390
+ const changed = await client.updateSettings({ values: settingValuesOf(sets, detail) });
1391
+ answer(io, session, changed.settings, () => [
1392
+ `Changed the settings of ${changed.key}:`,
1393
+ ...changed.settings.map((setting) => formatPluginSettingLine(setting, me.language)),
1394
+ ].join("\n"));
1395
+ return EXIT.ok;
1396
+ }
1397
+ /** Seals a secret of the plugin, read from standard input and shown never; or takes it away. */
1398
+ async function pluginSecret(io, session, client, rest) {
1399
+ const { values, positionals } = parse(rest, { remove: { type: "boolean" } });
1400
+ const name = theOne(positionals, `Say which secret: console plugin ${client.key} secret <name> [--remove]; the value is read from standard input.`);
1401
+ if (on(values, "remove")) {
1402
+ await client.removeSecret(name);
1403
+ answer(io, session, { name, set: false }, () => `Removed the secret ${name} of ${client.key}.`);
1404
+ return EXIT.ok;
1405
+ }
1406
+ const value = (await io.readStdin()).replace(/\r?\n$/, "");
1407
+ if (value.length === 0) {
1408
+ throw misuse(`Pipe the value in: console plugin ${client.key} secret ${name} < file. It is sealed by the console and shown never.`);
1409
+ }
1410
+ await client.setSecret(name, value);
1411
+ answer(io, session, { name, set: true }, () => `Set the secret ${name} of ${client.key}: sealed, and shown by nothing from now on.`);
1412
+ return EXIT.ok;
1413
+ }
1414
+ /** Grants a role of the plugin to a person, to the members of a project, or to everyone; an administrator's. */
1415
+ async function pluginGrant(io, session, client, rest, projectKey) {
1416
+ const { values, positionals } = parse(rest, {
1417
+ person: { type: "string" },
1418
+ everyone: { type: "boolean" },
1419
+ });
1420
+ const usage = `console plugin ${client.key} grant <role> --person <login> | --project <key> | --everyone`;
1421
+ const role = theOne(positionals, `Say which role: ${usage}`);
1422
+ const login = text(values, "person");
1423
+ 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}`);
1426
+ }
1427
+ let input;
1428
+ if (login !== undefined) {
1429
+ const person = await session.client.findPerson(login);
1430
+ if (person === undefined) {
1431
+ throw failed(`No person has the login ${login} among those you may see.`);
1432
+ }
1433
+ input = { role, person: person.id };
1434
+ }
1435
+ else if (projectKey !== undefined) {
1436
+ input = { role, project: projectKey };
1437
+ }
1438
+ else {
1439
+ input = { role, everyone: true };
1440
+ }
1441
+ const granted = await client.grant(input);
1442
+ answer(io, session, granted, () => `Granted: ${formatPluginGrantLine(granted)}`);
1443
+ return EXIT.ok;
1444
+ }
933
1445
  /** A version number given as a flag, or nothing; anything else is not understood. */
934
1446
  function versionOf(given) {
935
1447
  if (given === undefined) {
@@ -1011,9 +1523,10 @@ const rules = async (args, io) => {
1011
1523
  ? {
1012
1524
  workspace: forPerson(found.workspace, me.language),
1013
1525
  project: forPerson(found.project, me.language),
1526
+ ...(found.plugins === undefined ? {} : { plugins: found.plugins }),
1014
1527
  }
1015
1528
  : forPerson(found, me.language);
1016
- answer(io, session, read, () => formatRules(read));
1529
+ answer(io, session, read, () => formatRules(read, me.language));
1017
1530
  return EXIT.ok;
1018
1531
  };
1019
1532
  /**
@@ -1287,6 +1800,7 @@ const take = async (args, io) => {
1287
1800
  const rules = {
1288
1801
  workspace: forPerson(ruling.workspace, me.language),
1289
1802
  project: forPerson(ruling.project, me.language),
1803
+ ...(ruling.plugins === undefined ? {} : { plugins: ruling.plugins }),
1290
1804
  };
1291
1805
  answer(io, session, { run, task: taken, comments: comments.items, rules }, () => [
1292
1806
  `Run ${run.id} began on #${found.number} ${found.title}, as "${run.agent}" for ${run.person.login}. The task is in progress and theirs.${tookOver}${elsewhere}`,
@@ -1296,7 +1810,7 @@ const take = async (args, io) => {
1296
1810
  "",
1297
1811
  formatTask(taken, undefined, undefined, comments.items),
1298
1812
  "",
1299
- formatRules(rules),
1813
+ formatRules(rules, me.language),
1300
1814
  "",
1301
1815
  'Say how it goes: console report "<what you found or did>". Hand in what you make: console hand-in <https url | file path>.',
1302
1816
  'Ask when a person must decide: console ask "<question>" --option "..." --option "...". A word for the task\'s people that is not a report: console comment <number> "<markdown>". End with: console finish --summary "<what was done, what is left>".',
@@ -1923,8 +2437,8 @@ const workspace = async (args, io) => {
1923
2437
  set: (token) => client.setWorkspacePackToken({ token }),
1924
2438
  remove: () => client.removeWorkspacePackToken(),
1925
2439
  connect: () => client.connectWorkspacePackToken(),
1926
- async standing() {
1927
- const state = await client.workspacePackTokenConnection();
2440
+ async claim() {
2441
+ const state = await client.claimWorkspacePackTokenConnection();
1928
2442
  return state.status === "connected"
1929
2443
  ? { status: "connected", held: state.workspace }
1930
2444
  : state;
@@ -2061,7 +2575,7 @@ async function packToken(io, session, way, holder, format) {
2061
2575
  io.stdout(`To connect the pack, a person who may open its repository opens ${started.url} signed in at SkillCDN, checks that the page shows the code ${started.code}, and approves; the console receives the SkillCDN token and keeps it sealed, shown to nobody.\nWaiting for that, until ${moment(started.expiresAt)}.\n`);
2062
2576
  for (;;) {
2063
2577
  await io.sleep(started.intervalSeconds * 1000);
2064
- const state = await holder.standing();
2578
+ const state = await holder.claim();
2065
2579
  if (state.status === "pending") {
2066
2580
  continue;
2067
2581
  }
@@ -2098,8 +2612,8 @@ const projectPackToken = async (args, io) => {
2098
2612
  set: (token) => project.setPackToken({ token }),
2099
2613
  remove: () => project.removePackToken(),
2100
2614
  connect: () => project.connectPackToken(),
2101
- async standing() {
2102
- const state = await project.packTokenConnection();
2615
+ async claim() {
2616
+ const state = await project.claimPackTokenConnection();
2103
2617
  return state.status === "connected" ? { status: "connected", held: state.project } : state;
2104
2618
  },
2105
2619
  }, formatProject);
@@ -2287,6 +2801,8 @@ const COMMANDS = {
2287
2801
  skills,
2288
2802
  skill,
2289
2803
  rules,
2804
+ plugins,
2805
+ plugin,
2290
2806
  people,
2291
2807
  person,
2292
2808
  workspace,
@@ -2417,6 +2933,10 @@ function hintFor(error, io, seen) {
2417
2933
  return `Install the console's version: ${updateWords(io, seen.version)}; or ask the console's operator to update the console.\n`;
2418
2934
  case "auth.required":
2419
2935
  return "The token is not one the console knows, any more or at all. A person signs the command in again: console login\n";
2936
+ case "auth.deactivated":
2937
+ return "Tell the person: they are taken back in on the People page, by an administrator or by their organization, and this token works again then. console logout forgets it here meanwhile.\n";
2938
+ case "auth.not_admitted":
2939
+ return "Tell the person: they are back in when what let them in is named again, or when an administrator lists their login and they sign in again, and this token works again then. console logout forgets it here meanwhile.\n";
2420
2940
  case "project.not_found":
2421
2941
  return "No project has this key, or it is not yours to see. console projects lists the projects you may work in.\n";
2422
2942
  case "document.not_found":
@@ -2441,6 +2961,20 @@ function hintFor(error, io, seen) {
2441
2961
  return "What was asked is an administrator's, or an owner's of the project, and the person this token acts as is not one here. Hand the step to them.\n";
2442
2962
  case "project.key_taken":
2443
2963
  return "A project has that key already. console projects lists them.\n";
2964
+ case "project.too_many":
2965
+ return "The workspace holds as many projects as one may. Work goes on in a project there is.\n";
2966
+ case "project.too_many_bytes":
2967
+ return "The project's files hold as many bytes together as this console allows. Hand in a link to where the file is instead, and say so in the report.\n";
2968
+ case "task.too_many":
2969
+ return "The project holds as many tasks as one may, those put away counted. The work goes on in another project; ask the person which.\n";
2970
+ case "goal.too_many":
2971
+ return "The project holds as many goals as one may. console goals lists them; one done or dropped is still a goal.\n";
2972
+ case "decision.too_many":
2973
+ return "The project holds as many decisions as one may. Ask the person in the report instead.\n";
2974
+ case "document.too_many":
2975
+ return "The project holds as many pages as one may. Write on a page there is: console docs lists them.\n";
2976
+ case "run.too_many":
2977
+ return "The task has been taken as many times as one may. Write a new task for what is left, and take that.\n";
2444
2978
  case "project.invalid_address":
2445
2979
  return "The skills address is not one as SkillCDN spells it; none makes the organization's skills the project's.\n";
2446
2980
  case "project.invalid_icon":