mpx-api 1.0.0

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 ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "mpx-api",
3
+ "version": "1.0.0",
4
+ "description": "Developer-first API testing, mocking, and documentation CLI",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "mpx-api": "./bin/mpx-api.js"
8
+ },
9
+ "keywords": [
10
+ "api",
11
+ "testing",
12
+ "http",
13
+ "rest",
14
+ "mock",
15
+ "cli",
16
+ "postman",
17
+ "httpie",
18
+ "openapi",
19
+ "swagger"
20
+ ],
21
+ "author": "Mesaplex <support@mesaplex.com>",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/mesaplexdev/mpx-api.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/mesaplexdev/mpx-api/issues"
29
+ },
30
+ "homepage": "https://github.com/mesaplexdev/mpx-api#readme",
31
+ "engines": {
32
+ "node": ">=18.0.0"
33
+ },
34
+ "type": "module",
35
+ "scripts": {
36
+ "test": "node --test test/**/*.test.js",
37
+ "test:watch": "node --test --watch test/**/*.test.js",
38
+ "lint": "echo 'TODO: Add eslint'",
39
+ "prepublishOnly": "npm test"
40
+ },
41
+ "dependencies": {
42
+ "chalk": "^5.3.0",
43
+ "commander": "^12.0.0",
44
+ "yaml": "^2.3.4",
45
+ "undici": "^6.6.0",
46
+ "cli-highlight": "^2.1.11",
47
+ "tough-cookie": "^4.1.3",
48
+ "filenamify": "^6.0.0"
49
+ },
50
+ "devDependencies": {}
51
+ }
@@ -0,0 +1,175 @@
1
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { parse, stringify } from 'yaml';
3
+ import { ensureLocalDir } from '../lib/config.js';
4
+ import { formatSuccess, formatError, formatInfo } from '../lib/output.js';
5
+ import { runCollection } from '../lib/collection-runner.js';
6
+ import { formatTestResults } from '../lib/output.js';
7
+ import { join } from 'path';
8
+
9
+ export function registerCollectionCommands(program) {
10
+ const collection = program
11
+ .command('collection')
12
+ .description('Manage request collections');
13
+
14
+ collection
15
+ .command('init')
16
+ .description('Initialize a new collection in current directory')
17
+ .option('-n, --name <name>', 'Collection name', 'API Collection')
18
+ .action((options) => {
19
+ try {
20
+ ensureLocalDir();
21
+
22
+ const collectionData = {
23
+ name: options.name,
24
+ baseUrl: '',
25
+ requests: [],
26
+ };
27
+
28
+ const yamlPath = join('.mpx-api', 'collection.yaml');
29
+ writeFileSync(yamlPath, stringify(collectionData));
30
+
31
+ formatSuccess(`Collection initialized at ${yamlPath}`);
32
+ formatInfo('Add requests with: mpx-api collection add <name> <method> <url>');
33
+ } catch (err) {
34
+ formatError(err);
35
+ process.exit(1);
36
+ }
37
+ });
38
+
39
+ collection
40
+ .command('add <name> <method> <url>')
41
+ .description('Add a request to the collection')
42
+ .option('-H, --header <header...>', 'Add request headers')
43
+ .option('-j, --json <data>', 'JSON body')
44
+ .option('-d, --data <data>', 'Request body')
45
+ .action((name, method, url, options) => {
46
+ try {
47
+ const yamlPath = join('.mpx-api', 'collection.yaml');
48
+
49
+ if (!existsSync(yamlPath)) {
50
+ formatError(new Error('No collection found. Run "mpx-api collection init" first.'));
51
+ process.exit(1);
52
+ }
53
+
54
+ const content = readFileSync(yamlPath, 'utf8');
55
+ const data = parse(content);
56
+
57
+ const request = {
58
+ name,
59
+ method: method.toUpperCase(),
60
+ url,
61
+ };
62
+
63
+ if (options.header && options.header.length > 0) {
64
+ request.headers = {};
65
+ for (const header of options.header) {
66
+ const [key, ...valueParts] = header.split(':');
67
+ request.headers[key.trim()] = valueParts.join(':').trim();
68
+ }
69
+ }
70
+
71
+ if (options.json) {
72
+ try {
73
+ request.json = JSON.parse(options.json);
74
+ } catch (err) {
75
+ formatError(new Error(`Invalid JSON: ${err.message}`));
76
+ process.exit(1);
77
+ }
78
+ } else if (options.data) {
79
+ request.body = options.data;
80
+ }
81
+
82
+ data.requests = data.requests || [];
83
+ data.requests.push(request);
84
+
85
+ writeFileSync(yamlPath, stringify(data));
86
+ formatSuccess(`Added request "${name}" to collection`);
87
+ } catch (err) {
88
+ formatError(err);
89
+ process.exit(1);
90
+ }
91
+ });
92
+
93
+ collection
94
+ .command('run [file]')
95
+ .description('Run a collection')
96
+ .option('-e, --env <name>', 'Environment to use')
97
+ .option('--base-url <url>', 'Override base URL')
98
+ .action(async (file, options) => {
99
+ try {
100
+ const collectionPath = file || join('.mpx-api', 'collection.yaml');
101
+
102
+ if (!existsSync(collectionPath)) {
103
+ formatError(new Error(`Collection not found: ${collectionPath}`));
104
+ process.exit(1);
105
+ }
106
+
107
+ const content = readFileSync(collectionPath, 'utf8');
108
+ const collection = parse(content);
109
+
110
+ // Load environment if specified
111
+ let env = {};
112
+ if (options.env) {
113
+ const envPath = join('.mpx-api', 'environments', `${options.env}.yaml`);
114
+ if (existsSync(envPath)) {
115
+ const envContent = readFileSync(envPath, 'utf8');
116
+ env = parse(envContent);
117
+ } else {
118
+ formatWarning(`Environment "${options.env}" not found, continuing without it`);
119
+ }
120
+ }
121
+
122
+ const baseUrl = options.baseUrl || collection.baseUrl || '';
123
+
124
+ const results = await runCollection(collection, { env, baseUrl });
125
+
126
+ const allPassed = formatTestResults(results);
127
+
128
+ process.exit(allPassed ? 0 : 1);
129
+ } catch (err) {
130
+ formatError(err);
131
+ process.exit(1);
132
+ }
133
+ });
134
+
135
+ collection
136
+ .command('list')
137
+ .description('List all requests in collection')
138
+ .action(() => {
139
+ try {
140
+ const yamlPath = join('.mpx-api', 'collection.yaml');
141
+
142
+ if (!existsSync(yamlPath)) {
143
+ formatError(new Error('No collection found.'));
144
+ process.exit(1);
145
+ }
146
+
147
+ const content = readFileSync(yamlPath, 'utf8');
148
+ const data = parse(content);
149
+
150
+ console.log('');
151
+ console.log(`Collection: ${data.name || 'Unnamed'}`);
152
+ if (data.baseUrl) {
153
+ console.log(`Base URL: ${data.baseUrl}`);
154
+ }
155
+ console.log('');
156
+ console.log('Requests:');
157
+
158
+ if (!data.requests || data.requests.length === 0) {
159
+ console.log(' (none)');
160
+ } else {
161
+ for (const req of data.requests) {
162
+ console.log(` ${req.name}: ${req.method} ${req.url}`);
163
+ }
164
+ }
165
+ console.log('');
166
+ } catch (err) {
167
+ formatError(err);
168
+ process.exit(1);
169
+ }
170
+ });
171
+ }
172
+
173
+ function formatWarning(message) {
174
+ console.log(chalk.yellow('⚠'), message);
175
+ }
@@ -0,0 +1,140 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
2
+ import { parse } from 'yaml';
3
+ import { formatError, formatSuccess } from '../lib/output.js';
4
+ import { requireProLicense } from '../lib/license.js';
5
+
6
+ export function registerDocsCommand(program) {
7
+ program
8
+ .command('docs <collection>')
9
+ .description('Generate API documentation from collection (Pro)')
10
+ .option('-o, --output <file>', 'Output file', 'API.md')
11
+ .option('--format <format>', 'Output format (markdown, html)', 'markdown')
12
+ .action((collection, options) => {
13
+ try {
14
+ requireProLicense('Documentation generation');
15
+
16
+ if (!existsSync(collection)) {
17
+ formatError(new Error(`Collection not found: ${collection}`));
18
+ process.exit(1);
19
+ }
20
+
21
+ const content = readFileSync(collection, 'utf8');
22
+ const data = parse(content);
23
+
24
+ const markdown = generateMarkdownDocs(data);
25
+
26
+ writeFileSync(options.output, markdown);
27
+ formatSuccess(`Documentation generated: ${options.output}`);
28
+
29
+ } catch (err) {
30
+ formatError(err);
31
+ process.exit(1);
32
+ }
33
+ });
34
+ }
35
+
36
+ function generateMarkdownDocs(collection) {
37
+ const lines = [];
38
+
39
+ // Header
40
+ lines.push(`# ${collection.name || 'API Documentation'}`);
41
+ lines.push('');
42
+
43
+ if (collection.description) {
44
+ lines.push(collection.description);
45
+ lines.push('');
46
+ }
47
+
48
+ if (collection.baseUrl) {
49
+ lines.push(`**Base URL:** \`${collection.baseUrl}\``);
50
+ lines.push('');
51
+ }
52
+
53
+ lines.push('---');
54
+ lines.push('');
55
+
56
+ // Table of Contents
57
+ lines.push('## Table of Contents');
58
+ lines.push('');
59
+
60
+ for (const request of collection.requests || []) {
61
+ const anchor = request.name.toLowerCase().replace(/\s+/g, '-');
62
+ lines.push(`- [${request.name}](#${anchor})`);
63
+ }
64
+
65
+ lines.push('');
66
+ lines.push('---');
67
+ lines.push('');
68
+
69
+ // Requests
70
+ for (const request of collection.requests || []) {
71
+ lines.push(`## ${request.name}`);
72
+ lines.push('');
73
+
74
+ if (request.description) {
75
+ lines.push(request.description);
76
+ lines.push('');
77
+ }
78
+
79
+ // Request details
80
+ lines.push('```');
81
+ lines.push(`${request.method} ${request.url}`);
82
+ lines.push('```');
83
+ lines.push('');
84
+
85
+ // Headers
86
+ if (request.headers && Object.keys(request.headers).length > 0) {
87
+ lines.push('**Headers:**');
88
+ lines.push('');
89
+ lines.push('```');
90
+ for (const [key, value] of Object.entries(request.headers)) {
91
+ lines.push(`${key}: ${value}`);
92
+ }
93
+ lines.push('```');
94
+ lines.push('');
95
+ }
96
+
97
+ // Body
98
+ if (request.json) {
99
+ lines.push('**Request Body:**');
100
+ lines.push('');
101
+ lines.push('```json');
102
+ lines.push(JSON.stringify(request.json, null, 2));
103
+ lines.push('```');
104
+ lines.push('');
105
+ } else if (request.body) {
106
+ lines.push('**Request Body:**');
107
+ lines.push('');
108
+ lines.push('```');
109
+ lines.push(request.body);
110
+ lines.push('```');
111
+ lines.push('');
112
+ }
113
+
114
+ // Assertions
115
+ if (request.assert) {
116
+ lines.push('**Expected Response:**');
117
+ lines.push('');
118
+
119
+ for (const [key, value] of Object.entries(request.assert)) {
120
+ if (key === 'status') {
121
+ lines.push(`- Status Code: ${value}`);
122
+ } else {
123
+ lines.push(`- ${key}: ${JSON.stringify(value)}`);
124
+ }
125
+ }
126
+ lines.push('');
127
+ }
128
+
129
+ lines.push('---');
130
+ lines.push('');
131
+ }
132
+
133
+ // Footer
134
+ lines.push('---');
135
+ lines.push('');
136
+ lines.push('*Generated with [mpx-api](https://mpx-api.dev)*');
137
+ lines.push('');
138
+
139
+ return lines.join('\n');
140
+ }
@@ -0,0 +1,152 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs';
2
+ import { parse, stringify } from 'yaml';
3
+ import { join } from 'path';
4
+ import { ensureLocalDir } from '../lib/config.js';
5
+ import { formatSuccess, formatError, formatInfo } from '../lib/output.js';
6
+ import chalk from 'chalk';
7
+
8
+ export function registerEnvCommands(program) {
9
+ const env = program
10
+ .command('env')
11
+ .description('Manage environments');
12
+
13
+ env
14
+ .command('init')
15
+ .description('Initialize environments directory')
16
+ .action(() => {
17
+ try {
18
+ ensureLocalDir();
19
+ const envDir = join('.mpx-api', 'environments');
20
+
21
+ if (!existsSync(envDir)) {
22
+ mkdirSync(envDir, { recursive: true });
23
+ }
24
+
25
+ // Create default environments
26
+ const environments = ['dev', 'staging', 'production'];
27
+
28
+ for (const envName of environments) {
29
+ const envPath = join(envDir, `${envName}.yaml`);
30
+ if (!existsSync(envPath)) {
31
+ const envData = {
32
+ name: envName,
33
+ variables: {},
34
+ };
35
+ writeFileSync(envPath, stringify(envData));
36
+ }
37
+ }
38
+
39
+ formatSuccess('Environments initialized');
40
+ formatInfo(`Created: ${environments.join(', ')}`);
41
+ formatInfo('Set variables with: mpx-api env set <env> <key>=<value>');
42
+ } catch (err) {
43
+ formatError(err);
44
+ process.exit(1);
45
+ }
46
+ });
47
+
48
+ env
49
+ .command('set <environment> <variable>')
50
+ .description('Set an environment variable (KEY=value)')
51
+ .action((environment, variable) => {
52
+ try {
53
+ ensureLocalDir();
54
+ const envDir = join('.mpx-api', 'environments');
55
+
56
+ if (!existsSync(envDir)) {
57
+ formatError(new Error('Environments not initialized. Run "mpx-api env init" first.'));
58
+ process.exit(1);
59
+ }
60
+
61
+ const envPath = join(envDir, `${environment}.yaml`);
62
+
63
+ // Parse variable
64
+ const [key, ...valueParts] = variable.split('=');
65
+ const value = valueParts.join('=');
66
+
67
+ if (!key || !value) {
68
+ formatError(new Error('Invalid format. Use: KEY=value'));
69
+ process.exit(1);
70
+ }
71
+
72
+ let envData = { name: environment, variables: {} };
73
+
74
+ if (existsSync(envPath)) {
75
+ const content = readFileSync(envPath, 'utf8');
76
+ envData = parse(content) || envData;
77
+ }
78
+
79
+ envData.variables = envData.variables || {};
80
+ envData.variables[key] = value;
81
+
82
+ writeFileSync(envPath, stringify(envData));
83
+ formatSuccess(`Set ${key}=${value} in ${environment} environment`);
84
+ } catch (err) {
85
+ formatError(err);
86
+ process.exit(1);
87
+ }
88
+ });
89
+
90
+ env
91
+ .command('list [environment]')
92
+ .description('List all environments or variables in an environment')
93
+ .action((environment) => {
94
+ try {
95
+ const envDir = join('.mpx-api', 'environments');
96
+
97
+ if (!existsSync(envDir)) {
98
+ formatError(new Error('Environments not initialized.'));
99
+ process.exit(1);
100
+ }
101
+
102
+ if (environment) {
103
+ // List variables in specific environment
104
+ const envPath = join(envDir, `${environment}.yaml`);
105
+
106
+ if (!existsSync(envPath)) {
107
+ formatError(new Error(`Environment "${environment}" not found.`));
108
+ process.exit(1);
109
+ }
110
+
111
+ const content = readFileSync(envPath, 'utf8');
112
+ const data = parse(content);
113
+
114
+ console.log('');
115
+ console.log(chalk.bold(`Environment: ${environment}`));
116
+ console.log('');
117
+
118
+ if (!data.variables || Object.keys(data.variables).length === 0) {
119
+ console.log(' (no variables)');
120
+ } else {
121
+ for (const [key, value] of Object.entries(data.variables)) {
122
+ console.log(` ${chalk.cyan(key)}: ${value}`);
123
+ }
124
+ }
125
+ console.log('');
126
+ } else {
127
+ // List all environments
128
+ const files = readdirSync(envDir).filter(f => f.endsWith('.yaml'));
129
+
130
+ console.log('');
131
+ console.log(chalk.bold('Environments:'));
132
+ console.log('');
133
+
134
+ if (files.length === 0) {
135
+ console.log(' (none)');
136
+ } else {
137
+ for (const file of files) {
138
+ const envName = file.replace('.yaml', '');
139
+ const content = readFileSync(join(envDir, file), 'utf8');
140
+ const data = parse(content);
141
+ const varCount = Object.keys(data.variables || {}).length;
142
+ console.log(` ${chalk.cyan(envName)} (${varCount} variable${varCount !== 1 ? 's' : ''})`);
143
+ }
144
+ }
145
+ console.log('');
146
+ }
147
+ } catch (err) {
148
+ formatError(err);
149
+ process.exit(1);
150
+ }
151
+ });
152
+ }
@@ -0,0 +1,46 @@
1
+ import chalk from 'chalk';
2
+ import { loadHistory } from '../lib/history.js';
3
+ import { formatError } from '../lib/output.js';
4
+
5
+ export function registerHistoryCommand(program) {
6
+ program
7
+ .command('history')
8
+ .description('Show request history')
9
+ .option('-n, --limit <number>', 'Number of entries to show', '20')
10
+ .action((options) => {
11
+ try {
12
+ const limit = parseInt(options.limit);
13
+ const history = loadHistory(limit);
14
+
15
+ if (history.length === 0) {
16
+ console.log('');
17
+ console.log(chalk.gray('No history found.'));
18
+ console.log('');
19
+ return;
20
+ }
21
+
22
+ console.log('');
23
+ console.log(chalk.bold('Request History:'));
24
+ console.log('');
25
+
26
+ for (const entry of history) {
27
+ const timestamp = new Date(entry.timestamp).toLocaleString();
28
+ const statusColor = entry.status >= 200 && entry.status < 300 ? 'green' :
29
+ entry.status >= 400 ? 'yellow' : 'white';
30
+
31
+ console.log(
32
+ chalk.gray(timestamp) + ' ' +
33
+ chalk.bold(entry.method.padEnd(6)) +
34
+ chalk[statusColor](entry.status.toString().padEnd(4)) +
35
+ chalk.gray(`${entry.responseTime}ms`.padEnd(8)) +
36
+ entry.url
37
+ );
38
+ }
39
+
40
+ console.log('');
41
+ } catch (err) {
42
+ formatError(err);
43
+ process.exit(1);
44
+ }
45
+ });
46
+ }