flowstride 1.0.20-rc.2 → 1.0.20-rc.6

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.
Files changed (39) hide show
  1. package/NATIVE_CI.md +58 -34
  2. package/README.md +33 -40
  3. package/dist/adapters/web/playwright.js +6 -4
  4. package/dist/ci/config.js +6 -5
  5. package/dist/ci/runtime.js +27 -14
  6. package/dist/cli/commands/run.js +2 -1
  7. package/dist/cli/index.js +35 -50
  8. package/dist/cli/manual_server/workspace_server.js +44 -32
  9. package/dist/cli/utils/cloudSession.d.ts +7 -0
  10. package/dist/cli/utils/cloudSession.js +37 -0
  11. package/dist/cli/utils/proInstaller.d.ts +5 -0
  12. package/dist/cli/utils/proInstaller.js +141 -0
  13. package/dist/game/sdk.js +3 -2
  14. package/dist/index.d.ts +5 -0
  15. package/dist/index.js +9 -0
  16. package/dist/manual/security.d.ts +183 -0
  17. package/dist/manual/security.js +106 -0
  18. package/dist/runner/data_generators.d.ts +1 -0
  19. package/dist/runner/data_generators.js +3 -0
  20. package/dist/runner/dom_stripper.js +3 -0
  21. package/dist/runner/flow_orchestrator.js +13 -22
  22. package/dist/runner/flow_worker.d.ts +1 -0
  23. package/dist/runner/flow_worker.js +29 -13
  24. package/dist/runner/persistent_vault.js +2 -1
  25. package/dist/runner/plugin_manager.js +3 -1
  26. package/dist/security/localDashboard.d.ts +2 -0
  27. package/dist/security/localDashboard.js +22 -0
  28. package/dist/security/redaction.d.ts +4 -0
  29. package/dist/security/redaction.js +49 -0
  30. package/dist/types/index.d.ts +7 -0
  31. package/dist/utils/cloud.d.ts +6 -0
  32. package/dist/utils/cloud.js +33 -0
  33. package/dist/utils/projectPaths.d.ts +11 -0
  34. package/dist/utils/projectPaths.js +90 -0
  35. package/package.json +21 -14
  36. package/src/ui/dist/assets/index-DVwp0K-J.js +462 -0
  37. package/src/ui/dist/index.html +1 -1
  38. package/RELEASE_NOTES.md +0 -40
  39. package/src/ui/dist/assets/index-DIC22ZK_.js +0 -638
package/NATIVE_CI.md CHANGED
@@ -2,8 +2,7 @@
2
2
 
3
3
  This candidate adds native CI and game APIs to both engines. Managed Cloud
4
4
  execution remains retired. Existing npm OSS / private Pro archive distribution
5
- and fallback remain supported. Naija Throne's current runner is unchanged;
6
- its migration is a separate acceptance step. Native CI uses protocol 1.
5
+ and fallback remain supported. Consumer project migrations are managed separately. Native CI uses protocol 1.
7
6
  The public CLI selects compatible Pro engines or logs the reason and falls back
8
7
  to OSS. Older Pro CLIs may own the npm binary and do not understand the new
9
8
  commands; install both candidates when testing the Pro path.
@@ -19,9 +18,9 @@ currently hosted Cloud download still serves the existing release; this local
19
18
  candidate does not change that endpoint or its archive.
20
19
 
21
20
  ```sh
22
- npm install --save-dev /absolute/path/flowstride-1.0.20-rc.2.tgz
21
+ npm install --save-dev /absolute/path/flowstride-1.0.20-rc.6.tgz
23
22
  # Optional: install the local matching Pro candidate after project dependencies.
24
- npm install --no-save /absolute/path/flowstride-pro-1.0.15-rc.1.tgz
23
+ npm install --no-save /absolute/path/flowstride-pro-1.0.15-rc.5.tgz
25
24
  npx --no-install flowstride install
26
25
  npx --no-install flowstride run --ci
27
26
  ```
@@ -55,8 +54,9 @@ SIGINT/SIGTERM produce interrupted results. The run owns its optional local
55
54
  server and refuses an already-serving readiness URL.
56
55
 
57
56
  Cloud reporting is off by default in native CI. `cloudSync: true` explicitly
58
- allows existing authenticated reporting; its existing server-side security and
59
- storage limitations still apply. Normal interactive reporting and the restored
57
+ allows existing authenticated reporting; matching Cloud deployment and configured media storage are required. Reporting and
58
+ video status are written separately in the summary and `.flowstride/cloud-sync.json`;
59
+ a reporting failure does not turn a passed test into a failed test. Normal interactive reporting and the restored
60
60
  Cloud URL overrides remain separate from retired managed execution.
61
61
 
62
62
  `npx flowstride ci init github` generates a workflow explicitly, never during
@@ -122,34 +122,58 @@ three.module.js and three.core.js. OSS CLI handoff tests use
122
122
 
123
123
  Run the real archive install/handoff harness against both candidates, including
124
124
  missing/broken Pro and revoked-token fallback. Validate GitHub's Linux execution,
125
- real licensed download and Naija Throne migration after local acceptance.
125
+ real licensed download after local acceptance.
126
126
  No push, npm publication, Cloud archive replacement or Naija Throne migration
127
127
  is performed by building these candidates.
128
128
 
129
- ## Candidate acceptance automation
130
-
131
- The repository's `Native CI acceptance` workflow tests Node 22 and 24 on Linux.
132
- It uses a checksum-pinned archive in `.github/fixtures`, checks its compiled engine
133
- against a fresh build, checks referenced dashboard assets, then installs that exact
134
- archive in a clean consumer. It exercises native browser installation, passing and
135
- deliberately failing game execution, media, server cleanup and workflow preservation.
136
- It also runs the native source suite, including delayed uploads and cancellation.
137
- The Pro archive belongs only in the private Pro repository.
138
-
139
- These jobs do not publish npm packages, replace the hosted Pro archive, deploy
140
- Cloud, or migrate application repositories. Native Cloud reporting stays opt-in.
141
- Qualification covers Chromium and emulated desktop/phone/landscape profiles.
142
- Physical Android/iOS devices, Safari/Firefox and other game engines are outside
143
- this candidate's verified support scope. Real Cloud licensing, reporting and
144
- video storage require a separate live acceptance run.
145
-
146
- ## OSS npm preview release
147
-
148
- OSS `1.0.20-rc.2` requires Node.js 22.12.0+. Publish the verified archive under
149
- `next`; install with `npm install --save-dev flowstride@next` after publication.
150
- Use the release archive and checksum rather than rebuilding from a dirty checkout.
151
- The matching local Pro candidate remains `1.0.15-rc.1`; hosted Pro remains
152
- `1.0.14`. Complete authentication before reinstalling the local Pro candidate.
153
- The unchanged dashboard bundle is reused from the previously qualified rc.1
154
- archive; the engine is rebuilt from this source. The release input manifest
155
- records both that archive's checksum and the merged base commit.
129
+ ## Combined candidate behavior
130
+
131
+ Node.js 22.12 or newer is required. Flow discovery is recursive; `.first.flow`
132
+ files run before ordinary files. Reports identify nested flows by their relative
133
+ paths, and same-named videos receive distinct filenames. A failed scenario skips
134
+ its remaining steps and continues to later scenarios. `--bail` stops later work;
135
+ Pro executes bail runs sequentially. `only.Scenario:` selects that file's focus
136
+ scenario and report planning uses the same selection.
137
+
138
+ Configuration, plugin, environment, upload, vault and output paths must stay
139
+ inside the project, including resolved symlinks. Move shared fixtures into the
140
+ project rather than referring to external files. `FLOWSTRIDE_API_URL` takes
141
+ precedence over `NEXTJS_WEBHOOK_URL`; the legacy completion suffix is normalized.
142
+ Managed runner commands remain retired.
143
+
144
+ Logs redact credential fields, known generated secrets and signed capabilities.
145
+ Redaction cannot guarantee removal of arbitrary application data or visible
146
+ secrets in screenshots/videos. Review artifact access and test data accordingly.
147
+
148
+ The Pro installer rejects unsafe or incomplete archives, downloads above 32 MiB,
149
+ and checksum mismatches. Older Cloud servers without checksum headers remain
150
+ compatible and emit a warning; `FLOWSTRIDE_PRO_SHA256` can pin an expected digest.
151
+ On failed npm installation the previous Pro directory and CLI launchers are
152
+ restored. This is not a transaction over every project dependency npm may alter;
153
+ use a disposable project for candidate acceptance.
154
+
155
+ ## Dashboard tab history
156
+
157
+ Completed test tabs restore that file's last captured step by default. Selecting
158
+ an earlier step remembers its history cursor separately for each file. A sidebar
159
+ step also selects the owning tab. Return to Live applies only to the selected
160
+ file, and a new suite/rerun clears old cursors and screens. Runner console and
161
+ network panels show the selected file's records; the full Logs view retains
162
+ suite-wide history. Flows/steps without screenshots show an explicit empty
163
+ state rather than another flow's screen.
164
+
165
+ Run `npm run test:dashboard:unit` for the Zustand store/history unit tests and
166
+ `npm run test:dashboard` for those tests plus the real Chromium dashboard
167
+ regression. The browser regression runs real local browser flows, then replays
168
+ their worker events over WebSocket into the built dashboard. It also supplies
169
+ explicit API-only/background-event fixtures. No live Cloud is required.
170
+
171
+ ### Local cURL debugging
172
+
173
+ The interactive local dashboard keeps its cURL preview [REDACTED]. Copy raw cURL
174
+ uses the original request held separately in local memory, including its headers
175
+ and body. Other log fields stay redacted. Native CI and Cloud reporting continue redacting cURL and
176
+ credentials. Raw cURL is not restored for already-redacted historical runs.
177
+ The interactive runner binds to 127.0.0.1 and restricts HTTP Host/Origin and
178
+ WebSocket Origin to its own loopback address and port. Raw commands remain in
179
+ local debugger memory and IPC; they are not written to CI or Cloud reports.
package/README.md CHANGED
@@ -6,34 +6,6 @@ Flowstride OS combines the human-readable structure of Behavior-Driven Developme
6
6
 
7
7
  [Documentation](https://docs.flowstride.io/)
8
8
 
9
- ## Native CI and game testing preview — 1.0.20-rc.2
10
-
11
- This is an opt-in preview intended for the npm `next` tag. The default stable
12
- release remains available through `latest`. After the preview is published:
13
-
14
- ```sh
15
- npm install --save-dev flowstride@next
16
- npx --no-install flowstride install
17
- npx --no-install flowstride run --ci
18
- ```
19
-
20
- Node.js 22.12.0+ is required; acceptance covers the minimum version and current
21
- Node.js 22/24 on Linux, plus Node.js 24 on macOS. The preview adds native CI
22
- exit codes, JSON/JUnit reports, videos/screenshots, device profiles, explicit
23
- GitHub workflow generation, and supported canvas/Three.js testing APIs.
24
- See [native CI setup](NATIVE_CI.md) and [release notes](RELEASE_NOTES.md).
25
-
26
- For this preview, use OSS alone or manually install the matching private Pro
27
- `1.0.15-rc.1` candidate after project dependencies. Normal Cloud login still
28
- serves Pro `1.0.14`; signing in afterward can replace the preview's Pro package,
29
- and that older CLI does not support the new native commands. Cloud reporting
30
- and video-storage defects remain open. Native CI keeps Cloud reporting off by
31
- default. Managed Cloud execution remains retired.
32
-
33
- This release qualifies Chromium desktop and emulated phone/landscape profiles
34
- and browser-game fixtures. It does not establish physical-phone, Safari,
35
- Firefox, other game-engine, or complete application-game coverage.
36
-
37
9
  ---
38
10
 
39
11
  ## Why Flowstride?
@@ -49,7 +21,7 @@ Traditional E2E frameworks force you to choose between highly technical code (wh
49
21
 
50
22
  ## Installation & Quick Start
51
23
 
52
- Flowstride requires Node.js 22.12.0 or higher. Use a current patched Node.js 22 or 24 release.
24
+ Flowstride requires Node.js 18 or higher.
53
25
 
54
26
  **1. Install globally (or as a dev dependency in your project):**
55
27
 
@@ -99,20 +71,41 @@ npx flowstride login
99
71
 
100
72
  ---
101
73
 
102
- ## Execution data and Cloud integration
74
+ ## Privacy & Data Security
75
+
76
+ Flowstride is designed with a **zero-trust, local-first architecture**. Whether you are testing internal microservices, staging environments, or production APIs, your sensitive data, PHI, and credentials remain strictly on your infrastructure.
77
+
78
+ ### Open Source Core: 100% Local Isolation
79
+
80
+ The Open Source version of Flowstride operates entirely as an ephemeral, local execution engine.
81
+
82
+ - **Air-Gapped Execution:** Flowstride does not "phone home." The OS runner executes strictly on your local machine or within your private CI/CD pipeline. There is no external Flowstride database storing your test payloads or network logs.
83
+
84
+ - **Ephemeral Memory:** Flowstride is inherently stateless. API request headers, response bodies, and DOM snapshots exist only in Node.js RAM during execution. The millisecond the execution process terminates, all memory is wiped.
85
+
86
+ - **The DevTools Analogy:** Our network interception works natively within Playwright's browser context routing. It is fundamentally identical to opening the Chrome DevTools "Network" tab locally. We do not act as an external Man-In-The-Middle (MITM) proxy.
87
+
88
+ - **Opt-In Local State:** Artifacts saved to disk—such as execution videos (`.flowstride/mock-cloud/`) or explicitly persisted browser states (`.flowstride/sessions/`)—are written strictly to your local file system to mirror the functionality of tools like Postman. _(Note: We strongly recommend adding `.flowstride/` to your `.gitignore` to prevent accidental credential commits)._
89
+
90
+ ### Flowstride Enterprise: Secure Cloud Telemetry
91
+
92
+ For teams using Flowstride Enterprise to aggregate multi-flow metrics, we maintain a strict metadata-only boundary.
93
+
94
+ - **Metadata, Not Payloads:** When cloud sync is active, Flowstride routes only execution metadata to your team's dashboard (e.g., Workspace ID, execution duration, total steps, and pass/fail tallies). **Zero bytes** of request headers, bearer tokens, or JSON bodies are ever transmitted to Flowstride Cloud.
95
+
96
+ - **Deep Debugging Stays Local:** The rich, interactive visual dashboard, including the JSON explorer and API Replay tools, runs on a dynamic, locally bound port on your machine. You get the UI of a modern cloud app with the security of a local binary.
97
+
98
+ ### CI/CD Leak Protection & Terminal Hygiene
99
+
100
+ Standard testing frameworks do dump failing API requests, and response bodies directly into the terminal so developers can debug them. In a CI/CD environment, this is a massive security liability, as those secrets are permanently burned into plaintext Jenkins, GitHub Actions, or GitLab CI logs.
101
+
102
+ Flowstride completely neutralizes this threat by architecturally separating execution output from diagnostic telemetry:
103
103
 
104
- Tests run on your machine or CI runner. Native CI writes reports, screenshots,
105
- videos and attachments to the configured local artifact directory. Interactive
106
- API diagnostics are available in the local dashboard. These outputs can contain
107
- application data or credentials; automatic redaction is not guaranteed.
104
+ 1. **Clean Standard Output (`stdout`):** Flowstride's terminal output only displays high-level, human-readable BDD execution steps (e.g., `✔ Given "User lands on home page"` or `✖ Action Failed`). No JSON request bodies, `cURL` strings, or bearer tokens are ever printed to the terminal console.
108
105
 
109
- Cloud login/licensing, FlowMail OTP retrieval and explicitly enabled Cloud
110
- reporting contact their configured services. Native CI leaves Cloud reporting
111
- off unless `cloudSync: true` is explicitly configured. Existing Cloud reporting
112
- and storage limitations remain documented in the preview release notes.
106
+ 2. **WebSocket Isolation:** All sensitive raw network data (payloads, headers, and responses) is emitted _exclusively_ over a secure, ephemeral WebSocket connection directly to the developer's local UI dashboard.
113
107
 
114
- Use test credentials, keep runtime state and artifacts out of source control,
115
- and choose CI artifact access and retention appropriate to the test data.
108
+ 3. **Total Evaporation:** Because CI/CD runners only capture `stdout` text and do not connect to local WebSockets, your highly sensitive diagnostic data simply evaporates the moment the pipeline execution ends. Your pipeline logs remain 100% free of plaintext secrets.
116
109
 
117
110
  ---
118
111
 
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.WebAdapter = void 0;
40
+ const projectPaths_1 = require("../../utils/projectPaths");
40
41
  const playwright_1 = require("playwright");
41
42
  const fs = __importStar(require("fs"));
42
43
  const path = __importStar(require("path"));
@@ -81,7 +82,7 @@ class WebAdapter {
81
82
  ...(this.config.ci ? { handleSIGINT: false, handleSIGTERM: false } : {}),
82
83
  args: launchArgs,
83
84
  });
84
- const videoDir = path.join(process.cwd(), ".flowstride", "videos");
85
+ const videoDir = (0, projectPaths_1.projectPath)(process.cwd(), path.join('.flowstride', 'videos'));
85
86
  if (!fs.existsSync(videoDir))
86
87
  fs.mkdirSync(videoDir, { recursive: true });
87
88
  const profile = this.config.device === 'phone' || this.config.device === 'landscape'
@@ -125,7 +126,7 @@ class WebAdapter {
125
126
  };
126
127
  }
127
128
  async injectAudio(filePath) {
128
- const absolutePath = path.resolve(process.cwd(), filePath);
129
+ const absolutePath = (0, projectPaths_1.projectPath)(process.cwd(), filePath);
129
130
  if (!fs.existsSync(absolutePath)) {
130
131
  throw new Error(`Audio file not found at: ${absolutePath}`);
131
132
  }
@@ -274,7 +275,8 @@ class WebAdapter {
274
275
  });
275
276
  }
276
277
  synthesizeCurl(method, url, headers, postData) {
277
- let curl = `curl -X ${method.toUpperCase()} '${url}'`;
278
+ const safeUrl = url.replace(/'/g, "'\\''");
279
+ let curl = `curl -X ${method.toUpperCase()} '${safeUrl}'`;
278
280
  for (const [key, value] of Object.entries(headers)) {
279
281
  if (key.startsWith(":"))
280
282
  continue;
@@ -614,7 +616,7 @@ class WebAdapter {
614
616
  const targetLoc = await this.checkAmbiguity(loc, selector);
615
617
  await targetLoc.waitFor({ state: "visible" });
616
618
  await targetLoc.scrollIntoViewIfNeeded();
617
- const absolutePath = path.resolve(process.cwd(), filePath);
619
+ const absolutePath = (0, projectPaths_1.projectPath)(process.cwd(), filePath);
618
620
  if (!fs.existsSync(absolutePath)) {
619
621
  throw new Error(`File Upload Error: Could not find the file at "${absolutePath}". Please ensure the file exists in your project directory.`);
620
622
  }
package/dist/ci/config.js CHANGED
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.isCIEnvironment = isCIEnvironment;
7
7
  exports.loadNativeConfig = loadNativeConfig;
8
+ const projectPaths_1 = require("../utils/projectPaths");
8
9
  const node_fs_1 = __importDefault(require("node:fs"));
9
10
  const node_path_1 = __importDefault(require("node:path"));
10
11
  function isCIEnvironment() {
@@ -12,7 +13,7 @@ function isCIEnvironment() {
12
13
  .some(key => !!process.env[key] && process.env[key] !== 'false' && process.env[key] !== '0');
13
14
  }
14
15
  function loadNativeConfig(root, options) {
15
- const file = node_path_1.default.join(root, options.config || 'flowstride.config.json');
16
+ const file = (0, projectPaths_1.projectPath)(root, options.config || 'flowstride.config.json');
16
17
  let input = {};
17
18
  if (node_fs_1.default.existsSync(file)) {
18
19
  input = JSON.parse(node_fs_1.default.readFileSync(file, 'utf8'));
@@ -21,8 +22,8 @@ function loadNativeConfig(root, options) {
21
22
  }
22
23
  else if (options.config)
23
24
  throw new Error(`Configuration not found: ${file}`);
24
- const config = { ...input, timeout: input.timeout ?? 30000, headless: input.headless ?? true,
25
- ci: !!options.ci || isCIEnvironment(), device: options.device ?? input.device ?? 'desktop',
25
+ const config = { ...input, projectRoot: node_path_1.default.resolve(root), timeout: input.timeout ?? 30000, headless: input.headless ?? true,
26
+ ci: !!options.ci || isCIEnvironment(), bail: options.bail || input.bail || false, device: options.device ?? input.device ?? 'desktop',
26
27
  artifactsDir: input.artifactsDir ?? 'flowstride-artifacts', testDir: input.testDir ?? 'flows',
27
28
  defaultDialogBehavior: input.defaultDialogBehavior ?? 'dismiss',
28
29
  recordVideo: input.recordVideo ?? true, browserErrors: input.browserErrors ?? 'fail',
@@ -50,7 +51,7 @@ function loadNativeConfig(root, options) {
50
51
  continue;
51
52
  if (typeof config[key] !== 'string' || !config[key].trim())
52
53
  throw new Error(`${key} must be a nonempty path.`);
53
- const resolved = node_path_1.default.resolve(root, config[key]);
54
+ const resolved = (0, projectPaths_1.projectPath)(root, config[key]);
54
55
  const relative = node_path_1.default.relative(root, resolved);
55
56
  if ((key === 'artifactsDir' && !relative) || relative === '..' || relative.startsWith(`..${node_path_1.default.sep}`) || node_path_1.default.isAbsolute(relative))
56
57
  throw new Error(`${key} must be a directory inside the project.`);
@@ -67,7 +68,7 @@ function loadNativeConfig(root, options) {
67
68
  throw new Error('webServer readiness URL must use local HTTP(S).');
68
69
  if (server.timeout !== undefined && (!Number.isSafeInteger(server.timeout) || server.timeout < 1))
69
70
  throw new Error('webServer.timeout must be positive.');
70
- server.cwd = node_path_1.default.resolve(root, server.cwd || '.');
71
+ server.cwd = (0, projectPaths_1.projectPath)(root, server.cwd || '.');
71
72
  const relative = node_path_1.default.relative(root, server.cwd);
72
73
  if (relative === '..' || relative.startsWith(`..${node_path_1.default.sep}`) || node_path_1.default.isAbsolute(relative))
73
74
  throw new Error('webServer.cwd must be inside the project.');
@@ -4,19 +4,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runNativeCI = runNativeCI;
7
+ const node_crypto_1 = require("node:crypto");
8
+ const projectPaths_1 = require("../utils/projectPaths");
7
9
  const node_fs_1 = __importDefault(require("node:fs"));
8
10
  const node_path_1 = __importDefault(require("node:path"));
9
11
  const node_child_process_1 = require("node:child_process");
10
12
  const promises_1 = require("node:timers/promises");
11
13
  const lexer_1 = require("../parser/lexer");
12
14
  const parser_1 = require("../parser/parser");
13
- function listFlows(target) {
14
- const stat = node_fs_1.default.statSync(target);
15
- if (stat.isFile())
16
- return target.endsWith('.flow') ? [target] : [];
17
- return node_fs_1.default.readdirSync(target).filter(name => name.endsWith('.flow')).sort().map(name => node_path_1.default.join(target, name));
18
- }
19
- function safeName(name) { return name.replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 150); }
15
+ function safeName(name) { return name.replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 120) + '-' + (0, node_crypto_1.createHash)('sha256').update(name).digest('hex').slice(0, 12); }
20
16
  function xml(value) { return String(value).replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '').replace(/[<>&"']/g, char => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&apos;' }[char])); }
21
17
  async function runNativeCI(worker, orchestrator, target, config, engine) {
22
18
  const started = Date.now();
@@ -27,6 +23,8 @@ async function runNativeCI(worker, orchestrator, target, config, engine) {
27
23
  const steps = new Map();
28
24
  const completed = new Set();
29
25
  const errors = [];
26
+ const planned = new Map();
27
+ let bailed = false;
30
28
  let expected = 0;
31
29
  let files = [];
32
30
  let server;
@@ -36,6 +34,8 @@ async function runNativeCI(worker, orchestrator, target, config, engine) {
36
34
  if (!data?.payload)
37
35
  return;
38
36
  const p = data.payload;
37
+ if (data.type === 'BAIL')
38
+ bailed = true;
39
39
  if (data.type === 'LOG_EVENT' && p.status === 'error') {
40
40
  const active = [...steps.values()].reverse().find(step => step.fileName === p.fileName && step.status === 'running');
41
41
  if (active)
@@ -56,6 +56,8 @@ async function runNativeCI(worker, orchestrator, target, config, engine) {
56
56
  errors.push(`${p.fileName}: ${p.error || 'Execution failed'}`);
57
57
  if (data.type === 'BROWSER_ERROR' && config.browserErrors !== 'record')
58
58
  errors.push(`Browser: ${p.message}`);
59
+ if (data.type === 'CLOUD_SYNC')
60
+ config.cloudSyncResult = p;
59
61
  if (data.type === 'WORKER_ERROR')
60
62
  errors.push(p.message || 'Worker failed');
61
63
  };
@@ -67,11 +69,18 @@ async function runNativeCI(worker, orchestrator, target, config, engine) {
67
69
  process.on('SIGINT', interrupt);
68
70
  process.on('SIGTERM', interrupt);
69
71
  try {
70
- files = listFlows(target);
72
+ files = (0, projectPaths_1.discoverFlows)(target, config.projectRoot || process.cwd());
73
+ config.flowRoot = node_fs_1.default.statSync(target).isDirectory() ? target : node_path_1.default.dirname(target);
71
74
  if (!files.length)
72
75
  throw new Error('No .flow tests found.');
73
76
  for (const file of files) {
74
- const features = new parser_1.Parser(new lexer_1.Lexer(node_fs_1.default.readFileSync(file, 'utf8')).tokenise()).parse();
77
+ const features = (0, projectPaths_1.selectedFeatures)(new parser_1.Parser(new lexer_1.Lexer(node_fs_1.default.readFileSync(file, 'utf8')).tokenise()).parse());
78
+ for (const feature of features)
79
+ for (const scenario of feature.scenarios)
80
+ for (const step of scenario.steps) {
81
+ const fileName = (0, projectPaths_1.flowIdentity)(file, config.flowRoot);
82
+ planned.set(`${fileName}:${step.id}`, { id: step.id, description: step.blockDescription, command: step.action, fileName, status: 'skipped', reason: 'bail' });
83
+ }
75
84
  expected += features.reduce((n, feature) => n + feature.scenarios.reduce((m, scenario) => m + scenario.steps.length, 0), 0);
76
85
  }
77
86
  if (!expected)
@@ -150,26 +159,30 @@ async function runNativeCI(worker, orchestrator, target, config, engine) {
150
159
  }
151
160
  if (interrupted)
152
161
  errors.push('Run interrupted.');
162
+ if (bailed)
163
+ for (const [key, step] of planned)
164
+ if (!steps.has(key))
165
+ steps.set(key, step);
153
166
  const results = [...steps.values()];
154
167
  const terminal = results.filter(step => ['passed', 'failed', 'skipped', 'not_tested'].includes(step.status));
155
168
  const incomplete = Math.max(0, expected - terminal.length);
156
169
  for (const file of files)
157
- if (!completed.has(node_path_1.default.basename(file)))
158
- errors.push(`Incomplete file: ${node_path_1.default.basename(file)}`);
170
+ if (!bailed && !completed.has((0, projectPaths_1.flowIdentity)(file, config.flowRoot)))
171
+ errors.push(`Incomplete file: ${(0, projectPaths_1.flowIdentity)(file, config.flowRoot)}`);
159
172
  if (results.some(step => !['passed', 'failed', 'skipped', 'not_tested'].includes(step.status)))
160
173
  errors.push('Unfinished test steps.');
161
174
  if (terminal.length > expected)
162
175
  errors.push('Engine produced an unexpected step count.');
163
176
  const metrics = { total: expected, passed: results.filter(s => s.status === 'passed').length,
164
177
  failed: results.filter(s => s.status === 'failed').length, skipped: results.filter(s => ['skipped', 'not_tested'].includes(s.status)).length, incomplete };
165
- const result = { protocol: 1, engine, status: interrupted ? 'interrupted' : errors.length || metrics.failed || incomplete ? 'failed' : 'passed',
166
- duration: Date.now() - started, artifactsDir: directory, files: files.map(file => node_path_1.default.basename(file)), steps: results, errors, metrics };
178
+ const result = { cloudSync: config.cloudSyncResult, protocol: 1, engine, status: interrupted ? 'interrupted' : errors.length || metrics.failed || incomplete ? 'failed' : 'passed',
179
+ duration: Date.now() - started, artifactsDir: directory, files: files.map(file => (0, projectPaths_1.flowIdentity)(file, config.flowRoot)), steps: results, errors, metrics };
167
180
  // Existing workers finalize videos into these locations. Copy only run files,
168
181
  // never the project directory or credentials.
169
182
  if (config.recordVideo !== false)
170
183
  for (const file of files) {
171
184
  for (const folder of ['artifacts', 'mock-cloud']) {
172
- const video = node_path_1.default.join(process.cwd(), '.flowstride', folder, node_path_1.default.basename(file, '.flow') + '.webm');
185
+ const video = node_path_1.default.join(process.cwd(), '.flowstride', folder, (0, projectPaths_1.videoFileName)((0, projectPaths_1.flowIdentity)(file, config.flowRoot)));
173
186
  if (node_fs_1.default.existsSync(video) && node_fs_1.default.statSync(video).mtimeMs >= started) {
174
187
  node_fs_1.default.copyFileSync(video, node_path_1.default.join(directory, node_path_1.default.basename(video)));
175
188
  break;
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.runAction = runAction;
37
+ const projectPaths_1 = require("../../utils/projectPaths");
37
38
  const config_1 = require("../../ci/config");
38
39
  const runtime_1 = require("../../ci/runtime");
39
40
  const fs = __importStar(require("fs"));
@@ -53,7 +54,7 @@ const api_proxy_1 = require("../../sdk/api_proxy");
53
54
  async function runAction(targetPath, options) {
54
55
  const root = process.cwd();
55
56
  const profileEnvPath = options.env
56
- ? path.resolve(root, `.env.${options.env}`)
57
+ ? (0, projectPaths_1.projectPath)(root, `.env.${options.env}`)
57
58
  : null;
58
59
  const defaultEnvPath = path.resolve(root, ".env");
59
60
  if (profileEnvPath) {
package/dist/cli/index.js CHANGED
@@ -37,6 +37,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  return (mod && mod.__esModule) ? mod : { "default": mod };
38
38
  };
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
+ const cloud_1 = require("../utils/cloud");
41
+ const cloudSession_1 = require("./utils/cloudSession");
40
42
  const config_1 = require("../ci/config");
41
43
  const init_1 = require("../ci/init");
42
44
  const commander_1 = require("commander");
@@ -47,16 +49,21 @@ const open_1 = require("./commands/open");
47
49
  const fs = __importStar(require("fs"));
48
50
  const path = __importStar(require("path"));
49
51
  const os = __importStar(require("os"));
50
- const child_process_1 = require("child_process");
52
+ const proInstaller_1 = require("./utils/proInstaller");
51
53
  const prompts_1 = require("@inquirer/prompts");
52
54
  const chalk_1 = __importDefault(require("chalk"));
53
55
  const ora_1 = __importDefault(require("ora"));
54
56
  const program = new commander_1.Command();
55
- const API_BASE = process.env.FLOWSTRIDE_API_URL || "https://cloud.flowstride.io";
57
+ program.hook('preAction', (_program, command) => {
58
+ if (['login', 'status', 'run', 'update'].includes(command.name())) {
59
+ API_BASE = (0, cloudSession_1.loadCloudEnvironment)(process.cwd(), command.opts().env);
60
+ }
61
+ });
62
+ let API_BASE = (0, cloud_1.cloudApiBase)(process.env);
56
63
  program
57
64
  .name("flowstride")
58
65
  .description("Flowstride: The Enterprise Flow-First Automation CLI")
59
- .version("1.0.20-rc.2");
66
+ .version("1.0.20-rc.6");
60
67
  program
61
68
  .command("init")
62
69
  .description("Initialise a new Flowstride workspace in the current directory")
@@ -101,6 +108,7 @@ program
101
108
  console.error(chalk_1.default.red("✖ Error: A valid Developer Token is required."));
102
109
  process.exit(1);
103
110
  }
111
+ console.log(`[Cloud]: ${API_BASE}; login credential source: ${process.env.FLOWSTRIDE_TOKEN ? 'FLOWSTRIDE_TOKEN (environment)' : options.token ? '--token option' : 'interactive prompt'}`);
104
112
  const deviceName = os.hostname();
105
113
  const spinner = (0, ora_1.default)("Authenticating with Cloud...").start();
106
114
  try {
@@ -114,10 +122,11 @@ program
114
122
  });
115
123
  const data = await response.json();
116
124
  if (!response.ok) {
117
- spinner.fail(chalk_1.default.red(`Authentication Failed: ${data.error || "Invalid token or network error"}`));
125
+ spinner.fail(chalk_1.default.red(`Authentication Failed: ${(0, cloud_1.cloudFailure)({ response: { status: response.status, data } }, [token.trim()]).message}`));
118
126
  process.exit(1);
119
127
  }
120
128
  spinner.succeed(chalk_1.default.green("Authentication Successful!"));
129
+ console.log((0, cloudSession_1.tokenExpiryDescription)(data));
121
130
  const configDir = options.global
122
131
  ? path.join(os.homedir(), ".flowstride")
123
132
  : path.join(process.cwd(), ".flowstride");
@@ -146,42 +155,14 @@ program
146
155
  }
147
156
  if (installPro) {
148
157
  const installSpinner = (0, ora_1.default)("Downloading Enterprise Engine via secure proxy...").start();
149
- const tarballPath = path.join(process.cwd(), "flowstride-pro.tgz");
150
158
  try {
151
- const tarballResponse = await fetch(`${API_BASE}/api/workspace/download-pro`, {
152
- method: "GET",
153
- headers: { Authorization: `Bearer ${token.trim()}` },
154
- });
155
- if (!tarballResponse.ok) {
156
- throw new Error(`Secure proxy rejected request: ${tarballResponse.statusText}`);
157
- }
158
- const buffer = await tarballResponse.arrayBuffer();
159
- fs.writeFileSync(tarballPath, Buffer.from(buffer));
160
- installSpinner.text = "Wiring up Enterprise dependencies...";
161
- installSpinner.color = "cyan";
162
- // Let npm natively handle the extraction and binary symlinking
163
- (0, child_process_1.execSync)(`npm install "${tarballPath}" --no-save`, {
164
- cwd: process.cwd(),
165
- stdio: "ignore",
166
- });
159
+ await (0, proInstaller_1.installProEngine)(API_BASE, token.trim());
167
160
  installSpinner.succeed(chalk_1.default.green("Enterprise Engine installed successfully!"));
168
161
  }
169
- catch (installError) {
170
- installSpinner.stop();
171
- console.error(chalk_1.default.bgRed.white("\n[RAW SYSTEM ERROR]:"), installError);
172
- console.log(chalk_1.default.red("\n✖ Failed to install the Enterprise Engine. Please check your internet connection or verify your project configuration."));
173
- if (fs.existsSync(nodeModulesPath)) {
174
- fs.rmSync(nodeModulesPath, { recursive: true, force: true });
175
- }
176
- console.log(chalk_1.default.yellow("⚠️ Reverting login: Enterprise Engine is required to sync."));
177
- if (fs.existsSync(credentialsPath))
178
- fs.unlinkSync(credentialsPath);
179
- process.exit(1);
180
- }
181
- finally {
182
- if (fs.existsSync(tarballPath)) {
183
- fs.unlinkSync(tarballPath);
184
- }
162
+ catch (error) {
163
+ installSpinner.fail(error instanceof Error ? error.message : "Enterprise installation failed");
164
+ console.log(chalk_1.default.yellow("Credentials remain available. You can retry installation; runs can fall back to the Open Source engine."));
165
+ process.exitCode = 1;
185
166
  }
186
167
  }
187
168
  else {
@@ -196,7 +177,7 @@ program
196
177
  console.log(chalk_1.default.bold.green(`\n✅ Machine synchronized with Flowstride Enterprise.`));
197
178
  }
198
179
  catch (error) {
199
- spinner.fail(chalk_1.default.red(`Network Error: Could not connect to cloud.flowstride.io`));
180
+ spinner.fail(chalk_1.default.red((0, cloud_1.cloudFailure)(error, [token.trim()]).message));
200
181
  process.exit(1);
201
182
  }
202
183
  });
@@ -204,7 +185,8 @@ program
204
185
  .command("status")
205
186
  .description("Display the current Flowstride CLI authentication and engine tier")
206
187
  .action(async () => {
207
- let token = process.env.FLOWSTRIDE_TOKEN || null;
188
+ console.log((0, cloudSession_1.cloudSessionDescription)(API_BASE));
189
+ let token = process.env.FLOWSTRIDE_TOKEN?.trim() || null;
208
190
  const localCredentialsPath = path.join(process.cwd(), ".flowstride", "credentials.json");
209
191
  const globalCredentialsPath = path.join(os.homedir(), ".flowstride", "credentials.json");
210
192
  let activeCredentialsPath = localCredentialsPath; // Default for cleanup
@@ -213,7 +195,7 @@ program
213
195
  activeCredentialsPath = localCredentialsPath;
214
196
  try {
215
197
  const credentials = JSON.parse(fs.readFileSync(localCredentialsPath, "utf8"));
216
- token = credentials.machineToken;
198
+ token = credentials.machineToken?.trim() || null;
217
199
  }
218
200
  catch (err) { }
219
201
  }
@@ -221,7 +203,7 @@ program
221
203
  activeCredentialsPath = globalCredentialsPath;
222
204
  try {
223
205
  const credentials = JSON.parse(fs.readFileSync(globalCredentialsPath, "utf8"));
224
- token = credentials.machineToken;
206
+ token = credentials.machineToken?.trim() || null;
225
207
  }
226
208
  catch (err) { }
227
209
  }
@@ -243,9 +225,10 @@ program
243
225
  method: "GET",
244
226
  headers: { Authorization: `Bearer ${token}` },
245
227
  });
246
- if (response.status === 401) {
247
- console.log(chalk_1.default.red("🔴 AUTH EXPIRED: Your device session was revoked or expired."));
248
- if (fs.existsSync(activeCredentialsPath))
228
+ if (response.status === 401 || response.status === 403) {
229
+ const failureData = await response.json().catch(() => ({}));
230
+ console.log(chalk_1.default.red((0, cloud_1.cloudFailure)({ response: { status: response.status, data: failureData } }, [token]).message));
231
+ if (response.status === 401 && !process.env.FLOWSTRIDE_TOKEN && fs.existsSync(activeCredentialsPath))
249
232
  fs.unlinkSync(activeCredentialsPath);
250
233
  }
251
234
  else if (response.ok) {
@@ -284,7 +267,8 @@ program
284
267
  }
285
268
  if (options.ci)
286
269
  process.env.FLOWSTRIDE_NATIVE_CI = '1';
287
- let token = process.env.FLOWSTRIDE_TOKEN || null;
270
+ console.log((0, cloudSession_1.cloudSessionDescription)(API_BASE));
271
+ let token = process.env.FLOWSTRIDE_TOKEN?.trim() || null;
288
272
  let forceOS = false;
289
273
  const localCredentialsPath = path.join(process.cwd(), ".flowstride", "credentials.json");
290
274
  const globalCredentialsPath = path.join(os.homedir(), ".flowstride", "credentials.json");
@@ -294,7 +278,7 @@ program
294
278
  activeCredentialsPath = localCredentialsPath;
295
279
  try {
296
280
  const credentials = JSON.parse(fs.readFileSync(localCredentialsPath, "utf8"));
297
- token = credentials.machineToken;
281
+ token = credentials.machineToken?.trim() || null;
298
282
  }
299
283
  catch (err) { }
300
284
  }
@@ -302,7 +286,7 @@ program
302
286
  activeCredentialsPath = globalCredentialsPath;
303
287
  try {
304
288
  const credentials = JSON.parse(fs.readFileSync(globalCredentialsPath, "utf8"));
305
- token = credentials.machineToken;
289
+ token = credentials.machineToken?.trim() || null;
306
290
  }
307
291
  catch (err) { }
308
292
  }
@@ -325,9 +309,10 @@ program
325
309
  method: "GET",
326
310
  headers: { Authorization: `Bearer ${token}` },
327
311
  });
328
- if (response.status === 401) {
329
- console.log(chalk_1.default.red("🔴 AUTH EXPIRED: Your device session was revoked or expired."));
330
- if (fs.existsSync(activeCredentialsPath))
312
+ if (response.status === 401 || response.status === 403) {
313
+ const failureData = await response.json().catch(() => ({}));
314
+ console.log(chalk_1.default.red((0, cloud_1.cloudFailure)({ response: { status: response.status, data: failureData } }, [token]).message));
315
+ if (response.status === 401 && !process.env.FLOWSTRIDE_TOKEN && fs.existsSync(activeCredentialsPath))
331
316
  fs.unlinkSync(activeCredentialsPath);
332
317
  forceOS = true;
333
318
  }