@camstack/addon-terminal 0.1.10 → 0.1.11
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/dist/addon.js +227 -21
- package/dist/addon.mjs +227 -21
- package/package.json +7 -3
- package/python/requirements.txt +1 -0
package/dist/addon.js
CHANGED
|
@@ -21149,6 +21149,7 @@ var FaceInfoSchema = object({
|
|
|
21149
21149
|
var FaceFilterEnum = _enum([
|
|
21150
21150
|
"unassigned",
|
|
21151
21151
|
"recognized",
|
|
21152
|
+
"identified",
|
|
21152
21153
|
"all"
|
|
21153
21154
|
]);
|
|
21154
21155
|
var MediaFileLiteSchema$1 = object({
|
|
@@ -21177,6 +21178,8 @@ method(_void(), array(IdentitySchema).readonly()), method(object({ name: string(
|
|
|
21177
21178
|
kind: "mutation",
|
|
21178
21179
|
auth: "admin"
|
|
21179
21180
|
}), method(object({
|
|
21181
|
+
/** Restrict to one camera. Absent keeps the cluster-wide gallery view. */
|
|
21182
|
+
deviceId: number().int().optional(),
|
|
21180
21183
|
limit: number().int().positive().optional(),
|
|
21181
21184
|
filter: FaceFilterEnum.optional(),
|
|
21182
21185
|
/**
|
|
@@ -23406,6 +23409,10 @@ var OsdSourceSchema = discriminatedUnion("kind", [
|
|
|
23406
23409
|
capName: string().min(1).max(64),
|
|
23407
23410
|
/** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
|
|
23408
23411
|
valuePath: string().min(1).max(64)
|
|
23412
|
+
}),
|
|
23413
|
+
object({
|
|
23414
|
+
kind: literal("latest-recognition"),
|
|
23415
|
+
recognition: _enum(["person", "plate"])
|
|
23409
23416
|
})
|
|
23410
23417
|
]);
|
|
23411
23418
|
var OsdSlotBindingSchema = object({
|
|
@@ -23511,6 +23518,15 @@ method(object({ deviceId: number().int() }), object({
|
|
|
23511
23518
|
}), object({ success: literal(true) }), {
|
|
23512
23519
|
kind: "mutation",
|
|
23513
23520
|
auth: "admin"
|
|
23521
|
+
}), method(object({
|
|
23522
|
+
sourceDeviceId: number().int(),
|
|
23523
|
+
targetDeviceId: number().int()
|
|
23524
|
+
}), object({
|
|
23525
|
+
copied: number().int().nonnegative(),
|
|
23526
|
+
skipped: number().int().nonnegative()
|
|
23527
|
+
}), {
|
|
23528
|
+
kind: "mutation",
|
|
23529
|
+
auth: "admin"
|
|
23514
23530
|
}), method(object({
|
|
23515
23531
|
deviceId: number().int(),
|
|
23516
23532
|
slotId: string().min(1),
|
|
@@ -30204,6 +30220,12 @@ Object.freeze({
|
|
|
30204
30220
|
addonId: null,
|
|
30205
30221
|
access: "delete"
|
|
30206
30222
|
},
|
|
30223
|
+
"osdManager.copyDeviceConfiguration": {
|
|
30224
|
+
capName: "osd-manager",
|
|
30225
|
+
capScope: "system",
|
|
30226
|
+
addonId: null,
|
|
30227
|
+
access: "create"
|
|
30228
|
+
},
|
|
30207
30229
|
"osdManager.getConditionSupport": {
|
|
30208
30230
|
capName: "osd-manager",
|
|
30209
30231
|
capScope: "system",
|
|
@@ -33056,6 +33078,22 @@ function createNodePtySpawner() {
|
|
|
33056
33078
|
async function warmNodePty() {
|
|
33057
33079
|
await loadNodePty();
|
|
33058
33080
|
}
|
|
33081
|
+
//#endregion
|
|
33082
|
+
//#region src/terminal-camera-declarations.ts
|
|
33083
|
+
function buildTerminalCameraDeclarations(nodes, profilesByNode) {
|
|
33084
|
+
return nodes.flatMap((node) => {
|
|
33085
|
+
const nodeLabel = node.isHub ? "Hub" : node.name;
|
|
33086
|
+
return (profilesByNode.get(node.id) ?? []).map((profile) => ({
|
|
33087
|
+
stableId: profile.profileId === "monitor" ? `terminal-camera-${node.id}` : `terminal-camera-${node.id}-${profile.profileId}`,
|
|
33088
|
+
name: `Terminal ${profile.label} - ${nodeLabel}`,
|
|
33089
|
+
config: {
|
|
33090
|
+
nodeId: node.id,
|
|
33091
|
+
profileId: profile.profileId,
|
|
33092
|
+
profileLabel: profile.label
|
|
33093
|
+
}
|
|
33094
|
+
}));
|
|
33095
|
+
});
|
|
33096
|
+
}
|
|
33059
33097
|
function escapeXml(value) {
|
|
33060
33098
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
33061
33099
|
}
|
|
@@ -33070,7 +33108,11 @@ async function renderTerminalJpeg(lines) {
|
|
|
33070
33108
|
}
|
|
33071
33109
|
//#endregion
|
|
33072
33110
|
//#region src/terminal-camera-device.ts
|
|
33073
|
-
var terminalCameraSchema = object({
|
|
33111
|
+
var terminalCameraSchema = object({
|
|
33112
|
+
nodeId: string().min(1),
|
|
33113
|
+
profileId: string().min(1),
|
|
33114
|
+
profileLabel: string().min(1)
|
|
33115
|
+
});
|
|
33074
33116
|
var relay = null;
|
|
33075
33117
|
function installTerminalCameraRelay(next) {
|
|
33076
33118
|
relay = next;
|
|
@@ -33089,18 +33131,19 @@ var TerminalCameraDevice = class extends BaseDevice {
|
|
|
33089
33131
|
const activeRelay = relay;
|
|
33090
33132
|
if (!activeRelay) throw new Error("terminal camera relay is unavailable");
|
|
33091
33133
|
const nodeId = this.config.get("nodeId");
|
|
33092
|
-
|
|
33093
|
-
|
|
33134
|
+
const profileId = this.config.get("profileId");
|
|
33135
|
+
return [{
|
|
33136
|
+
camStreamId: profileId,
|
|
33094
33137
|
kind: "pull-http",
|
|
33095
|
-
url: activeRelay.streamUrl(nodeId,
|
|
33138
|
+
url: activeRelay.streamUrl(nodeId, profileId),
|
|
33096
33139
|
codec: "h264",
|
|
33097
33140
|
resolution: {
|
|
33098
33141
|
width: 960,
|
|
33099
33142
|
height: 640
|
|
33100
33143
|
},
|
|
33101
33144
|
fps: 2,
|
|
33102
|
-
label:
|
|
33103
|
-
}
|
|
33145
|
+
label: this.config.get("profileLabel")
|
|
33146
|
+
}];
|
|
33104
33147
|
}
|
|
33105
33148
|
setNodeOnline(online) {
|
|
33106
33149
|
this.markOnline(online);
|
|
@@ -38272,7 +38315,12 @@ function createTerminalDataPlaneHandler(manager) {
|
|
|
38272
38315
|
var ProfileIdSchema = string().trim().min(1).max(64).regex(/^[a-z][a-z0-9-]*$/, "must be a lowercase slug");
|
|
38273
38316
|
var ConfiguredTerminalProfileSchema = object({
|
|
38274
38317
|
enabled: boolean().default(true),
|
|
38275
|
-
profileId: ProfileIdSchema.refine((id) =>
|
|
38318
|
+
profileId: ProfileIdSchema.refine((id) => ![
|
|
38319
|
+
"monitor",
|
|
38320
|
+
"top",
|
|
38321
|
+
"glances",
|
|
38322
|
+
"shell"
|
|
38323
|
+
].includes(id), { message: "monitor, top, glances and shell are reserved profile IDs" }),
|
|
38276
38324
|
label: string().trim().min(1).max(100),
|
|
38277
38325
|
description: string().trim().max(500).optional().default(""),
|
|
38278
38326
|
executable: string().trim().min(1).max(1024).refine((value) => !value.includes("\0"), { message: "must not contain NUL bytes" }),
|
|
@@ -38287,17 +38335,36 @@ var ConfiguredTerminalProfileSchema = object({
|
|
|
38287
38335
|
* surface.
|
|
38288
38336
|
*/
|
|
38289
38337
|
function buildProfiles(options) {
|
|
38290
|
-
const profiles = [
|
|
38338
|
+
const profiles = [];
|
|
38339
|
+
if (options.btmEnabled !== false) profiles.push({
|
|
38291
38340
|
profileId: "monitor",
|
|
38292
|
-
label: "
|
|
38341
|
+
label: "BTM",
|
|
38293
38342
|
description: "bottom (btm) — CPU, memory, network and process monitor",
|
|
38294
38343
|
file: options.btmPath.trim().length > 0 ? options.btmPath.trim() : "btm",
|
|
38295
|
-
args: []
|
|
38296
|
-
|
|
38297
|
-
|
|
38298
|
-
|
|
38299
|
-
|
|
38300
|
-
|
|
38344
|
+
args: options.btmArgs ?? []
|
|
38345
|
+
});
|
|
38346
|
+
if (options.topEnabled !== false) profiles.push({
|
|
38347
|
+
profileId: "top",
|
|
38348
|
+
label: "Top",
|
|
38349
|
+
description: "The operating system process and resource monitor",
|
|
38350
|
+
file: options.topPath?.trim() || "top",
|
|
38351
|
+
args: options.topArgs ?? []
|
|
38352
|
+
});
|
|
38353
|
+
if (options.glancesEnabled !== false) {
|
|
38354
|
+
const configuredPath = options.glancesPath?.trim();
|
|
38355
|
+
const pythonPath = options.glancesPythonPath?.trim();
|
|
38356
|
+
profiles.push({
|
|
38357
|
+
profileId: "glances",
|
|
38358
|
+
label: "Glances",
|
|
38359
|
+
description: "Cross-platform curses monitor installed in CamStack embedded Python",
|
|
38360
|
+
file: configuredPath || pythonPath || "glances",
|
|
38361
|
+
args: configuredPath || !pythonPath ? options.glancesArgs ?? [] : [
|
|
38362
|
+
"-m",
|
|
38363
|
+
"glances",
|
|
38364
|
+
...options.glancesArgs ?? []
|
|
38365
|
+
]
|
|
38366
|
+
});
|
|
38367
|
+
}
|
|
38301
38368
|
if (options.allowShell) profiles.push({
|
|
38302
38369
|
profileId: "shell",
|
|
38303
38370
|
label: "Shell (interactive)",
|
|
@@ -38370,6 +38437,15 @@ var TerminalSessionManager = class {
|
|
|
38370
38437
|
this.opts = opts;
|
|
38371
38438
|
this.profiles = buildProfiles({
|
|
38372
38439
|
btmPath: opts.btmPath,
|
|
38440
|
+
btmEnabled: opts.btmEnabled,
|
|
38441
|
+
btmArgs: opts.btmArgs,
|
|
38442
|
+
topEnabled: opts.topEnabled,
|
|
38443
|
+
topPath: opts.topPath,
|
|
38444
|
+
topArgs: opts.topArgs,
|
|
38445
|
+
glancesEnabled: opts.glancesEnabled,
|
|
38446
|
+
glancesPath: opts.glancesPath,
|
|
38447
|
+
glancesArgs: opts.glancesArgs,
|
|
38448
|
+
glancesPythonPath: opts.glancesPythonPath,
|
|
38373
38449
|
allowShell: opts.allowShell,
|
|
38374
38450
|
shellPath: opts.shellPath,
|
|
38375
38451
|
customProfiles: opts.customProfiles,
|
|
@@ -38623,6 +38699,14 @@ var TerminalSessionManager = class {
|
|
|
38623
38699
|
//#region src/addon.ts
|
|
38624
38700
|
var DEFAULTS = {
|
|
38625
38701
|
btmPath: "",
|
|
38702
|
+
btmEnabled: true,
|
|
38703
|
+
btmArgs: [],
|
|
38704
|
+
topEnabled: true,
|
|
38705
|
+
topPath: "",
|
|
38706
|
+
topArgs: [],
|
|
38707
|
+
glancesEnabled: true,
|
|
38708
|
+
glancesPath: "",
|
|
38709
|
+
glancesArgs: [],
|
|
38626
38710
|
allowShell: false,
|
|
38627
38711
|
shellPath: "",
|
|
38628
38712
|
maxSessions: 4,
|
|
@@ -38636,17 +38720,29 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38636
38720
|
dataPlane = null;
|
|
38637
38721
|
cameraRelay = null;
|
|
38638
38722
|
cameraReconcileTimer = null;
|
|
38723
|
+
cameraProfilesByNode = /* @__PURE__ */ new Map();
|
|
38724
|
+
glancesPythonPath = "";
|
|
38639
38725
|
constructor() {
|
|
38640
38726
|
super({ ...DEFAULTS });
|
|
38641
38727
|
}
|
|
38642
38728
|
async onInitialize() {
|
|
38643
38729
|
await warmNodePty();
|
|
38730
|
+
this.glancesPythonPath = await this.ctx.deps.ensurePython() ?? "";
|
|
38644
38731
|
const manager = new TerminalSessionManager({
|
|
38645
38732
|
spawn: createNodePtySpawner(),
|
|
38646
38733
|
screenFactory: createXtermScreen,
|
|
38647
38734
|
resolveBinary: resolveExecutable,
|
|
38648
38735
|
logger: this.ctx.logger,
|
|
38649
38736
|
btmPath: this.config.btmPath,
|
|
38737
|
+
btmEnabled: this.config.btmEnabled,
|
|
38738
|
+
btmArgs: this.config.btmArgs,
|
|
38739
|
+
topEnabled: this.config.topEnabled,
|
|
38740
|
+
topPath: this.config.topPath,
|
|
38741
|
+
topArgs: this.config.topArgs,
|
|
38742
|
+
glancesEnabled: this.config.glancesEnabled,
|
|
38743
|
+
glancesPath: this.config.glancesPath,
|
|
38744
|
+
glancesArgs: this.config.glancesArgs,
|
|
38745
|
+
glancesPythonPath: this.glancesPythonPath,
|
|
38650
38746
|
allowShell: this.config.allowShell,
|
|
38651
38747
|
shellPath: this.config.shellPath,
|
|
38652
38748
|
maxSessions: this.config.maxSessions,
|
|
@@ -38691,6 +38787,15 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38691
38787
|
async onConfigChanged() {
|
|
38692
38788
|
this.manager?.reconfigureProfiles({
|
|
38693
38789
|
btmPath: this.config.btmPath,
|
|
38790
|
+
btmEnabled: this.config.btmEnabled,
|
|
38791
|
+
btmArgs: this.config.btmArgs,
|
|
38792
|
+
topEnabled: this.config.topEnabled,
|
|
38793
|
+
topPath: this.config.topPath,
|
|
38794
|
+
topArgs: this.config.topArgs,
|
|
38795
|
+
glancesEnabled: this.config.glancesEnabled,
|
|
38796
|
+
glancesPath: this.config.glancesPath,
|
|
38797
|
+
glancesArgs: this.config.glancesArgs,
|
|
38798
|
+
glancesPythonPath: this.glancesPythonPath,
|
|
38694
38799
|
allowShell: this.config.allowShell,
|
|
38695
38800
|
shellPath: this.config.shellPath,
|
|
38696
38801
|
maxSessions: this.config.maxSessions,
|
|
@@ -38715,6 +38820,44 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38715
38820
|
const topology = await this.ctx.api.nodes.topology.query();
|
|
38716
38821
|
if (topology.length === 0) throw new Error("cluster topology returned no nodes; refusing to withdraw declared cameras");
|
|
38717
38822
|
const nodes = topology.filter((node) => typeof node.id === "string" && node.id.length > 0);
|
|
38823
|
+
const unavailableNodeIds = /* @__PURE__ */ new Set();
|
|
38824
|
+
await Promise.all(nodes.map(async (node) => {
|
|
38825
|
+
try {
|
|
38826
|
+
this.cameraProfilesByNode.set(node.id, await this.cameraRelay.listProfiles(node.id));
|
|
38827
|
+
} catch (error) {
|
|
38828
|
+
if (!this.cameraProfilesByNode.has(node.id)) {
|
|
38829
|
+
unavailableNodeIds.add(node.id);
|
|
38830
|
+
this.ctx.logger.warn("terminal profiles unavailable — preserving existing cameras for this node", { meta: {
|
|
38831
|
+
nodeId: node.id,
|
|
38832
|
+
error: error instanceof Error ? error.message : String(error)
|
|
38833
|
+
} });
|
|
38834
|
+
}
|
|
38835
|
+
}
|
|
38836
|
+
}));
|
|
38837
|
+
const cameraDeclarations = [...buildTerminalCameraDeclarations(nodes, this.cameraProfilesByNode)];
|
|
38838
|
+
if (unavailableNodeIds.size > 0) {
|
|
38839
|
+
const existing = await this.ctx.api.deviceManager.listAll.query({ addonId: this.ctx.id });
|
|
38840
|
+
const unavailableNodes = nodes.filter((node) => unavailableNodeIds.has(node.id)).sort((left, right) => right.id.length - left.id.length);
|
|
38841
|
+
for (const row of existing) {
|
|
38842
|
+
const node = unavailableNodes.find((candidate) => {
|
|
38843
|
+
const base = `terminal-camera-${candidate.id}`;
|
|
38844
|
+
return row.stableId === base || row.stableId.startsWith(`${base}-`);
|
|
38845
|
+
});
|
|
38846
|
+
if (!node || cameraDeclarations.some((camera) => camera.stableId === row.stableId)) continue;
|
|
38847
|
+
const base = `terminal-camera-${node.id}`;
|
|
38848
|
+
const profileId = row.stableId === base ? "monitor" : row.stableId.slice(base.length + 1);
|
|
38849
|
+
const profileLabel = profileId === "monitor" ? "BTM" : profileId;
|
|
38850
|
+
cameraDeclarations.push({
|
|
38851
|
+
stableId: row.stableId,
|
|
38852
|
+
name: `Terminal ${profileLabel} - ${node.isHub ? "Hub" : node.name}`,
|
|
38853
|
+
config: {
|
|
38854
|
+
nodeId: node.id,
|
|
38855
|
+
profileId,
|
|
38856
|
+
profileLabel
|
|
38857
|
+
}
|
|
38858
|
+
});
|
|
38859
|
+
}
|
|
38860
|
+
}
|
|
38718
38861
|
const result = await new DeclaredDevices({
|
|
38719
38862
|
logger: this.ctx.logger.child("camera-declaration"),
|
|
38720
38863
|
addonId: this.ctx.id,
|
|
@@ -38733,12 +38876,12 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38733
38876
|
}).reconcile({
|
|
38734
38877
|
integrationName: TERMINAL_CAMERA_INTEGRATION,
|
|
38735
38878
|
placement: "hub",
|
|
38736
|
-
devices:
|
|
38737
|
-
stableId:
|
|
38738
|
-
name:
|
|
38879
|
+
devices: cameraDeclarations.map((camera) => ({
|
|
38880
|
+
stableId: camera.stableId,
|
|
38881
|
+
name: camera.name,
|
|
38739
38882
|
type: DeviceType.Camera,
|
|
38740
38883
|
DeviceClass: TerminalCameraDevice,
|
|
38741
|
-
config:
|
|
38884
|
+
config: camera.config,
|
|
38742
38885
|
role: "terminal-camera"
|
|
38743
38886
|
}))
|
|
38744
38887
|
});
|
|
@@ -38755,15 +38898,78 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38755
38898
|
description: "Interactive terminal sessions in the Admin UI. Only pre-declared profiles can be opened; a free-form command is never accepted.",
|
|
38756
38899
|
columns: 2,
|
|
38757
38900
|
fields: [
|
|
38901
|
+
this.field({
|
|
38902
|
+
type: "boolean",
|
|
38903
|
+
key: "btmEnabled",
|
|
38904
|
+
label: "Enable BTM camera",
|
|
38905
|
+
default: true,
|
|
38906
|
+
perNode: true
|
|
38907
|
+
}),
|
|
38758
38908
|
this.field({
|
|
38759
38909
|
type: "text",
|
|
38760
38910
|
key: "btmPath",
|
|
38761
|
-
label: "
|
|
38911
|
+
label: "BTM binary",
|
|
38762
38912
|
description: "Path to the `btm` (bottom) executable. Leave empty to resolve from PATH.",
|
|
38763
38913
|
placeholder: "btm",
|
|
38764
38914
|
default: "",
|
|
38765
38915
|
perNode: true
|
|
38766
38916
|
}),
|
|
38917
|
+
this.field({
|
|
38918
|
+
type: "tags",
|
|
38919
|
+
key: "btmArgs",
|
|
38920
|
+
label: "BTM arguments",
|
|
38921
|
+
description: "Exact arguments passed to btm.",
|
|
38922
|
+
default: [],
|
|
38923
|
+
perNode: true
|
|
38924
|
+
}),
|
|
38925
|
+
this.field({
|
|
38926
|
+
type: "boolean",
|
|
38927
|
+
key: "topEnabled",
|
|
38928
|
+
label: "Enable Top camera",
|
|
38929
|
+
default: true,
|
|
38930
|
+
perNode: true
|
|
38931
|
+
}),
|
|
38932
|
+
this.field({
|
|
38933
|
+
type: "text",
|
|
38934
|
+
key: "topPath",
|
|
38935
|
+
label: "Top binary",
|
|
38936
|
+
placeholder: "top",
|
|
38937
|
+
default: "",
|
|
38938
|
+
perNode: true
|
|
38939
|
+
}),
|
|
38940
|
+
this.field({
|
|
38941
|
+
type: "tags",
|
|
38942
|
+
key: "topArgs",
|
|
38943
|
+
label: "Top arguments",
|
|
38944
|
+
description: "Exact arguments passed to top.",
|
|
38945
|
+
default: [],
|
|
38946
|
+
perNode: true
|
|
38947
|
+
}),
|
|
38948
|
+
this.field({
|
|
38949
|
+
type: "boolean",
|
|
38950
|
+
key: "glancesEnabled",
|
|
38951
|
+
label: "Enable Glances camera",
|
|
38952
|
+
description: "Glances is installed automatically into CamStack embedded Python.",
|
|
38953
|
+
default: true,
|
|
38954
|
+
perNode: true
|
|
38955
|
+
}),
|
|
38956
|
+
this.field({
|
|
38957
|
+
type: "text",
|
|
38958
|
+
key: "glancesPath",
|
|
38959
|
+
label: "Glances binary override",
|
|
38960
|
+
description: "Optional executable override. Empty uses the automatically managed Python package.",
|
|
38961
|
+
placeholder: "glances",
|
|
38962
|
+
default: "",
|
|
38963
|
+
perNode: true
|
|
38964
|
+
}),
|
|
38965
|
+
this.field({
|
|
38966
|
+
type: "tags",
|
|
38967
|
+
key: "glancesArgs",
|
|
38968
|
+
label: "Glances arguments",
|
|
38969
|
+
description: "Exact arguments passed to Glances.",
|
|
38970
|
+
default: [],
|
|
38971
|
+
perNode: true
|
|
38972
|
+
}),
|
|
38767
38973
|
this.field({
|
|
38768
38974
|
type: "number",
|
|
38769
38975
|
key: "maxSessions",
|
|
@@ -38795,7 +39001,7 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38795
39001
|
}, {
|
|
38796
39002
|
id: "terminal-profiles",
|
|
38797
39003
|
title: "Custom profiles",
|
|
38798
|
-
description: "
|
|
39004
|
+
description: "Each enabled profile creates its own Terminal camera on this node and is available from the Terminal page. Session requests only carry the profile ID; commands cannot be overridden by clients.",
|
|
38799
39005
|
columns: 1,
|
|
38800
39006
|
fields: [this.field({
|
|
38801
39007
|
type: "editable-array",
|
package/dist/addon.mjs
CHANGED
|
@@ -21127,6 +21127,7 @@ var FaceInfoSchema = object({
|
|
|
21127
21127
|
var FaceFilterEnum = _enum([
|
|
21128
21128
|
"unassigned",
|
|
21129
21129
|
"recognized",
|
|
21130
|
+
"identified",
|
|
21130
21131
|
"all"
|
|
21131
21132
|
]);
|
|
21132
21133
|
var MediaFileLiteSchema$1 = object({
|
|
@@ -21155,6 +21156,8 @@ method(_void(), array(IdentitySchema).readonly()), method(object({ name: string(
|
|
|
21155
21156
|
kind: "mutation",
|
|
21156
21157
|
auth: "admin"
|
|
21157
21158
|
}), method(object({
|
|
21159
|
+
/** Restrict to one camera. Absent keeps the cluster-wide gallery view. */
|
|
21160
|
+
deviceId: number().int().optional(),
|
|
21158
21161
|
limit: number().int().positive().optional(),
|
|
21159
21162
|
filter: FaceFilterEnum.optional(),
|
|
21160
21163
|
/**
|
|
@@ -23384,6 +23387,10 @@ var OsdSourceSchema = discriminatedUnion("kind", [
|
|
|
23384
23387
|
capName: string().min(1).max(64),
|
|
23385
23388
|
/** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
|
|
23386
23389
|
valuePath: string().min(1).max(64)
|
|
23390
|
+
}),
|
|
23391
|
+
object({
|
|
23392
|
+
kind: literal("latest-recognition"),
|
|
23393
|
+
recognition: _enum(["person", "plate"])
|
|
23387
23394
|
})
|
|
23388
23395
|
]);
|
|
23389
23396
|
var OsdSlotBindingSchema = object({
|
|
@@ -23489,6 +23496,15 @@ method(object({ deviceId: number().int() }), object({
|
|
|
23489
23496
|
}), object({ success: literal(true) }), {
|
|
23490
23497
|
kind: "mutation",
|
|
23491
23498
|
auth: "admin"
|
|
23499
|
+
}), method(object({
|
|
23500
|
+
sourceDeviceId: number().int(),
|
|
23501
|
+
targetDeviceId: number().int()
|
|
23502
|
+
}), object({
|
|
23503
|
+
copied: number().int().nonnegative(),
|
|
23504
|
+
skipped: number().int().nonnegative()
|
|
23505
|
+
}), {
|
|
23506
|
+
kind: "mutation",
|
|
23507
|
+
auth: "admin"
|
|
23492
23508
|
}), method(object({
|
|
23493
23509
|
deviceId: number().int(),
|
|
23494
23510
|
slotId: string().min(1),
|
|
@@ -30182,6 +30198,12 @@ Object.freeze({
|
|
|
30182
30198
|
addonId: null,
|
|
30183
30199
|
access: "delete"
|
|
30184
30200
|
},
|
|
30201
|
+
"osdManager.copyDeviceConfiguration": {
|
|
30202
|
+
capName: "osd-manager",
|
|
30203
|
+
capScope: "system",
|
|
30204
|
+
addonId: null,
|
|
30205
|
+
access: "create"
|
|
30206
|
+
},
|
|
30185
30207
|
"osdManager.getConditionSupport": {
|
|
30186
30208
|
capName: "osd-manager",
|
|
30187
30209
|
capScope: "system",
|
|
@@ -33034,6 +33056,22 @@ function createNodePtySpawner() {
|
|
|
33034
33056
|
async function warmNodePty() {
|
|
33035
33057
|
await loadNodePty();
|
|
33036
33058
|
}
|
|
33059
|
+
//#endregion
|
|
33060
|
+
//#region src/terminal-camera-declarations.ts
|
|
33061
|
+
function buildTerminalCameraDeclarations(nodes, profilesByNode) {
|
|
33062
|
+
return nodes.flatMap((node) => {
|
|
33063
|
+
const nodeLabel = node.isHub ? "Hub" : node.name;
|
|
33064
|
+
return (profilesByNode.get(node.id) ?? []).map((profile) => ({
|
|
33065
|
+
stableId: profile.profileId === "monitor" ? `terminal-camera-${node.id}` : `terminal-camera-${node.id}-${profile.profileId}`,
|
|
33066
|
+
name: `Terminal ${profile.label} - ${nodeLabel}`,
|
|
33067
|
+
config: {
|
|
33068
|
+
nodeId: node.id,
|
|
33069
|
+
profileId: profile.profileId,
|
|
33070
|
+
profileLabel: profile.label
|
|
33071
|
+
}
|
|
33072
|
+
}));
|
|
33073
|
+
});
|
|
33074
|
+
}
|
|
33037
33075
|
function escapeXml(value) {
|
|
33038
33076
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
33039
33077
|
}
|
|
@@ -33048,7 +33086,11 @@ async function renderTerminalJpeg(lines) {
|
|
|
33048
33086
|
}
|
|
33049
33087
|
//#endregion
|
|
33050
33088
|
//#region src/terminal-camera-device.ts
|
|
33051
|
-
var terminalCameraSchema = object({
|
|
33089
|
+
var terminalCameraSchema = object({
|
|
33090
|
+
nodeId: string().min(1),
|
|
33091
|
+
profileId: string().min(1),
|
|
33092
|
+
profileLabel: string().min(1)
|
|
33093
|
+
});
|
|
33052
33094
|
var relay = null;
|
|
33053
33095
|
function installTerminalCameraRelay(next) {
|
|
33054
33096
|
relay = next;
|
|
@@ -33067,18 +33109,19 @@ var TerminalCameraDevice = class extends BaseDevice {
|
|
|
33067
33109
|
const activeRelay = relay;
|
|
33068
33110
|
if (!activeRelay) throw new Error("terminal camera relay is unavailable");
|
|
33069
33111
|
const nodeId = this.config.get("nodeId");
|
|
33070
|
-
|
|
33071
|
-
|
|
33112
|
+
const profileId = this.config.get("profileId");
|
|
33113
|
+
return [{
|
|
33114
|
+
camStreamId: profileId,
|
|
33072
33115
|
kind: "pull-http",
|
|
33073
|
-
url: activeRelay.streamUrl(nodeId,
|
|
33116
|
+
url: activeRelay.streamUrl(nodeId, profileId),
|
|
33074
33117
|
codec: "h264",
|
|
33075
33118
|
resolution: {
|
|
33076
33119
|
width: 960,
|
|
33077
33120
|
height: 640
|
|
33078
33121
|
},
|
|
33079
33122
|
fps: 2,
|
|
33080
|
-
label:
|
|
33081
|
-
}
|
|
33123
|
+
label: this.config.get("profileLabel")
|
|
33124
|
+
}];
|
|
33082
33125
|
}
|
|
33083
33126
|
setNodeOnline(online) {
|
|
33084
33127
|
this.markOnline(online);
|
|
@@ -38250,7 +38293,12 @@ function createTerminalDataPlaneHandler(manager) {
|
|
|
38250
38293
|
var ProfileIdSchema = string().trim().min(1).max(64).regex(/^[a-z][a-z0-9-]*$/, "must be a lowercase slug");
|
|
38251
38294
|
var ConfiguredTerminalProfileSchema = object({
|
|
38252
38295
|
enabled: boolean().default(true),
|
|
38253
|
-
profileId: ProfileIdSchema.refine((id) =>
|
|
38296
|
+
profileId: ProfileIdSchema.refine((id) => ![
|
|
38297
|
+
"monitor",
|
|
38298
|
+
"top",
|
|
38299
|
+
"glances",
|
|
38300
|
+
"shell"
|
|
38301
|
+
].includes(id), { message: "monitor, top, glances and shell are reserved profile IDs" }),
|
|
38254
38302
|
label: string().trim().min(1).max(100),
|
|
38255
38303
|
description: string().trim().max(500).optional().default(""),
|
|
38256
38304
|
executable: string().trim().min(1).max(1024).refine((value) => !value.includes("\0"), { message: "must not contain NUL bytes" }),
|
|
@@ -38265,17 +38313,36 @@ var ConfiguredTerminalProfileSchema = object({
|
|
|
38265
38313
|
* surface.
|
|
38266
38314
|
*/
|
|
38267
38315
|
function buildProfiles(options) {
|
|
38268
|
-
const profiles = [
|
|
38316
|
+
const profiles = [];
|
|
38317
|
+
if (options.btmEnabled !== false) profiles.push({
|
|
38269
38318
|
profileId: "monitor",
|
|
38270
|
-
label: "
|
|
38319
|
+
label: "BTM",
|
|
38271
38320
|
description: "bottom (btm) — CPU, memory, network and process monitor",
|
|
38272
38321
|
file: options.btmPath.trim().length > 0 ? options.btmPath.trim() : "btm",
|
|
38273
|
-
args: []
|
|
38274
|
-
|
|
38275
|
-
|
|
38276
|
-
|
|
38277
|
-
|
|
38278
|
-
|
|
38322
|
+
args: options.btmArgs ?? []
|
|
38323
|
+
});
|
|
38324
|
+
if (options.topEnabled !== false) profiles.push({
|
|
38325
|
+
profileId: "top",
|
|
38326
|
+
label: "Top",
|
|
38327
|
+
description: "The operating system process and resource monitor",
|
|
38328
|
+
file: options.topPath?.trim() || "top",
|
|
38329
|
+
args: options.topArgs ?? []
|
|
38330
|
+
});
|
|
38331
|
+
if (options.glancesEnabled !== false) {
|
|
38332
|
+
const configuredPath = options.glancesPath?.trim();
|
|
38333
|
+
const pythonPath = options.glancesPythonPath?.trim();
|
|
38334
|
+
profiles.push({
|
|
38335
|
+
profileId: "glances",
|
|
38336
|
+
label: "Glances",
|
|
38337
|
+
description: "Cross-platform curses monitor installed in CamStack embedded Python",
|
|
38338
|
+
file: configuredPath || pythonPath || "glances",
|
|
38339
|
+
args: configuredPath || !pythonPath ? options.glancesArgs ?? [] : [
|
|
38340
|
+
"-m",
|
|
38341
|
+
"glances",
|
|
38342
|
+
...options.glancesArgs ?? []
|
|
38343
|
+
]
|
|
38344
|
+
});
|
|
38345
|
+
}
|
|
38279
38346
|
if (options.allowShell) profiles.push({
|
|
38280
38347
|
profileId: "shell",
|
|
38281
38348
|
label: "Shell (interactive)",
|
|
@@ -38348,6 +38415,15 @@ var TerminalSessionManager = class {
|
|
|
38348
38415
|
this.opts = opts;
|
|
38349
38416
|
this.profiles = buildProfiles({
|
|
38350
38417
|
btmPath: opts.btmPath,
|
|
38418
|
+
btmEnabled: opts.btmEnabled,
|
|
38419
|
+
btmArgs: opts.btmArgs,
|
|
38420
|
+
topEnabled: opts.topEnabled,
|
|
38421
|
+
topPath: opts.topPath,
|
|
38422
|
+
topArgs: opts.topArgs,
|
|
38423
|
+
glancesEnabled: opts.glancesEnabled,
|
|
38424
|
+
glancesPath: opts.glancesPath,
|
|
38425
|
+
glancesArgs: opts.glancesArgs,
|
|
38426
|
+
glancesPythonPath: opts.glancesPythonPath,
|
|
38351
38427
|
allowShell: opts.allowShell,
|
|
38352
38428
|
shellPath: opts.shellPath,
|
|
38353
38429
|
customProfiles: opts.customProfiles,
|
|
@@ -38601,6 +38677,14 @@ var TerminalSessionManager = class {
|
|
|
38601
38677
|
//#region src/addon.ts
|
|
38602
38678
|
var DEFAULTS = {
|
|
38603
38679
|
btmPath: "",
|
|
38680
|
+
btmEnabled: true,
|
|
38681
|
+
btmArgs: [],
|
|
38682
|
+
topEnabled: true,
|
|
38683
|
+
topPath: "",
|
|
38684
|
+
topArgs: [],
|
|
38685
|
+
glancesEnabled: true,
|
|
38686
|
+
glancesPath: "",
|
|
38687
|
+
glancesArgs: [],
|
|
38604
38688
|
allowShell: false,
|
|
38605
38689
|
shellPath: "",
|
|
38606
38690
|
maxSessions: 4,
|
|
@@ -38614,17 +38698,29 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38614
38698
|
dataPlane = null;
|
|
38615
38699
|
cameraRelay = null;
|
|
38616
38700
|
cameraReconcileTimer = null;
|
|
38701
|
+
cameraProfilesByNode = /* @__PURE__ */ new Map();
|
|
38702
|
+
glancesPythonPath = "";
|
|
38617
38703
|
constructor() {
|
|
38618
38704
|
super({ ...DEFAULTS });
|
|
38619
38705
|
}
|
|
38620
38706
|
async onInitialize() {
|
|
38621
38707
|
await warmNodePty();
|
|
38708
|
+
this.glancesPythonPath = await this.ctx.deps.ensurePython() ?? "";
|
|
38622
38709
|
const manager = new TerminalSessionManager({
|
|
38623
38710
|
spawn: createNodePtySpawner(),
|
|
38624
38711
|
screenFactory: createXtermScreen,
|
|
38625
38712
|
resolveBinary: resolveExecutable,
|
|
38626
38713
|
logger: this.ctx.logger,
|
|
38627
38714
|
btmPath: this.config.btmPath,
|
|
38715
|
+
btmEnabled: this.config.btmEnabled,
|
|
38716
|
+
btmArgs: this.config.btmArgs,
|
|
38717
|
+
topEnabled: this.config.topEnabled,
|
|
38718
|
+
topPath: this.config.topPath,
|
|
38719
|
+
topArgs: this.config.topArgs,
|
|
38720
|
+
glancesEnabled: this.config.glancesEnabled,
|
|
38721
|
+
glancesPath: this.config.glancesPath,
|
|
38722
|
+
glancesArgs: this.config.glancesArgs,
|
|
38723
|
+
glancesPythonPath: this.glancesPythonPath,
|
|
38628
38724
|
allowShell: this.config.allowShell,
|
|
38629
38725
|
shellPath: this.config.shellPath,
|
|
38630
38726
|
maxSessions: this.config.maxSessions,
|
|
@@ -38669,6 +38765,15 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38669
38765
|
async onConfigChanged() {
|
|
38670
38766
|
this.manager?.reconfigureProfiles({
|
|
38671
38767
|
btmPath: this.config.btmPath,
|
|
38768
|
+
btmEnabled: this.config.btmEnabled,
|
|
38769
|
+
btmArgs: this.config.btmArgs,
|
|
38770
|
+
topEnabled: this.config.topEnabled,
|
|
38771
|
+
topPath: this.config.topPath,
|
|
38772
|
+
topArgs: this.config.topArgs,
|
|
38773
|
+
glancesEnabled: this.config.glancesEnabled,
|
|
38774
|
+
glancesPath: this.config.glancesPath,
|
|
38775
|
+
glancesArgs: this.config.glancesArgs,
|
|
38776
|
+
glancesPythonPath: this.glancesPythonPath,
|
|
38672
38777
|
allowShell: this.config.allowShell,
|
|
38673
38778
|
shellPath: this.config.shellPath,
|
|
38674
38779
|
maxSessions: this.config.maxSessions,
|
|
@@ -38693,6 +38798,44 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38693
38798
|
const topology = await this.ctx.api.nodes.topology.query();
|
|
38694
38799
|
if (topology.length === 0) throw new Error("cluster topology returned no nodes; refusing to withdraw declared cameras");
|
|
38695
38800
|
const nodes = topology.filter((node) => typeof node.id === "string" && node.id.length > 0);
|
|
38801
|
+
const unavailableNodeIds = /* @__PURE__ */ new Set();
|
|
38802
|
+
await Promise.all(nodes.map(async (node) => {
|
|
38803
|
+
try {
|
|
38804
|
+
this.cameraProfilesByNode.set(node.id, await this.cameraRelay.listProfiles(node.id));
|
|
38805
|
+
} catch (error) {
|
|
38806
|
+
if (!this.cameraProfilesByNode.has(node.id)) {
|
|
38807
|
+
unavailableNodeIds.add(node.id);
|
|
38808
|
+
this.ctx.logger.warn("terminal profiles unavailable — preserving existing cameras for this node", { meta: {
|
|
38809
|
+
nodeId: node.id,
|
|
38810
|
+
error: error instanceof Error ? error.message : String(error)
|
|
38811
|
+
} });
|
|
38812
|
+
}
|
|
38813
|
+
}
|
|
38814
|
+
}));
|
|
38815
|
+
const cameraDeclarations = [...buildTerminalCameraDeclarations(nodes, this.cameraProfilesByNode)];
|
|
38816
|
+
if (unavailableNodeIds.size > 0) {
|
|
38817
|
+
const existing = await this.ctx.api.deviceManager.listAll.query({ addonId: this.ctx.id });
|
|
38818
|
+
const unavailableNodes = nodes.filter((node) => unavailableNodeIds.has(node.id)).sort((left, right) => right.id.length - left.id.length);
|
|
38819
|
+
for (const row of existing) {
|
|
38820
|
+
const node = unavailableNodes.find((candidate) => {
|
|
38821
|
+
const base = `terminal-camera-${candidate.id}`;
|
|
38822
|
+
return row.stableId === base || row.stableId.startsWith(`${base}-`);
|
|
38823
|
+
});
|
|
38824
|
+
if (!node || cameraDeclarations.some((camera) => camera.stableId === row.stableId)) continue;
|
|
38825
|
+
const base = `terminal-camera-${node.id}`;
|
|
38826
|
+
const profileId = row.stableId === base ? "monitor" : row.stableId.slice(base.length + 1);
|
|
38827
|
+
const profileLabel = profileId === "monitor" ? "BTM" : profileId;
|
|
38828
|
+
cameraDeclarations.push({
|
|
38829
|
+
stableId: row.stableId,
|
|
38830
|
+
name: `Terminal ${profileLabel} - ${node.isHub ? "Hub" : node.name}`,
|
|
38831
|
+
config: {
|
|
38832
|
+
nodeId: node.id,
|
|
38833
|
+
profileId,
|
|
38834
|
+
profileLabel
|
|
38835
|
+
}
|
|
38836
|
+
});
|
|
38837
|
+
}
|
|
38838
|
+
}
|
|
38696
38839
|
const result = await new DeclaredDevices({
|
|
38697
38840
|
logger: this.ctx.logger.child("camera-declaration"),
|
|
38698
38841
|
addonId: this.ctx.id,
|
|
@@ -38711,12 +38854,12 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38711
38854
|
}).reconcile({
|
|
38712
38855
|
integrationName: TERMINAL_CAMERA_INTEGRATION,
|
|
38713
38856
|
placement: "hub",
|
|
38714
|
-
devices:
|
|
38715
|
-
stableId:
|
|
38716
|
-
name:
|
|
38857
|
+
devices: cameraDeclarations.map((camera) => ({
|
|
38858
|
+
stableId: camera.stableId,
|
|
38859
|
+
name: camera.name,
|
|
38717
38860
|
type: DeviceType.Camera,
|
|
38718
38861
|
DeviceClass: TerminalCameraDevice,
|
|
38719
|
-
config:
|
|
38862
|
+
config: camera.config,
|
|
38720
38863
|
role: "terminal-camera"
|
|
38721
38864
|
}))
|
|
38722
38865
|
});
|
|
@@ -38733,15 +38876,78 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38733
38876
|
description: "Interactive terminal sessions in the Admin UI. Only pre-declared profiles can be opened; a free-form command is never accepted.",
|
|
38734
38877
|
columns: 2,
|
|
38735
38878
|
fields: [
|
|
38879
|
+
this.field({
|
|
38880
|
+
type: "boolean",
|
|
38881
|
+
key: "btmEnabled",
|
|
38882
|
+
label: "Enable BTM camera",
|
|
38883
|
+
default: true,
|
|
38884
|
+
perNode: true
|
|
38885
|
+
}),
|
|
38736
38886
|
this.field({
|
|
38737
38887
|
type: "text",
|
|
38738
38888
|
key: "btmPath",
|
|
38739
|
-
label: "
|
|
38889
|
+
label: "BTM binary",
|
|
38740
38890
|
description: "Path to the `btm` (bottom) executable. Leave empty to resolve from PATH.",
|
|
38741
38891
|
placeholder: "btm",
|
|
38742
38892
|
default: "",
|
|
38743
38893
|
perNode: true
|
|
38744
38894
|
}),
|
|
38895
|
+
this.field({
|
|
38896
|
+
type: "tags",
|
|
38897
|
+
key: "btmArgs",
|
|
38898
|
+
label: "BTM arguments",
|
|
38899
|
+
description: "Exact arguments passed to btm.",
|
|
38900
|
+
default: [],
|
|
38901
|
+
perNode: true
|
|
38902
|
+
}),
|
|
38903
|
+
this.field({
|
|
38904
|
+
type: "boolean",
|
|
38905
|
+
key: "topEnabled",
|
|
38906
|
+
label: "Enable Top camera",
|
|
38907
|
+
default: true,
|
|
38908
|
+
perNode: true
|
|
38909
|
+
}),
|
|
38910
|
+
this.field({
|
|
38911
|
+
type: "text",
|
|
38912
|
+
key: "topPath",
|
|
38913
|
+
label: "Top binary",
|
|
38914
|
+
placeholder: "top",
|
|
38915
|
+
default: "",
|
|
38916
|
+
perNode: true
|
|
38917
|
+
}),
|
|
38918
|
+
this.field({
|
|
38919
|
+
type: "tags",
|
|
38920
|
+
key: "topArgs",
|
|
38921
|
+
label: "Top arguments",
|
|
38922
|
+
description: "Exact arguments passed to top.",
|
|
38923
|
+
default: [],
|
|
38924
|
+
perNode: true
|
|
38925
|
+
}),
|
|
38926
|
+
this.field({
|
|
38927
|
+
type: "boolean",
|
|
38928
|
+
key: "glancesEnabled",
|
|
38929
|
+
label: "Enable Glances camera",
|
|
38930
|
+
description: "Glances is installed automatically into CamStack embedded Python.",
|
|
38931
|
+
default: true,
|
|
38932
|
+
perNode: true
|
|
38933
|
+
}),
|
|
38934
|
+
this.field({
|
|
38935
|
+
type: "text",
|
|
38936
|
+
key: "glancesPath",
|
|
38937
|
+
label: "Glances binary override",
|
|
38938
|
+
description: "Optional executable override. Empty uses the automatically managed Python package.",
|
|
38939
|
+
placeholder: "glances",
|
|
38940
|
+
default: "",
|
|
38941
|
+
perNode: true
|
|
38942
|
+
}),
|
|
38943
|
+
this.field({
|
|
38944
|
+
type: "tags",
|
|
38945
|
+
key: "glancesArgs",
|
|
38946
|
+
label: "Glances arguments",
|
|
38947
|
+
description: "Exact arguments passed to Glances.",
|
|
38948
|
+
default: [],
|
|
38949
|
+
perNode: true
|
|
38950
|
+
}),
|
|
38745
38951
|
this.field({
|
|
38746
38952
|
type: "number",
|
|
38747
38953
|
key: "maxSessions",
|
|
@@ -38773,7 +38979,7 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
38773
38979
|
}, {
|
|
38774
38980
|
id: "terminal-profiles",
|
|
38775
38981
|
title: "Custom profiles",
|
|
38776
|
-
description: "
|
|
38982
|
+
description: "Each enabled profile creates its own Terminal camera on this node and is available from the Terminal page. Session requests only carry the profile ID; commands cannot be overridden by clients.",
|
|
38777
38983
|
columns: 1,
|
|
38778
38984
|
fields: [this.field({
|
|
38779
38985
|
type: "editable-array",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-terminal",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Interactive terminal sessions (pty + xterm) as a CamStack addon",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -34,8 +34,11 @@
|
|
|
34
34
|
"category": "system",
|
|
35
35
|
"name": "Terminal",
|
|
36
36
|
"version": "0.1.0",
|
|
37
|
-
"description": "Interactive terminal sessions on any cluster node,
|
|
37
|
+
"description": "Interactive terminal sessions on any cluster node, with one virtual camera for each configured profile. Commands remain bound to administrator-configured profiles.",
|
|
38
38
|
"entry": "./dist/index.js",
|
|
39
|
+
"python": {
|
|
40
|
+
"requirements": "./python/requirements.txt"
|
|
41
|
+
},
|
|
39
42
|
"icon": "assets/icon.svg",
|
|
40
43
|
"color": "#0ea5e9",
|
|
41
44
|
"instanceMode": "unique",
|
|
@@ -53,7 +56,8 @@
|
|
|
53
56
|
},
|
|
54
57
|
"files": [
|
|
55
58
|
"dist",
|
|
56
|
-
"assets"
|
|
59
|
+
"assets",
|
|
60
|
+
"python"
|
|
57
61
|
],
|
|
58
62
|
"scripts": {
|
|
59
63
|
"build": "vite build",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
glances>=4.3,<5
|