@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.
- package/README.md +6 -6
- package/dist/api.d.ts +4 -4
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +4 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +255 -33
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +20 -5
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +98 -26
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +39 -15
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/plugin-check.d.ts +5 -1
- package/dist/cli/plugin-check.d.ts.map +1 -1
- package/dist/cli/plugin-check.js +23 -4
- package/dist/cli/plugin-check.js.map +1 -1
- package/dist/cli/plugin-cli.d.ts.map +1 -1
- package/dist/cli/plugin-cli.js +10 -1
- package/dist/cli/plugin-cli.js.map +1 -1
- package/dist/cli/plugin-init.d.ts +2 -0
- package/dist/cli/plugin-init.d.ts.map +1 -1
- package/dist/cli/plugin-init.js +6 -0
- package/dist/cli/plugin-init.js.map +1 -1
- package/dist/client.d.ts +32 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +14 -2
- package/dist/client.js.map +1 -1
- package/dist/compat.js +1 -1
- package/dist/components/board.d.ts +8 -0
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +49 -18
- package/dist/components/board.js.map +1 -1
- package/dist/components/decision-form.d.ts +6 -3
- package/dist/components/decision-form.d.ts.map +1 -1
- package/dist/components/decision-form.js +24 -18
- package/dist/components/decision-form.js.map +1 -1
- package/dist/components/decision-list.d.ts +5 -1
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +25 -5
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +34 -8
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/goals.d.ts.map +1 -1
- package/dist/components/goals.js +2 -2
- package/dist/components/goals.js.map +1 -1
- package/dist/components/home.d.ts +35 -0
- package/dist/components/home.d.ts.map +1 -0
- package/dist/components/home.js +326 -0
- package/dist/components/home.js.map +1 -0
- package/dist/components/my-decisions.d.ts +20 -3
- package/dist/components/my-decisions.d.ts.map +1 -1
- package/dist/components/my-decisions.js +14 -1
- package/dist/components/my-decisions.js.map +1 -1
- package/dist/components/pack-connect.d.ts.map +1 -1
- package/dist/components/pack-connect.js +5 -4
- package/dist/components/pack-connect.js.map +1 -1
- package/dist/components/people.d.ts +45 -1
- package/dist/components/people.d.ts.map +1 -1
- package/dist/components/people.js +125 -4
- package/dist/components/people.js.map +1 -1
- package/dist/components/picker.d.ts +8 -0
- package/dist/components/picker.d.ts.map +1 -1
- package/dist/components/picker.js +2 -2
- package/dist/components/picker.js.map +1 -1
- package/dist/components/plugin-frame.d.ts +3 -13
- package/dist/components/plugin-frame.d.ts.map +1 -1
- package/dist/components/plugin-frame.js +75 -21
- package/dist/components/plugin-frame.js.map +1 -1
- package/dist/components/plugin-settings.d.ts +5 -1
- package/dist/components/plugin-settings.d.ts.map +1 -1
- package/dist/components/plugin-settings.js +283 -45
- package/dist/components/plugin-settings.js.map +1 -1
- package/dist/components/projects.d.ts +0 -8
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +4 -14
- package/dist/components/projects.js.map +1 -1
- package/dist/components/searches.d.ts +11 -0
- package/dist/components/searches.d.ts.map +1 -0
- package/dist/components/searches.js +30 -0
- package/dist/components/searches.js.map +1 -0
- package/dist/components/task-form.d.ts.map +1 -1
- package/dist/components/task-form.js +6 -5
- package/dist/components/task-form.js.map +1 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +2 -1
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts +16 -0
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +15 -2
- package/dist/components/ui.js.map +1 -1
- package/dist/connect-page.d.ts.map +1 -1
- package/dist/connect-page.js +5 -6
- package/dist/connect-page.js.map +1 -1
- package/dist/console.css +1123 -258
- package/dist/console.d.ts +12 -4
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +123 -27
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +18 -3
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +41 -1
- package/dist/data.js.map +1 -1
- package/dist/docs-page.d.ts.map +1 -1
- package/dist/docs-page.js +6 -5
- package/dist/docs-page.js.map +1 -1
- package/dist/home-tiles.d.ts +92 -0
- package/dist/home-tiles.d.ts.map +1 -0
- package/dist/home-tiles.js +294 -0
- package/dist/home-tiles.js.map +1 -0
- package/dist/i18n/en.d.ts +264 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +260 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/index.d.ts +4 -2
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +10 -11
- package/dist/i18n/index.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +255 -4
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +12 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +12 -0
- package/dist/limits.js.map +1 -1
- package/dist/plugin/args.d.ts +6 -1
- package/dist/plugin/args.d.ts.map +1 -1
- package/dist/plugin/args.js +16 -11
- package/dist/plugin/args.js.map +1 -1
- package/dist/plugin/bridge.d.ts +63 -3
- package/dist/plugin/bridge.d.ts.map +1 -1
- package/dist/plugin/bridge.js +54 -0
- package/dist/plugin/bridge.js.map +1 -1
- package/dist/plugin/capabilities.d.ts +7 -1
- package/dist/plugin/capabilities.d.ts.map +1 -1
- package/dist/plugin/capabilities.js +24 -1
- package/dist/plugin/capabilities.js.map +1 -1
- package/dist/plugin/context.d.ts +174 -12
- package/dist/plugin/context.d.ts.map +1 -1
- package/dist/plugin/define.d.ts +14 -1
- package/dist/plugin/define.d.ts.map +1 -1
- package/dist/plugin/define.js +18 -2
- package/dist/plugin/define.js.map +1 -1
- package/dist/plugin/limits.d.ts +9 -2
- package/dist/plugin/limits.d.ts.map +1 -1
- package/dist/plugin/limits.js +9 -2
- package/dist/plugin/limits.js.map +1 -1
- package/dist/plugin/manifest.d.ts +98 -6
- package/dist/plugin/manifest.d.ts.map +1 -1
- package/dist/plugin/manifest.js +171 -10
- package/dist/plugin/manifest.js.map +1 -1
- package/dist/plugin/rest-schemas.d.ts +120 -0
- package/dist/plugin/rest-schemas.d.ts.map +1 -1
- package/dist/plugin/rest-schemas.js +78 -5
- package/dist/plugin/rest-schemas.js.map +1 -1
- package/dist/plugin/sandbox.d.ts +4 -3
- package/dist/plugin/sandbox.d.ts.map +1 -1
- package/dist/plugin/sandbox.js +33 -6
- package/dist/plugin/sandbox.js.map +1 -1
- package/dist/plugin/semver.d.ts +26 -0
- package/dist/plugin/semver.d.ts.map +1 -0
- package/dist/plugin/semver.js +76 -0
- package/dist/plugin/semver.js.map +1 -0
- package/dist/plugin/testing.d.ts +15 -1
- package/dist/plugin/testing.d.ts.map +1 -1
- package/dist/plugin/testing.js +42 -5
- package/dist/plugin/testing.js.map +1 -1
- package/dist/plugin/web.d.ts +16 -1
- package/dist/plugin/web.d.ts.map +1 -1
- package/dist/plugin/web.js +44 -1
- package/dist/plugin/web.js.map +1 -1
- package/dist/plugin.d.ts +3 -2
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +3 -2
- package/dist/plugin.js.map +1 -1
- package/dist/router.d.ts +19 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +18 -0
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +11 -2
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +14 -1
- package/dist/routes.js.map +1 -1
- package/dist/rules.d.ts +2 -2
- package/dist/rules.js +3 -3
- package/dist/schemas.d.ts +864 -2
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +156 -9
- package/dist/schemas.js.map +1 -1
- package/dist/use-action.d.ts +8 -2
- package/dist/use-action.d.ts.map +1 -1
- package/dist/use-action.js +13 -6
- package/dist/use-action.js.map +1 -1
- package/dist/vocabulary.d.ts +3 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +9 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +10 -6
- package/docs/specs/plugins.md +83 -62
- package/docs/specs/rest.md +34 -16
- package/package.json +1 -1
- package/skills/building-a-plugin/SKILL.md +15 -12
- package/skills/planning-the-board/references/setting-up.md +2 -1
- package/skills/working-the-board/SKILL.md +3 -3
- package/src/api.ts +23 -0
- package/src/cli/cli.ts +323 -30
- package/src/cli/format.ts +122 -26
- package/src/cli/help.ts +43 -14
- package/src/cli/plugin-check.ts +41 -2
- package/src/cli/plugin-cli.ts +10 -1
- package/src/cli/plugin-init.ts +8 -0
- package/src/client.ts +78 -0
- package/src/compat.ts +1 -1
- package/src/components/board.tsx +86 -45
- package/src/components/decision-form.tsx +73 -36
- package/src/components/decision-list.tsx +45 -13
- package/src/components/event-feed.tsx +40 -7
- package/src/components/goals.tsx +4 -1
- package/src/components/home.tsx +615 -0
- package/src/components/my-decisions.tsx +50 -5
- package/src/components/pack-connect.tsx +5 -4
- package/src/components/people.tsx +500 -61
- package/src/components/picker.tsx +11 -2
- package/src/components/plugin-frame.tsx +85 -63
- package/src/components/plugin-settings.tsx +735 -185
- package/src/components/projects.tsx +14 -69
- package/src/components/searches.ts +47 -0
- package/src/components/task-form.tsx +23 -28
- package/src/components/task-view.tsx +7 -0
- package/src/components/ui.tsx +42 -2
- package/src/connect-page.tsx +5 -6
- package/src/console.tsx +303 -64
- package/src/data.ts +62 -3
- package/src/docs-page.tsx +6 -5
- package/src/home-tiles.ts +438 -0
- package/src/i18n/en.ts +274 -0
- package/src/i18n/index.ts +17 -14
- package/src/i18n/ko.ts +267 -4
- package/src/index.ts +18 -5
- package/src/limits.ts +12 -0
- package/src/plugin/args.ts +16 -11
- package/src/plugin/bridge.ts +122 -1
- package/src/plugin/capabilities.ts +28 -1
- package/src/plugin/context.ts +186 -9
- package/src/plugin/define.ts +38 -1
- package/src/plugin/limits.ts +9 -2
- package/src/plugin/manifest.ts +228 -13
- package/src/plugin/rest-schemas.ts +89 -6
- package/src/plugin/sandbox.ts +36 -8
- package/src/plugin/semver.ts +89 -0
- package/src/plugin/testing.ts +63 -6
- package/src/plugin/web.ts +66 -0
- package/src/plugin.ts +19 -0
- package/src/router.ts +25 -0
- package/src/routes.ts +17 -2
- package/src/rules.ts +3 -3
- package/src/schemas.ts +197 -10
- package/src/styles/console.css +1123 -258
- package/src/use-action.ts +16 -7
- package/src/vocabulary.ts +10 -0
- package/template/plugin/console-plugin.json +3 -1
package/src/plugin/define.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { ArgDefinitions, ArgDescription, ArgsOf } from "./args.js";
|
|
7
7
|
import type {
|
|
8
8
|
CollectionDefinition,
|
|
9
|
+
CollectionRetention,
|
|
9
10
|
EndpointRequest,
|
|
10
11
|
EndpointResponse,
|
|
11
12
|
PluginContext,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
MAX_PLUGIN_ENDPOINTS,
|
|
20
21
|
MAX_PLUGIN_INDEXES,
|
|
21
22
|
MAX_PLUGIN_OPERATIONS,
|
|
23
|
+
MAX_PLUGIN_RETENTION_DAYS,
|
|
22
24
|
MIN_PLUGIN_JOB_INTERVAL_MS,
|
|
23
25
|
} from "./limits.js";
|
|
24
26
|
import {
|
|
@@ -103,6 +105,13 @@ export interface CollectionInput<S extends RecordSchema> {
|
|
|
103
105
|
readonly readOwn?: readonly string[];
|
|
104
106
|
/** A version kept per write; on unless said otherwise. */
|
|
105
107
|
readonly history?: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* How long a record is kept: `days` (1 to `MAX_PLUGIN_RETENTION_DAYS`) after the moment `by`
|
|
110
|
+
* names, `createdAt` unless said, `updatedAt`, or an indexed field holding a day or an instant
|
|
111
|
+
* as ISO 8601 text. The console removes what is past it, with its versions, links and files;
|
|
112
|
+
* with `history: false`, a light collection for logs and deliveries. Kept for ever when left out.
|
|
113
|
+
*/
|
|
114
|
+
readonly retention?: { readonly days: number; readonly by?: string };
|
|
106
115
|
}
|
|
107
116
|
|
|
108
117
|
/** A collection of records the console keeps for the plugin, by name. */
|
|
@@ -154,6 +163,24 @@ export function defineCollection<S extends RecordSchema>(
|
|
|
154
163
|
if (input.readOwn !== undefined && input.owner === undefined) {
|
|
155
164
|
problems.push(`the collection ${name} has readOwn roles and no owner field`);
|
|
156
165
|
}
|
|
166
|
+
const retention = input.retention;
|
|
167
|
+
if (retention !== undefined) {
|
|
168
|
+
if (
|
|
169
|
+
!Number.isInteger(retention.days) ||
|
|
170
|
+
retention.days < 1 ||
|
|
171
|
+
retention.days > MAX_PLUGIN_RETENTION_DAYS
|
|
172
|
+
) {
|
|
173
|
+
problems.push(
|
|
174
|
+
`the collection ${name} keeps records for ${String(retention.days)} days; 1 to ${MAX_PLUGIN_RETENTION_DAYS}`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
const by = retention.by ?? "createdAt";
|
|
178
|
+
if (by !== "createdAt" && by !== "updatedAt" && !indexes.includes(by)) {
|
|
179
|
+
problems.push(
|
|
180
|
+
`the collection ${name} keeps records by ${by}, which is neither createdAt, updatedAt nor an indexed field`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
157
184
|
fail(problems);
|
|
158
185
|
return Object.freeze({
|
|
159
186
|
name,
|
|
@@ -165,6 +192,10 @@ export function defineCollection<S extends RecordSchema>(
|
|
|
165
192
|
read: Object.freeze([...(input.read ?? [])]),
|
|
166
193
|
readOwn: Object.freeze([...(input.readOwn ?? [])]),
|
|
167
194
|
history: input.history ?? true,
|
|
195
|
+
retention:
|
|
196
|
+
input.retention === undefined
|
|
197
|
+
? null
|
|
198
|
+
: Object.freeze({ days: input.retention.days, by: input.retention.by ?? "createdAt" }),
|
|
168
199
|
});
|
|
169
200
|
}
|
|
170
201
|
|
|
@@ -595,6 +626,8 @@ export interface CollectionDescription {
|
|
|
595
626
|
readonly read: readonly string[];
|
|
596
627
|
readonly readOwn: readonly string[];
|
|
597
628
|
readonly history: boolean;
|
|
629
|
+
/** How long a record is kept, or `null` for as long as the plugin is; none from a plugin built before it. */
|
|
630
|
+
readonly retention?: CollectionRetention | null | undefined;
|
|
598
631
|
}
|
|
599
632
|
|
|
600
633
|
export interface EndpointDescription {
|
|
@@ -670,6 +703,7 @@ export function describePlugin(definition: PluginDefinition): PluginDescription
|
|
|
670
703
|
read: [...collection.read],
|
|
671
704
|
readOwn: [...collection.readOwn],
|
|
672
705
|
history: collection.history,
|
|
706
|
+
retention: collection.retention ?? null,
|
|
673
707
|
})),
|
|
674
708
|
commands: Object.fromEntries(
|
|
675
709
|
Object.entries(definition.commands).map(([name, command]) => [
|
|
@@ -714,6 +748,7 @@ export function descriptionProblems(
|
|
|
714
748
|
manifest: {
|
|
715
749
|
readonly roles: Readonly<Record<string, unknown>>;
|
|
716
750
|
readonly capabilities: readonly string[];
|
|
751
|
+
readonly optionalCapabilities?: readonly string[] | undefined;
|
|
717
752
|
readonly settings: readonly { readonly key: string; readonly type: string }[];
|
|
718
753
|
readonly dataVersion: number;
|
|
719
754
|
},
|
|
@@ -729,7 +764,9 @@ export function descriptionProblems(
|
|
|
729
764
|
}
|
|
730
765
|
};
|
|
731
766
|
const needs = (where: string, capability: string) => {
|
|
732
|
-
if (
|
|
767
|
+
if (
|
|
768
|
+
![...manifest.capabilities, ...(manifest.optionalCapabilities ?? [])].includes(capability)
|
|
769
|
+
) {
|
|
733
770
|
problems.push(
|
|
734
771
|
`${where} needs the capability ${capability}, which the manifest does not declare`,
|
|
735
772
|
);
|
package/src/plugin/limits.ts
CHANGED
|
@@ -8,11 +8,16 @@ export const MAX_PLUGIN_DESCRIPTION_LENGTH = 200;
|
|
|
8
8
|
export const MAX_PLUGIN_AGENT_NOTE_LENGTH = 200;
|
|
9
9
|
/** A plugin's key, a role's, a page's, a widget's, a setting's, a collection's, a command's: `^[a-z][a-z0-9-]{1,31}$`. */
|
|
10
10
|
export const MAX_PLUGIN_KEY_LENGTH = 32;
|
|
11
|
-
/** How many roles, settings, pages and widgets one manifest declares. */
|
|
11
|
+
/** How many roles, settings, groups of settings, pages and widgets one manifest declares. */
|
|
12
12
|
export const MAX_PLUGIN_ROLES = 10;
|
|
13
|
+
/** How many other plugins one plugin may ask roles of (ADR-0072). */
|
|
14
|
+
export const MAX_PLUGIN_GRANTS_ASKED = 10;
|
|
13
15
|
export const MAX_PLUGIN_SETTINGS = 50;
|
|
16
|
+
export const MAX_PLUGIN_SETTING_GROUPS = 10;
|
|
14
17
|
export const MAX_PLUGIN_PAGES = 12;
|
|
15
18
|
export const MAX_PLUGIN_WIDGETS = 4;
|
|
19
|
+
/** How many capabilities a plugin asks, the optional ones counted. */
|
|
20
|
+
export const MAX_PLUGIN_CAPABILITIES = 50;
|
|
16
21
|
/** How many collections, commands and queries together, endpoints, and indexes of one collection a server module defines. */
|
|
17
22
|
export const MAX_PLUGIN_COLLECTIONS = 50;
|
|
18
23
|
export const MAX_PLUGIN_OPERATIONS = 100;
|
|
@@ -39,6 +44,8 @@ export const MAX_PLUGIN_COMMAND_FILES = 20;
|
|
|
39
44
|
export const MAX_PLUGIN_EVENT_BYTES = 16 * 1024;
|
|
40
45
|
export const PLUGIN_EVENTS_KEPT = 100_000;
|
|
41
46
|
export const PLUGIN_EVENT_DAYS = 90;
|
|
47
|
+
/** The most days a collection's retention may keep a record: ten years. */
|
|
48
|
+
export const MAX_PLUGIN_RETENTION_DAYS = 3650;
|
|
42
49
|
/** How many records one page of a list carries at most. */
|
|
43
50
|
export const PLUGIN_LIST_LIMIT = 200;
|
|
44
51
|
/** How long plugin code may run: a command; a query or an endpoint; a hook; a job; `onEnable`, `onDisable` and an upgrade. */
|
|
@@ -64,7 +71,7 @@ export const MAX_PLUGIN_SQL_ROWS = 10_000;
|
|
|
64
71
|
export const PLUGIN_SQL_CONNECTIONS = 4;
|
|
65
72
|
/** The memory a plugin's thread may hold, in megabytes, where the deployment says nothing. */
|
|
66
73
|
export const DEFAULT_PLUGIN_MEMORY_MB = 256;
|
|
67
|
-
/** How tall a widget's frame
|
|
74
|
+
/** How tall a widget's frame grew, in pixels, before a widget had sizes of a family (ADR-0059); kept for the readers of before, read by nothing. */
|
|
68
75
|
export const MAX_PLUGIN_WIDGET_HEIGHT = 480;
|
|
69
76
|
/** How many retries a hook gets before its event is skipped with a `plugin.failed` event. */
|
|
70
77
|
export const PLUGIN_HOOK_RETRIES = 5;
|
package/src/plugin/manifest.ts
CHANGED
|
@@ -8,14 +8,18 @@ import {
|
|
|
8
8
|
MAX_PROJECT_NAME_LENGTH,
|
|
9
9
|
} from "../limits.js";
|
|
10
10
|
import { hasForbiddenCodePoint } from "../text.js";
|
|
11
|
+
import { PLUGIN_WIDGET_SIZE_NAMES, type PluginWidgetSizeName } from "./bridge.js";
|
|
11
12
|
import { isPluginCapability } from "./capabilities.js";
|
|
12
13
|
import {
|
|
13
14
|
MAX_PLUGIN_AGENT_NOTE_LENGTH,
|
|
15
|
+
MAX_PLUGIN_CAPABILITIES,
|
|
14
16
|
MAX_PLUGIN_DESCRIPTION_LENGTH,
|
|
17
|
+
MAX_PLUGIN_GRANTS_ASKED,
|
|
15
18
|
MAX_PLUGIN_NAME_LENGTH,
|
|
16
19
|
MAX_PLUGIN_OPTIONS,
|
|
17
20
|
MAX_PLUGIN_PAGES,
|
|
18
21
|
MAX_PLUGIN_ROLES,
|
|
22
|
+
MAX_PLUGIN_SETTING_GROUPS,
|
|
19
23
|
MAX_PLUGIN_SETTINGS,
|
|
20
24
|
MAX_PLUGIN_WIDGETS,
|
|
21
25
|
} from "./limits.js";
|
|
@@ -135,6 +139,7 @@ export const PLUGIN_SETTING_TYPES = [
|
|
|
135
139
|
"boolean",
|
|
136
140
|
"enum",
|
|
137
141
|
"day",
|
|
142
|
+
"time",
|
|
138
143
|
"person",
|
|
139
144
|
"project",
|
|
140
145
|
"secret",
|
|
@@ -149,14 +154,89 @@ export type PluginPageLevel = (typeof PLUGIN_PAGE_LEVELS)[number];
|
|
|
149
154
|
export const PLUGIN_WIDGET_PLACES = ["home:top", "home:bottom"] as const;
|
|
150
155
|
export type PluginWidgetPlace = (typeof PLUGIN_WIDGET_PLACES)[number];
|
|
151
156
|
|
|
157
|
+
/** The sizes of before (ADR-0054), each read as one of the family (ADR-0059): a third as small, a half as medium, the width as wide. */
|
|
152
158
|
export const PLUGIN_WIDGET_SIZES = ["third", "half", "full"] as const;
|
|
153
159
|
export type PluginWidgetSize = (typeof PLUGIN_WIDGET_SIZES)[number];
|
|
154
160
|
|
|
161
|
+
export { PLUGIN_WIDGET_SIZE_NAMES, type PluginWidgetSizeName };
|
|
162
|
+
|
|
163
|
+
const LEGACY_WIDGET_SIZES: Readonly<Record<PluginWidgetSize, PluginWidgetSizeName>> = {
|
|
164
|
+
third: "small",
|
|
165
|
+
half: "medium",
|
|
166
|
+
full: "wide",
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/** The columns and the rows of the front page's grid of square cells each size takes (ADR-0061). */
|
|
170
|
+
export const PLUGIN_WIDGET_SIZE_CELLS: Readonly<
|
|
171
|
+
Record<PluginWidgetSizeName, { readonly columns: number; readonly rows: number }>
|
|
172
|
+
> = {
|
|
173
|
+
small: { columns: 2, rows: 2 },
|
|
174
|
+
medium: { columns: 4, rows: 2 },
|
|
175
|
+
large: { columns: 4, rows: 4 },
|
|
176
|
+
wide: { columns: 8, rows: 2 },
|
|
177
|
+
full: { columns: 8, rows: 4 },
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/** The most cells across a phone's grid has; a widget comes in a size that fits it (ADR-0062). */
|
|
181
|
+
const PLUGIN_WIDGET_PHONE_COLUMNS = 4;
|
|
182
|
+
|
|
183
|
+
/** Whether a size fits a phone's grid. */
|
|
184
|
+
const fitsPhone = (size: PluginWidgetSizeName): boolean =>
|
|
185
|
+
PLUGIN_WIDGET_SIZE_CELLS[size].columns <= PLUGIN_WIDGET_PHONE_COLUMNS;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The sizes a widget comes in and the one it starts at: `sizes` and `defaultSize` where the
|
|
189
|
+
* manifest declares them, else the one size of before read as one of the family, with a size a
|
|
190
|
+
* phone can hold beside it where that one is too wide.
|
|
191
|
+
*/
|
|
192
|
+
export function widgetSizesOf(widget: {
|
|
193
|
+
readonly size?: PluginWidgetSize | undefined;
|
|
194
|
+
readonly sizes?: readonly PluginWidgetSizeName[] | undefined;
|
|
195
|
+
readonly defaultSize?: PluginWidgetSizeName | undefined;
|
|
196
|
+
}): {
|
|
197
|
+
readonly sizes: readonly PluginWidgetSizeName[];
|
|
198
|
+
readonly defaultSize: PluginWidgetSizeName;
|
|
199
|
+
} {
|
|
200
|
+
if (widget.sizes !== undefined && widget.sizes.length > 0) {
|
|
201
|
+
return { sizes: widget.sizes, defaultSize: widget.defaultSize ?? widget.sizes[0] ?? "small" };
|
|
202
|
+
}
|
|
203
|
+
const one = LEGACY_WIDGET_SIZES[widget.size ?? "third"];
|
|
204
|
+
return { sizes: fitsPhone(one) ? [one] : ["medium", one], defaultSize: one };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The size of before nearest to one of the family, for a reader of before. */
|
|
208
|
+
export function legacyWidgetSize(size: PluginWidgetSizeName): PluginWidgetSize {
|
|
209
|
+
return size === "small" ? "third" : size === "medium" || size === "large" ? "half" : "full";
|
|
210
|
+
}
|
|
211
|
+
|
|
155
212
|
export const PLUGIN_BOARD_NEEDS = ["required", "optional", "none"] as const;
|
|
156
213
|
export type PluginBoardNeeds = (typeof PLUGIN_BOARD_NEEDS)[number];
|
|
157
214
|
|
|
158
215
|
const LANGUAGE_TAG = /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
|
|
159
|
-
const DAY =
|
|
216
|
+
const DAY = /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* True for a day as `YYYY-MM-DD` that the calendar has: February 30 and April 31 fit the shape
|
|
220
|
+
* and are no day, and a date made from them comes back as another day.
|
|
221
|
+
*/
|
|
222
|
+
export function isCalendarDay(value: unknown): value is string {
|
|
223
|
+
if (typeof value !== "string") {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
const match = DAY.exec(value);
|
|
227
|
+
if (match === null) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
const year = Number(match[1]);
|
|
231
|
+
const month = Number(match[2]);
|
|
232
|
+
const day = Number(match[3]);
|
|
233
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
234
|
+
return (
|
|
235
|
+
date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
/** A time of day as `HH:MM`, read in the workspace's time zone. */
|
|
239
|
+
export const TIME_OF_DAY = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
|
|
160
240
|
/** A path within the package: relative, forward slashes, never climbing out. */
|
|
161
241
|
const PACKAGE_PATH = /^(?!\/)(?!\.\.(?:\/|$))(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+$/;
|
|
162
242
|
/** A semver range as npm spells one, loosely: the console compares versions, the registry resolves. */
|
|
@@ -230,6 +310,12 @@ const pluginSettingSchema = z
|
|
|
230
310
|
z.array(settingOption).check(z.minLength(1), z.maxLength(MAX_PLUGIN_OPTIONS)),
|
|
231
311
|
),
|
|
232
312
|
of: z.optional(settingItemType),
|
|
313
|
+
/** The group of `settingGroups` the setting is shown under; left out, above every group. */
|
|
314
|
+
group: z.optional(key),
|
|
315
|
+
/** For a `secret`: the hosts it may be put into a request to and sealed from the answers of (`slack.com`, `*.example.com`), and no other; any host the plugin may reach when left out. */
|
|
316
|
+
hosts: z.optional(z.array(z.string().check(z.maxLength(300))).check(z.maxLength(20))),
|
|
317
|
+
/** For a `secret`: `verify`, a secret that verifies signatures and signs nothing, so that a request cannot be forged with it. */
|
|
318
|
+
use: z.optional(z.literal("verify")),
|
|
233
319
|
})
|
|
234
320
|
.check(
|
|
235
321
|
z.superRefine((setting, ctx) => {
|
|
@@ -241,6 +327,15 @@ const pluginSettingSchema = z
|
|
|
241
327
|
|
|
242
328
|
export type PluginSetting = z.infer<typeof pluginSettingSchema>;
|
|
243
329
|
|
|
330
|
+
/** A group the settings form is divided into: a panel with a name and, where given, a word under it. */
|
|
331
|
+
const pluginSettingGroupSchema = z.strictObject({
|
|
332
|
+
key,
|
|
333
|
+
name: localized(MAX_PLUGIN_NAME_LENGTH),
|
|
334
|
+
description: z.optional(localized(MAX_PLUGIN_DESCRIPTION_LENGTH)),
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
export type PluginSettingGroup = z.infer<typeof pluginSettingGroupSchema>;
|
|
338
|
+
|
|
244
339
|
/** True when `value` is what a setting of `type` holds, `options` being the values of an `enum`. */
|
|
245
340
|
function fitsSettingType(
|
|
246
341
|
type: Exclude<PluginSettingType, "secret" | "list">,
|
|
@@ -258,7 +353,9 @@ function fitsSettingType(
|
|
|
258
353
|
case "enum":
|
|
259
354
|
return typeof value === "string" && (options ?? []).includes(value);
|
|
260
355
|
case "day":
|
|
261
|
-
return
|
|
356
|
+
return isCalendarDay(value);
|
|
357
|
+
case "time":
|
|
358
|
+
return typeof value === "string" && TIME_OF_DAY.test(value);
|
|
262
359
|
case "person":
|
|
263
360
|
case "project":
|
|
264
361
|
return false;
|
|
@@ -274,8 +371,18 @@ function settingProblems(setting: {
|
|
|
274
371
|
max?: number;
|
|
275
372
|
options?: readonly { value: string }[];
|
|
276
373
|
of?: Exclude<PluginSettingType, "secret" | "list">;
|
|
374
|
+
hosts?: readonly string[];
|
|
375
|
+
use?: "verify";
|
|
277
376
|
}): string[] {
|
|
278
377
|
const problems: string[] = [];
|
|
378
|
+
if (setting.type !== "secret" && (setting.hosts !== undefined || setting.use !== undefined)) {
|
|
379
|
+
problems.push(`only a secret takes hosts or use`);
|
|
380
|
+
}
|
|
381
|
+
for (const host of setting.hosts ?? []) {
|
|
382
|
+
if (!isPluginCapability(`outbound:${host}`) || host === "webrtc") {
|
|
383
|
+
problems.push(`the host "${host}" is not a host as outbound:<host> names one`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
279
386
|
const bounded =
|
|
280
387
|
setting.type === "number" ||
|
|
281
388
|
setting.type === "text" ||
|
|
@@ -334,16 +441,60 @@ const pluginPageSchema = z.strictObject({
|
|
|
334
441
|
|
|
335
442
|
export type PluginPage = z.infer<typeof pluginPageSchema>;
|
|
336
443
|
|
|
337
|
-
const pluginWidgetSchema = z
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
444
|
+
const pluginWidgetSchema = z
|
|
445
|
+
.strictObject({
|
|
446
|
+
key,
|
|
447
|
+
name: localized(MAX_PLUGIN_NAME_LENGTH),
|
|
448
|
+
place: z.enum(PLUGIN_WIDGET_PLACES),
|
|
449
|
+
/** The one size of before; `sizes` says it better. */
|
|
450
|
+
size: z.optional(z.enum(PLUGIN_WIDGET_SIZES)),
|
|
451
|
+
/** The sizes the widget comes in (ADR-0059), each once; the person picks among them. */
|
|
452
|
+
sizes: z.optional(
|
|
453
|
+
z.array(z.enum(PLUGIN_WIDGET_SIZE_NAMES)).check(z.minLength(1), z.maxLength(5)),
|
|
454
|
+
),
|
|
455
|
+
/** The size it starts at, one of `sizes`; the first of them when left out. */
|
|
456
|
+
defaultSize: z.optional(z.enum(PLUGIN_WIDGET_SIZE_NAMES)),
|
|
457
|
+
roles: z.optional(roleList),
|
|
458
|
+
})
|
|
459
|
+
.check(
|
|
460
|
+
z.superRefine((widget, ctx) => {
|
|
461
|
+
if (widget.size === undefined && widget.sizes === undefined) {
|
|
462
|
+
ctx.addIssue({
|
|
463
|
+
code: "custom",
|
|
464
|
+
message: "needs sizes, the sizes it comes in",
|
|
465
|
+
input: widget,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
if (widget.sizes !== undefined && new Set(widget.sizes).size !== widget.sizes.length) {
|
|
469
|
+
ctx.addIssue({ code: "custom", message: "sizes names a size twice", input: widget });
|
|
470
|
+
}
|
|
471
|
+
if (widget.sizes !== undefined && !widget.sizes.some(fitsPhone)) {
|
|
472
|
+
ctx.addIssue({
|
|
473
|
+
code: "custom",
|
|
474
|
+
message: "sizes needs small, medium or large: a phone's grid is four cells across",
|
|
475
|
+
input: widget,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
if (widget.defaultSize !== undefined && !(widget.sizes ?? []).includes(widget.defaultSize)) {
|
|
479
|
+
ctx.addIssue({
|
|
480
|
+
code: "custom",
|
|
481
|
+
message: "defaultSize is not one of sizes",
|
|
482
|
+
input: widget,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}),
|
|
486
|
+
);
|
|
344
487
|
|
|
345
488
|
export type PluginWidget = z.infer<typeof pluginWidgetSchema>;
|
|
346
489
|
|
|
490
|
+
/** Every capability a plugin asks: those it needs, then those it can go without (ADR-0060). */
|
|
491
|
+
export function capabilitiesAsked(manifest: {
|
|
492
|
+
readonly capabilities: readonly string[];
|
|
493
|
+
readonly optionalCapabilities?: readonly string[] | undefined;
|
|
494
|
+
}): string[] {
|
|
495
|
+
return [...manifest.capabilities, ...(manifest.optionalCapabilities ?? [])];
|
|
496
|
+
}
|
|
497
|
+
|
|
347
498
|
const pluginMenuSchema = z
|
|
348
499
|
.strictObject({
|
|
349
500
|
group: key,
|
|
@@ -388,6 +539,11 @@ export type PluginBoard = z.infer<typeof pluginBoardSchema>;
|
|
|
388
539
|
const pluginRoleSchema = z.strictObject({
|
|
389
540
|
name: localized(MAX_PLUGIN_NAME_LENGTH),
|
|
390
541
|
description: localized(MAX_PLUGIN_DESCRIPTION_LENGTH),
|
|
542
|
+
/**
|
|
543
|
+
* A role held by what a person is in the directory, with no grant (ADR-0068): `reports`,
|
|
544
|
+
* whoever someone active reports to, read from the directory on every call.
|
|
545
|
+
*/
|
|
546
|
+
follows: z.optional(z.literal("reports")),
|
|
391
547
|
});
|
|
392
548
|
|
|
393
549
|
export type PluginRole = z.infer<typeof pluginRoleSchema>;
|
|
@@ -409,12 +565,19 @@ export const pluginManifestSchema = z
|
|
|
409
565
|
description: localized(MAX_PLUGIN_DESCRIPTION_LENGTH),
|
|
410
566
|
agentNote: z.optional(localized(MAX_PLUGIN_AGENT_NOTE_LENGTH)),
|
|
411
567
|
pluginApi: z.int().check(z.positive()),
|
|
568
|
+
/** The consoles the plugin runs on, as a semver range of the console's version; left out, any console that serves `pluginApi`. */
|
|
569
|
+
console: z.optional(z.string().check(z.regex(SEMVER_RANGE, "is a semver range"))),
|
|
412
570
|
server: packagePath,
|
|
413
571
|
web: z.optional(packagePath),
|
|
414
572
|
skills: z.optional(packagePath),
|
|
415
573
|
migrations: z.optional(packagePath),
|
|
416
574
|
capabilities: z._default(
|
|
417
|
-
z.array(z.string().check(z.maxLength(300))).check(z.maxLength(
|
|
575
|
+
z.array(z.string().check(z.maxLength(300))).check(z.maxLength(MAX_PLUGIN_CAPABILITIES)),
|
|
576
|
+
[],
|
|
577
|
+
),
|
|
578
|
+
/** What the plugin can go without (ADR-0060): an administrator turns each on, or leaves it off. */
|
|
579
|
+
optionalCapabilities: z._default(
|
|
580
|
+
z.array(z.string().check(z.maxLength(300))).check(z.maxLength(MAX_PLUGIN_CAPABILITIES)),
|
|
418
581
|
[],
|
|
419
582
|
),
|
|
420
583
|
roles: z._default(z.record(key, pluginRoleSchema), {}),
|
|
@@ -423,8 +586,26 @@ export const pluginManifestSchema = z
|
|
|
423
586
|
z.record(key, z.string().check(z.regex(SEMVER_RANGE, "is a semver range"))),
|
|
424
587
|
{},
|
|
425
588
|
),
|
|
589
|
+
/**
|
|
590
|
+
* The roles of other plugins the plugin asks to hold (ADR-0072), by their key, each with why,
|
|
591
|
+
* for the administrator who grants them: shown when enabling and on the plugin's page.
|
|
592
|
+
*/
|
|
593
|
+
grants: z._default(
|
|
594
|
+
z.record(
|
|
595
|
+
key,
|
|
596
|
+
z.strictObject({
|
|
597
|
+
roles: z.array(key).check(z.minLength(1), z.maxLength(MAX_PLUGIN_ROLES)),
|
|
598
|
+
why: localized(MAX_PLUGIN_DESCRIPTION_LENGTH),
|
|
599
|
+
}),
|
|
600
|
+
),
|
|
601
|
+
{},
|
|
602
|
+
),
|
|
426
603
|
board: z._default(pluginBoardSchema, { needs: "none" }),
|
|
427
604
|
settings: z._default(z.array(pluginSettingSchema).check(z.maxLength(MAX_PLUGIN_SETTINGS)), []),
|
|
605
|
+
settingGroups: z._default(
|
|
606
|
+
z.array(pluginSettingGroupSchema).check(z.maxLength(MAX_PLUGIN_SETTING_GROUPS)),
|
|
607
|
+
[],
|
|
608
|
+
),
|
|
428
609
|
menu: z.optional(pluginMenuSchema),
|
|
429
610
|
pages: z._default(z.array(pluginPageSchema).check(z.maxLength(MAX_PLUGIN_PAGES)), []),
|
|
430
611
|
widgets: z._default(z.array(pluginWidgetSchema).check(z.maxLength(MAX_PLUGIN_WIDGETS)), []),
|
|
@@ -433,7 +614,17 @@ export const pluginManifestSchema = z
|
|
|
433
614
|
})
|
|
434
615
|
.check(
|
|
435
616
|
z.superRefine((manifest, ctx) => {
|
|
436
|
-
for (const problem of
|
|
617
|
+
for (const problem of [
|
|
618
|
+
...manifestProblems(manifest),
|
|
619
|
+
...(manifest.key in manifest.grants ? ["grants: a plugin asks nothing of itself"] : []),
|
|
620
|
+
// Its own api through `ctx.plugins` would be itself called as another plugin, and, with
|
|
621
|
+
// `people:act`, every call of its own made for anyone (ADR-0072).
|
|
622
|
+
...(capabilitiesAsked(manifest).includes(`plugins:${manifest.key}`)
|
|
623
|
+
? [
|
|
624
|
+
`capabilities: plugins:${manifest.key} is the plugin itself; a plugin calls no api of its own`,
|
|
625
|
+
]
|
|
626
|
+
: []),
|
|
627
|
+
]) {
|
|
437
628
|
ctx.addIssue({ code: "custom", message: problem, input: manifest });
|
|
438
629
|
}
|
|
439
630
|
}),
|
|
@@ -462,15 +653,30 @@ function manifestProblems(
|
|
|
462
653
|
if (roles.length > MAX_PLUGIN_ROLES) {
|
|
463
654
|
problems.push(`declares ${roles.length} roles; ${MAX_PLUGIN_ROLES} at most`);
|
|
464
655
|
}
|
|
656
|
+
const pluginsAsked = Object.keys(manifest.grants);
|
|
657
|
+
if (pluginsAsked.length > MAX_PLUGIN_GRANTS_ASKED) {
|
|
658
|
+
problems.push(
|
|
659
|
+
`asks roles of ${pluginsAsked.length} plugins; ${MAX_PLUGIN_GRANTS_ASKED} at most`,
|
|
660
|
+
);
|
|
661
|
+
}
|
|
465
662
|
if (manifest.defaultRole !== undefined && !roles.includes(manifest.defaultRole)) {
|
|
466
663
|
problems.push(`defaultRole ${manifest.defaultRole} is not a declared role`);
|
|
467
664
|
}
|
|
468
|
-
|
|
665
|
+
const asked = capabilitiesAsked(manifest);
|
|
666
|
+
if (asked.length > MAX_PLUGIN_CAPABILITIES) {
|
|
667
|
+
problems.push(`capabilities: at most ${MAX_PLUGIN_CAPABILITIES}, the optional ones counted`);
|
|
668
|
+
}
|
|
669
|
+
if (manifest.optionalCapabilities.includes("storage:sql")) {
|
|
670
|
+
problems.push(
|
|
671
|
+
"optionalCapabilities: storage:sql cannot be optional: its schema is made when the plugin is enabled",
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
for (const capability of asked) {
|
|
469
675
|
if (!isPluginCapability(capability)) {
|
|
470
676
|
problems.push(`capabilities: ${capability} is not one the console knows`);
|
|
471
677
|
}
|
|
472
678
|
}
|
|
473
|
-
for (const capability of duplicates(
|
|
679
|
+
for (const capability of duplicates(asked)) {
|
|
474
680
|
problems.push(`capabilities: ${capability} is declared twice`);
|
|
475
681
|
}
|
|
476
682
|
const undeclared = (where: string, named: readonly string[] | undefined) => {
|
|
@@ -490,11 +696,20 @@ function manifestProblems(
|
|
|
490
696
|
["pages", manifest.pages.map((page) => page.key)],
|
|
491
697
|
["widgets", manifest.widgets.map((widget) => widget.key)],
|
|
492
698
|
["settings", manifest.settings.map((setting) => setting.key)],
|
|
699
|
+
["settingGroups", manifest.settingGroups.map((group) => group.key)],
|
|
493
700
|
] as const) {
|
|
494
701
|
for (const twice of duplicates(keys)) {
|
|
495
702
|
problems.push(`${what}: ${twice} is declared twice`);
|
|
496
703
|
}
|
|
497
704
|
}
|
|
705
|
+
const groups = manifest.settingGroups.map((group) => group.key);
|
|
706
|
+
for (const setting of manifest.settings) {
|
|
707
|
+
if (setting.group !== undefined && !groups.includes(setting.group)) {
|
|
708
|
+
problems.push(
|
|
709
|
+
`settings.${setting.key} names the group ${setting.group}, which settingGroups does not declare`,
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
498
713
|
if ((manifest.pages.length > 0 || manifest.widgets.length > 0) && manifest.web === undefined) {
|
|
499
714
|
problems.push("pages and widgets need web, the directory of the bundle");
|
|
500
715
|
}
|