apigrip 0.1.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/README.md +240 -0
- package/cli/commands/curl.js +11 -0
- package/cli/commands/env.js +174 -0
- package/cli/commands/last.js +63 -0
- package/cli/commands/list.js +35 -0
- package/cli/commands/mcp.js +25 -0
- package/cli/commands/projects.js +50 -0
- package/cli/commands/send.js +189 -0
- package/cli/commands/serve.js +46 -0
- package/cli/index.js +109 -0
- package/cli/output.js +168 -0
- package/cli/resolve-project.js +43 -0
- package/client/dist/assets/index-CtHBIuEv.js +75 -0
- package/client/dist/assets/index-kzeRjfI8.css +1 -0
- package/client/dist/index.html +19 -0
- package/core/curl-builder.js +218 -0
- package/core/curl-executor.js +370 -0
- package/core/env-resolver.js +244 -0
- package/core/git-info.js +41 -0
- package/core/params-store.js +94 -0
- package/core/preferences-store.js +150 -0
- package/core/projects-store.js +173 -0
- package/core/response-store.js +121 -0
- package/core/schema-validator.js +196 -0
- package/core/spec-discovery.js +109 -0
- package/core/spec-parser.js +172 -0
- package/lib/index.cjs +16 -0
- package/lib/index.js +294 -0
- package/mcp/server.js +257 -0
- package/package.json +70 -0
- package/server/index.js +53 -0
- package/server/routes/browse.js +61 -0
- package/server/routes/environments.js +92 -0
- package/server/routes/events.js +40 -0
- package/server/routes/params.js +38 -0
- package/server/routes/preferences.js +27 -0
- package/server/routes/project.js +94 -0
- package/server/routes/projects.js +51 -0
- package/server/routes/requests.js +192 -0
- package/server/routes/spec.js +92 -0
- package/server/spec-watcher.js +236 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// cli/commands/send.js — Send an API request via curl
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import jq from '@michaelhomer/jqjs';
|
|
5
|
+
import { parseSpec, extractEndpoints, getEndpointDetails } from '../../core/spec-parser.js';
|
|
6
|
+
import { buildCurlArgs, buildUrl, resolveVariables, shellQuote } from '../../core/curl-builder.js';
|
|
7
|
+
import { loadEnvironments, resolveEnvironment, resolveAllParams } from '../../core/env-resolver.js';
|
|
8
|
+
import { executeCurl } from '../../core/curl-executor.js';
|
|
9
|
+
import { saveLastResponse } from '../../core/response-store.js';
|
|
10
|
+
import { formatResponse } from '../output.js';
|
|
11
|
+
import { resolveProjectContext } from '../resolve-project.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve body data from -d argument.
|
|
15
|
+
* Supports: inline JSON, @filename (read from file), @- (read from stdin).
|
|
16
|
+
*/
|
|
17
|
+
function resolveBodyData(data) {
|
|
18
|
+
if (!data) return data;
|
|
19
|
+
if (typeof data !== 'string') return data;
|
|
20
|
+
if (!data.startsWith('@')) return data;
|
|
21
|
+
|
|
22
|
+
const source = data.slice(1);
|
|
23
|
+
if (source === '-') {
|
|
24
|
+
return fs.readFileSync(0, 'utf8').trim();
|
|
25
|
+
}
|
|
26
|
+
if (!fs.existsSync(source)) {
|
|
27
|
+
console.error(`Body file not found: ${source}`);
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return fs.readFileSync(source, 'utf8').trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Send command handler.
|
|
36
|
+
* @param {object} argv - Parsed CLI arguments
|
|
37
|
+
*/
|
|
38
|
+
export async function sendCommand(argv) {
|
|
39
|
+
const { projectDir, specPath } = await resolveProjectContext(argv);
|
|
40
|
+
if (!specPath) {
|
|
41
|
+
console.error('No spec file found');
|
|
42
|
+
process.exit(2);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let spec;
|
|
46
|
+
try {
|
|
47
|
+
spec = await parseSpec(specPath);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error(`Spec parse error: ${err.message}`);
|
|
50
|
+
process.exit(2);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const method = argv.method.toUpperCase();
|
|
54
|
+
const pathArg = argv.path;
|
|
55
|
+
const apiPath = typeof pathArg === 'string' ? (pathArg.startsWith('/') ? pathArg : '/' + pathArg) : '/' + String(pathArg);
|
|
56
|
+
const details = getEndpointDetails(spec, method, apiPath);
|
|
57
|
+
if (!details) {
|
|
58
|
+
console.error(`Endpoint not found: ${method} ${apiPath}`);
|
|
59
|
+
process.exit(3);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Parse path params from --pathParam key=value pairs
|
|
63
|
+
const pathParams = {};
|
|
64
|
+
if (argv.pathParam) {
|
|
65
|
+
for (const p of argv.pathParam) {
|
|
66
|
+
const [k, ...v] = p.split('=');
|
|
67
|
+
pathParams[k] = v.join('=');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const queryParams = {};
|
|
72
|
+
if (argv.query) {
|
|
73
|
+
for (const q of argv.query) {
|
|
74
|
+
const [k, ...v] = q.split('=');
|
|
75
|
+
queryParams[k] = v.join('=');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const headers = {};
|
|
79
|
+
if (argv.header) {
|
|
80
|
+
for (const h of argv.header) {
|
|
81
|
+
const [k, ...v] = h.split(':');
|
|
82
|
+
headers[k.trim()] = v.join(':').trim();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Resolve environment
|
|
87
|
+
let env = {};
|
|
88
|
+
try {
|
|
89
|
+
const envData = loadEnvironments(projectDir);
|
|
90
|
+
if (argv.env) {
|
|
91
|
+
envData.active = argv.env;
|
|
92
|
+
}
|
|
93
|
+
// Manually merge base + active environment
|
|
94
|
+
env = { ...envData.base };
|
|
95
|
+
if (envData.active && envData.environments[envData.active]) {
|
|
96
|
+
Object.assign(env, envData.environments[envData.active]);
|
|
97
|
+
}
|
|
98
|
+
} catch (e) {
|
|
99
|
+
// No env file, continue without
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Resolve {{ variable }} references
|
|
103
|
+
for (const [k, v] of Object.entries(pathParams)) {
|
|
104
|
+
pathParams[k] = resolveVariables(v, env);
|
|
105
|
+
}
|
|
106
|
+
for (const [k, v] of Object.entries(queryParams)) {
|
|
107
|
+
queryParams[k] = resolveVariables(v, env);
|
|
108
|
+
}
|
|
109
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
110
|
+
headers[k] = resolveVariables(v, env);
|
|
111
|
+
}
|
|
112
|
+
const bodyData = resolveBodyData(argv.data);
|
|
113
|
+
if (bodyData === null && argv.data) {
|
|
114
|
+
return; // resolveBodyData already printed error
|
|
115
|
+
}
|
|
116
|
+
const resolvedBody = bodyData ? resolveVariables(bodyData, env) : bodyData;
|
|
117
|
+
|
|
118
|
+
const servers = details.servers || spec.servers || [];
|
|
119
|
+
const url = buildUrl(servers, 0, {}, apiPath, pathParams, queryParams, env.base_url);
|
|
120
|
+
|
|
121
|
+
const contentType = resolvedBody ? 'application/json' : null;
|
|
122
|
+
const curlArgs = buildCurlArgs({
|
|
123
|
+
method,
|
|
124
|
+
url,
|
|
125
|
+
headers,
|
|
126
|
+
body: resolvedBody,
|
|
127
|
+
contentType,
|
|
128
|
+
insecure: env.insecure === true,
|
|
129
|
+
timeout: env.timeout || 30,
|
|
130
|
+
outputFile: 'TMPFILE',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (argv.curl) {
|
|
134
|
+
console.log(`curl ${curlArgs.map(a => shellQuote(a)).join(' ')}`);
|
|
135
|
+
process.exit(0);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (argv.dryRun) {
|
|
139
|
+
console.log(`Would send: ${method} ${url}`);
|
|
140
|
+
console.log(`curl ${curlArgs.map(a => shellQuote(a)).join(' ')}`);
|
|
141
|
+
process.exit(0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const response = await executeCurl(curlArgs);
|
|
146
|
+
|
|
147
|
+
// Cache response for later retrieval
|
|
148
|
+
try {
|
|
149
|
+
saveLastResponse(projectDir, method, apiPath, {
|
|
150
|
+
status: response.status,
|
|
151
|
+
status_text: response.status_text,
|
|
152
|
+
headers: response.headers,
|
|
153
|
+
body: response.body,
|
|
154
|
+
size_bytes: response.size_bytes,
|
|
155
|
+
timing: response.timing,
|
|
156
|
+
curl_command: response.curl_command,
|
|
157
|
+
});
|
|
158
|
+
} catch {
|
|
159
|
+
// Non-critical — don't fail the command if caching fails
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (argv.filter) {
|
|
163
|
+
// Apply jq filter to response body
|
|
164
|
+
let parsed;
|
|
165
|
+
try {
|
|
166
|
+
parsed = JSON.parse(response.body);
|
|
167
|
+
} catch {
|
|
168
|
+
console.error('Cannot apply --filter: response body is not valid JSON');
|
|
169
|
+
console.log(response.body);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const fn = jq.compile(argv.filter);
|
|
174
|
+
const results = [...fn(parsed)];
|
|
175
|
+
for (const r of results) {
|
|
176
|
+
console.log(typeof r === 'string' ? r : JSON.stringify(r, null, 2));
|
|
177
|
+
}
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.error(`jq filter error: ${err.message}`);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
console.log(formatResponse(response, { verbose: argv.verbose, headers: argv.headers }));
|
|
184
|
+
}
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.error(`Request failed: ${err.message}`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { parseSpec } from '../../core/spec-parser.js';
|
|
3
|
+
import { createServer } from '../../server/index.js';
|
|
4
|
+
import { resolveProjectContext } from '../resolve-project.js';
|
|
5
|
+
|
|
6
|
+
export async function serveCommand(argv) {
|
|
7
|
+
const { projectDir, specPath } = await resolveProjectContext(argv);
|
|
8
|
+
|
|
9
|
+
if (!fs.existsSync(projectDir)) {
|
|
10
|
+
console.error(`Project directory does not exist: ${projectDir}`);
|
|
11
|
+
process.exitCode = 2;
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let spec = null;
|
|
16
|
+
if (specPath) {
|
|
17
|
+
try {
|
|
18
|
+
spec = await parseSpec(specPath);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.error(`Warning: Spec parse error: ${err.message}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const noBrowse = argv.noBrowse || argv['no-browse'] || false;
|
|
25
|
+
const port = argv.port || 3000;
|
|
26
|
+
const host = argv.host || '127.0.0.1';
|
|
27
|
+
|
|
28
|
+
// Warn when exposing to network without --no-browse
|
|
29
|
+
if (host !== '127.0.0.1' && host !== 'localhost' && !noBrowse) {
|
|
30
|
+
console.warn('WARNING: Binding to a public interface without --no-browse.');
|
|
31
|
+
console.warn(' The /api/browse endpoint exposes your filesystem.');
|
|
32
|
+
console.warn(' Use --no-browse to disable directory listing and project switching.');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { app } = createServer({ projectDir, specPath, spec, noBrowse });
|
|
36
|
+
|
|
37
|
+
app.listen(port, host, () => {
|
|
38
|
+
console.log(`Server listening on http://${host}:${port}`);
|
|
39
|
+
if (argv.open) {
|
|
40
|
+
import('child_process').then(({ exec }) => {
|
|
41
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
42
|
+
exec(`${cmd} http://${host}:${port}`);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
package/cli/index.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import yargs from 'yargs';
|
|
3
|
+
import { hideBin } from 'yargs/helpers';
|
|
4
|
+
|
|
5
|
+
// Register commands
|
|
6
|
+
const cli = yargs(hideBin(process.argv))
|
|
7
|
+
.scriptName('apigrip')
|
|
8
|
+
.usage('$0 <command> [options]')
|
|
9
|
+
.command('serve', 'Start the web UI server', (yargs) => {
|
|
10
|
+
return yargs
|
|
11
|
+
.option('port', { type: 'number', default: 3000, describe: 'Port to listen on' })
|
|
12
|
+
.option('host', { type: 'string', default: '127.0.0.1', describe: 'Host to bind to' })
|
|
13
|
+
.option('open', { type: 'boolean', default: false, describe: 'Open browser on start' })
|
|
14
|
+
.option('no-browse', { type: 'boolean', default: false, describe: 'Disable directory browsing and project switching' });
|
|
15
|
+
}, async (argv) => {
|
|
16
|
+
const { serveCommand } = await import('./commands/serve.js');
|
|
17
|
+
await serveCommand(argv);
|
|
18
|
+
})
|
|
19
|
+
.command('send <method> <path>', 'Send an API request', (yargs) => {
|
|
20
|
+
return yargs
|
|
21
|
+
.positional('method', { type: 'string', describe: 'HTTP method' })
|
|
22
|
+
.positional('path', { type: 'string', describe: 'API path' })
|
|
23
|
+
.option('d', { alias: 'data', type: 'string', describe: 'Request body' })
|
|
24
|
+
.option('e', { alias: 'env', type: 'string', describe: 'Environment name' })
|
|
25
|
+
.option('query', { type: 'array', describe: 'Query params (key=value)' })
|
|
26
|
+
.option('header', { type: 'array', describe: 'Headers (Name: value)' })
|
|
27
|
+
.option('pathParam', { type: 'array', describe: 'Path params (key=value)' })
|
|
28
|
+
.option('v', { alias: 'verbose', type: 'boolean', describe: 'Verbose output' })
|
|
29
|
+
.option('i', { alias: 'headers', type: 'boolean', describe: 'Include headers' })
|
|
30
|
+
.option('dry-run', { type: 'boolean', describe: 'Print request without sending' })
|
|
31
|
+
.option('curl', { type: 'boolean', describe: 'Print curl command only' })
|
|
32
|
+
.option('filter', { type: 'string', describe: 'jq filter for response body (e.g. .data[0].name)' })
|
|
33
|
+
.option('spec', { type: 'string', describe: 'Path to spec file' })
|
|
34
|
+
.option('project', { type: 'string', describe: 'Project directory' });
|
|
35
|
+
}, async (argv) => {
|
|
36
|
+
const { sendCommand } = await import('./commands/send.js');
|
|
37
|
+
await sendCommand(argv);
|
|
38
|
+
})
|
|
39
|
+
.command('curl <method> <path>', 'Generate curl command', (yargs) => {
|
|
40
|
+
return yargs
|
|
41
|
+
.positional('method', { type: 'string' })
|
|
42
|
+
.positional('path', { type: 'string' })
|
|
43
|
+
.option('d', { alias: 'data', type: 'string' })
|
|
44
|
+
.option('e', { alias: 'env', type: 'string' })
|
|
45
|
+
.option('query', { type: 'array' })
|
|
46
|
+
.option('header', { type: 'array' })
|
|
47
|
+
.option('pathParam', { type: 'array', describe: 'Path params (key=value)' })
|
|
48
|
+
.option('spec', { type: 'string' })
|
|
49
|
+
.option('project', { type: 'string' });
|
|
50
|
+
}, async (argv) => {
|
|
51
|
+
const { curlCommand } = await import('./commands/curl.js');
|
|
52
|
+
await curlCommand(argv);
|
|
53
|
+
})
|
|
54
|
+
.command('list', 'List endpoints from spec', (yargs) => {
|
|
55
|
+
return yargs
|
|
56
|
+
.option('tag', { type: 'string', describe: 'Filter by tag' })
|
|
57
|
+
.option('search', { type: 'string', describe: 'Search endpoints' })
|
|
58
|
+
.option('json', { type: 'boolean', describe: 'JSON output' })
|
|
59
|
+
.option('spec', { type: 'string' })
|
|
60
|
+
.option('project', { type: 'string' });
|
|
61
|
+
}, async (argv) => {
|
|
62
|
+
const { listCommand } = await import('./commands/list.js');
|
|
63
|
+
await listCommand(argv);
|
|
64
|
+
})
|
|
65
|
+
.command('projects [action] [dir]', 'Manage project bookmarks', (yargs) => {
|
|
66
|
+
return yargs
|
|
67
|
+
.positional('action', { choices: ['add', 'remove'], describe: 'Action' })
|
|
68
|
+
.positional('dir', { type: 'string', describe: 'Directory path' });
|
|
69
|
+
}, async (argv) => {
|
|
70
|
+
const { projectsCommand } = await import('./commands/projects.js');
|
|
71
|
+
await projectsCommand(argv);
|
|
72
|
+
})
|
|
73
|
+
.command('last [method] [path]', 'Show last cached response for an endpoint', (yargs) => {
|
|
74
|
+
return yargs
|
|
75
|
+
.positional('method', { type: 'string', describe: 'HTTP method' })
|
|
76
|
+
.positional('path', { type: 'string', describe: 'API path' })
|
|
77
|
+
.option('json', { type: 'boolean', describe: 'Full JSON output' })
|
|
78
|
+
.option('v', { alias: 'verbose', type: 'boolean', describe: 'Verbose output' })
|
|
79
|
+
.option('i', { alias: 'headers', type: 'boolean', describe: 'Include headers' })
|
|
80
|
+
.option('project', { type: 'string', describe: 'Project directory' })
|
|
81
|
+
.option('spec', { type: 'string', describe: 'Path to spec file' });
|
|
82
|
+
}, async (argv) => {
|
|
83
|
+
const { lastCommand } = await import('./commands/last.js');
|
|
84
|
+
await lastCommand(argv);
|
|
85
|
+
})
|
|
86
|
+
.command('env [action] [name] [key] [value]', 'Manage environments', (yargs) => {
|
|
87
|
+
return yargs
|
|
88
|
+
.positional('action', { choices: ['show', 'set', 'delete', 'import'], describe: 'Action' })
|
|
89
|
+
.positional('name', { type: 'string', describe: 'Environment name or file path (for import)' })
|
|
90
|
+
.positional('key', { type: 'string', describe: 'Variable key' })
|
|
91
|
+
.positional('value', { type: 'string', describe: 'Variable value' })
|
|
92
|
+
.option('project', { type: 'string', describe: 'Project directory' })
|
|
93
|
+
.option('import-name', { type: 'string', describe: 'Target environment name for import (default: auto-detect)' })
|
|
94
|
+
.option('merge', { type: 'boolean', default: false, describe: 'Merge with existing values instead of replacing' });
|
|
95
|
+
}, async (argv) => {
|
|
96
|
+
const { envCommand } = await import('./commands/env.js');
|
|
97
|
+
await envCommand(argv);
|
|
98
|
+
})
|
|
99
|
+
.command('mcp', 'Start MCP server', (yargs) => {
|
|
100
|
+
return yargs
|
|
101
|
+
.option('project', { type: 'string' })
|
|
102
|
+
.option('spec', { type: 'string' });
|
|
103
|
+
}, async (argv) => {
|
|
104
|
+
const { mcpCommand } = await import('./commands/mcp.js');
|
|
105
|
+
await mcpCommand(argv);
|
|
106
|
+
})
|
|
107
|
+
.demandCommand(1, 'You need at least one command')
|
|
108
|
+
.help()
|
|
109
|
+
.argv;
|
package/cli/output.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// cli/output.js — CLI output formatting functions
|
|
2
|
+
|
|
3
|
+
const RESET = '\x1b[0m';
|
|
4
|
+
|
|
5
|
+
const METHOD_COLORS = {
|
|
6
|
+
GET: '\x1b[32m', // green
|
|
7
|
+
POST: '\x1b[35m', // magenta
|
|
8
|
+
PUT: '\x1b[33m', // yellow
|
|
9
|
+
DELETE: '\x1b[31m', // red
|
|
10
|
+
PATCH: '\x1b[33m', // yellow
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Color an HTTP method string using ANSI codes.
|
|
15
|
+
* @param {string} method - HTTP method (e.g. "GET", "POST")
|
|
16
|
+
* @returns {string} ANSI-colored method string
|
|
17
|
+
*/
|
|
18
|
+
export function colorMethod(method) {
|
|
19
|
+
const upper = method.toUpperCase();
|
|
20
|
+
const color = METHOD_COLORS[upper] || '';
|
|
21
|
+
if (!color) return upper;
|
|
22
|
+
return `${color}${upper}${RESET}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Format rows into a simple padded text table.
|
|
27
|
+
* @param {string[][]} rows - Array of row arrays
|
|
28
|
+
* @param {string[]} [headers] - Optional header row
|
|
29
|
+
* @returns {string} Formatted table string
|
|
30
|
+
*/
|
|
31
|
+
export function formatTable(rows, headers) {
|
|
32
|
+
const allRows = headers ? [headers, ...rows] : rows;
|
|
33
|
+
if (allRows.length === 0) return '';
|
|
34
|
+
|
|
35
|
+
// Determine the number of columns
|
|
36
|
+
const numCols = Math.max(...allRows.map(r => r.length));
|
|
37
|
+
|
|
38
|
+
// Calculate max width for each column
|
|
39
|
+
const colWidths = [];
|
|
40
|
+
for (let col = 0; col < numCols; col++) {
|
|
41
|
+
let maxWidth = 0;
|
|
42
|
+
for (const row of allRows) {
|
|
43
|
+
const cell = row[col] || '';
|
|
44
|
+
// Strip ANSI codes for width calculation
|
|
45
|
+
const stripped = cell.replace(/\x1b\[[0-9;]*m/g, '');
|
|
46
|
+
if (stripped.length > maxWidth) {
|
|
47
|
+
maxWidth = stripped.length;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
colWidths.push(maxWidth);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Format each row
|
|
54
|
+
const lines = allRows.map(row => {
|
|
55
|
+
const cells = [];
|
|
56
|
+
for (let col = 0; col < numCols; col++) {
|
|
57
|
+
const cell = row[col] || '';
|
|
58
|
+
const stripped = cell.replace(/\x1b\[[0-9;]*m/g, '');
|
|
59
|
+
const padding = colWidths[col] - stripped.length;
|
|
60
|
+
// Right-pad each cell (except last column)
|
|
61
|
+
if (col < numCols - 1) {
|
|
62
|
+
cells.push(cell + ' '.repeat(padding));
|
|
63
|
+
} else {
|
|
64
|
+
cells.push(cell);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return cells.join(' ');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return lines.join('\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Format a list of endpoints grouped by tag.
|
|
75
|
+
* @param {Array<{method: string, path: string, summary: string, tags: string[]}>} endpoints
|
|
76
|
+
* @param {object} [options]
|
|
77
|
+
* @param {boolean} [options.json] - If true, return JSON string instead
|
|
78
|
+
* @returns {string} Formatted endpoint list
|
|
79
|
+
*/
|
|
80
|
+
export function formatEndpointList(endpoints, options = {}) {
|
|
81
|
+
if (options.json) {
|
|
82
|
+
return JSON.stringify(endpoints);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!endpoints || endpoints.length === 0) {
|
|
86
|
+
return 'No endpoints found.';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Group endpoints by tag
|
|
90
|
+
const groups = new Map();
|
|
91
|
+
for (const ep of endpoints) {
|
|
92
|
+
const tags = ep.tags && ep.tags.length > 0 ? ep.tags : ['untagged'];
|
|
93
|
+
for (const tag of tags) {
|
|
94
|
+
if (!groups.has(tag)) {
|
|
95
|
+
groups.set(tag, []);
|
|
96
|
+
}
|
|
97
|
+
groups.get(tag).push(ep);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Calculate max path width across all endpoints for consistent alignment
|
|
102
|
+
let maxPathLen = 0;
|
|
103
|
+
for (const ep of endpoints) {
|
|
104
|
+
if (ep.path.length > maxPathLen) maxPathLen = ep.path.length;
|
|
105
|
+
}
|
|
106
|
+
const pathPad = Math.max(maxPathLen + 2, 20);
|
|
107
|
+
|
|
108
|
+
const lines = [];
|
|
109
|
+
let first = true;
|
|
110
|
+
for (const [tag, eps] of groups) {
|
|
111
|
+
if (!first) lines.push('');
|
|
112
|
+
first = false;
|
|
113
|
+
lines.push(`[${tag}]`);
|
|
114
|
+
for (const ep of eps) {
|
|
115
|
+
// Pad the raw method to 8 chars for alignment, accounting for ANSI codes
|
|
116
|
+
const methodStr = colorMethod(ep.method);
|
|
117
|
+
const rawLen = ep.method.length;
|
|
118
|
+
const padded = methodStr + ' '.repeat(Math.max(0, 8 - rawLen));
|
|
119
|
+
const summary = ep.summary || '';
|
|
120
|
+
lines.push(` ${padded}${ep.path.padEnd(pathPad)}${summary}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return lines.join('\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Format a curl/HTTP response for terminal output.
|
|
129
|
+
* @param {object} response - Response object with status, statusText, headers, body, verbose
|
|
130
|
+
* @param {object} [options]
|
|
131
|
+
* @param {boolean} [options.verbose] - Show verbose output (like curl -v)
|
|
132
|
+
* @param {boolean} [options.headers] - Show status line + headers + body
|
|
133
|
+
* @returns {string} Formatted response output
|
|
134
|
+
*/
|
|
135
|
+
export function formatResponse(response, options = {}) {
|
|
136
|
+
if (options.verbose) {
|
|
137
|
+
// Full verbose output
|
|
138
|
+
const parts = [];
|
|
139
|
+
if (response.verbose) {
|
|
140
|
+
parts.push(response.verbose);
|
|
141
|
+
}
|
|
142
|
+
if (response.body) {
|
|
143
|
+
parts.push(response.body);
|
|
144
|
+
}
|
|
145
|
+
return parts.join('\n');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (options.headers) {
|
|
149
|
+
// Status line + headers + body
|
|
150
|
+
const parts = [];
|
|
151
|
+
const httpVersion = response.http_version || response.httpVersion || '1.1';
|
|
152
|
+
const statusText = response.status_text || response.statusText || '';
|
|
153
|
+
parts.push(`HTTP/${httpVersion} ${response.status} ${statusText}`);
|
|
154
|
+
if (response.headers) {
|
|
155
|
+
for (const [name, value] of Object.entries(response.headers)) {
|
|
156
|
+
parts.push(`${name}: ${value}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
parts.push('');
|
|
160
|
+
if (response.body) {
|
|
161
|
+
parts.push(response.body);
|
|
162
|
+
}
|
|
163
|
+
return parts.join('\n');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Default: body only
|
|
167
|
+
return response.body || '';
|
|
168
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// cli/resolve-project.js — Shared project context resolution for all CLI commands
|
|
2
|
+
//
|
|
3
|
+
// Resolution order (per spec):
|
|
4
|
+
// 1. If --project or --spec is passed, use that
|
|
5
|
+
// 2. If cwd is inside a bookmarked project, use it
|
|
6
|
+
// 3. If cwd contains an OpenAPI spec file, use it directly
|
|
7
|
+
// 4. Otherwise, return null specPath
|
|
8
|
+
|
|
9
|
+
import { discoverSpec } from '../core/spec-discovery.js';
|
|
10
|
+
import { findProjectForDir } from '../core/projects-store.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the project directory and spec path from CLI args.
|
|
14
|
+
* @param {object} argv - Parsed CLI arguments (may have .project, .spec)
|
|
15
|
+
* @returns {Promise<{ projectDir: string, specPath: string|null }>}
|
|
16
|
+
*/
|
|
17
|
+
export async function resolveProjectContext(argv) {
|
|
18
|
+
// 1. Explicit flags take priority
|
|
19
|
+
if (argv.spec) {
|
|
20
|
+
return { projectDir: argv.project || process.cwd(), specPath: argv.spec };
|
|
21
|
+
}
|
|
22
|
+
if (argv.project) {
|
|
23
|
+
const discovered = await discoverSpec(argv.project);
|
|
24
|
+
return { projectDir: argv.project, specPath: discovered?.specPath || null };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 2. Check if cwd is inside a bookmarked project
|
|
28
|
+
const cwd = process.cwd();
|
|
29
|
+
const bookmark = findProjectForDir(cwd);
|
|
30
|
+
if (bookmark) {
|
|
31
|
+
const specPath = bookmark.spec || (await discoverSpec(bookmark.path))?.specPath || null;
|
|
32
|
+
return { projectDir: bookmark.path, specPath };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 3. Try spec discovery in cwd
|
|
36
|
+
const discovered = await discoverSpec(cwd);
|
|
37
|
+
if (discovered?.specPath) {
|
|
38
|
+
return { projectDir: cwd, specPath: discovered.specPath };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 4. Nothing found
|
|
42
|
+
return { projectDir: cwd, specPath: null };
|
|
43
|
+
}
|