@avsbhq/cli 3.7.0 → 3.8.2

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 = {
@@ -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
  }
@@ -34135,6 +34338,7 @@ var ApiClient = class extends ApiTransport {
34135
34338
  };
34136
34339
 
34137
34340
  // src/lib/flagsApi.ts
34341
+ var IF_MATCH_HEADER = "X-Avsb-If-Match";
34138
34342
  var FlagsApiClient = class extends ApiClient {
34139
34343
  v1Project(projectRef) {
34140
34344
  return `/api/v1/projects/${encodeURIComponent(String(projectRef))}`;
@@ -34153,9 +34357,9 @@ var FlagsApiClient = class extends ApiClient {
34153
34357
  }
34154
34358
  /**
34155
34359
  * One flag, WITH its ETag. The ETag is what makes a safe update possible:
34156
- * `avsb flags update` sends it back as `If-Match`, so a change someone else
34157
- * made between the read and the write is refused (412) instead of silently
34158
- * overwritten.
34360
+ * `avsb flags update` sends it back on the conditional-write header, so a
34361
+ * change someone else made between the read and the write is refused (412)
34362
+ * instead of silently overwritten.
34159
34363
  */
34160
34364
  async getFlag(projectRef, flagRef) {
34161
34365
  return this.requestWithMeta(
@@ -34179,7 +34383,7 @@ var FlagsApiClient = class extends ApiClient {
34179
34383
  {
34180
34384
  method: "PATCH",
34181
34385
  body: JSON.stringify(patch),
34182
- headers: ifMatch ? { "If-Match": ifMatch } : {}
34386
+ headers: ifMatch ? { [IF_MATCH_HEADER]: ifMatch } : {}
34183
34387
  }
34184
34388
  );
34185
34389
  }
@@ -34410,9 +34614,9 @@ function servedValueFor(flag) {
34410
34614
  if (!variation) return null;
34411
34615
  return { variationKey: variation.key, value: variation.value };
34412
34616
  }
34413
- function describeServedValue(flagKey, served) {
34617
+ function describeServedValue(flagKey, served, environmentKey = "production") {
34414
34618
  if (!served) {
34415
- 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\`.`;
34416
34620
  }
34417
34621
  return `${flagKey} serves ${JSON.stringify(served.value)} (variation ${served.variationKey}) when no targeting rule matches.`;
34418
34622
  }
@@ -34425,12 +34629,46 @@ function canPrompt(ctx) {
34425
34629
  if (envFlagEnabled(ctx.env.CI)) return false;
34426
34630
  return true;
34427
34631
  }
34428
- function isInteractiveRun(options = {}) {
34429
- return canPrompt({
34632
+ function currentPromptContext(options = {}) {
34633
+ return {
34430
34634
  input: options.input !== false,
34431
34635
  quiet: isQuiet(),
34432
34636
  isTty: process.stdin.isTTY === true,
34433
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();
34434
34672
  });
34435
34673
  }
34436
34674
 
@@ -34513,10 +34751,40 @@ function unquote(value) {
34513
34751
  return quote2 === '"' ? body.replace(/\\(["\\])/g, "$1") : body;
34514
34752
  }
34515
34753
 
34516
- // src/commands/init.write.ts
34754
+ // src/lib/gitignore.ts
34517
34755
  import { spawnSync as spawnSync2 } from "node:child_process";
34518
34756
  import fs3 from "node:fs";
34519
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";
34520
34788
 
34521
34789
  // src/lib/jsonTypeInference.ts
34522
34790
  var MAX_JSON_DEPTH = 4;
@@ -35022,14 +35290,14 @@ console.log('${input.flagKey}', flag.value, 'from', flag.source)
35022
35290
 
35023
35291
  // src/commands/init.write.ts
35024
35292
  function exampleFileExists(dir, profile, overridePath) {
35025
- return fs3.existsSync(resolveExamplePath(dir, profile, overridePath));
35293
+ return fs4.existsSync(resolveExamplePath(dir, profile, overridePath));
35026
35294
  }
35027
35295
  function resolveExamplePath(dir, profile, overridePath) {
35028
- return path4.resolve(dir, overridePath ?? profile.exampleFile);
35296
+ return path5.resolve(dir, overridePath ?? profile.exampleFile);
35029
35297
  }
35030
35298
  function writeExampleFile(input) {
35031
35299
  const filePath = resolveExamplePath(input.dir, input.profile, input.overridePath);
35032
- if (fs3.existsSync(filePath)) {
35300
+ if (fs4.existsSync(filePath)) {
35033
35301
  return { filePath, wrote: false, existed: true };
35034
35302
  }
35035
35303
  const contents = buildSample({
@@ -35041,14 +35309,14 @@ function writeExampleFile(input) {
35041
35309
  if (contents === null) {
35042
35310
  return { filePath, wrote: false, existed: false };
35043
35311
  }
35044
- fs3.mkdirSync(path4.dirname(filePath), { recursive: true });
35045
- fs3.writeFileSync(filePath, contents, "utf-8");
35312
+ fs4.mkdirSync(path5.dirname(filePath), { recursive: true });
35313
+ fs4.writeFileSync(filePath, contents, "utf-8");
35046
35314
  return { filePath, wrote: true, existed: false };
35047
35315
  }
35048
35316
  function runInstall(dir, manager, npmPackage, options) {
35049
35317
  const { command, args } = installArgv(manager, npmPackage);
35050
35318
  const printable = [command, ...args].join(" ");
35051
- const result = spawnSync2(command, args, {
35319
+ const result = spawnSync3(command, args, {
35052
35320
  cwd: dir,
35053
35321
  // A quiet or JSON run must leave stdout carrying only the result, so the
35054
35322
  // package manager's own output is captured and dropped instead of printed.
@@ -35078,10 +35346,10 @@ async function runCodegen(input) {
35078
35346
  projectId: String(input.projectShortId),
35079
35347
  env: input.environmentKey
35080
35348
  });
35081
- const filePath = path4.resolve(input.dir, input.outputPath ?? DEFAULT_CODEGEN_OUTPUT);
35349
+ const filePath = path5.resolve(input.dir, input.outputPath ?? DEFAULT_CODEGEN_OUTPUT);
35082
35350
  try {
35083
- fs3.mkdirSync(path4.dirname(filePath), { recursive: true });
35084
- fs3.writeFileSync(filePath, generated, "utf-8");
35351
+ fs4.mkdirSync(path5.dirname(filePath), { recursive: true });
35352
+ fs4.writeFileSync(filePath, generated, "utf-8");
35085
35353
  } catch (err) {
35086
35354
  throw new CliError(
35087
35355
  "cli_io_failed",
@@ -35092,14 +35360,14 @@ async function runCodegen(input) {
35092
35360
  }
35093
35361
  function readFileOrNull(filePath) {
35094
35362
  try {
35095
- return fs3.readFileSync(filePath, "utf-8");
35363
+ return fs4.readFileSync(filePath, "utf-8");
35096
35364
  } catch {
35097
35365
  return null;
35098
35366
  }
35099
35367
  }
35100
35368
  function listDirectory(dir) {
35101
35369
  try {
35102
- return fs3.readdirSync(dir);
35370
+ return fs4.readdirSync(dir);
35103
35371
  } catch {
35104
35372
  return [];
35105
35373
  }
@@ -35107,7 +35375,7 @@ function listDirectory(dir) {
35107
35375
 
35108
35376
  // src/commands/init.execute.ts
35109
35377
  async function executePlan(input) {
35110
- const files = { env: null, envOutcome: null, example: null, codegen: null };
35378
+ const files = { env: null, envOutcome: null, example: null, codegen: null, gitignore: null };
35111
35379
  let install = null;
35112
35380
  const { profile, environment } = input;
35113
35381
  if (stepFor(input.plan, "env").action === "write" && environment && profile.envFile && profile.envVar) {
@@ -35120,6 +35388,11 @@ async function executePlan(input) {
35120
35388
  files.env = result.filePath;
35121
35389
  files.envOutcome = result.outcome;
35122
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
+ }
35123
35396
  }
35124
35397
  const installStep = stepFor(input.plan, "install");
35125
35398
  if (profile.sdkPackage) {
@@ -35187,17 +35460,17 @@ async function watchForCheckIn(api, projectShortId, environment, wait) {
35187
35460
 
35188
35461
  // src/lib/projectScaffold.ts
35189
35462
  import { createHash as createHash2 } from "node:crypto";
35190
- import fs7 from "node:fs";
35191
- import path8 from "node:path";
35463
+ import fs8 from "node:fs";
35464
+ import path9 from "node:path";
35192
35465
 
35193
35466
  // src/lib/scaffold.ts
35194
- import fs6 from "node:fs";
35195
- import path7 from "node:path";
35467
+ import fs7 from "node:fs";
35468
+ import path8 from "node:path";
35196
35469
 
35197
35470
  // src/lib/manifest.ts
35198
35471
  import { createHash } from "node:crypto";
35199
- import fs4 from "node:fs";
35200
- import path5 from "node:path";
35472
+ import fs5 from "node:fs";
35473
+ import path6 from "node:path";
35201
35474
  var MANIFEST_FILENAME = ".avsb.json";
35202
35475
  function parseExperimentInput(input) {
35203
35476
  const urlMatch = input.match(/experiments\/(\d+)/);
@@ -35212,12 +35485,12 @@ function parseExperimentInput(input) {
35212
35485
  function findManifest(startDir) {
35213
35486
  let dir = startDir ?? process.cwd();
35214
35487
  for (let i = 0; i < 10; i++) {
35215
- const manifestPath = path5.join(dir, MANIFEST_FILENAME);
35216
- if (fs4.existsSync(manifestPath)) {
35217
- 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");
35218
35491
  return JSON.parse(raw);
35219
35492
  }
35220
- const parent = path5.dirname(dir);
35493
+ const parent = path6.dirname(dir);
35221
35494
  if (parent === dir) break;
35222
35495
  dir = parent;
35223
35496
  }
@@ -35226,17 +35499,17 @@ function findManifest(startDir) {
35226
35499
  function findManifestDir(startDir) {
35227
35500
  let dir = startDir ?? process.cwd();
35228
35501
  for (let i = 0; i < 10; i++) {
35229
- const manifestPath = path5.join(dir, MANIFEST_FILENAME);
35230
- if (fs4.existsSync(manifestPath)) return dir;
35231
- 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);
35232
35505
  if (parent === dir) break;
35233
35506
  dir = parent;
35234
35507
  }
35235
35508
  return null;
35236
35509
  }
35237
35510
  function writeManifest(dir, manifest) {
35238
- const manifestPath = path5.join(dir, MANIFEST_FILENAME);
35239
- 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));
35240
35513
  }
35241
35514
  function readManifestOrExit(startDir) {
35242
35515
  const dir = findManifestDir(startDir);
@@ -35274,30 +35547,30 @@ function computeSyncHashes(triggersJs, variations) {
35274
35547
  }
35275
35548
 
35276
35549
  // src/lib/snippetTypeDefs.ts
35277
- import fs5 from "node:fs";
35278
- import path6 from "node:path";
35550
+ import fs6 from "node:fs";
35551
+ import path7 from "node:path";
35279
35552
  import { createRequire } from "node:module";
35280
35553
  import { fileURLToPath } from "node:url";
35281
35554
  var SNIPPET_TYPES_PACKAGE = "@avsbhq/snippet-types";
35282
35555
  var SNIPPET_TYPE_DEF_FILES = ["avsb.d.ts", "variation-code.d.ts"];
35283
35556
  var MAX_WALK_UP = 24;
35284
35557
  function snippetTypeDefCandidates(filename, lookup) {
35285
- const candidates = [path6.join(lookup.moduleDir, "types", filename)];
35558
+ const candidates = [path7.join(lookup.moduleDir, "types", filename)];
35286
35559
  const packageDir = lookup.packageDir();
35287
- if (packageDir) candidates.push(path6.join(packageDir, filename));
35560
+ if (packageDir) candidates.push(path7.join(packageDir, filename));
35288
35561
  const isRepoRoot = lookup.isRepoRoot ?? defaultIsRepoRoot;
35289
35562
  let dir = lookup.moduleDir;
35290
35563
  for (let depth = 0; depth < MAX_WALK_UP; depth += 1) {
35291
- candidates.push(path6.join(dir, "packages", "avsb-snippet-types", filename));
35564
+ candidates.push(path7.join(dir, "packages", "avsb-snippet-types", filename));
35292
35565
  if (isRepoRoot(dir)) break;
35293
- const parent = path6.dirname(dir);
35566
+ const parent = path7.dirname(dir);
35294
35567
  if (parent === dir) break;
35295
35568
  dir = parent;
35296
35569
  }
35297
35570
  return candidates;
35298
35571
  }
35299
35572
  function defaultIsRepoRoot(dir) {
35300
- return fs5.existsSync(path6.join(dir, ".git"));
35573
+ return fs6.existsSync(path7.join(dir, ".git"));
35301
35574
  }
35302
35575
  function findSnippetTypeDefPath(filename, lookup) {
35303
35576
  for (const candidate of snippetTypeDefCandidates(filename, lookup)) {
@@ -35315,15 +35588,15 @@ function typeDefBanner(filename) {
35315
35588
  function defaultPackageDir() {
35316
35589
  try {
35317
35590
  const moduleRequire = createRequire(import.meta.url);
35318
- return path6.dirname(moduleRequire.resolve(`${SNIPPET_TYPES_PACKAGE}/package.json`));
35591
+ return path7.dirname(moduleRequire.resolve(`${SNIPPET_TYPES_PACKAGE}/package.json`));
35319
35592
  } catch {
35320
35593
  return null;
35321
35594
  }
35322
35595
  }
35323
35596
  function defaultLookup() {
35324
35597
  return {
35325
- moduleDir: path6.dirname(fileURLToPath(import.meta.url)),
35326
- exists: (candidate) => fs5.existsSync(candidate),
35598
+ moduleDir: path7.dirname(fileURLToPath(import.meta.url)),
35599
+ exists: (candidate) => fs6.existsSync(candidate),
35327
35600
  packageDir: defaultPackageDir
35328
35601
  };
35329
35602
  }
@@ -35340,7 +35613,7 @@ function readSnippetTypeDefs(lookup = defaultLookup()) {
35340
35613
  ]
35341
35614
  );
35342
35615
  }
35343
- return { filename, contents: typeDefBanner(filename) + fs5.readFileSync(found, "utf-8") };
35616
+ return { filename, contents: typeDefBanner(filename) + fs6.readFileSync(found, "utf-8") };
35344
35617
  });
35345
35618
  }
35346
35619
 
@@ -35365,7 +35638,7 @@ function writeTypeDefinitions(dir) {
35365
35638
  try {
35366
35639
  const written = [];
35367
35640
  for (const file of readSnippetTypeDefs()) {
35368
- fs6.writeFileSync(path7.join(dir, file.filename), file.contents);
35641
+ fs7.writeFileSync(path8.join(dir, file.filename), file.contents);
35369
35642
  written.push(file.filename);
35370
35643
  }
35371
35644
  return written;
@@ -35381,24 +35654,24 @@ function scaffoldExperiment(options) {
35381
35654
  const { data, script, style, apiBaseUrl, targetDir } = options;
35382
35655
  const { experiment, project, org } = data;
35383
35656
  const folderName = toKebabCase(experiment.name);
35384
- const baseDir = targetDir ?? path7.join(process.cwd(), folderName);
35385
- if (fs6.existsSync(baseDir)) {
35657
+ const baseDir = targetDir ?? path8.join(process.cwd(), folderName);
35658
+ if (fs7.existsSync(baseDir)) {
35386
35659
  throw new Error(`Directory already exists: ${baseDir}`);
35387
35660
  }
35388
- fs6.mkdirSync(baseDir, { recursive: true });
35661
+ fs7.mkdirSync(baseDir, { recursive: true });
35389
35662
  const files = [];
35390
35663
  const manifestVariations = [];
35391
- const triggerFile = `trigger.${script}`;
35392
- fs6.writeFileSync(path7.join(baseDir, triggerFile), experiment.triggersJs);
35393
- files.push(triggerFile);
35664
+ const triggerFile2 = `trigger.${script}`;
35665
+ fs7.writeFileSync(path8.join(baseDir, triggerFile2), experiment.triggersJs);
35666
+ files.push(triggerFile2);
35394
35667
  for (const variation of experiment.variations) {
35395
35668
  const folder = variationFolderName(variation.name);
35396
- const varDir = path7.join(baseDir, folder);
35397
- fs6.mkdirSync(varDir, { recursive: true });
35669
+ const varDir = path8.join(baseDir, folder);
35670
+ fs7.mkdirSync(varDir, { recursive: true });
35398
35671
  const scriptFile = `${folder}.${script}`;
35399
35672
  const styleFile = `${folder}.${style}`;
35400
- fs6.writeFileSync(path7.join(varDir, scriptFile), variation.controlJs);
35401
- 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);
35402
35675
  files.push(`${folder}/${scriptFile}`);
35403
35676
  files.push(`${folder}/${styleFile}`);
35404
35677
  manifestVariations.push({
@@ -35437,7 +35710,7 @@ function scaffoldExperiment(options) {
35437
35710
  writeManifest(baseDir, manifest);
35438
35711
  files.push(".avsb.json");
35439
35712
  if (script === "ts") {
35440
- fs6.writeFileSync(path7.join(baseDir, "tsconfig.json"), TSCONFIG_TEMPLATE);
35713
+ fs7.writeFileSync(path8.join(baseDir, "tsconfig.json"), TSCONFIG_TEMPLATE);
35441
35714
  files.push("tsconfig.json");
35442
35715
  files.push(...writeTypeDefinitions(baseDir));
35443
35716
  }
@@ -35453,10 +35726,10 @@ function refreshTypeDefinitions(dir, script) {
35453
35726
  }
35454
35727
  const changed = [];
35455
35728
  for (const file of current) {
35456
- const target = path7.join(dir, file.filename);
35457
- 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;
35458
35731
  if (onDisk === file.contents) continue;
35459
- fs6.writeFileSync(target, file.contents);
35732
+ fs7.writeFileSync(target, file.contents);
35460
35733
  changed.push(file.filename);
35461
35734
  }
35462
35735
  return changed;
@@ -35466,26 +35739,26 @@ function updateLocalFiles(dir, manifest, data) {
35466
35739
  const { script, style } = manifest.languages;
35467
35740
  const updated = [];
35468
35741
  updated.push(...refreshTypeDefinitions(dir, script));
35469
- const triggerFile = `trigger.${script}`;
35470
- const triggerPath = path7.join(dir, triggerFile);
35471
- 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") : "";
35472
35745
  if (currentTrigger !== experiment.triggersJs) {
35473
- fs6.writeFileSync(triggerPath, experiment.triggersJs);
35474
- updated.push(triggerFile);
35746
+ fs7.writeFileSync(triggerPath, experiment.triggersJs);
35747
+ updated.push(triggerFile2);
35475
35748
  }
35476
35749
  for (const variation of experiment.variations) {
35477
35750
  const mv = manifest.variations.find((v) => v.id === variation.id);
35478
35751
  if (!mv) continue;
35479
- const jsPath = path7.join(dir, mv.folder, mv.scriptFile);
35480
- const cssPath = path7.join(dir, mv.folder, mv.styleFile);
35481
- const currentJs = fs6.existsSync(jsPath) ? fs6.readFileSync(jsPath, "utf-8") : "";
35482
- 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") : "";
35483
35756
  if (currentJs !== variation.controlJs) {
35484
- fs6.writeFileSync(jsPath, variation.controlJs);
35757
+ fs7.writeFileSync(jsPath, variation.controlJs);
35485
35758
  updated.push(`${mv.folder}/${mv.scriptFile}`);
35486
35759
  }
35487
35760
  if (currentCss !== variation.controlCss) {
35488
- fs6.writeFileSync(cssPath, variation.controlCss);
35761
+ fs7.writeFileSync(cssPath, variation.controlCss);
35489
35762
  updated.push(`${mv.folder}/${mv.styleFile}`);
35490
35763
  }
35491
35764
  }
@@ -35518,27 +35791,27 @@ function diffLocalFiles(dir, manifest, data) {
35518
35791
  const { experiment } = data;
35519
35792
  const { script } = manifest.languages;
35520
35793
  const changed = [];
35521
- const triggerFile = `trigger.${script}`;
35522
- const triggerPath = path7.join(dir, triggerFile);
35523
- if (fs6.existsSync(triggerPath)) {
35524
- 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");
35525
35798
  if (localContent !== experiment.triggersJs) {
35526
- changed.push(triggerFile);
35799
+ changed.push(triggerFile2);
35527
35800
  }
35528
35801
  }
35529
35802
  for (const variation of experiment.variations) {
35530
35803
  const mv = manifest.variations.find((v) => v.id === variation.id);
35531
35804
  if (!mv) continue;
35532
- const jsPath = path7.join(dir, mv.folder, mv.scriptFile);
35533
- const cssPath = path7.join(dir, mv.folder, mv.styleFile);
35534
- if (fs6.existsSync(jsPath)) {
35535
- 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");
35536
35809
  if (localJs !== variation.controlJs) {
35537
35810
  changed.push(`${mv.folder}/${mv.scriptFile}`);
35538
35811
  }
35539
35812
  }
35540
- if (fs6.existsSync(cssPath)) {
35541
- const localCss = fs6.readFileSync(cssPath, "utf-8");
35813
+ if (fs7.existsSync(cssPath)) {
35814
+ const localCss = fs7.readFileSync(cssPath, "utf-8");
35542
35815
  if (localCss !== variation.controlCss) {
35543
35816
  changed.push(`${mv.folder}/${mv.styleFile}`);
35544
35817
  }
@@ -35579,21 +35852,21 @@ function parseProjectInput(input) {
35579
35852
  return num;
35580
35853
  }
35581
35854
  function writeProjectManifest(dir, manifest) {
35582
- const manifestPath = path8.join(dir, PROJECT_MANIFEST_FILENAME);
35583
- 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));
35584
35857
  }
35585
35858
  function readProjectManifest(dir) {
35586
- const manifestPath = path8.join(dir, PROJECT_MANIFEST_FILENAME);
35587
- if (!fs7.existsSync(manifestPath)) return null;
35588
- 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");
35589
35862
  return JSON.parse(raw);
35590
35863
  }
35591
35864
  function findProjectManifestDir(startDir) {
35592
35865
  let dir = startDir ?? process.cwd();
35593
35866
  for (let i = 0; i < 10; i++) {
35594
- const manifestPath = path8.join(dir, PROJECT_MANIFEST_FILENAME);
35595
- if (fs7.existsSync(manifestPath)) return dir;
35596
- 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);
35597
35870
  if (parent === dir) break;
35598
35871
  dir = parent;
35599
35872
  }
@@ -35613,20 +35886,29 @@ function readProjectManifestOrExit(startDir) {
35613
35886
  const manifest = readProjectManifest(dir);
35614
35887
  return { manifest, dir };
35615
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
+ }
35616
35898
  function scaffoldProject(options) {
35617
35899
  const { data, apiBaseUrl, targetDir } = options;
35618
35900
  const { project } = data;
35619
35901
  const lang = project.projectJsLang === "TYPESCRIPT" ? "ts" : "js";
35620
35902
  const folderName = `project-${project.shortId}`;
35621
- const baseDir = targetDir ?? path8.join(process.cwd(), folderName);
35622
- if (fs7.existsSync(baseDir)) {
35903
+ const baseDir = targetDir ?? path9.join(process.cwd(), folderName);
35904
+ if (fs8.existsSync(baseDir)) {
35623
35905
  throw new Error(`Directory already exists: ${baseDir}`);
35624
35906
  }
35625
- fs7.mkdirSync(baseDir, { recursive: true });
35907
+ fs8.mkdirSync(baseDir, { recursive: true });
35626
35908
  const files = [];
35627
35909
  const scriptFile = `project.${lang}`;
35628
35910
  const content = lang === "ts" ? project.editorProjectTs ?? project.editorProjectJs ?? "" : project.editorProjectJs ?? "";
35629
- fs7.writeFileSync(path8.join(baseDir, scriptFile), content);
35911
+ fs8.writeFileSync(path9.join(baseDir, scriptFile), content);
35630
35912
  files.push(scriptFile);
35631
35913
  const manifest = {
35632
35914
  version: 1,
@@ -35643,7 +35925,7 @@ function scaffoldProject(options) {
35643
35925
  writeProjectManifest(baseDir, manifest);
35644
35926
  files.push(".avsb-project.json");
35645
35927
  if (lang === "ts") {
35646
- fs7.writeFileSync(path8.join(baseDir, "tsconfig.json"), PROJECT_TSCONFIG);
35928
+ fs8.writeFileSync(path9.join(baseDir, "tsconfig.json"), PROJECT_TSCONFIG);
35647
35929
  files.push("tsconfig.json");
35648
35930
  files.push(...writeTypeDefinitions(baseDir));
35649
35931
  }
@@ -35654,11 +35936,11 @@ function updateProjectFile(dir, manifest, data) {
35654
35936
  const updated = [];
35655
35937
  updated.push(...refreshTypeDefinitions(dir, manifest.language));
35656
35938
  const scriptFile = `project.${manifest.language}`;
35657
- const scriptPath = path8.join(dir, scriptFile);
35658
- 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") : "";
35659
35941
  const remote = manifest.language === "ts" ? project.editorProjectTs ?? project.editorProjectJs ?? "" : project.editorProjectJs ?? "";
35660
35942
  if (current !== remote) {
35661
- fs7.writeFileSync(scriptPath, remote);
35943
+ fs8.writeFileSync(scriptPath, remote);
35662
35944
  updated.push(scriptFile);
35663
35945
  }
35664
35946
  manifest.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -35877,28 +36159,47 @@ function renderInitSummary(input) {
35877
36159
  }
35878
36160
  function describeDetection(input) {
35879
36161
  const manager = `installs with ${input.packageManager}`;
36162
+ const variable = input.profile.envVar && input.profile.envVarReason ? `, key in ${input.profile.envVar}: ${input.profile.envVarReason}` : "";
35880
36163
  if (input.detection.matchedDependency) {
35881
- 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})`;
35882
36165
  }
35883
36166
  if (input.profile.id === "none") {
35884
36167
  return "no package.json found here";
35885
36168
  }
35886
36169
  return `${input.profile.label} (${manager})`;
35887
36170
  }
35888
- function renderSteps(input) {
35889
- 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(
35890
36174
  (file) => file !== null
35891
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);
35892
36181
  if (written.length > 0) {
35893
36182
  heading("Written");
35894
36183
  fileTree(written);
35895
36184
  line();
35896
36185
  }
36186
+ if (leftAlone.length > 0) {
36187
+ heading("Left alone");
36188
+ fileTree(leftAlone);
36189
+ line();
36190
+ }
35897
36191
  const skipped = input.plan.filter((step) => step.action === "skip");
35898
36192
  for (const step of skipped) dim(` Skipped ${step.id}: ${step.detail}`);
35899
36193
  if (skipped.length > 0) line();
35900
36194
  }
35901
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) {
35902
36203
  const todo = [];
35903
36204
  if (input.install && !input.install.ran) {
35904
36205
  todo.push(`Install the SDK: ${input.install.command}`);
@@ -35910,17 +36211,15 @@ function renderWhatIsLeft(input) {
35910
36211
  todo.push(input.profile.caveat);
35911
36212
  }
35912
36213
  if (input.flagKey && !input.flagExists) {
36214
+ const envKey = input.environment?.key ?? "production";
35913
36215
  todo.push(
35914
- `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\`.`
35915
36217
  );
35916
36218
  }
35917
36219
  if (input.servedLine) {
35918
36220
  todo.push(input.servedLine);
35919
36221
  }
35920
- if (todo.length === 0) return;
35921
- line();
35922
- heading("Next");
35923
- for (const line2 of todo) dim(` ${line2}`);
36222
+ return todo;
35924
36223
  }
35925
36224
  function renderAssistantPrompt(input) {
35926
36225
  if (!input.environment && input.project.type === "FEATURE_FLAG") return;
@@ -35977,11 +36276,11 @@ var initCommand = withOutputOptions(
35977
36276
  ).action(async (opts) => {
35978
36277
  beginCommand("init", opts);
35979
36278
  try {
35980
- const dir = path9.resolve(opts.dir ?? ".");
36279
+ const dir = path10.resolve(opts.dir ?? ".");
35981
36280
  parseWaitSeconds(opts.wait);
35982
36281
  assertHeartbeatFlagsAgree(opts);
35983
36282
  const interactive = isInteractiveRun({ input: opts.input });
35984
- const pkg2 = parsePackageJson(readFileOrNull(path9.join(dir, "package.json")) ?? "");
36283
+ const pkg2 = parsePackageJson(readFileOrNull(path10.join(dir, "package.json")) ?? "");
35985
36284
  const detection = resolveDetection(pkg2, opts.framework);
35986
36285
  const profile = buildFrameworkProfile(detection);
35987
36286
  const packageManager = detectPackageManager(pkg2, listDirectory(dir));
@@ -36055,7 +36354,9 @@ var initCommand = withOutputOptions(
36055
36354
  id: profile.id,
36056
36355
  label: profile.label,
36057
36356
  detectedFrom: detection.matchedDependency,
36058
- packageManager
36357
+ packageManager,
36358
+ bundler: detection.bundler?.id ?? null,
36359
+ envVar: profile.envVar
36059
36360
  },
36060
36361
  flag: sample ? { key: sample.flagKey, exists: sample.flag !== null, served } : null,
36061
36362
  steps: plan,
@@ -36078,12 +36379,14 @@ var initCommand = withOutputOptions(
36078
36379
  install: outcome.install,
36079
36380
  flagKey: sample?.flagKey ?? null,
36080
36381
  flagExists: sample !== null && sample.flag !== null,
36081
- servedLine: sample?.flag ? describeServedValue(sample.flagKey, served) : null,
36382
+ servedLine: sample?.flag ? describeServedValue(sample.flagKey, served, opts.env) : null,
36082
36383
  heartbeat,
36083
36384
  proof,
36084
36385
  timeoutGuidance: heartbeat && !heartbeat.seen && environment ? heartbeatTimeoutGuidance({
36085
36386
  sdkKey: environment.sdkKey,
36086
36387
  environmentName: environment.name,
36388
+ environmentKey: environment.key,
36389
+ flagKey: sample?.flagKey ?? null,
36087
36390
  previousSeenAt: heartbeat.previousSeenAt,
36088
36391
  waitedMs: heartbeat.elapsedMs,
36089
36392
  cdnHost: new URL(DEFAULT_CDN_BASE_URL).host,
@@ -36133,8 +36436,8 @@ async function decideCodegen(opts, interactive, hasSdkKey) {
36133
36436
  }
36134
36437
 
36135
36438
  // src/commands/agents-md.ts
36136
- import fs8 from "node:fs";
36137
- import path10 from "node:path";
36439
+ import fs9 from "node:fs";
36440
+ import path11 from "node:path";
36138
36441
 
36139
36442
  // src/lib/agentsMd.ts
36140
36443
  var AGENTS_MARKER_START = "<!-- avsb:start -->";
@@ -36284,8 +36587,8 @@ var agentsMdInstallCommand = withOutputOptions(
36284
36587
  ).action((opts) => {
36285
36588
  beginCommand("agents-md install", opts);
36286
36589
  try {
36287
- const dir = path10.resolve(opts.dir ?? ".");
36288
- 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")) ?? "");
36289
36592
  const profile = buildFrameworkProfile(detectFramework(pkg2));
36290
36593
  const section = renderAgentsSection({
36291
36594
  projectShortId: readProjectShortId(opts.project),
@@ -36309,12 +36612,12 @@ var agentsMdInstallCommand = withOutputOptions(
36309
36612
  }
36310
36613
  });
36311
36614
  function writeTarget(dir, file, section) {
36312
- const filePath = path10.join(dir, file);
36615
+ const filePath = path11.join(dir, file);
36313
36616
  const existing = readFileOrNull(filePath);
36314
36617
  const applied = applyAgentsSection(existing, section);
36315
36618
  if (applied.outcome !== "unchanged") {
36316
36619
  try {
36317
- fs8.writeFileSync(filePath, applied.content, "utf-8");
36620
+ fs9.writeFileSync(filePath, applied.content, "utf-8");
36318
36621
  } catch (err) {
36319
36622
  throw new CliError(
36320
36623
  "cli_io_failed",
@@ -36350,7 +36653,8 @@ var loginCommand = new Command("login").description("Save a personal access toke
36350
36653
  `Replacing the saved login in ${getConfigPath()}: ${existing.detail}.`
36351
36654
  );
36352
36655
  }
36353
- if (existingConfig && options.input && !explicitToken && !envToken) {
36656
+ const interactive = canPrompt(currentPromptContext({ input: options.input }));
36657
+ if (existingConfig && interactive && !explicitToken && !envToken) {
36354
36658
  const active = existingConfig.logins.find(
36355
36659
  (login) => login.orgId === existingConfig.activeOrgId
36356
36660
  );
@@ -36372,7 +36676,12 @@ var loginCommand = new Command("login").description("Save a personal access toke
36372
36676
  const token = await resolveToken({
36373
36677
  explicitToken,
36374
36678
  envToken,
36375
- allowPrompt: options.input
36679
+ source: tokenSourceFor({
36680
+ explicitToken,
36681
+ envToken,
36682
+ input: options.input,
36683
+ isTty: process.stdin.isTTY === true
36684
+ })
36376
36685
  });
36377
36686
  const spinner = ora("Verifying token...").start();
36378
36687
  const client = new ApiClient({ token, apiBaseUrl }, { useEnvOverrides: false });
@@ -36380,8 +36689,9 @@ var loginCommand = new Command("login").description("Save a personal access toke
36380
36689
  try {
36381
36690
  verified = await client.verifyAuth();
36382
36691
  } catch (err) {
36383
- spinner.fail("Token rejected");
36384
- 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) {
36385
36695
  dim(` ${WHERE_TO_GET_A_TOKEN}`);
36386
36696
  }
36387
36697
  failCommand(err);
@@ -36416,13 +36726,22 @@ var loginCommand = new Command("login").description("Save a personal access toke
36416
36726
  failCommand(err);
36417
36727
  }
36418
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
+ ];
36419
36738
  async function resolveToken(input) {
36420
- if (input.explicitToken) return input.explicitToken;
36421
- if (input.envToken) {
36739
+ if (input.source === "flag" && input.explicitToken) return input.explicitToken;
36740
+ if (input.source === "env" && input.envToken) {
36422
36741
  info(`Using the token from ${TOKEN_ENV_VAR}.`);
36423
36742
  return input.envToken;
36424
36743
  }
36425
- if (!input.allowPrompt) {
36744
+ if (input.source === "refuse") {
36426
36745
  failCommand(
36427
36746
  new CliError(
36428
36747
  "cli_usage",
@@ -36430,6 +36749,13 @@ async function resolveToken(input) {
36430
36749
  )
36431
36750
  );
36432
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
+ }
36433
36759
  const { token } = await esm_default12.prompt([
36434
36760
  {
36435
36761
  type: "password",
@@ -36466,12 +36792,19 @@ var logoutCommand = new Command("logout").description("Delete the saved logins o
36466
36792
  });
36467
36793
 
36468
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
+ }
36469
36802
  function buildWhoamiRows(verified, session, configPath) {
36470
36803
  return [
36471
36804
  { label: "Email", value: verified.email },
36472
36805
  { label: "Organization", value: verified.orgName },
36473
36806
  { label: "Org ID", value: verified.orgId },
36474
- { label: "Role", value: verified.role },
36807
+ { label: "Role", value: roleName(verified.role) },
36475
36808
  { label: "API URL", value: session.apiBaseUrl },
36476
36809
  { label: "Token from", value: describeTokenSource(session, configPath) }
36477
36810
  ];
@@ -36497,7 +36830,7 @@ var whoamiCommand = withOutputOptions(
36497
36830
  email: verified.data.email,
36498
36831
  orgId: verified.data.orgId,
36499
36832
  orgName: verified.data.orgName,
36500
- role: verified.data.role,
36833
+ role: roleName(verified.data.role),
36501
36834
  apiBaseUrl: session.apiBaseUrl,
36502
36835
  tokenSource: session.tokenSource,
36503
36836
  configPath: getConfigPath()
@@ -36507,7 +36840,7 @@ var whoamiCommand = withOutputOptions(
36507
36840
  line();
36508
36841
  heading("Signed in");
36509
36842
  for (const row of buildWhoamiRows(verified.data, session, getConfigPath())) {
36510
- label(row.label, row.value);
36843
+ field(row.label, row.value);
36511
36844
  }
36512
36845
  line();
36513
36846
  });
@@ -36524,12 +36857,14 @@ function configForSession(session) {
36524
36857
  apiBaseUrl: session.apiBaseUrl
36525
36858
  };
36526
36859
  }
36860
+ function describeCandidates(candidates) {
36861
+ return candidates.map((c) => c.shortId != null ? `${c.shortId} (${c.name})` : c.name).join(", ");
36862
+ }
36527
36863
  function describeSwitchFailure(candidates, needle) {
36528
36864
  if (candidates.length === 0) {
36529
36865
  return `No organization matches "${needle}". This token cannot reach any organization.`;
36530
36866
  }
36531
- const known = candidates.map((c) => c.shortId != null ? `${c.shortId} (${c.name})` : c.name).join(", ");
36532
- return `No organization matches "${needle}". Available: ${known}.`;
36867
+ return `No organization matches "${needle}". Available: ${describeCandidates(candidates)}.`;
36533
36868
  }
36534
36869
  var orgCommand = withOutputOptions(
36535
36870
  new Command("org").description("Show, list, and switch the active organization")
@@ -36547,13 +36882,13 @@ var orgCommand = withOutputOptions(
36547
36882
  }
36548
36883
  heading("Current organization");
36549
36884
  if (session.orgName) {
36550
- label("Name", session.orgName);
36551
- label("ID", session.orgId);
36885
+ field("Name", session.orgName);
36886
+ field("ID", session.orgId);
36552
36887
  } else {
36553
- label("Name", "not saved on this machine");
36888
+ field("Name", "not saved on this machine");
36554
36889
  }
36555
- label("API URL", session.apiBaseUrl);
36556
- label("Token from", describeTokenSource(session, getConfigPath()));
36890
+ field("API URL", session.apiBaseUrl);
36891
+ field("Token from", describeTokenSource(session, getConfigPath()));
36557
36892
  if (session.tokenSource === "env") {
36558
36893
  dim(
36559
36894
  ` ${TOKEN_ENV_VAR} is set, so commands use that token. Run \`avsb whoami\` to see which account and organization it reaches.`
@@ -36563,8 +36898,8 @@ var orgCommand = withOutputOptions(
36563
36898
  orgCommand.addCommand(
36564
36899
  withOutputOptions(
36565
36900
  new Command("list").description("List the organizations this token can reach")
36566
- ).action(async (opts) => {
36567
- beginCommand("org list", opts);
36901
+ ).action(async (_opts, command) => {
36902
+ beginCommand("org list", command.optsWithGlobals());
36568
36903
  try {
36569
36904
  const session = ensureLoggedIn();
36570
36905
  const config = configForSession(session);
@@ -36628,12 +36963,16 @@ orgCommand.command("switch").description("Choose which organization commands use
36628
36963
  failCommand(err);
36629
36964
  }
36630
36965
  });
36631
- async function promptForOrg(candidates, allowPrompt) {
36632
- if (!allowPrompt) {
36633
- failCommand(
36634
- new CliError("cli_usage", "Name an organization: `avsb org switch <short id | id | name>`.")
36635
- );
36636
- }
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);
36637
36976
  const { chosen } = await esm_default12.prompt([
36638
36977
  {
36639
36978
  type: "list",
@@ -36749,6 +37088,15 @@ var pullCommand = new Command("pull").description("Pull latest experiment data f
36749
37088
  dim(` ${file}`);
36750
37089
  }
36751
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);
36752
37100
  const { proceed } = await esm_default12.prompt([
36753
37101
  {
36754
37102
  type: "confirm",
@@ -36784,14 +37132,16 @@ var pullCommand = new Command("pull").description("Pull latest experiment data f
36784
37132
 
36785
37133
  // src/commands/push.ts
36786
37134
  import { createHash as createHash3 } from "node:crypto";
36787
- import fs10 from "node:fs";
36788
- import path12 from "node:path";
37135
+
37136
+ // src/commands/push.payload.ts
37137
+ import fs11 from "node:fs";
37138
+ import path13 from "node:path";
36789
37139
 
36790
37140
  // src/lib/compiler.ts
36791
37141
  import { transform } from "esbuild";
36792
37142
  import * as sass from "sass";
36793
- import fs9 from "node:fs";
36794
- import path11 from "node:path";
37143
+ import fs10 from "node:fs";
37144
+ import path12 from "node:path";
36795
37145
  async function compileTypeScript(source) {
36796
37146
  const result = await transform(source, {
36797
37147
  loader: "ts",
@@ -36802,7 +37152,7 @@ async function compileTypeScript(source) {
36802
37152
  function compileScss(filePath) {
36803
37153
  const result = sass.compile(filePath, {
36804
37154
  style: "expanded",
36805
- loadPaths: [path11.dirname(filePath)]
37155
+ loadPaths: [path12.dirname(filePath)]
36806
37156
  });
36807
37157
  return result.css;
36808
37158
  }
@@ -36878,8 +37228,8 @@ function parseEsbuildErrors(err, fallbackFile) {
36878
37228
  ];
36879
37229
  }
36880
37230
  async function compileFile(filePath) {
36881
- const ext = path11.extname(filePath);
36882
- const source = fs9.readFileSync(filePath, "utf-8");
37231
+ const ext = path12.extname(filePath);
37232
+ const source = fs10.readFileSync(filePath, "utf-8");
36883
37233
  switch (ext) {
36884
37234
  case ".ts":
36885
37235
  return compileTypeScript(source);
@@ -36894,21 +37244,21 @@ async function compileFile(filePath) {
36894
37244
  }
36895
37245
  async function compileExperiment(dir, manifest) {
36896
37246
  const { script } = manifest.languages;
36897
- const triggerPath = path11.join(dir, `trigger.${script}`);
37247
+ const triggerPath = path12.join(dir, `trigger.${script}`);
36898
37248
  let triggersJs = "";
36899
- if (fs9.existsSync(triggerPath)) {
37249
+ if (fs10.existsSync(triggerPath)) {
36900
37250
  triggersJs = await compileFile(triggerPath);
36901
37251
  }
36902
37252
  const variations = [];
36903
37253
  for (const v of manifest.variations) {
36904
- const jsPath = path11.join(dir, v.folder, v.scriptFile);
36905
- 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);
36906
37256
  let js = "";
36907
37257
  let css = "";
36908
- if (fs9.existsSync(jsPath)) {
37258
+ if (fs10.existsSync(jsPath)) {
36909
37259
  js = await compileFile(jsPath);
36910
37260
  }
36911
- if (fs9.existsSync(cssPath)) {
37261
+ if (fs10.existsSync(cssPath)) {
36912
37262
  css = await compileFile(cssPath);
36913
37263
  }
36914
37264
  variations.push({
@@ -36927,14 +37277,91 @@ async function compileExperiment(dir, manifest) {
36927
37277
  };
36928
37278
  }
36929
37279
  function detectChangeType(filePath) {
36930
- const basename3 = path11.basename(filePath, path11.extname(filePath));
36931
- const ext = path11.extname(filePath);
37280
+ const basename3 = path12.basename(filePath, path12.extname(filePath));
37281
+ const ext = path12.extname(filePath);
36932
37282
  if (basename3 === "trigger") return "trigger";
36933
37283
  if (ext === ".scss" || ext === ".css") return "css-only";
36934
37284
  if (ext === ".ts" || ext === ".js") return "js-only";
36935
37285
  return "both";
36936
37286
  }
36937
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
+
36938
37365
  // src/commands/push.ts
36939
37366
  function sha2563(content) {
36940
37367
  return createHash3("sha256").update(content).digest("hex");
@@ -36959,35 +37386,60 @@ function detectRemoteChanges(remoteExp, syncedHashes) {
36959
37386
  }
36960
37387
  return changed;
36961
37388
  }
36962
- 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;
36963
37417
  try {
36964
37418
  const config = ensureLoggedIn();
36965
- const { manifest, dir } = readManifestOrExit();
37419
+ const read = readManifestOrExit();
37420
+ manifest = read.manifest;
37421
+ const { dir } = read;
36966
37422
  let spinner = ora("Compiling experiment...").start();
36967
- 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
+ }
36968
37431
  const client = new ApiClient(config);
36969
37432
  if (!options.force && manifest.syncedHashes) {
36970
37433
  spinner.text = "Checking for remote changes...";
36971
37434
  const remote = await client.getExperiment(manifest.experimentShortId);
36972
- const remoteExp = remote.data.experiment;
36973
- const remoteChanges = detectRemoteChanges(remoteExp, manifest.syncedHashes);
37435
+ const remoteChanges = detectRemoteChanges(remote.data.experiment, manifest.syncedHashes);
36974
37436
  if (remoteChanges.length > 0) {
36975
37437
  spinner.warn("Remote has changes you haven't pulled");
36976
37438
  line();
36977
37439
  heading("Files changed on remote:");
36978
- for (const file of remoteChanges) {
36979
- dim(` ${file}`);
36980
- }
37440
+ for (const file of remoteChanges) dim(` ${file}`);
36981
37441
  line();
36982
- const { proceed } = await esm_default12.prompt([
36983
- {
36984
- type: "confirm",
36985
- name: "proceed",
36986
- message: "Pushing will overwrite remote changes. Continue?",
36987
- default: false
36988
- }
36989
- ]);
36990
- if (!proceed) {
37442
+ if (!await confirmOverwrite()) {
36991
37443
  info("Push aborted. Run `avsb pull` to fetch remote changes first.");
36992
37444
  warn("Note: pulling will overwrite your local files.");
36993
37445
  process.exit(EXIT.success);
@@ -36999,78 +37451,37 @@ var pushCommand = new Command("push").description("Compile and push local change
36999
37451
  } else {
37000
37452
  spinner.text = "Pushing to platform...";
37001
37453
  }
37002
- const scriptLang = manifest.languages.script === "ts" ? "TYPESCRIPT" : "JAVASCRIPT";
37003
- const styleLang = manifest.languages.style === "scss" ? "SCSS" : "CSS";
37004
- const rawTrigger = fs10.readFileSync(
37005
- path12.join(dir, `trigger.${manifest.languages.script}`),
37006
- "utf-8"
37007
- );
37008
- const triggerCompile = await compileScript(rawTrigger, scriptLang, `trigger.${manifest.languages.script}`);
37009
- if (!triggerCompile.success) {
37010
- spinner.fail("Trigger compilation failed");
37011
- for (const e of triggerCompile.errors) {
37012
- error(` trigger.${manifest.languages.script}:${e.line}:${e.column}: ${e.message}`);
37013
- }
37014
- process.exit(EXIT.failure);
37015
- }
37016
- const rawVariations = [];
37017
- const compiledVariations = [];
37018
- for (const v of manifest.variations) {
37019
- const rawJs = fs10.readFileSync(path12.join(dir, v.folder, v.scriptFile), "utf-8");
37020
- const rawCss = fs10.readFileSync(path12.join(dir, v.folder, v.styleFile), "utf-8");
37021
- const jsCompile = await compileScript(rawJs, scriptLang, `${v.name}.${manifest.languages.script}`);
37022
- const cssCompile = compileStyle(rawCss, styleLang, `${v.name}.${manifest.languages.style}`);
37023
- if (!jsCompile.success) {
37024
- spinner.fail(`Variation "${v.name}" JS compilation failed`);
37025
- for (const e of jsCompile.errors) error(` ${e.file}:${e.line}:${e.column}: ${e.message}`);
37026
- process.exit(EXIT.failure);
37027
- }
37028
- if (!cssCompile.success) {
37029
- spinner.fail(`Variation "${v.name}" CSS compilation failed`);
37030
- for (const e of cssCompile.errors) error(` ${e.file}:${e.line}:${e.column}: ${e.message}`);
37031
- process.exit(EXIT.failure);
37032
- }
37033
- rawVariations.push({ id: v.id, controlJs: rawJs, controlCss: rawCss });
37034
- compiledVariations.push({
37035
- id: v.id,
37036
- controlJs: { source: rawJs, compiled: jsCompile.code },
37037
- controlCss: { source: rawCss, compiled: cssCompile.code }
37038
- });
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;
37039
37460
  }
37040
- const response = await client.pushExperiment(manifest.experimentShortId, {
37041
- triggersJs: { source: rawTrigger, compiled: triggerCompile.code },
37042
- variations: compiledVariations
37043
- });
37044
- manifest.syncedHashes = computeSyncHashes(rawTrigger, rawVariations);
37461
+ manifest.syncedHashes = computeSyncHashes(built.sources.triggersJs, built.sources.variations);
37045
37462
  writeManifest(dir, manifest);
37046
37463
  spinner.succeed("Pushed successfully");
37047
37464
  line();
37048
37465
  if (response.data.hasPendingChanges) {
37049
- const { publish } = await esm_default12.prompt([
37050
- {
37051
- type: "confirm",
37052
- name: "publish",
37053
- message: "Experiment is running. Publish changes now?",
37054
- default: false
37055
- }
37056
- ]);
37057
- if (publish) {
37466
+ if (await shouldPublish(options, response.data.status)) {
37058
37467
  const publishSpinner = ora("Publishing changes...").start();
37059
- await client.publishChanges(manifest.experimentShortId);
37468
+ const published = await client.publishChanges(manifest.experimentShortId);
37060
37469
  publishSpinner.succeed("Changes published");
37470
+ if (published.data.warning) warn(published.data.warning.message);
37061
37471
  } else {
37062
- 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.");
37063
37473
  }
37064
37474
  }
37065
37475
  line();
37066
37476
  success("Push complete");
37067
37477
  } catch (err) {
37068
- failCommand(err);
37478
+ const current = manifest;
37479
+ failCommand(err, current ? { labelForPath: (bodyPath) => labelForPushPath(current, bodyPath) } : {});
37069
37480
  }
37070
37481
  });
37071
37482
 
37072
37483
  // src/commands/dev.ts
37073
- import path14 from "node:path";
37484
+ import path15 from "node:path";
37074
37485
 
37075
37486
  // node_modules/chokidar/esm/index.js
37076
37487
  import { stat as statcb } from "fs";
@@ -37148,7 +37559,7 @@ var ReaddirpStream = class extends Readable {
37148
37559
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
37149
37560
  const statMethod = opts.lstat ? lstat : stat;
37150
37561
  if (wantBigintFsStats) {
37151
- this._stat = (path22) => statMethod(path22, { bigint: true });
37562
+ this._stat = (path23) => statMethod(path23, { bigint: true });
37152
37563
  } else {
37153
37564
  this._stat = statMethod;
37154
37565
  }
@@ -37173,8 +37584,8 @@ var ReaddirpStream = class extends Readable {
37173
37584
  const par = this.parent;
37174
37585
  const fil = par && par.files;
37175
37586
  if (fil && fil.length > 0) {
37176
- const { path: path22, depth } = par;
37177
- 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));
37178
37589
  const awaited = await Promise.all(slice);
37179
37590
  for (const entry of awaited) {
37180
37591
  if (!entry)
@@ -37214,20 +37625,20 @@ var ReaddirpStream = class extends Readable {
37214
37625
  this.reading = false;
37215
37626
  }
37216
37627
  }
37217
- async _exploreDir(path22, depth) {
37628
+ async _exploreDir(path23, depth) {
37218
37629
  let files;
37219
37630
  try {
37220
- files = await readdir(path22, this._rdOptions);
37631
+ files = await readdir(path23, this._rdOptions);
37221
37632
  } catch (error2) {
37222
37633
  this._onError(error2);
37223
37634
  }
37224
- return { files, depth, path: path22 };
37635
+ return { files, depth, path: path23 };
37225
37636
  }
37226
- async _formatEntry(dirent, path22) {
37637
+ async _formatEntry(dirent, path23) {
37227
37638
  let entry;
37228
37639
  const basename3 = this._isDirent ? dirent.name : dirent;
37229
37640
  try {
37230
- const fullPath = presolve(pjoin(path22, basename3));
37641
+ const fullPath = presolve(pjoin(path23, basename3));
37231
37642
  entry = { path: prelative(this._root, fullPath), fullPath, basename: basename3 };
37232
37643
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
37233
37644
  } catch (err) {
@@ -37627,16 +38038,16 @@ var delFromSet = (main3, prop, item) => {
37627
38038
  };
37628
38039
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
37629
38040
  var FsWatchInstances = /* @__PURE__ */ new Map();
37630
- function createFsWatchInstance(path22, options, listener, errHandler, emitRaw) {
38041
+ function createFsWatchInstance(path23, options, listener, errHandler, emitRaw) {
37631
38042
  const handleEvent = (rawEvent, evPath) => {
37632
- listener(path22);
37633
- emitRaw(rawEvent, evPath, { watchedPath: path22 });
37634
- if (evPath && path22 !== evPath) {
37635
- 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));
37636
38047
  }
37637
38048
  };
37638
38049
  try {
37639
- return fs_watch(path22, {
38050
+ return fs_watch(path23, {
37640
38051
  persistent: options.persistent
37641
38052
  }, handleEvent);
37642
38053
  } catch (error2) {
@@ -37652,12 +38063,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
37652
38063
  listener(val1, val2, val3);
37653
38064
  });
37654
38065
  };
37655
- var setFsWatchListener = (path22, fullPath, options, handlers) => {
38066
+ var setFsWatchListener = (path23, fullPath, options, handlers) => {
37656
38067
  const { listener, errHandler, rawEmitter } = handlers;
37657
38068
  let cont = FsWatchInstances.get(fullPath);
37658
38069
  let watcher;
37659
38070
  if (!options.persistent) {
37660
- watcher = createFsWatchInstance(path22, options, listener, errHandler, rawEmitter);
38071
+ watcher = createFsWatchInstance(path23, options, listener, errHandler, rawEmitter);
37661
38072
  if (!watcher)
37662
38073
  return;
37663
38074
  return watcher.close.bind(watcher);
@@ -37668,7 +38079,7 @@ var setFsWatchListener = (path22, fullPath, options, handlers) => {
37668
38079
  addAndConvert(cont, KEY_RAW, rawEmitter);
37669
38080
  } else {
37670
38081
  watcher = createFsWatchInstance(
37671
- path22,
38082
+ path23,
37672
38083
  options,
37673
38084
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
37674
38085
  errHandler,
@@ -37683,7 +38094,7 @@ var setFsWatchListener = (path22, fullPath, options, handlers) => {
37683
38094
  cont.watcherUnusable = true;
37684
38095
  if (isWindows && error2.code === "EPERM") {
37685
38096
  try {
37686
- const fd = await open(path22, "r");
38097
+ const fd = await open(path23, "r");
37687
38098
  await fd.close();
37688
38099
  broadcastErr(error2);
37689
38100
  } catch (err) {
@@ -37714,7 +38125,7 @@ var setFsWatchListener = (path22, fullPath, options, handlers) => {
37714
38125
  };
37715
38126
  };
37716
38127
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
37717
- var setFsWatchFileListener = (path22, fullPath, options, handlers) => {
38128
+ var setFsWatchFileListener = (path23, fullPath, options, handlers) => {
37718
38129
  const { listener, rawEmitter } = handlers;
37719
38130
  let cont = FsWatchFileInstances.get(fullPath);
37720
38131
  const copts = cont && cont.options;
@@ -37736,7 +38147,7 @@ var setFsWatchFileListener = (path22, fullPath, options, handlers) => {
37736
38147
  });
37737
38148
  const currmtime = curr.mtimeMs;
37738
38149
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
37739
- foreach(cont.listeners, (listener2) => listener2(path22, curr));
38150
+ foreach(cont.listeners, (listener2) => listener2(path23, curr));
37740
38151
  }
37741
38152
  })
37742
38153
  };
@@ -37764,13 +38175,13 @@ var NodeFsHandler = class {
37764
38175
  * @param listener on fs change
37765
38176
  * @returns closer for the watcher instance
37766
38177
  */
37767
- _watchWithNodeFs(path22, listener) {
38178
+ _watchWithNodeFs(path23, listener) {
37768
38179
  const opts = this.fsw.options;
37769
- const directory = sysPath.dirname(path22);
37770
- const basename3 = sysPath.basename(path22);
38180
+ const directory = sysPath.dirname(path23);
38181
+ const basename3 = sysPath.basename(path23);
37771
38182
  const parent = this.fsw._getWatchedDir(directory);
37772
38183
  parent.add(basename3);
37773
- const absolutePath = sysPath.resolve(path22);
38184
+ const absolutePath = sysPath.resolve(path23);
37774
38185
  const options = {
37775
38186
  persistent: opts.persistent
37776
38187
  };
@@ -37780,12 +38191,12 @@ var NodeFsHandler = class {
37780
38191
  if (opts.usePolling) {
37781
38192
  const enableBin = opts.interval !== opts.binaryInterval;
37782
38193
  options.interval = enableBin && isBinaryPath(basename3) ? opts.binaryInterval : opts.interval;
37783
- closer = setFsWatchFileListener(path22, absolutePath, options, {
38194
+ closer = setFsWatchFileListener(path23, absolutePath, options, {
37784
38195
  listener,
37785
38196
  rawEmitter: this.fsw._emitRaw
37786
38197
  });
37787
38198
  } else {
37788
- closer = setFsWatchListener(path22, absolutePath, options, {
38199
+ closer = setFsWatchListener(path23, absolutePath, options, {
37789
38200
  listener,
37790
38201
  errHandler: this._boundHandleError,
37791
38202
  rawEmitter: this.fsw._emitRaw
@@ -37807,7 +38218,7 @@ var NodeFsHandler = class {
37807
38218
  let prevStats = stats;
37808
38219
  if (parent.has(basename3))
37809
38220
  return;
37810
- const listener = async (path22, newStats) => {
38221
+ const listener = async (path23, newStats) => {
37811
38222
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
37812
38223
  return;
37813
38224
  if (!newStats || newStats.mtimeMs === 0) {
@@ -37821,11 +38232,11 @@ var NodeFsHandler = class {
37821
38232
  this.fsw._emit(EV.CHANGE, file, newStats2);
37822
38233
  }
37823
38234
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
37824
- this.fsw._closeFile(path22);
38235
+ this.fsw._closeFile(path23);
37825
38236
  prevStats = newStats2;
37826
38237
  const closer2 = this._watchWithNodeFs(file, listener);
37827
38238
  if (closer2)
37828
- this.fsw._addPathCloser(path22, closer2);
38239
+ this.fsw._addPathCloser(path23, closer2);
37829
38240
  } else {
37830
38241
  prevStats = newStats2;
37831
38242
  }
@@ -37857,7 +38268,7 @@ var NodeFsHandler = class {
37857
38268
  * @param item basename of this item
37858
38269
  * @returns true if no more processing is needed for this entry.
37859
38270
  */
37860
- async _handleSymlink(entry, directory, path22, item) {
38271
+ async _handleSymlink(entry, directory, path23, item) {
37861
38272
  if (this.fsw.closed) {
37862
38273
  return;
37863
38274
  }
@@ -37867,7 +38278,7 @@ var NodeFsHandler = class {
37867
38278
  this.fsw._incrReadyCount();
37868
38279
  let linkPath;
37869
38280
  try {
37870
- linkPath = await fsrealpath(path22);
38281
+ linkPath = await fsrealpath(path23);
37871
38282
  } catch (e) {
37872
38283
  this.fsw._emitReady();
37873
38284
  return true;
@@ -37877,12 +38288,12 @@ var NodeFsHandler = class {
37877
38288
  if (dir.has(item)) {
37878
38289
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
37879
38290
  this.fsw._symlinkPaths.set(full, linkPath);
37880
- this.fsw._emit(EV.CHANGE, path22, entry.stats);
38291
+ this.fsw._emit(EV.CHANGE, path23, entry.stats);
37881
38292
  }
37882
38293
  } else {
37883
38294
  dir.add(item);
37884
38295
  this.fsw._symlinkPaths.set(full, linkPath);
37885
- this.fsw._emit(EV.ADD, path22, entry.stats);
38296
+ this.fsw._emit(EV.ADD, path23, entry.stats);
37886
38297
  }
37887
38298
  this.fsw._emitReady();
37888
38299
  return true;
@@ -37911,9 +38322,9 @@ var NodeFsHandler = class {
37911
38322
  return;
37912
38323
  }
37913
38324
  const item = entry.path;
37914
- let path22 = sysPath.join(directory, item);
38325
+ let path23 = sysPath.join(directory, item);
37915
38326
  current.add(item);
37916
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path22, item)) {
38327
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path23, item)) {
37917
38328
  return;
37918
38329
  }
37919
38330
  if (this.fsw.closed) {
@@ -37922,8 +38333,8 @@ var NodeFsHandler = class {
37922
38333
  }
37923
38334
  if (item === target || !target && !previous.has(item)) {
37924
38335
  this.fsw._incrReadyCount();
37925
- path22 = sysPath.join(dir, sysPath.relative(dir, path22));
37926
- this._addToNodeFs(path22, initialAdd, wh, depth + 1);
38336
+ path23 = sysPath.join(dir, sysPath.relative(dir, path23));
38337
+ this._addToNodeFs(path23, initialAdd, wh, depth + 1);
37927
38338
  }
37928
38339
  }).on(EV.ERROR, this._boundHandleError);
37929
38340
  return new Promise((resolve3, reject) => {
@@ -37992,13 +38403,13 @@ var NodeFsHandler = class {
37992
38403
  * @param depth Child path actually targeted for watch
37993
38404
  * @param target Child path actually targeted for watch
37994
38405
  */
37995
- async _addToNodeFs(path22, initialAdd, priorWh, depth, target) {
38406
+ async _addToNodeFs(path23, initialAdd, priorWh, depth, target) {
37996
38407
  const ready = this.fsw._emitReady;
37997
- if (this.fsw._isIgnored(path22) || this.fsw.closed) {
38408
+ if (this.fsw._isIgnored(path23) || this.fsw.closed) {
37998
38409
  ready();
37999
38410
  return false;
38000
38411
  }
38001
- const wh = this.fsw._getWatchHelpers(path22);
38412
+ const wh = this.fsw._getWatchHelpers(path23);
38002
38413
  if (priorWh) {
38003
38414
  wh.filterPath = (entry) => priorWh.filterPath(entry);
38004
38415
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -38014,8 +38425,8 @@ var NodeFsHandler = class {
38014
38425
  const follow = this.fsw.options.followSymlinks;
38015
38426
  let closer;
38016
38427
  if (stats.isDirectory()) {
38017
- const absPath = sysPath.resolve(path22);
38018
- const targetPath = follow ? await fsrealpath(path22) : path22;
38428
+ const absPath = sysPath.resolve(path23);
38429
+ const targetPath = follow ? await fsrealpath(path23) : path23;
38019
38430
  if (this.fsw.closed)
38020
38431
  return;
38021
38432
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -38025,29 +38436,29 @@ var NodeFsHandler = class {
38025
38436
  this.fsw._symlinkPaths.set(absPath, targetPath);
38026
38437
  }
38027
38438
  } else if (stats.isSymbolicLink()) {
38028
- const targetPath = follow ? await fsrealpath(path22) : path22;
38439
+ const targetPath = follow ? await fsrealpath(path23) : path23;
38029
38440
  if (this.fsw.closed)
38030
38441
  return;
38031
38442
  const parent = sysPath.dirname(wh.watchPath);
38032
38443
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
38033
38444
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
38034
- closer = await this._handleDir(parent, stats, initialAdd, depth, path22, wh, targetPath);
38445
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path23, wh, targetPath);
38035
38446
  if (this.fsw.closed)
38036
38447
  return;
38037
38448
  if (targetPath !== void 0) {
38038
- this.fsw._symlinkPaths.set(sysPath.resolve(path22), targetPath);
38449
+ this.fsw._symlinkPaths.set(sysPath.resolve(path23), targetPath);
38039
38450
  }
38040
38451
  } else {
38041
38452
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
38042
38453
  }
38043
38454
  ready();
38044
38455
  if (closer)
38045
- this.fsw._addPathCloser(path22, closer);
38456
+ this.fsw._addPathCloser(path23, closer);
38046
38457
  return false;
38047
38458
  } catch (error2) {
38048
38459
  if (this.fsw._handleError(error2)) {
38049
38460
  ready();
38050
- return path22;
38461
+ return path23;
38051
38462
  }
38052
38463
  }
38053
38464
  }
@@ -38090,26 +38501,26 @@ function createPattern(matcher) {
38090
38501
  }
38091
38502
  return () => false;
38092
38503
  }
38093
- function normalizePath(path22) {
38094
- if (typeof path22 !== "string")
38504
+ function normalizePath(path23) {
38505
+ if (typeof path23 !== "string")
38095
38506
  throw new Error("string expected");
38096
- path22 = sysPath2.normalize(path22);
38097
- path22 = path22.replace(/\\/g, "/");
38507
+ path23 = sysPath2.normalize(path23);
38508
+ path23 = path23.replace(/\\/g, "/");
38098
38509
  let prepend = false;
38099
- if (path22.startsWith("//"))
38510
+ if (path23.startsWith("//"))
38100
38511
  prepend = true;
38101
38512
  const DOUBLE_SLASH_RE2 = /\/\//;
38102
- while (path22.match(DOUBLE_SLASH_RE2))
38103
- path22 = path22.replace(DOUBLE_SLASH_RE2, "/");
38513
+ while (path23.match(DOUBLE_SLASH_RE2))
38514
+ path23 = path23.replace(DOUBLE_SLASH_RE2, "/");
38104
38515
  if (prepend)
38105
- path22 = "/" + path22;
38106
- return path22;
38516
+ path23 = "/" + path23;
38517
+ return path23;
38107
38518
  }
38108
38519
  function matchPatterns(patterns, testString, stats) {
38109
- const path22 = normalizePath(testString);
38520
+ const path23 = normalizePath(testString);
38110
38521
  for (let index = 0; index < patterns.length; index++) {
38111
38522
  const pattern = patterns[index];
38112
- if (pattern(path22, stats)) {
38523
+ if (pattern(path23, stats)) {
38113
38524
  return true;
38114
38525
  }
38115
38526
  }
@@ -38149,19 +38560,19 @@ var toUnix = (string) => {
38149
38560
  }
38150
38561
  return str;
38151
38562
  };
38152
- var normalizePathToUnix = (path22) => toUnix(sysPath2.normalize(toUnix(path22)));
38153
- var normalizeIgnored = (cwd = "") => (path22) => {
38154
- if (typeof path22 === "string") {
38155
- 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));
38156
38567
  } else {
38157
- return path22;
38568
+ return path23;
38158
38569
  }
38159
38570
  };
38160
- var getAbsolutePath = (path22, cwd) => {
38161
- if (sysPath2.isAbsolute(path22)) {
38162
- return path22;
38571
+ var getAbsolutePath = (path23, cwd) => {
38572
+ if (sysPath2.isAbsolute(path23)) {
38573
+ return path23;
38163
38574
  }
38164
- return sysPath2.join(cwd, path22);
38575
+ return sysPath2.join(cwd, path23);
38165
38576
  };
38166
38577
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
38167
38578
  var DirEntry = class {
@@ -38216,10 +38627,10 @@ var DirEntry = class {
38216
38627
  var STAT_METHOD_F = "stat";
38217
38628
  var STAT_METHOD_L = "lstat";
38218
38629
  var WatchHelper = class {
38219
- constructor(path22, follow, fsw) {
38630
+ constructor(path23, follow, fsw) {
38220
38631
  this.fsw = fsw;
38221
- const watchPath = path22;
38222
- this.path = path22 = path22.replace(REPLACER_RE, "");
38632
+ const watchPath = path23;
38633
+ this.path = path23 = path23.replace(REPLACER_RE, "");
38223
38634
  this.watchPath = watchPath;
38224
38635
  this.fullWatchPath = sysPath2.resolve(watchPath);
38225
38636
  this.dirParts = [];
@@ -38341,20 +38752,20 @@ var FSWatcher = class extends EventEmitter {
38341
38752
  this._closePromise = void 0;
38342
38753
  let paths = unifyPaths(paths_);
38343
38754
  if (cwd) {
38344
- paths = paths.map((path22) => {
38345
- const absPath = getAbsolutePath(path22, cwd);
38755
+ paths = paths.map((path23) => {
38756
+ const absPath = getAbsolutePath(path23, cwd);
38346
38757
  return absPath;
38347
38758
  });
38348
38759
  }
38349
- paths.forEach((path22) => {
38350
- this._removeIgnoredPath(path22);
38760
+ paths.forEach((path23) => {
38761
+ this._removeIgnoredPath(path23);
38351
38762
  });
38352
38763
  this._userIgnored = void 0;
38353
38764
  if (!this._readyCount)
38354
38765
  this._readyCount = 0;
38355
38766
  this._readyCount += paths.length;
38356
- Promise.all(paths.map(async (path22) => {
38357
- 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);
38358
38769
  if (res)
38359
38770
  this._emitReady();
38360
38771
  return res;
@@ -38376,17 +38787,17 @@ var FSWatcher = class extends EventEmitter {
38376
38787
  return this;
38377
38788
  const paths = unifyPaths(paths_);
38378
38789
  const { cwd } = this.options;
38379
- paths.forEach((path22) => {
38380
- if (!sysPath2.isAbsolute(path22) && !this._closers.has(path22)) {
38790
+ paths.forEach((path23) => {
38791
+ if (!sysPath2.isAbsolute(path23) && !this._closers.has(path23)) {
38381
38792
  if (cwd)
38382
- path22 = sysPath2.join(cwd, path22);
38383
- path22 = sysPath2.resolve(path22);
38793
+ path23 = sysPath2.join(cwd, path23);
38794
+ path23 = sysPath2.resolve(path23);
38384
38795
  }
38385
- this._closePath(path22);
38386
- this._addIgnoredPath(path22);
38387
- if (this._watched.has(path22)) {
38796
+ this._closePath(path23);
38797
+ this._addIgnoredPath(path23);
38798
+ if (this._watched.has(path23)) {
38388
38799
  this._addIgnoredPath({
38389
- path: path22,
38800
+ path: path23,
38390
38801
  recursive: true
38391
38802
  });
38392
38803
  }
@@ -38450,38 +38861,38 @@ var FSWatcher = class extends EventEmitter {
38450
38861
  * @param stats arguments to be passed with event
38451
38862
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
38452
38863
  */
38453
- async _emit(event, path22, stats) {
38864
+ async _emit(event, path23, stats) {
38454
38865
  if (this.closed)
38455
38866
  return;
38456
38867
  const opts = this.options;
38457
38868
  if (isWindows)
38458
- path22 = sysPath2.normalize(path22);
38869
+ path23 = sysPath2.normalize(path23);
38459
38870
  if (opts.cwd)
38460
- path22 = sysPath2.relative(opts.cwd, path22);
38461
- const args = [path22];
38871
+ path23 = sysPath2.relative(opts.cwd, path23);
38872
+ const args = [path23];
38462
38873
  if (stats != null)
38463
38874
  args.push(stats);
38464
38875
  const awf = opts.awaitWriteFinish;
38465
38876
  let pw;
38466
- if (awf && (pw = this._pendingWrites.get(path22))) {
38877
+ if (awf && (pw = this._pendingWrites.get(path23))) {
38467
38878
  pw.lastChange = /* @__PURE__ */ new Date();
38468
38879
  return this;
38469
38880
  }
38470
38881
  if (opts.atomic) {
38471
38882
  if (event === EVENTS.UNLINK) {
38472
- this._pendingUnlinks.set(path22, [event, ...args]);
38883
+ this._pendingUnlinks.set(path23, [event, ...args]);
38473
38884
  setTimeout(() => {
38474
- this._pendingUnlinks.forEach((entry, path23) => {
38885
+ this._pendingUnlinks.forEach((entry, path24) => {
38475
38886
  this.emit(...entry);
38476
38887
  this.emit(EVENTS.ALL, ...entry);
38477
- this._pendingUnlinks.delete(path23);
38888
+ this._pendingUnlinks.delete(path24);
38478
38889
  });
38479
38890
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
38480
38891
  return this;
38481
38892
  }
38482
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path22)) {
38893
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path23)) {
38483
38894
  event = EVENTS.CHANGE;
38484
- this._pendingUnlinks.delete(path22);
38895
+ this._pendingUnlinks.delete(path23);
38485
38896
  }
38486
38897
  }
38487
38898
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -38499,16 +38910,16 @@ var FSWatcher = class extends EventEmitter {
38499
38910
  this.emitWithAll(event, args);
38500
38911
  }
38501
38912
  };
38502
- this._awaitWriteFinish(path22, awf.stabilityThreshold, event, awfEmit);
38913
+ this._awaitWriteFinish(path23, awf.stabilityThreshold, event, awfEmit);
38503
38914
  return this;
38504
38915
  }
38505
38916
  if (event === EVENTS.CHANGE) {
38506
- const isThrottled = !this._throttle(EVENTS.CHANGE, path22, 50);
38917
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path23, 50);
38507
38918
  if (isThrottled)
38508
38919
  return this;
38509
38920
  }
38510
38921
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
38511
- const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path22) : path22;
38922
+ const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path23) : path23;
38512
38923
  let stats2;
38513
38924
  try {
38514
38925
  stats2 = await stat3(fullPath);
@@ -38539,23 +38950,23 @@ var FSWatcher = class extends EventEmitter {
38539
38950
  * @param timeout duration of time to suppress duplicate actions
38540
38951
  * @returns tracking object or false if action should be suppressed
38541
38952
  */
38542
- _throttle(actionType, path22, timeout) {
38953
+ _throttle(actionType, path23, timeout) {
38543
38954
  if (!this._throttled.has(actionType)) {
38544
38955
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
38545
38956
  }
38546
38957
  const action = this._throttled.get(actionType);
38547
38958
  if (!action)
38548
38959
  throw new Error("invalid throttle");
38549
- const actionPath = action.get(path22);
38960
+ const actionPath = action.get(path23);
38550
38961
  if (actionPath) {
38551
38962
  actionPath.count++;
38552
38963
  return false;
38553
38964
  }
38554
38965
  let timeoutObject;
38555
38966
  const clear = () => {
38556
- const item = action.get(path22);
38967
+ const item = action.get(path23);
38557
38968
  const count = item ? item.count : 0;
38558
- action.delete(path22);
38969
+ action.delete(path23);
38559
38970
  clearTimeout(timeoutObject);
38560
38971
  if (item)
38561
38972
  clearTimeout(item.timeoutObject);
@@ -38563,7 +38974,7 @@ var FSWatcher = class extends EventEmitter {
38563
38974
  };
38564
38975
  timeoutObject = setTimeout(clear, timeout);
38565
38976
  const thr = { timeoutObject, clear, count: 0 };
38566
- action.set(path22, thr);
38977
+ action.set(path23, thr);
38567
38978
  return thr;
38568
38979
  }
38569
38980
  _incrReadyCount() {
@@ -38577,44 +38988,44 @@ var FSWatcher = class extends EventEmitter {
38577
38988
  * @param event
38578
38989
  * @param awfEmit Callback to be called when ready for event to be emitted.
38579
38990
  */
38580
- _awaitWriteFinish(path22, threshold, event, awfEmit) {
38991
+ _awaitWriteFinish(path23, threshold, event, awfEmit) {
38581
38992
  const awf = this.options.awaitWriteFinish;
38582
38993
  if (typeof awf !== "object")
38583
38994
  return;
38584
38995
  const pollInterval = awf.pollInterval;
38585
38996
  let timeoutHandler;
38586
- let fullPath = path22;
38587
- if (this.options.cwd && !sysPath2.isAbsolute(path22)) {
38588
- 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);
38589
39000
  }
38590
39001
  const now = /* @__PURE__ */ new Date();
38591
39002
  const writes = this._pendingWrites;
38592
39003
  function awaitWriteFinishFn(prevStat) {
38593
39004
  statcb(fullPath, (err, curStat) => {
38594
- if (err || !writes.has(path22)) {
39005
+ if (err || !writes.has(path23)) {
38595
39006
  if (err && err.code !== "ENOENT")
38596
39007
  awfEmit(err);
38597
39008
  return;
38598
39009
  }
38599
39010
  const now2 = Number(/* @__PURE__ */ new Date());
38600
39011
  if (prevStat && curStat.size !== prevStat.size) {
38601
- writes.get(path22).lastChange = now2;
39012
+ writes.get(path23).lastChange = now2;
38602
39013
  }
38603
- const pw = writes.get(path22);
39014
+ const pw = writes.get(path23);
38604
39015
  const df = now2 - pw.lastChange;
38605
39016
  if (df >= threshold) {
38606
- writes.delete(path22);
39017
+ writes.delete(path23);
38607
39018
  awfEmit(void 0, curStat);
38608
39019
  } else {
38609
39020
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
38610
39021
  }
38611
39022
  });
38612
39023
  }
38613
- if (!writes.has(path22)) {
38614
- writes.set(path22, {
39024
+ if (!writes.has(path23)) {
39025
+ writes.set(path23, {
38615
39026
  lastChange: now,
38616
39027
  cancelWait: () => {
38617
- writes.delete(path22);
39028
+ writes.delete(path23);
38618
39029
  clearTimeout(timeoutHandler);
38619
39030
  return event;
38620
39031
  }
@@ -38625,8 +39036,8 @@ var FSWatcher = class extends EventEmitter {
38625
39036
  /**
38626
39037
  * Determines whether user has asked to ignore this path.
38627
39038
  */
38628
- _isIgnored(path22, stats) {
38629
- if (this.options.atomic && DOT_RE.test(path22))
39039
+ _isIgnored(path23, stats) {
39040
+ if (this.options.atomic && DOT_RE.test(path23))
38630
39041
  return true;
38631
39042
  if (!this._userIgnored) {
38632
39043
  const { cwd } = this.options;
@@ -38636,17 +39047,17 @@ var FSWatcher = class extends EventEmitter {
38636
39047
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
38637
39048
  this._userIgnored = anymatch(list, void 0);
38638
39049
  }
38639
- return this._userIgnored(path22, stats);
39050
+ return this._userIgnored(path23, stats);
38640
39051
  }
38641
- _isntIgnored(path22, stat4) {
38642
- return !this._isIgnored(path22, stat4);
39052
+ _isntIgnored(path23, stat4) {
39053
+ return !this._isIgnored(path23, stat4);
38643
39054
  }
38644
39055
  /**
38645
39056
  * Provides a set of common helpers and properties relating to symlink handling.
38646
39057
  * @param path file or directory pattern being watched
38647
39058
  */
38648
- _getWatchHelpers(path22) {
38649
- return new WatchHelper(path22, this.options.followSymlinks, this);
39059
+ _getWatchHelpers(path23) {
39060
+ return new WatchHelper(path23, this.options.followSymlinks, this);
38650
39061
  }
38651
39062
  // Directory helpers
38652
39063
  // -----------------
@@ -38678,63 +39089,63 @@ var FSWatcher = class extends EventEmitter {
38678
39089
  * @param item base path of item/directory
38679
39090
  */
38680
39091
  _remove(directory, item, isDirectory) {
38681
- const path22 = sysPath2.join(directory, item);
38682
- const fullPath = sysPath2.resolve(path22);
38683
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path22) || this._watched.has(fullPath);
38684
- 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))
38685
39096
  return;
38686
39097
  if (!isDirectory && this._watched.size === 1) {
38687
39098
  this.add(directory, item, true);
38688
39099
  }
38689
- const wp = this._getWatchedDir(path22);
39100
+ const wp = this._getWatchedDir(path23);
38690
39101
  const nestedDirectoryChildren = wp.getChildren();
38691
- nestedDirectoryChildren.forEach((nested) => this._remove(path22, nested));
39102
+ nestedDirectoryChildren.forEach((nested) => this._remove(path23, nested));
38692
39103
  const parent = this._getWatchedDir(directory);
38693
39104
  const wasTracked = parent.has(item);
38694
39105
  parent.remove(item);
38695
39106
  if (this._symlinkPaths.has(fullPath)) {
38696
39107
  this._symlinkPaths.delete(fullPath);
38697
39108
  }
38698
- let relPath = path22;
39109
+ let relPath = path23;
38699
39110
  if (this.options.cwd)
38700
- relPath = sysPath2.relative(this.options.cwd, path22);
39111
+ relPath = sysPath2.relative(this.options.cwd, path23);
38701
39112
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
38702
39113
  const event = this._pendingWrites.get(relPath).cancelWait();
38703
39114
  if (event === EVENTS.ADD)
38704
39115
  return;
38705
39116
  }
38706
- this._watched.delete(path22);
39117
+ this._watched.delete(path23);
38707
39118
  this._watched.delete(fullPath);
38708
39119
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
38709
- if (wasTracked && !this._isIgnored(path22))
38710
- this._emit(eventName, path22);
38711
- this._closePath(path22);
39120
+ if (wasTracked && !this._isIgnored(path23))
39121
+ this._emit(eventName, path23);
39122
+ this._closePath(path23);
38712
39123
  }
38713
39124
  /**
38714
39125
  * Closes all watchers for a path
38715
39126
  */
38716
- _closePath(path22) {
38717
- this._closeFile(path22);
38718
- const dir = sysPath2.dirname(path22);
38719
- 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));
38720
39131
  }
38721
39132
  /**
38722
39133
  * Closes only file-specific watchers
38723
39134
  */
38724
- _closeFile(path22) {
38725
- const closers = this._closers.get(path22);
39135
+ _closeFile(path23) {
39136
+ const closers = this._closers.get(path23);
38726
39137
  if (!closers)
38727
39138
  return;
38728
39139
  closers.forEach((closer) => closer());
38729
- this._closers.delete(path22);
39140
+ this._closers.delete(path23);
38730
39141
  }
38731
- _addPathCloser(path22, closer) {
39142
+ _addPathCloser(path23, closer) {
38732
39143
  if (!closer)
38733
39144
  return;
38734
- let list = this._closers.get(path22);
39145
+ let list = this._closers.get(path23);
38735
39146
  if (!list) {
38736
39147
  list = [];
38737
- this._closers.set(path22, list);
39148
+ this._closers.set(path23, list);
38738
39149
  }
38739
39150
  list.push(closer);
38740
39151
  }
@@ -38764,7 +39175,7 @@ function watch(paths, options = {}) {
38764
39175
  var esm_default13 = { watch, FSWatcher };
38765
39176
 
38766
39177
  // src/lib/watcher.ts
38767
- import path13 from "node:path";
39178
+ import path14 from "node:path";
38768
39179
  function startWatcher(options) {
38769
39180
  const { dir, extensions, onChange } = options;
38770
39181
  const extSet = new Set(extensions);
@@ -38778,7 +39189,7 @@ function startWatcher(options) {
38778
39189
  });
38779
39190
  let debounceTimer = null;
38780
39191
  const handleChange2 = (filePath) => {
38781
- const ext = path13.extname(filePath);
39192
+ const ext = path14.extname(filePath);
38782
39193
  if (!extSet.has(ext)) return;
38783
39194
  if (debounceTimer) clearTimeout(debounceTimer);
38784
39195
  debounceTimer = setTimeout(() => {
@@ -38804,29 +39215,30 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
38804
39215
  import http from "node:http";
38805
39216
  var PORT_START = 4400;
38806
39217
  var PORT_END = 4409;
39218
+ var PING_INTERVAL_MS = 3e4;
39219
+ function portsBusyError(start, end) {
39220
+ return new CliError("cli_failed", `Ports ${start} to ${end} are all busy.`, [
39221
+ "Another `avsb dev` (or another program) is holding them. Stop it, then run this again.",
39222
+ `\`lsof -i :${start}\` shows what is listening on a port.`
39223
+ ]);
39224
+ }
39225
+ function isAddressInUse(err) {
39226
+ return typeof err === "object" && err !== null && "code" in err && err.code === "EADDRINUSE";
39227
+ }
38807
39228
  async function startDevServer(options) {
38808
39229
  const { experimentName, experimentId, onExtensionMessage } = options;
38809
- let resolvedPort = 0;
38810
- let httpServer = null;
38811
- let wss = null;
38812
- for (let port = PORT_START; port <= PORT_END; port++) {
39230
+ const start = options.portRange?.start ?? PORT_START;
39231
+ const end = options.portRange?.end ?? PORT_END;
39232
+ let bound = null;
39233
+ for (let port2 = start; port2 <= end && !bound; port2++) {
38813
39234
  try {
38814
- const result = await tryPort(port, experimentName, experimentId, onExtensionMessage);
38815
- httpServer = result.httpServer;
38816
- wss = result.wss;
38817
- resolvedPort = port;
38818
- break;
38819
- } catch {
38820
- continue;
39235
+ bound = { ...await tryPort(port2, experimentName, experimentId, onExtensionMessage), port: port2 };
39236
+ } catch (err) {
39237
+ if (!isAddressInUse(err)) throw err;
38821
39238
  }
38822
39239
  }
38823
- if (!httpServer || !wss) {
38824
- throw new Error(`Could not find available port in range ${PORT_START}-${PORT_END}`);
38825
- }
38826
- const pingInterval = setInterval(() => {
38827
- const msg = { type: "ping", timestamp: Date.now() };
38828
- broadcast(JSON.stringify(msg));
38829
- }, 3e4);
39240
+ if (!bound) throw portsBusyError(start, end);
39241
+ const { httpServer, wss, port } = bound;
38830
39242
  function broadcast(data) {
38831
39243
  wss.clients.forEach((client) => {
38832
39244
  if (client.readyState === import_websocket.default.OPEN) {
@@ -38834,8 +39246,12 @@ async function startDevServer(options) {
38834
39246
  }
38835
39247
  });
38836
39248
  }
39249
+ const pingInterval = setInterval(() => {
39250
+ const msg = { type: "ping", timestamp: Date.now() };
39251
+ broadcast(JSON.stringify(msg));
39252
+ }, PING_INTERVAL_MS);
38837
39253
  return {
38838
- port: resolvedPort,
39254
+ port,
38839
39255
  broadcast(msg) {
38840
39256
  broadcast(JSON.stringify(msg));
38841
39257
  },
@@ -38878,6 +39294,16 @@ function tryPort(port, experimentName, experimentId, onExtensionMessage) {
38878
39294
  );
38879
39295
  });
38880
39296
  const wss = new import_websocket_server.default({ server: httpServer });
39297
+ let settled = false;
39298
+ const fail2 = (err) => {
39299
+ if (settled) return;
39300
+ settled = true;
39301
+ wss.close();
39302
+ httpServer.close();
39303
+ reject(err);
39304
+ };
39305
+ httpServer.on("error", fail2);
39306
+ wss.on("error", fail2);
38881
39307
  wss.on("connection", (ws) => {
38882
39308
  ws.on("message", (raw) => {
38883
39309
  try {
@@ -38887,8 +39313,11 @@ function tryPort(port, experimentName, experimentId, onExtensionMessage) {
38887
39313
  }
38888
39314
  });
38889
39315
  });
38890
- httpServer.on("error", reject);
38891
- httpServer.listen(port, () => resolve3({ httpServer, wss }));
39316
+ httpServer.listen(port, () => {
39317
+ if (settled) return;
39318
+ settled = true;
39319
+ resolve3({ httpServer, wss });
39320
+ });
38892
39321
  });
38893
39322
  }
38894
39323
 
@@ -38944,7 +39373,7 @@ var devCommand = new Command("dev").description("Start dev server with live relo
38944
39373
  dir,
38945
39374
  extensions: [".ts", ".js", ".scss", ".css"],
38946
39375
  onChange: async (filePath) => {
38947
- const relativePath = path14.relative(dir, filePath);
39376
+ const relativePath = path15.relative(dir, filePath);
38948
39377
  info(`File changed: ${relativePath}`);
38949
39378
  try {
38950
39379
  compiled = await compileExperiment(dir, manifest);
@@ -39060,15 +39489,15 @@ var statusCommand = withOutputOptions(
39060
39489
  heading(manifest.experimentName);
39061
39490
  line();
39062
39491
  for (const row of buildStatusRows(manifest)) {
39063
- label(row.label, row.value);
39492
+ field(row.label, row.value);
39064
39493
  }
39065
39494
  line();
39066
39495
  });
39067
39496
 
39068
39497
  // src/commands/project.ts
39069
39498
  import { createHash as createHash4 } from "node:crypto";
39070
- import fs11 from "node:fs";
39071
- import path15 from "node:path";
39499
+ import fs12 from "node:fs";
39500
+ import path16 from "node:path";
39072
39501
  function sha2564(content) {
39073
39502
  return createHash4("sha256").update(content).digest("hex");
39074
39503
  }
@@ -39099,16 +39528,29 @@ var projectPullCommand = new Command("pull").description("Pull project script fr
39099
39528
  label("ID", `PRJ-${project.shortId}`);
39100
39529
  label("Language", project.projectJsLang === "TYPESCRIPT" ? "TypeScript" : "JavaScript");
39101
39530
  line();
39102
- const folderName = `project-${project.shortId}`;
39103
- const dir = path15.join(process.cwd(), folderName);
39104
- if (fs11.existsSync(dir)) {
39531
+ const target = resolveProjectPullTarget(process.cwd(), project.shortId);
39532
+ const dir = target.dir;
39533
+ if (target.kind === "blocked") {
39534
+ failCommand(
39535
+ new CliError("cli_usage", `${dir} already exists and is not a pulled copy of PRJ-${project.shortId}.`, [
39536
+ "Move or rename it, or run `avsb project pull` from another folder."
39537
+ ])
39538
+ );
39539
+ }
39540
+ if (target.kind === "existing") {
39105
39541
  const { manifest } = readProjectManifestOrExit(dir);
39106
39542
  if (!options.force) {
39107
39543
  const scriptFile = `project.${manifest.language}`;
39108
- const scriptPath = path15.join(dir, scriptFile);
39109
- const localContent = fs11.existsSync(scriptPath) ? fs11.readFileSync(scriptPath, "utf-8") : "";
39544
+ const scriptPath = path16.join(dir, scriptFile);
39545
+ const localContent = fs12.existsSync(scriptPath) ? fs12.readFileSync(scriptPath, "utf-8") : "";
39110
39546
  const remoteContent = manifest.language === "ts" ? project.editorProjectTs ?? project.editorProjectJs ?? "" : project.editorProjectJs ?? "";
39111
39547
  if (localContent !== remoteContent && localContent !== "") {
39548
+ const refusal = refusalWithoutPrompt({
39549
+ code: "cli_conflict",
39550
+ message: `Pulling would overwrite your local ${scriptFile}, and this run cannot ask first.`,
39551
+ hints: [`Run \`avsb project pull ${projectInput} --force\` to overwrite it.`]
39552
+ });
39553
+ if (refusal) failCommand(refusal);
39112
39554
  const { proceed } = await esm_default12.prompt([
39113
39555
  {
39114
39556
  type: "confirm",
@@ -39126,7 +39568,7 @@ var projectPullCommand = new Command("pull").description("Pull project script fr
39126
39568
  const updateSpinner = ora("Updating files...").start();
39127
39569
  const updated = updateProjectFile(dir, manifest, response.data);
39128
39570
  if (updated.length > 0) {
39129
- updateSpinner.succeed("Files updated");
39571
+ updateSpinner.succeed(`Files updated in ${path16.relative(process.cwd(), dir) || "this folder"}`);
39130
39572
  fileTree(updated);
39131
39573
  } else {
39132
39574
  updateSpinner.succeed("Already up to date");
@@ -39134,12 +39576,12 @@ var projectPullCommand = new Command("pull").description("Pull project script fr
39134
39576
  } else {
39135
39577
  const scaffoldSpinner = ora("Scaffolding project files...").start();
39136
39578
  try {
39137
- const result = scaffoldProject({ data: response.data, apiBaseUrl: config.apiBaseUrl });
39579
+ const result = scaffoldProject({ data: response.data, apiBaseUrl: config.apiBaseUrl, targetDir: dir });
39138
39580
  scaffoldSpinner.succeed("Files created");
39139
39581
  line();
39140
39582
  fileTree(result.files);
39141
39583
  line();
39142
- success(`Project cloned to ${folderName}/`);
39584
+ success(`Project cloned to ${path16.relative(process.cwd(), dir) || "."}/`);
39143
39585
  } catch (err) {
39144
39586
  scaffoldSpinner.fail("Scaffolding failed");
39145
39587
  failCommand(err);
@@ -39155,11 +39597,11 @@ var projectPushCommand = new Command("push").description("Compile and push proje
39155
39597
  const { manifest, dir } = readProjectManifestOrExit();
39156
39598
  const api = new ApiClient(config);
39157
39599
  const scriptFile = `project.${manifest.language}`;
39158
- const scriptPath = path15.join(dir, scriptFile);
39159
- if (!fs11.existsSync(scriptPath)) {
39600
+ const scriptPath = path16.join(dir, scriptFile);
39601
+ if (!fs12.existsSync(scriptPath)) {
39160
39602
  failCommand(new CliError("cli_no_manifest", `File not found: ${scriptFile}`));
39161
39603
  }
39162
- const source = fs11.readFileSync(scriptPath, "utf-8");
39604
+ const source = fs12.readFileSync(scriptPath, "utf-8");
39163
39605
  const projLang = manifest.language === "ts" ? "TYPESCRIPT" : "JAVASCRIPT";
39164
39606
  const compileSpinner = ora(`Compiling ${scriptFile}...`).start();
39165
39607
  const compileResult = await compileScript(source, projLang, scriptFile, { minify: true });
@@ -39181,6 +39623,18 @@ var projectPushCommand = new Command("push").description("Compile and push proje
39181
39623
  if (remoteHash !== manifest.syncedHash) {
39182
39624
  spinner.warn("Remote has changes you haven't pulled");
39183
39625
  line();
39626
+ if (!isInteractiveRun()) {
39627
+ failCommand(
39628
+ new CliError(
39629
+ "cli_conflict",
39630
+ "The platform copy changed since your last pull, and this run cannot ask before overwriting it.",
39631
+ [
39632
+ "Run `avsb project pull --force` to fetch the remote script first (it overwrites your local file),",
39633
+ "or `avsb project push --force` to overwrite the remote changes."
39634
+ ]
39635
+ )
39636
+ );
39637
+ }
39184
39638
  const { proceed } = await esm_default12.prompt([
39185
39639
  {
39186
39640
  type: "confirm",
@@ -39199,12 +39653,18 @@ var projectPushCommand = new Command("push").description("Compile and push proje
39199
39653
  }
39200
39654
  }
39201
39655
  const payload = manifest.language === "ts" ? { editorProjectTs: { source, compiled: compiledSource } } : { editorProjectJs: { source, compiled: compiledSource } };
39202
- await api.pushProject(manifest.projectShortId, payload);
39656
+ try {
39657
+ await api.pushProject(manifest.projectShortId, payload);
39658
+ } catch (err) {
39659
+ spinner.fail("Push refused");
39660
+ failCommand(err, { labelForPath: (bodyPath) => bodyPath.length > 0 ? scriptFile : void 0 });
39661
+ }
39203
39662
  manifest.syncedHash = sha2564(source);
39204
39663
  manifest.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
39205
39664
  writeProjectManifest(dir, manifest);
39206
39665
  spinner.succeed("Pushed successfully");
39207
39666
  line();
39667
+ info("Saved as unpublished changes. Publish the script from the project's Snippet settings to put it live.");
39208
39668
  success("Push complete");
39209
39669
  } catch (err) {
39210
39670
  failCommand(err);
@@ -39215,11 +39675,11 @@ var projectDevCommand = new Command("dev").description("Watch project script and
39215
39675
  ensureLoggedIn();
39216
39676
  const { manifest, dir } = readProjectManifestOrExit();
39217
39677
  const scriptFile = `project.${manifest.language}`;
39218
- const scriptPath = path15.join(dir, scriptFile);
39678
+ const scriptPath = path16.join(dir, scriptFile);
39219
39679
  heading(`Dev mode: ${manifest.projectName} (project script)`);
39220
39680
  line();
39221
39681
  let compiledJs;
39222
- const source = fs11.readFileSync(scriptPath, "utf-8");
39682
+ const source = fs12.readFileSync(scriptPath, "utf-8");
39223
39683
  if (manifest.language === "ts") {
39224
39684
  const spinner = ora("Compiling...").start();
39225
39685
  try {
@@ -39258,10 +39718,10 @@ var projectDevCommand = new Command("dev").description("Watch project script and
39258
39718
  dir,
39259
39719
  extensions: [".ts", ".js"],
39260
39720
  onChange: async (filePath) => {
39261
- const relativePath = path15.relative(dir, filePath);
39721
+ const relativePath = path16.relative(dir, filePath);
39262
39722
  info(`File changed: ${relativePath}`);
39263
39723
  try {
39264
- const newSource = fs11.readFileSync(filePath, "utf-8");
39724
+ const newSource = fs12.readFileSync(filePath, "utf-8");
39265
39725
  compiledJs = manifest.language === "ts" ? await compileTypeScript(newSource) : newSource;
39266
39726
  const updateMsg = {
39267
39727
  type: "project:update",
@@ -39507,6 +39967,57 @@ function isConflictError(err) {
39507
39967
  if (!(err instanceof Error)) return false;
39508
39968
  return /409|conflict|referenc/i.test(err.message);
39509
39969
  }
39970
+ function isRecord5(value) {
39971
+ return typeof value === "object" && value !== null && !Array.isArray(value);
39972
+ }
39973
+ function isCliMetric(value) {
39974
+ return isRecord5(value) && typeof value.id === "string" && typeof value.shortId === "number" && typeof value.name === "string" && typeof value.type === "string";
39975
+ }
39976
+ function readMetricList(data) {
39977
+ if (!isRecord5(data) || !Array.isArray(data.metrics)) return null;
39978
+ const metrics = data.metrics.filter(isCliMetric);
39979
+ const usage = isRecord5(data.usage) ? data.usage : {};
39980
+ return { metrics, usage };
39981
+ }
39982
+ function readMetric(data) {
39983
+ return isRecord5(data) && isCliMetric(data.metric) ? data.metric : null;
39984
+ }
39985
+ function formatMetricTable(metrics, usage) {
39986
+ const rows = metrics.map((metric) => ({
39987
+ id: metricIdLabel(metric),
39988
+ type: metric.type,
39989
+ name: truncate(metric.name, 32),
39990
+ definition: truncate(describeMetric(metric), 48),
39991
+ usage: formatUsage(usage[metric.id]),
39992
+ archived: metric.archivedAt != null
39993
+ }));
39994
+ const widths = {
39995
+ id: Math.max(2, ...rows.map((r) => r.id.length)),
39996
+ type: Math.max(4, ...rows.map((r) => r.type.length)),
39997
+ name: Math.max(4, ...rows.map((r) => r.name.length)),
39998
+ definition: Math.max(10, ...rows.map((r) => r.definition.length))
39999
+ };
40000
+ const heading2 = [
40001
+ "ID".padEnd(widths.id),
40002
+ "Type".padEnd(widths.type),
40003
+ "Name".padEnd(widths.name),
40004
+ "Definition".padEnd(widths.definition),
40005
+ "Usage"
40006
+ ].join(" ");
40007
+ return {
40008
+ heading: heading2,
40009
+ rows: rows.map((row) => {
40010
+ const line2 = [
40011
+ row.id.padEnd(widths.id),
40012
+ row.type.padEnd(widths.type),
40013
+ row.name.padEnd(widths.name),
40014
+ row.definition.padEnd(widths.definition),
40015
+ row.usage
40016
+ ].join(" ");
40017
+ return row.archived ? `${line2} (archived)` : line2;
40018
+ })
40019
+ };
40020
+ }
39510
40021
 
39511
40022
  // src/commands/metrics.ts
39512
40023
  var metricsListCommand = withOutputOptions(
@@ -39526,49 +40037,26 @@ var metricsListCommand = withOutputOptions(
39526
40037
  spinner.fail("Failed to fetch metrics");
39527
40038
  failCommand(err);
39528
40039
  }
39529
- const { metrics, usage } = response.data;
39530
40040
  if (emitJsonResult(response.data)) return;
39531
- if (metrics.length === 0) {
40041
+ const list = readMetricList(response.data);
40042
+ if (!list) {
40043
+ failCommand(
40044
+ new CliError("cli_failed", "The platform answered in a shape this CLI does not read.", [
40045
+ "Upgrade the CLI with `npm install -g @avsbhq/cli`, or pass --json to see the raw answer."
40046
+ ])
40047
+ );
40048
+ }
40049
+ if (list.metrics.length === 0) {
39532
40050
  line();
39533
40051
  info(
39534
40052
  'No metrics yet. Create one with `avsb metrics create --type click --name "..." --selector ".cta"`'
39535
40053
  );
39536
40054
  return;
39537
40055
  }
39538
- const rows = metrics.map((metric) => ({
39539
- id: metricIdLabel(metric),
39540
- type: metric.type,
39541
- name: truncate(metric.name, 32),
39542
- definition: truncate(describeMetric(metric), 48),
39543
- usage: formatUsage(usage[metric.id]),
39544
- archived: metric.archivedAt != null
39545
- }));
39546
- const widths = {
39547
- id: Math.max(2, ...rows.map((r) => r.id.length)),
39548
- type: Math.max(4, ...rows.map((r) => r.type.length)),
39549
- name: Math.max(4, ...rows.map((r) => r.name.length)),
39550
- definition: Math.max(10, ...rows.map((r) => r.definition.length))
39551
- };
40056
+ const table = formatMetricTable(list.metrics, list.usage);
39552
40057
  line();
39553
- heading(
39554
- [
39555
- "ID".padEnd(widths.id),
39556
- "Type".padEnd(widths.type),
39557
- "Name".padEnd(widths.name),
39558
- "Definition".padEnd(widths.definition),
39559
- "Usage"
39560
- ].join(" ")
39561
- );
39562
- for (const row of rows) {
39563
- const line2 = [
39564
- row.id.padEnd(widths.id),
39565
- row.type.padEnd(widths.type),
39566
- row.name.padEnd(widths.name),
39567
- row.definition.padEnd(widths.definition),
39568
- row.usage
39569
- ].join(" ");
39570
- line(row.archived ? `${line2} (archived)` : line2);
39571
- }
40058
+ heading(table.heading);
40059
+ for (const row of table.rows) line(row);
39572
40060
  line();
39573
40061
  });
39574
40062
  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) => {
@@ -39584,21 +40072,20 @@ var metricsCreateCommand = new Command("create").description("Create a new metri
39584
40072
  const body = buildCreateBody(type, opts, opts.name);
39585
40073
  const api = new ApiClient(config);
39586
40074
  const spinner = ora("Creating metric...").start();
40075
+ let response;
39587
40076
  try {
39588
- const response = await api.createMetric(shortId, body);
39589
- const { metric } = response.data;
39590
- spinner.succeed(`Metric created: ${metric.name} (${metricIdLabel(metric)})`);
39591
- printMetricDetails(metric);
40077
+ response = await api.createMetric(shortId, body);
39592
40078
  } catch (err) {
39593
40079
  spinner.fail("Failed to create metric");
39594
- const message = err instanceof Error ? err.message : "Unknown error";
39595
- if (/already exists|conflict|409|unique/i.test(message)) {
39596
- error(`Conflict: a metric with that name or key already exists. ${message}`);
39597
- } else {
39598
- error(message);
39599
- }
39600
- process.exit(EXIT.failure);
40080
+ failCommand(err);
39601
40081
  }
40082
+ const metric = readMetric(response.data);
40083
+ if (!metric) {
40084
+ spinner.warn("Metric created. Run `avsb metrics list` to see it.");
40085
+ return;
40086
+ }
40087
+ spinner.succeed(`Metric created: ${metric.name} (${metricIdLabel(metric)})`);
40088
+ printMetricDetails(metric);
39602
40089
  });
39603
40090
  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) => {
39604
40091
  const config = ensureLoggedIn();
@@ -39614,49 +40101,65 @@ var metricsUpdateCommand = new Command("update").argument("<metricId>", "Metric
39614
40101
  }
39615
40102
  const api = new ApiClient(config);
39616
40103
  const spinner = ora("Updating metric...").start();
40104
+ let response;
39617
40105
  try {
39618
- const response = await api.updateMetric(shortId, metricId, patch);
39619
- const { metric } = response.data;
39620
- spinner.succeed(`Metric updated: ${metric.name} (${metricIdLabel(metric)})`);
39621
- printMetricDetails(metric);
40106
+ response = await api.updateMetric(shortId, metricId, patch);
39622
40107
  } catch (err) {
39623
40108
  spinner.fail("Failed to update metric");
39624
40109
  failCommand(err);
39625
40110
  }
40111
+ const metric = readMetric(response.data);
40112
+ if (!metric) {
40113
+ spinner.warn(`Metric ${metricId} updated.`);
40114
+ return;
40115
+ }
40116
+ spinner.succeed(`Metric updated: ${metric.name} (${metricIdLabel(metric)})`);
40117
+ printMetricDetails(metric);
39626
40118
  });
39627
40119
  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) => {
39628
40120
  const config = ensureLoggedIn();
39629
40121
  const shortId = resolveProjectShortId(opts);
39630
40122
  const api = new ApiClient(config);
39631
40123
  const spinner = ora("Archiving metric...").start();
40124
+ let response;
39632
40125
  try {
39633
- const response = await api.archiveMetric(shortId, metricId);
39634
- const { metric } = response.data;
39635
- spinner.succeed(`Metric archived: ${metric.name} (${metricIdLabel(metric)})`);
40126
+ response = await api.archiveMetric(shortId, metricId);
39636
40127
  } catch (err) {
39637
40128
  spinner.fail("Failed to archive metric");
39638
40129
  failCommand(err);
39639
40130
  }
40131
+ const metric = readMetric(response.data);
40132
+ spinner.succeed(metric ? `Metric archived: ${metric.name} (${metricIdLabel(metric)})` : `Metric ${metricId} archived`);
39640
40133
  });
39641
40134
  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) => {
39642
40135
  const config = ensureLoggedIn();
39643
40136
  const shortId = resolveProjectShortId(opts);
39644
40137
  const api = new ApiClient(config);
39645
40138
  const spinner = ora("Restoring metric...").start();
40139
+ let response;
39646
40140
  try {
39647
- const response = await api.unarchiveMetric(shortId, metricId);
39648
- const { metric } = response.data;
39649
- spinner.succeed(`Metric restored from archive: ${metric.name} (${metricIdLabel(metric)})`);
40141
+ response = await api.unarchiveMetric(shortId, metricId);
39650
40142
  } catch (err) {
39651
40143
  spinner.fail("Failed to unarchive metric");
39652
40144
  failCommand(err);
39653
40145
  }
40146
+ const metric = readMetric(response.data);
40147
+ spinner.succeed(
40148
+ metric ? `Metric restored from archive: ${metric.name} (${metricIdLabel(metric)})` : `Metric ${metricId} restored from archive`
40149
+ );
39654
40150
  });
39655
40151
  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) => {
39656
40152
  const config = ensureLoggedIn();
39657
40153
  const shortId = resolveProjectShortId(opts);
39658
40154
  const api = new ApiClient(config);
39659
40155
  if (!opts.force) {
40156
+ if (!isInteractiveRun()) {
40157
+ failCommand(
40158
+ new CliError("cli_usage", `Deleting metric ${metricId} needs a confirmation this run cannot ask for.`, [
40159
+ `Pass -f to delete without asking: \`avsb metrics delete ${metricId} -f\``
40160
+ ])
40161
+ );
40162
+ }
39660
40163
  const { proceed } = await esm_default12.prompt([
39661
40164
  {
39662
40165
  type: "confirm",
@@ -39676,19 +40179,12 @@ var metricsDeleteCommand = new Command("delete").argument("<metricId>", "Metric
39676
40179
  spinner.succeed(`Metric deleted: ${metricId}`);
39677
40180
  } catch (err) {
39678
40181
  spinner.fail("Failed to delete metric");
39679
- const message = err instanceof Error ? err.message : "Unknown error";
39680
40182
  if (isConflictError(err)) {
39681
40183
  const refs = extractBindingsReferencing(err);
39682
- if (refs.length > 0) {
39683
- error(`Cannot delete: referenced by ${refs.join(", ")}.`);
39684
- } else {
39685
- error(`Cannot delete: metric bindings still reference this metric. ${message}`);
39686
- }
40184
+ if (refs.length > 0) error(`Referenced by ${refs.join(", ")}.`);
39687
40185
  info(`Tip: run \`avsb metrics archive ${metricId}\` instead to hide it without losing history.`);
39688
- } else {
39689
- error(message);
39690
40186
  }
39691
- process.exit(EXIT.failure);
40187
+ failCommand(err);
39692
40188
  }
39693
40189
  });
39694
40190
  var metricsCommand = new Command("metrics").description("Manage metric tracking definitions").addCommand(metricsListCommand).addCommand(metricsCreateCommand).addCommand(metricsUpdateCommand).addCommand(metricsArchiveCommand).addCommand(metricsUnarchiveCommand).addCommand(metricsDeleteCommand);
@@ -39903,6 +40399,40 @@ function isConflictError2(err) {
39903
40399
  if (!(err instanceof Error)) return false;
39904
40400
  return /409|conflict|attached|referenc/i.test(err.message);
39905
40401
  }
40402
+ function isRecord6(value) {
40403
+ return typeof value === "object" && value !== null && !Array.isArray(value);
40404
+ }
40405
+ function isCliMetricBinding(value) {
40406
+ return isRecord6(value) && typeof value.id === "string" && typeof value.shortId === "number" && typeof value.name === "string" && typeof value.measure === "string";
40407
+ }
40408
+ function readMetricBindingList(data) {
40409
+ if (!isRecord6(data) || !Array.isArray(data.metricBindings)) return null;
40410
+ return data.metricBindings.filter(isCliMetricBinding);
40411
+ }
40412
+ function readMetricBinding(data) {
40413
+ return isRecord6(data) && isCliMetricBinding(data.metricBinding) ? data.metricBinding : null;
40414
+ }
40415
+ function formatMetricBindingTable(bindings) {
40416
+ const rows = bindings.map((binding) => ({
40417
+ id: metricBindingIdLabel(binding),
40418
+ measure: measureBadge(binding.measure),
40419
+ name: truncate2(binding.name, 28),
40420
+ sentence: truncate2(renderMetricBindingSentence(binding), 64),
40421
+ archived: binding.archivedAt != null
40422
+ }));
40423
+ const widths = {
40424
+ id: Math.max(2, ...rows.map((r) => r.id.length)),
40425
+ measure: Math.max(7, ...rows.map((r) => r.measure.length)),
40426
+ name: Math.max(4, ...rows.map((r) => r.name.length))
40427
+ };
40428
+ return {
40429
+ heading: ["ID".padEnd(widths.id), "Measure".padEnd(widths.measure), "Name".padEnd(widths.name), "Sentence"].join(" "),
40430
+ rows: rows.map((row) => {
40431
+ const line2 = [row.id.padEnd(widths.id), row.measure.padEnd(widths.measure), row.name.padEnd(widths.name), row.sentence].join(" ");
40432
+ return row.archived ? `${line2} (archived)` : line2;
40433
+ })
40434
+ };
40435
+ }
39906
40436
 
39907
40437
  // src/commands/metric-bindings.body.ts
39908
40438
  function parseComposition(opts) {
@@ -39964,12 +40494,11 @@ function parseWinsorization(opts) {
39964
40494
  }
39965
40495
  return { enabled: true, upperPercentile: upper, lowerPercentile: lower };
39966
40496
  }
39967
- function buildCreateMetricBindingBody(projectId, measure, opts, name) {
40497
+ function buildCreateMetricBindingBody(measure, opts, name) {
39968
40498
  const direction = opts.direction ? normalizeDirection(opts.direction) : "INCREASE";
39969
40499
  const body = {
39970
40500
  name,
39971
40501
  description: opts.description ?? void 0,
39972
- projectId,
39973
40502
  measure,
39974
40503
  direction,
39975
40504
  isLibrary: true
@@ -40040,8 +40569,10 @@ function buildUpdateMetricBindingPatch(opts) {
40040
40569
  if (opts.denominatorMetric !== void 0) patch.denominatorMetricId = opts.denominatorMetric;
40041
40570
  if (opts.percentile !== void 0) {
40042
40571
  const p = Number(opts.percentile);
40043
- if (!Number.isFinite(p)) {
40044
- failCommand(new CliError("cli_usage", "--percentile must be numeric."));
40572
+ if (!Number.isFinite(p) || p <= 0 || p >= 100) {
40573
+ failCommand(
40574
+ new CliError("cli_usage", "--percentile must be a number strictly between 0 and 100.")
40575
+ );
40045
40576
  }
40046
40577
  patch.percentile = p;
40047
40578
  }
@@ -40078,8 +40609,15 @@ var metricBindingsListCommand = withOutputOptions(
40078
40609
  spinner.fail("Failed to fetch metric bindings");
40079
40610
  failCommand(err);
40080
40611
  }
40081
- const { metricBindings } = response.data;
40082
40612
  if (emitJsonResult(response.data)) return;
40613
+ const metricBindings = readMetricBindingList(response.data);
40614
+ if (!metricBindings) {
40615
+ failCommand(
40616
+ new CliError("cli_failed", "The platform answered in a shape this CLI does not read.", [
40617
+ "Upgrade the CLI with `npm install -g @avsbhq/cli`, or pass --json to see the raw answer."
40618
+ ])
40619
+ );
40620
+ }
40083
40621
  if (metricBindings.length === 0) {
40084
40622
  line();
40085
40623
  info(
@@ -40087,45 +40625,19 @@ var metricBindingsListCommand = withOutputOptions(
40087
40625
  );
40088
40626
  return;
40089
40627
  }
40090
- const rows = metricBindings.map((binding) => ({
40091
- id: metricBindingIdLabel(binding),
40092
- measure: measureBadge(binding.measure),
40093
- name: truncate2(binding.name, 28),
40094
- sentence: truncate2(renderMetricBindingSentence(binding), 64),
40095
- archived: binding.archivedAt != null
40096
- }));
40097
- const widths = {
40098
- id: Math.max(2, ...rows.map((r) => r.id.length)),
40099
- measure: Math.max(7, ...rows.map((r) => r.measure.length)),
40100
- name: Math.max(4, ...rows.map((r) => r.name.length))
40101
- };
40628
+ const table = formatMetricBindingTable(metricBindings);
40102
40629
  line();
40103
- heading(
40104
- [
40105
- "ID".padEnd(widths.id),
40106
- "Measure".padEnd(widths.measure),
40107
- "Name".padEnd(widths.name),
40108
- "Sentence"
40109
- ].join(" ")
40110
- );
40111
- for (const row of rows) {
40112
- const line2 = [
40113
- row.id.padEnd(widths.id),
40114
- row.measure.padEnd(widths.measure),
40115
- row.name.padEnd(widths.name),
40116
- row.sentence
40117
- ].join(" ");
40118
- line(row.archived ? `${line2} (archived)` : line2);
40119
- }
40630
+ heading(table.heading);
40631
+ for (const row of table.rows) line(row);
40120
40632
  line();
40121
40633
  }
40122
40634
  );
40123
40635
  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(
40124
40636
  "--measure <measure>",
40125
40637
  "Measure (UNIQUE_CONVERSIONS_PER_VISITOR | TOTAL_EVENTS | UNIQUE_VISITORS_WHO_FIRED | TOTAL_VALUE_PER_VISITOR | TOTAL_VALUE | PERCENTILE | RATE | COMPOSITE)"
40126
- ).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(
40127
- "--component <metricId:weight>",
40128
- "Repeatable component for COMPOSITE (e.g. --component b1:0.5 --component b2:0.5)",
40638
+ ).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(
40639
+ "--component <bindingId:weight>",
40640
+ "Repeatable COMPOSITE component, a metric binding and its weight (e.g. --component MBD-8:0.5 --component MBD-9:0.5)",
40129
40641
  (value, prev = []) => prev.concat(value),
40130
40642
  []
40131
40643
  ).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) => {
@@ -40139,29 +40651,26 @@ var metricBindingsCreateCommand = new Command("create").description("Create a ne
40139
40651
  const shortId = resolveProjectShortId2(opts);
40140
40652
  const measure = normalizeMeasure(opts.measure);
40141
40653
  const api = new ApiClient(config);
40142
- const projectResponse = await api.getProject(shortId);
40143
- const projectId = projectResponse.data.project.id;
40144
- const body = buildCreateMetricBindingBody(projectId, measure, opts, opts.name);
40654
+ const body = buildCreateMetricBindingBody(measure, opts, opts.name);
40145
40655
  const spinner = ora("Creating metric binding...").start();
40656
+ let response;
40146
40657
  try {
40147
- const response = await api.createMetricBinding(shortId, body);
40148
- const { metricBinding } = response.data;
40149
- spinner.succeed(`Metric binding created: ${metricBinding.name} (${metricBindingIdLabel(metricBinding)})`);
40150
- printMetricBindingDetails(metricBinding);
40658
+ response = await api.createMetricBinding(shortId, body);
40151
40659
  } catch (err) {
40152
40660
  spinner.fail("Failed to create metric binding");
40153
- const message = err instanceof Error ? err.message : "Unknown error";
40154
- if (/already exists|conflict|409|unique/i.test(message)) {
40155
- error(`Conflict: a metric binding with that name already exists. ${message}`);
40156
- } else {
40157
- error(message);
40158
- }
40159
- process.exit(EXIT.failure);
40661
+ failCommand(err);
40662
+ }
40663
+ const metricBinding = readMetricBinding(response.data);
40664
+ if (!metricBinding) {
40665
+ spinner.warn("Metric binding created. Run `avsb metric-bindings list` to see it.");
40666
+ return;
40160
40667
  }
40668
+ spinner.succeed(`Metric binding created: ${metricBinding.name} (${metricBindingIdLabel(metricBinding)})`);
40669
+ printMetricBindingDetails(metricBinding);
40161
40670
  });
40162
40671
  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(
40163
- "--component <metricId:weight>",
40164
- "Repeatable component (replaces composition)",
40672
+ "--component <bindingId:weight>",
40673
+ "Repeatable COMPOSITE component, a metric binding and its weight (replaces the composition)",
40165
40674
  (value, prev = []) => prev.concat(value),
40166
40675
  []
40167
40676
  ).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) => {
@@ -40169,59 +40678,73 @@ var metricBindingsUpdateCommand = new Command("update").argument("<metricBinding
40169
40678
  const shortId = resolveProjectShortId2(opts);
40170
40679
  const patch = buildUpdateMetricBindingPatch(opts);
40171
40680
  if (Object.keys(patch).length === 0) {
40172
- error("No update fields provided. Pass at least one of --name, --description, etc.");
40173
- process.exit(EXIT.failure);
40681
+ failCommand(
40682
+ new CliError("cli_usage", "No update fields provided. Pass at least one of --name, --description, --direction, etc.")
40683
+ );
40174
40684
  }
40175
40685
  const api = new ApiClient(config);
40176
40686
  const spinner = ora("Updating metric binding...").start();
40687
+ let response;
40177
40688
  try {
40178
- const response = await api.updateMetricBinding(shortId, metricBindingId, patch);
40179
- const { metricBinding, amendment } = response.data;
40180
- spinner.succeed(`Metric binding updated: ${metricBinding.name} (${metricBindingIdLabel(metricBinding)})`);
40181
- if (amendment?.required) {
40182
- warn(
40183
- `Change requires an amendment on sealed analysis plan(s). Fields: ${amendment.requiredFields.join(", ")}`
40184
- );
40185
- }
40186
- printMetricBindingDetails(metricBinding);
40689
+ response = await api.updateMetricBinding(shortId, metricBindingId, patch);
40187
40690
  } catch (err) {
40188
40691
  spinner.fail("Failed to update metric binding");
40189
40692
  failCommand(err);
40190
40693
  }
40694
+ const metricBinding = readMetricBinding(response.data);
40695
+ if (!metricBinding) {
40696
+ spinner.warn(`Metric binding ${metricBindingId} updated.`);
40697
+ return;
40698
+ }
40699
+ spinner.succeed(`Metric binding updated: ${metricBinding.name} (${metricBindingIdLabel(metricBinding)})`);
40700
+ printMetricBindingDetails(metricBinding);
40191
40701
  });
40192
40702
  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) => {
40193
40703
  const config = ensureLoggedIn();
40194
40704
  const shortId = resolveProjectShortId2(opts);
40195
40705
  const api = new ApiClient(config);
40196
40706
  const spinner = ora("Archiving metric binding...").start();
40707
+ let response;
40197
40708
  try {
40198
- const response = await api.archiveMetricBinding(shortId, metricBindingId);
40199
- const { metricBinding } = response.data;
40200
- spinner.succeed(`Metric binding archived: ${metricBinding.name} (${metricBindingIdLabel(metricBinding)})`);
40709
+ response = await api.archiveMetricBinding(shortId, metricBindingId);
40201
40710
  } catch (err) {
40202
40711
  spinner.fail("Failed to archive metric binding");
40203
40712
  failCommand(err);
40204
40713
  }
40714
+ const metricBinding = readMetricBinding(response.data);
40715
+ spinner.succeed(
40716
+ metricBinding ? `Metric binding archived: ${metricBinding.name} (${metricBindingIdLabel(metricBinding)})` : `Metric binding ${metricBindingId} archived`
40717
+ );
40205
40718
  });
40206
40719
  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) => {
40207
40720
  const config = ensureLoggedIn();
40208
40721
  const shortId = resolveProjectShortId2(opts);
40209
40722
  const api = new ApiClient(config);
40210
40723
  const spinner = ora("Restoring metric binding...").start();
40724
+ let response;
40211
40725
  try {
40212
- const response = await api.unarchiveMetricBinding(shortId, metricBindingId);
40213
- const { metricBinding } = response.data;
40214
- spinner.succeed(`Metric binding restored from archive: ${metricBinding.name} (${metricBindingIdLabel(metricBinding)})`);
40726
+ response = await api.unarchiveMetricBinding(shortId, metricBindingId);
40215
40727
  } catch (err) {
40216
40728
  spinner.fail("Failed to unarchive metric binding");
40217
40729
  failCommand(err);
40218
40730
  }
40731
+ const metricBinding = readMetricBinding(response.data);
40732
+ spinner.succeed(
40733
+ metricBinding ? `Metric binding restored from archive: ${metricBinding.name} (${metricBindingIdLabel(metricBinding)})` : `Metric binding ${metricBindingId} restored from archive`
40734
+ );
40219
40735
  });
40220
40736
  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) => {
40221
40737
  const config = ensureLoggedIn();
40222
40738
  const shortId = resolveProjectShortId2(opts);
40223
40739
  const api = new ApiClient(config);
40224
40740
  if (!opts.force) {
40741
+ if (!isInteractiveRun()) {
40742
+ failCommand(
40743
+ new CliError("cli_usage", `Deleting metric binding ${metricBindingId} needs a confirmation this run cannot ask for.`, [
40744
+ `Pass -f to delete without asking: \`avsb metric-bindings delete ${metricBindingId} -f\``
40745
+ ])
40746
+ );
40747
+ }
40225
40748
  const { proceed } = await esm_default12.prompt([
40226
40749
  {
40227
40750
  type: "confirm",
@@ -40241,28 +40764,21 @@ var metricBindingsDeleteCommand = new Command("delete").argument("<metricBinding
40241
40764
  spinner.succeed(`Metric binding deleted: ${metricBindingId}`);
40242
40765
  } catch (err) {
40243
40766
  spinner.fail("Failed to delete metric binding");
40244
- const message = err instanceof Error ? err.message : "Unknown error";
40245
40767
  if (isConflictError2(err)) {
40246
40768
  const refs = extractExperimentsReferencing(err);
40247
- if (refs.length > 0) {
40248
- error(`Cannot delete: referenced by ${refs.join(", ")}.`);
40249
- } else {
40250
- error(`Cannot delete: experiments still attach this metric binding. ${message}`);
40251
- }
40769
+ if (refs.length > 0) error(`Attached to ${refs.join(", ")}.`);
40252
40770
  info(
40253
40771
  `Tip: run \`avsb metric-bindings archive ${metricBindingId}\` instead to hide it without losing history.`
40254
40772
  );
40255
- } else {
40256
- error(message);
40257
40773
  }
40258
- process.exit(EXIT.failure);
40774
+ failCommand(err);
40259
40775
  }
40260
40776
  });
40261
40777
  var metricBindingsCommand = new Command("metric-bindings").description("Manage saved/library metric bindings").addCommand(metricBindingsListCommand).addCommand(metricBindingsCreateCommand).addCommand(metricBindingsUpdateCommand).addCommand(metricBindingsArchiveCommand).addCommand(metricBindingsUnarchiveCommand).addCommand(metricBindingsDeleteCommand);
40262
40778
 
40263
40779
  // src/commands/codegen.ts
40264
- import path16 from "node:path";
40265
- import fs12 from "node:fs/promises";
40780
+ import path17 from "node:path";
40781
+ import fs13 from "node:fs/promises";
40266
40782
 
40267
40783
  // src/lib/codegenApi.ts
40268
40784
  var CodegenApiClient = class extends ApiClient {
@@ -40319,6 +40835,9 @@ function serialiseManifest(manifest) {
40319
40835
  }
40320
40836
 
40321
40837
  // src/lib/codegenCheck.ts
40838
+ function withoutPublishStamp(text) {
40839
+ return text.replace(/^(\/\/ Source: .*\bpublishedAt=)\S+/m, "$1*").replace(/^(\s*"publishedAt": )(?:"[^"\n]*"|null)/gm, "$1*");
40840
+ }
40322
40841
  function diffArtifact(artifact) {
40323
40842
  const expectedLines = artifact.expected.split("\n");
40324
40843
  if (artifact.actual === null) {
@@ -40333,11 +40852,13 @@ function diffArtifact(artifact) {
40333
40852
  };
40334
40853
  }
40335
40854
  const actual = artifact.actual.replace(/\r\n/g, "\n");
40336
- if (actual === artifact.expected) return null;
40855
+ const expectedComparable = withoutPublishStamp(artifact.expected).split("\n");
40856
+ const actualComparable = withoutPublishStamp(actual).split("\n");
40857
+ if (expectedComparable.join("\n") === actualComparable.join("\n")) return null;
40337
40858
  const actualLines = actual.split("\n");
40338
- const shared = Math.min(expectedLines.length, actualLines.length);
40859
+ const shared = Math.min(expectedComparable.length, actualComparable.length);
40339
40860
  let index = 0;
40340
- while (index < shared && expectedLines[index] === actualLines[index]) index += 1;
40861
+ while (index < shared && expectedComparable[index] === actualComparable[index]) index += 1;
40341
40862
  return {
40342
40863
  path: artifact.path,
40343
40864
  reason: "different",
@@ -40491,7 +41012,7 @@ function planArtifacts(datafile, targets) {
40491
41012
  }
40492
41013
  async function readIfPresent(filePath) {
40493
41014
  try {
40494
- return await fs12.readFile(filePath, "utf-8");
41015
+ return await fs13.readFile(filePath, "utf-8");
40495
41016
  } catch (err) {
40496
41017
  if (isMissingFile(err)) return null;
40497
41018
  throw err;
@@ -40505,8 +41026,8 @@ async function writeArtifacts(artifacts) {
40505
41026
  for (const artifact of artifacts) {
40506
41027
  const existing = await readIfPresent(artifact.path);
40507
41028
  if (existing === artifact.contents) continue;
40508
- await fs12.mkdir(path16.dirname(artifact.path), { recursive: true });
40509
- await fs12.writeFile(artifact.path, artifact.contents, "utf-8");
41029
+ await fs13.mkdir(path17.dirname(artifact.path), { recursive: true });
41030
+ await fs13.writeFile(artifact.path, artifact.contents, "utf-8");
40510
41031
  written.push(artifact.path);
40511
41032
  }
40512
41033
  return written;
@@ -40528,6 +41049,7 @@ function flagCountLine(datafile) {
40528
41049
  }
40529
41050
  async function runCheck(artifacts, datafile, targets) {
40530
41051
  const drifts = diffArtifacts(await toComparisons(artifacts));
41052
+ const exitCode = drifts.length === 0 ? EXIT.success : EXIT.failure;
40531
41053
  if (emitJsonResult({
40532
41054
  upToDate: drifts.length === 0,
40533
41055
  checked: artifacts.map((a) => a.path),
@@ -40536,25 +41058,25 @@ async function runCheck(artifacts, datafile, targets) {
40536
41058
  flagCount: datafile.flags.length,
40537
41059
  publishedAt: datafile.publishedAt ?? null
40538
41060
  })) {
40539
- process.exit(drifts.length === 0 ? EXIT.success : EXIT.failure);
41061
+ return exitCode;
40540
41062
  }
40541
41063
  if (drifts.length === 0) {
40542
41064
  success(describeDriftSummary(drifts));
40543
41065
  dim(` Checked ${artifacts.length === 1 ? "1 file" : `${artifacts.length} files`} against ${flagCountLine(datafile)} in ${targets.env}.`);
40544
- process.exit(EXIT.success);
41066
+ return exitCode;
40545
41067
  }
40546
41068
  error(describeDriftSummary(drifts));
40547
41069
  for (const drift of drifts) {
40548
41070
  for (const line2 of describeDrift(drift)) errorDetail(line2);
40549
41071
  }
40550
41072
  errorDetail("Run the same command without --check, then commit the result.");
40551
- process.exit(EXIT.failure);
41073
+ return exitCode;
40552
41074
  }
40553
41075
  function reportWatchEvent(event, targets) {
40554
41076
  switch (event.kind) {
40555
41077
  case "polled":
40556
41078
  if (event.changed) {
40557
- success(`Updated ${event.written.map((p) => path16.relative(process.cwd(), p)).join(", ")}`);
41079
+ success(`Updated ${event.written.map((p) => path17.relative(process.cwd(), p)).join(", ")}`);
40558
41080
  }
40559
41081
  return;
40560
41082
  case "failed":
@@ -40645,6 +41167,7 @@ var codegenCommand = withOutputOptions(
40645
41167
  const project = resolveProjectRefOrExit(opts);
40646
41168
  const config = ensureLoggedIn();
40647
41169
  const spinner = startSpinner("Fetching datafile...");
41170
+ let checkExitCode = null;
40648
41171
  try {
40649
41172
  const client = new CodegenApiClient(config);
40650
41173
  const first = await client.getDatafileConditional(project.shortId, opts.env, null);
@@ -40657,15 +41180,15 @@ var codegenCommand = withOutputOptions(
40657
41180
  const datafile = first.body;
40658
41181
  spinner.succeed("Datafile fetched");
40659
41182
  const targets = {
40660
- typesPath: path16.resolve(opts.output),
40661
- manifestPath: opts.manifest ? path16.resolve(opts.manifest) : null,
41183
+ typesPath: path17.resolve(opts.output),
41184
+ manifestPath: opts.manifest ? path17.resolve(opts.manifest) : null,
40662
41185
  projectShortId: project.shortId,
40663
41186
  env: opts.env,
40664
41187
  augment: opts.augment !== false
40665
41188
  };
40666
41189
  const artifacts = planArtifacts(datafile, targets);
40667
41190
  if (opts.check) {
40668
- await runCheck(artifacts, datafile, targets);
41191
+ checkExitCode = await runCheck(artifacts, datafile, targets);
40669
41192
  return;
40670
41193
  }
40671
41194
  const written = await writeArtifacts(artifacts);
@@ -40695,12 +41218,14 @@ var codegenCommand = withOutputOptions(
40695
41218
  } catch (err) {
40696
41219
  spinner.fail("Codegen failed");
40697
41220
  failCommand(err);
41221
+ } finally {
41222
+ if (checkExitCode !== null) process.exit(checkExitCode);
40698
41223
  }
40699
41224
  });
40700
41225
 
40701
41226
  // src/commands/find-refs.ts
40702
- import fs13 from "node:fs/promises";
40703
- import path18 from "node:path";
41227
+ import fs14 from "node:fs/promises";
41228
+ import path19 from "node:path";
40704
41229
 
40705
41230
  // src/lib/findRefs.ts
40706
41231
  var FLAG_GETTERS = /* @__PURE__ */ new Map([
@@ -40863,7 +41388,7 @@ function byKeyName(a, b) {
40863
41388
  }
40864
41389
 
40865
41390
  // src/lib/ignoreRules.ts
40866
- import path17 from "node:path";
41391
+ import path18 from "node:path";
40867
41392
  var ALWAYS_SKIPPED_DIRECTORIES = [
40868
41393
  ".git",
40869
41394
  "node_modules",
@@ -40967,7 +41492,7 @@ function relativeToBase(relPath, base) {
40967
41492
  return relPath.slice(base.length + 1);
40968
41493
  }
40969
41494
  function toRelativePosix(root, target) {
40970
- return path17.relative(root, target).split(path17.sep).join("/");
41495
+ return path18.relative(root, target).split(path18.sep).join("/");
40971
41496
  }
40972
41497
 
40973
41498
  // src/commands/find-refs.ts
@@ -41023,13 +41548,13 @@ async function scanTree(root, extraRules, knownKeys, maxFiles) {
41023
41548
  if (truncated) return;
41024
41549
  let entries;
41025
41550
  try {
41026
- entries = await fs13.readdir(dir, { withFileTypes: true });
41551
+ entries = await fs14.readdir(dir, { withFileTypes: true });
41027
41552
  } catch {
41028
41553
  return;
41029
41554
  }
41030
41555
  let rules = inherited;
41031
41556
  if (entries.some((e) => e.isFile() && e.name === ".gitignore")) {
41032
- const contents = await readTextFile(path18.join(dir, ".gitignore"));
41557
+ const contents = await readTextFile(path19.join(dir, ".gitignore"));
41033
41558
  if (contents !== null) {
41034
41559
  rules = [...inherited, ...parseIgnoreFile(contents, toRelativePosix(root, dir))];
41035
41560
  }
@@ -41037,7 +41562,7 @@ async function scanTree(root, extraRules, knownKeys, maxFiles) {
41037
41562
  for (const entry of entries) {
41038
41563
  if (truncated) return;
41039
41564
  if (entry.isSymbolicLink()) continue;
41040
- const full = path18.join(dir, entry.name);
41565
+ const full = path19.join(dir, entry.name);
41041
41566
  const rel = toRelativePosix(root, full);
41042
41567
  if (entry.isDirectory()) {
41043
41568
  if (ALWAYS_SKIPPED_DIRECTORIES.includes(entry.name)) continue;
@@ -41046,7 +41571,7 @@ async function scanTree(root, extraRules, knownKeys, maxFiles) {
41046
41571
  continue;
41047
41572
  }
41048
41573
  if (!entry.isFile()) continue;
41049
- if (!SOURCE_EXTENSIONS.has(path18.extname(entry.name))) continue;
41574
+ if (!SOURCE_EXTENSIONS.has(path19.extname(entry.name))) continue;
41050
41575
  if (isIgnored(rel, false, rules)) continue;
41051
41576
  if (filesScanned >= maxFiles) {
41052
41577
  truncated = true;
@@ -41063,9 +41588,9 @@ async function scanTree(root, extraRules, knownKeys, maxFiles) {
41063
41588
  }
41064
41589
  async function readTextFile(filePath) {
41065
41590
  try {
41066
- const stat4 = await fs13.stat(filePath);
41591
+ const stat4 = await fs14.stat(filePath);
41067
41592
  if (stat4.size > MAX_FILE_BYTES) return null;
41068
- const contents = await fs13.readFile(filePath, "utf-8");
41593
+ const contents = await fs14.readFile(filePath, "utf-8");
41069
41594
  return contents.includes("\0") ? null : contents;
41070
41595
  } catch {
41071
41596
  return null;
@@ -41131,7 +41656,7 @@ var findRefsCommand = withOutputOptions(
41131
41656
  const api = new FlagsApiClient(config);
41132
41657
  const registry = await readRegistry(api, project.shortId);
41133
41658
  spinner.succeed(`Read ${registry.length} flag${registry.length === 1 ? "" : "s"}`);
41134
- const root = path18.resolve(opts.dir);
41659
+ const root = path19.resolve(opts.dir);
41135
41660
  await assertDirectory(root, opts.dir);
41136
41661
  const extraRules = (opts.ignore ?? []).flatMap((pattern) => parseIgnoreFile(pattern));
41137
41662
  const knownKeys = new Set(registry.map((f) => f.key));
@@ -41166,7 +41691,7 @@ function parseMaxFiles(raw) {
41166
41691
  }
41167
41692
  async function assertDirectory(root, asTyped) {
41168
41693
  try {
41169
- const stat4 = await fs13.stat(root);
41694
+ const stat4 = await fs14.stat(root);
41170
41695
  if (stat4.isDirectory()) return;
41171
41696
  } catch {
41172
41697
  }
@@ -41174,11 +41699,11 @@ async function assertDirectory(root, asTyped) {
41174
41699
  }
41175
41700
 
41176
41701
  // src/commands/datasets.ts
41177
- import fs15 from "node:fs";
41702
+ import fs16 from "node:fs";
41178
41703
 
41179
41704
  // src/commands/datasets.helpers.ts
41180
- import fs14 from "node:fs";
41181
- import path19 from "node:path";
41705
+ import fs15 from "node:fs";
41706
+ import path20 from "node:path";
41182
41707
  function parseVersionArg(raw) {
41183
41708
  const normalized = raw.trim().replace(/^[vV]/, "");
41184
41709
  if (!/^\d+$/.test(normalized)) {
@@ -41193,13 +41718,13 @@ function parseVersionArg(raw) {
41193
41718
  return shortId;
41194
41719
  }
41195
41720
  function validateNdjsonPath(filePath) {
41196
- const resolved = path19.resolve(filePath);
41197
- if (!fs14.existsSync(resolved) || !fs14.statSync(resolved).isFile()) {
41721
+ const resolved = path20.resolve(filePath);
41722
+ if (!fs15.existsSync(resolved) || !fs15.statSync(resolved).isFile()) {
41198
41723
  throw new Error(`File not found: ${resolved}`);
41199
41724
  }
41200
- if (path19.extname(resolved).toLowerCase() !== ".ndjson") {
41725
+ if (path20.extname(resolved).toLowerCase() !== ".ndjson") {
41201
41726
  throw new Error(
41202
- `Expected an .ndjson file, got "${path19.basename(resolved)}". Convert CSV files in the dashboard upload modal, or export NDJSON directly.`
41727
+ `Expected an .ndjson file, got "${path20.basename(resolved)}". Convert CSV files in the dashboard upload modal, or export NDJSON directly.`
41203
41728
  );
41204
41729
  }
41205
41730
  return resolved;
@@ -41388,7 +41913,7 @@ var datasetsPushCommand = new Command("push").argument("<slug>", "Dataset slug")
41388
41913
  try {
41389
41914
  const result = await runPushFlow(api, projectShortId, {
41390
41915
  slug,
41391
- fileBody: fs15.readFileSync(resolvedPath),
41916
+ fileBody: fs16.readFileSync(resolvedPath),
41392
41917
  label: opts.label,
41393
41918
  activate: opts.activate,
41394
41919
  poll: { timeoutMs: timeoutSeconds * 1e3 },
@@ -41482,6 +42007,12 @@ var datasetsRollbackCommand = new Command("rollback").argument("<slug>", "Datase
41482
42007
  fail(spinner, "Failed to resolve rollback target", err);
41483
42008
  }
41484
42009
  if (!opts.force) {
42010
+ const refusal = refusalWithoutPrompt({
42011
+ code: "cli_usage",
42012
+ message: `Rolling back "${slug}" (${summary}) needs a confirmation this run cannot ask for.`,
42013
+ hints: [`Pass -f to roll back without asking: \`avsb datasets rollback ${slug} -f\``]
42014
+ });
42015
+ if (refusal) failCommand(refusal);
41485
42016
  const { proceed } = await esm_default12.prompt([
41486
42017
  {
41487
42018
  type: "confirm",
@@ -41757,7 +42288,7 @@ function printSecretHint() {
41757
42288
  " A vs B shows a webhook signing secret only when the webhook is created and when its"
41758
42289
  );
41759
42290
  dim(" secret is rotated, so copy it then. If you no longer have it, rotate it:");
41760
- dim(" Dashboard: Organisation Settings > Integrations > Webhooks");
42291
+ dim(" Dashboard: Organization Settings > Integrations > Webhooks");
41761
42292
  dim(" API: POST /api/v1/projects/{projectId}/webhooks/{webhookId}/secret");
41762
42293
  dim(
41763
42294
  " Put it in AVSB_WEBHOOK_SECRET and verify with verifyWebhookSignature from @avsbhq/node."
@@ -41779,8 +42310,7 @@ var listenCommand = new Command("listen").description("Watch this project's webh
41779
42310
  if (opts.forward) {
41780
42311
  const validated = validateForwardUrl(opts.forward);
41781
42312
  if (!validated.ok) {
41782
- error(validated.reason);
41783
- process.exit(EXIT.failure);
42313
+ failCommand(new CliError("cli_usage", validated.reason));
41784
42314
  }
41785
42315
  forwardUrl = validated.url;
41786
42316
  }
@@ -42306,7 +42836,7 @@ var flagsCreateCommand = withOutputOptions(
42306
42836
  var flagsUpdateCommand = withOutputOptions(
42307
42837
  withProjectOption(
42308
42838
  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(
42309
- "--if-match <etag>",
42839
+ "--if-match <version>",
42310
42840
  "Only write if the flag still matches this version (from `avsb flags get`)"
42311
42841
  ).option("--force", "Write without checking the version first")
42312
42842
  )
@@ -42610,46 +43140,127 @@ var openCommand = withOutputOptions(
42610
43140
 
42611
43141
  // src/commands/completion.ts
42612
43142
  var COMPLETION_SHELLS = ["bash", "zsh", "fish"];
42613
- function commandNames(program3) {
42614
- return program3.commands.map((command) => command.name()).filter((name) => name !== "completion").sort();
43143
+ var ARGUMENT_CHOICES = {
43144
+ completion: COMPLETION_SHELLS,
43145
+ open: OPEN_TARGETS
43146
+ };
43147
+ function flagsOf(command, isRoot) {
43148
+ const flags = command.options.map((option) => option.long).filter((long) => typeof long === "string");
43149
+ flags.push("--help");
43150
+ if (isRoot) flags.push("--version");
43151
+ return [...new Set(flags)].sort();
43152
+ }
43153
+ function completionEntries(program3) {
43154
+ const entries = [];
43155
+ function visit(command, path23) {
43156
+ const subcommands = command.commands.map((child) => child.name()).sort();
43157
+ entries.push({
43158
+ path: path23,
43159
+ words: [...subcommands, ...ARGUMENT_CHOICES[path23] ?? []],
43160
+ flags: flagsOf(command, path23 === "")
43161
+ });
43162
+ for (const child of command.commands) {
43163
+ visit(child, path23 === "" ? child.name() : `${path23} ${child.name()}`);
43164
+ }
43165
+ }
43166
+ visit(program3, "");
43167
+ return entries;
42615
43168
  }
42616
- function bashCompletion(names) {
43169
+ var quoteBash = (value) => `"${value}"`;
43170
+ function bashCompletion(entries) {
43171
+ const known = entries.filter((entry) => entry.path !== "").map((entry) => quoteBash(entry.path));
43172
+ const cases = entries.map((entry) => {
43173
+ const offer = [...entry.words, ...entry.flags].join(" ");
43174
+ return ` ${quoteBash(entry.path)}) offer="${offer}" ;;`;
43175
+ });
42617
43176
  return `# avsb completion for bash. Add to ~/.bashrc:
42618
43177
  # eval "$(avsb completion bash)"
42619
43178
  _avsb_completions() {
42620
- local commands="${names.join(" ")}"
42621
- COMPREPLY=($(compgen -W "$commands" -- "\${COMP_WORDS[COMP_CWORD]}"))
43179
+ local cur="\${COMP_WORDS[COMP_CWORD]}" cmdpath="" word candidate i offer=""
43180
+ for ((i = 1; i < COMP_CWORD; i++)); do
43181
+ word="\${COMP_WORDS[i]}"
43182
+ [[ "$word" == -* ]] && continue
43183
+ candidate="\${cmdpath:+$cmdpath }$word"
43184
+ case "$candidate" in
43185
+ ${known.length > 0 ? known.join("|") : '"__none__"'}) cmdpath="$candidate" ;;
43186
+ esac
43187
+ done
43188
+ case "$cmdpath" in
43189
+ ${cases.join("\n")}
43190
+ esac
43191
+ COMPREPLY=($(compgen -W "$offer" -- "$cur"))
42622
43192
  }
42623
43193
  complete -F _avsb_completions avsb`;
42624
43194
  }
42625
- function zshCompletion(names) {
43195
+ var quoteZsh = (value) => `'${value}'`;
43196
+ function zshCompletion(entries) {
43197
+ const known = entries.filter((entry) => entry.path !== "").map((entry) => quoteZsh(entry.path));
43198
+ const cases = entries.map((entry) => {
43199
+ const offer = [...entry.words, ...entry.flags].map(quoteZsh).join(" ");
43200
+ return ` (${quoteZsh(entry.path)}) offer=(${offer}) ;;`;
43201
+ });
42626
43202
  return `# avsb completion for zsh. Add to ~/.zshrc:
42627
43203
  # eval "$(avsb completion zsh)"
42628
43204
  _avsb() {
42629
- local -a commands
42630
- commands=(${names.map((name) => `'${name}'`).join(" ")})
42631
- _describe 'command' commands
43205
+ local cmdpath="" word candidate i
43206
+ local -a offer
43207
+ for ((i = 2; i < CURRENT; i++)); do
43208
+ word="\${words[i]}"
43209
+ [[ "$word" == -* ]] && continue
43210
+ candidate="\${cmdpath:+$cmdpath }$word"
43211
+ case "$candidate" in
43212
+ (${known.length > 0 ? known.join("|") : "'__none__'"}) cmdpath="$candidate" ;;
43213
+ esac
43214
+ done
43215
+ case "$cmdpath" in
43216
+ ${cases.join("\n")}
43217
+ esac
43218
+ compadd -- $offer
42632
43219
  }
42633
43220
  compdef _avsb avsb`;
42634
43221
  }
42635
- function fishCompletion(names) {
42636
- const lines = names.map(
42637
- (name) => `complete -c avsb -n __fish_use_subcommand -a ${name}`
42638
- );
43222
+ var quoteFish = (value) => `'${value}'`;
43223
+ function fishCompletion(entries) {
43224
+ const known = entries.filter((entry) => entry.path !== "").map((entry) => quoteFish(entry.path));
43225
+ const lines = [];
43226
+ for (const entry of entries) {
43227
+ const condition = `'test (__avsb_path) = "${entry.path}"'`;
43228
+ if (entry.words.length > 0) {
43229
+ lines.push(`complete -c avsb -n ${condition} -a ${quoteFish(entry.words.join(" "))}`);
43230
+ }
43231
+ for (const flag of entry.flags) {
43232
+ lines.push(`complete -c avsb -n ${condition} -l ${flag.replace(/^--/, "")}`);
43233
+ }
43234
+ }
42639
43235
  return [
42640
43236
  "# avsb completion for fish. Save as:",
42641
43237
  "# avsb completion fish > ~/.config/fish/completions/avsb.fish",
43238
+ `set -g __avsb_known_paths ${known.join(" ")}`,
43239
+ // Drops the first token (`avsb`) with `set -e` rather than a [2..-1]
43240
+ // slice, and always prints exactly one line, so `test (__avsb_path) = ""`
43241
+ // compares one value even at the top level.
43242
+ "function __avsb_path",
43243
+ " set -l tokens (commandline -opc)",
43244
+ " set -e tokens[1]",
43245
+ " set -l cmdpath",
43246
+ " for word in $tokens",
43247
+ " string match -q -- '-*' $word; and continue",
43248
+ " set -l candidate (string join ' ' $cmdpath $word)",
43249
+ " contains -- $candidate $__avsb_known_paths; and set cmdpath $cmdpath $word",
43250
+ " end",
43251
+ " echo (string join ' ' $cmdpath)",
43252
+ "end",
42642
43253
  ...lines
42643
43254
  ].join("\n");
42644
43255
  }
42645
- function completionScript(shell, names) {
43256
+ function completionScript(shell, entries) {
42646
43257
  switch (shell) {
42647
43258
  case "bash":
42648
- return bashCompletion(names);
43259
+ return bashCompletion(entries);
42649
43260
  case "zsh":
42650
- return zshCompletion(names);
43261
+ return zshCompletion(entries);
42651
43262
  case "fish":
42652
- return fishCompletion(names);
43263
+ return fishCompletion(entries);
42653
43264
  }
42654
43265
  }
42655
43266
  function buildCompletionCommand(program3) {
@@ -42663,16 +43274,16 @@ function buildCompletionCommand(program3) {
42663
43274
  ])
42664
43275
  );
42665
43276
  }
42666
- line(completionScript(requested, commandNames(program3)));
43277
+ line(completionScript(requested, completionEntries(program3)));
42667
43278
  });
42668
43279
  }
42669
43280
 
42670
43281
  // src/lib/updateNotifier.ts
42671
- import fs16 from "node:fs";
43282
+ import fs17 from "node:fs";
42672
43283
  import os4 from "node:os";
42673
- import path20 from "node:path";
43284
+ import path21 from "node:path";
42674
43285
  var REGISTRY_URL = "https://registry.npmjs.org/@avsbhq/cli/latest";
42675
- var CACHE_FILE = path20.join(os4.homedir(), ".avsb", "update-check.json");
43286
+ var CACHE_FILE = path21.join(os4.homedir(), ".avsb", "update-check.json");
42676
43287
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
42677
43288
  var FETCH_TIMEOUT_MS = 1500;
42678
43289
  function isNewerVersion(latest, current) {
@@ -42712,8 +43323,8 @@ function updateMessage(latest, current) {
42712
43323
  }
42713
43324
  function writeCache(cache) {
42714
43325
  try {
42715
- fs16.mkdirSync(path20.dirname(CACHE_FILE), { recursive: true });
42716
- fs16.writeFileSync(CACHE_FILE, JSON.stringify(cache));
43326
+ fs17.mkdirSync(path21.dirname(CACHE_FILE), { recursive: true });
43327
+ fs17.writeFileSync(CACHE_FILE, JSON.stringify(cache));
42717
43328
  } catch {
42718
43329
  }
42719
43330
  }
@@ -42737,7 +43348,7 @@ async function checkForUpdate(currentVersion, notify) {
42737
43348
  }
42738
43349
  let cache = null;
42739
43350
  try {
42740
- cache = readCache(() => fs16.readFileSync(CACHE_FILE, "utf-8"));
43351
+ cache = readCache(() => fs17.readFileSync(CACHE_FILE, "utf-8"));
42741
43352
  } catch {
42742
43353
  cache = null;
42743
43354
  }
@@ -42750,8 +43361,8 @@ async function checkForUpdate(currentVersion, notify) {
42750
43361
  }
42751
43362
 
42752
43363
  // src/cli.ts
42753
- var __dirname = path21.dirname(fileURLToPath2(import.meta.url));
42754
- var pkg = JSON.parse(fs17.readFileSync(path21.resolve(__dirname, "..", "package.json"), "utf-8"));
43364
+ var __dirname = path22.dirname(fileURLToPath2(import.meta.url));
43365
+ var pkg = JSON.parse(fs18.readFileSync(path22.resolve(__dirname, "..", "package.json"), "utf-8"));
42755
43366
  var program2 = new Command();
42756
43367
  program2.name("avsb").description(
42757
43368
  "A vs B command line tool: experiments, flags, project scripts, metrics, datasets, webhooks, and typed flags"