@eve-horizon/cli 0.0.1
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 +275 -0
- package/bin/eve.js +2 -0
- package/dist/commands/api.js +350 -0
- package/dist/commands/auth.js +99 -0
- package/dist/commands/db.js +149 -0
- package/dist/commands/env.js +303 -0
- package/dist/commands/event.js +273 -0
- package/dist/commands/harness.js +96 -0
- package/dist/commands/job.js +2266 -0
- package/dist/commands/org.js +97 -0
- package/dist/commands/pipeline.js +403 -0
- package/dist/commands/profile.js +103 -0
- package/dist/commands/project.js +185 -0
- package/dist/commands/secrets.js +147 -0
- package/dist/commands/system.js +457 -0
- package/dist/commands/workflow.js +337 -0
- package/dist/index.js +97 -0
- package/dist/lib/args.js +57 -0
- package/dist/lib/client.js +116 -0
- package/dist/lib/config.js +76 -0
- package/dist/lib/context.js +45 -0
- package/dist/lib/help.js +938 -0
- package/dist/lib/logs.js +51 -0
- package/dist/lib/output.js +14 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# Eve Horizon CLI
|
|
2
|
+
|
|
3
|
+
Published CLI (npx) for interacting with the Eve Horizon API. Dev/ops tooling lives in `./bin/eh`.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx @eve/cli --help
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Philosophy
|
|
10
|
+
|
|
11
|
+
The CLI is the **primary interface** for Eve Horizon, designed for humans and AI agents. The REST API is the substrate: every operation is exposed via HTTP, and the CLI is a thin wrapper that handles argument parsing and output formatting. It does not bypass the API or contain business logic.
|
|
12
|
+
|
|
13
|
+
See [API Philosophy](../../docs/system/api-philosophy.md) and [OpenAPI](../../docs/system/openapi.md).
|
|
14
|
+
|
|
15
|
+
## Profiles (defaults + credentials)
|
|
16
|
+
|
|
17
|
+
Profiles store API URL, default org/project IDs, default harness, and Supabase auth config.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Create or update a profile
|
|
21
|
+
eve profile set local \
|
|
22
|
+
--api-url http://localhost:4701 \
|
|
23
|
+
--org org_defaulttestorg \
|
|
24
|
+
--project proj_xxx
|
|
25
|
+
|
|
26
|
+
# Set default harness (with optional variant)
|
|
27
|
+
eve profile set --harness mclaude
|
|
28
|
+
eve profile set --harness mclaude:fast # harness with variant
|
|
29
|
+
|
|
30
|
+
# Set active profile
|
|
31
|
+
eve profile use local
|
|
32
|
+
|
|
33
|
+
# Show active profile
|
|
34
|
+
eve profile show
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Profile harness defaults are used when scheduling jobs.
|
|
38
|
+
The `--harness` flag accepts `harness` or `harness:variant` format everywhere.
|
|
39
|
+
Priority: `--harness` flag > job hints > profile default > system default.
|
|
40
|
+
|
|
41
|
+
### Supabase config (cloud auth)
|
|
42
|
+
|
|
43
|
+
Store Supabase settings in a profile (recommended for cloud stacks):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
eve profile set prod \
|
|
47
|
+
--api-url https://api.eve-horizon.example.com \
|
|
48
|
+
--supabase-url https://your-project.supabase.co \
|
|
49
|
+
--supabase-anon-key <anon-key>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Auth
|
|
53
|
+
|
|
54
|
+
Auth is **optional** per stack. When the server has `EVE_AUTH_ENABLED=false`, login is not required.
|
|
55
|
+
Supabase settings can be stored in a profile or provided via `EVE_SUPABASE_URL` and
|
|
56
|
+
`EVE_SUPABASE_ANON_KEY`.
|
|
57
|
+
|
|
58
|
+
The CLI will refresh access tokens automatically if a refresh token is available.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Login (cloud)
|
|
62
|
+
eve auth login --email you@example.com --password '...' \
|
|
63
|
+
--profile prod
|
|
64
|
+
|
|
65
|
+
# Status / whoami
|
|
66
|
+
eve auth status
|
|
67
|
+
eve auth whoami
|
|
68
|
+
|
|
69
|
+
# Logout
|
|
70
|
+
eve auth logout
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Commands
|
|
74
|
+
|
|
75
|
+
### Organizations
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
eve org ensure "My Org"
|
|
79
|
+
eve org list
|
|
80
|
+
eve org get org_xxx
|
|
81
|
+
eve org update org_xxx --name "New Name"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Projects
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
eve project ensure --name my-project --slug MyProj --repo-url https://github.com/org/repo
|
|
88
|
+
eve project list
|
|
89
|
+
eve project get proj_xxx
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Jobs
|
|
93
|
+
|
|
94
|
+
Jobs follow a phase-based lifecycle: `idea` → `backlog` → `ready` → `active` → `review` → `done`
|
|
95
|
+
|
|
96
|
+
Jobs created without a `--phase` flag default to `ready`, making them immediately schedulable.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Create a job (defaults to ready phase)
|
|
100
|
+
eve job create --description "Fix the login bug in auth.ts"
|
|
101
|
+
eve job create --description "Add dark mode" --priority 1 --harness mclaude
|
|
102
|
+
|
|
103
|
+
# List and filter jobs
|
|
104
|
+
eve job list --phase ready
|
|
105
|
+
eve job ready # Schedulable jobs (ready, not blocked)
|
|
106
|
+
eve job blocked # Jobs waiting on dependencies
|
|
107
|
+
|
|
108
|
+
# View job details
|
|
109
|
+
eve job show MyProj-abc123
|
|
110
|
+
eve job tree MyProj-abc123 # Hierarchical view
|
|
111
|
+
|
|
112
|
+
# Update jobs
|
|
113
|
+
eve job update MyProj-abc123 --phase active --assignee agent-1
|
|
114
|
+
eve job close MyProj-abc123 --reason "Work completed"
|
|
115
|
+
eve job cancel MyProj-abc123 --reason "No longer needed"
|
|
116
|
+
|
|
117
|
+
# Dependencies
|
|
118
|
+
eve job dep add MyProj-abc123 MyProj-def456 # abc123 depends on def456
|
|
119
|
+
eve job dep list MyProj-abc123
|
|
120
|
+
|
|
121
|
+
# Claim/release workflow (typically used by scheduler/agents)
|
|
122
|
+
eve job claim MyProj-abc123 --harness mclaude
|
|
123
|
+
eve job release MyProj-abc123 --reason "Need more info"
|
|
124
|
+
eve job attempts MyProj-abc123
|
|
125
|
+
|
|
126
|
+
# Job execution and monitoring
|
|
127
|
+
eve job logs MyProj-abc123
|
|
128
|
+
eve job result MyProj-abc123 --format full # Get job results (text|json|full)
|
|
129
|
+
eve job result MyProj-abc123 --attempt 2 # Get results from specific attempt
|
|
130
|
+
eve job wait MyProj-abc123 --timeout 300 # Wait for job completion
|
|
131
|
+
eve job wait MyProj-abc123 --quiet --json # Wait quietly, JSON output
|
|
132
|
+
eve job follow MyProj-abc123 # Stream logs in real-time via SSE
|
|
133
|
+
eve job follow MyProj-abc123 --raw # Stream raw JSON lines
|
|
134
|
+
eve job follow MyProj-abc123 --no-result # Stream logs without final result
|
|
135
|
+
|
|
136
|
+
# Review workflow
|
|
137
|
+
eve job submit MyProj-abc123 --summary "Implemented fix"
|
|
138
|
+
eve job approve MyProj-abc123 --comment "LGTM"
|
|
139
|
+
eve job reject MyProj-abc123 --reason "Missing tests"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### Job Results
|
|
143
|
+
|
|
144
|
+
Fetch and display completed job results:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# Show full job result (default format)
|
|
148
|
+
eve job result MyProj-abc123
|
|
149
|
+
|
|
150
|
+
# Get results in different formats
|
|
151
|
+
eve job result MyProj-abc123 --format text # Plain text output only
|
|
152
|
+
eve job result MyProj-abc123 --format json # Full JSON structure
|
|
153
|
+
eve job result MyProj-abc123 --format full # Formatted with metadata (default)
|
|
154
|
+
|
|
155
|
+
# Get results from a specific attempt
|
|
156
|
+
eve job result MyProj-abc123 --attempt 2
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### Waiting for Job Completion
|
|
160
|
+
|
|
161
|
+
Block until a job completes, with optional timeout:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# Wait for job to complete (default timeout: 300s)
|
|
165
|
+
eve job wait MyProj-abc123
|
|
166
|
+
|
|
167
|
+
# Custom timeout (max 300s enforced by API)
|
|
168
|
+
eve job wait MyProj-abc123 --timeout 120
|
|
169
|
+
|
|
170
|
+
# Quiet mode (no progress output)
|
|
171
|
+
eve job wait MyProj-abc123 --quiet
|
|
172
|
+
|
|
173
|
+
# JSON output format
|
|
174
|
+
eve job wait MyProj-abc123 --json
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Exit codes:
|
|
178
|
+
- `0`: Job completed successfully
|
|
179
|
+
- `1`: Job failed
|
|
180
|
+
- `124`: Timeout reached
|
|
181
|
+
- `125`: Job was cancelled
|
|
182
|
+
|
|
183
|
+
#### Real-time Log Streaming
|
|
184
|
+
|
|
185
|
+
Stream job logs in real-time using Server-Sent Events (SSE):
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
# Stream logs with timestamps and formatted output
|
|
189
|
+
eve job follow MyProj-abc123
|
|
190
|
+
|
|
191
|
+
# Stream raw JSON lines (for parsing/filtering)
|
|
192
|
+
eve job follow MyProj-abc123 --raw
|
|
193
|
+
|
|
194
|
+
# Stream logs without printing final result
|
|
195
|
+
eve job follow MyProj-abc123 --no-result
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The `follow` command connects to the job's SSE endpoint and displays logs as they are generated. It shows:
|
|
199
|
+
- Timestamps for each log entry
|
|
200
|
+
- Tool names and actions
|
|
201
|
+
|
|
202
|
+
### Secrets
|
|
203
|
+
|
|
204
|
+
Secrets can be stored at user/org/project scope. Values are never returned in plaintext; `show` returns a masked value.
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
# Project secret
|
|
208
|
+
eve secrets set GITHUB_TOKEN ghp_xxx --project proj_xxx --type github_token
|
|
209
|
+
eve secrets list --project proj_xxx
|
|
210
|
+
eve secrets show GITHUB_TOKEN --project proj_xxx
|
|
211
|
+
eve secrets delete GITHUB_TOKEN --project proj_xxx
|
|
212
|
+
|
|
213
|
+
# Import host secrets from .env
|
|
214
|
+
eve secrets import --project proj_xxx --file .env
|
|
215
|
+
```
|
|
216
|
+
- Formatted, human-readable output
|
|
217
|
+
|
|
218
|
+
Use `--raw` for programmatic consumption or when piping to other tools.
|
|
219
|
+
|
|
220
|
+
#### Scheduling Hints
|
|
221
|
+
|
|
222
|
+
Jobs can include hints for the scheduler:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
eve job create --description "Heavy computation" \
|
|
226
|
+
--harness mclaude:fast \
|
|
227
|
+
--worker-type gpu \
|
|
228
|
+
--permission auto_edit \
|
|
229
|
+
--timeout 7200
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
#### Creating Sub-Jobs (for agents)
|
|
233
|
+
|
|
234
|
+
Agents can create and optionally claim sub-jobs inline:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
# Create sub-job under parent
|
|
238
|
+
eve job create --parent MyProj-abc123 --description "Implement feature X"
|
|
239
|
+
|
|
240
|
+
# Create and immediately claim (for inline execution)
|
|
241
|
+
eve job create --parent $EVE_JOB_ID --description "Sub-task" --claim
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Environment variables for agent context:
|
|
245
|
+
- `EVE_PROJECT_ID` - Current project
|
|
246
|
+
- `EVE_JOB_ID` - Current job being executed
|
|
247
|
+
- `EVE_ATTEMPT_ID` - Current attempt UUID
|
|
248
|
+
- `EVE_AGENT_ID` - Agent identifier
|
|
249
|
+
|
|
250
|
+
### Harnesses
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
eve harness list # List available harnesses
|
|
254
|
+
eve harness get mclaude # Show harness details and auth status
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Pagination
|
|
258
|
+
|
|
259
|
+
List endpoints accept `--limit` and `--offset`. Default limit is **10**, newest first.
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
eve job list --limit 10 --offset 20
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Dev CLI
|
|
266
|
+
|
|
267
|
+
Local dev/ops tooling:
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
./bin/eh --help
|
|
271
|
+
./bin/eh dev start
|
|
272
|
+
./bin/eh k8s start # Default runtime
|
|
273
|
+
./bin/eh docker start # Quick dev loop
|
|
274
|
+
./bin/eh db migrate
|
|
275
|
+
```
|
package/bin/eve.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleApi = handleApi;
|
|
4
|
+
exports.handleSpec = handleSpec;
|
|
5
|
+
exports.fetchApiSpec = fetchApiSpec;
|
|
6
|
+
const args_1 = require("../lib/args");
|
|
7
|
+
const client_1 = require("../lib/client");
|
|
8
|
+
const output_1 = require("../lib/output");
|
|
9
|
+
const node_fs_1 = require("node:fs");
|
|
10
|
+
const node_path_1 = require("node:path");
|
|
11
|
+
const node_child_process_1 = require("node:child_process");
|
|
12
|
+
async function handleApi(subcommand, positionals, flags, context) {
|
|
13
|
+
const jsonOutput = Boolean(flags.json);
|
|
14
|
+
switch (subcommand) {
|
|
15
|
+
case 'list':
|
|
16
|
+
return handleList(positionals, flags, context, jsonOutput);
|
|
17
|
+
case 'show':
|
|
18
|
+
return handleShow(positionals, flags, context, jsonOutput);
|
|
19
|
+
case 'spec':
|
|
20
|
+
return handleSpec(positionals, flags, context, jsonOutput);
|
|
21
|
+
case 'refresh':
|
|
22
|
+
return handleRefresh(positionals, flags, context, jsonOutput);
|
|
23
|
+
case 'examples':
|
|
24
|
+
return handleExamples(positionals, flags, context, jsonOutput);
|
|
25
|
+
case 'call':
|
|
26
|
+
return handleCall(positionals, flags, context);
|
|
27
|
+
default:
|
|
28
|
+
throw new Error('Usage: eve api <list|show|spec|refresh|examples|call>\n' +
|
|
29
|
+
' list [project] - list API sources\n' +
|
|
30
|
+
' show <name> [project] - show a single API source\n' +
|
|
31
|
+
' spec <name> [project] - show cached API spec\n' +
|
|
32
|
+
' refresh <name> [project] - refresh cached API spec\n' +
|
|
33
|
+
' examples <name> [project] - print curl examples from spec\n' +
|
|
34
|
+
' call <name> <method> <path> [options] - call an API with Eve auth');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function handleList(positionals, flags, context, jsonOutput) {
|
|
38
|
+
const projectId = positionals[0] ?? (0, args_1.getStringFlag)(flags, ['project']) ?? context.projectId;
|
|
39
|
+
if (!projectId) {
|
|
40
|
+
throw new Error('Usage: eve api list [project] [--project=<id>] [--env=<name>]');
|
|
41
|
+
}
|
|
42
|
+
const query = buildQuery({
|
|
43
|
+
env: (0, args_1.getStringFlag)(flags, ['env']),
|
|
44
|
+
});
|
|
45
|
+
const response = await (0, client_1.requestJson)(context, `/projects/${projectId}/apis${query}`);
|
|
46
|
+
if (jsonOutput) {
|
|
47
|
+
(0, output_1.outputJson)(response, jsonOutput);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!response.data || response.data.length === 0) {
|
|
51
|
+
console.log('No API sources found.');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
formatApiSourcesTable(response.data);
|
|
55
|
+
}
|
|
56
|
+
async function handleShow(positionals, flags, context, jsonOutput) {
|
|
57
|
+
const name = positionals[0] ?? (0, args_1.getStringFlag)(flags, ['name']);
|
|
58
|
+
const projectId = positionals[1] ?? (0, args_1.getStringFlag)(flags, ['project']) ?? context.projectId;
|
|
59
|
+
const env = (0, args_1.getStringFlag)(flags, ['env']);
|
|
60
|
+
if (!name || !projectId) {
|
|
61
|
+
throw new Error('Usage: eve api show <name> [project] [--project=<id>] [--env=<name>]');
|
|
62
|
+
}
|
|
63
|
+
const query = buildQuery({ env });
|
|
64
|
+
const response = await (0, client_1.requestJson)(context, `/projects/${projectId}/apis/${name}${query}`);
|
|
65
|
+
(0, output_1.outputJson)(response, jsonOutput);
|
|
66
|
+
}
|
|
67
|
+
async function handleSpec(positionals, flags, context, jsonOutput) {
|
|
68
|
+
const name = positionals[0] ?? (0, args_1.getStringFlag)(flags, ['name']);
|
|
69
|
+
const projectId = positionals[1] ?? (0, args_1.getStringFlag)(flags, ['project']) ?? context.projectId;
|
|
70
|
+
const env = (0, args_1.getStringFlag)(flags, ['env']);
|
|
71
|
+
if (!name || !projectId) {
|
|
72
|
+
throw new Error('Usage: eve api spec <name> [project] [--project=<id>] [--env=<name>]');
|
|
73
|
+
}
|
|
74
|
+
const spec = await fetchApiSpec(context, projectId, name, env);
|
|
75
|
+
if (typeof spec.data === 'string') {
|
|
76
|
+
(0, output_1.outputJson)(spec.data, jsonOutput, spec.raw);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
(0, output_1.outputJson)(spec.data, jsonOutput);
|
|
80
|
+
}
|
|
81
|
+
async function handleRefresh(positionals, flags, context, jsonOutput) {
|
|
82
|
+
const name = positionals[0] ?? (0, args_1.getStringFlag)(flags, ['name']);
|
|
83
|
+
const projectId = positionals[1] ?? (0, args_1.getStringFlag)(flags, ['project']) ?? context.projectId;
|
|
84
|
+
const env = (0, args_1.getStringFlag)(flags, ['env']);
|
|
85
|
+
if (!name || !projectId) {
|
|
86
|
+
throw new Error('Usage: eve api refresh <name> [project] [--project=<id>] [--env=<name>]');
|
|
87
|
+
}
|
|
88
|
+
const query = buildQuery({ env });
|
|
89
|
+
const response = await (0, client_1.requestJson)(context, `/projects/${projectId}/apis/${name}/refresh${query}`, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
});
|
|
92
|
+
(0, output_1.outputJson)(response, jsonOutput);
|
|
93
|
+
}
|
|
94
|
+
async function handleExamples(positionals, flags, context, jsonOutput) {
|
|
95
|
+
const name = positionals[0] ?? (0, args_1.getStringFlag)(flags, ['name']);
|
|
96
|
+
const projectId = positionals[1] ?? (0, args_1.getStringFlag)(flags, ['project']) ?? context.projectId;
|
|
97
|
+
const env = (0, args_1.getStringFlag)(flags, ['env']);
|
|
98
|
+
if (!name || !projectId) {
|
|
99
|
+
throw new Error('Usage: eve api examples <name> [project] [--project=<id>] [--env=<name>]');
|
|
100
|
+
}
|
|
101
|
+
const query = buildQuery({ env });
|
|
102
|
+
const api = await (0, client_1.requestJson)(context, `/projects/${projectId}/apis/${name}${query}`);
|
|
103
|
+
const spec = await fetchApiSpec(context, projectId, name, env);
|
|
104
|
+
if (jsonOutput) {
|
|
105
|
+
(0, output_1.outputJson)({ api, spec: spec.data }, jsonOutput);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!spec.data || typeof spec.data !== 'object') {
|
|
109
|
+
throw new Error('API spec is not JSON; examples require a JSON OpenAPI spec.');
|
|
110
|
+
}
|
|
111
|
+
const examples = buildCurlExamples(api, spec.data);
|
|
112
|
+
if (examples.length === 0) {
|
|
113
|
+
console.log('No endpoints found in spec.');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
console.log(`API examples for ${api.name} (${api.type})`);
|
|
117
|
+
console.log('');
|
|
118
|
+
examples.forEach((example) => {
|
|
119
|
+
console.log(example);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async function handleCall(positionals, flags, context) {
|
|
123
|
+
const name = positionals[0];
|
|
124
|
+
const methodInput = positionals[1];
|
|
125
|
+
const pathInput = positionals[2];
|
|
126
|
+
if (!name || !methodInput || !pathInput) {
|
|
127
|
+
throw new Error('Usage: eve api call <name> <method> <path> [--json <payload>] [--jq <expr>]');
|
|
128
|
+
}
|
|
129
|
+
const projectId = (0, args_1.getStringFlag)(flags, ['project']) ?? context.projectId;
|
|
130
|
+
const env = (0, args_1.getStringFlag)(flags, ['env']);
|
|
131
|
+
if (!projectId) {
|
|
132
|
+
throw new Error('Missing project id. Provide --project or set a profile default.');
|
|
133
|
+
}
|
|
134
|
+
const query = buildQuery({ env });
|
|
135
|
+
const api = await (0, client_1.requestJson)(context, `/projects/${projectId}/apis/${name}${query}`);
|
|
136
|
+
const jqExpr = (0, args_1.getStringFlag)(flags, ['jq']);
|
|
137
|
+
const printCurl = Boolean(flags['print-curl']);
|
|
138
|
+
const jsonPayloadFlag = flags.json;
|
|
139
|
+
if (jsonPayloadFlag === true) {
|
|
140
|
+
throw new Error('Usage: --json <payload|@file>');
|
|
141
|
+
}
|
|
142
|
+
const graphqlFlag = flags.graphql;
|
|
143
|
+
const graphqlQuery = graphqlFlag === true ? undefined : (0, args_1.getStringFlag)(flags, ['graphql']);
|
|
144
|
+
if (graphqlFlag === true && !graphqlQuery) {
|
|
145
|
+
throw new Error('Usage: --graphql <query|@file>');
|
|
146
|
+
}
|
|
147
|
+
if (graphqlQuery && jsonPayloadFlag) {
|
|
148
|
+
throw new Error('Use --variables for GraphQL variables; --json is for JSON bodies.');
|
|
149
|
+
}
|
|
150
|
+
const variablesFlag = (0, args_1.getStringFlag)(flags, ['variables']);
|
|
151
|
+
const variables = variablesFlag ? resolveJsonInput(variablesFlag) : undefined;
|
|
152
|
+
const jsonBody = typeof jsonPayloadFlag === 'string' ? resolveJsonInput(jsonPayloadFlag) : undefined;
|
|
153
|
+
const graphqlBody = graphqlQuery
|
|
154
|
+
? {
|
|
155
|
+
query: resolveTextInput(graphqlQuery),
|
|
156
|
+
...(variables ? { variables } : {}),
|
|
157
|
+
}
|
|
158
|
+
: undefined;
|
|
159
|
+
const body = graphqlBody ?? jsonBody;
|
|
160
|
+
const method = methodInput.toUpperCase();
|
|
161
|
+
const path = resolveApiPath(api.base_url, pathInput);
|
|
162
|
+
const tokenOverride = (0, args_1.getStringFlag)(flags, ['token']);
|
|
163
|
+
const authToken = tokenOverride ?? process.env.EVE_JOB_TOKEN ?? context.token;
|
|
164
|
+
if (printCurl) {
|
|
165
|
+
const curl = buildCurlCommand({
|
|
166
|
+
method,
|
|
167
|
+
url: path,
|
|
168
|
+
authToken,
|
|
169
|
+
jsonBody: body,
|
|
170
|
+
authHint: tokenOverride ? 'override' : (process.env.EVE_JOB_TOKEN ? 'job' : 'user'),
|
|
171
|
+
});
|
|
172
|
+
console.log(curl);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const response = await fetch(path, {
|
|
176
|
+
method,
|
|
177
|
+
headers: buildApiHeaders(authToken, body),
|
|
178
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
179
|
+
});
|
|
180
|
+
const text = await response.text();
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
183
|
+
}
|
|
184
|
+
const { data, jsonText } = parseResponse(text);
|
|
185
|
+
if (jqExpr) {
|
|
186
|
+
if (!jsonText) {
|
|
187
|
+
throw new Error('Cannot apply --jq to a non-JSON response.');
|
|
188
|
+
}
|
|
189
|
+
const output = runJq(jqExpr, jsonText);
|
|
190
|
+
process.stdout.write(output);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (data !== undefined) {
|
|
194
|
+
console.log(JSON.stringify(data, null, 2));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
process.stdout.write(text);
|
|
198
|
+
}
|
|
199
|
+
function buildQuery(params) {
|
|
200
|
+
const query = Object.entries(params)
|
|
201
|
+
.filter(([, value]) => value !== undefined)
|
|
202
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
|
|
203
|
+
.join('&');
|
|
204
|
+
return query ? `?${query}` : '';
|
|
205
|
+
}
|
|
206
|
+
function formatApiSourcesTable(apis) {
|
|
207
|
+
const nameWidth = Math.max(4, ...apis.map((api) => api.name.length));
|
|
208
|
+
const typeWidth = Math.max(4, ...apis.map((api) => api.type.length));
|
|
209
|
+
console.log(`${'NAME'.padEnd(nameWidth)} ${'TYPE'.padEnd(typeWidth)} BASE URL`);
|
|
210
|
+
console.log(`${'-'.repeat(nameWidth)} ${'-'.repeat(typeWidth)} --------`);
|
|
211
|
+
apis.forEach((api) => {
|
|
212
|
+
console.log(`${api.name.padEnd(nameWidth)} ${api.type.padEnd(typeWidth)} ${api.base_url}`);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
async function fetchApiSpec(context, projectId, name, env) {
|
|
216
|
+
const query = buildQuery({ env });
|
|
217
|
+
const response = await (0, client_1.requestJson)(context, `/projects/${projectId}/apis/${name}/spec${query}`);
|
|
218
|
+
if (response && typeof response === 'object' && 'schema' in response) {
|
|
219
|
+
const schema = response.schema;
|
|
220
|
+
if (typeof schema === 'string') {
|
|
221
|
+
return { data: schema.trim(), raw: schema };
|
|
222
|
+
}
|
|
223
|
+
return { data: schema, raw: JSON.stringify(schema) };
|
|
224
|
+
}
|
|
225
|
+
if (typeof response === 'string') {
|
|
226
|
+
return { data: response.trim(), raw: response };
|
|
227
|
+
}
|
|
228
|
+
return { data: response, raw: JSON.stringify(response) };
|
|
229
|
+
}
|
|
230
|
+
function buildCurlExamples(api, spec) {
|
|
231
|
+
const paths = spec.paths ?? {};
|
|
232
|
+
const examples = [];
|
|
233
|
+
Object.entries(paths).forEach(([path, methods]) => {
|
|
234
|
+
Object.entries(methods).forEach(([method, operation]) => {
|
|
235
|
+
const lower = method.toLowerCase();
|
|
236
|
+
if (!['get', 'post', 'put', 'patch', 'delete'].includes(lower)) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const requestBody = operation.requestBody?.content?.['application/json'];
|
|
240
|
+
const examplePayload = extractExamplePayload(requestBody);
|
|
241
|
+
const needsBody = ['post', 'put', 'patch'].includes(lower);
|
|
242
|
+
const curl = buildCurlCommand({
|
|
243
|
+
method: lower.toUpperCase(),
|
|
244
|
+
url: resolveApiPath(api.base_url, path),
|
|
245
|
+
authToken: api.auth_mode === 'eve' ? '$EVE_JOB_TOKEN' : undefined,
|
|
246
|
+
jsonBody: needsBody ? (examplePayload ?? {}) : undefined,
|
|
247
|
+
authHint: 'job',
|
|
248
|
+
});
|
|
249
|
+
const summary = operation.summary ?? operation.description;
|
|
250
|
+
if (summary) {
|
|
251
|
+
examples.push(`# ${summary}`);
|
|
252
|
+
}
|
|
253
|
+
examples.push(curl);
|
|
254
|
+
examples.push('');
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
return examples;
|
|
258
|
+
}
|
|
259
|
+
function extractExamplePayload(requestBody) {
|
|
260
|
+
if (!requestBody)
|
|
261
|
+
return undefined;
|
|
262
|
+
if (requestBody.example !== undefined)
|
|
263
|
+
return requestBody.example;
|
|
264
|
+
const examples = requestBody.examples ? Object.values(requestBody.examples) : [];
|
|
265
|
+
if (examples.length === 0)
|
|
266
|
+
return undefined;
|
|
267
|
+
return examples[0]?.value;
|
|
268
|
+
}
|
|
269
|
+
function resolveApiPath(baseUrl, path) {
|
|
270
|
+
if (path.startsWith('http://') || path.startsWith('https://')) {
|
|
271
|
+
return path;
|
|
272
|
+
}
|
|
273
|
+
if (!baseUrl) {
|
|
274
|
+
return path;
|
|
275
|
+
}
|
|
276
|
+
const trimmedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
277
|
+
const trimmedPath = path.startsWith('/') ? path : `/${path}`;
|
|
278
|
+
return `${trimmedBase}${trimmedPath}`;
|
|
279
|
+
}
|
|
280
|
+
function buildApiHeaders(authToken, body) {
|
|
281
|
+
const headers = {};
|
|
282
|
+
if (authToken) {
|
|
283
|
+
headers.Authorization = `Bearer ${authToken}`;
|
|
284
|
+
}
|
|
285
|
+
if (body !== undefined) {
|
|
286
|
+
headers['Content-Type'] = 'application/json';
|
|
287
|
+
}
|
|
288
|
+
return headers;
|
|
289
|
+
}
|
|
290
|
+
function resolveJsonInput(value) {
|
|
291
|
+
const text = resolveTextInput(value);
|
|
292
|
+
try {
|
|
293
|
+
return JSON.parse(text);
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
throw new Error(`Invalid JSON payload: ${error.message}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function resolveTextInput(value) {
|
|
300
|
+
if (value.startsWith('@')) {
|
|
301
|
+
const filePath = (0, node_path_1.resolve)(value.slice(1));
|
|
302
|
+
return (0, node_fs_1.readFileSync)(filePath, 'utf-8');
|
|
303
|
+
}
|
|
304
|
+
if (value.trim().startsWith('{') || value.trim().startsWith('[')) {
|
|
305
|
+
return value;
|
|
306
|
+
}
|
|
307
|
+
if ((0, node_fs_1.existsSync)(value)) {
|
|
308
|
+
return (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(value), 'utf-8');
|
|
309
|
+
}
|
|
310
|
+
return value;
|
|
311
|
+
}
|
|
312
|
+
function parseResponse(text) {
|
|
313
|
+
if (!text)
|
|
314
|
+
return { data: undefined, jsonText: undefined };
|
|
315
|
+
try {
|
|
316
|
+
const data = JSON.parse(text);
|
|
317
|
+
return { data, jsonText: JSON.stringify(data) };
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return { data: undefined, jsonText: undefined };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function runJq(expression, jsonText) {
|
|
324
|
+
const result = (0, node_child_process_1.spawnSync)('jq', [expression], {
|
|
325
|
+
input: jsonText,
|
|
326
|
+
encoding: 'utf-8',
|
|
327
|
+
});
|
|
328
|
+
if (result.error) {
|
|
329
|
+
if (result.error.code === 'ENOENT') {
|
|
330
|
+
throw new Error('jq is not installed. Install jq to use --jq output filtering.');
|
|
331
|
+
}
|
|
332
|
+
throw result.error;
|
|
333
|
+
}
|
|
334
|
+
if (result.status !== 0) {
|
|
335
|
+
throw new Error(result.stderr || 'jq failed to process the response.');
|
|
336
|
+
}
|
|
337
|
+
return result.stdout ?? '';
|
|
338
|
+
}
|
|
339
|
+
function buildCurlCommand(options) {
|
|
340
|
+
const parts = ['curl', '-sS', '-X', options.method, `'${options.url}'`];
|
|
341
|
+
if (options.authToken) {
|
|
342
|
+
const tokenValue = options.authHint === 'job' ? '$EVE_JOB_TOKEN' : options.authToken;
|
|
343
|
+
parts.push(`-H 'Authorization: Bearer ${tokenValue}'`);
|
|
344
|
+
}
|
|
345
|
+
if (options.jsonBody !== undefined) {
|
|
346
|
+
parts.push("-H 'Content-Type: application/json'");
|
|
347
|
+
parts.push(`-d '${JSON.stringify(options.jsonBody)}'`);
|
|
348
|
+
}
|
|
349
|
+
return parts.join(' ');
|
|
350
|
+
}
|