@rackbops/ac-plugin-contract 2.0.0-alpha.42 → 2.0.0-alpha.43
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/AUTHORING.md +75 -11
- package/dist/host-api.d.ts +10 -0
- package/dist/slots.d.ts +44 -3
- package/dist/views.d.ts +9 -0
- package/package.json +1 -1
package/AUTHORING.md
CHANGED
|
@@ -113,7 +113,12 @@ Rules the host enforces, at validation or load time:
|
|
|
113
113
|
installed plugin errors, naming the bundled owner — and the bundled one keeps running unaffected. This
|
|
114
114
|
is what stops a third-party package from disabling a bundled plugin (e.g. the auth provider, or `admin`
|
|
115
115
|
itself) just by declaring its id. Two bundled plugins sharing an id is still a build/CI mistake and both
|
|
116
|
-
error, the same for two installed plugins with no bundled plugin in the collision.
|
|
116
|
+
error, the same for two installed plugins with no bundled plugin in the collision. **An installed plugin
|
|
117
|
+
may neither provide nor consume the `auth` slot** (#452b, #604): the install gate (`ac plugins install`
|
|
118
|
+
and the admin Plugins card's install, which share it) refuses a manifest whose `contributes.provides` or
|
|
119
|
+
`contributes.consumes` names `auth` (any version), and
|
|
120
|
+
at runtime `slots.provide`/`slots.resolve` refuse that slot to an installed plugin whatever its manifest
|
|
121
|
+
declares. Every other slot, `notify@1` included, is open to an installed plugin.
|
|
117
122
|
- **`hostApiVersion`** must equal the host's `HOST_API_VERSION` (currently `1`); a mismatch is rejected.
|
|
118
123
|
- **`kind: "in-process"`** must declare a `server` entry — the *loader* records an error and skips a plugin that
|
|
119
124
|
doesn't (the schema itself requires only `id`, `hostApiVersion` and `kind`). `contributes` and every sub-field
|
|
@@ -187,10 +192,16 @@ export const createPlugin: CreatePlugin = (host) => ({
|
|
|
187
192
|
click, whether or not the manifest sets `confirm` -- there is no second "Confirm" click. `danger: true` gives
|
|
188
193
|
that primary button the danger style, gives its Ctrl/Cmd+K palette row a danger badge, and stops Enter pressed in
|
|
189
194
|
one of the form's fields from submitting it (a click on the button, or Enter or Space on the focused button,
|
|
190
|
-
still fires; Enter in a text area is a newline). A non-danger action still submits on Enter in
|
|
191
|
-
|
|
192
|
-
not fire
|
|
193
|
-
|
|
195
|
+
still fires; Enter in a text area is a newline). A non-danger action still submits on Enter in a text field;
|
|
196
|
+
Enter on a checkbox, a radio or a select never submits any action (#568), so ticking a label and pressing
|
|
197
|
+
Enter does not fire it: mark an action `danger` if it must not fire from a stray Enter in a text field either.
|
|
198
|
+
After a successful result the primary stays disabled until the modal closes, so an action that succeeded is
|
|
199
|
+
never run again from that modal, however fast it answers (a double-click whose first run resolves inside the
|
|
200
|
+
second click's interval, or a repeated Enter, cannot run it twice). The modal closes itself 1.5 s after the
|
|
201
|
+
success unless the user clicks in it first, and then it stays open, locked, until Cancel or Escape; opening the
|
|
202
|
+
same action again while that locked modal is up reuses it and does not re-run it. After a failed result the
|
|
203
|
+
primary is enabled again and a retry runs, so a double-click whose first run fails fast can run the action
|
|
204
|
+
twice (the second a retry): an action whose failure may have partly applied should tolerate that.
|
|
194
205
|
`confirm` stays a valid manifest flag, so an existing manifest keeps loading, but neither the modal nor the
|
|
195
206
|
palette acts on it.
|
|
196
207
|
- **Job** — implement `runJob(ctx)` and dispatch on `ctx.jobId`. The host runs only jobs you declared in
|
|
@@ -434,6 +445,31 @@ strip of the panel uncovered between the header and the overlay; and the panel i
|
|
|
434
445
|
the bar collapses. The layout arithmetic and the whole-pixel rounding were measured in desktop Chromium at
|
|
435
446
|
375 px; the phone behaviours in this list are derived from the CSS, not observed on a phone.
|
|
436
447
|
|
|
448
|
+
### A frontend that bundles a dependency (#98)
|
|
449
|
+
|
|
450
|
+
"Only the import-map externals" is what keeps one React on the page; it does not forbid a plugin from
|
|
451
|
+
bundling a library the import map does not carry. `plugins/ac-artifacts-weft` inlines
|
|
452
|
+
`@lepid-labs/weft-embed` (a framework-free reader, 1.15 MB) and its stylesheet into `dist/ui.js`, and its
|
|
453
|
+
`rollup.ui.config.mjs` is the reference. (The rollup configs of `issues-classic` and `ac-auth-cf-access`
|
|
454
|
+
inline a dependency into their SERVER bundle, and every other plugin's UI config inlines nothing; none of
|
|
455
|
+
them is a model for this.) What a bundled dependency changes:
|
|
456
|
+
|
|
457
|
+
- **One file.** The host serves exactly the manifest's `frontend` and never a sibling, so a stylesheet is a
|
|
458
|
+
string in the bundle that the panel injects as a `<style>`, once (the config's `inlineCss` plugin turns
|
|
459
|
+
`import css from "pkg/style.css"` into that string), never a `<link>` to a `.css` file.
|
|
460
|
+
- **Externals stay bare, everything else is inlined** (`@rollup/plugin-node-resolve`). The dependency goes in
|
|
461
|
+
`dependencies`, pinned exactly if its rendering is part of what you ship, and is absent at runtime: the
|
|
462
|
+
image carries `dist/` and no `node_modules`.
|
|
463
|
+
- **A library without types** gets a `src/<lib>.d.ts` that declares the module, listed in the UI tsconfigs.
|
|
464
|
+
- **Guard what shipped with a bundle test that runs the real build** (`src/bundle.test.ts` is the example):
|
|
465
|
+
anchor the import regex to the start of a line (inlined code carries doc comments such as ` * import
|
|
466
|
+
{unified} from 'unified'`), assert that no bare specifier outside the externals survives and that the
|
|
467
|
+
library is not one, that nothing needs `require(`, `import.meta` or `eval`, what the bundle exports, and a
|
|
468
|
+
size budget.
|
|
469
|
+
- **A stylesheet you did not write must not restyle the shell.** Check its selectors for what is scoped (a
|
|
470
|
+
parser over the CSS is enough: weft's 0.2.0 rules are all scoped to its own root), and have the smoke spec
|
|
471
|
+
compare the shell's computed styles with and without it in the page.
|
|
472
|
+
|
|
437
473
|
### The `@ac/host` browser module
|
|
438
474
|
|
|
439
475
|
`@ac/host` (typed as `BrowserHostApi` in `@ac/contract`) is the one shared module instance the shell and every
|
|
@@ -556,10 +592,13 @@ same-origin iframe (the Artifacts report preview does) forwards the hotkey: bind
|
|
|
556
592
|
iframe's `contentDocument` (skip a frame whose `contentDocument` is null or throws: cross-origin), and on Ctrl
|
|
557
593
|
or Cmd plus `k` with no Alt and no Shift, `preventDefault()` and dispatch `new CustomEvent("ac:quickcast-key")`
|
|
558
594
|
on the parent `window`. The shell opens the palette under the keystroke's own guard: only when no other dialog
|
|
559
|
-
is open, and an event while the palette is already open
|
|
560
|
-
Escape). Remove the listener when the frame goes away. Closing the palette with Escape
|
|
561
|
-
|
|
562
|
-
|
|
595
|
+
is open, and an event while the palette is already open leaves it open (closing stays with Ctrl/Cmd+K and
|
|
596
|
+
Escape). Remove the listener when the frame goes away. Closing the palette with Escape or Ctrl/Cmd+K again
|
|
597
|
+
returns focus to the frame element (#588): the native dialog's own restore does not put it back there for a
|
|
598
|
+
palette opened from inside a frame (observed in Chromium), so the shell remembers the element that had focus
|
|
599
|
+
when the event arrived and focuses it again when focus is nowhere useful after the close (on `body`, or
|
|
600
|
+
still on a node of the palette that just closed). Which element inside the frame's own document has focus
|
|
601
|
+
afterwards is the browser's call.
|
|
563
602
|
|
|
564
603
|
`fetch` makes a same-origin, credentialed, CSRF-aware request to a host/plugin route — a relative `path`
|
|
565
604
|
resolves against the console origin; a cross-origin absolute URL throws rather than being followed. A panel
|
|
@@ -997,7 +1036,25 @@ integer `rank` and the shell only sorts by it. The rule, in priority order:
|
|
|
997
1036
|
Ties break the same way, so the order is total. `order` is a hint the operator can always override with
|
|
998
1037
|
`tab_order`, so leave it off unless your panel belongs at the front (the Board declares `10`, Issues `20`,
|
|
999
1038
|
which puts them right after Home). `Home` is always first and is not orderable. A panel that lost a
|
|
1000
|
-
contested nav slot still has no tab, and still appears in quickcast at its ranked position
|
|
1039
|
+
contested nav slot still has no tab, and still appears in quickcast at its ranked position (once the
|
|
1040
|
+
operator's choice has flipped the slot, the position the other panel's tab would hold: the two swap ranks, #629).
|
|
1041
|
+
|
|
1042
|
+
**Contesting a nav slot (#65, #100).** A panel's `nav` names the slot it competes for the tab of. A slot with
|
|
1043
|
+
one panel is owned by it. When two or more panels from plugins that loaded without error name the same slot,
|
|
1044
|
+
the host picks one owner per request: the operator's choice, stored in the `nav.<slot>.panel` host setting
|
|
1045
|
+
(written through `POST /api/nav-slot` by the header's switcher, which renders only for a contested slot; an
|
|
1046
|
+
`AC_NAV_<SLOT>_PANEL` environment variable, when set, wins over it and pins the slot: the switcher for it is
|
|
1047
|
+
disabled, naming the variable, and `POST /api/nav-slot` answers `409`), else the smallest panel id. A panel
|
|
1048
|
+
that does not own its slot has no tab but keeps its route and its quickcast
|
|
1049
|
+
entry. The slot's tab stays where the default owner's would be, whichever panel owns it (#629), so a
|
|
1050
|
+
flip changes what the tab shows, not where it sits (a `tab_order` that names the chosen owner places it
|
|
1051
|
+
where the operator said). So declaring a `nav` another panel already has changes two things for every operator: the header gains
|
|
1052
|
+
the switcher, and the default owner is whichever panel id sorts first (an id that sorts after the existing
|
|
1053
|
+
owner leaves the default alone; one that sorts before it takes the tab from every operator who has not made a
|
|
1054
|
+
choice, and, since #629, also moves the slot's tab for an operator who has: its position is where the default
|
|
1055
|
+
owner's would be, and the default owner is the smallest id). The bundled example is the `artifacts` slot: the native viewer's `viewer` (`plugins/artifacts`, route
|
|
1056
|
+
`/artifacts`, the default) and the weft reader's `weft-reader` (`plugins/ac-artifacts-weft`, route
|
|
1057
|
+
`/artifacts-weft`).
|
|
1001
1058
|
|
|
1002
1059
|
**`isolation` (#124).** A panel may declare `isolation: "iframe"`: the value is reserved (design §4) and the
|
|
1003
1060
|
manifest accepts it, but nothing implements it in 2.0 and the host ignores it -- panels run in-process today,
|
|
@@ -1017,7 +1074,14 @@ refuses to activate a consumer whose slot has no provider, and says why (`resolv
|
|
|
1017
1074
|
`packages/host/src/plugins/slots.ts`, re-exported from `plugins/index.ts`). `ac-auth-cf-access`
|
|
1018
1075
|
(`auth@1`) and `ac-notify-discord` (`notify@1`) are the reference *providers*; see their own
|
|
1019
1076
|
`src/server.ts` for `host.slots.provide(slot, version, impl)`, and `design.md` §4 for the slot
|
|
1020
|
-
contract itself.
|
|
1077
|
+
contract itself. The consumer half is `host.slots.resolve<T>(slot, version)` (#97): the provider
|
|
1078
|
+
bound for a slot, or `null` when none is. It throws unless the plugin's own manifest lists the exact
|
|
1079
|
+
`{slot, version}` in `contributes.consumes`, and an installed plugin may not resolve `auth` at all. Call
|
|
1080
|
+
it at request time (inside a route handler or job), never in `activate()`: providers bind during
|
|
1081
|
+
activation in load order, so a consumer that activates first would read `null` there for good.
|
|
1082
|
+
`ac-artifacts-weft` (`artifacts@1`) is the reference consumer: the second plugin to declare `consumes`
|
|
1083
|
+
for it, after `artifacts`, which reads `provider-artifacts`' HTTP routes from the browser, and the first to
|
|
1084
|
+
call `resolve`. `hello` contributes none of the six points above via a slot (nor `attention` -- its own manifest
|
|
1021
1085
|
declares panels/cards/actions/jobs/routes/settings only, matching the intro above) -- it's the
|
|
1022
1086
|
reference for those six, not for slot provision or the attention rail; `plugins/scans` and
|
|
1023
1087
|
`plugins/overview` are the worked examples for `contributes.attention` (§8 above).
|
package/dist/host-api.d.ts
CHANGED
|
@@ -175,6 +175,16 @@ export interface SlotRegistry {
|
|
|
175
175
|
/** Register this plugin's provider instance for `slot@version`. Throws if that slot already has a
|
|
176
176
|
* provider (from this plugin or another) -- auth/notify are singletons, never a last-write-wins. */
|
|
177
177
|
provide(slot: string, version: number, impl: unknown): void;
|
|
178
|
+
/** The provider bound for `slot@version`, cast to `T` (a slot's interface in `slots.ts`, e.g.
|
|
179
|
+
* `ArtifactsProvider`), or `null` when nothing is bound. A plugin may resolve only a slot its
|
|
180
|
+
* manifest lists in `contributes.consumes`; any other throws, naming the plugin and the slot. An
|
|
181
|
+
* installed plugin may not resolve the `auth` slot at all, declared or not (as it may not provide it);
|
|
182
|
+
* the install gate also refuses a manifest that declares `provides` or `consumes` for `auth` (#604),
|
|
183
|
+
* so this call-time refusal is what remains for a plugin staged before that check.
|
|
184
|
+
* Call it at request time (inside a route handler or job), never in `activate()`: providers bind
|
|
185
|
+
* during activation in load order, so a consumer that activates before its provider would read `null`
|
|
186
|
+
* there for good (#97). */
|
|
187
|
+
resolve<T = unknown>(slot: string, version: number): T | null;
|
|
178
188
|
}
|
|
179
189
|
/** A logger prefixed with the plugin id. */
|
|
180
190
|
export interface LogService {
|
package/dist/slots.d.ts
CHANGED
|
@@ -99,19 +99,60 @@ export interface IssuesProvider {
|
|
|
99
99
|
/** A repo's own labels, independent of the standard (E5 #58). */
|
|
100
100
|
repoLabels(repo: RepoRef): Promise<Label[]>;
|
|
101
101
|
}
|
|
102
|
-
/** A node in the artifacts report tree (v1 seam; E6 #68 enriches).
|
|
102
|
+
/** A node in the artifacts report tree (v1 seam; E6 #68 enriches).
|
|
103
|
+
*
|
|
104
|
+
* A `report` node's `id` is its markdown source's path relative to the store's `artifacts/` directory
|
|
105
|
+
* (`Code-Reviews/2026-09-01/summary.md`), the id `source`/`read` take; a report with no markdown source
|
|
106
|
+
* (a rendered `.html` nothing in the store produced) falls back to its `href`. A `category` node's id is
|
|
107
|
+
* `category:<name>` and a `run` node's `run:<category>/<run>`; neither names a readable source. */
|
|
103
108
|
export interface ArtifactNode {
|
|
104
109
|
id: string;
|
|
105
110
|
title: string;
|
|
106
111
|
kind: "category" | "run" | "report";
|
|
107
112
|
children?: ArtifactNode[];
|
|
113
|
+
/** A report's markdown source path (the same relative path its `id` carries), absent when the report has
|
|
114
|
+
* no markdown source. A node without it is a rendered `.html` only: `source`/`read` then answer that
|
|
115
|
+
* rendered HTML, not markdown, so a consumer that expects markdown must skip such a node. Report nodes
|
|
116
|
+
* only (#97). */
|
|
117
|
+
md?: string;
|
|
118
|
+
/** A report's viewer path: the rendered `.html` once rendered (`provider-artifacts`' `GET /render/<href>`
|
|
119
|
+
* serves it from its docroot), else the `.md` source path, which that route does not serve (it reads the
|
|
120
|
+
* docroot only; read the text through `source`). Report nodes only (#97). */
|
|
121
|
+
href?: string;
|
|
122
|
+
}
|
|
123
|
+
/** One `search` result: a section of one report. */
|
|
124
|
+
export interface ArtifactSearchHit {
|
|
125
|
+
/** The id of the report the hit is in, built the same way as `ArtifactNode.id` for a report that has a
|
|
126
|
+
* markdown source (search only reads markdown), so a hit opens through `source`/`read`. A report the
|
|
127
|
+
* tree leaves out, such as a rendered `index` page, can still surface here. */
|
|
128
|
+
id: string;
|
|
129
|
+
/** The report's title as the search reads it (its first level-1 heading, else its file name with the
|
|
130
|
+
* extension dropped and `-`/`_` shown as spaces). For a rendered report that can differ from the tree
|
|
131
|
+
* node's title, which comes from the rendered page's `<title>`. */
|
|
132
|
+
title: string;
|
|
133
|
+
/** The matching section's heading id inside the rendered report, without a leading `#`. Absent when the
|
|
134
|
+
* report is not rendered, when its rendered heading count disagrees with the source's, or when the
|
|
135
|
+
* section has no id (a hit on the report's preamble, or an empty `id` attribute). */
|
|
136
|
+
anchor?: string;
|
|
137
|
+
/** A short excerpt around the match. */
|
|
138
|
+
snippet: string;
|
|
108
139
|
}
|
|
109
|
-
/** `artifacts@1`: tree
|
|
140
|
+
/** `artifacts@1`: the report tree, one report's source by id, and ranked search over the markdown
|
|
141
|
+
* sources. A consumer reaches it in-process through `HostApi.slots.resolve("artifacts", 1)` (#97). */
|
|
110
142
|
export interface ArtifactsProvider {
|
|
111
143
|
/** The report tree. */
|
|
112
144
|
tree(): Promise<ArtifactNode[]>;
|
|
113
|
-
/** Raw source of one report by id
|
|
145
|
+
/** Raw source of one report by id; throws exactly when `source` would answer `null`. */
|
|
114
146
|
read(id: string): Promise<string>;
|
|
147
|
+
/** Ranked cross-report search over the store's markdown sources, at most 50 hits (the cap `GET /search`
|
|
148
|
+
* has); `CONTEXT.md`, `CONTENT.md`, `Archive/` and `.sync-inbox/` are not searched. An empty array for
|
|
149
|
+
* an empty query. */
|
|
150
|
+
search(q: string): Promise<ArtifactSearchHit[]>;
|
|
151
|
+
/** Raw source of one report by id: `null` when no `.md` or `.html` resolves at that path inside the
|
|
152
|
+
* store (or the path escapes it). The tree, not `source`, says what counts as a report: a path the tree
|
|
153
|
+
* skips (an `index` page whose html exists, `CONTEXT.md`, anything under `Archive/`) still answers its
|
|
154
|
+
* text here. */
|
|
155
|
+
source(id: string): Promise<string | null>;
|
|
115
156
|
}
|
|
116
157
|
/** An authenticated identity. */
|
|
117
158
|
export interface Identity {
|
package/dist/views.d.ts
CHANGED
|
@@ -14,6 +14,15 @@ export interface PluginPanelView {
|
|
|
14
14
|
nav?: string;
|
|
15
15
|
export: string;
|
|
16
16
|
navOwner: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Present (and only ever `"env"` today) on every contestant of a contested nav slot whose choice of owner
|
|
19
|
+
* an `AC_NAV_<SLOT>_PANEL` environment variable fixes (#628), so the shell can render that slot's switcher
|
|
20
|
+
* control disabled with the reason, and a script can expect `409` from `POST /api/nav-slot` for it. The
|
|
21
|
+
* variable pins the slot whatever its value is (one naming no contestant leaves the default owner, and the
|
|
22
|
+
* slot is still pinned). Absent otherwise (never `undefined`-valued). Optional, so a shell built before it
|
|
23
|
+
* ignores it.
|
|
24
|
+
*/
|
|
25
|
+
navPinnedBy?: "env";
|
|
17
26
|
/**
|
|
18
27
|
* This panel's position among every panel in the response (#472): a non-negative integer, lower
|
|
19
28
|
* first, resolved by the host per request from the operator's `tab_order` setting, then each
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rackbops/ac-plugin-contract",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.43",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The artifact-console plugin ABI for out-of-repo plugin authors: HostApi, the acPlugin manifest contribution points, the generated manifest JSON Schema, and the @ac/host ambient browser declaration.",
|