@epure-sh/cli 0.0.0-stage → 0.1.8
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 +89 -2
- package/bin/epure-cli.js +517 -0
- package/lib/api-catalog.json +470 -0
- package/lib/api.js +17 -0
- package/lib/auth.js +469 -0
- package/package.json +32 -4
package/README.md
CHANGED
|
@@ -1,3 +1,90 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @epure-sh/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@epure-sh/cli)
|
|
4
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
5
|
+
|
|
6
|
+
> Official Command-Line Interface for [Epure](https://epure.sh) — error triage, issue queues, demangled stack traces, and Agent API inspection.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## ⚡ Zero-Install Quickstart (via `npx`)
|
|
11
|
+
|
|
12
|
+
Run directly without installing:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
export EPURE_TOKEN="epk_live_..."
|
|
16
|
+
|
|
17
|
+
# Verify token & organization role
|
|
18
|
+
npx @epure-sh/cli auth whoami
|
|
19
|
+
|
|
20
|
+
# Inspect unresolved issues
|
|
21
|
+
npx @epure-sh/cli --output table queue --limit 20
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 📦 Global Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g @epure-sh/cli
|
|
30
|
+
|
|
31
|
+
# Now you can use the 'epure' command directly:
|
|
32
|
+
epure auth whoami
|
|
33
|
+
epure queue
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 🛠️ Common Commands
|
|
39
|
+
|
|
40
|
+
### 1. View Unresolved Queue
|
|
41
|
+
```bash
|
|
42
|
+
epure queue --limit 25 --output table
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 2. Export Crash Context for Coding Agents
|
|
46
|
+
Export demangled stack traces and source lines to markdown:
|
|
47
|
+
```bash
|
|
48
|
+
epure context <ISSUE_UUID> --format markdown
|
|
49
|
+
# Or save directly to a file:
|
|
50
|
+
epure context <ISSUE_UUID> --format markdown --out bug-context.md
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 3. Triage & Resolve Issues
|
|
54
|
+
```bash
|
|
55
|
+
# Mark resolved with release tag
|
|
56
|
+
epure triage resolve <ISSUE_UUID> --release v1.2.0
|
|
57
|
+
|
|
58
|
+
# Snooze an issue for 24 hours
|
|
59
|
+
epure triage snooze <ISSUE_UUID> --mode hours
|
|
60
|
+
|
|
61
|
+
# Ignore issue from alerting
|
|
62
|
+
epure triage ignore <ISSUE_UUID>
|
|
63
|
+
|
|
64
|
+
# Reopen issue
|
|
65
|
+
epure triage reopen <ISSUE_UUID>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 4. Self-Hosted Instances
|
|
69
|
+
Point to your own Epure deployment:
|
|
70
|
+
```bash
|
|
71
|
+
export EPURE_URL="https://errors.mycompany.com"
|
|
72
|
+
export EPURE_TOKEN="epk_..."
|
|
73
|
+
epure queue
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 📄 License
|
|
79
|
+
|
|
80
|
+
Apache-2.0 © Epure
|
|
81
|
+
|
|
82
|
+
## Full resource API access (0.1.8)
|
|
83
|
+
|
|
84
|
+
CLI: `epure api-catalog`, then `epure api GET /api/v1/projects`. Mutations accept JSON, `@body.json`, or `-` for stdin. Multipart uploads: `epure api POST /api/v1/projects/PROJECT/releases/VERSION/files '{}' --file file=app.js.map`.
|
|
85
|
+
|
|
86
|
+
MCP: `epure_api_catalog` lists actions; `epure_api` accepts method/path/query/body and multipart files (field, filename, base64, contentType). REST tools and existing queue/context/resolve tools work without the optional hosted MCP endpoint.
|
|
87
|
+
|
|
88
|
+
Use a PAT: read:agent for reads, write:triage for triage, write:admin plus an appropriate organization role for project/channel/rule/monitor/release/team management. Credential changes and OAuth consent remain session/login operations. No permission bypass. Application response text is untrusted data. Default cloud endpoint: https://app.epure.sh.
|
|
89
|
+
|
|
90
|
+
For full management consent at login: `EPURE_SCOPES="read:agent write:triage write:admin" epure login` (or `epure-mcp login`). Existing credentials need reauthentication or a suitably scoped PAT.
|
package/bin/epure-cli.js
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @epure-sh/cli - Official Command-Line Interface for Epure
|
|
5
|
+
* Triage errors, inspect issue queues, demangle stack traces, and manage workspaces.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const http = require('http');
|
|
9
|
+
const https = require('https');
|
|
10
|
+
const { URL } = require('url');
|
|
11
|
+
const auth = require('../lib/auth');
|
|
12
|
+
|
|
13
|
+
const VERSION = '0.1.8';
|
|
14
|
+
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
|
|
17
|
+
function printHelp() {
|
|
18
|
+
console.log(`
|
|
19
|
+
@epure-sh/cli - Epure Command-Line Interface (v${VERSION})
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
npx @epure-sh/cli <command> [options]
|
|
23
|
+
epure <command> [options]
|
|
24
|
+
|
|
25
|
+
Authentication:
|
|
26
|
+
login Authenticate via browser OAuth (loopback or device code)
|
|
27
|
+
logout Remove saved credentials for current or specified instance
|
|
28
|
+
use <url> Switch active Epure instance
|
|
29
|
+
instances List all configured Epure instances
|
|
30
|
+
auth whoami Verify your active token and organization role
|
|
31
|
+
|
|
32
|
+
Core Commands:
|
|
33
|
+
queue List unresolved production issue queue
|
|
34
|
+
context <issue_id> Export demangled stack trace and code lines for an issue
|
|
35
|
+
issue show <issue_id> View full metadata for a specific issue
|
|
36
|
+
triage resolve <issue_id> Mark an issue resolved (optional: --release <rel>)
|
|
37
|
+
triage ignore <issue_id> Ignore an issue from alerting
|
|
38
|
+
triage reopen <issue_id> Reopen a resolved or ignored issue
|
|
39
|
+
alerts List recent alerting events (optional: --view unread)
|
|
40
|
+
api-catalog List available product API actions
|
|
41
|
+
api <METHOD> <PATH> [JSON|@file|-] Execute any authorized API action
|
|
42
|
+
|
|
43
|
+
Admin / Outbound Sales:
|
|
44
|
+
admin create-canary Pre-provision an isolated Epure Cloud workspace with live DSN & Magic Link
|
|
45
|
+
Flags: --domain <d>, --email <e>, [--name <n>], [--error <err>]
|
|
46
|
+
|
|
47
|
+
Global Options:
|
|
48
|
+
--token, -t <token> Override Epure Personal Access Token
|
|
49
|
+
--url, -u <url> Override Epure endpoint (default: current instance or https://app.epure.sh)
|
|
50
|
+
--device Force RFC 8628 Device Code flow (for SSH/headless)
|
|
51
|
+
--output, -o <json|table> Output format (default: json)
|
|
52
|
+
--help, -h Display this help message
|
|
53
|
+
--version, -v Display CLI version
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
epure login
|
|
57
|
+
epure login --url http://localhost:8080
|
|
58
|
+
epure use https://app.epure.sh
|
|
59
|
+
epure queue --limit 20
|
|
60
|
+
epure context 550e8400-e29b-41d4-a716-446655440000 --format markdown
|
|
61
|
+
epure triage resolve 550e8400-e29b-41d4-a716-446655440000 --release v1.2.0
|
|
62
|
+
`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
66
|
+
printHelp();
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
71
|
+
console.log(`@epure-sh/cli v${VERSION}`);
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Parse top-level flags
|
|
76
|
+
let explicitToken = process.env.EPURE_TOKEN || '';
|
|
77
|
+
let explicitUrl = process.env.EPURE_URL || '';
|
|
78
|
+
let forceDevice = false;
|
|
79
|
+
let outputFormat = 'json';
|
|
80
|
+
|
|
81
|
+
const filteredArgs = [];
|
|
82
|
+
for (let i = 0; i < args.length; i++) {
|
|
83
|
+
const arg = args[i];
|
|
84
|
+
if (arg === '--token' || arg === '-t') {
|
|
85
|
+
explicitToken = args[++i];
|
|
86
|
+
} else if (arg.startsWith('--token=')) {
|
|
87
|
+
explicitToken = arg.split('=')[1];
|
|
88
|
+
} else if (arg === '--url' || arg === '-u') {
|
|
89
|
+
explicitUrl = args[++i];
|
|
90
|
+
} else if (arg.startsWith('--url=')) {
|
|
91
|
+
explicitUrl = arg.split('=')[1];
|
|
92
|
+
} else if (arg === '--device' || arg === '--no-browser') {
|
|
93
|
+
forceDevice = true;
|
|
94
|
+
} else if (arg === '--output' || arg === '-o') {
|
|
95
|
+
outputFormat = args[++i];
|
|
96
|
+
} else if (arg.startsWith('--output=')) {
|
|
97
|
+
outputFormat = arg.split('=')[1];
|
|
98
|
+
} else {
|
|
99
|
+
filteredArgs.push(arg);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let activeToken = explicitToken;
|
|
104
|
+
let baseUrl = explicitUrl;
|
|
105
|
+
|
|
106
|
+
async function resolveAuthentication() {
|
|
107
|
+
if (explicitToken) {
|
|
108
|
+
baseUrl = auth.normalizeUrl(explicitUrl || 'https://app.epure.sh');
|
|
109
|
+
return explicitToken;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const cred = auth.getActiveCredentials(explicitUrl);
|
|
113
|
+
if (cred) {
|
|
114
|
+
baseUrl = cred.url;
|
|
115
|
+
try {
|
|
116
|
+
activeToken = await auth.ensureValidToken(cred);
|
|
117
|
+
return activeToken;
|
|
118
|
+
} catch {
|
|
119
|
+
activeToken = cred.access_token;
|
|
120
|
+
return activeToken;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
baseUrl = auth.normalizeUrl(explicitUrl || 'https://app.epure.sh');
|
|
125
|
+
return '';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function request(method, path, body = null, headers = {}) {
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
if (!activeToken && !path.startsWith('/api/v1/auth/')) {
|
|
131
|
+
return reject(
|
|
132
|
+
new Error(
|
|
133
|
+
'Authentication required. Run "epure login" or pass --token <token>'
|
|
134
|
+
)
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const fullUrl =
|
|
139
|
+
path.startsWith('http://') || path.startsWith('https://')
|
|
140
|
+
? path
|
|
141
|
+
: `${baseUrl}${path}`;
|
|
142
|
+
const urlObj = new URL(fullUrl);
|
|
143
|
+
const isHttps = urlObj.protocol === 'https:';
|
|
144
|
+
const client = isHttps ? https : http;
|
|
145
|
+
|
|
146
|
+
const payload = body
|
|
147
|
+
? typeof body === 'string'
|
|
148
|
+
? body
|
|
149
|
+
: JSON.stringify(body)
|
|
150
|
+
: null;
|
|
151
|
+
|
|
152
|
+
const reqHeaders = {
|
|
153
|
+
'User-Agent': `@epure-sh/cli/${VERSION}`,
|
|
154
|
+
...headers,
|
|
155
|
+
};
|
|
156
|
+
if (activeToken) {
|
|
157
|
+
reqHeaders['Authorization'] = `Bearer ${activeToken}`;
|
|
158
|
+
}
|
|
159
|
+
if (payload && !reqHeaders['Content-Type']) {
|
|
160
|
+
reqHeaders['Content-Type'] = 'application/json';
|
|
161
|
+
reqHeaders['Content-Length'] = Buffer.byteLength(payload);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const req = client.request(
|
|
165
|
+
fullUrl,
|
|
166
|
+
{
|
|
167
|
+
method,
|
|
168
|
+
headers: reqHeaders,
|
|
169
|
+
},
|
|
170
|
+
(res) => {
|
|
171
|
+
let resBody = '';
|
|
172
|
+
res.on('data', (chunk) => {
|
|
173
|
+
resBody += chunk;
|
|
174
|
+
});
|
|
175
|
+
res.on('end', () => {
|
|
176
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
177
|
+
resolve({
|
|
178
|
+
status: res.statusCode,
|
|
179
|
+
headers: res.headers,
|
|
180
|
+
text: resBody,
|
|
181
|
+
json: () => {
|
|
182
|
+
try {
|
|
183
|
+
return JSON.parse(resBody);
|
|
184
|
+
} catch {
|
|
185
|
+
return resBody;
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
} else {
|
|
190
|
+
let errDetail = resBody;
|
|
191
|
+
try {
|
|
192
|
+
const parsed = JSON.parse(resBody);
|
|
193
|
+
errDetail = parsed.error_description || parsed.error || parsed.message || resBody;
|
|
194
|
+
} catch {}
|
|
195
|
+
reject(new Error(`HTTP ${res.statusCode}: ${errDetail}`));
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
req.on('error', reject);
|
|
202
|
+
if (payload) {
|
|
203
|
+
req.write(payload);
|
|
204
|
+
}
|
|
205
|
+
req.end();
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function printOutput(data, tableRenderer = null) {
|
|
210
|
+
if (outputFormat === 'table' && tableRenderer) {
|
|
211
|
+
tableRenderer(data);
|
|
212
|
+
} else {
|
|
213
|
+
console.log(typeof data === 'string' ? data : JSON.stringify(data, null, 2));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function handleCommand() {
|
|
218
|
+
const cmd = filteredArgs[0];
|
|
219
|
+
const subCmd = filteredArgs[1];
|
|
220
|
+
|
|
221
|
+
if (cmd === 'login') {
|
|
222
|
+
const res = await auth.login({
|
|
223
|
+
url: explicitUrl,
|
|
224
|
+
device: forceDevice,
|
|
225
|
+
clientId: 'epure-cli',
|
|
226
|
+
});
|
|
227
|
+
console.log(`\n✓ Successfully authenticated with ${res.url}!`);
|
|
228
|
+
console.log(` Organization: ${res.org_id}`);
|
|
229
|
+
console.log(` Credentials saved to ${auth.getCredentialsPath()}\n`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (cmd === 'logout') {
|
|
234
|
+
const removed = auth.removeInstance(explicitUrl);
|
|
235
|
+
console.log(`✓ Logged out of ${removed}. Credentials removed.\n`);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (cmd === 'use') {
|
|
240
|
+
const target = filteredArgs[1];
|
|
241
|
+
if (!target) {
|
|
242
|
+
throw new Error('Usage: epure use <url>');
|
|
243
|
+
}
|
|
244
|
+
const switched = auth.switchInstance(target);
|
|
245
|
+
console.log(`✓ Active instance switched to: ${switched}\n`);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (cmd === 'instances') {
|
|
250
|
+
const info = auth.listInstances();
|
|
251
|
+
console.log('\nConfigured Epure Instances:');
|
|
252
|
+
if (info.instances.length === 0) {
|
|
253
|
+
console.log(' (no instances configured. Run "epure login")\n');
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
for (const inst of info.instances) {
|
|
257
|
+
const active = inst === info.current_instance ? '* ' : ' ';
|
|
258
|
+
console.log(`${active}${inst}`);
|
|
259
|
+
}
|
|
260
|
+
console.log('');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Admin / Canary Provisioning command
|
|
265
|
+
if (cmd === 'admin') {
|
|
266
|
+
let domain = '';
|
|
267
|
+
let email = '';
|
|
268
|
+
let name = '';
|
|
269
|
+
let error = '';
|
|
270
|
+
let secret = process.env.EPURE_INTERNAL_SECRET || explicitToken || 'epure_dev_internal_secret';
|
|
271
|
+
|
|
272
|
+
for (let i = 1; i < filteredArgs.length; i++) {
|
|
273
|
+
const a = filteredArgs[i];
|
|
274
|
+
if (a === '--domain') domain = filteredArgs[++i];
|
|
275
|
+
else if (a === '--email') email = filteredArgs[++i];
|
|
276
|
+
else if (a === '--name') name = filteredArgs[++i];
|
|
277
|
+
else if (a === '--error') error = filteredArgs[++i];
|
|
278
|
+
else if (a === '--secret') secret = filteredArgs[++i];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (subCmd === 'create-canary') {
|
|
282
|
+
if (!domain) {
|
|
283
|
+
throw new Error('Usage: epure admin create-canary --domain <domain> --email <email> [--name <name>] [--error <error>]');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (secret) {
|
|
287
|
+
activeToken = secret;
|
|
288
|
+
}
|
|
289
|
+
if (!baseUrl || baseUrl === 'https://app.epure.sh') {
|
|
290
|
+
baseUrl = process.env.EPURE_CLOUD_URL || explicitUrl || 'https://cloud.epure.sh';
|
|
291
|
+
}
|
|
292
|
+
if (!activeToken) {
|
|
293
|
+
await resolveAuthentication();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const body = {
|
|
297
|
+
domain,
|
|
298
|
+
email: email || undefined,
|
|
299
|
+
app_name: name || domain.replace(/\./g, '-'),
|
|
300
|
+
seed_error: error || undefined,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const res = await request('POST', '/api/internal/canary', body);
|
|
304
|
+
const data = res.json();
|
|
305
|
+
|
|
306
|
+
printOutput(data, (info) => {
|
|
307
|
+
console.log('\n⚡ Epure Cloud Canary Provisioned Successfully!');
|
|
308
|
+
console.log('───────────────────────────────────────────────────────');
|
|
309
|
+
console.log(` Domain: ${info.domain || domain}`);
|
|
310
|
+
if (email) console.log(` Prospect Email: ${email}`);
|
|
311
|
+
console.log(` Live DSN: ${info.dsn}`);
|
|
312
|
+
console.log(` 1-Click Magic: ${info.magic_link}`);
|
|
313
|
+
console.log(` MCP Command: ${info.mcp_cmd}`);
|
|
314
|
+
console.log(' Validity: 14 Days (Zero password/signup prompt)');
|
|
315
|
+
console.log('───────────────────────────────────────────────────────');
|
|
316
|
+
console.log(' Seeded Exceptions:');
|
|
317
|
+
console.log(` 1. [Primary] ${error || 'TypeError: Cannot read properties of undefined (reading auth)'}`);
|
|
318
|
+
console.log(' 2. [Timeout] DatabaseError: connection pool exhausted (timeout after 5000ms)');
|
|
319
|
+
console.log(' 3. [Webhook] UnhandledPromiseRejection: failed to process webhook payment payload\n');
|
|
320
|
+
});
|
|
321
|
+
return;
|
|
322
|
+
} else {
|
|
323
|
+
throw new Error(`Unknown admin command: ${subCmd}. Usage: epure admin create-canary`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (cmd === 'api-catalog') { printOutput(require('../lib/api-catalog.json')); return; }
|
|
328
|
+
|
|
329
|
+
// All other commands require authentication
|
|
330
|
+
await resolveAuthentication();
|
|
331
|
+
if (!activeToken) {
|
|
332
|
+
if (process.stdin.isTTY) {
|
|
333
|
+
console.log('No active session found. Initiating login...\n');
|
|
334
|
+
await auth.login({
|
|
335
|
+
url: explicitUrl,
|
|
336
|
+
device: forceDevice,
|
|
337
|
+
clientId: 'epure-cli',
|
|
338
|
+
});
|
|
339
|
+
await resolveAuthentication();
|
|
340
|
+
} else {
|
|
341
|
+
throw new Error(
|
|
342
|
+
'Authentication required. Run "epure login" or set EPURE_TOKEN.'
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
switch (cmd) {
|
|
348
|
+
case 'auth': {
|
|
349
|
+
if (subCmd === 'whoami') {
|
|
350
|
+
const res = await request('GET', '/api/v1/agent/whoami');
|
|
351
|
+
printOutput(res.json(), (data) => {
|
|
352
|
+
console.log('\nAuthenticated Agent Identity:');
|
|
353
|
+
console.log(` Endpoint: ${baseUrl}`);
|
|
354
|
+
console.log(` User ID: ${data.user_id}`);
|
|
355
|
+
console.log(` Org ID: ${data.org_id}`);
|
|
356
|
+
console.log(` Email: ${data.email || 'N/A'}`);
|
|
357
|
+
console.log(` Role: ${data.role}`);
|
|
358
|
+
console.log(` Token Scope: ${(data.scopes || []).join(', ') || 'full'}\n`);
|
|
359
|
+
});
|
|
360
|
+
} else {
|
|
361
|
+
throw new Error(`Unknown auth command: ${subCmd}. Usage: epure auth whoami`);
|
|
362
|
+
}
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
case 'queue': {
|
|
367
|
+
let limit = 20;
|
|
368
|
+
let project = '';
|
|
369
|
+
let env = '';
|
|
370
|
+
let q = '';
|
|
371
|
+
|
|
372
|
+
for (let i = 1; i < filteredArgs.length; i++) {
|
|
373
|
+
const a = filteredArgs[i];
|
|
374
|
+
if (a === '--limit') limit = parseInt(filteredArgs[++i], 10);
|
|
375
|
+
else if (a === '--project') project = filteredArgs[++i];
|
|
376
|
+
else if (a === '--env') env = filteredArgs[++i];
|
|
377
|
+
else if (a === '--query' || a === '-q') q = filteredArgs[++i];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const params = new URLSearchParams({ limit: limit.toString() });
|
|
381
|
+
if (project) params.set('project_id', project);
|
|
382
|
+
if (env) params.set('environment', env);
|
|
383
|
+
if (q) params.set('query', q);
|
|
384
|
+
|
|
385
|
+
const res = await request('GET', `/api/v1/agent/queue?${params.toString()}`);
|
|
386
|
+
const data = res.json();
|
|
387
|
+
|
|
388
|
+
printOutput(data, (body) => {
|
|
389
|
+
const issues = body.issues || [];
|
|
390
|
+
console.log(`\nUnresolved Production Queue (${issues.length} issues):\n`);
|
|
391
|
+
if (issues.length === 0) {
|
|
392
|
+
console.log(' All clear! No unresolved issues.');
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
for (const iss of issues) {
|
|
396
|
+
const count = String(iss.event_count || 1).padStart(5, ' ');
|
|
397
|
+
const title = iss.title || 'Unknown error';
|
|
398
|
+
console.log(` [${count} ev] ${iss.id} - ${title}`);
|
|
399
|
+
if (iss.culprit) {
|
|
400
|
+
console.log(` Culprit: ${iss.culprit}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
console.log('');
|
|
404
|
+
});
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
case 'context': {
|
|
409
|
+
const issueId = filteredArgs[1];
|
|
410
|
+
if (!issueId) {
|
|
411
|
+
throw new Error('Usage: epure context <issue_id> [--format markdown|json]');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
let format = 'markdown';
|
|
415
|
+
for (let i = 2; i < filteredArgs.length; i++) {
|
|
416
|
+
if (filteredArgs[i] === '--format') format = filteredArgs[++i];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const params = new URLSearchParams({ issue_id: issueId, format });
|
|
420
|
+
const res = await request('GET', `/api/v1/agent/issues/${encodeURIComponent(issueId)}/context?${params.toString()}`);
|
|
421
|
+
|
|
422
|
+
if (format === 'markdown') {
|
|
423
|
+
console.log(res.text);
|
|
424
|
+
} else {
|
|
425
|
+
printOutput(res.json());
|
|
426
|
+
}
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
case 'issue': {
|
|
431
|
+
if (subCmd === 'show') {
|
|
432
|
+
const issueId = filteredArgs[2];
|
|
433
|
+
if (!issueId) {
|
|
434
|
+
throw new Error('Usage: epure issue show <issue_id>');
|
|
435
|
+
}
|
|
436
|
+
const res = await request('GET', `/api/v1/agent/issues/${issueId}`);
|
|
437
|
+
printOutput(res.json());
|
|
438
|
+
} else {
|
|
439
|
+
throw new Error(`Unknown issue command: ${subCmd}. Usage: epure issue show <issue_id>`);
|
|
440
|
+
}
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
case 'triage': {
|
|
445
|
+
const action = subCmd;
|
|
446
|
+
const issueId = filteredArgs[2];
|
|
447
|
+
|
|
448
|
+
if (!action || !issueId) {
|
|
449
|
+
throw new Error(
|
|
450
|
+
'Usage: epure triage <resolve|ignore|reopen|snooze> <issue_id> [flags]'
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
let payload = { action };
|
|
455
|
+
|
|
456
|
+
for (let i = 3; i < filteredArgs.length; i++) {
|
|
457
|
+
const a = filteredArgs[i];
|
|
458
|
+
if (a === '--release') payload.release = filteredArgs[++i];
|
|
459
|
+
else if (a === '--commit') payload.commit_sha = filteredArgs[++i];
|
|
460
|
+
else if (a === '--mode') payload.snooze_mode = filteredArgs[++i];
|
|
461
|
+
else if (a === '--hours') payload.snooze_hours = parseInt(filteredArgs[++i], 10);
|
|
462
|
+
else if (a === '--count') payload.snooze_count = parseInt(filteredArgs[++i], 10);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const res = await request('POST', `/api/v1/agent/issues/${issueId}/triage`, payload);
|
|
466
|
+
printOutput(res.json(), (data) => {
|
|
467
|
+
console.log(`\n✓ Issue ${issueId} triaged: ${data.status || action}\n`);
|
|
468
|
+
});
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
case 'alerts': {
|
|
473
|
+
let view = '';
|
|
474
|
+
for (let i = 1; i < filteredArgs.length; i++) {
|
|
475
|
+
if (filteredArgs[i] === '--view') view = filteredArgs[++i];
|
|
476
|
+
}
|
|
477
|
+
const params = new URLSearchParams();
|
|
478
|
+
if (view) params.set('view', view);
|
|
479
|
+
|
|
480
|
+
const res = await request('GET', `/api/v1/agent/alerts?${params.toString()}`);
|
|
481
|
+
printOutput(res.json());
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
case 'api-catalog': { printOutput(require('../lib/api-catalog.json')); break; }
|
|
486
|
+
|
|
487
|
+
case 'api': {
|
|
488
|
+
const method = (filteredArgs[1] || 'GET').toUpperCase();
|
|
489
|
+
const path = filteredArgs[2];
|
|
490
|
+
if (!path) {
|
|
491
|
+
throw new Error('Usage: epure api <METHOD> <PATH> [JSON_BODY]');
|
|
492
|
+
}
|
|
493
|
+
const input = filteredArgs[3]?.startsWith('--') ? null : filteredArgs[3];
|
|
494
|
+
const rawBody = input === '-' ? require('fs').readFileSync(0,'utf8') : input?.startsWith('@') ? require('fs').readFileSync(input.slice(1),'utf8') : input || null;
|
|
495
|
+
if (!path.startsWith('/api/v1/')) throw new Error('Path must start with /api/v1/');
|
|
496
|
+
const files=[];
|
|
497
|
+
for(let i=3;i<filteredArgs.length;i++)if(filteredArgs[i]==='--file'){
|
|
498
|
+
const spec=filteredArgs[++i];if(!spec?.includes('='))throw new Error('--file expects field=filename');
|
|
499
|
+
const split=spec.indexOf('=');const filename=spec.slice(split+1);
|
|
500
|
+
files.push({field:spec.slice(0,split),filename:require('path').basename(filename),base64:require('fs').readFileSync(filename).toString('base64')});
|
|
501
|
+
}
|
|
502
|
+
const result=await require('../lib/api').apiRequest(baseUrl,activeToken,{method,path,body:rawBody?JSON.parse(rawBody):undefined,...(files.length?{files}:{})});
|
|
503
|
+
printOutput(result.data);
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
default:
|
|
508
|
+
console.error(`Unknown command: ${cmd}\n`);
|
|
509
|
+
printHelp();
|
|
510
|
+
process.exit(1);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
handleCommand().catch((err) => {
|
|
515
|
+
console.error(`\nError: ${err.message}\n`);
|
|
516
|
+
process.exit(1);
|
|
517
|
+
});
|