feedbackbasket-cli 0.3.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.
Files changed (63) hide show
  1. package/README.md +215 -0
  2. package/dist/bin/feedbackbasket.d.ts +2 -0
  3. package/dist/bin/feedbackbasket.js +3 -0
  4. package/dist/src/auth/login.d.ts +6 -0
  5. package/dist/src/auth/login.js +111 -0
  6. package/dist/src/auth/manager.d.ts +11 -0
  7. package/dist/src/auth/manager.js +38 -0
  8. package/dist/src/cli.d.ts +1 -0
  9. package/dist/src/cli.js +79 -0
  10. package/dist/src/client.d.ts +105 -0
  11. package/dist/src/client.js +133 -0
  12. package/dist/src/commands/auth.d.ts +5 -0
  13. package/dist/src/commands/auth.js +313 -0
  14. package/dist/src/commands/bugs.d.ts +3 -0
  15. package/dist/src/commands/bugs.js +120 -0
  16. package/dist/src/commands/doctor.d.ts +3 -0
  17. package/dist/src/commands/doctor.js +130 -0
  18. package/dist/src/commands/feedback-bulk-update.d.ts +3 -0
  19. package/dist/src/commands/feedback-bulk-update.js +39 -0
  20. package/dist/src/commands/feedback-delete.d.ts +3 -0
  21. package/dist/src/commands/feedback-delete.js +45 -0
  22. package/dist/src/commands/feedback-export.d.ts +3 -0
  23. package/dist/src/commands/feedback-export.js +43 -0
  24. package/dist/src/commands/feedback-note.d.ts +3 -0
  25. package/dist/src/commands/feedback-note.js +41 -0
  26. package/dist/src/commands/feedback-update.d.ts +3 -0
  27. package/dist/src/commands/feedback-update.js +54 -0
  28. package/dist/src/commands/feedback.d.ts +3 -0
  29. package/dist/src/commands/feedback.js +201 -0
  30. package/dist/src/commands/projects.d.ts +3 -0
  31. package/dist/src/commands/projects.js +218 -0
  32. package/dist/src/commands/setup.d.ts +3 -0
  33. package/dist/src/commands/setup.js +90 -0
  34. package/dist/src/commands/team.d.ts +3 -0
  35. package/dist/src/commands/team.js +112 -0
  36. package/dist/src/commands/widget.d.ts +3 -0
  37. package/dist/src/commands/widget.js +153 -0
  38. package/dist/src/config/config.d.ts +10 -0
  39. package/dist/src/config/config.js +41 -0
  40. package/dist/src/config/credentials.d.ts +12 -0
  41. package/dist/src/config/credentials.js +39 -0
  42. package/dist/src/output/codes.d.ts +16 -0
  43. package/dist/src/output/codes.js +29 -0
  44. package/dist/src/output/envelope.d.ts +24 -0
  45. package/dist/src/output/envelope.js +2 -0
  46. package/dist/src/output/errors.d.ts +13 -0
  47. package/dist/src/output/errors.js +37 -0
  48. package/dist/src/output/styled.d.ts +4 -0
  49. package/dist/src/output/styled.js +78 -0
  50. package/dist/src/output/theme.d.ts +25 -0
  51. package/dist/src/output/theme.js +43 -0
  52. package/dist/src/output/writer.d.ts +23 -0
  53. package/dist/src/output/writer.js +109 -0
  54. package/dist/src/prompt.d.ts +3 -0
  55. package/dist/src/prompt.js +32 -0
  56. package/dist/src/resolve.d.ts +9 -0
  57. package/dist/src/resolve.js +70 -0
  58. package/dist/src/types.d.ts +100 -0
  59. package/dist/src/types.js +2 -0
  60. package/dist/src/version.d.ts +2 -0
  61. package/dist/src/version.js +2 -0
  62. package/package.json +49 -0
  63. package/skills/feedbackbasket/SKILL.md +132 -0
@@ -0,0 +1,133 @@
1
+ import axios, { AxiosError } from 'axios';
2
+ import { USER_AGENT } from './version.js';
3
+ import { errAuth, errForbidden, errRateLimit, errNetwork, errAPI } from './output/errors.js';
4
+ export class FeedbackBasketClient {
5
+ http;
6
+ constructor(token, baseUrl) {
7
+ this.http = axios.create({
8
+ baseURL: `${baseUrl}/api/v1`,
9
+ timeout: 30_000,
10
+ headers: {
11
+ 'Authorization': `Bearer ${token}`,
12
+ 'Content-Type': 'application/json',
13
+ 'User-Agent': USER_AGENT,
14
+ },
15
+ });
16
+ }
17
+ async me() {
18
+ return this.request('GET', '/auth/me');
19
+ }
20
+ async listProjects() {
21
+ return this.request('GET', '/projects');
22
+ }
23
+ async getProject(id) {
24
+ return this.request('GET', `/projects/${encodeURIComponent(id)}`);
25
+ }
26
+ async createProject(data) {
27
+ return this.request('POST', '/projects', data);
28
+ }
29
+ async updateProject(id, data) {
30
+ return this.request('PATCH', `/projects/${encodeURIComponent(id)}`, data);
31
+ }
32
+ async deleteProject(id) {
33
+ return this.request('DELETE', `/projects/${encodeURIComponent(id)}`);
34
+ }
35
+ async getFeedback(params = {}) {
36
+ const query = buildQuery(params);
37
+ return this.request('GET', `/feedback${query}`);
38
+ }
39
+ async getFeedbackById(id) {
40
+ return this.request('GET', `/feedback/${encodeURIComponent(id)}`);
41
+ }
42
+ async getBugReports(params = {}) {
43
+ const query = buildQuery(params);
44
+ return this.request('GET', `/feedback/bugs${query}`);
45
+ }
46
+ async getBugStats(params = {}) {
47
+ const query = params.projectId ? `?projectId=${encodeURIComponent(params.projectId)}` : '';
48
+ return this.request('GET', `/feedback/bugs/stats${query}`);
49
+ }
50
+ async searchFeedback(query, opts = {}) {
51
+ return this.getFeedback({ search: query, ...opts });
52
+ }
53
+ // Widget
54
+ async getWidgetSettings(projectId) {
55
+ return this.request('GET', `/projects/${encodeURIComponent(projectId)}/widget`);
56
+ }
57
+ async updateWidgetSettings(projectId, settings) {
58
+ return this.request('PATCH', `/projects/${encodeURIComponent(projectId)}/widget`, settings);
59
+ }
60
+ async getWidgetScript(projectId) {
61
+ return this.request('GET', `/projects/${encodeURIComponent(projectId)}/widget-script`);
62
+ }
63
+ // Write operations
64
+ async updateFeedback(id, data) {
65
+ return this.request('PATCH', `/feedback/${encodeURIComponent(id)}`, data);
66
+ }
67
+ async createNote(feedbackId, content) {
68
+ return this.request('POST', `/feedback/${encodeURIComponent(feedbackId)}/notes`, { content });
69
+ }
70
+ async deleteFeedback(id) {
71
+ return this.request('DELETE', `/feedback/${encodeURIComponent(id)}`);
72
+ }
73
+ async bulkUpdateStatus(ids, status) {
74
+ return this.request('POST', '/feedback/bulk-update', { ids, status });
75
+ }
76
+ async updateNote(feedbackId, noteId, content) {
77
+ return this.request('PATCH', `/feedback/${encodeURIComponent(feedbackId)}/notes/${encodeURIComponent(noteId)}`, { content });
78
+ }
79
+ async deleteNote(feedbackId, noteId) {
80
+ return this.request('DELETE', `/feedback/${encodeURIComponent(feedbackId)}/notes/${encodeURIComponent(noteId)}`);
81
+ }
82
+ async exportFeedback(projectId, format = 'csv') {
83
+ const response = await this.http.get(`/projects/${encodeURIComponent(projectId)}/export?format=${format}`);
84
+ return typeof response.data === 'string' ? response.data : JSON.stringify(response.data, null, 2);
85
+ }
86
+ // Team
87
+ async listTeam() {
88
+ return this.request('GET', '/team');
89
+ }
90
+ async updateMemberRole(memberId, role) {
91
+ return this.request('PATCH', `/team/${encodeURIComponent(memberId)}`, { role });
92
+ }
93
+ async removeMember(memberId) {
94
+ return this.request('DELETE', `/team/${encodeURIComponent(memberId)}`);
95
+ }
96
+ async request(method, path, data) {
97
+ try {
98
+ const response = await this.http.request({ method, url: path, data });
99
+ return response.data;
100
+ }
101
+ catch (error) {
102
+ throw this.handleError(error);
103
+ }
104
+ }
105
+ handleError(error) {
106
+ if (error instanceof AxiosError) {
107
+ const status = error.response?.status;
108
+ const message = error.response?.data?.error
109
+ ?? error.response?.data?.message
110
+ ?? error.message;
111
+ if (!error.response) {
112
+ return errNetwork(error);
113
+ }
114
+ switch (status) {
115
+ case 401: return errAuth(message);
116
+ case 403: return errForbidden(message);
117
+ case 404: return errAPI(404, message);
118
+ case 429: return errRateLimit();
119
+ default: return errAPI(status ?? 500, message);
120
+ }
121
+ }
122
+ return error instanceof Error ? error : new Error(String(error));
123
+ }
124
+ }
125
+ function buildQuery(params) {
126
+ const parts = [];
127
+ for (const [key, value] of Object.entries(params)) {
128
+ if (value !== undefined && value !== null) {
129
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
130
+ }
131
+ }
132
+ return parts.length > 0 ? `?${parts.join('&')}` : '';
133
+ }
@@ -0,0 +1,5 @@
1
+ import { Command } from 'commander';
2
+ import type { OutputWriter } from '../output/writer.js';
3
+ export declare function createAuthCommand(getWriter: () => OutputWriter): Command;
4
+ export declare function createLoginCommand(getWriter: () => OutputWriter): Command;
5
+ export declare function createLogoutCommand(getWriter: () => OutputWriter): Command;
@@ -0,0 +1,313 @@
1
+ import { Command } from 'commander';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ import { AuthManager } from '../auth/manager.js';
6
+ import { browserLogin } from '../auth/login.js';
7
+ import { saveCredentials } from '../config/credentials.js';
8
+ import { loadConfig, saveConfig } from '../config/config.js';
9
+ import { FeedbackBasketClient } from '../client.js';
10
+ import { errAuth, errUsage } from '../output/errors.js';
11
+ import { brand, divider, logo } from '../output/theme.js';
12
+ import { select, confirm } from '../prompt.js';
13
+ export function createAuthCommand(getWriter) {
14
+ const auth = new Command('auth')
15
+ .description('Manage authentication');
16
+ // --- auth login ---
17
+ auth
18
+ .command('login')
19
+ .description('Authenticate with FeedbackBasket')
20
+ .option('--token <token>', 'Use an API token directly (for CI/headless)')
21
+ .option('--scope <scope>', 'Access scope: read or full', 'full')
22
+ .action(async (opts) => {
23
+ const writer = getWriter();
24
+ const config = loadConfig();
25
+ const scope = opts.scope === 'read' ? 'read' : 'full';
26
+ const isInteractive = !writer.isMachineOutput() && process.stdin.isTTY;
27
+ // ── Step 1: Authentication ──
28
+ if (isInteractive) {
29
+ console.log();
30
+ console.log(` ${logo()} CLI`);
31
+ console.log();
32
+ console.log(brand.muted(` Step 1: Authentication`));
33
+ console.log();
34
+ }
35
+ let token;
36
+ if (opts.token) {
37
+ token = opts.token;
38
+ }
39
+ else {
40
+ const result = await browserLogin(config.baseUrl, scope);
41
+ token = result.token;
42
+ }
43
+ // Verify token + get profile
44
+ const client = new FeedbackBasketClient(token, config.baseUrl);
45
+ let email;
46
+ let userId;
47
+ let organizationId;
48
+ try {
49
+ const profile = await client.me();
50
+ email = profile.email;
51
+ userId = profile.id;
52
+ organizationId = profile.organizationId;
53
+ }
54
+ catch (err) {
55
+ if (isInteractive) {
56
+ const msg = err instanceof Error ? err.message : String(err);
57
+ console.log(brand.warning(` Could not verify token: ${msg}`));
58
+ console.log(brand.muted(' Token saved — run feedbackbasket doctor to diagnose'));
59
+ console.log();
60
+ }
61
+ }
62
+ saveCredentials({
63
+ token,
64
+ scope,
65
+ userId,
66
+ email,
67
+ organizationId,
68
+ createdAt: new Date().toISOString(),
69
+ });
70
+ if (isInteractive) {
71
+ console.log(` ${brand.success('✓')} Authentication successful`);
72
+ if (email)
73
+ console.log(` ${brand.muted('Logged in as')} ${brand.bold(email)}`);
74
+ console.log();
75
+ }
76
+ // ── Step 2: Default Project (interactive only, first login) ──
77
+ let defaultProjectName;
78
+ if (isInteractive && !config.defaultProject) {
79
+ console.log();
80
+ console.log(brand.muted(` Step 2: Default Project (optional)`));
81
+ console.log();
82
+ try {
83
+ const projectsRes = await client.listProjects();
84
+ const projects = projectsRes.projects;
85
+ if (projects.length === 0) {
86
+ console.log(brand.muted(' No projects found. Create one from the dashboard.'));
87
+ console.log();
88
+ }
89
+ else if (projects.length === 1) {
90
+ // Auto-select single project
91
+ const p = projects[0];
92
+ config.defaultProject = p.id;
93
+ saveConfig(config);
94
+ defaultProjectName = p.name;
95
+ console.log(` ${brand.success('✓')} Default project: ${brand.bold(p.name)}`);
96
+ console.log(` ${brand.muted('Only project in your organization')}`);
97
+ console.log();
98
+ }
99
+ else {
100
+ console.log(` You have ${brand.bold(String(projects.length))} projects:\n`);
101
+ const choice = await select('Select default project', projects.map(p => {
102
+ const count = brand.muted(`(${p.totalFeedback} feedback)`);
103
+ return `${p.name} ${count}`;
104
+ }));
105
+ if (choice !== null) {
106
+ const p = projects[choice];
107
+ config.defaultProject = p.id;
108
+ saveConfig(config);
109
+ defaultProjectName = p.name;
110
+ console.log();
111
+ console.log(` ${brand.success('✓')} Default project: ${brand.bold(p.name)}`);
112
+ console.log(` ${brand.muted('Use --project to override per-command')}`);
113
+ }
114
+ else {
115
+ console.log();
116
+ console.log(` ${brand.muted('Skipped. Use --project or: feedbackbasket config set defaultProject <id>')}`);
117
+ }
118
+ console.log();
119
+ }
120
+ }
121
+ catch (err) {
122
+ const msg = err instanceof Error ? err.message : String(err);
123
+ console.log(brand.muted(` Could not fetch projects: ${msg}`));
124
+ console.log(brand.muted(' You can set a default later: feedbackbasket config set defaultProject <id>'));
125
+ console.log();
126
+ }
127
+ }
128
+ // ── Step 3: Agent Setup (interactive only) ──
129
+ let agentInstalled = false;
130
+ if (isInteractive) {
131
+ const claudeDir = join(homedir(), '.claude');
132
+ const skillPath = join(claudeDir, 'skills', 'feedbackbasket', 'SKILL.md');
133
+ const claudeExists = existsSync(claudeDir);
134
+ const skillExists = existsSync(skillPath);
135
+ if (claudeExists && !skillExists) {
136
+ console.log();
137
+ console.log(brand.muted(` Step 3: Agent Setup`));
138
+ console.log();
139
+ console.log(` ${brand.primary('Detected:')} Claude Code`);
140
+ console.log();
141
+ console.log(` This will:`);
142
+ console.log(` 1. Install FeedbackBasket skill to ~/.claude/skills/`);
143
+ console.log();
144
+ const doSetup = await confirm(' Set up for Claude Code?');
145
+ if (doSetup) {
146
+ try {
147
+ const { mkdirSync, copyFileSync } = await import('node:fs');
148
+ const { dirname } = await import('node:path');
149
+ const { fileURLToPath } = await import('node:url');
150
+ const thisDir = dirname(fileURLToPath(import.meta.url));
151
+ const projectRoot = join(thisDir, '..', '..', '..');
152
+ let skillSrc = join(projectRoot, 'skills', 'feedbackbasket', 'SKILL.md');
153
+ if (!existsSync(skillSrc)) {
154
+ skillSrc = join(thisDir, '..', '..', 'skills', 'feedbackbasket', 'SKILL.md');
155
+ }
156
+ if (existsSync(skillSrc)) {
157
+ const skillDir = join(claudeDir, 'skills', 'feedbackbasket');
158
+ mkdirSync(skillDir, { recursive: true });
159
+ copyFileSync(skillSrc, skillPath);
160
+ agentInstalled = true;
161
+ console.log(` ${brand.success('✓')} Agent skill installed`);
162
+ }
163
+ else {
164
+ console.log(` ${brand.error('✗')} SKILL.md not found in package`);
165
+ }
166
+ }
167
+ catch {
168
+ console.log(` ${brand.error('✗')} Failed to install skill`);
169
+ }
170
+ }
171
+ else {
172
+ console.log(brand.muted(' Skipped. Run later: feedbackbasket setup claude'));
173
+ }
174
+ console.log();
175
+ }
176
+ else if (claudeExists && skillExists) {
177
+ agentInstalled = true;
178
+ }
179
+ }
180
+ // ── Summary ──
181
+ if (isInteractive) {
182
+ console.log();
183
+ console.log(divider(35));
184
+ console.log(brand.bold(' Setup complete!'));
185
+ console.log(divider(35));
186
+ console.log();
187
+ console.log(` ${brand.success('✓')} Authenticated${email ? ` as ${email}` : ''}`);
188
+ if (defaultProjectName) {
189
+ console.log(` ${brand.success('✓')} Default project: ${defaultProjectName}`);
190
+ }
191
+ else if (config.defaultProject) {
192
+ console.log(` ${brand.success('✓')} Default project: ${config.defaultProject}`);
193
+ }
194
+ else {
195
+ console.log(` ${brand.muted('-')} No default project`);
196
+ }
197
+ if (agentInstalled) {
198
+ console.log(` ${brand.success('✓')} Claude Code skill`);
199
+ }
200
+ console.log();
201
+ console.log(' Try these commands:');
202
+ console.log();
203
+ console.log(` ${brand.command('feedbackbasket projects list')} List your projects`);
204
+ console.log(` ${brand.command('feedbackbasket feedback list')} View recent feedback`);
205
+ console.log(` ${brand.command('feedbackbasket bugs list')} View bug reports`);
206
+ console.log(` ${brand.command('feedbackbasket doctor')} Run diagnostics`);
207
+ console.log();
208
+ return; // Skip the JSON output in interactive wizard mode
209
+ }
210
+ // Machine output
211
+ writer.ok({ authenticated: true, email, scope, defaultProject: config.defaultProject }, {
212
+ summary: email ? `Logged in as ${email}` : 'Authenticated successfully',
213
+ breadcrumbs: [
214
+ { action: 'List projects', cmd: 'feedbackbasket projects list' },
215
+ { action: 'Check status', cmd: 'feedbackbasket auth status' },
216
+ { action: 'Run diagnostics', cmd: 'feedbackbasket doctor' },
217
+ ],
218
+ });
219
+ });
220
+ // --- auth logout ---
221
+ auth
222
+ .command('logout')
223
+ .description('Clear stored credentials')
224
+ .action(() => {
225
+ const writer = getWriter();
226
+ const manager = new AuthManager();
227
+ if (!manager.isAuthenticated()) {
228
+ writer.ok({ authenticated: false }, { summary: 'Already logged out' });
229
+ return;
230
+ }
231
+ if (manager.getSource() === 'env') {
232
+ throw errUsage('Credentials are set via FEEDBACKBASKET_TOKEN environment variable', 'Unset the variable: unset FEEDBACKBASKET_TOKEN');
233
+ }
234
+ manager.logout();
235
+ if (!writer.isMachineOutput()) {
236
+ console.log(` ${brand.success('✓')} Logged out`);
237
+ console.log();
238
+ }
239
+ writer.ok({ authenticated: false }, {
240
+ summary: 'Logged out',
241
+ breadcrumbs: [
242
+ { action: 'Log back in', cmd: 'feedbackbasket auth login' },
243
+ ],
244
+ });
245
+ });
246
+ // --- auth status ---
247
+ auth
248
+ .command('status')
249
+ .description('Show authentication status')
250
+ .action(() => {
251
+ const writer = getWriter();
252
+ const manager = new AuthManager();
253
+ const creds = manager.getCredentials();
254
+ if (!creds) {
255
+ throw errAuth('Not authenticated');
256
+ }
257
+ const config = loadConfig();
258
+ const data = {
259
+ authenticated: true,
260
+ source: manager.getSource(),
261
+ token: manager.getTokenPreview(),
262
+ scope: creds.scope,
263
+ email: creds.email ?? null,
264
+ organizationId: creds.organizationId ?? null,
265
+ defaultProject: config.defaultProject ?? null,
266
+ };
267
+ writer.ok(data, {
268
+ summary: `Authenticated${creds.email ? ` as ${creds.email}` : ''}`,
269
+ breadcrumbs: [
270
+ { action: 'List projects', cmd: 'feedbackbasket projects list' },
271
+ { action: 'Log out', cmd: 'feedbackbasket auth logout' },
272
+ ],
273
+ });
274
+ });
275
+ // --- auth token ---
276
+ auth
277
+ .command('token')
278
+ .description('Print the current access token (for scripting)')
279
+ .action(() => {
280
+ const manager = new AuthManager();
281
+ const token = manager.resolveToken();
282
+ if (!token) {
283
+ throw errAuth('Not authenticated');
284
+ }
285
+ process.stdout.write(token);
286
+ });
287
+ return auth;
288
+ }
289
+ // Top-level aliases: `feedbackbasket login` and `feedbackbasket logout`
290
+ export function createLoginCommand(getWriter) {
291
+ return new Command('login')
292
+ .description('Authenticate with FeedbackBasket (alias for auth login)')
293
+ .option('--token <token>', 'Use an API token directly (for CI/headless)')
294
+ .option('--scope <scope>', 'Access scope: read or full', 'full')
295
+ .action(async (opts) => {
296
+ // Delegate to auth login by re-parsing
297
+ const authCmd = createAuthCommand(getWriter);
298
+ const args = ['node', 'feedbackbasket', 'login'];
299
+ if (opts.token)
300
+ args.push('--token', opts.token);
301
+ if (opts.scope)
302
+ args.push('--scope', opts.scope);
303
+ await authCmd.parseAsync(args);
304
+ });
305
+ }
306
+ export function createLogoutCommand(getWriter) {
307
+ return new Command('logout')
308
+ .description('Clear stored credentials (alias for auth logout)')
309
+ .action(async () => {
310
+ const authCmd = createAuthCommand(getWriter);
311
+ await authCmd.parseAsync(['node', 'feedbackbasket', 'logout']);
312
+ });
313
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import type { OutputWriter } from '../output/writer.js';
3
+ export declare function createBugsCommand(getWriter: () => OutputWriter): Command;
@@ -0,0 +1,120 @@
1
+ import { Command } from 'commander';
2
+ import { FeedbackBasketClient } from '../client.js';
3
+ import { AuthManager } from '../auth/manager.js';
4
+ import { loadConfig } from '../config/config.js';
5
+ import { errAuth } from '../output/errors.js';
6
+ import { brand, divider } from '../output/theme.js';
7
+ function resolveProjectId(optProject) {
8
+ if (optProject)
9
+ return optProject;
10
+ const config = loadConfig();
11
+ return config.defaultProject;
12
+ }
13
+ export function createBugsCommand(getWriter) {
14
+ const bugs = new Command('bugs')
15
+ .description('View bug reports');
16
+ bugs
17
+ .command('list')
18
+ .description('List bug reports with severity classification')
19
+ .option('--project <id>', 'Filter by project ID')
20
+ .option('--severity <level>', 'Filter by severity (high, medium, low)')
21
+ .option('--status <status>', 'Filter by status')
22
+ .option('--search <query>', 'Search bug content')
23
+ .option('--limit <n>', 'Max results (1-100)', '20')
24
+ .option('--offset <n>', 'Offset for pagination', '0')
25
+ .option('--notes', 'Include internal notes')
26
+ .action(async (opts) => {
27
+ const writer = getWriter();
28
+ const client = requireClient();
29
+ const result = await client.getBugReports({
30
+ projectId: resolveProjectId(opts.project),
31
+ status: opts.status,
32
+ severity: opts.severity,
33
+ search: opts.search,
34
+ limit: parseInt(opts.limit, 10),
35
+ offset: parseInt(opts.offset, 10),
36
+ includeNotes: opts.notes ?? false,
37
+ });
38
+ if (!writer.isMachineOutput()) {
39
+ renderBugStats(result.stats);
40
+ console.log();
41
+ renderBugList(result.bugReports);
42
+ }
43
+ const breadcrumbs = [];
44
+ if (!opts.severity) {
45
+ breadcrumbs.push({ action: 'High severity only', cmd: `feedbackbasket bugs list --severity high${opts.project ? ` --project ${opts.project}` : ''}` });
46
+ }
47
+ if (result.pagination.hasMore) {
48
+ const nextOffset = parseInt(opts.offset, 10) + parseInt(opts.limit, 10);
49
+ breadcrumbs.push({ action: 'Next page', cmd: `feedbackbasket bugs list --offset ${nextOffset}${opts.project ? ` --project ${opts.project}` : ''}` });
50
+ }
51
+ breadcrumbs.push({ action: 'All feedback', cmd: 'feedbackbasket feedback list' });
52
+ writer.ok(result.bugReports, {
53
+ summary: `${result.stats.total} bug${result.stats.total === 1 ? '' : 's'} (${result.stats.bySeverity.high} high, ${result.stats.bySeverity.medium} medium, ${result.stats.bySeverity.low} low)`,
54
+ notice: result.pagination.hasMore ? `Showing ${result.bugReports.length} of ${result.pagination.totalCount}` : undefined,
55
+ breadcrumbs,
56
+ });
57
+ });
58
+ bugs
59
+ .command('stats')
60
+ .description('Show bug statistics summary')
61
+ .option('--project <id>', 'Filter by project ID')
62
+ .action(async (opts) => {
63
+ const writer = getWriter();
64
+ const client = requireClient();
65
+ const stats = await client.getBugStats({ projectId: opts.project });
66
+ if (!writer.isMachineOutput()) {
67
+ renderBugStats(stats);
68
+ }
69
+ writer.ok(stats, {
70
+ summary: `${stats.total} total bugs`,
71
+ breadcrumbs: [
72
+ { action: 'List all bugs', cmd: 'feedbackbasket bugs list' },
73
+ { action: 'High severity', cmd: 'feedbackbasket bugs list --severity high' },
74
+ ],
75
+ });
76
+ });
77
+ return bugs;
78
+ }
79
+ function requireClient() {
80
+ const manager = new AuthManager();
81
+ const token = manager.resolveToken();
82
+ if (!token)
83
+ throw errAuth();
84
+ const config = loadConfig();
85
+ return new FeedbackBasketClient(token, config.baseUrl);
86
+ }
87
+ function renderBugStats(stats) {
88
+ console.log(brand.bold('Bug Summary'));
89
+ console.log(divider(30));
90
+ console.log(` Total: ${brand.bold(String(stats.total))}`);
91
+ console.log(` ${brand.high('High')}: ${stats.bySeverity.high}`);
92
+ console.log(` ${brand.medium('Medium')}: ${stats.bySeverity.medium}`);
93
+ console.log(` ${brand.low('Low')}: ${stats.bySeverity.low}`);
94
+ const statusEntries = Object.entries(stats.byStatus);
95
+ if (statusEntries.length > 0) {
96
+ console.log();
97
+ console.log(brand.bold('By Status'));
98
+ for (const [status, count] of statusEntries) {
99
+ console.log(` ${status.padEnd(14)} ${count}`);
100
+ }
101
+ }
102
+ }
103
+ function renderBugList(bugs) {
104
+ if (bugs.length === 0) {
105
+ console.log(brand.muted(' No bug reports found'));
106
+ return;
107
+ }
108
+ const severityColor = { high: brand.high, medium: brand.medium, low: brand.low };
109
+ for (const bug of bugs) {
110
+ const sev = severityColor[bug.severity](`[${bug.severity.toUpperCase()}]`);
111
+ const status = brand.muted(`[${bug.status}]`);
112
+ const content = bug.content.length > 75 ? bug.content.slice(0, 72) + '...' : bug.content;
113
+ console.log(`${sev} ${status} ${brand.muted(bug.id)}`);
114
+ console.log(` ${content}`);
115
+ if (bug.aiSummary) {
116
+ console.log(` ${brand.hint(bug.aiSummary)}`);
117
+ }
118
+ console.log();
119
+ }
120
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import type { OutputWriter } from '../output/writer.js';
3
+ export declare function createDoctorCommand(getWriter: () => OutputWriter): Command;