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 CHANGED
@@ -1,4 +1,4 @@
1
- # Flowstride OS
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 `welcome.flow` sample file.)_
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
- _(This command executes your test while instantly booting up the local Flowstride Dashboard on `http://localhost:5173`.)_
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 chalk_1 = __importDefault(require("chalk"));
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", "welcome.flow");
47
- console.log(chalk_1.default.cyan("\n🚀 Flowstride: Initialising new workspace..."));
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(`${chalk_1.default.green("✔")} Created ${folder}/`);
59
+ console.log(`[OK] Created directory: ${folder}/`);
53
60
  }
54
61
  else {
55
- console.log(`${chalk_1.default.yellow("ℹ")} ${folder}/ already exists, skipping.`);
62
+ console.log(`[Info] Directory already exists: ${folder}/`);
56
63
  }
57
64
  });
58
- const welcomeContent = `Feature: Getting Started
65
+ // 2. Create Sample Flow
66
+ const exampleContent = `
67
+ Feature: User login flow
59
68
 
60
- Scenario: Verify Flowstride is Ready
69
+ Scenario: Ensure that user logs in successfully
61
70
 
62
- Given "User lands on the sample page";
63
- flow.open "https://example.com";
71
+ Given "User lands on home page";
72
+ flow.open "https://www.qacar.online/";
64
73
 
65
- When "User confirms Welcome!";
66
- flow.expect text "Welcome!";
74
+ When "User clicks on Log in, near Sign Up";
75
+ flow.click "Log in" near "Sign up";
67
76
 
68
- Then "Verify the main heading is visible";
69
- flow.expect visible "h1";
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, welcomeContent);
73
- console.log(`${chalk_1.default.green("✔")} Created flows/welcome.flow`);
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(chalk_1.default.bold.green("\n✨ Workspace ready!"));
76
- console.log(`Try running: ${chalk_1.default.cyan("flowstride run flows/welcome.flow")}\n`);
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 | undefined, options: Record<string, any>): Promise<void>;
1
+ export declare function runAction(targetPath: string, options: Record<string, any>): Promise<void>;
@@ -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
- const resolvedPath = targetPath
80
- ? path.resolve(root, targetPath)
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
- const worker = new flow_worker_1.FlowWorker(config, web, api, store, sessions, pluginManager);
134
- const orchestrator = new flow_orchestrator_1.FlowOrchestrator(worker, workerCount, config);
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.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 (path, options) => {
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
- await (0, run_1.runAction)(path, options);
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", "http://localhost:5173");
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
- res.writeHead(404);
165
- res.end();
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
- if (e.code === "EADDRINUSE") {
170
- console.error(chalk_1.default.red(`\n[Error]: Port 4321 is in use. Close the other terminal.`));
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(4321, async () => {
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("http://localhost:5173");
199
- console.log(chalk_1.default.yellow("[Dashboard]: Waiting for connection..."));
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.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",