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.
- package/NATIVE_CI.md +88 -145
- package/README.md +46 -221
- package/dist/adapters/web/playwright.d.ts +4 -1
- package/dist/adapters/web/playwright.js +75 -6
- package/dist/ci/config.js +6 -5
- package/dist/ci/runtime.js +27 -14
- package/dist/cli/commands/run.js +2 -1
- package/dist/cli/index.js +35 -50
- package/dist/cli/manual_server/workspace_server.js +44 -32
- package/dist/cli/utils/cloudSession.d.ts +7 -0
- package/dist/cli/utils/cloudSession.js +37 -0
- package/dist/cli/utils/proInstaller.d.ts +5 -0
- package/dist/cli/utils/proInstaller.js +141 -0
- package/dist/game/sdk.js +3 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +9 -0
- package/dist/manual/security.d.ts +183 -0
- package/dist/manual/security.js +106 -0
- package/dist/runner/data_generators.d.ts +1 -0
- package/dist/runner/data_generators.js +3 -0
- package/dist/runner/dom_stripper.js +3 -0
- package/dist/runner/flow_orchestrator.js +13 -22
- package/dist/runner/flow_worker.d.ts +1 -0
- package/dist/runner/flow_worker.js +68 -47
- package/dist/runner/persistent_vault.js +2 -1
- package/dist/runner/plugin_manager.js +3 -1
- package/dist/security/localDashboard.d.ts +2 -0
- package/dist/security/localDashboard.js +22 -0
- package/dist/security/redaction.d.ts +4 -0
- package/dist/security/redaction.js +51 -0
- package/dist/sessions/manager.js +3 -1
- package/dist/types/index.d.ts +9 -0
- package/dist/utils/cloud.d.ts +6 -0
- package/dist/utils/cloud.js +33 -0
- package/dist/utils/projectPaths.d.ts +11 -0
- package/dist/utils/projectPaths.js +90 -0
- package/dist/utils/replayRequest.d.ts +8 -0
- package/dist/utils/replayRequest.js +77 -0
- package/package.json +23 -14
- package/src/ui/dist/assets/index-B7MuBTWm.js +460 -0
- package/src/ui/dist/index.html +1 -1
- package/RELEASE_NOTES.md +0 -40
- 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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.');
|
package/dist/ci/runtime.js
CHANGED
|
@@ -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
|
|
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 => ({ '<': '<', '>': '>', '&': '&', '"': '"', "'": ''' }[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 =
|
|
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(
|
|
158
|
-
errors.push(`Incomplete 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 =>
|
|
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,
|
|
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;
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -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
|
-
?
|
|
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
|
|
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
|
-
|
|
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
|
|
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: ${
|
|
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
|
-
|
|
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 (
|
|
170
|
-
installSpinner.
|
|
171
|
-
console.
|
|
172
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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
|
-
|
|
330
|
-
|
|
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
|
}
|