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.
- package/README.md +215 -0
- package/dist/bin/feedbackbasket.d.ts +2 -0
- package/dist/bin/feedbackbasket.js +3 -0
- package/dist/src/auth/login.d.ts +6 -0
- package/dist/src/auth/login.js +111 -0
- package/dist/src/auth/manager.d.ts +11 -0
- package/dist/src/auth/manager.js +38 -0
- package/dist/src/cli.d.ts +1 -0
- package/dist/src/cli.js +79 -0
- package/dist/src/client.d.ts +105 -0
- package/dist/src/client.js +133 -0
- package/dist/src/commands/auth.d.ts +5 -0
- package/dist/src/commands/auth.js +313 -0
- package/dist/src/commands/bugs.d.ts +3 -0
- package/dist/src/commands/bugs.js +120 -0
- package/dist/src/commands/doctor.d.ts +3 -0
- package/dist/src/commands/doctor.js +130 -0
- package/dist/src/commands/feedback-bulk-update.d.ts +3 -0
- package/dist/src/commands/feedback-bulk-update.js +39 -0
- package/dist/src/commands/feedback-delete.d.ts +3 -0
- package/dist/src/commands/feedback-delete.js +45 -0
- package/dist/src/commands/feedback-export.d.ts +3 -0
- package/dist/src/commands/feedback-export.js +43 -0
- package/dist/src/commands/feedback-note.d.ts +3 -0
- package/dist/src/commands/feedback-note.js +41 -0
- package/dist/src/commands/feedback-update.d.ts +3 -0
- package/dist/src/commands/feedback-update.js +54 -0
- package/dist/src/commands/feedback.d.ts +3 -0
- package/dist/src/commands/feedback.js +201 -0
- package/dist/src/commands/projects.d.ts +3 -0
- package/dist/src/commands/projects.js +218 -0
- package/dist/src/commands/setup.d.ts +3 -0
- package/dist/src/commands/setup.js +90 -0
- package/dist/src/commands/team.d.ts +3 -0
- package/dist/src/commands/team.js +112 -0
- package/dist/src/commands/widget.d.ts +3 -0
- package/dist/src/commands/widget.js +153 -0
- package/dist/src/config/config.d.ts +10 -0
- package/dist/src/config/config.js +41 -0
- package/dist/src/config/credentials.d.ts +12 -0
- package/dist/src/config/credentials.js +39 -0
- package/dist/src/output/codes.d.ts +16 -0
- package/dist/src/output/codes.js +29 -0
- package/dist/src/output/envelope.d.ts +24 -0
- package/dist/src/output/envelope.js +2 -0
- package/dist/src/output/errors.d.ts +13 -0
- package/dist/src/output/errors.js +37 -0
- package/dist/src/output/styled.d.ts +4 -0
- package/dist/src/output/styled.js +78 -0
- package/dist/src/output/theme.d.ts +25 -0
- package/dist/src/output/theme.js +43 -0
- package/dist/src/output/writer.d.ts +23 -0
- package/dist/src/output/writer.js +109 -0
- package/dist/src/prompt.d.ts +3 -0
- package/dist/src/prompt.js +32 -0
- package/dist/src/resolve.d.ts +9 -0
- package/dist/src/resolve.js +70 -0
- package/dist/src/types.d.ts +100 -0
- package/dist/src/types.js +2 -0
- package/dist/src/version.d.ts +2 -0
- package/dist/src/version.js +2 -0
- package/package.json +49 -0
- package/skills/feedbackbasket/SKILL.md +132 -0
|
@@ -0,0 +1,218 @@
|
|
|
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, divider } from '../output/theme.js';
|
|
7
|
+
import { confirm } from '../prompt.js';
|
|
8
|
+
import { resolveProject } from '../resolve.js';
|
|
9
|
+
export function createProjectsCommand(getWriter) {
|
|
10
|
+
const projects = new Command('projects')
|
|
11
|
+
.alias('project')
|
|
12
|
+
.description('Manage projects');
|
|
13
|
+
// --- projects list ---
|
|
14
|
+
projects
|
|
15
|
+
.command('list')
|
|
16
|
+
.description('List all accessible projects')
|
|
17
|
+
.action(async () => {
|
|
18
|
+
const writer = getWriter();
|
|
19
|
+
const client = requireClient();
|
|
20
|
+
const result = await client.listProjects();
|
|
21
|
+
const data = result.projects;
|
|
22
|
+
if (!writer.isMachineOutput()) {
|
|
23
|
+
renderProjectsTable(data);
|
|
24
|
+
}
|
|
25
|
+
writer.ok(data, {
|
|
26
|
+
summary: `${result.totalProjects} project${result.totalProjects === 1 ? '' : 's'}`,
|
|
27
|
+
breadcrumbs: data.length > 0
|
|
28
|
+
? [
|
|
29
|
+
{ action: 'Show project', cmd: `feedbackbasket projects show ${data[0].id}` },
|
|
30
|
+
{ action: 'View feedback', cmd: `feedbackbasket feedback list --project ${data[0].id}` },
|
|
31
|
+
{ action: 'Create project', cmd: 'feedbackbasket projects create "My Project" --url https://example.com' },
|
|
32
|
+
]
|
|
33
|
+
: [{ action: 'Create project', cmd: 'feedbackbasket projects create "My Project" --url https://example.com' }],
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
// --- projects show ---
|
|
37
|
+
projects
|
|
38
|
+
.command('show <id-or-name>')
|
|
39
|
+
.description('Show project details (accepts ID or name)')
|
|
40
|
+
.action(async (idOrName) => {
|
|
41
|
+
const writer = getWriter();
|
|
42
|
+
const client = requireClient();
|
|
43
|
+
const project = await resolveProject(client, idOrName);
|
|
44
|
+
if (!writer.isMachineOutput()) {
|
|
45
|
+
renderProjectDetail(project);
|
|
46
|
+
}
|
|
47
|
+
writer.ok(project, {
|
|
48
|
+
summary: project.name,
|
|
49
|
+
breadcrumbs: [
|
|
50
|
+
{ action: 'View feedback', cmd: `feedbackbasket feedback list --project ${project.id}` },
|
|
51
|
+
{ action: 'View bugs', cmd: `feedbackbasket bugs list --project ${project.id}` },
|
|
52
|
+
{ action: 'Update project', cmd: `feedbackbasket projects update ${project.id} --name "New Name"` },
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
// --- projects create ---
|
|
57
|
+
projects
|
|
58
|
+
.command('create <name>')
|
|
59
|
+
.description('Create a new project')
|
|
60
|
+
.requiredOption('--url <url>', 'Project URL')
|
|
61
|
+
.option('--description <text>', 'Project description')
|
|
62
|
+
.action(async (name, opts) => {
|
|
63
|
+
const writer = getWriter();
|
|
64
|
+
const client = requireClient();
|
|
65
|
+
const result = await client.createProject({
|
|
66
|
+
name,
|
|
67
|
+
url: opts.url,
|
|
68
|
+
description: opts.description,
|
|
69
|
+
});
|
|
70
|
+
if (!writer.isMachineOutput()) {
|
|
71
|
+
console.log(` ${brand.success('✓')} Project created: ${brand.bold(result.project.name)}`);
|
|
72
|
+
console.log(` ${brand.muted('ID:')} ${result.project.id}`);
|
|
73
|
+
console.log();
|
|
74
|
+
}
|
|
75
|
+
writer.ok(result.project, {
|
|
76
|
+
summary: `Created project "${result.project.name}"`,
|
|
77
|
+
breadcrumbs: [
|
|
78
|
+
{ action: 'View project', cmd: `feedbackbasket projects show ${result.project.id}` },
|
|
79
|
+
{ action: 'Get widget script', cmd: `feedbackbasket widget script ${result.project.id}` },
|
|
80
|
+
{ action: 'List all projects', cmd: 'feedbackbasket projects list' },
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
// --- projects update ---
|
|
85
|
+
projects
|
|
86
|
+
.command('update <id-or-name>')
|
|
87
|
+
.description('Update a project (accepts ID or name)')
|
|
88
|
+
.option('--name <name>', 'New project name')
|
|
89
|
+
.option('--url <url>', 'New project URL')
|
|
90
|
+
.option('--description <text>', 'New project description')
|
|
91
|
+
.action(async (idOrName, opts) => {
|
|
92
|
+
const writer = getWriter();
|
|
93
|
+
if (!opts.name && !opts.url && opts.description === undefined) {
|
|
94
|
+
throw errUsage('At least one of --name, --url, or --description is required', 'Example: feedbackbasket projects update <id> --name "New Name"');
|
|
95
|
+
}
|
|
96
|
+
const client = requireClient();
|
|
97
|
+
const resolved = await resolveProject(client, idOrName);
|
|
98
|
+
const id = resolved.id;
|
|
99
|
+
const data = {};
|
|
100
|
+
if (opts.name)
|
|
101
|
+
data['name'] = opts.name;
|
|
102
|
+
if (opts.url)
|
|
103
|
+
data['url'] = opts.url;
|
|
104
|
+
if (opts.description !== undefined)
|
|
105
|
+
data['description'] = opts.description;
|
|
106
|
+
const updated = await client.updateProject(id, data);
|
|
107
|
+
if (!writer.isMachineOutput()) {
|
|
108
|
+
console.log(` ${brand.success('✓')} Project updated: ${brand.bold(updated.name)}`);
|
|
109
|
+
console.log();
|
|
110
|
+
}
|
|
111
|
+
writer.ok(updated, {
|
|
112
|
+
summary: `Updated project "${updated.name}"`,
|
|
113
|
+
breadcrumbs: [
|
|
114
|
+
{ action: 'View project', cmd: `feedbackbasket projects show ${id}` },
|
|
115
|
+
{ action: 'List all projects', cmd: 'feedbackbasket projects list' },
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
// --- projects delete ---
|
|
120
|
+
projects
|
|
121
|
+
.command('delete <id-or-name>')
|
|
122
|
+
.description('Delete a project (accepts ID or name)')
|
|
123
|
+
.option('--yes', 'Skip confirmation prompt')
|
|
124
|
+
.action(async (idOrName, opts) => {
|
|
125
|
+
const writer = getWriter();
|
|
126
|
+
const client = requireClient();
|
|
127
|
+
const resolved = await resolveProject(client, idOrName);
|
|
128
|
+
const id = resolved.id;
|
|
129
|
+
const projectName = resolved.name;
|
|
130
|
+
// Confirmation (skip in agent mode or --yes)
|
|
131
|
+
if (!opts.yes && !writer.isMachineOutput() && process.stdin.isTTY) {
|
|
132
|
+
console.log(` ${brand.warning('Warning:')} This will permanently delete project "${brand.bold(projectName)}"`);
|
|
133
|
+
console.log(` ${brand.muted('All feedback, notes, and settings will be lost.')}`);
|
|
134
|
+
console.log();
|
|
135
|
+
const confirmed = await confirm(` Delete "${projectName}"?`, false);
|
|
136
|
+
if (!confirmed) {
|
|
137
|
+
console.log(brand.muted(' Cancelled.'));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const result = await client.deleteProject(id);
|
|
142
|
+
if (!writer.isMachineOutput()) {
|
|
143
|
+
console.log(` ${brand.success('✓')} Deleted project "${brand.bold(result.name)}"`);
|
|
144
|
+
console.log();
|
|
145
|
+
}
|
|
146
|
+
writer.ok(result, {
|
|
147
|
+
summary: `Deleted project "${result.name}"`,
|
|
148
|
+
breadcrumbs: [
|
|
149
|
+
{ action: 'List remaining projects', cmd: 'feedbackbasket projects list' },
|
|
150
|
+
{ action: 'Create new project', cmd: 'feedbackbasket projects create "Name" --url https://...' },
|
|
151
|
+
],
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
return projects;
|
|
155
|
+
}
|
|
156
|
+
function requireClient() {
|
|
157
|
+
const manager = new AuthManager();
|
|
158
|
+
const token = manager.resolveToken();
|
|
159
|
+
if (!token)
|
|
160
|
+
throw errAuth();
|
|
161
|
+
const config = loadConfig();
|
|
162
|
+
return new FeedbackBasketClient(token, config.baseUrl);
|
|
163
|
+
}
|
|
164
|
+
function renderProjectsTable(projects) {
|
|
165
|
+
if (projects.length === 0)
|
|
166
|
+
return;
|
|
167
|
+
const nameW = Math.max(4, ...projects.map(p => p.name.length));
|
|
168
|
+
const header = [
|
|
169
|
+
brand.bold('Name'.padEnd(nameW)),
|
|
170
|
+
brand.bold('ID'.padEnd(26)),
|
|
171
|
+
brand.bold('Total'.padStart(6)),
|
|
172
|
+
brand.bold('Open'.padStart(6)),
|
|
173
|
+
brand.bold('Bugs'.padStart(6)),
|
|
174
|
+
].join(' ');
|
|
175
|
+
console.log(header);
|
|
176
|
+
console.log(divider(header.length));
|
|
177
|
+
for (const p of projects) {
|
|
178
|
+
const open = p.byStatus['OPEN'] ?? 0;
|
|
179
|
+
const bugs = p.byCategory['BUG'] ?? 0;
|
|
180
|
+
const row = [
|
|
181
|
+
p.name.padEnd(nameW),
|
|
182
|
+
brand.muted(p.id.padEnd(26)),
|
|
183
|
+
String(p.totalFeedback).padStart(6),
|
|
184
|
+
open > 0 ? brand.warning(String(open).padStart(6)) : brand.muted(String(open).padStart(6)),
|
|
185
|
+
bugs > 0 ? brand.error(String(bugs).padStart(6)) : brand.muted(String(bugs).padStart(6)),
|
|
186
|
+
].join(' ');
|
|
187
|
+
console.log(row);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function renderProjectDetail(project) {
|
|
191
|
+
console.log(brand.bold(project.name));
|
|
192
|
+
console.log(divider(40));
|
|
193
|
+
console.log();
|
|
194
|
+
console.log(` ${brand.label('ID'.padEnd(14))} ${project.id}`);
|
|
195
|
+
console.log(` ${brand.label('URL'.padEnd(14))} ${project.url}`);
|
|
196
|
+
if (project.description) {
|
|
197
|
+
console.log(` ${brand.label('Description'.padEnd(14))} ${project.description}`);
|
|
198
|
+
}
|
|
199
|
+
console.log(` ${brand.label('Created'.padEnd(14))} ${project.createdAt}`);
|
|
200
|
+
console.log(` ${brand.label('Feedback'.padEnd(14))} ${project.totalFeedback}`);
|
|
201
|
+
const statusEntries = Object.entries(project.byStatus);
|
|
202
|
+
if (statusEntries.length > 0) {
|
|
203
|
+
console.log();
|
|
204
|
+
console.log(brand.bold('By Status'));
|
|
205
|
+
for (const [status, count] of statusEntries) {
|
|
206
|
+
console.log(` ${status.padEnd(14)} ${count}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const catEntries = Object.entries(project.byCategory);
|
|
210
|
+
if (catEntries.length > 0) {
|
|
211
|
+
console.log();
|
|
212
|
+
console.log(brand.bold('By Category'));
|
|
213
|
+
for (const [cat, count] of catEntries) {
|
|
214
|
+
console.log(` ${cat.padEnd(18)} ${count}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
console.log();
|
|
218
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { existsSync, mkdirSync, copyFileSync } from 'node:fs';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { brand } from '../output/theme.js';
|
|
7
|
+
export function createSetupCommand(getWriter) {
|
|
8
|
+
const setup = new Command('setup')
|
|
9
|
+
.description('Set up agent integrations');
|
|
10
|
+
setup
|
|
11
|
+
.command('claude')
|
|
12
|
+
.description('Install FeedbackBasket skill for Claude Code')
|
|
13
|
+
.action(() => {
|
|
14
|
+
const writer = getWriter();
|
|
15
|
+
const results = [];
|
|
16
|
+
const claudeDir = join(homedir(), '.claude');
|
|
17
|
+
const claudeExists = existsSync(claudeDir);
|
|
18
|
+
if (!claudeExists) {
|
|
19
|
+
results.push({
|
|
20
|
+
step: 'Detect Claude Code',
|
|
21
|
+
status: 'fail',
|
|
22
|
+
message: `~/.claude/ not found. Is Claude Code installed?`,
|
|
23
|
+
});
|
|
24
|
+
if (!writer.isMachineOutput()) {
|
|
25
|
+
console.log(brand.error('Claude Code not detected'));
|
|
26
|
+
console.log(brand.muted(' ~/.claude/ directory not found'));
|
|
27
|
+
console.log(brand.muted(' Install Claude Code first: https://claude.ai/code'));
|
|
28
|
+
}
|
|
29
|
+
writer.ok({ installed: false, results }, {
|
|
30
|
+
summary: 'Claude Code not detected',
|
|
31
|
+
});
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const skillDir = join(claudeDir, 'skills', 'feedbackbasket');
|
|
35
|
+
const skillDest = join(skillDir, 'SKILL.md');
|
|
36
|
+
try {
|
|
37
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
const projectRoot = join(thisDir, '..', '..', '..');
|
|
39
|
+
const skillSrc = join(projectRoot, 'skills', 'feedbackbasket', 'SKILL.md');
|
|
40
|
+
if (!existsSync(skillSrc)) {
|
|
41
|
+
const altSrc = join(thisDir, '..', '..', 'skills', 'feedbackbasket', 'SKILL.md');
|
|
42
|
+
if (!existsSync(altSrc)) {
|
|
43
|
+
results.push({
|
|
44
|
+
step: 'Copy skill file',
|
|
45
|
+
status: 'fail',
|
|
46
|
+
message: 'SKILL.md source not found in package',
|
|
47
|
+
});
|
|
48
|
+
writer.ok({ installed: false, results }, {
|
|
49
|
+
summary: 'Skill file not found',
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
mkdirSync(skillDir, { recursive: true });
|
|
54
|
+
copyFileSync(altSrc, skillDest);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
mkdirSync(skillDir, { recursive: true });
|
|
58
|
+
copyFileSync(skillSrc, skillDest);
|
|
59
|
+
}
|
|
60
|
+
results.push({
|
|
61
|
+
step: 'Install skill',
|
|
62
|
+
status: 'ok',
|
|
63
|
+
message: skillDest,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
results.push({
|
|
68
|
+
step: 'Install skill',
|
|
69
|
+
status: 'fail',
|
|
70
|
+
message: error instanceof Error ? error.message : String(error),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (!writer.isMachineOutput()) {
|
|
74
|
+
for (const r of results) {
|
|
75
|
+
const icon = r.status === 'ok' ? brand.success('✓') : r.status === 'skip' ? brand.warning('-') : brand.error('✗');
|
|
76
|
+
console.log(` ${icon} ${r.step}: ${r.message}`);
|
|
77
|
+
}
|
|
78
|
+
console.log();
|
|
79
|
+
}
|
|
80
|
+
const allOk = results.every(r => r.status !== 'fail');
|
|
81
|
+
writer.ok({ installed: allOk, results }, {
|
|
82
|
+
summary: allOk ? 'FeedbackBasket skill installed for Claude Code' : 'Setup completed with errors',
|
|
83
|
+
breadcrumbs: [
|
|
84
|
+
{ action: 'Start a new Claude Code session to use FeedbackBasket commands', cmd: 'claude' },
|
|
85
|
+
{ action: 'Run diagnostics', cmd: 'feedbackbasket doctor' },
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
return setup;
|
|
90
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
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, divider } from '../output/theme.js';
|
|
7
|
+
import { confirm } from '../prompt.js';
|
|
8
|
+
export function createTeamCommand(getWriter) {
|
|
9
|
+
const team = new Command('team')
|
|
10
|
+
.description('Manage organization members');
|
|
11
|
+
// --- team list ---
|
|
12
|
+
team
|
|
13
|
+
.command('list')
|
|
14
|
+
.description('List organization members')
|
|
15
|
+
.action(async () => {
|
|
16
|
+
const writer = getWriter();
|
|
17
|
+
const client = requireClient();
|
|
18
|
+
const result = await client.listTeam();
|
|
19
|
+
if (!writer.isMachineOutput()) {
|
|
20
|
+
renderTeamTable(result.members);
|
|
21
|
+
}
|
|
22
|
+
writer.ok(result.members, {
|
|
23
|
+
summary: `${result.totalMembers} member${result.totalMembers === 1 ? '' : 's'}`,
|
|
24
|
+
breadcrumbs: [
|
|
25
|
+
{ action: 'Update role', cmd: 'feedbackbasket team role <memberId> --role admin' },
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
// --- team role ---
|
|
30
|
+
team
|
|
31
|
+
.command('role <memberId>')
|
|
32
|
+
.description('Update a member\'s role')
|
|
33
|
+
.requiredOption('--role <role>', 'New role: admin or member')
|
|
34
|
+
.action(async (memberId, opts) => {
|
|
35
|
+
const writer = getWriter();
|
|
36
|
+
const client = requireClient();
|
|
37
|
+
if (!['admin', 'member'].includes(opts.role)) {
|
|
38
|
+
throw errUsage('Role must be "admin" or "member"');
|
|
39
|
+
}
|
|
40
|
+
const result = await client.updateMemberRole(memberId, opts.role);
|
|
41
|
+
if (!writer.isMachineOutput()) {
|
|
42
|
+
console.log(` ${brand.success('✓')} Updated ${brand.bold(result.name)} to ${brand.bold(result.role)}`);
|
|
43
|
+
console.log();
|
|
44
|
+
}
|
|
45
|
+
writer.ok(result, {
|
|
46
|
+
summary: `Updated ${result.name} to ${result.role}`,
|
|
47
|
+
breadcrumbs: [
|
|
48
|
+
{ action: 'List team', cmd: 'feedbackbasket team list' },
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
// --- team remove ---
|
|
53
|
+
team
|
|
54
|
+
.command('remove <memberId>')
|
|
55
|
+
.description('Remove a member from the organization')
|
|
56
|
+
.option('--yes', 'Skip confirmation')
|
|
57
|
+
.action(async (memberId, opts) => {
|
|
58
|
+
const writer = getWriter();
|
|
59
|
+
const client = requireClient();
|
|
60
|
+
if (!opts.yes && !writer.isMachineOutput() && process.stdin.isTTY) {
|
|
61
|
+
const confirmed = await confirm(` Remove member ${memberId}?`, false);
|
|
62
|
+
if (!confirmed) {
|
|
63
|
+
console.log(brand.muted(' Cancelled.'));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const result = await client.removeMember(memberId);
|
|
68
|
+
if (!writer.isMachineOutput()) {
|
|
69
|
+
console.log(` ${brand.success('✓')} Removed ${brand.bold(result.name)} (${result.email})`);
|
|
70
|
+
console.log();
|
|
71
|
+
}
|
|
72
|
+
writer.ok(result, {
|
|
73
|
+
summary: `Removed ${result.name}`,
|
|
74
|
+
breadcrumbs: [
|
|
75
|
+
{ action: 'List team', cmd: 'feedbackbasket team list' },
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
return team;
|
|
80
|
+
}
|
|
81
|
+
function requireClient() {
|
|
82
|
+
const manager = new AuthManager();
|
|
83
|
+
const token = manager.resolveToken();
|
|
84
|
+
if (!token)
|
|
85
|
+
throw errAuth();
|
|
86
|
+
const config = loadConfig();
|
|
87
|
+
return new FeedbackBasketClient(token, config.baseUrl);
|
|
88
|
+
}
|
|
89
|
+
function renderTeamTable(members) {
|
|
90
|
+
if (members.length === 0)
|
|
91
|
+
return;
|
|
92
|
+
const nameW = Math.max(4, ...members.map(m => m.name.length));
|
|
93
|
+
const emailW = Math.max(5, ...members.map(m => m.email.length));
|
|
94
|
+
const header = [
|
|
95
|
+
brand.bold('Name'.padEnd(nameW)),
|
|
96
|
+
brand.bold('Email'.padEnd(emailW)),
|
|
97
|
+
brand.bold('Role'.padEnd(8)),
|
|
98
|
+
brand.bold('Member ID'),
|
|
99
|
+
].join(' ');
|
|
100
|
+
console.log(header);
|
|
101
|
+
console.log(divider(header.length));
|
|
102
|
+
for (const m of members) {
|
|
103
|
+
const roleColor = m.role === 'owner' ? brand.primary : m.role === 'admin' ? brand.warning : brand.muted;
|
|
104
|
+
const row = [
|
|
105
|
+
m.name.padEnd(nameW),
|
|
106
|
+
brand.muted(m.email.padEnd(emailW)),
|
|
107
|
+
roleColor(m.role.padEnd(8)),
|
|
108
|
+
brand.muted(m.memberId),
|
|
109
|
+
].join(' ');
|
|
110
|
+
console.log(row);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
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, divider } from '../output/theme.js';
|
|
7
|
+
import { resolveProject } from '../resolve.js';
|
|
8
|
+
export function createWidgetCommand(getWriter) {
|
|
9
|
+
const widget = new Command('widget')
|
|
10
|
+
.description('Manage feedback widget');
|
|
11
|
+
// --- widget settings ---
|
|
12
|
+
widget
|
|
13
|
+
.command('settings [project]')
|
|
14
|
+
.description('View or update widget settings')
|
|
15
|
+
.option('--color <hex>', 'Button color (e.g. #22c55e)')
|
|
16
|
+
.option('--label <text>', 'Button label')
|
|
17
|
+
.option('--position <pos>', 'Widget position (bottom-right, bottom-left, top-right, top-left)')
|
|
18
|
+
.option('--intro <text>', 'Intro message shown in the widget')
|
|
19
|
+
.option('--success <text>', 'Success message after submission')
|
|
20
|
+
.option('--trigger <mode>', 'Trigger mode (floating, manual)')
|
|
21
|
+
.option('--display <mode>', 'Display mode (modal, popover)')
|
|
22
|
+
.option('--email-required', 'Require email from submitters')
|
|
23
|
+
.option('--no-email-required', 'Make email optional')
|
|
24
|
+
.option('--icon-only', 'Show only the icon, no label')
|
|
25
|
+
.option('--no-icon-only', 'Show both icon and label')
|
|
26
|
+
.action(async (projectArg, opts) => {
|
|
27
|
+
const writer = getWriter();
|
|
28
|
+
const client = requireClient();
|
|
29
|
+
const projectId = await resolveProjectId(client, projectArg);
|
|
30
|
+
const hasUpdates = opts.color || opts.label || opts.position || opts.intro ||
|
|
31
|
+
opts.success || opts.trigger || opts.display ||
|
|
32
|
+
opts.emailRequired !== undefined || opts.iconOnly !== undefined;
|
|
33
|
+
if (hasUpdates) {
|
|
34
|
+
// Update mode
|
|
35
|
+
const settings = {};
|
|
36
|
+
if (opts.color)
|
|
37
|
+
settings.buttonColor = opts.color;
|
|
38
|
+
if (opts.label)
|
|
39
|
+
settings.buttonLabel = opts.label;
|
|
40
|
+
if (opts.position)
|
|
41
|
+
settings.position = opts.position;
|
|
42
|
+
if (opts.intro)
|
|
43
|
+
settings.introMessage = opts.intro;
|
|
44
|
+
if (opts.success)
|
|
45
|
+
settings.successMessage = opts.success;
|
|
46
|
+
if (opts.trigger)
|
|
47
|
+
settings.triggerMode = opts.trigger;
|
|
48
|
+
if (opts.display)
|
|
49
|
+
settings.displayMode = opts.display;
|
|
50
|
+
if (opts.emailRequired !== undefined)
|
|
51
|
+
settings.emailRequired = opts.emailRequired;
|
|
52
|
+
if (opts.iconOnly !== undefined)
|
|
53
|
+
settings.iconOnly = opts.iconOnly;
|
|
54
|
+
const result = await client.updateWidgetSettings(projectId, settings);
|
|
55
|
+
if (!writer.isMachineOutput()) {
|
|
56
|
+
console.log(` ${brand.success('✓')} Widget settings updated for ${brand.bold(result.projectName)}`);
|
|
57
|
+
console.log();
|
|
58
|
+
}
|
|
59
|
+
writer.ok(result.settings, {
|
|
60
|
+
summary: `Updated widget for "${result.projectName}"`,
|
|
61
|
+
breadcrumbs: [
|
|
62
|
+
{ action: 'Get embed code', cmd: `feedbackbasket widget script ${projectId}` },
|
|
63
|
+
{ action: 'View settings', cmd: `feedbackbasket widget settings ${projectId}` },
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// View mode
|
|
69
|
+
const result = await client.getWidgetSettings(projectId);
|
|
70
|
+
if (!writer.isMachineOutput()) {
|
|
71
|
+
renderWidgetSettings(result.projectName, result.settings);
|
|
72
|
+
}
|
|
73
|
+
writer.ok(result.settings, {
|
|
74
|
+
summary: `Widget settings for "${result.projectName}"`,
|
|
75
|
+
breadcrumbs: [
|
|
76
|
+
{ action: 'Update color', cmd: `feedbackbasket widget settings ${projectId} --color "#22c55e"` },
|
|
77
|
+
{ action: 'Get embed code', cmd: `feedbackbasket widget script ${projectId}` },
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
// --- widget script ---
|
|
83
|
+
widget
|
|
84
|
+
.command('script [project]')
|
|
85
|
+
.description('Get the embed script for your website')
|
|
86
|
+
.action(async (projectArg) => {
|
|
87
|
+
const writer = getWriter();
|
|
88
|
+
const client = requireClient();
|
|
89
|
+
const projectId = await resolveProjectId(client, projectArg);
|
|
90
|
+
const result = await client.getWidgetScript(projectId);
|
|
91
|
+
if (!writer.isMachineOutput()) {
|
|
92
|
+
console.log(brand.bold(`Widget embed code for ${result.projectName}`));
|
|
93
|
+
console.log(divider(50));
|
|
94
|
+
console.log();
|
|
95
|
+
console.log(brand.muted(' Add this to your HTML, before </body>:'));
|
|
96
|
+
console.log();
|
|
97
|
+
console.log(` ${brand.primary(result.embedCode)}`);
|
|
98
|
+
console.log();
|
|
99
|
+
console.log(brand.muted(` Script URL: ${result.scriptUrl}`));
|
|
100
|
+
console.log();
|
|
101
|
+
}
|
|
102
|
+
writer.ok(result, {
|
|
103
|
+
summary: `Embed code for "${result.projectName}"`,
|
|
104
|
+
breadcrumbs: [
|
|
105
|
+
{ action: 'Customize widget', cmd: `feedbackbasket widget settings ${projectId}` },
|
|
106
|
+
{ action: 'View project', cmd: `feedbackbasket projects show ${projectId}` },
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
return widget;
|
|
111
|
+
}
|
|
112
|
+
function requireClient() {
|
|
113
|
+
const manager = new AuthManager();
|
|
114
|
+
const token = manager.resolveToken();
|
|
115
|
+
if (!token)
|
|
116
|
+
throw errAuth();
|
|
117
|
+
const config = loadConfig();
|
|
118
|
+
return new FeedbackBasketClient(token, config.baseUrl);
|
|
119
|
+
}
|
|
120
|
+
async function resolveProjectId(client, projectArg) {
|
|
121
|
+
if (projectArg) {
|
|
122
|
+
const project = await resolveProject(client, projectArg);
|
|
123
|
+
return project.id;
|
|
124
|
+
}
|
|
125
|
+
const config = loadConfig();
|
|
126
|
+
if (config.defaultProject)
|
|
127
|
+
return config.defaultProject;
|
|
128
|
+
throw errUsage('Project is required. Pass a project name/ID or set a default.', 'feedbackbasket widget settings <project> or feedbackbasket config set defaultProject <id>');
|
|
129
|
+
}
|
|
130
|
+
function renderWidgetSettings(projectName, settings) {
|
|
131
|
+
console.log(brand.bold(`Widget settings — ${projectName}`));
|
|
132
|
+
console.log(divider(40));
|
|
133
|
+
console.log();
|
|
134
|
+
const display = [
|
|
135
|
+
['Button Color', String(settings.buttonColor ?? '')],
|
|
136
|
+
['Button Label', String(settings.buttonLabel ?? '')],
|
|
137
|
+
['Button Radius', String(settings.buttonRadius ?? '')],
|
|
138
|
+
['Icon Only', String(settings.iconOnly ?? false)],
|
|
139
|
+
['Show Icon', String(settings.showIcon ?? true)],
|
|
140
|
+
['Position', String(settings.position ?? '')],
|
|
141
|
+
['Trigger Mode', String(settings.triggerMode ?? '')],
|
|
142
|
+
['Display Mode', String(settings.displayMode ?? '')],
|
|
143
|
+
['Email Required', String(settings.emailRequired ?? false)],
|
|
144
|
+
['Intro Message', String(settings.introMessage ?? '')],
|
|
145
|
+
['Success Message', String(settings.successMessage ?? '')],
|
|
146
|
+
['Z-Index', String(settings.zIndex ?? '')],
|
|
147
|
+
['Show Branding', String(settings.showBranding ?? true)],
|
|
148
|
+
];
|
|
149
|
+
for (const [label, value] of display) {
|
|
150
|
+
console.log(` ${brand.label(label.padEnd(18))} ${value}`);
|
|
151
|
+
}
|
|
152
|
+
console.log();
|
|
153
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
defaultProject?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function setBaseUrlOverride(url: string): void;
|
|
6
|
+
export declare function configDir(): string;
|
|
7
|
+
export declare function ensureConfigDir(): void;
|
|
8
|
+
export declare function configPath(): string;
|
|
9
|
+
export declare function loadConfig(): Config;
|
|
10
|
+
export declare function saveConfig(config: Config): void;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
const DEFAULT_BASE_URL = 'https://feedbackbasket.com';
|
|
5
|
+
// Runtime override from --base-url flag
|
|
6
|
+
let baseUrlOverride;
|
|
7
|
+
export function setBaseUrlOverride(url) {
|
|
8
|
+
baseUrlOverride = url;
|
|
9
|
+
}
|
|
10
|
+
export function configDir() {
|
|
11
|
+
return join(homedir(), '.config', 'feedbackbasket');
|
|
12
|
+
}
|
|
13
|
+
export function ensureConfigDir() {
|
|
14
|
+
const dir = configDir();
|
|
15
|
+
if (!existsSync(dir)) {
|
|
16
|
+
mkdirSync(dir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function configPath() {
|
|
20
|
+
return join(configDir(), 'config.json');
|
|
21
|
+
}
|
|
22
|
+
export function loadConfig() {
|
|
23
|
+
const envUrl = process.env['FEEDBACKBASKET_BASE_URL'];
|
|
24
|
+
try {
|
|
25
|
+
const raw = readFileSync(configPath(), 'utf-8');
|
|
26
|
+
const parsed = JSON.parse(raw);
|
|
27
|
+
return {
|
|
28
|
+
baseUrl: baseUrlOverride ?? envUrl ?? parsed.baseUrl ?? DEFAULT_BASE_URL,
|
|
29
|
+
defaultProject: parsed.defaultProject,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return {
|
|
34
|
+
baseUrl: baseUrlOverride ?? envUrl ?? DEFAULT_BASE_URL,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function saveConfig(config) {
|
|
39
|
+
ensureConfigDir();
|
|
40
|
+
writeFileSync(configPath(), JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
41
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface Credentials {
|
|
2
|
+
token: string;
|
|
3
|
+
scope: 'read' | 'full';
|
|
4
|
+
userId?: string;
|
|
5
|
+
email?: string;
|
|
6
|
+
organizationId?: string;
|
|
7
|
+
createdAt: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function loadCredentials(): Credentials | null;
|
|
10
|
+
export declare function saveCredentials(creds: Credentials): void;
|
|
11
|
+
export declare function clearCredentials(): void;
|
|
12
|
+
export declare function maskToken(token: string): string;
|