@swmansion/argent 0.27.1-next.21 → 0.27.1-next.22

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-cmds.mjs CHANGED
@@ -9363,6 +9363,35 @@ var FLOW_NAME_CHARSET = "[A-Za-z0-9_-]+";
9363
9363
  var FLOW_NAME_PATTERN = new RegExp(`^${FLOW_NAME_CHARSET}$`);
9364
9364
  var FLOW_FILE_NAME_PATTERN = new RegExp(`^${FLOW_NAME_CHARSET}\\.yaml$`);
9365
9365
  var SCRIPT_FILE_NAME_PATTERN = new RegExp(`^${FLOW_NAME_CHARSET}\\.mjs$`);
9366
+ function interpolateFileInputPath(template, args) {
9367
+ let missing = false;
9368
+ const out = template.replace(/\$\{([A-Za-z0-9_]+)\}/g, (_m, name) => {
9369
+ const v = args[name];
9370
+ if (typeof v !== "string" || v.length === 0) {
9371
+ missing = true;
9372
+ return "";
9373
+ }
9374
+ return v;
9375
+ });
9376
+ return missing ? null : out;
9377
+ }
9378
+ var TOOL_FILE_EXTENSIONS = [".png", ".yaml"];
9379
+ function hasToolFileExtension(file2) {
9380
+ const name = file2.toLowerCase();
9381
+ return TOOL_FILE_EXTENSIONS.some((extension) => name.endsWith(extension));
9382
+ }
9383
+ function toolStepFiles(specs, args) {
9384
+ const files = [];
9385
+ for (const spec of specs ?? []) {
9386
+ if (spec.skipWhenSet !== void 0 && args[spec.skipWhenSet] !== void 0) continue;
9387
+ const filled = interpolateFileInputPath(spec.path, args);
9388
+ if (filled !== null) files.push({ spec, path: filled });
9389
+ }
9390
+ return files;
9391
+ }
9392
+ function isClientFileArgument({ spec, path: path23 }) {
9393
+ return spec.kind === "file" && spec.path === `\${${spec.target}}` && path23.startsWith("/") && hasToolFileExtension(path23);
9394
+ }
9366
9395
 
9367
9396
  // ../registry/src/flow-file-refs.ts
9368
9397
  import * as fs2 from "node:fs/promises";
@@ -9391,6 +9420,7 @@ function isRecord(value) {
9391
9420
  function collectFlowRequests(doc) {
9392
9421
  const runTargets = /* @__PURE__ */ new Set();
9393
9422
  const snapshots = /* @__PURE__ */ new Set();
9423
+ const toolSteps = [];
9394
9424
  const seen = /* @__PURE__ */ new Set();
9395
9425
  const visit = (steps, depth) => {
9396
9426
  if (!Array.isArray(steps) || depth > MAX_BLOCK_NESTING || seen.has(steps)) return;
@@ -9404,11 +9434,14 @@ function collectFlowRequests(doc) {
9404
9434
  }
9405
9435
  const snapshot = isRecord(step.snapshot) ? step.snapshot.name : step.snapshot;
9406
9436
  if (typeof snapshot === "string" && FLOW_NAME_PATTERN.test(snapshot)) snapshots.add(snapshot);
9437
+ if (typeof step.tool === "string") {
9438
+ toolSteps.push({ tool: step.tool, args: isRecord(step.args) ? step.args : {} });
9439
+ }
9407
9440
  visit(step.steps, depth + 1);
9408
9441
  }
9409
9442
  };
9410
9443
  if (isRecord(doc)) visit(doc.steps, 0);
9411
- return { runTargets: [...runTargets], snapshots: [...snapshots] };
9444
+ return { runTargets: [...runTargets], snapshots: [...snapshots], toolSteps };
9412
9445
  }
9413
9446
  function baselineKeyFor(canonicalPath, flowName) {
9414
9447
  const stem = path2.basename(canonicalPath, ".yaml");
@@ -24978,6 +25011,29 @@ async function readFlowMember(anchorDir, target, roots, budget, opts) {
24978
25011
  const sent = await sendBytes(member, canonical, budget, opts);
24979
25012
  return { ...sent, text: sent.bytes?.toString("utf8") };
24980
25013
  }
25014
+ async function readToolMember(file2, roots, budget, opts) {
25015
+ const member = { role: "tool", key: file2, path: file2 };
25016
+ const refuse = (reason) => ({
25017
+ member: { ...member, state: "refused", error: reason },
25018
+ sent: `refused (${reason})`
25019
+ });
25020
+ const { canonical } = await landing(file2);
25021
+ if (!roots.some((root) => isWithin(canonical, root))) {
25022
+ return refuse(`${file2} is outside every root this client serves (${roots.join(", ")})`);
25023
+ }
25024
+ if (!hasToolFileExtension(canonical)) {
25025
+ return refuse(`${file2} links to a file that is not one of ${TOOL_FILE_EXTENSIONS.join(", ")}`);
25026
+ }
25027
+ const st = await stat3(canonical).catch((err) => err);
25028
+ if (st instanceof Error) {
25029
+ if (st.code === "ENOENT" || st.code === "ENOTDIR") {
25030
+ return { member: { ...member, state: "missing" }, sent: "missing" };
25031
+ }
25032
+ return refuse(st.message);
25033
+ }
25034
+ if (st.isDirectory()) return refuse("EISDIR: illegal operation on a directory, read");
25035
+ return sendBytes(member, canonical, budget, opts);
25036
+ }
24981
25037
  function callPlatform(args) {
24982
25038
  if (typeof args.device === "string" && args.device.startsWith("chromium-cdp-")) return "chromium";
24983
25039
  if (typeof args.platform !== "string") return void 0;
@@ -25032,7 +25088,7 @@ async function sendBytes(member, real, budget, opts) {
25032
25088
  bytes
25033
25089
  };
25034
25090
  }
25035
- async function collectBaselineMembers(canonical, flowName, snapshots, args, roots, budget, opts, emit) {
25091
+ async function collectBaselineMembers(canonical, flowName, snapshots, args, roots, budget, opts, sent, emit) {
25036
25092
  const dir = path5.join(
25037
25093
  path5.dirname(canonical),
25038
25094
  "__baselines__",
@@ -25051,6 +25107,7 @@ async function collectBaselineMembers(canonical, flowName, snapshots, args, root
25051
25107
  if (!name.endsWith(".png")) continue;
25052
25108
  if (!updates && !prefixes.some((prefix) => name.startsWith(prefix))) continue;
25053
25109
  const file2 = path5.join(dir, name);
25110
+ if (sent.has(file2)) continue;
25054
25111
  const member = { role: "baseline", key: file2, path: file2 };
25055
25112
  const entry = await baselineEntry(file2, roots, updates);
25056
25113
  if (entry.state === "refused") {
@@ -25060,8 +25117,8 @@ async function collectBaselineMembers(canonical, flowName, snapshots, args, root
25060
25117
  } else if (updates) {
25061
25118
  emit({ ...member, state: "listed" }, "listed");
25062
25119
  } else {
25063
- const sent = await sendBytes(member, entry.real, budget, opts);
25064
- emit(sent.member, sent.sent);
25120
+ const sent2 = await sendBytes(member, entry.real, budget, opts);
25121
+ emit(sent2.member, sent2.sent);
25065
25122
  }
25066
25123
  }
25067
25124
  }
@@ -25096,6 +25153,17 @@ async function collectFlowMembers(rootPath, rootBytes, args, opts) {
25096
25153
  }
25097
25154
  const requests = collectFlowRequests(doc);
25098
25155
  for (const name of requests.snapshots) snapshots.add(name);
25156
+ for (const step of requests.toolSteps) {
25157
+ if (step.tool === "flow-execute") continue;
25158
+ for (const file3 of toolStepFiles(opts.toolFileInputs?.(step.tool), step.args)) {
25159
+ const { unwrapWhenSet } = file3.spec;
25160
+ if (unwrapWhenSet !== void 0 && step.args[unwrapWhenSet] !== void 0) continue;
25161
+ if (!isClientFileArgument(file3) || seen.has(file3.path)) continue;
25162
+ seen.add(file3.path);
25163
+ const read = await readToolMember(file3.path, roots, budget, opts);
25164
+ emit(read.member, read.sent);
25165
+ }
25166
+ }
25099
25167
  const anchorDir = path5.dirname(file2.canonical);
25100
25168
  for (const target of requests.runTargets) {
25101
25169
  const key = flowMemberKey(anchorDir, target);
@@ -25118,6 +25186,7 @@ async function collectFlowMembers(rootPath, rootBytes, args, opts) {
25118
25186
  roots,
25119
25187
  budget,
25120
25188
  opts,
25189
+ seen,
25121
25190
  emit
25122
25191
  );
25123
25192
  }
@@ -25282,7 +25351,8 @@ function createToolsClient(options = {}) {
25282
25351
  opts?.signal?.throwIfAborted();
25283
25352
  const { url: url2, token, remote } = await route();
25284
25353
  let finalArgs = args;
25285
- const meta3 = await fetchTool(name, { signal: opts?.signal });
25354
+ const tools2 = await fetchTools({ signal: opts?.signal });
25355
+ const meta3 = tools2.find((t2) => t2.name === name) ?? null;
25286
25356
  const baselineDirs = [];
25287
25357
  if (meta3?.fileInputs?.length) {
25288
25358
  if (remote) assertRequiredPresent(meta3, args);
@@ -25292,6 +25362,7 @@ function createToolsClient(options = {}) {
25292
25362
  log: diagnose,
25293
25363
  baselineDirs,
25294
25364
  collectMembers: collectFlowMembers,
25365
+ toolFileInputs: (tool) => tools2.find((t2) => t2.name === tool)?.fileInputs,
25295
25366
  signal: opts?.signal
25296
25367
  });
25297
25368
  }
@@ -29877,7 +29948,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
29877
29948
  var SESSION_ID2 = randomUUID5();
29878
29949
  function readCliVersion() {
29879
29950
  if (true) {
29880
- return "0.27.1-next.21";
29951
+ return "0.27.1-next.22";
29881
29952
  }
29882
29953
  return "0.0.0";
29883
29954
  }
@@ -31231,11 +31302,15 @@ counts the remaining flows skipped.
31231
31302
  The CLI sends a run to its tool-server: the local one that starts
31232
31303
  automatically, or the one that \`argent link\` or ARGENT_TOOLS_URL names. Over
31233
31304
  a link, the CLI uploads the flow file, the run: fragments that it reaches and
31234
- the snapshot baselines that the run compares. The CLI writes the new baselines
31235
- that the run returns. The tool-server rejects the flow before the first step
31236
- when the flow or a fragment has one of these steps:
31237
- - script:
31238
- - a tool: step that takes a file or records a flow
31305
+ the snapshot baselines that the run compares. It also uploads each .png or
31306
+ .yaml file that a tool: step takes as a file argument by an absolute path. The
31307
+ CLI writes the new baselines that the run returns. The tool-server rejects the
31308
+ flow before the first step when the flow or a fragment has one of these steps:
31309
+ - a script: step
31310
+ - a tool: step that runs or records a flow
31311
+ - a tool: step that takes a directory, an app or an output directory
31312
+ - a tool: step whose tool builds a file path from several arguments
31313
+ - a tool: step that names a relative path or a file other than .png or .yaml
31239
31314
  Without a link, all step kinds run.
31240
31315
 
31241
31316
  Subcommands:
package/dist/client.js CHANGED
@@ -8566,6 +8566,35 @@ var FLOW_NAME_CHARSET = "[A-Za-z0-9_-]+";
8566
8566
  var FLOW_NAME_PATTERN = new RegExp(`^${FLOW_NAME_CHARSET}$`);
8567
8567
  var FLOW_FILE_NAME_PATTERN = new RegExp(`^${FLOW_NAME_CHARSET}\\.yaml$`);
8568
8568
  var SCRIPT_FILE_NAME_PATTERN = new RegExp(`^${FLOW_NAME_CHARSET}\\.mjs$`);
8569
+ function interpolateFileInputPath(template, args) {
8570
+ let missing = false;
8571
+ const out = template.replace(/\$\{([A-Za-z0-9_]+)\}/g, (_m, name) => {
8572
+ const v = args[name];
8573
+ if (typeof v !== "string" || v.length === 0) {
8574
+ missing = true;
8575
+ return "";
8576
+ }
8577
+ return v;
8578
+ });
8579
+ return missing ? null : out;
8580
+ }
8581
+ var TOOL_FILE_EXTENSIONS = [".png", ".yaml"];
8582
+ function hasToolFileExtension(file) {
8583
+ const name = file.toLowerCase();
8584
+ return TOOL_FILE_EXTENSIONS.some((extension) => name.endsWith(extension));
8585
+ }
8586
+ function toolStepFiles(specs, args) {
8587
+ const files = [];
8588
+ for (const spec of specs ?? []) {
8589
+ if (spec.skipWhenSet !== void 0 && args[spec.skipWhenSet] !== void 0) continue;
8590
+ const filled = interpolateFileInputPath(spec.path, args);
8591
+ if (filled !== null) files.push({ spec, path: filled });
8592
+ }
8593
+ return files;
8594
+ }
8595
+ function isClientFileArgument({ spec, path: path12 }) {
8596
+ return spec.kind === "file" && spec.path === `\${${spec.target}}` && path12.startsWith("/") && hasToolFileExtension(path12);
8597
+ }
8569
8598
 
8570
8599
  // ../registry/src/flow-file-refs.ts
8571
8600
  import * as fs5 from "node:fs/promises";
@@ -8594,6 +8623,7 @@ function isRecord(value) {
8594
8623
  function collectFlowRequests(doc) {
8595
8624
  const runTargets = /* @__PURE__ */ new Set();
8596
8625
  const snapshots = /* @__PURE__ */ new Set();
8626
+ const toolSteps = [];
8597
8627
  const seen = /* @__PURE__ */ new Set();
8598
8628
  const visit = (steps, depth) => {
8599
8629
  if (!Array.isArray(steps) || depth > MAX_BLOCK_NESTING || seen.has(steps)) return;
@@ -8607,11 +8637,14 @@ function collectFlowRequests(doc) {
8607
8637
  }
8608
8638
  const snapshot = isRecord(step.snapshot) ? step.snapshot.name : step.snapshot;
8609
8639
  if (typeof snapshot === "string" && FLOW_NAME_PATTERN.test(snapshot)) snapshots.add(snapshot);
8640
+ if (typeof step.tool === "string") {
8641
+ toolSteps.push({ tool: step.tool, args: isRecord(step.args) ? step.args : {} });
8642
+ }
8610
8643
  visit(step.steps, depth + 1);
8611
8644
  }
8612
8645
  };
8613
8646
  if (isRecord(doc)) visit(doc.steps, 0);
8614
- return { runTargets: [...runTargets], snapshots: [...snapshots] };
8647
+ return { runTargets: [...runTargets], snapshots: [...snapshots], toolSteps };
8615
8648
  }
8616
8649
  function baselineKeyFor(canonicalPath, flowName) {
8617
8650
  const stem = path7.basename(canonicalPath, ".yaml");
@@ -9562,6 +9595,29 @@ async function readFlowMember(anchorDir, target, roots, budget, opts) {
9562
9595
  const sent = await sendBytes(member, canonical, budget, opts);
9563
9596
  return { ...sent, text: sent.bytes?.toString("utf8") };
9564
9597
  }
9598
+ async function readToolMember(file, roots, budget, opts) {
9599
+ const member = { role: "tool", key: file, path: file };
9600
+ const refuse = (reason) => ({
9601
+ member: { ...member, state: "refused", error: reason },
9602
+ sent: `refused (${reason})`
9603
+ });
9604
+ const { canonical } = await landing(file);
9605
+ if (!roots.some((root) => isWithin(canonical, root))) {
9606
+ return refuse(`${file} is outside every root this client serves (${roots.join(", ")})`);
9607
+ }
9608
+ if (!hasToolFileExtension(canonical)) {
9609
+ return refuse(`${file} links to a file that is not one of ${TOOL_FILE_EXTENSIONS.join(", ")}`);
9610
+ }
9611
+ const st = await stat3(canonical).catch((err) => err);
9612
+ if (st instanceof Error) {
9613
+ if (st.code === "ENOENT" || st.code === "ENOTDIR") {
9614
+ return { member: { ...member, state: "missing" }, sent: "missing" };
9615
+ }
9616
+ return refuse(st.message);
9617
+ }
9618
+ if (st.isDirectory()) return refuse("EISDIR: illegal operation on a directory, read");
9619
+ return sendBytes(member, canonical, budget, opts);
9620
+ }
9565
9621
  function callPlatform(args) {
9566
9622
  if (typeof args.device === "string" && args.device.startsWith("chromium-cdp-")) return "chromium";
9567
9623
  if (typeof args.platform !== "string") return void 0;
@@ -9616,7 +9672,7 @@ async function sendBytes(member, real, budget, opts) {
9616
9672
  bytes
9617
9673
  };
9618
9674
  }
9619
- async function collectBaselineMembers(canonical, flowName, snapshots, args, roots, budget, opts, emit) {
9675
+ async function collectBaselineMembers(canonical, flowName, snapshots, args, roots, budget, opts, sent, emit) {
9620
9676
  const dir = path10.join(
9621
9677
  path10.dirname(canonical),
9622
9678
  "__baselines__",
@@ -9635,6 +9691,7 @@ async function collectBaselineMembers(canonical, flowName, snapshots, args, root
9635
9691
  if (!name.endsWith(".png")) continue;
9636
9692
  if (!updates && !prefixes.some((prefix) => name.startsWith(prefix))) continue;
9637
9693
  const file = path10.join(dir, name);
9694
+ if (sent.has(file)) continue;
9638
9695
  const member = { role: "baseline", key: file, path: file };
9639
9696
  const entry = await baselineEntry(file, roots, updates);
9640
9697
  if (entry.state === "refused") {
@@ -9644,8 +9701,8 @@ async function collectBaselineMembers(canonical, flowName, snapshots, args, root
9644
9701
  } else if (updates) {
9645
9702
  emit({ ...member, state: "listed" }, "listed");
9646
9703
  } else {
9647
- const sent = await sendBytes(member, entry.real, budget, opts);
9648
- emit(sent.member, sent.sent);
9704
+ const sent2 = await sendBytes(member, entry.real, budget, opts);
9705
+ emit(sent2.member, sent2.sent);
9649
9706
  }
9650
9707
  }
9651
9708
  }
@@ -9680,6 +9737,17 @@ async function collectFlowMembers(rootPath, rootBytes, args, opts) {
9680
9737
  }
9681
9738
  const requests = collectFlowRequests(doc);
9682
9739
  for (const name of requests.snapshots) snapshots.add(name);
9740
+ for (const step of requests.toolSteps) {
9741
+ if (step.tool === "flow-execute") continue;
9742
+ for (const file2 of toolStepFiles(opts.toolFileInputs?.(step.tool), step.args)) {
9743
+ const { unwrapWhenSet } = file2.spec;
9744
+ if (unwrapWhenSet !== void 0 && step.args[unwrapWhenSet] !== void 0) continue;
9745
+ if (!isClientFileArgument(file2) || seen.has(file2.path)) continue;
9746
+ seen.add(file2.path);
9747
+ const read = await readToolMember(file2.path, roots, budget, opts);
9748
+ emit(read.member, read.sent);
9749
+ }
9750
+ }
9683
9751
  const anchorDir = path10.dirname(file.canonical);
9684
9752
  for (const target of requests.runTargets) {
9685
9753
  const key = flowMemberKey(anchorDir, target);
@@ -9702,6 +9770,7 @@ async function collectFlowMembers(rootPath, rootBytes, args, opts) {
9702
9770
  roots,
9703
9771
  budget,
9704
9772
  opts,
9773
+ seen,
9705
9774
  emit
9706
9775
  );
9707
9776
  }
@@ -9866,7 +9935,8 @@ function createToolsClient(options = {}) {
9866
9935
  opts?.signal?.throwIfAborted();
9867
9936
  const { url, token, remote } = await route();
9868
9937
  let finalArgs = args;
9869
- const meta = await fetchTool(name, { signal: opts?.signal });
9938
+ const tools = await fetchTools({ signal: opts?.signal });
9939
+ const meta = tools.find((t) => t.name === name) ?? null;
9870
9940
  const baselineDirs = [];
9871
9941
  if (meta?.fileInputs?.length) {
9872
9942
  if (remote) assertRequiredPresent(meta, args);
@@ -9876,6 +9946,7 @@ function createToolsClient(options = {}) {
9876
9946
  log: diagnose,
9877
9947
  baselineDirs,
9878
9948
  collectMembers: collectFlowMembers,
9949
+ toolFileInputs: (tool) => tools.find((t) => t.name === tool)?.fileInputs,
9879
9950
  signal: opts?.signal
9880
9951
  });
9881
9952
  }
@@ -16736,7 +16736,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
16736
16736
  var SESSION_ID = randomUUID4();
16737
16737
  function readCliVersion() {
16738
16738
  if (true) {
16739
- return "0.27.1-next.21";
16739
+ return "0.27.1-next.22";
16740
16740
  }
16741
16741
  return "0.0.0";
16742
16742
  }
@@ -23601,6 +23601,35 @@ var FLOW_NAME_CHARSET = "[A-Za-z0-9_-]+";
23601
23601
  var FLOW_NAME_PATTERN = new RegExp(`^${FLOW_NAME_CHARSET}$`);
23602
23602
  var FLOW_FILE_NAME_PATTERN = new RegExp(`^${FLOW_NAME_CHARSET}\\.yaml$`);
23603
23603
  var SCRIPT_FILE_NAME_PATTERN = new RegExp(`^${FLOW_NAME_CHARSET}\\.mjs$`);
23604
+ function interpolateFileInputPath(template, args) {
23605
+ let missing = false;
23606
+ const out = template.replace(/\$\{([A-Za-z0-9_]+)\}/g, (_m, name) => {
23607
+ const v = args[name];
23608
+ if (typeof v !== "string" || v.length === 0) {
23609
+ missing = true;
23610
+ return "";
23611
+ }
23612
+ return v;
23613
+ });
23614
+ return missing ? null : out;
23615
+ }
23616
+ var TOOL_FILE_EXTENSIONS = [".png", ".yaml"];
23617
+ function hasToolFileExtension(file) {
23618
+ const name = file.toLowerCase();
23619
+ return TOOL_FILE_EXTENSIONS.some((extension) => name.endsWith(extension));
23620
+ }
23621
+ function toolStepFiles(specs, args) {
23622
+ const files = [];
23623
+ for (const spec of specs ?? []) {
23624
+ if (spec.skipWhenSet !== void 0 && args[spec.skipWhenSet] !== void 0) continue;
23625
+ const filled = interpolateFileInputPath(spec.path, args);
23626
+ if (filled !== null) files.push({ spec, path: filled });
23627
+ }
23628
+ return files;
23629
+ }
23630
+ function isClientFileArgument({ spec, path: path15 }) {
23631
+ return spec.kind === "file" && spec.path === `\${${spec.target}}` && path15.startsWith("/") && hasToolFileExtension(path15);
23632
+ }
23604
23633
 
23605
23634
  // ../registry/src/flow-file-refs.ts
23606
23635
  import * as fs2 from "node:fs/promises";
@@ -23629,6 +23658,7 @@ function isRecord(value) {
23629
23658
  function collectFlowRequests(doc) {
23630
23659
  const runTargets = /* @__PURE__ */ new Set();
23631
23660
  const snapshots = /* @__PURE__ */ new Set();
23661
+ const toolSteps = [];
23632
23662
  const seen = /* @__PURE__ */ new Set();
23633
23663
  const visit = (steps, depth) => {
23634
23664
  if (!Array.isArray(steps) || depth > MAX_BLOCK_NESTING || seen.has(steps)) return;
@@ -23642,11 +23672,14 @@ function collectFlowRequests(doc) {
23642
23672
  }
23643
23673
  const snapshot = isRecord(step.snapshot) ? step.snapshot.name : step.snapshot;
23644
23674
  if (typeof snapshot === "string" && FLOW_NAME_PATTERN.test(snapshot)) snapshots.add(snapshot);
23675
+ if (typeof step.tool === "string") {
23676
+ toolSteps.push({ tool: step.tool, args: isRecord(step.args) ? step.args : {} });
23677
+ }
23645
23678
  visit(step.steps, depth + 1);
23646
23679
  }
23647
23680
  };
23648
23681
  if (isRecord(doc)) visit(doc.steps, 0);
23649
- return { runTargets: [...runTargets], snapshots: [...snapshots] };
23682
+ return { runTargets: [...runTargets], snapshots: [...snapshots], toolSteps };
23650
23683
  }
23651
23684
  function baselineKeyFor(canonicalPath, flowName) {
23652
23685
  const stem = path2.basename(canonicalPath, ".yaml");
@@ -24636,6 +24669,29 @@ async function readFlowMember(anchorDir, target, roots, budget, opts) {
24636
24669
  const sent = await sendBytes(member, canonical, budget, opts);
24637
24670
  return { ...sent, text: sent.bytes?.toString("utf8") };
24638
24671
  }
24672
+ async function readToolMember(file, roots, budget, opts) {
24673
+ const member = { role: "tool", key: file, path: file };
24674
+ const refuse = (reason) => ({
24675
+ member: { ...member, state: "refused", error: reason },
24676
+ sent: `refused (${reason})`
24677
+ });
24678
+ const { canonical } = await landing(file);
24679
+ if (!roots.some((root) => isWithin(canonical, root))) {
24680
+ return refuse(`${file} is outside every root this client serves (${roots.join(", ")})`);
24681
+ }
24682
+ if (!hasToolFileExtension(canonical)) {
24683
+ return refuse(`${file} links to a file that is not one of ${TOOL_FILE_EXTENSIONS.join(", ")}`);
24684
+ }
24685
+ const st = await stat3(canonical).catch((err) => err);
24686
+ if (st instanceof Error) {
24687
+ if (st.code === "ENOENT" || st.code === "ENOTDIR") {
24688
+ return { member: { ...member, state: "missing" }, sent: "missing" };
24689
+ }
24690
+ return refuse(st.message);
24691
+ }
24692
+ if (st.isDirectory()) return refuse("EISDIR: illegal operation on a directory, read");
24693
+ return sendBytes(member, canonical, budget, opts);
24694
+ }
24639
24695
  function callPlatform(args) {
24640
24696
  if (typeof args.device === "string" && args.device.startsWith("chromium-cdp-")) return "chromium";
24641
24697
  if (typeof args.platform !== "string") return void 0;
@@ -24690,7 +24746,7 @@ async function sendBytes(member, real, budget, opts) {
24690
24746
  bytes
24691
24747
  };
24692
24748
  }
24693
- async function collectBaselineMembers(canonical, flowName, snapshots, args, roots, budget, opts, emit) {
24749
+ async function collectBaselineMembers(canonical, flowName, snapshots, args, roots, budget, opts, sent, emit) {
24694
24750
  const dir = path5.join(
24695
24751
  path5.dirname(canonical),
24696
24752
  "__baselines__",
@@ -24709,6 +24765,7 @@ async function collectBaselineMembers(canonical, flowName, snapshots, args, root
24709
24765
  if (!name.endsWith(".png")) continue;
24710
24766
  if (!updates && !prefixes.some((prefix) => name.startsWith(prefix))) continue;
24711
24767
  const file = path5.join(dir, name);
24768
+ if (sent.has(file)) continue;
24712
24769
  const member = { role: "baseline", key: file, path: file };
24713
24770
  const entry = await baselineEntry(file, roots, updates);
24714
24771
  if (entry.state === "refused") {
@@ -24718,8 +24775,8 @@ async function collectBaselineMembers(canonical, flowName, snapshots, args, root
24718
24775
  } else if (updates) {
24719
24776
  emit({ ...member, state: "listed" }, "listed");
24720
24777
  } else {
24721
- const sent = await sendBytes(member, entry.real, budget, opts);
24722
- emit(sent.member, sent.sent);
24778
+ const sent2 = await sendBytes(member, entry.real, budget, opts);
24779
+ emit(sent2.member, sent2.sent);
24723
24780
  }
24724
24781
  }
24725
24782
  }
@@ -24754,6 +24811,17 @@ async function collectFlowMembers(rootPath, rootBytes, args, opts) {
24754
24811
  }
24755
24812
  const requests = collectFlowRequests(doc);
24756
24813
  for (const name of requests.snapshots) snapshots.add(name);
24814
+ for (const step of requests.toolSteps) {
24815
+ if (step.tool === "flow-execute") continue;
24816
+ for (const file2 of toolStepFiles(opts.toolFileInputs?.(step.tool), step.args)) {
24817
+ const { unwrapWhenSet } = file2.spec;
24818
+ if (unwrapWhenSet !== void 0 && step.args[unwrapWhenSet] !== void 0) continue;
24819
+ if (!isClientFileArgument(file2) || seen.has(file2.path)) continue;
24820
+ seen.add(file2.path);
24821
+ const read = await readToolMember(file2.path, roots, budget, opts);
24822
+ emit(read.member, read.sent);
24823
+ }
24824
+ }
24757
24825
  const anchorDir = path5.dirname(file.canonical);
24758
24826
  for (const target of requests.runTargets) {
24759
24827
  const key = flowMemberKey(anchorDir, target);
@@ -24776,6 +24844,7 @@ async function collectFlowMembers(rootPath, rootBytes, args, opts) {
24776
24844
  roots,
24777
24845
  budget,
24778
24846
  opts,
24847
+ seen,
24779
24848
  emit
24780
24849
  );
24781
24850
  }
@@ -24940,7 +25009,8 @@ function createToolsClient(options = {}) {
24940
25009
  opts?.signal?.throwIfAborted();
24941
25010
  const { url, token, remote } = await route();
24942
25011
  let finalArgs = args;
24943
- const meta2 = await fetchTool(name, { signal: opts?.signal });
25012
+ const tools = await fetchTools({ signal: opts?.signal });
25013
+ const meta2 = tools.find((t) => t.name === name) ?? null;
24944
25014
  const baselineDirs = [];
24945
25015
  if (meta2?.fileInputs?.length) {
24946
25016
  if (remote) assertRequiredPresent(meta2, args);
@@ -24950,6 +25020,7 @@ function createToolsClient(options = {}) {
24950
25020
  log: diagnose,
24951
25021
  baselineDirs,
24952
25022
  collectMembers: collectFlowMembers,
25023
+ toolFileInputs: (tool) => tools.find((t) => t.name === tool)?.fileInputs,
24953
25024
  signal: opts?.signal
24954
25025
  });
24955
25026
  }
@@ -225,7 +225,7 @@ export interface ArgentToolArgs {
225
225
  project_root: string;
226
226
  /** Path to the flow .yaml as readable by the tool-server. Internal — the argent client derives it from project_root and name automatically; leave unset. */
227
227
  flow_file?: string;
228
- /** Omit when name is set. Absolute path to a flow .yaml on the client. Over a link (argent link or ARGENT_TOOLS_URL), the argent client uploads the file, its run: fragments and the snapshot baselines that the run compares, and the tool-server runs those copies. Keep run: fragments under project_root or beside the flow file. With updateBaselines, the client writes the new baselines when the run ends. Over a link, the flow and its fragments must not have script: steps, or tool: steps that take a file or record a flow. Without a link, the tool-server reads the files in place and all step kinds run. */
228
+ /** Omit when name is set. Absolute path to a flow .yaml on the client. Over a link (argent link or ARGENT_TOOLS_URL), the argent client uploads the file, its run: fragments and the snapshot baselines that the run compares, and the tool-server runs those copies. Keep run: fragments under project_root or beside the flow file. With updateBaselines, the client writes the new baselines when the run ends. The client also uploads each absolute .png or .yaml path that a tool: step takes as a file argument, when the file is under project_root or beside the flow file. Over a link, the flow and its fragments must not have: script: steps; tool: steps that run or record a flow; tool: steps that take a directory, an app or an output directory; tool: steps whose tool builds a file path from several arguments; tool: steps that name a relative path or a file other than .png or .yaml. Without a link, the tool-server reads the files in place and all step kinds run. */
229
229
  flow_path?: string;
230
230
  /** Device id to run against (iOS UDID, Android/Vega serial, Chromium id) — the id list-devices reports. Auto-detected when omitted, but only when exactly one booted device matches (optionally narrowed by `platform`); with several booted the run fails and lists them, so pass this explicitly whenever more than one device is up. */
231
231
  device?: string;