bugit-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +33 -0
- package/src/api.js +47 -0
- package/src/config.js +36 -0
- package/src/format.js +108 -0
- package/src/index.js +274 -0
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bugit-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "BugIt CLI — capture bugs from your terminal",
|
|
5
|
+
"author": "Golden Azubuike <goldenazubuike@gmail.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://bugit-dev.vercel.app",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/goldenazubuike/bugit"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["bug", "tracker", "cli", "developer-tools", "debugging"],
|
|
13
|
+
"bin": {
|
|
14
|
+
"bug": "./src/index.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"src/"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"start": "node src/index.js"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"chalk": "^5.3.0",
|
|
24
|
+
"cli-table3": "^0.6.3",
|
|
25
|
+
"commander": "^12.0.0",
|
|
26
|
+
"open": "^11.0.0",
|
|
27
|
+
"ora": "^8.0.1"
|
|
28
|
+
},
|
|
29
|
+
"type": "module",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { getToken, API_URL } from './config.js';
|
|
2
|
+
|
|
3
|
+
export class ApiError extends Error {
|
|
4
|
+
constructor(message, status) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.status = status;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function apiFetch(path, options = {}) {
|
|
11
|
+
const controller = new AbortController();
|
|
12
|
+
const timer = setTimeout(() => controller.abort(), 5000);
|
|
13
|
+
|
|
14
|
+
const token = getToken();
|
|
15
|
+
const headers = {
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
18
|
+
...(options.headers ?? {}),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch(`${API_URL}${path}`, {
|
|
23
|
+
...options,
|
|
24
|
+
signal: controller.signal,
|
|
25
|
+
headers,
|
|
26
|
+
});
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
|
|
29
|
+
if (res.status === 401) {
|
|
30
|
+
throw new ApiError('Not authenticated — run: bug login', 401);
|
|
31
|
+
}
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
let msg = `HTTP ${res.status}`;
|
|
34
|
+
try { const body = await res.json(); msg = body.message ?? msg; } catch {}
|
|
35
|
+
throw new ApiError(msg, res.status);
|
|
36
|
+
}
|
|
37
|
+
if (res.status === 204) return null;
|
|
38
|
+
return res.json();
|
|
39
|
+
} catch (err) {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
if (err.name === 'AbortError') {
|
|
42
|
+
throw new ApiError('API unreachable — is the BugIt server running?', 0);
|
|
43
|
+
}
|
|
44
|
+
if (err instanceof ApiError) throw err;
|
|
45
|
+
throw new ApiError(`Network error: ${err.message}`, 0);
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
export const API_URL = process.env.BUGIT_API_URL ?? 'https://bugit-j70c.onrender.com';
|
|
6
|
+
|
|
7
|
+
const CONFIG_PATH = join(homedir(), '.buglogrc');
|
|
8
|
+
|
|
9
|
+
function readConfig() {
|
|
10
|
+
if (!existsSync(CONFIG_PATH)) return {};
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
13
|
+
} catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function writeConfig(config) {
|
|
19
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getToken() {
|
|
23
|
+
return readConfig().token ?? null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function saveToken(token) {
|
|
27
|
+
const config = readConfig();
|
|
28
|
+
config.token = token;
|
|
29
|
+
writeConfig(config);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function clearToken() {
|
|
33
|
+
const config = readConfig();
|
|
34
|
+
delete config.token;
|
|
35
|
+
writeConfig(config);
|
|
36
|
+
}
|
package/src/format.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import Table from 'cli-table3';
|
|
3
|
+
|
|
4
|
+
const SEV_COLOR = {
|
|
5
|
+
critical: chalk.red,
|
|
6
|
+
high: chalk.yellow,
|
|
7
|
+
medium: chalk.blue,
|
|
8
|
+
low: chalk.gray,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const STATUS_COLOR = {
|
|
12
|
+
open: chalk.yellow,
|
|
13
|
+
'in-progress': chalk.blue,
|
|
14
|
+
resolved: chalk.green,
|
|
15
|
+
wontfix: chalk.gray,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function colorSev(s) {
|
|
19
|
+
return (SEV_COLOR[s] ?? chalk.white)(s);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function colorStatus(s) {
|
|
23
|
+
return (STATUS_COLOR[s] ?? chalk.white)(s);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function shortId(id) {
|
|
27
|
+
return String(id).slice(0, 6);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function truncate(str, len) {
|
|
31
|
+
if (!str) return '';
|
|
32
|
+
return str.length > len ? str.slice(0, len - 1) + '…' : str;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function printBugTable(bugs) {
|
|
36
|
+
const table = new Table({
|
|
37
|
+
head: ['ID', 'TITLE', 'PROJECT', 'SEV', 'STATUS', 'DATE'].map((h) =>
|
|
38
|
+
chalk.bold(h),
|
|
39
|
+
),
|
|
40
|
+
colWidths: [8, 42, 14, 10, 13, 12],
|
|
41
|
+
style: { compact: true },
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
for (const bug of bugs) {
|
|
45
|
+
const date = new Date(bug.createdAt).toLocaleDateString('en-GB', {
|
|
46
|
+
day: '2-digit',
|
|
47
|
+
month: 'short',
|
|
48
|
+
});
|
|
49
|
+
table.push([
|
|
50
|
+
chalk.dim(shortId(bug._id)),
|
|
51
|
+
truncate(bug.title, 40),
|
|
52
|
+
truncate(bug.project, 12),
|
|
53
|
+
colorSev(bug.severity),
|
|
54
|
+
colorStatus(bug.status),
|
|
55
|
+
date,
|
|
56
|
+
]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(table.toString());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function printBugDetail(bug, comments = []) {
|
|
63
|
+
const line = chalk.dim('─'.repeat(60));
|
|
64
|
+
console.log();
|
|
65
|
+
console.log(chalk.bold(bug.title));
|
|
66
|
+
console.log(line);
|
|
67
|
+
console.log(`${chalk.dim('ID')} ${shortId(bug._id)} (${bug._id})`);
|
|
68
|
+
console.log(`${chalk.dim('Project')} ${bug.project || '—'}`);
|
|
69
|
+
console.log(`${chalk.dim('Severity')} ${colorSev(bug.severity)}`);
|
|
70
|
+
console.log(`${chalk.dim('Status')} ${colorStatus(bug.status)}`);
|
|
71
|
+
console.log(`${chalk.dim('Environment')} ${bug.environment || '—'}`);
|
|
72
|
+
console.log(`${chalk.dim('Source')} ${bug.source}`);
|
|
73
|
+
console.log(`${chalk.dim('Tags')} ${bug.tags?.join(', ') || '—'}`);
|
|
74
|
+
console.log(
|
|
75
|
+
`${chalk.dim('Created')} ${new Date(bug.createdAt).toLocaleString()}`,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (bug.description) {
|
|
79
|
+
console.log();
|
|
80
|
+
console.log(chalk.bold('Description'));
|
|
81
|
+
console.log(bug.description);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (bug.notes) {
|
|
85
|
+
console.log();
|
|
86
|
+
console.log(chalk.bold('Notes'));
|
|
87
|
+
console.log(bug.notes);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (Object.keys(bug.metadata ?? {}).length) {
|
|
91
|
+
console.log();
|
|
92
|
+
console.log(chalk.bold('Metadata'));
|
|
93
|
+
console.log(JSON.stringify(bug.metadata, null, 2));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (comments.length) {
|
|
97
|
+
console.log();
|
|
98
|
+
console.log(chalk.bold(`Comments (${comments.length})`));
|
|
99
|
+
console.log(line);
|
|
100
|
+
for (const c of comments) {
|
|
101
|
+
console.log(
|
|
102
|
+
`${chalk.dim(new Date(c.createdAt).toLocaleString())} ${c.body}`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log();
|
|
108
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import open from 'open';
|
|
6
|
+
import { saveToken, clearToken, getToken, API_URL } from './config.js';
|
|
7
|
+
import { apiFetch, ApiError } from './api.js';
|
|
8
|
+
import { printBugTable, printBugDetail, colorSev, colorStatus, shortId } from './format.js';
|
|
9
|
+
|
|
10
|
+
function handleError(err) {
|
|
11
|
+
if (err instanceof ApiError) {
|
|
12
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
13
|
+
} else {
|
|
14
|
+
console.error(chalk.red(`Unexpected error: ${err.message}`));
|
|
15
|
+
}
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
program.name('bug').description('BugIt CLI — log and review bugs fast').version('1.0.0');
|
|
20
|
+
|
|
21
|
+
// bug log
|
|
22
|
+
program
|
|
23
|
+
.command('log <title>')
|
|
24
|
+
.description('Log a new bug')
|
|
25
|
+
.option('-p, --project <name>', 'project name')
|
|
26
|
+
.option('-s, --sev <level>', 'severity: low|medium|high|critical', 'medium')
|
|
27
|
+
.option('-e, --env <environment>', 'environment: local|staging|prod')
|
|
28
|
+
.option('-d, --desc <text>', 'description / steps to reproduce')
|
|
29
|
+
.option('-n, --notes <text>', 'root cause or fix notes')
|
|
30
|
+
.option('-t, --tags <tags>', 'comma-separated tags')
|
|
31
|
+
.option('--source <source>', 'source override', 'cli')
|
|
32
|
+
.action(async (title, opts) => {
|
|
33
|
+
const spinner = ora('Logging bug…').start();
|
|
34
|
+
try {
|
|
35
|
+
const body = {
|
|
36
|
+
title,
|
|
37
|
+
source: opts.source,
|
|
38
|
+
severity: opts.sev,
|
|
39
|
+
...(opts.project && { project: opts.project }),
|
|
40
|
+
...(opts.env && { environment: opts.env }),
|
|
41
|
+
...(opts.desc && { description: opts.desc }),
|
|
42
|
+
...(opts.notes && { notes: opts.notes }),
|
|
43
|
+
...(opts.tags && { tags: opts.tags.split(',').map((t) => t.trim()) }),
|
|
44
|
+
};
|
|
45
|
+
const bug = await apiFetch('/bugs', { method: 'POST', body: JSON.stringify(body) });
|
|
46
|
+
spinner.succeed(
|
|
47
|
+
`Bug logged ${chalk.dim(shortId(bug._id))} — ${colorSev(bug.severity)} ${chalk.bold(bug.title)}`,
|
|
48
|
+
);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
spinner.fail();
|
|
51
|
+
handleError(err);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// bug list
|
|
56
|
+
program
|
|
57
|
+
.command('list')
|
|
58
|
+
.description('List bugs')
|
|
59
|
+
.option('-p, --project <name>', 'filter by project')
|
|
60
|
+
.option('--status <status>', 'filter by status')
|
|
61
|
+
.option('--sev <level>', 'filter by severity')
|
|
62
|
+
.option('--limit <n>', 'number of results', '20')
|
|
63
|
+
.action(async (opts) => {
|
|
64
|
+
const spinner = ora('Fetching bugs…').start();
|
|
65
|
+
try {
|
|
66
|
+
const params = new URLSearchParams({ limit: opts.limit });
|
|
67
|
+
if (opts.project) params.set('project', opts.project);
|
|
68
|
+
if (opts.status) params.set('status', opts.status);
|
|
69
|
+
if (opts.sev) params.set('severity', opts.sev);
|
|
70
|
+
|
|
71
|
+
const data = await apiFetch(`/bugs?${params}`);
|
|
72
|
+
spinner.stop();
|
|
73
|
+
|
|
74
|
+
if (!data.bugs.length) {
|
|
75
|
+
console.log(chalk.dim('No bugs found.'));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
printBugTable(data.bugs);
|
|
80
|
+
console.log(chalk.dim(`Showing ${data.bugs.length} of ${data.total}`));
|
|
81
|
+
} catch (err) {
|
|
82
|
+
spinner.fail();
|
|
83
|
+
handleError(err);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// bug view
|
|
88
|
+
program
|
|
89
|
+
.command('view <id>')
|
|
90
|
+
.description('View full bug detail')
|
|
91
|
+
.action(async (id) => {
|
|
92
|
+
const spinner = ora('Fetching bug…').start();
|
|
93
|
+
try {
|
|
94
|
+
const [bug, comments] = await Promise.all([
|
|
95
|
+
apiFetch(`/bugs/${id}`),
|
|
96
|
+
apiFetch(`/bugs/${id}/comments`),
|
|
97
|
+
]);
|
|
98
|
+
spinner.stop();
|
|
99
|
+
printBugDetail(bug, comments);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
spinner.fail();
|
|
102
|
+
handleError(err);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// bug update
|
|
107
|
+
program
|
|
108
|
+
.command('update <id>')
|
|
109
|
+
.description('Update a bug')
|
|
110
|
+
.option('--status <status>', 'new status')
|
|
111
|
+
.option('--sev <level>', 'new severity')
|
|
112
|
+
.option('--notes <text>', 'replace notes')
|
|
113
|
+
.option('--tags <tags>', 'replace tags (comma-separated)')
|
|
114
|
+
.action(async (id, opts) => {
|
|
115
|
+
const spinner = ora('Updating bug…').start();
|
|
116
|
+
try {
|
|
117
|
+
const body = {
|
|
118
|
+
...(opts.status && { status: opts.status }),
|
|
119
|
+
...(opts.sev && { severity: opts.sev }),
|
|
120
|
+
...(opts.notes && { notes: opts.notes }),
|
|
121
|
+
...(opts.tags && { tags: opts.tags.split(',').map((t) => t.trim()) }),
|
|
122
|
+
};
|
|
123
|
+
if (!Object.keys(body).length) {
|
|
124
|
+
spinner.fail('No fields to update.');
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
const bug = await apiFetch(`/bugs/${id}`, { method: 'PATCH', body: JSON.stringify(body) });
|
|
128
|
+
spinner.succeed(
|
|
129
|
+
`Updated ${chalk.dim(shortId(bug._id))} — status: ${colorStatus(bug.status)}, sev: ${colorSev(bug.severity)}`,
|
|
130
|
+
);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
spinner.fail();
|
|
133
|
+
handleError(err);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// bug resolve
|
|
138
|
+
program
|
|
139
|
+
.command('resolve <id>')
|
|
140
|
+
.description('Mark bug as resolved')
|
|
141
|
+
.action(async (id) => {
|
|
142
|
+
const spinner = ora('Resolving…').start();
|
|
143
|
+
try {
|
|
144
|
+
await apiFetch(`/bugs/${id}`, { method: 'PATCH', body: JSON.stringify({ status: 'resolved' }) });
|
|
145
|
+
spinner.succeed(chalk.green(`Bug ${shortId(id)} resolved.`));
|
|
146
|
+
} catch (err) {
|
|
147
|
+
spinner.fail();
|
|
148
|
+
handleError(err);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// bug wontfix
|
|
153
|
+
program
|
|
154
|
+
.command('wontfix <id>')
|
|
155
|
+
.description("Mark bug as won't fix")
|
|
156
|
+
.action(async (id) => {
|
|
157
|
+
const spinner = ora('Updating…').start();
|
|
158
|
+
try {
|
|
159
|
+
await apiFetch(`/bugs/${id}`, { method: 'PATCH', body: JSON.stringify({ status: 'wontfix' }) });
|
|
160
|
+
spinner.succeed(chalk.gray(`Bug ${shortId(id)} marked as wontfix.`));
|
|
161
|
+
} catch (err) {
|
|
162
|
+
spinner.fail();
|
|
163
|
+
handleError(err);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// bug pipe
|
|
168
|
+
program
|
|
169
|
+
.command('pipe <title>')
|
|
170
|
+
.description('Pipe stdin as bug description')
|
|
171
|
+
.option('-p, --project <name>', 'project name')
|
|
172
|
+
.option('-s, --sev <level>', 'severity', 'high')
|
|
173
|
+
.option('-e, --env <environment>', 'environment')
|
|
174
|
+
.option('-t, --tags <tags>', 'comma-separated tags')
|
|
175
|
+
.option('--source <source>', 'source override', 'cli')
|
|
176
|
+
.action(async (title, opts) => {
|
|
177
|
+
let piped = '';
|
|
178
|
+
if (!process.stdin.isTTY) {
|
|
179
|
+
for await (const chunk of process.stdin) piped += chunk;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const spinner = ora('Logging bug from stdin…').start();
|
|
183
|
+
try {
|
|
184
|
+
const body = {
|
|
185
|
+
title,
|
|
186
|
+
source: opts.source,
|
|
187
|
+
severity: opts.sev,
|
|
188
|
+
description: piped.trim(),
|
|
189
|
+
...(opts.project && { project: opts.project }),
|
|
190
|
+
...(opts.env && { environment: opts.env }),
|
|
191
|
+
...(opts.tags && { tags: opts.tags.split(',').map((t) => t.trim()) }),
|
|
192
|
+
};
|
|
193
|
+
const bug = await apiFetch('/bugs', { method: 'POST', body: JSON.stringify(body) });
|
|
194
|
+
spinner.succeed(
|
|
195
|
+
`Piped bug logged ${chalk.dim(shortId(bug._id))} — ${colorSev(bug.severity)} ${chalk.bold(bug.title)}`,
|
|
196
|
+
);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
spinner.fail();
|
|
199
|
+
handleError(err);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// bug login — device flow
|
|
204
|
+
program
|
|
205
|
+
.command('login')
|
|
206
|
+
.description('Authenticate via browser')
|
|
207
|
+
.action(async () => {
|
|
208
|
+
const spinner = ora('Starting authentication…').start();
|
|
209
|
+
let code, url;
|
|
210
|
+
try {
|
|
211
|
+
const data = await apiFetch('/auth/cli/init', { method: 'POST' });
|
|
212
|
+
code = data.code;
|
|
213
|
+
url = data.url;
|
|
214
|
+
} catch (err) {
|
|
215
|
+
spinner.fail();
|
|
216
|
+
handleError(err);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
spinner.succeed('Opening browser…');
|
|
221
|
+
console.log(chalk.dim(`If the browser doesn't open, visit:\n${url}`));
|
|
222
|
+
await open(url);
|
|
223
|
+
|
|
224
|
+
const pollSpinner = ora('Waiting for browser authentication…').start();
|
|
225
|
+
const deadline = Date.now() + 5 * 60 * 1000;
|
|
226
|
+
|
|
227
|
+
while (Date.now() < deadline) {
|
|
228
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
229
|
+
try {
|
|
230
|
+
const result = await apiFetch(`/auth/cli/session/${code}`);
|
|
231
|
+
if (result.status === 'approved') {
|
|
232
|
+
saveToken(result.token);
|
|
233
|
+
pollSpinner.succeed(chalk.green('Authenticated! You can now use the bug CLI.'));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
} catch (err) {
|
|
237
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
238
|
+
pollSpinner.fail('Session expired. Run bug login again.');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
pollSpinner.fail('Timed out waiting for authentication.');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// bug logout
|
|
248
|
+
program
|
|
249
|
+
.command('logout')
|
|
250
|
+
.description('Sign out and remove stored token')
|
|
251
|
+
.action(() => {
|
|
252
|
+
clearToken();
|
|
253
|
+
console.log(chalk.dim('Signed out. Run bug login to authenticate again.'));
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// bug whoami
|
|
257
|
+
program
|
|
258
|
+
.command('whoami')
|
|
259
|
+
.description('Show authentication status')
|
|
260
|
+
.action(async () => {
|
|
261
|
+
const token = getToken();
|
|
262
|
+
if (!token) {
|
|
263
|
+
console.log(chalk.yellow('Not authenticated. Run: bug login'));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const { email } = await apiFetch('/auth/me');
|
|
268
|
+
console.log(chalk.green(email) + chalk.dim(` → ${API_URL}`));
|
|
269
|
+
} catch {
|
|
270
|
+
console.log(chalk.yellow('Token stored but could not reach API.'));
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
program.parse();
|