@rackbops/ac-plugin-contract 2.0.0-alpha.39 → 2.0.0-alpha.41
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 +67 -9
- package/dist/manifest.d.ts +10 -1
- package/dist/slots.d.ts +4 -0
- package/package.json +1 -1
- package/schema.json +7 -1
package/AUTHORING.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Writing an artifact-console plugin
|
|
2
2
|
|
|
3
|
-
A plugin adds to the console without changing it: a nav **panel**, an Overview **card**,
|
|
4
|
-
**action
|
|
3
|
+
A plugin adds to the console without changing it: a nav **panel**, an Overview **card**, an
|
|
4
|
+
**action** (a button whose modal is its confirmation), a scheduled **job**, an HTTP **route**, and a **setting** are all contributed from data + a small
|
|
5
5
|
server module. The host validates the plugin's manifest before it runs any of the plugin's code, loads it at
|
|
6
6
|
startup, and surfaces it at `GET /api/plugins`.
|
|
7
7
|
|
|
@@ -119,6 +119,12 @@ Rules the host enforces, at validation or load time:
|
|
|
119
119
|
doesn't (the schema itself requires only `id`, `hostApiVersion` and `kind`). `contributes` and every sub-field
|
|
120
120
|
is optional in the schema — contribute only the points you use; each contribution's own required fields are
|
|
121
121
|
schema-enforced.
|
|
122
|
+
- **Jobs** (#137): a job's **`timeoutSec`** must be at least `1` (the schema's `minimum`; a zero or negative
|
|
123
|
+
timeout would fire at once and time out any run that awaits real I/O), and a job **`id`** must be unique
|
|
124
|
+
*within its plugin* -- two jobs with
|
|
125
|
+
one id are rejected as `duplicate job id "<id>" within the plugin`, like a duplicate panel, card or action id.
|
|
126
|
+
Job ids are plugin-scoped (a job is addressed as `<pluginId>/<jobId>`), so two different plugins may declare the
|
|
127
|
+
same job id.
|
|
122
128
|
- The manifest is the **declaration**; your server code supplies the runtime halves. An HTTP route and a job must
|
|
123
129
|
be *declared here* or the host will refuse to serve/run them (see §2).
|
|
124
130
|
|
|
@@ -138,7 +144,7 @@ export const createPlugin: CreatePlugin = (host) => ({
|
|
|
138
144
|
const greeting = (await host.settings.get("greeting")) ?? "hello"
|
|
139
145
|
return Response.json({ greeting })
|
|
140
146
|
})
|
|
141
|
-
//
|
|
147
|
+
// An action: the button, its flags and the fields come from the manifest; the handler is here.
|
|
142
148
|
host.actions.register({
|
|
143
149
|
id: "hello.ping",
|
|
144
150
|
run: async (input) => ({ echoed: input }),
|
|
@@ -174,10 +180,25 @@ export const createPlugin: CreatePlugin = (host) => ({
|
|
|
174
180
|
fails naming the file and line (an installed third-party plugin is not covered).
|
|
175
181
|
- **Action** — `host.actions.register({ id, run })`. The button, `confirm`/`danger` and the modal `fields` all
|
|
176
182
|
come from the manifest; here you supply only `run(input)`. (The action's manifest metadata drives the shell
|
|
177
|
-
UI; registering a `run` for an id the manifest omits simply leaves it without a button.)
|
|
183
|
+
UI; registering a `run` for an id the manifest omits simply leaves it without a button.) Opening the modal is
|
|
184
|
+
the confirmation (#530): its primary button, labelled with the action's `title`, fires `run` on the **first**
|
|
185
|
+
click, whether or not the manifest sets `confirm` -- there is no second "Confirm" click. `danger: true` gives
|
|
186
|
+
that primary button the danger style, gives its Ctrl/Cmd+K palette row a danger badge, and stops Enter pressed in
|
|
187
|
+
one of the form's fields from submitting it (a click on the button, or Enter or Space on the focused button,
|
|
188
|
+
still fires; Enter in a text area is a newline). A non-danger action still submits on Enter in any field,
|
|
189
|
+
including a checkbox or a radio (browsers treat that Enter as a submit): mark an action `danger` if it must
|
|
190
|
+
not fire from a stray Enter. The primary disables only while a request is in flight, so an action that
|
|
191
|
+
answers faster than an operator's second click can be run twice: make its `run` tolerate an overlapping call.
|
|
192
|
+
`confirm` stays a valid manifest flag, so an existing manifest keeps loading, but neither the modal nor the
|
|
193
|
+
palette acts on it.
|
|
178
194
|
- **Job** — implement `runJob(ctx)` and dispatch on `ctx.jobId`. The host runs only jobs you declared in
|
|
179
|
-
`contributes.jobs`, on their 5-field cron `schedule`. `ctx.log` appends to that run's recorded log
|
|
180
|
-
|
|
195
|
+
`contributes.jobs`, on their 5-field cron `schedule`. `ctx.log` appends to that run's recorded log (its last
|
|
196
|
+
50 lines are kept); the host prunes the run history when a run is recorded (#139): it keeps at most the
|
|
197
|
+
newest 200 runs of that job (per machine, for a `runOn: "agent"` job) and drops runs older than 30 days,
|
|
198
|
+
though never a job's (or machine's) newest run, so `host.jobs.lastRun` still answers for a job that runs
|
|
199
|
+
less often than monthly. A job that runs every minute therefore no longer grows the run history without
|
|
200
|
+
bound. `ctx.signal` aborts at `timeoutSec` seconds (at least 1, #137). `runOn: "host"` runs your `runJob`
|
|
201
|
+
once, in the host process.
|
|
181
202
|
`runOn: "agent"` (#51) also runs your `runJob` in the host process — the agent itself runs no plugin
|
|
182
203
|
code — but once per currently paired machine, with `ctx.machine` set to `{ id, name }`; do the
|
|
183
204
|
machine's own work through `host.agent.call(ctx.machine.id, …)`. A machine that doesn't answer when
|
|
@@ -392,6 +413,25 @@ cards -- reads when it is opened and after its own actions. Only Home is still r
|
|
|
392
413
|
which remounts it for Refresh, the heartbeat and a return after a minute; Home's open-issues card keeps
|
|
393
414
|
its count across those remounts, so within 15 minutes of its last load none of them re-reads it (#550).
|
|
394
415
|
|
|
416
|
+
**A panel that must fit the viewport reads the shell header's height (#368).** The shell header is in normal
|
|
417
|
+
flow and wraps, so its height changes with the viewport width and with what plugins put in it. The shell
|
|
418
|
+
measures it (`offsetHeight`) and publishes it in whole px as the CSS custom property `--ac-shell-header-h` on
|
|
419
|
+
`document.documentElement`: set before the first paint, kept current on a `ResizeObserver` on the header's
|
|
420
|
+
border box and on window `resize`, and removed when the shell unmounts. A panel's CSS reads
|
|
421
|
+
`var(--ac-shell-header-h, 0px)`; the `0px` fallback is what applies wherever the shell has not published it.
|
|
422
|
+
The Artifacts panel at <=720 px is the example:
|
|
423
|
+
`height: calc(100dvh - var(--ac-shell-header-h, 0px) - var(--rb-space-4))` with
|
|
424
|
+
`margin-bottom: calc(-1 * var(--rb-space-4))`, because the shell's `<main>` pads every side by
|
|
425
|
+
`var(--rb-space-4)` (the top is subtracted, the bottom cancelled), and its `position: fixed` drawer and scrim use
|
|
426
|
+
`top: var(--ac-shell-header-h, 0px)` so the header's controls stay reachable with them open. Limits: the
|
|
427
|
+
value is a whole number of px, so a header with a fractional real height leaves a panel sized against it up
|
|
428
|
+
to half a pixel over or under; `dvh` follows a phone's collapsing address bar but the shell root's own
|
|
429
|
+
`min-height` is `100vh`, so on a phone with the bar showing the page can scroll by the difference, and once
|
|
430
|
+
it has scrolled by some amount the in-flow header moves up while a `fixed` overlay stays put, leaving that
|
|
431
|
+
strip of the panel uncovered between the header and the overlay; and the panel itself resizes with `dvh` as
|
|
432
|
+
the bar collapses. The layout arithmetic and the whole-pixel rounding were measured in desktop Chromium at
|
|
433
|
+
375 px; the phone behaviours in this list are derived from the CSS, not observed on a phone.
|
|
434
|
+
|
|
395
435
|
### The `@ac/host` browser module
|
|
396
436
|
|
|
397
437
|
`@ac/host` (typed as `BrowserHostApi` in `@ac/contract`) is the one shared module instance the shell and every
|
|
@@ -435,6 +475,7 @@ const unregister = header.registerButton({ id: "hello-scan", label: "Scan", onCl
|
|
|
435
475
|
`actions.open` opens the universal confirm modal for a registered action — the plugin's own, or another
|
|
436
476
|
plugin's (`pluginId` names it; the Board and the Artifacts action bar open `provider-github`'s and others').
|
|
437
477
|
`target` scopes the draft to a specific object, e.g. an issue number, when the action isn't `scope: "global"`.
|
|
478
|
+
Opening the modal is the confirmation: the operator's first click on its primary button runs the action.
|
|
438
479
|
It is fire-and-forget, held until the shell mounts, and dropped if it never does:
|
|
439
480
|
|
|
440
481
|
```ts
|
|
@@ -465,7 +506,7 @@ the listener is called once for every action the modal dispatched and got a resu
|
|
|
465
506
|
and whoever opened it (your own `actions.open`, or the Ctrl/Cmd+K palette) — with the action's `pluginId`,
|
|
466
507
|
`actionId`, `ok`, the `error` message on a failure, and the opened ref's `target` (absent when the ref had none,
|
|
467
508
|
as from the palette) — what the action was opened *for*, not what the operator finally typed into the form. It
|
|
468
|
-
carries no input. Cancelling, or closing
|
|
509
|
+
carries no input. Cancelling, or closing the modal without clicking its primary button, calls nothing. Use it to
|
|
469
510
|
refresh what an action changed — the Board refetches the card an `issue.*` action named, so a closed issue's row
|
|
470
511
|
goes at once instead of at the next poll. It returns the unsubscribe; a listener that throws is logged and
|
|
471
512
|
skipped, and never reaches the modal or another listener. It needs no shell to be mounted: it simply hears
|
|
@@ -507,6 +548,17 @@ A `hint` (optional, e.g. a category or the plugin id) is searched the same as `l
|
|
|
507
548
|
`>hel` finds an item whose label doesn't contain "hel" but whose hint does — and renders as muted secondary
|
|
508
549
|
text in the row.
|
|
509
550
|
|
|
551
|
+
**Ctrl/Cmd+K from inside a same-origin iframe (#369).** A key pressed while an iframe has focus lands in the
|
|
552
|
+
iframe's own document, never the shell's, so the palette does not open by itself. A panel that hosts a
|
|
553
|
+
same-origin iframe (the Artifacts report preview does) forwards the hotkey: bind a `keydown` listener on the
|
|
554
|
+
iframe's `contentDocument` (skip a frame whose `contentDocument` is null or throws: cross-origin), and on Ctrl
|
|
555
|
+
or Cmd plus `k` with no Alt and no Shift, `preventDefault()` and dispatch `new CustomEvent("ac:quickcast-key")`
|
|
556
|
+
on the parent `window`. The shell opens the palette under the keystroke's own guard: only when no other dialog
|
|
557
|
+
is open, and an event while the palette is already open does nothing (closing stays with Ctrl/Cmd+K and
|
|
558
|
+
Escape). Remove the listener when the frame goes away. Closing the palette with Escape leaves focus on the
|
|
559
|
+
shell page rather than back inside the frame (observed in Chromium), so the user clicks the frame to type in
|
|
560
|
+
it again.
|
|
561
|
+
|
|
510
562
|
`fetch` makes a same-origin, credentialed, CSRF-aware request to a host/plugin route — a relative `path`
|
|
511
563
|
resolves against the console origin; a cross-origin absolute URL throws rather than being followed. A panel
|
|
512
564
|
reads a provider's routes through this rather than a bare `window.fetch`; a card should reach for `cached`
|
|
@@ -558,7 +610,9 @@ the guide is written against hello, so this is exactly what it does:
|
|
|
558
610
|
2. **The Vitest alias.** `@ac/host` only resolves for real through the host page's import map, which a plain
|
|
559
611
|
Node test runner has no equivalent of. If you test the frontend through Vitest, alias the specifier to
|
|
560
612
|
`packages/ui-shell/src/vendor/ac-host-entry.mjs` (the shell's own implementation — reuse it rather than
|
|
561
|
-
mocking), the way [`plugins/hello/vitest.config.ts`](../../plugins/hello/vitest.config.ts) does.
|
|
613
|
+
mocking), the way [`plugins/hello/vitest.config.ts`](../../plugins/hello/vitest.config.ts) does. (Leave out
|
|
614
|
+
that file's `setupFiles` line: it names this repo's own shared test-timeout script, which your plugin does not
|
|
615
|
+
have; set your own `waitFor` / test timeouts if your tests need more than Vitest's defaults.)
|
|
562
616
|
3. **Leaving it external in the build.** Nothing extra is needed here as long as you build with plain `tsc`
|
|
563
617
|
(§3, no bundler) — `tsc` only transpiles syntax, so `import { toast } from "@ac/host"` is emitted into
|
|
564
618
|
`dist/ui.js` verbatim, unresolved, for the browser's import map to satisfy at runtime, exactly like the
|
|
@@ -915,7 +969,7 @@ what it's for.
|
|
|
915
969
|
|
|
916
970
|
| Point | Declared in `contributes` | Supplied by |
|
|
917
971
|
|---|---|---|
|
|
918
|
-
| Panel | `panels[]` (`id`, `title`, `route`, `export`, `nav?`, `order?`) | a component export in `ui.js` |
|
|
972
|
+
| Panel | `panels[]` (`id`, `title`, `route`, `export`, `nav?`, `order?`, `isolation?`) | a component export in `ui.js` |
|
|
919
973
|
| Card | `cards[]` (`id`, `title`, `sizes`, `export`) | a component export in `ui.js` |
|
|
920
974
|
| Action | `actions[]` (`id`, `scope`, `title?`, `confirm?`, `danger?`, `fields?`) | `host.actions.register({ id, run })` |
|
|
921
975
|
| Job | `jobs[]` (`id`, `schedule`, `timeoutSec`, `runOn`) | `runJob(ctx)` dispatching on `ctx.jobId` |
|
|
@@ -943,6 +997,10 @@ Ties break the same way, so the order is total. `order` is a hint the operator c
|
|
|
943
997
|
which puts them right after Home). `Home` is always first and is not orderable. A panel that lost a
|
|
944
998
|
contested nav slot still has no tab, and still appears in quickcast at its ranked position.
|
|
945
999
|
|
|
1000
|
+
**`isolation` (#124).** A panel may declare `isolation: "iframe"`: the value is reserved (design §4) and the
|
|
1001
|
+
manifest accepts it, but nothing implements it in 2.0 and the host ignores it -- panels run in-process today,
|
|
1002
|
+
declared or not. `"iframe"` is the only accepted value, and it belongs on a panel, not on a card or the manifest.
|
|
1003
|
+
|
|
946
1004
|
A card never renders its own heading -- the wrapper supplies it from the manifest `title`
|
|
947
1005
|
(`HomeCards.tsx`'s `PluginCard`, `<Card><h3>{title}</h3>...</Card>`); a card rendering one of its
|
|
948
1006
|
own duplicates it on screen (#399).
|
package/dist/manifest.d.ts
CHANGED
|
@@ -27,6 +27,11 @@ export interface PanelContribution {
|
|
|
27
27
|
order?: number;
|
|
28
28
|
/** The export name in the plugin's frontend bundle. */
|
|
29
29
|
export: string;
|
|
30
|
+
/**
|
|
31
|
+
* Reserved (design §4, #124): `"iframe"` asks the host to mount this panel in an iframe. Not
|
|
32
|
+
* implemented in 2.0; the host accepts and ignores it, so a panel declaring it still loads in-process.
|
|
33
|
+
*/
|
|
34
|
+
isolation?: "iframe";
|
|
30
35
|
}
|
|
31
36
|
/** A contributed Overview card. */
|
|
32
37
|
export interface CardContribution {
|
|
@@ -105,7 +110,11 @@ export interface JobContribution {
|
|
|
105
110
|
id: string;
|
|
106
111
|
/** 5-field cron expression. */
|
|
107
112
|
schedule: string;
|
|
108
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* Per-run timeout in seconds. Must be at least 1: a zero or negative value would fire the timeout at
|
|
115
|
+
* once, timing out any run that awaits real I/O (#137).
|
|
116
|
+
* @minimum 1
|
|
117
|
+
*/
|
|
109
118
|
timeoutSec: number;
|
|
110
119
|
runOn: JobRunOn;
|
|
111
120
|
/** A `runOn: "agent"` job is not dispatched to a machine whose paired agent version compares lower
|
package/dist/slots.d.ts
CHANGED
|
@@ -118,6 +118,10 @@ export interface Identity {
|
|
|
118
118
|
id: string;
|
|
119
119
|
email?: string;
|
|
120
120
|
name?: string;
|
|
121
|
+
/** `"machine"` for a non-person credential (a Cloudflare Access service token); absent or `"user"` for a
|
|
122
|
+
* person. The host admits a machine identity on `GET`/`HEAD /api/readyz` only and refuses it on every other gated
|
|
123
|
+
* route; any other value is treated as a non-person too (#552). */
|
|
124
|
+
kind?: "user" | "machine";
|
|
121
125
|
}
|
|
122
126
|
/** `auth@1`: resolve the identity for a request, or null in open mode. */
|
|
123
127
|
export interface AuthProvider {
|
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.41",
|
|
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.",
|
package/schema.json
CHANGED
|
@@ -168,6 +168,11 @@
|
|
|
168
168
|
"export": {
|
|
169
169
|
"type": "string",
|
|
170
170
|
"description": "The export name in the plugin's frontend bundle."
|
|
171
|
+
},
|
|
172
|
+
"isolation": {
|
|
173
|
+
"type": "string",
|
|
174
|
+
"const": "iframe",
|
|
175
|
+
"description": "Reserved (design §4, #124): `\"iframe\"` asks the host to mount this panel in an iframe. Not implemented in 2.0; the host accepts and ignores it, so a panel declaring it still loads in-process."
|
|
171
176
|
}
|
|
172
177
|
},
|
|
173
178
|
"required": [
|
|
@@ -343,7 +348,8 @@
|
|
|
343
348
|
},
|
|
344
349
|
"timeoutSec": {
|
|
345
350
|
"type": "number",
|
|
346
|
-
"description": "Per-run timeout in seconds."
|
|
351
|
+
"description": "Per-run timeout in seconds. Must be at least 1: a zero or negative value would fire the timeout at once, timing out any run that awaits real I/O (#137).",
|
|
352
|
+
"minimum": 1
|
|
347
353
|
},
|
|
348
354
|
"runOn": {
|
|
349
355
|
"$ref": "#/definitions/JobRunOn"
|