@skillcdn/console 0.4.1 → 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 (271) hide show
  1. package/README.md +19 -2
  2. package/SKILLCDN.md +7 -5
  3. package/dist/api.d.ts +4 -3
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/api.js +4 -3
  6. package/dist/api.js.map +1 -1
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +543 -34
  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 +35 -2
  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 +97 -1
  34. package/dist/client.d.ts.map +1 -1
  35. package/dist/client.js +63 -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/plugin-frame.d.ts +52 -0
  48. package/dist/components/plugin-frame.d.ts.map +1 -0
  49. package/dist/components/plugin-frame.js +314 -0
  50. package/dist/components/plugin-frame.js.map +1 -0
  51. package/dist/components/plugin-settings.d.ts +38 -0
  52. package/dist/components/plugin-settings.d.ts.map +1 -0
  53. package/dist/components/plugin-settings.js +348 -0
  54. package/dist/components/plugin-settings.js.map +1 -0
  55. package/dist/components/shell.d.ts +2 -0
  56. package/dist/components/shell.d.ts.map +1 -1
  57. package/dist/components/shell.js +26 -1
  58. package/dist/components/shell.js.map +1 -1
  59. package/dist/components/workspace.d.ts +7 -1
  60. package/dist/components/workspace.d.ts.map +1 -1
  61. package/dist/components/workspace.js +1 -1
  62. package/dist/components/workspace.js.map +1 -1
  63. package/dist/console.css +327 -0
  64. package/dist/console.d.ts.map +1 -1
  65. package/dist/console.js +136 -26
  66. package/dist/console.js.map +1 -1
  67. package/dist/data.d.ts +7 -1
  68. package/dist/data.d.ts.map +1 -1
  69. package/dist/data.js +19 -2
  70. package/dist/data.js.map +1 -1
  71. package/dist/i18n/en.d.ts +149 -0
  72. package/dist/i18n/en.d.ts.map +1 -1
  73. package/dist/i18n/en.js +160 -0
  74. package/dist/i18n/en.js.map +1 -1
  75. package/dist/i18n/ko.d.ts.map +1 -1
  76. package/dist/i18n/ko.js +144 -0
  77. package/dist/i18n/ko.js.map +1 -1
  78. package/dist/index.d.ts +3 -1
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +3 -1
  81. package/dist/index.js.map +1 -1
  82. package/dist/plugin/args.d.ts +148 -0
  83. package/dist/plugin/args.d.ts.map +1 -0
  84. package/dist/plugin/args.js +369 -0
  85. package/dist/plugin/args.js.map +1 -0
  86. package/dist/plugin/bridge.d.ts +146 -0
  87. package/dist/plugin/bridge.d.ts.map +1 -0
  88. package/dist/plugin/bridge.js +125 -0
  89. package/dist/plugin/bridge.js.map +1 -0
  90. package/dist/plugin/capabilities.d.ts +20 -0
  91. package/dist/plugin/capabilities.d.ts.map +1 -0
  92. package/dist/plugin/capabilities.js +133 -0
  93. package/dist/plugin/capabilities.js.map +1 -0
  94. package/dist/plugin/context.d.ts +374 -0
  95. package/dist/plugin/context.d.ts.map +1 -0
  96. package/dist/plugin/context.js +2 -0
  97. package/dist/plugin/context.js.map +1 -0
  98. package/dist/plugin/define.d.ts +208 -0
  99. package/dist/plugin/define.d.ts.map +1 -0
  100. package/dist/plugin/define.js +482 -0
  101. package/dist/plugin/define.js.map +1 -0
  102. package/dist/plugin/definition-error.d.ts +11 -0
  103. package/dist/plugin/definition-error.d.ts.map +1 -0
  104. package/dist/plugin/definition-error.js +17 -0
  105. package/dist/plugin/definition-error.js.map +1 -0
  106. package/dist/plugin/limits.d.ts +67 -0
  107. package/dist/plugin/limits.d.ts.map +1 -0
  108. package/dist/plugin/limits.js +70 -0
  109. package/dist/plugin/limits.js.map +1 -0
  110. package/dist/plugin/load-worker.d.ts +7 -0
  111. package/dist/plugin/load-worker.d.ts.map +1 -0
  112. package/dist/plugin/load-worker.js +41 -0
  113. package/dist/plugin/load-worker.js.map +1 -0
  114. package/dist/plugin/load.d.ts +18 -0
  115. package/dist/plugin/load.d.ts.map +1 -0
  116. package/dist/plugin/load.js +62 -0
  117. package/dist/plugin/load.js.map +1 -0
  118. package/dist/plugin/manifest.d.ts +237 -0
  119. package/dist/plugin/manifest.d.ts.map +1 -0
  120. package/dist/plugin/manifest.js +411 -0
  121. package/dist/plugin/manifest.js.map +1 -0
  122. package/dist/plugin/refusals.d.ts +49 -0
  123. package/dist/plugin/refusals.d.ts.map +1 -0
  124. package/dist/plugin/refusals.js +79 -0
  125. package/dist/plugin/refusals.js.map +1 -0
  126. package/dist/plugin/rest-schemas.d.ts +556 -0
  127. package/dist/plugin/rest-schemas.d.ts.map +1 -0
  128. package/dist/plugin/rest-schemas.js +262 -0
  129. package/dist/plugin/rest-schemas.js.map +1 -0
  130. package/dist/plugin/sandbox.d.ts +23 -0
  131. package/dist/plugin/sandbox.d.ts.map +1 -0
  132. package/dist/plugin/sandbox.js +99 -0
  133. package/dist/plugin/sandbox.js.map +1 -0
  134. package/dist/plugin/testing.d.ts +45 -0
  135. package/dist/plugin/testing.d.ts.map +1 -0
  136. package/dist/plugin/testing.js +420 -0
  137. package/dist/plugin/testing.js.map +1 -0
  138. package/dist/plugin/web-react.d.ts +11 -0
  139. package/dist/plugin/web-react.d.ts.map +1 -0
  140. package/dist/plugin/web-react.js +34 -0
  141. package/dist/plugin/web-react.js.map +1 -0
  142. package/dist/plugin/web.d.ts +95 -0
  143. package/dist/plugin/web.d.ts.map +1 -0
  144. package/dist/plugin/web.js +245 -0
  145. package/dist/plugin/web.js.map +1 -0
  146. package/dist/plugin-nav.d.ts +29 -0
  147. package/dist/plugin-nav.d.ts.map +1 -0
  148. package/dist/plugin-nav.js +92 -0
  149. package/dist/plugin-nav.js.map +1 -0
  150. package/dist/plugin-testing.d.ts +2 -0
  151. package/dist/plugin-testing.d.ts.map +1 -0
  152. package/dist/plugin-testing.js +4 -0
  153. package/dist/plugin-testing.js.map +1 -0
  154. package/dist/plugin-web-react.d.ts +3 -0
  155. package/dist/plugin-web-react.d.ts.map +1 -0
  156. package/dist/plugin-web-react.js +3 -0
  157. package/dist/plugin-web-react.js.map +1 -0
  158. package/dist/plugin-web.d.ts +4 -0
  159. package/dist/plugin-web.d.ts.map +1 -0
  160. package/dist/plugin-web.js +5 -0
  161. package/dist/plugin-web.js.map +1 -0
  162. package/dist/plugin.d.ts +11 -0
  163. package/dist/plugin.d.ts.map +1 -0
  164. package/dist/plugin.js +15 -0
  165. package/dist/plugin.js.map +1 -0
  166. package/dist/router.d.ts +28 -0
  167. package/dist/router.d.ts.map +1 -1
  168. package/dist/router.js +47 -0
  169. package/dist/router.js.map +1 -1
  170. package/dist/routes.d.ts +4 -0
  171. package/dist/routes.d.ts.map +1 -1
  172. package/dist/routes.js +6 -0
  173. package/dist/routes.js.map +1 -1
  174. package/dist/schemas.d.ts +117 -0
  175. package/dist/schemas.d.ts.map +1 -1
  176. package/dist/schemas.js +40 -1
  177. package/dist/schemas.js.map +1 -1
  178. package/dist/tokens.d.ts +2 -0
  179. package/dist/tokens.d.ts.map +1 -0
  180. package/dist/tokens.js +101 -0
  181. package/dist/tokens.js.map +1 -0
  182. package/dist/vocabulary.d.ts +11 -1
  183. package/dist/vocabulary.d.ts.map +1 -1
  184. package/dist/vocabulary.js +24 -0
  185. package/dist/vocabulary.js.map +1 -1
  186. package/dist/web.d.ts.map +1 -1
  187. package/dist/web.js +2 -0
  188. package/dist/web.js.map +1 -1
  189. package/docs/specs/cli.md +11 -4
  190. package/docs/specs/plugins.md +487 -0
  191. package/docs/specs/rest.md +3 -2
  192. package/package.json +20 -2
  193. package/skills/building-a-console/SKILL.md +25 -26
  194. package/skills/building-a-console/references/configuring.md +40 -0
  195. package/skills/building-a-console/references/the-default-console.md +1 -1
  196. package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
  197. package/skills/building-a-plugin/SKILL.md +137 -0
  198. package/skills/deploying-a-console/SKILL.md +24 -26
  199. package/skills/deploying-a-console/references/access.md +13 -9
  200. package/skills/deploying-a-console/references/resources.md +25 -10
  201. package/skills/deploying-a-console/references/running-it-again.md +15 -0
  202. package/skills/deploying-a-console/references/sizing.md +2 -0
  203. package/skills/planning-the-board/SKILL.md +96 -0
  204. package/skills/planning-the-board/references/a-task.md +36 -0
  205. package/skills/planning-the-board/references/setting-up.md +44 -0
  206. package/skills/working-the-board/SKILL.md +43 -34
  207. package/skills/working-the-board/references/the-pack.md +19 -0
  208. package/src/api.ts +8 -0
  209. package/src/cli/cli.ts +697 -33
  210. package/src/cli/format.ts +356 -9
  211. package/src/cli/help.ts +41 -2
  212. package/src/cli/plugin-check.ts +470 -0
  213. package/src/cli/plugin-cli.ts +148 -0
  214. package/src/cli/plugin-init.ts +90 -0
  215. package/src/cli/plugin-main.ts +30 -0
  216. package/src/client.ts +294 -0
  217. package/src/compat.ts +2 -2
  218. package/src/components/board.tsx +62 -14
  219. package/src/components/event-feed.tsx +42 -1
  220. package/src/components/plugin-frame.tsx +437 -0
  221. package/src/components/plugin-settings.tsx +1111 -0
  222. package/src/components/shell.tsx +82 -14
  223. package/src/components/workspace.tsx +18 -0
  224. package/src/console.tsx +286 -19
  225. package/src/data.ts +27 -2
  226. package/src/i18n/en.ts +168 -0
  227. package/src/i18n/ko.ts +148 -0
  228. package/src/index.ts +12 -0
  229. package/src/plugin/args.ts +526 -0
  230. package/src/plugin/bridge.ts +314 -0
  231. package/src/plugin/capabilities.ts +150 -0
  232. package/src/plugin/context.ts +416 -0
  233. package/src/plugin/define.ts +792 -0
  234. package/src/plugin/definition-error.ts +19 -0
  235. package/src/plugin/limits.ts +70 -0
  236. package/src/plugin/load-worker.ts +50 -0
  237. package/src/plugin/load.ts +82 -0
  238. package/src/plugin/manifest.ts +561 -0
  239. package/src/plugin/refusals.ts +94 -0
  240. package/src/plugin/rest-schemas.ts +354 -0
  241. package/src/plugin/sandbox.ts +123 -0
  242. package/src/plugin/testing.ts +612 -0
  243. package/src/plugin/web-react.ts +45 -0
  244. package/src/plugin/web.ts +403 -0
  245. package/src/plugin-nav.ts +132 -0
  246. package/src/plugin-testing.ts +10 -0
  247. package/src/plugin-web-react.ts +4 -0
  248. package/src/plugin-web.ts +35 -0
  249. package/src/plugin.ts +126 -0
  250. package/src/router.ts +73 -0
  251. package/src/routes.ts +7 -0
  252. package/src/schemas.ts +43 -0
  253. package/src/styles/console.css +327 -0
  254. package/src/tokens.ts +101 -0
  255. package/src/vocabulary.ts +30 -0
  256. package/src/web.ts +2 -0
  257. package/template/plugin/.gitignore +2 -0
  258. package/template/plugin/README.md +26 -0
  259. package/template/plugin/SKILLCDN.md +13 -0
  260. package/template/plugin/build.mjs +40 -0
  261. package/template/plugin/console-plugin.json +50 -0
  262. package/template/plugin/package.json +27 -0
  263. package/template/plugin/skills/__key__/SKILL.md +23 -0
  264. package/template/plugin/src/check.test.ts +34 -0
  265. package/template/plugin/src/server/index.test.ts +41 -0
  266. package/template/plugin/src/server/index.ts +72 -0
  267. package/template/plugin/src/web/index.css +51 -0
  268. package/template/plugin/src/web/index.ts +115 -0
  269. package/template/plugin/tsconfig.json +17 -0
  270. package/skills/working-the-board/references/setting-up.md +0 -3
  271. /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";
@@ -772,18 +774,29 @@ const MAX_SKILL_PAGES = 64;
772
774
  /** How many results a search answers at most: what the deployment allows on one page. */
773
775
  const MAX_FIND_LIMIT = 25;
774
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. */
775
- async function packOf(project) {
776
- const listing = await project.skills();
777
+ async function packOf(reader) {
778
+ const listing = await reader.skills();
777
779
  return { listing, summary: summaryOf(listing) };
778
780
  }
779
- const skills = async (args, io) => {
780
- const { values, positionals } = parse(args, {
781
- search: { type: "string" },
782
- limit: { type: "string" },
783
- cursor: { type: "string" },
784
- });
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) {
785
798
  if (positionals.length > 1) {
786
- throw misuse("Say one folder: console skills [<folder>] [--search <words>]");
799
+ throw misuse(`Say one folder: ${extras.command} [<folder>] [--search <words>]`);
787
800
  }
788
801
  // The root of the pack is the mounted folder, which the deployment reads as no path at all.
789
802
  const given = positionals[0];
@@ -792,24 +805,24 @@ const skills = async (args, io) => {
792
805
  const limit = text(values, "limit");
793
806
  const cursor = text(values, "cursor");
794
807
  if (words === undefined && (limit !== undefined || cursor !== undefined)) {
795
- 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>]`);
796
809
  }
797
810
  const atMost = limit === undefined ? undefined : Number(limit);
798
811
  if (atMost !== undefined &&
799
812
  (!Number.isInteger(atMost) || atMost < 1 || atMost > MAX_FIND_LIMIT)) {
800
813
  throw misuse(`Not a number of results, 1 to ${MAX_FIND_LIMIT}: ${limit}`);
801
814
  }
802
- const session = await open(io, values);
803
- const project = await session.project();
804
- const { listing, summary } = await packOf(project);
815
+ const { listing, summary } = await packOf(reader);
805
816
  // The pack as it stands; or why it does not answer, which a folder or a search would meet too.
806
817
  if (summary === undefined || (words === undefined && given === undefined)) {
807
- 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"));
808
821
  return EXIT.ok;
809
822
  }
810
823
  if (words !== undefined) {
811
824
  // One page of what the words find; more is said, and asked for with --cursor.
812
- const found = await project.skillFind(words, { path: folder, limit: atMost, cursor });
825
+ const found = await reader.skillFind(words, { path: folder, limit: atMost, cursor });
813
826
  answer(io, session, found, () => formatSkillFind(found, summary));
814
827
  return EXIT.ok;
815
828
  }
@@ -818,7 +831,7 @@ const skills = async (args, io) => {
818
831
  let next;
819
832
  let last;
820
833
  for (let n = 0; n < MAX_SKILL_PAGES; n += 1) {
821
- last = await project.skillBrowse({ path: folder, cursor: next });
834
+ last = await reader.skillBrowse({ path: folder, cursor: next });
822
835
  if (last.read?.status !== "ready") {
823
836
  break;
824
837
  }
@@ -835,6 +848,16 @@ const skills = async (args, io) => {
835
848
  const whole = joinFolder(pages, summary);
836
849
  answer(io, session, { ...browsed, read: whole === undefined ? null : bodyOfFolder(whole) }, () => formatSkillBrowse(browsed, whole));
837
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
+ });
838
861
  };
839
862
  /** The most characters one read of a file asks for: what the console's API allows on one page. */
840
863
  const MAX_FILE_LIMIT = 100_000;
@@ -852,28 +875,25 @@ function countOf(given, name, bounds) {
852
875
  }
853
876
  return count;
854
877
  }
855
- const skill = async (args, io) => {
856
- const { values, positionals } = parse(args, {
857
- file: { type: "string" },
858
- offset: { type: "string" },
859
- limit: { type: "string" },
860
- });
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) {
861
883
  const file = text(values, "file");
862
884
  // A file is read by its own path from the repository's root; a skill's path beside it would be
863
885
  // ignored, which reads as if it mattered, so the two together are not understood.
864
886
  if (file !== undefined && positionals.length > 0) {
865
- 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.`);
866
888
  }
867
889
  // A section is a page of a file read as asked (SkillCDN 0.4): the flags mean nothing without one.
868
890
  if (file === undefined &&
869
891
  (text(values, "offset") !== undefined || text(values, "limit") !== undefined)) {
870
- 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>]`);
871
893
  }
872
894
  const offset = countOf(text(values, "offset"), "offset", { least: 0 });
873
895
  const limit = countOf(text(values, "limit"), "limit", { least: 1, most: MAX_FILE_LIMIT });
874
- const session = await open(io, values);
875
- const project = await session.project();
876
- const { listing, summary } = await packOf(project);
896
+ const { listing, summary } = await packOf(reader);
877
897
  if (summary === undefined) {
878
898
  answer(io, session, listing, () => formatSkills(listing, io.skillsDir));
879
899
  return EXIT.ok;
@@ -881,7 +901,7 @@ const skill = async (args, io) => {
881
901
  if (file !== undefined && (offset !== undefined || limit !== undefined)) {
882
902
  // One page as asked, the section a search or an outline named; the result says where the
883
903
  // file continues, and asks for nothing.
884
- const page = await project.skillFile(file, { offset, limit });
904
+ const page = await reader.skillFile(file, { offset, limit });
885
905
  const section = page.read === null ? undefined : joinFile([page], summary, limit);
886
906
  answer(io, session, { ...page, read: section === undefined ? null : bodyOfFile(section) }, () => formatSkillFile(page, section));
887
907
  return EXIT.ok;
@@ -892,7 +912,7 @@ const skill = async (args, io) => {
892
912
  let offset;
893
913
  let last;
894
914
  for (let n = 0; n < MAX_SKILL_PAGES; n += 1) {
895
- last = await project.skillFile(file, offset === undefined ? {} : { offset });
915
+ last = await reader.skillFile(file, offset === undefined ? {} : { offset });
896
916
  if (last.read === null) {
897
917
  break;
898
918
  }
@@ -910,7 +930,7 @@ const skill = async (args, io) => {
910
930
  answer(io, session, { ...fileRead, read: whole === undefined ? null : bodyOfFile(whole) }, () => formatSkillFile(fileRead, whole));
911
931
  return EXIT.ok;
912
932
  }
913
- 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);
914
934
  if ("error" in wanted) {
915
935
  throw misuse(wanted.error);
916
936
  }
@@ -919,7 +939,7 @@ const skill = async (args, io) => {
919
939
  let cursor;
920
940
  let last;
921
941
  for (let n = 0; n < MAX_SKILL_PAGES; n += 1) {
922
- last = await project.skill(wanted.path, cursor);
942
+ last = await reader.skill(wanted.path, cursor);
923
943
  if (last.read?.status !== "ready") {
924
944
  break;
925
945
  }
@@ -936,7 +956,492 @@ const skill = async (args, io) => {
936
956
  const whole = joinSkill(pages, summary);
937
957
  answer(io, session, { ...loaded, read: whole === undefined ? null : bodyOfSkill(whole) }, () => formatSkillLoad(loaded, whole));
938
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");
939
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
+ }
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
+ }
940
1445
  /** A version number given as a flag, or nothing; anything else is not understood. */
941
1446
  function versionOf(given) {
942
1447
  if (given === undefined) {
@@ -1018,9 +1523,10 @@ const rules = async (args, io) => {
1018
1523
  ? {
1019
1524
  workspace: forPerson(found.workspace, me.language),
1020
1525
  project: forPerson(found.project, me.language),
1526
+ ...(found.plugins === undefined ? {} : { plugins: found.plugins }),
1021
1527
  }
1022
1528
  : forPerson(found, me.language);
1023
- answer(io, session, read, () => formatRules(read));
1529
+ answer(io, session, read, () => formatRules(read, me.language));
1024
1530
  return EXIT.ok;
1025
1531
  };
1026
1532
  /**
@@ -1294,6 +1800,7 @@ const take = async (args, io) => {
1294
1800
  const rules = {
1295
1801
  workspace: forPerson(ruling.workspace, me.language),
1296
1802
  project: forPerson(ruling.project, me.language),
1803
+ ...(ruling.plugins === undefined ? {} : { plugins: ruling.plugins }),
1297
1804
  };
1298
1805
  answer(io, session, { run, task: taken, comments: comments.items, rules }, () => [
1299
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}`,
@@ -1303,7 +1810,7 @@ const take = async (args, io) => {
1303
1810
  "",
1304
1811
  formatTask(taken, undefined, undefined, comments.items),
1305
1812
  "",
1306
- formatRules(rules),
1813
+ formatRules(rules, me.language),
1307
1814
  "",
1308
1815
  'Say how it goes: console report "<what you found or did>". Hand in what you make: console hand-in <https url | file path>.',
1309
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>".',
@@ -2294,6 +2801,8 @@ const COMMANDS = {
2294
2801
  skills,
2295
2802
  skill,
2296
2803
  rules,
2804
+ plugins,
2805
+ plugin,
2297
2806
  people,
2298
2807
  person,
2299
2808
  workspace,