@termix-ssh/plugin-sdk 1.0.21 → 1.0.23
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/CHANGELOG.md +32 -0
- package/PLUGIN-GUIDE.md +130 -3
- package/cli/commands/build.mjs +55 -36
- package/cli/commands/validate.mjs +4 -0
- package/dist/backend.d.ts +139 -7
- package/dist/capabilities.js +3 -0
- package/dist/frontend.d.ts +7 -0
- package/dist/host/{PluginsTab-HQGSDSHS.js → PluginsTab-MNQU5OJ3.js} +22 -14
- package/dist/host/{chunk-JFQGI4OD.js → chunk-6WLECSNB.js} +71 -12
- package/dist/host/chunk-BP42N374.js +53 -0
- package/dist/host/{chunk-WWFRBU2E.js → chunk-C52NRTA6.js} +82 -51
- package/dist/host/{chunk-GTHINW3L.js → chunk-DN6YKMFJ.js} +34650 -34968
- package/dist/host/chunk-GBBKHJRQ.js +1526 -0
- package/dist/host/{chunk-FGX27MAA.js → chunk-GRH7GSKG.js} +1 -1
- package/dist/host/chunk-IMPLAAUM.js +345 -0
- package/dist/host/{chunk-A5CULDWV.js → chunk-X65V7ZRW.js} +121 -1574
- package/dist/host/core-classes.json +1 -1
- package/dist/host/frame-host.css +2 -0
- package/dist/host/frame-host.js +242 -0
- package/dist/host/{linked-server-UDSFY3N7.js → linked-server-YCAJ2NX3.js} +1 -1
- package/dist/host/{rbac-api-AAAPMF52.js → rbac-api-A64TXDL5.js} +5 -1
- package/dist/host/testing-host.js +9 -6
- package/dist/host/types/src/types/host-sidebar-preferences.d.ts +2 -0
- package/dist/host/types/src/types/index.d.ts +8 -0
- package/dist/host/types/src/ui/api/plugins-api.d.ts +2 -1
- package/dist/host/types/src/ui/api/rbac-api.d.ts +19 -1
- package/dist/host/types/src/ui/components/data-view.d.ts +1 -1
- package/dist/host/types/src/ui/lib/db-health-monitor.d.ts +5 -0
- package/dist/host/types/src/ui/main-axios.d.ts +1 -1
- package/dist/host/types/src/ui/plugin-host/loader.d.ts +6 -0
- package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +1 -1
- package/dist/host/types/src/ui/plugins/use-plugins-manager.d.ts +1 -1
- package/dist/host/types/src/ui/shell/tab-registry.d.ts +2 -0
- package/dist/host/ui.js +357 -370
- package/dist/host/{use-permissions-VAND5GEG.js → use-permissions-FLQAN6I7.js} +1 -1
- package/dist/host-commands.d.ts +7 -0
- package/dist/host-commands.js +35 -5
- package/dist/manifest.d.ts +33 -1
- package/dist/manifest.js +114 -1
- package/dist/operations.d.ts +222 -0
- package/dist/operations.js +650 -0
- package/dist/testing.d.ts +9 -1
- package/dist/testing.js +45 -0
- package/package.json +9 -1
- package/schema/manifest.schema.json +67 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.23
|
|
4
|
+
|
|
5
|
+
Plugin API 1.14.0.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `contributes.operations` declares a plugin's actions, events, resources and signals, so Automations, Fleets, the assistant, the CLI, the phone app and the command palette can find them with no wiring
|
|
10
|
+
- `ctx.operations` registers handlers, runs other plugins' declared actions, emits events and provides approval rules, with the new `operations:provide`, `operations:invoke` and `operations:approve` capabilities
|
|
11
|
+
- `@termix-ssh/plugin-sdk/operations` has the types, input checks, redaction and JSON Schema for declared actions
|
|
12
|
+
- `contributes.hostSections` declares cards for the host page
|
|
13
|
+
|
|
14
|
+
## 1.0.22
|
|
15
|
+
|
|
16
|
+
Plugin API 1.13.0.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `contributes.mobile` in the manifest says how a plugin shows in the Termix mobile app: `webview`, `native` or `none`, with an optional mobile bundle, screen sizes, views and a note
|
|
21
|
+
- `termix-plugin build` bundles `src/mobile` into the `contributes.mobile.entry` file, and `validate` checks that it exists
|
|
22
|
+
- `@termix-ssh/plugin-sdk/frame-host` runs one plugin view outside the shell, with core's bridge, registries and CSS, so the mobile app can show `webview` plugins
|
|
23
|
+
- SSH transport providers get the host's owner, sync id, connection origin and local only flag in `claims` and `open`, and core's SSH relay takes a `syncId` instead of a host id
|
|
24
|
+
- `ctx.sync.host.entities.idsOf` maps sync ids to the local ids of the user's own rows of one type
|
|
25
|
+
- Rail items take `useStatus` for a small ok, warning or error dot on the icon
|
|
26
|
+
- `shell.editHost(hostId)` opens the editor of a saved host
|
|
27
|
+
- A sync entity can be read only with `load`, `write` and `erase`, so a plugin can sync rows a user does not own, like a shared library, down to linked desktops
|
|
28
|
+
- `getFolderAuthOverride` and `setFolderAuthOverride` in `@termix-ssh/plugin-sdk/ui` read and set a user's own login for the shared hosts in a folder
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- `@termix-ssh/plugin-sdk/ui` is built in production mode, so outside Termix it no longer sends requests to the Vite dev server path
|
|
33
|
+
- `execCommand` buffers output and decodes it once as strict UTF-8 with a GBK fallback, exposed as `decodeCommandOutput`, so messages from a Windows host on a legacy codepage are readable instead of � characters
|
|
34
|
+
|
|
3
35
|
## 1.0.21
|
|
4
36
|
|
|
5
37
|
Plugin API 1.12.0.
|
package/PLUGIN-GUIDE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Termix plugin guide
|
|
2
2
|
|
|
3
|
-
SDK 1.0.
|
|
3
|
+
SDK 1.0.23 | Plugin API 1.14.0
|
|
4
4
|
|
|
5
5
|
This guide is for anyone, person or AI coding tool, building a Termix plugin. It ships inside
|
|
6
6
|
`@termix-ssh/plugin-sdk`, so the copy in your `node_modules` always matches the SDK you have installed.
|
|
@@ -175,7 +175,49 @@ Unknown fields are refused at every level, so a typo fails validation. The schem
|
|
|
175
175
|
|
|
176
176
|
`contributes` holds `tabs`, `panels`, `dashboardCards`, `actions`, `actionSlots`, `permissions`,
|
|
177
177
|
`settings`, `protocols`, `auth`, `syncEntities`, `keybindingActions`, `http`, `guest`, `guestViews`,
|
|
178
|
-
`uiPresets`, `uses`, `layouts` and `
|
|
178
|
+
`uiPresets`, `uses`, `layouts`, `themes` and `mobile`. Frontend `register*` calls need their ids declared here first.
|
|
179
|
+
|
|
180
|
+
### Mobile app
|
|
181
|
+
|
|
182
|
+
`contributes.mobile` says how the plugin shows in the Termix mobile app. Leave it out and the app treats the
|
|
183
|
+
plugin as having nothing to show (official plugins are mapped by the app itself).
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
"mobile": { "mode": "webview", "views": ["hello"], "sizes": ["phone", "tablet"] }
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
| Field | Notes |
|
|
190
|
+
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
191
|
+
| `mode` | `webview` (the app shows your UI from the server), `native` (the app has its own screens) or `none`. |
|
|
192
|
+
| `entry` | `webview` only. A bundle made for the app, like `dist/mobile.js`, next to `frontend.js`. Unset: your normal frontend runs in a phone sized frame. |
|
|
193
|
+
| `sizes` | `webview` only. `phone`, `tablet` or both (the default). |
|
|
194
|
+
| `views` | `webview` only. Ids from your `tabs`, `panels` or `dashboardCards` the app offers. Default: every tab. |
|
|
195
|
+
| `feature` | `native` only. The app feature that stands in for the plugin. Default: the plugin id. |
|
|
196
|
+
| `note` | A locale key or plain text the app shows next to the plugin. |
|
|
197
|
+
|
|
198
|
+
`build` makes `entry` from `src/mobile/index.tsx`, and it shares `frontend.css`.
|
|
199
|
+
|
|
200
|
+
How a `webview` plugin runs in the app:
|
|
201
|
+
|
|
202
|
+
- Only your plugin loads, with the same app object, SDK hooks and `/ui` kit as the web app. Each view opens
|
|
203
|
+
on its own screen: a tab with `host-context-menu` in `openFrom` opens from a host, the rest from the app's
|
|
204
|
+
plugin list.
|
|
205
|
+
- Every request and socket goes through the app, which adds the login, proxy headers and client
|
|
206
|
+
certificate. The app only lets through your own `/plugin-api`, `/plugin-ws` and `/plugin-assets`, your
|
|
207
|
+
settings and a short list of core reads (hosts, folders, permissions, current user, version, health,
|
|
208
|
+
activity). Other core routes answer 403, so call your own backend instead.
|
|
209
|
+
- Images load straight from the server, without proxy headers or a client certificate.
|
|
210
|
+
- Nothing in `localStorage` survives closing the view. Keep preferences in settings.
|
|
211
|
+
- Other plugins' views, extensions and components are not there, and neither is the shell (`useShell()`
|
|
212
|
+
is null). `/ui` names newer than the app's copy of the SDK make the view say it needs a newer app.
|
|
213
|
+
- The app checks your `engine.api` against the plugin API of its own copy of the SDK. When the app's API
|
|
214
|
+
is too old for the range, it asks the user to update the app instead of opening the view. When the range
|
|
215
|
+
is for an older API major, it asks to update the plugin. Ask only for the API you use.
|
|
216
|
+
- Turning a plugin off on the server hides it in the app the next time the app checks: when it comes back
|
|
217
|
+
to the front, when the list is pulled down, or when any request answers 503 with your `pluginId`.
|
|
218
|
+
|
|
219
|
+
Termix 26.10.5 and older refuse any manifest with `mobile`, so a plugin that declares it needs
|
|
220
|
+
`"engine": { "termix": ">=26.10.6", "api": "^1.13" }`.
|
|
179
221
|
|
|
180
222
|
Capabilities and permissions are different things. A capability (`hosts:read`, with a colon) is what your
|
|
181
223
|
code may reach. A permission (`hello.use`, with a dot) is what a user may do.
|
|
@@ -304,7 +346,7 @@ router.post("/notes", async (req, res) => {
|
|
|
304
346
|
- `ctx.rbac.require("manage")` on a single route, `await ctx.rbac.has("manage")` in code. A full id like
|
|
305
347
|
`hosts.edit` checks a core permission.
|
|
306
348
|
- Host access: `ctx.hosts.checkAccess(hostId, "connect" | "view" | "edit" | "manage")`.
|
|
307
|
-
- Transports: with `ssh:transport`, `ctx.ssh.transports.provide({ claims(hostId), open(
|
|
349
|
+
- Transports: with `ssh:transport`, `ctx.ssh.transports.provide({ claims(hostId, host), open(host) })`, where `host` has `id`, `ip`, `port` and on Termix 26.10.6+ also `userId`, `syncId`, `connectionOrigin` and `localOnly`
|
|
308
350
|
returns a stream the SSH client uses instead of a direct, proxy or jump host connection. Core also serves
|
|
309
351
|
`/plugin-ws/core/ssh-relay?hostId=` for callers with connect access, so one Termix can relay to another.
|
|
310
352
|
- Managed hosts: with `hosts:lock`, `ctx.hosts.locks.provide({ isLocked, note, allowManualCreate })` makes
|
|
@@ -372,6 +414,70 @@ ctx.schedule.every(60_000, () => refresh(), { jitterMs: 5_000 });
|
|
|
372
414
|
- Use a service: declare it in `requires`, then `ctx.services.get<T>("snippets.access")`. It returns
|
|
373
415
|
nothing while no provider runs, so always handle that.
|
|
374
416
|
|
|
417
|
+
### Operations
|
|
418
|
+
|
|
419
|
+
Declare what your plugin can do in `contributes.operations`, and it shows up in Automations, Fleets, the
|
|
420
|
+
assistant, the CLI, the phone app and the command palette with no other wiring. It also gets per host results,
|
|
421
|
+
dry runs, run history, approvals and drift checks.
|
|
422
|
+
|
|
423
|
+
```json
|
|
424
|
+
"capabilities": ["operations:provide"],
|
|
425
|
+
"engine": { "termix": ">=26.11.1-0", "api": "^1.13" },
|
|
426
|
+
"contributes": {
|
|
427
|
+
"operations": {
|
|
428
|
+
"actions": [{
|
|
429
|
+
"id": "demo.restart",
|
|
430
|
+
"titleKey": "ops.restart.title",
|
|
431
|
+
"scope": "host",
|
|
432
|
+
"inputs": [{ "key": "name", "type": "string", "labelKey": "ops.restart.name", "required": true }],
|
|
433
|
+
"risk": "change",
|
|
434
|
+
"permission": "use",
|
|
435
|
+
"hostAccess": "edit",
|
|
436
|
+
"dryRun": "describe"
|
|
437
|
+
}]
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
ctx.operations.register("demo.restart", {
|
|
444
|
+
describe: (input) => `Would restart ${input.name}`,
|
|
445
|
+
run: async (input, { hostId, signal, log }) => {
|
|
446
|
+
return { summary: `Restarted ${input.name}`, changed: true };
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
- Actions: `scope` is `host`, `resource` or `server`. `risk` is `read`, `change` or `destructive`. Host actions declare
|
|
452
|
+
`hostAccess`. Use `edit` for changes unless people with `connect` already do this today. A destructive action
|
|
453
|
+
must use `edit` or `manage` and needs a confirm.
|
|
454
|
+
- Inputs use the settings field types, plus `required`, `pattern`, `optionsFrom` (a read action fills the choices)
|
|
455
|
+
and type `resource`. Keys you did not declare are refused before your handler runs. `secret` values reach the handler
|
|
456
|
+
but are never stored or shown. Mark private outputs `sensitive`.
|
|
457
|
+
- `dryRun`: `native` calls `run` with `dryRun: true` and nothing may change. `describe` calls `describe` instead.
|
|
458
|
+
- `check` names a read action that proves the result, used for drift. `undo` names the reverse action, and `run` returns
|
|
459
|
+
`undoInput`. `idempotent` lets a rollout retry it. `exclusive` allows one at a time per host. `when` limits it to
|
|
460
|
+
hosts by setting or OS.
|
|
461
|
+
- `surfaces`: the assistant and MCP are off until you set them true. Only do that for actions safe for a model to
|
|
462
|
+
ask for. The rest are on unless you set them false.
|
|
463
|
+
- Resources describe a kind of thing, such as a container: a read action on one host returns `{ rows }` as `data`, and
|
|
464
|
+
core draws the All hosts table and host page section. Attach actions with `scope: "resource"`.
|
|
465
|
+
- Signals are status tiles: a read action returns `{ value, status, label }` as `data`, with status `ok`, `warn`, `fail` or
|
|
466
|
+
`unknown`. `fix` names the action offered when it is not ok. Keep these reads cheap, they run for every host.
|
|
467
|
+
- Events: declare them, then call `ctx.operations.emit(id, payload, { hostId })`.
|
|
468
|
+
- Handlers must watch `signal` and stop when it fires. Throw on failure; a failed run never counts against your
|
|
469
|
+
plugin's error budget. Throw an error with `code = "NEEDS_INTERACTION"` when a host needs a person to sign in first.
|
|
470
|
+
- Handlers have no request and no session. Take what you need from the input and `hostId`.
|
|
471
|
+
- Keep your REST routes. Inside one, call `ctx.operations.run(id, { hostId, input })` so a button press goes through
|
|
472
|
+
the same checks and lands in the same history.
|
|
473
|
+
- To run another plugin's action, add `operations:invoke`, list it in `operations.invokes` and call
|
|
474
|
+
`ctx.operations.invoke(id, { hostId, input, via: "automation" })`. It acts as the current user and rules still apply.
|
|
475
|
+
- `risk` is your plugin's claim. Plugins run in-process, so this is not a sandbox. Actions from plugins not signed as
|
|
476
|
+
official count as `change` when rules decide what needs a person, and stay off the assistant and MCP until an admin
|
|
477
|
+
allows them.
|
|
478
|
+
- Set `engine.termix` to at least `>=26.11.1-0`. Keep `engine.api` as it was unless you import something new from
|
|
479
|
+
`/ui`, or the phone app will ask people to update to see your plugin.
|
|
480
|
+
|
|
375
481
|
### Shared packages
|
|
376
482
|
|
|
377
483
|
These come from Termix and are never bundled: `@termix-ssh/plugin-sdk`, `express`, `ssh2`, `ws`, `multer`,
|
|
@@ -453,6 +559,23 @@ Other storage: `ctx.kv` for a few values, `ctx.settings` for settings, `ctx.secr
|
|
|
453
559
|
`ctx.files.dataDir()` for files. To sync a table to a linked desktop, use `ctx.sync.registerEntity` and
|
|
454
560
|
`contributes.syncEntities`. The entity name is part of the protocol and never changes.
|
|
455
561
|
|
|
562
|
+
A registered table syncs the rows each user owns, both ways. For data you keep for the whole server (a shared
|
|
563
|
+
library, rows other users share), register a read only entity with `table: null`, `readOnly: true` and `load`,
|
|
564
|
+
`write` and `erase` (Termix 26.10.6+). `load(userId)` returns the rows that user should get, each with a
|
|
565
|
+
`syncId`. The desktop keeps them with `write` and drops them with `erase`, and never pushes them back:
|
|
566
|
+
|
|
567
|
+
```ts
|
|
568
|
+
ctx.sync.registerEntity({
|
|
569
|
+
type: "sharedIcons",
|
|
570
|
+
table: null,
|
|
571
|
+
readOnly: true,
|
|
572
|
+
order: 70,
|
|
573
|
+
load: (userId) => iconsVisibleTo(userId),
|
|
574
|
+
write: (userId, row) => storeCopy(userId, row),
|
|
575
|
+
erase: (userId, syncId) => dropCopy(userId, syncId),
|
|
576
|
+
});
|
|
577
|
+
```
|
|
578
|
+
|
|
456
579
|
## Frontend
|
|
457
580
|
|
|
458
581
|
```tsx
|
|
@@ -903,6 +1026,7 @@ To run your own registry instead, see https://docs.termix.site/develop/registrie
|
|
|
903
1026
|
| `@termix-ssh/plugin-sdk/db` | `defineTable` and column builders |
|
|
904
1027
|
| `@termix-ssh/plugin-sdk/manifest` | `PluginManifest` type and manifest checks |
|
|
905
1028
|
| `@termix-ssh/plugin-sdk/capabilities` | The capability catalog |
|
|
1029
|
+
| `@termix-ssh/plugin-sdk/operations` | Types, input checks and JSON Schema for declared actions |
|
|
906
1030
|
| `@termix-ssh/plugin-sdk/settings` | Settings field validation |
|
|
907
1031
|
| `@termix-ssh/plugin-sdk/testing` | `createMockCtx`, `createTestDb`, `renderWithApp`, `createFakeContext` |
|
|
908
1032
|
| `@termix-ssh/plugin-sdk/vitest-preset` | `pluginVitestConfig` |
|
|
@@ -934,6 +1058,8 @@ To run your own registry instead, see https://docs.termix.site/develop/registrie
|
|
|
934
1058
|
| `desktop:local` | high | Use the user's own computer, desktop app only |
|
|
935
1059
|
| `hosts:export` | high | Export hosts with their saved logins |
|
|
936
1060
|
| `notify:hub` | high | Receive every alert (the alerts hub only) |
|
|
1061
|
+
| `operations:invoke` | high | Run other plugins' declared actions, listed in `invokes` |
|
|
1062
|
+
| `operations:approve` | high | Decide which actions need a person |
|
|
937
1063
|
| `ui:layout` | high | Replace the whole app layout |
|
|
938
1064
|
| `hosts:write` | medium | Add and edit hosts |
|
|
939
1065
|
| `hosts:status-provide` | medium | Be the source of host status dots |
|
|
@@ -949,6 +1075,7 @@ To run your own registry instead, see https://docs.termix.site/develop/registrie
|
|
|
949
1075
|
| `users:read` | medium | Usernames, roles and UI language |
|
|
950
1076
|
| `events:core` | medium | Listen to core events |
|
|
951
1077
|
| `notify:send` | medium | Send alerts |
|
|
1078
|
+
| `operations:provide` | medium | Offer declared actions and events |
|
|
952
1079
|
| `audit:read` | medium | Read the audit log |
|
|
953
1080
|
| `desktop:window` | medium | Open windows in the desktop app |
|
|
954
1081
|
| `hosts:read` | low | Read hosts the user can see |
|
package/cli/commands/build.mjs
CHANGED
|
@@ -25,6 +25,43 @@ const FRONTEND_ENTRIES = [
|
|
|
25
25
|
"src/frontend/index.mjs",
|
|
26
26
|
"src/frontend/index.js",
|
|
27
27
|
];
|
|
28
|
+
const MOBILE_ENTRIES = [
|
|
29
|
+
"src/mobile/index.tsx",
|
|
30
|
+
"src/mobile/index.ts",
|
|
31
|
+
"src/mobile/index.mjs",
|
|
32
|
+
"src/mobile/index.js",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/** Builds a bundle that runs in a browser against core's shared modules. */
|
|
36
|
+
async function buildBrowserBundle(entry, outfile, outDir) {
|
|
37
|
+
await esbuild.build({
|
|
38
|
+
entryPoints: [entry],
|
|
39
|
+
outfile,
|
|
40
|
+
bundle: true,
|
|
41
|
+
format: "esm",
|
|
42
|
+
platform: "browser",
|
|
43
|
+
target: "es2022",
|
|
44
|
+
jsx: "automatic",
|
|
45
|
+
sourcemap: true,
|
|
46
|
+
logLevel: "warning",
|
|
47
|
+
external: FRONTEND_EXTERNALS,
|
|
48
|
+
// Vite replaces these in the dev server; a plugin bundle has to have
|
|
49
|
+
// them too, or code that reads them throws when the plugin loads.
|
|
50
|
+
define: {
|
|
51
|
+
"process.env.NODE_ENV": '"production"',
|
|
52
|
+
"import.meta.env.DEV": "false",
|
|
53
|
+
"import.meta.env.PROD": "true",
|
|
54
|
+
"import.meta.env.MODE": '"production"',
|
|
55
|
+
"import.meta.env.SSR": "false",
|
|
56
|
+
"import.meta.env":
|
|
57
|
+
'{"DEV":false,"PROD":true,"MODE":"production","SSR":false,"BASE_URL":"/"}',
|
|
58
|
+
},
|
|
59
|
+
plugins: [
|
|
60
|
+
externalRequireInterop(FRONTEND_EXTERNALS),
|
|
61
|
+
staticUrlImports({ outDir }),
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
}
|
|
28
65
|
|
|
29
66
|
/**
|
|
30
67
|
* A bundled CommonJS package that calls require("react/jsx-runtime") (or any
|
|
@@ -192,15 +229,12 @@ export async function buildTailwind(cwd, outDir) {
|
|
|
192
229
|
// build() adds to what a compiler already emitted, so each half gets its own.
|
|
193
230
|
const compiler = () =>
|
|
194
231
|
compile(input, { base: path.dirname(THEME_CSS), onDependency: () => {} });
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
},
|
|
202
|
-
],
|
|
203
|
-
});
|
|
232
|
+
// The mobile bundle uses the same stylesheet.
|
|
233
|
+
const sources = ["frontend", "mobile"]
|
|
234
|
+
.map((dir) => path.join(cwd, "src", dir))
|
|
235
|
+
.filter((dir, index) => index === 0 || fs.existsSync(dir))
|
|
236
|
+
.map((base) => ({ base, pattern: "**/*", negated: false }));
|
|
237
|
+
const scanner = new Scanner({ sources });
|
|
204
238
|
// Flattened and minified the way core's own CSS is, so it does not rely
|
|
205
239
|
// on native CSS nesting.
|
|
206
240
|
const emit = async (candidates, options) =>
|
|
@@ -297,33 +331,18 @@ export async function build({ cwd }) {
|
|
|
297
331
|
|
|
298
332
|
const frontendEntry = resolveEntry(cwd, FRONTEND_ENTRIES);
|
|
299
333
|
if (frontendEntry) {
|
|
300
|
-
await
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
// them too, or code that reads them throws when the plugin loads.
|
|
313
|
-
define: {
|
|
314
|
-
"process.env.NODE_ENV": '"production"',
|
|
315
|
-
"import.meta.env.DEV": "false",
|
|
316
|
-
"import.meta.env.PROD": "true",
|
|
317
|
-
"import.meta.env.MODE": '"production"',
|
|
318
|
-
"import.meta.env.SSR": "false",
|
|
319
|
-
"import.meta.env":
|
|
320
|
-
'{"DEV":false,"PROD":true,"MODE":"production","SSR":false,"BASE_URL":"/"}',
|
|
321
|
-
},
|
|
322
|
-
plugins: [
|
|
323
|
-
externalRequireInterop(FRONTEND_EXTERNALS),
|
|
324
|
-
staticUrlImports({ outDir }),
|
|
325
|
-
],
|
|
326
|
-
});
|
|
334
|
+
await buildBrowserBundle(
|
|
335
|
+
frontendEntry,
|
|
336
|
+
path.join(outDir, "frontend.js"),
|
|
337
|
+
outDir,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// A bundle the mobile app loads in its WebView, next to the frontend one.
|
|
342
|
+
const mobileEntry = resolveEntry(cwd, MOBILE_ENTRIES);
|
|
343
|
+
const mobileOut = manifest.contributes?.mobile?.entry;
|
|
344
|
+
if (mobileEntry && typeof mobileOut === "string") {
|
|
345
|
+
await buildBrowserBundle(mobileEntry, path.join(cwd, mobileOut), outDir);
|
|
327
346
|
}
|
|
328
347
|
|
|
329
348
|
if (frontendEntry) await buildTailwind(cwd, outDir);
|
|
@@ -28,6 +28,10 @@ export async function validate({ cwd }) {
|
|
|
28
28
|
// A theme only plugin has no frontend code.
|
|
29
29
|
...(isThemeOnly(raw) ? [] : [raw.frontend ?? "dist/frontend.js"]),
|
|
30
30
|
raw.locales ?? "locales",
|
|
31
|
+
// Built from src/mobile by termix-plugin build.
|
|
32
|
+
...(typeof raw.contributes?.mobile?.entry === "string"
|
|
33
|
+
? [raw.contributes.mobile.entry]
|
|
34
|
+
: []),
|
|
31
35
|
];
|
|
32
36
|
for (const rel of referenced) {
|
|
33
37
|
if (!fs.existsSync(path.join(cwd, rel))) {
|
package/dist/backend.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { HostSshOptions } from "./ssh-options.js";
|
|
|
13
13
|
export type { HostSshOptions } from "./ssh-options.js";
|
|
14
14
|
import type { PluginManifest, PluginSettingsField } from "./manifest.js";
|
|
15
15
|
import type { PluginTableDefinition } from "./db.js";
|
|
16
|
+
import type { OperationResult, OperationRisk, OperationSurfaces } from "./operations.js";
|
|
16
17
|
/**
|
|
17
18
|
* Thrown when a plugin calls a guarded member without the capability.
|
|
18
19
|
*
|
|
@@ -149,8 +150,24 @@ export interface SyncWriteEvent {
|
|
|
149
150
|
export interface SyncEntityRegistration {
|
|
150
151
|
/** Stable wire name. Never change it: records on both sides match on it. */
|
|
151
152
|
type: string;
|
|
152
|
-
/** The table, as returned by ctx.db.define. */
|
|
153
|
+
/** The table, as returned by ctx.db.define. Null for an entity with `load`. */
|
|
153
154
|
table: unknown;
|
|
155
|
+
/**
|
|
156
|
+
* Server to desktop only: a desktop never pushes these rows. Required with
|
|
157
|
+
* `load`.
|
|
158
|
+
*/
|
|
159
|
+
readOnly?: boolean;
|
|
160
|
+
/**
|
|
161
|
+
* Builds the rows the user gets instead of reading the rows they own in
|
|
162
|
+
* `table`, for data kept for the whole server (a shared library, rows other
|
|
163
|
+
* users share with them). Rows are on the wire: plaintext, each with a
|
|
164
|
+
* syncId. Needs `readOnly: true`, `write` and `erase`.
|
|
165
|
+
*/
|
|
166
|
+
load?: (userId: string) => Promise<SyncRow[]>;
|
|
167
|
+
/** Keeps a copy of an inbound row on the desktop, for an entity with `load`. */
|
|
168
|
+
write?: (userId: string, wire: SyncRow) => Promise<void>;
|
|
169
|
+
/** Drops the desktop copy with this syncId, for an entity with `load`. */
|
|
170
|
+
erase?: (userId: string, syncId: string) => Promise<void>;
|
|
154
171
|
/** Column holding the owning user id. Defaults to "userId". */
|
|
155
172
|
userColumn?: string;
|
|
156
173
|
/** Fields stored encrypted with the owner's data key. Plaintext on the wire. */
|
|
@@ -391,6 +408,8 @@ export interface PluginSyncHost {
|
|
|
391
408
|
ensureCore: () => Promise<void>;
|
|
392
409
|
onChanged: (listener: () => void) => () => void;
|
|
393
410
|
onLocalWrite: (listener: () => void) => () => void;
|
|
411
|
+
/** Local ids of the user's own rows of a type, by sync id. Termix 26.10.6+. */
|
|
412
|
+
idsOf?: (type: string, userId: string, syncIds: string[]) => Promise<Record<string, number>>;
|
|
394
413
|
};
|
|
395
414
|
/** Rows and bookkeeping of one local user's replica. */
|
|
396
415
|
replica: {
|
|
@@ -1503,13 +1522,20 @@ export interface PluginNetwork {
|
|
|
1503
1522
|
* asked for every connection, so it must be quick. `open` returns a stream
|
|
1504
1523
|
* the SSH client runs over, like a relayed socket.
|
|
1505
1524
|
*/
|
|
1525
|
+
/** The host a transport is asked about. Fields past port come from Termix 26.10.6. */
|
|
1526
|
+
export interface PluginSshTransportHost {
|
|
1527
|
+
id: number;
|
|
1528
|
+
ip: string;
|
|
1529
|
+
port: number;
|
|
1530
|
+
/** Owner of the host row. */
|
|
1531
|
+
userId?: string | null;
|
|
1532
|
+
syncId?: string | null;
|
|
1533
|
+
connectionOrigin?: "local" | "remote" | null;
|
|
1534
|
+
localOnly?: boolean;
|
|
1535
|
+
}
|
|
1506
1536
|
export interface PluginSshTransportProvider {
|
|
1507
|
-
claims: (hostId: number) => boolean;
|
|
1508
|
-
open: (host:
|
|
1509
|
-
id: number;
|
|
1510
|
-
ip: string;
|
|
1511
|
-
port: number;
|
|
1512
|
-
}) => Promise<import("node:stream").Duplex>;
|
|
1537
|
+
claims: (hostId: number, host?: PluginSshTransportHost) => boolean;
|
|
1538
|
+
open: (host: PluginSshTransportHost) => Promise<import("node:stream").Duplex>;
|
|
1513
1539
|
}
|
|
1514
1540
|
export interface PluginSshTransports {
|
|
1515
1541
|
/** Needs ssh:transport. Removed on deactivate. */
|
|
@@ -2655,6 +2681,110 @@ export interface PluginSummary {
|
|
|
2655
2681
|
export interface PluginPlugins {
|
|
2656
2682
|
list: () => Promise<PluginSummary[]>;
|
|
2657
2683
|
}
|
|
2684
|
+
/** What a handler gets besides its input. */
|
|
2685
|
+
export interface PluginOperationContext {
|
|
2686
|
+
hostId?: number;
|
|
2687
|
+
/** The row a resource action targets. */
|
|
2688
|
+
resourceKey?: string;
|
|
2689
|
+
/** Null for a background read. */
|
|
2690
|
+
runId: string | null;
|
|
2691
|
+
/** Nothing may change when this is set. */
|
|
2692
|
+
dryRun: boolean;
|
|
2693
|
+
/** Fires when the run is cancelled, times out or the plugin stops. */
|
|
2694
|
+
signal: AbortSignal;
|
|
2695
|
+
log: (line: string) => void;
|
|
2696
|
+
}
|
|
2697
|
+
export interface PluginOperationHandler {
|
|
2698
|
+
run: (input: Record<string, unknown>, context: PluginOperationContext) => Promise<OperationResult> | OperationResult;
|
|
2699
|
+
/** The dry run text for an action declared with dryRun "describe". */
|
|
2700
|
+
describe?: (input: Record<string, unknown>, context: PluginOperationContext) => string | Promise<string>;
|
|
2701
|
+
}
|
|
2702
|
+
export interface PluginOperationInvokeOptions {
|
|
2703
|
+
hostId?: number;
|
|
2704
|
+
resourceKey?: string;
|
|
2705
|
+
input?: Record<string, unknown>;
|
|
2706
|
+
dryRun?: boolean;
|
|
2707
|
+
/** A person agreed to a destructive action. */
|
|
2708
|
+
confirmed?: boolean;
|
|
2709
|
+
idempotencyKey?: string;
|
|
2710
|
+
batchId?: string;
|
|
2711
|
+
/** What this plugin is doing: automation, assistant, mcp, rollout and so on. */
|
|
2712
|
+
via?: string;
|
|
2713
|
+
}
|
|
2714
|
+
export interface PluginOperationOutcome {
|
|
2715
|
+
runId: string | null;
|
|
2716
|
+
status: string;
|
|
2717
|
+
summary?: string;
|
|
2718
|
+
changed?: boolean;
|
|
2719
|
+
data?: unknown;
|
|
2720
|
+
error?: string;
|
|
2721
|
+
errorCode?: string;
|
|
2722
|
+
approvalId?: string;
|
|
2723
|
+
}
|
|
2724
|
+
export interface PluginOperationApprovalInput {
|
|
2725
|
+
actionId: string;
|
|
2726
|
+
pluginId: string;
|
|
2727
|
+
risk: OperationRisk;
|
|
2728
|
+
/** What rules should use. Plugins not signed as official count as change. */
|
|
2729
|
+
effectiveRisk: OperationRisk;
|
|
2730
|
+
origin: {
|
|
2731
|
+
kind: string;
|
|
2732
|
+
pluginId?: string;
|
|
2733
|
+
via?: string;
|
|
2734
|
+
};
|
|
2735
|
+
userId: string;
|
|
2736
|
+
hostId?: number;
|
|
2737
|
+
hostTags?: string[];
|
|
2738
|
+
batchId?: string;
|
|
2739
|
+
}
|
|
2740
|
+
export interface PluginOperationApprovalDecision {
|
|
2741
|
+
decision: "allow" | "approve" | "deny";
|
|
2742
|
+
reason?: string;
|
|
2743
|
+
allowSelf?: boolean;
|
|
2744
|
+
expiresInMinutes?: number;
|
|
2745
|
+
}
|
|
2746
|
+
export interface PluginOperationEvent {
|
|
2747
|
+
id: string;
|
|
2748
|
+
eventId: string;
|
|
2749
|
+
pluginId: string;
|
|
2750
|
+
hostId?: number;
|
|
2751
|
+
severity?: string;
|
|
2752
|
+
payload?: unknown;
|
|
2753
|
+
runId?: string;
|
|
2754
|
+
depth: number;
|
|
2755
|
+
at: string;
|
|
2756
|
+
}
|
|
2757
|
+
/**
|
|
2758
|
+
* Declared actions and events. See contributes.operations in the manifest.
|
|
2759
|
+
* register and emit need operations:provide. invoke, list and onEvent need
|
|
2760
|
+
* operations:invoke, and invoke only reaches ids in the manifest's invokes list.
|
|
2761
|
+
*/
|
|
2762
|
+
export interface PluginOperations {
|
|
2763
|
+
register: (id: string, handler: PluginOperationHandler) => void;
|
|
2764
|
+
/**
|
|
2765
|
+
* Runs one of this plugin's own actions from its route, so a button press
|
|
2766
|
+
* goes through the same checks and lands in the same history.
|
|
2767
|
+
*/
|
|
2768
|
+
run: (actionId: string, options?: PluginOperationInvokeOptions) => Promise<PluginOperationOutcome>;
|
|
2769
|
+
invoke: (actionId: string, options?: PluginOperationInvokeOptions) => Promise<PluginOperationOutcome>;
|
|
2770
|
+
list: (options?: {
|
|
2771
|
+
lang?: string;
|
|
2772
|
+
surface?: keyof OperationSurfaces;
|
|
2773
|
+
}) => Promise<unknown>;
|
|
2774
|
+
emit: (eventId: string, payload?: unknown, options?: {
|
|
2775
|
+
hostId?: number;
|
|
2776
|
+
}) => Promise<void>;
|
|
2777
|
+
onEvent: (subscription: {
|
|
2778
|
+
pattern: string;
|
|
2779
|
+
handler: (event: PluginOperationEvent) => void | Promise<void>;
|
|
2780
|
+
userId?: string;
|
|
2781
|
+
cooldownMs?: number;
|
|
2782
|
+
}) => () => void;
|
|
2783
|
+
/** Rules for what needs a person. Needs operations:approve. */
|
|
2784
|
+
approvals: {
|
|
2785
|
+
provide: (provider: (input: PluginOperationApprovalInput) => PluginOperationApprovalDecision | null | undefined | Promise<PluginOperationApprovalDecision | null | undefined>) => () => void;
|
|
2786
|
+
};
|
|
2787
|
+
}
|
|
2658
2788
|
export interface PluginContext {
|
|
2659
2789
|
readonly pluginId: string;
|
|
2660
2790
|
readonly manifest: PluginManifest;
|
|
@@ -2695,6 +2825,8 @@ export interface PluginContext {
|
|
|
2695
2825
|
readonly schedule: PluginSchedule;
|
|
2696
2826
|
/** Core's notification channels. Needs notify:send. */
|
|
2697
2827
|
readonly notify: PluginNotify;
|
|
2828
|
+
/** Declared actions and events, reachable from every surface. */
|
|
2829
|
+
readonly operations: PluginOperations;
|
|
2698
2830
|
/** Outbound HTTP through core's SSRF guard. Needs network:outbound. */
|
|
2699
2831
|
readonly fetch: PluginFetch;
|
|
2700
2832
|
/** Plain reachability checks. Needs network:probe. */
|
package/dist/capabilities.js
CHANGED
|
@@ -38,6 +38,8 @@ export const CAPABILITY_CATALOG = [
|
|
|
38
38
|
entry("ssh:transport", "high"),
|
|
39
39
|
entry("device:serial", "high"),
|
|
40
40
|
entry("notify:hub", "high"),
|
|
41
|
+
entry("operations:invoke", "high"),
|
|
42
|
+
entry("operations:approve", "high"),
|
|
41
43
|
entry("ui:layout", "high"),
|
|
42
44
|
entry("hosts:write", "medium"),
|
|
43
45
|
entry("hosts:export", "high"),
|
|
@@ -53,6 +55,7 @@ export const CAPABILITY_CATALOG = [
|
|
|
53
55
|
entry("users:read", "medium"),
|
|
54
56
|
entry("events:core", "medium"),
|
|
55
57
|
entry("notify:send", "medium"),
|
|
58
|
+
entry("operations:provide", "medium"),
|
|
56
59
|
entry("audit:read", "medium"),
|
|
57
60
|
entry("desktop:window", "medium"),
|
|
58
61
|
entry("desktop:local", "high"),
|
package/dist/frontend.d.ts
CHANGED
|
@@ -185,6 +185,8 @@ export interface ShellApi {
|
|
|
185
185
|
* user reviews and saves it. Absent in some shells.
|
|
186
186
|
*/
|
|
187
187
|
openHostEditor?: (draft?: HostDraft) => void;
|
|
188
|
+
/** Opens the editor of a saved host. Termix 26.10.6+, absent in some shells. */
|
|
189
|
+
editHost?: (hostId: number) => void;
|
|
188
190
|
/** Opens a rail view in the left sidebar. */
|
|
189
191
|
openRailView: (id: string) => void;
|
|
190
192
|
/** Closes a rail view wherever it is shown. */
|
|
@@ -412,6 +414,11 @@ export interface RailItemContribution {
|
|
|
412
414
|
* Zero or nothing shows no badge.
|
|
413
415
|
*/
|
|
414
416
|
useBadge?: () => number | null | undefined;
|
|
417
|
+
/**
|
|
418
|
+
* A hook for a small status dot on the icon: "ok" (green), "warning" or
|
|
419
|
+
* "error". Nothing shows no dot. Layouts from Termix 26.10.6 draw it.
|
|
420
|
+
*/
|
|
421
|
+
useStatus?: () => "ok" | "warning" | "error" | null | undefined;
|
|
415
422
|
}
|
|
416
423
|
export interface PanelProps {
|
|
417
424
|
/**
|