@avsbhq/cli 3.8.2 → 3.9.0

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 (2) hide show
  1. package/dist/cli.js +86 -14
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -26788,14 +26788,14 @@ var require_receiver = __commonJS({
26788
26788
  * @return {(Error|RangeError)} The error
26789
26789
  * @private
26790
26790
  */
26791
- createError(ErrorCtor, message, prefix, statusCode, errorCode) {
26791
+ createError(ErrorCtor, message, prefix, statusCode, errorCode2) {
26792
26792
  this._loop = false;
26793
26793
  this._errored = true;
26794
26794
  const err = new ErrorCtor(
26795
26795
  prefix ? `Invalid WebSocket frame: ${message}` : message
26796
26796
  );
26797
26797
  Error.captureStackTrace(err, this.createError);
26798
- err.code = errorCode;
26798
+ err.code = errorCode2;
26799
26799
  err[kStatusCode] = statusCode;
26800
26800
  return err;
26801
26801
  }
@@ -27681,7 +27681,7 @@ var require_websocket = __commonJS({
27681
27681
  var EventEmitter2 = __require("events");
27682
27682
  var https = __require("https");
27683
27683
  var http2 = __require("http");
27684
- var net = __require("net");
27684
+ var net2 = __require("net");
27685
27685
  var tls = __require("tls");
27686
27686
  var { randomBytes, createHash: createHash5 } = __require("crypto");
27687
27687
  var { Duplex, Readable: Readable2 } = __require("stream");
@@ -28415,12 +28415,12 @@ var require_websocket = __commonJS({
28415
28415
  }
28416
28416
  function netConnect(options) {
28417
28417
  options.path = options.socketPath;
28418
- return net.connect(options);
28418
+ return net2.connect(options);
28419
28419
  }
28420
28420
  function tlsConnect(options) {
28421
28421
  options.path = void 0;
28422
28422
  if (!options.servername && options.servername !== "") {
28423
- options.servername = net.isIP(options.host) ? "" : options.host;
28423
+ options.servername = net2.isIP(options.host) ? "" : options.host;
28424
28424
  }
28425
28425
  return tls.connect(options);
28426
28426
  }
@@ -39213,27 +39213,74 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
39213
39213
 
39214
39214
  // src/lib/wsServer.ts
39215
39215
  import http from "node:http";
39216
+ import net from "node:net";
39216
39217
  var PORT_START = 4400;
39217
39218
  var PORT_END = 4409;
39219
+ var DEFAULT_HOST = "127.0.0.1";
39218
39220
  var PING_INTERVAL_MS = 3e4;
39221
+ var PROBE_TIMEOUT_MS = 300;
39222
+ var LOOPBACK_PROBES = ["127.0.0.1", "::1"];
39219
39223
  function portsBusyError(start, end) {
39220
39224
  return new CliError("cli_failed", `Ports ${start} to ${end} are all busy.`, [
39221
39225
  "Another `avsb dev` (or another program) is holding them. Stop it, then run this again.",
39222
39226
  `\`lsof -i :${start}\` shows what is listening on a port.`
39223
39227
  ]);
39224
39228
  }
39229
+ function errorCode(err) {
39230
+ return typeof err === "object" && err !== null && "code" in err ? err.code : void 0;
39231
+ }
39225
39232
  function isAddressInUse(err) {
39226
- return typeof err === "object" && err !== null && "code" in err && err.code === "EADDRINUSE";
39233
+ return errorCode(err) === "EADDRINUSE";
39234
+ }
39235
+ var BAD_HOST_CODES = /* @__PURE__ */ new Set(["EADDRNOTAVAIL", "ENOTFOUND", "EAI_AGAIN", "EAI_NONAME"]);
39236
+ function badHostError(host) {
39237
+ return new CliError("cli_failed", `Cannot listen on ${host}: it is not an address of this machine.`, [
39238
+ `Leave out --host to listen on ${DEFAULT_HOST} (this machine only), or pass 0.0.0.0 to listen on every interface.`
39239
+ ]);
39240
+ }
39241
+ function isLoopbackHost(host) {
39242
+ return host === "localhost" || host === "::1" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
39243
+ }
39244
+ function describeDevServer(server) {
39245
+ if (isLoopbackHost(server.host)) {
39246
+ return { line: `Dev server running at ${server.url} (this machine only)`, warning: null };
39247
+ }
39248
+ return {
39249
+ line: `Dev server running at ${server.url}`,
39250
+ warning: `Listening on ${server.host}: other devices on your network can reach this server and read the code it serves.`
39251
+ };
39252
+ }
39253
+ function devServerUrl(host, port) {
39254
+ return `http://${host.includes(":") ? `[${host}]` : host}:${port}`;
39255
+ }
39256
+ function answersOn(address, port) {
39257
+ return new Promise((resolve3) => {
39258
+ const socket = net.connect({ host: address, port });
39259
+ const done = (answered) => {
39260
+ socket.destroy();
39261
+ resolve3(answered);
39262
+ };
39263
+ socket.setTimeout(PROBE_TIMEOUT_MS, () => done(false));
39264
+ socket.once("connect", () => done(true));
39265
+ socket.once("error", () => done(false));
39266
+ });
39267
+ }
39268
+ async function isPortAnsweredOnLoopback(port) {
39269
+ const answers = await Promise.all(LOOPBACK_PROBES.map((address) => answersOn(address, port)));
39270
+ return answers.some(Boolean);
39227
39271
  }
39228
39272
  async function startDevServer(options) {
39229
39273
  const { experimentName, experimentId, onExtensionMessage } = options;
39230
39274
  const start = options.portRange?.start ?? PORT_START;
39231
39275
  const end = options.portRange?.end ?? PORT_END;
39276
+ const host = options.host ?? DEFAULT_HOST;
39232
39277
  let bound = null;
39233
39278
  for (let port2 = start; port2 <= end && !bound; port2++) {
39279
+ if (await isPortAnsweredOnLoopback(port2)) continue;
39234
39280
  try {
39235
- bound = { ...await tryPort(port2, experimentName, experimentId, onExtensionMessage), port: port2 };
39281
+ bound = { ...await tryPort(port2, host, experimentName, experimentId, onExtensionMessage), port: port2 };
39236
39282
  } catch (err) {
39283
+ if (BAD_HOST_CODES.has(String(errorCode(err)))) throw badHostError(host);
39237
39284
  if (!isAddressInUse(err)) throw err;
39238
39285
  }
39239
39286
  }
@@ -39252,6 +39299,8 @@ async function startDevServer(options) {
39252
39299
  }, PING_INTERVAL_MS);
39253
39300
  return {
39254
39301
  port,
39302
+ host,
39303
+ url: devServerUrl(host, port),
39255
39304
  broadcast(msg) {
39256
39305
  broadcast(JSON.stringify(msg));
39257
39306
  },
@@ -39275,7 +39324,7 @@ async function startDevServer(options) {
39275
39324
  }
39276
39325
  };
39277
39326
  }
39278
- function tryPort(port, experimentName, experimentId, onExtensionMessage) {
39327
+ function tryPort(port, host, experimentName, experimentId, onExtensionMessage) {
39279
39328
  return new Promise((resolve3, reject) => {
39280
39329
  const httpServer = http.createServer((req, res) => {
39281
39330
  if (req.url === "/health") {
@@ -39313,7 +39362,7 @@ function tryPort(port, experimentName, experimentId, onExtensionMessage) {
39313
39362
  }
39314
39363
  });
39315
39364
  });
39316
- httpServer.listen(port, () => {
39365
+ httpServer.listen(port, host, () => {
39317
39366
  if (settled) return;
39318
39367
  settled = true;
39319
39368
  resolve3({ httpServer, wss });
@@ -39322,7 +39371,7 @@ function tryPort(port, experimentName, experimentId, onExtensionMessage) {
39322
39371
  }
39323
39372
 
39324
39373
  // src/commands/dev.ts
39325
- var devCommand = new Command("dev").description("Start dev server with live reload for the browser extension").action(async () => {
39374
+ var devCommand = new Command("dev").description("Start dev server with live reload for the browser extension").option("--host <host>", "Address to listen on: 127.0.0.1 is this machine only, 0.0.0.0 is every interface", DEFAULT_HOST).action(async (opts) => {
39326
39375
  try {
39327
39376
  ensureLoggedIn();
39328
39377
  const { manifest, dir } = readManifestOrExit();
@@ -39340,6 +39389,7 @@ var devCommand = new Command("dev").description("Start dev server with live relo
39340
39389
  const server = await startDevServer({
39341
39390
  experimentName: manifest.experimentName,
39342
39391
  experimentId: manifest.experimentId,
39392
+ host: opts.host,
39343
39393
  onExtensionMessage(msg, ws) {
39344
39394
  if (msg.type === "ext:connect") {
39345
39395
  const initMsg = {
@@ -39366,7 +39416,9 @@ var devCommand = new Command("dev").description("Start dev server with live relo
39366
39416
  }
39367
39417
  });
39368
39418
  line();
39369
- success(`Dev server running on port ${server.port}`);
39419
+ const listening = describeDevServer(server);
39420
+ success(listening.line);
39421
+ if (listening.warning) warn(listening.warning);
39370
39422
  info("Watching for changes...");
39371
39423
  line();
39372
39424
  const watcher = startWatcher({
@@ -39670,7 +39722,7 @@ var projectPushCommand = new Command("push").description("Compile and push proje
39670
39722
  failCommand(err);
39671
39723
  }
39672
39724
  });
39673
- var projectDevCommand = new Command("dev").description("Watch project script and hot-reload via browser extension").action(async () => {
39725
+ var projectDevCommand = new Command("dev").description("Watch project script and hot-reload via browser extension").option("--host <host>", "Address to listen on: 127.0.0.1 is this machine only, 0.0.0.0 is every interface", DEFAULT_HOST).action(async (opts) => {
39674
39726
  try {
39675
39727
  ensureLoggedIn();
39676
39728
  const { manifest, dir } = readProjectManifestOrExit();
@@ -39696,6 +39748,7 @@ var projectDevCommand = new Command("dev").description("Watch project script and
39696
39748
  const server = await startDevServer({
39697
39749
  experimentName: `${manifest.projectName} (project script)`,
39698
39750
  experimentId: manifest.projectId,
39751
+ host: opts.host,
39699
39752
  onExtensionMessage(msg, ws) {
39700
39753
  if (msg.type === "ext:connect") {
39701
39754
  const initMsg = {
@@ -39711,7 +39764,9 @@ var projectDevCommand = new Command("dev").description("Watch project script and
39711
39764
  }
39712
39765
  });
39713
39766
  line();
39714
- success(`Dev server running on port ${server.port}`);
39767
+ const listening = describeDevServer(server);
39768
+ success(listening.line);
39769
+ if (listening.warning) warn(listening.warning);
39715
39770
  info("Watching for changes...");
39716
39771
  line();
39717
39772
  const watcher = startWatcher({
@@ -39982,6 +40037,21 @@ function readMetricList(data) {
39982
40037
  function readMetric(data) {
39983
40038
  return isRecord5(data) && isCliMetric(data.metric) ? data.metric : null;
39984
40039
  }
40040
+ function plural(count, one, many) {
40041
+ return `${count} ${count === 1 ? one : many}`;
40042
+ }
40043
+ function describeArchiveCascade(data) {
40044
+ if (!isRecord5(data) || !isRecord5(data.cascade)) return null;
40045
+ const { experimentCount, flagRuleCount } = data.cascade;
40046
+ const experiments = typeof experimentCount === "number" ? experimentCount : 0;
40047
+ const flagRules = typeof flagRuleCount === "number" ? flagRuleCount : 0;
40048
+ if (experiments === 0 && flagRules === 0) return null;
40049
+ const parts = [
40050
+ experiments > 0 ? plural(experiments, "experiment", "experiments") : null,
40051
+ flagRules > 0 ? plural(flagRules, "flag rule", "flag rules") : null
40052
+ ].filter((part) => part !== null);
40053
+ return `Detached from ${parts.join(" and ")}. Unarchiving will not reattach it.`;
40054
+ }
39985
40055
  function formatMetricTable(metrics, usage) {
39986
40056
  const rows = metrics.map((metric) => ({
39987
40057
  id: metricIdLabel(metric),
@@ -40130,6 +40200,8 @@ var metricsArchiveCommand = new Command("archive").argument("<metricId>", "Metri
40130
40200
  }
40131
40201
  const metric = readMetric(response.data);
40132
40202
  spinner.succeed(metric ? `Metric archived: ${metric.name} (${metricIdLabel(metric)})` : `Metric ${metricId} archived`);
40203
+ const cascade = describeArchiveCascade(response.data);
40204
+ if (cascade) info(cascade);
40133
40205
  });
40134
40206
  var metricsUnarchiveCommand = new Command("unarchive").argument("<metricId>", "Metric ID").description("Restore an archived metric").option("--project <project>", "Project ID (PRJ-42 or numeric); defaults to local manifest").action(async (metricId, opts) => {
40135
40207
  const config = ensureLoggedIn();
@@ -43226,7 +43298,7 @@ function fishCompletion(entries) {
43226
43298
  for (const entry of entries) {
43227
43299
  const condition = `'test (__avsb_path) = "${entry.path}"'`;
43228
43300
  if (entry.words.length > 0) {
43229
- lines.push(`complete -c avsb -n ${condition} -a ${quoteFish(entry.words.join(" "))}`);
43301
+ lines.push(`complete -c avsb -n ${condition} -f -a ${quoteFish(entry.words.join(" "))}`);
43230
43302
  }
43231
43303
  for (const flag of entry.flags) {
43232
43304
  lines.push(`complete -c avsb -n ${condition} -l ${flag.replace(/^--/, "")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avsbhq/cli",
3
- "version": "3.8.2",
3
+ "version": "3.9.0",
4
4
  "description": "CLI tool for local A/B experiment development with the A vs B platform",
5
5
  "type": "module",
6
6
  "license": "MIT",