camstack 1.1.19 → 1.1.21

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.
@@ -14522,7 +14522,7 @@ function date4(params) {
14522
14522
  // ../../node_modules/zod/v4/classic/external.js
14523
14523
  config(en_default());
14524
14524
 
14525
- // ../types/dist/sleep-BiDFW0E7.mjs
14525
+ // ../types/dist/sleep-CZDdRBua.mjs
14526
14526
  var WELL_KNOWN_TABS = [
14527
14527
  {
14528
14528
  id: "overview",
@@ -15539,7 +15539,16 @@ var DecoderStatsSchema = external_exports.object({
15539
15539
  inputFps: external_exports.number(),
15540
15540
  outputFps: external_exports.number(),
15541
15541
  avgDecodeTimeMs: external_exports.number(),
15542
- droppedFrames: external_exports.number()
15542
+ droppedFrames: external_exports.number(),
15543
+ /**
15544
+ * Pull-mode adaptive-fps telemetry (optional — only pull sessions run the
15545
+ * lag-driven controller; push sessions omit these). `lagMs` is the EWMA of
15546
+ * the decoder's real-time drift (rising = falling behind live); `adaptiveFps`
15547
+ * is the current lag-throttled emit rate (≤ `effectiveFps` ceiling).
15548
+ */
15549
+ lagMs: external_exports.number().optional(),
15550
+ effectiveFps: external_exports.number().optional(),
15551
+ adaptiveFps: external_exports.number().optional()
15543
15552
  });
15544
15553
  var DecoderSessionConfigSchema = external_exports.object({
15545
15554
  codec: external_exports.string(),
@@ -15580,7 +15589,15 @@ var DecoderSessionConfigSchema = external_exports.object({
15580
15589
  * other — `pullFrames` returns nothing for an `'shm'` session and
15581
15590
  * `pullHandles` returns nothing for a `'callback'` session.
15582
15591
  */
15583
- frameSink: external_exports.enum(["callback", "shm"]).default("callback")
15592
+ frameSink: external_exports.enum(["callback", "shm"]).default("callback"),
15593
+ /**
15594
+ * Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
15595
+ * a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
15596
+ * real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
15597
+ * stream-broker's `streamingDebug` gate — off by default so production logs
15598
+ * stay quiet and the emit path pays zero per-frame cost when disabled.
15599
+ */
15600
+ debug: external_exports.boolean().optional()
15584
15601
  });
15585
15602
  var VideoEncodeSchema = external_exports.object({
15586
15603
  codec: external_exports.enum([
@@ -18137,6 +18154,67 @@ var coverCapability = {
18137
18154
  */
18138
18155
  runtimeState: CoverStatusSchema
18139
18156
  };
18157
+ var DayNightModeSchema = external_exports.enum([
18158
+ "auto",
18159
+ "day",
18160
+ "night",
18161
+ "schedule"
18162
+ ]);
18163
+ var NormalizedRangeSchema$1 = external_exports.object({
18164
+ min: external_exports.number(),
18165
+ max: external_exports.number(),
18166
+ step: external_exports.number()
18167
+ });
18168
+ var DayNightStatusSchema = external_exports.object({
18169
+ mode: DayNightModeSchema,
18170
+ /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
18171
+ sensitivity: external_exports.number().optional(),
18172
+ /** Delay before the IR-cut filter flips, in seconds. */
18173
+ switchDelaySec: external_exports.number().optional(),
18174
+ lastFetchedAt: external_exports.number()
18175
+ });
18176
+ var DayNightOptionsSchema = external_exports.object({
18177
+ /** Modes this camera accepts. Empty → the camera has no configurable day/night mode. */
18178
+ modes: external_exports.array(DayNightModeSchema),
18179
+ supportsSensitivity: external_exports.boolean(),
18180
+ /** Present when `supportsSensitivity` — the normalized 0–100 range. */
18181
+ sensitivity: NormalizedRangeSchema$1.optional(),
18182
+ supportsSwitchDelay: external_exports.boolean(),
18183
+ /** Present when `supportsSwitchDelay` — the allowed delay range in seconds. */
18184
+ switchDelaySec: NormalizedRangeSchema$1.optional()
18185
+ });
18186
+ var DayNightSettingsPatchSchema = external_exports.object({
18187
+ mode: DayNightModeSchema.optional(),
18188
+ sensitivity: external_exports.number().optional(),
18189
+ switchDelaySec: external_exports.number().optional()
18190
+ });
18191
+ var dayNightCapability = {
18192
+ name: "day-night",
18193
+ scope: "device",
18194
+ deviceNative: true,
18195
+ mode: "singleton",
18196
+ deviceTypes: [DeviceType.Camera],
18197
+ deviceConfig: { ui: {
18198
+ kind: "derived-form",
18199
+ builderId: "day-night",
18200
+ tab: "image"
18201
+ } },
18202
+ methods: {
18203
+ getOptions: method(external_exports.object({ deviceId: external_exports.number() }), DayNightOptionsSchema),
18204
+ setSettings: method(external_exports.object({
18205
+ deviceId: external_exports.number(),
18206
+ settings: DayNightSettingsPatchSchema
18207
+ }), external_exports.void(), {
18208
+ kind: "mutation",
18209
+ auth: "admin"
18210
+ })
18211
+ },
18212
+ status: {
18213
+ schema: DayNightStatusSchema,
18214
+ kind: "poll"
18215
+ },
18216
+ runtimeState: DayNightStatusSchema
18217
+ };
18140
18218
  var SourceInfoSchema = external_exports.object({
18141
18219
  /** Live dispatch key — mutable when the source system allows rename. */
18142
18220
  id: external_exports.string(),
@@ -18579,6 +18657,107 @@ var imageCapability = {
18579
18657
  */
18580
18658
  runtimeState: ImageStatusSchema
18581
18659
  };
18660
+ var ImageRotateSchema = external_exports.enum([
18661
+ "0",
18662
+ "90",
18663
+ "180",
18664
+ "270"
18665
+ ]);
18666
+ var WhiteBalanceModeSchema = external_exports.enum(["auto", "manual"]);
18667
+ var ExposureModeSchema = external_exports.enum(["auto", "manual"]);
18668
+ var BacklightModeSchema = external_exports.enum([
18669
+ "off",
18670
+ "blc",
18671
+ "wdr",
18672
+ "hlc"
18673
+ ]);
18674
+ var NormalizedRangeSchema = external_exports.object({
18675
+ min: external_exports.number(),
18676
+ max: external_exports.number(),
18677
+ step: external_exports.number()
18678
+ });
18679
+ var ImageSettingsStatusSchema = external_exports.object({
18680
+ /** Normalized 0–100. */
18681
+ brightness: external_exports.number().optional(),
18682
+ /** Normalized 0–100. */
18683
+ contrast: external_exports.number().optional(),
18684
+ /** Normalized 0–100. */
18685
+ saturation: external_exports.number().optional(),
18686
+ /** Normalized 0–100. */
18687
+ sharpness: external_exports.number().optional(),
18688
+ mirror: external_exports.boolean().optional(),
18689
+ flip: external_exports.boolean().optional(),
18690
+ rotate: ImageRotateSchema.optional(),
18691
+ whiteBalance: WhiteBalanceModeSchema.optional(),
18692
+ /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
18693
+ warmth: external_exports.number().optional(),
18694
+ exposureMode: ExposureModeSchema.optional(),
18695
+ backlightMode: BacklightModeSchema.optional(),
18696
+ lastFetchedAt: external_exports.number()
18697
+ });
18698
+ var ImageSettingsOptionsSchema = external_exports.object({
18699
+ supportsBrightness: external_exports.boolean(),
18700
+ brightness: NormalizedRangeSchema.optional(),
18701
+ supportsContrast: external_exports.boolean(),
18702
+ contrast: NormalizedRangeSchema.optional(),
18703
+ supportsSaturation: external_exports.boolean(),
18704
+ saturation: NormalizedRangeSchema.optional(),
18705
+ supportsSharpness: external_exports.boolean(),
18706
+ sharpness: NormalizedRangeSchema.optional(),
18707
+ supportsMirror: external_exports.boolean(),
18708
+ supportsFlip: external_exports.boolean(),
18709
+ /** Supported rotation values. Empty → rotation not configurable. */
18710
+ rotateOptions: external_exports.array(ImageRotateSchema),
18711
+ /** Supported white-balance modes. Empty → white-balance not configurable. */
18712
+ whiteBalanceModes: external_exports.array(WhiteBalanceModeSchema),
18713
+ supportsWarmth: external_exports.boolean(),
18714
+ /** Present when `supportsWarmth` — the normalized 0–100 range. */
18715
+ warmth: NormalizedRangeSchema.optional(),
18716
+ /** Supported exposure modes. Empty → exposure not configurable. */
18717
+ exposureModes: external_exports.array(ExposureModeSchema),
18718
+ /** Supported backlight modes. Empty → backlight-compensation not configurable. */
18719
+ backlightModes: external_exports.array(BacklightModeSchema)
18720
+ });
18721
+ var ImageSettingsPatchSchema = external_exports.object({
18722
+ brightness: external_exports.number().optional(),
18723
+ contrast: external_exports.number().optional(),
18724
+ saturation: external_exports.number().optional(),
18725
+ sharpness: external_exports.number().optional(),
18726
+ mirror: external_exports.boolean().optional(),
18727
+ flip: external_exports.boolean().optional(),
18728
+ rotate: ImageRotateSchema.optional(),
18729
+ whiteBalance: WhiteBalanceModeSchema.optional(),
18730
+ warmth: external_exports.number().optional(),
18731
+ exposureMode: ExposureModeSchema.optional(),
18732
+ backlightMode: BacklightModeSchema.optional()
18733
+ });
18734
+ var imageSettingsCapability = {
18735
+ name: "image-settings",
18736
+ scope: "device",
18737
+ deviceNative: true,
18738
+ mode: "singleton",
18739
+ deviceTypes: [DeviceType.Camera],
18740
+ deviceConfig: { ui: {
18741
+ kind: "derived-form",
18742
+ builderId: "image-settings",
18743
+ tab: "image"
18744
+ } },
18745
+ methods: {
18746
+ getOptions: method(external_exports.object({ deviceId: external_exports.number() }), ImageSettingsOptionsSchema),
18747
+ setSettings: method(external_exports.object({
18748
+ deviceId: external_exports.number(),
18749
+ settings: ImageSettingsPatchSchema
18750
+ }), external_exports.void(), {
18751
+ kind: "mutation",
18752
+ auth: "admin"
18753
+ })
18754
+ },
18755
+ status: {
18756
+ schema: ImageSettingsStatusSchema,
18757
+ kind: "poll"
18758
+ },
18759
+ runtimeState: ImageSettingsStatusSchema
18760
+ };
18582
18761
  var LawnMowerActivitySchema = external_exports.enum([
18583
18762
  "idle",
18584
18763
  "mowing",
@@ -19555,6 +19734,16 @@ var RunnerCameraConfigSchema = external_exports.object({
19555
19734
  * this gate is bypassed.
19556
19735
  */
19557
19736
  onboardMotionDrivesAnalyzer: external_exports.boolean().default(true),
19737
+ /**
19738
+ * Master toggle for the occupancy re-check. When `false` (DEFAULT) the runner
19739
+ * never arms the periodic recheck timer, regardless of `occupancyRecheckSec` —
19740
+ * this is off by default because the recheck re-subscribes a detection session
19741
+ * every N seconds while `watching`, a major source of pull-decoder re-dial
19742
+ * churn (each cycle creates+tears a session → RTSP re-dial → latency). The
19743
+ * `occupancyRecheckSec` / `occupancyRecheckFrames` sliders only take effect
19744
+ * (and only render) when this is enabled.
19745
+ */
19746
+ occupancyRecheckEnabled: external_exports.boolean().default(false),
19558
19747
  occupancyRecheckSec: external_exports.number().min(occupancyRecheckSecField.min).max(occupancyRecheckSecField.max).default(occupancyRecheckSecField.default),
19559
19748
  occupancyRecheckFrames: external_exports.number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default),
19560
19749
  /**
@@ -19590,6 +19779,8 @@ var RunnerCameraDeviceUIFields = [
19590
19779
  default: motionFpsField.default,
19591
19780
  showValue: true,
19592
19781
  unit: "fps",
19782
+ nullable: true,
19783
+ nullLabel: "Default",
19593
19784
  showWhen: {
19594
19785
  field: "motionSources",
19595
19786
  includes: "analyzer"
@@ -19604,7 +19795,9 @@ var RunnerCameraDeviceUIFields = [
19604
19795
  step: detectionFpsField.step,
19605
19796
  default: detectionFpsField.default,
19606
19797
  showValue: true,
19607
- unit: "fps"
19798
+ unit: "fps",
19799
+ nullable: true,
19800
+ nullLabel: "Default"
19608
19801
  },
19609
19802
  {
19610
19803
  key: "motionCooldownMs",
@@ -19617,7 +19810,9 @@ var RunnerCameraDeviceUIFields = [
19617
19810
  default: motionCooldownMsField.default,
19618
19811
  showValue: true,
19619
19812
  unit: "s",
19620
- displayScale: 1e3
19813
+ displayScale: 1e3,
19814
+ nullable: true,
19815
+ nullLabel: "Default"
19621
19816
  },
19622
19817
  {
19623
19818
  key: "onboardMotionDrivesAnalyzer",
@@ -19630,17 +19825,29 @@ var RunnerCameraDeviceUIFields = [
19630
19825
  includes: "onboard"
19631
19826
  }
19632
19827
  },
19828
+ {
19829
+ key: "occupancyRecheckEnabled",
19830
+ type: "boolean",
19831
+ style: "checkbox",
19832
+ label: "Occupancy re-check",
19833
+ description: "Periodically re-sample a few frames during the watching phase to confirm the scene is truly empty (catches stationary objects motion-gating would miss). Off by default \u2014 it adds decoder re-dial churn.",
19834
+ default: false
19835
+ },
19633
19836
  {
19634
19837
  key: "occupancyRecheckSec",
19635
19838
  type: "slider",
19636
19839
  label: "Occupancy re-check interval",
19637
- description: "How often (in seconds) the runner re-samples a few frames to confirm the scene is truly empty during the watching phase. 0 = disabled.",
19840
+ description: "How often (in seconds) the runner re-samples a few frames to confirm the scene is truly empty during the watching phase.",
19638
19841
  min: occupancyRecheckSecField.min,
19639
19842
  max: occupancyRecheckSecField.max,
19640
19843
  step: occupancyRecheckSecField.step,
19641
19844
  default: occupancyRecheckSecField.default,
19642
19845
  showValue: true,
19643
- unit: "s"
19846
+ unit: "s",
19847
+ showWhen: {
19848
+ field: "occupancyRecheckEnabled",
19849
+ equals: true
19850
+ }
19644
19851
  },
19645
19852
  {
19646
19853
  key: "occupancyRecheckFrames",
@@ -19651,7 +19858,11 @@ var RunnerCameraDeviceUIFields = [
19651
19858
  max: occupancyRecheckFramesField.max,
19652
19859
  step: occupancyRecheckFramesField.step,
19653
19860
  default: occupancyRecheckFramesField.default,
19654
- showValue: true
19861
+ showValue: true,
19862
+ showWhen: {
19863
+ field: "occupancyRecheckEnabled",
19864
+ equals: true
19865
+ }
19655
19866
  }
19656
19867
  ];
19657
19868
  var RunnerLocalLoadSchema = external_exports.object({
@@ -22155,7 +22366,7 @@ var audioCodecCapability = {
22155
22366
  name: "audio-codec",
22156
22367
  scope: "system",
22157
22368
  mode: "singleton",
22158
- preferredProvider: "audio-codec-ffmpeg",
22369
+ preferredProvider: "decoder-nodeav",
22159
22370
  methods: {
22160
22371
  /** Probe the local runtime and return the supported codec matrix. */
22161
22372
  listSupportedCodecs: method(external_exports.void(), external_exports.array(AudioCodecInfoSchema).readonly()),
@@ -22667,14 +22878,16 @@ var decoderCapability = {
22667
22878
  name: "decoder",
22668
22879
  scope: "system",
22669
22880
  mode: "singleton",
22670
- preferredProvider: "decoder-ffmpeg",
22881
+ preferredProvider: "decoder-nodeav",
22671
22882
  methods: {
22672
22883
  supportsCodec: method(external_exports.object({ codec: external_exports.string() }), external_exports.boolean()),
22673
22884
  getInfo: method(external_exports.void(), external_exports.object({
22674
22885
  id: external_exports.string(),
22675
22886
  name: external_exports.string(),
22676
22887
  isPullMode: external_exports.boolean().optional(),
22677
- priority: external_exports.number().optional()
22888
+ priority: external_exports.number().optional(),
22889
+ hwaccel: external_exports.string().optional(),
22890
+ probedBestHwaccel: external_exports.string().optional()
22678
22891
  })),
22679
22892
  createSession: method(DecoderSessionConfigSchema, external_exports.object({
22680
22893
  sessionId: external_exports.string(),
@@ -29738,6 +29951,18 @@ var METHOD_ACCESS_MAP = Object.freeze({
29738
29951
  addonId: null,
29739
29952
  access: "view"
29740
29953
  },
29954
+ "dayNight.getOptions": {
29955
+ capName: "day-night",
29956
+ capScope: "device",
29957
+ addonId: null,
29958
+ access: "view"
29959
+ },
29960
+ "dayNight.setSettings": {
29961
+ capName: "day-night",
29962
+ capScope: "device",
29963
+ addonId: null,
29964
+ access: "create"
29965
+ },
29741
29966
  "decoder.createSession": {
29742
29967
  capName: "decoder",
29743
29968
  capScope: "system",
@@ -30668,6 +30893,18 @@ var METHOD_ACCESS_MAP = Object.freeze({
30668
30893
  addonId: null,
30669
30894
  access: "create"
30670
30895
  },
30896
+ "imageSettings.getOptions": {
30897
+ capName: "image-settings",
30898
+ capScope: "device",
30899
+ addonId: null,
30900
+ access: "view"
30901
+ },
30902
+ "imageSettings.setSettings": {
30903
+ capName: "image-settings",
30904
+ capScope: "device",
30905
+ addonId: null,
30906
+ access: "create"
30907
+ },
30671
30908
  "integrations.create": {
30672
30909
  capName: "integrations",
30673
30910
  capScope: "system",
@@ -33400,6 +33637,10 @@ async function resolveHubFromDiscovered(nodes, httpsPort = DEFAULT_HUB_HTTPS_POR
33400
33637
  }
33401
33638
  return null;
33402
33639
  }
33640
+ async function filterHubNodes(nodes, httpsPort = DEFAULT_HUB_HTTPS_PORT) {
33641
+ const verdicts = await Promise.all(nodes.map((n) => probeHttps(n.address, httpsPort)));
33642
+ return nodes.filter((_, i) => verdicts[i] === true);
33643
+ }
33403
33644
  function parsePacket(text) {
33404
33645
  const parts = text.split("|");
33405
33646
  if (parts.length !== PACKET_FIELD_COUNT) return null;
@@ -33464,49 +33705,40 @@ async function runDiscover(args) {
33464
33705
  const group = typeof values["multicast-group"] === "string" ? values["multicast-group"] : void 0;
33465
33706
  const filterLabel = namespace ? `namespace "${namespace}"` : "all namespaces";
33466
33707
  console.log(
33467
- `[camstack] Listening for camstack nodes on ${filterLabel} (multicast ${group ?? DEFAULT_MULTICAST_GROUP}:${udpPort ?? DEFAULT_UDP_PORT}, ${(timeoutMs ?? 6e3) / 1e3}s)\u2026`
33708
+ `[camstack] Listening for camstack hubs on ${filterLabel} (multicast ${group ?? DEFAULT_MULTICAST_GROUP}:${udpPort ?? DEFAULT_UDP_PORT}, ${(timeoutMs ?? 6e3) / 1e3}s)\u2026`
33468
33709
  );
33469
- const nodes = await discoverNodes({
33710
+ const discovered = await discoverNodes({
33470
33711
  ...namespace !== void 0 ? { namespace } : {},
33471
33712
  ...timeoutMs !== void 0 ? { timeoutMs } : {},
33472
33713
  ...udpPort !== void 0 ? { udpPort } : {},
33473
33714
  ...group !== void 0 ? { multicastGroup: group } : {}
33474
33715
  });
33716
+ const hubs = await filterHubNodes(discovered);
33475
33717
  if (values.json === true) {
33476
- console.log(JSON.stringify(nodes, null, 2));
33718
+ console.log(JSON.stringify(hubs, null, 2));
33477
33719
  return;
33478
33720
  }
33479
- if (nodes.length === 0) {
33721
+ if (hubs.length === 0) {
33480
33722
  console.log(
33481
- "[camstack] No nodes responded. Verify the hub is running with a matching namespace + that you are on the same LAN."
33723
+ "[camstack] No hubs responded. Verify the hub is running with a matching namespace + that you are on the same LAN."
33482
33724
  );
33483
33725
  return;
33484
33726
  }
33485
- console.log(`[camstack] Found ${nodes.length} node(s):`);
33486
- const idWidth = Math.max(...nodes.map((n) => n.nodeID.length));
33487
- const nsWidth = Math.max(...nodes.map((n) => n.namespace.length));
33488
- for (const n of nodes) {
33727
+ console.log(`[camstack] Found ${hubs.length} hub(s):`);
33728
+ const idWidth = Math.max(...hubs.map((n) => n.nodeID.length));
33729
+ const nsWidth = Math.max(...hubs.map((n) => n.namespace.length));
33730
+ for (const n of hubs) {
33489
33731
  console.log(
33490
- ` \u2022 ${n.nodeID.padEnd(idWidth)} ${n.address} ns=${n.namespace.padEnd(nsWidth)} (moleculer tcp :${n.tcpPort})`
33732
+ ` \u2022 ${n.nodeID.padEnd(idWidth)} ${n.address} ns=${n.namespace.padEnd(nsWidth)} (https :${DEFAULT_HUB_HTTPS_PORT})`
33491
33733
  );
33492
- }
33493
- if (namespace) {
33494
- const hub = await resolveHubFromDiscovered(nodes);
33495
- if (hub) {
33496
- console.log(``);
33497
- console.log(`[camstack] Hub HTTPS surface: https://${hub.address}:${hub.port}`);
33498
- console.log(`[camstack] \u2192 camstack login -s https://${hub.address}:${hub.port}`);
33499
- } else {
33500
- console.log(``);
33501
- console.log(
33502
- `[camstack] No node responded on default HTTPS port ${DEFAULT_HUB_HTTPS_PORT}. Pass --server manually.`
33503
- );
33504
- }
33734
+ console.log(` \u2192 camstack login -s https://${n.address}:${DEFAULT_HUB_HTTPS_PORT}`);
33505
33735
  }
33506
33736
  }
33507
33737
 
33508
33738
  export {
33739
+ DEFAULT_HUB_HTTPS_PORT,
33509
33740
  discoverNodes,
33510
33741
  resolveHubFromDiscovered,
33742
+ filterHubNodes,
33511
33743
  runDiscover
33512
33744
  };
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runDiscover
4
- } from "./chunk-2EOZGJYA.js";
4
+ } from "./chunk-VL5TLTQF.js";
5
5
  import "./chunk-K3NQKI34.js";
6
6
 
7
7
  // src/cli.ts
@@ -137,8 +137,59 @@ function runSetup(args) {
137
137
 
138
138
  // src/commands/deploy.ts
139
139
  import { execSync } from "child_process";
140
+ import { randomBytes } from "crypto";
140
141
  import * as fs2 from "fs";
141
142
  import * as path2 from "path";
143
+
144
+ // src/http/hub-http.ts
145
+ import * as http from "http";
146
+ import * as https from "https";
147
+ import { Buffer as Buffer2 } from "buffer";
148
+ var insecureHubAgent = new https.Agent({ rejectUnauthorized: false, keepAlive: false });
149
+ var DEFAULT_TIMEOUT_MS = 3e4;
150
+ function hubRequest(url, opts = {}) {
151
+ const parsed = new URL(url);
152
+ const isHttps = parsed.protocol === "https:";
153
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
154
+ return new Promise((resolve4, reject) => {
155
+ const requestOptions = {
156
+ protocol: parsed.protocol,
157
+ hostname: parsed.hostname,
158
+ port: parsed.port || (isHttps ? 443 : 80),
159
+ path: `${parsed.pathname}${parsed.search}`,
160
+ method: opts.method ?? "GET",
161
+ headers: { ...opts.headers },
162
+ timeout: timeoutMs,
163
+ ...isHttps ? { agent: insecureHubAgent } : {}
164
+ };
165
+ const onResponse = (res) => {
166
+ const chunks = [];
167
+ res.on("data", (chunk) => chunks.push(chunk));
168
+ res.on("end", () => {
169
+ const buffer = Buffer2.concat(chunks);
170
+ const status = res.statusCode ?? 0;
171
+ resolve4({
172
+ ok: status >= 200 && status < 300,
173
+ status,
174
+ statusText: res.statusMessage ?? "",
175
+ headers: res.headers,
176
+ text: () => Promise.resolve(buffer.toString("utf8")),
177
+ json: () => Promise.resolve().then(() => JSON.parse(buffer.toString("utf8")))
178
+ });
179
+ });
180
+ res.on("error", reject);
181
+ };
182
+ const req = isHttps ? https.request(requestOptions, onResponse) : http.request(requestOptions, onResponse);
183
+ req.on("error", reject);
184
+ req.on("timeout", () => {
185
+ req.destroy(new Error(`Hub request to ${parsed.host} timed out after ${timeoutMs}ms`));
186
+ });
187
+ if (opts.body !== void 0) req.write(opts.body);
188
+ req.end();
189
+ });
190
+ }
191
+
192
+ // src/commands/deploy.ts
142
193
  function isRecord(value) {
143
194
  return value !== null && typeof value === "object" && !Array.isArray(value);
144
195
  }
@@ -167,7 +218,7 @@ async function withNetworkRetry(fn, opts) {
167
218
  async function verifyHubHealthy(serverUrl, opts) {
168
219
  for (let attempt = 1; attempt <= opts.attempts; attempt++) {
169
220
  try {
170
- const res = await fetch(`${serverUrl}/health`, { signal: AbortSignal.timeout(5e3) });
221
+ const res = await hubRequest(`${serverUrl}/health`, { timeoutMs: 5e3 });
171
222
  if (res.ok) {
172
223
  const body = await res.json().catch(() => ({}));
173
224
  if (body.ok === true) return true;
@@ -326,7 +377,7 @@ function packAddon(addonDir) {
326
377
  }
327
378
  async function fetchOnlineAgents(serverUrl, token) {
328
379
  const url = `${serverUrl}/trpc/nodes.topology?input=${encodeURIComponent('{"json":null}')}`;
329
- const res = await fetch(url, {
380
+ const res = await hubRequest(url, {
330
381
  headers: { Authorization: `Bearer ${token}` }
331
382
  });
332
383
  if (!res.ok) {
@@ -341,15 +392,36 @@ async function fetchOnlineAgents(serverUrl, token) {
341
392
  async function uploadToTarget(args) {
342
393
  const { serverUrl, token, tgzPath, nodeId, addonId } = args;
343
394
  const fileBuffer = fs2.readFileSync(tgzPath);
344
- const formData = new FormData();
345
- const blob = new Blob([fileBuffer], { type: "application/gzip" });
346
- formData.append("file", blob, path2.basename(tgzPath));
347
- if (nodeId) formData.append("nodeId", nodeId);
348
- if (addonId) formData.append("addonId", addonId);
349
- const response = await fetch(`${serverUrl}/api/addons/upload`, {
395
+ const boundary = `----camstackcli${randomBytes(16).toString("hex")}`;
396
+ const CRLF = "\r\n";
397
+ const parts = [
398
+ Buffer.from(
399
+ `--${boundary}${CRLF}Content-Disposition: form-data; name="file"; filename="${path2.basename(tgzPath)}"${CRLF}Content-Type: application/gzip${CRLF}${CRLF}`
400
+ ),
401
+ fileBuffer,
402
+ Buffer.from(CRLF)
403
+ ];
404
+ const appendField = (name, value) => {
405
+ parts.push(
406
+ Buffer.from(
407
+ `--${boundary}${CRLF}Content-Disposition: form-data; name="${name}"${CRLF}${CRLF}${value}${CRLF}`
408
+ )
409
+ );
410
+ };
411
+ if (nodeId) appendField("nodeId", nodeId);
412
+ if (addonId) appendField("addonId", addonId);
413
+ parts.push(Buffer.from(`--${boundary}--${CRLF}`));
414
+ const body = Buffer.concat(parts);
415
+ const response = await hubRequest(`${serverUrl}/api/addons/upload`, {
350
416
  method: "POST",
351
- headers: { Authorization: `Bearer ${token}` },
352
- body: formData
417
+ headers: {
418
+ Authorization: `Bearer ${token}`,
419
+ "Content-Type": `multipart/form-data; boundary=${boundary}`,
420
+ "Content-Length": String(body.length)
421
+ },
422
+ body,
423
+ // Large tarballs + a hub that may be mid-bounce — give it room.
424
+ timeoutMs: 12e4
353
425
  });
354
426
  const result = toDeployResult(await response.json().catch(() => ({})));
355
427
  if (!response.ok) {
@@ -557,7 +629,7 @@ async function askSelect(message, options) {
557
629
  async function callTrpcMutation(url, authorization, payload, isPayload) {
558
630
  const headers = { "Content-Type": "application/json" };
559
631
  if (authorization) headers["Authorization"] = authorization;
560
- const res = await fetch(url, {
632
+ const res = await hubRequest(url, {
561
633
  method: "POST",
562
634
  headers,
563
635
  body: JSON.stringify({ "0": { json: payload } })
@@ -608,7 +680,7 @@ function isUnknown(_value) {
608
680
  return true;
609
681
  }
610
682
  async function resolveServerInteractive(presetNamespace) {
611
- const { discoverNodes, resolveHubFromDiscovered } = await import("./discover-HWCPQL6X.js");
683
+ const { discoverNodes, resolveHubFromDiscovered, filterHubNodes, DEFAULT_HUB_HTTPS_PORT } = await import("./discover-3WTX7YIA.js");
612
684
  if (presetNamespace) {
613
685
  const spinner4 = clack.spinner();
614
686
  spinner4.start(`Discovering hub on LAN (namespace "${presetNamespace}")`);
@@ -619,41 +691,36 @@ async function resolveServerInteractive(presetNamespace) {
619
691
  }
620
692
  const hub = await resolveHubFromDiscovered(filtered);
621
693
  if (!hub) {
622
- spinner4.stop(`Found ${filtered.length} node(s) but none responded on HTTPS :4443.`);
694
+ spinner4.stop(`Found ${filtered.length} node(s) but none served the hub API on HTTPS :4443.`);
623
695
  throw new Error("Pass --server explicitly if the hub uses a non-default port.");
624
696
  }
625
697
  spinner4.stop(`Hub: ${hub.nodeID} @ https://${hub.address}:${hub.port}`);
626
698
  return `https://${hub.address}:${hub.port}`;
627
699
  }
628
700
  const spinner3 = clack.spinner();
629
- spinner3.start("Scanning LAN for camstack nodes (UDP multicast)");
701
+ spinner3.start("Discovering camstack hub on LAN (UDP multicast)");
630
702
  const all = await discoverNodes({});
631
- if (all.length === 0) {
632
- spinner3.stop("No camstack nodes responded on LAN.");
703
+ const hubs = await filterHubNodes(all);
704
+ if (hubs.length === 0) {
705
+ spinner3.stop("No camstack hub responded on LAN.");
633
706
  throw new Error("Either pass --server or verify the hub is running on the same network.");
634
707
  }
635
- spinner3.stop(`Found ${all.length} node(s).`);
708
+ if (hubs.length === 1) {
709
+ const only = hubs[0];
710
+ spinner3.stop(`Hub: ${only.nodeID} @ https://${only.address}:${DEFAULT_HUB_HTTPS_PORT}`);
711
+ return `https://${only.address}:${DEFAULT_HUB_HTTPS_PORT}`;
712
+ }
713
+ spinner3.stop(`Found ${hubs.length} hubs.`);
636
714
  const chosen = await askSelect(
637
715
  "Select a hub to log into",
638
- all.map((n) => ({
639
- value: `${n.address}|${n.tcpPort}|${n.nodeID}|${n.namespace}`,
716
+ hubs.map((n) => ({
717
+ value: `${n.address}|${n.nodeID}`,
640
718
  label: `${n.nodeID} ${n.address} (ns: ${n.namespace || "(none)"})`,
641
- hint: `tcp :${n.tcpPort}`
719
+ hint: `https :${DEFAULT_HUB_HTTPS_PORT}`
642
720
  }))
643
721
  );
644
- const [addr, , nodeID] = chosen.split("|");
645
- const chosenNode = all.find((n) => n.address === addr && n.nodeID === nodeID);
646
- const probeSpinner = clack.spinner();
647
- probeSpinner.start(`Probing https://${chosenNode.address}:4443`);
648
- const probed = await resolveHubFromDiscovered([chosenNode]);
649
- if (!probed) {
650
- probeSpinner.stop(
651
- `Node ${chosenNode.nodeID} at ${chosenNode.address} did not respond on HTTPS :4443.`
652
- );
653
- throw new Error("Pass --server explicitly if the hub uses a non-default port.");
654
- }
655
- probeSpinner.stop(`Reachable at https://${probed.address}:${probed.port}`);
656
- return `https://${probed.address}:${probed.port}`;
722
+ const [addr] = chosen.split("|");
723
+ return `https://${addr}:${DEFAULT_HUB_HTTPS_PORT}`;
657
724
  }
658
725
  var BACK_SENTINEL = "__back__";
659
726
  async function askWithBack(label, defaultValue) {
@@ -822,13 +889,10 @@ async function whoamiCommand(opts) {
822
889
  console.log("");
823
890
  console.log(`[camstack] Pinging ${session.server}/api/auth/whoami\u2026`);
824
891
  try {
825
- const controller = new AbortController();
826
- const timer = setTimeout(() => controller.abort(), 3e3);
827
- const res = await fetch(`${session.server}/api/auth/whoami`, {
892
+ const res = await hubRequest(`${session.server}/api/auth/whoami`, {
828
893
  headers: { Authorization: `Bearer ${session.token}` },
829
- signal: controller.signal
894
+ timeoutMs: 3e3
830
895
  });
831
- clearTimeout(timer);
832
896
  if (res.status === 401) {
833
897
  clearSession(session.server);
834
898
  console.error(`[camstack] \u2717 Token rejected by server (revoked / expired).`);
@@ -1230,7 +1294,7 @@ function printLine(e, addonId) {
1230
1294
  async function fetchLogs(server, token, addonId, limit) {
1231
1295
  const input = encodeURIComponent(JSON.stringify({ json: { addonId, limit } }));
1232
1296
  const url = `${server}/trpc/addons.getLogs?input=${input}`;
1233
- const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
1297
+ const res = await hubRequest(url, { headers: { Authorization: `Bearer ${token}` } });
1234
1298
  if (!res.ok) {
1235
1299
  const text2 = await res.text().catch(() => "");
1236
1300
  throw new Error(`getLogs HTTP ${res.status}: ${text2.slice(0, 200)}`);
@@ -1408,12 +1472,13 @@ function buildCommands() {
1408
1472
  help: () => [
1409
1473
  "Usage: camstack login [options]",
1410
1474
  "",
1411
- "Default flow (no flags): scans the LAN for camstack nodes via UDP multicast,",
1412
- " prompts you to pick one (arrow-key list), then user + password.",
1475
+ "Default flow (no flags): auto-discovers the hub on the LAN via UDP multicast",
1476
+ " (hub-only \u2014 agents never surface), then prompts for user + password. When a",
1477
+ " single hub is found it is used automatically; multiple hubs show a picker.",
1413
1478
  "",
1414
- "Options:",
1479
+ "Options (all optional):",
1415
1480
  " -s, --server <url> Skip discovery, use this URL directly ($CAMSTACK_SERVER)",
1416
- " -n, --namespace <ns> Skip the interactive picker \u2014 auto-resolves the hub for this namespace ($CAMSTACK_NAMESPACE)",
1481
+ " -n, --namespace <ns> Filter discovery to this cluster namespace ($CAMSTACK_NAMESPACE)",
1417
1482
  " -u, --username <name> Username (skips prompt)",
1418
1483
  " -p, --password <pwd> Password (skips prompt \u2014 prefer letting the CLI prompt)",
1419
1484
  ` --token-name <name> Name shown server-side (default: camstack-cli@${os4.hostname()})`
@@ -1421,12 +1486,13 @@ function buildCommands() {
1421
1486
  },
1422
1487
  {
1423
1488
  name: "discover",
1424
- summary: "Probe the LAN for camstack hubs/agents via UDP multicast",
1489
+ summary: "Probe the LAN for camstack hubs via UDP multicast",
1425
1490
  run: runDiscover,
1426
1491
  help: () => [
1427
1492
  "Usage: camstack discover [options]",
1428
1493
  "",
1429
- "With no flags: lists every camstack node visible on the LAN (any namespace).",
1494
+ "With no flags: lists every camstack HUB visible on the LAN (any namespace).",
1495
+ " Agents are never listed \u2014 only nodes serving the hub HTTPS API on :4443.",
1430
1496
  "",
1431
1497
  "Options:",
1432
1498
  " -n, --namespace <ns> Filter results to one namespace ($CAMSTACK_NAMESPACE)",
@@ -1590,7 +1656,12 @@ async function runLogin(args) {
1590
1656
  namespace: { type: "string", short: "n" },
1591
1657
  username: { type: "string", short: "u" },
1592
1658
  password: { type: "string", short: "p" },
1593
- "token-name": { type: "string" }
1659
+ "token-name": { type: "string" },
1660
+ // Accepted-and-ignored: `--no-verify` is a `deploy`-only flag (skip the
1661
+ // post-deploy health gate). Login has no verify step, but operators
1662
+ // muscle-memory it in — we swallow it here so `camstack login --no-verify`
1663
+ // never errors with "Unknown option --no-verify".
1664
+ "no-verify": { type: "boolean" }
1594
1665
  },
1595
1666
  false
1596
1667
  );
@@ -1712,9 +1783,6 @@ function optionalString(values, key, outKey) {
1712
1783
  return { [outKey ?? key]: v };
1713
1784
  }
1714
1785
  async function main() {
1715
- if (!process.env.CAMSTACK_STRICT_TLS) {
1716
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
1717
- }
1718
1786
  maybeNotifyUpdate("camstack", pkgVersion);
1719
1787
  const argv = process.argv.slice(2);
1720
1788
  const commands = buildCommands();
@@ -1,12 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ DEFAULT_HUB_HTTPS_PORT,
3
4
  discoverNodes,
5
+ filterHubNodes,
4
6
  resolveHubFromDiscovered,
5
7
  runDiscover
6
- } from "./chunk-2EOZGJYA.js";
8
+ } from "./chunk-VL5TLTQF.js";
7
9
  import "./chunk-K3NQKI34.js";
8
10
  export {
11
+ DEFAULT_HUB_HTTPS_PORT,
9
12
  discoverNodes,
13
+ filterHubNodes,
10
14
  resolveHubFromDiscovered,
11
15
  runDiscover
12
16
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "camstack",
3
- "version": "1.1.19",
3
+ "version": "1.1.21",
4
4
  "description": "CLI tool for managing and running CamStack server",
5
5
  "keywords": [
6
6
  "camstack",