@epure-sh/cli 0.0.0-stage → 0.1.7

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 (3) hide show
  1. package/README.md +79 -2
  2. package/bin/epure-cli.js +358 -0
  3. package/package.json +31 -4
package/README.md CHANGED
@@ -1,3 +1,80 @@
1
- # Temporary Holding Version
1
+ # @epure-sh/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/@epure-sh/cli.svg)](https://www.npmjs.com/package/@epure-sh/cli)
4
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](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
@@ -0,0 +1,358 @@
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 fs = require('fs');
11
+ const { URL } = require('url');
12
+
13
+ const VERSION = '0.1.7';
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
+ Core Commands:
26
+ auth whoami Verify your active token and organization role
27
+ queue List unresolved production issue queue
28
+ context <issue_id> Export demangled stack trace and code lines for an issue
29
+ issue show <issue_id> View full metadata for a specific issue
30
+ triage resolve <issue_id> Mark an issue resolved (optional: --release <rel>)
31
+ triage ignore <issue_id> Ignore an issue from alerting
32
+ triage reopen <issue_id> Reopen a resolved or ignored issue
33
+ triage snooze <issue_id> Snooze an issue (optional: --mode hours|count)
34
+ alerts List recent alerting events (optional: --view unread)
35
+ api <METHOD> <PATH> Execute raw authenticated API call against workspace
36
+
37
+ Global Options:
38
+ --token, -t <token> Epure Personal Access Token (or EPURE_TOKEN env var)
39
+ --url, -u <url> Epure endpoint (default: https://epure.sh or EPURE_URL)
40
+ --output, -o <json|table> Output format (default: json)
41
+ --help, -h Display this help message
42
+ --version, -v Display CLI version
43
+
44
+ Examples:
45
+ export EPURE_TOKEN="epk_live_..."
46
+ npx @epure-sh/cli auth whoami
47
+ npx @epure-sh/cli --output table queue --limit 20
48
+ npx @epure-sh/cli context 550e8400-e29b-41d4-a716-446655440000 --format markdown
49
+ npx @epure-sh/cli triage resolve 550e8400-e29b-41d4-a716-446655440000 --release v1.2.0
50
+ `);
51
+ }
52
+
53
+ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
54
+ printHelp();
55
+ process.exit(0);
56
+ }
57
+
58
+ if (args.includes('--version') || args.includes('-v')) {
59
+ console.log(`@epure-sh/cli v${VERSION}`);
60
+ process.exit(0);
61
+ }
62
+
63
+ // Parse top-level flags
64
+ let token = process.env.EPURE_TOKEN || '';
65
+ let baseUrl = process.env.EPURE_URL || 'https://epure.sh';
66
+ let outputFormat = 'json';
67
+
68
+ const filteredArgs = [];
69
+ for (let i = 0; i < args.length; i++) {
70
+ const arg = args[i];
71
+ if (arg === '--token' || arg === '-t') {
72
+ token = args[++i];
73
+ } else if (arg.startsWith('--token=')) {
74
+ token = arg.split('=')[1];
75
+ } else if (arg === '--url' || arg === '-u') {
76
+ baseUrl = args[++i];
77
+ } else if (arg.startsWith('--url=')) {
78
+ baseUrl = arg.split('=')[1];
79
+ } else if (arg === '--output' || arg === '-o') {
80
+ outputFormat = args[++i];
81
+ } else if (arg.startsWith('--output=')) {
82
+ outputFormat = arg.split('=')[1];
83
+ } else {
84
+ filteredArgs.push(arg);
85
+ }
86
+ }
87
+
88
+ baseUrl = baseUrl.replace(/\/+$/, '');
89
+
90
+ function request(method, path, body = null, headers = {}) {
91
+ return new Promise((resolve, reject) => {
92
+ if (!token && !path.startsWith('/api/v1/auth/')) {
93
+ return reject(new Error('EPURE_TOKEN is required. Pass --token <token> or export EPURE_TOKEN="epk_..."'));
94
+ }
95
+
96
+ const fullUrl = path.startsWith('http://') || path.startsWith('https://') ? path : `${baseUrl}${path}`;
97
+ const urlObj = new URL(fullUrl);
98
+ const isHttps = urlObj.protocol === 'https:';
99
+ const client = isHttps ? https : http;
100
+
101
+ const payload = body ? (typeof body === 'string' ? body : JSON.stringify(body)) : null;
102
+
103
+ const reqHeaders = {
104
+ 'User-Agent': `@epure-sh/cli/${VERSION}`,
105
+ ...headers,
106
+ };
107
+ if (token) {
108
+ reqHeaders['Authorization'] = `Bearer ${token}`;
109
+ }
110
+ if (payload && !reqHeaders['Content-Type']) {
111
+ reqHeaders['Content-Type'] = 'application/json';
112
+ reqHeaders['Content-Length'] = Buffer.byteLength(payload);
113
+ }
114
+
115
+ const req = client.request(
116
+ fullUrl,
117
+ {
118
+ method,
119
+ headers: reqHeaders,
120
+ },
121
+ (res) => {
122
+ let resBody = '';
123
+ res.on('data', (chunk) => {
124
+ resBody += chunk;
125
+ });
126
+ res.on('end', () => {
127
+ if (res.statusCode >= 200 && res.statusCode < 300) {
128
+ resolve({
129
+ status: res.statusCode,
130
+ headers: res.headers,
131
+ text: resBody,
132
+ json: () => {
133
+ try {
134
+ return JSON.parse(resBody);
135
+ } catch {
136
+ return resBody;
137
+ }
138
+ },
139
+ });
140
+ } else {
141
+ let errDetail = resBody;
142
+ try {
143
+ const parsed = JSON.parse(resBody);
144
+ errDetail = parsed.error || parsed.message || resBody;
145
+ } catch {}
146
+ reject(new Error(`HTTP ${res.statusCode}: ${errDetail}`));
147
+ }
148
+ });
149
+ }
150
+ );
151
+
152
+ req.on('error', reject);
153
+ if (payload) {
154
+ req.write(payload);
155
+ }
156
+ req.end();
157
+ });
158
+ }
159
+
160
+ function printOutput(data, tableRenderer = null) {
161
+ if (outputFormat === 'table' && tableRenderer) {
162
+ tableRenderer(data);
163
+ } else {
164
+ console.log(typeof data === 'string' ? data : JSON.stringify(data, null, 2));
165
+ }
166
+ }
167
+
168
+ async function handleCommand() {
169
+ const cmd = filteredArgs[0];
170
+ const subCmd = filteredArgs[1];
171
+
172
+ switch (cmd) {
173
+ case 'auth': {
174
+ if (subCmd === 'whoami') {
175
+ const res = await request('GET', '/api/v1/agent/whoami');
176
+ printOutput(res.json(), (data) => {
177
+ console.log('\nAuthenticated Agent Identity:');
178
+ console.log(` User ID: ${data.user_id}`);
179
+ console.log(` Org ID: ${data.org_id}`);
180
+ console.log(` Email: ${data.email || 'N/A'}`);
181
+ console.log(` Role: ${data.role}`);
182
+ console.log(` Token Scope: ${(data.scopes || []).join(', ') || 'full'}\n`);
183
+ });
184
+ } else {
185
+ throw new Error(`Unknown auth command: ${subCmd}. Usage: epure auth whoami`);
186
+ }
187
+ break;
188
+ }
189
+
190
+ case 'queue': {
191
+ let limit = 20;
192
+ let project = '';
193
+ let env = '';
194
+ let q = '';
195
+
196
+ for (let i = 1; i < filteredArgs.length; i++) {
197
+ const a = filteredArgs[i];
198
+ if (a === '--limit') limit = parseInt(filteredArgs[++i], 10);
199
+ else if (a === '--project') project = filteredArgs[++i];
200
+ else if (a === '--env') env = filteredArgs[++i];
201
+ else if (a === '--q') q = filteredArgs[++i];
202
+ }
203
+
204
+ let path = `/api/v1/agent/queue?limit=${limit}`;
205
+ if (project) path += `&project_id=${encodeURIComponent(project)}`;
206
+ if (env) path += `&environment=${encodeURIComponent(env)}`;
207
+ if (q) path += `&q=${encodeURIComponent(q)}`;
208
+
209
+ const res = await request('GET', path);
210
+ const data = res.json();
211
+ printOutput(data, (queue) => {
212
+ const issues = Array.isArray(queue) ? queue : queue.issues || [];
213
+ if (issues.length === 0) {
214
+ console.log('No unresolved issues in queue.');
215
+ return;
216
+ }
217
+ console.log(`\nUnresolved Queue (${issues.length} issues):`);
218
+ console.log('─'.repeat(100));
219
+ console.log(
220
+ 'ID'.padEnd(38) +
221
+ 'LEVEL'.padEnd(10) +
222
+ 'EVENTS'.padEnd(10) +
223
+ 'USERS'.padEnd(10) +
224
+ 'TITLE'
225
+ );
226
+ console.log('─'.repeat(100));
227
+ for (const item of issues) {
228
+ const id = (item.id || '').padEnd(38);
229
+ const lvl = (item.level || 'error').padEnd(10);
230
+ const evts = String(item.event_count || item.events || 1).padEnd(10);
231
+ const users = String(item.unique_user_count || item.users || 1).padEnd(10);
232
+ const title = (item.title || item.fingerprint || '').slice(0, 40);
233
+ console.log(`${id}${lvl}${evts}${users}${title}`);
234
+ }
235
+ console.log('─'.repeat(100) + '\n');
236
+ });
237
+ break;
238
+ }
239
+
240
+ case 'context': {
241
+ const issueId = filteredArgs[1];
242
+ if (!issueId) {
243
+ throw new Error('Usage: epure context <issue_id> [--format markdown|json] [--out <file>]');
244
+ }
245
+
246
+ let format = 'markdown';
247
+ let outFile = '';
248
+ for (let i = 2; i < filteredArgs.length; i++) {
249
+ const a = filteredArgs[i];
250
+ if (a === '--format') format = filteredArgs[++i];
251
+ else if (a === '--out') outFile = filteredArgs[++i];
252
+ }
253
+
254
+ const res = await request('GET', `/api/v1/agent/issues/${issueId}/context?format=${format}`);
255
+ const content = format === 'markdown' ? res.text : JSON.stringify(res.json(), null, 2);
256
+
257
+ if (outFile) {
258
+ fs.writeFileSync(outFile, content, 'utf8');
259
+ console.log(`Saved context for issue ${issueId} to ${outFile}`);
260
+ } else {
261
+ console.log(content);
262
+ }
263
+ break;
264
+ }
265
+
266
+ case 'issue': {
267
+ if (subCmd === 'show') {
268
+ const issueId = filteredArgs[2];
269
+ if (!issueId) throw new Error('Usage: epure issue show <issue_id>');
270
+ const res = await request('GET', `/api/v1/agent/issues/${issueId}`);
271
+ printOutput(res.json());
272
+ } else {
273
+ throw new Error(`Unknown issue command: ${subCmd}. Usage: epure issue show <issue_id>`);
274
+ }
275
+ break;
276
+ }
277
+
278
+ case 'triage': {
279
+ const action = subCmd;
280
+ const issueId = filteredArgs[2];
281
+ if (!issueId) {
282
+ throw new Error(`Usage: epure triage ${action || 'resolve|ignore|reopen|snooze'} <issue_id>`);
283
+ }
284
+
285
+ if (action === 'resolve') {
286
+ let release = '';
287
+ for (let i = 3; i < filteredArgs.length; i++) {
288
+ if (filteredArgs[i] === '--release') release = filteredArgs[++i];
289
+ }
290
+ const body = { status: 'resolved' };
291
+ if (release) body.resolved_in_release = release;
292
+ const res = await request('PATCH', `/api/v1/issues/${issueId}`, body);
293
+ printOutput(res.json());
294
+ } else if (action === 'ignore') {
295
+ const res = await request('PATCH', `/api/v1/issues/${issueId}`, { status: 'ignored' });
296
+ printOutput(res.json());
297
+ } else if (action === 'reopen') {
298
+ const res = await request('PATCH', `/api/v1/issues/${issueId}`, { status: 'unresolved' });
299
+ printOutput(res.json());
300
+ } else if (action === 'snooze') {
301
+ let mode = 'hours';
302
+ for (let i = 3; i < filteredArgs.length; i++) {
303
+ if (filteredArgs[i] === '--mode') mode = filteredArgs[++i];
304
+ }
305
+ const res = await request('POST', `/api/v1/issues/${issueId}/snooze`, { mode });
306
+ printOutput(res.json());
307
+ } else {
308
+ throw new Error(`Unknown triage action: ${action}. Use: resolve, ignore, reopen, or snooze`);
309
+ }
310
+ break;
311
+ }
312
+
313
+ case 'alerts': {
314
+ let view = 'unread';
315
+ let project = '';
316
+ let limit = 20;
317
+
318
+ for (let i = 1; i < filteredArgs.length; i++) {
319
+ const a = filteredArgs[i];
320
+ if (a === '--view') view = filteredArgs[++i];
321
+ else if (a === '--project') project = filteredArgs[++i];
322
+ else if (a === '--limit') limit = parseInt(filteredArgs[++i], 10);
323
+ }
324
+
325
+ let path = `/api/v1/alerts?view=${view}&limit=${limit}`;
326
+ if (project) path += `&project_id=${encodeURIComponent(project)}`;
327
+
328
+ const res = await request('GET', path);
329
+ printOutput(res.json());
330
+ break;
331
+ }
332
+
333
+ case 'api': {
334
+ const method = (filteredArgs[1] || 'GET').toUpperCase();
335
+ const apiPath = filteredArgs[2];
336
+ if (!apiPath) throw new Error('Usage: epure api <METHOD> <PATH> [--body <json>]');
337
+
338
+ let body = null;
339
+ for (let i = 3; i < filteredArgs.length; i++) {
340
+ if (filteredArgs[i] === '--body') body = filteredArgs[++i];
341
+ }
342
+
343
+ const res = await request(method, apiPath, body);
344
+ printOutput(res.json());
345
+ break;
346
+ }
347
+
348
+ default:
349
+ console.error(`Unknown command: ${cmd}`);
350
+ printHelp();
351
+ process.exit(1);
352
+ }
353
+ }
354
+
355
+ handleCommand().catch((err) => {
356
+ console.error(`Error: ${err.message}`);
357
+ process.exit(1);
358
+ });
package/package.json CHANGED
@@ -1,6 +1,33 @@
1
1
  {
2
2
  "name": "@epure-sh/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.7",
4
+ "description": "Official CLI for Epure: error triage, issue queues, demangled stack traces, and Agent API inspection",
5
+ "bin": {
6
+ "epure": "./bin/epure-cli.js",
7
+ "epure-cli": "./bin/epure-cli.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18.0.0"
15
+ },
16
+ "keywords": [
17
+ "epure",
18
+ "cli",
19
+ "error-tracking",
20
+ "sentry",
21
+ "triage",
22
+ "monitoring",
23
+ "telemetry",
24
+ "developer-tools"
25
+ ],
26
+ "author": "Epure <team@epure.sh>",
27
+ "license": "Apache-2.0",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/epure-sh/epure.git",
31
+ "directory": "packages/epure-cli"
32
+ }
33
+ }