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.
Files changed (41) hide show
  1. package/README.md +240 -0
  2. package/cli/commands/curl.js +11 -0
  3. package/cli/commands/env.js +174 -0
  4. package/cli/commands/last.js +63 -0
  5. package/cli/commands/list.js +35 -0
  6. package/cli/commands/mcp.js +25 -0
  7. package/cli/commands/projects.js +50 -0
  8. package/cli/commands/send.js +189 -0
  9. package/cli/commands/serve.js +46 -0
  10. package/cli/index.js +109 -0
  11. package/cli/output.js +168 -0
  12. package/cli/resolve-project.js +43 -0
  13. package/client/dist/assets/index-CtHBIuEv.js +75 -0
  14. package/client/dist/assets/index-kzeRjfI8.css +1 -0
  15. package/client/dist/index.html +19 -0
  16. package/core/curl-builder.js +218 -0
  17. package/core/curl-executor.js +370 -0
  18. package/core/env-resolver.js +244 -0
  19. package/core/git-info.js +41 -0
  20. package/core/params-store.js +94 -0
  21. package/core/preferences-store.js +150 -0
  22. package/core/projects-store.js +173 -0
  23. package/core/response-store.js +121 -0
  24. package/core/schema-validator.js +196 -0
  25. package/core/spec-discovery.js +109 -0
  26. package/core/spec-parser.js +172 -0
  27. package/lib/index.cjs +16 -0
  28. package/lib/index.js +294 -0
  29. package/mcp/server.js +257 -0
  30. package/package.json +70 -0
  31. package/server/index.js +53 -0
  32. package/server/routes/browse.js +61 -0
  33. package/server/routes/environments.js +92 -0
  34. package/server/routes/events.js +40 -0
  35. package/server/routes/params.js +38 -0
  36. package/server/routes/preferences.js +27 -0
  37. package/server/routes/project.js +94 -0
  38. package/server/routes/projects.js +51 -0
  39. package/server/routes/requests.js +192 -0
  40. package/server/routes/spec.js +92 -0
  41. 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
+ ![Apigrip demo](docs/apigrip-demo.gif)
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
+ }