@avsbhq/cli 3.8.1 → 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.
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { createRequire } from "node:module";
3
- const require = createRequire(import.meta.url);
2
+ import { createRequire as __cliCreateRequire } from "node:module";
3
+ const require = __cliCreateRequire(import.meta.url);
4
4
  var __create = Object.create;
5
5
  var __defProp = Object.defineProperty;
6
6
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -1150,8 +1150,8 @@ var require_command = __commonJS({
1150
1150
  "node_modules/commander/lib/command.js"(exports) {
1151
1151
  var EventEmitter2 = __require("node:events").EventEmitter;
1152
1152
  var childProcess = __require("node:child_process");
1153
- var path22 = __require("node:path");
1154
- var fs18 = __require("node:fs");
1153
+ var path23 = __require("node:path");
1154
+ var fs19 = __require("node:fs");
1155
1155
  var process11 = __require("node:process");
1156
1156
  var { Argument: Argument2, humanReadableArgName } = require_argument();
1157
1157
  var { CommanderError: CommanderError2 } = require_error();
@@ -2132,7 +2132,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2132
2132
  * @param {string} subcommandName
2133
2133
  */
2134
2134
  _checkForMissingExecutable(executableFile, executableDir, subcommandName) {
2135
- if (fs18.existsSync(executableFile)) return;
2135
+ if (fs19.existsSync(executableFile)) return;
2136
2136
  const executableDirMessage = executableDir ? `searched for local subcommand relative to directory '${executableDir}'` : "no directory for search for local subcommand, use .executableDir() to supply a custom directory";
2137
2137
  const executableMissing = `'${executableFile}' does not exist
2138
2138
  - if '${subcommandName}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead
@@ -2150,11 +2150,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
2150
2150
  let launchWithNode = false;
2151
2151
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
2152
2152
  function findFile(baseDir, baseName) {
2153
- const localBin = path22.resolve(baseDir, baseName);
2154
- if (fs18.existsSync(localBin)) return localBin;
2155
- if (sourceExt.includes(path22.extname(baseName))) return void 0;
2153
+ const localBin = path23.resolve(baseDir, baseName);
2154
+ if (fs19.existsSync(localBin)) return localBin;
2155
+ if (sourceExt.includes(path23.extname(baseName))) return void 0;
2156
2156
  const foundExt = sourceExt.find(
2157
- (ext) => fs18.existsSync(`${localBin}${ext}`)
2157
+ (ext) => fs19.existsSync(`${localBin}${ext}`)
2158
2158
  );
2159
2159
  if (foundExt) return `${localBin}${foundExt}`;
2160
2160
  return void 0;
@@ -2166,21 +2166,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
2166
2166
  if (this._scriptPath) {
2167
2167
  let resolvedScriptPath;
2168
2168
  try {
2169
- resolvedScriptPath = fs18.realpathSync(this._scriptPath);
2169
+ resolvedScriptPath = fs19.realpathSync(this._scriptPath);
2170
2170
  } catch {
2171
2171
  resolvedScriptPath = this._scriptPath;
2172
2172
  }
2173
- executableDir = path22.resolve(
2174
- path22.dirname(resolvedScriptPath),
2173
+ executableDir = path23.resolve(
2174
+ path23.dirname(resolvedScriptPath),
2175
2175
  executableDir
2176
2176
  );
2177
2177
  }
2178
2178
  if (executableDir) {
2179
2179
  let localFile = findFile(executableDir, executableFile);
2180
2180
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
2181
- const legacyName = path22.basename(
2181
+ const legacyName = path23.basename(
2182
2182
  this._scriptPath,
2183
- path22.extname(this._scriptPath)
2183
+ path23.extname(this._scriptPath)
2184
2184
  );
2185
2185
  if (legacyName !== this._name) {
2186
2186
  localFile = findFile(
@@ -2191,7 +2191,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2191
2191
  }
2192
2192
  executableFile = localFile || executableFile;
2193
2193
  }
2194
- launchWithNode = sourceExt.includes(path22.extname(executableFile));
2194
+ launchWithNode = sourceExt.includes(path23.extname(executableFile));
2195
2195
  let proc;
2196
2196
  if (process11.platform !== "win32") {
2197
2197
  if (launchWithNode) {
@@ -3038,7 +3038,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
3038
3038
  * @return {Command}
3039
3039
  */
3040
3040
  nameFromFilename(filename) {
3041
- this._name = path22.basename(filename, path22.extname(filename));
3041
+ this._name = path23.basename(filename, path23.extname(filename));
3042
3042
  return this;
3043
3043
  }
3044
3044
  /**
@@ -3052,9 +3052,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3052
3052
  * @param {string} [path]
3053
3053
  * @return {(string|null|Command)}
3054
3054
  */
3055
- executableDir(path23) {
3056
- if (path23 === void 0) return this._executableDir;
3057
- this._executableDir = path23;
3055
+ executableDir(path24) {
3056
+ if (path24 === void 0) return this._executableDir;
3057
+ this._executableDir = path24;
3058
3058
  return this;
3059
3059
  }
3060
3060
  /**
@@ -3227,13 +3227,13 @@ Expecting one of '${allowedValues.join("', '")}'`);
3227
3227
  * @param {(string | Function)} text - string to add, or a function returning a string
3228
3228
  * @return {Command} `this` command for chaining
3229
3229
  */
3230
- addHelpText(position, text) {
3230
+ addHelpText(position2, text) {
3231
3231
  const allowedValues = ["beforeAll", "before", "after", "afterAll"];
3232
- if (!allowedValues.includes(position)) {
3232
+ if (!allowedValues.includes(position2)) {
3233
3233
  throw new Error(`Unexpected value for position to addHelpText.
3234
3234
  Expecting one of '${allowedValues.join("', '")}'`);
3235
3235
  }
3236
- const helpEvent = `${position}Help`;
3236
+ const helpEvent = `${position2}Help`;
3237
3237
  this.on(helpEvent, (context) => {
3238
3238
  let helpStr;
3239
3239
  if (typeof text === "function") {
@@ -4418,15 +4418,15 @@ var require_route = __commonJS({
4418
4418
  };
4419
4419
  }
4420
4420
  function wrapConversion(toModel, graph) {
4421
- const path22 = [graph[toModel].parent, toModel];
4421
+ const path23 = [graph[toModel].parent, toModel];
4422
4422
  let fn = conversions[graph[toModel].parent][toModel];
4423
4423
  let cur = graph[toModel].parent;
4424
4424
  while (graph[cur].parent) {
4425
- path22.unshift(graph[cur].parent);
4425
+ path23.unshift(graph[cur].parent);
4426
4426
  fn = link(conversions[graph[cur].parent][cur], fn);
4427
4427
  cur = graph[cur].parent;
4428
4428
  }
4429
- fn.conversion = path22;
4429
+ fn.conversion = path23;
4430
4430
  return fn;
4431
4431
  }
4432
4432
  module.exports = function(fromModel) {
@@ -10482,10 +10482,10 @@ var require_lib2 = __commonJS({
10482
10482
  exports.analyse = analyse;
10483
10483
  var detectFile = (filepath, opts = {}) => new Promise((resolve3, reject) => {
10484
10484
  let fd;
10485
- const fs18 = (0, node_1.default)();
10485
+ const fs19 = (0, node_1.default)();
10486
10486
  const handler = (err, buffer) => {
10487
10487
  if (fd) {
10488
- fs18.closeSync(fd);
10488
+ fs19.closeSync(fd);
10489
10489
  }
10490
10490
  if (err) {
10491
10491
  reject(err);
@@ -10497,9 +10497,9 @@ var require_lib2 = __commonJS({
10497
10497
  };
10498
10498
  const sampleSize = (opts === null || opts === void 0 ? void 0 : opts.sampleSize) || 0;
10499
10499
  if (sampleSize > 0) {
10500
- fd = fs18.openSync(filepath, "r");
10500
+ fd = fs19.openSync(filepath, "r");
10501
10501
  let sample = Buffer.allocUnsafe(sampleSize);
10502
- fs18.read(fd, sample, 0, sampleSize, opts.offset, (err, bytesRead) => {
10502
+ fs19.read(fd, sample, 0, sampleSize, opts.offset, (err, bytesRead) => {
10503
10503
  if (err) {
10504
10504
  handler(err, null);
10505
10505
  } else {
@@ -10511,22 +10511,22 @@ var require_lib2 = __commonJS({
10511
10511
  });
10512
10512
  return;
10513
10513
  }
10514
- fs18.readFile(filepath, handler);
10514
+ fs19.readFile(filepath, handler);
10515
10515
  });
10516
10516
  exports.detectFile = detectFile;
10517
10517
  var detectFileSync = (filepath, opts = {}) => {
10518
- const fs18 = (0, node_1.default)();
10518
+ const fs19 = (0, node_1.default)();
10519
10519
  if (opts && opts.sampleSize) {
10520
- const fd = fs18.openSync(filepath, "r");
10520
+ const fd = fs19.openSync(filepath, "r");
10521
10521
  let sample = Buffer.allocUnsafe(opts.sampleSize);
10522
- const bytesRead = fs18.readSync(fd, sample, 0, opts.sampleSize, opts.offset);
10522
+ const bytesRead = fs19.readSync(fd, sample, 0, opts.sampleSize, opts.offset);
10523
10523
  if (bytesRead < opts.sampleSize) {
10524
10524
  sample = sample.subarray(0, bytesRead);
10525
10525
  }
10526
- fs18.closeSync(fd);
10526
+ fs19.closeSync(fd);
10527
10527
  return (0, exports.detect)(sample);
10528
10528
  }
10529
- return (0, exports.detect)(fs18.readFileSync(filepath));
10529
+ return (0, exports.detect)(fs19.readFileSync(filepath));
10530
10530
  };
10531
10531
  exports.detectFileSync = detectFileSync;
10532
10532
  exports.default = {
@@ -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
  }
@@ -29097,9 +29097,9 @@ var require_websocket_server = __commonJS({
29097
29097
  });
29098
29098
 
29099
29099
  // src/cli.ts
29100
- import fs17 from "node:fs";
29100
+ import fs18 from "node:fs";
29101
29101
  import { fileURLToPath as fileURLToPath2 } from "node:url";
29102
- import path21 from "node:path";
29102
+ import path22 from "node:path";
29103
29103
 
29104
29104
  // node_modules/commander/esm.mjs
29105
29105
  var import_index = __toESM(require_commander(), 1);
@@ -29119,7 +29119,7 @@ var {
29119
29119
  } = import_index.default;
29120
29120
 
29121
29121
  // src/commands/init.ts
29122
- import path9 from "node:path";
29122
+ import path10 from "node:path";
29123
29123
 
29124
29124
  // node_modules/@inquirer/core/dist/esm/lib/key.js
29125
29125
  var isUpKey = (key, keybindings = []) => (
@@ -30382,16 +30382,16 @@ var esm_default2 = createPrompt((config, done) => {
30382
30382
  } else if (isNumberKey(key)) {
30383
30383
  const selectedIndex = Number(key.name) - 1;
30384
30384
  let selectableIndex = -1;
30385
- const position = items.findIndex((item) => {
30385
+ const position2 = items.findIndex((item) => {
30386
30386
  if (Separator.isSeparator(item))
30387
30387
  return false;
30388
30388
  selectableIndex++;
30389
30389
  return selectableIndex === selectedIndex;
30390
30390
  });
30391
- const selectedItem = items[position];
30391
+ const selectedItem = items[position2];
30392
30392
  if (selectedItem && isSelectable(selectedItem)) {
30393
- setActive(position);
30394
- setItems(items.map((choice, i) => i === position ? toggle(choice) : choice));
30393
+ setActive(position2);
30394
+ setItems(items.map((choice, i) => i === position2 ? toggle(choice) : choice));
30395
30395
  }
30396
30396
  }
30397
30397
  });
@@ -31442,15 +31442,15 @@ var esm_default11 = createPrompt((config, done) => {
31442
31442
  } else if (isNumberKey(key) && !Number.isNaN(Number(rl.line))) {
31443
31443
  const selectedIndex = Number(rl.line) - 1;
31444
31444
  let selectableIndex = -1;
31445
- const position = items.findIndex((item2) => {
31445
+ const position2 = items.findIndex((item2) => {
31446
31446
  if (Separator.isSeparator(item2))
31447
31447
  return false;
31448
31448
  selectableIndex++;
31449
31449
  return selectableIndex === selectedIndex;
31450
31450
  });
31451
- const item = items[position];
31451
+ const item = items[position2];
31452
31452
  if (item != null && isSelectable3(item)) {
31453
- setActive(position);
31453
+ setActive(position2);
31454
31454
  }
31455
31455
  searchTimeoutRef.current = setTimeout(() => {
31456
31456
  rl.clearLine(0);
@@ -31529,9 +31529,9 @@ var import_run_async = __toESM(require_run_async(), 1);
31529
31529
  var import_mute_stream2 = __toESM(require_lib(), 1);
31530
31530
  import readline3 from "node:readline";
31531
31531
  var _ = {
31532
- set: (obj, path22 = "", value) => {
31532
+ set: (obj, path23 = "", value) => {
31533
31533
  let pointer = obj;
31534
- path22.split(".").forEach((key, index, arr) => {
31534
+ path23.split(".").forEach((key, index, arr) => {
31535
31535
  if (key === "__proto__" || key === "constructor")
31536
31536
  return;
31537
31537
  if (index === arr.length - 1) {
@@ -31542,8 +31542,8 @@ var _ = {
31542
31542
  pointer = pointer[key];
31543
31543
  });
31544
31544
  },
31545
- get: (obj, path22 = "", defaultValue) => {
31546
- const travel = (regexp) => String.prototype.split.call(path22, regexp).filter(Boolean).reduce(
31545
+ get: (obj, path23 = "", defaultValue) => {
31546
+ const travel = (regexp) => String.prototype.split.call(path23, regexp).filter(Boolean).reduce(
31547
31547
  // @ts-expect-error implicit any on res[key]
31548
31548
  (res, key) => res == null ? res : res[key],
31549
31549
  obj
@@ -31853,7 +31853,8 @@ var EXIT_FOR_CODE = {
31853
31853
  cli_not_found: EXIT.failure,
31854
31854
  cli_conflict: EXIT.conflict,
31855
31855
  cli_io_failed: EXIT.failure,
31856
- cli_failed: EXIT.failure
31856
+ cli_failed: EXIT.failure,
31857
+ cli_network: EXIT.failure
31857
31858
  };
31858
31859
  var CliError = class extends Error {
31859
31860
  code;
@@ -32062,9 +32063,9 @@ import os2 from "node:os";
32062
32063
  import tty from "node:tty";
32063
32064
  function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : process4.argv) {
32064
32065
  const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--";
32065
- const position = argv.indexOf(prefix + flag);
32066
+ const position2 = argv.indexOf(prefix + flag);
32066
32067
  const terminatorPosition = argv.indexOf("--");
32067
- return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
32068
+ return position2 !== -1 && (terminatorPosition === -1 || position2 < terminatorPosition);
32068
32069
  }
32069
32070
  var { env } = process4;
32070
32071
  var flagForceColor;
@@ -33105,6 +33106,14 @@ function label(key, value) {
33105
33106
  if (isQuiet()) return;
33106
33107
  writeOut(` ${source_default.dim(key + ":")} ${value}`);
33107
33108
  }
33109
+ function field(key, value) {
33110
+ if (isJsonOutput()) return;
33111
+ if (isQuiet()) {
33112
+ writeOut(`${key}: ${value}`);
33113
+ return;
33114
+ }
33115
+ writeOut(` ${source_default.dim(key + ":")} ${value}`);
33116
+ }
33108
33117
  function fileTree(lines) {
33109
33118
  if (isQuiet()) return;
33110
33119
  for (const line2 of lines) {
@@ -33117,6 +33126,62 @@ function line(msg = "") {
33117
33126
  writeOut(msg);
33118
33127
  }
33119
33128
 
33129
+ // src/lib/apiErrorDetails.ts
33130
+ function isRecord(value) {
33131
+ return typeof value === "object" && value !== null && !Array.isArray(value);
33132
+ }
33133
+ function readPath(value) {
33134
+ if (!Array.isArray(value)) return void 0;
33135
+ const path23 = value.filter((part) => typeof part === "string" || typeof part === "number");
33136
+ return path23.length === value.length ? path23 : void 0;
33137
+ }
33138
+ function readValidationIssues(details) {
33139
+ const raw = details?.issues;
33140
+ if (!Array.isArray(raw)) return [];
33141
+ const issues = [];
33142
+ for (const entry of raw) {
33143
+ if (!isRecord(entry) || typeof entry.message !== "string") continue;
33144
+ const path23 = readPath(entry.path) ?? [];
33145
+ const param = typeof entry.param === "string" ? entry.param : path23.join(".");
33146
+ issues.push({ path: path23, param, message: entry.message });
33147
+ }
33148
+ return issues;
33149
+ }
33150
+ function readCompilationErrors(details) {
33151
+ const raw = details?.compilationErrors;
33152
+ if (!Array.isArray(raw)) return [];
33153
+ const errors = [];
33154
+ for (const entry of raw) {
33155
+ if (!isRecord(entry) || typeof entry.file !== "string" || typeof entry.message !== "string") continue;
33156
+ const path23 = readPath(entry.path);
33157
+ errors.push({
33158
+ file: entry.file,
33159
+ line: typeof entry.line === "number" ? entry.line : 0,
33160
+ column: typeof entry.column === "number" ? entry.column : 0,
33161
+ message: entry.message,
33162
+ ...path23 ? { path: path23 } : {}
33163
+ });
33164
+ }
33165
+ return errors;
33166
+ }
33167
+ function position(file, line2, column) {
33168
+ if (line2 <= 0) return file;
33169
+ return column > 0 ? `${file}:${line2}:${column}` : `${file}:${line2}`;
33170
+ }
33171
+ function describeErrorDetails(details, labelForPath) {
33172
+ const lines = [];
33173
+ for (const issue of readValidationIssues(details)) {
33174
+ const label2 = issue.path.length > 0 ? labelForPath?.(issue.path) : void 0;
33175
+ const where = label2 ? `${label2} (${issue.param})` : issue.param || "(request body)";
33176
+ lines.push(`${where}: ${issue.message}`);
33177
+ }
33178
+ for (const error2 of readCompilationErrors(details)) {
33179
+ const label2 = error2.path ? labelForPath?.(error2.path) : void 0;
33180
+ lines.push(`${position(label2 ?? error2.file, error2.line, error2.column)}: ${error2.message}`);
33181
+ }
33182
+ return lines;
33183
+ }
33184
+
33120
33185
  // src/lib/tokenShape.ts
33121
33186
  var CREDENTIAL_NAMES = {
33122
33187
  personalAccessToken: "personal access token",
@@ -33171,7 +33236,7 @@ var ApiRequestError = class extends Error {
33171
33236
  return exitCodeForStatus(this.status);
33172
33237
  }
33173
33238
  };
33174
- function isRecord(value) {
33239
+ function isRecord2(value) {
33175
33240
  return typeof value === "object" && value !== null && !Array.isArray(value);
33176
33241
  }
33177
33242
  function asString(value) {
@@ -33185,16 +33250,16 @@ function parseRetryAfter(raw) {
33185
33250
  function toApiRequestError(input) {
33186
33251
  const retryAfterSeconds = parseRetryAfter(input.retryAfter);
33187
33252
  const headerRequestId = asString(input.requestId ?? void 0);
33188
- if (isRecord(input.body)) {
33253
+ if (isRecord2(input.body)) {
33189
33254
  const raw = input.body.error;
33190
- if (isRecord(raw)) {
33255
+ if (isRecord2(raw)) {
33191
33256
  return new ApiRequestError({
33192
33257
  status: input.status,
33193
33258
  message: asString(raw.message) ?? `API error ${input.status}`,
33194
33259
  code: asString(raw.code),
33195
33260
  docUrl: asString(raw.docUrl),
33196
33261
  requestId: asString(raw.requestId) ?? headerRequestId,
33197
- details: isRecord(raw.details) ? raw.details : void 0,
33262
+ details: isRecord2(raw.details) ? raw.details : void 0,
33198
33263
  retryAfterSeconds
33199
33264
  });
33200
33265
  }
@@ -33215,13 +33280,13 @@ function toApiRequestError(input) {
33215
33280
  }
33216
33281
  return new ApiRequestError({
33217
33282
  status: input.status,
33218
- message: `API error ${input.status}`,
33283
+ message: input.host ? `${input.host} answered HTTP ${input.status}, which is not an A vs B API response. Check the API URL.` : `API error ${input.status}`,
33219
33284
  requestId: headerRequestId,
33220
33285
  retryAfterSeconds
33221
33286
  });
33222
33287
  }
33223
- function describeApiFailure(error2) {
33224
- const lines = [];
33288
+ function describeApiFailure(error2, labelForPath) {
33289
+ const lines = describeErrorDetails(error2.details, labelForPath);
33225
33290
  if (error2.status === 401) {
33226
33291
  lines.push(
33227
33292
  `Check the credential: the CLI signs in with a ${CREDENTIAL_NAMES.personalAccessToken} (${CREDENTIAL_NAMES.personalAccessTokenPrefix}\u2026). Run \`avsb whoami\` to see which one is in use, or \`avsb login\` to replace it.`
@@ -33296,31 +33361,31 @@ function exitCodeFor(error2) {
33296
33361
  if (error2 instanceof CliError) return error2.exitCode;
33297
33362
  return EXIT.failure;
33298
33363
  }
33299
- function hintsFor(error2) {
33300
- if (error2 instanceof ApiRequestError) return describeApiFailure(error2);
33364
+ function hintsFor(error2, labelForPath) {
33365
+ if (error2 instanceof ApiRequestError) return describeApiFailure(error2, labelForPath);
33301
33366
  if (error2 instanceof CliError) return error2.hints;
33302
33367
  return [];
33303
33368
  }
33304
- function failCommand(error2) {
33369
+ function failCommand(error2, options = {}) {
33305
33370
  const body = toJsonErrorBody(error2);
33306
33371
  if (isJsonOutput()) {
33307
33372
  writeOut(JSON.stringify({ ok: false, command: commandName, error: body }, null, 2));
33308
33373
  }
33309
33374
  error(body.message);
33310
- for (const hint of hintsFor(error2)) errorDetail(hint);
33375
+ for (const hint of hintsFor(error2, options.labelForPath)) errorDetail(hint);
33311
33376
  process.exit(exitCodeFor(error2));
33312
33377
  }
33313
33378
 
33314
33379
  // src/lib/credentials.ts
33315
33380
  var CONFIG_VERSION = 2;
33316
- function isRecord2(value) {
33381
+ function isRecord3(value) {
33317
33382
  return typeof value === "object" && value !== null && !Array.isArray(value);
33318
33383
  }
33319
33384
  function isNonEmptyString(value) {
33320
33385
  return typeof value === "string" && value.trim().length > 0;
33321
33386
  }
33322
33387
  function isStoredLogin(value) {
33323
- return isRecord2(value) && isNonEmptyString(value.orgId) && typeof value.orgName === "string" && isNonEmptyString(value.token);
33388
+ return isRecord3(value) && isNonEmptyString(value.orgId) && typeof value.orgName === "string" && isNonEmptyString(value.token);
33324
33389
  }
33325
33390
  function parseConfigDocument(raw) {
33326
33391
  let parsed;
@@ -33329,7 +33394,7 @@ function parseConfigDocument(raw) {
33329
33394
  } catch {
33330
33395
  return { status: "unreadable", detail: "it is not valid JSON" };
33331
33396
  }
33332
- if (!isRecord2(parsed)) {
33397
+ if (!isRecord3(parsed)) {
33333
33398
  return { status: "unreadable", detail: "it does not contain a JSON object" };
33334
33399
  }
33335
33400
  if (parsed.version !== CONFIG_VERSION) {
@@ -33534,6 +33599,17 @@ var FRAMEWORK_IDS = [
33534
33599
  "node",
33535
33600
  "none"
33536
33601
  ];
33602
+ var BUNDLERS = [
33603
+ { id: "vite", dependency: "vite" },
33604
+ { id: "create-react-app", dependency: "react-scripts" },
33605
+ { id: "vue-cli", dependency: "@vue/cli-service" }
33606
+ ];
33607
+ function detectBundler(deps) {
33608
+ for (const bundler of BUNDLERS) {
33609
+ if (Object.prototype.hasOwnProperty.call(deps, bundler.dependency)) return bundler;
33610
+ }
33611
+ return null;
33612
+ }
33537
33613
  function parsePackageJson(raw) {
33538
33614
  let parsed;
33539
33615
  try {
@@ -33566,12 +33642,13 @@ function allDependencies(pkg2) {
33566
33642
  }
33567
33643
  function detectFramework(pkg2) {
33568
33644
  if (!pkg2) {
33569
- return { id: "none", matchedDependency: null, usesVite: false, usesSvelteKit: false };
33645
+ return { id: "none", matchedDependency: null, usesVite: false, usesSvelteKit: false, bundler: null };
33570
33646
  }
33571
33647
  const deps = allDependencies(pkg2);
33572
33648
  const has = (name) => Object.prototype.hasOwnProperty.call(deps, name);
33573
33649
  const usesVite = has("vite");
33574
33650
  const usesSvelteKit = has("@sveltejs/kit");
33651
+ const bundler = detectBundler(deps);
33575
33652
  const ordered = [
33576
33653
  { id: "nuxt", dependency: "nuxt" },
33577
33654
  { id: "next", dependency: "next" },
@@ -33588,11 +33665,12 @@ function detectFramework(pkg2) {
33588
33665
  id: candidate.id,
33589
33666
  matchedDependency: candidate.dependency,
33590
33667
  usesVite,
33591
- usesSvelteKit
33668
+ usesSvelteKit,
33669
+ bundler
33592
33670
  };
33593
33671
  }
33594
33672
  }
33595
- return { id: "node", matchedDependency: null, usesVite, usesSvelteKit };
33673
+ return { id: "node", matchedDependency: null, usesVite, usesSvelteKit, bundler };
33596
33674
  }
33597
33675
  function isFrameworkId(value) {
33598
33676
  return FRAMEWORK_IDS.includes(value);
@@ -33640,9 +33718,26 @@ function installArgv(manager, npmPackage) {
33640
33718
  }
33641
33719
 
33642
33720
  // src/lib/frameworkProfiles.ts
33643
- var VITE_ENV_VAR = "VITE_AVSB_SDK_KEY";
33644
- var VITE_ENV_FILE = ".env.local";
33645
- var UNPREFIXED_CAVEAT = "No Vite in this project, so the browser variable prefix is your bundler's call. Create React App wants REACT_APP_, Vite wants VITE_. Rename the variable to match, or pass the key to the provider directly.";
33721
+ var BROWSER_ENV_FILE = ".env.local";
33722
+ var UNPREFIXED_CAVEAT = "No Vite, Create React App or Vue CLI in this project, so the browser variable prefix is your bundler's call. Rename AVSB_SDK_KEY to the prefix your bundler exposes to the browser, or pass the key to the provider directly.";
33723
+ var BUNDLER_VARIABLES = {
33724
+ vite: { envVar: "VITE_AVSB_SDK_KEY", label: "Vite", prefix: "VITE_" },
33725
+ "create-react-app": { envVar: "REACT_APP_AVSB_SDK_KEY", label: "Create React App", prefix: "REACT_APP_" },
33726
+ "vue-cli": { envVar: "VUE_APP_AVSB_SDK_KEY", label: "Vue CLI", prefix: "VUE_APP_" }
33727
+ };
33728
+ function browserVariable(detection) {
33729
+ const bundler = detection.bundler;
33730
+ const applies = bundler !== null && (bundler.id === "vite" || bundler.id === "create-react-app" && detection.id === "react" || bundler.id === "vue-cli" && detection.id === "vue");
33731
+ if (!applies || bundler === null) {
33732
+ return { envVar: "AVSB_SDK_KEY", envVarReason: null, caveat: UNPREFIXED_CAVEAT };
33733
+ }
33734
+ const known = BUNDLER_VARIABLES[bundler.id];
33735
+ return {
33736
+ envVar: known.envVar,
33737
+ envVarReason: `found ${bundler.dependency}, and ${known.label} only exposes ${known.prefix} variables to the browser`,
33738
+ caveat: null
33739
+ };
33740
+ }
33646
33741
  function buildFrameworkProfile(detection) {
33647
33742
  switch (detection.id) {
33648
33743
  case "next":
@@ -33654,6 +33749,7 @@ function buildFrameworkProfile(detection) {
33654
33749
  envVar: "AVSB_SDK_KEY",
33655
33750
  exampleFile: "avsb-example.tsx",
33656
33751
  caveat: null,
33752
+ envVarReason: null,
33657
33753
  docsPath: "/docs/integrations/nextjs-app-router"
33658
33754
  };
33659
33755
  case "nuxt":
@@ -33665,6 +33761,7 @@ function buildFrameworkProfile(detection) {
33665
33761
  envVar: "NUXT_PUBLIC_AVSB_SDK_KEY",
33666
33762
  exampleFile: "components/AvsbExample.vue",
33667
33763
  caveat: 'Add "@avsbhq/nuxt" to the modules array in nuxt.config.ts. The module reads the key from this variable.',
33764
+ envVarReason: null,
33668
33765
  docsPath: "/docs/integrations/nuxt"
33669
33766
  };
33670
33767
  case "angular":
@@ -33676,6 +33773,7 @@ function buildFrameworkProfile(detection) {
33676
33773
  envVar: null,
33677
33774
  exampleFile: "src/app/avsb-example.component.ts",
33678
33775
  caveat: "Angular reads configuration from src/environments/environment.ts rather than an environment file, so the example carries the key inline. Move it into your environment file if you keep one per build target.",
33776
+ envVarReason: null,
33679
33777
  docsPath: "/docs/integrations/angular-cli"
33680
33778
  };
33681
33779
  case "svelte":
@@ -33687,15 +33785,15 @@ function buildFrameworkProfile(detection) {
33687
33785
  envVar: "PUBLIC_AVSB_SDK_KEY",
33688
33786
  exampleFile: "src/lib/AvsbExample.svelte",
33689
33787
  caveat: null,
33788
+ envVarReason: null,
33690
33789
  docsPath: "/docs/integrations/sveltekit"
33691
33790
  } : {
33692
33791
  id: "svelte",
33693
33792
  label: "Svelte",
33694
33793
  sdkPackage: "@avsbhq/svelte",
33695
- envFile: VITE_ENV_FILE,
33696
- envVar: detection.usesVite ? VITE_ENV_VAR : "AVSB_SDK_KEY",
33794
+ envFile: BROWSER_ENV_FILE,
33795
+ ...browserVariable(detection),
33697
33796
  exampleFile: "src/AvsbExample.svelte",
33698
- caveat: detection.usesVite ? null : UNPREFIXED_CAVEAT,
33699
33797
  docsPath: "/docs/integrations/vite-svelte"
33700
33798
  };
33701
33799
  case "vue":
@@ -33703,10 +33801,9 @@ function buildFrameworkProfile(detection) {
33703
33801
  id: "vue",
33704
33802
  label: "Vue",
33705
33803
  sdkPackage: "@avsbhq/vue",
33706
- envFile: VITE_ENV_FILE,
33707
- envVar: detection.usesVite ? VITE_ENV_VAR : "AVSB_SDK_KEY",
33804
+ envFile: BROWSER_ENV_FILE,
33805
+ ...browserVariable(detection),
33708
33806
  exampleFile: "src/AvsbExample.vue",
33709
- caveat: detection.usesVite ? null : UNPREFIXED_CAVEAT,
33710
33807
  docsPath: "/docs/integrations/vite-vue"
33711
33808
  };
33712
33809
  case "solid":
@@ -33714,10 +33811,9 @@ function buildFrameworkProfile(detection) {
33714
33811
  id: "solid",
33715
33812
  label: "Solid",
33716
33813
  sdkPackage: "@avsbhq/solid",
33717
- envFile: VITE_ENV_FILE,
33718
- envVar: detection.usesVite ? VITE_ENV_VAR : "AVSB_SDK_KEY",
33814
+ envFile: BROWSER_ENV_FILE,
33815
+ ...browserVariable(detection),
33719
33816
  exampleFile: "src/avsb-example.tsx",
33720
- caveat: detection.usesVite ? null : UNPREFIXED_CAVEAT,
33721
33817
  docsPath: "/docs/integrations/vite-solid"
33722
33818
  };
33723
33819
  case "react":
@@ -33725,10 +33821,9 @@ function buildFrameworkProfile(detection) {
33725
33821
  id: "react",
33726
33822
  label: "React",
33727
33823
  sdkPackage: "@avsbhq/react",
33728
- envFile: VITE_ENV_FILE,
33729
- envVar: detection.usesVite ? VITE_ENV_VAR : "AVSB_SDK_KEY",
33824
+ envFile: BROWSER_ENV_FILE,
33825
+ ...browserVariable(detection),
33730
33826
  exampleFile: "src/avsb-example.tsx",
33731
- caveat: detection.usesVite ? null : UNPREFIXED_CAVEAT,
33732
33827
  docsPath: "/docs/integrations/vite-react"
33733
33828
  };
33734
33829
  case "node":
@@ -33740,6 +33835,7 @@ function buildFrameworkProfile(detection) {
33740
33835
  envVar: "AVSB_SDK_KEY",
33741
33836
  exampleFile: "avsb-example.ts",
33742
33837
  caveat: null,
33838
+ envVarReason: null,
33743
33839
  docsPath: "/docs/developer-reference/avsb-node"
33744
33840
  };
33745
33841
  case "none":
@@ -33754,6 +33850,7 @@ function buildFrameworkProfile(detection) {
33754
33850
  // a feature flags project set up from a folder with no package.json was
33755
33851
  // being told it was a web experiments project.
33756
33852
  caveat: "No package.json here, so no framework was detected. Run this in your application directory to have the SDK wired up for you.",
33853
+ envVarReason: null,
33757
33854
  docsPath: "/docs/getting-started/installing-the-snippet"
33758
33855
  };
33759
33856
  }
@@ -33762,13 +33859,19 @@ function buildFrameworkProfile(detection) {
33762
33859
  // src/lib/heartbeatWait.ts
33763
33860
  var HEARTBEAT_INTERVAL_MS = 3e3;
33764
33861
  var HEARTBEAT_TIMEOUT_MS = 12e4;
33765
- var CLOCK_ALLOWANCE_MS = 6e4;
33862
+ var CLOCK_ALLOWANCE_MS = 5e3;
33766
33863
  function isFreshHeartbeat(lastSdkSeenAt, startedAt, allowanceMs = CLOCK_ALLOWANCE_MS) {
33767
33864
  if (!lastSdkSeenAt) return false;
33768
33865
  const seenAt = Date.parse(lastSdkSeenAt);
33769
33866
  if (Number.isNaN(seenAt)) return false;
33770
33867
  return seenAt >= startedAt - allowanceMs;
33771
33868
  }
33869
+ function isNewerThan(stamp, previous) {
33870
+ if (!stamp || !previous) return false;
33871
+ const seenAt = Date.parse(stamp);
33872
+ const before = Date.parse(previous);
33873
+ return !Number.isNaN(seenAt) && !Number.isNaN(before) && seenAt > before;
33874
+ }
33772
33875
  async function waitForHeartbeat(options) {
33773
33876
  const timeoutMs = options.timeoutMs ?? HEARTBEAT_TIMEOUT_MS;
33774
33877
  const intervalMs = options.intervalMs ?? HEARTBEAT_INTERVAL_MS;
@@ -33788,7 +33891,8 @@ async function waitForHeartbeat(options) {
33788
33891
  previousSeenAt = sample.lastSdkSeenAt;
33789
33892
  first = false;
33790
33893
  }
33791
- if (isFreshHeartbeat(sample.lastSdkSeenAt, options.startedAt) && sample.lastSdkSeenAt) {
33894
+ const arrivedWhileWatching = isNewerThan(sample.lastSdkSeenAt, previousSeenAt);
33895
+ if (sample.lastSdkSeenAt && (arrivedWhileWatching || isFreshHeartbeat(sample.lastSdkSeenAt, options.startedAt))) {
33792
33896
  return {
33793
33897
  seen: true,
33794
33898
  at: sample.lastSdkSeenAt,
@@ -33809,6 +33913,7 @@ function heartbeatTimeoutGuidance(input) {
33809
33913
  const seconds = Math.round(input.waitedMs / 1e3);
33810
33914
  const lines = [
33811
33915
  `No check-in from ${input.environmentName} in ${seconds}s. Everything above is written; only the proof is missing.`,
33916
+ `${turnFlagOnCommand(input.flagKey ?? null, input.environmentKey ?? "production")} An environment where nothing has been turned on has no configuration for the SDK to load, and the SDK checks in only after it loads one.`,
33812
33917
  "Start your app once, then run `avsb init --wait 120` again to watch for it.",
33813
33918
  `Check that the key your code loads is ${input.sdkKey}.`,
33814
33919
  `Check that outbound requests to ${input.cdnHost} and ${input.appHost} are allowed.`
@@ -33820,14 +33925,109 @@ function heartbeatTimeoutGuidance(input) {
33820
33925
  }
33821
33926
  return lines;
33822
33927
  }
33928
+ function turnFlagOnCommand(flagKey, environmentKey) {
33929
+ return `Turn a flag on with \`avsb flags toggle ${flagKey ?? "<key>"} --env ${environmentKey} --on\`.`;
33930
+ }
33823
33931
  function defaultSleep(ms) {
33824
33932
  return new Promise((resolve3) => setTimeout(resolve3, ms));
33825
33933
  }
33826
33934
 
33935
+ // src/lib/networkError.ts
33936
+ var DNS_CODES = /* @__PURE__ */ new Set(["ENOTFOUND", "EAI_AGAIN", "EAI_NONAME", "EAI_FAIL"]);
33937
+ var TIMEOUT_CODES = /* @__PURE__ */ new Set(["ETIMEDOUT", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_HEADERS_TIMEOUT", "ESOCKETTIMEDOUT"]);
33938
+ var RESET_CODES = /* @__PURE__ */ new Set(["ECONNRESET", "EPIPE", "UND_ERR_SOCKET", "ECONNABORTED"]);
33939
+ function isRecord4(value) {
33940
+ return typeof value === "object" && value !== null;
33941
+ }
33942
+ function networkErrorCode(err) {
33943
+ let current = err;
33944
+ for (let depth = 0; depth < 5 && isRecord4(current); depth++) {
33945
+ if (typeof current.code === "string") return current.code;
33946
+ if (Array.isArray(current.errors)) {
33947
+ for (const inner of current.errors) {
33948
+ const code = networkErrorCode(inner);
33949
+ if (code) return code;
33950
+ }
33951
+ }
33952
+ current = current.cause;
33953
+ }
33954
+ return null;
33955
+ }
33956
+ function networkErrorReason(err) {
33957
+ let current = err;
33958
+ let reason = null;
33959
+ for (let depth = 0; depth < 5 && isRecord4(current); depth++) {
33960
+ const message = current.message;
33961
+ if (typeof message === "string" && message.length > 0 && message !== "fetch failed") reason = message;
33962
+ current = current.cause;
33963
+ }
33964
+ return reason;
33965
+ }
33966
+ function classifyNetworkError(code) {
33967
+ if (code === null) return "other";
33968
+ if (DNS_CODES.has(code)) return "dns";
33969
+ if (code === "ECONNREFUSED") return "refused";
33970
+ if (TIMEOUT_CODES.has(code)) return "timeout";
33971
+ if (RESET_CODES.has(code)) return "reset";
33972
+ if (/CERT|TLS|SSL|SELF_SIGNED|UNABLE_TO_VERIFY|UNABLE_TO_GET_ISSUER/.test(code)) return "tls";
33973
+ return "other";
33974
+ }
33975
+ function hostOf(baseUrl) {
33976
+ try {
33977
+ return new URL(baseUrl).host;
33978
+ } catch {
33979
+ return baseUrl;
33980
+ }
33981
+ }
33982
+ function describeNetworkFailure(baseUrl, err) {
33983
+ const host = hostOf(baseUrl);
33984
+ const code = networkErrorCode(err);
33985
+ const kind = classifyNetworkError(code);
33986
+ const urlHint = `The API URL in use is ${baseUrl}. Check AVSB_API_URL, or the URL \`avsb login --url\` saved.`;
33987
+ const networkHint = "Check your network connection, VPN or proxy.";
33988
+ switch (kind) {
33989
+ case "dns":
33990
+ return new CliError("cli_network", `Cannot find ${host}: the name does not resolve (DNS lookup failed).`, [urlHint, networkHint]);
33991
+ case "refused":
33992
+ return new CliError("cli_network", `Nothing is listening at ${host}: the connection was refused.`, [urlHint]);
33993
+ case "timeout":
33994
+ return new CliError("cli_network", `${host} did not answer in time (the connection timed out).`, [networkHint]);
33995
+ case "reset":
33996
+ return new CliError("cli_network", `${host} closed the connection before answering.`, [networkHint]);
33997
+ case "tls":
33998
+ return new CliError("cli_network", `Could not make a secure connection to ${host} (${code}).`, [
33999
+ "A proxy or antivirus that inspects HTTPS can cause this. Check its certificate settings."
34000
+ ]);
34001
+ case "other": {
34002
+ const reason = code ?? networkErrorReason(err) ?? "no answer";
34003
+ return new CliError("cli_network", `Could not reach ${host} (${reason}).`, [urlHint, networkHint]);
34004
+ }
34005
+ }
34006
+ }
34007
+ function invalidBaseUrl(baseUrl) {
34008
+ let protocol = "";
34009
+ try {
34010
+ protocol = new URL(baseUrl).protocol;
34011
+ } catch {
34012
+ protocol = "";
34013
+ }
34014
+ if (protocol === "http:" || protocol === "https:") return null;
34015
+ return new CliError("cli_usage", `"${baseUrl}" is not a valid API URL.`, [
34016
+ "Use a full address such as https://app.avsb.cloud, in AVSB_API_URL or `avsb login --url`."
34017
+ ]);
34018
+ }
34019
+
33827
34020
  // src/lib/apiTransport.ts
33828
34021
  var API_VERSION = "v1";
33829
34022
  var API_VERSION_HEADER = "AvsB-API-Version";
33830
34023
  var REQUEST_ID_HEADER = "X-Request-Id";
34024
+ function hostOf2(baseUrl) {
34025
+ try {
34026
+ return new URL(baseUrl).host;
34027
+ } catch {
34028
+ return void 0;
34029
+ }
34030
+ }
33831
34031
  var ApiTransport = class {
33832
34032
  baseUrl;
33833
34033
  token;
@@ -33858,18 +34058,25 @@ var ApiTransport = class {
33858
34058
  * body parsing. It reports what came back and judges nothing, so both the
33859
34059
  * plain and the conditional paths below share exactly one implementation.
33860
34060
  */
33861
- async send(path22, options) {
34061
+ async send(path23, options) {
33862
34062
  this.assertUsableToken();
33863
- const url = `${this.baseUrl}${path22}`;
33864
- const res = await fetch(url, {
33865
- ...options,
33866
- headers: {
33867
- "Content-Type": "application/json",
33868
- Authorization: `Bearer ${this.token}`,
33869
- [API_VERSION_HEADER]: API_VERSION,
33870
- ...options.headers
33871
- }
33872
- });
34063
+ const badUrl = invalidBaseUrl(this.baseUrl);
34064
+ if (badUrl) throw badUrl;
34065
+ const url = `${this.baseUrl}${path23}`;
34066
+ let res;
34067
+ try {
34068
+ res = await fetch(url, {
34069
+ ...options,
34070
+ headers: {
34071
+ "Content-Type": "application/json",
34072
+ Authorization: `Bearer ${this.token}`,
34073
+ [API_VERSION_HEADER]: API_VERSION,
34074
+ ...options.headers
34075
+ }
34076
+ });
34077
+ } catch (err) {
34078
+ throw describeNetworkFailure(this.baseUrl, err);
34079
+ }
33873
34080
  let body = null;
33874
34081
  const text = await res.text();
33875
34082
  if (text.length > 0) {
@@ -33886,7 +34093,8 @@ var ApiTransport = class {
33886
34093
  status: res.status,
33887
34094
  body,
33888
34095
  requestId: res.headers.get(REQUEST_ID_HEADER),
33889
- retryAfter: res.headers.get("Retry-After")
34096
+ retryAfter: res.headers.get("Retry-After"),
34097
+ host: hostOf2(this.baseUrl)
33890
34098
  });
33891
34099
  }
33892
34100
  /**
@@ -33894,8 +34102,8 @@ var ApiTransport = class {
33894
34102
  * named methods. Returns the body and the ETag; `request` unwraps it for the
33895
34103
  * common case.
33896
34104
  */
33897
- async requestWithMeta(path22, options = {}) {
33898
- const { res, body } = await this.send(path22, options);
34105
+ async requestWithMeta(path23, options = {}) {
34106
+ const { res, body } = await this.send(path23, options);
33899
34107
  if (!res.ok) throw this.refusal(res, body);
33900
34108
  return { body, etag: res.headers.get("ETag") };
33901
34109
  }
@@ -33907,9 +34115,9 @@ var ApiTransport = class {
33907
34115
  * back to comparing what it generated. Nothing here assumes the platform
33908
34116
  * supports conditional reads.
33909
34117
  */
33910
- async requestIfNoneMatch(path22, etag) {
34118
+ async requestIfNoneMatch(path23, etag) {
33911
34119
  const { res, body } = await this.send(
33912
- path22,
34120
+ path23,
33913
34121
  etag ? { headers: { "If-None-Match": etag } } : {}
33914
34122
  );
33915
34123
  if (res.status === 304) {
@@ -33918,8 +34126,8 @@ var ApiTransport = class {
33918
34126
  if (!res.ok) throw this.refusal(res, body);
33919
34127
  return { status: "ok", body, etag: res.headers.get("ETag") };
33920
34128
  }
33921
- async request(path22, options = {}) {
33922
- const result = await this.requestWithMeta(path22, options);
34129
+ async request(path23, options = {}) {
34130
+ const result = await this.requestWithMeta(path23, options);
33923
34131
  return result.body;
33924
34132
  }
33925
34133
  };
@@ -33947,6 +34155,11 @@ var ApiClient = class extends ApiTransport {
33947
34155
  body: JSON.stringify(data)
33948
34156
  });
33949
34157
  }
34158
+ /**
34159
+ * Publish an experiment's pending changes, as the dashboard's Publish
34160
+ * changes button does. `warning` is set when the change is saved but the
34161
+ * live datafile could not be written yet (the platform retries it).
34162
+ */
33950
34163
  async publishChanges(shortId) {
33951
34164
  return this.request(`/api/cli/experiments/${shortId}/publish`, {
33952
34165
  method: "POST"
@@ -33961,11 +34174,6 @@ var ApiClient = class extends ApiTransport {
33961
34174
  body: JSON.stringify(data)
33962
34175
  });
33963
34176
  }
33964
- async publishProject(shortId) {
33965
- return this.request(`/api/cli/projects/${shortId}/publish`, {
33966
- method: "POST"
33967
- });
33968
- }
33969
34177
  /**
33970
34178
  * Fetch a project's published datafile for a given environment.
33971
34179
  *
@@ -33983,8 +34191,8 @@ var ApiClient = class extends ApiTransport {
33983
34191
  if (opts.type) params.set("type", opts.type);
33984
34192
  if (opts.includeArchived) params.set("includeArchived", "true");
33985
34193
  const qs = params.toString();
33986
- const path22 = `/api/cli/projects/${projectShortId}/metrics${qs ? `?${qs}` : ""}`;
33987
- return this.request(path22);
34194
+ const path23 = `/api/cli/projects/${projectShortId}/metrics${qs ? `?${qs}` : ""}`;
34195
+ return this.request(path23);
33988
34196
  }
33989
34197
  async createMetric(projectShortId, body) {
33990
34198
  return this.request(
@@ -33995,14 +34203,9 @@ var ApiClient = class extends ApiTransport {
33995
34203
  }
33996
34204
  );
33997
34205
  }
33998
- async getMetric(projectShortId, metricId) {
33999
- return this.request(
34000
- `/api/cli/projects/${projectShortId}/metrics/${metricId}`
34001
- );
34002
- }
34003
34206
  async updateMetric(projectShortId, metricId, patch) {
34004
34207
  return this.request(
34005
- `/api/cli/projects/${projectShortId}/metrics/${metricId}`,
34208
+ `/api/cli/projects/${projectShortId}/metrics/${encodeURIComponent(metricId)}`,
34006
34209
  {
34007
34210
  method: "PATCH",
34008
34211
  body: JSON.stringify(patch)
@@ -34011,19 +34214,19 @@ var ApiClient = class extends ApiTransport {
34011
34214
  }
34012
34215
  async archiveMetric(projectShortId, metricId) {
34013
34216
  return this.request(
34014
- `/api/cli/projects/${projectShortId}/metrics/${metricId}/archive`,
34217
+ `/api/cli/projects/${projectShortId}/metrics/${encodeURIComponent(metricId)}/archive`,
34015
34218
  { method: "POST" }
34016
34219
  );
34017
34220
  }
34018
34221
  async unarchiveMetric(projectShortId, metricId) {
34019
34222
  return this.request(
34020
- `/api/cli/projects/${projectShortId}/metrics/${metricId}/unarchive`,
34223
+ `/api/cli/projects/${projectShortId}/metrics/${encodeURIComponent(metricId)}/unarchive`,
34021
34224
  { method: "POST" }
34022
34225
  );
34023
34226
  }
34024
34227
  async deleteMetric(projectShortId, metricId) {
34025
34228
  return this.request(
34026
- `/api/cli/projects/${projectShortId}/metrics/${metricId}`,
34229
+ `/api/cli/projects/${projectShortId}/metrics/${encodeURIComponent(metricId)}`,
34027
34230
  { method: "DELETE" }
34028
34231
  );
34029
34232
  }
@@ -34035,8 +34238,8 @@ var ApiClient = class extends ApiTransport {
34035
34238
  if (opts.includeArchived) params.set("includeArchived", "true");
34036
34239
  if (opts.limit != null) params.set("limit", String(opts.limit));
34037
34240
  const qs = params.toString();
34038
- const path22 = `/api/cli/projects/${projectShortId}/metric-bindings${qs ? `?${qs}` : ""}`;
34039
- return this.request(path22);
34241
+ const path23 = `/api/cli/projects/${projectShortId}/metric-bindings${qs ? `?${qs}` : ""}`;
34242
+ return this.request(path23);
34040
34243
  }
34041
34244
  async createMetricBinding(projectShortId, body) {
34042
34245
  return this.request(
@@ -34047,14 +34250,9 @@ var ApiClient = class extends ApiTransport {
34047
34250
  }
34048
34251
  );
34049
34252
  }
34050
- async getMetricBinding(projectShortId, metricBindingId) {
34051
- return this.request(
34052
- `/api/cli/projects/${projectShortId}/metric-bindings/${metricBindingId}`
34053
- );
34054
- }
34055
34253
  async updateMetricBinding(projectShortId, metricBindingId, patch) {
34056
34254
  return this.request(
34057
- `/api/cli/projects/${projectShortId}/metric-bindings/${metricBindingId}`,
34255
+ `/api/cli/projects/${projectShortId}/metric-bindings/${encodeURIComponent(metricBindingId)}`,
34058
34256
  {
34059
34257
  method: "PATCH",
34060
34258
  body: JSON.stringify(patch)
@@ -34063,19 +34261,19 @@ var ApiClient = class extends ApiTransport {
34063
34261
  }
34064
34262
  async archiveMetricBinding(projectShortId, metricBindingId) {
34065
34263
  return this.request(
34066
- `/api/cli/projects/${projectShortId}/metric-bindings/${metricBindingId}/archive`,
34264
+ `/api/cli/projects/${projectShortId}/metric-bindings/${encodeURIComponent(metricBindingId)}/archive`,
34067
34265
  { method: "POST" }
34068
34266
  );
34069
34267
  }
34070
34268
  async unarchiveMetricBinding(projectShortId, metricBindingId) {
34071
34269
  return this.request(
34072
- `/api/cli/projects/${projectShortId}/metric-bindings/${metricBindingId}/unarchive`,
34270
+ `/api/cli/projects/${projectShortId}/metric-bindings/${encodeURIComponent(metricBindingId)}/unarchive`,
34073
34271
  { method: "POST" }
34074
34272
  );
34075
34273
  }
34076
34274
  async deleteMetricBinding(projectShortId, metricBindingId) {
34077
34275
  return this.request(
34078
- `/api/cli/projects/${projectShortId}/metric-bindings/${metricBindingId}`,
34276
+ `/api/cli/projects/${projectShortId}/metric-bindings/${encodeURIComponent(metricBindingId)}`,
34079
34277
  { method: "DELETE" }
34080
34278
  );
34081
34279
  }
@@ -34123,11 +34321,16 @@ var ApiClient = class extends ApiTransport {
34123
34321
  * presigned PUTs reject chunked transfer encoding).
34124
34322
  */
34125
34323
  async uploadNdjson(uploadUrl, body) {
34126
- const res = await fetch(uploadUrl, {
34127
- method: "PUT",
34128
- headers: { "Content-Type": "application/x-ndjson" },
34129
- body
34130
- });
34324
+ let res;
34325
+ try {
34326
+ res = await fetch(uploadUrl, {
34327
+ method: "PUT",
34328
+ headers: { "Content-Type": "application/x-ndjson" },
34329
+ body
34330
+ });
34331
+ } catch (err) {
34332
+ throw describeNetworkFailure(new URL(uploadUrl).origin, err);
34333
+ }
34131
34334
  if (!res.ok) {
34132
34335
  throw new Error(`Upload failed: HTTP ${res.status}`);
34133
34336
  }
@@ -34411,9 +34614,9 @@ function servedValueFor(flag) {
34411
34614
  if (!variation) return null;
34412
34615
  return { variationKey: variation.key, value: variation.value };
34413
34616
  }
34414
- function describeServedValue(flagKey, served) {
34617
+ function describeServedValue(flagKey, served, environmentKey = "production") {
34415
34618
  if (!served) {
34416
- return `${flagKey} is not being served in this environment yet, so your code gets the fallback you pass.`;
34619
+ return `${flagKey} is not being served in this environment yet, so your code gets the fallback you pass. Turn it on with \`avsb flags toggle ${flagKey} --env ${environmentKey} --on\`.`;
34417
34620
  }
34418
34621
  return `${flagKey} serves ${JSON.stringify(served.value)} (variation ${served.variationKey}) when no targeting rule matches.`;
34419
34622
  }
@@ -34426,12 +34629,46 @@ function canPrompt(ctx) {
34426
34629
  if (envFlagEnabled(ctx.env.CI)) return false;
34427
34630
  return true;
34428
34631
  }
34429
- function isInteractiveRun(options = {}) {
34430
- return canPrompt({
34632
+ function currentPromptContext(options = {}) {
34633
+ return {
34431
34634
  input: options.input !== false,
34432
34635
  quiet: isQuiet(),
34433
34636
  isTty: process.stdin.isTTY === true,
34434
34637
  env: process.env
34638
+ };
34639
+ }
34640
+ function isInteractiveRun(options = {}) {
34641
+ return canPrompt(currentPromptContext(options));
34642
+ }
34643
+ function refusalWithoutPrompt(refusal, ctx = currentPromptContext()) {
34644
+ if (canPrompt(ctx)) return null;
34645
+ return new CliError(refusal.code, refusal.message, refusal.hints);
34646
+ }
34647
+ var STDIN_IDLE_TIMEOUT_MS = 3e3;
34648
+ function readPipedStdin(stream = process.stdin, idleMs = STDIN_IDLE_TIMEOUT_MS) {
34649
+ return new Promise((resolve3) => {
34650
+ const chunks = [];
34651
+ let timer = null;
34652
+ const finish = () => {
34653
+ if (timer) clearTimeout(timer);
34654
+ stream.removeListener("data", onData);
34655
+ stream.removeListener("end", finish);
34656
+ stream.removeListener("error", finish);
34657
+ if ("pause" in stream && typeof stream.pause === "function") stream.pause();
34658
+ resolve3(Buffer.concat(chunks).toString("utf-8"));
34659
+ };
34660
+ const arm = () => {
34661
+ if (timer) clearTimeout(timer);
34662
+ timer = setTimeout(finish, idleMs);
34663
+ };
34664
+ const onData = (chunk) => {
34665
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
34666
+ arm();
34667
+ };
34668
+ stream.on("data", onData);
34669
+ stream.once("end", finish);
34670
+ stream.once("error", finish);
34671
+ arm();
34435
34672
  });
34436
34673
  }
34437
34674
 
@@ -34514,10 +34751,40 @@ function unquote(value) {
34514
34751
  return quote2 === '"' ? body.replace(/\\(["\\])/g, "$1") : body;
34515
34752
  }
34516
34753
 
34517
- // src/commands/init.write.ts
34754
+ // src/lib/gitignore.ts
34518
34755
  import { spawnSync as spawnSync2 } from "node:child_process";
34519
34756
  import fs3 from "node:fs";
34520
34757
  import path4 from "node:path";
34758
+ var GITIGNORE_BANNER = "# A vs B: the SDK key file `avsb init` writes";
34759
+ function gitIgnoreState(dir, file) {
34760
+ const result = spawnSync2("git", ["check-ignore", "-q", "--", file], { cwd: dir, stdio: "ignore" });
34761
+ if (result.error) return "no-repo";
34762
+ if (result.status === 0) return "ignored";
34763
+ if (result.status === 1) return "not-ignored";
34764
+ return "no-repo";
34765
+ }
34766
+ function appendGitignoreEntry(existing, file) {
34767
+ const entry = `${GITIGNORE_BANNER}
34768
+ ${file}
34769
+ `;
34770
+ if (existing === null || existing.length === 0) return entry;
34771
+ const separator = existing.endsWith("\n") ? "\n" : "\n\n";
34772
+ return `${existing}${separator}${entry}`;
34773
+ }
34774
+ function ensureGitignored(dir, file, state = gitIgnoreState) {
34775
+ const current = state(dir, file);
34776
+ if (current === "ignored") return { outcome: "already-ignored", gitignorePath: null };
34777
+ if (current === "no-repo") return { outcome: "no-repo", gitignorePath: null };
34778
+ const gitignorePath = path4.join(dir, ".gitignore");
34779
+ const existing = fs3.existsSync(gitignorePath) ? fs3.readFileSync(gitignorePath, "utf-8") : null;
34780
+ fs3.writeFileSync(gitignorePath, appendGitignoreEntry(existing, file), "utf-8");
34781
+ return { outcome: "added", gitignorePath };
34782
+ }
34783
+
34784
+ // src/commands/init.write.ts
34785
+ import { spawnSync as spawnSync3 } from "node:child_process";
34786
+ import fs4 from "node:fs";
34787
+ import path5 from "node:path";
34521
34788
 
34522
34789
  // src/lib/jsonTypeInference.ts
34523
34790
  var MAX_JSON_DEPTH = 4;
@@ -35023,14 +35290,14 @@ console.log('${input.flagKey}', flag.value, 'from', flag.source)
35023
35290
 
35024
35291
  // src/commands/init.write.ts
35025
35292
  function exampleFileExists(dir, profile, overridePath) {
35026
- return fs3.existsSync(resolveExamplePath(dir, profile, overridePath));
35293
+ return fs4.existsSync(resolveExamplePath(dir, profile, overridePath));
35027
35294
  }
35028
35295
  function resolveExamplePath(dir, profile, overridePath) {
35029
- return path4.resolve(dir, overridePath ?? profile.exampleFile);
35296
+ return path5.resolve(dir, overridePath ?? profile.exampleFile);
35030
35297
  }
35031
35298
  function writeExampleFile(input) {
35032
35299
  const filePath = resolveExamplePath(input.dir, input.profile, input.overridePath);
35033
- if (fs3.existsSync(filePath)) {
35300
+ if (fs4.existsSync(filePath)) {
35034
35301
  return { filePath, wrote: false, existed: true };
35035
35302
  }
35036
35303
  const contents = buildSample({
@@ -35042,14 +35309,14 @@ function writeExampleFile(input) {
35042
35309
  if (contents === null) {
35043
35310
  return { filePath, wrote: false, existed: false };
35044
35311
  }
35045
- fs3.mkdirSync(path4.dirname(filePath), { recursive: true });
35046
- fs3.writeFileSync(filePath, contents, "utf-8");
35312
+ fs4.mkdirSync(path5.dirname(filePath), { recursive: true });
35313
+ fs4.writeFileSync(filePath, contents, "utf-8");
35047
35314
  return { filePath, wrote: true, existed: false };
35048
35315
  }
35049
35316
  function runInstall(dir, manager, npmPackage, options) {
35050
35317
  const { command, args } = installArgv(manager, npmPackage);
35051
35318
  const printable = [command, ...args].join(" ");
35052
- const result = spawnSync2(command, args, {
35319
+ const result = spawnSync3(command, args, {
35053
35320
  cwd: dir,
35054
35321
  // A quiet or JSON run must leave stdout carrying only the result, so the
35055
35322
  // package manager's own output is captured and dropped instead of printed.
@@ -35079,10 +35346,10 @@ async function runCodegen(input) {
35079
35346
  projectId: String(input.projectShortId),
35080
35347
  env: input.environmentKey
35081
35348
  });
35082
- const filePath = path4.resolve(input.dir, input.outputPath ?? DEFAULT_CODEGEN_OUTPUT);
35349
+ const filePath = path5.resolve(input.dir, input.outputPath ?? DEFAULT_CODEGEN_OUTPUT);
35083
35350
  try {
35084
- fs3.mkdirSync(path4.dirname(filePath), { recursive: true });
35085
- fs3.writeFileSync(filePath, generated, "utf-8");
35351
+ fs4.mkdirSync(path5.dirname(filePath), { recursive: true });
35352
+ fs4.writeFileSync(filePath, generated, "utf-8");
35086
35353
  } catch (err) {
35087
35354
  throw new CliError(
35088
35355
  "cli_io_failed",
@@ -35093,14 +35360,14 @@ async function runCodegen(input) {
35093
35360
  }
35094
35361
  function readFileOrNull(filePath) {
35095
35362
  try {
35096
- return fs3.readFileSync(filePath, "utf-8");
35363
+ return fs4.readFileSync(filePath, "utf-8");
35097
35364
  } catch {
35098
35365
  return null;
35099
35366
  }
35100
35367
  }
35101
35368
  function listDirectory(dir) {
35102
35369
  try {
35103
- return fs3.readdirSync(dir);
35370
+ return fs4.readdirSync(dir);
35104
35371
  } catch {
35105
35372
  return [];
35106
35373
  }
@@ -35108,7 +35375,7 @@ function listDirectory(dir) {
35108
35375
 
35109
35376
  // src/commands/init.execute.ts
35110
35377
  async function executePlan(input) {
35111
- const files = { env: null, envOutcome: null, example: null, codegen: null };
35378
+ const files = { env: null, envOutcome: null, example: null, codegen: null, gitignore: null };
35112
35379
  let install = null;
35113
35380
  const { profile, environment } = input;
35114
35381
  if (stepFor(input.plan, "env").action === "write" && environment && profile.envFile && profile.envVar) {
@@ -35121,6 +35388,11 @@ async function executePlan(input) {
35121
35388
  files.env = result.filePath;
35122
35389
  files.envOutcome = result.outcome;
35123
35390
  info(describeEnvWrite(result, profile.envVar));
35391
+ const ignored = ensureGitignored(input.dir, profile.envFile);
35392
+ if (ignored.outcome === "added" && ignored.gitignorePath) {
35393
+ files.gitignore = ignored.gitignorePath;
35394
+ info(`Added ${profile.envFile} to ${ignored.gitignorePath}, so git does not pick it up`);
35395
+ }
35124
35396
  }
35125
35397
  const installStep = stepFor(input.plan, "install");
35126
35398
  if (profile.sdkPackage) {
@@ -35188,17 +35460,17 @@ async function watchForCheckIn(api, projectShortId, environment, wait) {
35188
35460
 
35189
35461
  // src/lib/projectScaffold.ts
35190
35462
  import { createHash as createHash2 } from "node:crypto";
35191
- import fs7 from "node:fs";
35192
- import path8 from "node:path";
35463
+ import fs8 from "node:fs";
35464
+ import path9 from "node:path";
35193
35465
 
35194
35466
  // src/lib/scaffold.ts
35195
- import fs6 from "node:fs";
35196
- import path7 from "node:path";
35467
+ import fs7 from "node:fs";
35468
+ import path8 from "node:path";
35197
35469
 
35198
35470
  // src/lib/manifest.ts
35199
35471
  import { createHash } from "node:crypto";
35200
- import fs4 from "node:fs";
35201
- import path5 from "node:path";
35472
+ import fs5 from "node:fs";
35473
+ import path6 from "node:path";
35202
35474
  var MANIFEST_FILENAME = ".avsb.json";
35203
35475
  function parseExperimentInput(input) {
35204
35476
  const urlMatch = input.match(/experiments\/(\d+)/);
@@ -35213,12 +35485,12 @@ function parseExperimentInput(input) {
35213
35485
  function findManifest(startDir) {
35214
35486
  let dir = startDir ?? process.cwd();
35215
35487
  for (let i = 0; i < 10; i++) {
35216
- const manifestPath = path5.join(dir, MANIFEST_FILENAME);
35217
- if (fs4.existsSync(manifestPath)) {
35218
- const raw = fs4.readFileSync(manifestPath, "utf-8");
35488
+ const manifestPath = path6.join(dir, MANIFEST_FILENAME);
35489
+ if (fs5.existsSync(manifestPath)) {
35490
+ const raw = fs5.readFileSync(manifestPath, "utf-8");
35219
35491
  return JSON.parse(raw);
35220
35492
  }
35221
- const parent = path5.dirname(dir);
35493
+ const parent = path6.dirname(dir);
35222
35494
  if (parent === dir) break;
35223
35495
  dir = parent;
35224
35496
  }
@@ -35227,17 +35499,17 @@ function findManifest(startDir) {
35227
35499
  function findManifestDir(startDir) {
35228
35500
  let dir = startDir ?? process.cwd();
35229
35501
  for (let i = 0; i < 10; i++) {
35230
- const manifestPath = path5.join(dir, MANIFEST_FILENAME);
35231
- if (fs4.existsSync(manifestPath)) return dir;
35232
- const parent = path5.dirname(dir);
35502
+ const manifestPath = path6.join(dir, MANIFEST_FILENAME);
35503
+ if (fs5.existsSync(manifestPath)) return dir;
35504
+ const parent = path6.dirname(dir);
35233
35505
  if (parent === dir) break;
35234
35506
  dir = parent;
35235
35507
  }
35236
35508
  return null;
35237
35509
  }
35238
35510
  function writeManifest(dir, manifest) {
35239
- const manifestPath = path5.join(dir, MANIFEST_FILENAME);
35240
- fs4.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
35511
+ const manifestPath = path6.join(dir, MANIFEST_FILENAME);
35512
+ fs5.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
35241
35513
  }
35242
35514
  function readManifestOrExit(startDir) {
35243
35515
  const dir = findManifestDir(startDir);
@@ -35275,30 +35547,30 @@ function computeSyncHashes(triggersJs, variations) {
35275
35547
  }
35276
35548
 
35277
35549
  // src/lib/snippetTypeDefs.ts
35278
- import fs5 from "node:fs";
35279
- import path6 from "node:path";
35550
+ import fs6 from "node:fs";
35551
+ import path7 from "node:path";
35280
35552
  import { createRequire } from "node:module";
35281
35553
  import { fileURLToPath } from "node:url";
35282
35554
  var SNIPPET_TYPES_PACKAGE = "@avsbhq/snippet-types";
35283
35555
  var SNIPPET_TYPE_DEF_FILES = ["avsb.d.ts", "variation-code.d.ts"];
35284
35556
  var MAX_WALK_UP = 24;
35285
35557
  function snippetTypeDefCandidates(filename, lookup) {
35286
- const candidates = [path6.join(lookup.moduleDir, "types", filename)];
35558
+ const candidates = [path7.join(lookup.moduleDir, "types", filename)];
35287
35559
  const packageDir = lookup.packageDir();
35288
- if (packageDir) candidates.push(path6.join(packageDir, filename));
35560
+ if (packageDir) candidates.push(path7.join(packageDir, filename));
35289
35561
  const isRepoRoot = lookup.isRepoRoot ?? defaultIsRepoRoot;
35290
35562
  let dir = lookup.moduleDir;
35291
35563
  for (let depth = 0; depth < MAX_WALK_UP; depth += 1) {
35292
- candidates.push(path6.join(dir, "packages", "avsb-snippet-types", filename));
35564
+ candidates.push(path7.join(dir, "packages", "avsb-snippet-types", filename));
35293
35565
  if (isRepoRoot(dir)) break;
35294
- const parent = path6.dirname(dir);
35566
+ const parent = path7.dirname(dir);
35295
35567
  if (parent === dir) break;
35296
35568
  dir = parent;
35297
35569
  }
35298
35570
  return candidates;
35299
35571
  }
35300
35572
  function defaultIsRepoRoot(dir) {
35301
- return fs5.existsSync(path6.join(dir, ".git"));
35573
+ return fs6.existsSync(path7.join(dir, ".git"));
35302
35574
  }
35303
35575
  function findSnippetTypeDefPath(filename, lookup) {
35304
35576
  for (const candidate of snippetTypeDefCandidates(filename, lookup)) {
@@ -35316,15 +35588,15 @@ function typeDefBanner(filename) {
35316
35588
  function defaultPackageDir() {
35317
35589
  try {
35318
35590
  const moduleRequire = createRequire(import.meta.url);
35319
- return path6.dirname(moduleRequire.resolve(`${SNIPPET_TYPES_PACKAGE}/package.json`));
35591
+ return path7.dirname(moduleRequire.resolve(`${SNIPPET_TYPES_PACKAGE}/package.json`));
35320
35592
  } catch {
35321
35593
  return null;
35322
35594
  }
35323
35595
  }
35324
35596
  function defaultLookup() {
35325
35597
  return {
35326
- moduleDir: path6.dirname(fileURLToPath(import.meta.url)),
35327
- exists: (candidate) => fs5.existsSync(candidate),
35598
+ moduleDir: path7.dirname(fileURLToPath(import.meta.url)),
35599
+ exists: (candidate) => fs6.existsSync(candidate),
35328
35600
  packageDir: defaultPackageDir
35329
35601
  };
35330
35602
  }
@@ -35341,7 +35613,7 @@ function readSnippetTypeDefs(lookup = defaultLookup()) {
35341
35613
  ]
35342
35614
  );
35343
35615
  }
35344
- return { filename, contents: typeDefBanner(filename) + fs5.readFileSync(found, "utf-8") };
35616
+ return { filename, contents: typeDefBanner(filename) + fs6.readFileSync(found, "utf-8") };
35345
35617
  });
35346
35618
  }
35347
35619
 
@@ -35366,7 +35638,7 @@ function writeTypeDefinitions(dir) {
35366
35638
  try {
35367
35639
  const written = [];
35368
35640
  for (const file of readSnippetTypeDefs()) {
35369
- fs6.writeFileSync(path7.join(dir, file.filename), file.contents);
35641
+ fs7.writeFileSync(path8.join(dir, file.filename), file.contents);
35370
35642
  written.push(file.filename);
35371
35643
  }
35372
35644
  return written;
@@ -35382,24 +35654,24 @@ function scaffoldExperiment(options) {
35382
35654
  const { data, script, style, apiBaseUrl, targetDir } = options;
35383
35655
  const { experiment, project, org } = data;
35384
35656
  const folderName = toKebabCase(experiment.name);
35385
- const baseDir = targetDir ?? path7.join(process.cwd(), folderName);
35386
- if (fs6.existsSync(baseDir)) {
35657
+ const baseDir = targetDir ?? path8.join(process.cwd(), folderName);
35658
+ if (fs7.existsSync(baseDir)) {
35387
35659
  throw new Error(`Directory already exists: ${baseDir}`);
35388
35660
  }
35389
- fs6.mkdirSync(baseDir, { recursive: true });
35661
+ fs7.mkdirSync(baseDir, { recursive: true });
35390
35662
  const files = [];
35391
35663
  const manifestVariations = [];
35392
- const triggerFile = `trigger.${script}`;
35393
- fs6.writeFileSync(path7.join(baseDir, triggerFile), experiment.triggersJs);
35394
- files.push(triggerFile);
35664
+ const triggerFile2 = `trigger.${script}`;
35665
+ fs7.writeFileSync(path8.join(baseDir, triggerFile2), experiment.triggersJs);
35666
+ files.push(triggerFile2);
35395
35667
  for (const variation of experiment.variations) {
35396
35668
  const folder = variationFolderName(variation.name);
35397
- const varDir = path7.join(baseDir, folder);
35398
- fs6.mkdirSync(varDir, { recursive: true });
35669
+ const varDir = path8.join(baseDir, folder);
35670
+ fs7.mkdirSync(varDir, { recursive: true });
35399
35671
  const scriptFile = `${folder}.${script}`;
35400
35672
  const styleFile = `${folder}.${style}`;
35401
- fs6.writeFileSync(path7.join(varDir, scriptFile), variation.controlJs);
35402
- fs6.writeFileSync(path7.join(varDir, styleFile), variation.controlCss);
35673
+ fs7.writeFileSync(path8.join(varDir, scriptFile), variation.controlJs);
35674
+ fs7.writeFileSync(path8.join(varDir, styleFile), variation.controlCss);
35403
35675
  files.push(`${folder}/${scriptFile}`);
35404
35676
  files.push(`${folder}/${styleFile}`);
35405
35677
  manifestVariations.push({
@@ -35438,7 +35710,7 @@ function scaffoldExperiment(options) {
35438
35710
  writeManifest(baseDir, manifest);
35439
35711
  files.push(".avsb.json");
35440
35712
  if (script === "ts") {
35441
- fs6.writeFileSync(path7.join(baseDir, "tsconfig.json"), TSCONFIG_TEMPLATE);
35713
+ fs7.writeFileSync(path8.join(baseDir, "tsconfig.json"), TSCONFIG_TEMPLATE);
35442
35714
  files.push("tsconfig.json");
35443
35715
  files.push(...writeTypeDefinitions(baseDir));
35444
35716
  }
@@ -35454,10 +35726,10 @@ function refreshTypeDefinitions(dir, script) {
35454
35726
  }
35455
35727
  const changed = [];
35456
35728
  for (const file of current) {
35457
- const target = path7.join(dir, file.filename);
35458
- const onDisk = fs6.existsSync(target) ? fs6.readFileSync(target, "utf-8") : null;
35729
+ const target = path8.join(dir, file.filename);
35730
+ const onDisk = fs7.existsSync(target) ? fs7.readFileSync(target, "utf-8") : null;
35459
35731
  if (onDisk === file.contents) continue;
35460
- fs6.writeFileSync(target, file.contents);
35732
+ fs7.writeFileSync(target, file.contents);
35461
35733
  changed.push(file.filename);
35462
35734
  }
35463
35735
  return changed;
@@ -35467,26 +35739,26 @@ function updateLocalFiles(dir, manifest, data) {
35467
35739
  const { script, style } = manifest.languages;
35468
35740
  const updated = [];
35469
35741
  updated.push(...refreshTypeDefinitions(dir, script));
35470
- const triggerFile = `trigger.${script}`;
35471
- const triggerPath = path7.join(dir, triggerFile);
35472
- const currentTrigger = fs6.existsSync(triggerPath) ? fs6.readFileSync(triggerPath, "utf-8") : "";
35742
+ const triggerFile2 = `trigger.${script}`;
35743
+ const triggerPath = path8.join(dir, triggerFile2);
35744
+ const currentTrigger = fs7.existsSync(triggerPath) ? fs7.readFileSync(triggerPath, "utf-8") : "";
35473
35745
  if (currentTrigger !== experiment.triggersJs) {
35474
- fs6.writeFileSync(triggerPath, experiment.triggersJs);
35475
- updated.push(triggerFile);
35746
+ fs7.writeFileSync(triggerPath, experiment.triggersJs);
35747
+ updated.push(triggerFile2);
35476
35748
  }
35477
35749
  for (const variation of experiment.variations) {
35478
35750
  const mv = manifest.variations.find((v) => v.id === variation.id);
35479
35751
  if (!mv) continue;
35480
- const jsPath = path7.join(dir, mv.folder, mv.scriptFile);
35481
- const cssPath = path7.join(dir, mv.folder, mv.styleFile);
35482
- const currentJs = fs6.existsSync(jsPath) ? fs6.readFileSync(jsPath, "utf-8") : "";
35483
- const currentCss = fs6.existsSync(cssPath) ? fs6.readFileSync(cssPath, "utf-8") : "";
35752
+ const jsPath = path8.join(dir, mv.folder, mv.scriptFile);
35753
+ const cssPath = path8.join(dir, mv.folder, mv.styleFile);
35754
+ const currentJs = fs7.existsSync(jsPath) ? fs7.readFileSync(jsPath, "utf-8") : "";
35755
+ const currentCss = fs7.existsSync(cssPath) ? fs7.readFileSync(cssPath, "utf-8") : "";
35484
35756
  if (currentJs !== variation.controlJs) {
35485
- fs6.writeFileSync(jsPath, variation.controlJs);
35757
+ fs7.writeFileSync(jsPath, variation.controlJs);
35486
35758
  updated.push(`${mv.folder}/${mv.scriptFile}`);
35487
35759
  }
35488
35760
  if (currentCss !== variation.controlCss) {
35489
- fs6.writeFileSync(cssPath, variation.controlCss);
35761
+ fs7.writeFileSync(cssPath, variation.controlCss);
35490
35762
  updated.push(`${mv.folder}/${mv.styleFile}`);
35491
35763
  }
35492
35764
  }
@@ -35519,27 +35791,27 @@ function diffLocalFiles(dir, manifest, data) {
35519
35791
  const { experiment } = data;
35520
35792
  const { script } = manifest.languages;
35521
35793
  const changed = [];
35522
- const triggerFile = `trigger.${script}`;
35523
- const triggerPath = path7.join(dir, triggerFile);
35524
- if (fs6.existsSync(triggerPath)) {
35525
- const localContent = fs6.readFileSync(triggerPath, "utf-8");
35794
+ const triggerFile2 = `trigger.${script}`;
35795
+ const triggerPath = path8.join(dir, triggerFile2);
35796
+ if (fs7.existsSync(triggerPath)) {
35797
+ const localContent = fs7.readFileSync(triggerPath, "utf-8");
35526
35798
  if (localContent !== experiment.triggersJs) {
35527
- changed.push(triggerFile);
35799
+ changed.push(triggerFile2);
35528
35800
  }
35529
35801
  }
35530
35802
  for (const variation of experiment.variations) {
35531
35803
  const mv = manifest.variations.find((v) => v.id === variation.id);
35532
35804
  if (!mv) continue;
35533
- const jsPath = path7.join(dir, mv.folder, mv.scriptFile);
35534
- const cssPath = path7.join(dir, mv.folder, mv.styleFile);
35535
- if (fs6.existsSync(jsPath)) {
35536
- const localJs = fs6.readFileSync(jsPath, "utf-8");
35805
+ const jsPath = path8.join(dir, mv.folder, mv.scriptFile);
35806
+ const cssPath = path8.join(dir, mv.folder, mv.styleFile);
35807
+ if (fs7.existsSync(jsPath)) {
35808
+ const localJs = fs7.readFileSync(jsPath, "utf-8");
35537
35809
  if (localJs !== variation.controlJs) {
35538
35810
  changed.push(`${mv.folder}/${mv.scriptFile}`);
35539
35811
  }
35540
35812
  }
35541
- if (fs6.existsSync(cssPath)) {
35542
- const localCss = fs6.readFileSync(cssPath, "utf-8");
35813
+ if (fs7.existsSync(cssPath)) {
35814
+ const localCss = fs7.readFileSync(cssPath, "utf-8");
35543
35815
  if (localCss !== variation.controlCss) {
35544
35816
  changed.push(`${mv.folder}/${mv.styleFile}`);
35545
35817
  }
@@ -35580,21 +35852,21 @@ function parseProjectInput(input) {
35580
35852
  return num;
35581
35853
  }
35582
35854
  function writeProjectManifest(dir, manifest) {
35583
- const manifestPath = path8.join(dir, PROJECT_MANIFEST_FILENAME);
35584
- fs7.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
35855
+ const manifestPath = path9.join(dir, PROJECT_MANIFEST_FILENAME);
35856
+ fs8.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
35585
35857
  }
35586
35858
  function readProjectManifest(dir) {
35587
- const manifestPath = path8.join(dir, PROJECT_MANIFEST_FILENAME);
35588
- if (!fs7.existsSync(manifestPath)) return null;
35589
- const raw = fs7.readFileSync(manifestPath, "utf-8");
35859
+ const manifestPath = path9.join(dir, PROJECT_MANIFEST_FILENAME);
35860
+ if (!fs8.existsSync(manifestPath)) return null;
35861
+ const raw = fs8.readFileSync(manifestPath, "utf-8");
35590
35862
  return JSON.parse(raw);
35591
35863
  }
35592
35864
  function findProjectManifestDir(startDir) {
35593
35865
  let dir = startDir ?? process.cwd();
35594
35866
  for (let i = 0; i < 10; i++) {
35595
- const manifestPath = path8.join(dir, PROJECT_MANIFEST_FILENAME);
35596
- if (fs7.existsSync(manifestPath)) return dir;
35597
- const parent = path8.dirname(dir);
35867
+ const manifestPath = path9.join(dir, PROJECT_MANIFEST_FILENAME);
35868
+ if (fs8.existsSync(manifestPath)) return dir;
35869
+ const parent = path9.dirname(dir);
35598
35870
  if (parent === dir) break;
35599
35871
  dir = parent;
35600
35872
  }
@@ -35614,20 +35886,29 @@ function readProjectManifestOrExit(startDir) {
35614
35886
  const manifest = readProjectManifest(dir);
35615
35887
  return { manifest, dir };
35616
35888
  }
35889
+ function resolveProjectPullTarget(cwd, shortId) {
35890
+ const enclosing = findProjectManifestDir(cwd);
35891
+ if (enclosing && readProjectManifest(enclosing)?.projectShortId === shortId) {
35892
+ return { kind: "existing", dir: enclosing };
35893
+ }
35894
+ const beside = path9.join(cwd, `project-${shortId}`);
35895
+ if (!fs8.existsSync(beside)) return { kind: "fresh", dir: beside };
35896
+ return readProjectManifest(beside)?.projectShortId === shortId ? { kind: "existing", dir: beside } : { kind: "blocked", dir: beside };
35897
+ }
35617
35898
  function scaffoldProject(options) {
35618
35899
  const { data, apiBaseUrl, targetDir } = options;
35619
35900
  const { project } = data;
35620
35901
  const lang = project.projectJsLang === "TYPESCRIPT" ? "ts" : "js";
35621
35902
  const folderName = `project-${project.shortId}`;
35622
- const baseDir = targetDir ?? path8.join(process.cwd(), folderName);
35623
- if (fs7.existsSync(baseDir)) {
35903
+ const baseDir = targetDir ?? path9.join(process.cwd(), folderName);
35904
+ if (fs8.existsSync(baseDir)) {
35624
35905
  throw new Error(`Directory already exists: ${baseDir}`);
35625
35906
  }
35626
- fs7.mkdirSync(baseDir, { recursive: true });
35907
+ fs8.mkdirSync(baseDir, { recursive: true });
35627
35908
  const files = [];
35628
35909
  const scriptFile = `project.${lang}`;
35629
35910
  const content = lang === "ts" ? project.editorProjectTs ?? project.editorProjectJs ?? "" : project.editorProjectJs ?? "";
35630
- fs7.writeFileSync(path8.join(baseDir, scriptFile), content);
35911
+ fs8.writeFileSync(path9.join(baseDir, scriptFile), content);
35631
35912
  files.push(scriptFile);
35632
35913
  const manifest = {
35633
35914
  version: 1,
@@ -35644,7 +35925,7 @@ function scaffoldProject(options) {
35644
35925
  writeProjectManifest(baseDir, manifest);
35645
35926
  files.push(".avsb-project.json");
35646
35927
  if (lang === "ts") {
35647
- fs7.writeFileSync(path8.join(baseDir, "tsconfig.json"), PROJECT_TSCONFIG);
35928
+ fs8.writeFileSync(path9.join(baseDir, "tsconfig.json"), PROJECT_TSCONFIG);
35648
35929
  files.push("tsconfig.json");
35649
35930
  files.push(...writeTypeDefinitions(baseDir));
35650
35931
  }
@@ -35655,11 +35936,11 @@ function updateProjectFile(dir, manifest, data) {
35655
35936
  const updated = [];
35656
35937
  updated.push(...refreshTypeDefinitions(dir, manifest.language));
35657
35938
  const scriptFile = `project.${manifest.language}`;
35658
- const scriptPath = path8.join(dir, scriptFile);
35659
- const current = fs7.existsSync(scriptPath) ? fs7.readFileSync(scriptPath, "utf-8") : "";
35939
+ const scriptPath = path9.join(dir, scriptFile);
35940
+ const current = fs8.existsSync(scriptPath) ? fs8.readFileSync(scriptPath, "utf-8") : "";
35660
35941
  const remote = manifest.language === "ts" ? project.editorProjectTs ?? project.editorProjectJs ?? "" : project.editorProjectJs ?? "";
35661
35942
  if (current !== remote) {
35662
- fs7.writeFileSync(scriptPath, remote);
35943
+ fs8.writeFileSync(scriptPath, remote);
35663
35944
  updated.push(scriptFile);
35664
35945
  }
35665
35946
  manifest.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -35878,28 +36159,47 @@ function renderInitSummary(input) {
35878
36159
  }
35879
36160
  function describeDetection(input) {
35880
36161
  const manager = `installs with ${input.packageManager}`;
36162
+ const variable = input.profile.envVar && input.profile.envVarReason ? `, key in ${input.profile.envVar}: ${input.profile.envVarReason}` : "";
35881
36163
  if (input.detection.matchedDependency) {
35882
- return `${input.profile.label} (found ${input.detection.matchedDependency} in package.json, ${manager})`;
36164
+ return `${input.profile.label} (found ${input.detection.matchedDependency} in package.json${variable}, ${manager})`;
35883
36165
  }
35884
36166
  if (input.profile.id === "none") {
35885
36167
  return "no package.json found here";
35886
36168
  }
35887
36169
  return `${input.profile.label} (${manager})`;
35888
36170
  }
35889
- function renderSteps(input) {
35890
- const written = [input.files.env, input.files.example, input.files.codegen].filter(
36171
+ function fileReport(files) {
36172
+ const envWritten = files.envOutcome === "created" || files.envOutcome === "appended";
36173
+ const written = [envWritten ? files.env : null, files.example, files.codegen, files.gitignore].filter(
35891
36174
  (file) => file !== null
35892
36175
  );
36176
+ const leftAlone = !envWritten && files.env !== null ? [files.env] : [];
36177
+ return { written, leftAlone };
36178
+ }
36179
+ function renderSteps(input) {
36180
+ const { written, leftAlone } = fileReport(input.files);
35893
36181
  if (written.length > 0) {
35894
36182
  heading("Written");
35895
36183
  fileTree(written);
35896
36184
  line();
35897
36185
  }
36186
+ if (leftAlone.length > 0) {
36187
+ heading("Left alone");
36188
+ fileTree(leftAlone);
36189
+ line();
36190
+ }
35898
36191
  const skipped = input.plan.filter((step) => step.action === "skip");
35899
36192
  for (const step of skipped) dim(` Skipped ${step.id}: ${step.detail}`);
35900
36193
  if (skipped.length > 0) line();
35901
36194
  }
35902
36195
  function renderWhatIsLeft(input) {
36196
+ const todo = whatIsLeftLines(input);
36197
+ if (todo.length === 0) return;
36198
+ line();
36199
+ heading("Next");
36200
+ for (const line2 of todo) dim(` ${line2}`);
36201
+ }
36202
+ function whatIsLeftLines(input) {
35903
36203
  const todo = [];
35904
36204
  if (input.install && !input.install.ran) {
35905
36205
  todo.push(`Install the SDK: ${input.install.command}`);
@@ -35911,17 +36211,15 @@ function renderWhatIsLeft(input) {
35911
36211
  todo.push(input.profile.caveat);
35912
36212
  }
35913
36213
  if (input.flagKey && !input.flagExists) {
36214
+ const envKey = input.environment?.key ?? "production";
35914
36215
  todo.push(
35915
- `The example reads "${input.flagKey}", which does not exist yet. Create it with \`avsb flags create ${input.flagKey}\`.`
36216
+ `The example reads "${input.flagKey}", which does not exist yet. Create it with \`avsb flags create ${input.flagKey}\`, then turn it on with \`avsb flags toggle ${input.flagKey} --env ${envKey} --on\`.`
35916
36217
  );
35917
36218
  }
35918
36219
  if (input.servedLine) {
35919
36220
  todo.push(input.servedLine);
35920
36221
  }
35921
- if (todo.length === 0) return;
35922
- line();
35923
- heading("Next");
35924
- for (const line2 of todo) dim(` ${line2}`);
36222
+ return todo;
35925
36223
  }
35926
36224
  function renderAssistantPrompt(input) {
35927
36225
  if (!input.environment && input.project.type === "FEATURE_FLAG") return;
@@ -35978,11 +36276,11 @@ var initCommand = withOutputOptions(
35978
36276
  ).action(async (opts) => {
35979
36277
  beginCommand("init", opts);
35980
36278
  try {
35981
- const dir = path9.resolve(opts.dir ?? ".");
36279
+ const dir = path10.resolve(opts.dir ?? ".");
35982
36280
  parseWaitSeconds(opts.wait);
35983
36281
  assertHeartbeatFlagsAgree(opts);
35984
36282
  const interactive = isInteractiveRun({ input: opts.input });
35985
- const pkg2 = parsePackageJson(readFileOrNull(path9.join(dir, "package.json")) ?? "");
36283
+ const pkg2 = parsePackageJson(readFileOrNull(path10.join(dir, "package.json")) ?? "");
35986
36284
  const detection = resolveDetection(pkg2, opts.framework);
35987
36285
  const profile = buildFrameworkProfile(detection);
35988
36286
  const packageManager = detectPackageManager(pkg2, listDirectory(dir));
@@ -36056,7 +36354,9 @@ var initCommand = withOutputOptions(
36056
36354
  id: profile.id,
36057
36355
  label: profile.label,
36058
36356
  detectedFrom: detection.matchedDependency,
36059
- packageManager
36357
+ packageManager,
36358
+ bundler: detection.bundler?.id ?? null,
36359
+ envVar: profile.envVar
36060
36360
  },
36061
36361
  flag: sample ? { key: sample.flagKey, exists: sample.flag !== null, served } : null,
36062
36362
  steps: plan,
@@ -36079,12 +36379,14 @@ var initCommand = withOutputOptions(
36079
36379
  install: outcome.install,
36080
36380
  flagKey: sample?.flagKey ?? null,
36081
36381
  flagExists: sample !== null && sample.flag !== null,
36082
- servedLine: sample?.flag ? describeServedValue(sample.flagKey, served) : null,
36382
+ servedLine: sample?.flag ? describeServedValue(sample.flagKey, served, opts.env) : null,
36083
36383
  heartbeat,
36084
36384
  proof,
36085
36385
  timeoutGuidance: heartbeat && !heartbeat.seen && environment ? heartbeatTimeoutGuidance({
36086
36386
  sdkKey: environment.sdkKey,
36087
36387
  environmentName: environment.name,
36388
+ environmentKey: environment.key,
36389
+ flagKey: sample?.flagKey ?? null,
36088
36390
  previousSeenAt: heartbeat.previousSeenAt,
36089
36391
  waitedMs: heartbeat.elapsedMs,
36090
36392
  cdnHost: new URL(DEFAULT_CDN_BASE_URL).host,
@@ -36134,8 +36436,8 @@ async function decideCodegen(opts, interactive, hasSdkKey) {
36134
36436
  }
36135
36437
 
36136
36438
  // src/commands/agents-md.ts
36137
- import fs8 from "node:fs";
36138
- import path10 from "node:path";
36439
+ import fs9 from "node:fs";
36440
+ import path11 from "node:path";
36139
36441
 
36140
36442
  // src/lib/agentsMd.ts
36141
36443
  var AGENTS_MARKER_START = "<!-- avsb:start -->";
@@ -36285,8 +36587,8 @@ var agentsMdInstallCommand = withOutputOptions(
36285
36587
  ).action((opts) => {
36286
36588
  beginCommand("agents-md install", opts);
36287
36589
  try {
36288
- const dir = path10.resolve(opts.dir ?? ".");
36289
- const pkg2 = parsePackageJson(readFileOrNull(path10.join(dir, "package.json")) ?? "");
36590
+ const dir = path11.resolve(opts.dir ?? ".");
36591
+ const pkg2 = parsePackageJson(readFileOrNull(path11.join(dir, "package.json")) ?? "");
36290
36592
  const profile = buildFrameworkProfile(detectFramework(pkg2));
36291
36593
  const section = renderAgentsSection({
36292
36594
  projectShortId: readProjectShortId(opts.project),
@@ -36310,12 +36612,12 @@ var agentsMdInstallCommand = withOutputOptions(
36310
36612
  }
36311
36613
  });
36312
36614
  function writeTarget(dir, file, section) {
36313
- const filePath = path10.join(dir, file);
36615
+ const filePath = path11.join(dir, file);
36314
36616
  const existing = readFileOrNull(filePath);
36315
36617
  const applied = applyAgentsSection(existing, section);
36316
36618
  if (applied.outcome !== "unchanged") {
36317
36619
  try {
36318
- fs8.writeFileSync(filePath, applied.content, "utf-8");
36620
+ fs9.writeFileSync(filePath, applied.content, "utf-8");
36319
36621
  } catch (err) {
36320
36622
  throw new CliError(
36321
36623
  "cli_io_failed",
@@ -36351,7 +36653,8 @@ var loginCommand = new Command("login").description("Save a personal access toke
36351
36653
  `Replacing the saved login in ${getConfigPath()}: ${existing.detail}.`
36352
36654
  );
36353
36655
  }
36354
- if (existingConfig && options.input && !explicitToken && !envToken) {
36656
+ const interactive = canPrompt(currentPromptContext({ input: options.input }));
36657
+ if (existingConfig && interactive && !explicitToken && !envToken) {
36355
36658
  const active = existingConfig.logins.find(
36356
36659
  (login) => login.orgId === existingConfig.activeOrgId
36357
36660
  );
@@ -36373,7 +36676,12 @@ var loginCommand = new Command("login").description("Save a personal access toke
36373
36676
  const token = await resolveToken({
36374
36677
  explicitToken,
36375
36678
  envToken,
36376
- allowPrompt: options.input
36679
+ source: tokenSourceFor({
36680
+ explicitToken,
36681
+ envToken,
36682
+ input: options.input,
36683
+ isTty: process.stdin.isTTY === true
36684
+ })
36377
36685
  });
36378
36686
  const spinner = ora("Verifying token...").start();
36379
36687
  const client = new ApiClient({ token, apiBaseUrl }, { useEnvOverrides: false });
@@ -36381,8 +36689,9 @@ var loginCommand = new Command("login").description("Save a personal access toke
36381
36689
  try {
36382
36690
  verified = await client.verifyAuth();
36383
36691
  } catch (err) {
36384
- spinner.fail("Token rejected");
36385
- if (err instanceof ApiRequestError && err.status === 401) {
36692
+ const rejected = err instanceof ApiRequestError && err.status === 401;
36693
+ spinner.fail(rejected ? "Token rejected" : "Could not check the token");
36694
+ if (rejected) {
36386
36695
  dim(` ${WHERE_TO_GET_A_TOKEN}`);
36387
36696
  }
36388
36697
  failCommand(err);
@@ -36417,13 +36726,22 @@ var loginCommand = new Command("login").description("Save a personal access toke
36417
36726
  failCommand(err);
36418
36727
  }
36419
36728
  });
36729
+ function tokenSourceFor(input) {
36730
+ if (input.explicitToken) return "flag";
36731
+ if (input.envToken) return "env";
36732
+ if (!input.input) return "refuse";
36733
+ return input.isTty ? "prompt" : "stdin";
36734
+ }
36735
+ var NO_TOKEN_HINTS = [
36736
+ `Pass --token <token>, set ${TOKEN_ENV_VAR}, or pipe it in: echo "$TOKEN" | avsb login`
36737
+ ];
36420
36738
  async function resolveToken(input) {
36421
- if (input.explicitToken) return input.explicitToken;
36422
- if (input.envToken) {
36739
+ if (input.source === "flag" && input.explicitToken) return input.explicitToken;
36740
+ if (input.source === "env" && input.envToken) {
36423
36741
  info(`Using the token from ${TOKEN_ENV_VAR}.`);
36424
36742
  return input.envToken;
36425
36743
  }
36426
- if (!input.allowPrompt) {
36744
+ if (input.source === "refuse") {
36427
36745
  failCommand(
36428
36746
  new CliError(
36429
36747
  "cli_usage",
@@ -36431,6 +36749,13 @@ async function resolveToken(input) {
36431
36749
  )
36432
36750
  );
36433
36751
  }
36752
+ if (input.source === "stdin") {
36753
+ const piped = (await readPipedStdin()).trim();
36754
+ if (piped.length === 0) {
36755
+ failCommand(new CliError("cli_usage", "No token supplied, and there is no terminal to ask in.", NO_TOKEN_HINTS));
36756
+ }
36757
+ return piped;
36758
+ }
36434
36759
  const { token } = await esm_default12.prompt([
36435
36760
  {
36436
36761
  type: "password",
@@ -36467,12 +36792,19 @@ var logoutCommand = new Command("logout").description("Delete the saved logins o
36467
36792
  });
36468
36793
 
36469
36794
  // src/commands/whoami.ts
36795
+ function roleName(role) {
36796
+ if (typeof role === "string") return role;
36797
+ if (typeof role === "object" && role !== null && "name" in role && typeof role.name === "string") {
36798
+ return role.name;
36799
+ }
36800
+ return "unknown";
36801
+ }
36470
36802
  function buildWhoamiRows(verified, session, configPath) {
36471
36803
  return [
36472
36804
  { label: "Email", value: verified.email },
36473
36805
  { label: "Organization", value: verified.orgName },
36474
36806
  { label: "Org ID", value: verified.orgId },
36475
- { label: "Role", value: verified.role },
36807
+ { label: "Role", value: roleName(verified.role) },
36476
36808
  { label: "API URL", value: session.apiBaseUrl },
36477
36809
  { label: "Token from", value: describeTokenSource(session, configPath) }
36478
36810
  ];
@@ -36498,7 +36830,7 @@ var whoamiCommand = withOutputOptions(
36498
36830
  email: verified.data.email,
36499
36831
  orgId: verified.data.orgId,
36500
36832
  orgName: verified.data.orgName,
36501
- role: verified.data.role,
36833
+ role: roleName(verified.data.role),
36502
36834
  apiBaseUrl: session.apiBaseUrl,
36503
36835
  tokenSource: session.tokenSource,
36504
36836
  configPath: getConfigPath()
@@ -36508,7 +36840,7 @@ var whoamiCommand = withOutputOptions(
36508
36840
  line();
36509
36841
  heading("Signed in");
36510
36842
  for (const row of buildWhoamiRows(verified.data, session, getConfigPath())) {
36511
- label(row.label, row.value);
36843
+ field(row.label, row.value);
36512
36844
  }
36513
36845
  line();
36514
36846
  });
@@ -36525,12 +36857,14 @@ function configForSession(session) {
36525
36857
  apiBaseUrl: session.apiBaseUrl
36526
36858
  };
36527
36859
  }
36860
+ function describeCandidates(candidates) {
36861
+ return candidates.map((c) => c.shortId != null ? `${c.shortId} (${c.name})` : c.name).join(", ");
36862
+ }
36528
36863
  function describeSwitchFailure(candidates, needle) {
36529
36864
  if (candidates.length === 0) {
36530
36865
  return `No organization matches "${needle}". This token cannot reach any organization.`;
36531
36866
  }
36532
- const known = candidates.map((c) => c.shortId != null ? `${c.shortId} (${c.name})` : c.name).join(", ");
36533
- return `No organization matches "${needle}". Available: ${known}.`;
36867
+ return `No organization matches "${needle}". Available: ${describeCandidates(candidates)}.`;
36534
36868
  }
36535
36869
  var orgCommand = withOutputOptions(
36536
36870
  new Command("org").description("Show, list, and switch the active organization")
@@ -36548,13 +36882,13 @@ var orgCommand = withOutputOptions(
36548
36882
  }
36549
36883
  heading("Current organization");
36550
36884
  if (session.orgName) {
36551
- label("Name", session.orgName);
36552
- label("ID", session.orgId);
36885
+ field("Name", session.orgName);
36886
+ field("ID", session.orgId);
36553
36887
  } else {
36554
- label("Name", "not saved on this machine");
36888
+ field("Name", "not saved on this machine");
36555
36889
  }
36556
- label("API URL", session.apiBaseUrl);
36557
- label("Token from", describeTokenSource(session, getConfigPath()));
36890
+ field("API URL", session.apiBaseUrl);
36891
+ field("Token from", describeTokenSource(session, getConfigPath()));
36558
36892
  if (session.tokenSource === "env") {
36559
36893
  dim(
36560
36894
  ` ${TOKEN_ENV_VAR} is set, so commands use that token. Run \`avsb whoami\` to see which account and organization it reaches.`
@@ -36564,8 +36898,8 @@ var orgCommand = withOutputOptions(
36564
36898
  orgCommand.addCommand(
36565
36899
  withOutputOptions(
36566
36900
  new Command("list").description("List the organizations this token can reach")
36567
- ).action(async (opts) => {
36568
- beginCommand("org list", opts);
36901
+ ).action(async (_opts, command) => {
36902
+ beginCommand("org list", command.optsWithGlobals());
36569
36903
  try {
36570
36904
  const session = ensureLoggedIn();
36571
36905
  const config = configForSession(session);
@@ -36629,12 +36963,16 @@ orgCommand.command("switch").description("Choose which organization commands use
36629
36963
  failCommand(err);
36630
36964
  }
36631
36965
  });
36632
- async function promptForOrg(candidates, allowPrompt) {
36633
- if (!allowPrompt) {
36634
- failCommand(
36635
- new CliError("cli_usage", "Name an organization: `avsb org switch <short id | id | name>`.")
36636
- );
36637
- }
36966
+ async function promptForOrg(candidates, input) {
36967
+ const refusal = refusalWithoutPrompt(
36968
+ {
36969
+ code: "cli_usage",
36970
+ message: "Name an organization: `avsb org switch <short id | id | name>`.",
36971
+ hints: [`Available: ${describeCandidates(candidates)}.`]
36972
+ },
36973
+ currentPromptContext({ input })
36974
+ );
36975
+ if (refusal) failCommand(refusal);
36638
36976
  const { chosen } = await esm_default12.prompt([
36639
36977
  {
36640
36978
  type: "list",
@@ -36750,6 +37088,15 @@ var pullCommand = new Command("pull").description("Pull latest experiment data f
36750
37088
  dim(` ${file}`);
36751
37089
  }
36752
37090
  line();
37091
+ const refusal = refusalWithoutPrompt({
37092
+ code: "cli_conflict",
37093
+ message: "Pulling would overwrite these local changes, and this run cannot ask first.",
37094
+ hints: [
37095
+ "Run `avsb pull --force` to overwrite them,",
37096
+ "or `avsb push` first to keep them on the platform."
37097
+ ]
37098
+ });
37099
+ if (refusal) failCommand(refusal);
36753
37100
  const { proceed } = await esm_default12.prompt([
36754
37101
  {
36755
37102
  type: "confirm",
@@ -36785,14 +37132,16 @@ var pullCommand = new Command("pull").description("Pull latest experiment data f
36785
37132
 
36786
37133
  // src/commands/push.ts
36787
37134
  import { createHash as createHash3 } from "node:crypto";
36788
- import fs10 from "node:fs";
36789
- import path12 from "node:path";
37135
+
37136
+ // src/commands/push.payload.ts
37137
+ import fs11 from "node:fs";
37138
+ import path13 from "node:path";
36790
37139
 
36791
37140
  // src/lib/compiler.ts
36792
37141
  import { transform } from "esbuild";
36793
37142
  import * as sass from "sass";
36794
- import fs9 from "node:fs";
36795
- import path11 from "node:path";
37143
+ import fs10 from "node:fs";
37144
+ import path12 from "node:path";
36796
37145
  async function compileTypeScript(source) {
36797
37146
  const result = await transform(source, {
36798
37147
  loader: "ts",
@@ -36803,7 +37152,7 @@ async function compileTypeScript(source) {
36803
37152
  function compileScss(filePath) {
36804
37153
  const result = sass.compile(filePath, {
36805
37154
  style: "expanded",
36806
- loadPaths: [path11.dirname(filePath)]
37155
+ loadPaths: [path12.dirname(filePath)]
36807
37156
  });
36808
37157
  return result.css;
36809
37158
  }
@@ -36879,8 +37228,8 @@ function parseEsbuildErrors(err, fallbackFile) {
36879
37228
  ];
36880
37229
  }
36881
37230
  async function compileFile(filePath) {
36882
- const ext = path11.extname(filePath);
36883
- const source = fs9.readFileSync(filePath, "utf-8");
37231
+ const ext = path12.extname(filePath);
37232
+ const source = fs10.readFileSync(filePath, "utf-8");
36884
37233
  switch (ext) {
36885
37234
  case ".ts":
36886
37235
  return compileTypeScript(source);
@@ -36895,21 +37244,21 @@ async function compileFile(filePath) {
36895
37244
  }
36896
37245
  async function compileExperiment(dir, manifest) {
36897
37246
  const { script } = manifest.languages;
36898
- const triggerPath = path11.join(dir, `trigger.${script}`);
37247
+ const triggerPath = path12.join(dir, `trigger.${script}`);
36899
37248
  let triggersJs = "";
36900
- if (fs9.existsSync(triggerPath)) {
37249
+ if (fs10.existsSync(triggerPath)) {
36901
37250
  triggersJs = await compileFile(triggerPath);
36902
37251
  }
36903
37252
  const variations = [];
36904
37253
  for (const v of manifest.variations) {
36905
- const jsPath = path11.join(dir, v.folder, v.scriptFile);
36906
- const cssPath = path11.join(dir, v.folder, v.styleFile);
37254
+ const jsPath = path12.join(dir, v.folder, v.scriptFile);
37255
+ const cssPath = path12.join(dir, v.folder, v.styleFile);
36907
37256
  let js = "";
36908
37257
  let css = "";
36909
- if (fs9.existsSync(jsPath)) {
37258
+ if (fs10.existsSync(jsPath)) {
36910
37259
  js = await compileFile(jsPath);
36911
37260
  }
36912
- if (fs9.existsSync(cssPath)) {
37261
+ if (fs10.existsSync(cssPath)) {
36913
37262
  css = await compileFile(cssPath);
36914
37263
  }
36915
37264
  variations.push({
@@ -36928,14 +37277,91 @@ async function compileExperiment(dir, manifest) {
36928
37277
  };
36929
37278
  }
36930
37279
  function detectChangeType(filePath) {
36931
- const basename3 = path11.basename(filePath, path11.extname(filePath));
36932
- const ext = path11.extname(filePath);
37280
+ const basename3 = path12.basename(filePath, path12.extname(filePath));
37281
+ const ext = path12.extname(filePath);
36933
37282
  if (basename3 === "trigger") return "trigger";
36934
37283
  if (ext === ".scss" || ext === ".css") return "css-only";
36935
37284
  if (ext === ".ts" || ext === ".js") return "js-only";
36936
37285
  return "both";
36937
37286
  }
36938
37287
 
37288
+ // src/commands/push.payload.ts
37289
+ function triggerFile(manifest) {
37290
+ return `trigger.${manifest.languages.script}`;
37291
+ }
37292
+ function variationFile(manifest, index, kind) {
37293
+ const variation = manifest.variations[index];
37294
+ if (!variation) return void 0;
37295
+ return path13.posix.join(variation.folder, kind === "script" ? variation.scriptFile : variation.styleFile);
37296
+ }
37297
+ function labelForPushPath(manifest, bodyPath) {
37298
+ const [head, index, field2] = bodyPath;
37299
+ if (head === "triggersJs") return triggerFile(manifest);
37300
+ if (head !== "variations" || typeof index !== "number") return void 0;
37301
+ if (field2 === "controlJs") return variationFile(manifest, index, "script");
37302
+ if (field2 === "controlCss") return variationFile(manifest, index, "style");
37303
+ return manifest.variations[index]?.folder;
37304
+ }
37305
+ function readSource(dir, relativePath) {
37306
+ const absolute = path13.join(dir, relativePath);
37307
+ if (!fs11.existsSync(absolute)) {
37308
+ return {
37309
+ missing: {
37310
+ file: relativePath,
37311
+ line: 0,
37312
+ column: 0,
37313
+ message: "File not found. Restore it, or run `avsb pull --force` to get the platform copy back."
37314
+ }
37315
+ };
37316
+ }
37317
+ return { source: fs11.readFileSync(absolute, "utf-8") };
37318
+ }
37319
+ async function buildPushPayload(dir, manifest) {
37320
+ const scriptLang = manifest.languages.script === "ts" ? "TYPESCRIPT" : "JAVASCRIPT";
37321
+ const styleLang = manifest.languages.style === "scss" ? "SCSS" : "CSS";
37322
+ const triggerPath = triggerFile(manifest);
37323
+ const trigger = readSource(dir, triggerPath);
37324
+ if ("missing" in trigger) return { ok: false, summary: `${triggerPath} is missing`, errors: [trigger.missing] };
37325
+ const triggerCompile = await compileScript(trigger.source, scriptLang, triggerPath);
37326
+ if (!triggerCompile.success) {
37327
+ return { ok: false, summary: "Trigger compilation failed", errors: triggerCompile.errors };
37328
+ }
37329
+ const payload = {
37330
+ triggersJs: { source: trigger.source, compiled: triggerCompile.code },
37331
+ variations: []
37332
+ };
37333
+ const sources = { triggersJs: trigger.source, variations: [] };
37334
+ for (const [index, variation] of manifest.variations.entries()) {
37335
+ const scriptPath = variationFile(manifest, index, "script") ?? variation.scriptFile;
37336
+ const stylePath = variationFile(manifest, index, "style") ?? variation.styleFile;
37337
+ const script = readSource(dir, scriptPath);
37338
+ if ("missing" in script) return { ok: false, summary: `${scriptPath} is missing`, errors: [script.missing] };
37339
+ const style = readSource(dir, stylePath);
37340
+ if ("missing" in style) return { ok: false, summary: `${stylePath} is missing`, errors: [style.missing] };
37341
+ const jsCompile = await compileScript(script.source, scriptLang, scriptPath);
37342
+ if (!jsCompile.success) {
37343
+ return { ok: false, summary: `Variation "${variation.name}" script compilation failed`, errors: jsCompile.errors };
37344
+ }
37345
+ const cssCompile = compileStyle(style.source, styleLang, stylePath);
37346
+ if (!cssCompile.success) {
37347
+ return { ok: false, summary: `Variation "${variation.name}" style compilation failed`, errors: cssCompile.errors };
37348
+ }
37349
+ payload.variations.push({
37350
+ id: variation.id,
37351
+ controlJs: { source: script.source, compiled: jsCompile.code },
37352
+ controlCss: { source: style.source, compiled: cssCompile.code }
37353
+ });
37354
+ sources.variations.push({ id: variation.id, controlJs: script.source, controlCss: style.source });
37355
+ }
37356
+ return { ok: true, payload, sources };
37357
+ }
37358
+ function publishPromptFor(status) {
37359
+ if (status === "PAUSED") {
37360
+ return "The experiment is paused. Publish the changes now? (Visitors see them when it resumes.)";
37361
+ }
37362
+ return "The experiment is running. Publish the changes now?";
37363
+ }
37364
+
36939
37365
  // src/commands/push.ts
36940
37366
  function sha2563(content) {
36941
37367
  return createHash3("sha256").update(content).digest("hex");
@@ -36960,35 +37386,60 @@ function detectRemoteChanges(remoteExp, syncedHashes) {
36960
37386
  }
36961
37387
  return changed;
36962
37388
  }
36963
- var pushCommand = new Command("push").description("Compile and push local changes to the platform").option("-f, --force", "Skip conflict check and push without confirmation").action(async (options) => {
37389
+ async function confirmOverwrite() {
37390
+ if (!isInteractiveRun()) {
37391
+ failCommand(
37392
+ new CliError(
37393
+ "cli_conflict",
37394
+ "The platform copy changed since your last pull, and this run cannot ask before overwriting it.",
37395
+ [
37396
+ "Run `avsb pull` to fetch the remote changes first (it overwrites your local files),",
37397
+ "or `avsb push --force` to overwrite the remote changes."
37398
+ ]
37399
+ )
37400
+ );
37401
+ }
37402
+ const { proceed } = await esm_default12.prompt([
37403
+ { type: "confirm", name: "proceed", message: "Pushing will overwrite remote changes. Continue?", default: false }
37404
+ ]);
37405
+ return proceed;
37406
+ }
37407
+ async function shouldPublish(options, status) {
37408
+ if (options.publish !== void 0) return options.publish;
37409
+ if (!isInteractiveRun()) return false;
37410
+ const { publish } = await esm_default12.prompt([
37411
+ { type: "confirm", name: "publish", message: publishPromptFor(status), default: false }
37412
+ ]);
37413
+ return publish;
37414
+ }
37415
+ var pushCommand = new Command("push").description("Compile and push local changes to the platform").option("-f, --force", "Skip conflict check and push without confirmation").option("--publish", "Publish the changes right away when the experiment is running or paused").option("--no-publish", "Leave the changes pending without asking").action(async (options) => {
37416
+ let manifest = null;
36964
37417
  try {
36965
37418
  const config = ensureLoggedIn();
36966
- const { manifest, dir } = readManifestOrExit();
37419
+ const read = readManifestOrExit();
37420
+ manifest = read.manifest;
37421
+ const { dir } = read;
36967
37422
  let spinner = ora("Compiling experiment...").start();
36968
- await compileExperiment(dir, manifest);
37423
+ const built = await buildPushPayload(dir, manifest);
37424
+ if (!built.ok) {
37425
+ spinner.fail(built.summary);
37426
+ for (const e of built.errors) {
37427
+ error(` ${e.line > 0 ? `${e.file}:${e.line}:${e.column}` : e.file}: ${e.message}`);
37428
+ }
37429
+ process.exit(EXIT.failure);
37430
+ }
36969
37431
  const client = new ApiClient(config);
36970
37432
  if (!options.force && manifest.syncedHashes) {
36971
37433
  spinner.text = "Checking for remote changes...";
36972
37434
  const remote = await client.getExperiment(manifest.experimentShortId);
36973
- const remoteExp = remote.data.experiment;
36974
- const remoteChanges = detectRemoteChanges(remoteExp, manifest.syncedHashes);
37435
+ const remoteChanges = detectRemoteChanges(remote.data.experiment, manifest.syncedHashes);
36975
37436
  if (remoteChanges.length > 0) {
36976
37437
  spinner.warn("Remote has changes you haven't pulled");
36977
37438
  line();
36978
37439
  heading("Files changed on remote:");
36979
- for (const file of remoteChanges) {
36980
- dim(` ${file}`);
36981
- }
37440
+ for (const file of remoteChanges) dim(` ${file}`);
36982
37441
  line();
36983
- const { proceed } = await esm_default12.prompt([
36984
- {
36985
- type: "confirm",
36986
- name: "proceed",
36987
- message: "Pushing will overwrite remote changes. Continue?",
36988
- default: false
36989
- }
36990
- ]);
36991
- if (!proceed) {
37442
+ if (!await confirmOverwrite()) {
36992
37443
  info("Push aborted. Run `avsb pull` to fetch remote changes first.");
36993
37444
  warn("Note: pulling will overwrite your local files.");
36994
37445
  process.exit(EXIT.success);
@@ -37000,78 +37451,37 @@ var pushCommand = new Command("push").description("Compile and push local change
37000
37451
  } else {
37001
37452
  spinner.text = "Pushing to platform...";
37002
37453
  }
37003
- const scriptLang = manifest.languages.script === "ts" ? "TYPESCRIPT" : "JAVASCRIPT";
37004
- const styleLang = manifest.languages.style === "scss" ? "SCSS" : "CSS";
37005
- const rawTrigger = fs10.readFileSync(
37006
- path12.join(dir, `trigger.${manifest.languages.script}`),
37007
- "utf-8"
37008
- );
37009
- const triggerCompile = await compileScript(rawTrigger, scriptLang, `trigger.${manifest.languages.script}`);
37010
- if (!triggerCompile.success) {
37011
- spinner.fail("Trigger compilation failed");
37012
- for (const e of triggerCompile.errors) {
37013
- error(` trigger.${manifest.languages.script}:${e.line}:${e.column}: ${e.message}`);
37014
- }
37015
- process.exit(EXIT.failure);
37016
- }
37017
- const rawVariations = [];
37018
- const compiledVariations = [];
37019
- for (const v of manifest.variations) {
37020
- const rawJs = fs10.readFileSync(path12.join(dir, v.folder, v.scriptFile), "utf-8");
37021
- const rawCss = fs10.readFileSync(path12.join(dir, v.folder, v.styleFile), "utf-8");
37022
- const jsCompile = await compileScript(rawJs, scriptLang, `${v.name}.${manifest.languages.script}`);
37023
- const cssCompile = compileStyle(rawCss, styleLang, `${v.name}.${manifest.languages.style}`);
37024
- if (!jsCompile.success) {
37025
- spinner.fail(`Variation "${v.name}" JS compilation failed`);
37026
- for (const e of jsCompile.errors) error(` ${e.file}:${e.line}:${e.column}: ${e.message}`);
37027
- process.exit(EXIT.failure);
37028
- }
37029
- if (!cssCompile.success) {
37030
- spinner.fail(`Variation "${v.name}" CSS compilation failed`);
37031
- for (const e of cssCompile.errors) error(` ${e.file}:${e.line}:${e.column}: ${e.message}`);
37032
- process.exit(EXIT.failure);
37033
- }
37034
- rawVariations.push({ id: v.id, controlJs: rawJs, controlCss: rawCss });
37035
- compiledVariations.push({
37036
- id: v.id,
37037
- controlJs: { source: rawJs, compiled: jsCompile.code },
37038
- controlCss: { source: rawCss, compiled: cssCompile.code }
37039
- });
37454
+ let response;
37455
+ try {
37456
+ response = await client.pushExperiment(manifest.experimentShortId, built.payload);
37457
+ } catch (err) {
37458
+ spinner.fail("Push refused");
37459
+ throw err;
37040
37460
  }
37041
- const response = await client.pushExperiment(manifest.experimentShortId, {
37042
- triggersJs: { source: rawTrigger, compiled: triggerCompile.code },
37043
- variations: compiledVariations
37044
- });
37045
- manifest.syncedHashes = computeSyncHashes(rawTrigger, rawVariations);
37461
+ manifest.syncedHashes = computeSyncHashes(built.sources.triggersJs, built.sources.variations);
37046
37462
  writeManifest(dir, manifest);
37047
37463
  spinner.succeed("Pushed successfully");
37048
37464
  line();
37049
37465
  if (response.data.hasPendingChanges) {
37050
- const { publish } = await esm_default12.prompt([
37051
- {
37052
- type: "confirm",
37053
- name: "publish",
37054
- message: "Experiment is running. Publish changes now?",
37055
- default: false
37056
- }
37057
- ]);
37058
- if (publish) {
37466
+ if (await shouldPublish(options, response.data.status)) {
37059
37467
  const publishSpinner = ora("Publishing changes...").start();
37060
- await client.publishChanges(manifest.experimentShortId);
37468
+ const published = await client.publishChanges(manifest.experimentShortId);
37061
37469
  publishSpinner.succeed("Changes published");
37470
+ if (published.data.warning) warn(published.data.warning.message);
37062
37471
  } else {
37063
- info("Changes saved as pending. Publish from the platform when ready.");
37472
+ info("Changes saved as pending. Publish them with `avsb push --publish`, or from the dashboard.");
37064
37473
  }
37065
37474
  }
37066
37475
  line();
37067
37476
  success("Push complete");
37068
37477
  } catch (err) {
37069
- failCommand(err);
37478
+ const current = manifest;
37479
+ failCommand(err, current ? { labelForPath: (bodyPath) => labelForPushPath(current, bodyPath) } : {});
37070
37480
  }
37071
37481
  });
37072
37482
 
37073
37483
  // src/commands/dev.ts
37074
- import path14 from "node:path";
37484
+ import path15 from "node:path";
37075
37485
 
37076
37486
  // node_modules/chokidar/esm/index.js
37077
37487
  import { stat as statcb } from "fs";
@@ -37149,7 +37559,7 @@ var ReaddirpStream = class extends Readable {
37149
37559
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
37150
37560
  const statMethod = opts.lstat ? lstat : stat;
37151
37561
  if (wantBigintFsStats) {
37152
- this._stat = (path22) => statMethod(path22, { bigint: true });
37562
+ this._stat = (path23) => statMethod(path23, { bigint: true });
37153
37563
  } else {
37154
37564
  this._stat = statMethod;
37155
37565
  }
@@ -37174,8 +37584,8 @@ var ReaddirpStream = class extends Readable {
37174
37584
  const par = this.parent;
37175
37585
  const fil = par && par.files;
37176
37586
  if (fil && fil.length > 0) {
37177
- const { path: path22, depth } = par;
37178
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path22));
37587
+ const { path: path23, depth } = par;
37588
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path23));
37179
37589
  const awaited = await Promise.all(slice);
37180
37590
  for (const entry of awaited) {
37181
37591
  if (!entry)
@@ -37215,20 +37625,20 @@ var ReaddirpStream = class extends Readable {
37215
37625
  this.reading = false;
37216
37626
  }
37217
37627
  }
37218
- async _exploreDir(path22, depth) {
37628
+ async _exploreDir(path23, depth) {
37219
37629
  let files;
37220
37630
  try {
37221
- files = await readdir(path22, this._rdOptions);
37631
+ files = await readdir(path23, this._rdOptions);
37222
37632
  } catch (error2) {
37223
37633
  this._onError(error2);
37224
37634
  }
37225
- return { files, depth, path: path22 };
37635
+ return { files, depth, path: path23 };
37226
37636
  }
37227
- async _formatEntry(dirent, path22) {
37637
+ async _formatEntry(dirent, path23) {
37228
37638
  let entry;
37229
37639
  const basename3 = this._isDirent ? dirent.name : dirent;
37230
37640
  try {
37231
- const fullPath = presolve(pjoin(path22, basename3));
37641
+ const fullPath = presolve(pjoin(path23, basename3));
37232
37642
  entry = { path: prelative(this._root, fullPath), fullPath, basename: basename3 };
37233
37643
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
37234
37644
  } catch (err) {
@@ -37628,16 +38038,16 @@ var delFromSet = (main3, prop, item) => {
37628
38038
  };
37629
38039
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
37630
38040
  var FsWatchInstances = /* @__PURE__ */ new Map();
37631
- function createFsWatchInstance(path22, options, listener, errHandler, emitRaw) {
38041
+ function createFsWatchInstance(path23, options, listener, errHandler, emitRaw) {
37632
38042
  const handleEvent = (rawEvent, evPath) => {
37633
- listener(path22);
37634
- emitRaw(rawEvent, evPath, { watchedPath: path22 });
37635
- if (evPath && path22 !== evPath) {
37636
- fsWatchBroadcast(sysPath.resolve(path22, evPath), KEY_LISTENERS, sysPath.join(path22, evPath));
38043
+ listener(path23);
38044
+ emitRaw(rawEvent, evPath, { watchedPath: path23 });
38045
+ if (evPath && path23 !== evPath) {
38046
+ fsWatchBroadcast(sysPath.resolve(path23, evPath), KEY_LISTENERS, sysPath.join(path23, evPath));
37637
38047
  }
37638
38048
  };
37639
38049
  try {
37640
- return fs_watch(path22, {
38050
+ return fs_watch(path23, {
37641
38051
  persistent: options.persistent
37642
38052
  }, handleEvent);
37643
38053
  } catch (error2) {
@@ -37653,12 +38063,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
37653
38063
  listener(val1, val2, val3);
37654
38064
  });
37655
38065
  };
37656
- var setFsWatchListener = (path22, fullPath, options, handlers) => {
38066
+ var setFsWatchListener = (path23, fullPath, options, handlers) => {
37657
38067
  const { listener, errHandler, rawEmitter } = handlers;
37658
38068
  let cont = FsWatchInstances.get(fullPath);
37659
38069
  let watcher;
37660
38070
  if (!options.persistent) {
37661
- watcher = createFsWatchInstance(path22, options, listener, errHandler, rawEmitter);
38071
+ watcher = createFsWatchInstance(path23, options, listener, errHandler, rawEmitter);
37662
38072
  if (!watcher)
37663
38073
  return;
37664
38074
  return watcher.close.bind(watcher);
@@ -37669,7 +38079,7 @@ var setFsWatchListener = (path22, fullPath, options, handlers) => {
37669
38079
  addAndConvert(cont, KEY_RAW, rawEmitter);
37670
38080
  } else {
37671
38081
  watcher = createFsWatchInstance(
37672
- path22,
38082
+ path23,
37673
38083
  options,
37674
38084
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
37675
38085
  errHandler,
@@ -37684,7 +38094,7 @@ var setFsWatchListener = (path22, fullPath, options, handlers) => {
37684
38094
  cont.watcherUnusable = true;
37685
38095
  if (isWindows && error2.code === "EPERM") {
37686
38096
  try {
37687
- const fd = await open(path22, "r");
38097
+ const fd = await open(path23, "r");
37688
38098
  await fd.close();
37689
38099
  broadcastErr(error2);
37690
38100
  } catch (err) {
@@ -37715,7 +38125,7 @@ var setFsWatchListener = (path22, fullPath, options, handlers) => {
37715
38125
  };
37716
38126
  };
37717
38127
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
37718
- var setFsWatchFileListener = (path22, fullPath, options, handlers) => {
38128
+ var setFsWatchFileListener = (path23, fullPath, options, handlers) => {
37719
38129
  const { listener, rawEmitter } = handlers;
37720
38130
  let cont = FsWatchFileInstances.get(fullPath);
37721
38131
  const copts = cont && cont.options;
@@ -37737,7 +38147,7 @@ var setFsWatchFileListener = (path22, fullPath, options, handlers) => {
37737
38147
  });
37738
38148
  const currmtime = curr.mtimeMs;
37739
38149
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
37740
- foreach(cont.listeners, (listener2) => listener2(path22, curr));
38150
+ foreach(cont.listeners, (listener2) => listener2(path23, curr));
37741
38151
  }
37742
38152
  })
37743
38153
  };
@@ -37765,13 +38175,13 @@ var NodeFsHandler = class {
37765
38175
  * @param listener on fs change
37766
38176
  * @returns closer for the watcher instance
37767
38177
  */
37768
- _watchWithNodeFs(path22, listener) {
38178
+ _watchWithNodeFs(path23, listener) {
37769
38179
  const opts = this.fsw.options;
37770
- const directory = sysPath.dirname(path22);
37771
- const basename3 = sysPath.basename(path22);
38180
+ const directory = sysPath.dirname(path23);
38181
+ const basename3 = sysPath.basename(path23);
37772
38182
  const parent = this.fsw._getWatchedDir(directory);
37773
38183
  parent.add(basename3);
37774
- const absolutePath = sysPath.resolve(path22);
38184
+ const absolutePath = sysPath.resolve(path23);
37775
38185
  const options = {
37776
38186
  persistent: opts.persistent
37777
38187
  };
@@ -37781,12 +38191,12 @@ var NodeFsHandler = class {
37781
38191
  if (opts.usePolling) {
37782
38192
  const enableBin = opts.interval !== opts.binaryInterval;
37783
38193
  options.interval = enableBin && isBinaryPath(basename3) ? opts.binaryInterval : opts.interval;
37784
- closer = setFsWatchFileListener(path22, absolutePath, options, {
38194
+ closer = setFsWatchFileListener(path23, absolutePath, options, {
37785
38195
  listener,
37786
38196
  rawEmitter: this.fsw._emitRaw
37787
38197
  });
37788
38198
  } else {
37789
- closer = setFsWatchListener(path22, absolutePath, options, {
38199
+ closer = setFsWatchListener(path23, absolutePath, options, {
37790
38200
  listener,
37791
38201
  errHandler: this._boundHandleError,
37792
38202
  rawEmitter: this.fsw._emitRaw
@@ -37808,7 +38218,7 @@ var NodeFsHandler = class {
37808
38218
  let prevStats = stats;
37809
38219
  if (parent.has(basename3))
37810
38220
  return;
37811
- const listener = async (path22, newStats) => {
38221
+ const listener = async (path23, newStats) => {
37812
38222
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
37813
38223
  return;
37814
38224
  if (!newStats || newStats.mtimeMs === 0) {
@@ -37822,11 +38232,11 @@ var NodeFsHandler = class {
37822
38232
  this.fsw._emit(EV.CHANGE, file, newStats2);
37823
38233
  }
37824
38234
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
37825
- this.fsw._closeFile(path22);
38235
+ this.fsw._closeFile(path23);
37826
38236
  prevStats = newStats2;
37827
38237
  const closer2 = this._watchWithNodeFs(file, listener);
37828
38238
  if (closer2)
37829
- this.fsw._addPathCloser(path22, closer2);
38239
+ this.fsw._addPathCloser(path23, closer2);
37830
38240
  } else {
37831
38241
  prevStats = newStats2;
37832
38242
  }
@@ -37858,7 +38268,7 @@ var NodeFsHandler = class {
37858
38268
  * @param item basename of this item
37859
38269
  * @returns true if no more processing is needed for this entry.
37860
38270
  */
37861
- async _handleSymlink(entry, directory, path22, item) {
38271
+ async _handleSymlink(entry, directory, path23, item) {
37862
38272
  if (this.fsw.closed) {
37863
38273
  return;
37864
38274
  }
@@ -37868,7 +38278,7 @@ var NodeFsHandler = class {
37868
38278
  this.fsw._incrReadyCount();
37869
38279
  let linkPath;
37870
38280
  try {
37871
- linkPath = await fsrealpath(path22);
38281
+ linkPath = await fsrealpath(path23);
37872
38282
  } catch (e) {
37873
38283
  this.fsw._emitReady();
37874
38284
  return true;
@@ -37878,12 +38288,12 @@ var NodeFsHandler = class {
37878
38288
  if (dir.has(item)) {
37879
38289
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
37880
38290
  this.fsw._symlinkPaths.set(full, linkPath);
37881
- this.fsw._emit(EV.CHANGE, path22, entry.stats);
38291
+ this.fsw._emit(EV.CHANGE, path23, entry.stats);
37882
38292
  }
37883
38293
  } else {
37884
38294
  dir.add(item);
37885
38295
  this.fsw._symlinkPaths.set(full, linkPath);
37886
- this.fsw._emit(EV.ADD, path22, entry.stats);
38296
+ this.fsw._emit(EV.ADD, path23, entry.stats);
37887
38297
  }
37888
38298
  this.fsw._emitReady();
37889
38299
  return true;
@@ -37912,9 +38322,9 @@ var NodeFsHandler = class {
37912
38322
  return;
37913
38323
  }
37914
38324
  const item = entry.path;
37915
- let path22 = sysPath.join(directory, item);
38325
+ let path23 = sysPath.join(directory, item);
37916
38326
  current.add(item);
37917
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path22, item)) {
38327
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path23, item)) {
37918
38328
  return;
37919
38329
  }
37920
38330
  if (this.fsw.closed) {
@@ -37923,8 +38333,8 @@ var NodeFsHandler = class {
37923
38333
  }
37924
38334
  if (item === target || !target && !previous.has(item)) {
37925
38335
  this.fsw._incrReadyCount();
37926
- path22 = sysPath.join(dir, sysPath.relative(dir, path22));
37927
- this._addToNodeFs(path22, initialAdd, wh, depth + 1);
38336
+ path23 = sysPath.join(dir, sysPath.relative(dir, path23));
38337
+ this._addToNodeFs(path23, initialAdd, wh, depth + 1);
37928
38338
  }
37929
38339
  }).on(EV.ERROR, this._boundHandleError);
37930
38340
  return new Promise((resolve3, reject) => {
@@ -37993,13 +38403,13 @@ var NodeFsHandler = class {
37993
38403
  * @param depth Child path actually targeted for watch
37994
38404
  * @param target Child path actually targeted for watch
37995
38405
  */
37996
- async _addToNodeFs(path22, initialAdd, priorWh, depth, target) {
38406
+ async _addToNodeFs(path23, initialAdd, priorWh, depth, target) {
37997
38407
  const ready = this.fsw._emitReady;
37998
- if (this.fsw._isIgnored(path22) || this.fsw.closed) {
38408
+ if (this.fsw._isIgnored(path23) || this.fsw.closed) {
37999
38409
  ready();
38000
38410
  return false;
38001
38411
  }
38002
- const wh = this.fsw._getWatchHelpers(path22);
38412
+ const wh = this.fsw._getWatchHelpers(path23);
38003
38413
  if (priorWh) {
38004
38414
  wh.filterPath = (entry) => priorWh.filterPath(entry);
38005
38415
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -38015,8 +38425,8 @@ var NodeFsHandler = class {
38015
38425
  const follow = this.fsw.options.followSymlinks;
38016
38426
  let closer;
38017
38427
  if (stats.isDirectory()) {
38018
- const absPath = sysPath.resolve(path22);
38019
- const targetPath = follow ? await fsrealpath(path22) : path22;
38428
+ const absPath = sysPath.resolve(path23);
38429
+ const targetPath = follow ? await fsrealpath(path23) : path23;
38020
38430
  if (this.fsw.closed)
38021
38431
  return;
38022
38432
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -38026,29 +38436,29 @@ var NodeFsHandler = class {
38026
38436
  this.fsw._symlinkPaths.set(absPath, targetPath);
38027
38437
  }
38028
38438
  } else if (stats.isSymbolicLink()) {
38029
- const targetPath = follow ? await fsrealpath(path22) : path22;
38439
+ const targetPath = follow ? await fsrealpath(path23) : path23;
38030
38440
  if (this.fsw.closed)
38031
38441
  return;
38032
38442
  const parent = sysPath.dirname(wh.watchPath);
38033
38443
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
38034
38444
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
38035
- closer = await this._handleDir(parent, stats, initialAdd, depth, path22, wh, targetPath);
38445
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path23, wh, targetPath);
38036
38446
  if (this.fsw.closed)
38037
38447
  return;
38038
38448
  if (targetPath !== void 0) {
38039
- this.fsw._symlinkPaths.set(sysPath.resolve(path22), targetPath);
38449
+ this.fsw._symlinkPaths.set(sysPath.resolve(path23), targetPath);
38040
38450
  }
38041
38451
  } else {
38042
38452
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
38043
38453
  }
38044
38454
  ready();
38045
38455
  if (closer)
38046
- this.fsw._addPathCloser(path22, closer);
38456
+ this.fsw._addPathCloser(path23, closer);
38047
38457
  return false;
38048
38458
  } catch (error2) {
38049
38459
  if (this.fsw._handleError(error2)) {
38050
38460
  ready();
38051
- return path22;
38461
+ return path23;
38052
38462
  }
38053
38463
  }
38054
38464
  }
@@ -38091,26 +38501,26 @@ function createPattern(matcher) {
38091
38501
  }
38092
38502
  return () => false;
38093
38503
  }
38094
- function normalizePath(path22) {
38095
- if (typeof path22 !== "string")
38504
+ function normalizePath(path23) {
38505
+ if (typeof path23 !== "string")
38096
38506
  throw new Error("string expected");
38097
- path22 = sysPath2.normalize(path22);
38098
- path22 = path22.replace(/\\/g, "/");
38507
+ path23 = sysPath2.normalize(path23);
38508
+ path23 = path23.replace(/\\/g, "/");
38099
38509
  let prepend = false;
38100
- if (path22.startsWith("//"))
38510
+ if (path23.startsWith("//"))
38101
38511
  prepend = true;
38102
38512
  const DOUBLE_SLASH_RE2 = /\/\//;
38103
- while (path22.match(DOUBLE_SLASH_RE2))
38104
- path22 = path22.replace(DOUBLE_SLASH_RE2, "/");
38513
+ while (path23.match(DOUBLE_SLASH_RE2))
38514
+ path23 = path23.replace(DOUBLE_SLASH_RE2, "/");
38105
38515
  if (prepend)
38106
- path22 = "/" + path22;
38107
- return path22;
38516
+ path23 = "/" + path23;
38517
+ return path23;
38108
38518
  }
38109
38519
  function matchPatterns(patterns, testString, stats) {
38110
- const path22 = normalizePath(testString);
38520
+ const path23 = normalizePath(testString);
38111
38521
  for (let index = 0; index < patterns.length; index++) {
38112
38522
  const pattern = patterns[index];
38113
- if (pattern(path22, stats)) {
38523
+ if (pattern(path23, stats)) {
38114
38524
  return true;
38115
38525
  }
38116
38526
  }
@@ -38150,19 +38560,19 @@ var toUnix = (string) => {
38150
38560
  }
38151
38561
  return str;
38152
38562
  };
38153
- var normalizePathToUnix = (path22) => toUnix(sysPath2.normalize(toUnix(path22)));
38154
- var normalizeIgnored = (cwd = "") => (path22) => {
38155
- if (typeof path22 === "string") {
38156
- return normalizePathToUnix(sysPath2.isAbsolute(path22) ? path22 : sysPath2.join(cwd, path22));
38563
+ var normalizePathToUnix = (path23) => toUnix(sysPath2.normalize(toUnix(path23)));
38564
+ var normalizeIgnored = (cwd = "") => (path23) => {
38565
+ if (typeof path23 === "string") {
38566
+ return normalizePathToUnix(sysPath2.isAbsolute(path23) ? path23 : sysPath2.join(cwd, path23));
38157
38567
  } else {
38158
- return path22;
38568
+ return path23;
38159
38569
  }
38160
38570
  };
38161
- var getAbsolutePath = (path22, cwd) => {
38162
- if (sysPath2.isAbsolute(path22)) {
38163
- return path22;
38571
+ var getAbsolutePath = (path23, cwd) => {
38572
+ if (sysPath2.isAbsolute(path23)) {
38573
+ return path23;
38164
38574
  }
38165
- return sysPath2.join(cwd, path22);
38575
+ return sysPath2.join(cwd, path23);
38166
38576
  };
38167
38577
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
38168
38578
  var DirEntry = class {
@@ -38217,10 +38627,10 @@ var DirEntry = class {
38217
38627
  var STAT_METHOD_F = "stat";
38218
38628
  var STAT_METHOD_L = "lstat";
38219
38629
  var WatchHelper = class {
38220
- constructor(path22, follow, fsw) {
38630
+ constructor(path23, follow, fsw) {
38221
38631
  this.fsw = fsw;
38222
- const watchPath = path22;
38223
- this.path = path22 = path22.replace(REPLACER_RE, "");
38632
+ const watchPath = path23;
38633
+ this.path = path23 = path23.replace(REPLACER_RE, "");
38224
38634
  this.watchPath = watchPath;
38225
38635
  this.fullWatchPath = sysPath2.resolve(watchPath);
38226
38636
  this.dirParts = [];
@@ -38342,20 +38752,20 @@ var FSWatcher = class extends EventEmitter {
38342
38752
  this._closePromise = void 0;
38343
38753
  let paths = unifyPaths(paths_);
38344
38754
  if (cwd) {
38345
- paths = paths.map((path22) => {
38346
- const absPath = getAbsolutePath(path22, cwd);
38755
+ paths = paths.map((path23) => {
38756
+ const absPath = getAbsolutePath(path23, cwd);
38347
38757
  return absPath;
38348
38758
  });
38349
38759
  }
38350
- paths.forEach((path22) => {
38351
- this._removeIgnoredPath(path22);
38760
+ paths.forEach((path23) => {
38761
+ this._removeIgnoredPath(path23);
38352
38762
  });
38353
38763
  this._userIgnored = void 0;
38354
38764
  if (!this._readyCount)
38355
38765
  this._readyCount = 0;
38356
38766
  this._readyCount += paths.length;
38357
- Promise.all(paths.map(async (path22) => {
38358
- const res = await this._nodeFsHandler._addToNodeFs(path22, !_internal, void 0, 0, _origAdd);
38767
+ Promise.all(paths.map(async (path23) => {
38768
+ const res = await this._nodeFsHandler._addToNodeFs(path23, !_internal, void 0, 0, _origAdd);
38359
38769
  if (res)
38360
38770
  this._emitReady();
38361
38771
  return res;
@@ -38377,17 +38787,17 @@ var FSWatcher = class extends EventEmitter {
38377
38787
  return this;
38378
38788
  const paths = unifyPaths(paths_);
38379
38789
  const { cwd } = this.options;
38380
- paths.forEach((path22) => {
38381
- if (!sysPath2.isAbsolute(path22) && !this._closers.has(path22)) {
38790
+ paths.forEach((path23) => {
38791
+ if (!sysPath2.isAbsolute(path23) && !this._closers.has(path23)) {
38382
38792
  if (cwd)
38383
- path22 = sysPath2.join(cwd, path22);
38384
- path22 = sysPath2.resolve(path22);
38793
+ path23 = sysPath2.join(cwd, path23);
38794
+ path23 = sysPath2.resolve(path23);
38385
38795
  }
38386
- this._closePath(path22);
38387
- this._addIgnoredPath(path22);
38388
- if (this._watched.has(path22)) {
38796
+ this._closePath(path23);
38797
+ this._addIgnoredPath(path23);
38798
+ if (this._watched.has(path23)) {
38389
38799
  this._addIgnoredPath({
38390
- path: path22,
38800
+ path: path23,
38391
38801
  recursive: true
38392
38802
  });
38393
38803
  }
@@ -38451,38 +38861,38 @@ var FSWatcher = class extends EventEmitter {
38451
38861
  * @param stats arguments to be passed with event
38452
38862
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
38453
38863
  */
38454
- async _emit(event, path22, stats) {
38864
+ async _emit(event, path23, stats) {
38455
38865
  if (this.closed)
38456
38866
  return;
38457
38867
  const opts = this.options;
38458
38868
  if (isWindows)
38459
- path22 = sysPath2.normalize(path22);
38869
+ path23 = sysPath2.normalize(path23);
38460
38870
  if (opts.cwd)
38461
- path22 = sysPath2.relative(opts.cwd, path22);
38462
- const args = [path22];
38871
+ path23 = sysPath2.relative(opts.cwd, path23);
38872
+ const args = [path23];
38463
38873
  if (stats != null)
38464
38874
  args.push(stats);
38465
38875
  const awf = opts.awaitWriteFinish;
38466
38876
  let pw;
38467
- if (awf && (pw = this._pendingWrites.get(path22))) {
38877
+ if (awf && (pw = this._pendingWrites.get(path23))) {
38468
38878
  pw.lastChange = /* @__PURE__ */ new Date();
38469
38879
  return this;
38470
38880
  }
38471
38881
  if (opts.atomic) {
38472
38882
  if (event === EVENTS.UNLINK) {
38473
- this._pendingUnlinks.set(path22, [event, ...args]);
38883
+ this._pendingUnlinks.set(path23, [event, ...args]);
38474
38884
  setTimeout(() => {
38475
- this._pendingUnlinks.forEach((entry, path23) => {
38885
+ this._pendingUnlinks.forEach((entry, path24) => {
38476
38886
  this.emit(...entry);
38477
38887
  this.emit(EVENTS.ALL, ...entry);
38478
- this._pendingUnlinks.delete(path23);
38888
+ this._pendingUnlinks.delete(path24);
38479
38889
  });
38480
38890
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
38481
38891
  return this;
38482
38892
  }
38483
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path22)) {
38893
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path23)) {
38484
38894
  event = EVENTS.CHANGE;
38485
- this._pendingUnlinks.delete(path22);
38895
+ this._pendingUnlinks.delete(path23);
38486
38896
  }
38487
38897
  }
38488
38898
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -38500,16 +38910,16 @@ var FSWatcher = class extends EventEmitter {
38500
38910
  this.emitWithAll(event, args);
38501
38911
  }
38502
38912
  };
38503
- this._awaitWriteFinish(path22, awf.stabilityThreshold, event, awfEmit);
38913
+ this._awaitWriteFinish(path23, awf.stabilityThreshold, event, awfEmit);
38504
38914
  return this;
38505
38915
  }
38506
38916
  if (event === EVENTS.CHANGE) {
38507
- const isThrottled = !this._throttle(EVENTS.CHANGE, path22, 50);
38917
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path23, 50);
38508
38918
  if (isThrottled)
38509
38919
  return this;
38510
38920
  }
38511
38921
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
38512
- const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path22) : path22;
38922
+ const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path23) : path23;
38513
38923
  let stats2;
38514
38924
  try {
38515
38925
  stats2 = await stat3(fullPath);
@@ -38540,23 +38950,23 @@ var FSWatcher = class extends EventEmitter {
38540
38950
  * @param timeout duration of time to suppress duplicate actions
38541
38951
  * @returns tracking object or false if action should be suppressed
38542
38952
  */
38543
- _throttle(actionType, path22, timeout) {
38953
+ _throttle(actionType, path23, timeout) {
38544
38954
  if (!this._throttled.has(actionType)) {
38545
38955
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
38546
38956
  }
38547
38957
  const action = this._throttled.get(actionType);
38548
38958
  if (!action)
38549
38959
  throw new Error("invalid throttle");
38550
- const actionPath = action.get(path22);
38960
+ const actionPath = action.get(path23);
38551
38961
  if (actionPath) {
38552
38962
  actionPath.count++;
38553
38963
  return false;
38554
38964
  }
38555
38965
  let timeoutObject;
38556
38966
  const clear = () => {
38557
- const item = action.get(path22);
38967
+ const item = action.get(path23);
38558
38968
  const count = item ? item.count : 0;
38559
- action.delete(path22);
38969
+ action.delete(path23);
38560
38970
  clearTimeout(timeoutObject);
38561
38971
  if (item)
38562
38972
  clearTimeout(item.timeoutObject);
@@ -38564,7 +38974,7 @@ var FSWatcher = class extends EventEmitter {
38564
38974
  };
38565
38975
  timeoutObject = setTimeout(clear, timeout);
38566
38976
  const thr = { timeoutObject, clear, count: 0 };
38567
- action.set(path22, thr);
38977
+ action.set(path23, thr);
38568
38978
  return thr;
38569
38979
  }
38570
38980
  _incrReadyCount() {
@@ -38578,44 +38988,44 @@ var FSWatcher = class extends EventEmitter {
38578
38988
  * @param event
38579
38989
  * @param awfEmit Callback to be called when ready for event to be emitted.
38580
38990
  */
38581
- _awaitWriteFinish(path22, threshold, event, awfEmit) {
38991
+ _awaitWriteFinish(path23, threshold, event, awfEmit) {
38582
38992
  const awf = this.options.awaitWriteFinish;
38583
38993
  if (typeof awf !== "object")
38584
38994
  return;
38585
38995
  const pollInterval = awf.pollInterval;
38586
38996
  let timeoutHandler;
38587
- let fullPath = path22;
38588
- if (this.options.cwd && !sysPath2.isAbsolute(path22)) {
38589
- fullPath = sysPath2.join(this.options.cwd, path22);
38997
+ let fullPath = path23;
38998
+ if (this.options.cwd && !sysPath2.isAbsolute(path23)) {
38999
+ fullPath = sysPath2.join(this.options.cwd, path23);
38590
39000
  }
38591
39001
  const now = /* @__PURE__ */ new Date();
38592
39002
  const writes = this._pendingWrites;
38593
39003
  function awaitWriteFinishFn(prevStat) {
38594
39004
  statcb(fullPath, (err, curStat) => {
38595
- if (err || !writes.has(path22)) {
39005
+ if (err || !writes.has(path23)) {
38596
39006
  if (err && err.code !== "ENOENT")
38597
39007
  awfEmit(err);
38598
39008
  return;
38599
39009
  }
38600
39010
  const now2 = Number(/* @__PURE__ */ new Date());
38601
39011
  if (prevStat && curStat.size !== prevStat.size) {
38602
- writes.get(path22).lastChange = now2;
39012
+ writes.get(path23).lastChange = now2;
38603
39013
  }
38604
- const pw = writes.get(path22);
39014
+ const pw = writes.get(path23);
38605
39015
  const df = now2 - pw.lastChange;
38606
39016
  if (df >= threshold) {
38607
- writes.delete(path22);
39017
+ writes.delete(path23);
38608
39018
  awfEmit(void 0, curStat);
38609
39019
  } else {
38610
39020
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
38611
39021
  }
38612
39022
  });
38613
39023
  }
38614
- if (!writes.has(path22)) {
38615
- writes.set(path22, {
39024
+ if (!writes.has(path23)) {
39025
+ writes.set(path23, {
38616
39026
  lastChange: now,
38617
39027
  cancelWait: () => {
38618
- writes.delete(path22);
39028
+ writes.delete(path23);
38619
39029
  clearTimeout(timeoutHandler);
38620
39030
  return event;
38621
39031
  }
@@ -38626,8 +39036,8 @@ var FSWatcher = class extends EventEmitter {
38626
39036
  /**
38627
39037
  * Determines whether user has asked to ignore this path.
38628
39038
  */
38629
- _isIgnored(path22, stats) {
38630
- if (this.options.atomic && DOT_RE.test(path22))
39039
+ _isIgnored(path23, stats) {
39040
+ if (this.options.atomic && DOT_RE.test(path23))
38631
39041
  return true;
38632
39042
  if (!this._userIgnored) {
38633
39043
  const { cwd } = this.options;
@@ -38637,17 +39047,17 @@ var FSWatcher = class extends EventEmitter {
38637
39047
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
38638
39048
  this._userIgnored = anymatch(list, void 0);
38639
39049
  }
38640
- return this._userIgnored(path22, stats);
39050
+ return this._userIgnored(path23, stats);
38641
39051
  }
38642
- _isntIgnored(path22, stat4) {
38643
- return !this._isIgnored(path22, stat4);
39052
+ _isntIgnored(path23, stat4) {
39053
+ return !this._isIgnored(path23, stat4);
38644
39054
  }
38645
39055
  /**
38646
39056
  * Provides a set of common helpers and properties relating to symlink handling.
38647
39057
  * @param path file or directory pattern being watched
38648
39058
  */
38649
- _getWatchHelpers(path22) {
38650
- return new WatchHelper(path22, this.options.followSymlinks, this);
39059
+ _getWatchHelpers(path23) {
39060
+ return new WatchHelper(path23, this.options.followSymlinks, this);
38651
39061
  }
38652
39062
  // Directory helpers
38653
39063
  // -----------------
@@ -38679,63 +39089,63 @@ var FSWatcher = class extends EventEmitter {
38679
39089
  * @param item base path of item/directory
38680
39090
  */
38681
39091
  _remove(directory, item, isDirectory) {
38682
- const path22 = sysPath2.join(directory, item);
38683
- const fullPath = sysPath2.resolve(path22);
38684
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path22) || this._watched.has(fullPath);
38685
- if (!this._throttle("remove", path22, 100))
39092
+ const path23 = sysPath2.join(directory, item);
39093
+ const fullPath = sysPath2.resolve(path23);
39094
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path23) || this._watched.has(fullPath);
39095
+ if (!this._throttle("remove", path23, 100))
38686
39096
  return;
38687
39097
  if (!isDirectory && this._watched.size === 1) {
38688
39098
  this.add(directory, item, true);
38689
39099
  }
38690
- const wp = this._getWatchedDir(path22);
39100
+ const wp = this._getWatchedDir(path23);
38691
39101
  const nestedDirectoryChildren = wp.getChildren();
38692
- nestedDirectoryChildren.forEach((nested) => this._remove(path22, nested));
39102
+ nestedDirectoryChildren.forEach((nested) => this._remove(path23, nested));
38693
39103
  const parent = this._getWatchedDir(directory);
38694
39104
  const wasTracked = parent.has(item);
38695
39105
  parent.remove(item);
38696
39106
  if (this._symlinkPaths.has(fullPath)) {
38697
39107
  this._symlinkPaths.delete(fullPath);
38698
39108
  }
38699
- let relPath = path22;
39109
+ let relPath = path23;
38700
39110
  if (this.options.cwd)
38701
- relPath = sysPath2.relative(this.options.cwd, path22);
39111
+ relPath = sysPath2.relative(this.options.cwd, path23);
38702
39112
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
38703
39113
  const event = this._pendingWrites.get(relPath).cancelWait();
38704
39114
  if (event === EVENTS.ADD)
38705
39115
  return;
38706
39116
  }
38707
- this._watched.delete(path22);
39117
+ this._watched.delete(path23);
38708
39118
  this._watched.delete(fullPath);
38709
39119
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
38710
- if (wasTracked && !this._isIgnored(path22))
38711
- this._emit(eventName, path22);
38712
- this._closePath(path22);
39120
+ if (wasTracked && !this._isIgnored(path23))
39121
+ this._emit(eventName, path23);
39122
+ this._closePath(path23);
38713
39123
  }
38714
39124
  /**
38715
39125
  * Closes all watchers for a path
38716
39126
  */
38717
- _closePath(path22) {
38718
- this._closeFile(path22);
38719
- const dir = sysPath2.dirname(path22);
38720
- this._getWatchedDir(dir).remove(sysPath2.basename(path22));
39127
+ _closePath(path23) {
39128
+ this._closeFile(path23);
39129
+ const dir = sysPath2.dirname(path23);
39130
+ this._getWatchedDir(dir).remove(sysPath2.basename(path23));
38721
39131
  }
38722
39132
  /**
38723
39133
  * Closes only file-specific watchers
38724
39134
  */
38725
- _closeFile(path22) {
38726
- const closers = this._closers.get(path22);
39135
+ _closeFile(path23) {
39136
+ const closers = this._closers.get(path23);
38727
39137
  if (!closers)
38728
39138
  return;
38729
39139
  closers.forEach((closer) => closer());
38730
- this._closers.delete(path22);
39140
+ this._closers.delete(path23);
38731
39141
  }
38732
- _addPathCloser(path22, closer) {
39142
+ _addPathCloser(path23, closer) {
38733
39143
  if (!closer)
38734
39144
  return;
38735
- let list = this._closers.get(path22);
39145
+ let list = this._closers.get(path23);
38736
39146
  if (!list) {
38737
39147
  list = [];
38738
- this._closers.set(path22, list);
39148
+ this._closers.set(path23, list);
38739
39149
  }
38740
39150
  list.push(closer);
38741
39151
  }
@@ -38765,7 +39175,7 @@ function watch(paths, options = {}) {
38765
39175
  var esm_default13 = { watch, FSWatcher };
38766
39176
 
38767
39177
  // src/lib/watcher.ts
38768
- import path13 from "node:path";
39178
+ import path14 from "node:path";
38769
39179
  function startWatcher(options) {
38770
39180
  const { dir, extensions, onChange } = options;
38771
39181
  const extSet = new Set(extensions);
@@ -38779,7 +39189,7 @@ function startWatcher(options) {
38779
39189
  });
38780
39190
  let debounceTimer = null;
38781
39191
  const handleChange2 = (filePath) => {
38782
- const ext = path13.extname(filePath);
39192
+ const ext = path14.extname(filePath);
38783
39193
  if (!extSet.has(ext)) return;
38784
39194
  if (debounceTimer) clearTimeout(debounceTimer);
38785
39195
  debounceTimer = setTimeout(() => {
@@ -38803,31 +39213,79 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
38803
39213
 
38804
39214
  // src/lib/wsServer.ts
38805
39215
  import http from "node:http";
39216
+ import net from "node:net";
38806
39217
  var PORT_START = 4400;
38807
39218
  var PORT_END = 4409;
39219
+ var DEFAULT_HOST = "127.0.0.1";
39220
+ var PING_INTERVAL_MS = 3e4;
39221
+ var PROBE_TIMEOUT_MS = 300;
39222
+ var LOOPBACK_PROBES = ["127.0.0.1", "::1"];
39223
+ function portsBusyError(start, end) {
39224
+ return new CliError("cli_failed", `Ports ${start} to ${end} are all busy.`, [
39225
+ "Another `avsb dev` (or another program) is holding them. Stop it, then run this again.",
39226
+ `\`lsof -i :${start}\` shows what is listening on a port.`
39227
+ ]);
39228
+ }
39229
+ function errorCode(err) {
39230
+ return typeof err === "object" && err !== null && "code" in err ? err.code : void 0;
39231
+ }
39232
+ function isAddressInUse(err) {
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);
39271
+ }
38808
39272
  async function startDevServer(options) {
38809
39273
  const { experimentName, experimentId, onExtensionMessage } = options;
38810
- let resolvedPort = 0;
38811
- let httpServer = null;
38812
- let wss = null;
38813
- for (let port = PORT_START; port <= PORT_END; port++) {
39274
+ const start = options.portRange?.start ?? PORT_START;
39275
+ const end = options.portRange?.end ?? PORT_END;
39276
+ const host = options.host ?? DEFAULT_HOST;
39277
+ let bound = null;
39278
+ for (let port2 = start; port2 <= end && !bound; port2++) {
39279
+ if (await isPortAnsweredOnLoopback(port2)) continue;
38814
39280
  try {
38815
- const result = await tryPort(port, experimentName, experimentId, onExtensionMessage);
38816
- httpServer = result.httpServer;
38817
- wss = result.wss;
38818
- resolvedPort = port;
38819
- break;
38820
- } catch {
38821
- continue;
39281
+ bound = { ...await tryPort(port2, host, experimentName, experimentId, onExtensionMessage), port: port2 };
39282
+ } catch (err) {
39283
+ if (BAD_HOST_CODES.has(String(errorCode(err)))) throw badHostError(host);
39284
+ if (!isAddressInUse(err)) throw err;
38822
39285
  }
38823
39286
  }
38824
- if (!httpServer || !wss) {
38825
- throw new Error(`Could not find available port in range ${PORT_START}-${PORT_END}`);
38826
- }
38827
- const pingInterval = setInterval(() => {
38828
- const msg = { type: "ping", timestamp: Date.now() };
38829
- broadcast(JSON.stringify(msg));
38830
- }, 3e4);
39287
+ if (!bound) throw portsBusyError(start, end);
39288
+ const { httpServer, wss, port } = bound;
38831
39289
  function broadcast(data) {
38832
39290
  wss.clients.forEach((client) => {
38833
39291
  if (client.readyState === import_websocket.default.OPEN) {
@@ -38835,8 +39293,14 @@ async function startDevServer(options) {
38835
39293
  }
38836
39294
  });
38837
39295
  }
39296
+ const pingInterval = setInterval(() => {
39297
+ const msg = { type: "ping", timestamp: Date.now() };
39298
+ broadcast(JSON.stringify(msg));
39299
+ }, PING_INTERVAL_MS);
38838
39300
  return {
38839
- port: resolvedPort,
39301
+ port,
39302
+ host,
39303
+ url: devServerUrl(host, port),
38840
39304
  broadcast(msg) {
38841
39305
  broadcast(JSON.stringify(msg));
38842
39306
  },
@@ -38860,7 +39324,7 @@ async function startDevServer(options) {
38860
39324
  }
38861
39325
  };
38862
39326
  }
38863
- function tryPort(port, experimentName, experimentId, onExtensionMessage) {
39327
+ function tryPort(port, host, experimentName, experimentId, onExtensionMessage) {
38864
39328
  return new Promise((resolve3, reject) => {
38865
39329
  const httpServer = http.createServer((req, res) => {
38866
39330
  if (req.url === "/health") {
@@ -38879,6 +39343,16 @@ function tryPort(port, experimentName, experimentId, onExtensionMessage) {
38879
39343
  );
38880
39344
  });
38881
39345
  const wss = new import_websocket_server.default({ server: httpServer });
39346
+ let settled = false;
39347
+ const fail2 = (err) => {
39348
+ if (settled) return;
39349
+ settled = true;
39350
+ wss.close();
39351
+ httpServer.close();
39352
+ reject(err);
39353
+ };
39354
+ httpServer.on("error", fail2);
39355
+ wss.on("error", fail2);
38882
39356
  wss.on("connection", (ws) => {
38883
39357
  ws.on("message", (raw) => {
38884
39358
  try {
@@ -38888,13 +39362,16 @@ function tryPort(port, experimentName, experimentId, onExtensionMessage) {
38888
39362
  }
38889
39363
  });
38890
39364
  });
38891
- httpServer.on("error", reject);
38892
- httpServer.listen(port, () => resolve3({ httpServer, wss }));
39365
+ httpServer.listen(port, host, () => {
39366
+ if (settled) return;
39367
+ settled = true;
39368
+ resolve3({ httpServer, wss });
39369
+ });
38893
39370
  });
38894
39371
  }
38895
39372
 
38896
39373
  // src/commands/dev.ts
38897
- 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) => {
38898
39375
  try {
38899
39376
  ensureLoggedIn();
38900
39377
  const { manifest, dir } = readManifestOrExit();
@@ -38912,6 +39389,7 @@ var devCommand = new Command("dev").description("Start dev server with live relo
38912
39389
  const server = await startDevServer({
38913
39390
  experimentName: manifest.experimentName,
38914
39391
  experimentId: manifest.experimentId,
39392
+ host: opts.host,
38915
39393
  onExtensionMessage(msg, ws) {
38916
39394
  if (msg.type === "ext:connect") {
38917
39395
  const initMsg = {
@@ -38938,14 +39416,16 @@ var devCommand = new Command("dev").description("Start dev server with live relo
38938
39416
  }
38939
39417
  });
38940
39418
  line();
38941
- success(`Dev server running on port ${server.port}`);
39419
+ const listening = describeDevServer(server);
39420
+ success(listening.line);
39421
+ if (listening.warning) warn(listening.warning);
38942
39422
  info("Watching for changes...");
38943
39423
  line();
38944
39424
  const watcher = startWatcher({
38945
39425
  dir,
38946
39426
  extensions: [".ts", ".js", ".scss", ".css"],
38947
39427
  onChange: async (filePath) => {
38948
- const relativePath = path14.relative(dir, filePath);
39428
+ const relativePath = path15.relative(dir, filePath);
38949
39429
  info(`File changed: ${relativePath}`);
38950
39430
  try {
38951
39431
  compiled = await compileExperiment(dir, manifest);
@@ -39061,15 +39541,15 @@ var statusCommand = withOutputOptions(
39061
39541
  heading(manifest.experimentName);
39062
39542
  line();
39063
39543
  for (const row of buildStatusRows(manifest)) {
39064
- label(row.label, row.value);
39544
+ field(row.label, row.value);
39065
39545
  }
39066
39546
  line();
39067
39547
  });
39068
39548
 
39069
39549
  // src/commands/project.ts
39070
39550
  import { createHash as createHash4 } from "node:crypto";
39071
- import fs11 from "node:fs";
39072
- import path15 from "node:path";
39551
+ import fs12 from "node:fs";
39552
+ import path16 from "node:path";
39073
39553
  function sha2564(content) {
39074
39554
  return createHash4("sha256").update(content).digest("hex");
39075
39555
  }
@@ -39100,16 +39580,29 @@ var projectPullCommand = new Command("pull").description("Pull project script fr
39100
39580
  label("ID", `PRJ-${project.shortId}`);
39101
39581
  label("Language", project.projectJsLang === "TYPESCRIPT" ? "TypeScript" : "JavaScript");
39102
39582
  line();
39103
- const folderName = `project-${project.shortId}`;
39104
- const dir = path15.join(process.cwd(), folderName);
39105
- if (fs11.existsSync(dir)) {
39583
+ const target = resolveProjectPullTarget(process.cwd(), project.shortId);
39584
+ const dir = target.dir;
39585
+ if (target.kind === "blocked") {
39586
+ failCommand(
39587
+ new CliError("cli_usage", `${dir} already exists and is not a pulled copy of PRJ-${project.shortId}.`, [
39588
+ "Move or rename it, or run `avsb project pull` from another folder."
39589
+ ])
39590
+ );
39591
+ }
39592
+ if (target.kind === "existing") {
39106
39593
  const { manifest } = readProjectManifestOrExit(dir);
39107
39594
  if (!options.force) {
39108
39595
  const scriptFile = `project.${manifest.language}`;
39109
- const scriptPath = path15.join(dir, scriptFile);
39110
- const localContent = fs11.existsSync(scriptPath) ? fs11.readFileSync(scriptPath, "utf-8") : "";
39596
+ const scriptPath = path16.join(dir, scriptFile);
39597
+ const localContent = fs12.existsSync(scriptPath) ? fs12.readFileSync(scriptPath, "utf-8") : "";
39111
39598
  const remoteContent = manifest.language === "ts" ? project.editorProjectTs ?? project.editorProjectJs ?? "" : project.editorProjectJs ?? "";
39112
39599
  if (localContent !== remoteContent && localContent !== "") {
39600
+ const refusal = refusalWithoutPrompt({
39601
+ code: "cli_conflict",
39602
+ message: `Pulling would overwrite your local ${scriptFile}, and this run cannot ask first.`,
39603
+ hints: [`Run \`avsb project pull ${projectInput} --force\` to overwrite it.`]
39604
+ });
39605
+ if (refusal) failCommand(refusal);
39113
39606
  const { proceed } = await esm_default12.prompt([
39114
39607
  {
39115
39608
  type: "confirm",
@@ -39127,7 +39620,7 @@ var projectPullCommand = new Command("pull").description("Pull project script fr
39127
39620
  const updateSpinner = ora("Updating files...").start();
39128
39621
  const updated = updateProjectFile(dir, manifest, response.data);
39129
39622
  if (updated.length > 0) {
39130
- updateSpinner.succeed("Files updated");
39623
+ updateSpinner.succeed(`Files updated in ${path16.relative(process.cwd(), dir) || "this folder"}`);
39131
39624
  fileTree(updated);
39132
39625
  } else {
39133
39626
  updateSpinner.succeed("Already up to date");
@@ -39135,12 +39628,12 @@ var projectPullCommand = new Command("pull").description("Pull project script fr
39135
39628
  } else {
39136
39629
  const scaffoldSpinner = ora("Scaffolding project files...").start();
39137
39630
  try {
39138
- const result = scaffoldProject({ data: response.data, apiBaseUrl: config.apiBaseUrl });
39631
+ const result = scaffoldProject({ data: response.data, apiBaseUrl: config.apiBaseUrl, targetDir: dir });
39139
39632
  scaffoldSpinner.succeed("Files created");
39140
39633
  line();
39141
39634
  fileTree(result.files);
39142
39635
  line();
39143
- success(`Project cloned to ${folderName}/`);
39636
+ success(`Project cloned to ${path16.relative(process.cwd(), dir) || "."}/`);
39144
39637
  } catch (err) {
39145
39638
  scaffoldSpinner.fail("Scaffolding failed");
39146
39639
  failCommand(err);
@@ -39156,11 +39649,11 @@ var projectPushCommand = new Command("push").description("Compile and push proje
39156
39649
  const { manifest, dir } = readProjectManifestOrExit();
39157
39650
  const api = new ApiClient(config);
39158
39651
  const scriptFile = `project.${manifest.language}`;
39159
- const scriptPath = path15.join(dir, scriptFile);
39160
- if (!fs11.existsSync(scriptPath)) {
39652
+ const scriptPath = path16.join(dir, scriptFile);
39653
+ if (!fs12.existsSync(scriptPath)) {
39161
39654
  failCommand(new CliError("cli_no_manifest", `File not found: ${scriptFile}`));
39162
39655
  }
39163
- const source = fs11.readFileSync(scriptPath, "utf-8");
39656
+ const source = fs12.readFileSync(scriptPath, "utf-8");
39164
39657
  const projLang = manifest.language === "ts" ? "TYPESCRIPT" : "JAVASCRIPT";
39165
39658
  const compileSpinner = ora(`Compiling ${scriptFile}...`).start();
39166
39659
  const compileResult = await compileScript(source, projLang, scriptFile, { minify: true });
@@ -39182,6 +39675,18 @@ var projectPushCommand = new Command("push").description("Compile and push proje
39182
39675
  if (remoteHash !== manifest.syncedHash) {
39183
39676
  spinner.warn("Remote has changes you haven't pulled");
39184
39677
  line();
39678
+ if (!isInteractiveRun()) {
39679
+ failCommand(
39680
+ new CliError(
39681
+ "cli_conflict",
39682
+ "The platform copy changed since your last pull, and this run cannot ask before overwriting it.",
39683
+ [
39684
+ "Run `avsb project pull --force` to fetch the remote script first (it overwrites your local file),",
39685
+ "or `avsb project push --force` to overwrite the remote changes."
39686
+ ]
39687
+ )
39688
+ );
39689
+ }
39185
39690
  const { proceed } = await esm_default12.prompt([
39186
39691
  {
39187
39692
  type: "confirm",
@@ -39200,27 +39705,33 @@ var projectPushCommand = new Command("push").description("Compile and push proje
39200
39705
  }
39201
39706
  }
39202
39707
  const payload = manifest.language === "ts" ? { editorProjectTs: { source, compiled: compiledSource } } : { editorProjectJs: { source, compiled: compiledSource } };
39203
- await api.pushProject(manifest.projectShortId, payload);
39708
+ try {
39709
+ await api.pushProject(manifest.projectShortId, payload);
39710
+ } catch (err) {
39711
+ spinner.fail("Push refused");
39712
+ failCommand(err, { labelForPath: (bodyPath) => bodyPath.length > 0 ? scriptFile : void 0 });
39713
+ }
39204
39714
  manifest.syncedHash = sha2564(source);
39205
39715
  manifest.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
39206
39716
  writeProjectManifest(dir, manifest);
39207
39717
  spinner.succeed("Pushed successfully");
39208
39718
  line();
39719
+ info("Saved as unpublished changes. Publish the script from the project's Snippet settings to put it live.");
39209
39720
  success("Push complete");
39210
39721
  } catch (err) {
39211
39722
  failCommand(err);
39212
39723
  }
39213
39724
  });
39214
- 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) => {
39215
39726
  try {
39216
39727
  ensureLoggedIn();
39217
39728
  const { manifest, dir } = readProjectManifestOrExit();
39218
39729
  const scriptFile = `project.${manifest.language}`;
39219
- const scriptPath = path15.join(dir, scriptFile);
39730
+ const scriptPath = path16.join(dir, scriptFile);
39220
39731
  heading(`Dev mode: ${manifest.projectName} (project script)`);
39221
39732
  line();
39222
39733
  let compiledJs;
39223
- const source = fs11.readFileSync(scriptPath, "utf-8");
39734
+ const source = fs12.readFileSync(scriptPath, "utf-8");
39224
39735
  if (manifest.language === "ts") {
39225
39736
  const spinner = ora("Compiling...").start();
39226
39737
  try {
@@ -39237,6 +39748,7 @@ var projectDevCommand = new Command("dev").description("Watch project script and
39237
39748
  const server = await startDevServer({
39238
39749
  experimentName: `${manifest.projectName} (project script)`,
39239
39750
  experimentId: manifest.projectId,
39751
+ host: opts.host,
39240
39752
  onExtensionMessage(msg, ws) {
39241
39753
  if (msg.type === "ext:connect") {
39242
39754
  const initMsg = {
@@ -39252,17 +39764,19 @@ var projectDevCommand = new Command("dev").description("Watch project script and
39252
39764
  }
39253
39765
  });
39254
39766
  line();
39255
- success(`Dev server running on port ${server.port}`);
39767
+ const listening = describeDevServer(server);
39768
+ success(listening.line);
39769
+ if (listening.warning) warn(listening.warning);
39256
39770
  info("Watching for changes...");
39257
39771
  line();
39258
39772
  const watcher = startWatcher({
39259
39773
  dir,
39260
39774
  extensions: [".ts", ".js"],
39261
39775
  onChange: async (filePath) => {
39262
- const relativePath = path15.relative(dir, filePath);
39776
+ const relativePath = path16.relative(dir, filePath);
39263
39777
  info(`File changed: ${relativePath}`);
39264
39778
  try {
39265
- const newSource = fs11.readFileSync(filePath, "utf-8");
39779
+ const newSource = fs12.readFileSync(filePath, "utf-8");
39266
39780
  compiledJs = manifest.language === "ts" ? await compileTypeScript(newSource) : newSource;
39267
39781
  const updateMsg = {
39268
39782
  type: "project:update",
@@ -39508,6 +40022,72 @@ function isConflictError(err) {
39508
40022
  if (!(err instanceof Error)) return false;
39509
40023
  return /409|conflict|referenc/i.test(err.message);
39510
40024
  }
40025
+ function isRecord5(value) {
40026
+ return typeof value === "object" && value !== null && !Array.isArray(value);
40027
+ }
40028
+ function isCliMetric(value) {
40029
+ return isRecord5(value) && typeof value.id === "string" && typeof value.shortId === "number" && typeof value.name === "string" && typeof value.type === "string";
40030
+ }
40031
+ function readMetricList(data) {
40032
+ if (!isRecord5(data) || !Array.isArray(data.metrics)) return null;
40033
+ const metrics = data.metrics.filter(isCliMetric);
40034
+ const usage = isRecord5(data.usage) ? data.usage : {};
40035
+ return { metrics, usage };
40036
+ }
40037
+ function readMetric(data) {
40038
+ return isRecord5(data) && isCliMetric(data.metric) ? data.metric : null;
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
+ }
40055
+ function formatMetricTable(metrics, usage) {
40056
+ const rows = metrics.map((metric) => ({
40057
+ id: metricIdLabel(metric),
40058
+ type: metric.type,
40059
+ name: truncate(metric.name, 32),
40060
+ definition: truncate(describeMetric(metric), 48),
40061
+ usage: formatUsage(usage[metric.id]),
40062
+ archived: metric.archivedAt != null
40063
+ }));
40064
+ const widths = {
40065
+ id: Math.max(2, ...rows.map((r) => r.id.length)),
40066
+ type: Math.max(4, ...rows.map((r) => r.type.length)),
40067
+ name: Math.max(4, ...rows.map((r) => r.name.length)),
40068
+ definition: Math.max(10, ...rows.map((r) => r.definition.length))
40069
+ };
40070
+ const heading2 = [
40071
+ "ID".padEnd(widths.id),
40072
+ "Type".padEnd(widths.type),
40073
+ "Name".padEnd(widths.name),
40074
+ "Definition".padEnd(widths.definition),
40075
+ "Usage"
40076
+ ].join(" ");
40077
+ return {
40078
+ heading: heading2,
40079
+ rows: rows.map((row) => {
40080
+ const line2 = [
40081
+ row.id.padEnd(widths.id),
40082
+ row.type.padEnd(widths.type),
40083
+ row.name.padEnd(widths.name),
40084
+ row.definition.padEnd(widths.definition),
40085
+ row.usage
40086
+ ].join(" ");
40087
+ return row.archived ? `${line2} (archived)` : line2;
40088
+ })
40089
+ };
40090
+ }
39511
40091
 
39512
40092
  // src/commands/metrics.ts
39513
40093
  var metricsListCommand = withOutputOptions(
@@ -39527,49 +40107,26 @@ var metricsListCommand = withOutputOptions(
39527
40107
  spinner.fail("Failed to fetch metrics");
39528
40108
  failCommand(err);
39529
40109
  }
39530
- const { metrics, usage } = response.data;
39531
40110
  if (emitJsonResult(response.data)) return;
39532
- if (metrics.length === 0) {
40111
+ const list = readMetricList(response.data);
40112
+ if (!list) {
40113
+ failCommand(
40114
+ new CliError("cli_failed", "The platform answered in a shape this CLI does not read.", [
40115
+ "Upgrade the CLI with `npm install -g @avsbhq/cli`, or pass --json to see the raw answer."
40116
+ ])
40117
+ );
40118
+ }
40119
+ if (list.metrics.length === 0) {
39533
40120
  line();
39534
40121
  info(
39535
40122
  'No metrics yet. Create one with `avsb metrics create --type click --name "..." --selector ".cta"`'
39536
40123
  );
39537
40124
  return;
39538
40125
  }
39539
- const rows = metrics.map((metric) => ({
39540
- id: metricIdLabel(metric),
39541
- type: metric.type,
39542
- name: truncate(metric.name, 32),
39543
- definition: truncate(describeMetric(metric), 48),
39544
- usage: formatUsage(usage[metric.id]),
39545
- archived: metric.archivedAt != null
39546
- }));
39547
- const widths = {
39548
- id: Math.max(2, ...rows.map((r) => r.id.length)),
39549
- type: Math.max(4, ...rows.map((r) => r.type.length)),
39550
- name: Math.max(4, ...rows.map((r) => r.name.length)),
39551
- definition: Math.max(10, ...rows.map((r) => r.definition.length))
39552
- };
40126
+ const table = formatMetricTable(list.metrics, list.usage);
39553
40127
  line();
39554
- heading(
39555
- [
39556
- "ID".padEnd(widths.id),
39557
- "Type".padEnd(widths.type),
39558
- "Name".padEnd(widths.name),
39559
- "Definition".padEnd(widths.definition),
39560
- "Usage"
39561
- ].join(" ")
39562
- );
39563
- for (const row of rows) {
39564
- const line2 = [
39565
- row.id.padEnd(widths.id),
39566
- row.type.padEnd(widths.type),
39567
- row.name.padEnd(widths.name),
39568
- row.definition.padEnd(widths.definition),
39569
- row.usage
39570
- ].join(" ");
39571
- line(row.archived ? `${line2} (archived)` : line2);
39572
- }
40128
+ heading(table.heading);
40129
+ for (const row of table.rows) line(row);
39573
40130
  line();
39574
40131
  });
39575
40132
  var metricsCreateCommand = new Command("create").description("Create a new metric").option("--project <project>", "Project ID (PRJ-42 or numeric); defaults to local manifest").option("--type <type>", "Metric type (CLICK | PAGEVIEW | CUSTOM)").option("--name <name>", "Display name").option("--description <text>", "Optional description").option("--selector <selector>", "CSS selector (CLICK only)").option("--page-url <url>", "Page URL filter (CLICK only)").option("--url-match <pattern>", "URL match pattern (PAGEVIEW only)").option("--match-mode <mode>", "URL match mode for PAGEVIEW (SIMPLE | EXACT | PATH | SUBSTRING | PATTERN | REGEX)").option("--key <key>", "Metric key (CUSTOM only)").option("--has-value", "Toggle value-field tracking on a CUSTOM metric").action(async (opts) => {
@@ -39585,21 +40142,20 @@ var metricsCreateCommand = new Command("create").description("Create a new metri
39585
40142
  const body = buildCreateBody(type, opts, opts.name);
39586
40143
  const api = new ApiClient(config);
39587
40144
  const spinner = ora("Creating metric...").start();
40145
+ let response;
39588
40146
  try {
39589
- const response = await api.createMetric(shortId, body);
39590
- const { metric } = response.data;
39591
- spinner.succeed(`Metric created: ${metric.name} (${metricIdLabel(metric)})`);
39592
- printMetricDetails(metric);
40147
+ response = await api.createMetric(shortId, body);
39593
40148
  } catch (err) {
39594
40149
  spinner.fail("Failed to create metric");
39595
- const message = err instanceof Error ? err.message : "Unknown error";
39596
- if (/already exists|conflict|409|unique/i.test(message)) {
39597
- error(`Conflict: a metric with that name or key already exists. ${message}`);
39598
- } else {
39599
- error(message);
39600
- }
39601
- process.exit(EXIT.failure);
40150
+ failCommand(err);
39602
40151
  }
40152
+ const metric = readMetric(response.data);
40153
+ if (!metric) {
40154
+ spinner.warn("Metric created. Run `avsb metrics list` to see it.");
40155
+ return;
40156
+ }
40157
+ spinner.succeed(`Metric created: ${metric.name} (${metricIdLabel(metric)})`);
40158
+ printMetricDetails(metric);
39603
40159
  });
39604
40160
  var metricsUpdateCommand = new Command("update").argument("<metricId>", "Metric ID (cuid or numeric short ID)").description("Update a metric").option("--project <project>", "Project ID (PRJ-42 or numeric); defaults to local manifest").option("--name <name>", "New display name").option("--description <text>", "New description").option("--selector <selector>", "New CSS selector (CLICK)").option("--page-url <url>", "New page URL filter (CLICK)").option("--url-match <pattern>", "New URL match pattern (PAGEVIEW)").option("--match-mode <mode>", "New URL match mode (PAGEVIEW)").option("--key <key>", "New metric key (CUSTOM)").option("--has-value", "Enable value-field tracking (CUSTOM)").option("--no-has-value", "Disable value-field tracking (CUSTOM)").action(async (metricId, opts) => {
39605
40161
  const config = ensureLoggedIn();
@@ -39615,49 +40171,67 @@ var metricsUpdateCommand = new Command("update").argument("<metricId>", "Metric
39615
40171
  }
39616
40172
  const api = new ApiClient(config);
39617
40173
  const spinner = ora("Updating metric...").start();
40174
+ let response;
39618
40175
  try {
39619
- const response = await api.updateMetric(shortId, metricId, patch);
39620
- const { metric } = response.data;
39621
- spinner.succeed(`Metric updated: ${metric.name} (${metricIdLabel(metric)})`);
39622
- printMetricDetails(metric);
40176
+ response = await api.updateMetric(shortId, metricId, patch);
39623
40177
  } catch (err) {
39624
40178
  spinner.fail("Failed to update metric");
39625
40179
  failCommand(err);
39626
40180
  }
40181
+ const metric = readMetric(response.data);
40182
+ if (!metric) {
40183
+ spinner.warn(`Metric ${metricId} updated.`);
40184
+ return;
40185
+ }
40186
+ spinner.succeed(`Metric updated: ${metric.name} (${metricIdLabel(metric)})`);
40187
+ printMetricDetails(metric);
39627
40188
  });
39628
40189
  var metricsArchiveCommand = new Command("archive").argument("<metricId>", "Metric ID").description("Archive a metric (soft-delete, preserves history)").option("--project <project>", "Project ID (PRJ-42 or numeric); defaults to local manifest").action(async (metricId, opts) => {
39629
40190
  const config = ensureLoggedIn();
39630
40191
  const shortId = resolveProjectShortId(opts);
39631
40192
  const api = new ApiClient(config);
39632
40193
  const spinner = ora("Archiving metric...").start();
40194
+ let response;
39633
40195
  try {
39634
- const response = await api.archiveMetric(shortId, metricId);
39635
- const { metric } = response.data;
39636
- spinner.succeed(`Metric archived: ${metric.name} (${metricIdLabel(metric)})`);
40196
+ response = await api.archiveMetric(shortId, metricId);
39637
40197
  } catch (err) {
39638
40198
  spinner.fail("Failed to archive metric");
39639
40199
  failCommand(err);
39640
40200
  }
40201
+ const metric = readMetric(response.data);
40202
+ spinner.succeed(metric ? `Metric archived: ${metric.name} (${metricIdLabel(metric)})` : `Metric ${metricId} archived`);
40203
+ const cascade = describeArchiveCascade(response.data);
40204
+ if (cascade) info(cascade);
39641
40205
  });
39642
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) => {
39643
40207
  const config = ensureLoggedIn();
39644
40208
  const shortId = resolveProjectShortId(opts);
39645
40209
  const api = new ApiClient(config);
39646
40210
  const spinner = ora("Restoring metric...").start();
40211
+ let response;
39647
40212
  try {
39648
- const response = await api.unarchiveMetric(shortId, metricId);
39649
- const { metric } = response.data;
39650
- spinner.succeed(`Metric restored from archive: ${metric.name} (${metricIdLabel(metric)})`);
40213
+ response = await api.unarchiveMetric(shortId, metricId);
39651
40214
  } catch (err) {
39652
40215
  spinner.fail("Failed to unarchive metric");
39653
40216
  failCommand(err);
39654
40217
  }
40218
+ const metric = readMetric(response.data);
40219
+ spinner.succeed(
40220
+ metric ? `Metric restored from archive: ${metric.name} (${metricIdLabel(metric)})` : `Metric ${metricId} restored from archive`
40221
+ );
39655
40222
  });
39656
40223
  var metricsDeleteCommand = new Command("delete").argument("<metricId>", "Metric ID").description("Delete a metric (rejected if metric bindings reference it)").option("--project <project>", "Project ID (PRJ-42 or numeric); defaults to local manifest").option("-f, --force", "Skip confirmation prompt").action(async (metricId, opts) => {
39657
40224
  const config = ensureLoggedIn();
39658
40225
  const shortId = resolveProjectShortId(opts);
39659
40226
  const api = new ApiClient(config);
39660
40227
  if (!opts.force) {
40228
+ if (!isInteractiveRun()) {
40229
+ failCommand(
40230
+ new CliError("cli_usage", `Deleting metric ${metricId} needs a confirmation this run cannot ask for.`, [
40231
+ `Pass -f to delete without asking: \`avsb metrics delete ${metricId} -f\``
40232
+ ])
40233
+ );
40234
+ }
39661
40235
  const { proceed } = await esm_default12.prompt([
39662
40236
  {
39663
40237
  type: "confirm",
@@ -39677,19 +40251,12 @@ var metricsDeleteCommand = new Command("delete").argument("<metricId>", "Metric
39677
40251
  spinner.succeed(`Metric deleted: ${metricId}`);
39678
40252
  } catch (err) {
39679
40253
  spinner.fail("Failed to delete metric");
39680
- const message = err instanceof Error ? err.message : "Unknown error";
39681
40254
  if (isConflictError(err)) {
39682
40255
  const refs = extractBindingsReferencing(err);
39683
- if (refs.length > 0) {
39684
- error(`Cannot delete: referenced by ${refs.join(", ")}.`);
39685
- } else {
39686
- error(`Cannot delete: metric bindings still reference this metric. ${message}`);
39687
- }
40256
+ if (refs.length > 0) error(`Referenced by ${refs.join(", ")}.`);
39688
40257
  info(`Tip: run \`avsb metrics archive ${metricId}\` instead to hide it without losing history.`);
39689
- } else {
39690
- error(message);
39691
40258
  }
39692
- process.exit(EXIT.failure);
40259
+ failCommand(err);
39693
40260
  }
39694
40261
  });
39695
40262
  var metricsCommand = new Command("metrics").description("Manage metric tracking definitions").addCommand(metricsListCommand).addCommand(metricsCreateCommand).addCommand(metricsUpdateCommand).addCommand(metricsArchiveCommand).addCommand(metricsUnarchiveCommand).addCommand(metricsDeleteCommand);
@@ -39904,6 +40471,40 @@ function isConflictError2(err) {
39904
40471
  if (!(err instanceof Error)) return false;
39905
40472
  return /409|conflict|attached|referenc/i.test(err.message);
39906
40473
  }
40474
+ function isRecord6(value) {
40475
+ return typeof value === "object" && value !== null && !Array.isArray(value);
40476
+ }
40477
+ function isCliMetricBinding(value) {
40478
+ return isRecord6(value) && typeof value.id === "string" && typeof value.shortId === "number" && typeof value.name === "string" && typeof value.measure === "string";
40479
+ }
40480
+ function readMetricBindingList(data) {
40481
+ if (!isRecord6(data) || !Array.isArray(data.metricBindings)) return null;
40482
+ return data.metricBindings.filter(isCliMetricBinding);
40483
+ }
40484
+ function readMetricBinding(data) {
40485
+ return isRecord6(data) && isCliMetricBinding(data.metricBinding) ? data.metricBinding : null;
40486
+ }
40487
+ function formatMetricBindingTable(bindings) {
40488
+ const rows = bindings.map((binding) => ({
40489
+ id: metricBindingIdLabel(binding),
40490
+ measure: measureBadge(binding.measure),
40491
+ name: truncate2(binding.name, 28),
40492
+ sentence: truncate2(renderMetricBindingSentence(binding), 64),
40493
+ archived: binding.archivedAt != null
40494
+ }));
40495
+ const widths = {
40496
+ id: Math.max(2, ...rows.map((r) => r.id.length)),
40497
+ measure: Math.max(7, ...rows.map((r) => r.measure.length)),
40498
+ name: Math.max(4, ...rows.map((r) => r.name.length))
40499
+ };
40500
+ return {
40501
+ heading: ["ID".padEnd(widths.id), "Measure".padEnd(widths.measure), "Name".padEnd(widths.name), "Sentence"].join(" "),
40502
+ rows: rows.map((row) => {
40503
+ const line2 = [row.id.padEnd(widths.id), row.measure.padEnd(widths.measure), row.name.padEnd(widths.name), row.sentence].join(" ");
40504
+ return row.archived ? `${line2} (archived)` : line2;
40505
+ })
40506
+ };
40507
+ }
39907
40508
 
39908
40509
  // src/commands/metric-bindings.body.ts
39909
40510
  function parseComposition(opts) {
@@ -39965,12 +40566,11 @@ function parseWinsorization(opts) {
39965
40566
  }
39966
40567
  return { enabled: true, upperPercentile: upper, lowerPercentile: lower };
39967
40568
  }
39968
- function buildCreateMetricBindingBody(projectId, measure, opts, name) {
40569
+ function buildCreateMetricBindingBody(measure, opts, name) {
39969
40570
  const direction = opts.direction ? normalizeDirection(opts.direction) : "INCREASE";
39970
40571
  const body = {
39971
40572
  name,
39972
40573
  description: opts.description ?? void 0,
39973
- projectId,
39974
40574
  measure,
39975
40575
  direction,
39976
40576
  isLibrary: true
@@ -40081,8 +40681,15 @@ var metricBindingsListCommand = withOutputOptions(
40081
40681
  spinner.fail("Failed to fetch metric bindings");
40082
40682
  failCommand(err);
40083
40683
  }
40084
- const { metricBindings } = response.data;
40085
40684
  if (emitJsonResult(response.data)) return;
40685
+ const metricBindings = readMetricBindingList(response.data);
40686
+ if (!metricBindings) {
40687
+ failCommand(
40688
+ new CliError("cli_failed", "The platform answered in a shape this CLI does not read.", [
40689
+ "Upgrade the CLI with `npm install -g @avsbhq/cli`, or pass --json to see the raw answer."
40690
+ ])
40691
+ );
40692
+ }
40086
40693
  if (metricBindings.length === 0) {
40087
40694
  line();
40088
40695
  info(
@@ -40090,45 +40697,19 @@ var metricBindingsListCommand = withOutputOptions(
40090
40697
  );
40091
40698
  return;
40092
40699
  }
40093
- const rows = metricBindings.map((binding) => ({
40094
- id: metricBindingIdLabel(binding),
40095
- measure: measureBadge(binding.measure),
40096
- name: truncate2(binding.name, 28),
40097
- sentence: truncate2(renderMetricBindingSentence(binding), 64),
40098
- archived: binding.archivedAt != null
40099
- }));
40100
- const widths = {
40101
- id: Math.max(2, ...rows.map((r) => r.id.length)),
40102
- measure: Math.max(7, ...rows.map((r) => r.measure.length)),
40103
- name: Math.max(4, ...rows.map((r) => r.name.length))
40104
- };
40700
+ const table = formatMetricBindingTable(metricBindings);
40105
40701
  line();
40106
- heading(
40107
- [
40108
- "ID".padEnd(widths.id),
40109
- "Measure".padEnd(widths.measure),
40110
- "Name".padEnd(widths.name),
40111
- "Sentence"
40112
- ].join(" ")
40113
- );
40114
- for (const row of rows) {
40115
- const line2 = [
40116
- row.id.padEnd(widths.id),
40117
- row.measure.padEnd(widths.measure),
40118
- row.name.padEnd(widths.name),
40119
- row.sentence
40120
- ].join(" ");
40121
- line(row.archived ? `${line2} (archived)` : line2);
40122
- }
40702
+ heading(table.heading);
40703
+ for (const row of table.rows) line(row);
40123
40704
  line();
40124
40705
  }
40125
40706
  );
40126
40707
  var metricBindingsCreateCommand = new Command("create").description("Create a new metric binding").option("--project <project>", "Project ID (PRJ-42 or numeric); defaults to local manifest").option(
40127
40708
  "--measure <measure>",
40128
40709
  "Measure (UNIQUE_CONVERSIONS_PER_VISITOR | TOTAL_EVENTS | UNIQUE_VISITORS_WHO_FIRED | TOTAL_VALUE_PER_VISITOR | TOTAL_VALUE | PERCENTILE | RATE | COMPOSITE)"
40129
- ).option("--direction <direction>", "Direction (INCREASE | DECREASE)", "INCREASE").option("--name <name>", "Display name").option("--description <text>", "Optional description").option("--metric <metricId>", "Source metric (required for single-metric measures)").option("--numerator-metric <metricId>", "Numerator metric (required for RATE)").option("--denominator-metric <metricId>", "Denominator metric (required for RATE)").option("--composition <json>", "COMPOSITE composition as JSON array of { metricId,weight}").option(
40130
- "--component <metricId:weight>",
40131
- "Repeatable component for COMPOSITE (e.g. --component b1:0.5 --component b2:0.5)",
40710
+ ).option("--direction <direction>", "Direction (INCREASE | DECREASE)", "INCREASE").option("--name <name>", "Display name").option("--description <text>", "Optional description").option("--metric <metricId>", "Source metric: MET-17, 17 or the long id (required for single-metric measures)").option("--numerator-metric <metricId>", "Numerator metric (required for RATE)").option("--denominator-metric <metricId>", "Denominator metric (required for RATE)").option("--composition <json>", "COMPOSITE components as a JSON array of { metricId, weight }, where metricId names a metric binding").option(
40711
+ "--component <bindingId:weight>",
40712
+ "Repeatable COMPOSITE component, a metric binding and its weight (e.g. --component MBD-8:0.5 --component MBD-9:0.5)",
40132
40713
  (value, prev = []) => prev.concat(value),
40133
40714
  []
40134
40715
  ).option("--percentile <n>", "Percentile (required for PERCENTILE measure, 0 < n < 100)").option("--value-source <name>", "Numeric field on the metric (value/percentile measures)", "value").option("--winsorize-upper <pct>", "Upper winsorization percentile (enables winsorization)").option("--winsorize-lower <pct>", "Lower winsorization percentile (enables winsorization)").action(async (opts) => {
@@ -40142,29 +40723,26 @@ var metricBindingsCreateCommand = new Command("create").description("Create a ne
40142
40723
  const shortId = resolveProjectShortId2(opts);
40143
40724
  const measure = normalizeMeasure(opts.measure);
40144
40725
  const api = new ApiClient(config);
40145
- const projectResponse = await api.getProject(shortId);
40146
- const projectId = projectResponse.data.project.id;
40147
- const body = buildCreateMetricBindingBody(projectId, measure, opts, opts.name);
40726
+ const body = buildCreateMetricBindingBody(measure, opts, opts.name);
40148
40727
  const spinner = ora("Creating metric binding...").start();
40728
+ let response;
40149
40729
  try {
40150
- const response = await api.createMetricBinding(shortId, body);
40151
- const { metricBinding } = response.data;
40152
- spinner.succeed(`Metric binding created: ${metricBinding.name} (${metricBindingIdLabel(metricBinding)})`);
40153
- printMetricBindingDetails(metricBinding);
40730
+ response = await api.createMetricBinding(shortId, body);
40154
40731
  } catch (err) {
40155
40732
  spinner.fail("Failed to create metric binding");
40156
- const message = err instanceof Error ? err.message : "Unknown error";
40157
- if (/already exists|conflict|409|unique/i.test(message)) {
40158
- error(`Conflict: a metric binding with that name already exists. ${message}`);
40159
- } else {
40160
- error(message);
40161
- }
40162
- process.exit(EXIT.failure);
40733
+ failCommand(err);
40734
+ }
40735
+ const metricBinding = readMetricBinding(response.data);
40736
+ if (!metricBinding) {
40737
+ spinner.warn("Metric binding created. Run `avsb metric-bindings list` to see it.");
40738
+ return;
40163
40739
  }
40740
+ spinner.succeed(`Metric binding created: ${metricBinding.name} (${metricBindingIdLabel(metricBinding)})`);
40741
+ printMetricBindingDetails(metricBinding);
40164
40742
  });
40165
40743
  var metricBindingsUpdateCommand = new Command("update").argument("<metricBindingId>", "Metric binding ID (cuid or numeric short ID)").description("Update a metric binding").option("--project <project>", "Project ID (PRJ-42 or numeric); defaults to local manifest").option("--name <name>", "New display name").option("--description <text>", "New description").option("--direction <direction>", "New direction (INCREASE | DECREASE)").option("--metric <metricId>", "New source metric").option("--numerator-metric <metricId>", "New numerator metric (RATE)").option("--denominator-metric <metricId>", "New denominator metric (RATE)").option("--composition <json>", "New COMPOSITE composition as JSON array of { metricId,weight}").option(
40166
- "--component <metricId:weight>",
40167
- "Repeatable component (replaces composition)",
40744
+ "--component <bindingId:weight>",
40745
+ "Repeatable COMPOSITE component, a metric binding and its weight (replaces the composition)",
40168
40746
  (value, prev = []) => prev.concat(value),
40169
40747
  []
40170
40748
  ).option("--percentile <n>", "New percentile (PERCENTILE measure)").option("--value-source <name>", "New value source field name").option("--winsorize-upper <pct>", "New upper winsorization percentile").option("--winsorize-lower <pct>", "New lower winsorization percentile").option("--library", "Mark as a library metric binding").option("--no-library", "Mark as a non-library metric binding").action(async (metricBindingId, opts) => {
@@ -40172,54 +40750,73 @@ var metricBindingsUpdateCommand = new Command("update").argument("<metricBinding
40172
40750
  const shortId = resolveProjectShortId2(opts);
40173
40751
  const patch = buildUpdateMetricBindingPatch(opts);
40174
40752
  if (Object.keys(patch).length === 0) {
40175
- error("No update fields provided. Pass at least one of --name, --description, etc.");
40176
- process.exit(EXIT.failure);
40753
+ failCommand(
40754
+ new CliError("cli_usage", "No update fields provided. Pass at least one of --name, --description, --direction, etc.")
40755
+ );
40177
40756
  }
40178
40757
  const api = new ApiClient(config);
40179
40758
  const spinner = ora("Updating metric binding...").start();
40759
+ let response;
40180
40760
  try {
40181
- const response = await api.updateMetricBinding(shortId, metricBindingId, patch);
40182
- const { metricBinding } = response.data;
40183
- spinner.succeed(`Metric binding updated: ${metricBinding.name} (${metricBindingIdLabel(metricBinding)})`);
40184
- printMetricBindingDetails(metricBinding);
40761
+ response = await api.updateMetricBinding(shortId, metricBindingId, patch);
40185
40762
  } catch (err) {
40186
40763
  spinner.fail("Failed to update metric binding");
40187
40764
  failCommand(err);
40188
40765
  }
40766
+ const metricBinding = readMetricBinding(response.data);
40767
+ if (!metricBinding) {
40768
+ spinner.warn(`Metric binding ${metricBindingId} updated.`);
40769
+ return;
40770
+ }
40771
+ spinner.succeed(`Metric binding updated: ${metricBinding.name} (${metricBindingIdLabel(metricBinding)})`);
40772
+ printMetricBindingDetails(metricBinding);
40189
40773
  });
40190
40774
  var metricBindingsArchiveCommand = new Command("archive").argument("<metricBindingId>", "Metric binding ID").description("Archive a metric binding (soft-delete, preserves history)").option("--project <project>", "Project ID (PRJ-42 or numeric); defaults to local manifest").action(async (metricBindingId, opts) => {
40191
40775
  const config = ensureLoggedIn();
40192
40776
  const shortId = resolveProjectShortId2(opts);
40193
40777
  const api = new ApiClient(config);
40194
40778
  const spinner = ora("Archiving metric binding...").start();
40779
+ let response;
40195
40780
  try {
40196
- const response = await api.archiveMetricBinding(shortId, metricBindingId);
40197
- const { metricBinding } = response.data;
40198
- spinner.succeed(`Metric binding archived: ${metricBinding.name} (${metricBindingIdLabel(metricBinding)})`);
40781
+ response = await api.archiveMetricBinding(shortId, metricBindingId);
40199
40782
  } catch (err) {
40200
40783
  spinner.fail("Failed to archive metric binding");
40201
40784
  failCommand(err);
40202
40785
  }
40786
+ const metricBinding = readMetricBinding(response.data);
40787
+ spinner.succeed(
40788
+ metricBinding ? `Metric binding archived: ${metricBinding.name} (${metricBindingIdLabel(metricBinding)})` : `Metric binding ${metricBindingId} archived`
40789
+ );
40203
40790
  });
40204
40791
  var metricBindingsUnarchiveCommand = new Command("unarchive").argument("<metricBindingId>", "Metric binding ID").description("Restore an archived metric binding").option("--project <project>", "Project ID (PRJ-42 or numeric); defaults to local manifest").action(async (metricBindingId, opts) => {
40205
40792
  const config = ensureLoggedIn();
40206
40793
  const shortId = resolveProjectShortId2(opts);
40207
40794
  const api = new ApiClient(config);
40208
40795
  const spinner = ora("Restoring metric binding...").start();
40796
+ let response;
40209
40797
  try {
40210
- const response = await api.unarchiveMetricBinding(shortId, metricBindingId);
40211
- const { metricBinding } = response.data;
40212
- spinner.succeed(`Metric binding restored from archive: ${metricBinding.name} (${metricBindingIdLabel(metricBinding)})`);
40798
+ response = await api.unarchiveMetricBinding(shortId, metricBindingId);
40213
40799
  } catch (err) {
40214
40800
  spinner.fail("Failed to unarchive metric binding");
40215
40801
  failCommand(err);
40216
40802
  }
40803
+ const metricBinding = readMetricBinding(response.data);
40804
+ spinner.succeed(
40805
+ metricBinding ? `Metric binding restored from archive: ${metricBinding.name} (${metricBindingIdLabel(metricBinding)})` : `Metric binding ${metricBindingId} restored from archive`
40806
+ );
40217
40807
  });
40218
40808
  var metricBindingsDeleteCommand = new Command("delete").argument("<metricBindingId>", "Metric binding ID").description("Delete a metric binding (rejected if experiments still attach it)").option("--project <project>", "Project ID (PRJ-42 or numeric); defaults to local manifest").option("-f, --force", "Skip confirmation prompt").action(async (metricBindingId, opts) => {
40219
40809
  const config = ensureLoggedIn();
40220
40810
  const shortId = resolveProjectShortId2(opts);
40221
40811
  const api = new ApiClient(config);
40222
40812
  if (!opts.force) {
40813
+ if (!isInteractiveRun()) {
40814
+ failCommand(
40815
+ new CliError("cli_usage", `Deleting metric binding ${metricBindingId} needs a confirmation this run cannot ask for.`, [
40816
+ `Pass -f to delete without asking: \`avsb metric-bindings delete ${metricBindingId} -f\``
40817
+ ])
40818
+ );
40819
+ }
40223
40820
  const { proceed } = await esm_default12.prompt([
40224
40821
  {
40225
40822
  type: "confirm",
@@ -40239,28 +40836,21 @@ var metricBindingsDeleteCommand = new Command("delete").argument("<metricBinding
40239
40836
  spinner.succeed(`Metric binding deleted: ${metricBindingId}`);
40240
40837
  } catch (err) {
40241
40838
  spinner.fail("Failed to delete metric binding");
40242
- const message = err instanceof Error ? err.message : "Unknown error";
40243
40839
  if (isConflictError2(err)) {
40244
40840
  const refs = extractExperimentsReferencing(err);
40245
- if (refs.length > 0) {
40246
- error(`Cannot delete: referenced by ${refs.join(", ")}.`);
40247
- } else {
40248
- error(`Cannot delete: experiments still attach this metric binding. ${message}`);
40249
- }
40841
+ if (refs.length > 0) error(`Attached to ${refs.join(", ")}.`);
40250
40842
  info(
40251
40843
  `Tip: run \`avsb metric-bindings archive ${metricBindingId}\` instead to hide it without losing history.`
40252
40844
  );
40253
- } else {
40254
- error(message);
40255
40845
  }
40256
- process.exit(EXIT.failure);
40846
+ failCommand(err);
40257
40847
  }
40258
40848
  });
40259
40849
  var metricBindingsCommand = new Command("metric-bindings").description("Manage saved/library metric bindings").addCommand(metricBindingsListCommand).addCommand(metricBindingsCreateCommand).addCommand(metricBindingsUpdateCommand).addCommand(metricBindingsArchiveCommand).addCommand(metricBindingsUnarchiveCommand).addCommand(metricBindingsDeleteCommand);
40260
40850
 
40261
40851
  // src/commands/codegen.ts
40262
- import path16 from "node:path";
40263
- import fs12 from "node:fs/promises";
40852
+ import path17 from "node:path";
40853
+ import fs13 from "node:fs/promises";
40264
40854
 
40265
40855
  // src/lib/codegenApi.ts
40266
40856
  var CodegenApiClient = class extends ApiClient {
@@ -40317,6 +40907,9 @@ function serialiseManifest(manifest) {
40317
40907
  }
40318
40908
 
40319
40909
  // src/lib/codegenCheck.ts
40910
+ function withoutPublishStamp(text) {
40911
+ return text.replace(/^(\/\/ Source: .*\bpublishedAt=)\S+/m, "$1*").replace(/^(\s*"publishedAt": )(?:"[^"\n]*"|null)/gm, "$1*");
40912
+ }
40320
40913
  function diffArtifact(artifact) {
40321
40914
  const expectedLines = artifact.expected.split("\n");
40322
40915
  if (artifact.actual === null) {
@@ -40331,11 +40924,13 @@ function diffArtifact(artifact) {
40331
40924
  };
40332
40925
  }
40333
40926
  const actual = artifact.actual.replace(/\r\n/g, "\n");
40334
- if (actual === artifact.expected) return null;
40927
+ const expectedComparable = withoutPublishStamp(artifact.expected).split("\n");
40928
+ const actualComparable = withoutPublishStamp(actual).split("\n");
40929
+ if (expectedComparable.join("\n") === actualComparable.join("\n")) return null;
40335
40930
  const actualLines = actual.split("\n");
40336
- const shared = Math.min(expectedLines.length, actualLines.length);
40931
+ const shared = Math.min(expectedComparable.length, actualComparable.length);
40337
40932
  let index = 0;
40338
- while (index < shared && expectedLines[index] === actualLines[index]) index += 1;
40933
+ while (index < shared && expectedComparable[index] === actualComparable[index]) index += 1;
40339
40934
  return {
40340
40935
  path: artifact.path,
40341
40936
  reason: "different",
@@ -40489,7 +41084,7 @@ function planArtifacts(datafile, targets) {
40489
41084
  }
40490
41085
  async function readIfPresent(filePath) {
40491
41086
  try {
40492
- return await fs12.readFile(filePath, "utf-8");
41087
+ return await fs13.readFile(filePath, "utf-8");
40493
41088
  } catch (err) {
40494
41089
  if (isMissingFile(err)) return null;
40495
41090
  throw err;
@@ -40503,8 +41098,8 @@ async function writeArtifacts(artifacts) {
40503
41098
  for (const artifact of artifacts) {
40504
41099
  const existing = await readIfPresent(artifact.path);
40505
41100
  if (existing === artifact.contents) continue;
40506
- await fs12.mkdir(path16.dirname(artifact.path), { recursive: true });
40507
- await fs12.writeFile(artifact.path, artifact.contents, "utf-8");
41101
+ await fs13.mkdir(path17.dirname(artifact.path), { recursive: true });
41102
+ await fs13.writeFile(artifact.path, artifact.contents, "utf-8");
40508
41103
  written.push(artifact.path);
40509
41104
  }
40510
41105
  return written;
@@ -40526,6 +41121,7 @@ function flagCountLine(datafile) {
40526
41121
  }
40527
41122
  async function runCheck(artifacts, datafile, targets) {
40528
41123
  const drifts = diffArtifacts(await toComparisons(artifacts));
41124
+ const exitCode = drifts.length === 0 ? EXIT.success : EXIT.failure;
40529
41125
  if (emitJsonResult({
40530
41126
  upToDate: drifts.length === 0,
40531
41127
  checked: artifacts.map((a) => a.path),
@@ -40534,25 +41130,25 @@ async function runCheck(artifacts, datafile, targets) {
40534
41130
  flagCount: datafile.flags.length,
40535
41131
  publishedAt: datafile.publishedAt ?? null
40536
41132
  })) {
40537
- process.exit(drifts.length === 0 ? EXIT.success : EXIT.failure);
41133
+ return exitCode;
40538
41134
  }
40539
41135
  if (drifts.length === 0) {
40540
41136
  success(describeDriftSummary(drifts));
40541
41137
  dim(` Checked ${artifacts.length === 1 ? "1 file" : `${artifacts.length} files`} against ${flagCountLine(datafile)} in ${targets.env}.`);
40542
- process.exit(EXIT.success);
41138
+ return exitCode;
40543
41139
  }
40544
41140
  error(describeDriftSummary(drifts));
40545
41141
  for (const drift of drifts) {
40546
41142
  for (const line2 of describeDrift(drift)) errorDetail(line2);
40547
41143
  }
40548
41144
  errorDetail("Run the same command without --check, then commit the result.");
40549
- process.exit(EXIT.failure);
41145
+ return exitCode;
40550
41146
  }
40551
41147
  function reportWatchEvent(event, targets) {
40552
41148
  switch (event.kind) {
40553
41149
  case "polled":
40554
41150
  if (event.changed) {
40555
- success(`Updated ${event.written.map((p) => path16.relative(process.cwd(), p)).join(", ")}`);
41151
+ success(`Updated ${event.written.map((p) => path17.relative(process.cwd(), p)).join(", ")}`);
40556
41152
  }
40557
41153
  return;
40558
41154
  case "failed":
@@ -40643,6 +41239,7 @@ var codegenCommand = withOutputOptions(
40643
41239
  const project = resolveProjectRefOrExit(opts);
40644
41240
  const config = ensureLoggedIn();
40645
41241
  const spinner = startSpinner("Fetching datafile...");
41242
+ let checkExitCode = null;
40646
41243
  try {
40647
41244
  const client = new CodegenApiClient(config);
40648
41245
  const first = await client.getDatafileConditional(project.shortId, opts.env, null);
@@ -40655,15 +41252,15 @@ var codegenCommand = withOutputOptions(
40655
41252
  const datafile = first.body;
40656
41253
  spinner.succeed("Datafile fetched");
40657
41254
  const targets = {
40658
- typesPath: path16.resolve(opts.output),
40659
- manifestPath: opts.manifest ? path16.resolve(opts.manifest) : null,
41255
+ typesPath: path17.resolve(opts.output),
41256
+ manifestPath: opts.manifest ? path17.resolve(opts.manifest) : null,
40660
41257
  projectShortId: project.shortId,
40661
41258
  env: opts.env,
40662
41259
  augment: opts.augment !== false
40663
41260
  };
40664
41261
  const artifacts = planArtifacts(datafile, targets);
40665
41262
  if (opts.check) {
40666
- await runCheck(artifacts, datafile, targets);
41263
+ checkExitCode = await runCheck(artifacts, datafile, targets);
40667
41264
  return;
40668
41265
  }
40669
41266
  const written = await writeArtifacts(artifacts);
@@ -40693,12 +41290,14 @@ var codegenCommand = withOutputOptions(
40693
41290
  } catch (err) {
40694
41291
  spinner.fail("Codegen failed");
40695
41292
  failCommand(err);
41293
+ } finally {
41294
+ if (checkExitCode !== null) process.exit(checkExitCode);
40696
41295
  }
40697
41296
  });
40698
41297
 
40699
41298
  // src/commands/find-refs.ts
40700
- import fs13 from "node:fs/promises";
40701
- import path18 from "node:path";
41299
+ import fs14 from "node:fs/promises";
41300
+ import path19 from "node:path";
40702
41301
 
40703
41302
  // src/lib/findRefs.ts
40704
41303
  var FLAG_GETTERS = /* @__PURE__ */ new Map([
@@ -40861,7 +41460,7 @@ function byKeyName(a, b) {
40861
41460
  }
40862
41461
 
40863
41462
  // src/lib/ignoreRules.ts
40864
- import path17 from "node:path";
41463
+ import path18 from "node:path";
40865
41464
  var ALWAYS_SKIPPED_DIRECTORIES = [
40866
41465
  ".git",
40867
41466
  "node_modules",
@@ -40965,7 +41564,7 @@ function relativeToBase(relPath, base) {
40965
41564
  return relPath.slice(base.length + 1);
40966
41565
  }
40967
41566
  function toRelativePosix(root, target) {
40968
- return path17.relative(root, target).split(path17.sep).join("/");
41567
+ return path18.relative(root, target).split(path18.sep).join("/");
40969
41568
  }
40970
41569
 
40971
41570
  // src/commands/find-refs.ts
@@ -41021,13 +41620,13 @@ async function scanTree(root, extraRules, knownKeys, maxFiles) {
41021
41620
  if (truncated) return;
41022
41621
  let entries;
41023
41622
  try {
41024
- entries = await fs13.readdir(dir, { withFileTypes: true });
41623
+ entries = await fs14.readdir(dir, { withFileTypes: true });
41025
41624
  } catch {
41026
41625
  return;
41027
41626
  }
41028
41627
  let rules = inherited;
41029
41628
  if (entries.some((e) => e.isFile() && e.name === ".gitignore")) {
41030
- const contents = await readTextFile(path18.join(dir, ".gitignore"));
41629
+ const contents = await readTextFile(path19.join(dir, ".gitignore"));
41031
41630
  if (contents !== null) {
41032
41631
  rules = [...inherited, ...parseIgnoreFile(contents, toRelativePosix(root, dir))];
41033
41632
  }
@@ -41035,7 +41634,7 @@ async function scanTree(root, extraRules, knownKeys, maxFiles) {
41035
41634
  for (const entry of entries) {
41036
41635
  if (truncated) return;
41037
41636
  if (entry.isSymbolicLink()) continue;
41038
- const full = path18.join(dir, entry.name);
41637
+ const full = path19.join(dir, entry.name);
41039
41638
  const rel = toRelativePosix(root, full);
41040
41639
  if (entry.isDirectory()) {
41041
41640
  if (ALWAYS_SKIPPED_DIRECTORIES.includes(entry.name)) continue;
@@ -41044,7 +41643,7 @@ async function scanTree(root, extraRules, knownKeys, maxFiles) {
41044
41643
  continue;
41045
41644
  }
41046
41645
  if (!entry.isFile()) continue;
41047
- if (!SOURCE_EXTENSIONS.has(path18.extname(entry.name))) continue;
41646
+ if (!SOURCE_EXTENSIONS.has(path19.extname(entry.name))) continue;
41048
41647
  if (isIgnored(rel, false, rules)) continue;
41049
41648
  if (filesScanned >= maxFiles) {
41050
41649
  truncated = true;
@@ -41061,9 +41660,9 @@ async function scanTree(root, extraRules, knownKeys, maxFiles) {
41061
41660
  }
41062
41661
  async function readTextFile(filePath) {
41063
41662
  try {
41064
- const stat4 = await fs13.stat(filePath);
41663
+ const stat4 = await fs14.stat(filePath);
41065
41664
  if (stat4.size > MAX_FILE_BYTES) return null;
41066
- const contents = await fs13.readFile(filePath, "utf-8");
41665
+ const contents = await fs14.readFile(filePath, "utf-8");
41067
41666
  return contents.includes("\0") ? null : contents;
41068
41667
  } catch {
41069
41668
  return null;
@@ -41129,7 +41728,7 @@ var findRefsCommand = withOutputOptions(
41129
41728
  const api = new FlagsApiClient(config);
41130
41729
  const registry = await readRegistry(api, project.shortId);
41131
41730
  spinner.succeed(`Read ${registry.length} flag${registry.length === 1 ? "" : "s"}`);
41132
- const root = path18.resolve(opts.dir);
41731
+ const root = path19.resolve(opts.dir);
41133
41732
  await assertDirectory(root, opts.dir);
41134
41733
  const extraRules = (opts.ignore ?? []).flatMap((pattern) => parseIgnoreFile(pattern));
41135
41734
  const knownKeys = new Set(registry.map((f) => f.key));
@@ -41164,7 +41763,7 @@ function parseMaxFiles(raw) {
41164
41763
  }
41165
41764
  async function assertDirectory(root, asTyped) {
41166
41765
  try {
41167
- const stat4 = await fs13.stat(root);
41766
+ const stat4 = await fs14.stat(root);
41168
41767
  if (stat4.isDirectory()) return;
41169
41768
  } catch {
41170
41769
  }
@@ -41172,11 +41771,11 @@ async function assertDirectory(root, asTyped) {
41172
41771
  }
41173
41772
 
41174
41773
  // src/commands/datasets.ts
41175
- import fs15 from "node:fs";
41774
+ import fs16 from "node:fs";
41176
41775
 
41177
41776
  // src/commands/datasets.helpers.ts
41178
- import fs14 from "node:fs";
41179
- import path19 from "node:path";
41777
+ import fs15 from "node:fs";
41778
+ import path20 from "node:path";
41180
41779
  function parseVersionArg(raw) {
41181
41780
  const normalized = raw.trim().replace(/^[vV]/, "");
41182
41781
  if (!/^\d+$/.test(normalized)) {
@@ -41191,13 +41790,13 @@ function parseVersionArg(raw) {
41191
41790
  return shortId;
41192
41791
  }
41193
41792
  function validateNdjsonPath(filePath) {
41194
- const resolved = path19.resolve(filePath);
41195
- if (!fs14.existsSync(resolved) || !fs14.statSync(resolved).isFile()) {
41793
+ const resolved = path20.resolve(filePath);
41794
+ if (!fs15.existsSync(resolved) || !fs15.statSync(resolved).isFile()) {
41196
41795
  throw new Error(`File not found: ${resolved}`);
41197
41796
  }
41198
- if (path19.extname(resolved).toLowerCase() !== ".ndjson") {
41797
+ if (path20.extname(resolved).toLowerCase() !== ".ndjson") {
41199
41798
  throw new Error(
41200
- `Expected an .ndjson file, got "${path19.basename(resolved)}". Convert CSV files in the dashboard upload modal, or export NDJSON directly.`
41799
+ `Expected an .ndjson file, got "${path20.basename(resolved)}". Convert CSV files in the dashboard upload modal, or export NDJSON directly.`
41201
41800
  );
41202
41801
  }
41203
41802
  return resolved;
@@ -41386,7 +41985,7 @@ var datasetsPushCommand = new Command("push").argument("<slug>", "Dataset slug")
41386
41985
  try {
41387
41986
  const result = await runPushFlow(api, projectShortId, {
41388
41987
  slug,
41389
- fileBody: fs15.readFileSync(resolvedPath),
41988
+ fileBody: fs16.readFileSync(resolvedPath),
41390
41989
  label: opts.label,
41391
41990
  activate: opts.activate,
41392
41991
  poll: { timeoutMs: timeoutSeconds * 1e3 },
@@ -41480,6 +42079,12 @@ var datasetsRollbackCommand = new Command("rollback").argument("<slug>", "Datase
41480
42079
  fail(spinner, "Failed to resolve rollback target", err);
41481
42080
  }
41482
42081
  if (!opts.force) {
42082
+ const refusal = refusalWithoutPrompt({
42083
+ code: "cli_usage",
42084
+ message: `Rolling back "${slug}" (${summary}) needs a confirmation this run cannot ask for.`,
42085
+ hints: [`Pass -f to roll back without asking: \`avsb datasets rollback ${slug} -f\``]
42086
+ });
42087
+ if (refusal) failCommand(refusal);
41483
42088
  const { proceed } = await esm_default12.prompt([
41484
42089
  {
41485
42090
  type: "confirm",
@@ -41755,7 +42360,7 @@ function printSecretHint() {
41755
42360
  " A vs B shows a webhook signing secret only when the webhook is created and when its"
41756
42361
  );
41757
42362
  dim(" secret is rotated, so copy it then. If you no longer have it, rotate it:");
41758
- dim(" Dashboard: Organisation Settings > Integrations > Webhooks");
42363
+ dim(" Dashboard: Organization Settings > Integrations > Webhooks");
41759
42364
  dim(" API: POST /api/v1/projects/{projectId}/webhooks/{webhookId}/secret");
41760
42365
  dim(
41761
42366
  " Put it in AVSB_WEBHOOK_SECRET and verify with verifyWebhookSignature from @avsbhq/node."
@@ -41777,8 +42382,7 @@ var listenCommand = new Command("listen").description("Watch this project's webh
41777
42382
  if (opts.forward) {
41778
42383
  const validated = validateForwardUrl(opts.forward);
41779
42384
  if (!validated.ok) {
41780
- error(validated.reason);
41781
- process.exit(EXIT.failure);
42385
+ failCommand(new CliError("cli_usage", validated.reason));
41782
42386
  }
41783
42387
  forwardUrl = validated.url;
41784
42388
  }
@@ -42304,7 +42908,7 @@ var flagsCreateCommand = withOutputOptions(
42304
42908
  var flagsUpdateCommand = withOutputOptions(
42305
42909
  withProjectOption(
42306
42910
  new Command("update").argument("<flag>", "Flag key, numeric short id, or id").description("Rename a flag or change its description").option("--name <name>", "New display name").option("--description <text>", 'New description. Pass "" to clear it').option("--key <key>", "New key (refused once the flag has served traffic)").option(
42307
- "--if-match <etag>",
42911
+ "--if-match <version>",
42308
42912
  "Only write if the flag still matches this version (from `avsb flags get`)"
42309
42913
  ).option("--force", "Write without checking the version first")
42310
42914
  )
@@ -42608,46 +43212,127 @@ var openCommand = withOutputOptions(
42608
43212
 
42609
43213
  // src/commands/completion.ts
42610
43214
  var COMPLETION_SHELLS = ["bash", "zsh", "fish"];
42611
- function commandNames(program3) {
42612
- return program3.commands.map((command) => command.name()).filter((name) => name !== "completion").sort();
43215
+ var ARGUMENT_CHOICES = {
43216
+ completion: COMPLETION_SHELLS,
43217
+ open: OPEN_TARGETS
43218
+ };
43219
+ function flagsOf(command, isRoot) {
43220
+ const flags = command.options.map((option) => option.long).filter((long) => typeof long === "string");
43221
+ flags.push("--help");
43222
+ if (isRoot) flags.push("--version");
43223
+ return [...new Set(flags)].sort();
43224
+ }
43225
+ function completionEntries(program3) {
43226
+ const entries = [];
43227
+ function visit(command, path23) {
43228
+ const subcommands = command.commands.map((child) => child.name()).sort();
43229
+ entries.push({
43230
+ path: path23,
43231
+ words: [...subcommands, ...ARGUMENT_CHOICES[path23] ?? []],
43232
+ flags: flagsOf(command, path23 === "")
43233
+ });
43234
+ for (const child of command.commands) {
43235
+ visit(child, path23 === "" ? child.name() : `${path23} ${child.name()}`);
43236
+ }
43237
+ }
43238
+ visit(program3, "");
43239
+ return entries;
42613
43240
  }
42614
- function bashCompletion(names) {
43241
+ var quoteBash = (value) => `"${value}"`;
43242
+ function bashCompletion(entries) {
43243
+ const known = entries.filter((entry) => entry.path !== "").map((entry) => quoteBash(entry.path));
43244
+ const cases = entries.map((entry) => {
43245
+ const offer = [...entry.words, ...entry.flags].join(" ");
43246
+ return ` ${quoteBash(entry.path)}) offer="${offer}" ;;`;
43247
+ });
42615
43248
  return `# avsb completion for bash. Add to ~/.bashrc:
42616
43249
  # eval "$(avsb completion bash)"
42617
43250
  _avsb_completions() {
42618
- local commands="${names.join(" ")}"
42619
- COMPREPLY=($(compgen -W "$commands" -- "\${COMP_WORDS[COMP_CWORD]}"))
43251
+ local cur="\${COMP_WORDS[COMP_CWORD]}" cmdpath="" word candidate i offer=""
43252
+ for ((i = 1; i < COMP_CWORD; i++)); do
43253
+ word="\${COMP_WORDS[i]}"
43254
+ [[ "$word" == -* ]] && continue
43255
+ candidate="\${cmdpath:+$cmdpath }$word"
43256
+ case "$candidate" in
43257
+ ${known.length > 0 ? known.join("|") : '"__none__"'}) cmdpath="$candidate" ;;
43258
+ esac
43259
+ done
43260
+ case "$cmdpath" in
43261
+ ${cases.join("\n")}
43262
+ esac
43263
+ COMPREPLY=($(compgen -W "$offer" -- "$cur"))
42620
43264
  }
42621
43265
  complete -F _avsb_completions avsb`;
42622
43266
  }
42623
- function zshCompletion(names) {
43267
+ var quoteZsh = (value) => `'${value}'`;
43268
+ function zshCompletion(entries) {
43269
+ const known = entries.filter((entry) => entry.path !== "").map((entry) => quoteZsh(entry.path));
43270
+ const cases = entries.map((entry) => {
43271
+ const offer = [...entry.words, ...entry.flags].map(quoteZsh).join(" ");
43272
+ return ` (${quoteZsh(entry.path)}) offer=(${offer}) ;;`;
43273
+ });
42624
43274
  return `# avsb completion for zsh. Add to ~/.zshrc:
42625
43275
  # eval "$(avsb completion zsh)"
42626
43276
  _avsb() {
42627
- local -a commands
42628
- commands=(${names.map((name) => `'${name}'`).join(" ")})
42629
- _describe 'command' commands
43277
+ local cmdpath="" word candidate i
43278
+ local -a offer
43279
+ for ((i = 2; i < CURRENT; i++)); do
43280
+ word="\${words[i]}"
43281
+ [[ "$word" == -* ]] && continue
43282
+ candidate="\${cmdpath:+$cmdpath }$word"
43283
+ case "$candidate" in
43284
+ (${known.length > 0 ? known.join("|") : "'__none__'"}) cmdpath="$candidate" ;;
43285
+ esac
43286
+ done
43287
+ case "$cmdpath" in
43288
+ ${cases.join("\n")}
43289
+ esac
43290
+ compadd -- $offer
42630
43291
  }
42631
43292
  compdef _avsb avsb`;
42632
43293
  }
42633
- function fishCompletion(names) {
42634
- const lines = names.map(
42635
- (name) => `complete -c avsb -n __fish_use_subcommand -a ${name}`
42636
- );
43294
+ var quoteFish = (value) => `'${value}'`;
43295
+ function fishCompletion(entries) {
43296
+ const known = entries.filter((entry) => entry.path !== "").map((entry) => quoteFish(entry.path));
43297
+ const lines = [];
43298
+ for (const entry of entries) {
43299
+ const condition = `'test (__avsb_path) = "${entry.path}"'`;
43300
+ if (entry.words.length > 0) {
43301
+ lines.push(`complete -c avsb -n ${condition} -f -a ${quoteFish(entry.words.join(" "))}`);
43302
+ }
43303
+ for (const flag of entry.flags) {
43304
+ lines.push(`complete -c avsb -n ${condition} -l ${flag.replace(/^--/, "")}`);
43305
+ }
43306
+ }
42637
43307
  return [
42638
43308
  "# avsb completion for fish. Save as:",
42639
43309
  "# avsb completion fish > ~/.config/fish/completions/avsb.fish",
43310
+ `set -g __avsb_known_paths ${known.join(" ")}`,
43311
+ // Drops the first token (`avsb`) with `set -e` rather than a [2..-1]
43312
+ // slice, and always prints exactly one line, so `test (__avsb_path) = ""`
43313
+ // compares one value even at the top level.
43314
+ "function __avsb_path",
43315
+ " set -l tokens (commandline -opc)",
43316
+ " set -e tokens[1]",
43317
+ " set -l cmdpath",
43318
+ " for word in $tokens",
43319
+ " string match -q -- '-*' $word; and continue",
43320
+ " set -l candidate (string join ' ' $cmdpath $word)",
43321
+ " contains -- $candidate $__avsb_known_paths; and set cmdpath $cmdpath $word",
43322
+ " end",
43323
+ " echo (string join ' ' $cmdpath)",
43324
+ "end",
42640
43325
  ...lines
42641
43326
  ].join("\n");
42642
43327
  }
42643
- function completionScript(shell, names) {
43328
+ function completionScript(shell, entries) {
42644
43329
  switch (shell) {
42645
43330
  case "bash":
42646
- return bashCompletion(names);
43331
+ return bashCompletion(entries);
42647
43332
  case "zsh":
42648
- return zshCompletion(names);
43333
+ return zshCompletion(entries);
42649
43334
  case "fish":
42650
- return fishCompletion(names);
43335
+ return fishCompletion(entries);
42651
43336
  }
42652
43337
  }
42653
43338
  function buildCompletionCommand(program3) {
@@ -42661,16 +43346,16 @@ function buildCompletionCommand(program3) {
42661
43346
  ])
42662
43347
  );
42663
43348
  }
42664
- line(completionScript(requested, commandNames(program3)));
43349
+ line(completionScript(requested, completionEntries(program3)));
42665
43350
  });
42666
43351
  }
42667
43352
 
42668
43353
  // src/lib/updateNotifier.ts
42669
- import fs16 from "node:fs";
43354
+ import fs17 from "node:fs";
42670
43355
  import os4 from "node:os";
42671
- import path20 from "node:path";
43356
+ import path21 from "node:path";
42672
43357
  var REGISTRY_URL = "https://registry.npmjs.org/@avsbhq/cli/latest";
42673
- var CACHE_FILE = path20.join(os4.homedir(), ".avsb", "update-check.json");
43358
+ var CACHE_FILE = path21.join(os4.homedir(), ".avsb", "update-check.json");
42674
43359
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
42675
43360
  var FETCH_TIMEOUT_MS = 1500;
42676
43361
  function isNewerVersion(latest, current) {
@@ -42710,8 +43395,8 @@ function updateMessage(latest, current) {
42710
43395
  }
42711
43396
  function writeCache(cache) {
42712
43397
  try {
42713
- fs16.mkdirSync(path20.dirname(CACHE_FILE), { recursive: true });
42714
- fs16.writeFileSync(CACHE_FILE, JSON.stringify(cache));
43398
+ fs17.mkdirSync(path21.dirname(CACHE_FILE), { recursive: true });
43399
+ fs17.writeFileSync(CACHE_FILE, JSON.stringify(cache));
42715
43400
  } catch {
42716
43401
  }
42717
43402
  }
@@ -42735,7 +43420,7 @@ async function checkForUpdate(currentVersion, notify) {
42735
43420
  }
42736
43421
  let cache = null;
42737
43422
  try {
42738
- cache = readCache(() => fs16.readFileSync(CACHE_FILE, "utf-8"));
43423
+ cache = readCache(() => fs17.readFileSync(CACHE_FILE, "utf-8"));
42739
43424
  } catch {
42740
43425
  cache = null;
42741
43426
  }
@@ -42748,8 +43433,8 @@ async function checkForUpdate(currentVersion, notify) {
42748
43433
  }
42749
43434
 
42750
43435
  // src/cli.ts
42751
- var __dirname = path21.dirname(fileURLToPath2(import.meta.url));
42752
- var pkg = JSON.parse(fs17.readFileSync(path21.resolve(__dirname, "..", "package.json"), "utf-8"));
43436
+ var __dirname = path22.dirname(fileURLToPath2(import.meta.url));
43437
+ var pkg = JSON.parse(fs18.readFileSync(path22.resolve(__dirname, "..", "package.json"), "utf-8"));
42753
43438
  var program2 = new Command();
42754
43439
  program2.name("avsb").description(
42755
43440
  "A vs B command line tool: experiments, flags, project scripts, metrics, datasets, webhooks, and typed flags"