@termix-ssh/plugin-sdk 1.0.20 → 1.0.22

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/PLUGIN-GUIDE.md +66 -4
  3. package/cli/commands/build.mjs +55 -36
  4. package/cli/commands/validate.mjs +4 -0
  5. package/dist/backend.d.ts +32 -7
  6. package/dist/frontend.d.ts +7 -0
  7. package/dist/host/{PluginsTab-ZSMTJ5C5.js → PluginsTab-CM2B33BD.js} +26 -16
  8. package/dist/host/chunk-7J4QNGFM.js +345 -0
  9. package/dist/host/{chunk-J3ZQPBDK.js → chunk-HQ6NNFSX.js} +115 -1575
  10. package/dist/host/chunk-I5F2PSMR.js +1526 -0
  11. package/dist/host/{chunk-2MU6WNSX.js → chunk-JRV2HIDK.js} +1 -1
  12. package/dist/host/{chunk-7LEJ3HYF.js → chunk-P26QKKU5.js} +41 -10
  13. package/dist/host/{chunk-CITUENSU.js → chunk-ROB2SH5Q.js} +77 -49
  14. package/dist/host/chunk-TK3Y437H.js +53 -0
  15. package/dist/host/{chunk-EYONBBRJ.js → chunk-VKNGNEF3.js} +34650 -34968
  16. package/dist/host/core-classes.json +1 -1
  17. package/dist/host/frame-host.css +2 -0
  18. package/dist/host/frame-host.js +242 -0
  19. package/dist/host/{linked-server-OUF3POT7.js → linked-server-J5G457OA.js} +1 -1
  20. package/dist/host/{rbac-api-LZVENREG.js → rbac-api-PY22W5UY.js} +5 -1
  21. package/dist/host/testing-host.js +9 -6
  22. package/dist/host/types/src/types/host-sidebar-preferences.d.ts +2 -0
  23. package/dist/host/types/src/types/index.d.ts +8 -0
  24. package/dist/host/types/src/ui/api/look-api.d.ts +2 -0
  25. package/dist/host/types/src/ui/api/plugins-api.d.ts +3 -2
  26. package/dist/host/types/src/ui/api/rbac-api.d.ts +19 -1
  27. package/dist/host/types/src/ui/components/data-view.d.ts +1 -1
  28. package/dist/host/types/src/ui/lib/app-return.d.ts +13 -0
  29. package/dist/host/types/src/ui/main-axios.d.ts +1 -1
  30. package/dist/host/types/src/ui/plugin-host/loader.d.ts +6 -0
  31. package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +1 -1
  32. package/dist/host/types/src/ui/plugins/plugin-model.d.ts +1 -1
  33. package/dist/host/types/src/ui/plugins/use-plugins-manager.d.ts +1 -1
  34. package/dist/host/types/src/ui/shell/tab-registry.d.ts +2 -0
  35. package/dist/host/ui.js +418 -388
  36. package/dist/host/{use-permissions-BYCDS5PD.js → use-permissions-EWRI3WLT.js} +1 -1
  37. package/dist/host-commands.d.ts +9 -6
  38. package/dist/host-commands.js +38 -11
  39. package/dist/host-scripts/docker.d.ts +137 -0
  40. package/dist/host-scripts/docker.js +345 -0
  41. package/dist/host-scripts/index.d.ts +15 -0
  42. package/dist/host-scripts/index.js +15 -0
  43. package/dist/host-scripts/journal.d.ts +45 -0
  44. package/dist/host-scripts/journal.js +194 -0
  45. package/dist/host-scripts/metrics.d.ts +134 -0
  46. package/dist/host-scripts/metrics.js +320 -0
  47. package/dist/host-scripts/openrc.d.ts +6 -0
  48. package/dist/host-scripts/openrc.js +90 -0
  49. package/dist/host-scripts/shell.d.ts +43 -0
  50. package/dist/host-scripts/shell.js +95 -0
  51. package/dist/host-scripts/systemd.d.ts +73 -0
  52. package/dist/host-scripts/systemd.js +320 -0
  53. package/dist/host-scripts/text.d.ts +23 -0
  54. package/dist/host-scripts/text.js +111 -0
  55. package/dist/manifest.d.ts +28 -1
  56. package/dist/manifest.js +108 -1
  57. package/package.json +9 -1
  58. package/schema/manifest.schema.json +42 -0
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Quoting, encoding and the error type the host scripts share.
3
+ *
4
+ * No Node APIs here: this entry also runs in the mobile app.
5
+ */
6
+ /** A bad input to a host script builder. `status` is the HTTP status to answer with. */
7
+ export class HostScriptError extends Error {
8
+ status;
9
+ extra;
10
+ constructor(message, status = 400, extra = {}) {
11
+ super(message);
12
+ this.status = status;
13
+ this.extra = extra;
14
+ this.name = "HostScriptError";
15
+ }
16
+ }
17
+ /** Wraps a value in single quotes for a POSIX shell. */
18
+ export function shellQuote(value) {
19
+ return `'${value.replace(/'/g, `'\\''`)}'`;
20
+ }
21
+ // Written by hand because some JS engines (Hermes) have no TextDecoder.
22
+ /** UTF-8 bytes of a string. */
23
+ export function utf8Encode(text) {
24
+ const out = [];
25
+ for (const char of text) {
26
+ const cp = char.codePointAt(0) ?? 0xfffd;
27
+ if (cp < 0x80)
28
+ out.push(cp);
29
+ else if (cp < 0x800)
30
+ out.push(0xc0 | (cp >> 6), 0x80 | (cp & 63));
31
+ else if (cp < 0x10000) {
32
+ out.push(0xe0 | (cp >> 12), 0x80 | ((cp >> 6) & 63), 0x80 | (cp & 63));
33
+ }
34
+ else {
35
+ out.push(0xf0 | (cp >> 18), 0x80 | ((cp >> 12) & 63), 0x80 | ((cp >> 6) & 63), 0x80 | (cp & 63));
36
+ }
37
+ }
38
+ return Uint8Array.from(out);
39
+ }
40
+ /** A string from UTF-8 bytes. Bad sequences become U+FFFD. */
41
+ export function utf8Decode(bytes) {
42
+ let out = "";
43
+ let i = 0;
44
+ while (i < bytes.length) {
45
+ const b = bytes[i];
46
+ let need = 0;
47
+ let cp = 0;
48
+ if (b < 0x80)
49
+ cp = b;
50
+ else if (b >= 0xc2 && b < 0xe0)
51
+ [need, cp] = [1, b & 31];
52
+ else if (b >= 0xe0 && b < 0xf0)
53
+ [need, cp] = [2, b & 15];
54
+ else if (b >= 0xf0 && b < 0xf5)
55
+ [need, cp] = [3, b & 7];
56
+ else {
57
+ out += "\ufffd";
58
+ i++;
59
+ continue;
60
+ }
61
+ let j = 1;
62
+ for (; j <= need; j++) {
63
+ const next = bytes[i + j];
64
+ if (next === undefined || (next & 0xc0) !== 0x80)
65
+ break;
66
+ cp = (cp << 6) | (next & 63);
67
+ }
68
+ const tooSmall = [0, 0x80, 0x800, 0x10000][need];
69
+ if (j <= need ||
70
+ cp < tooSmall ||
71
+ cp > 0x10ffff ||
72
+ (cp >= 0xd800 && cp < 0xe000)) {
73
+ out += "\ufffd";
74
+ i += Math.max(1, j);
75
+ continue;
76
+ }
77
+ out += String.fromCodePoint(cp);
78
+ i += need + 1;
79
+ }
80
+ return out;
81
+ }
82
+ const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
83
+ /** Base64 of the UTF-8 bytes of a string, for `base64 -d` on the host. */
84
+ export function toBase64(text) {
85
+ const bytes = utf8Encode(text);
86
+ let out = "";
87
+ for (let i = 0; i < bytes.length; i += 3) {
88
+ const a = bytes[i];
89
+ const b = i + 1 < bytes.length ? bytes[i + 1] : 0;
90
+ const c = i + 2 < bytes.length ? bytes[i + 2] : 0;
91
+ const n = (a << 16) | (b << 8) | c;
92
+ out += B64[(n >> 18) & 63] + B64[(n >> 12) & 63];
93
+ out += i + 1 < bytes.length ? B64[(n >> 6) & 63] : "=";
94
+ out += i + 2 < bytes.length ? B64[n & 63] : "=";
95
+ }
96
+ return out;
97
+ }
98
+ /** Reads key=value lines. Only the first = splits a line. */
99
+ export function parseKeys(output) {
100
+ const out = {};
101
+ for (const line of output.split("\n")) {
102
+ const eq = line.indexOf("=");
103
+ if (eq > 0)
104
+ out[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
105
+ }
106
+ return out;
107
+ }
108
+ /** Trims output and keeps it short enough to show. */
109
+ export function scrub(text, max = 600) {
110
+ return text.trim().slice(0, max);
111
+ }
@@ -20,7 +20,7 @@ export { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsPage, pluginDoc
20
20
  * "^1.2" needs 1.2 or later). engine.termix is checked against the core
21
21
  * version by the server.
22
22
  */
23
- export declare const PLUGIN_API_VERSION = "1.12.0";
23
+ export declare const PLUGIN_API_VERSION = "1.13.0";
24
24
  /** The API major, the default engine.api for a new plugin. */
25
25
  export declare const SUPPORTED_PLUGIN_API_VERSION: string;
26
26
  /** Whether this build's plugin API satisfies a manifest's engine.api. */
@@ -363,6 +363,33 @@ export interface PluginContributions {
363
363
  layouts?: PluginLayoutContribution[];
364
364
  /** Themes. Needs the ui:theme capability. */
365
365
  themes?: PluginThemeContribution[];
366
+ /** How the plugin shows in the Termix mobile app. Plugin API 1.13. */
367
+ mobile?: PluginMobileContribution;
368
+ }
369
+ /**
370
+ * - "webview": the app shows the plugin's UI from the server in a WebView.
371
+ * - "native": the app has its own screens for this plugin.
372
+ * - "none": nothing to show in the app.
373
+ */
374
+ export declare const MOBILE_MODES: readonly ["webview", "native", "none"];
375
+ export type PluginMobileMode = (typeof MOBILE_MODES)[number];
376
+ export declare const MOBILE_SIZES: readonly ["phone", "tablet"];
377
+ export type PluginMobileSize = (typeof MOBILE_SIZES)[number];
378
+ export interface PluginMobileContribution {
379
+ mode: PluginMobileMode;
380
+ /**
381
+ * webview: a bundle built for the app, in the same folder as the frontend
382
+ * bundle. Unset means the normal frontend runs in a phone sized frame.
383
+ */
384
+ entry?: string;
385
+ /** webview: screen sizes the UI works at. Defaults to both. */
386
+ sizes?: PluginMobileSize[];
387
+ /** webview: tab, panel or card ids the app offers. Defaults to every tab. */
388
+ views?: string[];
389
+ /** native: the app feature that stands in for it. Defaults to the plugin id. */
390
+ feature?: string;
391
+ /** i18n key in the plugin's namespace, or plain text, shown in the app. */
392
+ note?: string;
366
393
  }
367
394
  /** A field a protocol login carries besides username and password. */
368
395
  export interface PluginProtocolCredentialField {
package/dist/manifest.js CHANGED
@@ -22,7 +22,7 @@ export { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsPage, pluginDoc
22
22
  * "^1.2" needs 1.2 or later). engine.termix is checked against the core
23
23
  * version by the server.
24
24
  */
25
- export const PLUGIN_API_VERSION = "1.12.0";
25
+ export const PLUGIN_API_VERSION = "1.13.0";
26
26
  /** The API major, the default engine.api for a new plugin. */
27
27
  export const SUPPORTED_PLUGIN_API_VERSION = String(semver.major(PLUGIN_API_VERSION));
28
28
  /** Whether this build's plugin API satisfies a manifest's engine.api. */
@@ -171,6 +171,13 @@ export const SETTINGS_FIELD_TYPES = [
171
171
  export const SETTINGS_SCOPES = ["admin", "user", "host"];
172
172
  /** A layout id or theme id: short, lowercase, used as a CSS class for themes. */
173
173
  export const LOOK_ID_PATTERN = /^[a-z][a-z0-9-]{1,39}$/;
174
+ /**
175
+ * - "webview": the app shows the plugin's UI from the server in a WebView.
176
+ * - "native": the app has its own screens for this plugin.
177
+ * - "none": nothing to show in the app.
178
+ */
179
+ export const MOBILE_MODES = ["webview", "native", "none"];
180
+ export const MOBILE_SIZES = ["phone", "tablet"];
174
181
  /** Protocol ids core keeps for itself. */
175
182
  export const RESERVED_PROTOCOL_IDS = ["ssh"];
176
183
  /** Login keys a declared credential field may not reuse. */
@@ -254,6 +261,7 @@ const ALLOWED_CONTRIBUTES = new Set([
254
261
  "protocols",
255
262
  "layouts",
256
263
  "themes",
264
+ "mobile",
257
265
  ]);
258
266
  const ALLOWED_SETTINGS_FIELD = [
259
267
  "key",
@@ -387,8 +395,107 @@ export function validateManifest(manifest) {
387
395
  validateProvidesSecret(m.providesSecret, errors);
388
396
  validateRequiresSecret(m.requiresSecret, errors);
389
397
  validateContributes(m.contributes, typeof m.id === "string" ? m.id : undefined, errors);
398
+ if (isPlainObject(m.contributes)) {
399
+ validateMobile(m.contributes, typeof m.frontend === "string" ? m.frontend : DEFAULT_FRONTEND_ENTRY, errors);
400
+ }
390
401
  return errors;
391
402
  }
403
+ function folderOf(file) {
404
+ const parts = file.split("/").filter((part) => part !== "" && part !== ".");
405
+ return parts.slice(0, -1).join("/");
406
+ }
407
+ function validateMobile(contributes, frontend, errors) {
408
+ const mobile = contributes.mobile;
409
+ if (mobile === undefined)
410
+ return;
411
+ const where = "contributes.mobile";
412
+ if (!isPlainObject(mobile)) {
413
+ errors.push(`Field "${where}" must be an object`);
414
+ return;
415
+ }
416
+ rejectUnknown(mobile, ["mode", "entry", "sizes", "views", "feature", "note"], `"${where}"`, errors);
417
+ const mode = mobile.mode;
418
+ if (!MOBILE_MODES.includes(mode)) {
419
+ errors.push(`${where}.mode must be one of: ${MOBILE_MODES.join(", ")}`);
420
+ return;
421
+ }
422
+ for (const key of ["entry", "sizes", "views"]) {
423
+ if (mobile[key] !== undefined && mode !== "webview") {
424
+ errors.push(`${where}.${key} is only for mode "webview"`);
425
+ }
426
+ }
427
+ if (mobile.feature !== undefined && mode !== "native") {
428
+ errors.push(`${where}.feature is only for mode "native"`);
429
+ }
430
+ if (mobile.entry !== undefined &&
431
+ requireString(mobile.entry, `${where}.entry`, errors)) {
432
+ const before = errors.length;
433
+ requireRelativePath(mobile.entry, `${where}.entry`, errors);
434
+ if (!mobile.entry.endsWith(".js")) {
435
+ errors.push(`${where}.entry must be a .js file`);
436
+ }
437
+ else if (errors.length === before &&
438
+ folderOf(mobile.entry) !== folderOf(frontend)) {
439
+ // Core only serves the folder the frontend bundle sits in.
440
+ errors.push(`${where}.entry must sit in the same folder as the frontend bundle (${frontend})`);
441
+ }
442
+ }
443
+ if (mobile.sizes !== undefined) {
444
+ const sizes = mobile.sizes;
445
+ if (!Array.isArray(sizes) || sizes.length === 0) {
446
+ errors.push(`${where}.sizes must be a non-empty array`);
447
+ }
448
+ else {
449
+ const seen = new Set();
450
+ for (const size of sizes) {
451
+ if (!MOBILE_SIZES.includes(size)) {
452
+ errors.push(`${where}.sizes entries must be one of: ${MOBILE_SIZES.join(", ")}`);
453
+ }
454
+ else if (seen.has(size)) {
455
+ errors.push(`${where}.sizes duplicates "${String(size)}"`);
456
+ }
457
+ seen.add(size);
458
+ }
459
+ }
460
+ }
461
+ if (mobile.views !== undefined) {
462
+ const views = mobile.views;
463
+ if (!Array.isArray(views) || views.length === 0) {
464
+ errors.push(`${where}.views must be a non-empty array`);
465
+ }
466
+ else {
467
+ const declared = new Set();
468
+ for (const field of ["tabs", "panels", "dashboardCards"]) {
469
+ const list = contributes[field];
470
+ if (!Array.isArray(list))
471
+ continue;
472
+ for (const view of list) {
473
+ if (isPlainObject(view) && typeof view.id === "string") {
474
+ declared.add(view.id);
475
+ }
476
+ }
477
+ }
478
+ const seen = new Set();
479
+ for (const view of views) {
480
+ if (typeof view !== "string" || !declared.has(view)) {
481
+ errors.push(`${where}.views entries must be ids from contributes.tabs, panels or dashboardCards, got: ${JSON.stringify(view)}`);
482
+ }
483
+ else if (seen.has(view)) {
484
+ errors.push(`${where}.views duplicates "${view}"`);
485
+ }
486
+ seen.add(view);
487
+ }
488
+ }
489
+ }
490
+ if (mobile.feature !== undefined &&
491
+ (typeof mobile.feature !== "string" ||
492
+ !LOOK_ID_PATTERN.test(mobile.feature))) {
493
+ errors.push(`${where}.feature must match ${LOOK_ID_PATTERN} (lowercase letters, digits, dashes)`);
494
+ }
495
+ if (mobile.note !== undefined) {
496
+ requireString(mobile.note, `${where}.note`, errors);
497
+ }
498
+ }
392
499
  function validateFeatures(features, errors) {
393
500
  if (features === undefined)
394
501
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termix-ssh/plugin-sdk",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
4
4
  "description": "The contract between Termix core and a plugin: types, the capability catalog and the manifest schema.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -81,6 +81,10 @@
81
81
  "types": "./dist/host-commands.d.ts",
82
82
  "default": "./dist/host-commands.js"
83
83
  },
84
+ "./host-scripts": {
85
+ "types": "./dist/host-scripts/index.d.ts",
86
+ "default": "./dist/host-scripts/index.js"
87
+ },
84
88
  "./ssh-certs": {
85
89
  "types": "./dist/ssh-certs.d.ts",
86
90
  "default": "./dist/ssh-certs.js"
@@ -98,6 +102,10 @@
98
102
  "./testing-host": {
99
103
  "default": "./dist/host/testing-host.js"
100
104
  },
105
+ "./frame-host": {
106
+ "default": "./dist/host/frame-host.js"
107
+ },
108
+ "./frame-host.css": "./dist/host/frame-host.css",
101
109
  "./theme.css": "./dist/host/theme.css"
102
110
  },
103
111
  "files": [
@@ -1239,6 +1239,48 @@
1239
1239
  }
1240
1240
  }
1241
1241
  }
1242
+ },
1243
+ "mobile": {
1244
+ "type": "object",
1245
+ "description": "How the plugin shows in the Termix mobile app. Needs plugin API 1.13 (Termix 26.10.6).",
1246
+ "additionalProperties": false,
1247
+ "required": ["mode"],
1248
+ "properties": {
1249
+ "mode": {
1250
+ "type": "string",
1251
+ "enum": ["webview", "native", "none"],
1252
+ "description": "webview: the app shows the plugin's UI from the server. native: the app has its own screens. none: nothing to show in the app."
1253
+ },
1254
+ "entry": {
1255
+ "type": "string",
1256
+ "pattern": "\\.js$",
1257
+ "description": "webview only. A bundle built for the app, in the same folder as the frontend bundle. Unset means the normal frontend runs in a phone sized frame."
1258
+ },
1259
+ "sizes": {
1260
+ "type": "array",
1261
+ "minItems": 1,
1262
+ "uniqueItems": true,
1263
+ "items": { "type": "string", "enum": ["phone", "tablet"] },
1264
+ "description": "webview only. Screen sizes the UI works at. Defaults to both."
1265
+ },
1266
+ "views": {
1267
+ "type": "array",
1268
+ "minItems": 1,
1269
+ "uniqueItems": true,
1270
+ "items": { "type": "string", "minLength": 1 },
1271
+ "description": "webview only. Tab, panel or card ids the app offers. Defaults to every tab."
1272
+ },
1273
+ "feature": {
1274
+ "type": "string",
1275
+ "pattern": "^[a-z][a-z0-9-]{1,39}$",
1276
+ "description": "native only. The app feature that stands in for this plugin. Defaults to the plugin id."
1277
+ },
1278
+ "note": {
1279
+ "type": "string",
1280
+ "minLength": 1,
1281
+ "description": "i18n key in the plugin's namespace, or plain text, shown in the app."
1282
+ }
1283
+ }
1242
1284
  }
1243
1285
  }
1244
1286
  },