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,130 @@
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 { VERSION } from '../version.js';
6
+ import { AuthManager } from '../auth/manager.js';
7
+ import { loadConfig, configDir } from '../config/config.js';
8
+ import { FeedbackBasketClient } from '../client.js';
9
+ import { brand, divider, logo } from '../output/theme.js';
10
+ export function createDoctorCommand(getWriter) {
11
+ return new Command('doctor')
12
+ .description('Run diagnostics to check CLI health')
13
+ .option('--verbose', 'Show detailed diagnostic output')
14
+ .action(async () => {
15
+ const writer = getWriter();
16
+ const checks = [];
17
+ // 1. CLI version
18
+ checks.push({
19
+ name: 'CLI version',
20
+ status: 'pass',
21
+ message: `v${VERSION}`,
22
+ });
23
+ // 2. Config directory
24
+ const dir = configDir();
25
+ checks.push({
26
+ name: 'Config directory',
27
+ status: existsSync(dir) ? 'pass' : 'warn',
28
+ message: existsSync(dir) ? dir : `${dir} (not created yet)`,
29
+ hint: !existsSync(dir) ? 'Will be created on first auth login' : undefined,
30
+ });
31
+ // 3. Authentication
32
+ const manager = new AuthManager();
33
+ const token = manager.resolveToken();
34
+ if (token) {
35
+ checks.push({
36
+ name: 'Authentication',
37
+ status: 'pass',
38
+ message: `Authenticated via ${manager.getSource()} (${manager.getTokenPreview()})`,
39
+ });
40
+ }
41
+ else {
42
+ checks.push({
43
+ name: 'Authentication',
44
+ status: 'fail',
45
+ message: 'Not authenticated',
46
+ hint: 'Run: feedbackbasket auth login',
47
+ });
48
+ }
49
+ // 4. API connectivity
50
+ if (token) {
51
+ const config = loadConfig();
52
+ const client = new FeedbackBasketClient(token, config.baseUrl);
53
+ try {
54
+ const start = Date.now();
55
+ await client.listProjects();
56
+ const elapsed = Date.now() - start;
57
+ checks.push({
58
+ name: 'API connectivity',
59
+ status: 'pass',
60
+ message: `${config.baseUrl} (${elapsed}ms)`,
61
+ });
62
+ }
63
+ catch (error) {
64
+ const message = error instanceof Error ? error.message : String(error);
65
+ checks.push({
66
+ name: 'API connectivity',
67
+ status: 'fail',
68
+ message,
69
+ hint: 'Check your internet connection and base URL',
70
+ });
71
+ }
72
+ }
73
+ else {
74
+ checks.push({
75
+ name: 'API connectivity',
76
+ status: 'skip',
77
+ message: 'Skipped (not authenticated)',
78
+ });
79
+ }
80
+ // 5. Claude Code integration
81
+ const claudeSkillPath = join(homedir(), '.claude', 'skills', 'feedbackbasket', 'SKILL.md');
82
+ if (existsSync(claudeSkillPath)) {
83
+ checks.push({
84
+ name: 'Claude Code skill',
85
+ status: 'pass',
86
+ message: 'Installed',
87
+ });
88
+ }
89
+ else {
90
+ checks.push({
91
+ name: 'Claude Code skill',
92
+ status: 'warn',
93
+ message: 'Not installed',
94
+ hint: 'Run: feedbackbasket setup claude',
95
+ });
96
+ }
97
+ // Render
98
+ if (!writer.isMachineOutput()) {
99
+ renderChecks(checks);
100
+ }
101
+ const passed = checks.filter(c => c.status === 'pass').length;
102
+ const failed = checks.filter(c => c.status === 'fail').length;
103
+ const warned = checks.filter(c => c.status === 'warn').length;
104
+ writer.ok({ checks, passed, failed, warned }, {
105
+ summary: `${passed} passed, ${failed} failed, ${warned} warnings`,
106
+ breadcrumbs: failed > 0
107
+ ? [{ action: 'Authenticate', cmd: 'feedbackbasket auth login' }]
108
+ : [{ action: 'List projects', cmd: 'feedbackbasket projects list' }],
109
+ });
110
+ });
111
+ }
112
+ const statusIcon = {
113
+ pass: brand.success(' ✓'),
114
+ fail: brand.error(' ✗'),
115
+ warn: brand.warning(' !'),
116
+ skip: brand.muted(' -'),
117
+ };
118
+ function renderChecks(checks) {
119
+ console.log(`${logo()} CLI Diagnostics`);
120
+ console.log(divider(40));
121
+ console.log();
122
+ for (const check of checks) {
123
+ const icon = statusIcon[check.status] ?? ' ?';
124
+ console.log(`${icon} ${brand.bold(check.name)}: ${check.message}`);
125
+ if (check.hint) {
126
+ console.log(` ${brand.muted(check.hint)}`);
127
+ }
128
+ }
129
+ console.log();
130
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import type { OutputWriter } from '../output/writer.js';
3
+ export declare function createFeedbackBulkUpdateCommand(getWriter: () => OutputWriter): Command;
@@ -0,0 +1,39 @@
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, errUsage } from '../output/errors.js';
6
+ import { brand } from '../output/theme.js';
7
+ export function createFeedbackBulkUpdateCommand(getWriter) {
8
+ return new Command('bulk-update')
9
+ .description('Update status for multiple feedback items at once')
10
+ .requiredOption('--status <status>', 'New status (OPEN, UNDER_REVIEW, PLANNED, IN_PROGRESS, COMPLETE, CLOSED)')
11
+ .requiredOption('--ids <ids>', 'Comma-separated feedback IDs')
12
+ .action(async (opts) => {
13
+ const writer = getWriter();
14
+ const client = requireClient();
15
+ const ids = opts.ids.split(',').map((id) => id.trim()).filter(Boolean);
16
+ if (ids.length === 0) {
17
+ throw errUsage('At least one ID is required', 'Example: --ids id1,id2,id3');
18
+ }
19
+ const result = await client.bulkUpdateStatus(ids, opts.status);
20
+ if (!writer.isMachineOutput()) {
21
+ console.log(` ${brand.success('✓')} Updated ${result.updated} feedback items to ${brand.bold(result.status)}`);
22
+ console.log();
23
+ }
24
+ writer.ok(result, {
25
+ summary: `Updated ${result.updated} items to ${result.status}`,
26
+ breadcrumbs: [
27
+ { action: 'List feedback', cmd: 'feedbackbasket feedback list' },
28
+ ],
29
+ });
30
+ });
31
+ }
32
+ function requireClient() {
33
+ const manager = new AuthManager();
34
+ const token = manager.resolveToken();
35
+ if (!token)
36
+ throw errAuth();
37
+ const config = loadConfig();
38
+ return new FeedbackBasketClient(token, config.baseUrl);
39
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import type { OutputWriter } from '../output/writer.js';
3
+ export declare function createFeedbackDeleteCommand(getWriter: () => OutputWriter): Command;
@@ -0,0 +1,45 @@
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 } from '../output/theme.js';
7
+ import { confirm } from '../prompt.js';
8
+ export function createFeedbackDeleteCommand(getWriter) {
9
+ return new Command('delete')
10
+ .argument('<id>', 'Feedback ID to delete')
11
+ .description('Delete a feedback item')
12
+ .option('--yes', 'Skip confirmation prompt')
13
+ .action(async (id, opts) => {
14
+ const writer = getWriter();
15
+ const client = requireClient();
16
+ if (!opts.yes && !writer.isMachineOutput() && process.stdin.isTTY) {
17
+ console.log(` ${brand.warning('Warning:')} This will permanently delete feedback ${brand.bold(id)}`);
18
+ console.log();
19
+ const confirmed = await confirm(' Delete this feedback?', false);
20
+ if (!confirmed) {
21
+ console.log(brand.muted(' Cancelled.'));
22
+ return;
23
+ }
24
+ }
25
+ const result = await client.deleteFeedback(id);
26
+ if (!writer.isMachineOutput()) {
27
+ console.log(` ${brand.success('✓')} Deleted feedback ${id}`);
28
+ console.log();
29
+ }
30
+ writer.ok(result, {
31
+ summary: `Deleted feedback ${id}`,
32
+ breadcrumbs: [
33
+ { action: 'List feedback', cmd: 'feedbackbasket feedback list' },
34
+ ],
35
+ });
36
+ });
37
+ }
38
+ function requireClient() {
39
+ const manager = new AuthManager();
40
+ const token = manager.resolveToken();
41
+ if (!token)
42
+ throw errAuth();
43
+ const config = loadConfig();
44
+ return new FeedbackBasketClient(token, config.baseUrl);
45
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import type { OutputWriter } from '../output/writer.js';
3
+ export declare function createFeedbackExportCommand(getWriter: () => OutputWriter): Command;
@@ -0,0 +1,43 @@
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, errUsage } from '../output/errors.js';
6
+ import { resolveProject } from '../resolve.js';
7
+ export function createFeedbackExportCommand(getWriter) {
8
+ return new Command('export')
9
+ .description('Export feedback to CSV, Markdown, or JSON')
10
+ .argument('[project]', 'Project ID or name')
11
+ .option('--format <format>', 'Export format: csv, md, json', 'csv')
12
+ .action(async (projectArg, opts) => {
13
+ const writer = getWriter();
14
+ const client = requireClient();
15
+ const format = opts.format;
16
+ if (!['csv', 'md', 'json'].includes(format)) {
17
+ throw errUsage('Format must be csv, md, or json', 'Example: feedbackbasket feedback export --format json');
18
+ }
19
+ let projectId;
20
+ if (projectArg) {
21
+ const project = await resolveProject(client, projectArg);
22
+ projectId = project.id;
23
+ }
24
+ else {
25
+ const config = loadConfig();
26
+ if (!config.defaultProject) {
27
+ throw errUsage('Project is required for export.', 'feedbackbasket feedback export <project> or set a default project');
28
+ }
29
+ projectId = config.defaultProject;
30
+ }
31
+ const data = await client.exportFeedback(projectId, format);
32
+ // Export outputs raw data directly — not wrapped in envelope
33
+ console.log(data);
34
+ });
35
+ }
36
+ function requireClient() {
37
+ const manager = new AuthManager();
38
+ const token = manager.resolveToken();
39
+ if (!token)
40
+ throw errAuth();
41
+ const config = loadConfig();
42
+ return new FeedbackBasketClient(token, config.baseUrl);
43
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import type { OutputWriter } from '../output/writer.js';
3
+ export declare function createFeedbackNoteCommand(getWriter: () => OutputWriter): Command;
@@ -0,0 +1,41 @@
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, errUsage } from '../output/errors.js';
6
+ import { brand } from '../output/theme.js';
7
+ export function createFeedbackNoteCommand(getWriter) {
8
+ return new Command('note')
9
+ .argument('<id>', 'Feedback ID to add a note to')
10
+ .argument('[content]', 'Note content (or use --content)')
11
+ .description('Add an internal note to a feedback item')
12
+ .option('--content <text>', 'Note content (alternative to positional argument)')
13
+ .action(async (id, contentArg, opts) => {
14
+ const writer = getWriter();
15
+ const content = contentArg ?? opts.content;
16
+ if (!content) {
17
+ throw errUsage('Note content is required', 'Example: feedbackbasket feedback note <id> "Your note here"');
18
+ }
19
+ const client = requireClient();
20
+ const note = await client.createNote(id, content);
21
+ if (!writer.isMachineOutput()) {
22
+ console.log(brand.success(`Note added to feedback ${id}`));
23
+ console.log(` ${content}`);
24
+ }
25
+ writer.ok(note, {
26
+ summary: `Note added to feedback ${id}`,
27
+ breadcrumbs: [
28
+ { action: 'View feedback', cmd: `feedbackbasket feedback show ${id}` },
29
+ { action: 'Back to list', cmd: 'feedbackbasket feedback list' },
30
+ ],
31
+ });
32
+ });
33
+ }
34
+ function requireClient() {
35
+ const manager = new AuthManager();
36
+ const token = manager.resolveToken();
37
+ if (!token)
38
+ throw errAuth();
39
+ const config = loadConfig();
40
+ return new FeedbackBasketClient(token, config.baseUrl);
41
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import type { OutputWriter } from '../output/writer.js';
3
+ export declare function createFeedbackUpdateCommand(getWriter: () => OutputWriter): Command;
@@ -0,0 +1,54 @@
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, errUsage } from '../output/errors.js';
6
+ import { brand } from '../output/theme.js';
7
+ export function createFeedbackUpdateCommand(getWriter) {
8
+ return new Command('update')
9
+ .argument('<id>', 'Feedback ID to update')
10
+ .description('Update a feedback item')
11
+ .option('--status <status>', 'Set status (OPEN, UNDER_REVIEW, PLANNED, IN_PROGRESS, COMPLETE, CLOSED)')
12
+ .option('--category <category>', 'Set category (BUG, FEATURE_REQUEST, IMPROVEMENT, QUESTION)')
13
+ .option('--sentiment <sentiment>', 'Set sentiment (POSITIVE, NEGATIVE, NEUTRAL)')
14
+ .action(async (id, opts) => {
15
+ const writer = getWriter();
16
+ if (!opts.status && !opts.category && !opts.sentiment) {
17
+ throw errUsage('At least one of --status, --category, or --sentiment is required', 'Example: feedbackbasket feedback update <id> --status PLANNED');
18
+ }
19
+ const client = requireClient();
20
+ const data = {};
21
+ if (opts.status)
22
+ data['status'] = opts.status;
23
+ if (opts.category)
24
+ data['category'] = opts.category;
25
+ if (opts.sentiment)
26
+ data['sentiment'] = opts.sentiment;
27
+ const updated = await client.updateFeedback(id, data);
28
+ if (!writer.isMachineOutput()) {
29
+ console.log(brand.success(`Updated feedback ${id}`));
30
+ if (opts.status)
31
+ console.log(` Status: ${opts.status}`);
32
+ if (opts.category)
33
+ console.log(` Category: ${opts.category}`);
34
+ if (opts.sentiment)
35
+ console.log(` Sentiment: ${opts.sentiment}`);
36
+ }
37
+ writer.ok(updated, {
38
+ summary: `Updated feedback ${id}`,
39
+ breadcrumbs: [
40
+ { action: 'View updated item', cmd: `feedbackbasket feedback show ${id}` },
41
+ { action: 'Add a note', cmd: `feedbackbasket feedback note ${id} "<note>"` },
42
+ { action: 'Back to list', cmd: 'feedbackbasket feedback list' },
43
+ ],
44
+ });
45
+ });
46
+ }
47
+ function requireClient() {
48
+ const manager = new AuthManager();
49
+ const token = manager.resolveToken();
50
+ if (!token)
51
+ throw errAuth();
52
+ const config = loadConfig();
53
+ return new FeedbackBasketClient(token, config.baseUrl);
54
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import type { OutputWriter } from '../output/writer.js';
3
+ export declare function createFeedbackCommand(getWriter: () => OutputWriter): Command;
@@ -0,0 +1,201 @@
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 } from '../output/theme.js';
7
+ function resolveProjectId(optProject) {
8
+ if (optProject)
9
+ return optProject;
10
+ const config = loadConfig();
11
+ return config.defaultProject;
12
+ }
13
+ import { createFeedbackUpdateCommand } from './feedback-update.js';
14
+ import { createFeedbackNoteCommand } from './feedback-note.js';
15
+ import { createFeedbackDeleteCommand } from './feedback-delete.js';
16
+ import { createFeedbackBulkUpdateCommand } from './feedback-bulk-update.js';
17
+ import { createFeedbackExportCommand } from './feedback-export.js';
18
+ export function createFeedbackCommand(getWriter) {
19
+ const feedback = new Command('feedback')
20
+ .description('View and manage feedback');
21
+ // Write subcommands
22
+ feedback.addCommand(createFeedbackUpdateCommand(getWriter));
23
+ feedback.addCommand(createFeedbackNoteCommand(getWriter));
24
+ feedback.addCommand(createFeedbackDeleteCommand(getWriter));
25
+ feedback.addCommand(createFeedbackBulkUpdateCommand(getWriter));
26
+ feedback.addCommand(createFeedbackExportCommand(getWriter));
27
+ // --- feedback list ---
28
+ feedback
29
+ .command('list')
30
+ .description('List feedback with optional filters')
31
+ .option('--project <id>', 'Filter by project ID')
32
+ .option('--category <category>', 'Filter by category (BUG, FEATURE_REQUEST, IMPROVEMENT, QUESTION)')
33
+ .option('--status <status>', 'Filter by status (OPEN, UNDER_REVIEW, PLANNED, IN_PROGRESS, COMPLETE, CLOSED)')
34
+ .option('--sentiment <sentiment>', 'Filter by sentiment (POSITIVE, NEGATIVE, NEUTRAL)')
35
+ .option('--search <query>', 'Search feedback content')
36
+ .option('--limit <n>', 'Max results (1-100)', '20')
37
+ .option('--offset <n>', 'Offset for pagination', '0')
38
+ .option('--notes', 'Include internal notes')
39
+ .action(async (opts) => {
40
+ const writer = getWriter();
41
+ const client = requireClient();
42
+ const result = await client.getFeedback({
43
+ projectId: resolveProjectId(opts.project),
44
+ category: opts.category,
45
+ status: opts.status,
46
+ sentiment: opts.sentiment,
47
+ search: opts.search,
48
+ limit: parseInt(opts.limit, 10),
49
+ offset: parseInt(opts.offset, 10),
50
+ includeNotes: opts.notes ?? false,
51
+ });
52
+ if (!writer.isMachineOutput()) {
53
+ renderFeedbackList(result.feedback);
54
+ }
55
+ const breadcrumbs = [];
56
+ if (opts.project) {
57
+ breadcrumbs.push({ action: 'View bugs for this project', cmd: `feedbackbasket bugs list --project ${opts.project}` });
58
+ }
59
+ if (result.pagination.hasMore) {
60
+ const nextOffset = parseInt(opts.offset, 10) + parseInt(opts.limit, 10);
61
+ breadcrumbs.push({ action: 'Next page', cmd: `feedbackbasket feedback list --offset ${nextOffset} --limit ${opts.limit}${opts.project ? ` --project ${opts.project}` : ''}` });
62
+ }
63
+ breadcrumbs.push({ action: 'Search', cmd: 'feedbackbasket feedback search "<query>"' });
64
+ writer.ok(result.feedback, {
65
+ summary: `Showing ${result.feedback.length} of ${result.pagination.totalCount} feedback items`,
66
+ notice: result.pagination.hasMore ? `Use --offset ${parseInt(opts.offset, 10) + parseInt(opts.limit, 10)} to see more` : undefined,
67
+ breadcrumbs,
68
+ });
69
+ });
70
+ // --- feedback show ---
71
+ feedback
72
+ .command('show <id>')
73
+ .description('Show a single feedback item in detail')
74
+ .action(async (id) => {
75
+ const writer = getWriter();
76
+ const client = requireClient();
77
+ const item = await client.getFeedbackById(id);
78
+ if (!writer.isMachineOutput()) {
79
+ renderFeedbackDetail(item);
80
+ }
81
+ writer.ok(item, {
82
+ summary: `Feedback ${item.id}`,
83
+ breadcrumbs: [
84
+ { action: 'Update status', cmd: `feedbackbasket feedback update ${item.id} --status <STATUS>` },
85
+ { action: 'Add note', cmd: `feedbackbasket feedback note ${item.id} "<note>"` },
86
+ { action: 'Back to list', cmd: 'feedbackbasket feedback list' },
87
+ ],
88
+ });
89
+ });
90
+ // --- feedback search ---
91
+ feedback
92
+ .command('search <query>')
93
+ .description('Search feedback content across projects')
94
+ .option('--project <id>', 'Limit to a specific project')
95
+ .option('--category <category>', 'Filter by category')
96
+ .option('--limit <n>', 'Max results (1-50)', '10')
97
+ .action(async (query, opts) => {
98
+ const writer = getWriter();
99
+ const client = requireClient();
100
+ const result = await client.searchFeedback(query, {
101
+ projectId: resolveProjectId(opts.project),
102
+ category: opts.category,
103
+ limit: parseInt(opts.limit, 10),
104
+ });
105
+ if (!writer.isMachineOutput()) {
106
+ renderFeedbackList(result.feedback);
107
+ }
108
+ writer.ok(result.feedback, {
109
+ summary: `${result.pagination.totalCount} result${result.pagination.totalCount === 1 ? '' : 's'} for "${query}"`,
110
+ breadcrumbs: [
111
+ { action: 'List all feedback', cmd: 'feedbackbasket feedback list' },
112
+ { action: 'Search bugs', cmd: `feedbackbasket bugs list --search "${query}"` },
113
+ ],
114
+ });
115
+ });
116
+ return feedback;
117
+ }
118
+ function requireClient() {
119
+ const manager = new AuthManager();
120
+ const token = manager.resolveToken();
121
+ if (!token)
122
+ throw errAuth();
123
+ const config = loadConfig();
124
+ return new FeedbackBasketClient(token, config.baseUrl);
125
+ }
126
+ const categoryEmoji = {
127
+ BUG: brand.bug('[BUG]'),
128
+ FEATURE_REQUEST: brand.feature('[FEATURE]'),
129
+ IMPROVEMENT: brand.improvement('[IMPROVE]'),
130
+ QUESTION: brand.question('[QUESTION]'),
131
+ };
132
+ const priorityLabel = (score) => {
133
+ if (score == null)
134
+ return brand.muted('--');
135
+ if (score >= 70)
136
+ return brand.high(`P${score}`);
137
+ if (score >= 40)
138
+ return brand.medium(`P${score}`);
139
+ return brand.low(`P${score}`);
140
+ };
141
+ function renderFeedbackList(items) {
142
+ if (items.length === 0)
143
+ return;
144
+ for (const item of items) {
145
+ const cat = categoryEmoji[item.category ?? ''] ?? brand.muted('[?]');
146
+ const priority = priorityLabel(item.aiPriorityScore);
147
+ const content = item.content.length > 80 ? item.content.slice(0, 77) + '...' : item.content;
148
+ const status = brand.muted(`[${item.status}]`);
149
+ console.log(`${cat} ${priority} ${status} ${brand.muted(item.id)}`);
150
+ console.log(` ${content}`);
151
+ if (item.aiSummary) {
152
+ console.log(` ${brand.hint(item.aiSummary)}`);
153
+ }
154
+ console.log();
155
+ }
156
+ }
157
+ function renderFeedbackDetail(item) {
158
+ console.log(brand.bold(`Feedback ${item.id}`));
159
+ console.log(brand.divider('─'.repeat(50)));
160
+ console.log();
161
+ const fields = [
162
+ ['Status', item.status],
163
+ ['Category', item.category],
164
+ ['Sentiment', item.sentiment],
165
+ ['Priority', item.aiPriorityScore != null ? String(item.aiPriorityScore) : null],
166
+ ['Email', item.email],
167
+ ['Project', `${item.project.name} (${item.project.id})`],
168
+ ['Page URL', item.pageUrl],
169
+ ['Browser', item.browser],
170
+ ['OS', item.os],
171
+ ['Device', item.device],
172
+ ['Language', item.language],
173
+ ['Created', item.createdAt],
174
+ ];
175
+ for (const [label, value] of fields) {
176
+ if (value) {
177
+ console.log(` ${brand.bold(label.padEnd(12))} ${value}`);
178
+ }
179
+ }
180
+ console.log();
181
+ console.log(brand.bold('Content:'));
182
+ console.log(` ${item.content}`);
183
+ if (item.aiSummary) {
184
+ console.log();
185
+ console.log(brand.bold('AI Summary:'));
186
+ console.log(` ${brand.hint(item.aiSummary)}`);
187
+ }
188
+ if (item.reasoning) {
189
+ console.log();
190
+ console.log(brand.bold('AI Reasoning:'));
191
+ console.log(` ${brand.hint(item.reasoning)}`);
192
+ }
193
+ if (item.notes && item.notes.length > 0) {
194
+ console.log();
195
+ console.log(brand.bold(`Notes (${item.notes.length}):`));
196
+ for (const note of item.notes) {
197
+ console.log(` ${brand.muted(note.createdAt)} ${brand.primary(note.author.name)}`);
198
+ console.log(` ${note.content}`);
199
+ }
200
+ }
201
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import type { OutputWriter } from '../output/writer.js';
3
+ export declare function createProjectsCommand(getWriter: () => OutputWriter): Command;