flowstride 1.0.19 → 1.0.20-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/NATIVE_CI.md +179 -0
  2. package/dist/adapters/web/playwright.d.ts +1 -0
  3. package/dist/adapters/web/playwright.js +24 -18
  4. package/dist/ci/config.d.ts +3 -0
  5. package/dist/ci/config.js +77 -0
  6. package/dist/ci/deadline.d.ts +2 -0
  7. package/dist/ci/deadline.js +16 -0
  8. package/dist/ci/init.d.ts +1 -0
  9. package/dist/ci/init.js +21 -0
  10. package/dist/ci/runtime.d.ts +19 -0
  11. package/dist/ci/runtime.js +203 -0
  12. package/dist/cli/commands/run.js +17 -35
  13. package/dist/cli/index.js +78 -54
  14. package/dist/cli/manual_server/workspace_server.js +44 -32
  15. package/dist/cli/utils/cloudSession.d.ts +7 -0
  16. package/dist/cli/utils/cloudSession.js +37 -0
  17. package/dist/cli/utils/proInstaller.d.ts +5 -0
  18. package/dist/cli/utils/proInstaller.js +141 -0
  19. package/dist/game/bridge.d.ts +13 -0
  20. package/dist/game/bridge.js +15 -0
  21. package/dist/game/sdk.d.ts +27 -0
  22. package/dist/game/sdk.js +256 -0
  23. package/dist/game/three-bridge.d.ts +18 -0
  24. package/dist/game/three-bridge.js +59 -0
  25. package/dist/index.d.ts +5 -0
  26. package/dist/index.js +9 -0
  27. package/dist/manual/security.d.ts +183 -0
  28. package/dist/manual/security.js +106 -0
  29. package/dist/runner/data_generators.d.ts +1 -0
  30. package/dist/runner/data_generators.js +3 -0
  31. package/dist/runner/dom_stripper.js +3 -0
  32. package/dist/runner/flow_orchestrator.d.ts +5 -1
  33. package/dist/runner/flow_orchestrator.js +34 -19
  34. package/dist/runner/flow_worker.d.ts +1 -0
  35. package/dist/runner/flow_worker.js +56 -16
  36. package/dist/runner/persistent_vault.js +2 -1
  37. package/dist/runner/plugin_manager.d.ts +1 -0
  38. package/dist/runner/plugin_manager.js +35 -1
  39. package/dist/security/localDashboard.d.ts +2 -0
  40. package/dist/security/localDashboard.js +22 -0
  41. package/dist/security/redaction.d.ts +4 -0
  42. package/dist/security/redaction.js +49 -0
  43. package/dist/types/index.d.ts +43 -0
  44. package/dist/utils/cloud.d.ts +6 -0
  45. package/dist/utils/cloud.js +33 -0
  46. package/dist/utils/projectPaths.d.ts +11 -0
  47. package/dist/utils/projectPaths.js +90 -0
  48. package/examples/native-game/flows/canvas.flow +13 -0
  49. package/examples/native-game/flowstride.config.json +12 -0
  50. package/examples/native-game/server.mjs +12 -0
  51. package/package.json +27 -11
  52. package/src/ui/dist/assets/index-DVwp0K-J.js +462 -0
  53. package/src/ui/dist/index.html +1 -1
  54. package/src/ui/dist/assets/index-DIC22ZK_.js +0 -638
@@ -34,6 +34,9 @@ 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");
38
+ const config_1 = require("../../ci/config");
39
+ const runtime_1 = require("../../ci/runtime");
37
40
  const fs = __importStar(require("fs"));
38
41
  const path = __importStar(require("path"));
39
42
  const dotenv = __importStar(require("dotenv"));
@@ -51,7 +54,7 @@ const api_proxy_1 = require("../../sdk/api_proxy");
51
54
  async function runAction(targetPath, options) {
52
55
  const root = process.cwd();
53
56
  const profileEnvPath = options.env
54
- ? path.resolve(root, `.env.${options.env}`)
57
+ ? (0, projectPaths_1.projectPath)(root, `.env.${options.env}`)
55
58
  : null;
56
59
  const defaultEnvPath = path.resolve(root, ".env");
57
60
  if (profileEnvPath) {
@@ -69,12 +72,8 @@ async function runAction(targetPath, options) {
69
72
  console.log(`[Environment]: Loaded default .env file`);
70
73
  }
71
74
  }
72
- const isCI = !!(process.env.CI ||
73
- process.env.GITHUB_ACTIONS ||
74
- process.env.GITLAB_CI ||
75
- process.env.CIRCLECI ||
76
- process.env.JENKINS_URL ||
77
- process.env.FLOWSTRIDE_API_KEY);
75
+ const config = (0, config_1.loadNativeConfig)(root, options);
76
+ const isCI = !!config.ci;
78
77
  if (isCI) {
79
78
  console.log("\n[CI/CD]: Environment detected. Engaging Smart Automation Behaviors...");
80
79
  }
@@ -84,39 +83,13 @@ async function runAction(targetPath, options) {
84
83
  process.exit(1);
85
84
  }
86
85
  console.log(`\n[Flowstride]: Starting execution suite at "${resolvedPath}"...`);
87
- const configPath = path.resolve(root, "flowstride.config.json");
88
- let localConfig = {};
89
- if (fs.existsSync(configPath)) {
90
- try {
91
- const fileContent = fs.readFileSync(configPath, "utf-8");
92
- localConfig = JSON.parse(fileContent);
93
- console.log(`[Config]: Loaded project configuration from flowstride.config.json`);
94
- }
95
- catch (e) {
96
- console.warn(`[Warning]: Could not parse flowstride.config.json: ${e.message}`);
97
- }
98
- }
99
- const config = {
100
- headless: isCI
101
- ? true
102
- : options.headed
103
- ? false
104
- : (localConfig.headless ?? true),
105
- timeout: localConfig.timeout ?? 30000,
106
- bail: options.bail ?? localConfig.bail ?? false,
107
- baseUrl: process.env.BASE_URL ?? localConfig.baseUrl,
108
- defaultDialogBehavior: isCI
109
- ? "dismiss"
110
- : (localConfig.defaultDialogBehavior ?? "dismiss"),
111
- pluginsDir: localConfig.pluginsDir,
112
- };
113
86
  if (isCI) {
114
87
  console.log(` > Headless Mode: ${config.headless ? "Forced ON" : "OFF"}`);
115
88
  console.log(` > Dialog Protection: ${(config.defaultDialogBehavior || "dismiss").toUpperCase()}`);
116
89
  console.log(` > Artifact Auto-Staging: ENABLED\n`);
117
90
  }
118
- let workerCount = options.workers ? parseInt(options.workers, 10) : 1;
119
- if (isNaN(workerCount) || workerCount < 1) {
91
+ let workerCount = options.workers ? Number(options.workers) : 1;
92
+ if (!Number.isSafeInteger(workerCount) || workerCount < 1) {
120
93
  console.error(`\n[Error]: Invalid worker count "${options.workers}". Must be an integer greater than 0.`);
121
94
  process.exit(1);
122
95
  }
@@ -126,6 +99,8 @@ async function runAction(targetPath, options) {
126
99
  console.log(`[Warning]: Auto-capping to ${maxSafeWorkers} workers to prevent system thrashing and crashes.\n`);
127
100
  workerCount = maxSafeWorkers;
128
101
  }
102
+ if (config.manifest?.maxWorkers !== undefined && config.manifest.maxWorkers !== 'unlimited')
103
+ workerCount = Math.min(workerCount, config.manifest.maxWorkers);
129
104
  const web = new playwright_1.WebAdapter(config);
130
105
  const api = new undici_1.ApiAdapter(config);
131
106
  const store = new temporary_store_1.TemporaryStore();
@@ -144,6 +119,8 @@ async function runAction(targetPath, options) {
144
119
  try {
145
120
  const proWorkerModule = await import((0, url_1.pathToFileURL)(proWorkerPath).href);
146
121
  const proOrchModule = await import((0, url_1.pathToFileURL)(proOrchPath).href);
122
+ if (config.ci && proOrchModule.FlowOrchestrator.nativeCIProtocol !== 1)
123
+ throw new Error('Installed Pro engine does not support native CI protocol 1; install the matching Pro candidate.');
147
124
  ActiveFlowWorker = proWorkerModule.FlowWorker;
148
125
  ActiveFlowOrchestrator = proOrchModule.FlowOrchestrator;
149
126
  engineMode = "Enterprise (Pro)";
@@ -155,6 +132,11 @@ async function runAction(targetPath, options) {
155
132
  console.log(`\n[Engine]: Booting Flowstride in ${engineMode} mode...`);
156
133
  const worker = new ActiveFlowWorker(config, web, api, store, sessions, pluginManager);
157
134
  const orchestrator = new ActiveFlowOrchestrator(worker, workerCount, config);
135
+ if (isCI) {
136
+ const result = await (0, runtime_1.runNativeCI)(worker, orchestrator, resolvedPath, config, engineMode);
137
+ console.log(`[Flowstride]: ${result.status}; reports: ${config.artifactsDir}`);
138
+ process.exit(result.status === 'passed' ? 0 : result.status === 'interrupted' ? 130 : 1);
139
+ }
158
140
  try {
159
141
  await orchestrator.start(resolvedPath);
160
142
  console.log("\n[Flowstride]: Execution suite completed successfully.");
package/dist/cli/index.js CHANGED
@@ -37,29 +37,38 @@ 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");
42
+ const config_1 = require("../ci/config");
43
+ const init_1 = require("../ci/init");
40
44
  const commander_1 = require("commander");
41
45
  const run_1 = require("./commands/run");
42
46
  const report_1 = require("./commands/report");
43
- const init_1 = require("./commands/init");
47
+ const init_2 = require("./commands/init");
44
48
  const open_1 = require("./commands/open");
45
49
  const fs = __importStar(require("fs"));
46
50
  const path = __importStar(require("path"));
47
51
  const os = __importStar(require("os"));
48
- const child_process_1 = require("child_process");
52
+ const proInstaller_1 = require("./utils/proInstaller");
49
53
  const prompts_1 = require("@inquirer/prompts");
50
54
  const chalk_1 = __importDefault(require("chalk"));
51
55
  const ora_1 = __importDefault(require("ora"));
52
56
  const program = new commander_1.Command();
53
- const API_BASE = process.env.FLOWSTRIDE_API_URL || "https://cloud.flowstride.io";
57
+ program.hook('preAction', (_program, command) => {
58
+ if (['login', 'status', 'run', 'update'].includes(command.name())) {
59
+ API_BASE = (0, cloudSession_1.loadCloudEnvironment)(process.cwd(), command.opts().env);
60
+ }
61
+ });
62
+ let API_BASE = (0, cloud_1.cloudApiBase)(process.env);
54
63
  program
55
64
  .name("flowstride")
56
65
  .description("Flowstride: The Enterprise Flow-First Automation CLI")
57
- .version("1.0.19");
66
+ .version("1.0.20-rc.6");
58
67
  program
59
68
  .command("init")
60
69
  .description("Initialise a new Flowstride workspace in the current directory")
61
70
  .action(async () => {
62
- await (0, init_1.initAction)();
71
+ await (0, init_2.initAction)();
63
72
  });
64
73
  program
65
74
  .command("login")
@@ -99,6 +108,7 @@ program
99
108
  console.error(chalk_1.default.red("✖ Error: A valid Developer Token is required."));
100
109
  process.exit(1);
101
110
  }
111
+ console.log(`[Cloud]: ${API_BASE}; login credential source: ${process.env.FLOWSTRIDE_TOKEN ? 'FLOWSTRIDE_TOKEN (environment)' : options.token ? '--token option' : 'interactive prompt'}`);
102
112
  const deviceName = os.hostname();
103
113
  const spinner = (0, ora_1.default)("Authenticating with Cloud...").start();
104
114
  try {
@@ -112,10 +122,11 @@ program
112
122
  });
113
123
  const data = await response.json();
114
124
  if (!response.ok) {
115
- spinner.fail(chalk_1.default.red(`Authentication Failed: ${data.error || "Invalid token or network error"}`));
125
+ spinner.fail(chalk_1.default.red(`Authentication Failed: ${(0, cloud_1.cloudFailure)({ response: { status: response.status, data } }, [token.trim()]).message}`));
116
126
  process.exit(1);
117
127
  }
118
128
  spinner.succeed(chalk_1.default.green("Authentication Successful!"));
129
+ console.log((0, cloudSession_1.tokenExpiryDescription)(data));
119
130
  const configDir = options.global
120
131
  ? path.join(os.homedir(), ".flowstride")
121
132
  : path.join(process.cwd(), ".flowstride");
@@ -144,42 +155,14 @@ program
144
155
  }
145
156
  if (installPro) {
146
157
  const installSpinner = (0, ora_1.default)("Downloading Enterprise Engine via secure proxy...").start();
147
- const tarballPath = path.join(process.cwd(), "flowstride-pro.tgz");
148
158
  try {
149
- const tarballResponse = await fetch(`${API_BASE}/api/workspace/download-pro`, {
150
- method: "GET",
151
- headers: { Authorization: `Bearer ${token.trim()}` },
152
- });
153
- if (!tarballResponse.ok) {
154
- throw new Error(`Secure proxy rejected request: ${tarballResponse.statusText}`);
155
- }
156
- const buffer = await tarballResponse.arrayBuffer();
157
- fs.writeFileSync(tarballPath, Buffer.from(buffer));
158
- installSpinner.text = "Wiring up Enterprise dependencies...";
159
- installSpinner.color = "cyan";
160
- // Let npm natively handle the extraction and binary symlinking
161
- (0, child_process_1.execSync)(`npm install "${tarballPath}" --no-save`, {
162
- cwd: process.cwd(),
163
- stdio: "ignore",
164
- });
159
+ await (0, proInstaller_1.installProEngine)(API_BASE, token.trim());
165
160
  installSpinner.succeed(chalk_1.default.green("Enterprise Engine installed successfully!"));
166
161
  }
167
- catch (installError) {
168
- installSpinner.stop();
169
- console.error(chalk_1.default.bgRed.white("\n[RAW SYSTEM ERROR]:"), installError);
170
- console.log(chalk_1.default.red("\n✖ Failed to install the Enterprise Engine. Please check your internet connection or verify your project configuration."));
171
- if (fs.existsSync(nodeModulesPath)) {
172
- fs.rmSync(nodeModulesPath, { recursive: true, force: true });
173
- }
174
- console.log(chalk_1.default.yellow("⚠️ Reverting login: Enterprise Engine is required to sync."));
175
- if (fs.existsSync(credentialsPath))
176
- fs.unlinkSync(credentialsPath);
177
- process.exit(1);
178
- }
179
- finally {
180
- if (fs.existsSync(tarballPath)) {
181
- fs.unlinkSync(tarballPath);
182
- }
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;
183
166
  }
184
167
  }
185
168
  else {
@@ -194,7 +177,7 @@ program
194
177
  console.log(chalk_1.default.bold.green(`\n✅ Machine synchronized with Flowstride Enterprise.`));
195
178
  }
196
179
  catch (error) {
197
- spinner.fail(chalk_1.default.red(`Network Error: Could not connect to cloud.flowstride.io`));
180
+ spinner.fail(chalk_1.default.red((0, cloud_1.cloudFailure)(error, [token.trim()]).message));
198
181
  process.exit(1);
199
182
  }
200
183
  });
@@ -202,7 +185,8 @@ program
202
185
  .command("status")
203
186
  .description("Display the current Flowstride CLI authentication and engine tier")
204
187
  .action(async () => {
205
- let token = process.env.FLOWSTRIDE_TOKEN || null;
188
+ console.log((0, cloudSession_1.cloudSessionDescription)(API_BASE));
189
+ let token = process.env.FLOWSTRIDE_TOKEN?.trim() || null;
206
190
  const localCredentialsPath = path.join(process.cwd(), ".flowstride", "credentials.json");
207
191
  const globalCredentialsPath = path.join(os.homedir(), ".flowstride", "credentials.json");
208
192
  let activeCredentialsPath = localCredentialsPath; // Default for cleanup
@@ -211,7 +195,7 @@ program
211
195
  activeCredentialsPath = localCredentialsPath;
212
196
  try {
213
197
  const credentials = JSON.parse(fs.readFileSync(localCredentialsPath, "utf8"));
214
- token = credentials.machineToken;
198
+ token = credentials.machineToken?.trim() || null;
215
199
  }
216
200
  catch (err) { }
217
201
  }
@@ -219,7 +203,7 @@ program
219
203
  activeCredentialsPath = globalCredentialsPath;
220
204
  try {
221
205
  const credentials = JSON.parse(fs.readFileSync(globalCredentialsPath, "utf8"));
222
- token = credentials.machineToken;
206
+ token = credentials.machineToken?.trim() || null;
223
207
  }
224
208
  catch (err) { }
225
209
  }
@@ -241,9 +225,10 @@ program
241
225
  method: "GET",
242
226
  headers: { Authorization: `Bearer ${token}` },
243
227
  });
244
- if (response.status === 401) {
245
- console.log(chalk_1.default.red("🔴 AUTH EXPIRED: Your device session was revoked or expired."));
246
- if (fs.existsSync(activeCredentialsPath))
228
+ if (response.status === 401 || response.status === 403) {
229
+ const failureData = await response.json().catch(() => ({}));
230
+ console.log(chalk_1.default.red((0, cloud_1.cloudFailure)({ response: { status: response.status, data: failureData } }, [token]).message));
231
+ if (response.status === 401 && !process.env.FLOWSTRIDE_TOKEN && fs.existsSync(activeCredentialsPath))
247
232
  fs.unlinkSync(activeCredentialsPath);
248
233
  }
249
234
  else if (response.ok) {
@@ -271,12 +256,19 @@ program
271
256
  .option("--headed", "Run browser in headed mode")
272
257
  .option("--headless", "Run browser in headless mode")
273
258
  .option("--workers <number>", "Number of parallel workers to spawn (Pro feature)")
259
+ .option('--ci', 'Run without a dashboard and write CI reports')
260
+ .option('--device <profile>', 'desktop, phone, or landscape')
261
+ .option('--config <file>', 'Path to the Flowstride configuration')
262
+ .option('--env <name>', 'Environment profile')
274
263
  .action(async (targetPath, options) => {
275
264
  if (typeof targetPath === "object") {
276
265
  options = targetPath;
277
266
  targetPath = undefined;
278
267
  }
279
- let token = process.env.FLOWSTRIDE_TOKEN || null;
268
+ if (options.ci)
269
+ process.env.FLOWSTRIDE_NATIVE_CI = '1';
270
+ console.log((0, cloudSession_1.cloudSessionDescription)(API_BASE));
271
+ let token = process.env.FLOWSTRIDE_TOKEN?.trim() || null;
280
272
  let forceOS = false;
281
273
  const localCredentialsPath = path.join(process.cwd(), ".flowstride", "credentials.json");
282
274
  const globalCredentialsPath = path.join(os.homedir(), ".flowstride", "credentials.json");
@@ -286,7 +278,7 @@ program
286
278
  activeCredentialsPath = localCredentialsPath;
287
279
  try {
288
280
  const credentials = JSON.parse(fs.readFileSync(localCredentialsPath, "utf8"));
289
- token = credentials.machineToken;
281
+ token = credentials.machineToken?.trim() || null;
290
282
  }
291
283
  catch (err) { }
292
284
  }
@@ -294,7 +286,7 @@ program
294
286
  activeCredentialsPath = globalCredentialsPath;
295
287
  try {
296
288
  const credentials = JSON.parse(fs.readFileSync(globalCredentialsPath, "utf8"));
297
- token = credentials.machineToken;
289
+ token = credentials.machineToken?.trim() || null;
298
290
  }
299
291
  catch (err) { }
300
292
  }
@@ -317,15 +309,29 @@ program
317
309
  method: "GET",
318
310
  headers: { Authorization: `Bearer ${token}` },
319
311
  });
320
- if (response.status === 401) {
321
- console.log(chalk_1.default.red("🔴 AUTH EXPIRED: Your device session was revoked or expired."));
322
- if (fs.existsSync(activeCredentialsPath))
312
+ if (response.status === 401 || response.status === 403) {
313
+ const failureData = await response.json().catch(() => ({}));
314
+ console.log(chalk_1.default.red((0, cloud_1.cloudFailure)({ response: { status: response.status, data: failureData } }, [token]).message));
315
+ if (response.status === 401 && !process.env.FLOWSTRIDE_TOKEN && fs.existsSync(activeCredentialsPath))
323
316
  fs.unlinkSync(activeCredentialsPath);
324
317
  forceOS = true;
325
318
  }
326
319
  else if (response.ok) {
327
320
  const data = await response.json();
328
321
  const tier = data.tier.toUpperCase();
322
+ if ((0, config_1.loadNativeConfig)(process.cwd(), options).ci && fs.existsSync(nodeModulesPath)) {
323
+ const workspaceResponse = await fetch(`${API_BASE}/api/workspace/status`, {
324
+ headers: { Authorization: `Bearer ${token}` },
325
+ });
326
+ if (!workspaceResponse.ok)
327
+ forceOS = await handleOfflinePrompt();
328
+ else {
329
+ const workspaceData = await workspaceResponse.json();
330
+ options.manifest = workspaceData.manifest;
331
+ options.workspaceId = workspaceData.workspace?._id;
332
+ options.isCloudSyncActive = true;
333
+ }
334
+ }
329
335
  if (tier === "PERSONAL") {
330
336
  console.log(chalk_1.default.yellow(`🟡 [OS]: [${data.workspaceName}] (Tier: PERSONAL)`));
331
337
  }
@@ -344,7 +350,7 @@ program
344
350
  if (options.headed) {
345
351
  options.headless = false;
346
352
  }
347
- const resolvedTargetPath = targetPath ? targetPath : "./flows";
353
+ const resolvedTargetPath = targetPath || (0, config_1.loadNativeConfig)(process.cwd(), options).testDir;
348
354
  const strictUiFlowsDir = path.resolve(process.cwd(), resolvedTargetPath);
349
355
  if (!fs.existsSync(strictUiFlowsDir)) {
350
356
  console.error(`\n[ERROR]: Could not find ${strictUiFlowsDir}.`);
@@ -352,11 +358,17 @@ program
352
358
  process.exit(1);
353
359
  }
354
360
  options.forceOS = forceOS;
361
+ if (!forceOS && token) {
362
+ options.machineToken = token;
363
+ const file = fs.existsSync(localCredentialsPath) ? localCredentialsPath : globalCredentialsPath;
364
+ if (!options.workspaceId && fs.existsSync(file))
365
+ options.workspaceId = JSON.parse(fs.readFileSync(file, 'utf8')).workspaceId;
366
+ }
355
367
  await (0, run_1.runAction)(strictUiFlowsDir, options);
356
368
  });
357
369
  async function handleOfflinePrompt() {
358
370
  console.log(chalk_1.default.yellow("🛜 OFFLINE: Unable to reach Flowstride Cloud. Enterprise Sync paused."));
359
- if (process.env.CI || process.env.FLOWSTRIDE_TOKEN) {
371
+ if (process.env.CI || process.env.FLOWSTRIDE_NATIVE_CI || process.env.FLOWSTRIDE_TOKEN) {
360
372
  console.log(chalk_1.default.yellow("⚠️ CI/CD Environment: Automatically falling back to OS Engine."));
361
373
  return true;
362
374
  }
@@ -381,4 +393,16 @@ program
381
393
  .action(async () => {
382
394
  await (0, open_1.openAction)();
383
395
  });
396
+ program.command('install')
397
+ .description('Install Chromium matching this Flowstride engine')
398
+ .option('--with-deps', 'Install operating-system browser dependencies')
399
+ .action((options) => {
400
+ const { execFileSync } = require('node:child_process');
401
+ execFileSync(process.execPath, [path.join(path.dirname(require.resolve('playwright/package.json')), 'cli.js'), 'install', ...(options.withDeps ? ['--with-deps'] : []), 'chromium'], { stdio: 'inherit', env: { ...process.env, PLAYWRIGHT_SKIP_BROWSER_GC: '1' } });
402
+ });
403
+ program.command('ci')
404
+ .description('Set up native Flowstride CI')
405
+ .command('init <provider>')
406
+ .description('Generate a GitHub Actions workflow without overwriting existing files')
407
+ .action((provider) => (0, init_1.initCI)(provider));
384
408
  program.parse(process.argv);
@@ -37,6 +37,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.startManualWorkspaceServer = startManualWorkspaceServer;
40
+ const projectPaths_1 = require("../../utils/projectPaths");
41
+ const security_1 = require("../../manual/security");
40
42
  const http = __importStar(require("http"));
41
43
  const path = __importStar(require("path"));
42
44
  const fs = __importStar(require("fs"));
@@ -49,12 +51,12 @@ async function startManualWorkspaceServer() {
49
51
  console.error(chalk_1.default.red(`\n❌ Error: UI build bundle not found at ${uiDistPath}. Please build the UI first.`));
50
52
  process.exit(1);
51
53
  }
52
- const manualFlowsDir = path.join(process.cwd(), "manual_flows");
53
- const templatesDir = path.join(manualFlowsDir, "templates");
54
- const runsDir = path.join(manualFlowsDir, "runs");
55
- const mediaDir = path.join(manualFlowsDir, "media");
56
- const defectMediaDir = path.join(manualFlowsDir, "defect_media");
57
- const workspaceStateFile = path.join(manualFlowsDir, ".workspace_state.json");
54
+ const manualFlowsDir = (0, projectPaths_1.projectPath)(process.cwd(), path.join(process.cwd(), "manual_flows"));
55
+ const templatesDir = (0, projectPaths_1.projectPath)(process.cwd(), path.join(manualFlowsDir, "templates"));
56
+ const runsDir = (0, projectPaths_1.projectPath)(process.cwd(), path.join(manualFlowsDir, "runs"));
57
+ const mediaDir = (0, projectPaths_1.projectPath)(process.cwd(), path.join(manualFlowsDir, "media"));
58
+ const defectMediaDir = (0, projectPaths_1.projectPath)(process.cwd(), path.join(manualFlowsDir, "defect_media"));
59
+ const workspaceStateFile = (0, projectPaths_1.projectPath)(process.cwd(), path.join(manualFlowsDir, ".workspace_state.json"));
58
60
  if (!fs.existsSync(templatesDir))
59
61
  fs.mkdirSync(templatesDir, { recursive: true });
60
62
  if (!fs.existsSync(runsDir))
@@ -92,7 +94,7 @@ async function startManualWorkspaceServer() {
92
94
  return res.end(JSON.stringify({ error: "Invalid or missing file extension." }));
93
95
  }
94
96
  const safeFilename = `media_${Date.now()}_${Math.random().toString(36).substring(2, 9)}${ext.toLowerCase()}`;
95
- const filePath = path.join(mediaDir, safeFilename);
97
+ const filePath = (0, projectPaths_1.projectPath)(process.cwd(), path.join(mediaDir, safeFilename));
96
98
  const MAX_SIZE = 104857600;
97
99
  let uploadedSize = 0;
98
100
  let isAborted = false;
@@ -141,7 +143,7 @@ async function startManualWorkspaceServer() {
141
143
  }
142
144
  const rawFilename = targetPath.replace("/media/", "");
143
145
  const safeFilename = path.basename(rawFilename);
144
- const filePath = path.join(mediaDir, safeFilename);
146
+ const filePath = (0, projectPaths_1.projectPath)(process.cwd(), path.join(mediaDir, safeFilename));
145
147
  if (!fs.existsSync(filePath)) {
146
148
  res.writeHead(404, { "Content-Type": "application/json" });
147
149
  return res.end(JSON.stringify({ error: "Media file not found in vault" }));
@@ -166,7 +168,7 @@ async function startManualWorkspaceServer() {
166
168
  if (req.method === "GET" && pathname.startsWith("/media/")) {
167
169
  const filename = pathname.replace("/media/", "");
168
170
  const safeFilename = path.basename(filename);
169
- const filePath = path.join(mediaDir, safeFilename);
171
+ const filePath = (0, projectPaths_1.projectPath)(process.cwd(), path.join(mediaDir, safeFilename));
170
172
  if (!fs.existsSync(filePath)) {
171
173
  res.writeHead(404);
172
174
  return res.end("Media not found");
@@ -213,7 +215,7 @@ async function startManualWorkspaceServer() {
213
215
  return res.end(JSON.stringify({ error: "Invalid or missing file extension." }));
214
216
  }
215
217
  const safeFilename = `defect_${Date.now()}_${Math.random().toString(36).substring(2, 9)}${ext.toLowerCase()}`;
216
- const filePath = path.join(defectMediaDir, safeFilename);
218
+ const filePath = (0, projectPaths_1.projectPath)(process.cwd(), path.join(defectMediaDir, safeFilename));
217
219
  const MAX_SIZE = 41943040; // Capped at 40MB
218
220
  let uploadedSize = 0;
219
221
  let isAborted = false;
@@ -263,7 +265,7 @@ async function startManualWorkspaceServer() {
263
265
  }
264
266
  const rawFilename = targetPath.replace("/defect_media/", "");
265
267
  const safeFilename = path.basename(rawFilename);
266
- const filePath = path.join(defectMediaDir, safeFilename);
268
+ const filePath = (0, projectPaths_1.projectPath)(process.cwd(), path.join(defectMediaDir, safeFilename));
267
269
  if (fs.existsSync(filePath)) {
268
270
  fs.unlinkSync(filePath);
269
271
  console.log(chalk_1.default.yellow(`\n[Local Cache Cleared]: Asset securely removed from manual_flows/defect_media/${safeFilename}`));
@@ -286,7 +288,7 @@ async function startManualWorkspaceServer() {
286
288
  if (req.method === "GET" && pathname.startsWith("/defect_media/")) {
287
289
  const filename = pathname.replace("/defect_media/", "");
288
290
  const safeFilename = path.basename(filename);
289
- const filePath = path.join(defectMediaDir, safeFilename);
291
+ const filePath = (0, projectPaths_1.projectPath)(process.cwd(), path.join(defectMediaDir, safeFilename));
290
292
  if (!fs.existsSync(filePath)) {
291
293
  res.writeHead(404);
292
294
  return res.end("Defect media not found");
@@ -326,13 +328,17 @@ async function startManualWorkspaceServer() {
326
328
  // CORE SYSTEM ROUTES
327
329
  // ============================================================================
328
330
  if (req.method === "POST" && pathname === "/api/save-design") {
329
- let body = "";
330
- req.on("data", (chunk) => {
331
- body += chunk.toString();
332
- });
333
- req.on("end", () => {
331
+ let payload;
332
+ try {
333
+ payload = await (0, security_1.readManualPayload)(req);
334
+ }
335
+ catch (error) {
336
+ res.writeHead(error instanceof security_1.ManualInputError ? error.status : 400, { 'Content-Type': 'application/json' });
337
+ res.end(JSON.stringify({ error: error instanceof security_1.ManualInputError ? error.message : 'Invalid manual payload' }));
338
+ return;
339
+ }
340
+ const savePayload = async () => {
334
341
  try {
335
- const payload = JSON.parse(body);
336
342
  const rawFilename = payload.filename || `New Flow.json`;
337
343
  const canvasData = payload.canvasData;
338
344
  if (!canvasData) {
@@ -343,7 +349,7 @@ async function startManualWorkspaceServer() {
343
349
  if (!safeFilename.endsWith(".json")) {
344
350
  safeFilename += ".json";
345
351
  }
346
- const filePath = path.join(templatesDir, safeFilename);
352
+ const filePath = (0, projectPaths_1.projectPath)(process.cwd(), path.join(templatesDir, safeFilename));
347
353
  fs.writeFileSync(filePath, JSON.stringify(canvasData, null, 2), "utf-8");
348
354
  console.log(chalk_1.default.green(`\n[Saved]: Template securely vaulted to manual_flows/templates/${safeFilename}`));
349
355
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -360,7 +366,8 @@ async function startManualWorkspaceServer() {
360
366
  error: "Internal server error while saving template.",
361
367
  }));
362
368
  }
363
- });
369
+ };
370
+ await savePayload();
364
371
  return;
365
372
  }
366
373
  if (req.method === "GET" && pathname === "/api/templates") {
@@ -398,7 +405,7 @@ async function startManualWorkspaceServer() {
398
405
  error: "Forbidden: Only JSON configuration files can be loaded",
399
406
  }));
400
407
  }
401
- const filePath = path.join(templatesDir, safeFilename);
408
+ const filePath = (0, projectPaths_1.projectPath)(process.cwd(), path.join(templatesDir, safeFilename));
402
409
  if (!fs.existsSync(filePath)) {
403
410
  res.writeHead(404, { "Content-Type": "application/json" });
404
411
  return res.end(JSON.stringify({ error: "Template file not found" }));
@@ -430,7 +437,7 @@ async function startManualWorkspaceServer() {
430
437
  error: "Forbidden: Only JSON configuration files can be deleted",
431
438
  }));
432
439
  }
433
- const filePath = path.join(templatesDir, safeFilename);
440
+ const filePath = (0, projectPaths_1.projectPath)(process.cwd(), path.join(templatesDir, safeFilename));
434
441
  if (!fs.existsSync(filePath)) {
435
442
  res.writeHead(404, { "Content-Type": "application/json" });
436
443
  return res.end(JSON.stringify({ error: "Template file not found" }));
@@ -453,13 +460,17 @@ async function startManualWorkspaceServer() {
453
460
  return;
454
461
  }
455
462
  if (req.method === "POST" && pathname === "/api/save-run") {
456
- let body = "";
457
- req.on("data", (chunk) => {
458
- body += chunk.toString();
459
- });
460
- req.on("end", async () => {
463
+ let payload;
464
+ try {
465
+ payload = await (0, security_1.readManualPayload)(req);
466
+ }
467
+ catch (error) {
468
+ res.writeHead(error instanceof security_1.ManualInputError ? error.status : 400, { 'Content-Type': 'application/json' });
469
+ res.end(JSON.stringify({ error: error instanceof security_1.ManualInputError ? error.message : 'Invalid manual payload' }));
470
+ return;
471
+ }
472
+ const savePayload = async () => {
461
473
  try {
462
- const payload = JSON.parse(body);
463
474
  const rawFilename = payload.filename || `Execution_Run_${Date.now()}.json`;
464
475
  const canvasData = payload.canvasData;
465
476
  if (!canvasData) {
@@ -469,7 +480,7 @@ async function startManualWorkspaceServer() {
469
480
  const now = new Date();
470
481
  const dayFolder = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
471
482
  const timeFolder = `${String(now.getHours()).padStart(2, "0")}-${String(now.getMinutes()).padStart(2, "0")}-${String(now.getSeconds()).padStart(2, "0")}`;
472
- const runTargetDir = path.join(runsDir, dayFolder, timeFolder);
483
+ const runTargetDir = (0, projectPaths_1.projectPath)(process.cwd(), path.join(runsDir, dayFolder, timeFolder));
473
484
  if (!fs.existsSync(runTargetDir)) {
474
485
  fs.mkdirSync(runTargetDir, { recursive: true });
475
486
  }
@@ -477,7 +488,7 @@ async function startManualWorkspaceServer() {
477
488
  if (!safeFilename.endsWith(".json")) {
478
489
  safeFilename += ".json";
479
490
  }
480
- const filePath = path.join(runTargetDir, safeFilename);
491
+ const filePath = (0, projectPaths_1.projectPath)(process.cwd(), path.join(runTargetDir, safeFilename));
481
492
  fs.writeFileSync(filePath, JSON.stringify(canvasData, null, 2), "utf-8");
482
493
  const relativePath = path
483
494
  .join(dayFolder, timeFolder, safeFilename)
@@ -498,7 +509,8 @@ async function startManualWorkspaceServer() {
498
509
  error: "Internal server error while saving run.",
499
510
  }));
500
511
  }
501
- });
512
+ };
513
+ await savePayload();
502
514
  return;
503
515
  }
504
516
  if (req.method === "GET" && pathname === "/api/runs") {
@@ -589,7 +601,7 @@ async function startManualWorkspaceServer() {
589
601
  res.writeHead(403, { "Content-Type": "application/json" });
590
602
  return res.end(JSON.stringify({ error: "Forbidden: Invalid file path" }));
591
603
  }
592
- const filePath = path.join(runsDir, targetFilename);
604
+ const filePath = (0, projectPaths_1.projectPath)(process.cwd(), path.join(runsDir, targetFilename));
593
605
  const resolvedPath = path.resolve(filePath);
594
606
  const resolvedRunsDir = path.resolve(runsDir);
595
607
  if (!resolvedPath.startsWith(resolvedRunsDir) ||
@@ -0,0 +1,7 @@
1
+ /** Resolve .env before authentication, using the same precedence as execution. */
2
+ export declare function loadCloudEnvironment(root: string, profile?: string): string;
3
+ export declare function cloudSessionDescription(apiBase: string): string;
4
+ export declare function tokenExpiryDescription(data: {
5
+ tokenExpiresAt?: string;
6
+ tokenScope?: string;
7
+ }): string;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadCloudEnvironment = loadCloudEnvironment;
7
+ exports.cloudSessionDescription = cloudSessionDescription;
8
+ exports.tokenExpiryDescription = tokenExpiryDescription;
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const node_os_1 = __importDefault(require("node:os"));
12
+ const dotenv_1 = __importDefault(require("dotenv"));
13
+ const cloud_1 = require("../../utils/cloud");
14
+ /** Resolve .env before authentication, using the same precedence as execution. */
15
+ function loadCloudEnvironment(root, profile) {
16
+ if (profile && !/^[a-zA-Z0-9_-]+$/.test(profile))
17
+ throw new Error('Invalid environment profile name.');
18
+ const files = profile ? [`.env.${profile}`, '.env'] : ['.env'];
19
+ for (const file of files) {
20
+ const filename = node_path_1.default.join(root, file);
21
+ if (node_fs_1.default.existsSync(filename))
22
+ dotenv_1.default.config({ path: filename, quiet: true });
23
+ }
24
+ return (0, cloud_1.cloudApiBase)(process.env);
25
+ }
26
+ function cloudSessionDescription(apiBase) {
27
+ const source = process.env.FLOWSTRIDE_TOKEN ? 'FLOWSTRIDE_TOKEN (environment)'
28
+ : node_fs_1.default.existsSync(node_path_1.default.join(process.cwd(), '.flowstride', 'credentials.json')) ? 'project credentials'
29
+ : node_fs_1.default.existsSync(node_path_1.default.join(node_os_1.default.homedir(), '.flowstride', 'credentials.json')) ? 'global credentials' : 'no saved credentials';
30
+ return `[Cloud]: ${apiBase}; credential source: ${source}`;
31
+ }
32
+ function tokenExpiryDescription(data) {
33
+ const expiry = data.tokenExpiresAt ? new Date(data.tokenExpiresAt) : null;
34
+ const label = expiry && Number.isFinite(expiry.getTime()) ? expiry.toISOString() : 'unavailable from this Cloud server';
35
+ const scope = ['execution', 'admin', 'readonly'].includes(data.tokenScope || '') ? `; scope: ${data.tokenScope}` : '';
36
+ return `[Cloud]: Token expiry (UTC): ${label}${scope}. Logging in with the same key does not renew it.`;
37
+ }
@@ -0,0 +1,5 @@
1
+ export declare function validateProArchive(buffer: Buffer): Promise<string>;
2
+ export declare function installProEngine(apiBase: string, token: string, root?: string): Promise<{
3
+ version: string;
4
+ sha256: string;
5
+ }>;