flowstride 1.0.0 → 1.0.2
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/README.md +55 -4
- package/dist/cli/commands/init.js +75 -18
- package/dist/cli/commands/run.d.ts +1 -1
- package/dist/cli/commands/run.js +31 -7
- package/dist/cli/index.js +168 -5
- package/dist/runner/flow_orchestrator.js +64 -11
- package/package.json +5 -3
- package/src/ui/dist/assets/flow_logo-CjiL4Lvy.png +0 -0
- package/src/ui/dist/assets/index-Brl3nrIZ.js +270 -0
- package/src/ui/dist/assets/index-DXTn_ThB.css +1 -0
- package/src/ui/dist/favicon.png +0 -0
- package/src/ui/dist/favicon.svg +1 -0
- package/src/ui/dist/icons.svg +24 -0
- package/src/ui/dist/index.html +14 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Flowstride
|
|
1
|
+
# Flowstride
|
|
2
2
|
|
|
3
3
|
**The next-generation, BDD-flavored End-to-End Testing Framework.**
|
|
4
4
|
|
|
@@ -22,17 +22,68 @@ Traditional E2E frameworks force you to choose between highly technical code (wh
|
|
|
22
22
|
Flowstride requires Node.js 18 or higher.
|
|
23
23
|
|
|
24
24
|
**1. Install globally (or as a dev dependency in your project):**
|
|
25
|
+
|
|
26
|
+
```bash
|
|
25
27
|
npm install -g flowstride
|
|
28
|
+
```
|
|
26
29
|
|
|
27
30
|
**2. Initialize a new Flowstride workspace:**
|
|
31
|
+
|
|
32
|
+
```bash
|
|
28
33
|
flowstride init
|
|
34
|
+
```
|
|
29
35
|
|
|
30
|
-
_(This creates the default directory structure (`/flows`, `/reports`, `/plugins`) and generates a `
|
|
36
|
+
_(This creates the default directory structure (`/flows`, `/reports`, `/plugins`) and generates a `example.flow` sample file.)_
|
|
31
37
|
|
|
32
38
|
**3. Run your first test:**
|
|
33
|
-
flowstride run flows/welcome.flow
|
|
34
39
|
|
|
35
|
-
|
|
40
|
+
```bash
|
|
41
|
+
flowstride run
|
|
42
|
+
|
|
43
|
+
OR
|
|
44
|
+
|
|
45
|
+
flowstride run flows/example.flow
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
_(This command executes your test while instantly booting up the local Flowstride Dashboard.)_
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Privacy & Data Security
|
|
53
|
+
|
|
54
|
+
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.
|
|
55
|
+
|
|
56
|
+
### Open Source Core: 100% Local Isolation
|
|
57
|
+
|
|
58
|
+
The Open Source version of Flowstride operates entirely as an ephemeral, local execution engine.
|
|
59
|
+
|
|
60
|
+
- **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.
|
|
61
|
+
|
|
62
|
+
- **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.
|
|
63
|
+
|
|
64
|
+
- **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.
|
|
65
|
+
|
|
66
|
+
- **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)._
|
|
67
|
+
|
|
68
|
+
### Flowstride Enterprise: Secure Cloud Telemetry
|
|
69
|
+
|
|
70
|
+
For teams using Flowstride Enterprise to aggregate multi-flow metrics, we maintain a strict metadata-only boundary.
|
|
71
|
+
|
|
72
|
+
- **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.
|
|
73
|
+
|
|
74
|
+
- **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.
|
|
75
|
+
|
|
76
|
+
### CI/CD Leak Protection & Terminal Hygiene
|
|
77
|
+
|
|
78
|
+
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.
|
|
79
|
+
|
|
80
|
+
Flowstride completely neutralizes this threat by architecturally separating execution output from diagnostic telemetry:
|
|
81
|
+
|
|
82
|
+
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.
|
|
83
|
+
|
|
84
|
+
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.
|
|
85
|
+
|
|
86
|
+
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.
|
|
36
87
|
|
|
37
88
|
---
|
|
38
89
|
|
|
@@ -39,39 +39,96 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.initAction = initAction;
|
|
40
40
|
const fs = __importStar(require("fs"));
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
|
-
const
|
|
42
|
+
const child_process_1 = require("child_process");
|
|
43
|
+
const util_1 = require("util");
|
|
44
|
+
const ora_1 = __importDefault(require("ora"));
|
|
45
|
+
// Convert standard exec to an async/await promise
|
|
46
|
+
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
43
47
|
async function initAction() {
|
|
44
48
|
const root = process.cwd();
|
|
45
|
-
const folders = ["flows", "reports", "plugins"];
|
|
46
|
-
const sampleFlowPath = path.join(root, "flows", "
|
|
47
|
-
|
|
49
|
+
const folders = ["flows", "reports", "plugins", "fixtures"];
|
|
50
|
+
const sampleFlowPath = path.join(root, "flows", "example.flow");
|
|
51
|
+
const fixturesReadmePath = path.join(root, "fixtures", "README.md");
|
|
52
|
+
const gitignorePath = path.join(root, ".gitignore");
|
|
53
|
+
console.log("\n[Info] Initializing new workspace...");
|
|
54
|
+
// 1. Create Directories
|
|
48
55
|
folders.forEach((folder) => {
|
|
49
56
|
const dirPath = path.join(root, folder);
|
|
50
57
|
if (!fs.existsSync(dirPath)) {
|
|
51
58
|
fs.mkdirSync(dirPath);
|
|
52
|
-
console.log(
|
|
59
|
+
console.log(`[OK] Created directory: ${folder}/`);
|
|
53
60
|
}
|
|
54
61
|
else {
|
|
55
|
-
console.log(
|
|
62
|
+
console.log(`[Info] Directory already exists: ${folder}/`);
|
|
56
63
|
}
|
|
57
64
|
});
|
|
58
|
-
|
|
65
|
+
// 2. Create Sample Flow
|
|
66
|
+
const exampleContent = `
|
|
67
|
+
Feature: User login flow
|
|
59
68
|
|
|
60
|
-
Scenario:
|
|
69
|
+
Scenario: Ensure that user logs in successfully
|
|
61
70
|
|
|
62
|
-
Given "User lands on
|
|
63
|
-
flow.open "https://
|
|
71
|
+
Given "User lands on home page";
|
|
72
|
+
flow.open "https://www.qacar.online/";
|
|
64
73
|
|
|
65
|
-
When "User
|
|
66
|
-
flow.
|
|
74
|
+
When "User clicks on Log in, near Sign Up";
|
|
75
|
+
flow.click "Log in" near "Sign up";
|
|
67
76
|
|
|
68
|
-
|
|
69
|
-
flow.
|
|
77
|
+
When "User fill input fields"
|
|
78
|
+
flow.type "Email" "user@flowstridemail.com";
|
|
79
|
+
flow.type "Password" "Test@12345";
|
|
80
|
+
|
|
81
|
+
When "Click on Login button"
|
|
82
|
+
flow.click button "Login"
|
|
83
|
+
|
|
84
|
+
And "Wait for login to complete"
|
|
85
|
+
flow.expect visible "Logout"
|
|
86
|
+
|
|
87
|
+
When "Save the logged in session"
|
|
88
|
+
flow.save global session "AdminLogin" persist
|
|
89
|
+
|
|
90
|
+
Then "Logout of the Admin dashboard"
|
|
91
|
+
flow.click button "Logout [0]"
|
|
70
92
|
`;
|
|
71
93
|
if (!fs.existsSync(sampleFlowPath)) {
|
|
72
|
-
fs.writeFileSync(sampleFlowPath,
|
|
73
|
-
console.log(
|
|
94
|
+
fs.writeFileSync(sampleFlowPath, exampleContent);
|
|
95
|
+
console.log("[OK] Created sample flow: flows/example.flow");
|
|
96
|
+
}
|
|
97
|
+
// 3. Create Fixtures Readme
|
|
98
|
+
const fixturesReadmeContent = `# Fixtures
|
|
99
|
+
|
|
100
|
+
Store your audio, image, and data assets in this directory.
|
|
101
|
+
|
|
102
|
+
- Audio files (.mp3, .wav) used with: flow.injectAudio "fixtures/your-audio.mp3"
|
|
103
|
+
- Images used for visual verification or uploads.
|
|
104
|
+
`;
|
|
105
|
+
if (!fs.existsSync(fixturesReadmePath)) {
|
|
106
|
+
fs.writeFileSync(fixturesReadmePath, fixturesReadmeContent);
|
|
107
|
+
console.log("[OK] Created fixtures/README.md");
|
|
108
|
+
}
|
|
109
|
+
// 4. Secure the Workspace (.gitignore)
|
|
110
|
+
const ignorePayload = `\n# Flowstride Execution Data\n.flowstride\nflowstride-artifacts\n`;
|
|
111
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
112
|
+
fs.writeFileSync(gitignorePath, `node_modules\n${ignorePayload}`);
|
|
113
|
+
console.log("[OK] Created .gitignore to secure local session data");
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const existingIgnore = fs.readFileSync(gitignorePath, "utf8");
|
|
117
|
+
if (!existingIgnore.includes(".flowstride")) {
|
|
118
|
+
fs.appendFileSync(gitignorePath, ignorePayload);
|
|
119
|
+
console.log("[OK] Updated .gitignore to secure local session data");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
console.log();
|
|
123
|
+
const browserSpinner = (0, ora_1.default)("Downloading Chromium execution engine...").start();
|
|
124
|
+
try {
|
|
125
|
+
await execAsync("npx playwright install chromium");
|
|
126
|
+
browserSpinner.succeed("Chromium execution engine installed successfully.");
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
browserSpinner.fail("Could not auto-install Chromium.");
|
|
130
|
+
console.log("[Warning] Please run manually: npx playwright install chromium");
|
|
74
131
|
}
|
|
75
|
-
console.log(
|
|
76
|
-
console.log(`
|
|
132
|
+
console.log("\n[Success] Workspace ready.");
|
|
133
|
+
console.log(`[Info] Run tests using: flowstride run flows/example.flow\n`);
|
|
77
134
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function runAction(targetPath: string
|
|
1
|
+
export declare function runAction(targetPath: string, options: Record<string, any>): Promise<void>;
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -38,6 +38,7 @@ const fs = __importStar(require("fs"));
|
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const dotenv = __importStar(require("dotenv"));
|
|
40
40
|
const os = __importStar(require("os"));
|
|
41
|
+
const url_1 = require("url");
|
|
41
42
|
const flow_worker_1 = require("../../runner/flow_worker");
|
|
42
43
|
const flow_orchestrator_1 = require("../../runner/flow_orchestrator");
|
|
43
44
|
const playwright_1 = require("../../adapters/web/playwright");
|
|
@@ -76,12 +77,10 @@ async function runAction(targetPath, options) {
|
|
|
76
77
|
if (isCI) {
|
|
77
78
|
console.log("\n[CI/CD]: Environment detected. Engaging Smart Automation Behaviors...");
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
: path.resolve(root, "flows");
|
|
80
|
+
// The CLI (index.ts) now strictly guarantees targetPath is the absolute path to ./flows
|
|
81
|
+
const resolvedPath = targetPath;
|
|
82
82
|
if (!fs.existsSync(resolvedPath)) {
|
|
83
|
-
console.error(`\n[Error]: Cannot find target path "${resolvedPath}".`);
|
|
84
|
-
console.log(`[Info]: Try running 'flowstride init' to create a workspace first.\n`);
|
|
83
|
+
console.error(`\n[Error]: Cannot find strict target path "${resolvedPath}".`);
|
|
85
84
|
process.exit(1);
|
|
86
85
|
}
|
|
87
86
|
console.log(`\n[Flowstride]: Starting execution suite at "${resolvedPath}"...`);
|
|
@@ -130,8 +129,33 @@ async function runAction(targetPath, options) {
|
|
|
130
129
|
const pluginManager = new plugin_manager_1.PluginManager(config, web, store, apiProxy);
|
|
131
130
|
const vault = new persistent_vault_1.PersistentVault(root);
|
|
132
131
|
const sessions = new manager_1.SessionManager(web, api, vault);
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
// ==========================================
|
|
133
|
+
// DYNAMIC ENGINE ROUTER (The Plugin Handoff)
|
|
134
|
+
// ==========================================
|
|
135
|
+
let ActiveFlowWorker = flow_worker_1.FlowWorker;
|
|
136
|
+
let ActiveFlowOrchestrator = flow_orchestrator_1.FlowOrchestrator;
|
|
137
|
+
let engineMode = "Open Source";
|
|
138
|
+
// Check the local node_modules for the Pro engine
|
|
139
|
+
const proWorkerPath = path.join(process.cwd(), "node_modules", "@flowstride", "pro", "dist", "runner", "flow_worker.js");
|
|
140
|
+
const proOrchPath = path.join(process.cwd(), "node_modules", "@flowstride", "pro", "dist", "runner", "flow_orchestrator.js");
|
|
141
|
+
if (fs.existsSync(proWorkerPath) && fs.existsSync(proOrchPath)) {
|
|
142
|
+
try {
|
|
143
|
+
// Dynamically load the Pro classes using native absolute URL resolution
|
|
144
|
+
const proWorkerModule = await import((0, url_1.pathToFileURL)(proWorkerPath).href);
|
|
145
|
+
const proOrchModule = await import((0, url_1.pathToFileURL)(proOrchPath).href);
|
|
146
|
+
ActiveFlowWorker = proWorkerModule.FlowWorker;
|
|
147
|
+
ActiveFlowOrchestrator = proOrchModule.FlowOrchestrator;
|
|
148
|
+
engineMode = "Enterprise (Pro)";
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
console.warn(`\n[Warning]: Found @flowstride/pro but failed to boot it. Falling back to Open Source engine. (${err.message})`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
console.log(`\n[Engine]: Booting Flowstride in ${engineMode} mode...`);
|
|
155
|
+
// ==========================================
|
|
156
|
+
// Instantiate the chosen engine (OS or Pro)
|
|
157
|
+
const worker = new ActiveFlowWorker(config, web, api, store, sessions, pluginManager);
|
|
158
|
+
const orchestrator = new ActiveFlowOrchestrator(worker, workerCount, config);
|
|
135
159
|
try {
|
|
136
160
|
await orchestrator.start(resolvedPath);
|
|
137
161
|
console.log("\n[Flowstride]: Execution suite completed successfully.");
|
package/dist/cli/index.js
CHANGED
|
@@ -1,34 +1,197 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
+
};
|
|
3
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
40
|
const commander_1 = require("commander");
|
|
5
41
|
const run_1 = require("./commands/run");
|
|
6
42
|
const report_1 = require("./commands/report");
|
|
7
43
|
const init_1 = require("./commands/init");
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const os = __importStar(require("os"));
|
|
47
|
+
const child_process_1 = require("child_process");
|
|
48
|
+
const prompts_1 = require("@inquirer/prompts");
|
|
49
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
50
|
+
const ora_1 = __importDefault(require("ora"));
|
|
8
51
|
const program = new commander_1.Command();
|
|
9
52
|
program
|
|
10
53
|
.name("flowstride")
|
|
11
54
|
.description("Flowstride: The Enterprise Flow-First Automation CLI")
|
|
12
|
-
.version("1.0.
|
|
55
|
+
.version("1.0.2");
|
|
13
56
|
program
|
|
14
57
|
.command("init")
|
|
15
58
|
.description("Initialise a new Flowstride workspace in the current directory")
|
|
16
59
|
.action(async () => {
|
|
17
60
|
await (0, init_1.initAction)();
|
|
18
61
|
});
|
|
62
|
+
program
|
|
63
|
+
.command("login")
|
|
64
|
+
.description("Authenticate this machine with Flowstride Enterprise Cloud")
|
|
65
|
+
.action(async () => {
|
|
66
|
+
console.log(chalk_1.default.bold.blue("\n🌐 Flowstride Enterprise Cloud Sync\n"));
|
|
67
|
+
const userEmail = await (0, prompts_1.input)({ message: "Enter your Flowstride email:" });
|
|
68
|
+
const userPass = await (0, prompts_1.password)({ message: "Enter your password:" });
|
|
69
|
+
// Silently grab the computer's hostname without bothering the user
|
|
70
|
+
const deviceName = os.hostname();
|
|
71
|
+
const spinner = (0, ora_1.default)("Authenticating with Cloud...").start();
|
|
72
|
+
try {
|
|
73
|
+
// 1. Authenticate with Next.js Server
|
|
74
|
+
const authString = Buffer.from(`${userEmail}:${userPass}`).toString("base64");
|
|
75
|
+
const response = await fetch("https://cloud.flowstride.io/api/auth/cli-sync", {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: {
|
|
78
|
+
"Content-Type": "application/json",
|
|
79
|
+
Authorization: `Basic ${authString}`,
|
|
80
|
+
},
|
|
81
|
+
body: JSON.stringify({ deviceName }),
|
|
82
|
+
});
|
|
83
|
+
const data = await response.json();
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
spinner.fail(chalk_1.default.red(`Authentication Failed: ${data.error || "Unknown error"}`));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
spinner.succeed(chalk_1.default.green("Authentication Successful!"));
|
|
89
|
+
// 2. Save Machine Token Locally
|
|
90
|
+
const configDir = path.join(os.homedir(), ".flowstride");
|
|
91
|
+
if (!fs.existsSync(configDir)) {
|
|
92
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
93
|
+
}
|
|
94
|
+
const credentialsPath = path.join(configDir, "credentials.json");
|
|
95
|
+
fs.writeFileSync(credentialsPath, JSON.stringify({
|
|
96
|
+
machineToken: data.machineToken,
|
|
97
|
+
workspaceId: data.workspaceId,
|
|
98
|
+
deviceName: deviceName,
|
|
99
|
+
syncedAt: new Date().toISOString(),
|
|
100
|
+
}, null, 2));
|
|
101
|
+
console.log(chalk_1.default.gray(`\nCredentials saved to ${credentialsPath}`));
|
|
102
|
+
console.log(chalk_1.default.bold.green(`\n✅ Machine synchronized with Flowstride Enterprise.`));
|
|
103
|
+
// 3. The Auto-Installer Logic
|
|
104
|
+
if (data.githubToken) {
|
|
105
|
+
console.log(chalk_1.default.yellow(`\n⚙️ System Check: You have access to Enterprise features (AI Healing, Parallel Workers).`));
|
|
106
|
+
const localPackageJsonPath = path.join(process.cwd(), "package.json");
|
|
107
|
+
const nodeModulesPath = path.join(process.cwd(), "node_modules", "@flowstride", "pro");
|
|
108
|
+
if (fs.existsSync(localPackageJsonPath) &&
|
|
109
|
+
!fs.existsSync(nodeModulesPath)) {
|
|
110
|
+
const installPro = await (0, prompts_1.confirm)({
|
|
111
|
+
message: "The Pro Engine is not installed in this project. Would you like Flowstride to automatically install it now?",
|
|
112
|
+
default: true,
|
|
113
|
+
});
|
|
114
|
+
if (installPro) {
|
|
115
|
+
const installSpinner = (0, ora_1.default)("Configuring secure registry and installing @flowstride/pro...").start();
|
|
116
|
+
const npmrcPath = path.join(process.cwd(), ".npmrc");
|
|
117
|
+
let originalNpmrc = "";
|
|
118
|
+
try {
|
|
119
|
+
// A. Backup existing .npmrc if it exists
|
|
120
|
+
if (fs.existsSync(npmrcPath)) {
|
|
121
|
+
originalNpmrc = fs.readFileSync(npmrcPath, "utf8");
|
|
122
|
+
}
|
|
123
|
+
// B. Write the temporary GitHub configuration
|
|
124
|
+
const tempNpmrcContent = `
|
|
125
|
+
@flowstride:registry=https://npm.pkg.github.com
|
|
126
|
+
//npm.pkg.github.com/:_authToken=${data.githubToken}
|
|
127
|
+
`;
|
|
128
|
+
fs.writeFileSync(npmrcPath, (originalNpmrc ? originalNpmrc + "\n" : "") + tempNpmrcContent);
|
|
129
|
+
// C. Run the silent install
|
|
130
|
+
(0, child_process_1.execSync)("npm install @flowstride/pro --save-dev", {
|
|
131
|
+
stdio: "ignore",
|
|
132
|
+
cwd: process.cwd(),
|
|
133
|
+
});
|
|
134
|
+
installSpinner.succeed(chalk_1.default.green("Enterprise Engine installed successfully!"));
|
|
135
|
+
}
|
|
136
|
+
catch (installError) {
|
|
137
|
+
installSpinner.fail(chalk_1.default.red("Failed to install the Enterprise Engine automatically."));
|
|
138
|
+
console.error(chalk_1.default.gray("You may need to install it manually."));
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
// D. Secure Cleanup: Remove the token from .npmrc
|
|
142
|
+
if (originalNpmrc) {
|
|
143
|
+
fs.writeFileSync(npmrcPath, originalNpmrc); // Restore original
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
if (fs.existsSync(npmrcPath))
|
|
147
|
+
fs.unlinkSync(npmrcPath); // Delete if we created it
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
console.log(chalk_1.default.gray(`\nSkipped installation. You can run 'npm install @flowstride/pro --save-dev' manually later.`));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
spinner.fail(chalk_1.default.red(`Network Error: Could not connect to cloud.flowstride.io`));
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
19
162
|
program
|
|
20
163
|
.command("run")
|
|
21
|
-
.description("Execute .flow automation journeys")
|
|
22
|
-
.argument("[path]", "Path to .flow files or directory (defaults to ./flows)")
|
|
164
|
+
.description("Execute all .flow automation journeys in the ./flows directory")
|
|
23
165
|
.option("-b, --bail", "Stop execution on first failure", false)
|
|
24
166
|
.option("--headed", "Run browser in headed mode", false)
|
|
25
167
|
.option("--headless <boolean>", "Run browser in headless mode", (val) => val === "true", true)
|
|
26
168
|
.option("--workers <number>", "Number of parallel workers to spawn (Pro feature)")
|
|
27
|
-
.action(async (
|
|
169
|
+
.action(async (options) => {
|
|
170
|
+
const credentialsPath = path.join(os.homedir(), ".flowstride", "credentials.json");
|
|
171
|
+
let isPro = false;
|
|
172
|
+
if (fs.existsSync(credentialsPath)) {
|
|
173
|
+
try {
|
|
174
|
+
const credentials = JSON.parse(fs.readFileSync(credentialsPath, "utf8"));
|
|
175
|
+
isPro = true;
|
|
176
|
+
console.log(`[PRO]: Machine Synced to Enterprise Cloud`);
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
console.log(`[AUTH ERROR]: Invalid credentials file. Run 'flowstride login' to reconnect.`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
console.log(`[OS]: Local Mode (Run 'flowstride login' to sync to Enterprise Cloud)`);
|
|
184
|
+
}
|
|
28
185
|
if (options.headed) {
|
|
29
186
|
options.headless = false;
|
|
30
187
|
}
|
|
31
|
-
|
|
188
|
+
const strictUiFlowsDir = path.resolve(process.cwd(), "./flows");
|
|
189
|
+
if (!fs.existsSync(strictUiFlowsDir)) {
|
|
190
|
+
console.error(`\n[ERROR]: Could not find ${strictUiFlowsDir}.`);
|
|
191
|
+
console.error(`Flowstride requires all UI tests to be inside a './flows' directory.`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
await (0, run_1.runAction)(strictUiFlowsDir, options);
|
|
32
195
|
});
|
|
33
196
|
program
|
|
34
197
|
.command("report")
|
|
@@ -103,7 +103,7 @@ class FlowOrchestrator {
|
|
|
103
103
|
async start(targetPath) {
|
|
104
104
|
try {
|
|
105
105
|
this.httpServer = http.createServer((req, res) => {
|
|
106
|
-
res.setHeader("Access-Control-Allow-Origin", "
|
|
106
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
107
107
|
if (req.url && req.url.startsWith("/videos/")) {
|
|
108
108
|
let urlObj;
|
|
109
109
|
try {
|
|
@@ -161,15 +161,60 @@ class FlowOrchestrator {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
else {
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
const uiDistPath = path.join(__dirname, "../../src/ui/dist");
|
|
165
|
+
let reqPath = req.url === "/"
|
|
166
|
+
? "/index.html"
|
|
167
|
+
: req.url?.split("?")[0] || "/index.html";
|
|
168
|
+
let filePath = path.join(uiDistPath, reqPath);
|
|
169
|
+
if (!filePath.startsWith(uiDistPath)) {
|
|
170
|
+
res.writeHead(403);
|
|
171
|
+
return res.end("Forbidden");
|
|
172
|
+
}
|
|
173
|
+
if (fs.existsSync(filePath)) {
|
|
174
|
+
const extname = path.extname(filePath);
|
|
175
|
+
let contentType = "text/html";
|
|
176
|
+
switch (extname) {
|
|
177
|
+
case ".js":
|
|
178
|
+
contentType = "application/javascript";
|
|
179
|
+
break;
|
|
180
|
+
case ".css":
|
|
181
|
+
contentType = "text/css";
|
|
182
|
+
break;
|
|
183
|
+
case ".json":
|
|
184
|
+
contentType = "application/json";
|
|
185
|
+
break;
|
|
186
|
+
case ".png":
|
|
187
|
+
contentType = "image/png";
|
|
188
|
+
break;
|
|
189
|
+
case ".jpg":
|
|
190
|
+
contentType = "image/jpg";
|
|
191
|
+
break;
|
|
192
|
+
case ".svg":
|
|
193
|
+
contentType = "image/svg+xml";
|
|
194
|
+
break;
|
|
195
|
+
case ".ico":
|
|
196
|
+
contentType = "image/x-icon";
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
200
|
+
fs.createReadStream(filePath).pipe(res);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
const indexPath = path.join(uiDistPath, "index.html");
|
|
204
|
+
if (fs.existsSync(indexPath)) {
|
|
205
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
206
|
+
fs.createReadStream(indexPath).pipe(res);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
res.writeHead(404);
|
|
210
|
+
res.end("Flowstride UI not found. Dashboard bundle is missing.");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
166
213
|
}
|
|
167
214
|
});
|
|
168
215
|
this.httpServer.on("error", (e) => {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
process.exit(1);
|
|
172
|
-
}
|
|
216
|
+
console.error(chalk_1.default.red(`\n[Error]: Failed to start local server: ${e.message}`));
|
|
217
|
+
process.exit(1);
|
|
173
218
|
});
|
|
174
219
|
this.wss = new ws_1.WebSocketServer({ server: this.httpServer });
|
|
175
220
|
this.wss.on("connection", (ws) => {
|
|
@@ -193,13 +238,21 @@ class FlowOrchestrator {
|
|
|
193
238
|
}
|
|
194
239
|
});
|
|
195
240
|
});
|
|
196
|
-
this.httpServer.listen(
|
|
241
|
+
this.httpServer.listen(0, async () => {
|
|
242
|
+
const address = this.httpServer?.address();
|
|
243
|
+
const port = typeof address === "object" && address !== null ? address.port : null;
|
|
244
|
+
if (!port) {
|
|
245
|
+
console.error(chalk_1.default.red("\n[Error]: Could not bind to an available port."));
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
const localUrl = `http://localhost:${port}`;
|
|
197
249
|
const { default: open } = await import("open");
|
|
198
|
-
await open(
|
|
199
|
-
console.log(chalk_1.default.yellow(
|
|
250
|
+
await open(localUrl);
|
|
251
|
+
console.log(chalk_1.default.yellow(`\n[Dashboard]: Local server running at ${localUrl}`));
|
|
252
|
+
console.log(chalk_1.default.yellow("[Dashboard]: Waiting for UI connection..."));
|
|
200
253
|
while (this.wss.clients.size === 0)
|
|
201
254
|
await new Promise((r) => setTimeout(r, 250));
|
|
202
|
-
console.log(chalk_1.default.green("[Dashboard]: Connected!"));
|
|
255
|
+
console.log(chalk_1.default.green("[Dashboard]: Connected to UI!"));
|
|
203
256
|
await this.executeTestRun(targetPath);
|
|
204
257
|
});
|
|
205
258
|
process.on("SIGINT", () => {
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowstride",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "An opensource flowfirst test automation framework",
|
|
5
5
|
"workspaces": [
|
|
6
|
-
"packages
|
|
6
|
+
"packages/flowstride_report"
|
|
7
7
|
],
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"types": "dist/index.d.ts",
|
|
10
10
|
"files": [
|
|
11
|
-
"dist"
|
|
11
|
+
"dist",
|
|
12
|
+
"src/ui/dist"
|
|
12
13
|
],
|
|
13
14
|
"bin": {
|
|
14
15
|
"flowstride": "dist/cli/index.js"
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
"node": ">=18.0.0"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
35
|
+
"@inquirer/prompts": "^7.10.1",
|
|
34
36
|
"chalk": "^4.1.2",
|
|
35
37
|
"commander": "^11.0.0",
|
|
36
38
|
"dotenv": "^17.4.2",
|
|
Binary file
|