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.
- package/NATIVE_CI.md +58 -34
- package/README.md +33 -40
- package/dist/adapters/web/playwright.js +6 -4
- package/dist/ci/config.js +6 -5
- package/dist/ci/runtime.js +27 -14
- package/dist/cli/commands/run.js +2 -1
- package/dist/cli/index.js +35 -50
- package/dist/cli/manual_server/workspace_server.js +44 -32
- package/dist/cli/utils/cloudSession.d.ts +7 -0
- package/dist/cli/utils/cloudSession.js +37 -0
- package/dist/cli/utils/proInstaller.d.ts +5 -0
- package/dist/cli/utils/proInstaller.js +141 -0
- package/dist/game/sdk.js +3 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +9 -0
- package/dist/manual/security.d.ts +183 -0
- package/dist/manual/security.js +106 -0
- package/dist/runner/data_generators.d.ts +1 -0
- package/dist/runner/data_generators.js +3 -0
- package/dist/runner/dom_stripper.js +3 -0
- package/dist/runner/flow_orchestrator.js +13 -22
- package/dist/runner/flow_worker.d.ts +1 -0
- package/dist/runner/flow_worker.js +29 -13
- package/dist/runner/persistent_vault.js +2 -1
- package/dist/runner/plugin_manager.js +3 -1
- package/dist/security/localDashboard.d.ts +2 -0
- package/dist/security/localDashboard.js +22 -0
- package/dist/security/redaction.d.ts +4 -0
- package/dist/security/redaction.js +49 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/utils/cloud.d.ts +6 -0
- package/dist/utils/cloud.js +33 -0
- package/dist/utils/projectPaths.d.ts +11 -0
- package/dist/utils/projectPaths.js +90 -0
- package/package.json +21 -14
- package/src/ui/dist/assets/index-DVwp0K-J.js +462 -0
- package/src/ui/dist/index.html +1 -1
- package/RELEASE_NOTES.md +0 -40
- 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.
|
|
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.
|
|
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.
|
|
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;
|
|
59
|
-
|
|
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
|
|
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
|
-
##
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.');
|
package/dist/ci/runtime.js
CHANGED
|
@@ -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
|
|
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 => ({ '<': '<', '>': '>', '&': '&', '"': '"', "'": ''' }[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 =
|
|
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(
|
|
158
|
-
errors.push(`Incomplete 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 =>
|
|
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,
|
|
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;
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -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
|
-
?
|
|
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
|
|
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
|
-
|
|
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.
|
|
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: ${
|
|
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
|
-
|
|
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 (
|
|
170
|
-
installSpinner.
|
|
171
|
-
console.
|
|
172
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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
|
-
|
|
330
|
-
|
|
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
|
}
|