@skillcdn/console 0.4.3 → 0.4.4

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 (231) hide show
  1. package/README.md +4 -4
  2. package/dist/api.d.ts +3 -3
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +3 -3
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts.map +1 -1
  7. package/dist/cli/cli.js +95 -20
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts +4 -2
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +32 -9
  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 +17 -14
  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 +4 -0
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +2 -0
  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 +3 -3
  35. package/dist/components/board.js.map +1 -1
  36. package/dist/components/decision-form.d.ts +6 -0
  37. package/dist/components/decision-form.d.ts.map +1 -1
  38. package/dist/components/decision-form.js +23 -5
  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 +7 -1
  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/pack-connect.d.ts.map +1 -1
  55. package/dist/components/pack-connect.js +5 -4
  56. package/dist/components/pack-connect.js.map +1 -1
  57. package/dist/components/people.d.ts +2 -0
  58. package/dist/components/people.d.ts.map +1 -1
  59. package/dist/components/people.js +15 -2
  60. package/dist/components/people.js.map +1 -1
  61. package/dist/components/plugin-frame.d.ts +3 -12
  62. package/dist/components/plugin-frame.d.ts.map +1 -1
  63. package/dist/components/plugin-frame.js +53 -20
  64. package/dist/components/plugin-frame.js.map +1 -1
  65. package/dist/components/plugin-settings.d.ts.map +1 -1
  66. package/dist/components/plugin-settings.js +151 -26
  67. package/dist/components/plugin-settings.js.map +1 -1
  68. package/dist/components/projects.d.ts +0 -8
  69. package/dist/components/projects.d.ts.map +1 -1
  70. package/dist/components/projects.js +1 -13
  71. package/dist/components/projects.js.map +1 -1
  72. package/dist/components/task-view.d.ts.map +1 -1
  73. package/dist/components/task-view.js +2 -1
  74. package/dist/components/task-view.js.map +1 -1
  75. package/dist/components/ui.d.ts.map +1 -1
  76. package/dist/components/ui.js +5 -2
  77. package/dist/components/ui.js.map +1 -1
  78. package/dist/connect-page.d.ts.map +1 -1
  79. package/dist/connect-page.js +5 -6
  80. package/dist/connect-page.js.map +1 -1
  81. package/dist/console.css +761 -191
  82. package/dist/console.d.ts +10 -3
  83. package/dist/console.d.ts.map +1 -1
  84. package/dist/console.js +33 -13
  85. package/dist/console.js.map +1 -1
  86. package/dist/data.d.ts +4 -2
  87. package/dist/data.d.ts.map +1 -1
  88. package/dist/data.js +5 -0
  89. package/dist/data.js.map +1 -1
  90. package/dist/docs-page.d.ts.map +1 -1
  91. package/dist/docs-page.js +6 -5
  92. package/dist/docs-page.js.map +1 -1
  93. package/dist/home-tiles.d.ts +92 -0
  94. package/dist/home-tiles.d.ts.map +1 -0
  95. package/dist/home-tiles.js +294 -0
  96. package/dist/home-tiles.js.map +1 -0
  97. package/dist/i18n/en.d.ts +174 -0
  98. package/dist/i18n/en.d.ts.map +1 -1
  99. package/dist/i18n/en.js +175 -1
  100. package/dist/i18n/en.js.map +1 -1
  101. package/dist/i18n/index.d.ts +4 -2
  102. package/dist/i18n/index.d.ts.map +1 -1
  103. package/dist/i18n/index.js +10 -11
  104. package/dist/i18n/index.js.map +1 -1
  105. package/dist/i18n/ko.d.ts.map +1 -1
  106. package/dist/i18n/ko.js +170 -3
  107. package/dist/i18n/ko.js.map +1 -1
  108. package/dist/index.d.ts +4 -3
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +4 -3
  111. package/dist/index.js.map +1 -1
  112. package/dist/limits.d.ts +6 -0
  113. package/dist/limits.d.ts.map +1 -1
  114. package/dist/limits.js +6 -0
  115. package/dist/limits.js.map +1 -1
  116. package/dist/plugin/bridge.d.ts +44 -3
  117. package/dist/plugin/bridge.d.ts.map +1 -1
  118. package/dist/plugin/bridge.js +26 -0
  119. package/dist/plugin/bridge.js.map +1 -1
  120. package/dist/plugin/capabilities.d.ts +7 -1
  121. package/dist/plugin/capabilities.d.ts.map +1 -1
  122. package/dist/plugin/capabilities.js +14 -1
  123. package/dist/plugin/capabilities.js.map +1 -1
  124. package/dist/plugin/context.d.ts +18 -1
  125. package/dist/plugin/context.d.ts.map +1 -1
  126. package/dist/plugin/define.d.ts +1 -0
  127. package/dist/plugin/define.d.ts.map +1 -1
  128. package/dist/plugin/define.js +1 -1
  129. package/dist/plugin/define.js.map +1 -1
  130. package/dist/plugin/limits.d.ts +5 -2
  131. package/dist/plugin/limits.d.ts.map +1 -1
  132. package/dist/plugin/limits.js +5 -2
  133. package/dist/plugin/limits.js.map +1 -1
  134. package/dist/plugin/manifest.d.ts +83 -6
  135. package/dist/plugin/manifest.d.ts.map +1 -1
  136. package/dist/plugin/manifest.js +111 -7
  137. package/dist/plugin/manifest.js.map +1 -1
  138. package/dist/plugin/rest-schemas.d.ts +76 -0
  139. package/dist/plugin/rest-schemas.d.ts.map +1 -1
  140. package/dist/plugin/rest-schemas.js +23 -2
  141. package/dist/plugin/rest-schemas.js.map +1 -1
  142. package/dist/plugin/sandbox.d.ts +4 -3
  143. package/dist/plugin/sandbox.d.ts.map +1 -1
  144. package/dist/plugin/sandbox.js +33 -6
  145. package/dist/plugin/sandbox.js.map +1 -1
  146. package/dist/plugin/semver.d.ts +26 -0
  147. package/dist/plugin/semver.d.ts.map +1 -0
  148. package/dist/plugin/semver.js +76 -0
  149. package/dist/plugin/semver.js.map +1 -0
  150. package/dist/plugin/testing.d.ts.map +1 -1
  151. package/dist/plugin/testing.js +2 -1
  152. package/dist/plugin/testing.js.map +1 -1
  153. package/dist/plugin/web.d.ts +10 -1
  154. package/dist/plugin/web.d.ts.map +1 -1
  155. package/dist/plugin/web.js +34 -0
  156. package/dist/plugin/web.js.map +1 -1
  157. package/dist/plugin.d.ts +3 -2
  158. package/dist/plugin.d.ts.map +1 -1
  159. package/dist/plugin.js +3 -2
  160. package/dist/plugin.js.map +1 -1
  161. package/dist/rules.d.ts +2 -2
  162. package/dist/rules.js +3 -3
  163. package/dist/schemas.d.ts +497 -2
  164. package/dist/schemas.d.ts.map +1 -1
  165. package/dist/schemas.js +96 -9
  166. package/dist/schemas.js.map +1 -1
  167. package/dist/use-action.d.ts +8 -2
  168. package/dist/use-action.d.ts.map +1 -1
  169. package/dist/use-action.js +13 -6
  170. package/dist/use-action.js.map +1 -1
  171. package/dist/vocabulary.d.ts +3 -1
  172. package/dist/vocabulary.d.ts.map +1 -1
  173. package/dist/vocabulary.js +3 -0
  174. package/dist/vocabulary.js.map +1 -1
  175. package/docs/specs/cli.md +4 -3
  176. package/docs/specs/plugins.md +46 -33
  177. package/docs/specs/rest.md +16 -15
  178. package/package.json +1 -1
  179. package/skills/building-a-plugin/SKILL.md +8 -7
  180. package/skills/planning-the-board/references/setting-up.md +2 -1
  181. package/skills/working-the-board/SKILL.md +3 -3
  182. package/src/api.ts +6 -0
  183. package/src/cli/cli.ts +124 -19
  184. package/src/cli/format.ts +42 -9
  185. package/src/cli/help.ts +17 -13
  186. package/src/cli/plugin-check.ts +41 -2
  187. package/src/cli/plugin-cli.ts +10 -1
  188. package/src/cli/plugin-init.ts +8 -0
  189. package/src/client.ts +8 -0
  190. package/src/compat.ts +1 -1
  191. package/src/components/board.tsx +3 -3
  192. package/src/components/decision-form.tsx +75 -19
  193. package/src/components/decision-list.tsx +45 -13
  194. package/src/components/event-feed.tsx +7 -1
  195. package/src/components/goals.tsx +4 -1
  196. package/src/components/home.tsx +615 -0
  197. package/src/components/pack-connect.tsx +5 -4
  198. package/src/components/people.tsx +136 -54
  199. package/src/components/plugin-frame.tsx +62 -63
  200. package/src/components/plugin-settings.tsx +428 -87
  201. package/src/components/projects.tsx +0 -55
  202. package/src/components/task-view.tsx +7 -0
  203. package/src/components/ui.tsx +6 -2
  204. package/src/connect-page.tsx +5 -6
  205. package/src/console.tsx +80 -30
  206. package/src/data.ts +9 -2
  207. package/src/docs-page.tsx +6 -5
  208. package/src/home-tiles.ts +438 -0
  209. package/src/i18n/en.ts +178 -0
  210. package/src/i18n/index.ts +17 -14
  211. package/src/i18n/ko.ts +174 -3
  212. package/src/index.ts +7 -5
  213. package/src/limits.ts +6 -0
  214. package/src/plugin/bridge.ts +77 -1
  215. package/src/plugin/capabilities.ts +17 -1
  216. package/src/plugin/context.ts +18 -1
  217. package/src/plugin/define.ts +4 -1
  218. package/src/plugin/limits.ts +5 -2
  219. package/src/plugin/manifest.ts +154 -10
  220. package/src/plugin/rest-schemas.ts +27 -1
  221. package/src/plugin/sandbox.ts +36 -8
  222. package/src/plugin/semver.ts +89 -0
  223. package/src/plugin/testing.ts +2 -1
  224. package/src/plugin/web.ts +49 -0
  225. package/src/plugin.ts +18 -0
  226. package/src/rules.ts +3 -3
  227. package/src/schemas.ts +118 -10
  228. package/src/styles/console.css +761 -191
  229. package/src/use-action.ts +16 -7
  230. package/src/vocabulary.ts +4 -0
  231. package/template/plugin/console-plugin.json +3 -1
@@ -13,6 +13,15 @@ export const FORBIDDEN_BUILTINS: readonly string[] = [
13
13
  "dgram",
14
14
  "dns",
15
15
  "dns/promises",
16
+ // The same network, under the names Node keeps for compatibility.
17
+ "_http_agent",
18
+ "_http_client",
19
+ "_http_common",
20
+ "_http_incoming",
21
+ "_http_outgoing",
22
+ "_http_server",
23
+ "_tls_common",
24
+ "_tls_wrap",
16
25
  "child_process",
17
26
  "cluster",
18
27
  "worker_threads",
@@ -67,6 +76,15 @@ export interface HardenedThread {
67
76
  readonly removed: readonly string[];
68
77
  }
69
78
 
79
+ /** What a plugin is told of a built-in it may not have, by whichever way it asked. */
80
+ function refusedBuiltin(specifier: string): Error {
81
+ const error = new Error(
82
+ `the host refuses ${specifier}: a plugin reaches the console through ctx alone`,
83
+ );
84
+ (error as { code?: string }).code = "ERR_PLUGIN_FORBIDDEN_BUILTIN";
85
+ return error;
86
+ }
87
+
70
88
  /** The module hook: refuses the forbidden built-ins and lets everything else resolve as it would. */
71
89
  export function forbiddingResolve<C, R>(
72
90
  specifier: string,
@@ -74,19 +92,16 @@ export function forbiddingResolve<C, R>(
74
92
  nextResolve: (specifier: string, context: C) => R,
75
93
  ): R {
76
94
  if (isForbiddenBuiltin(specifier)) {
77
- const error = new Error(
78
- `the host refuses ${specifier}: a plugin reaches the console through ctx alone`,
79
- );
80
- (error as { code?: string }).code = "ERR_PLUGIN_FORBIDDEN_BUILTIN";
81
- throw error;
95
+ throw refusedBuiltin(specifier);
82
96
  }
83
97
  return nextResolve(specifier, context);
84
98
  }
85
99
 
86
100
  /**
87
- * Hardens the current thread: refuses the forbidden built-ins through the module hooks, removes
88
- * the globals, empties the environment, and blocks `process.binding`. Call it in a worker thread
89
- * before importing plugin code; it reaches nothing outside that thread.
101
+ * Hardens the current thread: refuses the forbidden built-ins through the module hooks and through
102
+ * `process.getBuiltinModule`, which goes round them, removes the globals, empties the environment,
103
+ * and blocks `process.binding`. Call it in a worker thread before importing plugin code; it reaches
104
+ * nothing outside that thread.
90
105
  */
91
106
  export function hardenThread(
92
107
  moduleApi: Pick<typeof import("node:module"), "registerHooks">,
@@ -112,6 +127,19 @@ export function hardenThread(
112
127
  for (const name of ["binding", "_linkedBinding", "dlopen"]) {
113
128
  Object.defineProperty(proc, name, { value: refuse, writable: false, configurable: false });
114
129
  }
130
+ const getBuiltinModule = proc.getBuiltinModule;
131
+ if (typeof getBuiltinModule === "function") {
132
+ Object.defineProperty(proc, "getBuiltinModule", {
133
+ value: (specifier: string): unknown => {
134
+ if (isForbiddenBuiltin(specifier)) {
135
+ throw refusedBuiltin(specifier);
136
+ }
137
+ return getBuiltinModule.call(proc, specifier);
138
+ },
139
+ writable: false,
140
+ configurable: false,
141
+ });
142
+ }
115
143
  const env = proc.env as Record<string, unknown> | undefined;
116
144
  if (env !== undefined) {
117
145
  for (const key of Object.keys(env)) {
@@ -0,0 +1,89 @@
1
+ // Versions and ranges as a manifest spells them (docs/specs/plugins.md, "The manifest"): the
2
+ // range of another plugin's version in `requires`, and the range of the console's version in
3
+ // `console`. Read loosely, as npm spells the common ones, with no library: a plugin declares
4
+ // `>=0.4.3 <0.6`, and the console, the authoring command and the tests compare the same way.
5
+
6
+ export type Triple = readonly [number, number, number];
7
+
8
+ /**
9
+ * A version read loosely: `0.4.3`, `v0.4.3`, `0.6` (as `0.6.0`), `1` (as `1.0.0`), a prerelease or
10
+ * a build after the numbers ignored; nothing for anything else.
11
+ */
12
+ export function parseLooseVersion(text: string): Triple | undefined {
13
+ const match = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:[-+].*)?$/.exec(text.trim());
14
+ return match === null
15
+ ? undefined
16
+ : [Number(match[1]), Number(match[2] ?? "0"), Number(match[3] ?? "0")];
17
+ }
18
+
19
+ export function compareTriples(a: Triple, b: Triple): number {
20
+ return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
21
+ }
22
+
23
+ /**
24
+ * Whether a version is within a range as npm spells the common ones: `*`, exact, `^`, `~`, `>=`,
25
+ * `>`, `<=`, `<`, several with spaces between (all must hold) and alternatives with `||` between
26
+ * (one must hold). A partial version in the range is read as `.0` filled in: `<0.6` is `<0.6.0`.
27
+ */
28
+ export function satisfiesRange(version: string, range: string): boolean {
29
+ const have = parseLooseVersion(version);
30
+ const trimmed = range.trim();
31
+ if (trimmed === "*" || trimmed === "" || trimmed === "x" || trimmed === "latest") {
32
+ return true;
33
+ }
34
+ if (have === undefined) {
35
+ return false;
36
+ }
37
+ return trimmed.split(/\s*\|\|\s*/).some((alternative) =>
38
+ alternative.split(/\s+/).every((part) => {
39
+ const operator = /^(\^|~|>=|<=|>|<|=)?(.*)$/.exec(part);
40
+ const wanted = parseLooseVersion(operator?.[2] ?? "");
41
+ if (wanted === undefined) {
42
+ return false;
43
+ }
44
+ switch (operator?.[1]) {
45
+ case "^":
46
+ return (
47
+ compareTriples(have, wanted) >= 0 &&
48
+ (wanted[0] === 0 ? have[0] === 0 && have[1] === wanted[1] : have[0] === wanted[0])
49
+ );
50
+ case "~":
51
+ return (
52
+ compareTriples(have, wanted) >= 0 && have[0] === wanted[0] && have[1] === wanted[1]
53
+ );
54
+ case ">=":
55
+ return compareTriples(have, wanted) >= 0;
56
+ case ">":
57
+ return compareTriples(have, wanted) > 0;
58
+ case "<=":
59
+ return compareTriples(have, wanted) <= 0;
60
+ case "<":
61
+ return compareTriples(have, wanted) < 0;
62
+ default:
63
+ return compareTriples(have, wanted) === 0;
64
+ }
65
+ }),
66
+ );
67
+ }
68
+
69
+ /**
70
+ * The version a dependency spells, read loosely: `^0.4.3`, `~0.4.3`, `>=0.4.3` and `0.4.3` are
71
+ * all `0.4.3`; `workspace:*`, `*`, `latest` and a tag name none.
72
+ */
73
+ export function versionOfDependency(spec: string): Triple | undefined {
74
+ return parseLooseVersion(spec.trim().replace(/^(?:\^|~|>=|=)\s*/, ""));
75
+ }
76
+
77
+ export function formatTriple(version: Triple): string {
78
+ return `${version[0]}.${version[1]}.${version[2]}`;
79
+ }
80
+
81
+ /**
82
+ * The range of consoles a plugin written against `version` runs on, as `console-plugin init`
83
+ * writes it: from that version on, within its line, since the plugin API moves with the console's
84
+ * line before 1.0 (`>=0.4.3 <0.5.0`), and within its major from 1.0 (`>=1.2.0 <2.0.0`).
85
+ */
86
+ export function lineRangeFrom(version: Triple): string {
87
+ const below: Triple = version[0] === 0 ? [0, version[1] + 1, 0] : [version[0] + 1, 0, 0];
88
+ return `>=${formatTriple(version)} <${formatTriple(below)}`;
89
+ }
@@ -519,7 +519,7 @@ function refusingConsole(given: TestConsoleOptions | undefined): PluginConsole {
519
519
  return {
520
520
  workspace:
521
521
  given?.workspace ?? (async () => forbidden("console.workspace", "console.workspace")),
522
- people: fill("people", ["list", "get"], given?.people),
522
+ people: fill("people", ["list", "get", "granted"], given?.people),
523
523
  projects: fill("projects", ["list", "get", "members"], given?.projects),
524
524
  tasks: fill("tasks", ["list", "get", "create", "update", "comment"], given?.tasks),
525
525
  decisions: fill("decisions", ["list", "get", "ask"], given?.decisions),
@@ -538,6 +538,7 @@ function build(store: Store, options: TestContextOptions, caller: PluginCaller):
538
538
  version: "0.0.0",
539
539
  dataVersion: 1,
540
540
  settings: {},
541
+ capabilities: [],
541
542
  board: null,
542
543
  projects: "all",
543
544
  ...options.plugin,
package/src/plugin/web.ts CHANGED
@@ -11,11 +11,13 @@ import {
11
11
  type BridgeFile,
12
12
  type BridgeInit,
13
13
  type BridgeMethod,
14
+ type BridgePicture,
14
15
  type BridgeToPage,
15
16
  isBridgePath,
16
17
  isBridgeToFrame,
17
18
  MAX_BRIDGE_TITLE_LENGTH,
18
19
  PLUGIN_BRIDGE_VERSION,
20
+ type PluginWidgetSizeName,
19
21
  } from "./bridge.js";
20
22
  import type { PluginRecord, RecordPage, RecordVersion } from "./context.js";
21
23
 
@@ -111,6 +113,13 @@ export interface PluginFrame {
111
113
  type: string,
112
114
  ): Promise<{ readonly handle: string }>;
113
115
  };
116
+ readonly people: {
117
+ /**
118
+ * An address to put in an image, of the person of that id (ADR-0063), with `people:read`; or
119
+ * `null` for a person with no picture to show. Kept for the life of the frame.
120
+ */
121
+ picture(id: string): Promise<string | null>;
122
+ };
114
123
  /** The console's page follows a path of the console's in place; a URL is refused. */
115
124
  navigate(path: string): void;
116
125
  /** The console's page downloads a file of this plugin. */
@@ -125,12 +134,21 @@ export interface PluginFrame {
125
134
  onTheme(handler: (theme: Readonly<Record<string, string>>) => void): () => void;
126
135
  /** The console navigated within this page's address. */
127
136
  onPath(handler: (path: string) => void): () => void;
137
+ /** The person gave the widget another size (ADR-0059); `context.size` was the first. */
138
+ onSize(handler: (size: PluginWidgetSizeName) => void): () => void;
128
139
  /** Stops listening to the console's page. */
129
140
  close(): void;
130
141
  }
131
142
 
132
143
  const DEFAULT_CONNECT_MS = 10_000;
133
144
 
145
+ /** What a page is told for a person with no picture to show, which is no failure of the page's. */
146
+ const NO_PICTURE: ReadonlySet<string> = new Set([
147
+ "plugin.not_found",
148
+ "picture.refused",
149
+ "picture.unavailable",
150
+ ]);
151
+
134
152
  function globalWindow(): FrameWindow {
135
153
  const candidate = (globalThis as { window?: FrameWindow }).window;
136
154
  if (candidate === undefined) {
@@ -219,7 +237,9 @@ export function connect(options: ConnectOptions = {}): Promise<PluginFrame> {
219
237
  const eventHandlers = new Set<(kind: BridgeEventKind, data: unknown) => void>();
220
238
  const themeHandlers = new Set<(theme: Readonly<Record<string, string>>) => void>();
221
239
  const pathHandlers = new Set<(path: string) => void>();
240
+ const sizeHandlers = new Set<(size: PluginWidgetSizeName) => void>();
222
241
  const subscribed = new Set<BridgeEventKind>();
242
+ const pictures = new Map<string, Promise<string | null>>();
223
243
  let counter = 0;
224
244
  let stopHeight: (() => void) | undefined;
225
245
 
@@ -301,6 +321,26 @@ export function connect(options: ConnectOptions = {}): Promise<PluginFrame> {
301
321
  }>;
302
322
  },
303
323
  },
324
+ people: {
325
+ picture: (id) => {
326
+ const kept = pictures.get(id);
327
+ if (kept !== undefined) {
328
+ return kept;
329
+ }
330
+ const asked = (call("people.picture", { id }) as Promise<BridgePicture>).then(
331
+ ({ type, bytes }) => URL.createObjectURL(new Blob([bytes], { type })),
332
+ (error: unknown) => {
333
+ if (error instanceof PluginBridgeError && NO_PICTURE.has(error.code)) {
334
+ return null;
335
+ }
336
+ pictures.delete(id);
337
+ throw error;
338
+ },
339
+ );
340
+ pictures.set(id, asked);
341
+ return asked;
342
+ },
343
+ },
304
344
  navigate: (path) => {
305
345
  if (!isBridgePath(path)) {
306
346
  throw new PluginBridgeError({
@@ -338,6 +378,10 @@ export function connect(options: ConnectOptions = {}): Promise<PluginFrame> {
338
378
  pathHandlers.add(handler);
339
379
  return () => pathHandlers.delete(handler);
340
380
  },
381
+ onSize: (handler) => {
382
+ sizeHandlers.add(handler);
383
+ return () => sizeHandlers.delete(handler);
384
+ },
341
385
  close: () => {
342
386
  win.removeEventListener("message", listener);
343
387
  stopHeight?.();
@@ -393,6 +437,11 @@ export function connect(options: ConnectOptions = {}): Promise<PluginFrame> {
393
437
  handler(message.path);
394
438
  }
395
439
  return;
440
+ case "size":
441
+ for (const handler of sizeHandlers) {
442
+ handler(message.size);
443
+ }
444
+ return;
396
445
  default:
397
446
  return;
398
447
  }
package/src/plugin.ts CHANGED
@@ -29,6 +29,7 @@ export {
29
29
  type CapabilityWords,
30
30
  capabilityWords,
31
31
  isPluginCapability,
32
+ OUTBOUND_WEBRTC,
32
33
  outboundAllows,
33
34
  PLUGIN_CAPABILITIES,
34
35
  PLUGIN_CAPABILITY_PREFIXES,
@@ -72,8 +73,10 @@ export { PluginDefinitionError } from "./plugin/definition-error.js";
72
73
  export * from "./plugin/limits.js";
73
74
  export {
74
75
  CURRENT_PLUGIN_API,
76
+ capabilitiesAsked,
75
77
  isPluginKey,
76
78
  type Localized,
79
+ legacyWidgetSize,
77
80
  localizedText,
78
81
  type ManifestParse,
79
82
  PLUGIN_API_MAJORS,
@@ -83,6 +86,8 @@ export {
83
86
  PLUGIN_PAGE_LEVELS,
84
87
  PLUGIN_SETTING_TYPES,
85
88
  PLUGIN_WIDGET_PLACES,
89
+ PLUGIN_WIDGET_SIZE_CELLS,
90
+ PLUGIN_WIDGET_SIZE_NAMES,
86
91
  PLUGIN_WIDGET_SIZES,
87
92
  type PluginBoard,
88
93
  type PluginBoardNeeds,
@@ -93,16 +98,20 @@ export {
93
98
  type PluginPageLevel,
94
99
  type PluginRole,
95
100
  type PluginSetting,
101
+ type PluginSettingGroup,
96
102
  type PluginSettingType,
97
103
  type PluginWidget,
98
104
  type PluginWidgetPlace,
99
105
  type PluginWidgetSize,
106
+ type PluginWidgetSizeName,
100
107
  parsePluginManifest,
101
108
  pluginManifestSchema,
102
109
  RESERVED_COMMAND_NAMES,
103
110
  RESERVED_PAGE_KEYS,
104
111
  RESERVED_PLUGIN_KEYS,
105
112
  SETTING_KEY_PATTERN,
113
+ TIME_OF_DAY,
114
+ widgetSizesOf,
106
115
  } from "./plugin/manifest.js";
107
116
  export {
108
117
  isPluginError,
@@ -124,3 +133,12 @@ export {
124
133
  REMOVED_GLOBALS,
125
134
  removedGlobalsIn,
126
135
  } from "./plugin/sandbox.js";
136
+ export {
137
+ compareTriples,
138
+ formatTriple,
139
+ lineRangeFrom,
140
+ parseLooseVersion,
141
+ satisfiesRange,
142
+ type Triple,
143
+ versionOfDependency,
144
+ } from "./plugin/semver.js";
package/src/rules.ts CHANGED
@@ -83,7 +83,7 @@ export function isReferenceRules(body: string): boolean {
83
83
  /** What stands in the rules for the language of the person the agent works for. */
84
84
  export const RULES_LANGUAGE_PLACEHOLDER = "{language}";
85
85
 
86
- /** What the rules say when the person chose no language: the task's, as the people of the board wrote it. */
86
+ /** What the rules say for a person with no language yet: the task's, as the people of the board wrote it. */
87
87
  const TASKS_LANGUAGE = "the language the task is written in";
88
88
 
89
89
  /** The name of a language in English, from its tag (`ko` is Korean); the tag itself when it is not one. */
@@ -101,8 +101,8 @@ export function languageNameOf(tag: string): string {
101
101
 
102
102
  /**
103
103
  * The rules as they read for a person: `{language}` filled with the name of the language they
104
- * chose for the pages, or with the task's language when they chose none. The command fills
105
- * them for the person whose token it holds; the pages for whoever reads them.
104
+ * read in (ADR-0065), or with the task's language while they have none. The command fills them
105
+ * for the person whose token it holds; the pages for whoever reads them.
106
106
  */
107
107
  export function fillRules(body: string, language: string | null | undefined): string {
108
108
  const name = language == null ? TASKS_LANGUAGE : languageNameOf(language);
package/src/schemas.ts CHANGED
@@ -16,9 +16,12 @@ import {
16
16
  MAX_BODY_LENGTH,
17
17
  MAX_CATEGORY_KEY_LENGTH,
18
18
  MAX_CATEGORY_NAME_LENGTH,
19
+ MAX_DECIDERS,
19
20
  MAX_DOCUMENT_LENGTH,
20
21
  MAX_DOCUMENT_PATH_LENGTH,
21
22
  MAX_FILE_NAME_LENGTH,
23
+ MAX_HOME_ROW,
24
+ MAX_HOME_TILES,
22
25
  MAX_LANGUAGE_TAG_LENGTH,
23
26
  MAX_LINK_LABEL_LENGTH,
24
27
  MAX_LINKS,
@@ -42,6 +45,7 @@ import {
42
45
  MAX_WORKERS,
43
46
  MIN_OPTIONS,
44
47
  } from "./limits.js";
48
+ import { PLUGIN_WIDGET_SIZE_NAMES } from "./plugin/manifest.js";
45
49
  import { hasForbiddenCodePoint } from "./text.js";
46
50
  import {
47
51
  ARTIFACT_KINDS,
@@ -100,6 +104,14 @@ const day = z.iso.date();
100
104
  const count = z.int().check(z.nonnegative());
101
105
 
102
106
  /** A person of the workspace, as the pages show them. Nothing here is a secret. */
107
+ /** A language tag as a browser sends one (`en`, `ko-KR`). */
108
+ const languageTag = z
109
+ .string()
110
+ .check(
111
+ z.maxLength(MAX_LANGUAGE_TAG_LENGTH),
112
+ z.regex(/^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/, "must be a language tag"),
113
+ );
114
+
103
115
  export const restPersonSchema = z.object({
104
116
  id: uuid,
105
117
  /** What the account is called at its provider: a login at the git host, an address at Google. */
@@ -116,6 +128,19 @@ export const restPersonSchema = z.object({
116
128
  source: z.enum(MEMBERSHIP_SOURCES),
117
129
  /** When they were deactivated, or `null`. */
118
130
  deactivatedAt: z.nullable(instant),
131
+ /**
132
+ * The id of the person they report to, the reporting line of the directory an administrator
133
+ * keeps, or `null` for nobody; what a plugin reads as the default approver, the default
134
+ * assignee, in place of a line of its own. Answered from 0.4.4 and read as optional, so that
135
+ * a console built on this package still reads an older console's people.
136
+ */
137
+ managerId: z.optional(z.nullable(uuid)),
138
+ /**
139
+ * The language the person reads in (ADR-0065), a tag: the one they chose, else the one the pages
140
+ * first saw them in; `null` until they have opened the pages. What a plugin or an agent writes
141
+ * for them in. Answered from 0.4.4 and read as optional, as `managerId` is.
142
+ */
143
+ language: z.optional(z.nullable(languageTag)),
119
144
  });
120
145
  export type RestPerson = z.infer<typeof restPersonSchema>;
121
146
 
@@ -195,16 +220,59 @@ export const timeZoneName = z
195
220
  z.regex(/^[A-Za-z0-9_+-]+(?:\/[A-Za-z0-9_+-]+)*$/, "must be a time zone name"),
196
221
  );
197
222
 
198
- /** A language tag as a browser sends one (`en`, `ko-KR`): what the pages speak, by the person's choice. */
199
- const languageTag = z
223
+ /** What `PATCH /api/v1/me` is sent: the language the person chose, or `null` to choose none. */
224
+ /** The sizes of a tile on the front page (ADR-0061): a project's icon, and the widgets' family. */
225
+ export const HOME_TILE_SIZES = ["icon", ...PLUGIN_WIDGET_SIZE_NAMES] as const;
226
+
227
+ /** A tile by what it is: a project by its id, a widget by its plugin's key and its own. */
228
+ const homeTileId = z
200
229
  .string()
201
230
  .check(
202
- z.maxLength(MAX_LANGUAGE_TAG_LENGTH),
203
- z.regex(/^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/, "must be a language tag"),
231
+ z.regex(
232
+ /^(?:project:[0-9a-f-]{36}|widget:[a-z][a-z0-9-]{0,31}:[a-z][a-z0-9-]{0,31})$/,
233
+ "must name a project or a widget",
234
+ ),
204
235
  );
205
236
 
206
- /** What `PATCH /api/v1/me` is sent: the language the person chose, or `null` to choose none. */
207
- export const restMePatchSchema = z.object({ language: z.nullable(languageTag) });
237
+ /** One tile of a person's own front page, at a place of the grid and at a size. */
238
+ export const restHomeTileSchema = z.object({
239
+ tile: homeTileId,
240
+ x: z.int().check(z.gte(0), z.lte(11)),
241
+ y: z.int().check(z.gte(0), z.lte(MAX_HOME_ROW)),
242
+ size: z.enum(HOME_TILE_SIZES),
243
+ });
244
+ export type RestHomeTile = z.infer<typeof restHomeTileSchema>;
245
+
246
+ const homeLayoutSchema = z.array(restHomeTileSchema).check(z.maxLength(MAX_HOME_TILES));
247
+
248
+ /**
249
+ * A person's own front page (ADR-0061): the tiles they took off it, which stay off, and what
250
+ * they arranged at each width of the grid, by its columns; what they have not seen yet takes
251
+ * the first room.
252
+ */
253
+ export const restHomeSchema = z.object({
254
+ hidden: z.array(homeTileId).check(z.maxLength(MAX_HOME_TILES)),
255
+ layouts: z.object({
256
+ 12: z.optional(homeLayoutSchema),
257
+ 8: z.optional(homeLayoutSchema),
258
+ 6: z.optional(homeLayoutSchema),
259
+ 4: z.optional(homeLayoutSchema),
260
+ }),
261
+ });
262
+ export type RestHome = z.infer<typeof restHomeSchema>;
263
+
264
+ /** What `PATCH /api/v1/me` is sent: the language, the front page, either or both; `null` lets the console choose. */
265
+ export const restMePatchSchema = z
266
+ .object({
267
+ language: z.optional(z.nullable(languageTag)),
268
+ home: z.optional(z.nullable(restHomeSchema)),
269
+ })
270
+ .check(
271
+ z.refine(
272
+ (patch) => patch.language !== undefined || patch.home !== undefined,
273
+ "must name a language or the front page",
274
+ ),
275
+ );
208
276
  export type RestMePatch = z.infer<typeof restMePatchSchema>;
209
277
 
210
278
  /**
@@ -274,8 +342,13 @@ export const restMeSchema = z.object({
274
342
  * person's own knows it holds one. `null` on a session, and for nobody.
275
343
  */
276
344
  agent: z.nullable(z.string()),
277
- /** The language the person chose for the pages (ADR-0012), or `null` for the browser's. */
345
+ /** The language the person reads in (ADR-0065): the one they chose, else the one the pages first saw them in; `null` until then. */
278
346
  language: z.nullable(languageTag),
347
+ /**
348
+ * The person's own front page (ADR-0061), or `null` while they have arranged nothing; answered
349
+ * from 0.4.4 and read as optional.
350
+ */
351
+ home: z.optional(z.nullable(restHomeSchema)),
279
352
  signIn: z.array(restProviderSchema),
280
353
  /**
281
354
  * The console's version and the range of command versions it takes, as npm spells one
@@ -313,11 +386,14 @@ export const restPersonPatchSchema = z
313
386
  .object({
314
387
  role: z.optional(z.enum(PERSON_ROLES)),
315
388
  status: z.optional(z.enum(PERSON_STATUSES)),
389
+ /** Whom the person reports to: another active person of the workspace, or `null` for nobody. */
390
+ managerId: z.optional(z.nullable(uuid)),
316
391
  })
317
392
  .check(
318
393
  z.refine(
319
- (patch) => patch.role !== undefined || patch.status !== undefined,
320
- "must name a role or a status",
394
+ (patch) =>
395
+ patch.role !== undefined || patch.status !== undefined || patch.managerId !== undefined,
396
+ "must name a role, a status or a manager",
321
397
  ),
322
398
  );
323
399
  export type RestPersonPatch = z.infer<typeof restPersonPatchSchema>;
@@ -505,6 +581,8 @@ export const restTaskSchema = z.object({
505
581
  owner: restPersonSchema,
506
582
  /** The plugin that made it, by key (ADR-0052), or `null`; left out by a console older than the plugins. */
507
583
  plugin: z.optional(z.nullable(z.string())),
584
+ /** The agent the task was written through, or `null` when a person wrote it themselves (ADR-0056); the owner may have changed since. Left out by an older console. */
585
+ agent: z.optional(z.nullable(z.string())),
508
586
  /** The people at work on it (ADR-0037), in the order they came to it; `[]` while nobody is. */
509
587
  workers: z.array(restPersonSchema),
510
588
  /** The task this one is part of, or `null`. */
@@ -615,6 +693,8 @@ export const restGoalSchema = z.object({
615
693
  /** The day the goal is meant to be reached by, or `null`. */
616
694
  dueOn: z.nullable(day),
617
695
  createdBy: restPersonSchema,
696
+ /** The agent the goal was set through, or `null` when a person set it themselves (ADR-0056); left out by an older console. */
697
+ agent: z.optional(z.nullable(z.string())),
618
698
  /** The tasks that work towards it, counted by state. */
619
699
  taskCounts: restTaskCountsSchema,
620
700
  createdAt: instant,
@@ -714,8 +794,15 @@ export const restDecisionSchema = z.object({
714
794
  /** The task's number, when there is a task, or `null`. */
715
795
  taskNumber: z.nullable(z.int()),
716
796
  raisedBy: restPersonSchema,
717
- /** Who is to answer (ADR-0042): the raiser, unless another person of the project was named. */
797
+ /** The first of those to answer (ADR-0042): the raiser, unless others of the project were named. */
718
798
  decider: restPersonSchema,
799
+ /**
800
+ * Everyone the decision is addressed to (ADR-0057), `decider` first: each finds it among their
801
+ * decisions, any of them may answer, and the first answer ends it. Answered from 0.4.4 and read
802
+ * as optional, so that a console built on this package still reads an older console's
803
+ * decisions, where it is `[decider]`.
804
+ */
805
+ deciders: z.optional(z.array(restPersonSchema)),
719
806
  /** The run that raised it, as the agent asked, or `null` when a person did. */
720
807
  run: z.nullable(z.object({ id: uuid, agent: z.string() })),
721
808
  /** The plugin that raised it, by key (ADR-0052), or `null`; left out by a console older than the plugins. */
@@ -729,10 +816,24 @@ export const restDecisionSchema = z.object({
729
816
  note: z.nullable(z.string()),
730
817
  by: restPersonSchema,
731
818
  at: instant,
819
+ /**
820
+ * The agent the answer came through, when the person's agent wrote it in their name
821
+ * (`console answer`, ADR-0021), or `null` when they answered themselves (ADR-0056).
822
+ * Answered from 0.4.4 and read as optional, so that a console built on this package still
823
+ * reads an older console's decisions.
824
+ */
825
+ agent: z.optional(z.nullable(z.string())),
732
826
  }),
733
827
  ),
734
828
  /** What followed the decision, in Markdown, written afterwards; `null` until someone does. */
735
829
  outcome: z.nullable(z.string()),
830
+ /**
831
+ * Who wrote what followed, the agent they wrote it through (`null` when themselves) and when
832
+ * (ADR-0056); each `null` until someone does. Answered from 0.4.4 and read as optional.
833
+ */
834
+ outcomeBy: z.optional(z.nullable(restPersonSchema)),
835
+ outcomeAgent: z.optional(z.nullable(z.string())),
836
+ outcomeAt: z.optional(z.nullable(instant)),
736
837
  /**
737
838
  * The files put on it when it was raised (ADR-0055), in the order they came; a picture among
738
839
  * them is shown where the context names it as `![words](its name)`, else after it. Answered
@@ -820,6 +921,11 @@ export const restDecisionInputSchema = z.object({
820
921
  runId: z.optional(z.nullable(uuid)),
821
922
  /** Who is to answer (ADR-0042): a person who may work in the project; the asker when left out. */
822
923
  deciderId: z.optional(uuid),
924
+ /**
925
+ * Several to answer (ADR-0057), in place of `deciderId`: each a person who may work in the
926
+ * project, up to `MAX_DECIDERS`; any of them may answer, and the first answer ends the decision.
927
+ */
928
+ deciderIds: z.optional(z.array(uuid).check(z.minLength(1), z.maxLength(MAX_DECIDERS))),
823
929
  });
824
930
  export type RestDecisionInput = z.infer<typeof restDecisionInputSchema>;
825
931
 
@@ -873,6 +979,8 @@ export const restEventDataSchema = z.object({
873
979
  /** For `person.role_changed` and the events about a project's members: whose role, and what it became. */
874
980
  login: z.optional(z.string()),
875
981
  role: z.optional(z.string()),
982
+ /** For `person.manager_changed`: the login of whom the person reports to now, or `null` for nobody. */
983
+ manager: z.optional(z.nullable(z.string())),
876
984
  /** For the events about a project: its key and its name. */
877
985
  key: z.optional(z.string()),
878
986
  name: z.optional(z.string()),