@skillcdn/console 0.4.1 → 0.4.3

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 (299) hide show
  1. package/README.md +19 -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 +569 -48
  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 +283 -7
  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 +44 -7
  16. package/dist/cli/help.js.map +1 -1
  17. package/dist/cli/plugin-check.d.ts +26 -0
  18. package/dist/cli/plugin-check.d.ts.map +1 -0
  19. package/dist/cli/plugin-check.js +396 -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 +189 -0
  24. package/dist/cli/plugin-cli.js.map +1 -0
  25. package/dist/cli/plugin-init.d.ts +37 -0
  26. package/dist/cli/plugin-init.d.ts.map +1 -0
  27. package/dist/cli/plugin-init.js +206 -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 +100 -3
  34. package/dist/client.d.ts.map +1 -1
  35. package/dist/client.js +79 -7
  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/comments.d.ts.map +1 -1
  44. package/dist/components/comments.js +1 -1
  45. package/dist/components/comments.js.map +1 -1
  46. package/dist/components/decision-form.d.ts +2 -1
  47. package/dist/components/decision-form.d.ts.map +1 -1
  48. package/dist/components/decision-form.js +36 -4
  49. package/dist/components/decision-form.js.map +1 -1
  50. package/dist/components/decision-list.d.ts +4 -0
  51. package/dist/components/decision-list.d.ts.map +1 -1
  52. package/dist/components/decision-list.js +29 -3
  53. package/dist/components/decision-list.js.map +1 -1
  54. package/dist/components/event-feed.d.ts +1 -1
  55. package/dist/components/event-feed.d.ts.map +1 -1
  56. package/dist/components/event-feed.js +33 -1
  57. package/dist/components/event-feed.js.map +1 -1
  58. package/dist/components/my-decisions.d.ts +2 -0
  59. package/dist/components/my-decisions.d.ts.map +1 -1
  60. package/dist/components/my-decisions.js +2 -2
  61. package/dist/components/my-decisions.js.map +1 -1
  62. package/dist/components/plugin-frame.d.ts +52 -0
  63. package/dist/components/plugin-frame.d.ts.map +1 -0
  64. package/dist/components/plugin-frame.js +314 -0
  65. package/dist/components/plugin-frame.js.map +1 -0
  66. package/dist/components/plugin-settings.d.ts +38 -0
  67. package/dist/components/plugin-settings.d.ts.map +1 -0
  68. package/dist/components/plugin-settings.js +348 -0
  69. package/dist/components/plugin-settings.js.map +1 -0
  70. package/dist/components/shell.d.ts +2 -0
  71. package/dist/components/shell.d.ts.map +1 -1
  72. package/dist/components/shell.js +26 -1
  73. package/dist/components/shell.js.map +1 -1
  74. package/dist/components/task-view.d.ts +2 -1
  75. package/dist/components/task-view.d.ts.map +1 -1
  76. package/dist/components/task-view.js.map +1 -1
  77. package/dist/components/workspace.d.ts +7 -1
  78. package/dist/components/workspace.d.ts.map +1 -1
  79. package/dist/components/workspace.js +1 -1
  80. package/dist/components/workspace.js.map +1 -1
  81. package/dist/console.css +330 -1
  82. package/dist/console.d.ts.map +1 -1
  83. package/dist/console.js +152 -41
  84. package/dist/console.js.map +1 -1
  85. package/dist/data.d.ts +9 -2
  86. package/dist/data.d.ts.map +1 -1
  87. package/dist/data.js +45 -9
  88. package/dist/data.js.map +1 -1
  89. package/dist/i18n/en.d.ts +155 -0
  90. package/dist/i18n/en.d.ts.map +1 -1
  91. package/dist/i18n/en.js +166 -0
  92. package/dist/i18n/en.js.map +1 -1
  93. package/dist/i18n/ko.d.ts.map +1 -1
  94. package/dist/i18n/ko.js +150 -0
  95. package/dist/i18n/ko.js.map +1 -1
  96. package/dist/index.d.ts +3 -1
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +3 -1
  99. package/dist/index.js.map +1 -1
  100. package/dist/limits.d.ts +2 -0
  101. package/dist/limits.d.ts.map +1 -1
  102. package/dist/limits.js +2 -0
  103. package/dist/limits.js.map +1 -1
  104. package/dist/plugin/args.d.ts +148 -0
  105. package/dist/plugin/args.d.ts.map +1 -0
  106. package/dist/plugin/args.js +369 -0
  107. package/dist/plugin/args.js.map +1 -0
  108. package/dist/plugin/bridge.d.ts +146 -0
  109. package/dist/plugin/bridge.d.ts.map +1 -0
  110. package/dist/plugin/bridge.js +125 -0
  111. package/dist/plugin/bridge.js.map +1 -0
  112. package/dist/plugin/capabilities.d.ts +20 -0
  113. package/dist/plugin/capabilities.d.ts.map +1 -0
  114. package/dist/plugin/capabilities.js +133 -0
  115. package/dist/plugin/capabilities.js.map +1 -0
  116. package/dist/plugin/context.d.ts +380 -0
  117. package/dist/plugin/context.d.ts.map +1 -0
  118. package/dist/plugin/context.js +2 -0
  119. package/dist/plugin/context.js.map +1 -0
  120. package/dist/plugin/define.d.ts +208 -0
  121. package/dist/plugin/define.d.ts.map +1 -0
  122. package/dist/plugin/define.js +482 -0
  123. package/dist/plugin/define.js.map +1 -0
  124. package/dist/plugin/definition-error.d.ts +11 -0
  125. package/dist/plugin/definition-error.d.ts.map +1 -0
  126. package/dist/plugin/definition-error.js +17 -0
  127. package/dist/plugin/definition-error.js.map +1 -0
  128. package/dist/plugin/limits.d.ts +67 -0
  129. package/dist/plugin/limits.d.ts.map +1 -0
  130. package/dist/plugin/limits.js +70 -0
  131. package/dist/plugin/limits.js.map +1 -0
  132. package/dist/plugin/load-worker.d.ts +7 -0
  133. package/dist/plugin/load-worker.d.ts.map +1 -0
  134. package/dist/plugin/load-worker.js +41 -0
  135. package/dist/plugin/load-worker.js.map +1 -0
  136. package/dist/plugin/load.d.ts +18 -0
  137. package/dist/plugin/load.d.ts.map +1 -0
  138. package/dist/plugin/load.js +62 -0
  139. package/dist/plugin/load.js.map +1 -0
  140. package/dist/plugin/manifest.d.ts +237 -0
  141. package/dist/plugin/manifest.d.ts.map +1 -0
  142. package/dist/plugin/manifest.js +411 -0
  143. package/dist/plugin/manifest.js.map +1 -0
  144. package/dist/plugin/refusals.d.ts +49 -0
  145. package/dist/plugin/refusals.d.ts.map +1 -0
  146. package/dist/plugin/refusals.js +79 -0
  147. package/dist/plugin/refusals.js.map +1 -0
  148. package/dist/plugin/rest-schemas.d.ts +556 -0
  149. package/dist/plugin/rest-schemas.d.ts.map +1 -0
  150. package/dist/plugin/rest-schemas.js +262 -0
  151. package/dist/plugin/rest-schemas.js.map +1 -0
  152. package/dist/plugin/sandbox.d.ts +23 -0
  153. package/dist/plugin/sandbox.d.ts.map +1 -0
  154. package/dist/plugin/sandbox.js +99 -0
  155. package/dist/plugin/sandbox.js.map +1 -0
  156. package/dist/plugin/testing.d.ts +45 -0
  157. package/dist/plugin/testing.d.ts.map +1 -0
  158. package/dist/plugin/testing.js +420 -0
  159. package/dist/plugin/testing.js.map +1 -0
  160. package/dist/plugin/web-react.d.ts +11 -0
  161. package/dist/plugin/web-react.d.ts.map +1 -0
  162. package/dist/plugin/web-react.js +34 -0
  163. package/dist/plugin/web-react.js.map +1 -0
  164. package/dist/plugin/web.d.ts +95 -0
  165. package/dist/plugin/web.d.ts.map +1 -0
  166. package/dist/plugin/web.js +245 -0
  167. package/dist/plugin/web.js.map +1 -0
  168. package/dist/plugin-nav.d.ts +29 -0
  169. package/dist/plugin-nav.d.ts.map +1 -0
  170. package/dist/plugin-nav.js +92 -0
  171. package/dist/plugin-nav.js.map +1 -0
  172. package/dist/plugin-testing.d.ts +2 -0
  173. package/dist/plugin-testing.d.ts.map +1 -0
  174. package/dist/plugin-testing.js +4 -0
  175. package/dist/plugin-testing.js.map +1 -0
  176. package/dist/plugin-web-react.d.ts +3 -0
  177. package/dist/plugin-web-react.d.ts.map +1 -0
  178. package/dist/plugin-web-react.js +3 -0
  179. package/dist/plugin-web-react.js.map +1 -0
  180. package/dist/plugin-web.d.ts +4 -0
  181. package/dist/plugin-web.d.ts.map +1 -0
  182. package/dist/plugin-web.js +5 -0
  183. package/dist/plugin-web.js.map +1 -0
  184. package/dist/plugin.d.ts +11 -0
  185. package/dist/plugin.d.ts.map +1 -0
  186. package/dist/plugin.js +15 -0
  187. package/dist/plugin.js.map +1 -0
  188. package/dist/router.d.ts +28 -0
  189. package/dist/router.d.ts.map +1 -1
  190. package/dist/router.js +47 -0
  191. package/dist/router.js.map +1 -1
  192. package/dist/routes.d.ts +4 -0
  193. package/dist/routes.d.ts.map +1 -1
  194. package/dist/routes.js +6 -0
  195. package/dist/routes.js.map +1 -1
  196. package/dist/schemas.d.ts +194 -9
  197. package/dist/schemas.d.ts.map +1 -1
  198. package/dist/schemas.js +98 -17
  199. package/dist/schemas.js.map +1 -1
  200. package/dist/tokens.d.ts +2 -0
  201. package/dist/tokens.d.ts.map +1 -0
  202. package/dist/tokens.js +101 -0
  203. package/dist/tokens.js.map +1 -0
  204. package/dist/vocabulary.d.ts +11 -1
  205. package/dist/vocabulary.d.ts.map +1 -1
  206. package/dist/vocabulary.js +24 -0
  207. package/dist/vocabulary.js.map +1 -1
  208. package/dist/web.d.ts.map +1 -1
  209. package/dist/web.js +2 -0
  210. package/dist/web.js.map +1 -1
  211. package/docs/specs/cli.md +12 -5
  212. package/docs/specs/plugins.md +488 -0
  213. package/docs/specs/rest.md +11 -9
  214. package/package.json +26 -8
  215. package/skills/building-a-console/SKILL.md +25 -26
  216. package/skills/building-a-console/references/configuring.md +40 -0
  217. package/skills/building-a-console/references/the-default-console.md +1 -1
  218. package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
  219. package/skills/building-a-plugin/SKILL.md +137 -0
  220. package/skills/deploying-a-console/SKILL.md +24 -26
  221. package/skills/deploying-a-console/references/access.md +13 -9
  222. package/skills/deploying-a-console/references/resources.md +25 -10
  223. package/skills/deploying-a-console/references/running-it-again.md +15 -0
  224. package/skills/deploying-a-console/references/sizing.md +2 -0
  225. package/skills/planning-the-board/SKILL.md +96 -0
  226. package/skills/planning-the-board/references/a-task.md +36 -0
  227. package/skills/planning-the-board/references/setting-up.md +44 -0
  228. package/skills/working-the-board/SKILL.md +43 -34
  229. package/skills/working-the-board/references/the-pack.md +19 -0
  230. package/src/api.ts +12 -0
  231. package/src/cli/cli.ts +737 -55
  232. package/src/cli/format.ts +368 -10
  233. package/src/cli/help.ts +50 -6
  234. package/src/cli/plugin-check.ts +497 -0
  235. package/src/cli/plugin-cli.ts +237 -0
  236. package/src/cli/plugin-init.ts +257 -0
  237. package/src/cli/plugin-main.ts +30 -0
  238. package/src/client.ts +322 -9
  239. package/src/compat.ts +2 -2
  240. package/src/components/board.tsx +62 -14
  241. package/src/components/comments.tsx +1 -0
  242. package/src/components/decision-form.tsx +92 -11
  243. package/src/components/decision-list.tsx +71 -2
  244. package/src/components/event-feed.tsx +42 -1
  245. package/src/components/my-decisions.tsx +4 -1
  246. package/src/components/plugin-frame.tsx +437 -0
  247. package/src/components/plugin-settings.tsx +1111 -0
  248. package/src/components/shell.tsx +82 -14
  249. package/src/components/task-view.tsx +2 -1
  250. package/src/components/workspace.tsx +18 -0
  251. package/src/console.tsx +304 -31
  252. package/src/data.ts +59 -11
  253. package/src/i18n/en.ts +175 -0
  254. package/src/i18n/ko.ts +155 -0
  255. package/src/index.ts +12 -0
  256. package/src/limits.ts +2 -0
  257. package/src/plugin/args.ts +526 -0
  258. package/src/plugin/bridge.ts +314 -0
  259. package/src/plugin/capabilities.ts +150 -0
  260. package/src/plugin/context.ts +421 -0
  261. package/src/plugin/define.ts +792 -0
  262. package/src/plugin/definition-error.ts +19 -0
  263. package/src/plugin/limits.ts +70 -0
  264. package/src/plugin/load-worker.ts +50 -0
  265. package/src/plugin/load.ts +82 -0
  266. package/src/plugin/manifest.ts +561 -0
  267. package/src/plugin/refusals.ts +94 -0
  268. package/src/plugin/rest-schemas.ts +354 -0
  269. package/src/plugin/sandbox.ts +123 -0
  270. package/src/plugin/testing.ts +612 -0
  271. package/src/plugin/web-react.ts +45 -0
  272. package/src/plugin/web.ts +403 -0
  273. package/src/plugin-nav.ts +132 -0
  274. package/src/plugin-testing.ts +10 -0
  275. package/src/plugin-web-react.ts +4 -0
  276. package/src/plugin-web.ts +35 -0
  277. package/src/plugin.ts +126 -0
  278. package/src/router.ts +73 -0
  279. package/src/routes.ts +7 -0
  280. package/src/schemas.ts +115 -18
  281. package/src/styles/console.css +330 -1
  282. package/src/tokens.ts +101 -0
  283. package/src/vocabulary.ts +30 -0
  284. package/src/web.ts +2 -0
  285. package/template/plugin/.gitignore +2 -0
  286. package/template/plugin/README.md +26 -0
  287. package/template/plugin/SKILLCDN.md +13 -0
  288. package/template/plugin/build.mjs +40 -0
  289. package/template/plugin/console-plugin.json +50 -0
  290. package/template/plugin/package.json +27 -0
  291. package/template/plugin/skills/__key__/SKILL.md +23 -0
  292. package/template/plugin/src/check.test.ts +34 -0
  293. package/template/plugin/src/server/index.test.ts +41 -0
  294. package/template/plugin/src/server/index.ts +72 -0
  295. package/template/plugin/src/web/index.css +51 -0
  296. package/template/plugin/src/web/index.ts +115 -0
  297. package/template/plugin/tsconfig.json +17 -0
  298. package/skills/working-the-board/references/setting-up.md +0 -3
  299. /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");
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
+ }
939
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
  /**
@@ -1210,6 +1716,22 @@ const comment = async (args, io) => {
1210
1716
  if (body.trim().length === 0 && paths.length === 0) {
1211
1717
  throw misuse(`Say something, or attach a file. ${ask}`);
1212
1718
  }
1719
+ const files = await attachmentsOf(io, paths);
1720
+ const session = await open(io, values);
1721
+ const project = await session.project();
1722
+ const task = await project.task(ref);
1723
+ const written = await project.comment(task.id, { body, files });
1724
+ answer(io, session, written, () => [
1725
+ `Commented on #${task.number} ${task.title}${written.files.length === 0 ? "" : `, with ${written.files.length} file${written.files.length === 1 ? "" : "s"}`}.`,
1726
+ formatComment(written),
1727
+ ].join("\n"));
1728
+ return EXIT.ok;
1729
+ };
1730
+ /**
1731
+ * The files named by --attach, read from this machine: each called by its name, without the
1732
+ * path, and typed by its extension where the extension says.
1733
+ */
1734
+ async function attachmentsOf(io, paths) {
1213
1735
  const files = [];
1214
1736
  for (const path of paths) {
1215
1737
  let bytes;
@@ -1223,20 +1745,12 @@ const comment = async (args, io) => {
1223
1745
  const contentType = CONTENT_TYPES[extname(name).toLowerCase()];
1224
1746
  files.push({ name, bytes, ...(contentType === undefined ? {} : { contentType }) });
1225
1747
  }
1226
- const session = await open(io, values);
1227
- const project = await session.project();
1228
- const task = await project.task(ref);
1229
- const written = await project.comment(task.id, { body, files });
1230
- answer(io, session, written, () => [
1231
- `Commented on #${task.number} ${task.title}${written.files.length === 0 ? "" : `, with ${written.files.length} file${written.files.length === 1 ? "" : "s"}`}.`,
1232
- formatComment(written),
1233
- ].join("\n"));
1234
- return EXIT.ok;
1235
- };
1748
+ return files;
1749
+ }
1236
1750
  /**
1237
1751
  * A file of the project, read from the console and written on this machine: one a run handed
1238
- * in, one put in a comment, or one attached to a page, by its id, so that an agent can look at
1239
- * a picture a person put on the board.
1752
+ * in, one put in a comment or on a decision, or one attached to a page, by its id, so that an
1753
+ * agent can look at a picture a person put on the board.
1240
1754
  */
1241
1755
  const file = async (args, io) => {
1242
1756
  const { values, positionals } = parse(args, { out: { type: "string" } });
@@ -1294,6 +1808,7 @@ const take = async (args, io) => {
1294
1808
  const rules = {
1295
1809
  workspace: forPerson(ruling.workspace, me.language),
1296
1810
  project: forPerson(ruling.project, me.language),
1811
+ ...(ruling.plugins === undefined ? {} : { plugins: ruling.plugins }),
1297
1812
  };
1298
1813
  answer(io, session, { run, task: taken, comments: comments.items, rules }, () => [
1299
1814
  `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 +1818,7 @@ const take = async (args, io) => {
1303
1818
  "",
1304
1819
  formatTask(taken, undefined, undefined, comments.items),
1305
1820
  "",
1306
- formatRules(rules),
1821
+ formatRules(rules, me.language),
1307
1822
  "",
1308
1823
  'Say how it goes: console report "<what you found or did>". Hand in what you make: console hand-in <https url | file path>.',
1309
1824
  '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>".',
@@ -1395,6 +1910,7 @@ const ask = async (args, io) => {
1395
1910
  run: { type: "string" },
1396
1911
  wait: { type: "string" },
1397
1912
  to: { type: "string" },
1913
+ attach: { type: "string", multiple: true },
1398
1914
  });
1399
1915
  const question = theOne(positionals, 'Say the question: console ask "<question>" --option "<label>" --option "<label>"');
1400
1916
  const to = text(values, "to");
@@ -1407,6 +1923,8 @@ const ask = async (args, io) => {
1407
1923
  }
1408
1924
  const seconds = secondsOf(text(values, "wait"), DEFAULT_ASK_WAIT_SECONDS);
1409
1925
  const body = await bodyOf(io, values, text(values, "body"));
1926
+ // The files put on the decision (ADR-0055): a picture of what is to be chosen between, for one.
1927
+ const files = await attachmentsOf(io, texts(values, "attach"));
1410
1928
  const session = await open(io, values);
1411
1929
  const project = await session.project();
1412
1930
  const runId = await currentRun(project, values);
@@ -1418,11 +1936,12 @@ const ask = async (args, io) => {
1418
1936
  runId,
1419
1937
  ...(body === undefined ? {} : { body }),
1420
1938
  ...(typeof deciderId === "string" ? { deciderId } : {}),
1421
- });
1939
+ }, files);
1940
+ const withFiles = files.length === 0 ? "" : `, with ${files.length} file${files.length === 1 ? "" : "s"}`;
1422
1941
  // The id is printed before any waiting, so that a wait cut short loses nothing.
1423
1942
  io.stdout(session.json
1424
1943
  ? `${JSON.stringify(raised, null, 2)}\n`
1425
- : `Decision ${raised.id} raised on run ${runId}; the run waits for ${raised.decider.login}.\ndecision id: ${raised.id}\n${formatOptions(raised)}\n`);
1944
+ : `Decision ${raised.id} raised on run ${runId}${withFiles}; the run waits for ${raised.decider.login}.\ndecision id: ${raised.id}\n${formatOptions(raised)}\n`);
1426
1945
  return settle(io, session, project, raised, seconds, true);
1427
1946
  };
1428
1947
  /**
@@ -2294,6 +2813,8 @@ const COMMANDS = {
2294
2813
  skills,
2295
2814
  skill,
2296
2815
  rules,
2816
+ plugins,
2817
+ plugin,
2297
2818
  people,
2298
2819
  person,
2299
2820
  workspace,