@swmansion/argent 0.27.1-next.20 → 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";
@@ -9390,6 +9419,8 @@ function isRecord(value) {
9390
9419
  }
9391
9420
  function collectFlowRequests(doc) {
9392
9421
  const runTargets = /* @__PURE__ */ new Set();
9422
+ const snapshots = /* @__PURE__ */ new Set();
9423
+ const toolSteps = [];
9393
9424
  const seen = /* @__PURE__ */ new Set();
9394
9425
  const visit = (steps, depth) => {
9395
9426
  if (!Array.isArray(steps) || depth > MAX_BLOCK_NESTING || seen.has(steps)) return;
@@ -9401,11 +9432,20 @@ function collectFlowRequests(doc) {
9401
9432
  const target = completeRunExtension(run2);
9402
9433
  if (FLOW_FILE_NAME_PATTERN.test(path2.posix.basename(target))) runTargets.add(target);
9403
9434
  }
9435
+ const snapshot = isRecord(step.snapshot) ? step.snapshot.name : step.snapshot;
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
+ }
9404
9440
  visit(step.steps, depth + 1);
9405
9441
  }
9406
9442
  };
9407
9443
  if (isRecord(doc)) visit(doc.steps, 0);
9408
- return { runTargets: [...runTargets] };
9444
+ return { runTargets: [...runTargets], snapshots: [...snapshots], toolSteps };
9445
+ }
9446
+ function baselineKeyFor(canonicalPath, flowName) {
9447
+ const stem = path2.basename(canonicalPath, ".yaml");
9448
+ return FLOW_NAME_PATTERN.test(stem) ? stem : flowName;
9409
9449
  }
9410
9450
  function flowMemberKey(anchorDir, target) {
9411
9451
  return `${anchorDir}\0${target}`;
@@ -24461,7 +24501,17 @@ function parseLinkTarget(input) {
24461
24501
  // ../argent-tools-client/src/file-inputs.ts
24462
24502
  import { createHash as createHash2, randomUUID as randomUUID3 } from "node:crypto";
24463
24503
  import { createReadStream, rmSync } from "node:fs";
24464
- import { mkdir as mkdir3, readFile as readFile3, rm as rm2, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
24504
+ import {
24505
+ chmod as chmod3,
24506
+ lstat,
24507
+ mkdir as mkdir3,
24508
+ readFile as readFile3,
24509
+ realpath as realpath2,
24510
+ rename as rename2,
24511
+ rm as rm2,
24512
+ stat as stat2,
24513
+ writeFile as writeFile3
24514
+ } from "node:fs/promises";
24465
24515
  import { constants, tmpdir } from "node:os";
24466
24516
  import * as path4 from "node:path";
24467
24517
 
@@ -24770,9 +24820,61 @@ function isAllowedClientFilePath(p) {
24770
24820
  function isClientFileDirective2(value) {
24771
24821
  return !!value && typeof value === "object" && value[CLIENT_FILE_MARKER2] === true && typeof value.path === "string" && typeof value.content === "string";
24772
24822
  }
24773
- async function applyClientFileDirectives(result) {
24823
+ async function replaceFile(target, bytes, mode) {
24824
+ const temp = path4.join(path4.dirname(target), `.baseline-${randomUUID3()}.tmp`);
24825
+ try {
24826
+ await writeFile3(temp, bytes, { flag: "wx" });
24827
+ if (mode !== void 0) await chmod3(temp, mode & 511);
24828
+ await rename2(temp, target);
24829
+ } catch (err) {
24830
+ await rm2(temp, { force: true });
24831
+ throw err;
24832
+ }
24833
+ }
24834
+ async function writeBaselineFile(file2, bytes, allowedDirs) {
24835
+ if (!path4.isAbsolute(file2) || path4.normalize(file2) !== file2 || file2.split(/[\\/]/).includes("..") || !file2.endsWith(".png") || !allowedDirs.includes(path4.dirname(file2))) {
24836
+ return allowedDirs.length === 0 ? "this call writes no baselines" : `${file2} is not a baseline of this call (${allowedDirs.map((d) => `${d}/<name>.png`).join(", ")})`;
24837
+ }
24838
+ if (bytes.length > MAX_CONTENT_BYTES) {
24839
+ return `${file2}: the baseline is larger than the 32 MiB cap on a file this client writes`;
24840
+ }
24841
+ const dir = path4.dirname(file2);
24842
+ await mkdir3(dir, { recursive: true });
24843
+ const link2 = await lstat(file2).catch(() => null);
24844
+ const real = link2 === null ? null : await realpath2(file2).catch(() => null);
24845
+ if (link2 !== null && real === null) return `${file2} is a symbolic link to a missing file`;
24846
+ if (real !== null) {
24847
+ if (!real.endsWith(".png")) return `${file2} links to a file that is not a PNG file`;
24848
+ if (path4.dirname(real) !== await realpath2(dir)) {
24849
+ return `${file2} links outside its baseline directory`;
24850
+ }
24851
+ }
24852
+ const existing = real === null ? null : await stat2(real);
24853
+ if (existing !== null && !existing.isFile()) return `${file2} is not a regular file`;
24854
+ await replaceFile(
24855
+ real ?? path4.join(await realpath2(dir), path4.basename(file2)),
24856
+ bytes,
24857
+ existing?.mode
24858
+ );
24859
+ return null;
24860
+ }
24861
+ async function applyClientFileDirectives(result, opts = {}) {
24774
24862
  const written = [];
24863
+ const failed = [];
24775
24864
  async function walk(value) {
24865
+ if (isClientFileDirective2(value) && value.encoding === "base64") {
24866
+ const error52 = await writeBaselineFile(
24867
+ value.path,
24868
+ Buffer.from(value.content, "base64"),
24869
+ opts.allowedDirs ?? []
24870
+ ).catch((err) => err instanceof Error ? err.message : String(err));
24871
+ if (error52 !== null) {
24872
+ failed.push({ path: value.path, error: error52 });
24873
+ return { path: value.path, error: error52 };
24874
+ }
24875
+ written.push(value.path);
24876
+ return value.path;
24877
+ }
24776
24878
  if (isClientFileDirective2(value)) {
24777
24879
  if (!isAllowedClientFilePath(value.path)) return null;
24778
24880
  try {
@@ -24785,7 +24887,9 @@ async function applyClientFileDirectives(result) {
24785
24887
  }
24786
24888
  }
24787
24889
  if (Array.isArray(value)) {
24788
- return Promise.all(value.map(walk));
24890
+ const out = [];
24891
+ for (const item of value) out.push(await walk(item));
24892
+ return out;
24789
24893
  }
24790
24894
  if (value && typeof value === "object") {
24791
24895
  const out = {};
@@ -24797,12 +24901,12 @@ async function applyClientFileDirectives(result) {
24797
24901
  return value;
24798
24902
  }
24799
24903
  const rewritten = await walk(result);
24800
- return { result: rewritten, written };
24904
+ return { result: rewritten, written, failed };
24801
24905
  }
24802
24906
 
24803
24907
  // ../argent-tools-client/src/flow-files.ts
24804
24908
  var import_yaml = __toESM(require_dist(), 1);
24805
- import { readFile as readFile4, realpath as realpath2, stat as stat3 } from "node:fs/promises";
24909
+ import { readdir as readdir4, readFile as readFile4, realpath as realpath3, stat as stat3 } from "node:fs/promises";
24806
24910
  import * as path5 from "node:path";
24807
24911
  var INLINE_MEMBERS_BYTES = 256 * 1024;
24808
24912
  var FLOW_FILES_LOG_ENV = "ARGENT_FLOW_FILES_LOG";
@@ -24837,7 +24941,7 @@ function savedFlowProject(file2) {
24837
24941
  }
24838
24942
  async function landing(spelled) {
24839
24943
  try {
24840
- const canonical = await realpath2(spelled);
24944
+ const canonical = await realpath3(spelled);
24841
24945
  return { canonical, fence: canonical };
24842
24946
  } catch (err) {
24843
24947
  const failure = err.code === "ENOENT" ? { missing: true } : { error: err.message };
@@ -24846,7 +24950,7 @@ async function landing(spelled) {
24846
24950
  for (; ; ) {
24847
24951
  const parent = path5.dirname(at);
24848
24952
  if (parent === at) return { canonical, fence: spelled, ...failure };
24849
- const real = await realpath2(parent).catch(() => null);
24953
+ const real = await realpath3(parent).catch(() => null);
24850
24954
  if (real !== null) {
24851
24955
  return { canonical, fence: path5.join(real, path5.basename(at)), ...failure };
24852
24956
  }
@@ -24866,7 +24970,7 @@ async function closureRoots(rootPath, rootCanonical, projectRoot) {
24866
24970
  }
24867
24971
  const real = [];
24868
24972
  for (const candidate of candidates) {
24869
- const resolved = await realpath2(candidate).catch(() => null);
24973
+ const resolved = await realpath3(candidate).catch(() => null);
24870
24974
  if (resolved !== null && !real.includes(resolved)) real.push(resolved);
24871
24975
  }
24872
24976
  return real.filter((root) => !real.some((other) => other !== root && isWithin(root, other)));
@@ -24904,31 +25008,120 @@ async function readFlowMember(anchorDir, target, roots, budget, opts) {
24904
25008
  if (!/\.ya?ml$/i.test(path5.basename(canonical))) {
24905
25009
  return refuse(`${target} links to a file that is not a YAML file`);
24906
25010
  }
24907
- const read = await readFileInputWire(canonical, { includeContent: true });
25011
+ const sent = await sendBytes(member, canonical, budget, opts);
25012
+ return { ...sent, text: sent.bytes?.toString("utf8") };
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
+ }
25037
+ function callPlatform(args) {
25038
+ if (typeof args.device === "string" && args.device.startsWith("chromium-cdp-")) return "chromium";
25039
+ if (typeof args.platform !== "string") return void 0;
25040
+ return args.platform === "ios-remote" ? "ios" : args.platform;
25041
+ }
25042
+ async function baselineEntry(file2, roots, forWrite) {
25043
+ const { canonical, error: error52 } = await landing(file2);
25044
+ if (!roots.some((root) => isWithin(canonical, root))) {
25045
+ return {
25046
+ state: "refused",
25047
+ error: `${file2} is outside every root this client serves (${roots.join(", ")})`
25048
+ };
25049
+ }
25050
+ if (error52 !== void 0) return { state: "refused", error: error52 };
25051
+ const st = await stat3(canonical).catch((err) => err);
25052
+ if (st instanceof Error) {
25053
+ if (st.code !== "ENOENT") return { state: "refused", error: st.message };
25054
+ return forWrite ? { state: "refused", error: `${file2} is a symbolic link to a missing file` } : { state: "missing" };
25055
+ }
25056
+ if (!canonical.endsWith(".png")) {
25057
+ return { state: "refused", error: `${file2} links to a file that is not a PNG file` };
25058
+ }
25059
+ if (forWrite && !st.isFile()) return { state: "refused", error: `${file2} is not a regular file` };
25060
+ if (st.isDirectory()) {
25061
+ return { state: "refused", error: "EISDIR: illegal operation on a directory, read" };
25062
+ }
25063
+ return st.isFile() ? { state: "ok", real: canonical } : { state: "missing" };
25064
+ }
25065
+ async function sendBytes(member, real, budget, opts) {
25066
+ const read = await readFileInputWire(real, { includeContent: true });
24908
25067
  if (read?.contentOmitted) {
24909
- return refuse(`${canonical} is larger than the 32 MiB cap on a file sent to the tool-server`);
25068
+ const error52 = `${real} is larger than the 32 MiB cap on a file sent to the tool-server`;
25069
+ return { member: { ...member, state: "refused", error: error52 }, sent: `refused (${error52})` };
24910
25070
  }
24911
25071
  if (read?.content === void 0) {
24912
- return refuse(
24913
- await readFile4(canonical).then(
24914
- () => `${canonical} could not be read on this client`,
24915
- (err) => err instanceof Error ? err.message : String(err)
24916
- )
25072
+ const error52 = await readFile4(real).then(
25073
+ () => `${real} could not be read on this client`,
25074
+ (err) => err instanceof Error ? err.message : String(err)
24917
25075
  );
25076
+ return { member: { ...member, state: "refused", error: error52 }, sent: `refused (${error52})` };
24918
25077
  }
24919
- const text2 = Buffer.from(read.content, "base64").toString("utf8");
25078
+ const bytes = Buffer.from(read.content, "base64");
24920
25079
  const size = read.size ?? 0;
24921
25080
  if (budget.inline + size <= INLINE_MEMBERS_BYTES || !opts.uploadEndpoint) {
24922
25081
  budget.inline += size;
24923
- return { member: { ...member, ...read }, text: text2, sent: `inline ${size}` };
25082
+ return { member: { ...member, ...read }, sent: `inline ${size}`, bytes };
24924
25083
  }
24925
- const uploaded = await uploadFile(canonical, opts.uploadEndpoint, opts.signal);
25084
+ const uploaded = await uploadFile(real, opts.uploadEndpoint, opts.signal);
24926
25085
  return {
24927
25086
  member: { ...member, size: read.size, mtimeMs: read.mtimeMs, ...uploaded },
24928
- text: text2,
24929
- sent: `upload ${size}`
25087
+ sent: `upload ${size}`,
25088
+ bytes
24930
25089
  };
24931
25090
  }
25091
+ async function collectBaselineMembers(canonical, flowName, snapshots, args, roots, budget, opts, sent, emit) {
25092
+ const dir = path5.join(
25093
+ path5.dirname(canonical),
25094
+ "__baselines__",
25095
+ baselineKeyFor(canonical, flowName)
25096
+ );
25097
+ const real = (await landing(dir)).canonical;
25098
+ if (!roots.some((root) => isWithin(real, root))) return;
25099
+ const updates = args.updateBaselines === true;
25100
+ if (updates) opts.baselineDirs?.push(dir);
25101
+ const platform = callPlatform(args);
25102
+ const prefixes = snapshots.map(
25103
+ (name) => `${name}__${platform === void 0 ? "" : `${platform}-`}`
25104
+ );
25105
+ const names = await readdir4(dir).catch(() => []);
25106
+ for (const name of names.sort()) {
25107
+ if (!name.endsWith(".png")) continue;
25108
+ if (!updates && !prefixes.some((prefix) => name.startsWith(prefix))) continue;
25109
+ const file2 = path5.join(dir, name);
25110
+ if (sent.has(file2)) continue;
25111
+ const member = { role: "baseline", key: file2, path: file2 };
25112
+ const entry = await baselineEntry(file2, roots, updates);
25113
+ if (entry.state === "refused") {
25114
+ emit({ ...member, state: "refused", error: entry.error }, `refused (${entry.error})`);
25115
+ } else if (entry.state === "missing") {
25116
+ emit({ ...member, state: "missing" }, "missing");
25117
+ } else if (updates) {
25118
+ emit({ ...member, state: "listed" }, "listed");
25119
+ } else {
25120
+ const sent2 = await sendBytes(member, entry.real, budget, opts);
25121
+ emit(sent2.member, sent2.sent);
25122
+ }
25123
+ }
25124
+ }
24932
25125
  async function collectFlowMembers(rootPath, rootBytes, args, opts) {
24933
25126
  if (!namesValidFlow(args)) return {};
24934
25127
  const canonical = await canonicalFlowPath(rootPath);
@@ -24940,10 +25133,16 @@ async function collectFlowMembers(rootPath, rootBytes, args, opts) {
24940
25133
  const roots = await closureRoots(rootPath, canonical, args.project_root);
24941
25134
  const members = [];
24942
25135
  const seen = /* @__PURE__ */ new Set();
25136
+ const snapshots = /* @__PURE__ */ new Set();
24943
25137
  const budget = { inline: 0 };
24944
25138
  const logging = process.env[FLOW_FILES_LOG_ENV] === "1";
24945
25139
  const log2 = opts.log ?? ((line) => void process.stderr.write(`${line}
24946
25140
  `));
25141
+ const emit = (member, sent) => {
25142
+ members.push(member);
25143
+ const subject = member.role === "flow" ? member.canonical : member.key;
25144
+ if (logging) log2(printable(`[flow-files] ${member.role} ${subject}: ${sent}`));
25145
+ };
24947
25146
  const queue = [{ canonical, text: rootBytes.toString("utf8"), hop: 0 }];
24948
25147
  for (let file2 = queue.shift(); file2 !== void 0; file2 = queue.shift()) {
24949
25148
  let doc;
@@ -24952,19 +25151,45 @@ async function collectFlowMembers(rootPath, rootBytes, args, opts) {
24952
25151
  } catch {
24953
25152
  continue;
24954
25153
  }
25154
+ const requests = collectFlowRequests(doc);
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
+ }
24955
25167
  const anchorDir = path5.dirname(file2.canonical);
24956
- for (const target of collectFlowRequests(doc).runTargets) {
25168
+ for (const target of requests.runTargets) {
24957
25169
  const key = flowMemberKey(anchorDir, target);
24958
25170
  if (seen.has(key)) continue;
24959
25171
  seen.add(key);
24960
25172
  const { member, text: text2, sent } = await readFlowMember(anchorDir, target, roots, budget, opts);
24961
- members.push(member);
24962
- if (logging) log2(printable(`[flow-files] flow ${member.canonical}: ${sent}`));
25173
+ emit(member, sent);
24963
25174
  if (text2 !== void 0 && file2.hop + 1 < MAX_RUN_DEPTH) {
24964
25175
  queue.push({ canonical: member.canonical, text: text2, hop: file2.hop + 1 });
24965
25176
  }
24966
25177
  }
24967
25178
  }
25179
+ if (snapshots.size > 0) {
25180
+ const flowName = path5.basename(rootPath, ".yaml");
25181
+ await collectBaselineMembers(
25182
+ canonical,
25183
+ flowName,
25184
+ [...snapshots],
25185
+ args,
25186
+ roots,
25187
+ budget,
25188
+ opts,
25189
+ seen,
25190
+ emit
25191
+ );
25192
+ }
24968
25193
  return { canonical, spelling, members };
24969
25194
  }
24970
25195
 
@@ -24999,7 +25224,20 @@ async function consumeToolStream(name, body, onProgress, signal) {
24999
25224
  }
25000
25225
  };
25001
25226
  const decoder = new TextDecoder();
25002
- let buffered = "";
25227
+ let pieces = [];
25228
+ const take = (text2) => {
25229
+ let start = 0;
25230
+ let newline;
25231
+ while ((newline = text2.indexOf("\n", start)) !== -1) {
25232
+ signal?.throwIfAborted();
25233
+ pieces.push(text2.slice(start, newline));
25234
+ const line = pieces.join("");
25235
+ pieces = [];
25236
+ start = newline + 1;
25237
+ handleLine(line);
25238
+ }
25239
+ if (start < text2.length) pieces.push(text2.slice(start));
25240
+ };
25003
25241
  try {
25004
25242
  for (; ; ) {
25005
25243
  let chunk;
@@ -25011,17 +25249,11 @@ async function consumeToolStream(name, body, onProgress, signal) {
25011
25249
  }
25012
25250
  const { done, value } = chunk;
25013
25251
  if (done) break;
25014
- buffered += decoder.decode(value, { stream: true });
25015
- let newline;
25016
- while ((newline = buffered.indexOf("\n")) !== -1) {
25017
- signal?.throwIfAborted();
25018
- const line = buffered.slice(0, newline);
25019
- buffered = buffered.slice(newline + 1);
25020
- handleLine(line);
25021
- }
25252
+ take(decoder.decode(value, { stream: true }));
25022
25253
  }
25023
- buffered += decoder.decode();
25024
- if (buffered.trim()) handleLine(buffered);
25254
+ take(decoder.decode());
25255
+ const last = pieces.join("");
25256
+ if (last.trim()) handleLine(last);
25025
25257
  } catch (err) {
25026
25258
  void reader.cancel().catch(() => {
25027
25259
  });
@@ -25031,8 +25263,7 @@ async function consumeToolStream(name, body, onProgress, signal) {
25031
25263
  if (!final) {
25032
25264
  throw brokenStream(name, "the stream ended without a result", progress);
25033
25265
  }
25034
- const { result: data } = await applyClientFileDirectives(final.data);
25035
- return { data, note: final.note };
25266
+ return { data: final.data, note: final.note };
25036
25267
  }
25037
25268
  function errorBodyMessage(body) {
25038
25269
  if (Array.isArray(body.issues) && typeof body.message === "string") return body.message;
@@ -25120,17 +25351,31 @@ function createToolsClient(options = {}) {
25120
25351
  opts?.signal?.throwIfAborted();
25121
25352
  const { url: url2, token, remote } = await route();
25122
25353
  let finalArgs = args;
25123
- 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;
25356
+ const baselineDirs = [];
25124
25357
  if (meta3?.fileInputs?.length) {
25125
25358
  if (remote) assertRequiredPresent(meta3, args);
25126
25359
  finalArgs = await prepareFileInputs(meta3.fileInputs, args ?? {}, {
25127
25360
  includeContent: remote,
25128
25361
  uploadEndpoint: remote ? { url: url2, token } : void 0,
25129
25362
  log: diagnose,
25363
+ baselineDirs,
25130
25364
  collectMembers: collectFlowMembers,
25365
+ toolFileInputs: (tool) => tools2.find((t2) => t2.name === tool)?.fileInputs,
25131
25366
  signal: opts?.signal
25132
25367
  });
25133
25368
  }
25369
+ const settle = async (data) => {
25370
+ const applied = await applyClientFileDirectives(data, { allowedDirs: baselineDirs });
25371
+ for (const { path: file2, error: error52 } of applied.failed) {
25372
+ diagnose(`The baseline ${file2} was not written on this client: ${error52}`);
25373
+ }
25374
+ if (process.env.ARGENT_FLOW_FILES_LOG === "1") {
25375
+ for (const file2 of applied.written) diagnose(`[flow-files] baseline ${file2}: written`);
25376
+ }
25377
+ return applied.result;
25378
+ };
25134
25379
  const stream = opts?.onProgress !== void 0 || carriesMembers(finalArgs);
25135
25380
  const sentOnce = carriesUpload(finalArgs);
25136
25381
  const res = await doFetch(
@@ -25162,7 +25407,11 @@ function createToolsClient(options = {}) {
25162
25407
  }),
25163
25408
  opts?.signal
25164
25409
  );
25165
- return { ...streamed, outputHint: meta3?.outputHint };
25410
+ return {
25411
+ data: await settle(streamed.data),
25412
+ note: streamed.note,
25413
+ outputHint: meta3?.outputHint
25414
+ };
25166
25415
  }
25167
25416
  let json2;
25168
25417
  try {
@@ -25186,17 +25435,16 @@ function createToolsClient(options = {}) {
25186
25435
  });
25187
25436
  }
25188
25437
  opts?.signal?.throwIfAborted();
25189
- const { result: data } = await applyClientFileDirectives(json2.data);
25190
- return { data, note: json2.note, outputHint: meta3?.outputHint };
25438
+ return { data: await settle(json2.data), note: json2.note, outputHint: meta3?.outputHint };
25191
25439
  }
25192
25440
  return { fetchTools, fetchTool, callTool, baseUrl };
25193
25441
  }
25194
25442
 
25195
25443
  // ../argent-tools-client/src/artifacts.ts
25196
- import { copyFile, mkdir as mkdir4, readFile as readFile5, realpath as realpath3, rm as rm3, stat as stat4, writeFile as writeFile4 } from "node:fs/promises";
25444
+ import { copyFile, mkdir as mkdir4, readFile as readFile5, realpath as realpath4, rm as rm3, stat as stat4, writeFile as writeFile4 } from "node:fs/promises";
25197
25445
  import { constants as fsConstants } from "node:fs";
25198
25446
  import { tmpdir as tmpdir2 } from "node:os";
25199
- import { basename as basename6, dirname as dirname7, extname as extname2, isAbsolute as isAbsolute5, join as join11, normalize, resolve as resolve6, sep as sep4 } from "node:path";
25447
+ import { basename as basename6, dirname as dirname7, extname as extname2, isAbsolute as isAbsolute5, join as join11, normalize as normalize2, resolve as resolve6, sep as sep4 } from "node:path";
25200
25448
  import { createHash as createHash3 } from "node:crypto";
25201
25449
 
25202
25450
  // ../configuration-core/src/flags.ts
@@ -25935,8 +26183,8 @@ function durableBaseDir() {
25935
26183
  const projectRoot = findProjectRoot(process.cwd());
25936
26184
  return projectRoot ?? dirname7(argentHomeDir());
25937
26185
  }
25938
- var ALLOWED_SAVE_DIRS = /* @__PURE__ */ new Set([normalize(".argent/recordings")]);
25939
- var RECORDINGS_SAVE_DIR = normalize(".argent/recordings");
26186
+ var ALLOWED_SAVE_DIRS = /* @__PURE__ */ new Set([normalize2(".argent/recordings")]);
26187
+ var RECORDINGS_SAVE_DIR = normalize2(".argent/recordings");
25940
26188
  function configuredRecordingsDir() {
25941
26189
  let value;
25942
26190
  try {
@@ -25995,7 +26243,7 @@ async function writeDurableUnique(dir, filename, write) {
25995
26243
  }
25996
26244
  function durableSaveTarget(handle) {
25997
26245
  if (typeof handle.saveDir !== "string" || !handle.saveDir || handle.archive) return null;
25998
- const rel = normalize(handle.saveDir);
26246
+ const rel = normalize2(handle.saveDir);
25999
26247
  if (isAbsolute5(rel) || rel === ".." || rel.startsWith(`..${sep4}`) || rel.split(sep4).includes("..")) {
26000
26248
  return null;
26001
26249
  }
@@ -26017,8 +26265,8 @@ function durableSaveTarget(handle) {
26017
26265
  }
26018
26266
  async function confineToRealBase(dir, base, rel) {
26019
26267
  try {
26020
- const realDir = await realpath3(dir);
26021
- const realBase = await realpath3(base);
26268
+ const realDir = await realpath4(dir);
26269
+ const realBase = await realpath4(base);
26022
26270
  return realDir === join11(realBase, rel);
26023
26271
  } catch {
26024
26272
  return false;
@@ -29700,7 +29948,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
29700
29948
  var SESSION_ID2 = randomUUID5();
29701
29949
  function readCliVersion() {
29702
29950
  if (true) {
29703
- return "0.27.1-next.20";
29951
+ return "0.27.1-next.22";
29704
29952
  }
29705
29953
  return "0.0.0";
29706
29954
  }
@@ -31053,11 +31301,16 @@ counts the remaining flows skipped.
31053
31301
 
31054
31302
  The CLI sends a run to its tool-server: the local one that starts
31055
31303
  automatically, or the one that \`argent link\` or ARGENT_TOOLS_URL names. Over
31056
- a link, the CLI uploads the flow file and the run: fragments that it reaches.
31057
- The tool-server rejects the flow before the first step when the flow or a
31058
- fragment has one of these steps:
31059
- - script: or snapshot:
31060
- - a tool: step that takes a file or records a flow
31304
+ a link, the CLI uploads the flow file, the run: fragments that it reaches and
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
31061
31314
  Without a link, all step kinds run.
31062
31315
 
31063
31316
  Subcommands:
@@ -31596,6 +31849,14 @@ function isFlowReport(data) {
31596
31849
  const steps = data?.steps;
31597
31850
  return Array.isArray(steps) && steps.every((step) => !!step && typeof step === "object");
31598
31851
  }
31852
+ function unwrittenBaselines(report) {
31853
+ const writes = report.baselineWrites;
31854
+ if (!Array.isArray(writes)) return [];
31855
+ return writes.flatMap((write) => {
31856
+ const { path: file2, error: error52 } = write ?? {};
31857
+ return typeof file2 === "string" && typeof error52 === "string" ? [`${file2}: ${error52}`] : [];
31858
+ });
31859
+ }
31599
31860
  async function runFlowDirectory(dir, args, projectRoot, options) {
31600
31861
  const printError = (message, err = message) => {
31601
31862
  console.error(args.json ? JSON.stringify(errorRecord(err, message)) : message);
@@ -31666,8 +31927,17 @@ async function runFlowDirectory(dir, args, projectRoot, options) {
31666
31927
  flowPath,
31667
31928
  baseUrl
31668
31929
  );
31669
- results.push({ path: rel, status: report.ok ? "pass" : "fail", report });
31930
+ const unwritten = unwrittenBaselines(report);
31931
+ results.push({ path: rel, status: report.ok && !unwritten.length ? "pass" : "fail", report });
31670
31932
  if (!report.ok) failures.push({ path: rel, ...summarizeFailure(report), rerun });
31933
+ else if (unwritten.length) {
31934
+ failures.push({
31935
+ path: rel,
31936
+ headline: "baselines not written",
31937
+ detail: unwritten.join("\n"),
31938
+ rerun
31939
+ });
31940
+ }
31671
31941
  if (!args.json) {
31672
31942
  for (const line of renderFailedSteps(report)) console.log(line);
31673
31943
  console.log(` ${renderSummary(report, { withDevice: true })}`);
@@ -31928,7 +32198,12 @@ ${renderSummary(report, { withDevice: true })}`);
31928
32198
  } else {
31929
32199
  console.log(renderReport(report));
31930
32200
  }
31931
- return exitAfterFlush(report.ok ? 0 : 1);
32201
+ const unwritten = unwrittenBaselines(report);
32202
+ if (!args.json && !args.jsonStream) {
32203
+ for (const line of unwritten)
32204
+ console.log(` ${STATUS_GLYPH.error} baseline not written: ${line}`);
32205
+ }
32206
+ return exitAfterFlush(report.ok && unwritten.length === 0 ? 0 : 1);
31932
32207
  }
31933
32208
 
31934
32209
  // ../argent-cli/src/tools.ts