@matimo/bruno 0.1.3 → 0.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matimo/bruno",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Bruno CLI tools for Matimo — API collection execution, import, and validation",
5
5
  "files": [
6
6
  "tools",
@@ -9,9 +9,12 @@
9
9
  ],
10
10
  "dependencies": {
11
11
  "@usebruno/cli": "^3.3.0",
12
- "@matimo/core": "0.1.3"
12
+ "@matimo/core": "0.1.4"
13
13
  },
14
14
  "peerDependencies": {
15
- "matimo": "0.1.3"
15
+ "matimo": "0.1.4"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc"
16
19
  }
17
20
  }
@@ -0,0 +1,41 @@
1
+ import { execFileSync } from 'child_process';
2
+ export const BRU_MIN_VERSION_STR = '1.0.0';
3
+ const BRU_MIN_VERSION = [1, 0, 0];
4
+ /**
5
+ * Verify the Bruno CLI is installed and meets the minimum required version.
6
+ *
7
+ * - Throws if `bru` is not found in PATH (ENOENT).
8
+ * - Throws if the installed version is below {@link BRU_MIN_VERSION_STR}.
9
+ * - Skips silently if the version string cannot be parsed (graceful degradation).
10
+ */
11
+ export function checkBruVersion() {
12
+ let versionOutput;
13
+ try {
14
+ versionOutput = execFileSync('bru', ['--version'], { encoding: 'utf-8', stdio: 'pipe' });
15
+ }
16
+ catch (err) {
17
+ if (err.code === 'ENOENT') {
18
+ throw new Error("Bruno CLI ('bru') is not installed or not in PATH. " +
19
+ 'Install it with: npm install -g @usebruno/cli');
20
+ }
21
+ // Other error — skip version check (bru is installed but --version failed for another reason)
22
+ return;
23
+ }
24
+ const match = versionOutput.trim().match(/^(\d+)\.(\d+)\.(\d+)/);
25
+ if (!match)
26
+ return; // Unparseable output — skip check
27
+ const installed = [
28
+ parseInt(match[1], 10),
29
+ parseInt(match[2], 10),
30
+ parseInt(match[3], 10),
31
+ ];
32
+ const [iMaj, iMin, iPatch] = installed;
33
+ const [minMaj, minMin, minPatch] = BRU_MIN_VERSION;
34
+ const belowMin = iMaj < minMaj ||
35
+ (iMaj === minMaj && iMin < minMin) ||
36
+ (iMaj === minMaj && iMin === minMin && iPatch < minPatch);
37
+ if (belowMin) {
38
+ throw new Error(`Bruno CLI version ${versionOutput.trim()} is below the minimum required version ` +
39
+ `${BRU_MIN_VERSION_STR}. Upgrade with: npm install -g @usebruno/cli`);
40
+ }
41
+ }
@@ -60,7 +60,7 @@ parameters:
60
60
 
61
61
  execution:
62
62
  type: function
63
- code: index.ts
63
+ code: index.js
64
64
  timeout: 10000
65
65
 
66
66
  output_schema:
@@ -0,0 +1,82 @@
1
+ import { getGlobalMatimoLogger } from '@matimo/core/runtime';
2
+ import { promises as fs } from 'fs';
3
+ import * as path from 'path';
4
+ const logger = getGlobalMatimoLogger();
5
+ function generateBruContent(params) {
6
+ const requestName = params.request_name;
7
+ const method = params.method.toLowerCase();
8
+ const url = params.url;
9
+ const headers = params.headers || {};
10
+ const body = params.body;
11
+ const tests = params.tests;
12
+ const documentation = params.documentation;
13
+ let content = '';
14
+ content += `meta {\n name: ${requestName}\n type: http\n seq: 1\n}\n\n`;
15
+ if (documentation) {
16
+ content += `docs {\n ${documentation}\n}\n\n`;
17
+ }
18
+ content += `${method} {\n url: ${url}\n body: ${body ? 'json' : 'none'}\n auth: inherit\n}\n\n`;
19
+ if (Object.keys(headers).length > 0) {
20
+ content += `headers {\n`;
21
+ for (const [k, v] of Object.entries(headers)) {
22
+ content += ` ${k}: ${v}\n`;
23
+ }
24
+ content += `}\n\n`;
25
+ }
26
+ if (body) {
27
+ content += `body:json {\n`;
28
+ content += body
29
+ .split('\n')
30
+ .map((line) => ` ${line}`)
31
+ .join('\n');
32
+ content += `\n}\n\n`;
33
+ }
34
+ if (tests) {
35
+ content += `tests {\n`;
36
+ content += tests
37
+ .split('\n')
38
+ .map((line) => ` ${line}`)
39
+ .join('\n');
40
+ content += `\n}\n`;
41
+ }
42
+ return content;
43
+ }
44
+ export default async function execute(params) {
45
+ const collectionPath = params.collection_path;
46
+ const requestName = params.request_name;
47
+ if (!collectionPath || !requestName) {
48
+ return {
49
+ success: false,
50
+ request_path: '',
51
+ request_name: '',
52
+ message: 'collection_path and request_name are required',
53
+ };
54
+ }
55
+ try {
56
+ logger.info(`Adding request ${requestName} to collection at ${collectionPath}`);
57
+ const absoluteCollectionPath = path.resolve(collectionPath);
58
+ // Write requests into a dedicated requests/ subfolder (consistent with Bruno convention)
59
+ const requestsDir = path.join(absoluteCollectionPath, 'requests');
60
+ await fs.mkdir(requestsDir, { recursive: true });
61
+ const filename = `${requestName.toLowerCase().replace(/\s+/g, '-')}.bru`;
62
+ const requestPath = path.join(requestsDir, filename);
63
+ const content = generateBruContent(params);
64
+ await fs.writeFile(requestPath, content, 'utf-8');
65
+ logger.info(`Request written to ${requestPath}`);
66
+ return {
67
+ success: true,
68
+ request_path: requestPath,
69
+ request_name: requestName,
70
+ message: `Request '${requestName}' added to collection successfully`,
71
+ };
72
+ }
73
+ catch (error) {
74
+ logger.error(`Add request failed: ${error instanceof Error ? error.message : String(error)}`);
75
+ return {
76
+ success: false,
77
+ request_path: '',
78
+ request_name: requestName,
79
+ message: `Failed to add request: ${error instanceof Error ? error.message : String(error)}`,
80
+ };
81
+ }
82
+ }
@@ -1,4 +1,4 @@
1
- import { getGlobalMatimoLogger } from '@matimo/core';
1
+ import { getGlobalMatimoLogger } from '@matimo/core/runtime';
2
2
  import { promises as fs } from 'fs';
3
3
  import * as path from 'path';
4
4
 
@@ -16,7 +16,7 @@ parameters:
16
16
 
17
17
  execution:
18
18
  type: function
19
- code: index.ts
19
+ code: index.js
20
20
  timeout: 15000
21
21
 
22
22
  output_schema:
@@ -0,0 +1,45 @@
1
+ import { getGlobalMatimoLogger } from '@matimo/core/runtime';
2
+ import { promises as fs } from 'fs';
3
+ import * as path from 'path';
4
+ const logger = getGlobalMatimoLogger();
5
+ export default async function execute(params) {
6
+ const collectionPath = params.collection_path;
7
+ const collectionName = params.collection_name;
8
+ if (!collectionPath || !collectionName) {
9
+ return {
10
+ success: false,
11
+ collection_path: '',
12
+ message: 'collection_path and collection_name parameters are required',
13
+ errors: ['collection_path and collection_name parameters are required'],
14
+ };
15
+ }
16
+ try {
17
+ logger.info(`Creating collection: ${collectionName} at ${collectionPath}`);
18
+ const absolutePath = path.resolve(collectionPath);
19
+ await fs.mkdir(absolutePath, { recursive: true });
20
+ const brunoJson = {
21
+ version: '1',
22
+ name: collectionName,
23
+ type: 'collection',
24
+ ignore: [],
25
+ };
26
+ const brunoJsonPath = path.join(absolutePath, 'bruno.json');
27
+ await fs.writeFile(brunoJsonPath, JSON.stringify(brunoJson, null, 2), 'utf-8');
28
+ logger.info('Collection created successfully');
29
+ return {
30
+ success: true,
31
+ collection_path: absolutePath,
32
+ message: `Collection "${collectionName}" created at ${absolutePath}`,
33
+ errors: [],
34
+ };
35
+ }
36
+ catch (error) {
37
+ logger.error(`Create collection failed: ${error instanceof Error ? error.message : String(error)}`);
38
+ return {
39
+ success: false,
40
+ collection_path: collectionPath,
41
+ message: 'Collection creation failed',
42
+ errors: [error instanceof Error ? error.message : String(error)],
43
+ };
44
+ }
45
+ }
@@ -1,4 +1,4 @@
1
- import { getGlobalMatimoLogger } from '@matimo/core';
1
+ import { getGlobalMatimoLogger } from '@matimo/core/runtime';
2
2
  import { promises as fs } from 'fs';
3
3
  import * as path from 'path';
4
4
 
@@ -11,7 +11,7 @@ parameters:
11
11
 
12
12
  execution:
13
13
  type: function
14
- code: index.ts
14
+ code: index.js
15
15
  timeout: 10000
16
16
 
17
17
  output_schema:
@@ -0,0 +1,106 @@
1
+ import { getGlobalMatimoLogger } from '@matimo/core/runtime';
2
+ import { promises as fs } from 'fs';
3
+ import * as path from 'path';
4
+ const logger = getGlobalMatimoLogger();
5
+ async function scanBruFiles(dir, results) {
6
+ let entries;
7
+ try {
8
+ entries = await fs.readdir(dir);
9
+ }
10
+ catch {
11
+ return;
12
+ }
13
+ for (const entry of entries) {
14
+ const fullPath = path.join(dir, entry);
15
+ try {
16
+ const stat = await fs.stat(fullPath);
17
+ if (stat.isDirectory() && entry !== 'node_modules') {
18
+ await scanBruFiles(fullPath, results);
19
+ }
20
+ else if (!stat.isDirectory() && entry.endsWith('.bru')) {
21
+ try {
22
+ const content = await fs.readFile(fullPath, 'utf-8');
23
+ const methodMatch = content.match(/^(get|post|put|patch|delete|head|options)\s*\{/im);
24
+ const method = methodMatch ? methodMatch[1].toUpperCase() : 'UNKNOWN';
25
+ const nameMatch = content.match(/meta\s*\{[^}]*name:\s*(.+)/i);
26
+ const name = nameMatch ? nameMatch[1].trim() : path.basename(entry, '.bru');
27
+ const urlMatch = content.match(/^\s+url:\s*(.+)$/m);
28
+ const url = urlMatch ? urlMatch[1].trim() : '';
29
+ const tagsMatch = content.match(/tags:\s*\[([^\]]*)\]/);
30
+ const tags = tagsMatch
31
+ ? tagsMatch[1].split(',').map((t) => t.trim()).filter(Boolean)
32
+ : [];
33
+ const hasTests = /\btests\s*\{/.test(content);
34
+ results.push({ name, method, url, path: fullPath, tags, has_tests: hasTests });
35
+ }
36
+ catch {
37
+ results.push({
38
+ name: path.basename(entry, '.bru'),
39
+ method: 'UNKNOWN',
40
+ url: '',
41
+ path: fullPath,
42
+ tags: [],
43
+ has_tests: false,
44
+ });
45
+ }
46
+ }
47
+ }
48
+ catch {
49
+ // skip
50
+ }
51
+ }
52
+ }
53
+ export default async function execute(params) {
54
+ const collectionPath = params.collection_path;
55
+ if (!collectionPath) {
56
+ return {
57
+ success: false,
58
+ collection: undefined,
59
+ errors: ['collection_path parameter is required'],
60
+ };
61
+ }
62
+ try {
63
+ logger.info(`Getting collection info: ${collectionPath}`);
64
+ const absolutePath = path.resolve(collectionPath);
65
+ const brunoJsonPath = path.join(absolutePath, 'bruno.json');
66
+ let collectionName = path.basename(absolutePath);
67
+ // Verify the path exists before proceeding
68
+ try {
69
+ await fs.stat(absolutePath);
70
+ }
71
+ catch {
72
+ return {
73
+ success: false,
74
+ collection: undefined,
75
+ errors: [`Collection path not found: ${collectionPath}`],
76
+ };
77
+ }
78
+ try {
79
+ const raw = await fs.readFile(brunoJsonPath, 'utf-8');
80
+ const parsed = JSON.parse(raw);
81
+ if (parsed.name)
82
+ collectionName = parsed.name;
83
+ }
84
+ catch {
85
+ // bruno.json missing — use dirname as name
86
+ }
87
+ const requests = [];
88
+ await scanBruFiles(absolutePath, requests);
89
+ return {
90
+ success: true,
91
+ collection: {
92
+ name: collectionName,
93
+ path: absolutePath,
94
+ requests,
95
+ },
96
+ };
97
+ }
98
+ catch (error) {
99
+ logger.error(`Get collection info failed: ${error instanceof Error ? error.message : String(error)}`);
100
+ return {
101
+ success: false,
102
+ collection: undefined,
103
+ errors: [error instanceof Error ? error.message : String(error)],
104
+ };
105
+ }
106
+ }
@@ -1,4 +1,4 @@
1
- import { getGlobalMatimoLogger } from '@matimo/core';
1
+ import { getGlobalMatimoLogger } from '@matimo/core/runtime';
2
2
  import { promises as fs } from 'fs';
3
3
  import * as path from 'path';
4
4
 
@@ -39,7 +39,7 @@ parameters:
39
39
 
40
40
  execution:
41
41
  type: function
42
- code: index.ts
42
+ code: index.js
43
43
  timeout: 30000
44
44
 
45
45
  output_schema:
@@ -0,0 +1,93 @@
1
+ import { getGlobalMatimoLogger } from '@matimo/core/runtime';
2
+ import { execFileSync } from 'child_process';
3
+ import { promises as fs } from 'fs';
4
+ import * as path from 'path';
5
+ import { checkBruVersion } from '../bru-utils.js';
6
+ const logger = getGlobalMatimoLogger();
7
+ /** Count .bru files recursively — compatible with Node 18+. */
8
+ async function countBruFilesRecursively(dir) {
9
+ let count = 0;
10
+ let entries;
11
+ try {
12
+ entries = await fs.readdir(dir);
13
+ }
14
+ catch {
15
+ return 0;
16
+ }
17
+ for (const entry of entries) {
18
+ const fullPath = path.join(dir, entry);
19
+ try {
20
+ const stat = await fs.stat(fullPath);
21
+ if (stat.isDirectory() && entry !== 'node_modules') {
22
+ count += await countBruFilesRecursively(fullPath);
23
+ }
24
+ else if (!stat.isDirectory() && entry.endsWith('.bru')) {
25
+ count++;
26
+ }
27
+ }
28
+ catch {
29
+ // skip
30
+ }
31
+ }
32
+ return count;
33
+ }
34
+ export default async function execute(params) {
35
+ const specSource = params.spec_source;
36
+ const outputDirectory = params.output_directory;
37
+ if (!specSource || !outputDirectory) {
38
+ return {
39
+ success: false,
40
+ collection_path: '',
41
+ collection_name: '',
42
+ requests_created: 0,
43
+ message: 'spec_source and output_directory parameters are required',
44
+ errors: ['spec_source and output_directory parameters are required'],
45
+ };
46
+ }
47
+ checkBruVersion();
48
+ const collectionName = params.collection_name || 'Imported Collection';
49
+ const absoluteOutput = path.resolve(outputDirectory);
50
+ try {
51
+ logger.info(`Importing OpenAPI from: ${specSource} to ${absoluteOutput}`);
52
+ const args = [
53
+ 'import', 'openapi',
54
+ '--source', specSource,
55
+ '--output', absoluteOutput,
56
+ '--collection-name', collectionName,
57
+ ];
58
+ if (params.group_by)
59
+ args.push('--group-by', params.group_by);
60
+ if (params.insecure === true)
61
+ args.push('--insecure');
62
+ logger.debug(`Executing: bru ${args.join(' ')}`);
63
+ execFileSync('bru', args, { encoding: 'utf-8', stdio: 'pipe' });
64
+ logger.info('OpenAPI import completed');
65
+ // Count generated .bru files using a Node 18-compatible recursive walk
66
+ let requestsCreated = 0;
67
+ try {
68
+ requestsCreated = await countBruFilesRecursively(absoluteOutput);
69
+ }
70
+ catch {
71
+ // best-effort count
72
+ }
73
+ return {
74
+ success: true,
75
+ collection_path: absoluteOutput,
76
+ collection_name: collectionName,
77
+ requests_created: requestsCreated,
78
+ message: `Collection "${collectionName}" imported from OpenAPI spec`,
79
+ errors: [],
80
+ };
81
+ }
82
+ catch (error) {
83
+ logger.error(`OpenAPI import failed: ${error instanceof Error ? error.message : String(error)}`);
84
+ return {
85
+ success: false,
86
+ collection_path: absoluteOutput,
87
+ collection_name: collectionName,
88
+ requests_created: 0,
89
+ message: 'Import failed',
90
+ errors: [error instanceof Error ? error.message : String(error)],
91
+ };
92
+ }
93
+ }
@@ -1,8 +1,8 @@
1
- import { getGlobalMatimoLogger } from '@matimo/core';
1
+ import { getGlobalMatimoLogger } from '@matimo/core/runtime';
2
2
  import { execFileSync } from 'child_process';
3
3
  import { promises as fs } from 'fs';
4
4
  import * as path from 'path';
5
- import { checkBruVersion } from '../bru-utils';
5
+ import { checkBruVersion } from '../bru-utils.js';
6
6
 
7
7
  const logger = getGlobalMatimoLogger();
8
8
 
@@ -16,7 +16,7 @@ parameters:
16
16
 
17
17
  execution:
18
18
  type: function
19
- code: index.ts
19
+ code: index.js
20
20
  timeout: 10000
21
21
 
22
22
  output_schema:
@@ -0,0 +1,94 @@
1
+ import { getGlobalMatimoLogger } from '@matimo/core/runtime';
2
+ import { promises as fs } from 'fs';
3
+ import * as path from 'path';
4
+ const logger = getGlobalMatimoLogger();
5
+ async function countBruFilesRecursively(dir) {
6
+ let count = 0;
7
+ let entries;
8
+ try {
9
+ entries = await fs.readdir(dir);
10
+ }
11
+ catch {
12
+ return 0;
13
+ }
14
+ for (const entry of entries) {
15
+ const fullPath = path.join(dir, entry);
16
+ try {
17
+ const stat = await fs.stat(fullPath);
18
+ if (stat.isDirectory() && entry !== 'node_modules') {
19
+ count += await countBruFilesRecursively(fullPath);
20
+ }
21
+ else if (!stat.isDirectory() && entry.endsWith('.bru')) {
22
+ count++;
23
+ }
24
+ }
25
+ catch {
26
+ // skip
27
+ }
28
+ }
29
+ return count;
30
+ }
31
+ async function findCollections(dir) {
32
+ const results = [];
33
+ let entries;
34
+ try {
35
+ entries = await fs.readdir(dir);
36
+ }
37
+ catch {
38
+ return results;
39
+ }
40
+ if (entries.includes('bruno.json')) {
41
+ const brunoJsonPath = path.join(dir, 'bruno.json');
42
+ let name = path.basename(dir);
43
+ try {
44
+ const raw = await fs.readFile(brunoJsonPath, 'utf-8');
45
+ const parsed = JSON.parse(raw);
46
+ if (parsed.name)
47
+ name = parsed.name;
48
+ }
49
+ catch {
50
+ // use dirname
51
+ }
52
+ // Count .bru files recursively so nested requests are included
53
+ const requestCount = await countBruFilesRecursively(dir);
54
+ results.push({ name, path: dir, request_count: requestCount });
55
+ }
56
+ for (const entry of entries) {
57
+ const fullPath = path.join(dir, entry);
58
+ try {
59
+ const stat = await fs.stat(fullPath);
60
+ if (stat.isDirectory() && entry !== 'node_modules') {
61
+ const nested = await findCollections(fullPath);
62
+ results.push(...nested);
63
+ }
64
+ }
65
+ catch {
66
+ // skip
67
+ }
68
+ }
69
+ return results;
70
+ }
71
+ export default async function execute(params) {
72
+ const workspacePath = params.workspace_path;
73
+ if (!workspacePath) {
74
+ return { success: false, collections: [], errors: ['workspace_path parameter is required'] };
75
+ }
76
+ try {
77
+ logger.info(`Listing collections in: ${workspacePath}`);
78
+ const absolutePath = path.resolve(workspacePath);
79
+ let collections = await findCollections(absolutePath);
80
+ if (params.filter) {
81
+ const filter = params.filter.toLowerCase();
82
+ collections = collections.filter((c) => c.name.toLowerCase().includes(filter));
83
+ }
84
+ return { success: true, collections };
85
+ }
86
+ catch (error) {
87
+ logger.error(`List collections failed: ${error instanceof Error ? error.message : String(error)}`);
88
+ return {
89
+ success: false,
90
+ collections: [],
91
+ errors: [error instanceof Error ? error.message : String(error)],
92
+ };
93
+ }
94
+ }
@@ -1,4 +1,4 @@
1
- import { getGlobalMatimoLogger } from '@matimo/core';
1
+ import { getGlobalMatimoLogger } from '@matimo/core/runtime';
2
2
  import { promises as fs } from 'fs';
3
3
  import * as path from 'path';
4
4
 
@@ -76,7 +76,7 @@ parameters:
76
76
 
77
77
  execution:
78
78
  type: function
79
- code: index.ts
79
+ code: index.js
80
80
  timeout: 120000
81
81
 
82
82
  output_schema:
@@ -0,0 +1,96 @@
1
+ import { getGlobalMatimoLogger } from '@matimo/core/runtime';
2
+ import { execFileSync } from 'child_process';
3
+ import { promises as fs } from 'fs';
4
+ import * as path from 'path';
5
+ import * as os from 'os';
6
+ import { checkBruVersion } from '../bru-utils.js';
7
+ const logger = getGlobalMatimoLogger();
8
+ export default async function execute(params) {
9
+ const collectionPath = params.collection_path;
10
+ if (!collectionPath) {
11
+ return {
12
+ success: false,
13
+ summary: { total_requests: 0, passed: 0, failed: 0, execution_time_ms: 0 },
14
+ results: [],
15
+ errors: ['collection_path parameter is required'],
16
+ };
17
+ }
18
+ checkBruVersion();
19
+ const absolutePath = path.resolve(collectionPath);
20
+ const reportPath = path.resolve(params.report_path ?? path.join(os.tmpdir(), `bru-report-${Date.now()}.json`));
21
+ try {
22
+ logger.info(`Running Bruno collection: ${absolutePath}`);
23
+ const args = ['run', '.', '-r', '--reporter-json', reportPath];
24
+ if (params.environment)
25
+ args.push('--env', params.environment);
26
+ if (params.env_file)
27
+ args.push('--env-file', params.env_file);
28
+ if (params.data_file)
29
+ args.push('--csv-file-path', params.data_file);
30
+ if (params.iteration_count)
31
+ args.push('--iteration-count', String(params.iteration_count));
32
+ if (params.delay_ms)
33
+ args.push('--delay', String(params.delay_ms));
34
+ if (params.tags)
35
+ args.push('--tags', params.tags);
36
+ if (params.exclude_tags)
37
+ args.push('--exclude-tags', params.exclude_tags);
38
+ if (params.tests_only === true)
39
+ args.push('--tests-only');
40
+ if (params.bail_on_failure === true)
41
+ args.push('--bail');
42
+ if (params.parallel === true)
43
+ args.push('--parallel');
44
+ args.push('--sandbox', params.sandbox_mode || 'safe');
45
+ logger.debug(`Executing: bru ${args.join(' ')}`);
46
+ let exitCode = 0;
47
+ try {
48
+ execFileSync('bru', args, { encoding: 'utf-8', stdio: 'pipe', cwd: absolutePath });
49
+ }
50
+ catch (execError) {
51
+ // bru exits non-zero when tests fail — that's OK, we still read the report
52
+ exitCode = 1;
53
+ logger.warn(`bru run exited with non-zero status: ${execError instanceof Error ? execError.message : String(execError)}`);
54
+ }
55
+ // Read JSON report written by --reporter-json
56
+ let reportData = {};
57
+ try {
58
+ const raw = await fs.readFile(reportPath, 'utf-8');
59
+ reportData = JSON.parse(raw);
60
+ }
61
+ catch {
62
+ logger.warn('Could not read/parse JSON report');
63
+ }
64
+ // Map Bruno report fields to schema-declared keys
65
+ const summaryRaw = reportData.summary ?? {};
66
+ const totalRequests = (summaryRaw.totalRequests ?? 0);
67
+ const passed = (summaryRaw.passedRequests ?? 0);
68
+ const failed = (summaryRaw.failedRequests ?? 0);
69
+ const executionTimeMs = (summaryRaw.totalTime ?? 0);
70
+ const rawResults = reportData.results ?? [];
71
+ const results = rawResults.map((r) => {
72
+ const req = r;
73
+ return {
74
+ name: req.suiteName ?? req.name ?? 'unknown',
75
+ success: req.status === 'pass' || req.passed === true,
76
+ status: req.response?.status ?? 0,
77
+ };
78
+ });
79
+ return {
80
+ success: exitCode === 0,
81
+ summary: { total_requests: totalRequests, passed, failed, execution_time_ms: executionTimeMs },
82
+ results,
83
+ report_path: reportPath,
84
+ errors: [],
85
+ };
86
+ }
87
+ catch (error) {
88
+ logger.error(`Collection execution failed: ${error instanceof Error ? error.message : String(error)}`);
89
+ return {
90
+ success: false,
91
+ summary: { total_requests: 0, passed: 0, failed: 0, execution_time_ms: 0 },
92
+ results: [],
93
+ errors: [error instanceof Error ? error.message : String(error)],
94
+ };
95
+ }
96
+ }
@@ -1,9 +1,9 @@
1
- import { getGlobalMatimoLogger } from '@matimo/core';
1
+ import { getGlobalMatimoLogger } from '@matimo/core/runtime';
2
2
  import { execFileSync } from 'child_process';
3
3
  import { promises as fs } from 'fs';
4
4
  import * as path from 'path';
5
5
  import * as os from 'os';
6
- import { checkBruVersion } from '../bru-utils';
6
+ import { checkBruVersion } from '../bru-utils.js';
7
7
 
8
8
  const logger = getGlobalMatimoLogger();
9
9
 
@@ -32,7 +32,7 @@ parameters:
32
32
 
33
33
  execution:
34
34
  type: function
35
- code: index.ts
35
+ code: index.js
36
36
  timeout: 60000
37
37
 
38
38
  output_schema:
@@ -0,0 +1,95 @@
1
+ import { getGlobalMatimoLogger } from '@matimo/core/runtime';
2
+ import { execFileSync } from 'child_process';
3
+ import { promises as fs } from 'fs';
4
+ import * as path from 'path';
5
+ import { checkBruVersion } from '../bru-utils.js';
6
+ const logger = getGlobalMatimoLogger();
7
+ /** Search recursively for a .bru file matching the given slug, return path relative to root. */
8
+ async function findBruFile(root, slug) {
9
+ let entries;
10
+ try {
11
+ entries = await fs.readdir(root);
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ if (entries.includes(`${slug}.bru`)) {
17
+ return `${slug}.bru`;
18
+ }
19
+ for (const entry of entries) {
20
+ const fullPath = path.join(root, entry);
21
+ try {
22
+ const stat = await fs.stat(fullPath);
23
+ if (stat.isDirectory() && entry !== 'node_modules') {
24
+ const found = await findBruFile(fullPath, slug);
25
+ if (found)
26
+ return path.join(entry, found);
27
+ }
28
+ }
29
+ catch {
30
+ // skip
31
+ }
32
+ }
33
+ return null;
34
+ }
35
+ export default async function execute(params) {
36
+ const collectionPath = params.collection_path;
37
+ const requestName = params.request_name;
38
+ if (!collectionPath || !requestName) {
39
+ return {
40
+ success: false,
41
+ request: requestName ?? '',
42
+ status: 0,
43
+ duration_ms: 0,
44
+ errors: ['collection_path and request_name parameters are required'],
45
+ };
46
+ }
47
+ checkBruVersion();
48
+ const absolutePath = path.resolve(collectionPath);
49
+ const slug = requestName.toLowerCase().replace(/\s+/g, '-');
50
+ try {
51
+ logger.info(`Running request: ${requestName} from ${absolutePath}`);
52
+ // Locate the .bru file (may be in a requests/ subfolder)
53
+ const bruRelPath = (await findBruFile(absolutePath, slug)) ?? `${slug}.bru`;
54
+ const args = ['run', bruRelPath];
55
+ if (params.environment)
56
+ args.push('--env', params.environment);
57
+ if (params.env_file)
58
+ args.push('--env-file', params.env_file);
59
+ args.push('--sandbox', params.sandbox_mode || 'safe');
60
+ logger.debug(`Executing: bru ${args.join(' ')}`);
61
+ const start = Date.now();
62
+ let success = true;
63
+ let output = '';
64
+ try {
65
+ output = execFileSync('bru', args, { encoding: 'utf-8', stdio: 'pipe', cwd: absolutePath });
66
+ }
67
+ catch (execError) {
68
+ success = false;
69
+ // Extract stdout/stderr from the error so assertion failures are visible
70
+ const err = execError;
71
+ output = [err.stdout, err.stderr, err.message].filter(Boolean).join('\n');
72
+ }
73
+ const durationMs = Date.now() - start;
74
+ // Parse status code from bru run output (e.g. "200 OK")
75
+ const statusMatch = output.match(/\b([1-5]\d{2})\b/);
76
+ const status = statusMatch ? parseInt(statusMatch[1], 10) : (success ? 200 : 0);
77
+ return {
78
+ success,
79
+ request: requestName,
80
+ status,
81
+ duration_ms: durationMs,
82
+ errors: success ? [] : [output],
83
+ };
84
+ }
85
+ catch (error) {
86
+ logger.error(`Request execution failed: ${error instanceof Error ? error.message : String(error)}`);
87
+ return {
88
+ success: false,
89
+ request: requestName,
90
+ status: 0,
91
+ duration_ms: 0,
92
+ errors: [error instanceof Error ? error.message : String(error)],
93
+ };
94
+ }
95
+ }
@@ -1,8 +1,8 @@
1
- import { getGlobalMatimoLogger } from '@matimo/core';
1
+ import { getGlobalMatimoLogger } from '@matimo/core/runtime';
2
2
  import { execFileSync } from 'child_process';
3
3
  import { promises as fs } from 'fs';
4
4
  import * as path from 'path';
5
- import { checkBruVersion } from '../bru-utils';
5
+ import { checkBruVersion } from '../bru-utils.js';
6
6
 
7
7
  const logger = getGlobalMatimoLogger();
8
8