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 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();