@matimo/bruno 0.1.3 → 0.1.5
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 +6 -3
- package/tools/bru-utils.js +41 -0
- package/tools/bruno_add_request/definition.yaml +1 -1
- package/tools/bruno_add_request/index.js +82 -0
- package/tools/bruno_add_request/index.ts +1 -1
- package/tools/bruno_create_collection/definition.yaml +1 -1
- package/tools/bruno_create_collection/index.js +45 -0
- package/tools/bruno_create_collection/index.ts +1 -1
- package/tools/bruno_get_collection_info/definition.yaml +1 -1
- package/tools/bruno_get_collection_info/index.js +106 -0
- package/tools/bruno_get_collection_info/index.ts +1 -1
- package/tools/bruno_import_openapi/definition.yaml +1 -1
- package/tools/bruno_import_openapi/index.js +93 -0
- package/tools/bruno_import_openapi/index.ts +2 -2
- package/tools/bruno_list_collections/definition.yaml +1 -1
- package/tools/bruno_list_collections/index.js +94 -0
- package/tools/bruno_list_collections/index.ts +1 -1
- package/tools/bruno_run_collection/definition.yaml +1 -1
- package/tools/bruno_run_collection/index.js +96 -0
- package/tools/bruno_run_collection/index.ts +2 -2
- package/tools/bruno_run_request/definition.yaml +1 -1
- package/tools/bruno_run_request/index.js +95 -0
- package/tools/bruno_run_request/index.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matimo/bruno",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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.
|
|
12
|
+
"@matimo/core": "0.1.5"
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
|
-
"matimo": "0.1.
|
|
15
|
+
"matimo": "0.1.5"
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
|
|
@@ -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
|
+
}
|
|
@@ -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
|
|
|
@@ -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
|
|