ariadne-studio 0.1.0 → 0.1.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.mjs CHANGED
@@ -4012,10 +4012,10 @@ var require_resolve_block_map = __commonJS({
4012
4012
  let offset = bm.offset;
4013
4013
  let commentEnd = null;
4014
4014
  for (const collItem of bm.items) {
4015
- const { start, key, sep: sep5, value } = collItem;
4015
+ const { start, key, sep: sep6, value } = collItem;
4016
4016
  const keyProps = resolveProps.resolveProps(start, {
4017
4017
  indicator: "explicit-key-ind",
4018
- next: key ?? sep5?.[0],
4018
+ next: key ?? sep6?.[0],
4019
4019
  offset,
4020
4020
  onError,
4021
4021
  parentIndent: bm.indent,
@@ -4029,7 +4029,7 @@ var require_resolve_block_map = __commonJS({
4029
4029
  else if ("indent" in key && key.indent !== bm.indent)
4030
4030
  onError(offset, "BAD_INDENT", startColMsg);
4031
4031
  }
4032
- if (!keyProps.anchor && !keyProps.tag && !sep5) {
4032
+ if (!keyProps.anchor && !keyProps.tag && !sep6) {
4033
4033
  commentEnd = keyProps.end;
4034
4034
  if (keyProps.comment) {
4035
4035
  if (map.comment)
@@ -4053,7 +4053,7 @@ var require_resolve_block_map = __commonJS({
4053
4053
  ctx.atKey = false;
4054
4054
  if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode))
4055
4055
  onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
4056
- const valueProps = resolveProps.resolveProps(sep5 ?? [], {
4056
+ const valueProps = resolveProps.resolveProps(sep6 ?? [], {
4057
4057
  indicator: "map-value-ind",
4058
4058
  next: value,
4059
4059
  offset: keyNode.range[2],
@@ -4069,7 +4069,7 @@ var require_resolve_block_map = __commonJS({
4069
4069
  if (ctx.options.strict && keyProps.start < valueProps.found.offset - 1024)
4070
4070
  onError(keyNode.range, "KEY_OVER_1024_CHARS", "The : indicator must be at most 1024 chars after the start of an implicit block mapping key");
4071
4071
  }
4072
- const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset, sep5, null, valueProps, onError);
4072
+ const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset, sep6, null, valueProps, onError);
4073
4073
  if (ctx.schema.compat)
4074
4074
  utilFlowIndentCheck.flowIndentCheck(bm.indent, value, onError);
4075
4075
  offset = valueNode.range[2];
@@ -4160,7 +4160,7 @@ var require_resolve_end = __commonJS({
4160
4160
  let comment = "";
4161
4161
  if (end) {
4162
4162
  let hasSpace = false;
4163
- let sep5 = "";
4163
+ let sep6 = "";
4164
4164
  for (const token of end) {
4165
4165
  const { source, type } = token;
4166
4166
  switch (type) {
@@ -4174,13 +4174,13 @@ var require_resolve_end = __commonJS({
4174
4174
  if (!comment)
4175
4175
  comment = cb;
4176
4176
  else
4177
- comment += sep5 + cb;
4178
- sep5 = "";
4177
+ comment += sep6 + cb;
4178
+ sep6 = "";
4179
4179
  break;
4180
4180
  }
4181
4181
  case "newline":
4182
4182
  if (comment)
4183
- sep5 += source;
4183
+ sep6 += source;
4184
4184
  hasSpace = true;
4185
4185
  break;
4186
4186
  default:
@@ -4223,18 +4223,18 @@ var require_resolve_flow_collection = __commonJS({
4223
4223
  let offset = fc.offset + fc.start.source.length;
4224
4224
  for (let i = 0; i < fc.items.length; ++i) {
4225
4225
  const collItem = fc.items[i];
4226
- const { start, key, sep: sep5, value } = collItem;
4226
+ const { start, key, sep: sep6, value } = collItem;
4227
4227
  const props = resolveProps.resolveProps(start, {
4228
4228
  flow: fcName,
4229
4229
  indicator: "explicit-key-ind",
4230
- next: key ?? sep5?.[0],
4230
+ next: key ?? sep6?.[0],
4231
4231
  offset,
4232
4232
  onError,
4233
4233
  parentIndent: fc.indent,
4234
4234
  startOnNewline: false
4235
4235
  });
4236
4236
  if (!props.found) {
4237
- if (!props.anchor && !props.tag && !sep5 && !value) {
4237
+ if (!props.anchor && !props.tag && !sep6 && !value) {
4238
4238
  if (i === 0 && props.comma)
4239
4239
  onError(props.comma, "UNEXPECTED_TOKEN", `Unexpected , in ${fcName}`);
4240
4240
  else if (i < fc.items.length - 1)
@@ -4288,8 +4288,8 @@ var require_resolve_flow_collection = __commonJS({
4288
4288
  }
4289
4289
  }
4290
4290
  }
4291
- if (!isMap && !sep5 && !props.found) {
4292
- const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep5, null, props, onError);
4291
+ if (!isMap && !sep6 && !props.found) {
4292
+ const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep6, null, props, onError);
4293
4293
  coll.items.push(valueNode);
4294
4294
  offset = valueNode.range[2];
4295
4295
  if (isBlock(value))
@@ -4301,7 +4301,7 @@ var require_resolve_flow_collection = __commonJS({
4301
4301
  if (isBlock(key))
4302
4302
  onError(keyNode.range, "BLOCK_IN_FLOW", blockMsg);
4303
4303
  ctx.atKey = false;
4304
- const valueProps = resolveProps.resolveProps(sep5 ?? [], {
4304
+ const valueProps = resolveProps.resolveProps(sep6 ?? [], {
4305
4305
  flow: fcName,
4306
4306
  indicator: "map-value-ind",
4307
4307
  next: value,
@@ -4312,8 +4312,8 @@ var require_resolve_flow_collection = __commonJS({
4312
4312
  });
4313
4313
  if (valueProps.found) {
4314
4314
  if (!isMap && !props.found && ctx.options.strict) {
4315
- if (sep5)
4316
- for (const st of sep5) {
4315
+ if (sep6)
4316
+ for (const st of sep6) {
4317
4317
  if (st === valueProps.found)
4318
4318
  break;
4319
4319
  if (st.type === "newline") {
@@ -4330,7 +4330,7 @@ var require_resolve_flow_collection = __commonJS({
4330
4330
  else
4331
4331
  onError(valueProps.start, "MISSING_CHAR", `Missing , or : between ${fcName} items`);
4332
4332
  }
4333
- const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end, sep5, null, valueProps, onError) : null;
4333
+ const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end, sep6, null, valueProps, onError) : null;
4334
4334
  if (valueNode) {
4335
4335
  if (isBlock(value))
4336
4336
  onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg);
@@ -4510,7 +4510,7 @@ var require_resolve_block_scalar = __commonJS({
4510
4510
  chompStart = i + 1;
4511
4511
  }
4512
4512
  let value = "";
4513
- let sep5 = "";
4513
+ let sep6 = "";
4514
4514
  let prevMoreIndented = false;
4515
4515
  for (let i = 0; i < contentStart; ++i)
4516
4516
  value += lines[i][0].slice(trimIndent) + "\n";
@@ -4527,24 +4527,24 @@ var require_resolve_block_scalar = __commonJS({
4527
4527
  indent = "";
4528
4528
  }
4529
4529
  if (type === Scalar2.Scalar.BLOCK_LITERAL) {
4530
- value += sep5 + indent.slice(trimIndent) + content;
4531
- sep5 = "\n";
4530
+ value += sep6 + indent.slice(trimIndent) + content;
4531
+ sep6 = "\n";
4532
4532
  } else if (indent.length > trimIndent || content[0] === " ") {
4533
- if (sep5 === " ")
4534
- sep5 = "\n";
4535
- else if (!prevMoreIndented && sep5 === "\n")
4536
- sep5 = "\n\n";
4537
- value += sep5 + indent.slice(trimIndent) + content;
4538
- sep5 = "\n";
4533
+ if (sep6 === " ")
4534
+ sep6 = "\n";
4535
+ else if (!prevMoreIndented && sep6 === "\n")
4536
+ sep6 = "\n\n";
4537
+ value += sep6 + indent.slice(trimIndent) + content;
4538
+ sep6 = "\n";
4539
4539
  prevMoreIndented = true;
4540
4540
  } else if (content === "") {
4541
- if (sep5 === "\n")
4541
+ if (sep6 === "\n")
4542
4542
  value += "\n";
4543
4543
  else
4544
- sep5 = "\n";
4544
+ sep6 = "\n";
4545
4545
  } else {
4546
- value += sep5 + content;
4547
- sep5 = " ";
4546
+ value += sep6 + content;
4547
+ sep6 = " ";
4548
4548
  prevMoreIndented = false;
4549
4549
  }
4550
4550
  }
@@ -4727,25 +4727,25 @@ var require_resolve_flow_scalar = __commonJS({
4727
4727
  trimBoth = /^[ \t]+|[ \t]+$/g;
4728
4728
  }
4729
4729
  let res = match[1].replace(trimEnd, "");
4730
- let sep5 = " ";
4730
+ let sep6 = " ";
4731
4731
  let pos = line.lastIndex;
4732
4732
  while (match = line.exec(source)) {
4733
4733
  const lm = match[1].replace(trimBoth, "");
4734
4734
  if (lm === "") {
4735
- if (sep5 === "\n")
4736
- res += sep5;
4735
+ if (sep6 === "\n")
4736
+ res += sep6;
4737
4737
  else
4738
- sep5 = "\n";
4738
+ sep6 = "\n";
4739
4739
  } else {
4740
- res += sep5 + lm;
4741
- sep5 = " ";
4740
+ res += sep6 + lm;
4741
+ sep6 = " ";
4742
4742
  }
4743
4743
  pos = line.lastIndex;
4744
4744
  }
4745
4745
  const last = /[ \t]*(.*)/sy;
4746
4746
  last.lastIndex = pos;
4747
4747
  match = last.exec(source);
4748
- return res + sep5 + (match?.[1] ?? "");
4748
+ return res + sep6 + (match?.[1] ?? "");
4749
4749
  }
4750
4750
  function doubleQuotedValue(source, onError) {
4751
4751
  let res = "";
@@ -5555,14 +5555,14 @@ var require_cst_stringify = __commonJS({
5555
5555
  }
5556
5556
  }
5557
5557
  }
5558
- function stringifyItem({ start, key, sep: sep5, value }) {
5558
+ function stringifyItem({ start, key, sep: sep6, value }) {
5559
5559
  let res = "";
5560
5560
  for (const st of start)
5561
5561
  res += st.source;
5562
5562
  if (key)
5563
5563
  res += stringifyToken(key);
5564
- if (sep5)
5565
- for (const st of sep5)
5564
+ if (sep6)
5565
+ for (const st of sep6)
5566
5566
  res += st.source;
5567
5567
  if (value)
5568
5568
  res += stringifyToken(value);
@@ -6729,18 +6729,18 @@ var require_parser = __commonJS({
6729
6729
  if (this.type === "map-value-ind") {
6730
6730
  const prev = getPrevProps(this.peek(2));
6731
6731
  const start = getFirstKeyStartProps(prev);
6732
- let sep5;
6732
+ let sep6;
6733
6733
  if (scalar.end) {
6734
- sep5 = scalar.end;
6735
- sep5.push(this.sourceToken);
6734
+ sep6 = scalar.end;
6735
+ sep6.push(this.sourceToken);
6736
6736
  delete scalar.end;
6737
6737
  } else
6738
- sep5 = [this.sourceToken];
6738
+ sep6 = [this.sourceToken];
6739
6739
  const map = {
6740
6740
  type: "block-map",
6741
6741
  offset: scalar.offset,
6742
6742
  indent: scalar.indent,
6743
- items: [{ start, key: scalar, sep: sep5 }]
6743
+ items: [{ start, key: scalar, sep: sep6 }]
6744
6744
  };
6745
6745
  this.onKeyLine = true;
6746
6746
  this.stack[this.stack.length - 1] = map;
@@ -6893,15 +6893,15 @@ var require_parser = __commonJS({
6893
6893
  } else if (isFlowToken(it.key) && !includesToken(it.sep, "newline")) {
6894
6894
  const start2 = getFirstKeyStartProps(it.start);
6895
6895
  const key = it.key;
6896
- const sep5 = it.sep;
6897
- sep5.push(this.sourceToken);
6896
+ const sep6 = it.sep;
6897
+ sep6.push(this.sourceToken);
6898
6898
  delete it.key;
6899
6899
  delete it.sep;
6900
6900
  this.stack.push({
6901
6901
  type: "block-map",
6902
6902
  offset: this.offset,
6903
6903
  indent: this.indent,
6904
- items: [{ start: start2, key, sep: sep5 }]
6904
+ items: [{ start: start2, key, sep: sep6 }]
6905
6905
  });
6906
6906
  } else if (start.length > 0) {
6907
6907
  it.sep = it.sep.concat(start, this.sourceToken);
@@ -7095,13 +7095,13 @@ var require_parser = __commonJS({
7095
7095
  const prev = getPrevProps(parent);
7096
7096
  const start = getFirstKeyStartProps(prev);
7097
7097
  fixFlowSeqItems(fc);
7098
- const sep5 = fc.end.splice(1, fc.end.length);
7099
- sep5.push(this.sourceToken);
7098
+ const sep6 = fc.end.splice(1, fc.end.length);
7099
+ sep6.push(this.sourceToken);
7100
7100
  const map = {
7101
7101
  type: "block-map",
7102
7102
  offset: fc.offset,
7103
7103
  indent: fc.indent,
7104
- items: [{ start, key: fc, sep: sep5 }]
7104
+ items: [{ start, key: fc, sep: sep6 }]
7105
7105
  };
7106
7106
  this.onKeyLine = true;
7107
7107
  this.stack[this.stack.length - 1] = map;
@@ -11337,7 +11337,7 @@ var require_parseTokenList = __commonJS({
11337
11337
  // ../../node_modules/.pnpm/depd@2.0.0/node_modules/depd/index.js
11338
11338
  var require_depd = __commonJS({
11339
11339
  "../../node_modules/.pnpm/depd@2.0.0/node_modules/depd/index.js"(exports, module) {
11340
- var relative4 = __require("path").relative;
11340
+ var relative5 = __require("path").relative;
11341
11341
  module.exports = depd;
11342
11342
  var basePath = process.cwd();
11343
11343
  function containsNamespace(str, namespace) {
@@ -11529,7 +11529,7 @@ var require_depd = __commonJS({
11529
11529
  return formatted;
11530
11530
  }
11531
11531
  function formatLocation(callSite) {
11532
- return relative4(basePath, callSite[0]) + ":" + callSite[1] + ":" + callSite[2];
11532
+ return relative5(basePath, callSite[0]) + ":" + callSite[1] + ":" + callSite[2];
11533
11533
  }
11534
11534
  function getStack() {
11535
11535
  var limit = Error.stackTraceLimit;
@@ -12062,11 +12062,11 @@ var require_send = __commonJS({
12062
12062
  var { parseTokenList } = require_parseTokenList();
12063
12063
  var { createHttpError } = require_createHttpError();
12064
12064
  var extname2 = path.extname;
12065
- var join16 = path.join;
12065
+ var join18 = path.join;
12066
12066
  var normalize = path.normalize;
12067
- var resolve6 = path.resolve;
12068
- var sep5 = path.sep;
12069
- var Readable = stream.Readable;
12067
+ var resolve7 = path.resolve;
12068
+ var sep6 = path.sep;
12069
+ var Readable2 = stream.Readable;
12070
12070
  var BYTES_RANGE_REGEXP = /^ *bytes=/;
12071
12071
  var MAX_MAXAGE = 60 * 60 * 24 * 365 * 1e3;
12072
12072
  var UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
@@ -12118,7 +12118,7 @@ var require_send = __commonJS({
12118
12118
  }
12119
12119
  onFile = options.onFile;
12120
12120
  }
12121
- const root = options.root ? resolve6(options.root) : null;
12121
+ const root = options.root ? resolve7(options.root) : null;
12122
12122
  const highWaterMark = Number.isSafeInteger(options.highWaterMark) && options.highWaterMark > 0 ? options.highWaterMark : null;
12123
12123
  return {
12124
12124
  acceptRanges,
@@ -12150,21 +12150,21 @@ var require_send = __commonJS({
12150
12150
  let parts;
12151
12151
  if (root !== null) {
12152
12152
  if (path2) {
12153
- path2 = normalize("." + sep5 + path2);
12153
+ path2 = normalize("." + sep6 + path2);
12154
12154
  }
12155
12155
  if (UP_PATH_REGEXP.test(path2)) {
12156
12156
  debug('malicious path "%s"', path2);
12157
12157
  return { statusCode: 403 };
12158
12158
  }
12159
- parts = path2.split(sep5);
12160
- path2 = normalize(join16(root, path2));
12159
+ parts = path2.split(sep6);
12160
+ path2 = normalize(join18(root, path2));
12161
12161
  } else {
12162
12162
  if (UP_PATH_REGEXP.test(path2)) {
12163
12163
  debug('malicious path "%s"', path2);
12164
12164
  return { statusCode: 403 };
12165
12165
  }
12166
- parts = normalize(path2).split(sep5);
12167
- path2 = resolve6(path2);
12166
+ parts = normalize(path2).split(sep6);
12167
+ path2 = resolve7(path2);
12168
12168
  }
12169
12169
  return { path: path2, parts };
12170
12170
  }
@@ -12260,9 +12260,9 @@ var require_send = __commonJS({
12260
12260
  );
12261
12261
  }
12262
12262
  function tryStat(path2) {
12263
- return new Promise((resolve7) => {
12263
+ return new Promise((resolve8) => {
12264
12264
  fs.stat(path2, function onstat(error, stat3) {
12265
- resolve7({ error, stat: stat3 });
12265
+ resolve8({ error, stat: stat3 });
12266
12266
  });
12267
12267
  });
12268
12268
  }
@@ -12281,7 +12281,7 @@ var require_send = __commonJS({
12281
12281
  return {
12282
12282
  statusCode,
12283
12283
  headers,
12284
- stream: Readable.from(doc[0]),
12284
+ stream: Readable2.from(doc[0]),
12285
12285
  // metadata
12286
12286
  type: "error",
12287
12287
  metadata: { error: createHttpError(statusCode, err) }
@@ -12307,7 +12307,7 @@ var require_send = __commonJS({
12307
12307
  return {
12308
12308
  statusCode: 304,
12309
12309
  headers,
12310
- stream: Readable.from(""),
12310
+ stream: Readable2.from(""),
12311
12311
  // metadata
12312
12312
  type: "file",
12313
12313
  metadata: { path: path2, stat: stat3 }
@@ -12398,7 +12398,7 @@ var require_send = __commonJS({
12398
12398
  return {
12399
12399
  statusCode,
12400
12400
  headers,
12401
- stream: Readable.from(""),
12401
+ stream: Readable2.from(""),
12402
12402
  // metadata
12403
12403
  type: "file",
12404
12404
  metadata: { path: path2, stat: stat3 }
@@ -12433,7 +12433,7 @@ var require_send = __commonJS({
12433
12433
  return {
12434
12434
  statusCode: 301,
12435
12435
  headers,
12436
- stream: Readable.from(doc[0]),
12436
+ stream: Readable2.from(doc[0]),
12437
12437
  // metadata
12438
12438
  type: "directory",
12439
12439
  metadata: { requestPath: options.path, path: path2 }
@@ -12443,7 +12443,7 @@ var require_send = __commonJS({
12443
12443
  let err;
12444
12444
  for (let i = 0; i < options.index.length; i++) {
12445
12445
  const index = options.index[i];
12446
- const p = join16(path2, index);
12446
+ const p = join18(path2, index);
12447
12447
  const { error, stat: stat3 } = await tryStat(p);
12448
12448
  if (error) {
12449
12449
  err = error;
@@ -12459,7 +12459,7 @@ var require_send = __commonJS({
12459
12459
  }
12460
12460
  async function sendFile(request, path2, options) {
12461
12461
  const { error, stat: stat3 } = await tryStat(path2);
12462
- if (error && error.code === "ENOENT" && !extname2(path2) && path2[path2.length - 1] !== sep5) {
12462
+ if (error && error.code === "ENOENT" && !extname2(path2) && path2[path2.length - 1] !== sep6) {
12463
12463
  let err = error;
12464
12464
  for (let i = 0; i < options.extensions.length; i++) {
12465
12465
  const extension = options.extensions[i];
@@ -13243,41 +13243,41 @@ var require_queue = __commonJS({
13243
13243
  queue.drained = drained;
13244
13244
  return queue;
13245
13245
  function push(value) {
13246
- var p = new Promise(function(resolve6, reject) {
13246
+ var p = new Promise(function(resolve7, reject) {
13247
13247
  pushCb(value, function(err, result) {
13248
13248
  if (err) {
13249
13249
  reject(err);
13250
13250
  return;
13251
13251
  }
13252
- resolve6(result);
13252
+ resolve7(result);
13253
13253
  });
13254
13254
  });
13255
13255
  p.catch(noop);
13256
13256
  return p;
13257
13257
  }
13258
13258
  function unshift(value) {
13259
- var p = new Promise(function(resolve6, reject) {
13259
+ var p = new Promise(function(resolve7, reject) {
13260
13260
  unshiftCb(value, function(err, result) {
13261
13261
  if (err) {
13262
13262
  reject(err);
13263
13263
  return;
13264
13264
  }
13265
- resolve6(result);
13265
+ resolve7(result);
13266
13266
  });
13267
13267
  });
13268
13268
  p.catch(noop);
13269
13269
  return p;
13270
13270
  }
13271
13271
  function drained() {
13272
- var p = new Promise(function(resolve6) {
13272
+ var p = new Promise(function(resolve7) {
13273
13273
  process.nextTick(function() {
13274
13274
  if (queue.idle()) {
13275
- resolve6();
13275
+ resolve7();
13276
13276
  } else {
13277
13277
  var previousDrain = queue.drain;
13278
13278
  queue.drain = function() {
13279
13279
  if (typeof previousDrain === "function") previousDrain();
13280
- resolve6();
13280
+ resolve7();
13281
13281
  queue.drain = previousDrain;
13282
13282
  };
13283
13283
  }
@@ -13617,7 +13617,7 @@ var require_static = __commonJS({
13617
13617
  var path = __require("node:path");
13618
13618
  var { fileURLToPath: fileURLToPath4 } = __require("node:url");
13619
13619
  var { statSync: statSync2 } = __require("node:fs");
13620
- var { readdir: readdir7, stat: stat3 } = __require("node:fs/promises");
13620
+ var { readdir: readdir9, stat: stat3 } = __require("node:fs/promises");
13621
13621
  var { basename: basename3 } = __require("node:path");
13622
13622
  var { glob } = require_index_min();
13623
13623
  var fp = require_fastify_plugin();
@@ -13796,8 +13796,8 @@ var require_static = __commonJS({
13796
13796
  }
13797
13797
  }
13798
13798
  }
13799
- for (const [dirname9, rootPath] of indexDirs.entries()) {
13800
- const pathname = dirname9 + (dirname9.endsWith("/") ? "" : "/");
13799
+ for (const [dirname11, rootPath] of indexDirs.entries()) {
13800
+ const pathname = dirname11 + (dirname11.endsWith("/") ? "" : "/");
13801
13801
  const file = "/" + pathname.replace(prefix, "");
13802
13802
  setUpHeadAndGet(routeOpts, pathname, file, rootPath);
13803
13803
  if (opts.redirect === true) {
@@ -14153,7 +14153,7 @@ var require_static = __commonJS({
14153
14153
  for (const segment of segments) {
14154
14154
  let entries;
14155
14155
  try {
14156
- entries = await readdir7(parent);
14156
+ entries = await readdir9(parent);
14157
14157
  } catch (error) {
14158
14158
  return { status: isMissingPathError(error) ? "missing" : "unverifiable" };
14159
14159
  }
@@ -14599,8 +14599,8 @@ var require_create_promise = __commonJS({
14599
14599
  reject: null,
14600
14600
  promise: null
14601
14601
  };
14602
- obj.promise = new Promise((resolve6, reject) => {
14603
- obj.resolve = resolve6;
14602
+ obj.promise = new Promise((resolve7, reject) => {
14603
+ obj.resolve = resolve7;
14604
14604
  obj.reject = reject;
14605
14605
  });
14606
14606
  return obj;
@@ -14873,11 +14873,11 @@ var require_thenify = __commonJS({
14873
14873
  return;
14874
14874
  }
14875
14875
  debug("thenify");
14876
- return (resolve6, reject) => {
14876
+ return (resolve7, reject) => {
14877
14877
  const p = this._loadRegistered();
14878
14878
  return p.then(() => {
14879
14879
  this[kThenifyDoNotWrap] = true;
14880
- return resolve6(this._server);
14880
+ return resolve7(this._server);
14881
14881
  }, reject);
14882
14882
  };
14883
14883
  }
@@ -15015,12 +15015,12 @@ var require_avvio = __commonJS({
15015
15015
  inherits(Boot, EE);
15016
15016
  if ("asyncDispose" in Symbol) {
15017
15017
  Boot.prototype[Symbol.asyncDispose] = function() {
15018
- return new Promise((resolve6, reject) => {
15018
+ return new Promise((resolve7, reject) => {
15019
15019
  this.close((err) => {
15020
15020
  if (err) {
15021
15021
  return reject(err);
15022
15022
  }
15023
- resolve6();
15023
+ resolve7();
15024
15024
  });
15025
15025
  });
15026
15026
  };
@@ -15167,12 +15167,12 @@ var require_avvio = __commonJS({
15167
15167
  throw new AVV_ERR_CALLBACK_NOT_FN("close", typeof func);
15168
15168
  }
15169
15169
  } else {
15170
- promise = new Promise(function(resolve6, reject) {
15170
+ promise = new Promise(function(resolve7, reject) {
15171
15171
  func = function(err) {
15172
15172
  if (err) {
15173
15173
  return reject(err);
15174
15174
  }
15175
- resolve6();
15175
+ resolve7();
15176
15176
  };
15177
15177
  });
15178
15178
  }
@@ -15192,7 +15192,7 @@ var require_avvio = __commonJS({
15192
15192
  queueMicrotask(this.start.bind(this));
15193
15193
  return;
15194
15194
  }
15195
- return new Promise((resolve6, reject) => {
15195
+ return new Promise((resolve7, reject) => {
15196
15196
  this._readyQ.push(readyPromiseCB);
15197
15197
  this.start();
15198
15198
  const relativeContext = this._current[0].server;
@@ -15200,7 +15200,7 @@ var require_avvio = __commonJS({
15200
15200
  if (err) {
15201
15201
  reject(err);
15202
15202
  } else {
15203
- resolve6(relativeContext);
15203
+ resolve7(relativeContext);
15204
15204
  }
15205
15205
  process.nextTick(done);
15206
15206
  }
@@ -16595,8 +16595,8 @@ var require_promise = __commonJS({
16595
16595
  var { kTestInternals } = require_symbols2();
16596
16596
  function withResolvers() {
16597
16597
  let res, rej;
16598
- const promise = new Promise((resolve6, reject) => {
16599
- res = resolve6;
16598
+ const promise = new Promise((resolve7, reject) => {
16599
+ res = resolve7;
16600
16600
  rej = reject;
16601
16601
  });
16602
16602
  return { promise, resolve: res, reject: rej };
@@ -16695,15 +16695,15 @@ var require_server = __commonJS({
16695
16695
  if (cb === void 0) {
16696
16696
  const listening = listenPromise.call(this, server, listenOptions);
16697
16697
  return listening.then((address) => {
16698
- const { promise, resolve: resolve6 } = PonyPromise.withResolvers();
16698
+ const { promise, resolve: resolve7 } = PonyPromise.withResolvers();
16699
16699
  if (host === "localhost") {
16700
16700
  multipleBindings.call(this, server, httpHandler, options, listenOptions, () => {
16701
16701
  this[kState].listening = true;
16702
- resolve6(address);
16702
+ resolve7(address);
16703
16703
  onListenHookRunner(this);
16704
16704
  });
16705
16705
  } else {
16706
- resolve6(address);
16706
+ resolve7(address);
16707
16707
  onListenHookRunner(this);
16708
16708
  }
16709
16709
  return promise;
@@ -16830,7 +16830,7 @@ var require_server = __commonJS({
16830
16830
  }
16831
16831
  return this.ready().then(() => {
16832
16832
  if (this[kState].aborted) return;
16833
- const { promise, resolve: resolve6, reject } = PonyPromise.withResolvers();
16833
+ const { promise, resolve: resolve7, reject } = PonyPromise.withResolvers();
16834
16834
  const errEventHandler = (err) => {
16835
16835
  cleanup();
16836
16836
  this[kState].listening = false;
@@ -16839,7 +16839,7 @@ var require_server = __commonJS({
16839
16839
  const listeningEventHandler = () => {
16840
16840
  cleanup();
16841
16841
  this[kState].listening = true;
16842
- resolve6(logServerAddress.call(
16842
+ resolve7(logServerAddress.call(
16843
16843
  this,
16844
16844
  server,
16845
16845
  listenOptions.listenTextResolver || defaultResolveServerListeningText
@@ -21116,7 +21116,7 @@ var require_atomic_sleep = __commonJS({
21116
21116
  "../../node_modules/.pnpm/atomic-sleep@1.0.0/node_modules/atomic-sleep/index.js"(exports, module) {
21117
21117
  "use strict";
21118
21118
  if (typeof SharedArrayBuffer !== "undefined" && typeof Atomics !== "undefined") {
21119
- let sleep = function(ms) {
21119
+ let sleep2 = function(ms) {
21120
21120
  const valid = ms > 0 && ms < Infinity;
21121
21121
  if (valid === false) {
21122
21122
  if (typeof ms !== "number" && typeof ms !== "bigint") {
@@ -21127,9 +21127,9 @@ var require_atomic_sleep = __commonJS({
21127
21127
  Atomics.wait(nil, 0, 0, Number(ms));
21128
21128
  };
21129
21129
  const nil = new Int32Array(new SharedArrayBuffer(4));
21130
- module.exports = sleep;
21130
+ module.exports = sleep2;
21131
21131
  } else {
21132
- let sleep = function(ms) {
21132
+ let sleep2 = function(ms) {
21133
21133
  const valid = ms > 0 && ms < Infinity;
21134
21134
  if (valid === false) {
21135
21135
  if (typeof ms !== "number" && typeof ms !== "bigint") {
@@ -21141,7 +21141,7 @@ var require_atomic_sleep = __commonJS({
21141
21141
  while (target > Date.now()) {
21142
21142
  }
21143
21143
  };
21144
- module.exports = sleep;
21144
+ module.exports = sleep2;
21145
21145
  }
21146
21146
  }
21147
21147
  });
@@ -21154,7 +21154,7 @@ var require_sonic_boom = __commonJS({
21154
21154
  var EventEmitter = __require("events");
21155
21155
  var inherits = __require("util").inherits;
21156
21156
  var path = __require("path");
21157
- var sleep = require_atomic_sleep();
21157
+ var sleep2 = require_atomic_sleep();
21158
21158
  var assert = __require("assert");
21159
21159
  var BUSY_WRITE_TIMEOUT = 100;
21160
21160
  var kEmptyBuffer = Buffer.allocUnsafe(0);
@@ -21227,7 +21227,7 @@ var require_sonic_boom = __commonJS({
21227
21227
  if (!(this instanceof SonicBoom)) {
21228
21228
  return new SonicBoom(opts);
21229
21229
  }
21230
- let { fd, dest, minLength, maxLength, maxWrite, periodicFlush, sync, append = true, mkdir: mkdir10, retryEAGAIN, fsync, contentMode, mode } = opts || {};
21230
+ let { fd, dest, minLength, maxLength, maxWrite, periodicFlush, sync, append = true, mkdir: mkdir13, retryEAGAIN, fsync, contentMode, mode } = opts || {};
21231
21231
  fd = fd || dest;
21232
21232
  this._len = 0;
21233
21233
  this.fd = -1;
@@ -21252,7 +21252,7 @@ var require_sonic_boom = __commonJS({
21252
21252
  this.append = append || false;
21253
21253
  this.mode = mode;
21254
21254
  this.retryEAGAIN = retryEAGAIN || (() => true);
21255
- this.mkdir = mkdir10 || false;
21255
+ this.mkdir = mkdir13 || false;
21256
21256
  let fsWriteSync;
21257
21257
  let fsWrite;
21258
21258
  if (contentMode === kContentModeBuffer) {
@@ -21300,7 +21300,7 @@ var require_sonic_boom = __commonJS({
21300
21300
  if ((err.code === "EAGAIN" || err.code === "EBUSY") && this.retryEAGAIN(err, this._writingBuf.length, this._len - this._writingBuf.length)) {
21301
21301
  if (this.sync) {
21302
21302
  try {
21303
- sleep(BUSY_WRITE_TIMEOUT);
21303
+ sleep2(BUSY_WRITE_TIMEOUT);
21304
21304
  this.release(void 0, 0);
21305
21305
  } catch (err2) {
21306
21306
  this.release(err2);
@@ -21613,7 +21613,7 @@ var require_sonic_boom = __commonJS({
21613
21613
  if (shouldRetry && !this.retryEAGAIN(err, buf.length, this._len - buf.length)) {
21614
21614
  throw err;
21615
21615
  }
21616
- sleep(BUSY_WRITE_TIMEOUT);
21616
+ sleep2(BUSY_WRITE_TIMEOUT);
21617
21617
  }
21618
21618
  }
21619
21619
  try {
@@ -21650,7 +21650,7 @@ var require_sonic_boom = __commonJS({
21650
21650
  if (shouldRetry && !this.retryEAGAIN(err, buf.length, this._len - buf.length)) {
21651
21651
  throw err;
21652
21652
  }
21653
- sleep(BUSY_WRITE_TIMEOUT);
21653
+ sleep2(BUSY_WRITE_TIMEOUT);
21654
21654
  }
21655
21655
  }
21656
21656
  }
@@ -21964,7 +21964,7 @@ var require_thread_stream = __commonJS({
21964
21964
  var { version: version2 } = require_package();
21965
21965
  var { EventEmitter } = __require("events");
21966
21966
  var { Worker } = __require("worker_threads");
21967
- var { join: join16 } = __require("path");
21967
+ var { join: join18 } = __require("path");
21968
21968
  var { pathToFileURL } = __require("url");
21969
21969
  var { wait } = require_wait();
21970
21970
  var {
@@ -22015,7 +22015,7 @@ var require_thread_stream = __commonJS({
22015
22015
  function createWorker(stream, opts) {
22016
22016
  const { filename, workerData } = opts;
22017
22017
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
22018
- const toExecute = bundlerOverrides["thread-stream-worker"] || join16(__dirname, "lib", "worker.js");
22018
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join18(__dirname, "lib", "worker.js");
22019
22019
  const worker = new Worker(toExecute, {
22020
22020
  ...opts.workerOpts,
22021
22021
  name: opts.workerOpts?.name || "thread-stream",
@@ -22481,11 +22481,11 @@ var require_transport = __commonJS({
22481
22481
  "../../node_modules/.pnpm/pino@10.4.0/node_modules/pino/lib/transport.js"(exports, module) {
22482
22482
  "use strict";
22483
22483
  var { createRequire: createRequire2 } = __require("module");
22484
- var { existsSync: existsSync9 } = __require("node:fs");
22484
+ var { existsSync: existsSync10 } = __require("node:fs");
22485
22485
  var getCallers = require_caller();
22486
- var { join: join16, isAbsolute: isAbsolute2, sep: sep5 } = __require("node:path");
22486
+ var { join: join18, isAbsolute: isAbsolute2, sep: sep6 } = __require("node:path");
22487
22487
  var { fileURLToPath: fileURLToPath4 } = __require("node:url");
22488
- var sleep = require_atomic_sleep();
22488
+ var sleep2 = require_atomic_sleep();
22489
22489
  var onExit = require_on_exit_leak_free();
22490
22490
  var ThreadStream = require_thread_stream();
22491
22491
  var { transportUsesMultistreamSym } = require_symbols3();
@@ -22556,7 +22556,7 @@ var require_transport = __commonJS({
22556
22556
  return false;
22557
22557
  }
22558
22558
  }
22559
- return isAbsolute2(path) && !existsSync9(path);
22559
+ return isAbsolute2(path) && !existsSync10(path);
22560
22560
  }
22561
22561
  function stripQuotes(value) {
22562
22562
  const first = value[0];
@@ -22609,7 +22609,7 @@ var require_transport = __commonJS({
22609
22609
  return;
22610
22610
  }
22611
22611
  stream.flushSync();
22612
- sleep(100);
22612
+ sleep2(100);
22613
22613
  stream.end();
22614
22614
  }
22615
22615
  return stream;
@@ -22626,7 +22626,7 @@ var require_transport = __commonJS({
22626
22626
  stream.flushSync();
22627
22627
  }
22628
22628
  function transport(fullOptions) {
22629
- const { pipeline, targets, levels, dedupe, worker = {}, caller = getCallers(), sync = false } = fullOptions;
22629
+ const { pipeline: pipeline2, targets, levels, dedupe, worker = {}, caller = getCallers(), sync = false } = fullOptions;
22630
22630
  const options = {
22631
22631
  ...fullOptions.options
22632
22632
  };
@@ -22638,7 +22638,7 @@ var require_transport = __commonJS({
22638
22638
  throw new Error("only one of target or targets can be specified");
22639
22639
  }
22640
22640
  if (targets) {
22641
- target = bundlerOverrides["pino-worker"] || join16(__dirname, "worker.js");
22641
+ target = bundlerOverrides["pino-worker"] || join18(__dirname, "worker.js");
22642
22642
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
22643
22643
  return {
22644
22644
  ...dest,
@@ -22658,9 +22658,9 @@ var require_transport = __commonJS({
22658
22658
  const targetsHaveLevel = options.targets.some((t) => t.level != null);
22659
22659
  const pipelinesHaveLevel = options.pipelines.some((p) => p.some((t) => t.level != null));
22660
22660
  usesMultistream = options.targets.length + options.pipelines.length > 1 || targetsHaveLevel || pipelinesHaveLevel;
22661
- } else if (pipeline) {
22662
- target = bundlerOverrides["pino-worker"] || join16(__dirname, "worker.js");
22663
- options.pipelines = [pipeline.map((dest) => {
22661
+ } else if (pipeline2) {
22662
+ target = bundlerOverrides["pino-worker"] || join18(__dirname, "worker.js");
22663
+ options.pipelines = [pipeline2.map((dest) => {
22664
22664
  return {
22665
22665
  ...dest,
22666
22666
  target: fixTarget(dest.target)
@@ -22674,7 +22674,7 @@ var require_transport = __commonJS({
22674
22674
  options.dedupe = dedupe;
22675
22675
  }
22676
22676
  options.pinoWillSendConfig = true;
22677
- const name = targets || pipeline ? "pino.transport" : target;
22677
+ const name = targets || pipeline2 ? "pino.transport" : target;
22678
22678
  const stream = buildStream(fixTarget(target), options, worker, sync, name);
22679
22679
  if (usesMultistream) {
22680
22680
  stream[transportUsesMultistreamSym] = true;
@@ -22686,12 +22686,12 @@ var require_transport = __commonJS({
22686
22686
  return origin;
22687
22687
  }
22688
22688
  if (origin === "pino/file") {
22689
- return join16(__dirname, "..", "file.js");
22689
+ return join18(__dirname, "..", "file.js");
22690
22690
  }
22691
22691
  let fixTarget2;
22692
22692
  for (const filePath of callers) {
22693
22693
  try {
22694
- const context = filePath === "node:repl" ? process.cwd() + sep5 : filePath;
22694
+ const context = filePath === "node:repl" ? process.cwd() + sep6 : filePath;
22695
22695
  fixTarget2 = createRequire2(context).resolve(origin);
22696
22696
  break;
22697
22697
  } catch (err) {
@@ -23666,7 +23666,7 @@ var require_safe_stable_stringify = __commonJS({
23666
23666
  return circularValue;
23667
23667
  }
23668
23668
  let res = "";
23669
- let join16 = ",";
23669
+ let join18 = ",";
23670
23670
  const originalIndentation = indentation;
23671
23671
  if (Array.isArray(value)) {
23672
23672
  if (value.length === 0) {
@@ -23680,7 +23680,7 @@ var require_safe_stable_stringify = __commonJS({
23680
23680
  indentation += spacer;
23681
23681
  res += `
23682
23682
  ${indentation}`;
23683
- join16 = `,
23683
+ join18 = `,
23684
23684
  ${indentation}`;
23685
23685
  }
23686
23686
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -23688,13 +23688,13 @@ ${indentation}`;
23688
23688
  for (; i < maximumValuesToStringify - 1; i++) {
23689
23689
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
23690
23690
  res += tmp2 !== void 0 ? tmp2 : "null";
23691
- res += join16;
23691
+ res += join18;
23692
23692
  }
23693
23693
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
23694
23694
  res += tmp !== void 0 ? tmp : "null";
23695
23695
  if (value.length - 1 > maximumBreadth) {
23696
23696
  const removedKeys = value.length - maximumBreadth - 1;
23697
- res += `${join16}"... ${getItemCount(removedKeys)} not stringified"`;
23697
+ res += `${join18}"... ${getItemCount(removedKeys)} not stringified"`;
23698
23698
  }
23699
23699
  if (spacer !== "") {
23700
23700
  res += `
@@ -23715,7 +23715,7 @@ ${originalIndentation}`;
23715
23715
  let separator = "";
23716
23716
  if (spacer !== "") {
23717
23717
  indentation += spacer;
23718
- join16 = `,
23718
+ join18 = `,
23719
23719
  ${indentation}`;
23720
23720
  whitespace = " ";
23721
23721
  }
@@ -23729,13 +23729,13 @@ ${indentation}`;
23729
23729
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
23730
23730
  if (tmp !== void 0) {
23731
23731
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
23732
- separator = join16;
23732
+ separator = join18;
23733
23733
  }
23734
23734
  }
23735
23735
  if (keyLength > maximumBreadth) {
23736
23736
  const removedKeys = keyLength - maximumBreadth;
23737
23737
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
23738
- separator = join16;
23738
+ separator = join18;
23739
23739
  }
23740
23740
  if (spacer !== "" && separator.length > 1) {
23741
23741
  res = `
@@ -23776,7 +23776,7 @@ ${originalIndentation}`;
23776
23776
  }
23777
23777
  const originalIndentation = indentation;
23778
23778
  let res = "";
23779
- let join16 = ",";
23779
+ let join18 = ",";
23780
23780
  if (Array.isArray(value)) {
23781
23781
  if (value.length === 0) {
23782
23782
  return "[]";
@@ -23789,7 +23789,7 @@ ${originalIndentation}`;
23789
23789
  indentation += spacer;
23790
23790
  res += `
23791
23791
  ${indentation}`;
23792
- join16 = `,
23792
+ join18 = `,
23793
23793
  ${indentation}`;
23794
23794
  }
23795
23795
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -23797,13 +23797,13 @@ ${indentation}`;
23797
23797
  for (; i < maximumValuesToStringify - 1; i++) {
23798
23798
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
23799
23799
  res += tmp2 !== void 0 ? tmp2 : "null";
23800
- res += join16;
23800
+ res += join18;
23801
23801
  }
23802
23802
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
23803
23803
  res += tmp !== void 0 ? tmp : "null";
23804
23804
  if (value.length - 1 > maximumBreadth) {
23805
23805
  const removedKeys = value.length - maximumBreadth - 1;
23806
- res += `${join16}"... ${getItemCount(removedKeys)} not stringified"`;
23806
+ res += `${join18}"... ${getItemCount(removedKeys)} not stringified"`;
23807
23807
  }
23808
23808
  if (spacer !== "") {
23809
23809
  res += `
@@ -23816,7 +23816,7 @@ ${originalIndentation}`;
23816
23816
  let whitespace = "";
23817
23817
  if (spacer !== "") {
23818
23818
  indentation += spacer;
23819
- join16 = `,
23819
+ join18 = `,
23820
23820
  ${indentation}`;
23821
23821
  whitespace = " ";
23822
23822
  }
@@ -23825,7 +23825,7 @@ ${indentation}`;
23825
23825
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
23826
23826
  if (tmp !== void 0) {
23827
23827
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
23828
- separator = join16;
23828
+ separator = join18;
23829
23829
  }
23830
23830
  }
23831
23831
  if (spacer !== "" && separator.length > 1) {
@@ -23883,20 +23883,20 @@ ${originalIndentation}`;
23883
23883
  indentation += spacer;
23884
23884
  let res2 = `
23885
23885
  ${indentation}`;
23886
- const join17 = `,
23886
+ const join19 = `,
23887
23887
  ${indentation}`;
23888
23888
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
23889
23889
  let i = 0;
23890
23890
  for (; i < maximumValuesToStringify - 1; i++) {
23891
23891
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
23892
23892
  res2 += tmp2 !== void 0 ? tmp2 : "null";
23893
- res2 += join17;
23893
+ res2 += join19;
23894
23894
  }
23895
23895
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
23896
23896
  res2 += tmp !== void 0 ? tmp : "null";
23897
23897
  if (value.length - 1 > maximumBreadth) {
23898
23898
  const removedKeys = value.length - maximumBreadth - 1;
23899
- res2 += `${join17}"... ${getItemCount(removedKeys)} not stringified"`;
23899
+ res2 += `${join19}"... ${getItemCount(removedKeys)} not stringified"`;
23900
23900
  }
23901
23901
  res2 += `
23902
23902
  ${originalIndentation}`;
@@ -23912,16 +23912,16 @@ ${originalIndentation}`;
23912
23912
  return '"[Object]"';
23913
23913
  }
23914
23914
  indentation += spacer;
23915
- const join16 = `,
23915
+ const join18 = `,
23916
23916
  ${indentation}`;
23917
23917
  let res = "";
23918
23918
  let separator = "";
23919
23919
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
23920
23920
  if (isTypedArrayWithEntries(value)) {
23921
- res += stringifyTypedArray(value, join16, maximumBreadth);
23921
+ res += stringifyTypedArray(value, join18, maximumBreadth);
23922
23922
  keys = keys.slice(value.length);
23923
23923
  maximumPropertiesToStringify -= value.length;
23924
- separator = join16;
23924
+ separator = join18;
23925
23925
  }
23926
23926
  if (deterministic) {
23927
23927
  keys = sort(keys, comparator);
@@ -23932,13 +23932,13 @@ ${indentation}`;
23932
23932
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
23933
23933
  if (tmp !== void 0) {
23934
23934
  res += `${separator}${strEscape(key2)}: ${tmp}`;
23935
- separator = join16;
23935
+ separator = join18;
23936
23936
  }
23937
23937
  }
23938
23938
  if (keyLength > maximumBreadth) {
23939
23939
  const removedKeys = keyLength - maximumBreadth;
23940
23940
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
23941
- separator = join16;
23941
+ separator = join18;
23942
23942
  }
23943
23943
  if (separator !== "") {
23944
23944
  res = `
@@ -31077,7 +31077,7 @@ var require_compile = __commonJS({
31077
31077
  const schOrFunc = root.refs[ref];
31078
31078
  if (schOrFunc)
31079
31079
  return schOrFunc;
31080
- let _sch = resolve6.call(this, root, ref);
31080
+ let _sch = resolve7.call(this, root, ref);
31081
31081
  if (_sch === void 0) {
31082
31082
  const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
31083
31083
  const { schemaId } = this.opts;
@@ -31104,7 +31104,7 @@ var require_compile = __commonJS({
31104
31104
  function sameSchemaEnv(s1, s2) {
31105
31105
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
31106
31106
  }
31107
- function resolve6(root, ref) {
31107
+ function resolve7(root, ref) {
31108
31108
  let sch;
31109
31109
  while (typeof (sch = this.refs[ref]) == "string")
31110
31110
  ref = sch;
@@ -31215,14 +31215,14 @@ var require_utils = __commonJS({
31215
31215
  BYTE_HEX[i] = "%" + HEX_DIGITS[i >> 4] + HEX_DIGITS[i & 15];
31216
31216
  }
31217
31217
  }
31218
- function percentEncodeNonAscii(cp2) {
31219
- if (cp2 < 2048) {
31220
- return BYTE_HEX[192 | cp2 >> 6] + BYTE_HEX[128 | cp2 & 63];
31218
+ function percentEncodeNonAscii(cp3) {
31219
+ if (cp3 < 2048) {
31220
+ return BYTE_HEX[192 | cp3 >> 6] + BYTE_HEX[128 | cp3 & 63];
31221
31221
  }
31222
- if (cp2 < 65536) {
31223
- return BYTE_HEX[224 | cp2 >> 12] + BYTE_HEX[128 | cp2 >> 6 & 63] + BYTE_HEX[128 | cp2 & 63];
31222
+ if (cp3 < 65536) {
31223
+ return BYTE_HEX[224 | cp3 >> 12] + BYTE_HEX[128 | cp3 >> 6 & 63] + BYTE_HEX[128 | cp3 & 63];
31224
31224
  }
31225
- return BYTE_HEX[240 | cp2 >> 18] + BYTE_HEX[128 | cp2 >> 12 & 63] + BYTE_HEX[128 | cp2 >> 6 & 63] + BYTE_HEX[128 | cp2 & 63];
31225
+ return BYTE_HEX[240 | cp3 >> 18] + BYTE_HEX[128 | cp3 >> 12 & 63] + BYTE_HEX[128 | cp3 >> 6 & 63] + BYTE_HEX[128 | cp3 & 63];
31226
31226
  }
31227
31227
  function stringArrayToHexStripped(input) {
31228
31228
  let acc = "";
@@ -31617,8 +31617,8 @@ var require_utils = __commonJS({
31617
31617
  function encodeFragment(input) {
31618
31618
  return encodeComponent(input, isQueryFragmentCharacter);
31619
31619
  }
31620
- function isEscapeSafe(cp2) {
31621
- return cp2 >= 48 && cp2 <= 57 || cp2 >= 65 && cp2 <= 90 || cp2 >= 97 && cp2 <= 122 || cp2 === 42 || cp2 === 43 || cp2 === 45 || cp2 === 46 || cp2 === 47 || cp2 === 64 || cp2 === 95;
31620
+ function isEscapeSafe(cp3) {
31621
+ return cp3 >= 48 && cp3 <= 57 || cp3 >= 65 && cp3 <= 90 || cp3 >= 97 && cp3 <= 122 || cp3 === 42 || cp3 === 43 || cp3 === 45 || cp3 === 46 || cp3 === 47 || cp3 === 64 || cp3 === 95;
31622
31622
  }
31623
31623
  function normalizeQueryFragmentEncoding(input) {
31624
31624
  let output = "";
@@ -31968,7 +31968,7 @@ var require_fast_uri = __commonJS({
31968
31968
  }
31969
31969
  return uri;
31970
31970
  }
31971
- function resolve6(baseURI, relativeURI, options) {
31971
+ function resolve7(baseURI, relativeURI, options) {
31972
31972
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
31973
31973
  const {
31974
31974
  parsed: baseParsed,
@@ -32004,50 +32004,50 @@ var require_fast_uri = __commonJS({
32004
32004
  schemelessOptions.skipEscape = true;
32005
32005
  return serialize(resolved, schemelessOptions);
32006
32006
  }
32007
- function resolveComponent(base, relative4, options, skipNormalization) {
32007
+ function resolveComponent(base, relative5, options, skipNormalization) {
32008
32008
  const target = {};
32009
32009
  if (!skipNormalization) {
32010
32010
  base = parse2(serialize(base, options), options);
32011
- relative4 = parse2(serialize(relative4, options), options);
32011
+ relative5 = parse2(serialize(relative5, options), options);
32012
32012
  }
32013
32013
  options = options || {};
32014
- if (!options.tolerant && relative4.scheme) {
32015
- target.scheme = relative4.scheme;
32016
- target.userinfo = relative4.userinfo;
32017
- target.host = relative4.host;
32018
- target.port = relative4.port;
32019
- target.path = removeDotSegments(relative4.path || "");
32020
- target.query = relative4.query;
32014
+ if (!options.tolerant && relative5.scheme) {
32015
+ target.scheme = relative5.scheme;
32016
+ target.userinfo = relative5.userinfo;
32017
+ target.host = relative5.host;
32018
+ target.port = relative5.port;
32019
+ target.path = removeDotSegments(relative5.path || "");
32020
+ target.query = relative5.query;
32021
32021
  } else {
32022
- if (relative4.userinfo !== void 0 || relative4.host !== void 0 || relative4.port !== void 0) {
32023
- target.userinfo = relative4.userinfo;
32024
- target.host = relative4.host;
32025
- target.port = relative4.port;
32026
- target.path = removeDotSegments(relative4.path || "");
32027
- target.query = relative4.query;
32022
+ if (relative5.userinfo !== void 0 || relative5.host !== void 0 || relative5.port !== void 0) {
32023
+ target.userinfo = relative5.userinfo;
32024
+ target.host = relative5.host;
32025
+ target.port = relative5.port;
32026
+ target.path = removeDotSegments(relative5.path || "");
32027
+ target.query = relative5.query;
32028
32028
  } else {
32029
- if (!relative4.path) {
32029
+ if (!relative5.path) {
32030
32030
  target.path = base.path;
32031
- if (relative4.query !== void 0) {
32032
- target.query = relative4.query;
32031
+ if (relative5.query !== void 0) {
32032
+ target.query = relative5.query;
32033
32033
  } else {
32034
32034
  target.query = base.query;
32035
32035
  }
32036
32036
  } else {
32037
- if (relative4.path[0] === "/") {
32038
- target.path = removeDotSegments(relative4.path);
32037
+ if (relative5.path[0] === "/") {
32038
+ target.path = removeDotSegments(relative5.path);
32039
32039
  } else {
32040
32040
  const baseHasAuthority = base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0;
32041
32041
  if (baseHasAuthority && !base.path) {
32042
- target.path = "/" + relative4.path;
32042
+ target.path = "/" + relative5.path;
32043
32043
  } else if (!base.path) {
32044
- target.path = relative4.path;
32044
+ target.path = relative5.path;
32045
32045
  } else {
32046
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative4.path;
32046
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative5.path;
32047
32047
  }
32048
32048
  target.path = base.scheme === void 0 && !baseHasAuthority && target.path[0] !== "/" ? removeDotSegmentsRelative(target.path) : removeDotSegments(target.path);
32049
32049
  }
32050
- target.query = relative4.query;
32050
+ target.query = relative5.query;
32051
32051
  }
32052
32052
  target.userinfo = base.userinfo;
32053
32053
  target.host = base.host;
@@ -32055,7 +32055,7 @@ var require_fast_uri = __commonJS({
32055
32055
  }
32056
32056
  target.scheme = base.scheme;
32057
32057
  }
32058
- target.fragment = relative4.fragment;
32058
+ target.fragment = relative5.fragment;
32059
32059
  return target;
32060
32060
  }
32061
32061
  function equal(uriA, uriB, options) {
@@ -32359,7 +32359,7 @@ var require_fast_uri = __commonJS({
32359
32359
  var fastUri = {
32360
32360
  SCHEMES,
32361
32361
  normalize,
32362
- resolve: resolve6,
32362
+ resolve: resolve7,
32363
32363
  resolveComponent,
32364
32364
  equal,
32365
32365
  serialize,
@@ -37023,17 +37023,17 @@ var require_utils2 = __commonJS({
37023
37023
  BYTE_HEX[i] = "%" + HEX_DIGITS[i >> 4] + HEX_DIGITS[i & 15];
37024
37024
  }
37025
37025
  }
37026
- function isEscapeSafe(cp2) {
37027
- return cp2 >= 48 && cp2 <= 57 || cp2 >= 65 && cp2 <= 90 || cp2 >= 97 && cp2 <= 122 || cp2 === 42 || cp2 === 43 || cp2 === 45 || cp2 === 46 || cp2 === 47 || cp2 === 64 || cp2 === 95;
37026
+ function isEscapeSafe(cp3) {
37027
+ return cp3 >= 48 && cp3 <= 57 || cp3 >= 65 && cp3 <= 90 || cp3 >= 97 && cp3 <= 122 || cp3 === 42 || cp3 === 43 || cp3 === 45 || cp3 === 46 || cp3 === 47 || cp3 === 64 || cp3 === 95;
37028
37028
  }
37029
- function percentEncodeNonAscii(cp2) {
37030
- if (cp2 < 2048) {
37031
- return BYTE_HEX[192 | cp2 >> 6] + BYTE_HEX[128 | cp2 & 63];
37029
+ function percentEncodeNonAscii(cp3) {
37030
+ if (cp3 < 2048) {
37031
+ return BYTE_HEX[192 | cp3 >> 6] + BYTE_HEX[128 | cp3 & 63];
37032
37032
  }
37033
- if (cp2 < 65536) {
37034
- return BYTE_HEX[224 | cp2 >> 12] + BYTE_HEX[128 | cp2 >> 6 & 63] + BYTE_HEX[128 | cp2 & 63];
37033
+ if (cp3 < 65536) {
37034
+ return BYTE_HEX[224 | cp3 >> 12] + BYTE_HEX[128 | cp3 >> 6 & 63] + BYTE_HEX[128 | cp3 & 63];
37035
37035
  }
37036
- return BYTE_HEX[240 | cp2 >> 18] + BYTE_HEX[128 | cp2 >> 12 & 63] + BYTE_HEX[128 | cp2 >> 6 & 63] + BYTE_HEX[128 | cp2 & 63];
37036
+ return BYTE_HEX[240 | cp3 >> 18] + BYTE_HEX[128 | cp3 >> 12 & 63] + BYTE_HEX[128 | cp3 >> 6 & 63] + BYTE_HEX[128 | cp3 & 63];
37037
37037
  }
37038
37038
  function normalizePathEncoding(input) {
37039
37039
  let output = "";
@@ -37796,7 +37796,7 @@ var require_fast_uri2 = __commonJS({
37796
37796
  }
37797
37797
  return uri;
37798
37798
  }
37799
- function resolve6(baseURI, relativeURI, options) {
37799
+ function resolve7(baseURI, relativeURI, options) {
37800
37800
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
37801
37801
  const {
37802
37802
  parsed: baseParsed,
@@ -37846,51 +37846,51 @@ var require_fast_uri2 = __commonJS({
37846
37846
  target.ipv6Zone = source.ipv6Zone;
37847
37847
  }
37848
37848
  }
37849
- function resolveComponent(base, relative4, options, skipNormalization) {
37849
+ function resolveComponent(base, relative5, options, skipNormalization) {
37850
37850
  const target = {};
37851
37851
  if (!skipNormalization) {
37852
37852
  base = parse2(serialize(base, options), options);
37853
- relative4 = parse2(serialize(relative4, options), options);
37853
+ relative5 = parse2(serialize(relative5, options), options);
37854
37854
  }
37855
37855
  options = options || {};
37856
- if (!options.tolerant && relative4.scheme) {
37857
- target.scheme = relative4.scheme;
37858
- target.userinfo = relative4.userinfo;
37859
- copyHost(target, relative4);
37860
- target.port = relative4.port;
37861
- const relativeSchemeHandler = getSchemeHandler(relative4.scheme);
37862
- target.path = relativeSchemeHandler && relativeSchemeHandler.absolutePath ? relative4.path || "" : removeDotSegments(relative4.path || "");
37863
- target.query = relative4.query;
37856
+ if (!options.tolerant && relative5.scheme) {
37857
+ target.scheme = relative5.scheme;
37858
+ target.userinfo = relative5.userinfo;
37859
+ copyHost(target, relative5);
37860
+ target.port = relative5.port;
37861
+ const relativeSchemeHandler = getSchemeHandler(relative5.scheme);
37862
+ target.path = relativeSchemeHandler && relativeSchemeHandler.absolutePath ? relative5.path || "" : removeDotSegments(relative5.path || "");
37863
+ target.query = relative5.query;
37864
37864
  } else {
37865
- if (relative4.userinfo !== void 0 || relative4.host !== void 0 || relative4.port !== void 0) {
37866
- target.userinfo = relative4.userinfo;
37867
- copyHost(target, relative4);
37868
- target.port = relative4.port;
37869
- target.path = removeDotSegments(relative4.path || "");
37870
- target.query = relative4.query;
37865
+ if (relative5.userinfo !== void 0 || relative5.host !== void 0 || relative5.port !== void 0) {
37866
+ target.userinfo = relative5.userinfo;
37867
+ copyHost(target, relative5);
37868
+ target.port = relative5.port;
37869
+ target.path = removeDotSegments(relative5.path || "");
37870
+ target.query = relative5.query;
37871
37871
  } else {
37872
- if (!relative4.path) {
37872
+ if (!relative5.path) {
37873
37873
  target.path = base.path;
37874
- if (relative4.query !== void 0) {
37875
- target.query = relative4.query;
37874
+ if (relative5.query !== void 0) {
37875
+ target.query = relative5.query;
37876
37876
  } else {
37877
37877
  target.query = base.query;
37878
37878
  }
37879
37879
  } else {
37880
- if (relative4.path[0] === "/") {
37881
- target.path = removeDotSegments(relative4.path);
37880
+ if (relative5.path[0] === "/") {
37881
+ target.path = removeDotSegments(relative5.path);
37882
37882
  } else {
37883
37883
  const baseHasAuthority = base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0;
37884
37884
  if (baseHasAuthority && !base.path) {
37885
- target.path = "/" + relative4.path;
37885
+ target.path = "/" + relative5.path;
37886
37886
  } else if (!base.path) {
37887
- target.path = relative4.path;
37887
+ target.path = relative5.path;
37888
37888
  } else {
37889
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative4.path;
37889
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative5.path;
37890
37890
  }
37891
37891
  target.path = base.scheme === void 0 && !baseHasAuthority && target.path[0] !== "/" ? removeDotSegmentsRelative(target.path) : removeDotSegments(target.path);
37892
37892
  }
37893
- target.query = relative4.query;
37893
+ target.query = relative5.query;
37894
37894
  }
37895
37895
  target.userinfo = base.userinfo;
37896
37896
  copyHost(target, base);
@@ -37898,7 +37898,7 @@ var require_fast_uri2 = __commonJS({
37898
37898
  }
37899
37899
  target.scheme = base.scheme;
37900
37900
  }
37901
- target.fragment = relative4.fragment;
37901
+ target.fragment = relative5.fragment;
37902
37902
  return target;
37903
37903
  }
37904
37904
  function equal(uriA, uriB, options) {
@@ -38223,7 +38223,7 @@ var require_fast_uri2 = __commonJS({
38223
38223
  var fastUri = {
38224
38224
  SCHEMES,
38225
38225
  normalize,
38226
- resolve: resolve6,
38226
+ resolve: resolve7,
38227
38227
  resolveComponent,
38228
38228
  equal,
38229
38229
  serialize,
@@ -49446,7 +49446,7 @@ var require_form_data = __commonJS({
49446
49446
  "../../node_modules/.pnpm/light-my-request@6.8.0/node_modules/light-my-request/lib/form-data.js"(exports, module) {
49447
49447
  "use strict";
49448
49448
  var { randomUUID } = __require("node:crypto");
49449
- var { Readable } = __require("node:stream");
49449
+ var { Readable: Readable2 } = __require("node:stream");
49450
49450
  var textEncoder;
49451
49451
  function isFormDataLike(payload) {
49452
49452
  return payload && typeof payload === "object" && typeof payload.append === "function" && typeof payload.delete === "function" && typeof payload.get === "function" && typeof payload.getAll === "function" && typeof payload.has === "function" && typeof payload.set === "function" && payload[Symbol.toStringTag] === "FormData";
@@ -49485,7 +49485,7 @@ Content-Type: ${value.type || "application/octet-stream"}\r
49485
49485
  }
49486
49486
  yield textEncoder.encode(`--${boundary}--`);
49487
49487
  }
49488
- const stream = Readable.from(asyncIterator());
49488
+ const stream = Readable2.from(asyncIterator());
49489
49489
  return {
49490
49490
  stream,
49491
49491
  contentType: `multipart/form-data; boundary=${boundary}`
@@ -49500,7 +49500,7 @@ Content-Type: ${value.type || "application/octet-stream"}\r
49500
49500
  var require_request2 = __commonJS({
49501
49501
  "../../node_modules/.pnpm/light-my-request@6.8.0/node_modules/light-my-request/lib/request.js"(exports, module) {
49502
49502
  "use strict";
49503
- var { Readable, addAbortSignal } = __require("node:stream");
49503
+ var { Readable: Readable2, addAbortSignal } = __require("node:stream");
49504
49504
  var util = __require("node:util");
49505
49505
  var assert = __require("node:assert");
49506
49506
  var { createDeprecation } = require_process_warning();
@@ -49567,7 +49567,7 @@ var require_request2 = __commonJS({
49567
49567
  }
49568
49568
  }
49569
49569
  function Request2(options) {
49570
- Readable.call(this, {
49570
+ Readable2.call(this, {
49571
49571
  autoDestroy: false
49572
49572
  });
49573
49573
  const parsedURL = parseURL(options.url || options.path, options.query);
@@ -49724,7 +49724,7 @@ var require_request2 = __commonJS({
49724
49724
  }
49725
49725
  });
49726
49726
  }
49727
- util.inherits(Request2, Readable);
49727
+ util.inherits(Request2, Readable2);
49728
49728
  util.inherits(CustomRequest, Request2);
49729
49729
  Request2.prototype.destroy = function(error) {
49730
49730
  if (this.destroyed || this._lightMyRequest.isDone) return;
@@ -49978,7 +49978,7 @@ var require_response = __commonJS({
49978
49978
  "../../node_modules/.pnpm/light-my-request@6.8.0/node_modules/light-my-request/lib/response.js"(exports, module) {
49979
49979
  "use strict";
49980
49980
  var http = __require("node:http");
49981
- var { Writable, Readable, addAbortSignal } = __require("node:stream");
49981
+ var { Writable, Readable: Readable2, addAbortSignal } = __require("node:stream");
49982
49982
  var util = __require("node:util");
49983
49983
  var setCookie = require_set_cookie();
49984
49984
  function Response2(req, onEnd, reject) {
@@ -49986,7 +49986,7 @@ var require_response = __commonJS({
49986
49986
  this.isLightMyRequest = true;
49987
49987
  if (req._lightMyRequest?.payloadAsStream) {
49988
49988
  const read = this.emit.bind(this, "drain");
49989
- this._lightMyRequest = { headers: null, trailers: {}, stream: new Readable({ read }) };
49989
+ this._lightMyRequest = { headers: null, trailers: {}, stream: new Readable2({ read }) };
49990
49990
  const signal = req._lightMyRequest.signal;
49991
49991
  if (signal) {
49992
49992
  addAbortSignal(signal, this._lightMyRequest.stream);
@@ -50132,7 +50132,7 @@ var require_response = __commonJS({
50132
50132
  if (response._lightMyRequest.stream) {
50133
50133
  return response._lightMyRequest.stream;
50134
50134
  }
50135
- return Readable.from(response._lightMyRequest.payloadChunks);
50135
+ return Readable2.from(response._lightMyRequest.payloadChunks);
50136
50136
  };
50137
50137
  return res;
50138
50138
  }
@@ -51207,9 +51207,9 @@ var require_light_my_request = __commonJS({
51207
51207
  const res = new Response2(req, callback);
51208
51208
  return makeRequest(dispatchFunc, server, req, res);
51209
51209
  } else {
51210
- return new Promise((resolve6, reject) => {
51210
+ return new Promise((resolve7, reject) => {
51211
51211
  const req = new RequestConstructor(options);
51212
- const res = new Response2(req, resolve6, reject);
51212
+ const res = new Response2(req, resolve7, reject);
51213
51213
  makeRequest(dispatchFunc, server, req, res);
51214
51214
  });
51215
51215
  }
@@ -52149,10 +52149,276 @@ ${body}`);
52149
52149
  });
52150
52150
 
52151
52151
  // src/cli.ts
52152
- import { existsSync as existsSync8, readFileSync as readFileSync2 } from "node:fs";
52153
- import { delimiter as delimiter3, join as join15, resolve as resolve5 } from "node:path";
52152
+ import { existsSync as existsSync9, readFileSync as readFileSync2 } from "node:fs";
52153
+ import { delimiter as delimiter3, join as join17, resolve as resolve6 } from "node:path";
52154
52154
  import { fileURLToPath as fileURLToPath3 } from "node:url";
52155
52155
 
52156
+ // ../../packages/core/src/image/png.ts
52157
+ import { crc32, deflateSync, inflateSync } from "node:zlib";
52158
+ var SIGNATURE = Uint8Array.from([137, 80, 78, 71, 13, 10, 26, 10]);
52159
+ var CHANNELS = { 0: 1, 2: 3, 4: 2, 6: 4 };
52160
+ var COLOR_TYPE = { 1: 0, 2: 4, 3: 2, 4: 6 };
52161
+ var PngError = class extends Error {
52162
+ name = "PngError";
52163
+ };
52164
+ function readUint32(bytes, offset) {
52165
+ return (bytes[offset] << 24 | bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3]) >>> 0;
52166
+ }
52167
+ function paeth(left, up, upLeft) {
52168
+ const estimate = left + up - upLeft;
52169
+ const toLeft = Math.abs(estimate - left);
52170
+ const toUp = Math.abs(estimate - up);
52171
+ const toUpLeft = Math.abs(estimate - upLeft);
52172
+ if (toLeft <= toUp && toLeft <= toUpLeft) return left;
52173
+ return toUp <= toUpLeft ? up : upLeft;
52174
+ }
52175
+ function decodePng(file) {
52176
+ if (file.length < 8 || SIGNATURE.some((byte, i) => file[i] !== byte)) throw new PngError("Not a PNG file");
52177
+ let offset = 8;
52178
+ let width = 0;
52179
+ let height = 0;
52180
+ let channels;
52181
+ const idat = [];
52182
+ while (offset + 8 <= file.length) {
52183
+ const length = readUint32(file, offset);
52184
+ const type = String.fromCharCode(file[offset + 4], file[offset + 5], file[offset + 6], file[offset + 7]);
52185
+ const body = file.subarray(offset + 8, offset + 8 + length);
52186
+ if (body.length !== length) throw new PngError("Truncated PNG chunk");
52187
+ if (type === "IHDR") {
52188
+ width = readUint32(body, 0);
52189
+ height = readUint32(body, 4);
52190
+ const bitDepth = body[8];
52191
+ const colorType = body[9];
52192
+ const interlace = body[12];
52193
+ if (bitDepth !== 8) throw new PngError(`Unsupported PNG bit depth ${bitDepth}`);
52194
+ if (interlace !== 0) throw new PngError("Interlaced PNGs are not supported");
52195
+ channels = CHANNELS[colorType];
52196
+ if (!channels) throw new PngError(`Unsupported PNG color type ${colorType}`);
52197
+ } else if (type === "IDAT") {
52198
+ idat.push(body);
52199
+ } else if (type === "IEND") {
52200
+ break;
52201
+ }
52202
+ offset += 12 + length;
52203
+ }
52204
+ if (!channels || width === 0 || height === 0) throw new PngError("PNG has no image header");
52205
+ const packed = inflateSync(Buffer.concat(idat));
52206
+ const stride = width * channels;
52207
+ if (packed.length < height * (stride + 1)) throw new PngError("PNG image data is too short");
52208
+ const data = new Uint8Array(height * stride);
52209
+ for (let y = 0; y < height; y++) {
52210
+ const filter = packed[y * (stride + 1)];
52211
+ const source = y * (stride + 1) + 1;
52212
+ const row = y * stride;
52213
+ const previous = row - stride;
52214
+ for (let x = 0; x < stride; x++) {
52215
+ const raw = packed[source + x];
52216
+ const left = x >= channels ? data[row + x - channels] : 0;
52217
+ const up = y > 0 ? data[previous + x] : 0;
52218
+ const upLeft = y > 0 && x >= channels ? data[previous + x - channels] : 0;
52219
+ let value;
52220
+ switch (filter) {
52221
+ case 0:
52222
+ value = raw;
52223
+ break;
52224
+ case 1:
52225
+ value = raw + left;
52226
+ break;
52227
+ case 2:
52228
+ value = raw + up;
52229
+ break;
52230
+ case 3:
52231
+ value = raw + (left + up >> 1);
52232
+ break;
52233
+ case 4:
52234
+ value = raw + paeth(left, up, upLeft);
52235
+ break;
52236
+ default:
52237
+ throw new PngError(`Unknown PNG filter ${filter}`);
52238
+ }
52239
+ data[row + x] = value & 255;
52240
+ }
52241
+ }
52242
+ return { width, height, channels, data };
52243
+ }
52244
+ function chunk(type, body) {
52245
+ const head = Buffer.alloc(8);
52246
+ head.writeUInt32BE(body.length, 0);
52247
+ head.write(type, 4, "latin1");
52248
+ const crc = Buffer.alloc(4);
52249
+ crc.writeUInt32BE(crc32(Buffer.concat([head.subarray(4), body])) >>> 0, 0);
52250
+ return Buffer.concat([head, body, crc]);
52251
+ }
52252
+ function encodePng(image) {
52253
+ const { width, height, channels, data } = image;
52254
+ const stride = width * channels;
52255
+ const filtered = Buffer.alloc(height * (stride + 1));
52256
+ for (let y = 0; y < height; y++) {
52257
+ filtered[y * (stride + 1)] = 2;
52258
+ for (let x = 0; x < stride; x++) {
52259
+ const up = y > 0 ? data[(y - 1) * stride + x] : 0;
52260
+ filtered[y * (stride + 1) + 1 + x] = data[y * stride + x] - up & 255;
52261
+ }
52262
+ }
52263
+ const header = Buffer.alloc(13);
52264
+ header.writeUInt32BE(width, 0);
52265
+ header.writeUInt32BE(height, 4);
52266
+ header[8] = 8;
52267
+ header[9] = COLOR_TYPE[channels];
52268
+ return Buffer.concat([
52269
+ Buffer.from(SIGNATURE),
52270
+ chunk("IHDR", header),
52271
+ chunk("IDAT", deflateSync(filtered)),
52272
+ chunk("IEND", new Uint8Array(0))
52273
+ ]);
52274
+ }
52275
+ function resizeImage(image, width, height) {
52276
+ const { channels } = image;
52277
+ const data = new Uint8Array(width * height * channels);
52278
+ const sums = new Float64Array(channels);
52279
+ for (let y = 0; y < height; y++) {
52280
+ const y0 = Math.floor(y * image.height / height);
52281
+ const y1 = Math.max(y0 + 1, Math.floor((y + 1) * image.height / height));
52282
+ for (let x = 0; x < width; x++) {
52283
+ const x0 = Math.floor(x * image.width / width);
52284
+ const x1 = Math.max(x0 + 1, Math.floor((x + 1) * image.width / width));
52285
+ sums.fill(0);
52286
+ for (let sy = y0; sy < y1; sy++) {
52287
+ for (let sx = x0; sx < x1; sx++) {
52288
+ const at = (sy * image.width + sx) * channels;
52289
+ for (let c = 0; c < channels; c++) sums[c] += image.data[at + c];
52290
+ }
52291
+ }
52292
+ const count = (y1 - y0) * (x1 - x0);
52293
+ const out = (y * width + x) * channels;
52294
+ for (let c = 0; c < channels; c++) data[out + c] = Math.round(sums[c] / count);
52295
+ }
52296
+ }
52297
+ return { width, height, channels, data };
52298
+ }
52299
+
52300
+ // ../../packages/core/src/image/similarity.ts
52301
+ var DEFAULTS = {
52302
+ columns: 27,
52303
+ rows: 120,
52304
+ cropTop: 0.06,
52305
+ cropBottom: 0.05,
52306
+ tolerance: 14,
52307
+ maxShiftRows: 8
52308
+ };
52309
+ var LOOKS_DIFFERENT_BELOW = 85;
52310
+ function luminanceAt(image, index) {
52311
+ const { channels, data } = image;
52312
+ const at = index * channels;
52313
+ if (channels <= 2) return data[at];
52314
+ return 0.299 * data[at] + 0.587 * data[at + 1] + 0.114 * data[at + 2];
52315
+ }
52316
+ function luminanceGrid(image, options = {}) {
52317
+ const { columns, rows, cropTop, cropBottom } = { ...DEFAULTS, ...options };
52318
+ const top = Math.floor(image.height * cropTop);
52319
+ const bottom = Math.max(top + rows, Math.ceil(image.height * (1 - cropBottom)));
52320
+ const usable = Math.min(image.height, bottom) - top;
52321
+ const sums = new Float64Array(columns * rows);
52322
+ const counts = new Float64Array(columns * rows);
52323
+ for (let y = top; y < top + usable; y += 2) {
52324
+ const row = Math.min(rows - 1, Math.floor((y - top) * rows / usable));
52325
+ for (let x = 0; x < image.width; x += 2) {
52326
+ const column = Math.min(columns - 1, Math.floor(x * columns / image.width));
52327
+ const cell = row * columns + column;
52328
+ sums[cell] += luminanceAt(image, y * image.width + x);
52329
+ counts[cell] += 1;
52330
+ }
52331
+ }
52332
+ for (let i = 0; i < sums.length; i++) sums[i] = counts[i] > 0 ? sums[i] / counts[i] : 0;
52333
+ return sums;
52334
+ }
52335
+ function background(a, b) {
52336
+ const buckets = /* @__PURE__ */ new Map();
52337
+ for (const grid of [a, b]) {
52338
+ for (const value of grid) {
52339
+ const bucket = Math.round(value / 8);
52340
+ buckets.set(bucket, (buckets.get(bucket) ?? 0) + 1);
52341
+ }
52342
+ }
52343
+ let best = 0;
52344
+ let bestCount = -1;
52345
+ for (const [bucket, count] of buckets) {
52346
+ if (count > bestCount) {
52347
+ best = bucket;
52348
+ bestCount = count;
52349
+ }
52350
+ }
52351
+ return best * 8;
52352
+ }
52353
+ function scoreAt(gridA, gridB, columns, rows, shift, bg, tolerance) {
52354
+ let content = 0;
52355
+ let matched = 0;
52356
+ let cells = 0;
52357
+ let allMatched = 0;
52358
+ for (let row = Math.max(0, -shift); row < Math.min(rows, rows - shift); row++) {
52359
+ for (let column = 0; column < columns; column++) {
52360
+ const va = gridA[row * columns + column];
52361
+ const at = (row + shift) * columns + column;
52362
+ const vb = gridB[at];
52363
+ const up = row + shift > 0 ? gridB[at - columns] : vb;
52364
+ const down = row + shift < rows - 1 ? gridB[at + columns] : vb;
52365
+ const low = Math.min(up, vb, down) - tolerance;
52366
+ const high = Math.max(up, vb, down) + tolerance;
52367
+ const same = va >= low && va <= high;
52368
+ cells++;
52369
+ if (same) allMatched++;
52370
+ if (Math.abs(va - bg) > 10 || Math.abs(vb - bg) > 10) {
52371
+ content++;
52372
+ if (same) matched++;
52373
+ }
52374
+ }
52375
+ }
52376
+ if (content < cells * 0.02) return cells === 0 ? 0 : allMatched / cells;
52377
+ return matched / content;
52378
+ }
52379
+ function screenSimilarity(a, b, options = {}) {
52380
+ const { tolerance, columns, rows, maxShiftRows } = { ...DEFAULTS, ...options };
52381
+ const gridA = luminanceGrid(a, options);
52382
+ const gridB = luminanceGrid(b, options);
52383
+ const bg = background(gridA, gridB);
52384
+ let best = 0;
52385
+ for (let shift = -maxShiftRows; shift <= maxShiftRows; shift++) {
52386
+ best = Math.max(best, scoreAt(gridA, gridB, columns, rows, shift, bg, tolerance));
52387
+ }
52388
+ return Math.round(best * 1e3) / 10;
52389
+ }
52390
+
52391
+ // ../../packages/core/src/device/hidden-screen.ts
52392
+ var SYSTEM_UI = "com.android.systemui";
52393
+ var BLACK_BELOW = 2;
52394
+ function hasAppElements(nodes) {
52395
+ return nodes.some((node) => node.systemBar === void 0 && node.bundleId !== SYSTEM_UI);
52396
+ }
52397
+ function isBlackImage(image) {
52398
+ return luminanceGrid(image, { cropTop: 0, cropBottom: 0 }).every((cell) => cell < BLACK_BELOW);
52399
+ }
52400
+ function screenView(nodes, png) {
52401
+ let black = false;
52402
+ try {
52403
+ black = isBlackImage(decodePng(png));
52404
+ } catch {
52405
+ }
52406
+ if (!black) return "visible";
52407
+ return hasAppElements(nodes) ? "secure" : "hidden";
52408
+ }
52409
+ function parseKeyguardShowing(output) {
52410
+ const match = /isKeyguardShowing=(true|false)/.exec(output);
52411
+ return match ? match[1] === "true" : void 0;
52412
+ }
52413
+ function blackPicture(viewport) {
52414
+ const width = Math.max(1, Math.round(viewport.width / 10));
52415
+ const height = Math.max(1, Math.round(viewport.height / 10));
52416
+ return encodePng({ width, height, channels: 1, data: new Uint8Array(width * height) });
52417
+ }
52418
+ function looksLikeHomeApp(app) {
52419
+ return /launcher|\.home(\.|$)/i.test(app);
52420
+ }
52421
+
52156
52422
  // ../../packages/core/src/device/hit-test.ts
52157
52423
  function rectContains(rect, point) {
52158
52424
  return point.x >= rect.x && point.y >= rect.y && point.x < rect.x + rect.width && point.y < rect.y + rect.height;
@@ -52211,15 +52477,15 @@ import { promisify as promisify3 } from "node:util";
52211
52477
 
52212
52478
  // ../../packages/core/src/replay/maestro-driver.ts
52213
52479
  import { execFile as execFile2 } from "node:child_process";
52214
- import { mkdtemp, readFile, readdir, realpath, rm, writeFile } from "node:fs/promises";
52480
+ import { mkdtemp, readFile, readdir as readdir2, realpath, rm as rm2, writeFile } from "node:fs/promises";
52215
52481
  import { tmpdir } from "node:os";
52216
52482
  import { dirname as dirname2, join as join3 } from "node:path";
52217
52483
  import { promisify as promisify2 } from "node:util";
52218
52484
 
52219
52485
  // ../../packages/core/src/device/session.ts
52220
52486
  import { execFile } from "node:child_process";
52221
- import { existsSync as existsSync2 } from "node:fs";
52222
- import { mkdir } from "node:fs/promises";
52487
+ import { existsSync as existsSync3 } from "node:fs";
52488
+ import { mkdir as mkdir2 } from "node:fs/promises";
52223
52489
  import { homedir as homedir2 } from "node:os";
52224
52490
  import { dirname, join as join2 } from "node:path";
52225
52491
  import { promisify } from "node:util";
@@ -52230,35 +52496,132 @@ import {
52230
52496
  import { AppError, isAgentDeviceError } from "agent-device";
52231
52497
 
52232
52498
  // ../../packages/core/src/replay/maestro-bin.ts
52233
- import { existsSync } from "node:fs";
52499
+ import { existsSync as existsSync2 } from "node:fs";
52234
52500
  import { homedir } from "node:os";
52235
- import { join, posix, win32 } from "node:path";
52501
+ import { join, posix, win32 as win322 } from "node:path";
52502
+
52503
+ // ../../packages/core/src/replay/shared-folder.ts
52504
+ import { execFileSync } from "node:child_process";
52505
+ import { existsSync } from "node:fs";
52506
+ import { mkdir, readdir, rm } from "node:fs/promises";
52507
+ import { win32 } from "node:path";
52508
+ var SYSTEM = "S-1-5-18";
52509
+ var ADMINISTRATORS = "S-1-5-32-544";
52510
+ var TRUSTED_INSTALLER = "S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464";
52511
+ var TRUSTED = /* @__PURE__ */ new Set(["SY", "BA", "LA", "CO", "OW", TRUSTED_INSTALLER]);
52512
+ var READ_ONLY_CODES = /* @__PURE__ */ new Set(["FR", "FX", "GR", "GX", "RC"]);
52513
+ var READ_ONLY_BITS = 2685534377;
52514
+ function onlyReads(rights) {
52515
+ if (/^0x[0-9a-f]+$/i.test(rights)) return (Number(rights) & ~READ_ONLY_BITS) === 0;
52516
+ const codes = rights.match(/../g) ?? [];
52517
+ return codes.length > 0 && codes.every((code) => READ_ONLY_CODES.has(code.toUpperCase()));
52518
+ }
52519
+ function onlyTrustedCanChange(sddl, me) {
52520
+ const trusted = (who) => who === me || TRUSTED.has(who);
52521
+ const owner = /^O:(.+?)(?:G:|D:|S:|$)/.exec(sddl)?.[1];
52522
+ const dacl = /D:([A-Z_]*)((?:\([^()]*\))*)(?:S:.*)?$/.exec(sddl);
52523
+ if (!owner || !trusted(owner) || !dacl || dacl[1].includes("NO_ACCESS_CONTROL")) return false;
52524
+ const entries = dacl[2].slice(1, -1).split(")(").filter(Boolean);
52525
+ return entries.every((entry) => {
52526
+ const [type = "", , rights = "", , , who = ""] = entry.split(";");
52527
+ return type === "D" || trusted(who) || onlyReads(rights);
52528
+ });
52529
+ }
52530
+ function runSystemTool(tool, args) {
52531
+ const file = win32.join(process.env.SystemRoot ?? "C:\\Windows", "System32", tool);
52532
+ const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => name.toUpperCase() !== "PSMODULEPATH"));
52533
+ return execFileSync(file, args, { encoding: "utf8", env, timeout: 3e4, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
52534
+ }
52535
+ function mySid() {
52536
+ const sid = runSystemTool("whoami.exe", ["/user", "/fo", "csv", "/nh"]).match(/S-1-\d+(?:-\d+)+/g)?.at(-1);
52537
+ if (!sid) throw new Error("whoami did not say who this is");
52538
+ return sid;
52539
+ }
52540
+ function folderSddl(dir) {
52541
+ const command = `(Get-Acl -LiteralPath '${dir.replaceAll("'", "''")}').Sddl`;
52542
+ return runSystemTool(win32.join("WindowsPowerShell", "v1.0", "powershell.exe"), ["-NoProfile", "-NonInteractive", "-Command", command]).replace(/\s+/g, "");
52543
+ }
52544
+ function folderIsPrivate(dir) {
52545
+ if (!existsSync(dir)) return true;
52546
+ try {
52547
+ return onlyTrustedCanChange(folderSddl(dir), mySid());
52548
+ } catch {
52549
+ return false;
52550
+ }
52551
+ }
52552
+ var privateFolders = /* @__PURE__ */ new Set();
52553
+ function sharedFolderSafe(dir) {
52554
+ if (privateFolders.has(dir)) return true;
52555
+ const safe = folderIsPrivate(dir);
52556
+ if (safe && existsSync(dir)) privateFolders.add(dir);
52557
+ return safe;
52558
+ }
52559
+ async function claimFolder(dir) {
52560
+ if (existsSync(dir) && folderIsPrivate(dir)) return;
52561
+ await rm(dir, { recursive: true, force: true, maxRetries: 3 });
52562
+ await mkdir(dir, { recursive: true });
52563
+ runSystemTool("icacls.exe", [dir, "/inheritance:r", "/grant:r", `*${mySid()}:(OI)(CI)F`, `*${SYSTEM}:(OI)(CI)F`, `*${ADMINISTRATORS}:(OI)(CI)F`]);
52564
+ const sddl = folderSddl(dir);
52565
+ if (!onlyTrustedCanChange(sddl, mySid()) || (await readdir(dir)).length > 0) throw new Error(`${dir} is still open to other accounts: ${sddl}`);
52566
+ }
52567
+
52568
+ // ../../packages/core/src/replay/maestro-bin.ts
52569
+ function isPlainAscii(text) {
52570
+ return /^[\x20-\x7e]*$/.test(text);
52571
+ }
52572
+ function toolsTarget(env = process.env, platform = process.platform) {
52573
+ if (platform !== "win32") return void 0;
52574
+ const base = [env.LOCALAPPDATA, env.ProgramData].find((dir) => dir && isPlainAscii(dir));
52575
+ return base ? win322.join(base, "Ariadne", "tools") : void 0;
52576
+ }
52577
+ function sharedToolsRoot(env = process.env, platform = process.platform) {
52578
+ if (platform !== "win32" || env.LOCALAPPDATA && isPlainAscii(env.LOCALAPPDATA)) return void 0;
52579
+ return env.ProgramData && isPlainAscii(env.ProgramData) ? win322.join(env.ProgramData, "Ariadne") : void 0;
52580
+ }
52581
+ function toolsDir(env = process.env, platform = process.platform, safe = sharedFolderSafe) {
52582
+ const shared = sharedToolsRoot(env, platform);
52583
+ return shared && !safe(shared) ? void 0 : toolsTarget(env, platform);
52584
+ }
52585
+ function toolsJavaHome(env = process.env, platform = process.platform) {
52586
+ const dir = toolsDir(env, platform);
52587
+ if (!dir) return void 0;
52588
+ const home = win322.join(dir, "java");
52589
+ return existsSync2(win322.join(home, "bin", "java.exe")) ? home : void 0;
52590
+ }
52236
52591
  function maestroNames(platform) {
52237
52592
  return platform === "win32" ? ["maestro.bat", "maestro.cmd", "maestro.exe"] : ["maestro"];
52238
52593
  }
52239
52594
  function maestroCandidates(platform, env, home) {
52240
- const path = platform === "win32" ? win32 : posix;
52595
+ const path = platform === "win32" ? win322 : posix;
52241
52596
  const dirs = [path.join(home, ".maestro", "bin")];
52242
52597
  if (platform === "win32") dirs.push(path.join(`${env.SystemDrive || "C:"}\\`, "maestro", "bin"));
52598
+ const tools = toolsDir(env, platform);
52599
+ if (tools) dirs.push(path.join(tools, "maestro", "bin"));
52243
52600
  dirs.push(...(env.PATH ?? "").split(path.delimiter).filter(Boolean));
52244
52601
  const candidates = env.MAESTRO_BIN ? [env.MAESTRO_BIN] : [];
52245
52602
  for (const dir of dirs) for (const name of maestroNames(platform)) candidates.push(path.join(dir, name));
52246
52603
  return candidates;
52247
52604
  }
52248
52605
  function findMaestro() {
52249
- const found = maestroCandidates(process.platform, process.env, homedir()).find((candidate) => existsSync(candidate));
52606
+ const found = maestroCandidates(process.platform, process.env, homedir()).find((candidate) => existsSync2(candidate));
52250
52607
  if (found) return found;
52251
52608
  throw new Error(
52252
52609
  process.platform === "win32" ? "Maestro CLI not found. Add the bin folder of the Maestro download to PATH, or set MAESTRO_BIN to its maestro.bat." : "Maestro CLI not found. Set MAESTRO_BIN or install it to ~/.maestro/bin/maestro."
52253
52610
  );
52254
52611
  }
52255
- function androidHome() {
52256
- const fromEnv = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
52257
- if (fromEnv) return fromEnv;
52612
+ function sdkGuesses() {
52258
52613
  const guesses = [join(homedir(), "Android", "Sdk")];
52259
52614
  if (process.platform === "darwin") guesses.unshift(join(homedir(), "Library", "Android", "sdk"));
52260
52615
  if (process.platform === "win32" && process.env.LOCALAPPDATA) guesses.unshift(join(process.env.LOCALAPPDATA, "Android", "Sdk"));
52261
- return guesses.find((guess) => existsSync(guess));
52616
+ return guesses;
52617
+ }
52618
+ function defaultSdkDir() {
52619
+ return sdkGuesses()[0];
52620
+ }
52621
+ function androidHome() {
52622
+ const fromEnv = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
52623
+ if (fromEnv) return fromEnv;
52624
+ return sdkGuesses().find((guess) => existsSync2(guess));
52262
52625
  }
52263
52626
 
52264
52627
  // ../../packages/core/src/device/session.ts
@@ -52267,7 +52630,7 @@ var LONG_PRESS_MS = 3e3;
52267
52630
  function adbPath() {
52268
52631
  const home = androidHome();
52269
52632
  const candidate = home ? join2(home, "platform-tools", process.platform === "win32" ? "adb.exe" : "adb") : void 0;
52270
- return candidate && existsSync2(candidate) ? candidate : "adb";
52633
+ return candidate && existsSync3(candidate) ? candidate : "adb";
52271
52634
  }
52272
52635
  function defaultStateDir() {
52273
52636
  return process.env.AGENT_DEVICE_STATE_DIR || join2(homedir2(), ".agent-device");
@@ -52414,7 +52777,7 @@ var DeviceSession = class {
52414
52777
  */
52415
52778
  async capture(screenshotPath) {
52416
52779
  const snapshot = await this.snapshot();
52417
- await mkdir(dirname(screenshotPath), { recursive: true });
52780
+ await mkdir2(dirname(screenshotPath), { recursive: true });
52418
52781
  const shot = await this.client.capture.screenshot({ path: screenshotPath, scale: this.screenshotScale });
52419
52782
  return { ...snapshot, screenshotPath: shot.path || screenshotPath };
52420
52783
  }
@@ -52542,7 +52905,7 @@ var DeviceSession = class {
52542
52905
  };
52543
52906
 
52544
52907
  // ../../packages/core/src/export/zip.ts
52545
- import { crc32, deflateRawSync, inflateRawSync } from "node:zlib";
52908
+ import { crc32 as crc322, deflateRawSync, inflateRawSync } from "node:zlib";
52546
52909
  var DOS_TIME = 0;
52547
52910
  var DOS_DATE = 0 << 9 | 1 << 5 | 1;
52548
52911
  var END_OF_DIRECTORY = 101010256;
@@ -52582,7 +52945,7 @@ function zip(entries) {
52582
52945
  const name = Buffer.from(entry.name, "utf8");
52583
52946
  const raw = typeof entry.data === "string" ? Buffer.from(entry.data, "utf8") : Buffer.from(entry.data);
52584
52947
  const compressed = deflateRawSync(raw);
52585
- const crc = crc32(raw) >>> 0;
52948
+ const crc = crc322(raw) >>> 0;
52586
52949
  const local = Buffer.alloc(30);
52587
52950
  local.writeUInt32LE(67324752, 0);
52588
52951
  local.writeUInt16LE(20, 4);
@@ -52630,7 +52993,7 @@ var DriverInstallRefused = class extends Error {
52630
52993
  };
52631
52994
  async function clientJar(maestroBin) {
52632
52995
  const lib = join3(dirname2(await realpath(maestroBin)), "..", "lib");
52633
- const name = (await readdir(lib)).find((file) => /^maestro-client.*\.jar$/.test(file));
52996
+ const name = (await readdir2(lib)).find((file) => /^maestro-client.*\.jar$/.test(file));
52634
52997
  if (!name) throw new Error(`No maestro-client jar in ${lib}`);
52635
52998
  return join3(lib, name);
52636
52999
  }
@@ -52656,7 +53019,7 @@ async function installMaestroDriver(deviceId, maestroBin) {
52656
53019
  await installApk(deviceId, path);
52657
53020
  }
52658
53021
  } finally {
52659
- await rm(dir, { recursive: true, force: true });
53022
+ await rm2(dir, { recursive: true, force: true });
52660
53023
  }
52661
53024
  }
52662
53025
 
@@ -52845,14 +53208,56 @@ async function readPhoneReport(shell2) {
52845
53208
  };
52846
53209
  }
52847
53210
 
53211
+ // ../../packages/core/src/device/tested-on.ts
53212
+ var PACKAGE_NAME = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z0-9_]+)+$/;
53213
+ var defined2 = (value) => {
53214
+ const trimmed = value?.trim();
53215
+ return trimmed && trimmed !== "null" ? trimmed : void 0;
53216
+ };
53217
+ function parseAppVersion(dumpsys) {
53218
+ const versionCode = defined2(/^\s*versionCode=(\d+)/m.exec(dumpsys)?.[1]);
53219
+ const versionName = defined2(/^\s*versionName=(.*)$/m.exec(dumpsys)?.[1]);
53220
+ return { ...versionName ? { versionName } : {}, ...versionCode ? { versionCode } : {} };
53221
+ }
53222
+ var EMULATOR_HARDWARE = /* @__PURE__ */ new Set(["ranchu", "goldfish", "vbox86"]);
53223
+ var EMULATOR_MAKERS = /* @__PURE__ */ new Set(["genymotion", "genymobile"]);
53224
+ function appVersionText(app) {
53225
+ if (app.versionName && app.versionCode) return `${app.versionName} (${app.versionCode})`;
53226
+ return app.versionName ?? app.versionCode;
53227
+ }
53228
+ async function readTestedOn(shell2, appIds) {
53229
+ const read = (...args) => shell2(args).then((output) => output, () => "");
53230
+ const [maker, model, android, kernelQemu, bootQemu, hardware, characteristics] = await Promise.all(
53231
+ ["ro.product.manufacturer", "ro.product.model", "ro.build.version.release", "ro.kernel.qemu", "ro.boot.qemu", "ro.hardware", "ro.build.characteristics"].map(
53232
+ async (name) => defined2(await read("getprop", name))
53233
+ )
53234
+ );
53235
+ const emulatorSign = kernelQemu === "1" || bootQemu === "1" || hardware !== void 0 && EMULATOR_HARDWARE.has(hardware) || maker !== void 0 && EMULATOR_MAKERS.has(maker.toLowerCase()) || // A comma-separated list; emulator images built from Android's own sources say "emulator" in it.
53236
+ (characteristics?.split(",").some((part) => part.trim() === "emulator") ?? false);
53237
+ const emulator = emulatorSign ? true : hardware !== void 0 ? false : void 0;
53238
+ const apps = [];
53239
+ for (const appId of new Set(appIds)) {
53240
+ const version2 = PACKAGE_NAME.test(appId) ? parseAppVersion(await read("dumpsys", "package", appId)) : {};
53241
+ apps.push({ appId, ...version2 });
53242
+ }
53243
+ return {
53244
+ ...maker ? { maker } : {},
53245
+ ...model ? { model } : {},
53246
+ ...android ? { android } : {},
53247
+ ...emulator !== void 0 ? { emulator } : {},
53248
+ apps
53249
+ };
53250
+ }
53251
+
52848
53252
  // ../../packages/core/src/device/phone-input.ts
52849
53253
  var execFileAsync3 = promisify3(execFile3);
52850
53254
  var ADB_TIMEOUT_MS = 1e4;
53255
+ var ADB_MAX_OUTPUT = 16 * 1024 * 1024;
52851
53256
  var DEFAULT_LONG_PRESS_MS = 400;
52852
53257
  var TOUCH_SLOP_DP = 8;
52853
53258
  var NAVIGATION_MODE_GESTURES2 = "2";
52854
53259
  async function shell(deviceId, ...args) {
52855
- const { stdout } = await execFileAsync3(adbPath(), ["-s", deviceId, "shell", ...args], { timeout: ADB_TIMEOUT_MS, windowsHide: true });
53260
+ const { stdout } = await execFileAsync3(adbPath(), ["-s", deviceId, "shell", ...args], { timeout: ADB_TIMEOUT_MS, maxBuffer: ADB_MAX_OUTPUT, windowsHide: true });
52856
53261
  return stdout;
52857
53262
  }
52858
53263
  async function readPhoneInput(deviceId) {
@@ -52896,6 +53301,9 @@ var phoneInputTools = {
52896
53301
  xiaomi: (deviceId) => readXiaomiPhone((args) => shell(deviceId, ...args)),
52897
53302
  allowDriver: (deviceId) => allowDriverToOpenApps((args) => shell(deviceId, ...args)),
52898
53303
  report: (deviceId) => readPhoneReport((args) => shell(deviceId, ...args)),
53304
+ testedOn: (deviceId, appIds) => readTestedOn((args) => shell(deviceId, ...args), appIds),
53305
+ // Filtered on the phone: the rest of the dump names what is on screen, notifications too.
53306
+ locked: async (deviceId) => parseKeyguardShowing(await shell(deviceId, "dumpsys window | grep isKeyguardShowing=")),
52899
53307
  stopDriver: (deviceId) => stopDriver((args) => shell(deviceId, ...args)),
52900
53308
  installDriver: installMaestroDriver
52901
53309
  };
@@ -52947,6 +53355,20 @@ function unescapeMaestroRegex(value) {
52947
53355
  return value.replace(/\\([.*+?^${}()|[\]\\])/g, "$1");
52948
53356
  }
52949
53357
 
53358
+ // ../../packages/core/src/maestro/model.ts
53359
+ var MAESTRO_KEYS = ["Home", "Back", "Enter", "Backspace", "Tab", "Escape", "Volume Up", "Volume Down", "Power", "Lock"];
53360
+ var MAESTRO_ORIENTATIONS = ["PORTRAIT", "LANDSCAPE_LEFT", "LANDSCAPE_RIGHT", "UPSIDE_DOWN"];
53361
+ var VARIABLE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
53362
+ function flowVariables(flow) {
53363
+ const names = /* @__PURE__ */ new Set();
53364
+ for (const match of JSON.stringify(flow.steps).matchAll(VARIABLE)) names.add(match[1]);
53365
+ return [...names].sort();
53366
+ }
53367
+ var NEVER_ON_SCREEN = "ariadne-wait";
53368
+ function waitName(seconds) {
53369
+ return `Wait ${seconds} ${seconds === 1 ? "second" : "seconds"}`;
53370
+ }
53371
+
52950
53372
  // ../../packages/core/src/recorder/block-values.ts
52951
53373
  var SECRET_NAME = /(PASS|PWD|SECRET|TOKEN|PIN|OTP|API_?KEY|PRIVATE)/i;
52952
53374
  var REFERENCE = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/;
@@ -52961,6 +53383,10 @@ function referencedKey(value) {
52961
53383
  function reference(key) {
52962
53384
  return `\${${key}}`;
52963
53385
  }
53386
+ var PASSWORD_NAME = /pass(word|wd|code|phrase)|(^|_)pass(_|$)/i;
53387
+ function passwordSetting(keys) {
53388
+ return keys.includes("PASSWORD") ? "PASSWORD" : [...keys].sort().find((key) => PASSWORD_NAME.test(key)) ?? "PASSWORD";
53389
+ }
52964
53390
  function label(prefix) {
52965
53391
  return prefix.split("_").filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1).toLowerCase()).join(" ");
52966
53392
  }
@@ -53033,6 +53459,8 @@ function readableId(id) {
53033
53459
  return last.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_\-.]+/g, " ").trim().toLowerCase();
53034
53460
  }
53035
53461
  function elementName(selector) {
53462
+ const setting = selector.kind === "point" ? null : ENV_PLACEHOLDER.exec(selector.value);
53463
+ if (setting) return `the element the ${setting[1]} setting names`;
53036
53464
  switch (selector.kind) {
53037
53465
  case "text": {
53038
53466
  const nth = selector.index === void 0 ? "" : ` (number ${selector.index + 1} on screen)`;
@@ -53112,6 +53540,8 @@ function describeStep(step, scrollTarget) {
53112
53540
  return "Restart the app";
53113
53541
  case "openLink":
53114
53542
  return `Open the link ${step.url}`;
53543
+ case "wait":
53544
+ return waitName(step.seconds);
53115
53545
  case "hideKeyboard":
53116
53546
  return "Hide the keyboard";
53117
53547
  case "erase":
@@ -53163,6 +53593,8 @@ function describeMaestroStep(step) {
53163
53593
  return `Check that ${elementName(step.selector)} is not visible`;
53164
53594
  case "openLink":
53165
53595
  return `Open the link ${step.link}`;
53596
+ case "wait":
53597
+ return waitName(step.seconds);
53166
53598
  case "takeScreenshot":
53167
53599
  return "Take a screenshot";
53168
53600
  case "startRecording":
@@ -53226,6 +53658,7 @@ function caseDocument(testCase, names = {}, lastRun) {
53226
53658
  createdAt: testCase.createdAt,
53227
53659
  ...testCase.editedAt ? { editedAt: testCase.editedAt } : {},
53228
53660
  ...testCase.muted ? { muted: true } : {},
53661
+ ...testCase.reference ? { reference: testCase.reference } : {},
53229
53662
  preconditions,
53230
53663
  steps,
53231
53664
  endChecks: testCase.expectations.map(describeExpectation)
@@ -53259,6 +53692,7 @@ function caseMarkdown(doc, imagePath) {
53259
53692
  lines.push(`# ${doc.name}`, "");
53260
53693
  lines.push("| | |", "|---|---|");
53261
53694
  lines.push(`| Case id | \`${doc.id}\` |`);
53695
+ if (doc.reference) lines.push(`| Reference | ${mdCell(doc.reference)} |`);
53262
53696
  lines.push(`| App | \`${doc.appId}\` |`);
53263
53697
  lines.push(`| Recorded | ${doc.createdAt.slice(0, 10)} |`);
53264
53698
  if (doc.editedAt) lines.push(`| Last edited | ${doc.editedAt.slice(0, 10)} |`);
@@ -53289,12 +53723,13 @@ function caseMarkdown(doc, imagePath) {
53289
53723
  return lines.join("\n");
53290
53724
  }
53291
53725
  function casesSheets(docs) {
53292
- const overview = [["Case", "Case id", "App", "Preconditions", "Steps", "Expected at the end", "Last run", "Muted"]];
53726
+ const overview = [["Case", "Case id", "Reference", "App", "Preconditions", "Steps", "Expected at the end", "Last run", "Muted"]];
53293
53727
  const steps = [["Case", "Step", "Action", "Expected result", "Screenshot"]];
53294
53728
  for (const doc of docs) {
53295
53729
  overview.push([
53296
53730
  doc.name,
53297
53731
  doc.id,
53732
+ doc.reference ?? "",
53298
53733
  doc.appId,
53299
53734
  doc.preconditions.join("\n"),
53300
53735
  String(doc.steps.length),
@@ -53307,11 +53742,72 @@ function casesSheets(docs) {
53307
53742
  if (doc.endChecks.length > 0) steps.push([doc.name, "End", "Check the final screen", doc.endChecks.join("\n"), ""]);
53308
53743
  }
53309
53744
  return [
53310
- { name: "Cases", rows: overview, widths: [36, 34, 26, 44, 8, 40, 40, 8] },
53745
+ { name: "Cases", rows: overview, widths: [36, 34, 16, 26, 44, 8, 40, 40, 8] },
53311
53746
  { name: "Steps", rows: steps, widths: [36, 6, 54, 44, 44] }
53312
53747
  ];
53313
53748
  }
53314
53749
 
53750
+ // ../../packages/core/src/export/report.ts
53751
+ function looksDifferent(similarity) {
53752
+ return similarity !== void 0 && similarity < LOOKS_DIFFERENT_BELOW;
53753
+ }
53754
+ function runReportRows(testCase, run2, names = {}) {
53755
+ const results = new Map(run2.steps.map((step) => [step.flowIndex, step]));
53756
+ return caseFlowItems(testCase).map((item, flowIndex) => {
53757
+ const result = results.get(flowIndex);
53758
+ const recorded = item.kind === "step" ? item.step.screenshot : void 0;
53759
+ return {
53760
+ number: flowIndex + 1,
53761
+ kind: item.kind === "step" ? item.step.kind : item.kind === "expectation" ? "end" : item.kind,
53762
+ label: describeFlowItem(item, testCase.appId, names),
53763
+ ...result ? { status: result.status } : {},
53764
+ ...result?.error ? { error: result.error } : {},
53765
+ ...result?.durationMs !== void 0 ? { durationMs: result.durationMs } : {},
53766
+ ...result?.startedAt !== void 0 ? { startedAt: result.startedAt } : {},
53767
+ ...recorded ? { recorded } : {},
53768
+ ...result?.replayScreenshot ? { replayed: result.replayScreenshot } : {},
53769
+ ...result?.similarity !== void 0 ? { similarity: result.similarity } : {},
53770
+ looksDifferent: looksDifferent(result?.similarity)
53771
+ };
53772
+ });
53773
+ }
53774
+
53775
+ // ../../packages/core/src/export/evidence.ts
53776
+ var OUTCOME_TEXT = {
53777
+ passed: "Passed",
53778
+ flaky: "Passed when run again",
53779
+ failed: "Failed",
53780
+ error: "Could not run",
53781
+ muted: "Not run: muted",
53782
+ pending: "Not run: Run all was stopped",
53783
+ running: "Not run: Run all was stopped"
53784
+ };
53785
+ function outcomeText(outcome) {
53786
+ return OUTCOME_TEXT[outcome];
53787
+ }
53788
+ function evidenceSteps(testCase, run2, names = {}) {
53789
+ const items = caseFlowItems(testCase);
53790
+ return runReportRows(testCase, run2, names).map((row, index) => {
53791
+ const item = items[index];
53792
+ const expected = item?.kind === "step" ? stepExpected(item.step) : item?.kind === "expectation" ? describeExpectation(item.expectation) : "";
53793
+ return { ...row, expected };
53794
+ });
53795
+ }
53796
+ function evidenceScreen(step, mode, failureScreenshot) {
53797
+ const failed = step.status === "failed";
53798
+ if (mode === "checks" && !failed && step.kind !== "expect" && step.kind !== "end") return void 0;
53799
+ return step.replayed ?? (failed ? failureScreenshot : void 0);
53800
+ }
53801
+ var pad = (value) => String(value).padStart(2, "0");
53802
+ function localTimeText(iso) {
53803
+ const date = new Date(iso);
53804
+ if (Number.isNaN(date.getTime())) return iso;
53805
+ const offset = -date.getTimezoneOffset();
53806
+ const sign = offset < 0 ? "-" : "+";
53807
+ const zone = `UTC${sign}${pad(Math.floor(Math.abs(offset) / 60))}:${pad(Math.abs(offset) % 60)}`;
53808
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())} (${zone})`;
53809
+ }
53810
+
53315
53811
  // ../../packages/core/src/export/html.ts
53316
53812
  function escapeHtml(value) {
53317
53813
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -53343,14 +53839,14 @@ img.shot { width: 150px; border: 1px solid #dcdfe6; border-radius: 4px; display:
53343
53839
  footer { margin-top: 28px; color: #8a93a3; font-size: 12px; }
53344
53840
  `;
53345
53841
  var BRAND = `<header class="brand"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="20" height="20" aria-hidden="true"><rect width="64" height="64" rx="14" fill="#1c2b4a"/><path d="M14 20 V50 H50 V14 H26 V38 H38 V26" fill="none" stroke="#e8b43a" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/><circle cx="14" cy="14" r="4.5" fill="none" stroke="#e8b43a" stroke-width="3.5"/><circle cx="38" cy="26" r="5" fill="#e8b43a"/></svg><span>Ariadne</span></header>`;
53346
- function page(title, body) {
53842
+ function page(title, body, style = "") {
53347
53843
  return `<!doctype html>
53348
53844
  <html lang="en">
53349
53845
  <head>
53350
53846
  <meta charset="utf-8">
53351
53847
  <meta name="viewport" content="width=device-width, initial-scale=1">
53352
53848
  <title>${escapeHtml(title)}</title>
53353
- <style>${STYLE}</style>
53849
+ <style>${STYLE}${style}</style>
53354
53850
  </head>
53355
53851
  <body>
53356
53852
  ${BRAND}
@@ -53372,7 +53868,7 @@ function caseSection(doc, image) {
53372
53868
  const status = doc.lastRun ? doc.lastRun.passed ? "passed" : "failed" : "not-run";
53373
53869
  return `<section class="case">
53374
53870
  <h1>${escapeHtml(doc.name)}</h1>
53375
- <div class="meta">Case <code>${escapeHtml(doc.id)}</code> / app <code>${escapeHtml(doc.appId)}</code> / recorded ${escapeHtml(doc.createdAt.slice(0, 10))}${doc.editedAt ? ` / edited ${escapeHtml(doc.editedAt.slice(0, 10))}` : ""}${doc.muted ? ' / <span class="muted">muted</span>' : ""}</div>
53871
+ <div class="meta">Case <code>${escapeHtml(doc.id)}</code>${doc.reference ? ` / reference ${escapeHtml(doc.reference)}` : ""} / app <code>${escapeHtml(doc.appId)}</code> / recorded ${escapeHtml(doc.createdAt.slice(0, 10))}${doc.editedAt ? ` / edited ${escapeHtml(doc.editedAt.slice(0, 10))}` : ""}${doc.muted ? ' / <span class="muted">muted</span>' : ""}</div>
53376
53872
  <div>Last run: <span class="${status}">${escapeHtml(lastRunText(doc))}</span></div>
53377
53873
  <h2>Preconditions</h2>
53378
53874
  <ol>${pre}</ol>
@@ -53426,121 +53922,123 @@ function suiteReportHtml(suite, cases, image) {
53426
53922
  <table><thead><tr><th>Case</th><th>Result</th><th>Details</th><th>Screen</th></tr></thead><tbody>${rows}</tbody></table>`
53427
53923
  );
53428
53924
  }
53429
-
53430
- // ../../packages/core/src/image/similarity.ts
53431
- var DEFAULTS = {
53432
- columns: 27,
53433
- rows: 120,
53434
- cropTop: 0.06,
53435
- cropBottom: 0.05,
53436
- tolerance: 14,
53437
- maxShiftRows: 8
53438
- };
53439
- var LOOKS_DIFFERENT_BELOW = 85;
53440
- function luminanceAt(image, index) {
53441
- const { channels, data } = image;
53442
- const at = index * channels;
53443
- if (channels <= 2) return data[at];
53444
- return 0.299 * data[at] + 0.587 * data[at + 1] + 0.114 * data[at + 2];
53445
- }
53446
- function luminanceGrid(image, options = {}) {
53447
- const { columns, rows, cropTop, cropBottom } = { ...DEFAULTS, ...options };
53448
- const top = Math.floor(image.height * cropTop);
53449
- const bottom = Math.max(top + rows, Math.ceil(image.height * (1 - cropBottom)));
53450
- const usable = Math.min(image.height, bottom) - top;
53451
- const sums = new Float64Array(columns * rows);
53452
- const counts = new Float64Array(columns * rows);
53453
- for (let y = top; y < top + usable; y += 2) {
53454
- const row = Math.min(rows - 1, Math.floor((y - top) * rows / usable));
53455
- for (let x = 0; x < image.width; x += 2) {
53456
- const column = Math.min(columns - 1, Math.floor(x * columns / image.width));
53457
- const cell = row * columns + column;
53458
- sums[cell] += luminanceAt(image, y * image.width + x);
53459
- counts[cell] += 1;
53460
- }
53461
- }
53462
- for (let i = 0; i < sums.length; i++) sums[i] = counts[i] > 0 ? sums[i] / counts[i] : 0;
53463
- return sums;
53464
- }
53465
- function background(a, b) {
53466
- const buckets = /* @__PURE__ */ new Map();
53467
- for (const grid of [a, b]) {
53468
- for (const value of grid) {
53469
- const bucket = Math.round(value / 8);
53470
- buckets.set(bucket, (buckets.get(bucket) ?? 0) + 1);
53471
- }
53472
- }
53473
- let best = 0;
53474
- let bestCount = -1;
53475
- for (const [bucket, count] of buckets) {
53476
- if (count > bestCount) {
53477
- best = bucket;
53478
- bestCount = count;
53479
- }
53480
- }
53481
- return best * 8;
53482
- }
53483
- function scoreAt(gridA, gridB, columns, rows, shift, bg, tolerance) {
53484
- let content = 0;
53485
- let matched = 0;
53486
- let cells = 0;
53487
- let allMatched = 0;
53488
- for (let row = Math.max(0, -shift); row < Math.min(rows, rows - shift); row++) {
53489
- for (let column = 0; column < columns; column++) {
53490
- const va = gridA[row * columns + column];
53491
- const at = (row + shift) * columns + column;
53492
- const vb = gridB[at];
53493
- const up = row + shift > 0 ? gridB[at - columns] : vb;
53494
- const down = row + shift < rows - 1 ? gridB[at + columns] : vb;
53495
- const low = Math.min(up, vb, down) - tolerance;
53496
- const high = Math.max(up, vb, down) + tolerance;
53497
- const same = va >= low && va <= high;
53498
- cells++;
53499
- if (same) allMatched++;
53500
- if (Math.abs(va - bg) > 10 || Math.abs(vb - bg) > 10) {
53501
- content++;
53502
- if (same) matched++;
53503
- }
53504
- }
53505
- }
53506
- if (content < cells * 0.02) return cells === 0 ? 0 : allMatched / cells;
53507
- return matched / content;
53508
- }
53509
- function screenSimilarity(a, b, options = {}) {
53510
- const { tolerance, columns, rows, maxShiftRows } = { ...DEFAULTS, ...options };
53511
- const gridA = luminanceGrid(a, options);
53512
- const gridB = luminanceGrid(b, options);
53513
- const bg = background(gridA, gridB);
53514
- let best = 0;
53515
- for (let shift = -maxShiftRows; shift <= maxShiftRows; shift++) {
53516
- best = Math.max(best, scoreAt(gridA, gridB, columns, rows, shift, bg, tolerance));
53517
- }
53518
- return Math.round(best * 1e3) / 10;
53519
- }
53520
-
53521
- // ../../packages/core/src/export/report.ts
53522
- function looksDifferent(similarity) {
53523
- return similarity !== void 0 && similarity < LOOKS_DIFFERENT_BELOW;
53925
+ var EVIDENCE_STYLE = `
53926
+ .cover { page-break-after: always; }
53927
+ table.facts { width: auto; min-width: 60%; }
53928
+ table.facts th { width: 130px; }
53929
+ table.signoff th:first-child { width: 110px; }
53930
+ table.signoff td { height: 44px; }
53931
+ .case h2 { margin-top: 0; }
53932
+ .written { white-space: pre-wrap; }
53933
+ .hash { font-size: 11px; word-break: break-all; }
53934
+ .print-button { margin-bottom: 16px; padding: 6px 14px; font: inherit; cursor: pointer; }
53935
+ @media print { a { color: inherit; text-decoration: none; } .print-button { display: none; } tr { page-break-inside: avoid; } }
53936
+ `;
53937
+ function deviceText(testedOn) {
53938
+ if (!testedOn) return void 0;
53939
+ const name = [testedOn.maker, testedOn.model].filter(Boolean).join(" ");
53940
+ const android = testedOn.android ? `Android ${testedOn.android}` : "";
53941
+ const text = [name, android].filter(Boolean).join(", ");
53942
+ const kind = testedOn.emulator === true ? "Emulator" : testedOn.emulator === false ? "Real phone" : "";
53943
+ if (!text) return kind || void 0;
53944
+ return kind ? `${kind}: ${text}` : text;
53945
+ }
53946
+ function recordLine(record) {
53947
+ return `<code>${escapeHtml(record.path)}</code><div class="hash">SHA-256 <code>${escapeHtml(record.sha256)}</code></div>`;
53948
+ }
53949
+ function evidenceCaseSection(item, number, image, time, screens) {
53950
+ const run2 = item.run;
53951
+ const anyFailedRow = item.steps.some((step) => step.status === "failed");
53952
+ const rows = item.steps.map((step) => {
53953
+ const status = step.status ?? "not-run";
53954
+ const shotPath = evidenceScreen(step, screens, run2?.failureScreenshot);
53955
+ const shot = shotPath ? image(shotPath) : void 0;
53956
+ return `<tr><td class="num">${step.number}</td><td>${escapeHtml(step.label)}${step.error ? `<div class="error">${escapeHtml(step.error)}</div>` : ""}</td><td>${escapeHtml(step.expected)}</td><td class="${status}">${status === "not-run" ? "not run" : status}${step.looksDifferent ? '<div class="warn">looks different from the recording</div>' : ""}</td><td>${img(shot, `Screen after step ${step.number}`)}</td></tr>`;
53957
+ }).join("");
53958
+ const failure = run2?.failureScreenshot && !anyFailedRow ? image(run2.failureScreenshot) : void 0;
53959
+ const meta = [
53960
+ `Case <code>${escapeHtml(item.caseId)}</code>`,
53961
+ ...item.reference ? [`reference ${escapeHtml(item.reference)}`] : [],
53962
+ ...item.appId ? [`app <code>${escapeHtml(item.appId)}</code>`] : [],
53963
+ ...run2 ? [`run <code>${escapeHtml(run2.runId)}</code> at ${escapeHtml(time(run2.startedAt))}`] : [],
53964
+ ...item.attempts > 1 ? [`${item.attempts} runs`] : []
53965
+ ].join(" / ");
53966
+ return `<section class="case" id="case-${number}">
53967
+ <h2>${number}. ${escapeHtml(item.name)}</h2>
53968
+ <div class="meta">${meta}</div>
53969
+ ${item.record ? `<div class="meta">Results from ${recordLine(item.record)}</div>` : ""}
53970
+ <p>Result: <span class="${item.outcome}">${escapeHtml(outcomeText(item.outcome))}</span></p>
53971
+ ${item.stale ? '<p class="warn">The case was edited after this run. The steps below are the case as it is now, so they may not line up with what ran.</p>' : ""}
53972
+ ${item.missing ? '<p class="warn">The case was deleted after Run all, so its steps are not shown.</p>' : ""}
53973
+ ${item.note ? `<p class="meta">${escapeHtml(item.note)}</p>` : ""}
53974
+ ${run2?.error ? `<p class="error">${escapeHtml(run2.error)}</p>` : ""}
53975
+ ${failure ? `<h3>Screen when it failed</h3>${img(failure, "Screen when the run failed")}` : ""}
53976
+ ${rows ? `<table><thead><tr><th>#</th><th>Step</th><th>Expected result</th><th>Result</th><th>Screen after</th></tr></thead><tbody>${rows}</tbody></table>` : ""}
53977
+ </section>`;
53524
53978
  }
53525
- function runReportRows(testCase, run2, names = {}) {
53526
- const results = new Map(run2.steps.map((step) => [step.flowIndex, step]));
53527
- return caseFlowItems(testCase).map((item, flowIndex) => {
53528
- const result = results.get(flowIndex);
53529
- const recorded = item.kind === "step" ? item.step.screenshot : void 0;
53530
- return {
53531
- number: flowIndex + 1,
53532
- kind: item.kind === "step" ? item.step.kind : item.kind === "expectation" ? "end" : item.kind,
53533
- label: describeFlowItem(item, testCase.appId, names),
53534
- ...result ? { status: result.status } : {},
53535
- ...result?.error ? { error: result.error } : {},
53536
- ...result?.durationMs !== void 0 ? { durationMs: result.durationMs } : {},
53537
- ...result?.startedAt !== void 0 ? { startedAt: result.startedAt } : {},
53538
- ...recorded ? { recorded } : {},
53539
- ...result?.replayScreenshot ? { replayed: result.replayScreenshot } : {},
53540
- ...result?.similarity !== void 0 ? { similarity: result.similarity } : {},
53541
- looksDifferent: looksDifferent(result?.similarity)
53542
- };
53543
- });
53979
+ function evidenceHtml(input) {
53980
+ const { suite, cases, image } = input;
53981
+ const time = input.time ?? localTimeText;
53982
+ const screens = input.screens ?? "all";
53983
+ const release2 = input.release?.trim() ?? "";
53984
+ const testedBy = input.testedBy?.trim() ?? "";
53985
+ const signoffRole = input.signoffRole?.trim() || "Accepted by";
53986
+ const caseApps = [...new Set(cases.map((item) => item.appId).filter((appId) => Boolean(appId)))];
53987
+ const apps = suite.testedOn?.apps.length ? suite.testedOn.apps : caseApps.map((appId) => ({ appId }));
53988
+ const appLines = apps.map((app) => `<code>${escapeHtml(app.appId)}</code> ${escapeHtml(appVersionText(app) ?? "version not read")}`).join("<br>");
53989
+ const counts = /* @__PURE__ */ new Map();
53990
+ for (const item of cases) {
53991
+ const text = outcomeText(item.outcome).toLowerCase();
53992
+ counts.set(text, (counts.get(text) ?? 0) + 1);
53993
+ }
53994
+ const tally = [...counts].map(([text, count]) => `${count} ${text}`).join(", ");
53995
+ const passed = suite.status === "finished" && cases.some((item) => item.outcome === "passed" || item.outcome === "flaky") && cases.every((item) => ["passed", "flaky", "muted"].includes(item.outcome));
53996
+ const failedCount = cases.filter((item) => item.outcome === "failed" || item.outcome === "error").length;
53997
+ const verdict = passed ? "Every case that ran passed." : failedCount > 0 ? `${failedCount} of ${cases.length} cases did not pass.` : "Not every case ran.";
53998
+ const withSection = new Set(cases.filter((item) => item.steps.length > 0 || item.run).map((item) => item.caseId));
53999
+ const withReference = cases.some((item) => item.reference);
54000
+ const summaryRows = cases.map((item, index) => {
54001
+ const number = index + 1;
54002
+ const name = withSection.has(item.caseId) ? `<a href="#case-${number}">${escapeHtml(item.name)}</a>` : escapeHtml(item.name);
54003
+ const problems = [item.failedStep ? `Failed at: ${item.failedStep}` : "", item.error ?? ""].filter(Boolean).join("\n");
54004
+ const notes = [item.note ?? "", item.stale ? "The case was edited after this run." : "", item.missing ? "The case was deleted after Run all." : ""].filter(Boolean).join("\n");
54005
+ return `<tr><td class="num">${number}</td>${withReference ? `<td>${escapeHtml(item.reference ?? "")}</td>` : ""}<td>${name}</td><td class="${item.outcome}">${escapeHtml(outcomeText(item.outcome))}</td><td>${problems ? `<div class="error">${escapeHtml(problems)}</div>` : ""}${notes ? `<div class="meta">${escapeHtml(notes)}</div>` : ""}</td></tr>`;
54006
+ }).join("");
54007
+ const runText = `${time(suite.startedAt)} to ${suite.finishedAt ? time(suite.finishedAt) : "not finished"}`;
54008
+ const facts = [
54009
+ ["Release", escapeHtml(release2)],
54010
+ [apps.length > 1 ? "Apps" : "App", appLines || "none"],
54011
+ ["Device", escapeHtml(deviceText(suite.testedOn) ?? "not read")],
54012
+ ["Run", `${escapeHtml(runText)}<div class="meta">Run all <code>${escapeHtml(suite.suiteId)}</code></div>`],
54013
+ ["Failures", suite.retryFailed ? "A case that failed ran once more. If it passed then, it shows as passed when run again." : "Each case ran once."],
54014
+ ["Screens", screens === "checks" ? "After checks and after a step that failed." : "After every step."],
54015
+ ["Tested by", escapeHtml(testedBy)],
54016
+ ["Result", `<span class="${passed ? "passed" : "failed"}">${escapeHtml(verdict)}</span><div class="meta">${escapeHtml(tally || "No cases.")}</div>`],
54017
+ ["Known issues", `<div class="written">${escapeHtml(input.knownIssues?.trim() ?? "")}</div>`],
54018
+ ["Conclusion", `<div class="written">${escapeHtml(input.conclusion?.trim() ?? "")}</div>`],
54019
+ ...input.record ? [["Record", recordLine(input.record)]] : []
54020
+ ];
54021
+ const factRows = facts.map(([label2, value]) => `<tr><th>${label2}</th><td>${value}</td></tr>`).join("");
54022
+ const stopped = suite.status === "stopped" ? '<p class="warn">Run all was stopped before the end.</p>' : suite.status === "running" ? '<p class="warn">Run all was still running when this was made.</p>' : "";
54023
+ const sections = cases.map((item, index) => withSection.has(item.caseId) ? evidenceCaseSection(item, index + 1, image, time, screens) : "").filter(Boolean).join("\n");
54024
+ return page(
54025
+ `Test results report${release2 ? `: ${release2}` : ""}`,
54026
+ `<button type="button" class="print-button" onclick="window.print()">Print or save as PDF</button>
54027
+ <section class="cover">
54028
+ <h1>Test results report</h1>
54029
+ <table class="facts"><tbody>${factRows}</tbody></table>
54030
+ ${stopped}
54031
+ <h2>Cases</h2>
54032
+ ${cases.length === 0 ? "<p>No cases.</p>" : `<table><thead><tr><th>#</th>${withReference ? "<th>Reference</th>" : ""}<th>Case</th><th>Result</th><th>Details</th></tr></thead><tbody>${summaryRows}</tbody></table>`}
54033
+ <h2>Sign-off</h2>
54034
+ <table class="signoff"><thead><tr><th></th><th>Name</th><th>Date</th><th>Signature</th></tr></thead><tbody>
54035
+ <tr><th>Tested by</th><td>${escapeHtml(testedBy)}</td><td></td><td></td></tr>
54036
+ <tr><th>${escapeHtml(signoffRole)}</th><td></td><td></td><td></td></tr>
54037
+ </tbody></table>
54038
+ </section>
54039
+ ${sections}`,
54040
+ EVIDENCE_STYLE
54041
+ );
53544
54042
  }
53545
54043
 
53546
54044
  // ../../packages/core/src/export/xlsx.ts
@@ -53608,165 +54106,18 @@ function buildXlsx(sheets) {
53608
54106
  ]);
53609
54107
  }
53610
54108
 
53611
- // ../../packages/core/src/image/png.ts
53612
- import { crc32 as crc322, deflateSync, inflateSync } from "node:zlib";
53613
- var SIGNATURE = Uint8Array.from([137, 80, 78, 71, 13, 10, 26, 10]);
53614
- var CHANNELS = { 0: 1, 2: 3, 4: 2, 6: 4 };
53615
- var COLOR_TYPE = { 1: 0, 2: 4, 3: 2, 4: 6 };
53616
- var PngError = class extends Error {
53617
- name = "PngError";
53618
- };
53619
- function readUint32(bytes, offset) {
53620
- return (bytes[offset] << 24 | bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3]) >>> 0;
53621
- }
53622
- function paeth(left, up, upLeft) {
53623
- const estimate = left + up - upLeft;
53624
- const toLeft = Math.abs(estimate - left);
53625
- const toUp = Math.abs(estimate - up);
53626
- const toUpLeft = Math.abs(estimate - upLeft);
53627
- if (toLeft <= toUp && toLeft <= toUpLeft) return left;
53628
- return toUp <= toUpLeft ? up : upLeft;
53629
- }
53630
- function decodePng(file) {
53631
- if (file.length < 8 || SIGNATURE.some((byte, i) => file[i] !== byte)) throw new PngError("Not a PNG file");
53632
- let offset = 8;
53633
- let width = 0;
53634
- let height = 0;
53635
- let channels;
53636
- const idat = [];
53637
- while (offset + 8 <= file.length) {
53638
- const length = readUint32(file, offset);
53639
- const type = String.fromCharCode(file[offset + 4], file[offset + 5], file[offset + 6], file[offset + 7]);
53640
- const body = file.subarray(offset + 8, offset + 8 + length);
53641
- if (body.length !== length) throw new PngError("Truncated PNG chunk");
53642
- if (type === "IHDR") {
53643
- width = readUint32(body, 0);
53644
- height = readUint32(body, 4);
53645
- const bitDepth = body[8];
53646
- const colorType = body[9];
53647
- const interlace = body[12];
53648
- if (bitDepth !== 8) throw new PngError(`Unsupported PNG bit depth ${bitDepth}`);
53649
- if (interlace !== 0) throw new PngError("Interlaced PNGs are not supported");
53650
- channels = CHANNELS[colorType];
53651
- if (!channels) throw new PngError(`Unsupported PNG color type ${colorType}`);
53652
- } else if (type === "IDAT") {
53653
- idat.push(body);
53654
- } else if (type === "IEND") {
53655
- break;
53656
- }
53657
- offset += 12 + length;
53658
- }
53659
- if (!channels || width === 0 || height === 0) throw new PngError("PNG has no image header");
53660
- const packed = inflateSync(Buffer.concat(idat));
53661
- const stride = width * channels;
53662
- if (packed.length < height * (stride + 1)) throw new PngError("PNG image data is too short");
53663
- const data = new Uint8Array(height * stride);
53664
- for (let y = 0; y < height; y++) {
53665
- const filter = packed[y * (stride + 1)];
53666
- const source = y * (stride + 1) + 1;
53667
- const row = y * stride;
53668
- const previous = row - stride;
53669
- for (let x = 0; x < stride; x++) {
53670
- const raw = packed[source + x];
53671
- const left = x >= channels ? data[row + x - channels] : 0;
53672
- const up = y > 0 ? data[previous + x] : 0;
53673
- const upLeft = y > 0 && x >= channels ? data[previous + x - channels] : 0;
53674
- let value;
53675
- switch (filter) {
53676
- case 0:
53677
- value = raw;
53678
- break;
53679
- case 1:
53680
- value = raw + left;
53681
- break;
53682
- case 2:
53683
- value = raw + up;
53684
- break;
53685
- case 3:
53686
- value = raw + (left + up >> 1);
53687
- break;
53688
- case 4:
53689
- value = raw + paeth(left, up, upLeft);
53690
- break;
53691
- default:
53692
- throw new PngError(`Unknown PNG filter ${filter}`);
53693
- }
53694
- data[row + x] = value & 255;
53695
- }
53696
- }
53697
- return { width, height, channels, data };
53698
- }
53699
- function chunk(type, body) {
53700
- const head = Buffer.alloc(8);
53701
- head.writeUInt32BE(body.length, 0);
53702
- head.write(type, 4, "latin1");
53703
- const crc = Buffer.alloc(4);
53704
- crc.writeUInt32BE(crc322(Buffer.concat([head.subarray(4), body])) >>> 0, 0);
53705
- return Buffer.concat([head, body, crc]);
53706
- }
53707
- function encodePng(image) {
53708
- const { width, height, channels, data } = image;
53709
- const stride = width * channels;
53710
- const filtered = Buffer.alloc(height * (stride + 1));
53711
- for (let y = 0; y < height; y++) {
53712
- filtered[y * (stride + 1)] = 2;
53713
- for (let x = 0; x < stride; x++) {
53714
- const up = y > 0 ? data[(y - 1) * stride + x] : 0;
53715
- filtered[y * (stride + 1) + 1 + x] = data[y * stride + x] - up & 255;
53716
- }
53717
- }
53718
- const header = Buffer.alloc(13);
53719
- header.writeUInt32BE(width, 0);
53720
- header.writeUInt32BE(height, 4);
53721
- header[8] = 8;
53722
- header[9] = COLOR_TYPE[channels];
53723
- return Buffer.concat([
53724
- Buffer.from(SIGNATURE),
53725
- chunk("IHDR", header),
53726
- chunk("IDAT", deflateSync(filtered)),
53727
- chunk("IEND", new Uint8Array(0))
53728
- ]);
53729
- }
53730
- function resizeImage(image, width, height) {
53731
- const { channels } = image;
53732
- const data = new Uint8Array(width * height * channels);
53733
- const sums = new Float64Array(channels);
53734
- for (let y = 0; y < height; y++) {
53735
- const y0 = Math.floor(y * image.height / height);
53736
- const y1 = Math.max(y0 + 1, Math.floor((y + 1) * image.height / height));
53737
- for (let x = 0; x < width; x++) {
53738
- const x0 = Math.floor(x * image.width / width);
53739
- const x1 = Math.max(x0 + 1, Math.floor((x + 1) * image.width / width));
53740
- sums.fill(0);
53741
- for (let sy = y0; sy < y1; sy++) {
53742
- for (let sx = x0; sx < x1; sx++) {
53743
- const at = (sy * image.width + sx) * channels;
53744
- for (let c = 0; c < channels; c++) sums[c] += image.data[at + c];
53745
- }
53746
- }
53747
- const count = (y1 - y0) * (x1 - x0);
53748
- const out = (y * width + x) * channels;
53749
- for (let c = 0; c < channels; c++) data[out + c] = Math.round(sums[c] / count);
53750
- }
53751
- }
53752
- return { width, height, channels, data };
53753
- }
53754
-
53755
54109
  // ../../packages/core/src/maestro/block-parse.ts
53756
54110
  var import_yaml2 = __toESM(require_dist(), 1);
53757
54111
 
53758
- // ../../packages/core/src/maestro/model.ts
53759
- var MAESTRO_KEYS = ["Home", "Back", "Enter", "Backspace", "Tab", "Escape", "Volume Up", "Volume Down", "Power", "Lock"];
53760
- var MAESTRO_ORIENTATIONS = ["PORTRAIT", "LANDSCAPE_LEFT", "LANDSCAPE_RIGHT", "UPSIDE_DOWN"];
53761
- var VARIABLE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
53762
- function flowVariables(flow) {
53763
- const names = /* @__PURE__ */ new Set();
53764
- for (const match of JSON.stringify(flow.steps).matchAll(VARIABLE)) names.add(match[1]);
53765
- return [...names].sort();
53766
- }
53767
-
53768
54112
  // ../../packages/core/src/maestro/parse.ts
53769
54113
  var import_yaml = __toESM(require_dist(), 1);
54114
+ var LONGEST_TIMER_MS = 2147483647;
54115
+ function isWaitForm(arg) {
54116
+ if (!isRecord(arg) || Object.keys(arg).some((key) => !["visible", "timeout", "optional", "label"].includes(key))) return false;
54117
+ const { visible, timeout, optional, label: label2 } = arg;
54118
+ if (visible !== NEVER_ON_SCREEN || optional !== true || typeof timeout !== "number") return false;
54119
+ return timeout > 0 && timeout <= LONGEST_TIMER_MS && (label2 === void 0 || label2 === waitName(timeout / 1e3));
54120
+ }
53770
54121
  var MaestroParseError = class extends Error {
53771
54122
  name = "MaestroParseError";
53772
54123
  };
@@ -53956,6 +54307,9 @@ function parseStep(raw, position) {
53956
54307
  expectKeys(arg, ["link"], `openLink at ${where}`);
53957
54308
  return { kind: "openLink", link: expectString(arg.link, `openLink.link at ${where}`) };
53958
54309
  }
54310
+ case "extendedWaitUntil":
54311
+ if (isWaitForm(arg)) return { kind: "wait", seconds: arg.timeout / 1e3 };
54312
+ throw new MaestroParseError(`Unsupported command "extendedWaitUntil" at ${where}`);
53959
54313
  case "takeScreenshot": {
53960
54314
  if (typeof arg === "string") return { kind: "takeScreenshot", path: arg };
53961
54315
  if (!isRecord(arg)) throw new MaestroParseError(`takeScreenshot needs a path at ${where}`);
@@ -54005,7 +54359,8 @@ function fromFlow(flow) {
54005
54359
  commands: flow.steps.map((step) => ({ kind: "step", step })),
54006
54360
  maestroOnly: [],
54007
54361
  variables: flowVariables(flow),
54008
- flow
54362
+ flow,
54363
+ ownFlow: flow
54009
54364
  };
54010
54365
  }
54011
54366
  function parseBlockFlow(yamlText) {
@@ -54104,6 +54459,10 @@ function stepToYaml(step) {
54104
54459
  return { assertNotVisible: selectorToYaml(step.selector) };
54105
54460
  case "openLink":
54106
54461
  return { openLink: quoted(step.link) };
54462
+ case "wait":
54463
+ return {
54464
+ extendedWaitUntil: { visible: quoted(NEVER_ON_SCREEN), timeout: step.seconds * 1e3, optional: true, label: quoted(waitName(step.seconds)) }
54465
+ };
54107
54466
  case "takeScreenshot":
54108
54467
  return step.optional === void 0 ? { takeScreenshot: { path: step.path } } : { takeScreenshot: { path: step.path, optional: step.optional } };
54109
54468
  case "startRecording":
@@ -54281,7 +54640,7 @@ function chooseSelector(node, allNodes, point, viewport) {
54281
54640
  }
54282
54641
 
54283
54642
  // ../../packages/core/src/project/store.ts
54284
- import { access, cp, mkdir as mkdir2, readdir as readdir2, readFile as readFile2, rename, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
54643
+ import { access, cp, mkdir as mkdir3, readdir as readdir3, readFile as readFile2, rename, rm as rm3, writeFile as writeFile2 } from "node:fs/promises";
54285
54644
  import { basename, dirname as dirname3, isAbsolute, join as join4, relative, resolve, sep } from "node:path";
54286
54645
 
54287
54646
  // ../../packages/core/src/recorder/recorder.ts
@@ -54484,6 +54843,9 @@ var Recorder = class {
54484
54843
  recordOpenLink(url, screenshotPath) {
54485
54844
  return this.recordSimple({ kind: "openLink", url }, screenshotPath);
54486
54845
  }
54846
+ recordWait(seconds, screenshotPath) {
54847
+ return this.recordSimple({ kind: "wait", seconds }, screenshotPath);
54848
+ }
54487
54849
  recordHideKeyboard(screenshotPath) {
54488
54850
  return this.recordSimple({ kind: "hideKeyboard" }, screenshotPath);
54489
54851
  }
@@ -54549,6 +54911,7 @@ var Recorder = class {
54549
54911
  expectations: this.expectations.map((expectation) => structuredClone(expectation)),
54550
54912
  ...meta.editedAt ? { editedAt: meta.editedAt } : {},
54551
54913
  ...meta.muted ? { muted: true } : {},
54914
+ ...meta.reference ? { reference: meta.reference } : {},
54552
54915
  ...this.evidenceCounter > 0 ? { lastEvidenceNumber: this.evidenceCounter } : {}
54553
54916
  };
54554
54917
  }
@@ -54604,6 +54967,8 @@ function stepToMaestro(step, scrollUntilVisible, blockFlow) {
54604
54967
  return { kind: "launchApp" };
54605
54968
  case "openLink":
54606
54969
  return { kind: "openLink", link: step.url };
54970
+ case "wait":
54971
+ return { kind: "wait", seconds: step.seconds };
54607
54972
  case "hideKeyboard":
54608
54973
  return { kind: "hideKeyboard" };
54609
54974
  case "scroll":
@@ -54646,6 +55011,7 @@ function agentDeviceReplays(step) {
54646
55011
  case "pressKey":
54647
55012
  return AGENT_DEVICE_KEYS.has(step.key);
54648
55013
  case "setOrientation":
55014
+ case "wait":
54649
55015
  return false;
54650
55016
  case "longPressOn":
54651
55017
  case "doubleTapOn":
@@ -54669,6 +55035,8 @@ function directAction(step) {
54669
55035
  } else if ((step.kind === "longPressOn" || step.kind === "doubleTapOn") && step.selector.kind === "point") {
54670
55036
  const point = { xPercent: step.selector.xPercent, yPercent: step.selector.yPercent };
54671
55037
  return { kind: step.kind === "longPressOn" ? "longPress" : "doubleTap", point };
55038
+ } else if (step.kind === "wait") {
55039
+ return { kind: "wait", seconds: step.seconds };
54672
55040
  }
54673
55041
  throw new Error(`The studio cannot replay ${JSON.stringify(step)} on the device`);
54674
55042
  }
@@ -54702,18 +55070,46 @@ async function exists(path) {
54702
55070
  return false;
54703
55071
  }
54704
55072
  }
54705
- async function writeFileAtomic(path, content) {
54706
- const temp = `${path}.${process.pid}.tmp`;
54707
- await writeFile2(temp, content, "utf8");
54708
- await rename(temp, path);
55073
+ var IN_USE = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY"]);
55074
+ var RENAME_TRIES = 10;
55075
+ async function renameWhenFree(from, to, platform = process.platform, move = rename) {
55076
+ for (let attempt = 1; ; attempt++) {
55077
+ try {
55078
+ return await move(from, to);
55079
+ } catch (error) {
55080
+ const code = error.code;
55081
+ if (platform !== "win32" || !code || !IN_USE.has(code) || attempt >= RENAME_TRIES) throw error;
55082
+ await new Promise((wake) => setTimeout(wake, Math.min(20 * 2 ** (attempt - 1), 400)));
55083
+ }
55084
+ }
54709
55085
  }
54710
- function slugify(name) {
55086
+ var writing = /* @__PURE__ */ new Map();
55087
+ async function writeFileAtomic(path, content, move = rename) {
55088
+ const write = async () => {
55089
+ const temp = `${path}.${process.pid}.tmp`;
55090
+ await writeFile2(temp, content, "utf8");
55091
+ try {
55092
+ await renameWhenFree(temp, path, process.platform, move);
55093
+ } catch (error) {
55094
+ await rm3(temp, { force: true }).catch(() => void 0);
55095
+ throw error;
55096
+ }
55097
+ };
55098
+ const current = (writing.get(path) ?? Promise.resolve()).then(write, write);
55099
+ writing.set(path, current);
55100
+ try {
55101
+ await current;
55102
+ } finally {
55103
+ if (writing.get(path) === current) writing.delete(path);
55104
+ }
55105
+ }
55106
+ function slugify(name, fallback = "case") {
54711
55107
  const slug = name.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48).replace(/-+$/g, "");
54712
- return slug || "case";
55108
+ return slug || fallback;
54713
55109
  }
54714
55110
  function timestampSuffix(date) {
54715
- const pad = (value) => String(value).padStart(2, "0");
54716
- return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}`;
55111
+ const pad2 = (value) => String(value).padStart(2, "0");
55112
+ return `${date.getFullYear()}${pad2(date.getMonth() + 1)}${pad2(date.getDate())}-${pad2(date.getHours())}${pad2(date.getMinutes())}`;
54717
55113
  }
54718
55114
  function makeCaseId(name, date) {
54719
55115
  return `${slugify(name)}-${timestampSuffix(date)}`;
@@ -54789,16 +55185,16 @@ var ProjectStore = class _ProjectStore {
54789
55185
  * project.json points elsewhere. Folders outside the project are not created.
54790
55186
  */
54791
55187
  async ensureLayout() {
54792
- await mkdir2(this.dir, { recursive: true });
55188
+ await mkdir3(this.dir, { recursive: true });
54793
55189
  if (!await exists(this.path("project.json"))) {
54794
55190
  await this.writeSettings(DEFAULT_SETTINGS);
54795
55191
  if (!await exists(this.path(".gitignore"))) await writeFile2(this.path(".gitignore"), PROJECT_GITIGNORE, "utf8");
54796
55192
  }
54797
55193
  const settings = await this.readSettings();
54798
55194
  this.paths = resolveProjectPaths(this.dir, settings.paths);
54799
- for (const name of DIRS) await mkdir2(join4(this.dir, name), { recursive: true });
55195
+ for (const name of DIRS) await mkdir3(join4(this.dir, name), { recursive: true });
54800
55196
  for (const folder of [...this.paths.blockDirs, this.paths.flowsDir]) {
54801
- if (isInside(folder, this.dir)) await mkdir2(folder, { recursive: true });
55197
+ if (isInside(folder, this.dir)) await mkdir3(folder, { recursive: true });
54802
55198
  }
54803
55199
  }
54804
55200
  /** The block folders and the flows folder, absolute. */
@@ -54820,6 +55216,11 @@ var ProjectStore = class _ProjectStore {
54820
55216
  runFlowFile(blockFile) {
54821
55217
  return posixRelative(this.paths.flowsDir, resolve(this.dir, blockFile));
54822
55218
  }
55219
+ /** How a block refers to another block: from its own folder, or from where a new block goes. */
55220
+ blockFlowPaths(file) {
55221
+ const folder = file === void 0 ? this.paths.blockDirs[0] ?? this.dir : dirname3(this.blockPath(file));
55222
+ return { runFlowFile: (blockFile) => posixRelative(folder, resolve(this.dir, blockFile)) };
55223
+ }
54823
55224
  /** The flows folder as seen from `absoluteDir`, forward slashes, e.g. "../../flows" from runs/<runId>/. */
54824
55225
  flowsDirFrom(absoluteDir) {
54825
55226
  return posixRelative(absoluteDir, this.paths.flowsDir) || ".";
@@ -54891,7 +55292,7 @@ var ProjectStore = class _ProjectStore {
54891
55292
  const blocks = [];
54892
55293
  for (const dir of this.paths.blockDirs) {
54893
55294
  if (!await exists(dir)) continue;
54894
- const files = (await readdir2(dir)).filter((file) => /\.ya?ml$/i.test(file)).sort();
55295
+ const files = (await readdir3(dir)).filter((file) => /\.ya?ml$/i.test(file)).sort();
54895
55296
  for (const file of files) {
54896
55297
  const absolute = join4(dir, file);
54897
55298
  const projectPath = this.relative(absolute);
@@ -54916,11 +55317,81 @@ var ProjectStore = class _ProjectStore {
54916
55317
  }
54917
55318
  return blocks;
54918
55319
  }
55320
+ /**
55321
+ * Writes a block over its own file, or with no `file` to a new one named
55322
+ * after it in the first block folder. Returns the block's project path.
55323
+ */
55324
+ async writeBlock(flow, file) {
55325
+ const path = file === void 0 ? await this.newBlockPath(flow.name ?? "") : this.blockFilePath(file);
55326
+ await mkdir3(dirname3(path), { recursive: true });
55327
+ await writeFileAtomic(path, toMaestroYaml(flow));
55328
+ return this.relative(path);
55329
+ }
55330
+ async deleteBlock(file) {
55331
+ await rm3(this.blockFilePath(file));
55332
+ }
55333
+ /** blockPath for changing a block: unlike reading, it refuses every other file in the project, such as .env. */
55334
+ blockFilePath(file) {
55335
+ const path = this.blockPath(file);
55336
+ if (!/\.ya?ml$/i.test(path) || !this.paths.blockDirs.some((dir) => isInside(path, dir))) throw new Error(`Not a block file: ${file}`);
55337
+ return path;
55338
+ }
55339
+ /**
55340
+ * Whether `steps`, as the block `file`, lead back to that file, directly or
55341
+ * through the flows they run. A flow that cannot be read leads nowhere, and
55342
+ * listBlocks reports it on its own.
55343
+ */
55344
+ async reachesBlock(file, steps) {
55345
+ const target = this.blockPath(file);
55346
+ const seen = /* @__PURE__ */ new Set();
55347
+ const follow = async (from, commands) => {
55348
+ for (const step of commands) {
55349
+ if (step.kind !== "runFlow" || step.file.includes("${")) continue;
55350
+ const next = resolve(dirname3(from), step.file);
55351
+ if (next === target) return true;
55352
+ if (seen.has(next)) continue;
55353
+ seen.add(next);
55354
+ if (await follow(next, await readSteps(next))) return true;
55355
+ }
55356
+ return false;
55357
+ };
55358
+ return follow(target, steps);
55359
+ }
55360
+ /** Where a new block with this name would go, as a project path. */
55361
+ async newBlockFile(name) {
55362
+ return this.relative(await this.newBlockPath(name));
55363
+ }
55364
+ /** "Log in" -> blocks/log-in.yaml, then log-in-2.yaml. A file a case or a block runs is never reused. */
55365
+ async newBlockPath(name) {
55366
+ const [dir] = this.paths.blockDirs;
55367
+ if (!dir) throw new Error('project.json lists no block folders in "paths.blocks".');
55368
+ const slug = slugify(name, "block");
55369
+ for (let number = 1; ; number++) {
55370
+ const base = join4(dir, number === 1 ? slug : `${slug}-${number}`);
55371
+ if (!await exists(`${base}.yaml`) && !await exists(`${base}.yml`)) return `${base}.yaml`;
55372
+ }
55373
+ }
55374
+ /** The names of the saved cases and the other blocks that run a block. */
55375
+ async blockUsers(file) {
55376
+ const cases = [];
55377
+ for (const summary of await this.listCases()) {
55378
+ const { name, preconditionBlocks, steps } = await this.readCase(summary.id);
55379
+ if (preconditionBlocks.some((use) => use.file === file) || steps.some((step) => step.kind === "block" && step.file === file)) cases.push(name);
55380
+ }
55381
+ const target = this.blockPath(file);
55382
+ const blocks = [];
55383
+ for (const block of await this.listBlocks()) {
55384
+ if (block.file === file) continue;
55385
+ const path = this.blockPath(block.file);
55386
+ if ((await readSteps(path)).some((step) => step.kind === "runFlow" && resolve(dirname3(path), step.file) === target)) blocks.push(block.name);
55387
+ }
55388
+ return [...cases, ...blocks];
55389
+ }
54919
55390
  async listCases() {
54920
55391
  const dir = this.path("cases");
54921
55392
  if (!await exists(dir)) return [];
54922
55393
  const summaries = [];
54923
- for (const file of (await readdir2(dir)).filter((name) => name.endsWith(".json"))) {
55394
+ for (const file of (await readdir3(dir)).filter((name) => name.endsWith(".json"))) {
54924
55395
  try {
54925
55396
  const testCase = JSON.parse(await readFile2(join4(dir, file), "utf8"));
54926
55397
  summaries.push({
@@ -54948,7 +55419,7 @@ var ProjectStore = class _ProjectStore {
54948
55419
  }
54949
55420
  /** Writes cases/<id>.json only; callers regenerate the flow with writeFlow. */
54950
55421
  async writeCase(testCase) {
54951
- await mkdir2(this.path("cases"), { recursive: true });
55422
+ await mkdir3(this.path("cases"), { recursive: true });
54952
55423
  await writeFileAtomic(this.casePath(testCase.id), `${JSON.stringify(testCase, null, 2)}
54953
55424
  `);
54954
55425
  }
@@ -54979,7 +55450,7 @@ var ProjectStore = class _ProjectStore {
54979
55450
  const dir = this.evidenceDir(caseId);
54980
55451
  if (!await exists(dir)) return 0;
54981
55452
  let max = 0;
54982
- for (const name of await readdir2(dir)) {
55453
+ for (const name of await readdir3(dir)) {
54983
55454
  const match = /^step-(\d+)\.png$/.exec(name);
54984
55455
  if (match) max = Math.max(max, Number(match[1]));
54985
55456
  }
@@ -55017,7 +55488,7 @@ var ProjectStore = class _ProjectStore {
55017
55488
  const source = this.path(path);
55018
55489
  if (!await exists(source)) return void 0;
55019
55490
  const target = `evidence/${toCaseId}/${basename(path)}`;
55020
- await mkdir2(this.evidenceDir(toCaseId), { recursive: true });
55491
+ await mkdir3(this.evidenceDir(toCaseId), { recursive: true });
55021
55492
  await cp(source, this.path(target));
55022
55493
  return target;
55023
55494
  }
@@ -55040,7 +55511,7 @@ var ProjectStore = class _ProjectStore {
55040
55511
  return this.path(`${SUITES_DIR}/${suiteId}.json`);
55041
55512
  }
55042
55513
  async saveSuite(suite) {
55043
- await mkdir2(this.path(SUITES_DIR), { recursive: true });
55514
+ await mkdir3(this.path(SUITES_DIR), { recursive: true });
55044
55515
  await writeFileAtomic(this.suiteFile(suite.suiteId), `${JSON.stringify(suite, null, 2)}
55045
55516
  `);
55046
55517
  }
@@ -55051,7 +55522,7 @@ var ProjectStore = class _ProjectStore {
55051
55522
  async listSuites(limit = 20) {
55052
55523
  const dir = this.path(SUITES_DIR);
55053
55524
  if (!await exists(dir)) return [];
55054
- const names = (await readdir2(dir)).filter((name) => name.endsWith(".json")).sort().reverse().slice(0, limit);
55525
+ const names = (await readdir3(dir)).filter((name) => name.endsWith(".json")).sort().reverse().slice(0, limit);
55055
55526
  const suites = [];
55056
55527
  for (const name of names) {
55057
55528
  try {
@@ -55098,7 +55569,7 @@ var ProjectStore = class _ProjectStore {
55098
55569
  }
55099
55570
  async saveRun(summary) {
55100
55571
  const dir = this.runDir(summary.runId);
55101
- await mkdir2(dir, { recursive: true });
55572
+ await mkdir3(dir, { recursive: true });
55102
55573
  const file = join4(dir, "summary.json");
55103
55574
  await writeFileAtomic(file, `${JSON.stringify(summary, null, 2)}
55104
55575
  `);
@@ -55113,7 +55584,7 @@ var ProjectStore = class _ProjectStore {
55113
55584
  if (!await exists(dir)) return [];
55114
55585
  const pattern = runIdPattern(caseId);
55115
55586
  const runs = [];
55116
- for (const name of (await readdir2(dir)).filter((entry) => pattern.test(entry))) {
55587
+ for (const name of (await readdir3(dir)).filter((entry) => pattern.test(entry))) {
55117
55588
  try {
55118
55589
  runs.push(await this.readRun(name));
55119
55590
  } catch {
@@ -55123,20 +55594,20 @@ var ProjectStore = class _ProjectStore {
55123
55594
  }
55124
55595
  /** Removes the case file, its flow, its screenshots and every run of it. */
55125
55596
  async deleteCase(caseId) {
55126
- await rm2(this.casePath(caseId), { force: true });
55127
- await rm2(this.flowPath(caseId), { force: true });
55128
- await rm2(this.evidenceDir(caseId), { recursive: true, force: true });
55597
+ await rm3(this.casePath(caseId), { force: true });
55598
+ await rm3(this.flowPath(caseId), { force: true });
55599
+ await rm3(this.evidenceDir(caseId), { recursive: true, force: true });
55129
55600
  const runs = this.path("runs");
55130
55601
  if (!await exists(runs)) return;
55131
55602
  const pattern = runIdPattern(caseId);
55132
- for (const name of (await readdir2(runs)).filter((entry) => pattern.test(entry))) {
55133
- await rm2(join4(runs, name), { recursive: true, force: true });
55603
+ for (const name of (await readdir3(runs)).filter((entry) => pattern.test(entry))) {
55604
+ await rm3(join4(runs, name), { recursive: true, force: true });
55134
55605
  }
55135
55606
  }
55136
55607
  /** Writes cases/<id>.json and regenerates flows/<id>.yaml from `flow`. */
55137
55608
  async saveCase(testCase, flow) {
55138
55609
  const casePath = this.casePath(testCase.id);
55139
- await mkdir2(this.path("cases"), { recursive: true });
55610
+ await mkdir3(this.path("cases"), { recursive: true });
55140
55611
  await writeFileAtomic(casePath, `${JSON.stringify(testCase, null, 2)}
55141
55612
  `);
55142
55613
  const { flowPath, yaml } = await this.writeFlow(testCase.id, flow);
@@ -55146,11 +55617,15 @@ var ProjectStore = class _ProjectStore {
55146
55617
  async writeFlow(caseId, flow) {
55147
55618
  const flowPath = this.flowPath(caseId);
55148
55619
  const yaml = toMaestroYaml(flow);
55149
- await mkdir2(this.paths.flowsDir, { recursive: true });
55620
+ await mkdir3(this.paths.flowsDir, { recursive: true });
55150
55621
  await writeFile2(flowPath, yaml, "utf8");
55151
55622
  return { flowPath, yaml };
55152
55623
  }
55153
55624
  };
55625
+ async function readSteps(path) {
55626
+ const outline = await readFile2(path, "utf8").then(parseBlockFlow).catch(() => void 0);
55627
+ return outline?.commands.flatMap((command) => command.kind === "step" ? [command.step] : []) ?? [];
55628
+ }
55154
55629
  var MAX_RUNFLOW_DEPTH = 8;
55155
55630
  async function readBlockOutline(absolute, seen = []) {
55156
55631
  const outline = parseBlockFlow(await readFile2(absolute, "utf8"));
@@ -55174,7 +55649,8 @@ async function readBlockOutline(absolute, seen = []) {
55174
55649
  try {
55175
55650
  nested = await readBlockOutline(target, chain);
55176
55651
  } catch (error) {
55177
- throw new Error(`It runs ${file}, which cannot be read: ${error instanceof Error ? error.message : String(error)}`);
55652
+ const reason = (error instanceof Error ? error.message : String(error)).split("\n")[0].replace(/:$/, "");
55653
+ throw new Error(`It runs ${file}, which cannot be read: ${reason}`);
55178
55654
  }
55179
55655
  for (const name of nested.maestroOnly) nestedOnly.add(name);
55180
55656
  }
@@ -55357,8 +55833,8 @@ function relocateFlow(flow, flowsDirFromTarget) {
55357
55833
 
55358
55834
  // ../../packages/core/src/replay/maestro-run.ts
55359
55835
  import { spawn as spawn2 } from "node:child_process";
55360
- import { access as access2, mkdir as mkdir3, readdir as readdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
55361
- import { join as join5 } from "node:path";
55836
+ import { access as access2, mkdir as mkdir4, readdir as readdir4, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
55837
+ import { join as join5, win32 as win324 } from "node:path";
55362
55838
 
55363
55839
  // ../../packages/core/src/replay/run-result.ts
55364
55840
  var BOOKKEEPING = /* @__PURE__ */ new Set(["defineVariablesCommand", "applyConfigurationCommand"]);
@@ -55534,7 +56010,7 @@ function deviceProblemField(passed, diagnostics, steps, appId) {
55534
56010
  }
55535
56011
 
55536
56012
  // ../../packages/core/src/replay/windows-command.ts
55537
- import { win32 as win322 } from "node:path";
56013
+ import { win32 as win323 } from "node:path";
55538
56014
  var CMD_META = /([()\][%!^"`<>&|;, *?])/g;
55539
56015
  function quoteWindowsArgument(arg) {
55540
56016
  let quoted2 = '"';
@@ -55557,7 +56033,7 @@ function escapeForCmd(text, parses) {
55557
56033
  function spawnCommand(file, args, platform = process.platform, comspec = process.env.ComSpec) {
55558
56034
  if (platform !== "win32" || !/\.(bat|cmd)$/i.test(file)) return { command: file, args: [...args] };
55559
56035
  if (args.some((arg) => /[\r\n]/.test(arg))) throw new Error("An argument for a .bat or .cmd file cannot contain a line break.");
55560
- const line = [`"${win322.normalize(file)}"`, ...args.map((arg) => escapeForCmd(quoteWindowsArgument(arg), 2))].join(" ");
56036
+ const line = [`"${win323.normalize(file)}"`, ...args.map((arg) => escapeForCmd(quoteWindowsArgument(arg), 2))].join(" ");
55561
56037
  return { command: comspec || "cmd.exe", args: ["/d", "/s", "/v:off", "/c", `"${line}"`], windowsVerbatimArguments: true };
55562
56038
  }
55563
56039
 
@@ -55574,18 +56050,24 @@ function stopRun(child) {
55574
56050
  return setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS);
55575
56051
  }
55576
56052
  function maestroEnv(androidHome2, env = process.env, platform = process.platform) {
55577
- const result = { ...env, ...androidHome2 ? { ANDROID_HOME: androidHome2 } : {} };
56053
+ const javaHome = toolsJavaHome(env, platform);
56054
+ const result = { ...env, ...androidHome2 ? { ANDROID_HOME: androidHome2 } : {}, ...javaHome ? { JAVA_HOME: javaHome } : {} };
55578
56055
  if (platform === "win32") {
55579
- result.MAESTRO_OPTS = [env.MAESTRO_OPTS, "-Dstdout.encoding=UTF-8", "-Dstderr.encoding=UTF-8"].filter(Boolean).join(" ");
56056
+ result.MAESTRO_OPTS = [env.MAESTRO_OPTS, "-Dfile.encoding=UTF-8", "-Dstdout.encoding=UTF-8", "-Dstderr.encoding=UTF-8"].filter(Boolean).join(" ");
55580
56057
  }
55581
56058
  return result;
55582
56059
  }
56060
+ function pathForMaestro(path, cwd, platform = process.platform) {
56061
+ if (platform !== "win32" || isPlainAscii(path)) return path;
56062
+ const inside = win324.relative(cwd, path);
56063
+ return isPlainAscii(inside) && !inside.startsWith("..") && !win324.isAbsolute(inside) ? inside : path;
56064
+ }
55583
56065
  async function runMaestro(options) {
55584
- await mkdir3(options.debugDir, { recursive: true });
55585
- const args = ["--device", options.deviceId, "test", "--no-ansi", "--debug-output", options.debugDir, "--flatten-debug-output"];
56066
+ await mkdir4(options.debugDir, { recursive: true });
56067
+ const args = ["--device", options.deviceId, "test", "--no-ansi", "--debug-output", pathForMaestro(options.debugDir, options.cwd), "--flatten-debug-output"];
55586
56068
  if (options.keepDriver) args.push("--no-reinstall-driver");
55587
56069
  for (const [key, value] of Object.entries(options.env)) args.push("-e", `${key}=${value}`);
55588
- args.push(options.flowPath);
56070
+ args.push(pathForMaestro(options.flowPath, options.cwd));
55589
56071
  const outputFile = join5(options.debugDir, "output.txt");
55590
56072
  const chunks = [];
55591
56073
  const started2 = Date.now();
@@ -55633,7 +56115,7 @@ async function exists2(path) {
55633
56115
  }
55634
56116
  }
55635
56117
  async function readMaestroCommands(debugDir) {
55636
- const entries = await readdir3(debugDir, { withFileTypes: true }).catch(() => []);
56118
+ const entries = await readdir4(debugDir, { withFileTypes: true }).catch(() => []);
55637
56119
  const dirs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
55638
56120
  for (const name of dirs) {
55639
56121
  const file = join5(debugDir, name, "commands.json");
@@ -55652,7 +56134,7 @@ function redactStrings(value, env) {
55652
56134
  return value;
55653
56135
  }
55654
56136
  async function redactRunFiles(dir, env) {
55655
- const entries = await readdir3(dir, { withFileTypes: true, recursive: true }).catch(() => []);
56137
+ const entries = await readdir4(dir, { withFileTypes: true, recursive: true }).catch(() => []);
55656
56138
  for (const entry of entries) {
55657
56139
  if (!entry.isFile() || !TEXT_FILE.test(entry.name)) continue;
55658
56140
  const file = join5(entry.parentPath, entry.name);
@@ -55925,18 +56407,18 @@ function caseHealth(runs, editedAt, limit = 10) {
55925
56407
  // ../studio/src/server/index.ts
55926
56408
  var import_static = __toESM(require_static(), 1);
55927
56409
  var import_fastify = __toESM(require_fastify(), 1);
55928
- import { existsSync as existsSync5 } from "node:fs";
55929
- import { rm as rm7 } from "node:fs/promises";
55930
- import { resolve as resolve3 } from "node:path";
56410
+ import { existsSync as existsSync6 } from "node:fs";
56411
+ import { rm as rm11 } from "node:fs/promises";
56412
+ import { resolve as resolve4 } from "node:path";
55931
56413
  import { fileURLToPath as fileURLToPath2 } from "node:url";
55932
56414
 
55933
56415
  // ../studio/src/server/phone-capture.ts
55934
- import { appendFile, copyFile, mkdir as mkdir5 } from "node:fs/promises";
56416
+ import { appendFile, copyFile as copyFile2, mkdir as mkdir6 } from "node:fs/promises";
55935
56417
  import { dirname as dirname5 } from "node:path";
55936
56418
 
55937
56419
  // ../studio/src/server/common.ts
55938
56420
  import { createReadStream } from "node:fs";
55939
- import { access as access3, mkdir as mkdir4, writeFile as writeFile4 } from "node:fs/promises";
56421
+ import { access as access3, copyFile, mkdir as mkdir5, readFile as readFile4, rename as rename2, rm as rm4, writeFile as writeFile4 } from "node:fs/promises";
55940
56422
  import { dirname as dirname4 } from "node:path";
55941
56423
 
55942
56424
  // ../studio/src/server/state.ts
@@ -55955,6 +56437,9 @@ function enqueue(state, task) {
55955
56437
  function scratchScreenshot(state) {
55956
56438
  return join6(state.scratchDir, "screen.png");
55957
56439
  }
56440
+ function capturingScreenshot(state) {
56441
+ return join6(state.scratchDir, "capturing.png");
56442
+ }
55958
56443
  function toLiteNode(node) {
55959
56444
  const lite = {};
55960
56445
  if (node.ref) lite.ref = node.ref;
@@ -55974,7 +56459,10 @@ function toScreenState(latest) {
55974
56459
  height: latest.viewport.height,
55975
56460
  imageUrl: `/api/screen/image?ts=${Date.parse(latest.takenAt) || Date.now()}`,
55976
56461
  takenAt: latest.takenAt,
55977
- nodes: latest.nodes.filter((node) => node.visibleToUser !== false && node.interactionBlocked === void 0 && node.rect).map(toLiteNode)
56462
+ nodes: latest.nodes.filter((node) => node.visibleToUser !== false && node.interactionBlocked === void 0 && node.rect).map(toLiteNode),
56463
+ ...latest.hidden ? { hidden: true } : {},
56464
+ ...latest.secure ? { secure: true } : {},
56465
+ ...latest.elsewhere ? { elsewhere: latest.elsewhere } : {}
55978
56466
  };
55979
56467
  }
55980
56468
  function evidenceUrl(projectRelative) {
@@ -55991,6 +56479,7 @@ function currentCaseView(state) {
55991
56479
  if (!state.recorder?.active) return null;
55992
56480
  const view = toCaseView(state.recorder.toCase());
55993
56481
  const editing = state.editing;
56482
+ if (state.recordingBlock) return { ...view, forBlock: true };
55994
56483
  return editing ? { ...view, editing: { mode: editing.mode, keptSteps: editing.keptSteps, tailSteps: editing.tail.length } } : view;
55995
56484
  }
55996
56485
  function suiteRunning(state) {
@@ -56047,6 +56536,29 @@ function requireSession(state) {
56047
56536
  throw new HttpError(409, "NO_SESSION", "No device connected. Pick a device, enter the app id and click Connect first.");
56048
56537
  }
56049
56538
  }
56539
+ function readPicks(body) {
56540
+ const picks = (body ?? {}).picks;
56541
+ if (picks === void 0) return [];
56542
+ if (!Array.isArray(picks) || !picks.every((pick) => Number.isInteger(pick) && pick >= 0)) {
56543
+ throw new HttpError(400, "BAD_PICK", 'Expected "picks" to be a list of candidate ids.');
56544
+ }
56545
+ return [...new Set(picks)];
56546
+ }
56547
+ function pickedCandidates(state, picks) {
56548
+ const screen = state.offered ?? state.latest;
56549
+ const candidates = screen ? expectationCandidates(screen.nodes, screen.viewport) : [];
56550
+ return picks.map((pick) => {
56551
+ const candidate = candidates[pick];
56552
+ if (!candidate) throw new HttpError(400, "BAD_PICK", `There is no candidate ${pick} on the current screen.`);
56553
+ return candidate;
56554
+ });
56555
+ }
56556
+ function requireNoRecording(state) {
56557
+ if (state.recorder?.active) throw new HttpError(409, "CASE_ACTIVE", "Finish or discard the recording first.");
56558
+ }
56559
+ function requireRecording(state, recorder) {
56560
+ if (state.recorder !== recorder) throw new HttpError(409, "NO_CASE", "That recording has already ended.");
56561
+ }
56050
56562
  function requireCase(state) {
56051
56563
  if (!state.recorder?.active) throw new HttpError(409, "NO_CASE", "No case is being recorded. Start a case first.");
56052
56564
  return state.recorder;
@@ -56055,8 +56567,43 @@ async function projectEnv(state) {
56055
56567
  const settings = await state.project.readSettings();
56056
56568
  return { ...settings.env ?? {}, ...await state.project.readEnv() };
56057
56569
  }
56570
+ async function phoneLocked(state) {
56571
+ const { deviceId } = state.session;
56572
+ if (!deviceId) return false;
56573
+ const locked = await state.phoneTools.locked(deviceId).catch(() => void 0);
56574
+ if (locked === void 0) return state.lockAnswered === deviceId;
56575
+ state.lockAnswered = deviceId;
56576
+ return locked;
56577
+ }
56578
+ async function movePicture(from, path) {
56579
+ await mkdir5(dirname4(path), { recursive: true });
56580
+ try {
56581
+ await rename2(from, path);
56582
+ } catch {
56583
+ await copyFile(from, path);
56584
+ await rm4(from, { force: true });
56585
+ }
56586
+ }
56058
56587
  async function captureInto(state, path) {
56059
- state.latest = await state.session.capture(path);
56588
+ const capture = await state.session.capture(capturingScreenshot(state));
56589
+ let locked;
56590
+ try {
56591
+ locked = await phoneLocked(state);
56592
+ if (locked) await writeFile4(capture.screenshotPath, blackPicture(capture.viewport));
56593
+ await movePicture(capture.screenshotPath, path);
56594
+ } catch (error) {
56595
+ await rm4(capture.screenshotPath, { force: true }).catch(() => void 0);
56596
+ throw error;
56597
+ }
56598
+ const saved = { ...capture, screenshotPath: path };
56599
+ if (locked) {
56600
+ state.latest = { ...saved, nodes: [], keyboard: void 0, hidden: true };
56601
+ return state.latest;
56602
+ }
56603
+ const view = screenView(capture.nodes, await readFile4(path).catch(() => new Uint8Array()));
56604
+ const front = view === "hidden" ? void 0 : foregroundApp(capture);
56605
+ const elsewhere = front && front !== state.session.appId && front !== SYSTEM_UI ? { app: front, home: looksLikeHomeApp(front) } : void 0;
56606
+ state.latest = { ...saved, ...view === "hidden" ? { hidden: true } : {}, ...view === "secure" ? { secure: true } : {}, ...elsewhere ? { elsewhere } : {} };
56060
56607
  return state.latest;
56061
56608
  }
56062
56609
  async function captureScratch(state) {
@@ -56126,6 +56673,8 @@ async function performDirect(state, action) {
56126
56673
  const at = pointOnScreen(action.point, (await state.session.snapshot()).viewport);
56127
56674
  return action.kind === "longPress" ? state.session.longPressAt(at) : state.session.doubleTapAt(at);
56128
56675
  }
56676
+ case "wait":
56677
+ return new Promise((resolve7) => setTimeout(resolve7, action.seconds * 1e3));
56129
56678
  }
56130
56679
  }
56131
56680
  async function replayOnDevice(state, flow, steps, env, scratchPath) {
@@ -56136,7 +56685,7 @@ async function replayOnDevice(state, flow, steps, env, scratchPath) {
56136
56685
  }
56137
56686
  const header = flow.name === void 0 ? { appId: flow.appId } : { appId: flow.appId, name: flow.name };
56138
56687
  const relocated = relocateFlow({ ...header, steps: segment.steps }, state.project.flowsDirFrom(dirname4(scratchPath)));
56139
- await mkdir4(dirname4(scratchPath), { recursive: true });
56688
+ await mkdir5(dirname4(scratchPath), { recursive: true });
56140
56689
  await writeFile4(scratchPath, toMaestroYaml(relocated), "utf8");
56141
56690
  await state.session.replayFlow(scratchPath, env);
56142
56691
  }
@@ -56154,8 +56703,8 @@ var KEYBOARD_OPENS_WITHIN_MS = 1500;
56154
56703
  var EVIDENCE_DELAY_MS = 900;
56155
56704
  var EVIDENCE_LIMIT_MS = 4e3;
56156
56705
  var MAX_SNAPSHOT_FAILURES = 25;
56706
+ var LOCK_CHECK_MS = 1e3;
56157
56707
  var STALE_SNAPSHOT_MS = 1e3;
56158
- var SYSTEM_UI = "com.android.systemui";
56159
56708
  var TIMINGS_KEPT = 4e3;
56160
56709
  function bump(counts, key) {
56161
56710
  counts[key] = (counts[key] ?? 0) + 1;
@@ -56179,6 +56728,8 @@ var PhoneCapture = class {
56179
56728
  stopped = false;
56180
56729
  paused = false;
56181
56730
  cameBack = false;
56731
+ locked = false;
56732
+ lockCheckedAt = 0;
56182
56733
  revision = 0;
56183
56734
  noticeCount = 0;
56184
56735
  notice;
@@ -56245,7 +56796,7 @@ var PhoneCapture = class {
56245
56796
  ...this.state.phoneError ? { stoppedBy: this.state.phoneError } : {}
56246
56797
  };
56247
56798
  const path = this.state.project.path("diagnostics/phone-sessions.jsonl");
56248
- await mkdir5(dirname5(path), { recursive: true });
56799
+ await mkdir6(dirname5(path), { recursive: true });
56249
56800
  await appendFile(path, `${JSON.stringify(line)}
56250
56801
  `, "utf8");
56251
56802
  }
@@ -56273,7 +56824,7 @@ var PhoneCapture = class {
56273
56824
  return;
56274
56825
  }
56275
56826
  }
56276
- await new Promise((resolve6) => setTimeout(resolve6, SNAPSHOT_GAP_MS));
56827
+ await new Promise((resolve7) => setTimeout(resolve7, SNAPSHOT_GAP_MS));
56277
56828
  }
56278
56829
  }
56279
56830
  async see() {
@@ -56296,18 +56847,30 @@ var PhoneCapture = class {
56296
56847
  this.viewport = viewport;
56297
56848
  const screen = { nodes: snapshot.nodes, viewport, startedAt, endedAt, ...snapshot.keyboard ? { keyboard: snapshot.keyboard } : {} };
56298
56849
  const app = foregroundApp(screen);
56299
- this.trackApp(app);
56850
+ this.trackApp(app, await this.lockScreenUp());
56300
56851
  if (!this.paused) this.noteField(screen);
56301
56852
  this.history.push({ ...screen, ...app ? { app } : {}, outside: this.paused });
56302
56853
  if (this.history.length > HISTORY) this.history.shift();
56303
56854
  }
56304
- /** Home, the launcher or the notification shade pause recording until the app is back. */
56305
- trackApp(app) {
56306
- if (app === this.appId && this.paused) {
56855
+ /**
56856
+ * Android's word for it, at most once a second. A lock screen need not be
56857
+ * System UI on every maker's phone, and its PIN pad must never become steps.
56858
+ */
56859
+ async lockScreenUp() {
56860
+ const now = Date.now();
56861
+ if (now - this.lockCheckedAt >= LOCK_CHECK_MS) {
56862
+ this.lockCheckedAt = now;
56863
+ this.locked = await phoneLocked(this.state);
56864
+ }
56865
+ return this.locked;
56866
+ }
56867
+ /** Home, the launcher, the notification shade or the lock screen pause recording until the app is back. */
56868
+ trackApp(app, locked) {
56869
+ if (app === this.appId && this.paused && !locked) {
56307
56870
  this.paused = false;
56308
56871
  this.cameBack = true;
56309
56872
  this.revision += 1;
56310
- } else if (app !== void 0 && !this.paused && (app === this.input.homeApp || app === SYSTEM_UI)) {
56873
+ } else if (!this.paused && (locked || app !== void 0 && (app === this.input.homeApp || app === SYSTEM_UI))) {
56311
56874
  this.paused = true;
56312
56875
  this.counts.pauses += 1;
56313
56876
  this.revision += 1;
@@ -56317,7 +56880,7 @@ var PhoneCapture = class {
56317
56880
  const keyboardOpening = this.typing !== void 0 && gesture.downAt - this.focusedAt < KEYBOARD_OPENS_WITHIN_MS;
56318
56881
  const until = Date.now() + (keyboardOpening ? OPENING_WAIT_MS : AT_TOUCH_WAIT_MS);
56319
56882
  while (!this.history.some((screen) => screen.startedAt >= gesture.downAt) && Date.now() < until && !this.stopped) {
56320
- await new Promise((resolve6) => setTimeout(resolve6, SNAPSHOT_GAP_MS));
56883
+ await new Promise((resolve7) => setTimeout(resolve7, SNAPSHOT_GAP_MS));
56321
56884
  }
56322
56885
  const finished = this.history.filter((screen) => screen.endedAt <= gesture.downAt);
56323
56886
  const usable = finished.filter((screen) => screen.app !== void 0);
@@ -56457,9 +57020,7 @@ var PhoneCapture = class {
56457
57020
  if (typing.password) {
56458
57021
  if (typing.secretKeys === 0) return;
56459
57022
  typing.secretKeys = 0;
56460
- const keys = Object.keys(await projectEnv(this.state)).sort();
56461
- const key = keys.find((candidate) => /pass/i.test(candidate)) ?? "PASSWORD";
56462
- const text2 = `\${${key}}`;
57023
+ const text2 = reference(passwordSetting(Object.keys(await projectEnv(this.state))));
56463
57024
  this.record("type", text2, (recorder, screenshot) => recorder.recordInput(text2, screenshot), "password");
56464
57025
  return;
56465
57026
  }
@@ -56487,9 +57048,9 @@ var PhoneCapture = class {
56487
57048
  const [first, ...others] = pending;
56488
57049
  if (!first) return;
56489
57050
  try {
56490
- await mkdir5(dirname5(first.path), { recursive: true });
57051
+ await mkdir6(dirname5(first.path), { recursive: true });
56491
57052
  await captureInto(this.state, first.path);
56492
- for (const other of others) await copyFile(first.path, other.path);
57053
+ for (const other of others) await copyFile2(first.path, other.path);
56493
57054
  } catch (error) {
56494
57055
  for (const item of pending) delete item.step.screenshot;
56495
57056
  console.error(`[studio] no screenshot for a phone step: ${describeDeviceError(error).message}`);
@@ -56563,19 +57124,244 @@ function registerPhoneRoutes(app, state) {
56563
57124
  }
56564
57125
 
56565
57126
  // ../studio/src/server/diagnostics.ts
56566
- import { readFile as readFile4, readdir as readdir4 } from "node:fs/promises";
57127
+ import { readFile as readFile5, readdir as readdir6 } from "node:fs/promises";
56567
57128
  import { homedir as homedir3, release, userInfo } from "node:os";
56568
- import { join as join8 } from "node:path";
57129
+ import { join as join9 } from "node:path";
57130
+
57131
+ // ../studio/src/server/setup.ts
57132
+ import { execFile as execFile5, spawn as spawn3 } from "node:child_process";
57133
+ import { existsSync as existsSync4 } from "node:fs";
57134
+ import { delimiter, join as join8 } from "node:path";
57135
+
57136
+ // ../studio/src/shared/setup-steps.ts
57137
+ var MIN_JAVA = 17;
57138
+ function javaIsUsable(java) {
57139
+ return java.found && (java.major === void 0 || java.major >= MIN_JAVA);
57140
+ }
57141
+ function missingTools(info) {
57142
+ const missing = [];
57143
+ if (!info.adb.found) missing.push("adb");
57144
+ if (!javaIsUsable(info.java)) missing.push("java");
57145
+ if (!info.maestro.found) missing.push("maestro");
57146
+ return missing;
57147
+ }
57148
+
57149
+ // ../studio/src/server/tool-install.ts
57150
+ import { execFile as execFile4 } from "node:child_process";
57151
+ import { createHash } from "node:crypto";
57152
+ import { createWriteStream } from "node:fs";
57153
+ import { cp as cp2, mkdir as mkdir7, readdir as readdir5, rename as rename3, rm as rm5 } from "node:fs/promises";
57154
+ import { dirname as dirname6, join as join7 } from "node:path";
57155
+ import { Readable, Transform } from "node:stream";
57156
+ import { pipeline } from "node:stream/promises";
57157
+ import { setTimeout as sleep } from "node:timers/promises";
57158
+ var WINDOWS_X64 = [
57159
+ {
57160
+ id: "adb",
57161
+ name: "Android tools",
57162
+ url: "https://dl.google.com/android/repository/platform-tools_r37.0.1-win.zip",
57163
+ bytes: 8044989,
57164
+ sha256: "45f4d63113e895ebde0c90f194099a4676b6ac653bd28d54314a9e022bbc1a99",
57165
+ host: "dl.google.com",
57166
+ check: { file: "adb.exe", args: ["version"] }
57167
+ },
57168
+ {
57169
+ id: "java",
57170
+ name: "Java",
57171
+ url: "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.21%2B9/OpenJDK17U-jre_x64_windows_hotspot_17.0.21_9.zip",
57172
+ bytes: 43860024,
57173
+ sha256: "fe4656bd78fb09812dc38c49da777f79cc33e7ee435a254be82f9e7f40d9d117",
57174
+ host: "github.com",
57175
+ check: { file: join7("bin", "java.exe"), args: ["-version"] }
57176
+ },
57177
+ {
57178
+ id: "maestro",
57179
+ name: "Maestro",
57180
+ url: "https://github.com/mobile-dev-inc/maestro/releases/download/cli-2.12.1/maestro.zip",
57181
+ bytes: 349832427,
57182
+ sha256: "54dbb0716425626604973cbb374c585ca30272375d9ce15177a1152765ddd9d7",
57183
+ host: "github.com",
57184
+ check: { file: join7("bin", "maestro.bat"), args: ["--version"] }
57185
+ }
57186
+ ];
57187
+ var STALL_MS = 6e4;
57188
+ var CHECK_TIMEOUT_MS = 12e4;
57189
+ var UNZIP_TIMEOUT_MS = 3e5;
57190
+ var MOVE_ATTEMPTS = 6;
57191
+ var MOVE_RETRY_MS = 500;
57192
+ function unzipWithTar(zipFile, into) {
57193
+ const tar = join7(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe");
57194
+ const folder = dirname6(zipFile);
57195
+ return new Promise((resolvePromise, reject) => {
57196
+ const args = ["-xf", zipFile.slice(folder.length + 1), "-C", into.slice(folder.length + 1)];
57197
+ execFile4(tar, args, { cwd: folder, timeout: UNZIP_TIMEOUT_MS, windowsHide: true }, (error) => error ? reject(error) : resolvePromise());
57198
+ });
57199
+ }
57200
+ function startProgram(file, args, androidHome2) {
57201
+ const command = spawnCommand(file, args);
57202
+ return new Promise((resolvePromise, reject) => {
57203
+ execFile4(
57204
+ command.command,
57205
+ command.args,
57206
+ { env: maestroEnv(androidHome2()), timeout: CHECK_TIMEOUT_MS, windowsHide: true, windowsVerbatimArguments: command.windowsVerbatimArguments ?? false },
57207
+ (error) => error ? reject(error) : resolvePromise()
57208
+ );
57209
+ });
57210
+ }
57211
+ function defaultInstallIo(androidHome2, env = process.env, platform = process.platform, arch = process.arch) {
57212
+ const tools = toolsTarget(env, platform);
57213
+ const shared = sharedToolsRoot(env, platform);
57214
+ return {
57215
+ downloads: tools && arch === "x64" ? WINDOWS_X64 : [],
57216
+ workDir: join7(tools ?? "", ".incoming"),
57217
+ destination: (id) => id === "adb" ? join7(androidHome2() ?? defaultSdkDir(), "platform-tools") : join7(tools ?? "", id),
57218
+ fetch: (...args) => fetch(...args),
57219
+ unzip: unzipWithTar,
57220
+ start: (file, args) => startProgram(file, args, androidHome2),
57221
+ prepare: async () => {
57222
+ if (!shared) return;
57223
+ await claimFolder(shared).catch((error) => {
57224
+ console.error(`[studio] Set up this computer: claiming ${shared}: ${error instanceof Error ? error.message : String(error)}`);
57225
+ throw new PlainError(`Ariadne will not run tools from ${shared}, because another account on this computer may be able to change them. Ask your IT team to check who owns that folder, because another account's tools may be in it, then press Try again.`);
57226
+ });
57227
+ }
57228
+ };
57229
+ }
57230
+ var PlainError = class extends Error {
57231
+ };
57232
+ function codeOf(error) {
57233
+ return error?.code;
57234
+ }
57235
+ function explain(download, error) {
57236
+ if (error instanceof PlainError) return error.message;
57237
+ const code = codeOf(error);
57238
+ if (code === "ENOSPC") return `There is not enough free disk space for ${download.name}. Free some space and press Try again.`;
57239
+ if (code === "EACCES" || code === "EPERM" || code === "EBUSY") {
57240
+ return `Ariadne could not save ${download.name} on this computer. Press Try again. If it keeps failing, an antivirus or a company rule may be blocking it.`;
57241
+ }
57242
+ return `Could not download ${download.name}. Check the internet connection and press Try again. On a company computer, ask your IT team to allow ${download.host}.`;
57243
+ }
57244
+ async function moveFolder(from, to) {
57245
+ for (let attempt = 1; attempt < MOVE_ATTEMPTS; attempt += 1) {
57246
+ try {
57247
+ await rename3(from, to);
57248
+ return;
57249
+ } catch (error) {
57250
+ if (codeOf(error) === "EXDEV") break;
57251
+ await sleep(MOVE_RETRY_MS);
57252
+ }
57253
+ }
57254
+ await cp2(from, to, { recursive: true });
57255
+ }
57256
+ var ToolInstaller = class {
57257
+ constructor(io) {
57258
+ this.io = io;
57259
+ }
57260
+ io;
57261
+ running = false;
57262
+ round = Promise.resolve();
57263
+ tools = [];
57264
+ status() {
57265
+ return {
57266
+ available: this.io.downloads.length > 0,
57267
+ running: this.running,
57268
+ offered: this.io.downloads.map(({ id, name, bytes }) => ({ id, name, bytes })),
57269
+ tools: this.tools.map((tool) => ({ ...tool }))
57270
+ };
57271
+ }
57272
+ /** Starts fetching these tools in the background; the guide follows with status(). */
57273
+ start(ids) {
57274
+ if (this.running) return;
57275
+ const chosen = this.io.downloads.filter((download) => ids.includes(download.id));
57276
+ if (chosen.length === 0) return;
57277
+ this.tools = chosen.map(({ id, name, bytes }) => ({ id, name, state: "waiting", received: 0, total: bytes }));
57278
+ this.running = true;
57279
+ this.round = this.installAll(chosen).finally(() => {
57280
+ this.running = false;
57281
+ });
57282
+ }
57283
+ /** Settles when every tool has had its turn. One that fails does not stop the next. */
57284
+ finished() {
57285
+ return this.round;
57286
+ }
57287
+ async installAll(chosen) {
57288
+ for (const download of chosen) {
57289
+ const progress = this.tools.find((tool) => tool.id === download.id);
57290
+ try {
57291
+ await this.install(download, progress);
57292
+ progress.state = "done";
57293
+ } catch (error) {
57294
+ progress.state = "failed";
57295
+ progress.error = explain(download, error);
57296
+ console.error(`[studio] Set up this computer: ${download.name} failed: ${error instanceof Error ? error.message : String(error)}`);
57297
+ }
57298
+ }
57299
+ await rm5(this.io.workDir, { recursive: true, force: true, maxRetries: 3 }).catch(() => void 0);
57300
+ }
57301
+ async install(download, progress) {
57302
+ await this.io.prepare?.();
57303
+ const work = join7(this.io.workDir, download.id);
57304
+ await rm5(work, { recursive: true, force: true, maxRetries: 3 });
57305
+ const unpacked = join7(work, "unpacked");
57306
+ await mkdir7(unpacked, { recursive: true });
57307
+ const zipFile = join7(work, "download.zip");
57308
+ progress.state = "downloading";
57309
+ await this.download(download, zipFile, progress);
57310
+ progress.state = "unpacking";
57311
+ await this.io.unzip(zipFile, unpacked).catch((error) => {
57312
+ console.error(`[studio] Set up this computer: unpacking ${download.name}: ${error instanceof Error ? error.message : String(error)}`);
57313
+ throw new PlainError(`Ariadne could not unpack ${download.name}. Check that the disk has free space and press Try again.`);
57314
+ });
57315
+ const [folder, ...others] = await readdir5(unpacked);
57316
+ if (!folder || others.length > 0) throw new PlainError(`The download of ${download.name} was not what Ariadne expected, so it was not used.`);
57317
+ const destination = this.io.destination(download.id);
57318
+ await mkdir7(dirname6(destination), { recursive: true });
57319
+ await rm5(destination, { recursive: true, force: true, maxRetries: 3 });
57320
+ await moveFolder(join7(unpacked, folder), destination);
57321
+ progress.state = "checking";
57322
+ if (download.id === "maestro" && this.tools.find((tool) => tool.id === "java")?.state === "failed") return;
57323
+ await this.io.start(join7(destination, download.check.file), download.check.args).catch((error) => {
57324
+ console.error(`[studio] Set up this computer: starting ${download.name}: ${error instanceof Error ? error.message : String(error)}`);
57325
+ if (["EACCES", "EPERM", "UNKNOWN"].includes(String(codeOf(error)))) {
57326
+ throw new PlainError(
57327
+ `Ariadne downloaded ${download.name}, but Windows would not start it. A company computer may block programs in your user folder: ask your IT team to allow ${destination}.`
57328
+ );
57329
+ }
57330
+ throw new PlainError(`Ariadne downloaded ${download.name}, but it did not start correctly. Press Try again. If it keeps failing, Help > Show logs has the reason.`);
57331
+ });
57332
+ }
57333
+ async download(download, file, progress) {
57334
+ const aborter = new AbortController();
57335
+ let stall = setTimeout(() => aborter.abort(), STALL_MS);
57336
+ try {
57337
+ const response = await this.io.fetch(download.url, { signal: aborter.signal });
57338
+ if (!response.ok || !response.body) throw new Error(`${download.url} answered ${response.status}`);
57339
+ const hash = createHash("sha256");
57340
+ const meter = new Transform({
57341
+ transform(chunk2, _encoding, done) {
57342
+ clearTimeout(stall);
57343
+ stall = setTimeout(() => aborter.abort(), STALL_MS);
57344
+ hash.update(chunk2);
57345
+ progress.received += chunk2.length;
57346
+ done(null, chunk2);
57347
+ }
57348
+ });
57349
+ await pipeline(Readable.fromWeb(response.body), meter, createWriteStream(file));
57350
+ if (hash.digest("hex") !== download.sha256) {
57351
+ throw new PlainError(`The download of ${download.name} did not match what Ariadne expected, so it was not used. Press Try again.`);
57352
+ }
57353
+ } finally {
57354
+ clearTimeout(stall);
57355
+ }
57356
+ }
57357
+ };
56569
57358
 
56570
57359
  // ../studio/src/server/setup.ts
56571
- import { execFile as execFile4, spawn as spawn3 } from "node:child_process";
56572
- import { existsSync as existsSync3 } from "node:fs";
56573
- import { delimiter, join as join7 } from "node:path";
56574
57360
  var EXEC_TIMEOUT_MS = 1e4;
56575
57361
  function defaultSetupTools(androidHome2, findMaestro2) {
56576
57362
  return {
56577
57363
  exec: (file, args) => new Promise((resolvePromise, reject) => {
56578
- execFile4(file, args, { timeout: EXEC_TIMEOUT_MS, windowsHide: true }, (error, stdout, stderr) => {
57364
+ execFile5(file, args, { timeout: EXEC_TIMEOUT_MS, windowsHide: true }, (error, stdout, stderr) => {
56579
57365
  if (error) reject(error);
56580
57366
  else resolvePromise({ stdout, stderr });
56581
57367
  });
@@ -56587,9 +57373,14 @@ function defaultSetupTools(androidHome2, findMaestro2) {
56587
57373
  },
56588
57374
  androidHome: androidHome2,
56589
57375
  findMaestro: findMaestro2,
57376
+ javaBin: () => {
57377
+ const home = toolsJavaHome();
57378
+ return home ? join8(home, "bin", "java.exe") : "java";
57379
+ },
57380
+ installer: new ToolInstaller(defaultInstallIo(androidHome2)),
56590
57381
  platform: process.platform,
56591
57382
  path: process.env.PATH ?? "",
56592
- exists: existsSync3
57383
+ exists: existsSync4
56593
57384
  };
56594
57385
  }
56595
57386
  function osName(platform) {
@@ -56601,9 +57392,9 @@ function osName(platform) {
56601
57392
  function findProgram(tools, sdkDir, name) {
56602
57393
  const file = tools.platform === "win32" ? `${name}.exe` : name;
56603
57394
  const home = tools.androidHome();
56604
- const candidates = home ? [join7(home, sdkDir, file)] : [];
57395
+ const candidates = home ? [join8(home, sdkDir, file)] : [];
56605
57396
  for (const dir of tools.path.split(delimiter)) {
56606
- if (dir) candidates.push(join7(dir, file));
57397
+ if (dir) candidates.push(join8(dir, file));
56607
57398
  }
56608
57399
  return candidates.find((candidate) => tools.exists(candidate));
56609
57400
  }
@@ -56638,7 +57429,7 @@ function parseAvdList(output) {
56638
57429
  }
56639
57430
  async function checkJava(tools) {
56640
57431
  try {
56641
- const { stdout, stderr } = await tools.exec("java", ["-version"]);
57432
+ const { stdout, stderr } = await tools.exec(tools.javaBin(), ["-version"]);
56642
57433
  const parsed = parseJavaVersion(`${stderr}
56643
57434
  ${stdout}`);
56644
57435
  return parsed ? { found: true, version: parsed.version, major: parsed.major } : { found: true };
@@ -56706,6 +57497,20 @@ async function checkSetup(tools) {
56706
57497
  }
56707
57498
  function registerSetupRoutes(app, tools) {
56708
57499
  app.get("/api/setup", async () => checkSetup(tools));
57500
+ app.get("/api/setup/install", async () => tools.installer.status());
57501
+ app.post("/api/setup/install", async (request) => {
57502
+ if (!tools.installer.status().available) {
57503
+ throw new HttpError(409, "CANNOT_INSTALL", "Ariadne cannot fetch the tools on this computer. Follow the manual steps in Device setup.");
57504
+ }
57505
+ if (!tools.installer.status().running) {
57506
+ const missing = missingTools(await checkSetup(tools));
57507
+ if (missing.includes("adb") && (request.body ?? {}).acceptTerms !== true) {
57508
+ throw new HttpError(400, "TERMS_NOT_ACCEPTED", "The Android tools need Google's Android SDK terms accepted first.");
57509
+ }
57510
+ tools.installer.start(missing);
57511
+ }
57512
+ return tools.installer.status();
57513
+ });
56709
57514
  app.post("/api/setup/emulator", async (request) => {
56710
57515
  const name = (request.body ?? {}).name;
56711
57516
  if (typeof name !== "string" || !name.trim()) throw new HttpError(400, "BAD_REQUEST", 'Expected the "name" of an emulator.');
@@ -56798,11 +57603,11 @@ function cleaner({ env, typed, phones, paths }) {
56798
57603
  };
56799
57604
  }
56800
57605
  async function deviceLogLines(dir) {
56801
- const entries = await readdir4(dir, { withFileTypes: true, recursive: true }).catch(() => []);
57606
+ const entries = await readdir6(dir, { withFileTypes: true, recursive: true }).catch(() => []);
56802
57607
  const logs = entries.filter((entry) => entry.isFile() && entry.name === "device-logcat.txt");
56803
57608
  const lines = [];
56804
57609
  for (const entry of logs) {
56805
- const text = await readFile4(join8(entry.parentPath, entry.name), "utf8").catch(() => "");
57610
+ const text = await readFile5(join9(entry.parentPath, entry.name), "utf8").catch(() => "");
56806
57611
  lines.push(...text.split(/\r?\n/).filter((line) => DEVICE_LOG_LINES.test(line)));
56807
57612
  }
56808
57613
  return lines.length > 0 ? lines.slice(-MAX_DEVICE_LOG_LINES).join("\n") : void 0;
@@ -56817,8 +57622,8 @@ async function runEntries(state, caseIds, clean) {
56817
57622
  const dir = state.project.runDir(run2.runId);
56818
57623
  entries.push({ name: `runs/${run2.runId}/summary.json`, data: clean(JSON.stringify(run2, withoutTypedText, 2)) });
56819
57624
  for (const file of ["output.txt", "maestro.log"]) {
56820
- const path = join8(dir, file);
56821
- if (await exists3(path)) entries.push({ name: `runs/${run2.runId}/${file}`, data: clean(await readFile4(path, "utf8")) });
57625
+ const path = join9(dir, file);
57626
+ if (await exists3(path)) entries.push({ name: `runs/${run2.runId}/${file}`, data: clean(await readFile5(path, "utf8")) });
56822
57627
  }
56823
57628
  const deviceLog = await deviceLogLines(dir);
56824
57629
  if (deviceLog) entries.push({ name: `runs/${run2.runId}/device-log-filtered.txt`, data: clean(deviceLog) });
@@ -56828,7 +57633,7 @@ async function runEntries(state, caseIds, clean) {
56828
57633
  }
56829
57634
  async function readProjectFile(state, path) {
56830
57635
  try {
56831
- return await readFile4(state.project.path(path));
57636
+ return await readFile5(state.project.path(path));
56832
57637
  } catch {
56833
57638
  return void 0;
56834
57639
  }
@@ -56883,7 +57688,7 @@ async function buildDiagnostics(state, options, screenshots) {
56883
57688
  { name: "README.txt", data: README },
56884
57689
  { name: "summary.json", data: clean(JSON.stringify(summary, null, 2)) },
56885
57690
  { name: "log.txt", data: clean((state.log?.lines() ?? []).join("\n")) },
56886
- ...await exists3(sessionsFile) ? [{ name: "phone-sessions.jsonl", data: clean(await readFile4(sessionsFile, "utf8")) }] : [],
57691
+ ...await exists3(sessionsFile) ? [{ name: "phone-sessions.jsonl", data: clean(await readFile5(sessionsFile, "utf8")) }] : [],
56887
57692
  ...cases.map((testCase) => ({ name: `cases/${testCase.id}.json`, data: clean(JSON.stringify(plainCase(testCase), null, 2)) })),
56888
57693
  ...await runEntries(state, summaries.map((item) => item.id), clean),
56889
57694
  ...screenshots ? await screenshotEntries(state, cases) : []
@@ -56941,16 +57746,242 @@ function captureLog(limit = KEPT_LINES) {
56941
57746
  }
56942
57747
 
56943
57748
  // ../studio/src/server/routes.ts
56944
- import { copyFile as copyFile2, mkdir as mkdir9, readdir as readdir6, rm as rm5 } from "node:fs/promises";
56945
- import { dirname as dirname7, join as join11 } from "node:path";
57749
+ import { copyFile as copyFile3, mkdir as mkdir12, readdir as readdir8, rm as rm9 } from "node:fs/promises";
57750
+ import { dirname as dirname9, join as join13 } from "node:path";
57751
+
57752
+ // ../studio/src/shared/api-types.ts
57753
+ var MAX_WAIT_SECONDS = 120;
57754
+
57755
+ // ../studio/src/shared/block-check.ts
57756
+ var MAX_NAME = 80;
57757
+ var MAX_LINK = 2e3;
57758
+ var ONLY_A_REFERENCE = /^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/;
57759
+ var NO_NAME = "${}";
57760
+ function isOpenableLink(url) {
57761
+ return url.length <= MAX_LINK && /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/\S+$/.test(url) && !/^(file|content|javascript):/i.test(url);
57762
+ }
57763
+ function textProblem(text, missing) {
57764
+ if (!text.trim()) return `needs ${missing}`;
57765
+ if (text === NO_NAME) return "needs a name for its setting";
57766
+ if (/[\r\n]/.test(text)) return "must fit on one line";
57767
+ return void 0;
57768
+ }
57769
+ function selectorProblem(selector, missing, canBePoint) {
57770
+ if (selector.kind !== "point") return textProblem(selector.value, missing);
57771
+ return canBePoint ? void 0 : `needs ${missing}, and a point on the screen cannot be looked for`;
57772
+ }
57773
+ function stepProblem(step) {
57774
+ switch (step.kind) {
57775
+ case "tapOn":
57776
+ case "longPressOn":
57777
+ case "doubleTapOn":
57778
+ return selectorProblem(step.selector, "something to press", true);
57779
+ case "assertVisible":
57780
+ case "assertNotVisible":
57781
+ case "scrollUntilVisible":
57782
+ return selectorProblem(step.selector, "something to look for", false);
57783
+ case "inputText":
57784
+ return textProblem(step.text, "the text to type");
57785
+ case "openLink":
57786
+ if (textProblem(step.link, "a link")) return textProblem(step.link, "a link");
57787
+ return ONLY_A_REFERENCE.test(step.link) || isOpenableLink(step.link) ? void 0 : "needs a link such as https://example.com/claims or myapp://claims/123";
57788
+ case "wait":
57789
+ return Number.isInteger(step.seconds) && step.seconds >= 1 && step.seconds <= MAX_WAIT_SECONDS ? void 0 : `must wait 1 to ${MAX_WAIT_SECONDS} seconds`;
57790
+ case "eraseText":
57791
+ return step.chars === void 0 || Number.isInteger(step.chars) && step.chars >= 1 ? void 0 : "must erase at least 1 character";
57792
+ case "runFlow":
57793
+ return textProblem(step.file, "a flow file");
57794
+ default:
57795
+ return void 0;
57796
+ }
57797
+ }
57798
+ function blockProblem(name, steps) {
57799
+ if (!name.trim()) return "Give the block a name.";
57800
+ if (name.length > MAX_NAME || /[\r\n]/.test(name)) return `A block's name fits on one line of at most ${MAX_NAME} characters.`;
57801
+ if (steps.length === 0) return "Add at least one step.";
57802
+ for (const [position, step] of steps.entries()) {
57803
+ const problem = stepProblem(step);
57804
+ if (problem) return `Step ${position + 1} ${problem}.`;
57805
+ }
57806
+ return void 0;
57807
+ }
57808
+ function hideSavedValues(steps, env) {
57809
+ return steps.map((step) => {
57810
+ if (step.kind !== "inputText") return step;
57811
+ const text = normalizeValues({ text: step.text }, env)?.text;
57812
+ return text === void 0 ? step : { ...step, text };
57813
+ });
57814
+ }
57815
+
57816
+ // ../studio/src/server/blocks.ts
57817
+ import { readFile as readFile6, rm as rm6 } from "node:fs/promises";
57818
+ import { dirname as dirname7, resolve as resolve2 } from "node:path";
57819
+ var COMMENT = /^\s*#|\s#\s/m;
57820
+ async function readBlock(state, query) {
57821
+ const { file } = query ?? {};
57822
+ const block = (await state.project.listBlocks()).find((candidate) => candidate.file === file);
57823
+ if (!block) throw new HttpError(404, "UNKNOWN_BLOCK", `There is no block ${String(file)} in the project's block folders.`);
57824
+ return block;
57825
+ }
57826
+ function ownFlow(block) {
57827
+ const flow = block.outline?.ownFlow;
57828
+ if (!flow) {
57829
+ throw new HttpError(409, "BLOCK_NOT_EDITABLE", `${block.name} uses commands Ariadne does not read, so its file can only be changed by hand.`);
57830
+ }
57831
+ return flow;
57832
+ }
57833
+ async function blockDetail(state, block) {
57834
+ const flow = ownFlow(block);
57835
+ return {
57836
+ file: block.file,
57837
+ name: block.name,
57838
+ appId: flow.appId,
57839
+ steps: flow.steps,
57840
+ fixed: Object.keys(flow.env ?? {}),
57841
+ usedBy: await state.project.blockUsers(block.file),
57842
+ hasComments: COMMENT.test(await readFile6(state.project.blockPath(block.file), "utf8"))
57843
+ };
57844
+ }
57845
+ function readSave(body) {
57846
+ const { name, steps, appId } = body ?? {};
57847
+ if (typeof name !== "string" || !Array.isArray(steps) || appId !== void 0 && typeof appId !== "string") {
57848
+ throw new HttpError(400, "BAD_BLOCK", "Expected { name, steps } with the steps as a list.");
57849
+ }
57850
+ return { name: name.trim(), steps, ...appId !== void 0 ? { appId: appId.trim() } : {} };
57851
+ }
57852
+ async function checkedFlow(state, header, steps) {
57853
+ let flow;
57854
+ try {
57855
+ flow = parseMaestroFlow(toMaestroYaml({ ...header, steps }));
57856
+ } catch (error) {
57857
+ throw new HttpError(400, "BAD_BLOCK", `These steps cannot be saved as a block: ${error instanceof Error ? error.message : String(error)}`);
57858
+ }
57859
+ const problem = blockProblem(flow.name ?? "", flow.steps);
57860
+ if (problem) throw new HttpError(400, "BAD_BLOCK", problem);
57861
+ return { ...flow, steps: hideSavedValues(flow.steps, await projectEnv(state)) };
57862
+ }
57863
+ async function refuseOtherFlows(state, file, steps, existing) {
57864
+ const kept = new Set(existing.map((step) => JSON.stringify(step)));
57865
+ const blocks = (await state.project.listBlocks()).filter((block) => !block.error);
57866
+ const working = new Set(blocks.map((block) => state.project.blockPath(block.file)));
57867
+ const folder = dirname7(state.project.blockPath(file));
57868
+ for (const step of steps) {
57869
+ if (step.kind !== "runFlow" || kept.has(JSON.stringify(step)) || working.has(resolve2(folder, step.file))) continue;
57870
+ throw new HttpError(400, "BAD_BLOCK", `A block can only run another working block of this project, and ${step.file} is not one.`);
57871
+ }
57872
+ }
57873
+ async function refuseLoop(state, file, steps) {
57874
+ if (await state.project.reachesBlock(file, steps)) {
57875
+ throw new HttpError(400, "BAD_BLOCK", "A block cannot run itself, directly or through a block it runs. Remove the step that leads back to it.");
57876
+ }
57877
+ }
57878
+ function chainSteps(chain, paths) {
57879
+ return [...chain].reverse().flatMap((testCase, position) => caseToFlow(testCase, [], paths).steps.slice(position === 0 ? 0 : 1));
57880
+ }
57881
+ async function savedBlock(state, file) {
57882
+ return blockDetail(state, await readBlock(state, { file }));
57883
+ }
57884
+ function registerBlockRoutes(app, state) {
57885
+ app.get("/api/blocks/detail", async (request) => blockDetail(state, await readBlock(state, request.query)));
57886
+ app.post("/api/blocks", async (request) => {
57887
+ const { name, steps, appId } = readSave(request.body);
57888
+ if (!appId) throw new HttpError(400, "BAD_BLOCK", "Enter the app id under Device first: a block says which app it is for.");
57889
+ return enqueue(state, async () => {
57890
+ requireNoRecording(state);
57891
+ const flow = await checkedFlow(state, { appId, name }, steps);
57892
+ try {
57893
+ const file = await state.project.newBlockFile(name);
57894
+ await refuseOtherFlows(state, file, flow.steps, []);
57895
+ await refuseLoop(state, file, flow.steps);
57896
+ return await savedBlock(state, await state.project.writeBlock(flow, file));
57897
+ } catch (error) {
57898
+ if (error instanceof HttpError) throw error;
57899
+ throw new HttpError(409, "BAD_BLOCK", error instanceof Error ? error.message : String(error));
57900
+ }
57901
+ });
57902
+ });
57903
+ app.put("/api/blocks/detail", async (request) => {
57904
+ const { name, steps } = readSave(request.body);
57905
+ return enqueue(state, async () => {
57906
+ requireNoRecording(state);
57907
+ const block = await readBlock(state, request.query);
57908
+ const { appId, env, steps: existing } = ownFlow(block);
57909
+ const flow = await checkedFlow(state, { appId, name, ...env ? { env } : {} }, steps);
57910
+ await refuseOtherFlows(state, block.file, flow.steps, existing);
57911
+ await refuseLoop(state, block.file, flow.steps);
57912
+ await state.project.writeBlock(flow, block.file);
57913
+ return savedBlock(state, block.file);
57914
+ });
57915
+ });
57916
+ app.post("/api/blocks/record", async (request) => {
57917
+ const { name } = request.body ?? {};
57918
+ if (typeof name !== "string" || !name.trim()) throw new HttpError(400, "BAD_REQUEST", "Expected the name of the block.");
57919
+ requireSession(state);
57920
+ return enqueue(state, async () => {
57921
+ requireNoRecording(state);
57922
+ const id = await state.project.allocateCaseId("block");
57923
+ const screen = toScreenState(await captureScratch(state));
57924
+ const recorder = new Recorder();
57925
+ recorder.start({ id, name: name.trim(), appId: state.session.appId, deviceId: state.session.deviceId, envKeys: await state.project.envKeys() });
57926
+ state.recorder = recorder;
57927
+ state.editing = void 0;
57928
+ state.recordingBlock = true;
57929
+ return { caseId: id, screen };
57930
+ });
57931
+ });
57932
+ app.post("/api/blocks/record/finish", async (request) => {
57933
+ const picks = readPicks(request.body);
57934
+ const recorder = requireCase(state);
57935
+ if (!state.recordingBlock) throw new HttpError(409, "NOT_RECORDING_BLOCK", "This recording is a case. Finish it from the Record tab.");
57936
+ const { file } = request.body ?? {};
57937
+ const block = file === void 0 ? void 0 : await readBlock(state, { file });
57938
+ await stopPhoneCapture(state);
57939
+ return enqueue(state, async () => {
57940
+ requireRecording(state, recorder);
57941
+ for (const candidate of pickedCandidates(state, picks)) recorder.addExpectation({ selector: candidate.selector, node: candidate.node });
57942
+ const testCase = recorder.toCase();
57943
+ const steps = caseToFlow(testCase, [], state.project.blockFlowPaths(block?.file)).steps.slice(1);
57944
+ state.recorder = void 0;
57945
+ state.recordingBlock = void 0;
57946
+ state.offered = void 0;
57947
+ await rm6(state.project.evidenceDir(testCase.id), { recursive: true, force: true, maxRetries: 3 }).catch((error) => {
57948
+ console.error(`[studio] could not remove the screenshots in ${state.project.relative(state.project.evidenceDir(testCase.id))}: ${error instanceof Error ? error.message : String(error)}`);
57949
+ });
57950
+ return { steps };
57951
+ });
57952
+ });
57953
+ app.get("/api/cases/:caseId/block-draft", async (request) => {
57954
+ const caseId = readId(request.params, "caseId");
57955
+ const { name, appId } = await readSavedCase(state, caseId);
57956
+ let chain;
57957
+ try {
57958
+ chain = await state.project.startChain(caseId);
57959
+ } catch (error) {
57960
+ throw new HttpError(409, "BROKEN_CHAIN", error instanceof Error ? error.message : String(error));
57961
+ }
57962
+ return { name, appId, steps: chainSteps(chain, state.project.blockFlowPaths()) };
57963
+ });
57964
+ app.delete("/api/blocks/detail", async (request) => {
57965
+ return enqueue(state, async () => {
57966
+ requireNoRecording(state);
57967
+ const block = await readBlock(state, request.query);
57968
+ const users = await state.project.blockUsers(block.file);
57969
+ if (users.length > 0) {
57970
+ throw new HttpError(409, "BLOCK_IN_USE", `${block.name} is used by ${users.join(", ")}. Take it out of those first, or keep it.`);
57971
+ }
57972
+ await state.project.deleteBlock(block.file);
57973
+ return { ok: true };
57974
+ });
57975
+ });
57976
+ }
56946
57977
 
56947
57978
  // ../studio/src/server/cases.ts
56948
- import { readFile as readFile6, rm as rm4 } from "node:fs/promises";
57979
+ import { readFile as readFile8, rm as rm8 } from "node:fs/promises";
56949
57980
 
56950
57981
  // ../studio/src/server/replay.ts
56951
57982
  import { createReadStream as createReadStream2 } from "node:fs";
56952
- import { mkdir as mkdir6, readFile as readFile5, rename as rename2, rm as rm3, stat, writeFile as writeFile5 } from "node:fs/promises";
56953
- import { dirname as dirname6, join as join9, sep as sep2 } from "node:path";
57983
+ import { mkdir as mkdir8, readFile as readFile7, rename as rename4, rm as rm7, stat, writeFile as writeFile5 } from "node:fs/promises";
57984
+ import { dirname as dirname8, join as join10, sep as sep2 } from "node:path";
56954
57985
  async function addSimilarity(state, testCase, summary) {
56955
57986
  const items = caseFlowItems(testCase);
56956
57987
  const steps = [];
@@ -56962,7 +57993,7 @@ async function addSimilarity(state, testCase, summary) {
56962
57993
  continue;
56963
57994
  }
56964
57995
  try {
56965
- const [a, b] = await Promise.all([readFile5(state.project.path(recorded)), readFile5(state.project.path(result.replayScreenshot))]);
57996
+ const [a, b] = await Promise.all([readFile7(state.project.path(recorded)), readFile7(state.project.path(result.replayScreenshot))]);
56966
57997
  steps.push({ ...result, similarity: screenSimilarity(decodePng(a), decodePng(b)) });
56967
57998
  } catch {
56968
57999
  steps.push(result);
@@ -56971,10 +58002,10 @@ async function addSimilarity(state, testCase, summary) {
56971
58002
  return { ...summary, steps };
56972
58003
  }
56973
58004
  async function keepRunVideo(debugDir, flowDirName) {
56974
- const saved = join9(debugDir, flowDirName, "startRecording", `${RUN_VIDEO_NAME}.mp4`);
58005
+ const saved = join10(debugDir, flowDirName, "startRecording", `${RUN_VIDEO_NAME}.mp4`);
56975
58006
  if (!await exists3(saved)) return void 0;
56976
- const kept = join9(debugDir, "video.mp4");
56977
- await rename2(saved, kept);
58007
+ const kept = join10(debugDir, "video.mp4");
58008
+ await rename4(saved, kept);
56978
58009
  return kept;
56979
58010
  }
56980
58011
  async function frameVideo(state, testCase, summary, commands, device) {
@@ -57028,7 +58059,7 @@ async function runCase(state, caseId, options) {
57028
58059
  const runId = await project.allocateRunId(caseId);
57029
58060
  const debugDir = project.runDir(runId);
57030
58061
  const ranFlowPath = `${debugDir}${sep2}flow.yaml`;
57031
- await mkdir6(debugDir, { recursive: true });
58062
+ await mkdir8(debugDir, { recursive: true });
57032
58063
  const { runVideo } = await project.readSettings();
57033
58064
  await writeFile5(ranFlowPath, toMaestroYaml(instrumentForRun(flow, project.flowsDirFrom(debugDir), { video: runVideo === true })), "utf8");
57034
58065
  console.log(`[studio] replay ${caseId} on ${options.deviceId}: running ${project.relative(ranFlowPath)}`);
@@ -57053,10 +58084,10 @@ async function runCase(state, caseId, options) {
57053
58084
  const read = await readMaestroCommands(debugDir);
57054
58085
  const video = runVideo && read ? await keepRunVideo(debugDir, read.flowDirName) : void 0;
57055
58086
  const diagnostics = result.exitCode === 0 ? void 0 : [
57056
- await readFile5(result.outputFile, "utf8").catch(() => ""),
57057
- await readFile5(`${debugDir}${sep2}maestro.log`, "utf8").catch(() => ""),
58087
+ await readFile7(result.outputFile, "utf8").catch(() => ""),
58088
+ await readFile7(`${debugDir}${sep2}maestro.log`, "utf8").catch(() => ""),
57058
58089
  // Android drops a blocked launch silently: only the device log shows it.
57059
- read ? await readFile5(join9(debugDir, read.flowDirName, "logs", "device-logcat.txt"), "utf8").catch(() => "") : ""
58090
+ read ? await readFile7(join10(debugDir, read.flowDirName, "logs", "device-logcat.txt"), "utf8").catch(() => "") : ""
57060
58091
  ].join("\n");
57061
58092
  let summary = summarizeRun({
57062
58093
  runId,
@@ -57103,13 +58134,13 @@ async function runPrefixWithMaestro(state, prefix) {
57103
58134
  if (prefix.partial.startFrom) await writeChainFlows(state, prefix.partial.startFrom);
57104
58135
  const env = await projectEnv(state);
57105
58136
  const flowPath = project.partialFlowPath(prefix.caseId);
57106
- const dir = dirname6(flowPath);
57107
- const debugDir = join9(dir, `${prefix.caseId}-maestro`);
58137
+ const dir = dirname8(flowPath);
58138
+ const debugDir = join10(dir, `${prefix.caseId}-maestro`);
57108
58139
  const written = await caseFlow(state, prefix.partial);
57109
58140
  const skipped = prefix.skipLaunch ? 1 : 0;
57110
58141
  const flow = relocateFlow({ ...written, steps: written.steps.slice(skipped) }, project.flowsDirFrom(dir));
57111
- await rm3(debugDir, { recursive: true, force: true });
57112
- await mkdir6(dir, { recursive: true });
58142
+ await rm7(debugDir, { recursive: true, force: true });
58143
+ await mkdir8(dir, { recursive: true });
57113
58144
  await writeFile5(flowPath, toMaestroYaml(flow), "utf8");
57114
58145
  console.log(`[studio] ${prefix.what} on ${deviceId}: running ${project.relative(flowPath)} with Maestro`);
57115
58146
  let result;
@@ -57131,7 +58162,7 @@ async function runPrefixWithMaestro(state, prefix) {
57131
58162
  } catch (error) {
57132
58163
  throw new HttpError(500, "MAESTRO_FAILED", `Could not run Maestro: ${error instanceof Error ? error.message : String(error)}`);
57133
58164
  } finally {
57134
- await rm3(flowPath, { force: true });
58165
+ await rm7(flowPath, { force: true });
57135
58166
  }
57136
58167
  const read = await readMaestroCommands(debugDir);
57137
58168
  const summary = summarizeRun({
@@ -57149,7 +58180,7 @@ async function runPrefixWithMaestro(state, prefix) {
57149
58180
  });
57150
58181
  await redactRunFiles(debugDir, env).catch(() => void 0);
57151
58182
  if (summary.passed) {
57152
- await rm3(debugDir, { recursive: true, force: true });
58183
+ await rm7(debugDir, { recursive: true, force: true });
57153
58184
  console.log(`[studio] ${prefix.what}: Maestro passed after ${Math.round(result.durationMs / 1e3)} s`);
57154
58185
  return;
57155
58186
  }
@@ -57214,11 +58245,30 @@ async function failedStepText(state, testCase, summary) {
57214
58245
  const blocks = await blockNames(state);
57215
58246
  return describeFlowItem(item, testCase.appId, { caseName: (id) => names.get(id), blockName: (file) => blocks.get(file) });
57216
58247
  }
58248
+ async function readTestedOn2(state, suite) {
58249
+ const appIds = [];
58250
+ for (const item of suite.cases) {
58251
+ if (item.outcome === "muted") continue;
58252
+ const testCase = await state.project.readCase(item.caseId).catch(() => void 0);
58253
+ if (testCase) appIds.push(testCase.appId);
58254
+ }
58255
+ try {
58256
+ return await state.phoneTools.testedOn(suite.deviceId, [...new Set(appIds)]);
58257
+ } catch (error) {
58258
+ console.error(`[studio] could not read the phone and app versions for ${suite.suiteId}: ${error instanceof Error ? error.message : String(error)}`);
58259
+ return void 0;
58260
+ }
58261
+ }
57217
58262
  async function runSuite(state, job) {
57218
58263
  const { project } = state;
57219
58264
  const suite = job.run;
57220
58265
  const save = () => project.saveSuite(suite).catch((error) => console.error(`[studio] could not save ${suite.suiteId}:`, error));
57221
58266
  await save();
58267
+ const testedOn = await readTestedOn2(state, suite);
58268
+ if (testedOn) {
58269
+ suite.testedOn = testedOn;
58270
+ await save();
58271
+ }
57222
58272
  try {
57223
58273
  await withSessionHandedToMaestro(state, async () => {
57224
58274
  for (const item of suite.cases) {
@@ -57275,7 +58325,7 @@ async function runSuite(state, job) {
57275
58325
  }
57276
58326
  async function imageDataUrl(state, projectRelative, maxWidth = 540) {
57277
58327
  try {
57278
- const bytes = await readFile5(state.project.path(projectRelative));
58328
+ const bytes = await readFile7(state.project.path(projectRelative));
57279
58329
  let png = bytes;
57280
58330
  try {
57281
58331
  const image = decodePng(bytes);
@@ -57289,10 +58339,10 @@ async function imageDataUrl(state, projectRelative, maxWidth = 540) {
57289
58339
  return void 0;
57290
58340
  }
57291
58341
  }
57292
- async function imageMap(state, paths) {
58342
+ async function imageMap(state, paths, maxWidth) {
57293
58343
  const map = /* @__PURE__ */ new Map();
57294
58344
  for (const path of new Set(paths.filter((item) => Boolean(item)))) {
57295
- const url = await imageDataUrl(state, path);
58345
+ const url = await imageDataUrl(state, path, maxWidth);
57296
58346
  if (url) map.set(path, url);
57297
58347
  }
57298
58348
  return (path) => map.get(path);
@@ -57477,7 +58527,7 @@ function optionalText(body, key, maxLength) {
57477
58527
  }
57478
58528
  async function caseDetail(state, caseId) {
57479
58529
  const testCase = await readSavedCase(state, caseId);
57480
- const yaml = await readFile6(state.project.flowPath(caseId), "utf8").catch(() => "");
58530
+ const yaml = await readFile8(state.project.flowPath(caseId), "utf8").catch(() => "");
57481
58531
  const summaries = await state.project.listRuns(caseId);
57482
58532
  const all = await state.project.listCases();
57483
58533
  const ref = (id) => ({ id, name: all.find((item) => item.id === id)?.name ?? id });
@@ -57520,6 +58570,7 @@ function registerCaseRoutes(app, state) {
57520
58570
  refuseIfRecording(state, caseId);
57521
58571
  const testCase = await readSavedCase(state, caseId);
57522
58572
  const name = optionalText(request.body, "name", 200);
58573
+ const reference2 = optionalText(request.body, "reference", 100);
57523
58574
  const muted = request.body?.muted;
57524
58575
  if (muted !== void 0 && typeof muted !== "boolean") throw new HttpError(400, "BAD_REQUEST", 'Expected "muted" to be true or false.');
57525
58576
  if (name !== void 0) {
@@ -57528,6 +58579,8 @@ function registerCaseRoutes(app, state) {
57528
58579
  }
57529
58580
  if (muted === true) testCase.muted = true;
57530
58581
  if (muted === false) delete testCase.muted;
58582
+ if (reference2) testCase.reference = reference2;
58583
+ else if (reference2 !== void 0) delete testCase.reference;
57531
58584
  await saveEdited(state, testCase, false);
57532
58585
  return caseDetail(state, caseId);
57533
58586
  });
@@ -57560,7 +58613,7 @@ function registerCaseRoutes(app, state) {
57560
58613
  const [removed] = testCase.steps.splice(index - 1, 1);
57561
58614
  testCase.steps = renumber(testCase.steps);
57562
58615
  if (removed?.screenshot && !testCase.steps.some((step) => step.screenshot === removed.screenshot)) {
57563
- await rm4(state.project.path(removed.screenshot), { force: true });
58616
+ await rm8(state.project.path(removed.screenshot), { force: true });
57564
58617
  }
57565
58618
  await saveEdited(state, testCase, true);
57566
58619
  return caseDetail(state, caseId);
@@ -57661,6 +58714,7 @@ function registerCaseRoutes(app, state) {
57661
58714
  const partial = { ...testCase, steps: testCase.steps.slice(0, kept), expectations: [] };
57662
58715
  const withMaestro = await replayNeedsMaestro(state, partial);
57663
58716
  return enqueue(state, async () => {
58717
+ requireNoRecording(state);
57664
58718
  const { project, session } = state;
57665
58719
  const envKeys = await project.envKeys();
57666
58720
  if (withMaestro) {
@@ -57678,7 +58732,7 @@ function registerCaseRoutes(app, state) {
57678
58732
  const described = describeDeviceError(error);
57679
58733
  throw new HttpError(502, described.code, `Running the case up to step ${kept} failed: ${described.message}`);
57680
58734
  } finally {
57681
- await rm4(partialPath, { force: true });
58735
+ await rm8(partialPath, { force: true });
57682
58736
  }
57683
58737
  }
57684
58738
  const screen = toScreenState(await captureScratch(state));
@@ -57702,7 +58756,8 @@ function registerCaseRoutes(app, state) {
57702
58756
  expectations: mode === "insert" ? testCase.expectations : [],
57703
58757
  evidenceFloor: floor,
57704
58758
  editedAt: nowIso(),
57705
- ...testCase.muted ? { muted: true } : {}
58759
+ ...testCase.muted ? { muted: true } : {},
58760
+ ...testCase.reference ? { reference: testCase.reference } : {}
57706
58761
  });
57707
58762
  state.recorder = recorder;
57708
58763
  state.editing = {
@@ -57718,9 +58773,116 @@ function registerCaseRoutes(app, state) {
57718
58773
  });
57719
58774
  }
57720
58775
 
58776
+ // ../studio/src/server/evidence.ts
58777
+ import { createHash as createHash2 } from "node:crypto";
58778
+ import { mkdir as mkdir10, readFile as readFile9, writeFile as writeFile7 } from "node:fs/promises";
58779
+ import { join as join11, relative as relative2, sep as sep3 } from "node:path";
58780
+ var SCREEN_WIDTH = 300;
58781
+ var MAX_LINE = 200;
58782
+ var MAX_ROLE = 40;
58783
+ var MAX_NOTES = 1e3;
58784
+ function readText(query, key, maxLength, multiline = false) {
58785
+ const value = query?.[key];
58786
+ if (value === void 0) return void 0;
58787
+ if (typeof value !== "string") throw new HttpError(400, "BAD_REQUEST", `${key} must be text.`);
58788
+ const unified = value.replace(/\r\n?/g, "\n");
58789
+ const text = (multiline ? unified.replace(/[\u0000-\u0009\u000b-\u001f\u007f]+/g, " ") : unified.replace(/[\u0000-\u001f\u007f]+/g, " ")).trim();
58790
+ if (text.length > maxLength) throw new HttpError(400, "BAD_REQUEST", `${key} must be at most ${maxLength} characters.`);
58791
+ return text || void 0;
58792
+ }
58793
+ function readScreens(query) {
58794
+ const value = query?.screens;
58795
+ if (value === void 0 || value === "all") return "all";
58796
+ if (value === "checks") return "checks";
58797
+ throw new HttpError(400, "BAD_REQUEST", 'screens must be "all" or "checks".');
58798
+ }
58799
+ function recordOf(state, file, bytes) {
58800
+ return { path: relative2(state.project.dir, file).split(sep3).join("/"), sha256: createHash2("sha256").update(bytes).digest("hex") };
58801
+ }
58802
+ function registerEvidenceRoutes(app, state) {
58803
+ app.get("/api/suites/:suiteId/evidence", async (request, reply) => {
58804
+ const suiteId = readId(request.params, "suiteId");
58805
+ const release2 = readText(request.query, "release", MAX_LINE);
58806
+ const testedBy = readText(request.query, "testedBy", MAX_LINE);
58807
+ const signoffRole = readText(request.query, "signoffRole", MAX_ROLE);
58808
+ const knownIssues = readText(request.query, "knownIssues", MAX_NOTES, true);
58809
+ const conclusion = readText(request.query, "conclusion", MAX_LINE);
58810
+ const screens = readScreens(request.query);
58811
+ if (state.suite?.run.suiteId === suiteId && state.suite.run.status === "running") {
58812
+ throw new HttpError(409, "SUITE_RUNNING", "Run all is still running. Make the evidence pack when it ends.");
58813
+ }
58814
+ const suiteFile = state.project.suiteFile(suiteId);
58815
+ const suiteBytes = await readFile9(suiteFile).catch(() => void 0);
58816
+ let suite;
58817
+ try {
58818
+ suite = suiteBytes ? JSON.parse(suiteBytes.toString("utf8")) : void 0;
58819
+ } catch {
58820
+ suite = void 0;
58821
+ }
58822
+ if (!suiteBytes || !suite) throw new HttpError(404, "NOT_FOUND", `No suite run ${suiteId}.`);
58823
+ const names = await reportNames(state);
58824
+ const cases = [];
58825
+ for (const item of suite.cases) {
58826
+ const testCase = await state.project.readCase(item.caseId).catch(() => void 0);
58827
+ const lastRunId = item.runIds.at(-1);
58828
+ const run2 = lastRunId ? await state.project.readRun(lastRunId).catch(() => void 0) : void 0;
58829
+ const summaryFile = lastRunId ? join11(state.project.runDir(lastRunId), "summary.json") : void 0;
58830
+ const summaryBytes = run2 && summaryFile ? await readFile9(summaryFile).catch(() => void 0) : void 0;
58831
+ const error = item.error ?? (run2 && !run2.passed ? run2.error : void 0);
58832
+ cases.push({
58833
+ caseId: item.caseId,
58834
+ // The name it had when it ran, which is what the summary of that Run all says too.
58835
+ name: item.name,
58836
+ outcome: item.outcome,
58837
+ attempts: item.runIds.length,
58838
+ ...testCase ? { appId: testCase.appId } : {},
58839
+ ...testCase?.reference ? { reference: testCase.reference } : {},
58840
+ ...run2 ? {
58841
+ run: {
58842
+ runId: run2.runId,
58843
+ startedAt: run2.startedAt,
58844
+ passed: run2.passed,
58845
+ ...run2.error ? { error: run2.error } : {},
58846
+ ...!run2.passed && run2.failureScreenshot ? { failureScreenshot: run2.failureScreenshot } : {}
58847
+ }
58848
+ } : {},
58849
+ ...summaryFile && summaryBytes ? { record: recordOf(state, summaryFile, summaryBytes) } : {},
58850
+ steps: testCase && run2 ? evidenceSteps(testCase, run2, names) : [],
58851
+ ...item.failedStep ? { failedStep: item.failedStep } : {},
58852
+ ...error ? { error } : {},
58853
+ ...item.note ? { note: item.note } : {},
58854
+ ...testCase?.editedAt && run2 && run2.startedAt < testCase.editedAt ? { stale: true } : {},
58855
+ ...testCase ? {} : { missing: true }
58856
+ });
58857
+ }
58858
+ const image = await imageMap(
58859
+ state,
58860
+ cases.flatMap((item) => [...item.steps.map((step) => evidenceScreen(step, screens, item.run?.failureScreenshot)), item.run?.failureScreenshot]),
58861
+ SCREEN_WIDTH
58862
+ );
58863
+ const html = evidenceHtml({
58864
+ suite,
58865
+ cases,
58866
+ image,
58867
+ screens,
58868
+ record: recordOf(state, suiteFile, suiteBytes),
58869
+ ...release2 ? { release: release2 } : {},
58870
+ ...testedBy ? { testedBy } : {},
58871
+ ...signoffRole ? { signoffRole } : {},
58872
+ ...knownIssues ? { knownIssues } : {},
58873
+ ...conclusion ? { conclusion } : {}
58874
+ });
58875
+ const dir = state.project.exportsDir();
58876
+ await mkdir10(dir, { recursive: true });
58877
+ await writeFile7(join11(dir, `evidence-${suiteId}.html`), html, "utf8");
58878
+ console.log(`[studio] wrote exports/evidence-${suiteId}.html`);
58879
+ return reply.type("text/html; charset=utf-8").header("cache-control", "no-store").send(html);
58880
+ });
58881
+ }
58882
+
57721
58883
  // ../studio/src/server/exports.ts
57722
- import { mkdir as mkdir8, readFile as readFile7, writeFile as writeFile7 } from "node:fs/promises";
57723
- import { join as join10 } from "node:path";
58884
+ import { mkdir as mkdir11, readFile as readFile10, writeFile as writeFile8 } from "node:fs/promises";
58885
+ import { join as join12 } from "node:path";
57724
58886
  var CONTENT_TYPE = {
57725
58887
  md: "text/markdown; charset=utf-8",
57726
58888
  html: "text/html; charset=utf-8",
@@ -57747,7 +58909,7 @@ async function screenshotDataUrls(state, docs) {
57747
58909
  for (const step of doc.steps) {
57748
58910
  if (!step.screenshot || map.has(step.screenshot)) continue;
57749
58911
  try {
57750
- const bytes = await readFile7(state.project.path(step.screenshot));
58912
+ const bytes = await readFile10(state.project.path(step.screenshot));
57751
58913
  map.set(step.screenshot, `data:image/png;base64,${bytes.toString("base64")}`);
57752
58914
  } catch {
57753
58915
  }
@@ -57767,8 +58929,8 @@ async function render(state, docs, format3, title) {
57767
58929
  }
57768
58930
  async function sendExport(state, reply, fileName, format3, content) {
57769
58931
  const dir = state.project.exportsDir();
57770
- await mkdir8(dir, { recursive: true });
57771
- await writeFile7(join10(dir, fileName), content);
58932
+ await mkdir11(dir, { recursive: true });
58933
+ await writeFile8(join12(dir, fileName), content);
57772
58934
  console.log(`[studio] wrote exports/${fileName}`);
57773
58935
  return reply.type(CONTENT_TYPE[format3]).header("content-disposition", `attachment; filename="${fileName}"`).header("cache-control", "no-store").send(content);
57774
58936
  }
@@ -57795,8 +58957,8 @@ function registerExportRoutes(app, state) {
57795
58957
 
57796
58958
  // ../studio/src/server/files.ts
57797
58959
  import { createReadStream as createReadStream3 } from "node:fs";
57798
- import { open, readdir as readdir5, realpath as realpath2, stat as stat2 } from "node:fs/promises";
57799
- import { basename as basename2, extname, posix as posix4, relative as relative2, resolve as resolve2, sep as sep3 } from "node:path";
58960
+ import { open, readdir as readdir7, realpath as realpath2, stat as stat2 } from "node:fs/promises";
58961
+ import { basename as basename2, extname, posix as posix4, relative as relative3, resolve as resolve3, sep as sep4 } from "node:path";
57800
58962
  var MAX_TEXT_BYTES = 512 * 1024;
57801
58963
  var HIDDEN_NAMES = /* @__PURE__ */ new Set([".git", "node_modules"]);
57802
58964
  var IMAGE_TYPES = {
@@ -57811,7 +58973,7 @@ function isHidden(name) {
57811
58973
  return lower.startsWith(".env") || HIDDEN_NAMES.has(lower);
57812
58974
  }
57813
58975
  function isInside2(path, dir) {
57814
- return path === dir || path.startsWith(dir + sep3);
58976
+ return path === dir || path.startsWith(dir + sep4);
57815
58977
  }
57816
58978
  async function rootFolders(state) {
57817
58979
  const { dir } = state.project;
@@ -57826,10 +58988,10 @@ function readPath(query) {
57826
58988
  return path;
57827
58989
  }
57828
58990
  async function resolveEntry(state, path) {
57829
- const real = await realpath2(resolve2(state.project.dir, path)).catch(() => void 0);
58991
+ const real = await realpath2(resolve3(state.project.dir, path)).catch(() => void 0);
57830
58992
  const roots = await Promise.all((await rootFolders(state)).map((folder) => realpath2(folder)));
57831
58993
  const root = real === void 0 ? void 0 : roots.find((folder) => isInside2(real, folder));
57832
- if (real === void 0 || root === void 0 || relative2(root, real).split(sep3).some(isHidden)) {
58994
+ if (real === void 0 || root === void 0 || relative3(root, real).split(sep4).some(isHidden)) {
57833
58995
  throw new HttpError(404, "NOT_FOUND", "That is not a file in this project.");
57834
58996
  }
57835
58997
  return real;
@@ -57862,7 +59024,7 @@ function registerFileRoutes(app, state) {
57862
59024
  const path = readPath(request.query);
57863
59025
  const folder = await resolveEntry(state, path);
57864
59026
  if (!(await stat2(folder)).isDirectory()) throw new HttpError(400, "BAD_REQUEST", "That is a file, not a folder.");
57865
- const items = await readdir5(folder, { withFileTypes: true });
59027
+ const items = await readdir7(folder, { withFileTypes: true });
57866
59028
  const entries = items.filter((item) => (item.isDirectory() || item.isFile()) && !isHidden(item.name)).map((item) => ({ path: posix4.join(path, item.name), name: item.name, kind: item.isDirectory() ? "folder" : "file" })).sort(byKindThenName);
57867
59029
  return { entries };
57868
59030
  });
@@ -57896,6 +59058,13 @@ function readPoint(body) {
57896
59058
  }
57897
59059
  return { x, y };
57898
59060
  }
59061
+ function readSeconds(body) {
59062
+ const { seconds } = body ?? {};
59063
+ if (typeof seconds !== "number" || !Number.isInteger(seconds) || seconds < 1 || seconds > MAX_WAIT_SECONDS) {
59064
+ throw new HttpError(400, "BAD_REQUEST", `A wait is 1 to ${MAX_WAIT_SECONDS} seconds.`);
59065
+ }
59066
+ return seconds;
59067
+ }
57899
59068
  function readString(body, key) {
57900
59069
  const value = body ?? {};
57901
59070
  if (typeof value[key] !== "string" || !value[key].trim()) {
@@ -57972,20 +59141,12 @@ async function replayCaseNow(state, testCase, steps, env) {
57972
59141
  const described = describeDeviceError(error);
57973
59142
  throw new HttpError(502, described.code, redactValues(`The case ${testCase.id} failed: ${described.message}`, env));
57974
59143
  } finally {
57975
- await rm5(scratchPath, { force: true });
59144
+ await rm9(scratchPath, { force: true });
57976
59145
  }
57977
59146
  }
57978
- function readPicks(body) {
57979
- const picks = (body ?? {}).picks;
57980
- if (picks === void 0) return [];
57981
- if (!Array.isArray(picks) || !picks.every((pick) => Number.isInteger(pick) && pick >= 0)) {
57982
- throw new HttpError(400, "BAD_PICK", 'Expected "picks" to be a list of candidate ids.');
57983
- }
57984
- return [...new Set(picks)];
57985
- }
57986
59147
  function readLink(body) {
57987
59148
  const url = readString(body, "url").trim();
57988
- if (url.length > 2e3 || !/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/\S+$/.test(url) || /^(file|content|javascript):/i.test(url)) {
59149
+ if (!isOpenableLink(url)) {
57989
59150
  throw new HttpError(400, "BAD_LINK", "Expected a link such as https://example.com/claims or myapp://claims/123.");
57990
59151
  }
57991
59152
  return url;
@@ -58030,6 +59191,7 @@ async function projectInfo(state) {
58030
59191
  appId: settings.appId,
58031
59192
  platform: settings.platform,
58032
59193
  runVideo: settings.runVideo === true,
59194
+ evidencePack: settings.evidencePack === true,
58033
59195
  videoHasSteps: state.maestro.findFfmpeg() !== void 0,
58034
59196
  ...settings.lastDeviceId ? { lastDeviceId: settings.lastDeviceId } : {},
58035
59197
  blocks: blocks.map((block) => {
@@ -58051,7 +59213,8 @@ async function projectInfo(state) {
58051
59213
  ...clears ? { clearsAppData: true } : {},
58052
59214
  ...block.runsWith ? { runsWith: block.runsWith } : {},
58053
59215
  ...outline && outline.maestroOnly.length > 0 ? { maestroOnly: outline.maestroOnly } : {},
58054
- ...block.error ? { error: block.error } : {}
59216
+ ...block.error ? { error: block.error } : {},
59217
+ ...outline?.ownFlow ? { editable: true } : {}
58055
59218
  };
58056
59219
  }),
58057
59220
  envKeys: envKeyList,
@@ -58061,14 +59224,33 @@ async function projectInfo(state) {
58061
59224
  }
58062
59225
  async function freshLatest(state) {
58063
59226
  let latest = state.latest;
59227
+ if (latest?.hidden) return latest;
58064
59228
  if (!latest || Date.now() - Date.parse(latest.takenAt) > STALE_SNAPSHOT_MS2) {
58065
59229
  const fresh = await state.session.snapshot();
59230
+ if (!hasAppElements(fresh.nodes) || await phoneLocked(state)) return captureScratch(state);
58066
59231
  latest = { ...fresh, screenshotPath: latest?.screenshotPath ?? scratchScreenshot(state) };
58067
59232
  state.latest = latest;
58068
59233
  }
58069
59234
  return latest;
58070
59235
  }
59236
+ async function refuseHiddenScreen(state) {
59237
+ if (!state.latest?.hidden && !await phoneLocked(state)) return;
59238
+ const now = await captureScratch(state);
59239
+ throw new HttpError(
59240
+ 409,
59241
+ "SCREEN_HIDDEN",
59242
+ now.hidden ? "The phone's screen is off or locked, so nothing was pressed or recorded. Wake it and unlock it, then try again." : "The phone's screen is back. Nothing was pressed or recorded: have a look at it here, then try again."
59243
+ );
59244
+ }
59245
+ async function typeUnsavedPassword(state, text) {
59246
+ try {
59247
+ await state.session.typeText(text);
59248
+ } catch {
59249
+ throw new HttpError(502, "TYPE_FAILED", "Could not type into the password field. Check the phone, then try again.");
59250
+ }
59251
+ }
58071
59252
  async function recordedAction(state, act, record, kind, text) {
59253
+ if (kind !== "pressKey") await refuseHiddenScreen(state);
58072
59254
  await act();
58073
59255
  let screenshotPath = scratchScreenshot(state);
58074
59256
  let step;
@@ -58119,6 +59301,12 @@ function registerRoutes(app, state) {
58119
59301
  await state.project.updateSettings({ runVideo: on });
58120
59302
  return projectInfo(state);
58121
59303
  });
59304
+ app.put("/api/project/evidence-pack", async (request) => {
59305
+ const on = request.body?.on;
59306
+ if (typeof on !== "boolean") throw new HttpError(400, "BAD_REQUEST", "Say whether to offer the evidence pack.");
59307
+ await state.project.updateSettings({ evidencePack: on });
59308
+ return projectInfo(state);
59309
+ });
58122
59310
  app.get("/api/devices", async () => state.session.listDevices());
58123
59311
  app.get("/api/session", async () => ({
58124
59312
  open: state.session.isOpen,
@@ -58163,6 +59351,17 @@ function registerRoutes(app, state) {
58163
59351
  requireSession(state);
58164
59352
  return enqueue(state, async () => toScreenState(await captureScratch(state)));
58165
59353
  });
59354
+ app.post("/api/screen/pick", async (request) => {
59355
+ const point = readPoint(request.body);
59356
+ requireSession(state);
59357
+ return enqueue(state, async () => {
59358
+ await refuseHiddenScreen(state);
59359
+ const latest = await freshLatest(state);
59360
+ const node = resolveTapTarget(latest.nodes, point, latest.viewport);
59361
+ const selector = chooseSelector(node, latest.nodes, point, latest.viewport);
59362
+ return { selector, password: node?.password === true, ...node ? { resolved: toLiteNode(node) } : {} };
59363
+ });
59364
+ });
58166
59365
  app.get("/api/screen/image", async (_request, reply) => {
58167
59366
  if (!state.latest || !await exists3(state.latest.screenshotPath)) {
58168
59367
  throw new HttpError(404, "NO_SCREEN", "Nothing has been captured yet.");
@@ -58193,22 +59392,26 @@ function registerRoutes(app, state) {
58193
59392
  return enqueue(state, async () => {
58194
59393
  const recorder = state.recorder;
58195
59394
  let recordedText = text;
59395
+ let unsaved = false;
58196
59396
  if (recorder?.active) {
58197
59397
  const env = await projectEnv(state);
58198
59398
  const key = Object.keys(env).sort().find((candidate) => env[candidate] === text);
58199
59399
  if (key) {
58200
59400
  recordedText = `\${${key}}`;
58201
59401
  } else if (state.latest && focusedPasswordField(state.latest.nodes)) {
58202
- throw new HttpError(
59402
+ if (state.recordingBlock) {
59403
+ recordedText = reference(passwordSetting(Object.keys(env)));
59404
+ unsaved = true;
59405
+ } else throw new HttpError(
58203
59406
  400,
58204
59407
  "PASSWORD_FIELD",
58205
- `This is a password field. Put the password in ${join11(state.project.dir, ".env")} (for example PASSWORD=...) and type exactly that value; the case then records \${PASSWORD} instead of the secret.`
59408
+ `This is a password field. Put the password in ${join13(state.project.dir, ".env")} (for example PASSWORD=...) and type exactly that value; the case then records \${PASSWORD} instead of the secret.`
58206
59409
  );
58207
59410
  }
58208
59411
  }
58209
59412
  return recordedAction(
58210
59413
  state,
58211
- () => state.session.typeText(text),
59414
+ () => unsaved ? typeUnsavedPassword(state, text) : state.session.typeText(text),
58212
59415
  (active, screenshot) => active.recordInput(recordedText, screenshot),
58213
59416
  "type",
58214
59417
  recordedText
@@ -58359,6 +59562,7 @@ function registerRoutes(app, state) {
58359
59562
  const block = await prepareBlockUse(state, requested, env);
58360
59563
  const { use } = block;
58361
59564
  return enqueue(state, async () => {
59565
+ await refuseHiddenScreen(state);
58362
59566
  if (block.runsWithMaestro) {
58363
59567
  const testCase = state.recorder.toCase();
58364
59568
  const step = { index: 1, kind: "block", file: use.file, ...use.values ? { values: use.values } : {}, at: nowIso() };
@@ -58379,6 +59583,30 @@ function registerRoutes(app, state) {
58379
59583
  );
58380
59584
  });
58381
59585
  });
59586
+ app.post("/api/blocks/try", async (request) => {
59587
+ const requested = readBlockUse(request.body);
59588
+ requireSession(state);
59589
+ const env = await projectEnv(state);
59590
+ const block = await prepareBlockUse(state, requested, env);
59591
+ const { use } = block;
59592
+ return enqueue(state, async () => {
59593
+ requireNoRecording(state);
59594
+ await refuseHiddenScreen(state);
59595
+ if (block.runsWithMaestro) {
59596
+ const step = { index: 1, kind: "block", file: use.file, ...use.values ? { values: use.values } : {}, at: nowIso() };
59597
+ const appId = state.session.appId ?? (await state.project.readSettings()).appId;
59598
+ await runPrefixWithMaestro(state, {
59599
+ caseId: "try-block",
59600
+ partial: { id: "try-block", name: use.file, appId, deviceId: state.session.deviceId ?? "", createdAt: nowIso(), preconditionBlocks: [], steps: [step], expectations: [] },
59601
+ skipLaunch: true,
59602
+ what: `Block ${use.file}`
59603
+ });
59604
+ } else {
59605
+ await runBlockNow(state, block, env, `Block ${use.file}`);
59606
+ }
59607
+ return toScreenState(await captureScratch(state));
59608
+ });
59609
+ });
58382
59610
  app.post("/api/project/test-users", async (request) => {
58383
59611
  const { name, values } = request.body ?? {};
58384
59612
  if (typeof name !== "string" || !isValues(values) || Object.keys(values).length === 0) {
@@ -58399,6 +59627,18 @@ function registerRoutes(app, state) {
58399
59627
  }
58400
59628
  return projectInfo(state);
58401
59629
  });
59630
+ app.post("/api/project/saved-values", async (request) => {
59631
+ const { values } = request.body ?? {};
59632
+ if (!isValues(values) || Object.keys(values).length === 0 || Object.values(values).some((value) => !value)) {
59633
+ throw new HttpError(400, "BAD_SAVED_VALUE", 'Expected { values } with a value for each setting, such as { "EMAIL": "..." }.');
59634
+ }
59635
+ try {
59636
+ await state.project.addEnvValues(values);
59637
+ } catch (error) {
59638
+ throw new HttpError(409, "BAD_SAVED_VALUE", error instanceof Error ? error.message : String(error));
59639
+ }
59640
+ return projectInfo(state);
59641
+ });
58402
59642
  app.post("/api/case/start", async (request) => {
58403
59643
  const name = readString(request.body, "name").trim();
58404
59644
  const blocks = readBlocks(request.body);
@@ -58426,6 +59666,7 @@ function registerRoutes(app, state) {
58426
59666
  const preconditionBlocks = prepared.map((block) => block.use);
58427
59667
  const withMaestro = await replayNeedsMaestro(state, { preconditionBlocks, steps: [], ...startFrom ? { startFrom } : {} });
58428
59668
  return enqueue(state, async () => {
59669
+ requireNoRecording(state);
58429
59670
  const deviceId = state.session.deviceId;
58430
59671
  const appId = state.session.appId;
58431
59672
  const caseId = await state.project.allocateCaseId(name);
@@ -58465,6 +59706,15 @@ function registerRoutes(app, state) {
58465
59706
  return { caseId, screen };
58466
59707
  });
58467
59708
  });
59709
+ app.post("/api/case/wait", async (request) => {
59710
+ const seconds = readSeconds(request.body);
59711
+ requireSession(state);
59712
+ requireCase(state);
59713
+ return enqueue(
59714
+ state,
59715
+ () => recordedAction(state, async () => void 0, (recorder, screenshot) => recorder.recordWait(seconds, screenshot), "wait")
59716
+ );
59717
+ });
58468
59718
  app.post("/api/case/expect", async (request) => {
58469
59719
  const point = readPoint(request.body);
58470
59720
  requireSession(state);
@@ -58478,8 +59728,8 @@ function registerRoutes(app, state) {
58478
59728
  const evidence = state.project.evidencePath(recorder.caseId, recorder.evidenceNumberForNext("expect"));
58479
59729
  let screenshot;
58480
59730
  if (await exists3(latest.screenshotPath)) {
58481
- await mkdir9(dirname7(evidence.absolute), { recursive: true });
58482
- await copyFile2(latest.screenshotPath, evidence.absolute);
59731
+ await mkdir12(dirname9(evidence.absolute), { recursive: true });
59732
+ await copyFile3(latest.screenshotPath, evidence.absolute);
58483
59733
  screenshot = evidence.relative;
58484
59734
  }
58485
59735
  const step = recorder.recordExpect(node, point, latest.viewport, screenshot, latest.nodes);
@@ -58491,6 +59741,7 @@ function registerRoutes(app, state) {
58491
59741
  requireSession(state);
58492
59742
  return enqueue(state, async () => {
58493
59743
  const latest = await freshLatest(state);
59744
+ state.offered = latest;
58494
59745
  const candidates = expectationCandidates(latest.nodes, latest.viewport);
58495
59746
  const previous = state.editing?.mode === "rerecord" ? state.editing.originalExpectations : [];
58496
59747
  const preselected = candidates.filter((candidate) => previous.some((expectation) => sameSelector(expectation.selector, candidate.selector))).map((candidate) => candidate.id);
@@ -58500,16 +59751,13 @@ function registerRoutes(app, state) {
58500
59751
  app.post("/api/case/finish", async (request) => {
58501
59752
  const picks = readPicks(request.body);
58502
59753
  const recorder = requireCase(state);
59754
+ if (state.recordingBlock) throw new HttpError(409, "RECORDING_BLOCK", "This recording is for a block. Finish it from the Record tab.");
58503
59755
  await stopPhoneCapture(state);
58504
59756
  return enqueue(state, async () => {
59757
+ requireRecording(state, recorder);
58505
59758
  const editing = state.editing;
58506
59759
  if (editing?.mode !== "insert") {
58507
- const candidates = state.latest ? expectationCandidates(state.latest.nodes, state.latest.viewport) : [];
58508
- const chosen = picks.map((pick) => {
58509
- const candidate = candidates[pick];
58510
- if (!candidate) throw new HttpError(400, "BAD_PICK", `There is no candidate ${pick} on the current screen.`);
58511
- return candidate;
58512
- });
59760
+ const chosen = pickedCandidates(state, picks);
58513
59761
  if (editing) recorder.clearExpectations();
58514
59762
  for (const candidate of chosen) recorder.addExpectation({ selector: candidate.selector, node: candidate.node });
58515
59763
  }
@@ -58521,12 +59769,13 @@ function registerRoutes(app, state) {
58521
59769
  const original = await state.project.readCase(editing.caseId).catch(() => void 0);
58522
59770
  const kept = new Set(testCase.steps.map((step) => step.screenshot));
58523
59771
  for (const step of original?.steps ?? []) {
58524
- if (step.screenshot && !kept.has(step.screenshot)) await rm5(state.project.path(step.screenshot), { force: true });
59772
+ if (step.screenshot && !kept.has(step.screenshot)) await rm9(state.project.path(step.screenshot), { force: true });
58525
59773
  }
58526
59774
  }
58527
59775
  const saved = await saveCaseAndFlow(state, testCase);
58528
59776
  state.recorder = void 0;
58529
59777
  state.editing = void 0;
59778
+ state.offered = void 0;
58530
59779
  return {
58531
59780
  caseId: testCase.id,
58532
59781
  flowPath: saved.flowPath,
@@ -58539,19 +59788,21 @@ function registerRoutes(app, state) {
58539
59788
  const recorder = requireCase(state);
58540
59789
  await stopPhoneCapture(state);
58541
59790
  return enqueue(state, async () => {
59791
+ requireRecording(state, recorder);
58542
59792
  const caseId = recorder.caseId;
58543
59793
  const editing = state.editing;
58544
59794
  state.recorder = void 0;
58545
59795
  state.editing = void 0;
59796
+ state.recordingBlock = void 0;
58546
59797
  if (editing) {
58547
59798
  const dir = state.project.evidenceDir(caseId);
58548
- for (const name of await readdir6(dir).catch(() => [])) {
59799
+ for (const name of await readdir8(dir).catch(() => [])) {
58549
59800
  const number = evidenceNumber(name);
58550
- if (number !== void 0 && number >= editing.firstNewEvidence) await rm5(join11(dir, name), { force: true });
59801
+ if (number !== void 0 && number >= editing.firstNewEvidence) await rm9(join13(dir, name), { force: true });
58551
59802
  }
58552
59803
  return { ok: true, caseId, restored: true };
58553
59804
  }
58554
- await rm5(state.project.evidenceDir(caseId), { recursive: true, force: true });
59805
+ await rm9(state.project.evidenceDir(caseId), { recursive: true, force: true });
58555
59806
  return { ok: true, caseId };
58556
59807
  });
58557
59808
  });
@@ -58566,19 +59817,21 @@ function registerRoutes(app, state) {
58566
59817
  return sendPng(reply, path);
58567
59818
  });
58568
59819
  registerCaseRoutes(app, state);
59820
+ registerBlockRoutes(app, state);
58569
59821
  registerReplayRoutes(app, state);
58570
59822
  registerExportRoutes(app, state);
59823
+ registerEvidenceRoutes(app, state);
58571
59824
  registerFileRoutes(app, state);
58572
59825
  registerPhoneRoutes(app, state);
58573
59826
  }
58574
59827
 
58575
59828
  // ../studio/src/server/run-video.ts
58576
- import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
58577
- import { existsSync as existsSync4 } from "node:fs";
58578
- import { mkdtemp as mkdtemp3, readFile as readFile8, rename as rename3, rm as rm6, writeFile as writeFile8 } from "node:fs/promises";
59829
+ import { execFile as execFile6, spawn as spawn4 } from "node:child_process";
59830
+ import { existsSync as existsSync5 } from "node:fs";
59831
+ import { mkdtemp as mkdtemp3, readFile as readFile11, rename as rename5, rm as rm10, writeFile as writeFile9 } from "node:fs/promises";
58579
59832
  import { createRequire } from "node:module";
58580
59833
  import { tmpdir as tmpdir3 } from "node:os";
58581
- import { delimiter as delimiter2, dirname as dirname8, join as join12 } from "node:path";
59834
+ import { delimiter as delimiter2, dirname as dirname10, join as join14 } from "node:path";
58582
59835
  import { fileURLToPath } from "node:url";
58583
59836
  import { promisify as promisify4 } from "node:util";
58584
59837
 
@@ -59168,10 +60421,10 @@ function ffmpegCandidates(platform, env) {
59168
60421
  const name = platform === "win32" ? "ffmpeg.exe" : "ffmpeg";
59169
60422
  const dirs = (env.PATH ?? "").split(delimiter2).filter(Boolean);
59170
60423
  if (platform === "darwin") dirs.push("/opt/homebrew/bin", "/usr/local/bin");
59171
- return [...env.ARIADNE_FFMPEG ? [env.ARIADNE_FFMPEG] : [], ...dirs.map((dir) => join12(dir, name))];
60424
+ return [...env.ARIADNE_FFMPEG ? [env.ARIADNE_FFMPEG] : [], ...dirs.map((dir) => join14(dir, name))];
59172
60425
  }
59173
60426
  function findFfmpeg() {
59174
- return ffmpegCandidates(process.platform, process.env).find((candidate) => existsSync4(candidate));
60427
+ return ffmpegCandidates(process.platform, process.env).find((candidate) => existsSync5(candidate));
59175
60428
  }
59176
60429
  function parseVideoInfo(output) {
59177
60430
  const size = /Video:.*?\b(\d{2,5})x(\d{2,5})\b/.exec(output);
@@ -59184,7 +60437,7 @@ function parseVideoInfo(output) {
59184
60437
  };
59185
60438
  }
59186
60439
  async function videoInfo(ffmpeg, file) {
59187
- const output = await promisify4(execFile5)(ffmpeg, ["-hide_banner", "-i", file]).then(
60440
+ const output = await promisify4(execFile6)(ffmpeg, ["-hide_banner", "-i", file]).then(
59188
60441
  (result) => result.stderr,
59189
60442
  (error) => error.stderr ?? ""
59190
60443
  );
@@ -59193,9 +60446,9 @@ async function videoInfo(ffmpeg, file) {
59193
60446
  var started;
59194
60447
  function startRasteriser(assetsDir) {
59195
60448
  if (!started) {
59196
- const bundled = join12(assetsDir, "index_bg.wasm");
59197
- const wasm2 = existsSync4(bundled) ? bundled : createRequire(import.meta.url).resolve("@resvg/resvg-wasm/index_bg.wasm");
59198
- started = readFile8(wasm2).then((bytes) => initWasm(bytes));
60449
+ const bundled = join14(assetsDir, "index_bg.wasm");
60450
+ const wasm2 = existsSync5(bundled) ? bundled : createRequire(import.meta.url).resolve("@resvg/resvg-wasm/index_bg.wasm");
60451
+ started = readFile11(wasm2).then((bytes) => initWasm(bytes));
59199
60452
  }
59200
60453
  return started;
59201
60454
  }
@@ -59208,7 +60461,7 @@ function pngOf(svg, fonts) {
59208
60461
  return png;
59209
60462
  }
59210
60463
  function runFfmpeg(ffmpeg, args) {
59211
- return new Promise((resolve6, reject) => {
60464
+ return new Promise((resolve7, reject) => {
59212
60465
  const child = spawn4(ffmpeg, args, { stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
59213
60466
  let errors = "";
59214
60467
  child.stderr.on("data", (chunk2) => {
@@ -59221,7 +60474,7 @@ function runFfmpeg(ffmpeg, args) {
59221
60474
  });
59222
60475
  child.on("close", (code) => {
59223
60476
  clearTimeout(timer);
59224
- if (code === 0) resolve6();
60477
+ if (code === 0) resolve7();
59225
60478
  else reject(new Error(errors.trim() || `ffmpeg exited with code ${code}`));
59226
60479
  });
59227
60480
  });
@@ -59234,25 +60487,25 @@ async function frameRunVideo(input) {
59234
60487
  const clock = runVideoClock(input.commands, info.durationMs);
59235
60488
  if (!clock) throw new Error("Maestro reported no start for the phone video");
59236
60489
  await startRasteriser(assetsDir);
59237
- const fonts = await Promise.all(["NotoSansMono-Regular.ttf", "NotoSansMono-Bold.ttf"].map((name) => readFile8(join12(assetsDir, name))));
60490
+ const fonts = await Promise.all(["NotoSansMono-Regular.ttf", "NotoSansMono-Bold.ttf"].map((name) => readFile11(join14(assetsDir, name))));
59238
60491
  const phone = runVideoPhoneBox(info);
59239
60492
  const states = runVideoStates(rows, clock, info.durationMs);
59240
- const work = await mkdtemp3(join12(tmpdir3(), "ariadne-run-video-"));
60493
+ const work = await mkdtemp3(join14(tmpdir3(), "ariadne-run-video-"));
59241
60494
  try {
59242
60495
  const lines = ["ffconcat version 1.0"];
59243
60496
  const totalMs = info.durationMs + RUN_VIDEO_HOLD_MS;
59244
60497
  for (const [index, state] of states.entries()) {
59245
60498
  const name = `state-${String(index).padStart(4, "0")}.png`;
59246
60499
  const svg = runVideoFrameSvg({ caseName: input.caseName, device: input.device, appId: input.appId, passed: input.passed, rows, state, phone });
59247
- await writeFile8(join12(work, name), pngOf(svg, fonts));
60500
+ await writeFile9(join14(work, name), pngOf(svg, fonts));
59248
60501
  const untilMs = states[index + 1]?.fromMs ?? totalMs;
59249
60502
  lines.push(`file '${name}'`, `duration ${((untilMs - state.fromMs) / 1e3).toFixed(3)}`);
59250
60503
  if (index === states.length - 1) lines.push(`file '${name}'`);
59251
60504
  }
59252
- await writeFile8(join12(work, "states.ffconcat"), `${lines.join("\n")}
60505
+ await writeFile9(join14(work, "states.ffconcat"), `${lines.join("\n")}
59253
60506
  `);
59254
- await writeFile8(join12(work, "corners.png"), pngOf(runVideoCornersSvg(phone), fonts));
59255
- const output = join12(dirname8(video), "video.framed.mp4");
60507
+ await writeFile9(join14(work, "corners.png"), pngOf(runVideoCornersSvg(phone), fonts));
60508
+ const output = join14(dirname10(video), "video.framed.mp4");
59256
60509
  const graph = [
59257
60510
  `[1:v]fps=30,tpad=stop_mode=clone:stop_duration=${RUN_VIDEO_HOLD_MS / 1e3},scale=${phone.width}:${phone.height}:flags=lanczos[phone]`,
59258
60511
  `[0:v]fps=30,format=yuv420p[frame]`,
@@ -59269,11 +60522,11 @@ async function frameRunVideo(input) {
59269
60522
  "-safe",
59270
60523
  "0",
59271
60524
  "-i",
59272
- join12(work, "states.ffconcat"),
60525
+ join14(work, "states.ffconcat"),
59273
60526
  "-i",
59274
60527
  video,
59275
60528
  "-i",
59276
- join12(work, "corners.png"),
60529
+ join14(work, "corners.png"),
59277
60530
  "-filter_complex",
59278
60531
  graph,
59279
60532
  "-map",
@@ -59289,9 +60542,9 @@ async function frameRunVideo(input) {
59289
60542
  "+faststart",
59290
60543
  output
59291
60544
  ]);
59292
- await rename3(output, video);
60545
+ await rename5(output, video);
59293
60546
  } finally {
59294
- await rm6(work, { recursive: true, force: true });
60547
+ await rm10(work, { recursive: true, force: true });
59295
60548
  }
59296
60549
  }
59297
60550
 
@@ -59320,7 +60573,7 @@ async function buildServer(options) {
59320
60573
  return reply.status(403).send({ error: { code: "NOT_THIS_COMPUTER", message: "This request did not come from Ariadne on this computer." } });
59321
60574
  });
59322
60575
  app.addHook("onResponse", async (request, reply) => {
59323
- if (request.url.startsWith("/api/screen/image") || request.url.startsWith("/api/evidence/")) return;
60576
+ if (request.url.startsWith("/api/screen/image") || request.url.startsWith("/api/screen/refresh") || request.url.startsWith("/api/evidence/")) return;
59324
60577
  if (request.url.startsWith("/api/runs/")) return;
59325
60578
  if (!request.url.startsWith("/api/")) return;
59326
60579
  console.log(
@@ -59332,7 +60585,7 @@ async function buildServer(options) {
59332
60585
  registerSetupRoutes(app, setup);
59333
60586
  registerDiagnosticsRoutes(app, state, { appVersion: options.appVersion ?? "development", setup });
59334
60587
  const uiDir = options.uiDir ?? uiDist;
59335
- if (existsSync5(uiDir)) {
60588
+ if (existsSync6(uiDir)) {
59336
60589
  await app.register(import_static.default, { root: uiDir, prefix: "/" });
59337
60590
  app.setNotFoundHandler((request, reply) => {
59338
60591
  if (request.url.startsWith("/api/")) {
@@ -59344,7 +60597,7 @@ async function buildServer(options) {
59344
60597
  app.addHook("onClose", async () => {
59345
60598
  await stopPhoneCapture(state).catch(() => void 0);
59346
60599
  if (state.session.isOpen) await state.session.close().catch(() => void 0);
59347
- await rm7(state.scratchDir, { recursive: true, force: true }).catch(() => void 0);
60600
+ await rm11(state.scratchDir, { recursive: true, force: true }).catch(() => void 0);
59348
60601
  });
59349
60602
  return app;
59350
60603
  }
@@ -59369,8 +60622,8 @@ async function startStudio(options) {
59369
60622
  app,
59370
60623
  url: studioUrl(host, port),
59371
60624
  port,
59372
- projectDir: resolve3(options.projectDir),
59373
- servesUi: existsSync5(options.uiDir ?? uiDist),
60625
+ projectDir: resolve4(options.projectDir),
60626
+ servesUi: existsSync6(options.uiDir ?? uiDist),
59374
60627
  close: async () => {
59375
60628
  await app.close();
59376
60629
  log.stop();
@@ -59379,8 +60632,8 @@ async function startStudio(options) {
59379
60632
  }
59380
60633
 
59381
60634
  // src/args.ts
59382
- import { existsSync as existsSync6 } from "node:fs";
59383
- import { join as join13, resolve as resolve4 } from "node:path";
60635
+ import { existsSync as existsSync7 } from "node:fs";
60636
+ import { join as join15, resolve as resolve5 } from "node:path";
59384
60637
  import { parseArgs } from "node:util";
59385
60638
  var DEFAULT_PORT2 = 4711;
59386
60639
  var DEFAULT_HOST = "127.0.0.1";
@@ -59417,16 +60670,16 @@ point at Maestro and the Android SDK when they are not in the usual places.`;
59417
60670
  var UsageError = class extends Error {
59418
60671
  };
59419
60672
  function isProjectDir(dir) {
59420
- return existsSync6(join13(dir, "project.json")) && existsSync6(join13(dir, "cases"));
60673
+ return existsSync7(join15(dir, "project.json")) && existsSync7(join15(dir, "cases"));
59421
60674
  }
59422
60675
  function projectDirIn(dir) {
59423
- const inner = join13(dir, ARIADNE_DIR);
59424
- return !existsSync6(join13(dir, "project.json")) && existsSync6(join13(inner, "project.json")) ? inner : dir;
60676
+ const inner = join15(dir, ARIADNE_DIR);
60677
+ return !existsSync7(join15(dir, "project.json")) && existsSync7(join15(inner, "project.json")) ? inner : dir;
59425
60678
  }
59426
60679
  function defaultProjectDir(cwd) {
59427
- if (isProjectDir(cwd)) return resolve4(cwd);
59428
- const inner = projectDirIn(resolve4(cwd));
59429
- return inner !== resolve4(cwd) ? inner : resolve4(cwd, DEFAULT_PROJECT_FOLDER);
60680
+ if (isProjectDir(cwd)) return resolve5(cwd);
60681
+ const inner = projectDirIn(resolve5(cwd));
60682
+ return inner !== resolve5(cwd) ? inner : resolve5(cwd, DEFAULT_PROJECT_FOLDER);
59430
60683
  }
59431
60684
  function parsePort(value, source) {
59432
60685
  const port = Number(value);
@@ -59459,13 +60712,13 @@ function parseCliArgs(argv, env, cwd) {
59459
60712
  if (values.version) return { kind: "version" };
59460
60713
  if (positionals[0] === "init") {
59461
60714
  if (positionals.length > 2) throw new UsageError(`Expected at most one folder after init, got ${positionals.length - 1}: ${positionals.slice(1).join(" ")}`);
59462
- return { kind: "init", folder: resolve4(cwd, positionals[1] ?? ".") };
60715
+ return { kind: "init", folder: resolve5(cwd, positionals[1] ?? ".") };
59463
60716
  }
59464
60717
  if (positionals.length > 1) {
59465
60718
  throw new UsageError(`Expected at most one project folder, got ${positionals.length}: ${positionals.join(" ")}`);
59466
60719
  }
59467
60720
  const folder = positionals[0] ?? env.ARIADNE_PROJECT_DIR;
59468
- const projectDir = folder ? projectDirIn(resolve4(cwd, folder)) : defaultProjectDir(cwd);
60721
+ const projectDir = folder ? projectDirIn(resolve5(cwd, folder)) : defaultProjectDir(cwd);
59469
60722
  const port = values.port !== void 0 ? parsePort(values.port, "--port") : env.PORT ? parsePort(env.PORT, "PORT") : DEFAULT_PORT2;
59470
60723
  const host = values.host?.trim() || env.HOST?.trim() || DEFAULT_HOST;
59471
60724
  const sessionName = values.session?.trim() || env.ARIADNE_SESSION_NAME?.trim() || DEFAULT_SESSION;
@@ -59473,8 +60726,8 @@ function parseCliArgs(argv, env, cwd) {
59473
60726
  }
59474
60727
 
59475
60728
  // src/init.ts
59476
- import { existsSync as existsSync7, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
59477
- import { join as join14, relative as relative3, sep as sep4 } from "node:path";
60729
+ import { existsSync as existsSync8, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
60730
+ import { join as join16, relative as relative4, sep as sep5 } from "node:path";
59478
60731
  var RECORDED_DIR = "recorded";
59479
60732
  var BLOCK_FOLDER_NAMES = ["subflows", "shared", "common", "helpers", "utils"];
59480
60733
  var CONFIG_KEYS = /^(flows|includeTags|excludeTags|executionOrder|testOutputDir|disableRetries|platform|notifications)\s*:/m;
@@ -59489,7 +60742,7 @@ function isDirectory(path) {
59489
60742
  }
59490
60743
  function hasMaestroConfig(dir) {
59491
60744
  for (const name of ["config.yaml", "config.yml"]) {
59492
- const file = join14(dir, name);
60745
+ const file = join16(dir, name);
59493
60746
  try {
59494
60747
  if (CONFIG_KEYS.test(readFileSync(file, "utf8"))) return true;
59495
60748
  } catch {
@@ -59498,7 +60751,7 @@ function hasMaestroConfig(dir) {
59498
60751
  return false;
59499
60752
  }
59500
60753
  function findMaestroWorkspace(folder) {
59501
- const dotMaestro = join14(folder, ".maestro");
60754
+ const dotMaestro = join16(folder, ".maestro");
59502
60755
  if (isDirectory(dotMaestro)) return dotMaestro;
59503
60756
  if (hasMaestroConfig(folder)) return folder;
59504
60757
  let entries = [];
@@ -59509,7 +60762,7 @@ function findMaestroWorkspace(folder) {
59509
60762
  }
59510
60763
  for (const name of entries) {
59511
60764
  if (name === ARIADNE_DIR || SKIPPED_DIRS.has(name)) continue;
59512
- const dir = join14(folder, name);
60765
+ const dir = join16(folder, name);
59513
60766
  if (isDirectory(dir) && hasMaestroConfig(dir)) return dir;
59514
60767
  }
59515
60768
  return void 0;
@@ -59524,7 +60777,7 @@ function flowFiles(dir, depth = 0) {
59524
60777
  }
59525
60778
  const found = [];
59526
60779
  for (const name of entries) {
59527
- const path = join14(dir, name);
60780
+ const path = join16(dir, name);
59528
60781
  if (isDirectory(path)) {
59529
60782
  if (!SKIPPED_DIRS.has(name) && name !== ARIADNE_DIR) found.push(...flowFiles(path, depth + 1));
59530
60783
  } else if (/\.ya?ml$/i.test(name) && !(depth === 0 && /^config\.ya?ml$/i.test(name))) {
@@ -59554,7 +60807,7 @@ function commonAppId(workspace) {
59554
60807
  function blockFolders2(workspace) {
59555
60808
  let entries = [];
59556
60809
  try {
59557
- entries = readdirSync(workspace).filter((name) => isDirectory(join14(workspace, name)));
60810
+ entries = readdirSync(workspace).filter((name) => isDirectory(join16(workspace, name)));
59558
60811
  } catch {
59559
60812
  return [];
59560
60813
  }
@@ -59563,19 +60816,19 @@ function blockFolders2(workspace) {
59563
60816
  var InitError = class extends Error {
59564
60817
  };
59565
60818
  function toPosix(path) {
59566
- return path.split(sep4).join("/");
60819
+ return path.split(sep5).join("/");
59567
60820
  }
59568
60821
  function initProject(folder) {
59569
- const projectDir = join14(folder, ARIADNE_DIR);
59570
- const projectFile = join14(projectDir, "project.json");
59571
- if (existsSync7(projectFile)) {
60822
+ const projectDir = join16(folder, ARIADNE_DIR);
60823
+ const projectFile = join16(projectDir, "project.json");
60824
+ if (existsSync8(projectFile)) {
59572
60825
  throw new InitError(`${projectFile} already exists; this folder already has an Ariadne project. Nothing was changed.`);
59573
60826
  }
59574
60827
  const created = [];
59575
60828
  const makeDir = (dir) => {
59576
60829
  if (isDirectory(dir)) return;
59577
60830
  mkdirSync(dir, { recursive: true });
59578
- created.push(dir + sep4);
60831
+ created.push(dir + sep5);
59579
60832
  };
59580
60833
  const settings = { appId: "", platform: "android" };
59581
60834
  const workspace = findMaestroWorkspace(folder);
@@ -59584,7 +60837,7 @@ function initProject(folder) {
59584
60837
  const found = commonAppId(workspace);
59585
60838
  flowCount = found.flowCount;
59586
60839
  if (found.appId) settings.appId = found.appId;
59587
- const fromProject = toPosix(relative3(projectDir, workspace));
60840
+ const fromProject = toPosix(relative4(projectDir, workspace));
59588
60841
  const blocks = blockFolders2(workspace).map((name) => `${fromProject}/${name}`);
59589
60842
  settings.paths = { ...blocks.length > 0 ? { blocks } : {}, flows: `${fromProject}/${RECORDED_DIR}` };
59590
60843
  }
@@ -59592,18 +60845,18 @@ function initProject(folder) {
59592
60845
  writeFileSync(projectFile, `${JSON.stringify(settings, null, 2)}
59593
60846
  `, "utf8");
59594
60847
  created.push(projectFile);
59595
- makeDir(join14(projectDir, "cases"));
59596
- const gitignore = join14(projectDir, ".gitignore");
59597
- if (!existsSync7(gitignore)) {
60848
+ makeDir(join16(projectDir, "cases"));
60849
+ const gitignore = join16(projectDir, ".gitignore");
60850
+ if (!existsSync8(gitignore)) {
59598
60851
  writeFileSync(gitignore, PROJECT_GITIGNORE, "utf8");
59599
60852
  created.push(gitignore);
59600
60853
  }
59601
- if (workspace) makeDir(join14(workspace, RECORDED_DIR));
60854
+ if (workspace) makeDir(join16(workspace, RECORDED_DIR));
59602
60855
  return { projectDir, settings, ...workspace ? { workspace } : {}, flowCount, created };
59603
60856
  }
59604
60857
  function initReport(result, folder, cwd) {
59605
60858
  const shown = (path) => {
59606
- const rel = relative3(cwd, path);
60859
+ const rel = relative4(cwd, path);
59607
60860
  return rel.startsWith("..") ? path.replace(/[\\/]+$/, "") : toPosix(rel) || ".";
59608
60861
  };
59609
60862
  const lines = [];
@@ -59614,14 +60867,14 @@ function initReport(result, folder, cwd) {
59614
60867
  lines.push("No Maestro workspace found (no .maestro folder or Maestro config.yaml), so blocks and flows stay inside the project.");
59615
60868
  }
59616
60869
  lines.push("");
59617
- for (const path of result.created) lines.push(` created ${shown(path)}${path.endsWith(sep4) ? "/" : ""}`);
60870
+ for (const path of result.created) lines.push(` created ${shown(path)}${path.endsWith(sep5) ? "/" : ""}`);
59618
60871
  lines.push("");
59619
60872
  const { paths } = result.settings;
59620
60873
  lines.push(` appId: ${result.settings.appId || "(not set; enter it in the studio)"}`);
59621
60874
  if (paths?.flows) lines.push(` flows: ${paths.flows} (recorded cases are written here)`);
59622
60875
  lines.push(` blocks: ${paths?.blocks?.join(", ") ?? (paths ? '(none found; add folders to "paths.blocks" in project.json)' : "blocks/")}`);
59623
60876
  lines.push("");
59624
- const target = relative3(cwd, folder) && shown(folder);
60877
+ const target = relative4(cwd, folder) && shown(folder);
59625
60878
  lines.push(`Next: ariadne-studio${target ? ` ${/^[\w./:\\-]+$/.test(target) ? target : `"${target}"`}` : ""}`);
59626
60879
  return lines;
59627
60880
  }
@@ -59655,13 +60908,9 @@ function version() {
59655
60908
  }
59656
60909
  }
59657
60910
  function addSdkToPath() {
59658
- const home = androidHome();
59659
- if (!home) return;
59660
- const platformTools = join15(home, "platform-tools");
60911
+ const platformTools = join17(androidHome() ?? defaultSdkDir(), "platform-tools");
59661
60912
  const dirs = (process.env.PATH ?? "").split(delimiter3).filter(Boolean);
59662
- if (existsSync8(platformTools) && !dirs.includes(platformTools)) {
59663
- process.env.PATH = [...dirs, platformTools].join(delimiter3);
59664
- }
60913
+ if (!dirs.includes(platformTools)) process.env.PATH = [...dirs, platformTools].join(delimiter3);
59665
60914
  }
59666
60915
  function notifyParent(message) {
59667
60916
  process.send?.(message);
@@ -59685,7 +60934,7 @@ function shellQuote(path) {
59685
60934
  async function explainPortInUse(options) {
59686
60935
  const url = studioUrl(options.host, options.port);
59687
60936
  const otherProject = await runningStudioAt(url);
59688
- if (otherProject && resolve5(otherProject) === options.projectDir) {
60937
+ if (otherProject && resolve6(otherProject) === options.projectDir) {
59689
60938
  console.log(`Ariadne Studio is already running for this project at ${url}`);
59690
60939
  if (options.open) openBrowser(url);
59691
60940
  return 0;
@@ -59707,8 +60956,8 @@ async function run(options) {
59707
60956
  port: options.port,
59708
60957
  host: options.host,
59709
60958
  sessionName: options.sessionName,
59710
- ...existsSync8(bundledUi) ? { uiDir: bundledUi } : {},
59711
- ...existsSync8(bundledVideoAssets) ? { videoAssetsDir: bundledVideoAssets } : {}
60959
+ ...existsSync9(bundledUi) ? { uiDir: bundledUi } : {},
60960
+ ...existsSync9(bundledVideoAssets) ? { videoAssetsDir: bundledVideoAssets } : {}
59712
60961
  });
59713
60962
  } catch (error) {
59714
60963
  if (isAddressInUse(error)) {