@avsbhq/cli 3.8.2 → 3.9.1

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 +107 -15
  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,87 @@ 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";
39217
+
39218
+ // src/lib/devOrigin.ts
39219
+ var LOCAL_PAGE = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d{1,5})?$/;
39220
+ var CHROME_EXTENSION = /^chrome-extension:\/\/[a-p]{32}$/;
39221
+ var FIREFOX_EXTENSION = /^moz-extension:\/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
39222
+ function isAllowedDevOrigin(origin) {
39223
+ if (origin === void 0 || origin === "") return true;
39224
+ const value = origin.trim().toLowerCase();
39225
+ return LOCAL_PAGE.test(value) || CHROME_EXTENSION.test(value) || FIREFOX_EXTENSION.test(value);
39226
+ }
39227
+ var REFUSED_ORIGIN_STATUS = 403;
39228
+
39229
+ // src/lib/wsServer.ts
39216
39230
  var PORT_START = 4400;
39217
39231
  var PORT_END = 4409;
39232
+ var DEFAULT_HOST = "127.0.0.1";
39218
39233
  var PING_INTERVAL_MS = 3e4;
39234
+ var PROBE_TIMEOUT_MS = 300;
39235
+ var LOOPBACK_PROBES = ["127.0.0.1", "::1"];
39219
39236
  function portsBusyError(start, end) {
39220
39237
  return new CliError("cli_failed", `Ports ${start} to ${end} are all busy.`, [
39221
39238
  "Another `avsb dev` (or another program) is holding them. Stop it, then run this again.",
39222
39239
  `\`lsof -i :${start}\` shows what is listening on a port.`
39223
39240
  ]);
39224
39241
  }
39242
+ function errorCode(err) {
39243
+ return typeof err === "object" && err !== null && "code" in err ? err.code : void 0;
39244
+ }
39225
39245
  function isAddressInUse(err) {
39226
- return typeof err === "object" && err !== null && "code" in err && err.code === "EADDRINUSE";
39246
+ return errorCode(err) === "EADDRINUSE";
39247
+ }
39248
+ var BAD_HOST_CODES = /* @__PURE__ */ new Set(["EADDRNOTAVAIL", "ENOTFOUND", "EAI_AGAIN", "EAI_NONAME"]);
39249
+ function badHostError(host) {
39250
+ return new CliError("cli_failed", `Cannot listen on ${host}: it is not an address of this machine.`, [
39251
+ `Leave out --host to listen on ${DEFAULT_HOST} (this machine only), or pass 0.0.0.0 to listen on every interface.`
39252
+ ]);
39253
+ }
39254
+ function isLoopbackHost(host) {
39255
+ return host === "localhost" || host === "::1" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
39256
+ }
39257
+ function describeDevServer(server) {
39258
+ if (isLoopbackHost(server.host)) {
39259
+ return { line: `Dev server running at ${server.url} (this machine only)`, warning: null };
39260
+ }
39261
+ return {
39262
+ line: `Dev server running at ${server.url}`,
39263
+ warning: `Listening on ${server.host}: other devices on your network can reach this server and read the code it serves.`
39264
+ };
39265
+ }
39266
+ function devServerUrl(host, port) {
39267
+ return `http://${host.includes(":") ? `[${host}]` : host}:${port}`;
39268
+ }
39269
+ function answersOn(address, port) {
39270
+ return new Promise((resolve3) => {
39271
+ const socket = net.connect({ host: address, port });
39272
+ const done = (answered) => {
39273
+ socket.destroy();
39274
+ resolve3(answered);
39275
+ };
39276
+ socket.setTimeout(PROBE_TIMEOUT_MS, () => done(false));
39277
+ socket.once("connect", () => done(true));
39278
+ socket.once("error", () => done(false));
39279
+ });
39280
+ }
39281
+ async function isPortAnsweredOnLoopback(port) {
39282
+ const answers = await Promise.all(LOOPBACK_PROBES.map((address) => answersOn(address, port)));
39283
+ return answers.some(Boolean);
39227
39284
  }
39228
39285
  async function startDevServer(options) {
39229
39286
  const { experimentName, experimentId, onExtensionMessage } = options;
39230
39287
  const start = options.portRange?.start ?? PORT_START;
39231
39288
  const end = options.portRange?.end ?? PORT_END;
39289
+ const host = options.host ?? DEFAULT_HOST;
39232
39290
  let bound = null;
39233
39291
  for (let port2 = start; port2 <= end && !bound; port2++) {
39292
+ if (await isPortAnsweredOnLoopback(port2)) continue;
39234
39293
  try {
39235
- bound = { ...await tryPort(port2, experimentName, experimentId, onExtensionMessage), port: port2 };
39294
+ bound = { ...await tryPort(port2, host, experimentName, experimentId, onExtensionMessage), port: port2 };
39236
39295
  } catch (err) {
39296
+ if (BAD_HOST_CODES.has(String(errorCode(err)))) throw badHostError(host);
39237
39297
  if (!isAddressInUse(err)) throw err;
39238
39298
  }
39239
39299
  }
@@ -39252,6 +39312,8 @@ async function startDevServer(options) {
39252
39312
  }, PING_INTERVAL_MS);
39253
39313
  return {
39254
39314
  port,
39315
+ host,
39316
+ url: devServerUrl(host, port),
39255
39317
  broadcast(msg) {
39256
39318
  broadcast(JSON.stringify(msg));
39257
39319
  },
@@ -39275,7 +39337,7 @@ async function startDevServer(options) {
39275
39337
  }
39276
39338
  };
39277
39339
  }
39278
- function tryPort(port, experimentName, experimentId, onExtensionMessage) {
39340
+ function tryPort(port, host, experimentName, experimentId, onExtensionMessage) {
39279
39341
  return new Promise((resolve3, reject) => {
39280
39342
  const httpServer = http.createServer((req, res) => {
39281
39343
  if (req.url === "/health") {
@@ -39293,7 +39355,14 @@ function tryPort(port, experimentName, experimentId, onExtensionMessage) {
39293
39355
  })
39294
39356
  );
39295
39357
  });
39296
- const wss = new import_websocket_server.default({ server: httpServer });
39358
+ const wss = new import_websocket_server.default({
39359
+ server: httpServer,
39360
+ // A web page must not reach the code this server streams (devOrigin.ts).
39361
+ verifyClient: (info2, done) => {
39362
+ if (isAllowedDevOrigin(info2.req.headers.origin)) done(true);
39363
+ else done(false, REFUSED_ORIGIN_STATUS, "Origin not allowed");
39364
+ }
39365
+ });
39297
39366
  let settled = false;
39298
39367
  const fail2 = (err) => {
39299
39368
  if (settled) return;
@@ -39313,7 +39382,7 @@ function tryPort(port, experimentName, experimentId, onExtensionMessage) {
39313
39382
  }
39314
39383
  });
39315
39384
  });
39316
- httpServer.listen(port, () => {
39385
+ httpServer.listen(port, host, () => {
39317
39386
  if (settled) return;
39318
39387
  settled = true;
39319
39388
  resolve3({ httpServer, wss });
@@ -39322,7 +39391,7 @@ function tryPort(port, experimentName, experimentId, onExtensionMessage) {
39322
39391
  }
39323
39392
 
39324
39393
  // src/commands/dev.ts
39325
- var devCommand = new Command("dev").description("Start dev server with live reload for the browser extension").action(async () => {
39394
+ 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
39395
  try {
39327
39396
  ensureLoggedIn();
39328
39397
  const { manifest, dir } = readManifestOrExit();
@@ -39340,6 +39409,7 @@ var devCommand = new Command("dev").description("Start dev server with live relo
39340
39409
  const server = await startDevServer({
39341
39410
  experimentName: manifest.experimentName,
39342
39411
  experimentId: manifest.experimentId,
39412
+ host: opts.host,
39343
39413
  onExtensionMessage(msg, ws) {
39344
39414
  if (msg.type === "ext:connect") {
39345
39415
  const initMsg = {
@@ -39366,7 +39436,9 @@ var devCommand = new Command("dev").description("Start dev server with live relo
39366
39436
  }
39367
39437
  });
39368
39438
  line();
39369
- success(`Dev server running on port ${server.port}`);
39439
+ const listening = describeDevServer(server);
39440
+ success(listening.line);
39441
+ if (listening.warning) warn(listening.warning);
39370
39442
  info("Watching for changes...");
39371
39443
  line();
39372
39444
  const watcher = startWatcher({
@@ -39670,7 +39742,7 @@ var projectPushCommand = new Command("push").description("Compile and push proje
39670
39742
  failCommand(err);
39671
39743
  }
39672
39744
  });
39673
- var projectDevCommand = new Command("dev").description("Watch project script and hot-reload via browser extension").action(async () => {
39745
+ 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
39746
  try {
39675
39747
  ensureLoggedIn();
39676
39748
  const { manifest, dir } = readProjectManifestOrExit();
@@ -39696,6 +39768,7 @@ var projectDevCommand = new Command("dev").description("Watch project script and
39696
39768
  const server = await startDevServer({
39697
39769
  experimentName: `${manifest.projectName} (project script)`,
39698
39770
  experimentId: manifest.projectId,
39771
+ host: opts.host,
39699
39772
  onExtensionMessage(msg, ws) {
39700
39773
  if (msg.type === "ext:connect") {
39701
39774
  const initMsg = {
@@ -39711,7 +39784,9 @@ var projectDevCommand = new Command("dev").description("Watch project script and
39711
39784
  }
39712
39785
  });
39713
39786
  line();
39714
- success(`Dev server running on port ${server.port}`);
39787
+ const listening = describeDevServer(server);
39788
+ success(listening.line);
39789
+ if (listening.warning) warn(listening.warning);
39715
39790
  info("Watching for changes...");
39716
39791
  line();
39717
39792
  const watcher = startWatcher({
@@ -39982,6 +40057,21 @@ function readMetricList(data) {
39982
40057
  function readMetric(data) {
39983
40058
  return isRecord5(data) && isCliMetric(data.metric) ? data.metric : null;
39984
40059
  }
40060
+ function plural(count, one, many) {
40061
+ return `${count} ${count === 1 ? one : many}`;
40062
+ }
40063
+ function describeArchiveCascade(data) {
40064
+ if (!isRecord5(data) || !isRecord5(data.cascade)) return null;
40065
+ const { experimentCount, flagRuleCount } = data.cascade;
40066
+ const experiments = typeof experimentCount === "number" ? experimentCount : 0;
40067
+ const flagRules = typeof flagRuleCount === "number" ? flagRuleCount : 0;
40068
+ if (experiments === 0 && flagRules === 0) return null;
40069
+ const parts = [
40070
+ experiments > 0 ? plural(experiments, "experiment", "experiments") : null,
40071
+ flagRules > 0 ? plural(flagRules, "flag rule", "flag rules") : null
40072
+ ].filter((part) => part !== null);
40073
+ return `Detached from ${parts.join(" and ")}. Unarchiving will not reattach it.`;
40074
+ }
39985
40075
  function formatMetricTable(metrics, usage) {
39986
40076
  const rows = metrics.map((metric) => ({
39987
40077
  id: metricIdLabel(metric),
@@ -40130,6 +40220,8 @@ var metricsArchiveCommand = new Command("archive").argument("<metricId>", "Metri
40130
40220
  }
40131
40221
  const metric = readMetric(response.data);
40132
40222
  spinner.succeed(metric ? `Metric archived: ${metric.name} (${metricIdLabel(metric)})` : `Metric ${metricId} archived`);
40223
+ const cascade = describeArchiveCascade(response.data);
40224
+ if (cascade) info(cascade);
40133
40225
  });
40134
40226
  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
40227
  const config = ensureLoggedIn();
@@ -43226,7 +43318,7 @@ function fishCompletion(entries) {
43226
43318
  for (const entry of entries) {
43227
43319
  const condition = `'test (__avsb_path) = "${entry.path}"'`;
43228
43320
  if (entry.words.length > 0) {
43229
- lines.push(`complete -c avsb -n ${condition} -a ${quoteFish(entry.words.join(" "))}`);
43321
+ lines.push(`complete -c avsb -n ${condition} -f -a ${quoteFish(entry.words.join(" "))}`);
43230
43322
  }
43231
43323
  for (const flag of entry.flags) {
43232
43324
  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.1",
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",