@camstack/addon-terminal 0.1.29 → 0.1.31

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 (3) hide show
  1. package/dist/addon.js +350 -30
  2. package/dist/addon.mjs +350 -30
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -20003,6 +20003,9 @@ method(_void(), ProviderInfoSchema), method(object({ config: record(string(), un
20003
20003
  location: StorageLocationSchema,
20004
20004
  relativePath: string()
20005
20005
  }), _void(), { kind: "mutation" }), method(object({ location: StorageLocationSchema }), number().nullable()), method(BeginUploadInputSchema, BeginUploadResultSchema, { kind: "mutation" }), method(WriteChunkInputSchema, _void(), { kind: "mutation" }), method(FinalizeUploadInputSchema, _void(), { kind: "mutation" }), method(AbortUploadInputSchema, _void(), { kind: "mutation" }), method(BeginDownloadInputSchema, BeginDownloadResultSchema, { kind: "mutation" }), method(ReadChunkInputSchema, _instanceof(Uint8Array)), method(EndDownloadInputSchema, _void(), { kind: "mutation" });
20006
+ /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
20007
+ var ProfileSettingsSchemaBridge = unknown().nullable();
20008
+ var ProfileSettingsBagSchema = record(string(), unknown());
20006
20009
  /**
20007
20010
  * A live terminal session hosted by the provider addon. Output and input do
20008
20011
  * NOT flow through the capability — they use the addon data plane
@@ -20032,7 +20035,14 @@ var TerminalSessionInfoSchema = object({
20032
20035
  var TerminalProfileInfoSchema = object({
20033
20036
  profileId: string(),
20034
20037
  label: string(),
20035
- description: string().optional()
20038
+ description: string().optional(),
20039
+ /** Spawn defaults the instance form copies on create. */
20040
+ executable: string().optional(),
20041
+ args: array(string()).readonly().optional(),
20042
+ cwd: string().optional(),
20043
+ environment: array(string()).readonly().optional(),
20044
+ /** ConfigUISchema for instance knobs, or null when the profile has none. */
20045
+ settingsSchema: ProfileSettingsSchemaBridge.optional()
20036
20046
  });
20037
20047
  /**
20038
20048
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -20045,7 +20055,12 @@ var TerminalInstanceInfoSchema = object({
20045
20055
  profileId: string(),
20046
20056
  profileLabel: string(),
20047
20057
  name: string(),
20048
- enabled: boolean()
20058
+ enabled: boolean(),
20059
+ executable: string(),
20060
+ args: array(string()).readonly(),
20061
+ cwd: string(),
20062
+ environment: array(string()).readonly(),
20063
+ profileSettings: ProfileSettingsBagSchema
20049
20064
  });
20050
20065
  var TerminalLegacyCameraSchema = object({
20051
20066
  stableId: string(),
@@ -20091,7 +20106,24 @@ var terminalSessionCapability = {
20091
20106
  createInstance: method(object({
20092
20107
  targetNodeId: string().min(1),
20093
20108
  profileId: string().min(1),
20094
- name: string().trim().min(1).max(160).optional()
20109
+ name: string().trim().min(1).max(160).optional(),
20110
+ executable: string().max(1024).optional(),
20111
+ args: array(string().max(2048)).max(64).optional(),
20112
+ cwd: string().max(1024).optional(),
20113
+ environment: array(string().max(4096)).max(64).optional(),
20114
+ profileSettings: ProfileSettingsBagSchema.optional()
20115
+ }), TerminalInstanceInfoSchema, {
20116
+ kind: "mutation",
20117
+ auth: "admin"
20118
+ }),
20119
+ updateInstance: method(object({
20120
+ instanceId: string().min(1),
20121
+ name: string().trim().min(1).max(160).optional(),
20122
+ executable: string().max(1024).optional(),
20123
+ args: array(string().max(2048)).max(64).optional(),
20124
+ cwd: string().max(1024).optional(),
20125
+ environment: array(string().max(4096)).max(64).optional(),
20126
+ profileSettings: ProfileSettingsBagSchema.optional()
20095
20127
  }), TerminalInstanceInfoSchema, {
20096
20128
  kind: "mutation",
20097
20129
  auth: "admin"
@@ -20126,7 +20158,11 @@ var terminalSessionCapability = {
20126
20158
  openSession: method(object({
20127
20159
  profileId: string(),
20128
20160
  cols: number().int().positive(),
20129
- rows: number().int().positive()
20161
+ rows: number().int().positive(),
20162
+ executable: string().max(1024).optional(),
20163
+ args: array(string().max(2048)).max(64).optional(),
20164
+ cwd: string().max(1024).optional(),
20165
+ environment: array(string().max(4096)).max(64).optional()
20130
20166
  }), TerminalSessionInfoSchema, {
20131
20167
  kind: "mutation",
20132
20168
  auth: "admin"
@@ -24015,10 +24051,10 @@ var lawnMowerControlCapability = {
24015
24051
  *
24016
24052
  * • The SDK (mobile / web client) consumes `getConnectionEndpoints()`
24017
24053
  * to receive an ordered list of candidate base URLs it should race
24018
- * on connect — LAN IPv4 first (lowest latency when on same network),
24019
- * then public hostname (if a tunnel is up), then IPv6. The SDK can
24020
- * race them with short timeouts and stick with the winner for the
24021
- * session.
24054
+ * on connect — LAN IPv4 and stable LAN IPv6 first (lowest latency
24055
+ * when on the same network), then public hostname (if a tunnel is
24056
+ * up). The SDK can race them with short timeouts and stick with the
24057
+ * winner for the session.
24022
24058
  *
24023
24059
  * Why hub-only: agents are not directly addressable by the operator's
24024
24060
  * clients — they reverse-connect to the hub. Exposing their interfaces
@@ -24181,6 +24217,20 @@ var AllowedAddressesSchema = object({
24181
24217
  * Network Addresses admin page and persisted by the addon.
24182
24218
  */
24183
24219
  addresses: array(string()).readonly() });
24220
+ var TlsStatusSchema = object({
24221
+ mode: _enum([
24222
+ "generated",
24223
+ "uploaded",
24224
+ "disabled"
24225
+ ]),
24226
+ leafFingerprintSha256: string().nullable(),
24227
+ caFingerprintSha256: string().nullable(),
24228
+ validTo: string().nullable(),
24229
+ sans: array(string()),
24230
+ caCertPem: string().nullable(),
24231
+ reissueError: string().nullable(),
24232
+ restartRequired: boolean()
24233
+ });
24184
24234
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
24185
24235
  /**
24186
24236
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -24200,7 +24250,20 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
24200
24250
  * to avoid mixed-content blocks in the browser. The public
24201
24251
  * tunnel always emits `https://` regardless. */
24202
24252
  scheme: _enum(["http", "https"]).optional()
24203
- }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
24253
+ }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" }), method(_void(), TlsStatusSchema), method(object({ reason: string().optional() }), TlsStatusSchema, {
24254
+ kind: "mutation",
24255
+ auth: "admin"
24256
+ }), method(object({
24257
+ certPem: string().min(1),
24258
+ keyPem: string().min(1),
24259
+ caPem: string().optional()
24260
+ }), TlsStatusSchema, {
24261
+ kind: "mutation",
24262
+ auth: "admin"
24263
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
24264
+ kind: "mutation",
24265
+ auth: "admin"
24266
+ });
24204
24267
  var LockControlStatusSchema = object({
24205
24268
  /** Lifecycle state of the lock. `jammed` means the motor reported
24206
24269
  * failure to reach the target — operator intervention required. */
@@ -32225,6 +32288,12 @@ Object.freeze({
32225
32288
  addonId: null,
32226
32289
  access: "create"
32227
32290
  },
32291
+ "localNetwork.downloadCa": {
32292
+ capName: "local-network",
32293
+ capScope: "system",
32294
+ addonId: null,
32295
+ access: "view"
32296
+ },
32228
32297
  "localNetwork.getAllowedAddresses": {
32229
32298
  capName: "local-network",
32230
32299
  capScope: "system",
@@ -32249,18 +32318,36 @@ Object.freeze({
32249
32318
  addonId: null,
32250
32319
  access: "view"
32251
32320
  },
32321
+ "localNetwork.getTlsStatus": {
32322
+ capName: "local-network",
32323
+ capScope: "system",
32324
+ addonId: null,
32325
+ access: "view"
32326
+ },
32252
32327
  "localNetwork.list": {
32253
32328
  capName: "local-network",
32254
32329
  capScope: "system",
32255
32330
  addonId: null,
32256
32331
  access: "view"
32257
32332
  },
32333
+ "localNetwork.regenerateCertificate": {
32334
+ capName: "local-network",
32335
+ capScope: "system",
32336
+ addonId: null,
32337
+ access: "create"
32338
+ },
32258
32339
  "localNetwork.resetAllowlistToBestMatch": {
32259
32340
  capName: "local-network",
32260
32341
  capScope: "system",
32261
32342
  addonId: null,
32262
32343
  access: "delete"
32263
32344
  },
32345
+ "localNetwork.revertToGeneratedCertificate": {
32346
+ capName: "local-network",
32347
+ capScope: "system",
32348
+ addonId: null,
32349
+ access: "create"
32350
+ },
32264
32351
  "localNetwork.setAllowedAddresses": {
32265
32352
  capName: "local-network",
32266
32353
  capScope: "system",
@@ -32273,6 +32360,12 @@ Object.freeze({
32273
32360
  addonId: null,
32274
32361
  access: "create"
32275
32362
  },
32363
+ "localNetwork.uploadCertificate": {
32364
+ capName: "local-network",
32365
+ capScope: "system",
32366
+ addonId: null,
32367
+ access: "create"
32368
+ },
32276
32369
  "lockControl.lock": {
32277
32370
  capName: "lock-control",
32278
32371
  capScope: "device",
@@ -35153,6 +35246,12 @@ Object.freeze({
35153
35246
  addonId: null,
35154
35247
  access: "create"
35155
35248
  },
35249
+ "terminalSession.updateInstance": {
35250
+ capName: "terminal-session",
35251
+ capScope: "system",
35252
+ addonId: null,
35253
+ access: "create"
35254
+ },
35156
35255
  "terminalSession.writeInput": {
35157
35256
  capName: "terminal-session",
35158
35257
  capScope: "system",
@@ -37640,6 +37739,35 @@ Object.freeze(Object.fromEntries([{
37640
37739
  }]
37641
37740
  }].map((s) => [s.stepId, s.defaultModelId])));
37642
37741
  string().min(1);
37742
+ var CLUSTER_STEP_SETTING_FIELDS = [{
37743
+ stepId: "face-embedding",
37744
+ key: "minLandmarkFaceSize",
37745
+ label: "Min face size for recognition (detection px)",
37746
+ description: "Refuse to embed a face smaller than this IN THE DETECTION FRAME. Applies to every node — the enrolled gallery is one index, and two admission floors would fill it from two policies. 0 disables the gate.",
37747
+ type: "slider",
37748
+ min: 0,
37749
+ max: 64,
37750
+ step: 2,
37751
+ default: 24
37752
+ }];
37753
+ function clusterStepSettingKey(stepId, fieldKey) {
37754
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
37755
+ }
37756
+ var ClusterSettingNumberSchema = number().finite();
37757
+ function readClusterStepSettings(config) {
37758
+ const out = {};
37759
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
37760
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
37761
+ const value = parsed.success ? parsed.data : field.default;
37762
+ const existing = out[field.stepId] ?? {};
37763
+ out[field.stepId] = {
37764
+ ...existing,
37765
+ [field.key]: value
37766
+ };
37767
+ }
37768
+ return out;
37769
+ }
37770
+ readClusterStepSettings({});
37643
37771
  object({
37644
37772
  /**
37645
37773
  * Fraction of the box's own size added on EACH side before cutting.
@@ -43172,7 +43300,7 @@ var require_xterm_headless = /* @__PURE__ */ __commonJSMin(((exports) => {
43172
43300
  var import_addon_serialize = require_addon_serialize();
43173
43301
  var import_xterm_headless = require_xterm_headless();
43174
43302
  var SCROLLBACK_LINES = 2e3;
43175
- function createXtermScreen(cols, rows) {
43303
+ function createXtermScreen$1(cols, rows) {
43176
43304
  const term = new import_xterm_headless.Terminal({
43177
43305
  cols,
43178
43306
  rows,
@@ -43316,7 +43444,7 @@ var TerminalCameraRelay = class {
43316
43444
  instanceId,
43317
43445
  nodeId,
43318
43446
  profileId,
43319
- screen: createXtermScreen(120, 40),
43447
+ screen: createXtermScreen$1(120, 40),
43320
43448
  sessionId: null,
43321
43449
  cursor: 0,
43322
43450
  clients: 0,
@@ -43347,7 +43475,8 @@ var TerminalCameraRelay = class {
43347
43475
  const opening = this.api.openSession(state.nodeId, {
43348
43476
  profileId: state.profileId,
43349
43477
  cols: 120,
43350
- rows: 40
43478
+ rows: 40,
43479
+ instanceId: state.instanceId
43351
43480
  });
43352
43481
  state.openPromise = opening;
43353
43482
  try {
@@ -43372,7 +43501,7 @@ var TerminalCameraRelay = class {
43372
43501
  applyBatch(state, batch) {
43373
43502
  if (batch.reset) {
43374
43503
  state.screen.dispose();
43375
- state.screen = createXtermScreen(120, 40);
43504
+ state.screen = createXtermScreen$1(120, 40);
43376
43505
  if (batch.snapshot) state.screen.write(batch.snapshot);
43377
43506
  }
43378
43507
  let exited = false;
@@ -43734,8 +43863,26 @@ var TerminalInstanceSchema = object({
43734
43863
  profileId: string().min(1).max(64),
43735
43864
  profileLabel: string().min(1).max(120),
43736
43865
  name: string().min(1).max(160),
43737
- enabled: boolean()
43866
+ enabled: boolean(),
43867
+ executable: string().max(1024).default(""),
43868
+ args: array(string().max(2048)).max(64).default([]),
43869
+ cwd: string().max(1024).default(""),
43870
+ environment: array(string().max(4096)).max(64).default([]),
43871
+ profileSettings: record(string(), unknown()).default({})
43738
43872
  });
43873
+ function environmentEntriesToRecord(entries) {
43874
+ const out = {};
43875
+ for (const entry of entries) {
43876
+ const eq = entry.indexOf("=");
43877
+ if (eq <= 0) continue;
43878
+ out[entry.slice(0, eq)] = entry.slice(eq + 1);
43879
+ }
43880
+ return out;
43881
+ }
43882
+ function environmentRecordToEntries(env) {
43883
+ if (env === void 0) return [];
43884
+ return Object.entries(env).map(([name, value]) => `${name}=${value}`);
43885
+ }
43739
43886
  /**
43740
43887
  * Config is operator-writable, so malformed or duplicate rows are ignored
43741
43888
  * rather than allowed to make declaration reconciliation destructive.
@@ -43935,6 +44082,108 @@ function findProfile(profiles, profileId) {
43935
44082
  return profiles.find((p) => p.profileId === profileId);
43936
44083
  }
43937
44084
  //#endregion
44085
+ //#region src/profile-settings.ts
44086
+ var GLANCES_PLUGINS = [
44087
+ {
44088
+ key: "showCpu",
44089
+ plugin: "cpu",
44090
+ label: "CPU"
44091
+ },
44092
+ {
44093
+ key: "showMem",
44094
+ plugin: "mem",
44095
+ label: "Memory"
44096
+ },
44097
+ {
44098
+ key: "showLoad",
44099
+ plugin: "load",
44100
+ label: "Load"
44101
+ },
44102
+ {
44103
+ key: "showNetwork",
44104
+ plugin: "network",
44105
+ label: "Network"
44106
+ },
44107
+ {
44108
+ key: "showDiskIo",
44109
+ plugin: "diskio",
44110
+ label: "Disk I/O"
44111
+ },
44112
+ {
44113
+ key: "showFs",
44114
+ plugin: "fs",
44115
+ label: "Filesystems"
44116
+ },
44117
+ {
44118
+ key: "showProcessList",
44119
+ plugin: "processlist",
44120
+ label: "Process list"
44121
+ },
44122
+ {
44123
+ key: "showContainers",
44124
+ plugin: "containers",
44125
+ label: "Containers"
44126
+ },
44127
+ {
44128
+ key: "showSensors",
44129
+ plugin: "sensors",
44130
+ label: "Sensors"
44131
+ }
44132
+ ];
44133
+ function glancesBooleanField(key, label) {
44134
+ return {
44135
+ type: "boolean",
44136
+ key,
44137
+ label,
44138
+ default: true,
44139
+ style: "switch"
44140
+ };
44141
+ }
44142
+ function glancesSettingsSchema() {
44143
+ return { sections: [{
44144
+ id: "glances-panels",
44145
+ title: "Glances panels",
44146
+ description: "Turn off a panel to pass --disable-plugin to this Terminal only. All on is the measured default (~1% of one core at the camera grid).",
44147
+ columns: 2,
44148
+ fields: GLANCES_PLUGINS.map((plugin) => glancesBooleanField(plugin.key, plugin.label))
44149
+ }] };
44150
+ }
44151
+ function settingsSchemaForProfile(profileId) {
44152
+ if (profileId === "glances") return glancesSettingsSchema();
44153
+ return null;
44154
+ }
44155
+ function glancesSettingsToArgs(settings) {
44156
+ const disabled = GLANCES_PLUGINS.filter((plugin) => settings[plugin.key] === false).map((plugin) => plugin.plugin);
44157
+ if (disabled.length === 0) return [];
44158
+ return ["--disable-plugin", disabled.join(",")];
44159
+ }
44160
+ function sanitizeProfileSettings(profileId, raw) {
44161
+ if (profileId !== "glances") return {};
44162
+ const bag = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
44163
+ const out = {
44164
+ showCpu: true,
44165
+ showMem: true,
44166
+ showLoad: true,
44167
+ showNetwork: true,
44168
+ showDiskIo: true,
44169
+ showFs: true,
44170
+ showProcessList: true,
44171
+ showContainers: true,
44172
+ showSensors: true
44173
+ };
44174
+ for (const plugin of GLANCES_PLUGINS) if (typeof bag[plugin.key] === "boolean") out[plugin.key] = bag[plugin.key];
44175
+ return out;
44176
+ }
44177
+ function profileSettingsToArgs(profileId, settings) {
44178
+ if (profileId !== "glances") return [];
44179
+ return glancesSettingsToArgs(sanitizeProfileSettings(profileId, settings));
44180
+ }
44181
+ function spawnArgsForInstance(input) {
44182
+ const extra = profileSettingsToArgs(input.profileId, input.profileSettings);
44183
+ if (input.instanceArgs.length > 0) return [...input.instanceArgs, ...extra];
44184
+ if (extra.length > 0) return [...input.profileArgs, ...extra];
44185
+ }
44186
+ //#endregion
43938
44187
  //#region src/terminal-session-manager.ts
43939
44188
  var MIN_GRID = 1;
43940
44189
  var MAX_COLS = 1e3;
@@ -43999,7 +44248,12 @@ var TerminalSessionManager = class {
43999
44248
  return this.profiles.map((p) => ({
44000
44249
  profileId: p.profileId,
44001
44250
  label: p.label,
44002
- ...p.description !== void 0 ? { description: p.description } : {}
44251
+ ...p.description !== void 0 ? { description: p.description } : {},
44252
+ executable: p.file,
44253
+ args: [...p.args],
44254
+ cwd: p.cwd ?? "",
44255
+ environment: environmentRecordToEntries(p.env),
44256
+ settingsSchema: settingsSchemaForProfile(p.profileId)
44003
44257
  }));
44004
44258
  }
44005
44259
  setInstanceControl(control) {
@@ -44011,6 +44265,9 @@ var TerminalSessionManager = class {
44011
44265
  async createInstance(input) {
44012
44266
  return this.requireInstanceControl().createInstance(input);
44013
44267
  }
44268
+ async updateInstance(input) {
44269
+ return this.requireInstanceControl().updateInstance(input);
44270
+ }
44014
44271
  async deleteInstance(input) {
44015
44272
  await this.requireInstanceControl().deleteInstance(input);
44016
44273
  }
@@ -44033,24 +44290,29 @@ var TerminalSessionManager = class {
44033
44290
  const cols = clampDimension(input.cols, MAX_COLS);
44034
44291
  const rows = clampDimension(input.rows, MAX_ROWS);
44035
44292
  const sessionId = this.mintId();
44293
+ const file = input.executable?.trim() || profile.file;
44294
+ const args = input.args !== void 0 ? [...input.args] : [...profile.args];
44295
+ const cwd = input.cwd?.trim() || profile.cwd || this.opts.cwd;
44296
+ const env = {
44297
+ ...this.opts.env,
44298
+ ...profile.env,
44299
+ ...environmentEntriesToRecord(input.environment ?? [])
44300
+ };
44036
44301
  const executable = [{
44037
- file: profile.file,
44038
- args: profile.args
44039
- }, ...profile.fallbacks ?? []].map((candidate) => ({
44302
+ file,
44303
+ args
44304
+ }, ...input.executable?.trim() ? [] : profile.fallbacks ?? []].map((candidate) => ({
44040
44305
  ...candidate,
44041
44306
  resolvedFile: this.resolveBinary(candidate.file)
44042
44307
  })).find((candidate) => candidate.resolvedFile !== null);
44043
- if (executable === void 0) throw new Error(`Terminal profile '${profile.profileId}' cannot start: executable '${profile.file}' was not found on PATH. Install it in the server image or set its path in Terminal settings.`);
44308
+ if (executable === void 0) throw new Error(`Terminal profile '${profile.profileId}' cannot start: executable '${file}' was not found on PATH. Install it in the server image or set its path in Terminal settings.`);
44044
44309
  const pty = this.opts.spawn({
44045
44310
  file: executable.resolvedFile,
44046
44311
  args: executable.args,
44047
44312
  cols,
44048
44313
  rows,
44049
- ...profile.cwd !== void 0 || this.opts.cwd !== void 0 ? { cwd: profile.cwd ?? this.opts.cwd } : {},
44050
- ...profile.env !== void 0 || this.opts.env !== void 0 ? { env: {
44051
- ...this.opts.env,
44052
- ...profile.env
44053
- } } : {}
44314
+ ...cwd !== void 0 && cwd !== "" ? { cwd } : {},
44315
+ ...Object.keys(env).length > 0 ? { env } : {}
44054
44316
  });
44055
44317
  const screen = this.opts.screenFactory(cols, rows);
44056
44318
  const info = {
@@ -44148,7 +44410,7 @@ var TerminalSessionManager = class {
44148
44410
  const oldestSeq = session.output[0]?.seq ?? session.nextSeq;
44149
44411
  if (input.afterSeq === 0 || input.afterSeq < oldestSeq - 1 || input.afterSeq > cursor) {
44150
44412
  await session.screen.flush();
44151
- const exit = [...session.output].reverse().find((event) => event.kind === "exit");
44413
+ const exit = session.output.toReversed().find((event) => event.kind === "exit");
44152
44414
  return {
44153
44415
  cursor,
44154
44416
  reset: true,
@@ -44392,7 +44654,28 @@ var TerminalAddon = class extends BaseAddon {
44392
44654
  const localNodeId = declarationOwnerNodeId(this.ctx.kernel.localNodeId);
44393
44655
  const cameraRelay = new TerminalCameraRelay({
44394
44656
  listProfiles: (nodeId) => nodeId === localNodeId ? manager.listProfiles() : this.ctx.api.terminalSession.listProfiles.query(void 0, nodePin(nodeId)),
44395
- openSession: (nodeId, input) => nodeId === localNodeId ? manager.openSession(input) : this.ctx.api.terminalSession.openSession.mutate(input, nodePin(nodeId)),
44657
+ openSession: async (nodeId, input) => {
44658
+ const instance = input.instanceId ? this.terminalInstances().find((row) => row.id === input.instanceId) : void 0;
44659
+ const profile = (nodeId === localNodeId ? await manager.listProfiles() : await this.ctx.api.terminalSession.listProfiles.query(void 0, nodePin(nodeId))).find((candidate) => candidate.profileId === (instance?.profileId ?? input.profileId));
44660
+ const args = instance ? spawnArgsForInstance({
44661
+ profileId: instance.profileId,
44662
+ instanceArgs: instance.args,
44663
+ profileArgs: profile?.args ?? [],
44664
+ profileSettings: instance.profileSettings
44665
+ }) : void 0;
44666
+ const payload = {
44667
+ profileId: input.profileId,
44668
+ cols: input.cols,
44669
+ rows: input.rows,
44670
+ ...instance ? {
44671
+ ...instance.executable ? { executable: instance.executable } : {},
44672
+ ...args !== void 0 ? { args: [...args] } : {},
44673
+ ...instance.cwd ? { cwd: instance.cwd } : {},
44674
+ ...instance.environment.length > 0 ? { environment: [...instance.environment] } : {}
44675
+ } : {}
44676
+ };
44677
+ return nodeId === localNodeId ? manager.openSession(payload) : this.ctx.api.terminalSession.openSession.mutate(payload, nodePin(nodeId));
44678
+ },
44396
44679
  pullOutput: (nodeId, input) => nodeId === localNodeId ? manager.pullOutput(input) : this.ctx.api.terminalSession.pullOutput.mutate(input, nodePin(nodeId)),
44397
44680
  closeSession: async (nodeId, sessionId) => {
44398
44681
  if (nodeId === localNodeId) await manager.close({ sessionId });
@@ -44597,6 +44880,7 @@ var TerminalAddon = class extends BaseAddon {
44597
44880
  return {
44598
44881
  listInstances: async () => this.terminalInstances().map((instance) => this.instanceInfo(instance)),
44599
44882
  createInstance: async (input) => this.createTerminalInstance(input),
44883
+ updateInstance: async (input) => this.updateTerminalInstance(input),
44600
44884
  deleteInstance: async ({ instanceId }) => this.deleteTerminalInstance(instanceId),
44601
44885
  setInstanceEnabled: async ({ instanceId, enabled }) => this.setTerminalInstanceEnabled(instanceId, enabled),
44602
44886
  listLegacyCameras: async () => this.listLegacyTerminalCameras(),
@@ -44616,7 +44900,12 @@ var TerminalAddon = class extends BaseAddon {
44616
44900
  profileId: instance.profileId,
44617
44901
  profileLabel: instance.profileLabel,
44618
44902
  name: instance.name,
44619
- enabled: instance.enabled
44903
+ enabled: instance.enabled,
44904
+ executable: instance.executable,
44905
+ args: instance.args,
44906
+ cwd: instance.cwd,
44907
+ environment: instance.environment,
44908
+ profileSettings: instance.profileSettings
44620
44909
  };
44621
44910
  }
44622
44911
  replaceTerminalCameraTombstones(stableIds) {
@@ -44644,11 +44933,37 @@ var TerminalAddon = class extends BaseAddon {
44644
44933
  profileId: profile.profileId,
44645
44934
  profileLabel: profile.label,
44646
44935
  name,
44647
- enabled: true
44936
+ enabled: true,
44937
+ executable: input.executable?.trim() || profile.executable || "",
44938
+ args: input.args ? [...input.args] : [...profile.args ?? []],
44939
+ cwd: input.cwd?.trim() || profile.cwd || "",
44940
+ environment: input.environment ? [...input.environment] : [...profile.environment ?? []],
44941
+ profileSettings: sanitizeProfileSettings(profile.profileId, input.profileSettings)
44648
44942
  };
44649
44943
  await this.updateGlobalSettings({ terminalInstances: [...this.config.terminalInstances, instance] });
44650
44944
  return this.instanceInfo(instance);
44651
44945
  }
44946
+ async updateTerminalInstance(input) {
44947
+ const updated = await this.instanceMutationQueue.run(() => this.updateTerminalInstanceUnlocked(input));
44948
+ await this.cameraRelay?.closeInstance(updated.instanceId);
44949
+ await this.reconcileTerminalCameras();
44950
+ return updated;
44951
+ }
44952
+ async updateTerminalInstanceUnlocked(input) {
44953
+ const instance = this.terminalInstances().find((candidate) => candidate.id === input.instanceId);
44954
+ if (!instance) throw new Error(`No such Terminal instance: ${input.instanceId}`);
44955
+ const updated = {
44956
+ ...instance,
44957
+ ...input.name !== void 0 ? { name: input.name.trim() } : {},
44958
+ ...input.executable !== void 0 ? { executable: input.executable.trim() } : {},
44959
+ ...input.args !== void 0 ? { args: [...input.args] } : {},
44960
+ ...input.cwd !== void 0 ? { cwd: input.cwd.trim() } : {},
44961
+ ...input.environment !== void 0 ? { environment: [...input.environment] } : {},
44962
+ ...input.profileSettings !== void 0 ? { profileSettings: sanitizeProfileSettings(instance.profileId, input.profileSettings) } : {}
44963
+ };
44964
+ await this.updateGlobalSettings({ terminalInstances: this.config.terminalInstances.map((candidate) => candidate.id === input.instanceId ? updated : candidate) });
44965
+ return this.instanceInfo(updated);
44966
+ }
44652
44967
  async deleteTerminalInstance(instanceId) {
44653
44968
  await this.instanceMutationQueue.run(() => this.deleteTerminalInstanceUnlocked(instanceId));
44654
44969
  await this.reconcileTerminalCameras();
@@ -44708,7 +45023,12 @@ var TerminalAddon = class extends BaseAddon {
44708
45023
  profileId: "monitor",
44709
45024
  profileLabel: legacy.profileLabel,
44710
45025
  name: requestedName?.trim() || legacy.name,
44711
- enabled: true
45026
+ enabled: true,
45027
+ executable: "",
45028
+ args: [],
45029
+ cwd: "",
45030
+ environment: [],
45031
+ profileSettings: {}
44712
45032
  };
44713
45033
  await this.updateGlobalSettings({ terminalInstances: [...this.config.terminalInstances, instance] });
44714
45034
  return this.instanceInfo(instance);
@@ -44947,7 +45267,7 @@ exports.buildCellRuns = buildCellRuns;
44947
45267
  exports.buildProfiles = buildProfiles;
44948
45268
  exports.createNodePtySpawner = createNodePtySpawner;
44949
45269
  exports.createTerminalDataPlaneHandler = createTerminalDataPlaneHandler;
44950
- exports.createXtermScreen = createXtermScreen;
45270
+ exports.createXtermScreen = createXtermScreen$1;
44951
45271
  exports.findProfile = findProfile;
44952
45272
  exports.resolveCellStyle = resolveCellStyle;
44953
45273
  exports.terminalPaletteColor = terminalPaletteColor;
package/dist/addon.mjs CHANGED
@@ -19980,6 +19980,9 @@ method(_void(), ProviderInfoSchema), method(object({ config: record(string(), un
19980
19980
  location: StorageLocationSchema,
19981
19981
  relativePath: string()
19982
19982
  }), _void(), { kind: "mutation" }), method(object({ location: StorageLocationSchema }), number().nullable()), method(BeginUploadInputSchema, BeginUploadResultSchema, { kind: "mutation" }), method(WriteChunkInputSchema, _void(), { kind: "mutation" }), method(FinalizeUploadInputSchema, _void(), { kind: "mutation" }), method(AbortUploadInputSchema, _void(), { kind: "mutation" }), method(BeginDownloadInputSchema, BeginDownloadResultSchema, { kind: "mutation" }), method(ReadChunkInputSchema, _instanceof(Uint8Array)), method(EndDownloadInputSchema, _void(), { kind: "mutation" });
19983
+ /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
19984
+ var ProfileSettingsSchemaBridge = unknown().nullable();
19985
+ var ProfileSettingsBagSchema = record(string(), unknown());
19983
19986
  /**
19984
19987
  * A live terminal session hosted by the provider addon. Output and input do
19985
19988
  * NOT flow through the capability — they use the addon data plane
@@ -20009,7 +20012,14 @@ var TerminalSessionInfoSchema = object({
20009
20012
  var TerminalProfileInfoSchema = object({
20010
20013
  profileId: string(),
20011
20014
  label: string(),
20012
- description: string().optional()
20015
+ description: string().optional(),
20016
+ /** Spawn defaults the instance form copies on create. */
20017
+ executable: string().optional(),
20018
+ args: array(string()).readonly().optional(),
20019
+ cwd: string().optional(),
20020
+ environment: array(string()).readonly().optional(),
20021
+ /** ConfigUISchema for instance knobs, or null when the profile has none. */
20022
+ settingsSchema: ProfileSettingsSchemaBridge.optional()
20013
20023
  });
20014
20024
  /**
20015
20025
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -20022,7 +20032,12 @@ var TerminalInstanceInfoSchema = object({
20022
20032
  profileId: string(),
20023
20033
  profileLabel: string(),
20024
20034
  name: string(),
20025
- enabled: boolean()
20035
+ enabled: boolean(),
20036
+ executable: string(),
20037
+ args: array(string()).readonly(),
20038
+ cwd: string(),
20039
+ environment: array(string()).readonly(),
20040
+ profileSettings: ProfileSettingsBagSchema
20026
20041
  });
20027
20042
  var TerminalLegacyCameraSchema = object({
20028
20043
  stableId: string(),
@@ -20068,7 +20083,24 @@ var terminalSessionCapability = {
20068
20083
  createInstance: method(object({
20069
20084
  targetNodeId: string().min(1),
20070
20085
  profileId: string().min(1),
20071
- name: string().trim().min(1).max(160).optional()
20086
+ name: string().trim().min(1).max(160).optional(),
20087
+ executable: string().max(1024).optional(),
20088
+ args: array(string().max(2048)).max(64).optional(),
20089
+ cwd: string().max(1024).optional(),
20090
+ environment: array(string().max(4096)).max(64).optional(),
20091
+ profileSettings: ProfileSettingsBagSchema.optional()
20092
+ }), TerminalInstanceInfoSchema, {
20093
+ kind: "mutation",
20094
+ auth: "admin"
20095
+ }),
20096
+ updateInstance: method(object({
20097
+ instanceId: string().min(1),
20098
+ name: string().trim().min(1).max(160).optional(),
20099
+ executable: string().max(1024).optional(),
20100
+ args: array(string().max(2048)).max(64).optional(),
20101
+ cwd: string().max(1024).optional(),
20102
+ environment: array(string().max(4096)).max(64).optional(),
20103
+ profileSettings: ProfileSettingsBagSchema.optional()
20072
20104
  }), TerminalInstanceInfoSchema, {
20073
20105
  kind: "mutation",
20074
20106
  auth: "admin"
@@ -20103,7 +20135,11 @@ var terminalSessionCapability = {
20103
20135
  openSession: method(object({
20104
20136
  profileId: string(),
20105
20137
  cols: number().int().positive(),
20106
- rows: number().int().positive()
20138
+ rows: number().int().positive(),
20139
+ executable: string().max(1024).optional(),
20140
+ args: array(string().max(2048)).max(64).optional(),
20141
+ cwd: string().max(1024).optional(),
20142
+ environment: array(string().max(4096)).max(64).optional()
20107
20143
  }), TerminalSessionInfoSchema, {
20108
20144
  kind: "mutation",
20109
20145
  auth: "admin"
@@ -23992,10 +24028,10 @@ var lawnMowerControlCapability = {
23992
24028
  *
23993
24029
  * • The SDK (mobile / web client) consumes `getConnectionEndpoints()`
23994
24030
  * to receive an ordered list of candidate base URLs it should race
23995
- * on connect — LAN IPv4 first (lowest latency when on same network),
23996
- * then public hostname (if a tunnel is up), then IPv6. The SDK can
23997
- * race them with short timeouts and stick with the winner for the
23998
- * session.
24031
+ * on connect — LAN IPv4 and stable LAN IPv6 first (lowest latency
24032
+ * when on the same network), then public hostname (if a tunnel is
24033
+ * up). The SDK can race them with short timeouts and stick with the
24034
+ * winner for the session.
23999
24035
  *
24000
24036
  * Why hub-only: agents are not directly addressable by the operator's
24001
24037
  * clients — they reverse-connect to the hub. Exposing their interfaces
@@ -24158,6 +24194,20 @@ var AllowedAddressesSchema = object({
24158
24194
  * Network Addresses admin page and persisted by the addon.
24159
24195
  */
24160
24196
  addresses: array(string()).readonly() });
24197
+ var TlsStatusSchema = object({
24198
+ mode: _enum([
24199
+ "generated",
24200
+ "uploaded",
24201
+ "disabled"
24202
+ ]),
24203
+ leafFingerprintSha256: string().nullable(),
24204
+ caFingerprintSha256: string().nullable(),
24205
+ validTo: string().nullable(),
24206
+ sans: array(string()),
24207
+ caCertPem: string().nullable(),
24208
+ reissueError: string().nullable(),
24209
+ restartRequired: boolean()
24210
+ });
24161
24211
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
24162
24212
  /**
24163
24213
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -24177,7 +24227,20 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
24177
24227
  * to avoid mixed-content blocks in the browser. The public
24178
24228
  * tunnel always emits `https://` regardless. */
24179
24229
  scheme: _enum(["http", "https"]).optional()
24180
- }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
24230
+ }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" }), method(_void(), TlsStatusSchema), method(object({ reason: string().optional() }), TlsStatusSchema, {
24231
+ kind: "mutation",
24232
+ auth: "admin"
24233
+ }), method(object({
24234
+ certPem: string().min(1),
24235
+ keyPem: string().min(1),
24236
+ caPem: string().optional()
24237
+ }), TlsStatusSchema, {
24238
+ kind: "mutation",
24239
+ auth: "admin"
24240
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
24241
+ kind: "mutation",
24242
+ auth: "admin"
24243
+ });
24181
24244
  var LockControlStatusSchema = object({
24182
24245
  /** Lifecycle state of the lock. `jammed` means the motor reported
24183
24246
  * failure to reach the target — operator intervention required. */
@@ -32202,6 +32265,12 @@ Object.freeze({
32202
32265
  addonId: null,
32203
32266
  access: "create"
32204
32267
  },
32268
+ "localNetwork.downloadCa": {
32269
+ capName: "local-network",
32270
+ capScope: "system",
32271
+ addonId: null,
32272
+ access: "view"
32273
+ },
32205
32274
  "localNetwork.getAllowedAddresses": {
32206
32275
  capName: "local-network",
32207
32276
  capScope: "system",
@@ -32226,18 +32295,36 @@ Object.freeze({
32226
32295
  addonId: null,
32227
32296
  access: "view"
32228
32297
  },
32298
+ "localNetwork.getTlsStatus": {
32299
+ capName: "local-network",
32300
+ capScope: "system",
32301
+ addonId: null,
32302
+ access: "view"
32303
+ },
32229
32304
  "localNetwork.list": {
32230
32305
  capName: "local-network",
32231
32306
  capScope: "system",
32232
32307
  addonId: null,
32233
32308
  access: "view"
32234
32309
  },
32310
+ "localNetwork.regenerateCertificate": {
32311
+ capName: "local-network",
32312
+ capScope: "system",
32313
+ addonId: null,
32314
+ access: "create"
32315
+ },
32235
32316
  "localNetwork.resetAllowlistToBestMatch": {
32236
32317
  capName: "local-network",
32237
32318
  capScope: "system",
32238
32319
  addonId: null,
32239
32320
  access: "delete"
32240
32321
  },
32322
+ "localNetwork.revertToGeneratedCertificate": {
32323
+ capName: "local-network",
32324
+ capScope: "system",
32325
+ addonId: null,
32326
+ access: "create"
32327
+ },
32241
32328
  "localNetwork.setAllowedAddresses": {
32242
32329
  capName: "local-network",
32243
32330
  capScope: "system",
@@ -32250,6 +32337,12 @@ Object.freeze({
32250
32337
  addonId: null,
32251
32338
  access: "create"
32252
32339
  },
32340
+ "localNetwork.uploadCertificate": {
32341
+ capName: "local-network",
32342
+ capScope: "system",
32343
+ addonId: null,
32344
+ access: "create"
32345
+ },
32253
32346
  "lockControl.lock": {
32254
32347
  capName: "lock-control",
32255
32348
  capScope: "device",
@@ -35130,6 +35223,12 @@ Object.freeze({
35130
35223
  addonId: null,
35131
35224
  access: "create"
35132
35225
  },
35226
+ "terminalSession.updateInstance": {
35227
+ capName: "terminal-session",
35228
+ capScope: "system",
35229
+ addonId: null,
35230
+ access: "create"
35231
+ },
35133
35232
  "terminalSession.writeInput": {
35134
35233
  capName: "terminal-session",
35135
35234
  capScope: "system",
@@ -37617,6 +37716,35 @@ Object.freeze(Object.fromEntries([{
37617
37716
  }]
37618
37717
  }].map((s) => [s.stepId, s.defaultModelId])));
37619
37718
  string().min(1);
37719
+ var CLUSTER_STEP_SETTING_FIELDS = [{
37720
+ stepId: "face-embedding",
37721
+ key: "minLandmarkFaceSize",
37722
+ label: "Min face size for recognition (detection px)",
37723
+ description: "Refuse to embed a face smaller than this IN THE DETECTION FRAME. Applies to every node — the enrolled gallery is one index, and two admission floors would fill it from two policies. 0 disables the gate.",
37724
+ type: "slider",
37725
+ min: 0,
37726
+ max: 64,
37727
+ step: 2,
37728
+ default: 24
37729
+ }];
37730
+ function clusterStepSettingKey(stepId, fieldKey) {
37731
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
37732
+ }
37733
+ var ClusterSettingNumberSchema = number().finite();
37734
+ function readClusterStepSettings(config) {
37735
+ const out = {};
37736
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
37737
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
37738
+ const value = parsed.success ? parsed.data : field.default;
37739
+ const existing = out[field.stepId] ?? {};
37740
+ out[field.stepId] = {
37741
+ ...existing,
37742
+ [field.key]: value
37743
+ };
37744
+ }
37745
+ return out;
37746
+ }
37747
+ readClusterStepSettings({});
37620
37748
  object({
37621
37749
  /**
37622
37750
  * Fraction of the box's own size added on EACH side before cutting.
@@ -43149,7 +43277,7 @@ var require_xterm_headless = /* @__PURE__ */ __commonJSMin(((exports) => {
43149
43277
  var import_addon_serialize = require_addon_serialize();
43150
43278
  var import_xterm_headless = require_xterm_headless();
43151
43279
  var SCROLLBACK_LINES = 2e3;
43152
- function createXtermScreen(cols, rows) {
43280
+ function createXtermScreen$1(cols, rows) {
43153
43281
  const term = new import_xterm_headless.Terminal({
43154
43282
  cols,
43155
43283
  rows,
@@ -43293,7 +43421,7 @@ var TerminalCameraRelay = class {
43293
43421
  instanceId,
43294
43422
  nodeId,
43295
43423
  profileId,
43296
- screen: createXtermScreen(120, 40),
43424
+ screen: createXtermScreen$1(120, 40),
43297
43425
  sessionId: null,
43298
43426
  cursor: 0,
43299
43427
  clients: 0,
@@ -43324,7 +43452,8 @@ var TerminalCameraRelay = class {
43324
43452
  const opening = this.api.openSession(state.nodeId, {
43325
43453
  profileId: state.profileId,
43326
43454
  cols: 120,
43327
- rows: 40
43455
+ rows: 40,
43456
+ instanceId: state.instanceId
43328
43457
  });
43329
43458
  state.openPromise = opening;
43330
43459
  try {
@@ -43349,7 +43478,7 @@ var TerminalCameraRelay = class {
43349
43478
  applyBatch(state, batch) {
43350
43479
  if (batch.reset) {
43351
43480
  state.screen.dispose();
43352
- state.screen = createXtermScreen(120, 40);
43481
+ state.screen = createXtermScreen$1(120, 40);
43353
43482
  if (batch.snapshot) state.screen.write(batch.snapshot);
43354
43483
  }
43355
43484
  let exited = false;
@@ -43711,8 +43840,26 @@ var TerminalInstanceSchema = object({
43711
43840
  profileId: string().min(1).max(64),
43712
43841
  profileLabel: string().min(1).max(120),
43713
43842
  name: string().min(1).max(160),
43714
- enabled: boolean()
43843
+ enabled: boolean(),
43844
+ executable: string().max(1024).default(""),
43845
+ args: array(string().max(2048)).max(64).default([]),
43846
+ cwd: string().max(1024).default(""),
43847
+ environment: array(string().max(4096)).max(64).default([]),
43848
+ profileSettings: record(string(), unknown()).default({})
43715
43849
  });
43850
+ function environmentEntriesToRecord(entries) {
43851
+ const out = {};
43852
+ for (const entry of entries) {
43853
+ const eq = entry.indexOf("=");
43854
+ if (eq <= 0) continue;
43855
+ out[entry.slice(0, eq)] = entry.slice(eq + 1);
43856
+ }
43857
+ return out;
43858
+ }
43859
+ function environmentRecordToEntries(env) {
43860
+ if (env === void 0) return [];
43861
+ return Object.entries(env).map(([name, value]) => `${name}=${value}`);
43862
+ }
43716
43863
  /**
43717
43864
  * Config is operator-writable, so malformed or duplicate rows are ignored
43718
43865
  * rather than allowed to make declaration reconciliation destructive.
@@ -43912,6 +44059,108 @@ function findProfile(profiles, profileId) {
43912
44059
  return profiles.find((p) => p.profileId === profileId);
43913
44060
  }
43914
44061
  //#endregion
44062
+ //#region src/profile-settings.ts
44063
+ var GLANCES_PLUGINS = [
44064
+ {
44065
+ key: "showCpu",
44066
+ plugin: "cpu",
44067
+ label: "CPU"
44068
+ },
44069
+ {
44070
+ key: "showMem",
44071
+ plugin: "mem",
44072
+ label: "Memory"
44073
+ },
44074
+ {
44075
+ key: "showLoad",
44076
+ plugin: "load",
44077
+ label: "Load"
44078
+ },
44079
+ {
44080
+ key: "showNetwork",
44081
+ plugin: "network",
44082
+ label: "Network"
44083
+ },
44084
+ {
44085
+ key: "showDiskIo",
44086
+ plugin: "diskio",
44087
+ label: "Disk I/O"
44088
+ },
44089
+ {
44090
+ key: "showFs",
44091
+ plugin: "fs",
44092
+ label: "Filesystems"
44093
+ },
44094
+ {
44095
+ key: "showProcessList",
44096
+ plugin: "processlist",
44097
+ label: "Process list"
44098
+ },
44099
+ {
44100
+ key: "showContainers",
44101
+ plugin: "containers",
44102
+ label: "Containers"
44103
+ },
44104
+ {
44105
+ key: "showSensors",
44106
+ plugin: "sensors",
44107
+ label: "Sensors"
44108
+ }
44109
+ ];
44110
+ function glancesBooleanField(key, label) {
44111
+ return {
44112
+ type: "boolean",
44113
+ key,
44114
+ label,
44115
+ default: true,
44116
+ style: "switch"
44117
+ };
44118
+ }
44119
+ function glancesSettingsSchema() {
44120
+ return { sections: [{
44121
+ id: "glances-panels",
44122
+ title: "Glances panels",
44123
+ description: "Turn off a panel to pass --disable-plugin to this Terminal only. All on is the measured default (~1% of one core at the camera grid).",
44124
+ columns: 2,
44125
+ fields: GLANCES_PLUGINS.map((plugin) => glancesBooleanField(plugin.key, plugin.label))
44126
+ }] };
44127
+ }
44128
+ function settingsSchemaForProfile(profileId) {
44129
+ if (profileId === "glances") return glancesSettingsSchema();
44130
+ return null;
44131
+ }
44132
+ function glancesSettingsToArgs(settings) {
44133
+ const disabled = GLANCES_PLUGINS.filter((plugin) => settings[plugin.key] === false).map((plugin) => plugin.plugin);
44134
+ if (disabled.length === 0) return [];
44135
+ return ["--disable-plugin", disabled.join(",")];
44136
+ }
44137
+ function sanitizeProfileSettings(profileId, raw) {
44138
+ if (profileId !== "glances") return {};
44139
+ const bag = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
44140
+ const out = {
44141
+ showCpu: true,
44142
+ showMem: true,
44143
+ showLoad: true,
44144
+ showNetwork: true,
44145
+ showDiskIo: true,
44146
+ showFs: true,
44147
+ showProcessList: true,
44148
+ showContainers: true,
44149
+ showSensors: true
44150
+ };
44151
+ for (const plugin of GLANCES_PLUGINS) if (typeof bag[plugin.key] === "boolean") out[plugin.key] = bag[plugin.key];
44152
+ return out;
44153
+ }
44154
+ function profileSettingsToArgs(profileId, settings) {
44155
+ if (profileId !== "glances") return [];
44156
+ return glancesSettingsToArgs(sanitizeProfileSettings(profileId, settings));
44157
+ }
44158
+ function spawnArgsForInstance(input) {
44159
+ const extra = profileSettingsToArgs(input.profileId, input.profileSettings);
44160
+ if (input.instanceArgs.length > 0) return [...input.instanceArgs, ...extra];
44161
+ if (extra.length > 0) return [...input.profileArgs, ...extra];
44162
+ }
44163
+ //#endregion
43915
44164
  //#region src/terminal-session-manager.ts
43916
44165
  var MIN_GRID = 1;
43917
44166
  var MAX_COLS = 1e3;
@@ -43976,7 +44225,12 @@ var TerminalSessionManager = class {
43976
44225
  return this.profiles.map((p) => ({
43977
44226
  profileId: p.profileId,
43978
44227
  label: p.label,
43979
- ...p.description !== void 0 ? { description: p.description } : {}
44228
+ ...p.description !== void 0 ? { description: p.description } : {},
44229
+ executable: p.file,
44230
+ args: [...p.args],
44231
+ cwd: p.cwd ?? "",
44232
+ environment: environmentRecordToEntries(p.env),
44233
+ settingsSchema: settingsSchemaForProfile(p.profileId)
43980
44234
  }));
43981
44235
  }
43982
44236
  setInstanceControl(control) {
@@ -43988,6 +44242,9 @@ var TerminalSessionManager = class {
43988
44242
  async createInstance(input) {
43989
44243
  return this.requireInstanceControl().createInstance(input);
43990
44244
  }
44245
+ async updateInstance(input) {
44246
+ return this.requireInstanceControl().updateInstance(input);
44247
+ }
43991
44248
  async deleteInstance(input) {
43992
44249
  await this.requireInstanceControl().deleteInstance(input);
43993
44250
  }
@@ -44010,24 +44267,29 @@ var TerminalSessionManager = class {
44010
44267
  const cols = clampDimension(input.cols, MAX_COLS);
44011
44268
  const rows = clampDimension(input.rows, MAX_ROWS);
44012
44269
  const sessionId = this.mintId();
44270
+ const file = input.executable?.trim() || profile.file;
44271
+ const args = input.args !== void 0 ? [...input.args] : [...profile.args];
44272
+ const cwd = input.cwd?.trim() || profile.cwd || this.opts.cwd;
44273
+ const env = {
44274
+ ...this.opts.env,
44275
+ ...profile.env,
44276
+ ...environmentEntriesToRecord(input.environment ?? [])
44277
+ };
44013
44278
  const executable = [{
44014
- file: profile.file,
44015
- args: profile.args
44016
- }, ...profile.fallbacks ?? []].map((candidate) => ({
44279
+ file,
44280
+ args
44281
+ }, ...input.executable?.trim() ? [] : profile.fallbacks ?? []].map((candidate) => ({
44017
44282
  ...candidate,
44018
44283
  resolvedFile: this.resolveBinary(candidate.file)
44019
44284
  })).find((candidate) => candidate.resolvedFile !== null);
44020
- if (executable === void 0) throw new Error(`Terminal profile '${profile.profileId}' cannot start: executable '${profile.file}' was not found on PATH. Install it in the server image or set its path in Terminal settings.`);
44285
+ if (executable === void 0) throw new Error(`Terminal profile '${profile.profileId}' cannot start: executable '${file}' was not found on PATH. Install it in the server image or set its path in Terminal settings.`);
44021
44286
  const pty = this.opts.spawn({
44022
44287
  file: executable.resolvedFile,
44023
44288
  args: executable.args,
44024
44289
  cols,
44025
44290
  rows,
44026
- ...profile.cwd !== void 0 || this.opts.cwd !== void 0 ? { cwd: profile.cwd ?? this.opts.cwd } : {},
44027
- ...profile.env !== void 0 || this.opts.env !== void 0 ? { env: {
44028
- ...this.opts.env,
44029
- ...profile.env
44030
- } } : {}
44291
+ ...cwd !== void 0 && cwd !== "" ? { cwd } : {},
44292
+ ...Object.keys(env).length > 0 ? { env } : {}
44031
44293
  });
44032
44294
  const screen = this.opts.screenFactory(cols, rows);
44033
44295
  const info = {
@@ -44125,7 +44387,7 @@ var TerminalSessionManager = class {
44125
44387
  const oldestSeq = session.output[0]?.seq ?? session.nextSeq;
44126
44388
  if (input.afterSeq === 0 || input.afterSeq < oldestSeq - 1 || input.afterSeq > cursor) {
44127
44389
  await session.screen.flush();
44128
- const exit = [...session.output].reverse().find((event) => event.kind === "exit");
44390
+ const exit = session.output.toReversed().find((event) => event.kind === "exit");
44129
44391
  return {
44130
44392
  cursor,
44131
44393
  reset: true,
@@ -44369,7 +44631,28 @@ var TerminalAddon = class extends BaseAddon {
44369
44631
  const localNodeId = declarationOwnerNodeId(this.ctx.kernel.localNodeId);
44370
44632
  const cameraRelay = new TerminalCameraRelay({
44371
44633
  listProfiles: (nodeId) => nodeId === localNodeId ? manager.listProfiles() : this.ctx.api.terminalSession.listProfiles.query(void 0, nodePin(nodeId)),
44372
- openSession: (nodeId, input) => nodeId === localNodeId ? manager.openSession(input) : this.ctx.api.terminalSession.openSession.mutate(input, nodePin(nodeId)),
44634
+ openSession: async (nodeId, input) => {
44635
+ const instance = input.instanceId ? this.terminalInstances().find((row) => row.id === input.instanceId) : void 0;
44636
+ const profile = (nodeId === localNodeId ? await manager.listProfiles() : await this.ctx.api.terminalSession.listProfiles.query(void 0, nodePin(nodeId))).find((candidate) => candidate.profileId === (instance?.profileId ?? input.profileId));
44637
+ const args = instance ? spawnArgsForInstance({
44638
+ profileId: instance.profileId,
44639
+ instanceArgs: instance.args,
44640
+ profileArgs: profile?.args ?? [],
44641
+ profileSettings: instance.profileSettings
44642
+ }) : void 0;
44643
+ const payload = {
44644
+ profileId: input.profileId,
44645
+ cols: input.cols,
44646
+ rows: input.rows,
44647
+ ...instance ? {
44648
+ ...instance.executable ? { executable: instance.executable } : {},
44649
+ ...args !== void 0 ? { args: [...args] } : {},
44650
+ ...instance.cwd ? { cwd: instance.cwd } : {},
44651
+ ...instance.environment.length > 0 ? { environment: [...instance.environment] } : {}
44652
+ } : {}
44653
+ };
44654
+ return nodeId === localNodeId ? manager.openSession(payload) : this.ctx.api.terminalSession.openSession.mutate(payload, nodePin(nodeId));
44655
+ },
44373
44656
  pullOutput: (nodeId, input) => nodeId === localNodeId ? manager.pullOutput(input) : this.ctx.api.terminalSession.pullOutput.mutate(input, nodePin(nodeId)),
44374
44657
  closeSession: async (nodeId, sessionId) => {
44375
44658
  if (nodeId === localNodeId) await manager.close({ sessionId });
@@ -44574,6 +44857,7 @@ var TerminalAddon = class extends BaseAddon {
44574
44857
  return {
44575
44858
  listInstances: async () => this.terminalInstances().map((instance) => this.instanceInfo(instance)),
44576
44859
  createInstance: async (input) => this.createTerminalInstance(input),
44860
+ updateInstance: async (input) => this.updateTerminalInstance(input),
44577
44861
  deleteInstance: async ({ instanceId }) => this.deleteTerminalInstance(instanceId),
44578
44862
  setInstanceEnabled: async ({ instanceId, enabled }) => this.setTerminalInstanceEnabled(instanceId, enabled),
44579
44863
  listLegacyCameras: async () => this.listLegacyTerminalCameras(),
@@ -44593,7 +44877,12 @@ var TerminalAddon = class extends BaseAddon {
44593
44877
  profileId: instance.profileId,
44594
44878
  profileLabel: instance.profileLabel,
44595
44879
  name: instance.name,
44596
- enabled: instance.enabled
44880
+ enabled: instance.enabled,
44881
+ executable: instance.executable,
44882
+ args: instance.args,
44883
+ cwd: instance.cwd,
44884
+ environment: instance.environment,
44885
+ profileSettings: instance.profileSettings
44597
44886
  };
44598
44887
  }
44599
44888
  replaceTerminalCameraTombstones(stableIds) {
@@ -44621,11 +44910,37 @@ var TerminalAddon = class extends BaseAddon {
44621
44910
  profileId: profile.profileId,
44622
44911
  profileLabel: profile.label,
44623
44912
  name,
44624
- enabled: true
44913
+ enabled: true,
44914
+ executable: input.executable?.trim() || profile.executable || "",
44915
+ args: input.args ? [...input.args] : [...profile.args ?? []],
44916
+ cwd: input.cwd?.trim() || profile.cwd || "",
44917
+ environment: input.environment ? [...input.environment] : [...profile.environment ?? []],
44918
+ profileSettings: sanitizeProfileSettings(profile.profileId, input.profileSettings)
44625
44919
  };
44626
44920
  await this.updateGlobalSettings({ terminalInstances: [...this.config.terminalInstances, instance] });
44627
44921
  return this.instanceInfo(instance);
44628
44922
  }
44923
+ async updateTerminalInstance(input) {
44924
+ const updated = await this.instanceMutationQueue.run(() => this.updateTerminalInstanceUnlocked(input));
44925
+ await this.cameraRelay?.closeInstance(updated.instanceId);
44926
+ await this.reconcileTerminalCameras();
44927
+ return updated;
44928
+ }
44929
+ async updateTerminalInstanceUnlocked(input) {
44930
+ const instance = this.terminalInstances().find((candidate) => candidate.id === input.instanceId);
44931
+ if (!instance) throw new Error(`No such Terminal instance: ${input.instanceId}`);
44932
+ const updated = {
44933
+ ...instance,
44934
+ ...input.name !== void 0 ? { name: input.name.trim() } : {},
44935
+ ...input.executable !== void 0 ? { executable: input.executable.trim() } : {},
44936
+ ...input.args !== void 0 ? { args: [...input.args] } : {},
44937
+ ...input.cwd !== void 0 ? { cwd: input.cwd.trim() } : {},
44938
+ ...input.environment !== void 0 ? { environment: [...input.environment] } : {},
44939
+ ...input.profileSettings !== void 0 ? { profileSettings: sanitizeProfileSettings(instance.profileId, input.profileSettings) } : {}
44940
+ };
44941
+ await this.updateGlobalSettings({ terminalInstances: this.config.terminalInstances.map((candidate) => candidate.id === input.instanceId ? updated : candidate) });
44942
+ return this.instanceInfo(updated);
44943
+ }
44629
44944
  async deleteTerminalInstance(instanceId) {
44630
44945
  await this.instanceMutationQueue.run(() => this.deleteTerminalInstanceUnlocked(instanceId));
44631
44946
  await this.reconcileTerminalCameras();
@@ -44685,7 +45000,12 @@ var TerminalAddon = class extends BaseAddon {
44685
45000
  profileId: "monitor",
44686
45001
  profileLabel: legacy.profileLabel,
44687
45002
  name: requestedName?.trim() || legacy.name,
44688
- enabled: true
45003
+ enabled: true,
45004
+ executable: "",
45005
+ args: [],
45006
+ cwd: "",
45007
+ environment: [],
45008
+ profileSettings: {}
44689
45009
  };
44690
45010
  await this.updateGlobalSettings({ terminalInstances: [...this.config.terminalInstances, instance] });
44691
45011
  return this.instanceInfo(instance);
@@ -44916,4 +45236,4 @@ var TerminalAddon = class extends BaseAddon {
44916
45236
  }
44917
45237
  };
44918
45238
  //#endregion
44919
- export { TerminalAddon, createXtermScreen as a, buildCellRuns as c, terminalRgbColor as d, createNodePtySpawner as f, createTerminalDataPlaneHandler as i, resolveCellStyle as l, buildProfiles as n, TERMINAL_DEFAULT_BG as o, warmNodePty as p, findProfile as r, TERMINAL_DEFAULT_FG as s, TerminalSessionManager as t, terminalPaletteColor as u };
45239
+ export { TerminalAddon, createXtermScreen$1 as a, buildCellRuns as c, terminalRgbColor as d, createNodePtySpawner as f, createTerminalDataPlaneHandler as i, resolveCellStyle as l, buildProfiles as n, TERMINAL_DEFAULT_BG as o, warmNodePty as p, findProfile as r, TERMINAL_DEFAULT_FG as s, TerminalSessionManager as t, terminalPaletteColor as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-terminal",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "description": "Interactive terminal sessions (pty + xterm) as a CamStack addon",
5
5
  "keywords": [
6
6
  "camstack",