@skillcdn/console 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/README.md +6 -6
  2. package/dist/api.d.ts +4 -4
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +4 -4
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts.map +1 -1
  7. package/dist/cli/cli.js +255 -33
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts +20 -5
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +98 -26
  12. package/dist/cli/format.js.map +1 -1
  13. package/dist/cli/help.d.ts.map +1 -1
  14. package/dist/cli/help.js +39 -15
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/cli/plugin-check.d.ts +5 -1
  17. package/dist/cli/plugin-check.d.ts.map +1 -1
  18. package/dist/cli/plugin-check.js +23 -4
  19. package/dist/cli/plugin-check.js.map +1 -1
  20. package/dist/cli/plugin-cli.d.ts.map +1 -1
  21. package/dist/cli/plugin-cli.js +10 -1
  22. package/dist/cli/plugin-cli.js.map +1 -1
  23. package/dist/cli/plugin-init.d.ts +2 -0
  24. package/dist/cli/plugin-init.d.ts.map +1 -1
  25. package/dist/cli/plugin-init.js +6 -0
  26. package/dist/cli/plugin-init.js.map +1 -1
  27. package/dist/client.d.ts +32 -1
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +14 -2
  30. package/dist/client.js.map +1 -1
  31. package/dist/compat.js +1 -1
  32. package/dist/components/board.d.ts +8 -0
  33. package/dist/components/board.d.ts.map +1 -1
  34. package/dist/components/board.js +49 -18
  35. package/dist/components/board.js.map +1 -1
  36. package/dist/components/decision-form.d.ts +6 -3
  37. package/dist/components/decision-form.d.ts.map +1 -1
  38. package/dist/components/decision-form.js +24 -18
  39. package/dist/components/decision-form.js.map +1 -1
  40. package/dist/components/decision-list.d.ts +5 -1
  41. package/dist/components/decision-list.d.ts.map +1 -1
  42. package/dist/components/decision-list.js +25 -5
  43. package/dist/components/decision-list.js.map +1 -1
  44. package/dist/components/event-feed.d.ts.map +1 -1
  45. package/dist/components/event-feed.js +34 -8
  46. package/dist/components/event-feed.js.map +1 -1
  47. package/dist/components/goals.d.ts.map +1 -1
  48. package/dist/components/goals.js +2 -2
  49. package/dist/components/goals.js.map +1 -1
  50. package/dist/components/home.d.ts +35 -0
  51. package/dist/components/home.d.ts.map +1 -0
  52. package/dist/components/home.js +326 -0
  53. package/dist/components/home.js.map +1 -0
  54. package/dist/components/my-decisions.d.ts +20 -3
  55. package/dist/components/my-decisions.d.ts.map +1 -1
  56. package/dist/components/my-decisions.js +14 -1
  57. package/dist/components/my-decisions.js.map +1 -1
  58. package/dist/components/pack-connect.d.ts.map +1 -1
  59. package/dist/components/pack-connect.js +5 -4
  60. package/dist/components/pack-connect.js.map +1 -1
  61. package/dist/components/people.d.ts +45 -1
  62. package/dist/components/people.d.ts.map +1 -1
  63. package/dist/components/people.js +125 -4
  64. package/dist/components/people.js.map +1 -1
  65. package/dist/components/picker.d.ts +8 -0
  66. package/dist/components/picker.d.ts.map +1 -1
  67. package/dist/components/picker.js +2 -2
  68. package/dist/components/picker.js.map +1 -1
  69. package/dist/components/plugin-frame.d.ts +3 -13
  70. package/dist/components/plugin-frame.d.ts.map +1 -1
  71. package/dist/components/plugin-frame.js +75 -21
  72. package/dist/components/plugin-frame.js.map +1 -1
  73. package/dist/components/plugin-settings.d.ts +5 -1
  74. package/dist/components/plugin-settings.d.ts.map +1 -1
  75. package/dist/components/plugin-settings.js +283 -45
  76. package/dist/components/plugin-settings.js.map +1 -1
  77. package/dist/components/projects.d.ts +0 -8
  78. package/dist/components/projects.d.ts.map +1 -1
  79. package/dist/components/projects.js +4 -14
  80. package/dist/components/projects.js.map +1 -1
  81. package/dist/components/searches.d.ts +11 -0
  82. package/dist/components/searches.d.ts.map +1 -0
  83. package/dist/components/searches.js +30 -0
  84. package/dist/components/searches.js.map +1 -0
  85. package/dist/components/task-form.d.ts.map +1 -1
  86. package/dist/components/task-form.js +6 -5
  87. package/dist/components/task-form.js.map +1 -1
  88. package/dist/components/task-view.d.ts.map +1 -1
  89. package/dist/components/task-view.js +2 -1
  90. package/dist/components/task-view.js.map +1 -1
  91. package/dist/components/ui.d.ts +16 -0
  92. package/dist/components/ui.d.ts.map +1 -1
  93. package/dist/components/ui.js +15 -2
  94. package/dist/components/ui.js.map +1 -1
  95. package/dist/connect-page.d.ts.map +1 -1
  96. package/dist/connect-page.js +5 -6
  97. package/dist/connect-page.js.map +1 -1
  98. package/dist/console.css +1123 -258
  99. package/dist/console.d.ts +12 -4
  100. package/dist/console.d.ts.map +1 -1
  101. package/dist/console.js +123 -27
  102. package/dist/console.js.map +1 -1
  103. package/dist/data.d.ts +18 -3
  104. package/dist/data.d.ts.map +1 -1
  105. package/dist/data.js +41 -1
  106. package/dist/data.js.map +1 -1
  107. package/dist/docs-page.d.ts.map +1 -1
  108. package/dist/docs-page.js +6 -5
  109. package/dist/docs-page.js.map +1 -1
  110. package/dist/home-tiles.d.ts +92 -0
  111. package/dist/home-tiles.d.ts.map +1 -0
  112. package/dist/home-tiles.js +294 -0
  113. package/dist/home-tiles.js.map +1 -0
  114. package/dist/i18n/en.d.ts +264 -1
  115. package/dist/i18n/en.d.ts.map +1 -1
  116. package/dist/i18n/en.js +260 -1
  117. package/dist/i18n/en.js.map +1 -1
  118. package/dist/i18n/index.d.ts +4 -2
  119. package/dist/i18n/index.d.ts.map +1 -1
  120. package/dist/i18n/index.js +10 -11
  121. package/dist/i18n/index.js.map +1 -1
  122. package/dist/i18n/ko.d.ts.map +1 -1
  123. package/dist/i18n/ko.js +255 -4
  124. package/dist/i18n/ko.js.map +1 -1
  125. package/dist/index.d.ts +7 -5
  126. package/dist/index.d.ts.map +1 -1
  127. package/dist/index.js +7 -5
  128. package/dist/index.js.map +1 -1
  129. package/dist/limits.d.ts +12 -0
  130. package/dist/limits.d.ts.map +1 -1
  131. package/dist/limits.js +12 -0
  132. package/dist/limits.js.map +1 -1
  133. package/dist/plugin/args.d.ts +6 -1
  134. package/dist/plugin/args.d.ts.map +1 -1
  135. package/dist/plugin/args.js +16 -11
  136. package/dist/plugin/args.js.map +1 -1
  137. package/dist/plugin/bridge.d.ts +63 -3
  138. package/dist/plugin/bridge.d.ts.map +1 -1
  139. package/dist/plugin/bridge.js +54 -0
  140. package/dist/plugin/bridge.js.map +1 -1
  141. package/dist/plugin/capabilities.d.ts +7 -1
  142. package/dist/plugin/capabilities.d.ts.map +1 -1
  143. package/dist/plugin/capabilities.js +24 -1
  144. package/dist/plugin/capabilities.js.map +1 -1
  145. package/dist/plugin/context.d.ts +174 -12
  146. package/dist/plugin/context.d.ts.map +1 -1
  147. package/dist/plugin/define.d.ts +14 -1
  148. package/dist/plugin/define.d.ts.map +1 -1
  149. package/dist/plugin/define.js +18 -2
  150. package/dist/plugin/define.js.map +1 -1
  151. package/dist/plugin/limits.d.ts +9 -2
  152. package/dist/plugin/limits.d.ts.map +1 -1
  153. package/dist/plugin/limits.js +9 -2
  154. package/dist/plugin/limits.js.map +1 -1
  155. package/dist/plugin/manifest.d.ts +98 -6
  156. package/dist/plugin/manifest.d.ts.map +1 -1
  157. package/dist/plugin/manifest.js +171 -10
  158. package/dist/plugin/manifest.js.map +1 -1
  159. package/dist/plugin/rest-schemas.d.ts +120 -0
  160. package/dist/plugin/rest-schemas.d.ts.map +1 -1
  161. package/dist/plugin/rest-schemas.js +78 -5
  162. package/dist/plugin/rest-schemas.js.map +1 -1
  163. package/dist/plugin/sandbox.d.ts +4 -3
  164. package/dist/plugin/sandbox.d.ts.map +1 -1
  165. package/dist/plugin/sandbox.js +33 -6
  166. package/dist/plugin/sandbox.js.map +1 -1
  167. package/dist/plugin/semver.d.ts +26 -0
  168. package/dist/plugin/semver.d.ts.map +1 -0
  169. package/dist/plugin/semver.js +76 -0
  170. package/dist/plugin/semver.js.map +1 -0
  171. package/dist/plugin/testing.d.ts +15 -1
  172. package/dist/plugin/testing.d.ts.map +1 -1
  173. package/dist/plugin/testing.js +42 -5
  174. package/dist/plugin/testing.js.map +1 -1
  175. package/dist/plugin/web.d.ts +16 -1
  176. package/dist/plugin/web.d.ts.map +1 -1
  177. package/dist/plugin/web.js +44 -1
  178. package/dist/plugin/web.js.map +1 -1
  179. package/dist/plugin.d.ts +3 -2
  180. package/dist/plugin.d.ts.map +1 -1
  181. package/dist/plugin.js +3 -2
  182. package/dist/plugin.js.map +1 -1
  183. package/dist/router.d.ts +19 -0
  184. package/dist/router.d.ts.map +1 -1
  185. package/dist/router.js +18 -0
  186. package/dist/router.js.map +1 -1
  187. package/dist/routes.d.ts +11 -2
  188. package/dist/routes.d.ts.map +1 -1
  189. package/dist/routes.js +14 -1
  190. package/dist/routes.js.map +1 -1
  191. package/dist/rules.d.ts +2 -2
  192. package/dist/rules.js +3 -3
  193. package/dist/schemas.d.ts +864 -2
  194. package/dist/schemas.d.ts.map +1 -1
  195. package/dist/schemas.js +156 -9
  196. package/dist/schemas.js.map +1 -1
  197. package/dist/use-action.d.ts +8 -2
  198. package/dist/use-action.d.ts.map +1 -1
  199. package/dist/use-action.js +13 -6
  200. package/dist/use-action.js.map +1 -1
  201. package/dist/vocabulary.d.ts +3 -1
  202. package/dist/vocabulary.d.ts.map +1 -1
  203. package/dist/vocabulary.js +9 -0
  204. package/dist/vocabulary.js.map +1 -1
  205. package/docs/specs/cli.md +10 -6
  206. package/docs/specs/plugins.md +83 -62
  207. package/docs/specs/rest.md +34 -16
  208. package/package.json +1 -1
  209. package/skills/building-a-plugin/SKILL.md +15 -12
  210. package/skills/planning-the-board/references/setting-up.md +2 -1
  211. package/skills/working-the-board/SKILL.md +3 -3
  212. package/src/api.ts +23 -0
  213. package/src/cli/cli.ts +323 -30
  214. package/src/cli/format.ts +122 -26
  215. package/src/cli/help.ts +43 -14
  216. package/src/cli/plugin-check.ts +41 -2
  217. package/src/cli/plugin-cli.ts +10 -1
  218. package/src/cli/plugin-init.ts +8 -0
  219. package/src/client.ts +78 -0
  220. package/src/compat.ts +1 -1
  221. package/src/components/board.tsx +86 -45
  222. package/src/components/decision-form.tsx +73 -36
  223. package/src/components/decision-list.tsx +45 -13
  224. package/src/components/event-feed.tsx +40 -7
  225. package/src/components/goals.tsx +4 -1
  226. package/src/components/home.tsx +615 -0
  227. package/src/components/my-decisions.tsx +50 -5
  228. package/src/components/pack-connect.tsx +5 -4
  229. package/src/components/people.tsx +500 -61
  230. package/src/components/picker.tsx +11 -2
  231. package/src/components/plugin-frame.tsx +85 -63
  232. package/src/components/plugin-settings.tsx +735 -185
  233. package/src/components/projects.tsx +14 -69
  234. package/src/components/searches.ts +47 -0
  235. package/src/components/task-form.tsx +23 -28
  236. package/src/components/task-view.tsx +7 -0
  237. package/src/components/ui.tsx +42 -2
  238. package/src/connect-page.tsx +5 -6
  239. package/src/console.tsx +303 -64
  240. package/src/data.ts +62 -3
  241. package/src/docs-page.tsx +6 -5
  242. package/src/home-tiles.ts +438 -0
  243. package/src/i18n/en.ts +274 -0
  244. package/src/i18n/index.ts +17 -14
  245. package/src/i18n/ko.ts +267 -4
  246. package/src/index.ts +18 -5
  247. package/src/limits.ts +12 -0
  248. package/src/plugin/args.ts +16 -11
  249. package/src/plugin/bridge.ts +122 -1
  250. package/src/plugin/capabilities.ts +28 -1
  251. package/src/plugin/context.ts +186 -9
  252. package/src/plugin/define.ts +38 -1
  253. package/src/plugin/limits.ts +9 -2
  254. package/src/plugin/manifest.ts +228 -13
  255. package/src/plugin/rest-schemas.ts +89 -6
  256. package/src/plugin/sandbox.ts +36 -8
  257. package/src/plugin/semver.ts +89 -0
  258. package/src/plugin/testing.ts +63 -6
  259. package/src/plugin/web.ts +66 -0
  260. package/src/plugin.ts +19 -0
  261. package/src/router.ts +25 -0
  262. package/src/routes.ts +17 -2
  263. package/src/rules.ts +3 -3
  264. package/src/schemas.ts +197 -10
  265. package/src/styles/console.css +1123 -258
  266. package/src/use-action.ts +16 -7
  267. package/src/vocabulary.ts +10 -0
  268. package/template/plugin/console-plugin.json +3 -1
package/src/cli/format.ts CHANGED
@@ -30,6 +30,7 @@ import type {
30
30
  RestRulesPlugin,
31
31
  RestRun,
32
32
  RestTask,
33
+ RestTeam,
33
34
  RestVersion,
34
35
  RestVersionSummary,
35
36
  RestWork,
@@ -119,12 +120,15 @@ export function formatProject(project: RestProject): string {
119
120
  return lines.join("\n");
120
121
  }
121
122
 
122
- /** One person: their login, their name where the host gives one, what they are in the workspace, and that they are out when they are. */
123
- export function formatPersonLine(person: RestPerson): string {
123
+ /** One person: their login, their name where the host gives one, what they are in the workspace, that they are out when they are, and whom they report to where known. */
124
+ export function formatPersonLine(person: RestPerson, reportsTo?: string | undefined): string {
124
125
  const standing = person.status === "active" ? person.role : `${person.role}, deactivated`;
126
+ const line = reportsTo === undefined ? "" : `, reports to ${reportsTo}`;
127
+ // The work email where the console answers one: to an administrator, and for oneself.
128
+ const email = typeof person.email === "string" ? ` <${person.email}>` : "";
125
129
  return person.name === null
126
- ? `${person.login} (${standing})`
127
- : `${person.login} ${person.name} (${standing})`;
130
+ ? `${person.login}${email} (${standing}${line})`
131
+ : `${person.login} ${person.name}${email} (${standing}${line})`;
128
132
  }
129
133
 
130
134
  /** The provider a listing names, as people call it. */
@@ -219,6 +223,7 @@ export function formatTask(
219
223
  `#${task.number} ${task.title}`,
220
224
  `state: ${task.state} priority: ${task.priority} owner: ${task.owner.login} workers: ${task.workers.length === 0 ? "nobody" : logins(task.workers)}`,
221
225
  `id: ${task.id}${task.parentId === null ? "" : ` part of: ${task.parentId}`}`,
226
+ ...(task.agent == null ? [] : [`written through: ${task.agent}`]),
222
227
  ...scheduleLines(task),
223
228
  ];
224
229
  if (task.links.length > 0) {
@@ -379,7 +384,7 @@ export function formatGoalLine(goal: RestGoal): string {
379
384
  export function formatGoal(goal: RestGoal, tasks?: readonly RestTask[]): string {
380
385
  const lines = [
381
386
  `goal ${goal.id}: ${goal.title}`,
382
- `state: ${goal.state} category: ${goal.category?.key ?? "none"} starts: ${goal.startsOn ?? "none"} due: ${goal.dueOn ?? "none"} set by: ${goal.createdBy.login}`,
387
+ `state: ${goal.state} category: ${goal.category?.key ?? "none"} starts: ${goal.startsOn ?? "none"} due: ${goal.dueOn ?? "none"} set by: ${goal.createdBy.login}${through(goal.agent)}`,
383
388
  `progress: ${progressOf(goal)}`,
384
389
  ];
385
390
  if (goal.body.trim().length > 0) {
@@ -431,9 +436,27 @@ export function formatPersonWork(person: RestPerson, work: RestPersonWork): stri
431
436
  ].join("\n");
432
437
  }
433
438
 
434
- /** Whom a decision is for, where that is not the one who asked (ADR-0042). */
439
+ /** The agent a thing came through, beside the person (ADR-0056); nothing when they did it themselves. */
440
+ const through = (agent: string | null | undefined): string =>
441
+ agent === null || agent === undefined ? "" : ` through ${agent}`;
442
+
443
+ /** Everyone a decision is addressed to (ADR-0057): `deciders` where the console answers it, else the one decider. */
444
+ const decidersOf = (decision: RestDecision): readonly RestPerson[] =>
445
+ decision.deciders !== undefined && decision.deciders.length > 0
446
+ ? decision.deciders
447
+ : [decision.decider];
448
+
449
+ /** The logins of everyone a decision is addressed to, with commas between. */
450
+ export const deciderLogins = (decision: RestDecision): string =>
451
+ decidersOf(decision)
452
+ .map((person) => person.login)
453
+ .join(", ");
454
+
455
+ /** Whom a decision is for, where that is not the one who asked alone (ADR-0042, ADR-0057). */
435
456
  const addressed = (decision: RestDecision): string =>
436
- decision.decider.id === decision.raisedBy.id ? "" : `, for ${decision.decider.login}`;
457
+ decidersOf(decision).every((person) => person.id === decision.raisedBy.id)
458
+ ? ""
459
+ : `, for ${deciderLogins(decision)}`;
437
460
 
438
461
  export function formatDecisionLine(decision: RestDecision): string {
439
462
  const state = decision.answer === null ? "waiting" : `answered ${decision.answer.option}`;
@@ -454,11 +477,11 @@ export function formatOptions(decision: RestDecision): string {
454
477
  export function formatAnswer(decision: RestDecision): string {
455
478
  const { answer } = decision;
456
479
  if (answer === null) {
457
- return `waits for ${decision.decider.login}.`;
480
+ return `waits for ${deciderLogins(decision)}.`;
458
481
  }
459
482
  const chosen = decision.options.find((option) => option.id === answer.option);
460
483
  const lines = [
461
- `Answered: ${answer.option}) ${chosen?.label ?? answer.option}${chosen?.own === true ? ", in their own words" : ""}, by ${answer.by.login} at ${answer.at}`,
484
+ `Answered: ${answer.option}) ${chosen?.label ?? answer.option}${chosen?.own === true ? ", in their own words" : ""}, by ${answer.by.login}${through(answer.agent)} at ${answer.at}`,
462
485
  ];
463
486
  if (answer.note !== null && answer.note.length > 0) {
464
487
  lines.push(`Note: ${answer.note}`);
@@ -488,6 +511,13 @@ export function formatDecision(decision: RestDecision): string {
488
511
  lines.push("", "options:", formatOptions(decision), "", formatAnswer(decision));
489
512
  if (decision.outcome !== null && decision.outcome.trim().length > 0) {
490
513
  lines.push("", "what followed:", indent(decision.outcome));
514
+ if (decision.outcomeBy != null && decision.outcomeAt != null) {
515
+ lines.push(
516
+ indent(
517
+ `written by ${decision.outcomeBy.login}${through(decision.outcomeAgent)} at ${decision.outcomeAt}`,
518
+ ),
519
+ );
520
+ }
491
521
  }
492
522
  return lines.join("\n");
493
523
  }
@@ -590,6 +620,9 @@ export function localized(
590
620
  export function formatPluginLine(plugin: RestPlugin, language: string | null | undefined): string {
591
621
  const notes = [
592
622
  plugin.roles.length === 0 ? undefined : `roles: ${plugin.roles.join(", ")}`,
623
+ plugin.console === null || plugin.console === undefined
624
+ ? undefined
625
+ : `console: ${plugin.console}`,
593
626
  plugin.problems.length === 0 ? undefined : `problems: ${plugin.problems.join("; ")}`,
594
627
  ].filter((note): note is string => note !== undefined);
595
628
  return `${plugin.key} ${localized(plugin.name, language)} ${plugin.version} ${plugin.state}${notes.length === 0 ? "" : ` ${notes.join(" ")}`}`;
@@ -607,12 +640,22 @@ export function formatPlugins(plugins: RestPlugins, language: string | null | un
607
640
  ].join("\n");
608
641
  }
609
642
 
610
- /** How a command or a query of a plugin is given, as help prints it: the positionals, then the flags. */
611
- export function formatOperationUsage(key: string, operation: RestPluginOperation): string {
643
+ /**
644
+ * How a command or a query of a plugin is given, as help prints it: the positionals, then the
645
+ * flags. In brief, for a listing that an agent reads whole at every first use, the positionals
646
+ * and the required flags alone, the rest as `[options]`, since `help <command>` has them all.
647
+ */
648
+ export function formatOperationUsage(
649
+ key: string,
650
+ operation: RestPluginOperation,
651
+ { brief = false }: { readonly brief?: boolean } = {},
652
+ ): string {
612
653
  const entries = Object.entries(operation.args);
654
+ const shown = brief ? entries.filter(([, arg]) => arg.positional || !arg.optional) : entries;
613
655
  const usage = [
614
- ...entries.filter(([, arg]) => arg.positional).map(([name, arg]) => argUsage(name, arg)),
615
- ...entries.filter(([, arg]) => !arg.positional).map(([name, arg]) => argUsage(name, arg)),
656
+ ...shown.filter(([, arg]) => arg.positional).map(([name, arg]) => argUsage(name, arg)),
657
+ ...shown.filter(([, arg]) => !arg.positional).map(([name, arg]) => argUsage(name, arg)),
658
+ ...(brief && shown.length < entries.length ? ["[options]"] : []),
616
659
  ];
617
660
  return `console plugin ${key} ${operation.name}${usage.map((part) => ` ${part}`).join("")}`;
618
661
  }
@@ -673,11 +716,11 @@ export function formatPluginHelp(detail: RestPluginDetail, name: string | undefi
673
716
  }
674
717
  return [
675
718
  ...detail.operations.flatMap((operation) => [
676
- formatOperationUsage(detail.key, operation),
719
+ formatOperationUsage(detail.key, operation, { brief: true }),
677
720
  ` ${operation.kind === "query" ? "[query] " : ""}${operation.about}`,
678
721
  ]),
679
722
  "",
680
- `One in full: console plugin ${detail.key} help <command>.`,
723
+ `One in full, with every option: console plugin ${detail.key} help <command>.`,
681
724
  ].join("\n");
682
725
  }
683
726
  const operation = detail.operations.find((candidate) => candidate.name === name);
@@ -720,7 +763,13 @@ export function formatPluginSettingLine(
720
763
  const holds =
721
764
  setting.type === "secret"
722
765
  ? setting.set
723
- ? `set${setting.setAt === null ? "" : ` ${moment(setting.setAt)}`}`
766
+ ? `set${setting.setAt === null ? "" : ` ${moment(setting.setAt)}`}${
767
+ setting.source === null ||
768
+ setting.source === undefined ||
769
+ setting.source === "administrator"
770
+ ? ""
771
+ : ` from ${setting.source}`
772
+ }${setting.expiresAt === null || setting.expiresAt === undefined ? "" : `, expires ${moment(setting.expiresAt)}`}`
724
773
  : "not set"
725
774
  : setting.set
726
775
  ? setting.value === null
@@ -735,14 +784,25 @@ export function formatPluginSettingLine(
735
784
  /** A grant in a line: its id, the role, whom it is to, since when. */
736
785
  export function formatPluginGrantLine(grant: RestPluginGrant): string {
737
786
  const whom =
738
- grant.person !== null
739
- ? `person ${grant.person.login}`
740
- : grant.project !== null
741
- ? `the members of ${grant.project.key}`
742
- : "everyone";
787
+ grant.plugin !== null && grant.plugin !== undefined
788
+ ? `the plugin ${grant.plugin}`
789
+ : grant.team !== null && grant.team !== undefined
790
+ ? `the team ${grant.team.key}`
791
+ : grant.person !== null
792
+ ? `person ${grant.person.login}`
793
+ : grant.project !== null
794
+ ? `the members of ${grant.project.key}`
795
+ : "everyone";
743
796
  return `${grant.id} ${grant.role} to ${whom} since ${moment(grant.createdAt)}`;
744
797
  }
745
798
 
799
+ /** A team in a line (ADR-0073): its key (ADR-0074), its name, how many are in it, and their logins. */
800
+ export function formatTeamLine(team: RestTeam, loginOf: ReadonlyMap<string, string>): string {
801
+ const logins = team.memberIds.map((id) => loginOf.get(id) ?? id);
802
+ const count = team.memberIds.length === 1 ? "1 member" : `${team.memberIds.length} members`;
803
+ return `${team.key} ${team.name} ${count}${logins.length === 0 ? "" : `: ${logins.join(", ")}`}`;
804
+ }
805
+
746
806
  /** One plugin in full: what it is, how it stands, what one may run, its skills and its settings. */
747
807
  export function formatPlugin(
748
808
  detail: RestPluginDetail,
@@ -761,6 +821,9 @@ export function formatPlugin(
761
821
  if (detail.problems.length > 0) {
762
822
  lines.push(`problems: ${detail.problems.join("; ")}`);
763
823
  }
824
+ if (detail.console !== null && detail.console !== undefined) {
825
+ lines.push(`Runs on consoles ${detail.console}.`);
826
+ }
764
827
  lines.push(
765
828
  detail.roles.length === 0
766
829
  ? "You hold no role in it."
@@ -778,11 +841,11 @@ export function formatPlugin(
778
841
  lines.push("Commands and queries:");
779
842
  for (const operation of detail.operations) {
780
843
  lines.push(
781
- ` ${formatOperationUsage(detail.key, operation)}`,
844
+ ` ${formatOperationUsage(detail.key, operation, { brief: true })}`,
782
845
  ` ${operation.kind === "query" ? "[query] " : ""}${operation.about}`,
783
846
  );
784
847
  }
785
- lines.push(`One in full: console plugin ${detail.key} help <command>.`);
848
+ lines.push(`One in full, with every option: console plugin ${detail.key} help <command>.`);
786
849
  }
787
850
  if (detail.skills !== null) {
788
851
  lines.push(
@@ -806,18 +869,51 @@ export function formatPlugin(
806
869
  ...detail.grants.map((grant) => ` ${formatPluginGrantLine(grant)}`),
807
870
  );
808
871
  }
872
+ lines.push(...formatPluginAsks(detail, language));
809
873
  return lines.join("\n");
810
874
  }
811
875
 
876
+ /**
877
+ * The roles the plugin asks of other plugins (ADR-0072), each with how it stands and, where not
878
+ * granted, the command that grants it; nothing where it asks none.
879
+ */
880
+ export function formatPluginAsks(
881
+ detail: RestPluginDetail,
882
+ language: string | null | undefined,
883
+ ): string[] {
884
+ const asks = detail.asks ?? [];
885
+ if (asks.length === 0) {
886
+ return [];
887
+ }
888
+ return [
889
+ "",
890
+ "Asks of other plugins:",
891
+ ...asks.map((ask) => {
892
+ const standing =
893
+ ask.state === "granted"
894
+ ? "granted"
895
+ : ask.state === "unavailable"
896
+ ? `${ask.plugin} is not enabled`
897
+ : `not granted; console plugin ${ask.plugin} grant ${ask.role} --plugin ${detail.key}`;
898
+ return ` ${ask.role} of ${ask.plugin}: ${standing} ${localized(ask.why, language)}`;
899
+ }),
900
+ ];
901
+ }
902
+
812
903
  /** What a command or a query answered, as the command prints it. */
813
904
  export function formatInvocation(result: RestPluginInvocation): string {
814
905
  if (result.kind === "query") {
815
906
  return result.data === undefined ? "null" : JSON.stringify(result.data, null, 2);
816
907
  }
817
908
  const text = result.text.length === 0 ? "Done." : result.text;
818
- return result.waiting === null
819
- ? text
820
- : `${text}\ndecision id: ${result.waiting}\nThe decision waits for a person: console decision ${result.waiting} --wait 100`;
909
+ if (result.waiting === null) {
910
+ return text;
911
+ }
912
+ const where =
913
+ result.project === null || result.project === undefined
914
+ ? ""
915
+ : ` in the project ${result.project}`;
916
+ return `${text}\ndecision id: ${result.waiting}\nThe decision waits for a person${where}: console decision ${result.waiting} --wait 100`;
821
917
  }
822
918
 
823
919
  /** A record's data on one line, cut where it runs long. */
package/src/cli/help.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  MAX_CATEGORY_KEY_LENGTH,
8
8
  MAX_CATEGORY_NAME_LENGTH,
9
9
  MAX_COMMENTS_PER_TASK,
10
+ MAX_DECIDERS,
10
11
  MAX_DECISIONS_PER_PROJECT,
11
12
  MAX_DOCUMENT_LENGTH,
12
13
  MAX_DOCUMENT_PATH_LENGTH,
@@ -228,6 +229,30 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
228
229
  about:
229
230
  "The people of the workspace you see, by login, with their name where the host gives one and what they are in it: admin, who configures the workspace, or member, who works in the projects they are in; a person taken out is marked deactivated. Everyone, unless the workspace shows a member only the people they share a project with (console workspace says which; an administrator sees everyone). Then, where you see everyone, the logins an administrator let in that nobody has signed in with yet. The logins are what --worker, member and person take; a login is found exactly, so an owner lists someone the list does not show.",
230
231
  },
232
+ teams: {
233
+ usage: "teams",
234
+ synopsis: "The teams of the workspace, each with who is in it.",
235
+ about:
236
+ "The teams of the workspace (a name and its members: a unit to call people by, and to grant a plugin's role to, ADR-0073), by name, each with its key, what it is called by (ADR-0074), how many are in it and their logins. console team makes and keeps them, for an administrator.",
237
+ },
238
+ team: {
239
+ usage:
240
+ 'team new <key> "<name>" | team rename <key> "<new name>" | team remove <key> | team add <key> <login> | team drop <key> <login>',
241
+ synopsis:
242
+ "Makes, renames or removes a team, or puts a person in one or takes them out; for an administrator.",
243
+ about:
244
+ "For an administrator: a team is a key, a name and its members, any number per person. The key is what a grant, a plugin and this command call the team by (ADR-0074): lowercase letters, digits and hyphens, one among the teams' and the projects' keys, and it never changes; the name, one per workspace, is what people read. new makes one; rename gives it another name, the key staying; remove takes it away, with its memberships and the plugin roles granted to it; add and drop put a person in and take them out, by login. The board records each as the workspace's own event, and a plugin with people:read:all hears of them.",
245
+ refusals: [
246
+ "auth.forbidden: only an administrator keeps the teams",
247
+ "request.invalid: the key is not lowercase letters, digits and hyphens, or the name is empty or too long",
248
+ "team.not_found: no team has that key",
249
+ "team.key_taken: a team or a project has that key already",
250
+ "team.name_taken: a team of that name is there already",
251
+ "team.too_many: the workspace holds as many teams as one may",
252
+ "team.invalid_member: the person is not an active person of the workspace",
253
+ "team.not_a_member: the person is not in the team",
254
+ ],
255
+ },
231
256
  workspace: {
232
257
  usage:
233
258
  "workspace | workspace edit [--people-visibility <everyone|shared-projects>] [--week-start <auto|monday|sunday>] [--time-zone <zone|none>] | workspace pack-token --connect | workspace pack-token --stdin | workspace pack-token --remove",
@@ -247,14 +272,16 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
247
272
  },
248
273
  person: {
249
274
  usage:
250
- "person role <login> <admin|member> | person add <login> [--host gh|google] | person remove <login> | person deactivate <login> | person reactivate <login>",
275
+ "person role <login> <admin|member> | person manager <login> <manager login|none> | person email <login> <address|none> | person add <login> [--host gh|google] | person remove <login> | person deactivate <login> | person reactivate <login>",
251
276
  synopsis:
252
- "Says what a person is in the workspace, lets a login in or takes one out; for an administrator.",
277
+ "Says what a person is in the workspace, whom they report to and their work email, lets a login in or takes one out; for an administrator.",
253
278
  about:
254
- "For an administrator: the person themselves on the People page, or their agent here, named on it. role says what a person is in the workspace; the board keeps at least one administrator in. add lets a login in: whoever signs in with that account at that host is a person from then on; the host is read off the login (an address is Google's, anything else GitHub's) unless --host says. remove takes a login away that nobody has signed in with yet. deactivate takes a person out: refused on every request at once, their agents with them, kept with everything they did, and listed nowhere; reactivate takes them back in. A person the organization let in comes and goes with it, and is neither.",
279
+ "For an administrator: the person themselves on the People page, or their agent here, named on it. role says what a person is in the workspace; the board keeps at least one administrator in. manager says whom a person reports to, the reporting line of the directory, which the plugins read as a default (the approver of their leave, say) in place of a line of their own: another active person of the workspace who does not report to them, directly or through others, or none; console people shows it. email writes the person's work email, or none, by which a plugin allowed to read it matches them to an account elsewhere: a person who signs in at Google has their address from the sign-in, and an administrator writes everyone else's; it is read by an administrator, the person and such a plugin, and by nobody from the feed, which says only that it changed. add lets a login in: whoever signs in with that account at that host is a person from then on; the host is read off the login (an address is Google's, anything else GitHub's) unless --host says. remove takes a login away that nobody has signed in with yet. deactivate takes a person out: refused on every request at once, their agents with them, kept with everything they did, and listed nowhere; reactivate takes them back in. A person the organization let in comes and goes with it, and is neither.",
255
280
  refusals: [
256
281
  "auth.forbidden: only an administrator says who is in and what they are",
257
282
  "person.last_admin: the board keeps at least one administrator in; make another one first",
283
+ "person.invalid_manager: the manager is not another active person of the workspace, or reports to this person already",
284
+ "person.email_taken: the address is another person's in this workspace",
258
285
  "person.of_organization: the organization says whether that person is in",
259
286
  "listing.exists: the login is let in already, or is a person's who is in",
260
287
  "listing.unknown_host: nobody signs in through that provider here",
@@ -378,17 +405,18 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
378
405
  },
379
406
  plugin: {
380
407
  usage:
381
- "plugin <key> | plugin <key> help [<command>] | plugin <key> <command> [<args>] [--<name> <value>]... [--file <path> | -] [--run <id>] | plugin <key> records <collection> [<id>] [--where <field>=<value>]... [--search <words>] [--project <key>] [--order <field>[:desc]] [--limit <n>] [--after <cursor>] [--archived] [--versions] | plugin <key> skills [<folder>|<file>] [--search <words> [--limit <n>] [--cursor <cursor>]] | plugin <key> skill <path> | plugin <key> skill --file <path> [--offset <n>] [--limit <n>] | plugin <key> enable --consent <capability>,... [--board new|<key>] [--projects all|<key>,...] [--set <key>=<value>]... | plugin <key> disable | plugin <key> settings [--set <key>=<value>]... | plugin <key> secret <name> [--remove] | plugin <key> grant <role> --person <login>|--project <key>|--everyone | plugin <key> revoke <grant id>",
408
+ "plugin <key> | plugin <key> help [<command>] | plugin <key> <command> [<args>] [--<name> <value>]... [--file <path> | -] [--run <id>] | plugin <key> records <collection> [<id>] [--where <field>=<value>]... [--search <words>] [--project <key>] [--order <field>[:desc]] [--limit <n>] [--after <cursor>] [--archived] [--versions] | plugin <key> skills [<folder>|<file>] [--search <words> [--limit <n>] [--cursor <cursor>]] | plugin <key> skill <path> | plugin <key> skill --file <path> [--offset <n>] [--limit <n>] | plugin <key> enable --consent <capability>,... [--board new|<key>] [--projects all|<key>,...] [--set <key>=<value>]... | plugin <key> disable | plugin <key> capabilities [--consent <capability>,...|none] | plugin <key> settings [--set <key>=<value>]... | plugin <key> secret <name> [--remove] | plugin <key> grant <role> --person <login>|--project <key>|--plugin <key>|--team <key>|--everyone | plugin <key> revoke <grant id>",
382
409
  overview:
383
- "plugin <key> [help [<command>] | <command> [<args>] | records <collection> | skills | skill <path> | enable | disable | settings | secret | grant | revoke]",
410
+ "plugin <key> [help [<command>] | <command> [<args>] | records <collection> | skills | skill <path> | enable | disable | capabilities | settings | secret | grant | revoke]",
384
411
  synopsis:
385
412
  "One plugin: what it is for and its commands; a command run as you; its records, its skills; its settings, for an administrator.",
386
413
  about:
387
- "One plugin of this console, by its key. Alone: what it is, the sentence for agents, its state, the roles you hold in it, its board project, then one line per command and query you may run, with its usage, where its skills are, and its settings. help [<command>]: one command or query in full, as console help prints one: its arguments, their types and bounds, and the refusals; every argument is given by its place when positional, else as --<name> <value> or --<name>=<value>; a boolean as --<name> alone; a list as the flag given again, or with commas between; a day as YYYY-MM-DD; a person by login, or me; a project by key; a file by --file <path>, or - for standard input, which the command uploads first, with the plugin's files capability. <command> [<args>]: runs the command or the query as the person whose token you hold; the console parses the words against the definition and refuses the first that does not fit; prints what the command says, or a query's answer as JSON, a file a query answers saved under its name; --json prints the whole answer. A command that raised a decision prints its id and exits with 3: console decision <id> --wait 100 waits for the answer. The event the command leaves names your open run in the plugin's board project, --run <id> saying which when several are open; an argument marked secret is hidden in it. records <collection>: the records you may read, one a line, newest first; --where <field>=<value>, given again for several, narrows by a field the collection indexes (id, owner and project always; a number, true, false and null are read as such); --search <words> by words; --project <key> to those linked to a project; --order <field>, with :desc for the newest first; a page of --limit with --after <cursor> as the last page printed; --archived the ones put away; <id> reads one record whole, --versions its versions. skills and skill: the plugin's own skills, a pack read from its package, exactly as console skills and console skill read a project's pack. enable, disable, settings, secret, grant and revoke are an administrator's. enable prints what each capability the plugin asks opens and what a malicious plugin would reach through it, and refuses unless --consent names every capability again; --board new makes the board project the manifest proposes, --board <key> chooses one, left out where the plugin needs none; --projects all, or the keys of the projects the plugin is on; --set a setting's value, which settings changes afterwards too, an empty value clearing one, a number or true and false read as such. secret <name> reads the value from standard input, seals it and shows it never; --remove takes it away. grant gives a role to a person by login, to the members of a project by key, or to everyone, and revoke takes a grant back by its id, which console plugin <key> lists to an administrator.",
414
+ "One plugin of this console, by its key. Alone: what it is, the sentence for agents, its state, the roles you hold in it, its board project, then one line per command and query you may run, with its usage, where its skills are, and its settings. help [<command>]: one command or query in full, as console help prints one: its arguments, their types and bounds, and the refusals; every argument is given by its place when positional, else as --<name> <value> or --<name>=<value>; a boolean as --<name> alone; a list as the flag given again, or with commas between; a day as YYYY-MM-DD; a person by login, or me; a project by key; a file by --file <path>, or - for standard input, which the command uploads first, with the plugin's files capability. <command> [<args>]: runs the command or the query as the person whose token you hold; the console parses the words against the definition and refuses the first that does not fit; prints what the command says, or a query's answer as JSON, a file a query answers saved under its name; --json prints the whole answer. A command that raised a decision prints its id and exits with 3: console decision <id> --wait 100 waits for the answer. The event the command leaves names your open run in the plugin's board project, --run <id> saying which when several are open; an argument marked secret is hidden in it. records <collection>: the records you may read, one a line, newest first; --where <field>=<value>, given again for several, narrows by a field the collection indexes (id, owner and project always; a number, true, false and null are read as such); --search <words> by words; --project <key> to those linked to a project; --order <field>, with :desc for the newest first; a page of --limit with --after <cursor> as the last page printed; --archived the ones put away; <id> reads one record whole, --versions its versions. skills and skill: the plugin's own skills, a pack read from its package, exactly as console skills and console skill read a project's pack. enable, disable, settings, secret, grant and revoke are an administrator's. enable prints what each capability the plugin asks opens and what a malicious plugin would reach through it, and refuses unless --consent names every capability again; --board new makes the board project the manifest proposes, --board <key> chooses one, left out where the plugin needs none; --projects all, or the keys of the projects the plugin is on; --set a setting's value, which settings changes afterwards too, an empty value clearing one, a number or true and false read as such. secret <name> reads the value from standard input, seals it and shows it never; --remove takes it away. grant gives a role to a person by login, to the members of a project by key, to another plugin by key, to the members of a team by key, or to everyone, and revoke takes a grant back by its id, which console plugin <key> lists to an administrator.",
388
415
  limits: [
389
416
  "a command line: up to 200 words of up to 50,000 characters each; one file, up to the file bound (console help hand-in), with the plugin's files capability",
390
417
  "records: a page of up to 200; where: a field the collection indexes",
391
- "enable: consent names every capability the manifest asks; settings and secrets as the manifest bounds them",
418
+ "enable: consent names every capability the manifest requires, and the optional ones to turn on; settings and secrets as the manifest bounds them",
419
+ "capabilities: --consent names the optional capabilities the plugin holds from now on, none for none of them; those it requires stay",
392
420
  ],
393
421
  refusals: [
394
422
  "plugin.not_found: no plugin has that key, or you may not know of it; or the plugin has no such command or query",
@@ -451,25 +479,26 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
451
479
  },
452
480
  ask: {
453
481
  usage:
454
- 'ask "<question>" --option "<label>" --option "<label>" [...] [--body <markdown>|--file <path>] [--attach <path>]... [--to <login|me>] [--wait <seconds>] [--run <id>]',
482
+ 'ask "<question>" --option "<label>" --option "<label>" [...] [--body <markdown>|--file <path>] [--attach <path>]... [--to <login|me>]... [--wait <seconds>] [--run <id>]',
455
483
  overview:
456
- 'ask "<question>" --option "<label>" --option "<label>" [...] [--body <markdown>|--file <path>] [--attach <path>]... [--to <login|me>] [--wait <seconds>]',
484
+ 'ask "<question>" --option "<label>" --option "<label>" [...] [--body <markdown>|--file <path>] [--attach <path>]... [--to <login|me>]... [--wait <seconds>]',
457
485
  synopsis:
458
486
  "Raises a decision a person must make, with options, and waits a while for the answer.",
459
- about: `Raises a decision a person must make: the question, the options to choose from, and what they need to know. --attach puts a file on the decision, in the one request: a picture of what is to be chosen between, a screenshot of what was made; a picture the body names as ![words](its name) is shown there, the rest after the body, and the person reads each back with \`console file <id>\` (ADR-0055). It is for the person you work for unless --to names another of the project, who then finds it among their decisions (ADR-0042); anyone of the project may still answer. The run waits for the answer. The command waits 60 seconds for it unless --wait says otherwise, and exits with 3 if it is still to come: keep waiting with \`console decision <id> --wait 100\`, within what your shell gives one command. The decision's id is printed as soon as it is raised. The person chooses one of the options, or answers in their own words: the answer then names one more option, their words, marked own; act on it as given. ${ON_THE_RUN}`,
487
+ about: `Raises a decision a person must make: the question, the options to choose from, and what they need to know. --attach puts a file on the decision, in the one request: a picture of what is to be chosen between, a screenshot of what was made; a picture the body names as ![words](its name) is shown there, the rest after the body, and the person reads each back with \`console file <id>\` (ADR-0055). It is for the person you work for unless --to names another of the project, who then finds it among their decisions (ADR-0042); --to given again names several, each of whom finds it among theirs, and the first answer ends it (ADR-0057); anyone of the project may still answer. The run waits for the answer. The command waits 60 seconds for it unless --wait says otherwise, and exits with 3 if it is still to come: keep waiting with \`console decision <id> --wait 100\`, within what your shell gives one command. The decision's id is printed as soon as it is raised. The person chooses one of the options, or answers in their own words: the answer then names one more option, their words, marked own; act on it as given. ${ON_THE_RUN}`,
460
488
  limits: [
461
489
  `question: one line, up to ${MAX_QUESTION_LENGTH} characters`,
462
490
  `options: ${MIN_OPTIONS} to ${MAX_OPTIONS}, each one line up to ${MAX_OPTION_LABEL_LENGTH} characters`,
463
491
  `body: ${MARKDOWN}`,
464
492
  `attach: a file: up to ${MAX_FILE_BYTES} bytes, not empty; called by its name, without the path; a picture is placed in the body as ![words](its name)`,
465
493
  `at most ${MAX_FILES_PER_DECISION} files on one decision, together up to ${MAX_FILE_BYTES} bytes`,
466
- "to: a login of the project, or me",
494
+ `to: a login of the project, or me; up to ${MAX_DECIDERS} of them`,
467
495
  `at most ${MAX_DECISIONS_PER_PROJECT} decisions in one project`,
468
496
  ],
469
497
  refusals: [
470
498
  ...RUN_CHOICE,
471
499
  "decision.invalid_run: the run is not yours, or is over",
472
- "decision.invalid_decider: the person named may not work in the project",
500
+ "decision.invalid_decider: a person named may not work in the project",
501
+ "decision.too_many_deciders: more people named than one decision takes",
473
502
  "decision.too_many: the project holds as many decisions as one may",
474
503
  "decision.too_many_files: more files than one decision carries",
475
504
  "request.too_large: a file, or the files together, over the bound",
@@ -479,7 +508,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
479
508
  usage: 'answer <id> --option <n>|--own "<answer>" [--note <markdown>|--file <path>]',
480
509
  synopsis: "Writes a person's answer on a decision: the option they chose, or their own words.",
481
510
  about:
482
- "Writes on the board the answer a person gave: one of the decision's options by its number, or their own words, which become one more option of the decision marked as theirs; and a note about it. The answer is the person's, written in their name through you (ADR-0021): a choice they made in chat, in a meeting or by changing the task is written here, with where and when they made it in the note, so that the record is whole and a run that waited goes on. A decision is answered once; what followed is written with console decision <id> --outcome.",
511
+ "Writes on the board the answer a person gave: one of the decision's options by its number, or their own words, which become one more option of the decision marked as theirs; and a note about it. The answer is the person's, written in their name through you (ADR-0021), and the record says so: the decision names you beside them as the agent the answer came through, where an answer given on the pages names the person alone (ADR-0056). A choice they made in chat, in a meeting or by changing the task is written here, with where and when they made it in the note, so that the record is whole and a run that waited goes on. A decision is answered once; what followed is written with console decision <id> --outcome.",
483
512
  limits: [
484
513
  `own: one line, up to ${MAX_OPTION_LABEL_LENGTH} characters`,
485
514
  `note: Markdown, up to ${MAX_NOTE_LENGTH} characters`,
@@ -495,7 +524,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
495
524
  synopsis:
496
525
  "One decision in full; waits for its answer with --wait; writes what followed with --outcome.",
497
526
  about:
498
- "One decision, with its context, its options, its answer (one of the options, or the person's own words as one more, marked own) and what followed. With --wait, holds on for the answer that long, and exits with 3 if it is still to come. With --outcome, writes down what followed the decision, so that it reads as a record.",
527
+ "One decision, with its context, its options, its answer (one of the options, or the person's own words as one more, marked own) and what followed. With --wait, holds on for the answer that long, and exits with 3 if it is still to come. With --outcome, writes down what followed the decision, so that it reads as a record; the record names who wrote it, you beside them, and when (ADR-0056).",
499
528
  limits: [`outcome: ${MARKDOWN}`],
500
529
  refusals: ["decision.not_found: no decision has that id"],
501
530
  },
@@ -8,11 +8,13 @@ import { descriptionProblems, type PluginDescription } from "../plugin/define.js
8
8
  import type { ServerLoad } from "../plugin/load.js";
9
9
  import {
10
10
  CURRENT_PLUGIN_API,
11
+ capabilitiesAsked,
11
12
  PLUGIN_API_MAJORS,
12
13
  type PluginManifest,
13
14
  parsePluginManifest,
14
15
  } from "../plugin/manifest.js";
15
16
  import { forbiddenImportsIn, removedGlobalsIn } from "../plugin/sandbox.js";
17
+ import { formatTriple, satisfiesRange, versionOfDependency } from "../plugin/semver.js";
16
18
  import { isLanguageMap } from "./plugin-init.js";
17
19
 
18
20
  export interface CheckReport {
@@ -330,10 +332,16 @@ export function sameAsEnglish(value: unknown, at = ""): string[] {
330
332
  );
331
333
  }
332
334
 
335
+ export interface CheckOptions {
336
+ /** The version of the console this command belongs to, held against the manifest's `console`; left out, not compared. */
337
+ readonly consoleVersion?: string | undefined;
338
+ }
339
+
333
340
  /** Reads the plugin at `root` as a console would, and says what it would refuse. */
334
341
  export async function checkPlugin(
335
342
  root: string,
336
343
  loadServer: (file: string) => Promise<ServerLoad>,
344
+ options: CheckOptions = {},
337
345
  ): Promise<CheckReport> {
338
346
  const errors: string[] = [];
339
347
  const warnings: string[] = [];
@@ -365,6 +373,15 @@ export async function checkPlugin(
365
373
  `console-plugin.json: ${path} is the same as en: translate it, or leave the language out`,
366
374
  );
367
375
  }
376
+ if (
377
+ manifest.console !== undefined &&
378
+ options.consoleVersion !== undefined &&
379
+ !satisfiesRange(options.consoleVersion, manifest.console)
380
+ ) {
381
+ warnings.push(
382
+ `console-plugin.json: console ${manifest.console} leaves out ${options.consoleVersion}, the console this command belongs to`,
383
+ );
384
+ }
368
385
 
369
386
  const packageRead = await readJson(join(root, "package.json"));
370
387
  let version = "0.0.0";
@@ -393,6 +410,25 @@ export async function checkPlugin(
393
410
  if (pkg.type !== "module") {
394
411
  errors.push('package.json: type is not "module"; the server module is ESM');
395
412
  }
413
+ // The console the plugin is built against is the one its types come from: a devDependency
414
+ // outside the consoles it declares is a plugin built for a console it says it does not run on.
415
+ const devDependencies = (
416
+ typeof pkg.devDependencies === "object" && pkg.devDependencies !== null
417
+ ? pkg.devDependencies
418
+ : {}
419
+ ) as Record<string, unknown>;
420
+ const dependency = devDependencies["@skillcdn/console"];
421
+ const builtAgainst =
422
+ typeof dependency === "string" ? versionOfDependency(dependency) : undefined;
423
+ if (
424
+ manifest.console !== undefined &&
425
+ builtAgainst !== undefined &&
426
+ !satisfiesRange(formatTriple(builtAgainst), manifest.console)
427
+ ) {
428
+ errors.push(
429
+ `package.json: devDependencies names @skillcdn/console ${String(dependency)}, outside the consoles the manifest declares (console ${manifest.console})`,
430
+ );
431
+ }
396
432
  const scripts = (
397
433
  typeof pkg.scripts === "object" && pkg.scripts !== null ? pkg.scripts : {}
398
434
  ) as Record<string, unknown>;
@@ -456,14 +492,17 @@ export async function checkPlugin(
456
492
  errors.push(`${manifest.server}: ${problem}`);
457
493
  }
458
494
  if (
459
- manifest.capabilities.includes("endpoints") &&
495
+ capabilitiesAsked(manifest).includes("endpoints") &&
460
496
  Object.keys(description.endpoints).length === 0
461
497
  ) {
462
498
  warnings.push(
463
499
  "console-plugin.json: asks the capability endpoints and the server module defines no endpoint",
464
500
  );
465
501
  }
466
- if (manifest.capabilities.includes("jobs") && Object.keys(description.jobs).length === 0) {
502
+ if (
503
+ capabilitiesAsked(manifest).includes("jobs") &&
504
+ Object.keys(description.jobs).length === 0
505
+ ) {
467
506
  warnings.push(
468
507
  "console-plugin.json: asks the capability jobs and the server module defines no job",
469
508
  );
@@ -5,6 +5,7 @@ import { resolve } from "node:path";
5
5
  import { parseArgs } from "node:util";
6
6
  import type { ServerLoad } from "../plugin/load.js";
7
7
  import { isPluginKey, RESERVED_PLUGIN_KEYS } from "../plugin/manifest.js";
8
+ import { lineRangeFrom, versionOfDependency } from "../plugin/semver.js";
8
9
  import { checkPlugin } from "./plugin-check.js";
9
10
  import { LANGUAGE_TAG, nameFromKey, PACKAGE_NAME, writePluginFromTemplate } from "./plugin-init.js";
10
11
 
@@ -176,12 +177,18 @@ async function init(args: readonly string[], io: PluginCliIo): Promise<number> {
176
177
  return PLUGIN_EXIT.usage;
177
178
  }
178
179
  const consoleDependency = values.console ?? (io.version === undefined ? "*" : `^${io.version}`);
180
+ // The consoles the plugin runs on: the line of the version it is written against, read from
181
+ // the dependency where it names one (`^0.4.3`, `0.4.3`); a dependency that names none
182
+ // (`workspace:*`) leaves the manifest naming no console.
183
+ const writtenAgainst = versionOfDependency(consoleDependency);
184
+ const consoleRange = writtenAgainst === undefined ? undefined : lineRangeFrom(writtenAgainst);
179
185
  const target = resolve(io.cwd, dir);
180
186
  try {
181
187
  const written = await writePluginFromTemplate(io.templateDir, target, {
182
188
  key,
183
189
  name,
184
190
  consoleDependency,
191
+ consoleRange,
185
192
  packageName,
186
193
  repository:
187
194
  values.repository === undefined
@@ -220,7 +227,9 @@ async function check(args: readonly string[], io: PluginCliIo): Promise<number>
220
227
  return PLUGIN_EXIT.usage;
221
228
  }
222
229
  const root = resolve(io.cwd, dir ?? ".");
223
- const report = await checkPlugin(root, (file) => io.loadServer(file));
230
+ const report = await checkPlugin(root, (file) => io.loadServer(file), {
231
+ consoleVersion: io.version,
232
+ });
224
233
  io.stdout(`console-plugin check ${root}\n`);
225
234
  io.stdout(wordsOf(report.errors, "error"));
226
235
  io.stdout(wordsOf(report.warnings, "warning"));
@@ -13,6 +13,8 @@ export interface InitOptions {
13
13
  readonly name: string;
14
14
  /** What the plugin's devDependency on `@skillcdn/console` says: `^<version>` of the command, unless said. */
15
15
  readonly consoleDependency: string;
16
+ /** The consoles the plugin runs on, the manifest's `console`: the line of the version it is written against; left out, the manifest names none. */
17
+ readonly consoleRange?: string | undefined;
16
18
  /** The package's name: `console-plugin-<key>` unless said. A scoped name is published with `publishConfig.access` `public`. */
17
19
  readonly packageName?: string | undefined;
18
20
  /** The repository the plugin lives in and, in a repository of several, its directory: `repository`, `homepage`, `bugs` and the manifest's `store`. */
@@ -40,12 +42,18 @@ export function substitutions(options: InitOptions): readonly [string, string][]
40
42
  ["__package__", packageNameOf(options)],
41
43
  ["__key__", options.key],
42
44
  ["__name__", options.name],
45
+ ["__consoleRange__", options.consoleRange ?? "*"],
43
46
  ["__console__", options.consoleDependency],
44
47
  ];
45
48
  }
46
49
 
47
50
  export function substitute(text: string, options: InitOptions): string {
48
51
  let result = text;
52
+ if (options.consoleRange === undefined) {
53
+ // No console to name: the manifest's line goes, and any console that serves the plugin API
54
+ // runs the plugin, as before the field. Taken out as text, so that the file keeps its shape.
55
+ result = result.replace(/^[ \t]*"console": "__consoleRange__",\r?\n/m, "");
56
+ }
49
57
  for (const [word, value] of substitutions(options)) {
50
58
  result = result.replaceAll(word, value);
51
59
  }