@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/dist/host-commands.d.ts
CHANGED
|
@@ -17,6 +17,13 @@ export interface ExecResult {
|
|
|
17
17
|
stderr: string;
|
|
18
18
|
code: number | null;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Decodes raw command output. Strict UTF-8 first; when that fails, tries GBK,
|
|
22
|
+
* which is what a Chinese-locale Windows console sends (its bytes are almost
|
|
23
|
+
* never valid UTF-8) and keeps it only when it decodes without replacement
|
|
24
|
+
* characters. Anything else falls back to lossy UTF-8, the old behavior.
|
|
25
|
+
*/
|
|
26
|
+
export declare function decodeCommandOutput(buf: Buffer): string;
|
|
20
27
|
/** Runs one non-interactive command over an existing ssh2 client. */
|
|
21
28
|
export declare function execCommand(client: Client, command: string, timeoutMs?: number): Promise<ExecResult>;
|
|
22
29
|
export type PackageManager = "apt" | "dnf" | "yum" | "pacman" | null;
|
package/dist/host-commands.js
CHANGED
|
@@ -11,6 +11,30 @@
|
|
|
11
11
|
// Homebrew and OrbStack paths a non-interactive shell misses.
|
|
12
12
|
import { EXTRA_PATH_DIRS } from "./host-scripts/docker.js";
|
|
13
13
|
export { EXTRA_PATH_DIRS };
|
|
14
|
+
/**
|
|
15
|
+
* Decodes raw command output. Strict UTF-8 first; when that fails, tries GBK,
|
|
16
|
+
* which is what a Chinese-locale Windows console sends (its bytes are almost
|
|
17
|
+
* never valid UTF-8) and keeps it only when it decodes without replacement
|
|
18
|
+
* characters. Anything else falls back to lossy UTF-8, the old behavior.
|
|
19
|
+
*/
|
|
20
|
+
export function decodeCommandOutput(buf) {
|
|
21
|
+
try {
|
|
22
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(buf);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// not valid UTF-8, try the legacy codepage
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
// A Node built with small-ICU has no GBK decoder and throws here.
|
|
29
|
+
const gbk = new TextDecoder("gbk").decode(buf);
|
|
30
|
+
if (!gbk.includes("�"))
|
|
31
|
+
return gbk;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// expected - fall through to lossy UTF-8
|
|
35
|
+
}
|
|
36
|
+
return buf.toString("utf8");
|
|
37
|
+
}
|
|
14
38
|
/** Runs one non-interactive command over an existing ssh2 client. */
|
|
15
39
|
export function execCommand(client, command, timeoutMs = 30000) {
|
|
16
40
|
return new Promise((resolve, reject) => {
|
|
@@ -48,8 +72,10 @@ export function execCommand(client, command, timeoutMs = 30000) {
|
|
|
48
72
|
return;
|
|
49
73
|
}
|
|
50
74
|
stream = _stream;
|
|
51
|
-
|
|
52
|
-
|
|
75
|
+
// Chunks are buffered and decoded once on close: a multi-byte character
|
|
76
|
+
// can be split across chunks, and charset detection needs it whole.
|
|
77
|
+
const stdout = [];
|
|
78
|
+
const stderr = [];
|
|
53
79
|
let exitCode = null;
|
|
54
80
|
stream
|
|
55
81
|
.on("close", (code) => {
|
|
@@ -57,11 +83,15 @@ export function execCommand(client, command, timeoutMs = 30000) {
|
|
|
57
83
|
settled = true;
|
|
58
84
|
exitCode = typeof code === "number" ? code : null;
|
|
59
85
|
cleanup();
|
|
60
|
-
resolve({
|
|
86
|
+
resolve({
|
|
87
|
+
stdout: decodeCommandOutput(Buffer.concat(stdout)),
|
|
88
|
+
stderr: decodeCommandOutput(Buffer.concat(stderr)),
|
|
89
|
+
code: exitCode,
|
|
90
|
+
});
|
|
61
91
|
}
|
|
62
92
|
})
|
|
63
93
|
.on("data", (data) => {
|
|
64
|
-
stdout
|
|
94
|
+
stdout.push(data);
|
|
65
95
|
})
|
|
66
96
|
.on("error", (streamErr) => {
|
|
67
97
|
if (!settled) {
|
|
@@ -73,7 +103,7 @@ export function execCommand(client, command, timeoutMs = 30000) {
|
|
|
73
103
|
if (stream.stderr) {
|
|
74
104
|
stream.stderr
|
|
75
105
|
.on("data", (data) => {
|
|
76
|
-
stderr
|
|
106
|
+
stderr.push(data);
|
|
77
107
|
})
|
|
78
108
|
.on("error", (stderrErr) => {
|
|
79
109
|
if (!settled) {
|
package/dist/manifest.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* "permissionGroups" used to validate clean and be silently dropped, which
|
|
10
10
|
* made a manifest look like it declared something it did not.
|
|
11
11
|
*/
|
|
12
|
+
import { type PluginOperationsContribution } from "./operations.js";
|
|
12
13
|
import { type PluginEnvVar } from "./docs.js";
|
|
13
14
|
export { validateThemeCss } from "./theme-css.js";
|
|
14
15
|
export { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsPage, pluginDocsUrl, pluginEnvVars, type PluginEnvVar, } from "./docs.js";
|
|
@@ -20,7 +21,7 @@ export { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsPage, pluginDoc
|
|
|
20
21
|
* "^1.2" needs 1.2 or later). engine.termix is checked against the core
|
|
21
22
|
* version by the server.
|
|
22
23
|
*/
|
|
23
|
-
export declare const PLUGIN_API_VERSION = "1.
|
|
24
|
+
export declare const PLUGIN_API_VERSION = "1.14.0";
|
|
24
25
|
/** The API major, the default engine.api for a new plugin. */
|
|
25
26
|
export declare const SUPPORTED_PLUGIN_API_VERSION: string;
|
|
26
27
|
/** Whether this build's plugin API satisfies a manifest's engine.api. */
|
|
@@ -305,6 +306,10 @@ export interface PluginContributions {
|
|
|
305
306
|
tabs?: PluginTabContribution[];
|
|
306
307
|
panels?: PluginViewContribution[];
|
|
307
308
|
dashboardCards?: PluginViewContribution[];
|
|
309
|
+
/** Cards on the host page, filled with app.registerHostSection. */
|
|
310
|
+
hostSections?: PluginViewContribution[];
|
|
311
|
+
/** What this plugin can do, for every surface to find on its own. */
|
|
312
|
+
operations?: PluginOperationsContribution;
|
|
308
313
|
/**
|
|
309
314
|
* The frontend also runs on anonymous guest pages (shared-session and
|
|
310
315
|
* collab links), where it is activated with app.guest set.
|
|
@@ -363,6 +368,33 @@ export interface PluginContributions {
|
|
|
363
368
|
layouts?: PluginLayoutContribution[];
|
|
364
369
|
/** Themes. Needs the ui:theme capability. */
|
|
365
370
|
themes?: PluginThemeContribution[];
|
|
371
|
+
/** How the plugin shows in the Termix mobile app. Plugin API 1.13. */
|
|
372
|
+
mobile?: PluginMobileContribution;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* - "webview": the app shows the plugin's UI from the server in a WebView.
|
|
376
|
+
* - "native": the app has its own screens for this plugin.
|
|
377
|
+
* - "none": nothing to show in the app.
|
|
378
|
+
*/
|
|
379
|
+
export declare const MOBILE_MODES: readonly ["webview", "native", "none"];
|
|
380
|
+
export type PluginMobileMode = (typeof MOBILE_MODES)[number];
|
|
381
|
+
export declare const MOBILE_SIZES: readonly ["phone", "tablet"];
|
|
382
|
+
export type PluginMobileSize = (typeof MOBILE_SIZES)[number];
|
|
383
|
+
export interface PluginMobileContribution {
|
|
384
|
+
mode: PluginMobileMode;
|
|
385
|
+
/**
|
|
386
|
+
* webview: a bundle built for the app, in the same folder as the frontend
|
|
387
|
+
* bundle. Unset means the normal frontend runs in a phone sized frame.
|
|
388
|
+
*/
|
|
389
|
+
entry?: string;
|
|
390
|
+
/** webview: screen sizes the UI works at. Defaults to both. */
|
|
391
|
+
sizes?: PluginMobileSize[];
|
|
392
|
+
/** webview: tab, panel or card ids the app offers. Defaults to every tab. */
|
|
393
|
+
views?: string[];
|
|
394
|
+
/** native: the app feature that stands in for it. Defaults to the plugin id. */
|
|
395
|
+
feature?: string;
|
|
396
|
+
/** i18n key in the plugin's namespace, or plain text, shown in the app. */
|
|
397
|
+
note?: string;
|
|
366
398
|
}
|
|
367
399
|
/** A field a protocol login carries besides username and password. */
|
|
368
400
|
export interface PluginProtocolCredentialField {
|
package/dist/manifest.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import semver from "semver";
|
|
13
13
|
import { isKnownCapability } from "./capabilities.js";
|
|
14
|
+
import { checkOperationReferences, validateOperations, } from "./operations.js";
|
|
14
15
|
import { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsUrl, } from "./docs.js";
|
|
15
16
|
export { validateThemeCss } from "./theme-css.js";
|
|
16
17
|
export { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsPage, pluginDocsUrl, pluginEnvVars, } from "./docs.js";
|
|
@@ -22,7 +23,7 @@ export { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsPage, pluginDoc
|
|
|
22
23
|
* "^1.2" needs 1.2 or later). engine.termix is checked against the core
|
|
23
24
|
* version by the server.
|
|
24
25
|
*/
|
|
25
|
-
export const PLUGIN_API_VERSION = "1.
|
|
26
|
+
export const PLUGIN_API_VERSION = "1.14.0";
|
|
26
27
|
/** The API major, the default engine.api for a new plugin. */
|
|
27
28
|
export const SUPPORTED_PLUGIN_API_VERSION = String(semver.major(PLUGIN_API_VERSION));
|
|
28
29
|
/** Whether this build's plugin API satisfies a manifest's engine.api. */
|
|
@@ -171,6 +172,13 @@ export const SETTINGS_FIELD_TYPES = [
|
|
|
171
172
|
export const SETTINGS_SCOPES = ["admin", "user", "host"];
|
|
172
173
|
/** A layout id or theme id: short, lowercase, used as a CSS class for themes. */
|
|
173
174
|
export const LOOK_ID_PATTERN = /^[a-z][a-z0-9-]{1,39}$/;
|
|
175
|
+
/**
|
|
176
|
+
* - "webview": the app shows the plugin's UI from the server in a WebView.
|
|
177
|
+
* - "native": the app has its own screens for this plugin.
|
|
178
|
+
* - "none": nothing to show in the app.
|
|
179
|
+
*/
|
|
180
|
+
export const MOBILE_MODES = ["webview", "native", "none"];
|
|
181
|
+
export const MOBILE_SIZES = ["phone", "tablet"];
|
|
174
182
|
/** Protocol ids core keeps for itself. */
|
|
175
183
|
export const RESERVED_PROTOCOL_IDS = ["ssh"];
|
|
176
184
|
/** Login keys a declared credential field may not reuse. */
|
|
@@ -238,6 +246,8 @@ const ALLOWED_CONTRIBUTES = new Set([
|
|
|
238
246
|
"tabs",
|
|
239
247
|
"panels",
|
|
240
248
|
"dashboardCards",
|
|
249
|
+
"hostSections",
|
|
250
|
+
"operations",
|
|
241
251
|
"guest",
|
|
242
252
|
"guestViews",
|
|
243
253
|
"actions",
|
|
@@ -254,6 +264,7 @@ const ALLOWED_CONTRIBUTES = new Set([
|
|
|
254
264
|
"protocols",
|
|
255
265
|
"layouts",
|
|
256
266
|
"themes",
|
|
267
|
+
"mobile",
|
|
257
268
|
]);
|
|
258
269
|
const ALLOWED_SETTINGS_FIELD = [
|
|
259
270
|
"key",
|
|
@@ -387,8 +398,107 @@ export function validateManifest(manifest) {
|
|
|
387
398
|
validateProvidesSecret(m.providesSecret, errors);
|
|
388
399
|
validateRequiresSecret(m.requiresSecret, errors);
|
|
389
400
|
validateContributes(m.contributes, typeof m.id === "string" ? m.id : undefined, errors);
|
|
401
|
+
if (isPlainObject(m.contributes)) {
|
|
402
|
+
validateMobile(m.contributes, typeof m.frontend === "string" ? m.frontend : DEFAULT_FRONTEND_ENTRY, errors);
|
|
403
|
+
}
|
|
390
404
|
return errors;
|
|
391
405
|
}
|
|
406
|
+
function folderOf(file) {
|
|
407
|
+
const parts = file.split("/").filter((part) => part !== "" && part !== ".");
|
|
408
|
+
return parts.slice(0, -1).join("/");
|
|
409
|
+
}
|
|
410
|
+
function validateMobile(contributes, frontend, errors) {
|
|
411
|
+
const mobile = contributes.mobile;
|
|
412
|
+
if (mobile === undefined)
|
|
413
|
+
return;
|
|
414
|
+
const where = "contributes.mobile";
|
|
415
|
+
if (!isPlainObject(mobile)) {
|
|
416
|
+
errors.push(`Field "${where}" must be an object`);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
rejectUnknown(mobile, ["mode", "entry", "sizes", "views", "feature", "note"], `"${where}"`, errors);
|
|
420
|
+
const mode = mobile.mode;
|
|
421
|
+
if (!MOBILE_MODES.includes(mode)) {
|
|
422
|
+
errors.push(`${where}.mode must be one of: ${MOBILE_MODES.join(", ")}`);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
for (const key of ["entry", "sizes", "views"]) {
|
|
426
|
+
if (mobile[key] !== undefined && mode !== "webview") {
|
|
427
|
+
errors.push(`${where}.${key} is only for mode "webview"`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (mobile.feature !== undefined && mode !== "native") {
|
|
431
|
+
errors.push(`${where}.feature is only for mode "native"`);
|
|
432
|
+
}
|
|
433
|
+
if (mobile.entry !== undefined &&
|
|
434
|
+
requireString(mobile.entry, `${where}.entry`, errors)) {
|
|
435
|
+
const before = errors.length;
|
|
436
|
+
requireRelativePath(mobile.entry, `${where}.entry`, errors);
|
|
437
|
+
if (!mobile.entry.endsWith(".js")) {
|
|
438
|
+
errors.push(`${where}.entry must be a .js file`);
|
|
439
|
+
}
|
|
440
|
+
else if (errors.length === before &&
|
|
441
|
+
folderOf(mobile.entry) !== folderOf(frontend)) {
|
|
442
|
+
// Core only serves the folder the frontend bundle sits in.
|
|
443
|
+
errors.push(`${where}.entry must sit in the same folder as the frontend bundle (${frontend})`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (mobile.sizes !== undefined) {
|
|
447
|
+
const sizes = mobile.sizes;
|
|
448
|
+
if (!Array.isArray(sizes) || sizes.length === 0) {
|
|
449
|
+
errors.push(`${where}.sizes must be a non-empty array`);
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
const seen = new Set();
|
|
453
|
+
for (const size of sizes) {
|
|
454
|
+
if (!MOBILE_SIZES.includes(size)) {
|
|
455
|
+
errors.push(`${where}.sizes entries must be one of: ${MOBILE_SIZES.join(", ")}`);
|
|
456
|
+
}
|
|
457
|
+
else if (seen.has(size)) {
|
|
458
|
+
errors.push(`${where}.sizes duplicates "${String(size)}"`);
|
|
459
|
+
}
|
|
460
|
+
seen.add(size);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (mobile.views !== undefined) {
|
|
465
|
+
const views = mobile.views;
|
|
466
|
+
if (!Array.isArray(views) || views.length === 0) {
|
|
467
|
+
errors.push(`${where}.views must be a non-empty array`);
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
const declared = new Set();
|
|
471
|
+
for (const field of ["tabs", "panels", "dashboardCards"]) {
|
|
472
|
+
const list = contributes[field];
|
|
473
|
+
if (!Array.isArray(list))
|
|
474
|
+
continue;
|
|
475
|
+
for (const view of list) {
|
|
476
|
+
if (isPlainObject(view) && typeof view.id === "string") {
|
|
477
|
+
declared.add(view.id);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const seen = new Set();
|
|
482
|
+
for (const view of views) {
|
|
483
|
+
if (typeof view !== "string" || !declared.has(view)) {
|
|
484
|
+
errors.push(`${where}.views entries must be ids from contributes.tabs, panels or dashboardCards, got: ${JSON.stringify(view)}`);
|
|
485
|
+
}
|
|
486
|
+
else if (seen.has(view)) {
|
|
487
|
+
errors.push(`${where}.views duplicates "${view}"`);
|
|
488
|
+
}
|
|
489
|
+
seen.add(view);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (mobile.feature !== undefined &&
|
|
494
|
+
(typeof mobile.feature !== "string" ||
|
|
495
|
+
!LOOK_ID_PATTERN.test(mobile.feature))) {
|
|
496
|
+
errors.push(`${where}.feature must match ${LOOK_ID_PATTERN} (lowercase letters, digits, dashes)`);
|
|
497
|
+
}
|
|
498
|
+
if (mobile.note !== undefined) {
|
|
499
|
+
requireString(mobile.note, `${where}.note`, errors);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
392
502
|
function validateFeatures(features, errors) {
|
|
393
503
|
if (features === undefined)
|
|
394
504
|
return;
|
|
@@ -728,6 +838,8 @@ function validateContributes(contributes, pluginId, errors) {
|
|
|
728
838
|
validateTabs(contributes.tabs, errors);
|
|
729
839
|
validateViews(contributes.panels, "panels", errors);
|
|
730
840
|
validateViews(contributes.dashboardCards, "dashboardCards", errors);
|
|
841
|
+
validateViews(contributes.hostSections, "hostSections", errors);
|
|
842
|
+
validateOperations(contributes.operations, pluginId, errors);
|
|
731
843
|
if (contributes.guest !== undefined &&
|
|
732
844
|
typeof contributes.guest !== "boolean") {
|
|
733
845
|
errors.push('Field "contributes.guest" must be a boolean');
|
|
@@ -1660,6 +1772,7 @@ export function parseManifest(raw) {
|
|
|
1660
1772
|
errors.push(`contributes.actions["${action.id}"].permission "${action.permission}" is not declared in contributes.permissions`);
|
|
1661
1773
|
}
|
|
1662
1774
|
}
|
|
1775
|
+
checkOperationReferences(manifest.contributes?.operations, manifest.capabilities, (name) => declared.has(qualifyPermission(manifest.id, name)), errors);
|
|
1663
1776
|
// A settings field may gate on one of this plugin's own permissions, or on
|
|
1664
1777
|
// a core/other-plugin id used as given. Only the first form is checkable
|
|
1665
1778
|
// here, and an undeclared one would hide the field from every admin.
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The operations contract: what a plugin declares it can do, in one shape every
|
|
3
|
+
* surface reads (Automations, Fleets, AI, CLI, Mobile, palette, API, MCP).
|
|
4
|
+
*
|
|
5
|
+
* Declarations are data in the manifest, so a surface can list and describe an
|
|
6
|
+
* action without loading the plugin's code. Handlers are registered at
|
|
7
|
+
* activation with ctx.operations.register and must match a declaration.
|
|
8
|
+
*
|
|
9
|
+
* No Node imports: the frontend, the CLI and Mobile read this file too.
|
|
10
|
+
*/
|
|
11
|
+
import type { PluginSettingsOption } from "./manifest.js";
|
|
12
|
+
export declare const OPERATION_RISKS: readonly ["read", "change", "destructive"];
|
|
13
|
+
export type OperationRisk = (typeof OPERATION_RISKS)[number];
|
|
14
|
+
export declare const OPERATION_SCOPES: readonly ["host", "resource", "server"];
|
|
15
|
+
export type OperationScope = (typeof OPERATION_SCOPES)[number];
|
|
16
|
+
export declare const OPERATION_HOST_ACCESS: readonly ["connect", "view", "edit", "manage"];
|
|
17
|
+
export type OperationHostAccess = (typeof OPERATION_HOST_ACCESS)[number];
|
|
18
|
+
export declare const OPERATION_DRY_RUN: readonly ["native", "describe"];
|
|
19
|
+
/** native: the handler checks ctx.dryRun. describe: core reports its text. */
|
|
20
|
+
export type OperationDryRun = (typeof OPERATION_DRY_RUN)[number];
|
|
21
|
+
export declare const OPERATION_FIELD_TYPES: readonly ["boolean", "string", "number", "select", "multiselect", "secret", "textarea", "json", "resource"];
|
|
22
|
+
export type OperationFieldType = (typeof OPERATION_FIELD_TYPES)[number];
|
|
23
|
+
export declare const OPERATION_SEVERITIES: readonly ["info", "warning", "critical"];
|
|
24
|
+
export type OperationSeverity = (typeof OPERATION_SEVERITIES)[number];
|
|
25
|
+
export declare const SIGNAL_KINDS: readonly ["count", "gauge", "status", "list"];
|
|
26
|
+
export type SignalKind = (typeof SIGNAL_KINDS)[number];
|
|
27
|
+
export declare const SIGNAL_STATUSES: readonly ["ok", "warn", "fail", "unknown"];
|
|
28
|
+
export type SignalStatus = (typeof SIGNAL_STATUSES)[number];
|
|
29
|
+
export declare const RESOURCE_COLUMN_TYPES: readonly ["text", "status", "bytes", "percent", "time", "tags"];
|
|
30
|
+
export type ResourceColumnType = (typeof RESOURCE_COLUMN_TYPES)[number];
|
|
31
|
+
/** One input or output of an action. A subset of a settings field, plus rules. */
|
|
32
|
+
export interface OperationField {
|
|
33
|
+
key: string;
|
|
34
|
+
type: OperationFieldType;
|
|
35
|
+
labelKey?: string;
|
|
36
|
+
descriptionKey?: string;
|
|
37
|
+
placeholderKey?: string;
|
|
38
|
+
default?: unknown;
|
|
39
|
+
options?: PluginSettingsOption[];
|
|
40
|
+
min?: number;
|
|
41
|
+
max?: number;
|
|
42
|
+
/** Key of a boolean field in the same list that must be on. */
|
|
43
|
+
requires?: string;
|
|
44
|
+
group?: string;
|
|
45
|
+
required?: boolean;
|
|
46
|
+
/** string and textarea only. A regular expression the whole value matches. */
|
|
47
|
+
pattern?: string;
|
|
48
|
+
/** Read action whose rows fill the choices, for a list known only at run time. */
|
|
49
|
+
optionsFrom?: string;
|
|
50
|
+
/** type "resource" only: the resource id whose list fills the picker. */
|
|
51
|
+
resource?: string;
|
|
52
|
+
/** Outputs only: dropped before a run is stored or shown. */
|
|
53
|
+
sensitive?: boolean;
|
|
54
|
+
}
|
|
55
|
+
export interface OperationWhen {
|
|
56
|
+
/** Host setting values that must match, such as { enableDocker: true }. */
|
|
57
|
+
settings?: Record<string, boolean | string | number>;
|
|
58
|
+
/** Lowercase OS names. A host with no known OS passes. */
|
|
59
|
+
os?: string[];
|
|
60
|
+
}
|
|
61
|
+
export interface OperationSurfaces {
|
|
62
|
+
/** Offered to the AI assistant. Off unless set. */
|
|
63
|
+
assistant?: boolean;
|
|
64
|
+
/** Offered over MCP. Off unless set. */
|
|
65
|
+
mcp?: boolean;
|
|
66
|
+
/** Set false to keep out of the command palette. */
|
|
67
|
+
palette?: boolean;
|
|
68
|
+
/** Set false to keep out of Automations. */
|
|
69
|
+
automations?: boolean;
|
|
70
|
+
/** Set false to keep out of the phone app. */
|
|
71
|
+
mobile?: boolean;
|
|
72
|
+
/** Set false to keep out of the CLI. */
|
|
73
|
+
cli?: boolean;
|
|
74
|
+
}
|
|
75
|
+
export interface OperationAction {
|
|
76
|
+
/** `<pluginId>.<name>`. */
|
|
77
|
+
id: string;
|
|
78
|
+
/** Bump when inputs change in a way saved references cannot follow. */
|
|
79
|
+
version?: number;
|
|
80
|
+
titleKey: string;
|
|
81
|
+
descriptionKey?: string;
|
|
82
|
+
icon?: string;
|
|
83
|
+
scope: OperationScope;
|
|
84
|
+
/** Required for scope "resource": the resource this acts on. */
|
|
85
|
+
resource?: string;
|
|
86
|
+
inputs?: OperationField[];
|
|
87
|
+
outputs?: OperationField[];
|
|
88
|
+
risk: OperationRisk;
|
|
89
|
+
/** One of this plugin's own permissions, short or full. */
|
|
90
|
+
permission: string;
|
|
91
|
+
/** Lowest share level. Required for host and resource scopes. */
|
|
92
|
+
hostAccess?: OperationHostAccess;
|
|
93
|
+
dryRun?: OperationDryRun;
|
|
94
|
+
/** A read action of this plugin that proves the result. */
|
|
95
|
+
check?: string;
|
|
96
|
+
/** An action of this plugin that reverses this one. */
|
|
97
|
+
undo?: string;
|
|
98
|
+
/** Safe to run twice, so a rollout may retry it. */
|
|
99
|
+
idempotent?: boolean;
|
|
100
|
+
/** Only one such action per host at a time. */
|
|
101
|
+
exclusive?: boolean;
|
|
102
|
+
timeoutMs?: number;
|
|
103
|
+
when?: OperationWhen;
|
|
104
|
+
surfaces?: OperationSurfaces;
|
|
105
|
+
}
|
|
106
|
+
export interface OperationEvent {
|
|
107
|
+
id: string;
|
|
108
|
+
titleKey: string;
|
|
109
|
+
descriptionKey?: string;
|
|
110
|
+
severity?: OperationSeverity;
|
|
111
|
+
scope: "host" | "server";
|
|
112
|
+
payload?: OperationField[];
|
|
113
|
+
}
|
|
114
|
+
export interface ResourceColumn {
|
|
115
|
+
key: string;
|
|
116
|
+
titleKey: string;
|
|
117
|
+
type?: ResourceColumnType;
|
|
118
|
+
}
|
|
119
|
+
export interface OperationResource {
|
|
120
|
+
id: string;
|
|
121
|
+
titleKey: string;
|
|
122
|
+
icon?: string;
|
|
123
|
+
/** Read action that returns { rows: [...] } for one host. */
|
|
124
|
+
list: string;
|
|
125
|
+
/** Row field that is unique on a host. */
|
|
126
|
+
key: string;
|
|
127
|
+
/** Row field shown as the name. */
|
|
128
|
+
name: string;
|
|
129
|
+
/** Row field holding a status word. */
|
|
130
|
+
status?: string;
|
|
131
|
+
/** Row field holding a link, for auto links on the homepage. */
|
|
132
|
+
url?: string;
|
|
133
|
+
/** Tab type that opens this on a host. */
|
|
134
|
+
tab?: string;
|
|
135
|
+
columns?: ResourceColumn[];
|
|
136
|
+
filters?: string[];
|
|
137
|
+
}
|
|
138
|
+
export interface OperationSignal {
|
|
139
|
+
id: string;
|
|
140
|
+
titleKey: string;
|
|
141
|
+
/** Read action that returns a SignalResult. */
|
|
142
|
+
read: string;
|
|
143
|
+
scope: "host" | "server";
|
|
144
|
+
kind: SignalKind;
|
|
145
|
+
refreshSec?: number;
|
|
146
|
+
/** Action offered as the fix when this signal is not ok. */
|
|
147
|
+
fix?: string;
|
|
148
|
+
}
|
|
149
|
+
export interface PluginOperationsContribution {
|
|
150
|
+
actions?: OperationAction[];
|
|
151
|
+
events?: OperationEvent[];
|
|
152
|
+
resources?: OperationResource[];
|
|
153
|
+
signals?: OperationSignal[];
|
|
154
|
+
/**
|
|
155
|
+
* Actions this plugin may run through ctx.operations.invoke: exact ids,
|
|
156
|
+
* `<pluginId>.*`, or `*`.
|
|
157
|
+
*/
|
|
158
|
+
invokes?: string[];
|
|
159
|
+
}
|
|
160
|
+
/** What a signal's read action returns. */
|
|
161
|
+
export interface SignalResult {
|
|
162
|
+
value?: number | string | null;
|
|
163
|
+
status: SignalStatus;
|
|
164
|
+
label?: string;
|
|
165
|
+
items?: {
|
|
166
|
+
label: string;
|
|
167
|
+
status?: SignalStatus;
|
|
168
|
+
}[];
|
|
169
|
+
link?: {
|
|
170
|
+
tab?: string;
|
|
171
|
+
data?: Record<string, unknown>;
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** What a resource's list action returns. */
|
|
175
|
+
export interface ResourceListResult {
|
|
176
|
+
rows: Record<string, unknown>[];
|
|
177
|
+
}
|
|
178
|
+
/** What a handler returns. */
|
|
179
|
+
export interface OperationResult {
|
|
180
|
+
/** One plain line for history and per host results. */
|
|
181
|
+
summary: string;
|
|
182
|
+
/** Whether anything actually changed. */
|
|
183
|
+
changed?: boolean;
|
|
184
|
+
data?: unknown;
|
|
185
|
+
/** Input for the `undo` action that reverses this exact run. */
|
|
186
|
+
undoInput?: Record<string, unknown>;
|
|
187
|
+
}
|
|
188
|
+
export declare const OPERATION_ID_PATTERN: RegExp;
|
|
189
|
+
export declare const MAX_OPERATION_TIMEOUT_MS = 3600000;
|
|
190
|
+
export declare const REDACTED_VALUE = "[redacted]";
|
|
191
|
+
/** Shape checks for contributes.operations, run by validateManifest. */
|
|
192
|
+
export declare function validateOperations(value: unknown, pluginId: string | undefined, errors: string[]): void;
|
|
193
|
+
/**
|
|
194
|
+
* The cross-field rules: every id one declaration names must exist in this
|
|
195
|
+
* plugin, or a surface would offer a button that goes nowhere.
|
|
196
|
+
*/
|
|
197
|
+
export declare function checkOperationReferences(operations: PluginOperationsContribution | undefined, capabilities: readonly string[], hasPermission: (name: string) => boolean, errors: string[]): void;
|
|
198
|
+
/** Whether a plugin's invokes list lets it run an action. */
|
|
199
|
+
export declare function invokeAllowed(invokes: readonly string[] | undefined, actionId: string): boolean;
|
|
200
|
+
export declare function riskRank(risk: OperationRisk): number;
|
|
201
|
+
/** A surface's own switch. The assistant and MCP are opt in, the rest opt out. */
|
|
202
|
+
export declare function onSurface(action: Pick<OperationAction, "surfaces">, surface: keyof OperationSurfaces): boolean;
|
|
203
|
+
/** Whether an action applies to a host, from fields any client can read. */
|
|
204
|
+
export declare function matchesWhen(when: OperationWhen | undefined, host: {
|
|
205
|
+
settings?: Record<string, unknown>;
|
|
206
|
+
os?: string | null;
|
|
207
|
+
}): boolean;
|
|
208
|
+
export interface OperationInputResult {
|
|
209
|
+
values: Record<string, unknown>;
|
|
210
|
+
errors: string[];
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Turns raw input into validated values. Keys no field declares are refused,
|
|
214
|
+
* so nothing a handler did not ask for ever reaches it.
|
|
215
|
+
*/
|
|
216
|
+
export declare function parseOperationInput(fields: readonly OperationField[] | undefined, raw: unknown): OperationInputResult;
|
|
217
|
+
/** A copy safe to store or show: secret and sensitive values are replaced. */
|
|
218
|
+
export declare function redactFields(fields: readonly OperationField[] | undefined, values: unknown): unknown;
|
|
219
|
+
/** Trims text to a byte-friendly length with a plain marker. */
|
|
220
|
+
export declare function capText(text: string, maxLength: number): string;
|
|
221
|
+
/** JSON Schema for a field list, for AI tools, MCP and OpenAPI. */
|
|
222
|
+
export declare function toJsonSchema(fields: readonly OperationField[] | undefined, describe?: (field: OperationField) => string | undefined): Record<string, unknown>;
|