flowstride 1.0.20-rc.2 → 1.0.20

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 (43) hide show
  1. package/NATIVE_CI.md +88 -145
  2. package/README.md +46 -221
  3. package/dist/adapters/web/playwright.d.ts +4 -1
  4. package/dist/adapters/web/playwright.js +75 -6
  5. package/dist/ci/config.js +6 -5
  6. package/dist/ci/runtime.js +27 -14
  7. package/dist/cli/commands/run.js +2 -1
  8. package/dist/cli/index.js +35 -50
  9. package/dist/cli/manual_server/workspace_server.js +44 -32
  10. package/dist/cli/utils/cloudSession.d.ts +7 -0
  11. package/dist/cli/utils/cloudSession.js +37 -0
  12. package/dist/cli/utils/proInstaller.d.ts +5 -0
  13. package/dist/cli/utils/proInstaller.js +141 -0
  14. package/dist/game/sdk.js +3 -2
  15. package/dist/index.d.ts +5 -0
  16. package/dist/index.js +9 -0
  17. package/dist/manual/security.d.ts +183 -0
  18. package/dist/manual/security.js +106 -0
  19. package/dist/runner/data_generators.d.ts +1 -0
  20. package/dist/runner/data_generators.js +3 -0
  21. package/dist/runner/dom_stripper.js +3 -0
  22. package/dist/runner/flow_orchestrator.js +13 -22
  23. package/dist/runner/flow_worker.d.ts +1 -0
  24. package/dist/runner/flow_worker.js +68 -47
  25. package/dist/runner/persistent_vault.js +2 -1
  26. package/dist/runner/plugin_manager.js +3 -1
  27. package/dist/security/localDashboard.d.ts +2 -0
  28. package/dist/security/localDashboard.js +22 -0
  29. package/dist/security/redaction.d.ts +4 -0
  30. package/dist/security/redaction.js +51 -0
  31. package/dist/sessions/manager.js +3 -1
  32. package/dist/types/index.d.ts +9 -0
  33. package/dist/utils/cloud.d.ts +6 -0
  34. package/dist/utils/cloud.js +33 -0
  35. package/dist/utils/projectPaths.d.ts +11 -0
  36. package/dist/utils/projectPaths.js +90 -0
  37. package/dist/utils/replayRequest.d.ts +8 -0
  38. package/dist/utils/replayRequest.js +77 -0
  39. package/package.json +23 -14
  40. package/src/ui/dist/assets/index-B7MuBTWm.js +460 -0
  41. package/src/ui/dist/index.html +1 -1
  42. package/RELEASE_NOTES.md +0 -40
  43. package/src/ui/dist/assets/index-DIC22ZK_.js +0 -638
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.WebAdapter = void 0;
40
+ const projectPaths_1 = require("../../utils/projectPaths");
40
41
  const playwright_1 = require("playwright");
41
42
  const fs = __importStar(require("fs"));
42
43
  const path = __importStar(require("path"));
@@ -51,6 +52,21 @@ class WebAdapter {
51
52
  page = null;
52
53
  discoveredPages = [];
53
54
  dialogHandler = null;
55
+ pendingSessionStorageRestore = null;
56
+ async clearSessionStorageRestore() {
57
+ const pending = this.pendingSessionStorageRestore;
58
+ this.pendingSessionStorageRestore = null;
59
+ if (!pending)
60
+ return;
61
+ pending.page.off('framenavigated', pending.onNavigation);
62
+ try {
63
+ await pending.script.dispose();
64
+ }
65
+ catch (error) {
66
+ if (!pending.page.isClosed())
67
+ throw error;
68
+ }
69
+ }
54
70
  constructor(config) {
55
71
  this.config = config;
56
72
  }
@@ -60,6 +76,7 @@ class WebAdapter {
60
76
  return this.page;
61
77
  }
62
78
  async initialise(audioInjectionPath) {
79
+ await this.clearSessionStorageRestore();
63
80
  const launchArgs = this.config.headless
64
81
  ? [
65
82
  "--use-fake-ui-for-media-stream",
@@ -81,7 +98,7 @@ class WebAdapter {
81
98
  ...(this.config.ci ? { handleSIGINT: false, handleSIGTERM: false } : {}),
82
99
  args: launchArgs,
83
100
  });
84
- const videoDir = path.join(process.cwd(), ".flowstride", "videos");
101
+ const videoDir = (0, projectPaths_1.projectPath)(process.cwd(), path.join('.flowstride', 'videos'));
85
102
  if (!fs.existsSync(videoDir))
86
103
  fs.mkdirSync(videoDir, { recursive: true });
87
104
  const profile = this.config.device === 'phone' || this.config.device === 'landscape'
@@ -125,7 +142,7 @@ class WebAdapter {
125
142
  };
126
143
  }
127
144
  async injectAudio(filePath) {
128
- const absolutePath = path.resolve(process.cwd(), filePath);
145
+ const absolutePath = (0, projectPaths_1.projectPath)(process.cwd(), filePath);
129
146
  if (!fs.existsSync(absolutePath)) {
130
147
  throw new Error(`Audio file not found at: ${absolutePath}`);
131
148
  }
@@ -182,9 +199,26 @@ class WebAdapter {
182
199
  await this.page.bringToFront();
183
200
  await this.page.waitForLoadState("domcontentloaded").catch(() => { });
184
201
  }
185
- async injectStorage(cookies, localStorage, fallbackDomain) {
202
+ async injectStorage(cookies, localStorage, fallbackDomain, sessionStorage, storageOrigin) {
186
203
  if (!this.context || !this.page)
187
204
  return;
205
+ await this.clearSessionStorageRestore();
206
+ const page = this.page;
207
+ const originOf = (value) => {
208
+ try {
209
+ const url = new URL(value || '');
210
+ return ['http:', 'https:'].includes(url.protocol) ? url.origin : undefined;
211
+ }
212
+ catch {
213
+ return undefined;
214
+ }
215
+ };
216
+ const hasSessionStorage = !!sessionStorage && Object.keys(sessionStorage).length > 0;
217
+ const origin = storageOrigin !== undefined ? originOf(storageOrigin)
218
+ : originOf(page.url()) || originOf(this.config.baseUrl);
219
+ if (hasSessionStorage && !origin) {
220
+ throw new Error('Session storage restoration requires a saved HTTP(S) origin, an open application page, or baseUrl.');
221
+ }
188
222
  if (cookies && cookies.length > 0) {
189
223
  const formattedCookies = [];
190
224
  for (const cookie of cookies) {
@@ -231,6 +265,32 @@ class WebAdapter {
231
265
  }, localStorage);
232
266
  }
233
267
  }
268
+ if (hasSessionStorage) {
269
+ const restore = (data) => {
270
+ if (window !== window.top || window.location.origin !== data.origin)
271
+ return;
272
+ for (const [key, value] of Object.entries(data.values))
273
+ window.sessionStorage.setItem(key, value);
274
+ };
275
+ const data = { origin: origin, values: sessionStorage };
276
+ if (originOf(page.url()) === origin) {
277
+ await page.evaluate(restore, data);
278
+ }
279
+ else {
280
+ // Seed before application scripts run; remove the seed after the first
281
+ // matching document so reloads preserve later application changes.
282
+ const script = await page.addInitScript(restore, data);
283
+ const onNavigation = (frame) => {
284
+ if (frame !== page.mainFrame() || originOf(frame.url()) !== origin)
285
+ return;
286
+ void this.clearSessionStorageRestore().catch(() => {
287
+ console.warn('[Session]: Could not remove the completed session storage initializer.');
288
+ });
289
+ };
290
+ this.pendingSessionStorageRestore = { page, script, onNavigation };
291
+ page.on('framenavigated', onNavigation);
292
+ }
293
+ }
234
294
  }
235
295
  async getState() {
236
296
  const cookies = (await this.context?.cookies()) || [];
@@ -249,7 +309,14 @@ class WebAdapter {
249
309
  console.warn("Warning: Could not access storage. Ensure page is loaded.");
250
310
  }
251
311
  }
252
- return { cookies, localStorage, sessionStorage };
312
+ let storageOrigin;
313
+ try {
314
+ const url = new URL(this.page?.url() || '');
315
+ if (['http:', 'https:'].includes(url.protocol))
316
+ storageOrigin = url.origin;
317
+ }
318
+ catch { }
319
+ return { cookies, localStorage, sessionStorage, storageOrigin };
253
320
  }
254
321
  getCurrentUrl() {
255
322
  return this.page ? this.page.url() : "about:blank";
@@ -274,7 +341,8 @@ class WebAdapter {
274
341
  });
275
342
  }
276
343
  synthesizeCurl(method, url, headers, postData) {
277
- let curl = `curl -X ${method.toUpperCase()} '${url}'`;
344
+ const safeUrl = url.replace(/'/g, "'\\''");
345
+ let curl = `curl -X ${method.toUpperCase()} '${safeUrl}'`;
278
346
  for (const [key, value] of Object.entries(headers)) {
279
347
  if (key.startsWith(":"))
280
348
  continue;
@@ -614,7 +682,7 @@ class WebAdapter {
614
682
  const targetLoc = await this.checkAmbiguity(loc, selector);
615
683
  await targetLoc.waitFor({ state: "visible" });
616
684
  await targetLoc.scrollIntoViewIfNeeded();
617
- const absolutePath = path.resolve(process.cwd(), filePath);
685
+ const absolutePath = (0, projectPaths_1.projectPath)(process.cwd(), filePath);
618
686
  if (!fs.existsSync(absolutePath)) {
619
687
  throw new Error(`File Upload Error: Could not find the file at "${absolutePath}". Please ensure the file exists in your project directory.`);
620
688
  }
@@ -1194,6 +1262,7 @@ class WebAdapter {
1194
1262
  }
1195
1263
  async shutdown(videoTargetPath) {
1196
1264
  try {
1265
+ await this.clearSessionStorageRestore();
1197
1266
  const video = this.page?.video();
1198
1267
  if (this.context) {
1199
1268
  await this.context.close();
package/dist/ci/config.js CHANGED
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.isCIEnvironment = isCIEnvironment;
7
7
  exports.loadNativeConfig = loadNativeConfig;
8
+ const projectPaths_1 = require("../utils/projectPaths");
8
9
  const node_fs_1 = __importDefault(require("node:fs"));
9
10
  const node_path_1 = __importDefault(require("node:path"));
10
11
  function isCIEnvironment() {
@@ -12,7 +13,7 @@ function isCIEnvironment() {
12
13
  .some(key => !!process.env[key] && process.env[key] !== 'false' && process.env[key] !== '0');
13
14
  }
14
15
  function loadNativeConfig(root, options) {
15
- const file = node_path_1.default.join(root, options.config || 'flowstride.config.json');
16
+ const file = (0, projectPaths_1.projectPath)(root, options.config || 'flowstride.config.json');
16
17
  let input = {};
17
18
  if (node_fs_1.default.existsSync(file)) {
18
19
  input = JSON.parse(node_fs_1.default.readFileSync(file, 'utf8'));
@@ -21,8 +22,8 @@ function loadNativeConfig(root, options) {
21
22
  }
22
23
  else if (options.config)
23
24
  throw new Error(`Configuration not found: ${file}`);
24
- const config = { ...input, timeout: input.timeout ?? 30000, headless: input.headless ?? true,
25
- ci: !!options.ci || isCIEnvironment(), device: options.device ?? input.device ?? 'desktop',
25
+ const config = { ...input, projectRoot: node_path_1.default.resolve(root), timeout: input.timeout ?? 30000, headless: input.headless ?? true,
26
+ ci: !!options.ci || isCIEnvironment(), bail: options.bail || input.bail || false, device: options.device ?? input.device ?? 'desktop',
26
27
  artifactsDir: input.artifactsDir ?? 'flowstride-artifacts', testDir: input.testDir ?? 'flows',
27
28
  defaultDialogBehavior: input.defaultDialogBehavior ?? 'dismiss',
28
29
  recordVideo: input.recordVideo ?? true, browserErrors: input.browserErrors ?? 'fail',
@@ -50,7 +51,7 @@ function loadNativeConfig(root, options) {
50
51
  continue;
51
52
  if (typeof config[key] !== 'string' || !config[key].trim())
52
53
  throw new Error(`${key} must be a nonempty path.`);
53
- const resolved = node_path_1.default.resolve(root, config[key]);
54
+ const resolved = (0, projectPaths_1.projectPath)(root, config[key]);
54
55
  const relative = node_path_1.default.relative(root, resolved);
55
56
  if ((key === 'artifactsDir' && !relative) || relative === '..' || relative.startsWith(`..${node_path_1.default.sep}`) || node_path_1.default.isAbsolute(relative))
56
57
  throw new Error(`${key} must be a directory inside the project.`);
@@ -67,7 +68,7 @@ function loadNativeConfig(root, options) {
67
68
  throw new Error('webServer readiness URL must use local HTTP(S).');
68
69
  if (server.timeout !== undefined && (!Number.isSafeInteger(server.timeout) || server.timeout < 1))
69
70
  throw new Error('webServer.timeout must be positive.');
70
- server.cwd = node_path_1.default.resolve(root, server.cwd || '.');
71
+ server.cwd = (0, projectPaths_1.projectPath)(root, server.cwd || '.');
71
72
  const relative = node_path_1.default.relative(root, server.cwd);
72
73
  if (relative === '..' || relative.startsWith(`..${node_path_1.default.sep}`) || node_path_1.default.isAbsolute(relative))
73
74
  throw new Error('webServer.cwd must be inside the project.');
@@ -4,19 +4,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runNativeCI = runNativeCI;
7
+ const node_crypto_1 = require("node:crypto");
8
+ const projectPaths_1 = require("../utils/projectPaths");
7
9
  const node_fs_1 = __importDefault(require("node:fs"));
8
10
  const node_path_1 = __importDefault(require("node:path"));
9
11
  const node_child_process_1 = require("node:child_process");
10
12
  const promises_1 = require("node:timers/promises");
11
13
  const lexer_1 = require("../parser/lexer");
12
14
  const parser_1 = require("../parser/parser");
13
- function listFlows(target) {
14
- const stat = node_fs_1.default.statSync(target);
15
- if (stat.isFile())
16
- return target.endsWith('.flow') ? [target] : [];
17
- return node_fs_1.default.readdirSync(target).filter(name => name.endsWith('.flow')).sort().map(name => node_path_1.default.join(target, name));
18
- }
19
- function safeName(name) { return name.replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 150); }
15
+ function safeName(name) { return name.replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 120) + '-' + (0, node_crypto_1.createHash)('sha256').update(name).digest('hex').slice(0, 12); }
20
16
  function xml(value) { return String(value).replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '').replace(/[<>&"']/g, char => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&apos;' }[char])); }
21
17
  async function runNativeCI(worker, orchestrator, target, config, engine) {
22
18
  const started = Date.now();
@@ -27,6 +23,8 @@ async function runNativeCI(worker, orchestrator, target, config, engine) {
27
23
  const steps = new Map();
28
24
  const completed = new Set();
29
25
  const errors = [];
26
+ const planned = new Map();
27
+ let bailed = false;
30
28
  let expected = 0;
31
29
  let files = [];
32
30
  let server;
@@ -36,6 +34,8 @@ async function runNativeCI(worker, orchestrator, target, config, engine) {
36
34
  if (!data?.payload)
37
35
  return;
38
36
  const p = data.payload;
37
+ if (data.type === 'BAIL')
38
+ bailed = true;
39
39
  if (data.type === 'LOG_EVENT' && p.status === 'error') {
40
40
  const active = [...steps.values()].reverse().find(step => step.fileName === p.fileName && step.status === 'running');
41
41
  if (active)
@@ -56,6 +56,8 @@ async function runNativeCI(worker, orchestrator, target, config, engine) {
56
56
  errors.push(`${p.fileName}: ${p.error || 'Execution failed'}`);
57
57
  if (data.type === 'BROWSER_ERROR' && config.browserErrors !== 'record')
58
58
  errors.push(`Browser: ${p.message}`);
59
+ if (data.type === 'CLOUD_SYNC')
60
+ config.cloudSyncResult = p;
59
61
  if (data.type === 'WORKER_ERROR')
60
62
  errors.push(p.message || 'Worker failed');
61
63
  };
@@ -67,11 +69,18 @@ async function runNativeCI(worker, orchestrator, target, config, engine) {
67
69
  process.on('SIGINT', interrupt);
68
70
  process.on('SIGTERM', interrupt);
69
71
  try {
70
- files = listFlows(target);
72
+ files = (0, projectPaths_1.discoverFlows)(target, config.projectRoot || process.cwd());
73
+ config.flowRoot = node_fs_1.default.statSync(target).isDirectory() ? target : node_path_1.default.dirname(target);
71
74
  if (!files.length)
72
75
  throw new Error('No .flow tests found.');
73
76
  for (const file of files) {
74
- const features = new parser_1.Parser(new lexer_1.Lexer(node_fs_1.default.readFileSync(file, 'utf8')).tokenise()).parse();
77
+ const features = (0, projectPaths_1.selectedFeatures)(new parser_1.Parser(new lexer_1.Lexer(node_fs_1.default.readFileSync(file, 'utf8')).tokenise()).parse());
78
+ for (const feature of features)
79
+ for (const scenario of feature.scenarios)
80
+ for (const step of scenario.steps) {
81
+ const fileName = (0, projectPaths_1.flowIdentity)(file, config.flowRoot);
82
+ planned.set(`${fileName}:${step.id}`, { id: step.id, description: step.blockDescription, command: step.action, fileName, status: 'skipped', reason: 'bail' });
83
+ }
75
84
  expected += features.reduce((n, feature) => n + feature.scenarios.reduce((m, scenario) => m + scenario.steps.length, 0), 0);
76
85
  }
77
86
  if (!expected)
@@ -150,26 +159,30 @@ async function runNativeCI(worker, orchestrator, target, config, engine) {
150
159
  }
151
160
  if (interrupted)
152
161
  errors.push('Run interrupted.');
162
+ if (bailed)
163
+ for (const [key, step] of planned)
164
+ if (!steps.has(key))
165
+ steps.set(key, step);
153
166
  const results = [...steps.values()];
154
167
  const terminal = results.filter(step => ['passed', 'failed', 'skipped', 'not_tested'].includes(step.status));
155
168
  const incomplete = Math.max(0, expected - terminal.length);
156
169
  for (const file of files)
157
- if (!completed.has(node_path_1.default.basename(file)))
158
- errors.push(`Incomplete file: ${node_path_1.default.basename(file)}`);
170
+ if (!bailed && !completed.has((0, projectPaths_1.flowIdentity)(file, config.flowRoot)))
171
+ errors.push(`Incomplete file: ${(0, projectPaths_1.flowIdentity)(file, config.flowRoot)}`);
159
172
  if (results.some(step => !['passed', 'failed', 'skipped', 'not_tested'].includes(step.status)))
160
173
  errors.push('Unfinished test steps.');
161
174
  if (terminal.length > expected)
162
175
  errors.push('Engine produced an unexpected step count.');
163
176
  const metrics = { total: expected, passed: results.filter(s => s.status === 'passed').length,
164
177
  failed: results.filter(s => s.status === 'failed').length, skipped: results.filter(s => ['skipped', 'not_tested'].includes(s.status)).length, incomplete };
165
- const result = { protocol: 1, engine, status: interrupted ? 'interrupted' : errors.length || metrics.failed || incomplete ? 'failed' : 'passed',
166
- duration: Date.now() - started, artifactsDir: directory, files: files.map(file => node_path_1.default.basename(file)), steps: results, errors, metrics };
178
+ const result = { cloudSync: config.cloudSyncResult, protocol: 1, engine, status: interrupted ? 'interrupted' : errors.length || metrics.failed || incomplete ? 'failed' : 'passed',
179
+ duration: Date.now() - started, artifactsDir: directory, files: files.map(file => (0, projectPaths_1.flowIdentity)(file, config.flowRoot)), steps: results, errors, metrics };
167
180
  // Existing workers finalize videos into these locations. Copy only run files,
168
181
  // never the project directory or credentials.
169
182
  if (config.recordVideo !== false)
170
183
  for (const file of files) {
171
184
  for (const folder of ['artifacts', 'mock-cloud']) {
172
- const video = node_path_1.default.join(process.cwd(), '.flowstride', folder, node_path_1.default.basename(file, '.flow') + '.webm');
185
+ const video = node_path_1.default.join(process.cwd(), '.flowstride', folder, (0, projectPaths_1.videoFileName)((0, projectPaths_1.flowIdentity)(file, config.flowRoot)));
173
186
  if (node_fs_1.default.existsSync(video) && node_fs_1.default.statSync(video).mtimeMs >= started) {
174
187
  node_fs_1.default.copyFileSync(video, node_path_1.default.join(directory, node_path_1.default.basename(video)));
175
188
  break;
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.runAction = runAction;
37
+ const projectPaths_1 = require("../../utils/projectPaths");
37
38
  const config_1 = require("../../ci/config");
38
39
  const runtime_1 = require("../../ci/runtime");
39
40
  const fs = __importStar(require("fs"));
@@ -53,7 +54,7 @@ const api_proxy_1 = require("../../sdk/api_proxy");
53
54
  async function runAction(targetPath, options) {
54
55
  const root = process.cwd();
55
56
  const profileEnvPath = options.env
56
- ? path.resolve(root, `.env.${options.env}`)
57
+ ? (0, projectPaths_1.projectPath)(root, `.env.${options.env}`)
57
58
  : null;
58
59
  const defaultEnvPath = path.resolve(root, ".env");
59
60
  if (profileEnvPath) {
package/dist/cli/index.js CHANGED
@@ -37,6 +37,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  return (mod && mod.__esModule) ? mod : { "default": mod };
38
38
  };
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
+ const cloud_1 = require("../utils/cloud");
41
+ const cloudSession_1 = require("./utils/cloudSession");
40
42
  const config_1 = require("../ci/config");
41
43
  const init_1 = require("../ci/init");
42
44
  const commander_1 = require("commander");
@@ -47,16 +49,21 @@ const open_1 = require("./commands/open");
47
49
  const fs = __importStar(require("fs"));
48
50
  const path = __importStar(require("path"));
49
51
  const os = __importStar(require("os"));
50
- const child_process_1 = require("child_process");
52
+ const proInstaller_1 = require("./utils/proInstaller");
51
53
  const prompts_1 = require("@inquirer/prompts");
52
54
  const chalk_1 = __importDefault(require("chalk"));
53
55
  const ora_1 = __importDefault(require("ora"));
54
56
  const program = new commander_1.Command();
55
- const API_BASE = process.env.FLOWSTRIDE_API_URL || "https://cloud.flowstride.io";
57
+ program.hook('preAction', (_program, command) => {
58
+ if (['login', 'status', 'run', 'update'].includes(command.name())) {
59
+ API_BASE = (0, cloudSession_1.loadCloudEnvironment)(process.cwd(), command.opts().env);
60
+ }
61
+ });
62
+ let API_BASE = (0, cloud_1.cloudApiBase)(process.env);
56
63
  program
57
64
  .name("flowstride")
58
65
  .description("Flowstride: The Enterprise Flow-First Automation CLI")
59
- .version("1.0.20-rc.2");
66
+ .version("1.0.20");
60
67
  program
61
68
  .command("init")
62
69
  .description("Initialise a new Flowstride workspace in the current directory")
@@ -101,6 +108,7 @@ program
101
108
  console.error(chalk_1.default.red("✖ Error: A valid Developer Token is required."));
102
109
  process.exit(1);
103
110
  }
111
+ console.log(`[Cloud]: ${API_BASE}; login credential source: ${process.env.FLOWSTRIDE_TOKEN ? 'FLOWSTRIDE_TOKEN (environment)' : options.token ? '--token option' : 'interactive prompt'}`);
104
112
  const deviceName = os.hostname();
105
113
  const spinner = (0, ora_1.default)("Authenticating with Cloud...").start();
106
114
  try {
@@ -114,10 +122,11 @@ program
114
122
  });
115
123
  const data = await response.json();
116
124
  if (!response.ok) {
117
- spinner.fail(chalk_1.default.red(`Authentication Failed: ${data.error || "Invalid token or network error"}`));
125
+ spinner.fail(chalk_1.default.red(`Authentication Failed: ${(0, cloud_1.cloudFailure)({ response: { status: response.status, data } }, [token.trim()]).message}`));
118
126
  process.exit(1);
119
127
  }
120
128
  spinner.succeed(chalk_1.default.green("Authentication Successful!"));
129
+ console.log((0, cloudSession_1.tokenExpiryDescription)(data));
121
130
  const configDir = options.global
122
131
  ? path.join(os.homedir(), ".flowstride")
123
132
  : path.join(process.cwd(), ".flowstride");
@@ -146,42 +155,14 @@ program
146
155
  }
147
156
  if (installPro) {
148
157
  const installSpinner = (0, ora_1.default)("Downloading Enterprise Engine via secure proxy...").start();
149
- const tarballPath = path.join(process.cwd(), "flowstride-pro.tgz");
150
158
  try {
151
- const tarballResponse = await fetch(`${API_BASE}/api/workspace/download-pro`, {
152
- method: "GET",
153
- headers: { Authorization: `Bearer ${token.trim()}` },
154
- });
155
- if (!tarballResponse.ok) {
156
- throw new Error(`Secure proxy rejected request: ${tarballResponse.statusText}`);
157
- }
158
- const buffer = await tarballResponse.arrayBuffer();
159
- fs.writeFileSync(tarballPath, Buffer.from(buffer));
160
- installSpinner.text = "Wiring up Enterprise dependencies...";
161
- installSpinner.color = "cyan";
162
- // Let npm natively handle the extraction and binary symlinking
163
- (0, child_process_1.execSync)(`npm install "${tarballPath}" --no-save`, {
164
- cwd: process.cwd(),
165
- stdio: "ignore",
166
- });
159
+ await (0, proInstaller_1.installProEngine)(API_BASE, token.trim());
167
160
  installSpinner.succeed(chalk_1.default.green("Enterprise Engine installed successfully!"));
168
161
  }
169
- catch (installError) {
170
- installSpinner.stop();
171
- console.error(chalk_1.default.bgRed.white("\n[RAW SYSTEM ERROR]:"), installError);
172
- console.log(chalk_1.default.red("\n✖ Failed to install the Enterprise Engine. Please check your internet connection or verify your project configuration."));
173
- if (fs.existsSync(nodeModulesPath)) {
174
- fs.rmSync(nodeModulesPath, { recursive: true, force: true });
175
- }
176
- console.log(chalk_1.default.yellow("⚠️ Reverting login: Enterprise Engine is required to sync."));
177
- if (fs.existsSync(credentialsPath))
178
- fs.unlinkSync(credentialsPath);
179
- process.exit(1);
180
- }
181
- finally {
182
- if (fs.existsSync(tarballPath)) {
183
- fs.unlinkSync(tarballPath);
184
- }
162
+ catch (error) {
163
+ installSpinner.fail(error instanceof Error ? error.message : "Enterprise installation failed");
164
+ console.log(chalk_1.default.yellow("Credentials remain available. You can retry installation; runs can fall back to the Open Source engine."));
165
+ process.exitCode = 1;
185
166
  }
186
167
  }
187
168
  else {
@@ -196,7 +177,7 @@ program
196
177
  console.log(chalk_1.default.bold.green(`\n✅ Machine synchronized with Flowstride Enterprise.`));
197
178
  }
198
179
  catch (error) {
199
- spinner.fail(chalk_1.default.red(`Network Error: Could not connect to cloud.flowstride.io`));
180
+ spinner.fail(chalk_1.default.red((0, cloud_1.cloudFailure)(error, [token.trim()]).message));
200
181
  process.exit(1);
201
182
  }
202
183
  });
@@ -204,7 +185,8 @@ program
204
185
  .command("status")
205
186
  .description("Display the current Flowstride CLI authentication and engine tier")
206
187
  .action(async () => {
207
- let token = process.env.FLOWSTRIDE_TOKEN || null;
188
+ console.log((0, cloudSession_1.cloudSessionDescription)(API_BASE));
189
+ let token = process.env.FLOWSTRIDE_TOKEN?.trim() || null;
208
190
  const localCredentialsPath = path.join(process.cwd(), ".flowstride", "credentials.json");
209
191
  const globalCredentialsPath = path.join(os.homedir(), ".flowstride", "credentials.json");
210
192
  let activeCredentialsPath = localCredentialsPath; // Default for cleanup
@@ -213,7 +195,7 @@ program
213
195
  activeCredentialsPath = localCredentialsPath;
214
196
  try {
215
197
  const credentials = JSON.parse(fs.readFileSync(localCredentialsPath, "utf8"));
216
- token = credentials.machineToken;
198
+ token = credentials.machineToken?.trim() || null;
217
199
  }
218
200
  catch (err) { }
219
201
  }
@@ -221,7 +203,7 @@ program
221
203
  activeCredentialsPath = globalCredentialsPath;
222
204
  try {
223
205
  const credentials = JSON.parse(fs.readFileSync(globalCredentialsPath, "utf8"));
224
- token = credentials.machineToken;
206
+ token = credentials.machineToken?.trim() || null;
225
207
  }
226
208
  catch (err) { }
227
209
  }
@@ -243,9 +225,10 @@ program
243
225
  method: "GET",
244
226
  headers: { Authorization: `Bearer ${token}` },
245
227
  });
246
- if (response.status === 401) {
247
- console.log(chalk_1.default.red("🔴 AUTH EXPIRED: Your device session was revoked or expired."));
248
- if (fs.existsSync(activeCredentialsPath))
228
+ if (response.status === 401 || response.status === 403) {
229
+ const failureData = await response.json().catch(() => ({}));
230
+ console.log(chalk_1.default.red((0, cloud_1.cloudFailure)({ response: { status: response.status, data: failureData } }, [token]).message));
231
+ if (response.status === 401 && !process.env.FLOWSTRIDE_TOKEN && fs.existsSync(activeCredentialsPath))
249
232
  fs.unlinkSync(activeCredentialsPath);
250
233
  }
251
234
  else if (response.ok) {
@@ -284,7 +267,8 @@ program
284
267
  }
285
268
  if (options.ci)
286
269
  process.env.FLOWSTRIDE_NATIVE_CI = '1';
287
- let token = process.env.FLOWSTRIDE_TOKEN || null;
270
+ console.log((0, cloudSession_1.cloudSessionDescription)(API_BASE));
271
+ let token = process.env.FLOWSTRIDE_TOKEN?.trim() || null;
288
272
  let forceOS = false;
289
273
  const localCredentialsPath = path.join(process.cwd(), ".flowstride", "credentials.json");
290
274
  const globalCredentialsPath = path.join(os.homedir(), ".flowstride", "credentials.json");
@@ -294,7 +278,7 @@ program
294
278
  activeCredentialsPath = localCredentialsPath;
295
279
  try {
296
280
  const credentials = JSON.parse(fs.readFileSync(localCredentialsPath, "utf8"));
297
- token = credentials.machineToken;
281
+ token = credentials.machineToken?.trim() || null;
298
282
  }
299
283
  catch (err) { }
300
284
  }
@@ -302,7 +286,7 @@ program
302
286
  activeCredentialsPath = globalCredentialsPath;
303
287
  try {
304
288
  const credentials = JSON.parse(fs.readFileSync(globalCredentialsPath, "utf8"));
305
- token = credentials.machineToken;
289
+ token = credentials.machineToken?.trim() || null;
306
290
  }
307
291
  catch (err) { }
308
292
  }
@@ -325,9 +309,10 @@ program
325
309
  method: "GET",
326
310
  headers: { Authorization: `Bearer ${token}` },
327
311
  });
328
- if (response.status === 401) {
329
- console.log(chalk_1.default.red("🔴 AUTH EXPIRED: Your device session was revoked or expired."));
330
- if (fs.existsSync(activeCredentialsPath))
312
+ if (response.status === 401 || response.status === 403) {
313
+ const failureData = await response.json().catch(() => ({}));
314
+ console.log(chalk_1.default.red((0, cloud_1.cloudFailure)({ response: { status: response.status, data: failureData } }, [token]).message));
315
+ if (response.status === 401 && !process.env.FLOWSTRIDE_TOKEN && fs.existsSync(activeCredentialsPath))
331
316
  fs.unlinkSync(activeCredentialsPath);
332
317
  forceOS = true;
333
318
  }