@termix-ssh/plugin-sdk 1.0.11 → 1.0.12
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 +21 -0
- package/PLUGIN-GUIDE.md +95 -3
- package/cli/commands/build.mjs +36 -22
- package/cli/commands/pack.mjs +12 -2
- package/cli/commands/validate.mjs +63 -3
- package/cli/lib/plugin-dir.mjs +11 -0
- package/cli/lib/tmxplug.mjs +9 -0
- package/dist/capabilities.js +2 -0
- package/dist/frontend.d.ts +152 -0
- package/dist/frontend.js +7 -0
- package/dist/host/PluginsTab-EV4MSGY6.js +2933 -0
- package/dist/host/{chunk-RLQBKJRK.js → chunk-AWR6FYDO.js} +8873 -11163
- package/dist/host/{chunk-7RRTYD6B.js → chunk-RUPLD6RF.js} +1497 -15
- package/dist/host/{chunk-LSWB3ZZV.js → chunk-VEWOYAMZ.js} +6 -1
- package/dist/host/chunk-X4QZKO5G.js +3120 -0
- package/dist/host/chunk-XKA3DBB2.js +10375 -0
- package/dist/host/chunk-YGYBM7SQ.js +58 -0
- package/dist/host/core-classes.json +1 -1
- package/dist/host/{linked-server-UCW74T2V.js → linked-server-B4NEG4MA.js} +2 -2
- package/dist/host/{rbac-api-6UYMFIFV.js → rbac-api-OUO3LUOC.js} +2 -2
- package/dist/host/testing-host.js +123 -57
- package/dist/host/types/src/types/ui-types.d.ts +2 -1
- package/dist/host/types/src/ui/api/audit-log-api.d.ts +46 -0
- package/dist/host/types/src/ui/api/host-defaults-api.d.ts +42 -0
- package/dist/host/types/src/ui/api/host-tags-api.d.ts +2 -0
- package/dist/host/types/src/ui/api/look-api.d.ts +40 -0
- package/dist/host/types/src/ui/api/open-tabs-api.d.ts +4 -0
- package/dist/host/types/src/ui/api/plugins-api.d.ts +26 -0
- package/dist/host/types/src/ui/api/private-endpoints-api.d.ts +6 -0
- package/dist/host/types/src/ui/api/tls-api.d.ts +25 -0
- package/dist/host/types/src/ui/auth/ElectronLoginForm.d.ts +9 -0
- package/dist/host/types/src/ui/components/command.d.ts +78 -0
- package/dist/host/types/src/ui/components/folder-style.d.ts +16 -0
- package/dist/host/types/src/ui/components/theme-provider.d.ts +2 -1
- package/dist/host/types/src/ui/components/version-badge.d.ts +7 -0
- package/dist/host/types/src/ui/hooks/use-page-visible-interval.d.ts +10 -0
- package/dist/host/types/src/ui/lib/activity-types.d.ts +15 -0
- package/dist/host/types/src/ui/lib/app-version.d.ts +2 -0
- package/dist/host/types/src/ui/lib/apply-ui-preset.d.ts +19 -0
- package/dist/host/types/src/ui/lib/create-id.d.ts +2 -0
- package/dist/host/types/src/ui/lib/database-transfer-url.d.ts +8 -0
- package/dist/host/types/src/ui/lib/desktop-updates.d.ts +29 -0
- package/dist/host/types/src/ui/lib/host-connection-tabs.d.ts +14 -0
- package/dist/host/types/src/ui/lib/host-to-ssh-host.d.ts +4 -0
- package/dist/host/types/src/ui/lib/issue-url.d.ts +21 -0
- package/dist/host/types/src/ui/lib/local-adaptive-preferences.d.ts +9 -0
- package/dist/host/types/src/ui/lib/pane-nav-hotkey.d.ts +7 -0
- package/dist/host/types/src/ui/lib/plugin-i18n.d.ts +4 -3
- package/dist/host/types/src/ui/lib/role-label.d.ts +3 -0
- package/dist/host/types/src/ui/look/LookSettings.d.ts +8 -0
- package/dist/host/types/src/ui/look/layout-registry.d.ts +34 -0
- package/dist/host/types/src/ui/look/look-store.d.ts +29 -0
- package/dist/host/types/src/ui/look/resolve-look.d.ts +18 -0
- package/dist/host/types/src/ui/look/theme-css.d.ts +7 -0
- package/dist/host/types/src/ui/look/theme-registry.d.ts +41 -0
- package/dist/host/types/src/ui/look/use-look-theme.d.ts +12 -0
- package/dist/host/types/src/ui/manage/manage-requests.d.ts +34 -0
- package/dist/host/types/src/ui/plugin-host/loader.d.ts +2 -0
- package/dist/host/types/src/ui/plugin-host/scope.d.ts +6 -0
- package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +102 -0
- package/dist/host/types/src/ui/plugin-host/view-ownership.d.ts +1 -1
- package/dist/host/types/src/ui/plugins/PluginConsentPrompt.d.ts +20 -0
- package/dist/host/types/src/ui/plugins/PluginDetail.d.ts +7 -0
- package/dist/host/types/src/ui/plugins/PluginReleaseNotes.d.ts +14 -0
- package/dist/host/types/src/ui/plugins/PluginsTab.d.ts +1 -0
- package/dist/host/types/src/ui/plugins/RegistriesSection.d.ts +4 -0
- package/dist/host/types/src/ui/plugins/plugin-bits.d.ts +23 -0
- package/dist/host/types/src/ui/plugins/plugin-model.d.ts +116 -0
- package/dist/host/types/src/ui/plugins/use-plugins-manager.d.ts +75 -0
- package/dist/host/types/src/ui/settings/appearance/appearance-controls.d.ts +53 -0
- package/dist/host/types/src/ui/settings/sync/LinkServerDialog.d.ts +12 -0
- package/dist/host/types/src/ui/settings/sync/ProxySettingsFields.d.ts +15 -0
- package/dist/host/types/src/ui/settings/sync/saved-servers.d.ts +3 -0
- package/dist/host/types/src/ui/shell/controller/ShellControllerProvider.d.ts +20 -0
- package/dist/host/types/src/ui/shell/controller/plugin-shell.d.ts +7 -0
- package/dist/host/types/src/ui/shell/controller/shell-store.d.ts +4 -0
- package/dist/host/types/src/ui/shell/controller/types.d.ts +156 -0
- package/dist/host/types/src/ui/shell/controller/use-hosts.d.ts +10 -0
- package/dist/host/types/src/ui/shell/controller/use-regions.d.ts +5 -0
- package/dist/host/types/src/ui/shell/controller/use-shell-keybindings.d.ts +12 -0
- package/dist/host/types/src/ui/shell/controller/use-shell-lifecycle.d.ts +17 -0
- package/dist/host/types/src/ui/shell/controller/use-split.d.ts +21 -0
- package/dist/host/types/src/ui/shell/controller/use-tabs.d.ts +70 -0
- package/dist/host/types/src/ui/shell/controller/use-workspace-layout.d.ts +4 -0
- package/dist/host/types/src/ui/shell/reconnect-tabs.d.ts +6 -0
- package/dist/host/types/src/ui/shell/shell-layout.d.ts +46 -0
- package/dist/host/types/src/ui/shell/split/EmptyPanePicker.d.ts +32 -0
- package/dist/host/types/src/ui/shell/split/SplitDropOverlay.d.ts +9 -0
- package/dist/host/types/src/ui/shell/split/SplitLayoutMenu.d.ts +16 -0
- package/dist/host/types/src/ui/shell/split/SplitView.d.ts +31 -0
- package/dist/host/types/src/ui/shell/split/split-drag.d.ts +58 -0
- package/dist/host/types/src/ui/shell/split/split-presets.d.ts +28 -0
- package/dist/host/types/src/ui/shell/split/split-tabs.d.ts +95 -0
- package/dist/host/types/src/ui/shell/split/split-targets.d.ts +30 -0
- package/dist/host/types/src/ui/shell/split/split-tree.d.ts +89 -0
- package/dist/host/types/src/ui/shell/stage/TabStage.d.ts +9 -0
- package/dist/host/types/src/ui/shell/stage/tab-stage-store.d.ts +24 -0
- package/dist/host/types/src/ui/shell/tab-close-guards.d.ts +9 -0
- package/dist/host/types/src/ui/shell/tab-handles.d.ts +8 -0
- package/dist/host/types/src/ui/shell/tabUtils.d.ts +15 -0
- package/dist/host/types/src/ui/sidebar/AuthEnrollmentSections.d.ts +5 -0
- package/dist/host/types/src/ui/sidebar/HostManagerData.d.ts +4 -0
- package/dist/host/types/src/ui/sidebar/build-host-tree.d.ts +25 -0
- package/dist/host/types/src/ui/sidebar/host-export-payload.d.ts +30 -0
- package/dist/host/ui.js +6236 -4055
- package/dist/host/{use-permissions-GYCBY2AU.js → use-permissions-6VSHHUMB.js} +2 -2
- package/dist/manifest.d.ts +41 -1
- package/dist/manifest.js +89 -1
- package/dist/testing/setup.js +1 -0
- package/dist/theme-css.d.ts +11 -0
- package/dist/theme-css.js +142 -0
- package/dist/vitest-preset.d.ts +18 -0
- package/dist/vitest-preset.js +14 -3
- package/package.json +1 -1
- package/schema/manifest.schema.json +111 -8
- package/tsconfig.plugin.json +1 -0
- package/dist/host/chunk-YC2MAKSP.js +0 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.12
|
|
4
|
+
|
|
5
|
+
Plugin API 1.4.0.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `contributes.layouts` and `app.registerLayout()` for a whole-app layout, with the `ui:layout` capability. Needs `engine.api` 1.4
|
|
10
|
+
- `contributes.themes` for CSS-only themes, with the `ui:theme` capability. `termix-plugin validate` checks theme CSS and `pack` ships it
|
|
11
|
+
- `useShell()` gives a layout the live shell: tabs, splits, hosts, docks, the command palette and the registries. The shell's home tab is left out until some plugin registers a `dashboard` tab type
|
|
12
|
+
- `TabStage` and `ShellConfirmCapture` in `@termix-ssh/plugin-sdk/ui`, for a layout to place tabs
|
|
13
|
+
- `validateThemeCss()` in `@termix-ssh/plugin-sdk/manifest`
|
|
14
|
+
- `@termix-ssh/plugin-sdk/ui` also exports what a whole layout is built from: the API client, shared types, hooks, `useShellController()` and `APP_VERSION`. These reach into core, so they can change with a core release
|
|
15
|
+
- `@termix-ssh/plugin-sdk/testing-host` exports the registries and permission seam a layout's own tests fill directly
|
|
16
|
+
- A plugin that only holds themes needs no source: `termix-plugin build` writes an empty backend and `validate` does not ask for a frontend
|
|
17
|
+
- `termix-plugin validate` flags `ui:layout` or `ui:theme` with nothing contributed, and a layout plugin with no frontend
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- The manifest JSON schema lists the `notify:hub` and `plugins:read` capabilities
|
|
22
|
+
- Plugin tests and type checks work with the SDK linked from a local Termix checkout
|
|
23
|
+
|
|
3
24
|
## 1.0.11
|
|
4
25
|
|
|
5
26
|
Plugin API 1.3.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.12 | Plugin API 1.4.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,7 @@ 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` and `
|
|
178
|
+
`uiPresets`, `uses`, `layouts` and `themes`. Frontend `register*` calls need their ids declared here first.
|
|
179
179
|
|
|
180
180
|
Capabilities and permissions are different things. A capability (`hosts:read`, with a colon) is what your
|
|
181
181
|
code may reach. A permission (`hello.use`, with a dot) is what a user may do.
|
|
@@ -478,6 +478,7 @@ your plugin's error boundary and Suspense.
|
|
|
478
478
|
| `registerComponent`, `registerExtension` | Offer a component by id, add to an extension point |
|
|
479
479
|
| `registerOnboardingStep` | A first run setup step |
|
|
480
480
|
| `registerLoginMethod`, `registerSecondFactorUI`, `registerSshAuthEditor` | Sign in UI |
|
|
481
|
+
| `registerLayout` | A whole-app layout (`contributes.layouts`, API 1.4) |
|
|
481
482
|
| `api` | Axios client for your `/plugin-api/<id>/` routes, auth included |
|
|
482
483
|
| `fetch(path)`, `wsUrl(path)` | Raw fetch for streams, socket URL and auth |
|
|
483
484
|
| `docs`, `tabs`, `confirm`, `listHosts`, `getHost`, `t`, `hasPermission`, `desktop` | Helpers |
|
|
@@ -485,7 +486,7 @@ your plugin's error boundary and Suspense.
|
|
|
485
486
|
|
|
486
487
|
Hooks from `@termix-ssh/plugin-sdk/frontend`: `useTranslation`, `usePermission`, `useSettings`, `useHost`,
|
|
487
488
|
`useHosts`, `usePluginApi`, `useCurrentUser`, `useTheme`, `useToast`, `useDocsUrl`, `useTabs`,
|
|
488
|
-
`useSlotContributions`, `useSshAuthTypes`, `useExtensions`.
|
|
489
|
+
`useSlotContributions`, `useSshAuthTypes`, `useExtensions`, `useShell`.
|
|
489
490
|
|
|
490
491
|
Shared from Termix and never bundled: `react`, `react-dom`, `i18next`, `react-i18next`, `sonner` and the
|
|
491
492
|
SDK's `frontend` and `ui`. Anything else, like `lucide-react`, is bundled. A host your code gets only carries
|
|
@@ -496,6 +497,95 @@ Slots you can fill include `terminal.toolbar`, `terminal.toolbarStatus`, `termin
|
|
|
496
497
|
`hosts.panel`, `credentials.badges`, `shell.overlay`, `shell.rightEdge` and `tab.menu`. See
|
|
497
498
|
https://docs.termix.site/develop/more-extension-points.
|
|
498
499
|
|
|
500
|
+
## Layouts and themes
|
|
501
|
+
|
|
502
|
+
A plugin can replace the whole app layout or add themes (API 1.4). Users pick both in Settings, Appearance;
|
|
503
|
+
admins set the default for everyone.
|
|
504
|
+
|
|
505
|
+
A layout is declared in `contributes.layouts` (`id`, `label`, optional `description` and `defaultTheme`) and
|
|
506
|
+
needs the `ui:layout` capability, which admins see as high risk because a layout draws every screen. Register
|
|
507
|
+
it in `activate`:
|
|
508
|
+
|
|
509
|
+
```tsx
|
|
510
|
+
import { useShell, type TermixApp } from "@termix-ssh/plugin-sdk/frontend";
|
|
511
|
+
import {
|
|
512
|
+
ShellConfirmCapture,
|
|
513
|
+
SurfaceScope,
|
|
514
|
+
TabStage,
|
|
515
|
+
} from "@termix-ssh/plugin-sdk/ui";
|
|
516
|
+
|
|
517
|
+
function MyLayout() {
|
|
518
|
+
const shell = useShell();
|
|
519
|
+
if (!shell) return null;
|
|
520
|
+
return (
|
|
521
|
+
<div className="flex h-dvh w-screen flex-col bg-background">
|
|
522
|
+
<header>
|
|
523
|
+
{/* tabs from shell.tabs.topLevel, shell.palette.open() and so on */}
|
|
524
|
+
</header>
|
|
525
|
+
<SurfaceScope kind="tab">
|
|
526
|
+
<ShellConfirmCapture />
|
|
527
|
+
<TabStage />
|
|
528
|
+
</SurfaceScope>
|
|
529
|
+
</div>
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export function activate(app: TermixApp) {
|
|
534
|
+
app.registerLayout({
|
|
535
|
+
id: "my-layout",
|
|
536
|
+
label: "layout.name",
|
|
537
|
+
component: MyLayout,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
- `useShell()` is the live shell with nothing drawn: tabs, splits, hosts, the left and right docks, the
|
|
543
|
+
command palette, the registries as hooks and the user. Core keeps all the state, so switching layouts keeps
|
|
544
|
+
every session.
|
|
545
|
+
- `TabStage` is where tabs show. Place it anywhere; tabs are never remounted when it moves, so a terminal
|
|
546
|
+
keeps its connection. Wrap it in `SurfaceScope kind="tab"` with a `ShellConfirmCapture` inside, so
|
|
547
|
+
close questions and inline views show over the tab area. Overlays and onboarding stay in core.
|
|
548
|
+
- The layout draws the command palette. Core keeps whether it is open (`palette.isOpen`, Ctrl+K calls
|
|
549
|
+
`palette.open()`) and what is in it (`usePaletteEntries`, `usePaletteGroups`, rail items, hosts).
|
|
550
|
+
- The shell always keeps a home tab with the id and type `dashboard`. `useShell()` leaves it out until some
|
|
551
|
+
plugin registers a `dashboard` tab type, so a layout without a home screen has no empty tab to draw.
|
|
552
|
+
- Settings, Manage and the host editor belong to Classic Layout. A layout without them should still render
|
|
553
|
+
`LookSettings` somewhere so people can switch back. `tabs.openSingleton("plugins")` opens core's own
|
|
554
|
+
Plugins tab in any layout. Switching layouts closes the tabs the old layout's plugin declared.
|
|
555
|
+
- `mobileComponent` is used on phones and narrow windows. The desktop app always gets `component`.
|
|
556
|
+
- Only the chosen layout's plugin is loaded. If it fails to load or crashes twice, Termix shows a repair
|
|
557
|
+
screen where people can switch, reset, update, turn it off or (for a bundled plugin) restore the copy that
|
|
558
|
+
shipped. `?safe-layout=1` opens it any time.
|
|
559
|
+
- Termix has no layout of its own. The default is the bundled Classic Layout plugin
|
|
560
|
+
(https://github.com/Termix-SSH/Plugin-Classic-Layout), which is also a full example. It owns the rail,
|
|
561
|
+
sidebar, tab bar, docks, Dashboard, Manage, Settings and command palette, registers the `dashboard`,
|
|
562
|
+
`host-manager` and `settings` tab types, and offers the host list and dashboard as the components
|
|
563
|
+
`classic-layout.hostTree` and `classic-layout.dashboard` for other plugins to place with `PluginComponent`.
|
|
564
|
+
It reads the shell with `useShellController()` from `@termix-ssh/plugin-sdk/ui`, which exposes more than
|
|
565
|
+
`useShell()` and may change with Termix. A third party layout should start from `useShell()`.
|
|
566
|
+
- Example Top Bar Layout (https://github.com/Termix-SSH/Plugin-Example-Topbar-Layout) is a small third party
|
|
567
|
+
style layout built only on `useShell()`, `TabStage` and `@termix-ssh/plugin-sdk/ui`, with a phone version.
|
|
568
|
+
|
|
569
|
+
A theme is CSS only. Declare it in `contributes.themes` (`id`, `label`, `mode` light or dark, `css` path,
|
|
570
|
+
optional `swatch` colors for the picker) with the `ui:theme` capability. The id becomes a class on `<html>`,
|
|
571
|
+
so the file may only hold rules whose selectors start with `.<id>`, `:root.<id>` or `html.<id>`, optionally
|
|
572
|
+
inside `@media` or `@supports`. `@import`, `url()` to another host and other at-rules are refused by
|
|
573
|
+
`termix-plugin validate` and by the server. Set the same tokens core's themes set (`--background`,
|
|
574
|
+
`--foreground`, `--card`, `--border`, `--muted` and the rest).
|
|
575
|
+
|
|
576
|
+
A plugin that only holds themes needs no code. Leave out `src/`: `termix-plugin build` writes an empty backend and
|
|
577
|
+
`validate` does not ask for a frontend. Keep the CSS in `themes/` and a test that checks it with
|
|
578
|
+
`validateThemeCss()`. Notes:
|
|
579
|
+
|
|
580
|
+
- A theme with the id of a built-in (`light` or `dark`) replaces core's basic copy while the plugin runs.
|
|
581
|
+
- The first theme a bundled default-look plugin lists is the default for new installs.
|
|
582
|
+
- Themes show in the picker in the order core lists its own, then the plugin's order.
|
|
583
|
+
- A theme someone saved that no longer exists shows as System. It comes back when the plugin does.
|
|
584
|
+
- Accent color, UI font, font size and density are not part of a theme. Core applies them on top.
|
|
585
|
+
- Bundled Classic Themes (https://github.com/Termix-SSH/Plugin-Classic-Themes) is a full example, and
|
|
586
|
+
Example Theme (https://github.com/Termix-SSH/Plugin-Example-Theme) a small one with one light and one dark
|
|
587
|
+
theme.
|
|
588
|
+
|
|
499
589
|
## Looking native
|
|
500
590
|
|
|
501
591
|
Build UI from `@termix-ssh/plugin-sdk/ui` so it matches Termix and follows the theme. The full list of
|
|
@@ -775,6 +865,7 @@ To run your own registry instead, see https://docs.termix.site/develop/registrie
|
|
|
775
865
|
| `auth:provide` | high | Add login methods, second factors, SSH auth types |
|
|
776
866
|
| `device:serial` | high | Open serial and USB ports on the server |
|
|
777
867
|
| `notify:hub` | high | Receive every alert (the alerts hub only) |
|
|
868
|
+
| `ui:layout` | high | Replace the whole app layout |
|
|
778
869
|
| `hosts:write` | medium | Add and edit hosts |
|
|
779
870
|
| `credentials:use` | medium | Connect with saved credentials without seeing them |
|
|
780
871
|
| `credentials:write` | medium | Save new credentials |
|
|
@@ -794,6 +885,7 @@ To run your own registry instead, see https://docs.termix.site/develop/registrie
|
|
|
794
885
|
| `settings:read-core` | low | Read Termix server settings |
|
|
795
886
|
| `plugins:read` | low | See installed plugins |
|
|
796
887
|
| `ui:surface` | low | Add tabs, panels and settings |
|
|
888
|
+
| `ui:theme` | low | Add color themes, CSS only |
|
|
797
889
|
|
|
798
890
|
An update that adds a capability waits for an admin to approve it.
|
|
799
891
|
|
package/cli/commands/build.mjs
CHANGED
|
@@ -5,7 +5,12 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import * as esbuild from "esbuild";
|
|
6
6
|
import { BACKEND_EXTERNALS, FRONTEND_EXTERNALS } from "../lib/externals.mjs";
|
|
7
7
|
import { staticUrlImports } from "../lib/static-url-imports.mjs";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
readManifest,
|
|
10
|
+
resolveEntry,
|
|
11
|
+
copyDir,
|
|
12
|
+
isThemeOnly,
|
|
13
|
+
} from "../lib/plugin-dir.mjs";
|
|
9
14
|
import { openapi } from "./openapi.mjs";
|
|
10
15
|
import { splitByCoreClasses } from "../lib/css-classes.mjs";
|
|
11
16
|
|
|
@@ -232,32 +237,41 @@ export async function build({ cwd }) {
|
|
|
232
237
|
fs.mkdirSync(outDir, { recursive: true });
|
|
233
238
|
|
|
234
239
|
const backendEntry = resolveEntry(cwd, BACKEND_ENTRIES);
|
|
235
|
-
if (!backendEntry) {
|
|
240
|
+
if (!backendEntry && !isThemeOnly(manifest)) {
|
|
236
241
|
throw new Error(
|
|
237
242
|
`${pluginId}: no backend entry (looked for ${BACKEND_ENTRIES.join(", ")})`,
|
|
238
243
|
);
|
|
239
244
|
}
|
|
240
245
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
246
|
+
if (backendEntry) {
|
|
247
|
+
await esbuild.build({
|
|
248
|
+
entryPoints: [backendEntry],
|
|
249
|
+
outfile: path.join(outDir, "backend.js"),
|
|
250
|
+
bundle: true,
|
|
251
|
+
format: "esm",
|
|
252
|
+
platform: "node",
|
|
253
|
+
target: "node22",
|
|
254
|
+
sourcemap: true,
|
|
255
|
+
logLevel: "warning",
|
|
256
|
+
// A native dependency's compiled .node binary is resolved by the
|
|
257
|
+
// package's own relative paths, which break once esbuild inlines its JS
|
|
258
|
+
// elsewhere. Declaring it in nativeDependencies keeps it a real
|
|
259
|
+
// node_modules import instead, resolved at runtime like a host-provided
|
|
260
|
+
// package.
|
|
261
|
+
external: [...BACKEND_EXTERNALS, ...(manifest.nativeDependencies ?? [])],
|
|
262
|
+
loader: BINARY_LOADERS,
|
|
263
|
+
// Bundled CJS deps still call require() for Node builtins at runtime.
|
|
264
|
+
// ESM has no ambient require, so give esbuild's require shim a real one.
|
|
265
|
+
banner: { js: REQUIRE_BANNER },
|
|
266
|
+
});
|
|
267
|
+
} else {
|
|
268
|
+
// Core loads every plugin through a backend entry, so a theme only
|
|
269
|
+
// plugin gets an empty one.
|
|
270
|
+
fs.writeFileSync(
|
|
271
|
+
path.join(outDir, "backend.js"),
|
|
272
|
+
"export function activate() {}\n",
|
|
273
|
+
);
|
|
274
|
+
}
|
|
261
275
|
|
|
262
276
|
for (const worker of readWorkerEntries(cwd, pluginId)) {
|
|
263
277
|
await esbuild.build({
|
package/cli/commands/pack.mjs
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { readManifest } from "../lib/plugin-dir.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
collectFiles,
|
|
6
|
+
createTmxplug,
|
|
7
|
+
themeFiles,
|
|
8
|
+
TMXPLUG_CONTENTS,
|
|
9
|
+
} from "../lib/tmxplug.mjs";
|
|
5
10
|
import { sha256 } from "../lib/signing.mjs";
|
|
6
11
|
import { validate } from "./validate.mjs";
|
|
7
12
|
|
|
@@ -32,7 +37,12 @@ export async function pack({ cwd, args = [] }) {
|
|
|
32
37
|
fs.mkdirSync(outDir, { recursive: true });
|
|
33
38
|
const outfile = path.join(outDir, `${pluginId}-${version}.tmxplug`);
|
|
34
39
|
|
|
35
|
-
const archive = createTmxplug(
|
|
40
|
+
const archive = createTmxplug(
|
|
41
|
+
cwd,
|
|
42
|
+
collectFiles(cwd, [
|
|
43
|
+
...new Set([...TMXPLUG_CONTENTS, ...themeFiles(manifest)]),
|
|
44
|
+
]),
|
|
45
|
+
);
|
|
36
46
|
fs.writeFileSync(outfile, archive);
|
|
37
47
|
|
|
38
48
|
console.log(`packed ${path.basename(outfile)}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { readManifest } from "../lib/plugin-dir.mjs";
|
|
4
|
+
import { readManifest, isThemeOnly } from "../lib/plugin-dir.mjs";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The manifest rules live in src/manifest.ts, which the server uses too, so
|
|
@@ -17,7 +17,7 @@ export async function loadSdkModule(name) {
|
|
|
17
17
|
|
|
18
18
|
export async function validate({ cwd }) {
|
|
19
19
|
const raw = readManifest(cwd);
|
|
20
|
-
const { parseManifest } = await loadSdkModule("manifest");
|
|
20
|
+
const { parseManifest, validateThemeCss } = await loadSdkModule("manifest");
|
|
21
21
|
const { errors } = parseManifest(raw);
|
|
22
22
|
|
|
23
23
|
const problems = [...errors];
|
|
@@ -25,7 +25,8 @@ export async function validate({ cwd }) {
|
|
|
25
25
|
// The manifest names files. They have to be there.
|
|
26
26
|
const referenced = [
|
|
27
27
|
raw.backend ?? "dist/backend.js",
|
|
28
|
-
|
|
28
|
+
// A theme only plugin has no frontend code.
|
|
29
|
+
...(isThemeOnly(raw) ? [] : [raw.frontend ?? "dist/frontend.js"]),
|
|
29
30
|
raw.locales ?? "locales",
|
|
30
31
|
];
|
|
31
32
|
for (const rel of referenced) {
|
|
@@ -34,6 +35,8 @@ export async function validate({ cwd }) {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
problems.push(...validateThemeFiles(cwd, raw, validateThemeCss));
|
|
39
|
+
problems.push(...validateLookCapabilities(cwd, raw));
|
|
37
40
|
problems.push(
|
|
38
41
|
...(await validateMigrations(cwd, raw.id ?? path.basename(cwd))),
|
|
39
42
|
);
|
|
@@ -53,6 +56,63 @@ export async function validate({ cwd }) {
|
|
|
53
56
|
console.log(`ok ${raw.id ?? path.basename(cwd)}`);
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
/** Every declared theme file exists and holds only CSS a theme may hold. */
|
|
60
|
+
export function validateThemeFiles(cwd, raw, validateThemeCss) {
|
|
61
|
+
const problems = [];
|
|
62
|
+
const themes = raw?.contributes?.themes;
|
|
63
|
+
if (!Array.isArray(themes)) return problems;
|
|
64
|
+
for (const theme of themes) {
|
|
65
|
+
if (typeof theme?.css !== "string" || typeof theme?.id !== "string") {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const file = path.join(cwd, theme.css);
|
|
69
|
+
if (!fs.existsSync(file)) {
|
|
70
|
+
problems.push(`theme ${theme.id}: ${theme.css} does not exist`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
for (const error of validateThemeCss(
|
|
74
|
+
fs.readFileSync(file, "utf8"),
|
|
75
|
+
theme.id,
|
|
76
|
+
)) {
|
|
77
|
+
problems.push(`theme ${theme.id}: ${error}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return problems;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* ui:layout and ui:theme must match what the manifest contributes, both
|
|
85
|
+
* ways. The manifest check already flags a contribution without its
|
|
86
|
+
* capability; this flags a capability with nothing behind it, and a layout
|
|
87
|
+
* with no frontend to register it.
|
|
88
|
+
*/
|
|
89
|
+
export function validateLookCapabilities(cwd, raw) {
|
|
90
|
+
const problems = [];
|
|
91
|
+
const capabilities = Array.isArray(raw?.capabilities) ? raw.capabilities : [];
|
|
92
|
+
const layouts = raw?.contributes?.layouts;
|
|
93
|
+
const themes = raw?.contributes?.themes;
|
|
94
|
+
const hasLayouts = Array.isArray(layouts) && layouts.length > 0;
|
|
95
|
+
const hasThemes = Array.isArray(themes) && themes.length > 0;
|
|
96
|
+
if (capabilities.includes("ui:layout") && !hasLayouts) {
|
|
97
|
+
problems.push('"ui:layout" is declared but contributes.layouts is empty');
|
|
98
|
+
}
|
|
99
|
+
if (capabilities.includes("ui:theme") && !hasThemes) {
|
|
100
|
+
problems.push('"ui:theme" is declared but contributes.themes is empty');
|
|
101
|
+
}
|
|
102
|
+
if (hasLayouts) {
|
|
103
|
+
const entries = ["index.tsx", "index.ts", "index.mjs", "index.js"];
|
|
104
|
+
const frontend = entries.some((entry) =>
|
|
105
|
+
fs.existsSync(path.join(cwd, "src", "frontend", entry)),
|
|
106
|
+
);
|
|
107
|
+
if (!frontend && raw.frontend === undefined) {
|
|
108
|
+
problems.push(
|
|
109
|
+
"contributes.layouts needs a frontend that calls app.registerLayout",
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return problems;
|
|
114
|
+
}
|
|
115
|
+
|
|
56
116
|
/** Env vars core sets for everyone, so a plugin need not list them. */
|
|
57
117
|
const CORE_ENV = new Set([
|
|
58
118
|
"DATA_DIR",
|
package/cli/lib/plugin-dir.mjs
CHANGED
|
@@ -14,6 +14,17 @@ export function readManifest(cwd) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/** The first of these that exists, or null. */
|
|
17
|
+
/** Only themes: CSS files and no code, so no source entry is needed. */
|
|
18
|
+
export function isThemeOnly(manifest) {
|
|
19
|
+
const capabilities = manifest.capabilities ?? [];
|
|
20
|
+
return (
|
|
21
|
+
capabilities.length > 0 &&
|
|
22
|
+
capabilities.every((capability) => capability === "ui:theme") &&
|
|
23
|
+
Array.isArray(manifest.contributes?.themes) &&
|
|
24
|
+
manifest.contributes.themes.length > 0
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
export function resolveEntry(cwd, candidates) {
|
|
18
29
|
for (const candidate of candidates) {
|
|
19
30
|
const full = path.join(cwd, candidate);
|
package/cli/lib/tmxplug.mjs
CHANGED
|
@@ -2,6 +2,15 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import zlib from "node:zlib";
|
|
4
4
|
|
|
5
|
+
/** Theme CSS files a manifest declares, which travel with the plugin too. */
|
|
6
|
+
export function themeFiles(manifest) {
|
|
7
|
+
const themes = manifest?.contributes?.themes;
|
|
8
|
+
if (!Array.isArray(themes)) return [];
|
|
9
|
+
return themes
|
|
10
|
+
.map((theme) => theme?.css)
|
|
11
|
+
.filter((css) => typeof css === "string");
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
/** What a .tmxplug holds, in the order the spec lists it. */
|
|
6
15
|
export const TMXPLUG_CONTENTS = [
|
|
7
16
|
"manifest.json",
|
package/dist/capabilities.js
CHANGED
|
@@ -35,6 +35,7 @@ export const CAPABILITY_CATALOG = [
|
|
|
35
35
|
entry("auth:provide", "high"),
|
|
36
36
|
entry("device:serial", "high"),
|
|
37
37
|
entry("notify:hub", "high"),
|
|
38
|
+
entry("ui:layout", "high"),
|
|
38
39
|
entry("hosts:write", "medium"),
|
|
39
40
|
entry("credentials:use", "medium"),
|
|
40
41
|
entry("credentials:write", "medium"),
|
|
@@ -54,6 +55,7 @@ export const CAPABILITY_CATALOG = [
|
|
|
54
55
|
entry("settings:read-core", "low"),
|
|
55
56
|
entry("plugins:read", "low"),
|
|
56
57
|
entry("ui:surface", "low"),
|
|
58
|
+
entry("ui:theme", "low"),
|
|
57
59
|
];
|
|
58
60
|
export const CAPABILITY_IDS = CAPABILITY_CATALOG.map((capability) => capability.id);
|
|
59
61
|
const BY_ID = new Map(CAPABILITY_CATALOG.map((capability) => [capability.id, capability]));
|
package/dist/frontend.d.ts
CHANGED
|
@@ -222,6 +222,146 @@ export interface TabsApi extends Pick<ShellApi, "openTab" | "openSingletonTab" |
|
|
|
222
222
|
/** Called once the shell has restored its tabs after login. */
|
|
223
223
|
onReady: (listener: () => void) => Disposer;
|
|
224
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* A whole-app layout. Core renders the chosen one inside its providers and an
|
|
227
|
+
* error boundary; everything it shows comes from useShell(). Declare it in
|
|
228
|
+
* manifest contributes.layouts and add the ui:layout capability.
|
|
229
|
+
*/
|
|
230
|
+
export interface LayoutContribution {
|
|
231
|
+
id: string;
|
|
232
|
+
/** Your own i18n key or plain text, the same as the manifest's label. */
|
|
233
|
+
label: string;
|
|
234
|
+
component: ComponentType;
|
|
235
|
+
/** Shown on phones and narrow windows. The desktop app never uses it. */
|
|
236
|
+
mobileComponent?: ComponentType;
|
|
237
|
+
/** Theme to start with when nobody chose one. */
|
|
238
|
+
defaultTheme?: string;
|
|
239
|
+
}
|
|
240
|
+
/** A tab as a layout sees it. Splits are tabs too, with a `split` field. */
|
|
241
|
+
export interface ShellTabState extends PluginTabRecord {
|
|
242
|
+
instanceId: string;
|
|
243
|
+
customLabel?: string;
|
|
244
|
+
/** Set while the tab sits in a split pane. */
|
|
245
|
+
parentSplitTabId?: string;
|
|
246
|
+
openedAt?: number;
|
|
247
|
+
}
|
|
248
|
+
/** What a dock region shows and how it is sized, in Normal-size pixels. */
|
|
249
|
+
export interface ShellDockState {
|
|
250
|
+
view: string | null;
|
|
251
|
+
open: boolean;
|
|
252
|
+
width: number;
|
|
253
|
+
dragging: boolean;
|
|
254
|
+
setView: (view: string | null) => void;
|
|
255
|
+
setWidth: (width: number) => void;
|
|
256
|
+
/** Starts a drag from the dock's edge, from a mousedown. */
|
|
257
|
+
startResize: (event: {
|
|
258
|
+
clientX: number;
|
|
259
|
+
preventDefault: () => void;
|
|
260
|
+
}) => void;
|
|
261
|
+
resetWidth: () => void;
|
|
262
|
+
}
|
|
263
|
+
/** The live shell: state and actions, nothing drawn. Read with useShell(). */
|
|
264
|
+
export interface ShellController {
|
|
265
|
+
tabs: {
|
|
266
|
+
list: ShellTabState[];
|
|
267
|
+
/** What a tab bar shows: tabs not sitting in a split. */
|
|
268
|
+
topLevel: ShellTabState[];
|
|
269
|
+
activeId: string;
|
|
270
|
+
/** True once saved tabs were restored after login. */
|
|
271
|
+
ready: boolean;
|
|
272
|
+
/** The user turned on tab numbers. */
|
|
273
|
+
showNumbers: boolean;
|
|
274
|
+
setActive: (id: string) => void;
|
|
275
|
+
reorder: (topLevel: ShellTabState[]) => void;
|
|
276
|
+
close: (id: string) => Promise<void>;
|
|
277
|
+
rename: (id: string, label: string) => void;
|
|
278
|
+
/** Reconnects or reloads a session tab. */
|
|
279
|
+
refresh: (id: string) => void;
|
|
280
|
+
reconnectAll: () => void;
|
|
281
|
+
openSingleton: (type: string) => string | undefined;
|
|
282
|
+
};
|
|
283
|
+
split: {
|
|
284
|
+
/** The split shown now, or null. */
|
|
285
|
+
activeId: string | null;
|
|
286
|
+
focusedPaneId: string | null;
|
|
287
|
+
/** Every split with its panes, for a split menu. */
|
|
288
|
+
summaries: Array<{
|
|
289
|
+
id: string;
|
|
290
|
+
label: string;
|
|
291
|
+
full: boolean;
|
|
292
|
+
panes: Array<{
|
|
293
|
+
id: string;
|
|
294
|
+
index: number;
|
|
295
|
+
tabId: string | null;
|
|
296
|
+
label: string | null;
|
|
297
|
+
}>;
|
|
298
|
+
}>;
|
|
299
|
+
activeFull: boolean;
|
|
300
|
+
/** Split a tab, apply a preset, unsplit, add to a pane. */
|
|
301
|
+
handleAction: (action: Record<string, unknown> & {
|
|
302
|
+
kind: string;
|
|
303
|
+
}) => void;
|
|
304
|
+
};
|
|
305
|
+
hosts: {
|
|
306
|
+
list: PluginHostRecord[];
|
|
307
|
+
loading: boolean;
|
|
308
|
+
loaded: boolean;
|
|
309
|
+
reload: () => Promise<void>;
|
|
310
|
+
};
|
|
311
|
+
regions: {
|
|
312
|
+
left: ShellDockState & {
|
|
313
|
+
view: string;
|
|
314
|
+
};
|
|
315
|
+
right: ShellDockState;
|
|
316
|
+
/** What a rail click does: toggles or switches the left dock. */
|
|
317
|
+
railClick: (view: string) => void;
|
|
318
|
+
openInRightDock: (view: string) => void;
|
|
319
|
+
toggleRightDock: () => void;
|
|
320
|
+
};
|
|
321
|
+
palette: {
|
|
322
|
+
isOpen: boolean;
|
|
323
|
+
open: () => void;
|
|
324
|
+
close: () => void;
|
|
325
|
+
};
|
|
326
|
+
fullscreen: {
|
|
327
|
+
active: boolean;
|
|
328
|
+
toggle: () => void;
|
|
329
|
+
};
|
|
330
|
+
/** The same callbacks tabs and panels get. */
|
|
331
|
+
actions: ShellApi;
|
|
332
|
+
env: {
|
|
333
|
+
isMobile: boolean;
|
|
334
|
+
isElectron: boolean;
|
|
335
|
+
user: {
|
|
336
|
+
username: string;
|
|
337
|
+
isAdmin: boolean;
|
|
338
|
+
};
|
|
339
|
+
permissions: string[];
|
|
340
|
+
branding: {
|
|
341
|
+
appName: string | null;
|
|
342
|
+
tagline: string | null;
|
|
343
|
+
logo: string | null;
|
|
344
|
+
};
|
|
345
|
+
};
|
|
346
|
+
/** Core registries, as hooks a layout calls while rendering. */
|
|
347
|
+
registries: {
|
|
348
|
+
useRailItems: () => ReadonlyArray<{
|
|
349
|
+
id: string;
|
|
350
|
+
} & Record<string, unknown>>;
|
|
351
|
+
usePanels: () => ReadonlyArray<{
|
|
352
|
+
id: string;
|
|
353
|
+
} & Record<string, unknown>>;
|
|
354
|
+
useTabTypes: () => ReadonlyArray<{
|
|
355
|
+
id: string;
|
|
356
|
+
} & Record<string, unknown>>;
|
|
357
|
+
useDashboardCards: () => ReadonlyArray<{
|
|
358
|
+
id: string;
|
|
359
|
+
} & Record<string, unknown>>;
|
|
360
|
+
};
|
|
361
|
+
logout: (options?: {
|
|
362
|
+
manual?: boolean;
|
|
363
|
+
}) => void;
|
|
364
|
+
}
|
|
225
365
|
export interface RailItemContribution {
|
|
226
366
|
/** Must be declared in manifest contributes.tabs or contributes.panels. */
|
|
227
367
|
id: string;
|
|
@@ -886,6 +1026,12 @@ export interface TermixApp extends TermixAppInfo {
|
|
|
886
1026
|
registerSshAuthEditor: (editor: SshAuthEditorContribution) => Disposer;
|
|
887
1027
|
registerLoginMethod: (method: LoginMethodContribution) => Disposer;
|
|
888
1028
|
registerSecondFactorUI: (factor: SecondFactorContribution) => Disposer;
|
|
1029
|
+
/**
|
|
1030
|
+
* A whole-app layout. Declared in manifest contributes.layouts with the
|
|
1031
|
+
* ui:layout capability. Only the chosen layout's plugin is loaded, so
|
|
1032
|
+
* register it in activate. Needs plugin API 1.4.
|
|
1033
|
+
*/
|
|
1034
|
+
registerLayout: (layout: LayoutContribution) => Disposer;
|
|
889
1035
|
/**
|
|
890
1036
|
* Translates a key from the plugin's own locales, for code outside a
|
|
891
1037
|
* component (a toast from activate). Components use useTranslation().
|
|
@@ -993,6 +1139,7 @@ export interface PluginHostBridge {
|
|
|
993
1139
|
useTheme: () => {
|
|
994
1140
|
theme: "light" | "dark";
|
|
995
1141
|
};
|
|
1142
|
+
useShell: () => ShellController | null;
|
|
996
1143
|
toast: ToastApi;
|
|
997
1144
|
getApi: (pluginId: string) => PluginApiClient;
|
|
998
1145
|
getApiFor: (pluginId: string, origin: unknown) => PluginApiClient;
|
|
@@ -1154,6 +1301,11 @@ export declare function useCurrentUser(): CurrentUser | null;
|
|
|
1154
1301
|
export declare function useTheme(): {
|
|
1155
1302
|
theme: "light" | "dark";
|
|
1156
1303
|
};
|
|
1304
|
+
/**
|
|
1305
|
+
* The live shell, for a layout. Null outside the signed-in app. Needs plugin
|
|
1306
|
+
* API 1.4.
|
|
1307
|
+
*/
|
|
1308
|
+
export declare function useShell(): ShellController | null;
|
|
1157
1309
|
export declare function useToast(): ToastApi;
|
|
1158
1310
|
export declare function logActivity(type: string, hostId: number, hostName: string): Promise<void>;
|
|
1159
1311
|
/** Tells the shell hosts were added or changed, so its lists reload. */
|
package/dist/frontend.js
CHANGED
|
@@ -98,6 +98,13 @@ export function useCurrentUser() {
|
|
|
98
98
|
export function useTheme() {
|
|
99
99
|
return requireHost().useTheme();
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* The live shell, for a layout. Null outside the signed-in app. Needs plugin
|
|
103
|
+
* API 1.4.
|
|
104
|
+
*/
|
|
105
|
+
export function useShell() {
|
|
106
|
+
return requireHost().useShell();
|
|
107
|
+
}
|
|
101
108
|
export function useToast() {
|
|
102
109
|
return requireHost().toast;
|
|
103
110
|
}
|