flowstride 1.0.15 → 1.0.16

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/dist/cli/index.js CHANGED
@@ -54,7 +54,7 @@ const API_BASE = process.env.FLOWSTRIDE_API_URL || "https://cloud.flowstride.io"
54
54
  program
55
55
  .name("flowstride")
56
56
  .description("Flowstride: The Enterprise Flow-First Automation CLI")
57
- .version("1.0.15");
57
+ .version("1.0.16");
58
58
  program
59
59
  .command("init")
60
60
  .description("Initialise a new Flowstride workspace in the current directory")
@@ -64,33 +64,40 @@ program
64
64
  program
65
65
  .command("login")
66
66
  .description("Authenticate this machine with Flowstride Enterprise Cloud")
67
- .action(async () => {
67
+ .option("-t, --token <token>", "Enterprise Developer Token")
68
+ .option("-g, --global", "Save the token globally for all projects on this machine", false)
69
+ .action(async (options) => {
68
70
  const localPackageJsonPath = path.join(process.cwd(), "package.json");
69
- if (!fs.existsSync(localPackageJsonPath)) {
70
- console.error(chalk_1.default.red("✖ Error: Please navigate to a valid Flowstride project directory before logging in."));
71
- process.exit(1);
72
- }
73
- const pkg = JSON.parse(fs.readFileSync(localPackageJsonPath, "utf8"));
74
- const deps = {
75
- ...(pkg.dependencies || {}),
76
- ...(pkg.devDependencies || {}),
77
- };
78
- if (!deps["flowstride"] && !deps["flowstride-os"]) {
79
- console.error(chalk_1.default.red("✖ Error: Please navigate to a valid Flowstride project directory before logging in."));
80
- process.exit(1);
71
+ // Enforce valid project directory ONLY if doing a local login
72
+ if (!options.global) {
73
+ if (!fs.existsSync(localPackageJsonPath)) {
74
+ console.error(chalk_1.default.red("✖ Error: Please navigate to a valid Flowstride project directory before logging in locally, or use --global."));
75
+ process.exit(1);
76
+ }
77
+ const pkg = JSON.parse(fs.readFileSync(localPackageJsonPath, "utf8"));
78
+ const deps = {
79
+ ...(pkg.dependencies || {}),
80
+ ...(pkg.devDependencies || {}),
81
+ };
82
+ if (!deps["flowstride"] && !deps["flowstride-os"]) {
83
+ console.error(chalk_1.default.red("✖ Error: Please navigate to a valid Flowstride project directory before logging in locally, or use --global."));
84
+ process.exit(1);
85
+ }
81
86
  }
82
87
  console.log(chalk_1.default.bold.blue("\n🌐 Flowstride Enterprise Cloud Sync\n"));
83
- let authHeader = "";
84
- if (process.env.FLOWSTRIDE_API_KEY) {
85
- console.log(chalk_1.default.yellow("🤖 CI/CD Environment Detected. Authenticating via API Key..."));
86
- authHeader = `Bearer ${process.env.FLOWSTRIDE_API_KEY}`;
88
+ let token = process.env.FLOWSTRIDE_TOKEN || options.token;
89
+ if (process.env.FLOWSTRIDE_TOKEN) {
90
+ console.log(chalk_1.default.yellow("🤖 CI/CD Environment Detected. Authenticating via FLOWSTRIDE_TOKEN..."));
87
91
  }
88
- else {
89
- const userEmail = await (0, prompts_1.input)({
90
- message: "Enter your Flowstride email:",
92
+ else if (!token) {
93
+ token = await (0, prompts_1.password)({
94
+ message: "Paste your Enterprise Developer Token:",
95
+ mask: "*",
91
96
  });
92
- const userPass = await (0, prompts_1.password)({ message: "Enter your password:" });
93
- authHeader = `Basic ${Buffer.from(`${userEmail}:${userPass}`).toString("base64")}`;
97
+ }
98
+ if (!token || token.trim().length === 0) {
99
+ console.error(chalk_1.default.red("✖ Error: A valid Developer Token is required."));
100
+ process.exit(1);
94
101
  }
95
102
  const deviceName = os.hostname();
96
103
  const spinner = (0, ora_1.default)("Authenticating with Cloud...").start();
@@ -99,34 +106,37 @@ program
99
106
  method: "POST",
100
107
  headers: {
101
108
  "Content-Type": "application/json",
102
- Authorization: authHeader,
109
+ Authorization: `Bearer ${token.trim()}`,
103
110
  },
104
111
  body: JSON.stringify({ deviceName }),
105
112
  });
106
113
  const data = await response.json();
107
114
  if (!response.ok) {
108
- spinner.fail(chalk_1.default.red(`Authentication Failed: ${data.error || "Unknown error"}`));
115
+ spinner.fail(chalk_1.default.red(`Authentication Failed: ${data.error || "Invalid token or network error"}`));
109
116
  process.exit(1);
110
117
  }
111
118
  spinner.succeed(chalk_1.default.green("Authentication Successful!"));
112
- const configDir = path.join(process.cwd(), ".flowstride", "login");
119
+ const configDir = options.global
120
+ ? path.join(os.homedir(), ".flowstride")
121
+ : path.join(process.cwd(), ".flowstride");
113
122
  if (!fs.existsSync(configDir)) {
114
123
  fs.mkdirSync(configDir, { recursive: true });
115
124
  }
116
- const credentialsPath = path.join(configDir, "login.json");
125
+ const credentialsPath = path.join(configDir, "credentials.json");
117
126
  fs.writeFileSync(credentialsPath, JSON.stringify({
118
- machineToken: data.machineToken,
127
+ machineToken: data.machineToken || token.trim(),
119
128
  workspaceId: data.workspaceId,
120
129
  deviceName: deviceName,
121
130
  syncedAt: new Date().toISOString(),
122
131
  }, null, 2));
123
- console.log(chalk_1.default.gray(`\nCredentials securely scoped to ${credentialsPath}`));
132
+ const scopeText = options.global ? "GLOBALLY" : "LOCALLY to this project";
133
+ console.log(chalk_1.default.gray(`\nCredentials securely scoped ${scopeText} at: ${credentialsPath}`));
124
134
  if (data.isProEligible) {
125
135
  console.log(chalk_1.default.yellow(`\n⚙️ System Check: You have access to Enterprise features (AI Healing, Parallel Workers).`));
126
136
  const nodeModulesPath = path.join(process.cwd(), "node_modules", "@flowstride", "pro");
127
137
  if (!fs.existsSync(nodeModulesPath)) {
128
138
  let installPro = true;
129
- if (!process.env.FLOWSTRIDE_API_KEY && !process.env.CI) {
139
+ if (!process.env.FLOWSTRIDE_TOKEN && !process.env.CI) {
130
140
  installPro = await (0, prompts_1.confirm)({
131
141
  message: "The Pro Engine is not installed in this project. Would you like Flowstride to automatically install it now?",
132
142
  default: true,
@@ -138,7 +148,7 @@ program
138
148
  try {
139
149
  const tarballResponse = await fetch(`${API_BASE}/api/workspace/download-pro`, {
140
150
  method: "GET",
141
- headers: { Authorization: `Bearer ${data.machineToken}` },
151
+ headers: { Authorization: `Bearer ${token.trim()}` },
142
152
  });
143
153
  if (!tarballResponse.ok) {
144
154
  throw new Error(`Secure proxy rejected request: ${tarballResponse.statusText}`);
@@ -200,21 +210,34 @@ program
200
210
  .command("status")
201
211
  .description("Display the current Flowstride CLI authentication and engine tier")
202
212
  .action(async () => {
203
- let token = process.env.FLOWSTRIDE_API_KEY || null;
204
- const credentialsPath = path.join(process.cwd(), ".flowstride", "login", "login.json");
205
- const nodeModulesPath = path.join(process.cwd(), "node_modules", "@flowstride", "pro");
206
- if (!token && fs.existsSync(credentialsPath)) {
207
- try {
208
- const credentials = JSON.parse(fs.readFileSync(credentialsPath, "utf8"));
209
- token = credentials.machineToken;
213
+ let token = process.env.FLOWSTRIDE_TOKEN || null;
214
+ const localCredentialsPath = path.join(process.cwd(), ".flowstride", "credentials.json");
215
+ const globalCredentialsPath = path.join(os.homedir(), ".flowstride", "credentials.json");
216
+ let activeCredentialsPath = localCredentialsPath; // Default for cleanup
217
+ if (!token) {
218
+ if (fs.existsSync(localCredentialsPath)) {
219
+ activeCredentialsPath = localCredentialsPath;
220
+ try {
221
+ const credentials = JSON.parse(fs.readFileSync(localCredentialsPath, "utf8"));
222
+ token = credentials.machineToken;
223
+ }
224
+ catch (err) { }
225
+ }
226
+ else if (fs.existsSync(globalCredentialsPath)) {
227
+ activeCredentialsPath = globalCredentialsPath;
228
+ try {
229
+ const credentials = JSON.parse(fs.readFileSync(globalCredentialsPath, "utf8"));
230
+ token = credentials.machineToken;
231
+ }
232
+ catch (err) { }
210
233
  }
211
- catch (err) { }
212
234
  }
213
- if (!process.env.FLOWSTRIDE_API_KEY &&
235
+ const nodeModulesPath = path.join(process.cwd(), "node_modules", "@flowstride", "pro");
236
+ if (!process.env.FLOWSTRIDE_TOKEN &&
214
237
  token &&
215
238
  !fs.existsSync(nodeModulesPath)) {
216
- if (fs.existsSync(credentialsPath))
217
- fs.unlinkSync(credentialsPath);
239
+ if (fs.existsSync(activeCredentialsPath))
240
+ fs.unlinkSync(activeCredentialsPath);
218
241
  token = null;
219
242
  }
220
243
  if (!token) {
@@ -228,8 +251,8 @@ program
228
251
  });
229
252
  if (response.status === 401) {
230
253
  console.log(chalk_1.default.red("🔴 AUTH EXPIRED: Your device session was revoked or expired."));
231
- if (fs.existsSync(credentialsPath))
232
- fs.unlinkSync(credentialsPath);
254
+ if (fs.existsSync(activeCredentialsPath))
255
+ fs.unlinkSync(activeCredentialsPath);
233
256
  }
234
257
  else if (response.ok) {
235
258
  const data = await response.json();
@@ -261,22 +284,35 @@ program
261
284
  options = targetPath;
262
285
  targetPath = undefined;
263
286
  }
264
- let token = process.env.FLOWSTRIDE_API_KEY || null;
287
+ let token = process.env.FLOWSTRIDE_TOKEN || null;
265
288
  let forceOS = false;
266
- const credentialsPath = path.join(process.cwd(), ".flowstride", "login", "login.json");
267
- const nodeModulesPath = path.join(process.cwd(), "node_modules", "@flowstride", "pro");
268
- if (!token && fs.existsSync(credentialsPath)) {
269
- try {
270
- const credentials = JSON.parse(fs.readFileSync(credentialsPath, "utf8"));
271
- token = credentials.machineToken;
289
+ const localCredentialsPath = path.join(process.cwd(), ".flowstride", "credentials.json");
290
+ const globalCredentialsPath = path.join(os.homedir(), ".flowstride", "credentials.json");
291
+ let activeCredentialsPath = localCredentialsPath;
292
+ if (!token) {
293
+ if (fs.existsSync(localCredentialsPath)) {
294
+ activeCredentialsPath = localCredentialsPath;
295
+ try {
296
+ const credentials = JSON.parse(fs.readFileSync(localCredentialsPath, "utf8"));
297
+ token = credentials.machineToken;
298
+ }
299
+ catch (err) { }
300
+ }
301
+ else if (fs.existsSync(globalCredentialsPath)) {
302
+ activeCredentialsPath = globalCredentialsPath;
303
+ try {
304
+ const credentials = JSON.parse(fs.readFileSync(globalCredentialsPath, "utf8"));
305
+ token = credentials.machineToken;
306
+ }
307
+ catch (err) { }
272
308
  }
273
- catch (err) { }
274
309
  }
275
- if (!process.env.FLOWSTRIDE_API_KEY &&
310
+ const nodeModulesPath = path.join(process.cwd(), "node_modules", "@flowstride", "pro");
311
+ if (!process.env.FLOWSTRIDE_TOKEN &&
276
312
  token &&
277
313
  !fs.existsSync(nodeModulesPath)) {
278
- if (fs.existsSync(credentialsPath))
279
- fs.unlinkSync(credentialsPath);
314
+ if (fs.existsSync(activeCredentialsPath))
315
+ fs.unlinkSync(activeCredentialsPath);
280
316
  token = null;
281
317
  }
282
318
  if (!token) {
@@ -291,8 +327,8 @@ program
291
327
  });
292
328
  if (response.status === 401) {
293
329
  console.log(chalk_1.default.red("🔴 AUTH EXPIRED: Your device session was revoked or expired."));
294
- if (fs.existsSync(credentialsPath))
295
- fs.unlinkSync(credentialsPath);
330
+ if (fs.existsSync(activeCredentialsPath))
331
+ fs.unlinkSync(activeCredentialsPath);
296
332
  forceOS = true;
297
333
  }
298
334
  else if (response.ok) {
@@ -328,7 +364,7 @@ program
328
364
  });
329
365
  async function handleOfflinePrompt() {
330
366
  console.log(chalk_1.default.yellow("🛜 OFFLINE: Unable to reach Flowstride Cloud. Enterprise Sync paused."));
331
- if (process.env.CI || process.env.FLOWSTRIDE_API_KEY) {
367
+ if (process.env.CI || process.env.FLOWSTRIDE_TOKEN) {
332
368
  console.log(chalk_1.default.yellow("⚠️ CI/CD Environment: Automatically falling back to OS Engine."));
333
369
  return true;
334
370
  }
@@ -357,7 +357,6 @@ class Parser {
357
357
  throw new Error(`Line ${viaToken.line}: Expected 'via'`);
358
358
  payload.verb = this.consume(lexer_1.TokenType.COMMAND, "Expected API verb (e.g. POST)").value.toLowerCase();
359
359
  payload.url = this.consume(lexer_1.TokenType.STRING, "Endpoint URL expected").value;
360
- // --- ENSURE ARRAY FORMAT FOR API CLIENT ---
361
360
  payload.headers = [];
362
361
  this.parseApiModifiers(payload);
363
362
  break;
@@ -368,7 +367,6 @@ class Parser {
368
367
  case "delete":
369
368
  case "graphql":
370
369
  payload.url = this.consume(lexer_1.TokenType.STRING, "Endpoint URL expected").value;
371
- // --- ENSURE ARRAY FORMAT FOR API CLIENT ---
372
370
  payload.headers = [];
373
371
  this.parseApiModifiers(payload);
374
372
  break;
@@ -617,16 +615,13 @@ class Parser {
617
615
  throw new Error(`Syntax Error on line ${targetToken.line}: Invalid keyword '${target}'. Flowstride uses strict camelCase. Did you mean 'reqHeader'?`);
618
616
  }
619
617
  if (target === "reqHeader") {
620
- // --- THE NEW MULTI-FORMAT HEADER PARSER ---
621
618
  const firstToken = this.consume(lexer_1.TokenType.STRING, "Header key or JSON block expected");
622
- // Lookahead: If the next token is ALSO a string, use the legacy Key-Value parsing
623
619
  if (!this.isAtEnd() && this.peek().type === lexer_1.TokenType.STRING) {
624
620
  const hKey = firstToken.value;
625
621
  const hVal = this.advance().value;
626
622
  payload.headers.push({ key: hKey, value: hVal });
627
623
  }
628
624
  else {
629
- // Otherwise, it must be the new multi-line JSON Block format
630
625
  try {
631
626
  const headerObj = JSON.parse(firstToken.value);
632
627
  if (typeof headerObj !== "object" ||
@@ -634,7 +629,6 @@ class Parser {
634
629
  Array.isArray(headerObj)) {
635
630
  throw new Error("Parsed JSON is not a Key-Value object.");
636
631
  }
637
- // Loop through the parsed block and push them into the array for the API client
638
632
  for (const [k, v] of Object.entries(headerObj)) {
639
633
  payload.headers.push({ key: k, value: String(v) });
640
634
  }
@@ -643,7 +637,6 @@ class Parser {
643
637
  throw new Error(`Line ${firstToken.line}: Invalid JSON format for reqHeader block. Ensure it is a valid JSON object, or use the legacy format with two strings (Key and Value).`);
644
638
  }
645
639
  }
646
- // ------------------------------------------
647
640
  }
648
641
  else if (target === "reqBody") {
649
642
  payload.body = this.consume(lexer_1.TokenType.STRING, "Body content expected").value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowstride",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "description": "An opensource flowfirst test automation framework",
5
5
  "workspaces": [
6
6
  "packages/flowstride_report"