@flowriki/agent 0.1.0-beta.2 → 0.1.0-beta.3

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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/engine.ts
4
- import os4 from "os";
4
+ import os5 from "os";
5
5
 
6
6
  // ../agent-core/dist/index.js
7
7
  import { z as z4 } from "zod";
@@ -11,13 +11,14 @@ import crypto22 from "crypto";
11
11
  import { createRequire as createRequire3 } from "module";
12
12
  import { WebSocket, WebSocketServer } from "ws";
13
13
  import fs23 from "fs";
14
+ import os2 from "os";
14
15
  import path23 from "path";
15
16
 
16
17
  // ../orchestrator/dist/index.js
17
18
  import fs4 from "fs";
18
19
  import path3 from "path";
19
20
 
20
- // ../protocol/dist/chunk-CS3GWM4N.js
21
+ // ../protocol/dist/chunk-ROEX7TWK.js
21
22
  import { z } from "zod";
22
23
  var LocatorOverrideSchema = z.object({
23
24
  strategy: z.enum(["css", "xpath", "id", "testId", "text", "label", "placeholder", "altText", "title"]),
@@ -249,6 +250,9 @@ var RecordedStepSchema = RecordedTargetSchema.extend({
249
250
  * stays unresolved and is reported, never invented. */
250
251
  secretRef: z.string().regex(SECRET_REF_PATTERN).optional()
251
252
  }).strict();
253
+ function slugify(name) {
254
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
255
+ }
252
256
  var CURRENT_SCHEMA_VERSION = 5;
253
257
  var FlowDocumentSchema = z.object({
254
258
  schemaVersion: z.literal(CURRENT_SCHEMA_VERSION),
@@ -572,6 +576,7 @@ var ProjectMarkerSchema = z3.object({
572
576
  /** `v1:ed25519:<base64url>` */
573
577
  signature: z3.string()
574
578
  });
579
+ var PRODUCTION_SERVICE = "https://flowriki-cloud.onrender.com";
575
580
 
576
581
  // ../core/dist/index.js
577
582
  function proposeAssertions(before, after) {
@@ -4200,9 +4205,6 @@ function clampRepeat(value) {
4200
4205
  if (!Number.isFinite(n) || n < 2) return DEFAULT_FLAKINESS_REPEAT;
4201
4206
  return Math.min(n, MAX_FLAKINESS_REPEAT);
4202
4207
  }
4203
- function slugify(name) {
4204
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
4205
- }
4206
4208
  function uniqueRef(flowSlug, name, taken) {
4207
4209
  const base = `${flowSlug}_${slugify(name) || "secret"}`;
4208
4210
  if (!taken.has(base)) {
@@ -5427,6 +5429,20 @@ function ensureRootCommit(root) {
5427
5429
  git2(root, ["commit", "--allow-empty", "-m", "Initialise repository"]);
5428
5430
  return git2(root, ["rev-list", "--max-parents=0", "HEAD"]).split("\n").pop();
5429
5431
  }
5432
+ function deliveredRun(delivered) {
5433
+ const { flow, secrets, config } = delivered;
5434
+ return {
5435
+ readConfig: () => config,
5436
+ // By id, and only the one that was sent. A machine serving no checkout has
5437
+ // no other flow to confuse it with.
5438
+ getFlow: (id) => id === flow.id ? flow : null,
5439
+ readSecrets: () => secrets,
5440
+ baseUrlFor: (f) => f.baseUrl ?? config.baseUrl ?? "",
5441
+ environmentFor: (f) => ({ ...config.environment, ...f.environment ?? {} }),
5442
+ // See RunSource.resolveFile: there is no project directory here.
5443
+ resolveFile: () => void 0
5444
+ };
5445
+ }
5430
5446
 
5431
5447
  // ../agent-core/dist/index.js
5432
5448
  var PROTOCOL_VERSION = 1;
@@ -5833,7 +5849,9 @@ var Agent = class {
5833
5849
  `this agent speaks protocol ${PROTOCOL_VERSION}; the command is version ${command.v}`
5834
5850
  );
5835
5851
  }
5836
- const project = this.opts.projects.find((p) => p.id === command.projectId);
5852
+ const delivered = command.args["flow"];
5853
+ const produces = command.name.startsWith("record.") || command.name === "run.cancel";
5854
+ const project = this.opts.projects.find((p) => p.id === command.projectId) ?? (delivered || produces ? { id: command.projectId, name: String(delivered?.name ?? command.projectId), root: "" } : void 0);
5837
5855
  if (!project) {
5838
5856
  return this.refuse(
5839
5857
  command.id,
@@ -6104,6 +6122,15 @@ function dialWebSocket(opts) {
6104
6122
  socket.once("error", failed);
6105
6123
  });
6106
6124
  }
6125
+ var scratchRoots = /* @__PURE__ */ new Map();
6126
+ function scratchRoot(projectId) {
6127
+ let root = scratchRoots.get(projectId);
6128
+ if (!root) {
6129
+ root = fs23.mkdtempSync(path23.join(os2.tmpdir(), `flowriki-record-${projectId.replace(/[^\w-]/g, "")}-`));
6130
+ scratchRoots.set(projectId, root);
6131
+ }
6132
+ return root;
6133
+ }
6107
6134
  function buildHandlers(deps) {
6108
6135
  const { storeFor, storage = null, onActivity, lease, recorder } = deps;
6109
6136
  const debugging = deps.debugger;
@@ -6389,14 +6416,22 @@ function buildHandlers(deps) {
6389
6416
  if (command.flowRevision === void 0) {
6390
6417
  return { ok: false, reason: "stale_revision", detail: "reload the flow and supply its revision before running" };
6391
6418
  }
6392
- const store = storeFor(project.root);
6393
6419
  const flowId = String(command.args["flowId"] ?? "");
6394
- const flow = store.getFlow(flowId);
6420
+ const carried = command.args["flow"];
6421
+ const source = carried ? deliveredRun({
6422
+ flow: carried,
6423
+ secrets: command.args["secrets"] ?? {},
6424
+ config: command.args["config"] ?? {}
6425
+ }) : storeFor(project.root);
6426
+ if (!carried && !project.root) {
6427
+ return { ok: false, reason: "failed", detail: "this machine has no checkout of that project" };
6428
+ }
6429
+ const flow = source.getFlow(flowId);
6395
6430
  if (!flow) return { ok: false, reason: "failed", detail: `no flow ${flowId} in this project` };
6396
6431
  if (flow.revision !== command.flowRevision) {
6397
6432
  return { ok: false, reason: "stale_revision", detail: "the flow changed \u2014 reload it before running" };
6398
6433
  }
6399
- const missing = store.unresolvedSecrets(flow);
6434
+ const missing = carried ? [] : storeFor(project.root).unresolvedSecrets(flow);
6400
6435
  if (missing.length) {
6401
6436
  return {
6402
6437
  ok: false,
@@ -6418,7 +6453,7 @@ function buildHandlers(deps) {
6418
6453
  signal: aborter.signal,
6419
6454
  ...typeof command.args["headless"] === "boolean" ? { headless: command.args["headless"] } : {},
6420
6455
  ...command.args["repeat"] ? { repeat: Number(command.args["repeat"]) } : {},
6421
- store,
6456
+ store: source,
6422
6457
  storage,
6423
6458
  flowId,
6424
6459
  expectedRevision: command.flowRevision,
@@ -6473,14 +6508,28 @@ function buildHandlers(deps) {
6473
6508
  const url = optional("url");
6474
6509
  const route = optional("path");
6475
6510
  const recordedBy = optional("recordedBy");
6511
+ const headless = typeof command.args["headless"] === "boolean" ? command.args["headless"] : void 0;
6512
+ const root = project.root || scratchRoot(project.id);
6513
+ if (!project.root) {
6514
+ if (!/^f_[a-z0-9]+(?:-[a-z0-9]+)*$/.test(featureId)) throw new Error("Invalid feature ID");
6515
+ const workspace = new Store(root);
6516
+ if (!workspace.listFeatures().some((feature) => feature.id === featureId)) workspace.createFeature(featureId.slice(2));
6517
+ }
6476
6518
  const started = await recorder.start({
6477
- root: project.root,
6519
+ // A SCRATCH DIRECTORY when this machine has no checkout of the
6520
+ // project. The recorder writes as it goes — that is what makes a
6521
+ // half-finished recording survivable — and the flow it produces is
6522
+ // returned on stop for the service to commit to the repository. A
6523
+ // machine that has the project checked out still records into it.
6524
+ root,
6525
+ projectId: project.id,
6478
6526
  featureId,
6479
6527
  name,
6480
6528
  ...url ? { url } : {},
6481
6529
  ...route ? { path: route } : {},
6482
6530
  ...recordedBy ? { recordedBy } : {},
6483
- ...command.args["environment"] ? { environment: command.args["environment"] } : {}
6531
+ ...command.args["environment"] ? { environment: command.args["environment"] } : {},
6532
+ ...headless === void 0 ? {} : { headless }
6484
6533
  });
6485
6534
  lease?.activate("run");
6486
6535
  onActivity?.({ kind: "recording", projectId: project.id, detail: name });
@@ -6495,7 +6544,7 @@ function buildHandlers(deps) {
6495
6544
  "record.stop": async (_command, project) => {
6496
6545
  if (!recorder) return { ok: false, reason: "failed", detail: "this agent cannot record \u2014 no browser is configured" };
6497
6546
  try {
6498
- const saved = await recorder.stop();
6547
+ const saved = await recorder.stop(project.id);
6499
6548
  if (!saved.ok) return { ok: false, reason: "failed", detail: saved.error };
6500
6549
  return { ok: true, result: { flow: saved.flow, projectId: project.id } };
6501
6550
  } finally {
@@ -6572,12 +6621,15 @@ function createAgentRuntime(options) {
6572
6621
  let journal = null;
6573
6622
  const coordinators = /* @__PURE__ */ new Map();
6574
6623
  let recordingIn = null;
6624
+ let recordingProject = null;
6625
+ let lastRecording = null;
6575
6626
  const coordinatorFor = (projectRoot) => {
6576
6627
  let existing = coordinators.get(projectRoot);
6577
6628
  if (!existing) {
6578
6629
  existing = new RecordingCoordinator({
6579
6630
  store: new Store(projectRoot),
6580
6631
  emit: (event) => {
6632
+ if (event.type === "recording.stopped") lastRecording = { ok: true, flow: event.flow };
6581
6633
  journal?.append({ name: `recording.${event.type.replace(/^recording\./, "")}`, data: event });
6582
6634
  }
6583
6635
  });
@@ -6636,13 +6688,17 @@ function createAgentRuntime(options) {
6636
6688
  const coordinator = coordinatorFor(opts.root);
6637
6689
  const { root: _root, ...rest } = opts;
6638
6690
  const started = await coordinator.start(rest);
6691
+ recordingProject = opts["projectId"];
6692
+ lastRecording = null;
6639
6693
  recordingIn = coordinator;
6640
6694
  return started;
6641
6695
  },
6642
- stop: async () => {
6643
- if (!recordingIn) return { ok: false, error: "no recording is in progress on this machine" };
6696
+ stop: async (projectId) => {
6697
+ if (projectId && recordingProject && projectId !== recordingProject) return { ok: false, error: "This recording belongs to another project" };
6698
+ if (lastRecording?.ok) return lastRecording;
6699
+ if (!recordingIn) return lastRecording ?? { ok: false, error: "no recording is in progress on this machine" };
6644
6700
  const result = await recordingIn.stop();
6645
- recordingIn = null;
6701
+ lastRecording = result;
6646
6702
  return result;
6647
6703
  },
6648
6704
  discard: async () => {
@@ -6708,20 +6764,20 @@ import fs7 from "fs";
6708
6764
 
6709
6765
  // src/paths.ts
6710
6766
  import fs6 from "fs";
6711
- import os2 from "os";
6767
+ import os3 from "os";
6712
6768
  import path5 from "path";
6713
6769
  function dataDir(env = process.env) {
6714
6770
  const override = env["FLOWRIKI_HOME"]?.trim();
6715
6771
  if (override) return path5.resolve(override);
6716
6772
  if (process.platform === "win32") {
6717
6773
  const appData = env["APPDATA"]?.trim();
6718
- return appData ? path5.join(appData, "Flowriki") : path5.join(os2.homedir(), "AppData", "Roaming", "Flowriki");
6774
+ return appData ? path5.join(appData, "Flowriki") : path5.join(os3.homedir(), "AppData", "Roaming", "Flowriki");
6719
6775
  }
6720
6776
  if (process.platform === "darwin") {
6721
- return path5.join(os2.homedir(), "Library", "Application Support", "Flowriki");
6777
+ return path5.join(os3.homedir(), "Library", "Application Support", "Flowriki");
6722
6778
  }
6723
6779
  const state = env["XDG_STATE_HOME"]?.trim();
6724
- return state ? path5.join(state, "flowriki") : path5.join(os2.homedir(), ".local", "state", "flowriki");
6780
+ return state ? path5.join(state, "flowriki") : path5.join(os3.homedir(), ".local", "state", "flowriki");
6725
6781
  }
6726
6782
  function ensureDataDir(env = process.env) {
6727
6783
  const dir = dataDir(env);
@@ -6788,7 +6844,6 @@ function forgetConnection(service, env = process.env) {
6788
6844
  var fingerprint = (secret) => crypto3.createHash("sha256").update(secret).digest("hex").slice(0, 12);
6789
6845
 
6790
6846
  // src/service.ts
6791
- var PRODUCTION_SERVICE = "https://flowriki-cloud.onrender.com";
6792
6847
  var BadServiceUrl = class extends Error {
6793
6848
  };
6794
6849
  function resolveService(explicit, env = process.env) {
@@ -6811,11 +6866,11 @@ function resolveService(explicit, env = process.env) {
6811
6866
 
6812
6867
  // src/lock.ts
6813
6868
  import fs8 from "fs";
6814
- import os3 from "os";
6869
+ import os4 from "os";
6815
6870
  function takeLock(env = process.env) {
6816
6871
  ensureDataDir(env);
6817
6872
  const file = lockPath(env);
6818
- const mine = { pid: process.pid, since: (/* @__PURE__ */ new Date()).toISOString(), host: os3.hostname() };
6873
+ const mine = { pid: process.pid, since: (/* @__PURE__ */ new Date()).toISOString(), host: os4.hostname() };
6819
6874
  for (let attempt = 0; attempt < 2; attempt += 1) {
6820
6875
  try {
6821
6876
  fs8.writeFileSync(file, JSON.stringify(mine), { flag: "wx", mode: 384 });
@@ -6891,7 +6946,7 @@ function openBrowser(url, env = process.env) {
6891
6946
  }
6892
6947
 
6893
6948
  // src/engine.ts
6894
- var AGENT_VERSION = "0.1.0-beta.2";
6949
+ var AGENT_VERSION = "0.1.0-beta.3";
6895
6950
  async function connect(options = {}) {
6896
6951
  let service;
6897
6952
  try {
@@ -6908,7 +6963,7 @@ async function connect(options = {}) {
6908
6963
  message: "the stored connection is no longer accepted \u2014 asking for a new one"
6909
6964
  });
6910
6965
  }
6911
- const machineName = options.machineName?.trim() || `${os4.userInfo().username}@${os4.hostname()}`;
6966
+ const machineName = options.machineName?.trim() || `${os5.userInfo().username}@${os5.hostname()}`;
6912
6967
  let asked;
6913
6968
  try {
6914
6969
  const res = await fetch(`${service}/api/pairing-requests`, {
@@ -7122,7 +7177,7 @@ async function doctor(serviceArg) {
7122
7177
  }
7123
7178
  function inspectPlaywright() {
7124
7179
  try {
7125
- const browsersPath = process.env["PLAYWRIGHT_BROWSERS_PATH"] ?? (process.platform === "win32" ? path6.join(os4.homedir(), "AppData", "Local", "ms-playwright") : process.platform === "darwin" ? path6.join(os4.homedir(), "Library", "Caches", "ms-playwright") : path6.join(os4.homedir(), ".cache", "ms-playwright"));
7180
+ const browsersPath = process.env["PLAYWRIGHT_BROWSERS_PATH"] ?? (process.platform === "win32" ? path6.join(os5.homedir(), "AppData", "Local", "ms-playwright") : process.platform === "darwin" ? path6.join(os5.homedir(), "Library", "Caches", "ms-playwright") : path6.join(os5.homedir(), ".cache", "ms-playwright"));
7126
7181
  const chromiumInstalled = fs9.existsSync(browsersPath) ? fs9.readdirSync(browsersPath).some((d) => d.startsWith("chromium")) : false;
7127
7182
  return { version: null, browsersPath, chromiumInstalled };
7128
7183
  } catch {
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  resolveCheckouts,
8
8
  start,
9
9
  status
10
- } from "./chunk-EYRGRDNB.js";
10
+ } from "./chunk-XT52RW5S.js";
11
11
 
12
12
  // src/cli.ts
13
13
  import { parseArgs } from "util";
package/dist/engine.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  resolveCheckouts,
8
8
  start,
9
9
  status
10
- } from "./chunk-EYRGRDNB.js";
10
+ } from "./chunk-XT52RW5S.js";
11
11
  export {
12
12
  AGENT_VERSION,
13
13
  connect,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowriki/agent",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.3",
4
4
  "description": "Run Flowriki UI tests on your own computer. Connects this machine to your Flowriki account.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",