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
package/README.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Apigrip
|
|
2
|
+
|
|
3
|
+
A spec-first, read-only OpenAPI client for developers who generate their API specs from code.
|
|
4
|
+
|
|
5
|
+
The spec file on disk is the only source of truth. It is never imported, copied, or converted. When it changes (because you regenerated it, switched branches, or pulled), the tool reflects the changes immediately.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install
|
|
13
|
+
npm start -- --project /path/to/your/api
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Open `http://127.0.0.1:3000` in your browser.
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
|
|
20
|
+
- Node.js 20+
|
|
21
|
+
- curl 7.70+ (for `--write-out` JSON format)
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- Browse endpoints from any OpenAPI 2.0, 3.0.x, or 3.1.x spec
|
|
26
|
+
- Send requests via curl subprocess with full `curl -v` level detail
|
|
27
|
+
- Live reload on spec changes, git branch switches, and `$ref` dependency updates
|
|
28
|
+
- Parameter persistence across restarts (auto-saved per project, per endpoint)
|
|
29
|
+
- Named environments with `{{ variable }}` syntax resolved at send time
|
|
30
|
+
- Git status bar showing branch, commit, and dirty/clean state
|
|
31
|
+
- `Ctrl+P` command palette with fuzzy search across projects and endpoints
|
|
32
|
+
- Response schema validation against the spec
|
|
33
|
+
- Dracula dark theme by default, light mode available
|
|
34
|
+
- MCP server for AI coding assistants
|
|
35
|
+
|
|
36
|
+
## CLI
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Start the web UI
|
|
40
|
+
apigrip serve # default port 3000
|
|
41
|
+
apigrip serve --port 8080 --open # custom port, open browser
|
|
42
|
+
|
|
43
|
+
# Send requests from the terminal
|
|
44
|
+
apigrip send GET /users # basic request
|
|
45
|
+
apigrip send POST /users -d '{"name": "John"}' # with body
|
|
46
|
+
apigrip send GET /users/{id} --pathParam id=42 # path param
|
|
47
|
+
apigrip send GET /users -e Staging --verbose # use named environment
|
|
48
|
+
apigrip send GET /users --dry-run # print without sending
|
|
49
|
+
apigrip send GET /users --curl # print curl command only
|
|
50
|
+
|
|
51
|
+
# Generate curl command (shorthand for send --curl)
|
|
52
|
+
apigrip curl GET /users -e Production
|
|
53
|
+
|
|
54
|
+
# List endpoints
|
|
55
|
+
apigrip list # grouped by tag
|
|
56
|
+
apigrip list --search "auth" --json # fuzzy search, JSON output
|
|
57
|
+
|
|
58
|
+
# Manage projects
|
|
59
|
+
apigrip projects # list bookmarks
|
|
60
|
+
apigrip projects add ./my-api # bookmark a directory
|
|
61
|
+
apigrip projects remove my-api # remove bookmark
|
|
62
|
+
|
|
63
|
+
# Manage environments
|
|
64
|
+
apigrip env # list environments
|
|
65
|
+
apigrip env show Staging # show variables
|
|
66
|
+
apigrip env set Staging api_key "sk-123" # set a variable
|
|
67
|
+
apigrip env delete Staging api_key # delete a variable
|
|
68
|
+
apigrip env import envs.json # import from JSON file
|
|
69
|
+
apigrip env import staging.json --import-name Staging # into named env
|
|
70
|
+
|
|
71
|
+
# View last cached response
|
|
72
|
+
apigrip last # list all cached endpoints
|
|
73
|
+
apigrip last GET /users # show last response body
|
|
74
|
+
apigrip last GET /users --json # full JSON output
|
|
75
|
+
|
|
76
|
+
# MCP server (for AI assistants)
|
|
77
|
+
apigrip mcp --project /path/to/api
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Exit codes
|
|
81
|
+
|
|
82
|
+
| Code | Meaning |
|
|
83
|
+
|------|---------|
|
|
84
|
+
| 0 | Success (HTTP 4xx/5xx responses are still exit 0) |
|
|
85
|
+
| 1 | Network error (DNS, connection refused, timeout) |
|
|
86
|
+
| 2 | Spec parse error |
|
|
87
|
+
| 3 | Endpoint not found |
|
|
88
|
+
|
|
89
|
+
### Project context
|
|
90
|
+
|
|
91
|
+
CLI commands auto-detect the project from the current working directory:
|
|
92
|
+
|
|
93
|
+
1. If `--project` or `--spec` is passed, use that
|
|
94
|
+
2. If cwd is inside a bookmarked project, use it
|
|
95
|
+
3. If cwd contains an OpenAPI spec file, use it directly
|
|
96
|
+
4. Otherwise, error
|
|
97
|
+
|
|
98
|
+
## Web UI
|
|
99
|
+
|
|
100
|
+
Two-panel layout: request on the left, response on the right.
|
|
101
|
+
|
|
102
|
+
**Left panel** -- endpoint browser (tag view or path tree view), server selector, parameter inputs, request body editor, send button.
|
|
103
|
+
|
|
104
|
+
**Right panel** -- status badge, timing, response body (pretty-printed), headers, verbose `curl -v` output, copyable curl command.
|
|
105
|
+
|
|
106
|
+
**Keyboard shortcuts**: `Ctrl+P` (command palette), `Ctrl+O` (open project), `Ctrl+E` (environment editor), `Ctrl+Enter` (send request).
|
|
107
|
+
|
|
108
|
+
## Environments
|
|
109
|
+
|
|
110
|
+
Environments are named sets of key-value pairs stored per project in the config directory (never in your project).
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
Base Environment -> base_url=http://localhost:8080, api_version=v1
|
|
114
|
+
+ Staging -> base_url=https://staging.example.com, auth_token=sk-...
|
|
115
|
+
+ Production -> base_url=https://api.example.com, auth_token=pk-...
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Reference variables anywhere: server URL, parameters, headers, request body. They resolve at send time.
|
|
119
|
+
|
|
120
|
+
Reserved keys: `timeout` (integer, seconds -- default 30), `insecure` (boolean -- passes `--insecure` to curl).
|
|
121
|
+
|
|
122
|
+
## Library API
|
|
123
|
+
|
|
124
|
+
Use apigrip programmatically in your own scripts and tools:
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
// ESM
|
|
128
|
+
import { loadSpec, send, validateBody } from 'apigrip';
|
|
129
|
+
|
|
130
|
+
// CommonJS
|
|
131
|
+
const { loadSpec, send } = await require('apigrip');
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Quick examples
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
// Load and inspect a spec
|
|
138
|
+
const { spec, endpoints } = await loadSpec('./my-api/');
|
|
139
|
+
console.log(`${endpoints.length} endpoints found`);
|
|
140
|
+
|
|
141
|
+
// Send a request with full validation
|
|
142
|
+
const res = await send('./my-api/', 'GET', '/users', {
|
|
143
|
+
params: { query: { limit: '10' } },
|
|
144
|
+
env: 'staging',
|
|
145
|
+
projectDir: './my-api/',
|
|
146
|
+
});
|
|
147
|
+
console.log(res.status, res.validation);
|
|
148
|
+
|
|
149
|
+
// Retrieve last cached response
|
|
150
|
+
import { loadLastResponse } from 'apigrip';
|
|
151
|
+
const cached = loadLastResponse('./my-api/', 'GET', '/users');
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### All exports
|
|
155
|
+
|
|
156
|
+
| Category | Functions |
|
|
157
|
+
|----------|-----------|
|
|
158
|
+
| **Spec** | `discoverSpec`, `parseSpec`, `extractEndpoints`, `getEndpointDetails`, `getRefDeps` |
|
|
159
|
+
| **Request** | `buildUrl`, `buildCurlArgs`, `executeCurl`, `cancelRequest`, `checkCurl`, `shellQuote` |
|
|
160
|
+
| **Environment** | `loadEnvironments`, `saveEnvironments`, `resolveEnvironment`, `resolveVariables`, `resolveAllParams`, `getConfigDir`, `getProjectHash` |
|
|
161
|
+
| **Validation** | `validateBody`, `validateResponse` |
|
|
162
|
+
| **Storage** | `loadParams`, `saveParams`, `saveLastResponse`, `loadLastResponse`, `loadProjects`, `addProject`, `removeProject`, `loadPreferences`, `savePreferences`, `getGitInfo` |
|
|
163
|
+
| **Convenience** | `loadSpec`, `send` |
|
|
164
|
+
|
|
165
|
+
Direct core module access is also available: `import { parseSpec } from 'apigrip/core/spec-parser.js'`
|
|
166
|
+
|
|
167
|
+
## MCP server
|
|
168
|
+
|
|
169
|
+
Expose the spec to AI coding assistants via the Model Context Protocol:
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"mcpServers": {
|
|
174
|
+
"api": {
|
|
175
|
+
"command": "apigrip",
|
|
176
|
+
"args": ["mcp", "--project", "/path/to/api"]
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Tools:** `list_endpoints`, `get_endpoint`, `get_schema`, `validate_request_body`, `send_request`, `get_last_response`, `list_environments`, `get_environment`, `get_project_path`, `get_spec_path`
|
|
183
|
+
|
|
184
|
+
**Resources:** `spec://parsed`, `project://info`
|
|
185
|
+
|
|
186
|
+
## Project structure
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
apigrip/
|
|
190
|
+
├── cli/ CLI entry point and commands
|
|
191
|
+
│ ├── index.js Yargs command registration
|
|
192
|
+
│ ├── output.js CLI formatting (table, JSON, color)
|
|
193
|
+
│ ├── resolve-project.js Shared project context resolution
|
|
194
|
+
│ └── commands/ serve, send, curl, list, projects, env, last, mcp
|
|
195
|
+
├── core/ Shared logic (spec parsing, curl, environments, persistence)
|
|
196
|
+
│ ├── spec-discovery.js Find OpenAPI spec in a directory
|
|
197
|
+
│ ├── spec-parser.js Parse/dereference specs (2.0, 3.0, 3.1)
|
|
198
|
+
│ ├── curl-builder.js Build curl args array
|
|
199
|
+
│ ├── curl-executor.js Execute curl subprocess
|
|
200
|
+
│ ├── env-resolver.js Load/merge environments, resolve {{ variables }}
|
|
201
|
+
│ ├── git-info.js Git status via CLI
|
|
202
|
+
│ ├── params-store.js Per-endpoint parameter persistence
|
|
203
|
+
│ ├── response-store.js Per-endpoint response caching
|
|
204
|
+
│ ├── projects-store.js Project bookmark CRUD
|
|
205
|
+
│ ├── preferences-store.js Global + per-project preferences
|
|
206
|
+
│ └── schema-validator.js JSON Schema validation (ajv)
|
|
207
|
+
├── lib/ Programmatic library API
|
|
208
|
+
│ ├── index.js ESM entrypoint (re-exports + loadSpec, send)
|
|
209
|
+
│ └── index.cjs CJS compatibility wrapper
|
|
210
|
+
├── mcp/ MCP server wiring
|
|
211
|
+
│ └── server.js Tools + resources -> core modules
|
|
212
|
+
├── server/ Express server, routes, SSE, file watcher
|
|
213
|
+
│ ├── index.js Server factory, route mounting
|
|
214
|
+
│ ├── spec-watcher.js Chokidar watchers (spec, $ref deps, .git/)
|
|
215
|
+
│ └── routes/ REST endpoint handlers
|
|
216
|
+
├── client/ React frontend (Vite build)
|
|
217
|
+
│ ├── components/ TopBar, EndpointList, RequestPanel, ResponsePanel, ...
|
|
218
|
+
│ ├── lib/ api.js (HTTP client), sse.js (SSE client)
|
|
219
|
+
│ └── styles/ CSS variables (Dracula palette), layout, components
|
|
220
|
+
└── test/ Node.js built-in test runner
|
|
221
|
+
├── core/ Unit tests for all core modules
|
|
222
|
+
├── cli/ CLI command tests
|
|
223
|
+
├── server/ Route + SSE integration tests
|
|
224
|
+
├── mcp/ MCP server tests
|
|
225
|
+
└── fixtures/ Test specs (apigrip-openapi.yaml, petstore-2.0.json)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Config is stored at `$XDG_CONFIG_HOME/apigrip/` (or `~/.config/apigrip/`). Project directories are never written to.
|
|
229
|
+
|
|
230
|
+
## Testing
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
npm test
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Runs 282 tests across 17 test files using Node's built-in test runner (`node --test`). The test suite uses the tool's own OpenAPI spec as a fixture (self-referential testing).
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
ISC
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// cli/commands/curl.js — Generate curl command for an API endpoint
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Curl command handler. Delegates to send with --curl flag.
|
|
5
|
+
* @param {object} argv - Parsed CLI arguments
|
|
6
|
+
*/
|
|
7
|
+
export async function curlCommand(argv) {
|
|
8
|
+
argv.curl = true;
|
|
9
|
+
const { sendCommand } = await import('./send.js');
|
|
10
|
+
return sendCommand(argv);
|
|
11
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadEnvironments, saveEnvironments, resolveEnvironment } from '../../core/env-resolver.js';
|
|
4
|
+
import { resolveProjectContext } from '../resolve-project.js';
|
|
5
|
+
|
|
6
|
+
export async function envCommand(argv) {
|
|
7
|
+
const { projectDir } = await resolveProjectContext(argv);
|
|
8
|
+
const action = argv.action;
|
|
9
|
+
|
|
10
|
+
if (!action) {
|
|
11
|
+
// List environments
|
|
12
|
+
const data = loadEnvironments(projectDir);
|
|
13
|
+
const names = Object.keys(data.environments);
|
|
14
|
+
if (names.length === 0 && Object.keys(data.base).length === 0) {
|
|
15
|
+
console.log('No environments configured.');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
console.log(`Active: ${data.active || '(none)'}`);
|
|
19
|
+
console.log(`Base: ${JSON.stringify(data.base)}`);
|
|
20
|
+
for (const name of names) {
|
|
21
|
+
const marker = name === data.active ? ' *' : '';
|
|
22
|
+
console.log(`${name}${marker}: ${JSON.stringify(data.environments[name])}`);
|
|
23
|
+
}
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (action === 'show') {
|
|
28
|
+
const name = argv.name;
|
|
29
|
+
const data = loadEnvironments(projectDir);
|
|
30
|
+
if (!name) {
|
|
31
|
+
// Show resolved environment
|
|
32
|
+
const resolved = resolveEnvironment(data);
|
|
33
|
+
console.log(JSON.stringify(resolved, null, 2));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const env = data.environments[name];
|
|
37
|
+
if (!env) {
|
|
38
|
+
console.error(`Environment not found: ${name}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
console.log(JSON.stringify(env, null, 2));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (action === 'set') {
|
|
46
|
+
const { name, key, value } = argv;
|
|
47
|
+
if (!name || !key || value === undefined) {
|
|
48
|
+
console.error('Usage: apigrip env set <name> <key> <value>');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
const data = loadEnvironments(projectDir);
|
|
52
|
+
if (name === 'base') {
|
|
53
|
+
data.base[key] = value;
|
|
54
|
+
} else {
|
|
55
|
+
if (!data.environments[name]) {
|
|
56
|
+
data.environments[name] = {};
|
|
57
|
+
}
|
|
58
|
+
data.environments[name][key] = value;
|
|
59
|
+
}
|
|
60
|
+
saveEnvironments(projectDir, data);
|
|
61
|
+
console.log(`Set ${name}.${key} = ${value}`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (action === 'import') {
|
|
66
|
+
const file = argv.name; // first positional after action
|
|
67
|
+
if (!file) {
|
|
68
|
+
console.error('Usage: apigrip env import <file> [--name <env>] [--merge]');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const filePath = path.resolve(file);
|
|
73
|
+
if (!fs.existsSync(filePath)) {
|
|
74
|
+
console.error(`File not found: ${filePath}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let json;
|
|
79
|
+
try {
|
|
80
|
+
json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error(`Invalid JSON: ${err.message}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (typeof json !== 'object' || json === null || Array.isArray(json)) {
|
|
87
|
+
console.error('JSON must be an object');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const data = loadEnvironments(projectDir);
|
|
92
|
+
const targetName = argv.importName;
|
|
93
|
+
const merge = argv.merge || false;
|
|
94
|
+
|
|
95
|
+
if (targetName) {
|
|
96
|
+
// Import as a named environment (or base)
|
|
97
|
+
if (targetName === 'base') {
|
|
98
|
+
data.base = merge ? { ...data.base, ...json } : json;
|
|
99
|
+
saveEnvironments(projectDir, data);
|
|
100
|
+
console.log(`Imported ${Object.keys(json).length} variable(s) into base`);
|
|
101
|
+
} else {
|
|
102
|
+
const existing = data.environments[targetName] || {};
|
|
103
|
+
data.environments[targetName] = merge ? { ...existing, ...json } : json;
|
|
104
|
+
saveEnvironments(projectDir, data);
|
|
105
|
+
console.log(`Imported ${Object.keys(json).length} variable(s) into ${targetName}`);
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
// Auto-detect: full config vs flat key-value
|
|
109
|
+
const isFullConfig = json.base !== undefined || json.environments !== undefined;
|
|
110
|
+
if (isFullConfig) {
|
|
111
|
+
if (merge) {
|
|
112
|
+
if (json.base) data.base = { ...data.base, ...json.base };
|
|
113
|
+
if (json.environments) {
|
|
114
|
+
for (const [name, vars] of Object.entries(json.environments)) {
|
|
115
|
+
data.environments[name] = { ...(data.environments[name] || {}), ...vars };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (json.active !== undefined) data.active = json.active;
|
|
119
|
+
} else {
|
|
120
|
+
if (json.base) data.base = json.base;
|
|
121
|
+
if (json.environments) data.environments = json.environments;
|
|
122
|
+
if (json.active !== undefined) data.active = json.active;
|
|
123
|
+
}
|
|
124
|
+
saveEnvironments(projectDir, data);
|
|
125
|
+
const envCount = json.environments ? Object.keys(json.environments).length : 0;
|
|
126
|
+
console.log(`Imported full config (${envCount} environment(s))`);
|
|
127
|
+
} else {
|
|
128
|
+
// Flat key-value: import into base
|
|
129
|
+
data.base = merge ? { ...data.base, ...json } : json;
|
|
130
|
+
saveEnvironments(projectDir, data);
|
|
131
|
+
console.log(`Imported ${Object.keys(json).length} variable(s) into base`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (action === 'delete') {
|
|
138
|
+
const { name, key } = argv;
|
|
139
|
+
if (!name) {
|
|
140
|
+
console.error('Usage: apigrip env delete <name> [key]');
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
const data = loadEnvironments(projectDir);
|
|
144
|
+
if (!key) {
|
|
145
|
+
// Delete entire environment
|
|
146
|
+
if (name === 'base') {
|
|
147
|
+
console.error('Cannot delete base environment');
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
if (!data.environments[name]) {
|
|
151
|
+
console.error(`Environment not found: ${name}`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
delete data.environments[name];
|
|
155
|
+
if (data.active === name) {
|
|
156
|
+
data.active = null;
|
|
157
|
+
}
|
|
158
|
+
saveEnvironments(projectDir, data);
|
|
159
|
+
console.log(`Deleted environment: ${name}`);
|
|
160
|
+
} else {
|
|
161
|
+
// Delete a key
|
|
162
|
+
if (name === 'base') {
|
|
163
|
+
delete data.base[key];
|
|
164
|
+
} else if (data.environments[name]) {
|
|
165
|
+
delete data.environments[name][key];
|
|
166
|
+
} else {
|
|
167
|
+
console.error(`Environment not found: ${name}`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
saveEnvironments(projectDir, data);
|
|
171
|
+
console.log(`Deleted ${name}.${key}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// cli/commands/last.js — Show the last cached response for an endpoint
|
|
2
|
+
|
|
3
|
+
import { loadLastResponse, loadAllResponses } from '../../core/response-store.js';
|
|
4
|
+
import { formatResponse } from '../output.js';
|
|
5
|
+
import { resolveProjectContext } from '../resolve-project.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Last command handler.
|
|
9
|
+
* @param {object} argv - Parsed CLI arguments
|
|
10
|
+
*/
|
|
11
|
+
export async function lastCommand(argv) {
|
|
12
|
+
const { projectDir } = await resolveProjectContext(argv);
|
|
13
|
+
|
|
14
|
+
// No method/path: list all cached endpoints
|
|
15
|
+
if (!argv.method) {
|
|
16
|
+
const all = loadAllResponses(projectDir);
|
|
17
|
+
const keys = Object.keys(all);
|
|
18
|
+
if (keys.length === 0) {
|
|
19
|
+
console.log('No cached responses.');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
for (const key of keys) {
|
|
23
|
+
const r = all[key];
|
|
24
|
+
const age = r.saved_at ? timeSince(r.saved_at) : 'unknown';
|
|
25
|
+
console.log(`${key} ${r.status} ${age}`);
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const method = argv.method.toUpperCase();
|
|
31
|
+
const pathArg = argv.path;
|
|
32
|
+
if (!pathArg) {
|
|
33
|
+
console.error('Usage: apigrip last <method> <path>');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
const apiPath = pathArg.startsWith('/') ? pathArg : '/' + pathArg;
|
|
37
|
+
|
|
38
|
+
const cached = loadLastResponse(projectDir, method, apiPath);
|
|
39
|
+
if (!cached) {
|
|
40
|
+
console.error(`No cached response for ${method} ${apiPath}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (argv.json) {
|
|
45
|
+
console.log(JSON.stringify(cached, null, 2));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Reuse the same formatter as send
|
|
50
|
+
console.log(formatResponse(cached, { verbose: argv.verbose, headers: argv.headers }));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function timeSince(isoString) {
|
|
54
|
+
const diff = Date.now() - new Date(isoString).getTime();
|
|
55
|
+
const seconds = Math.floor(diff / 1000);
|
|
56
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
57
|
+
const minutes = Math.floor(seconds / 60);
|
|
58
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
59
|
+
const hours = Math.floor(minutes / 60);
|
|
60
|
+
if (hours < 24) return `${hours}h ago`;
|
|
61
|
+
const days = Math.floor(hours / 24);
|
|
62
|
+
return `${days}d ago`;
|
|
63
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// cli/commands/list.js — List endpoints from an OpenAPI spec
|
|
2
|
+
|
|
3
|
+
import { parseSpec, extractEndpoints } from '../../core/spec-parser.js';
|
|
4
|
+
import { formatEndpointList } from '../output.js';
|
|
5
|
+
import { resolveProjectContext } from '../resolve-project.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* List command handler.
|
|
9
|
+
* @param {object} argv - Parsed CLI arguments
|
|
10
|
+
*/
|
|
11
|
+
export async function listCommand(argv) {
|
|
12
|
+
const { specPath } = await resolveProjectContext(argv);
|
|
13
|
+
if (!specPath) {
|
|
14
|
+
console.error('No spec file found');
|
|
15
|
+
process.exit(2);
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const spec = await parseSpec(specPath);
|
|
19
|
+
let endpoints = extractEndpoints(spec);
|
|
20
|
+
if (argv.tag) {
|
|
21
|
+
endpoints = endpoints.filter(e => e.tags.includes(argv.tag));
|
|
22
|
+
}
|
|
23
|
+
if (argv.search) {
|
|
24
|
+
const search = argv.search.toLowerCase();
|
|
25
|
+
endpoints = endpoints.filter(e =>
|
|
26
|
+
e.path.toLowerCase().includes(search) ||
|
|
27
|
+
(e.summary || '').toLowerCase().includes(search)
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
console.log(formatEndpointList(endpoints, { json: argv.json }));
|
|
31
|
+
} catch (err) {
|
|
32
|
+
console.error(`Spec parse error: ${err.message}`);
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { parseSpec } from '../../core/spec-parser.js';
|
|
2
|
+
import { createMcpServer } from '../../mcp/server.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { resolveProjectContext } from '../resolve-project.js';
|
|
5
|
+
|
|
6
|
+
export async function mcpCommand(argv) {
|
|
7
|
+
const { projectDir, specPath } = await resolveProjectContext(argv);
|
|
8
|
+
|
|
9
|
+
if (!specPath) {
|
|
10
|
+
console.error('No spec file found');
|
|
11
|
+
process.exit(2);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let spec;
|
|
15
|
+
try {
|
|
16
|
+
spec = await parseSpec(specPath);
|
|
17
|
+
} catch (err) {
|
|
18
|
+
console.error(`Spec parse error: ${err.message}`);
|
|
19
|
+
process.exit(2);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const server = await createMcpServer({ projectDir, specPath, spec });
|
|
23
|
+
const transport = new StdioServerTransport();
|
|
24
|
+
await server.connect(transport);
|
|
25
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { loadProjects, addProject, removeProject } from '../../core/projects-store.js';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export async function projectsCommand(argv) {
|
|
5
|
+
const action = argv.action;
|
|
6
|
+
|
|
7
|
+
if (!action) {
|
|
8
|
+
// List projects
|
|
9
|
+
const projects = loadProjects();
|
|
10
|
+
if (projects.length === 0) {
|
|
11
|
+
console.log('No bookmarked projects.');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
for (const p of projects) {
|
|
15
|
+
console.log(`${p.name} ${p.path}${p.spec ? ' (' + p.spec + ')' : ''}`);
|
|
16
|
+
}
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (action === 'add') {
|
|
21
|
+
const dir = argv.dir ? path.resolve(argv.dir) : process.cwd();
|
|
22
|
+
try {
|
|
23
|
+
const project = addProject(dir);
|
|
24
|
+
console.log(`Added: ${project.name} (${project.path})`);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.error(err.message);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (action === 'remove') {
|
|
33
|
+
if (!argv.dir) {
|
|
34
|
+
console.error('Usage: apigrip projects remove <name-or-path>');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const removed = removeProject(argv.dir);
|
|
39
|
+
if (removed) {
|
|
40
|
+
console.log(`Removed: ${argv.dir}`);
|
|
41
|
+
} else {
|
|
42
|
+
console.error(`Not found: ${argv.dir}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error(err.message);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|