flowstride 1.0.15 → 1.0.17

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
@@ -44,7 +44,6 @@ const init_1 = require("./commands/init");
44
44
  const fs = __importStar(require("fs"));
45
45
  const path = __importStar(require("path"));
46
46
  const os = __importStar(require("os"));
47
- const tar = __importStar(require("tar"));
48
47
  const child_process_1 = require("child_process");
49
48
  const prompts_1 = require("@inquirer/prompts");
50
49
  const chalk_1 = __importDefault(require("chalk"));
@@ -54,7 +53,7 @@ const API_BASE = process.env.FLOWSTRIDE_API_URL || "https://cloud.flowstride.io"
54
53
  program
55
54
  .name("flowstride")
56
55
  .description("Flowstride: The Enterprise Flow-First Automation CLI")
57
- .version("1.0.15");
56
+ .version("1.0.17");
58
57
  program
59
58
  .command("init")
60
59
  .description("Initialise a new Flowstride workspace in the current directory")
@@ -64,33 +63,40 @@ program
64
63
  program
65
64
  .command("login")
66
65
  .description("Authenticate this machine with Flowstride Enterprise Cloud")
67
- .action(async () => {
66
+ .option("-t, --token <token>", "Enterprise Developer Token")
67
+ .option("-g, --global", "Save the token globally for all projects on this machine", false)
68
+ .action(async (options) => {
68
69
  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);
70
+ // Enforce valid project directory ONLY if doing a local login
71
+ if (!options.global) {
72
+ if (!fs.existsSync(localPackageJsonPath)) {
73
+ console.error(chalk_1.default.red("✖ Error: Please navigate to a valid Flowstride project directory before logging in locally, or use --global."));
74
+ process.exit(1);
75
+ }
76
+ const pkg = JSON.parse(fs.readFileSync(localPackageJsonPath, "utf8"));
77
+ const deps = {
78
+ ...(pkg.dependencies || {}),
79
+ ...(pkg.devDependencies || {}),
80
+ };
81
+ if (!deps["flowstride"] && !deps["flowstride-os"]) {
82
+ console.error(chalk_1.default.red("✖ Error: Please navigate to a valid Flowstride project directory before logging in locally, or use --global."));
83
+ process.exit(1);
84
+ }
81
85
  }
82
86
  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}`;
87
+ let token = process.env.FLOWSTRIDE_TOKEN || options.token;
88
+ if (process.env.FLOWSTRIDE_TOKEN) {
89
+ console.log(chalk_1.default.yellow("🤖 CI/CD Environment Detected. Authenticating via FLOWSTRIDE_TOKEN..."));
87
90
  }
88
- else {
89
- const userEmail = await (0, prompts_1.input)({
90
- message: "Enter your Flowstride email:",
91
+ else if (!token) {
92
+ token = await (0, prompts_1.password)({
93
+ message: "Paste your Enterprise Developer Token:",
94
+ mask: "*",
91
95
  });
92
- const userPass = await (0, prompts_1.password)({ message: "Enter your password:" });
93
- authHeader = `Basic ${Buffer.from(`${userEmail}:${userPass}`).toString("base64")}`;
96
+ }
97
+ if (!token || token.trim().length === 0) {
98
+ console.error(chalk_1.default.red("✖ Error: A valid Developer Token is required."));
99
+ process.exit(1);
94
100
  }
95
101
  const deviceName = os.hostname();
96
102
  const spinner = (0, ora_1.default)("Authenticating with Cloud...").start();
@@ -99,34 +105,37 @@ program
99
105
  method: "POST",
100
106
  headers: {
101
107
  "Content-Type": "application/json",
102
- Authorization: authHeader,
108
+ Authorization: `Bearer ${token.trim()}`,
103
109
  },
104
110
  body: JSON.stringify({ deviceName }),
105
111
  });
106
112
  const data = await response.json();
107
113
  if (!response.ok) {
108
- spinner.fail(chalk_1.default.red(`Authentication Failed: ${data.error || "Unknown error"}`));
114
+ spinner.fail(chalk_1.default.red(`Authentication Failed: ${data.error || "Invalid token or network error"}`));
109
115
  process.exit(1);
110
116
  }
111
117
  spinner.succeed(chalk_1.default.green("Authentication Successful!"));
112
- const configDir = path.join(process.cwd(), ".flowstride", "login");
118
+ const configDir = options.global
119
+ ? path.join(os.homedir(), ".flowstride")
120
+ : path.join(process.cwd(), ".flowstride");
113
121
  if (!fs.existsSync(configDir)) {
114
122
  fs.mkdirSync(configDir, { recursive: true });
115
123
  }
116
- const credentialsPath = path.join(configDir, "login.json");
124
+ const credentialsPath = path.join(configDir, "credentials.json");
117
125
  fs.writeFileSync(credentialsPath, JSON.stringify({
118
- machineToken: data.machineToken,
126
+ machineToken: data.machineToken || token.trim(),
119
127
  workspaceId: data.workspaceId,
120
128
  deviceName: deviceName,
121
129
  syncedAt: new Date().toISOString(),
122
130
  }, null, 2));
123
- console.log(chalk_1.default.gray(`\nCredentials securely scoped to ${credentialsPath}`));
131
+ const scopeText = options.global ? "GLOBALLY" : "LOCALLY to this project";
132
+ console.log(chalk_1.default.gray(`\nCredentials securely scoped ${scopeText} at: ${credentialsPath}`));
124
133
  if (data.isProEligible) {
125
134
  console.log(chalk_1.default.yellow(`\n⚙️ System Check: You have access to Enterprise features (AI Healing, Parallel Workers).`));
126
135
  const nodeModulesPath = path.join(process.cwd(), "node_modules", "@flowstride", "pro");
127
136
  if (!fs.existsSync(nodeModulesPath)) {
128
137
  let installPro = true;
129
- if (!process.env.FLOWSTRIDE_API_KEY && !process.env.CI) {
138
+ if (!process.env.FLOWSTRIDE_TOKEN && !process.env.CI) {
130
139
  installPro = await (0, prompts_1.confirm)({
131
140
  message: "The Pro Engine is not installed in this project. Would you like Flowstride to automatically install it now?",
132
141
  default: true,
@@ -138,26 +147,18 @@ program
138
147
  try {
139
148
  const tarballResponse = await fetch(`${API_BASE}/api/workspace/download-pro`, {
140
149
  method: "GET",
141
- headers: { Authorization: `Bearer ${data.machineToken}` },
150
+ headers: { Authorization: `Bearer ${token.trim()}` },
142
151
  });
143
152
  if (!tarballResponse.ok) {
144
153
  throw new Error(`Secure proxy rejected request: ${tarballResponse.statusText}`);
145
154
  }
146
155
  const buffer = await tarballResponse.arrayBuffer();
147
156
  fs.writeFileSync(tarballPath, Buffer.from(buffer));
148
- installSpinner.text = "Silently extracting Enterprise Engine...";
149
- installSpinner.color = "cyan";
150
- if (!fs.existsSync(nodeModulesPath)) {
151
- fs.mkdirSync(nodeModulesPath, { recursive: true });
152
- }
153
- await tar.x({
154
- file: tarballPath,
155
- cwd: nodeModulesPath,
156
- strip: 1,
157
- });
158
157
  installSpinner.text = "Wiring up Enterprise dependencies...";
159
- (0, child_process_1.execSync)("npm install --omit=dev", {
160
- cwd: nodeModulesPath,
158
+ installSpinner.color = "cyan";
159
+ // Let npm natively handle the extraction and binary symlinking
160
+ (0, child_process_1.execSync)(`npm install "${tarballPath}" --no-save`, {
161
+ cwd: process.cwd(),
161
162
  stdio: "ignore",
162
163
  });
163
164
  installSpinner.succeed(chalk_1.default.green("Enterprise Engine installed successfully!"));
@@ -200,21 +201,34 @@ program
200
201
  .command("status")
201
202
  .description("Display the current Flowstride CLI authentication and engine tier")
202
203
  .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;
204
+ let token = process.env.FLOWSTRIDE_TOKEN || null;
205
+ const localCredentialsPath = path.join(process.cwd(), ".flowstride", "credentials.json");
206
+ const globalCredentialsPath = path.join(os.homedir(), ".flowstride", "credentials.json");
207
+ let activeCredentialsPath = localCredentialsPath; // Default for cleanup
208
+ if (!token) {
209
+ if (fs.existsSync(localCredentialsPath)) {
210
+ activeCredentialsPath = localCredentialsPath;
211
+ try {
212
+ const credentials = JSON.parse(fs.readFileSync(localCredentialsPath, "utf8"));
213
+ token = credentials.machineToken;
214
+ }
215
+ catch (err) { }
216
+ }
217
+ else if (fs.existsSync(globalCredentialsPath)) {
218
+ activeCredentialsPath = globalCredentialsPath;
219
+ try {
220
+ const credentials = JSON.parse(fs.readFileSync(globalCredentialsPath, "utf8"));
221
+ token = credentials.machineToken;
222
+ }
223
+ catch (err) { }
210
224
  }
211
- catch (err) { }
212
225
  }
213
- if (!process.env.FLOWSTRIDE_API_KEY &&
226
+ const nodeModulesPath = path.join(process.cwd(), "node_modules", "@flowstride", "pro");
227
+ if (!process.env.FLOWSTRIDE_TOKEN &&
214
228
  token &&
215
229
  !fs.existsSync(nodeModulesPath)) {
216
- if (fs.existsSync(credentialsPath))
217
- fs.unlinkSync(credentialsPath);
230
+ if (fs.existsSync(activeCredentialsPath))
231
+ fs.unlinkSync(activeCredentialsPath);
218
232
  token = null;
219
233
  }
220
234
  if (!token) {
@@ -228,8 +242,8 @@ program
228
242
  });
229
243
  if (response.status === 401) {
230
244
  console.log(chalk_1.default.red("🔴 AUTH EXPIRED: Your device session was revoked or expired."));
231
- if (fs.existsSync(credentialsPath))
232
- fs.unlinkSync(credentialsPath);
245
+ if (fs.existsSync(activeCredentialsPath))
246
+ fs.unlinkSync(activeCredentialsPath);
233
247
  }
234
248
  else if (response.ok) {
235
249
  const data = await response.json();
@@ -261,22 +275,35 @@ program
261
275
  options = targetPath;
262
276
  targetPath = undefined;
263
277
  }
264
- let token = process.env.FLOWSTRIDE_API_KEY || null;
278
+ let token = process.env.FLOWSTRIDE_TOKEN || null;
265
279
  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;
280
+ const localCredentialsPath = path.join(process.cwd(), ".flowstride", "credentials.json");
281
+ const globalCredentialsPath = path.join(os.homedir(), ".flowstride", "credentials.json");
282
+ let activeCredentialsPath = localCredentialsPath;
283
+ if (!token) {
284
+ if (fs.existsSync(localCredentialsPath)) {
285
+ activeCredentialsPath = localCredentialsPath;
286
+ try {
287
+ const credentials = JSON.parse(fs.readFileSync(localCredentialsPath, "utf8"));
288
+ token = credentials.machineToken;
289
+ }
290
+ catch (err) { }
291
+ }
292
+ else if (fs.existsSync(globalCredentialsPath)) {
293
+ activeCredentialsPath = globalCredentialsPath;
294
+ try {
295
+ const credentials = JSON.parse(fs.readFileSync(globalCredentialsPath, "utf8"));
296
+ token = credentials.machineToken;
297
+ }
298
+ catch (err) { }
272
299
  }
273
- catch (err) { }
274
300
  }
275
- if (!process.env.FLOWSTRIDE_API_KEY &&
301
+ const nodeModulesPath = path.join(process.cwd(), "node_modules", "@flowstride", "pro");
302
+ if (!process.env.FLOWSTRIDE_TOKEN &&
276
303
  token &&
277
304
  !fs.existsSync(nodeModulesPath)) {
278
- if (fs.existsSync(credentialsPath))
279
- fs.unlinkSync(credentialsPath);
305
+ if (fs.existsSync(activeCredentialsPath))
306
+ fs.unlinkSync(activeCredentialsPath);
280
307
  token = null;
281
308
  }
282
309
  if (!token) {
@@ -291,8 +318,8 @@ program
291
318
  });
292
319
  if (response.status === 401) {
293
320
  console.log(chalk_1.default.red("🔴 AUTH EXPIRED: Your device session was revoked or expired."));
294
- if (fs.existsSync(credentialsPath))
295
- fs.unlinkSync(credentialsPath);
321
+ if (fs.existsSync(activeCredentialsPath))
322
+ fs.unlinkSync(activeCredentialsPath);
296
323
  forceOS = true;
297
324
  }
298
325
  else if (response.ok) {
@@ -328,7 +355,7 @@ program
328
355
  });
329
356
  async function handleOfflinePrompt() {
330
357
  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) {
358
+ if (process.env.CI || process.env.FLOWSTRIDE_TOKEN) {
332
359
  console.log(chalk_1.default.yellow("⚠️ CI/CD Environment: Automatically falling back to OS Engine."));
333
360
  return true;
334
361
  }
@@ -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.17",
4
4
  "description": "An opensource flowfirst test automation framework",
5
5
  "workspaces": [
6
6
  "packages/flowstride_report"