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/CONTRIBUTING.md +71 -0
- package/LICENSE +21 -0
- package/README.md +345 -0
- package/bin/mpx-api.js +52 -0
- package/examples/github-api.yaml +35 -0
- package/examples/jsonplaceholder.yaml +52 -0
- package/examples/openapi-petstore.yaml +88 -0
- package/package.json +51 -0
- package/src/commands/collection.js +175 -0
- package/src/commands/docs.js +140 -0
- package/src/commands/env.js +152 -0
- package/src/commands/history.js +46 -0
- package/src/commands/load.js +161 -0
- package/src/commands/mock.js +178 -0
- package/src/commands/request.js +91 -0
- package/src/commands/test.js +55 -0
- package/src/lib/assertion.js +170 -0
- package/src/lib/collection-runner.js +66 -0
- package/src/lib/config.js +71 -0
- package/src/lib/history.js +51 -0
- package/src/lib/http-client.js +179 -0
- package/src/lib/license.js +35 -0
- package/src/lib/output.js +172 -0
- package/src/lib/template.js +63 -0
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
|
+
}
|