feedbackbasket-cli 0.9.3 → 0.11.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 +48 -0
- package/dist/src/cli.js +4 -0
- package/dist/src/client.d.ts +17 -3
- package/dist/src/client.js +73 -35
- package/dist/src/commands/feedback-reply.d.ts +2 -0
- package/dist/src/commands/feedback-reply.js +23 -13
- package/dist/src/commands/feedback.js +1 -1
- package/dist/src/commands/mobile.d.ts +7 -0
- package/dist/src/commands/mobile.js +292 -0
- package/dist/src/commands/waitlist.d.ts +3 -0
- package/dist/src/commands/waitlist.js +99 -0
- package/dist/src/commands/widget.js +55 -3
- package/dist/src/help.js +8 -1
- package/dist/src/types.d.ts +50 -1
- package/dist/src/version.d.ts +2 -2
- package/dist/src/version.js +1 -1
- package/package.json +4 -4
- package/skills/feedbackbasket/SKILL.md +95 -18
|
@@ -0,0 +1,292 @@
|
|
|
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
|
+
const BUNDLE_ID_PATTERN = /^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/;
|
|
10
|
+
const SAFE_PROJECT_REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*(?: [A-Za-z0-9][A-Za-z0-9._-]*)*$/;
|
|
11
|
+
const VERIFY_POLL_INTERVAL_MS = 5_000;
|
|
12
|
+
const MAX_VERIFY_WAIT_SECONDS = 300;
|
|
13
|
+
export function createMobileCommand(getWriter) {
|
|
14
|
+
const mobile = new Command('mobile')
|
|
15
|
+
.description('Set up and verify mobile app feedback');
|
|
16
|
+
mobile
|
|
17
|
+
.command('status [project]')
|
|
18
|
+
.description('Show mobile feedback configuration and connection status')
|
|
19
|
+
.option('--include-publishable-key', 'Include the full publishable mobile project key')
|
|
20
|
+
.action(async (projectArg, opts) => {
|
|
21
|
+
const writer = getWriter();
|
|
22
|
+
const client = requireClient();
|
|
23
|
+
const projectId = await resolveProjectId(client, projectArg);
|
|
24
|
+
const ref = projectRef(projectArg, projectId);
|
|
25
|
+
const result = await client.getMobileIntegration(projectId, Boolean(opts.includePublishableKey));
|
|
26
|
+
if (!writer.isMachineOutput())
|
|
27
|
+
renderMobileStatus(result);
|
|
28
|
+
writer.ok(result, {
|
|
29
|
+
summary: `Mobile feedback for "${result.project.name}"`,
|
|
30
|
+
breadcrumbs: result.integration
|
|
31
|
+
? [
|
|
32
|
+
{ action: 'Verify SDK connection', cmd: `feedbackbasket mobile verify ${ref}` },
|
|
33
|
+
{ action: 'Add a bundle ID', cmd: `feedbackbasket mobile bundle-ids ${ref} --add com.example.app` },
|
|
34
|
+
]
|
|
35
|
+
: [
|
|
36
|
+
{ action: 'Enable mobile feedback', cmd: `feedbackbasket mobile setup ${ref}` },
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
mobile
|
|
41
|
+
.command('setup [project]')
|
|
42
|
+
.description('Enable mobile feedback and add allowed bundle IDs')
|
|
43
|
+
.option('--bundle-id <bundle-id>', 'Allowed iOS bundle ID (repeatable)', collect, [])
|
|
44
|
+
.option('--include-publishable-key', 'Include the full publishable key required to configure the app')
|
|
45
|
+
.action(async (projectArg, opts) => {
|
|
46
|
+
const writer = getWriter();
|
|
47
|
+
const client = requireClient();
|
|
48
|
+
const projectId = await resolveProjectId(client, projectArg);
|
|
49
|
+
const bundleIds = validateBundleIds(opts.bundleId);
|
|
50
|
+
const result = await client.updateMobileIntegration(projectId, { enabled: true, addBundleIds: bundleIds }, Boolean(opts.includePublishableKey));
|
|
51
|
+
if (!writer.isMachineOutput()) {
|
|
52
|
+
console.log(` ${brand.success('✓')} Mobile feedback enabled for ${brand.bold(result.project.name)}`);
|
|
53
|
+
if (!opts.includePublishableKey) {
|
|
54
|
+
console.log(` ${brand.muted('The publishable key is masked. Re-run with --include-publishable-key when configuring the app.')}`);
|
|
55
|
+
}
|
|
56
|
+
console.log();
|
|
57
|
+
}
|
|
58
|
+
const ref = projectRef(projectArg, projectId);
|
|
59
|
+
writer.ok(withSetupGuidance(result), {
|
|
60
|
+
summary: `Mobile feedback ready for "${result.project.name}"`,
|
|
61
|
+
breadcrumbs: [
|
|
62
|
+
{ action: 'Check connection', cmd: `feedbackbasket mobile verify ${ref}` },
|
|
63
|
+
{ action: 'View mobile status', cmd: `feedbackbasket mobile status ${ref}` },
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
mobile
|
|
68
|
+
.command('bundle-ids [project]')
|
|
69
|
+
.description('Add or remove allowed iOS bundle IDs')
|
|
70
|
+
.option('--add <bundle-id>', 'Bundle ID to add (repeatable)', collect, [])
|
|
71
|
+
.option('--remove <bundle-id>', 'Bundle ID to remove (repeatable)', collect, [])
|
|
72
|
+
.action(async (projectArg, opts) => {
|
|
73
|
+
const writer = getWriter();
|
|
74
|
+
const client = requireClient();
|
|
75
|
+
const additions = validateBundleIds(opts.add);
|
|
76
|
+
const removals = validateBundleIds(opts.remove);
|
|
77
|
+
if (additions.length === 0 && removals.length === 0) {
|
|
78
|
+
throw errUsage('Pass at least one --add or --remove bundle ID', 'Example: feedbackbasket mobile bundle-ids myapp --add com.example.app');
|
|
79
|
+
}
|
|
80
|
+
const overlap = additions.filter((bundleId) => removals.includes(bundleId));
|
|
81
|
+
if (overlap.length > 0) {
|
|
82
|
+
throw errUsage(`The same bundle ID cannot be added and removed: ${overlap.join(', ')}`);
|
|
83
|
+
}
|
|
84
|
+
const projectId = await resolveProjectId(client, projectArg);
|
|
85
|
+
const result = await client.updateMobileIntegration(projectId, {
|
|
86
|
+
addBundleIds: additions,
|
|
87
|
+
removeBundleIds: removals,
|
|
88
|
+
});
|
|
89
|
+
if (!writer.isMachineOutput()) {
|
|
90
|
+
console.log(` ${brand.success('✓')} Allowed bundle IDs updated for ${brand.bold(result.project.name)}`);
|
|
91
|
+
console.log();
|
|
92
|
+
}
|
|
93
|
+
writer.ok(result, {
|
|
94
|
+
summary: `Updated mobile bundle IDs for "${result.project.name}"`,
|
|
95
|
+
breadcrumbs: [
|
|
96
|
+
{ action: 'View mobile status', cmd: `feedbackbasket mobile status ${projectRef(projectArg, projectId)}` },
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
mobile
|
|
101
|
+
.command('verify [project]')
|
|
102
|
+
.description('Verify that the SDK has connected from the expected app')
|
|
103
|
+
.option('--bundle-id <bundle-id>', 'Expected bundle ID')
|
|
104
|
+
.option('--wait <seconds>', 'Wait for the first matching heartbeat (maximum 300 seconds)', '0')
|
|
105
|
+
.action(async (projectArg, opts) => {
|
|
106
|
+
const writer = getWriter();
|
|
107
|
+
const client = requireClient();
|
|
108
|
+
const projectId = await resolveProjectId(client, projectArg);
|
|
109
|
+
const expectedBundleId = opts.bundleId
|
|
110
|
+
? validateBundleIds([opts.bundleId])[0]
|
|
111
|
+
: undefined;
|
|
112
|
+
const waitSeconds = parseWaitSeconds(opts.wait);
|
|
113
|
+
const deadline = Date.now() + waitSeconds * 1_000;
|
|
114
|
+
let result = await client.getMobileIntegration(projectId);
|
|
115
|
+
while (!isMobileConnectionVerified(result, expectedBundleId) && Date.now() < deadline) {
|
|
116
|
+
await sleep(Math.min(VERIFY_POLL_INTERVAL_MS, deadline - Date.now()));
|
|
117
|
+
result = await client.getMobileIntegration(projectId);
|
|
118
|
+
}
|
|
119
|
+
const verified = isMobileConnectionVerified(result, expectedBundleId);
|
|
120
|
+
const verification = {
|
|
121
|
+
verified,
|
|
122
|
+
expectedBundleId: expectedBundleId ?? null,
|
|
123
|
+
project: result.project,
|
|
124
|
+
integration: result.integration,
|
|
125
|
+
};
|
|
126
|
+
if (!writer.isMachineOutput())
|
|
127
|
+
renderVerification(verification);
|
|
128
|
+
writer.ok(verification, {
|
|
129
|
+
summary: verified
|
|
130
|
+
? `Verified mobile connection for "${result.project.name}"`
|
|
131
|
+
: `Mobile connection not yet verified for "${result.project.name}"`,
|
|
132
|
+
notice: verified
|
|
133
|
+
? undefined
|
|
134
|
+
: 'Build and launch the app, then run this command again with --wait 120.',
|
|
135
|
+
});
|
|
136
|
+
if (!verified)
|
|
137
|
+
process.exitCode = 2;
|
|
138
|
+
});
|
|
139
|
+
mobile
|
|
140
|
+
.command('disable [project]')
|
|
141
|
+
.description('Disable mobile feedback without changing the website widget')
|
|
142
|
+
.option('--yes', 'Confirm disabling mobile feedback')
|
|
143
|
+
.action(async (projectArg, opts) => {
|
|
144
|
+
const writer = getWriter();
|
|
145
|
+
const client = requireClient();
|
|
146
|
+
const projectId = await resolveProjectId(client, projectArg);
|
|
147
|
+
await requireDestructiveConfirmation(writer, Boolean(opts.yes), 'Disable mobile feedback? Installed apps will stop submitting feedback.', '--yes is required to disable mobile feedback in agent mode');
|
|
148
|
+
const result = await client.updateMobileIntegration(projectId, { enabled: false });
|
|
149
|
+
if (!writer.isMachineOutput()) {
|
|
150
|
+
console.log(` ${brand.success('✓')} Mobile feedback disabled for ${brand.bold(result.project.name)}`);
|
|
151
|
+
console.log();
|
|
152
|
+
}
|
|
153
|
+
writer.ok(result, {
|
|
154
|
+
summary: `Disabled mobile feedback for "${result.project.name}"`,
|
|
155
|
+
breadcrumbs: [
|
|
156
|
+
{ action: 'Re-enable mobile feedback', cmd: `feedbackbasket mobile setup ${projectRef(projectArg, projectId)}` },
|
|
157
|
+
],
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
mobile
|
|
161
|
+
.command('rotate-key [project]')
|
|
162
|
+
.description('Rotate the publishable mobile key (existing app builds will stop working)')
|
|
163
|
+
.option('--yes', 'Confirm key rotation')
|
|
164
|
+
.option('--include-publishable-key', 'Include the newly generated publishable key')
|
|
165
|
+
.action(async (projectArg, opts) => {
|
|
166
|
+
const writer = getWriter();
|
|
167
|
+
const client = requireClient();
|
|
168
|
+
const projectId = await resolveProjectId(client, projectArg);
|
|
169
|
+
await requireDestructiveConfirmation(writer, Boolean(opts.yes), 'Rotate the mobile project key? Existing app builds will stop submitting feedback.', '--yes is required to rotate a mobile project key in agent mode');
|
|
170
|
+
const result = await client.rotateMobileProjectKey(projectId, Boolean(opts.includePublishableKey));
|
|
171
|
+
if (!writer.isMachineOutput()) {
|
|
172
|
+
console.log(` ${brand.warning('!')} Mobile project key rotated for ${brand.bold(result.project.name)}`);
|
|
173
|
+
console.log(' Update and release every installed app that used the previous key.');
|
|
174
|
+
console.log();
|
|
175
|
+
}
|
|
176
|
+
writer.ok(result, {
|
|
177
|
+
summary: `Rotated mobile project key for "${result.project.name}"`,
|
|
178
|
+
notice: 'Existing app builds using the previous key can no longer submit feedback.',
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
return mobile;
|
|
182
|
+
}
|
|
183
|
+
function requireClient() {
|
|
184
|
+
const manager = new AuthManager();
|
|
185
|
+
const token = manager.resolveToken();
|
|
186
|
+
if (!token)
|
|
187
|
+
throw errAuth();
|
|
188
|
+
return new FeedbackBasketClient(token, loadConfig().baseUrl);
|
|
189
|
+
}
|
|
190
|
+
async function resolveProjectId(client, projectArg) {
|
|
191
|
+
if (projectArg)
|
|
192
|
+
return (await resolveProject(client, projectArg)).id;
|
|
193
|
+
const config = loadConfig();
|
|
194
|
+
if (config.defaultProject)
|
|
195
|
+
return config.defaultProject;
|
|
196
|
+
throw errUsage('Project is required. Pass a project name/ID or set a default.', 'feedbackbasket mobile status <project>');
|
|
197
|
+
}
|
|
198
|
+
function collect(value, previous) {
|
|
199
|
+
return [...previous, value];
|
|
200
|
+
}
|
|
201
|
+
export function projectRef(projectArg, projectId) {
|
|
202
|
+
const ref = projectArg?.trim();
|
|
203
|
+
if (!ref || !SAFE_PROJECT_REF_PATTERN.test(ref))
|
|
204
|
+
return projectId;
|
|
205
|
+
return ref.includes(' ') ? `"${ref}"` : ref;
|
|
206
|
+
}
|
|
207
|
+
export function validateBundleIds(bundleIds) {
|
|
208
|
+
const normalized = Array.from(new Set(bundleIds.map((value) => value.trim()).filter(Boolean)));
|
|
209
|
+
const invalid = normalized.find((bundleId) => !BUNDLE_ID_PATTERN.test(bundleId));
|
|
210
|
+
if (invalid) {
|
|
211
|
+
throw errUsage(`Invalid bundle ID "${invalid}"`, 'Bundle IDs must look like com.example.app');
|
|
212
|
+
}
|
|
213
|
+
if (normalized.length > 20)
|
|
214
|
+
throw errUsage('A project can have at most 20 bundle IDs');
|
|
215
|
+
return normalized;
|
|
216
|
+
}
|
|
217
|
+
export function isMobileConnectionVerified(result, expectedBundleId) {
|
|
218
|
+
const integration = result.integration;
|
|
219
|
+
if (!integration?.enabled || !integration.connection.connected)
|
|
220
|
+
return false;
|
|
221
|
+
return !expectedBundleId || integration.connection.bundleId === expectedBundleId;
|
|
222
|
+
}
|
|
223
|
+
function parseWaitSeconds(value) {
|
|
224
|
+
const seconds = Number(value);
|
|
225
|
+
if (!Number.isInteger(seconds) || seconds < 0 || seconds > MAX_VERIFY_WAIT_SECONDS) {
|
|
226
|
+
throw errUsage(`--wait must be a whole number between 0 and ${MAX_VERIFY_WAIT_SECONDS}`);
|
|
227
|
+
}
|
|
228
|
+
return seconds;
|
|
229
|
+
}
|
|
230
|
+
function sleep(milliseconds) {
|
|
231
|
+
return new Promise((resolve) => setTimeout(resolve, Math.max(0, milliseconds)));
|
|
232
|
+
}
|
|
233
|
+
async function requireDestructiveConfirmation(writer, confirmedByFlag, question, agentHint) {
|
|
234
|
+
if (confirmedByFlag)
|
|
235
|
+
return;
|
|
236
|
+
if (writer.isMachineOutput() || !process.stdin.isTTY) {
|
|
237
|
+
throw errUsage(agentHint);
|
|
238
|
+
}
|
|
239
|
+
if (!(await confirm(` ${question}`, false))) {
|
|
240
|
+
throw errUsage('Action cancelled');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function withSetupGuidance(result) {
|
|
244
|
+
return {
|
|
245
|
+
...result,
|
|
246
|
+
setup: {
|
|
247
|
+
publishableKeyIncluded: result.integration?.publishableKeyIncluded ?? false,
|
|
248
|
+
supportedIntegrations: ['swiftui', 'uikit', 'react-native', 'flutter', 'hosted-form'],
|
|
249
|
+
nextSteps: result.integration?.publishableKeyIncluded
|
|
250
|
+
? [
|
|
251
|
+
'Detect the mobile framework and minimum supported platform version.',
|
|
252
|
+
'Configure the app with the returned publishable mobile project key.',
|
|
253
|
+
'Add an accessible Send feedback action to an existing Settings, Help, or Support screen.',
|
|
254
|
+
'Build and launch the app, then verify its SDK heartbeat.',
|
|
255
|
+
]
|
|
256
|
+
: [
|
|
257
|
+
'Re-run setup with --include-publishable-key when you are ready to configure the app.',
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function renderMobileStatus(result) {
|
|
263
|
+
console.log(brand.bold(`Mobile feedback — ${result.project.name}`));
|
|
264
|
+
console.log(divider(54));
|
|
265
|
+
if (!result.integration) {
|
|
266
|
+
console.log(brand.muted(' Mobile feedback is not enabled.'));
|
|
267
|
+
console.log();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const integration = result.integration;
|
|
271
|
+
console.log(` ${brand.label('Status'.padEnd(18))} ${integration.enabled ? 'Enabled' : 'Disabled'}`);
|
|
272
|
+
console.log(` ${brand.label('Publishable key'.padEnd(18))} ${integration.publishableKey}`);
|
|
273
|
+
console.log(` ${brand.label('Bundle IDs'.padEnd(18))} ${integration.bundleIds.join(', ') || 'Any bundle ID'}`);
|
|
274
|
+
console.log(` ${brand.label('Connection'.padEnd(18))} ${integration.connection.connected ? 'Connected' : 'Waiting for first heartbeat'}`);
|
|
275
|
+
if (integration.connection.lastSeenAt) {
|
|
276
|
+
console.log(` ${brand.label('Last seen'.padEnd(18))} ${integration.connection.lastSeenAt}`);
|
|
277
|
+
}
|
|
278
|
+
if (integration.connection.bundleId) {
|
|
279
|
+
console.log(` ${brand.label('Last bundle'.padEnd(18))} ${integration.connection.bundleId}`);
|
|
280
|
+
}
|
|
281
|
+
console.log();
|
|
282
|
+
}
|
|
283
|
+
function renderVerification(result) {
|
|
284
|
+
const icon = result.verified ? brand.success('✓') : brand.warning('!');
|
|
285
|
+
console.log(` ${icon} ${result.verified ? 'Mobile SDK connection verified' : 'Mobile SDK connection not yet verified'}`);
|
|
286
|
+
if (result.expectedBundleId)
|
|
287
|
+
console.log(` ${brand.muted(`Expected bundle: ${result.expectedBundleId}`)}`);
|
|
288
|
+
if (result.integration?.connection.lastSeenAt) {
|
|
289
|
+
console.log(` ${brand.muted(`Last heartbeat: ${result.integration.connection.lastSeenAt}`)}`);
|
|
290
|
+
}
|
|
291
|
+
console.log();
|
|
292
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
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 { Format } from '../output/writer.js';
|
|
7
|
+
import { brand, divider } from '../output/theme.js';
|
|
8
|
+
import { resolveProject } from '../resolve.js';
|
|
9
|
+
export function createWaitlistCommand(getWriter) {
|
|
10
|
+
const waitlist = new Command('waitlist')
|
|
11
|
+
.description('View and export waitlist signups');
|
|
12
|
+
waitlist
|
|
13
|
+
.command('list [project]')
|
|
14
|
+
.description('List waitlist signups with optional search')
|
|
15
|
+
.option('--search <query>', 'Search email addresses and names')
|
|
16
|
+
.option('--limit <n>', 'Max results (1-100)', '50')
|
|
17
|
+
.option('--offset <n>', 'Offset for pagination', '0')
|
|
18
|
+
.action(async (projectArg, opts) => {
|
|
19
|
+
const writer = getWriter();
|
|
20
|
+
const client = requireClient();
|
|
21
|
+
const projectId = await resolveProjectId(client, projectArg);
|
|
22
|
+
const limit = parseInteger(opts.limit, '--limit', 1, 100);
|
|
23
|
+
const offset = parseInteger(opts.offset, '--offset', 0);
|
|
24
|
+
const result = await client.getWaitlist(projectId, {
|
|
25
|
+
search: opts.search,
|
|
26
|
+
limit,
|
|
27
|
+
offset,
|
|
28
|
+
});
|
|
29
|
+
if (writer.effectiveFormat() === Format.Styled) {
|
|
30
|
+
renderWaitlist(result.project.name, result.project.captureMode, result.entries, result.totalSignups, result.pagination.totalCount);
|
|
31
|
+
if (result.pagination.hasMore) {
|
|
32
|
+
console.log(brand.muted(` Next page: feedbackbasket waitlist list ${projectId} --offset ${offset + limit} --limit ${limit}`));
|
|
33
|
+
console.log();
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
writer.ok(result, {
|
|
38
|
+
summary: `Showing ${result.entries.length} of ${result.pagination.totalCount} matching waitlist signups`,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
waitlist
|
|
42
|
+
.command('export [project]')
|
|
43
|
+
.description('Export all waitlist signups as CSV')
|
|
44
|
+
.action(async (projectArg) => {
|
|
45
|
+
const client = requireClient();
|
|
46
|
+
const projectId = await resolveProjectId(client, projectArg);
|
|
47
|
+
console.log(await client.exportWaitlist(projectId));
|
|
48
|
+
});
|
|
49
|
+
return waitlist;
|
|
50
|
+
}
|
|
51
|
+
function requireClient() {
|
|
52
|
+
const manager = new AuthManager();
|
|
53
|
+
const token = manager.resolveToken();
|
|
54
|
+
if (!token)
|
|
55
|
+
throw errAuth();
|
|
56
|
+
const config = loadConfig();
|
|
57
|
+
return new FeedbackBasketClient(token, config.baseUrl);
|
|
58
|
+
}
|
|
59
|
+
async function resolveProjectId(client, projectArg) {
|
|
60
|
+
if (projectArg)
|
|
61
|
+
return (await resolveProject(client, projectArg)).id;
|
|
62
|
+
const config = loadConfig();
|
|
63
|
+
if (config.defaultProject)
|
|
64
|
+
return config.defaultProject;
|
|
65
|
+
throw errUsage('Project is required. Pass a project name/ID or set a default.', 'feedbackbasket waitlist list <project>');
|
|
66
|
+
}
|
|
67
|
+
function parseInteger(value, flag, minimum, maximum) {
|
|
68
|
+
const parsed = Number(value);
|
|
69
|
+
if (!Number.isInteger(parsed) ||
|
|
70
|
+
parsed < minimum ||
|
|
71
|
+
(maximum !== undefined && parsed > maximum)) {
|
|
72
|
+
const range = maximum === undefined ? `${minimum} or greater` : `${minimum}-${maximum}`;
|
|
73
|
+
throw errUsage(`${flag} must be an integer in the range ${range}`);
|
|
74
|
+
}
|
|
75
|
+
return parsed;
|
|
76
|
+
}
|
|
77
|
+
function renderWaitlist(projectName, captureMode, entries, totalSignups, matchingSignups) {
|
|
78
|
+
console.log(brand.bold(`Waitlist — ${projectName}`));
|
|
79
|
+
console.log(divider(50));
|
|
80
|
+
console.log();
|
|
81
|
+
console.log(` ${brand.label('Capture mode')} ${captureMode}`);
|
|
82
|
+
console.log(` ${brand.label('Total signups')} ${totalSignups}`);
|
|
83
|
+
if (matchingSignups !== totalSignups) {
|
|
84
|
+
console.log(` ${brand.label('Matches')} ${matchingSignups}`);
|
|
85
|
+
}
|
|
86
|
+
console.log();
|
|
87
|
+
if (entries.length === 0) {
|
|
88
|
+
console.log(` ${brand.muted('No waitlist signups found')}`);
|
|
89
|
+
console.log();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
console.log(` ${brand.bold(entry.email)}${entry.name ? ` — ${entry.name}` : ''}`);
|
|
94
|
+
console.log(` ${brand.muted(new Date(entry.updatedAt).toLocaleString())}`);
|
|
95
|
+
if (entry.pageUrl)
|
|
96
|
+
console.log(` ${brand.muted(entry.pageUrl)}`);
|
|
97
|
+
console.log();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -51,6 +51,7 @@ export function createWidgetCommand(getWriter) {
|
|
|
51
51
|
widget
|
|
52
52
|
.command('settings [project]')
|
|
53
53
|
.description('View or update widget settings')
|
|
54
|
+
.option('--capture-mode <mode>', 'Capture mode (feedback, waitlist)')
|
|
54
55
|
.option('--color <hex>', 'Button color (e.g. #22c55e)')
|
|
55
56
|
.option('--label <text>', 'Button label')
|
|
56
57
|
.option('--position <pos>', 'Widget position (bottom-right, bottom-left, middle-right-edge, middle-left-edge, bottom-right-edge, bottom-left-edge)')
|
|
@@ -77,6 +78,10 @@ export function createWidgetCommand(getWriter) {
|
|
|
77
78
|
.option('--no-show-icon', 'Hide the icon')
|
|
78
79
|
.option('--show-branding', 'Show FeedbackBasket branding')
|
|
79
80
|
.option('--no-show-branding', 'Hide FeedbackBasket branding when plan allows it')
|
|
81
|
+
.option('--allow-console-errors', 'Let visitors include captured console errors with feedback')
|
|
82
|
+
.option('--no-allow-console-errors', 'Hide the console error sharing option')
|
|
83
|
+
.option('--error-tracking', 'Enable automatic browser error tracking')
|
|
84
|
+
.option('--no-error-tracking', 'Disable automatic browser error tracking')
|
|
80
85
|
.option('--z-index <value>', 'Widget z-index')
|
|
81
86
|
.option('--guided', 'Enable guided feedback types')
|
|
82
87
|
.option('--disable-guided', 'Disable guided feedback types')
|
|
@@ -84,20 +89,26 @@ export function createWidgetCommand(getWriter) {
|
|
|
84
89
|
const writer = getWriter();
|
|
85
90
|
const client = requireClient();
|
|
86
91
|
const projectId = await resolveProjectId(client, projectArg);
|
|
87
|
-
const hasUpdates = opts.color || opts.label || opts.position || opts.intro ||
|
|
92
|
+
const hasUpdates = opts.captureMode || opts.color || opts.label || opts.position || opts.intro ||
|
|
88
93
|
opts.success || opts.trigger || opts.display ||
|
|
89
94
|
opts.buttonRadius || opts.buttonSize || opts.icon ||
|
|
90
95
|
opts.emailRequired !== undefined || opts.showEmail !== undefined ||
|
|
91
96
|
opts.emailReadOnly !== undefined || opts.hideEmailWhenPrefilled !== undefined ||
|
|
92
97
|
opts.allowAttachments !== undefined || opts.iconOnly !== undefined ||
|
|
93
98
|
opts.showIcon !== undefined || opts.showBranding !== undefined ||
|
|
99
|
+
opts.allowConsoleErrors !== undefined || opts.errorTracking !== undefined ||
|
|
94
100
|
opts.zIndex || opts.guided || opts.disableGuided;
|
|
95
101
|
if (hasUpdates) {
|
|
96
102
|
if (opts.guided && opts.disableGuided) {
|
|
97
103
|
throw errUsage('Choose either --guided or --disable-guided, not both');
|
|
98
104
|
}
|
|
105
|
+
if (opts.captureMode && !['feedback', 'waitlist'].includes(opts.captureMode)) {
|
|
106
|
+
throw errUsage('Capture mode must be feedback or waitlist');
|
|
107
|
+
}
|
|
99
108
|
// Update mode
|
|
100
109
|
const settings = {};
|
|
110
|
+
if (opts.captureMode)
|
|
111
|
+
settings.captureMode = opts.captureMode;
|
|
101
112
|
if (opts.color)
|
|
102
113
|
settings.buttonColor = opts.color;
|
|
103
114
|
if (opts.label)
|
|
@@ -134,6 +145,10 @@ export function createWidgetCommand(getWriter) {
|
|
|
134
145
|
settings.showIcon = opts.showIcon;
|
|
135
146
|
if (opts.showBranding !== undefined)
|
|
136
147
|
settings.showBranding = opts.showBranding;
|
|
148
|
+
if (opts.allowConsoleErrors !== undefined)
|
|
149
|
+
settings.allowConsoleErrors = opts.allowConsoleErrors;
|
|
150
|
+
if (opts.errorTracking !== undefined)
|
|
151
|
+
settings.errorTrackingEnabled = opts.errorTracking;
|
|
137
152
|
if (opts.zIndex)
|
|
138
153
|
settings.zIndex = parseInt(opts.zIndex, 10);
|
|
139
154
|
if (opts.guided || opts.disableGuided) {
|
|
@@ -253,9 +268,26 @@ export function createWidgetCommand(getWriter) {
|
|
|
253
268
|
console.log();
|
|
254
269
|
console.log(brand.muted(` Script URL: ${result.scriptUrl}`));
|
|
255
270
|
console.log();
|
|
256
|
-
|
|
271
|
+
if (settingsResult.settings.captureMode === 'waitlist') {
|
|
272
|
+
renderWaitlistHelp();
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
renderInlineTriggerHelp(settingsResult.settings);
|
|
276
|
+
}
|
|
257
277
|
}
|
|
258
|
-
|
|
278
|
+
const output = result.captureMode === 'waitlist'
|
|
279
|
+
? {
|
|
280
|
+
...result,
|
|
281
|
+
waitlist: {
|
|
282
|
+
formAttribute: 'data-feedbackbasket-waitlist',
|
|
283
|
+
requiredField: 'email',
|
|
284
|
+
optionalField: 'name',
|
|
285
|
+
stateAttribute: 'data-feedbackbasket-state',
|
|
286
|
+
events: ['feedbackbasket:waitlist:success', 'feedbackbasket:waitlist:error'],
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
: result;
|
|
290
|
+
writer.ok(output, {
|
|
259
291
|
summary: `Embed code for "${result.projectName}"`,
|
|
260
292
|
breadcrumbs: [
|
|
261
293
|
{ action: 'Customize widget', cmd: `feedbackbasket widget settings ${projectId}` },
|
|
@@ -362,6 +394,23 @@ function renderInlineTriggerHelp(settings) {
|
|
|
362
394
|
console.log(brand.muted(' Modal mode stays centered; passing the trigger is safe for future popup changes.'));
|
|
363
395
|
}
|
|
364
396
|
console.log(brand.muted(' Existing calls to window.FeedbackWidget.openFeedbackForm() still work.'));
|
|
397
|
+
console.log(brand.muted(' Do not call internal methods such as open() or openModal().'));
|
|
398
|
+
console.log();
|
|
399
|
+
}
|
|
400
|
+
function renderWaitlistHelp() {
|
|
401
|
+
console.log(brand.bold('Waitlist form setup'));
|
|
402
|
+
console.log(divider(50));
|
|
403
|
+
console.log();
|
|
404
|
+
console.log(brand.muted(' Keep the project script installed and annotate your own form:'));
|
|
405
|
+
console.log();
|
|
406
|
+
console.log(` ${brand.primary('<form data-feedbackbasket-waitlist>')}`);
|
|
407
|
+
console.log(` ${brand.primary(' <input name="name" autocomplete="name">')}`);
|
|
408
|
+
console.log(` ${brand.primary(' <input name="email" type="email" autocomplete="email" required>')}`);
|
|
409
|
+
console.log(` ${brand.primary(' <button type="submit">Join the waitlist</button>')}`);
|
|
410
|
+
console.log(` ${brand.primary('</form>')}`);
|
|
411
|
+
console.log();
|
|
412
|
+
console.log(brand.muted(' Email is required; name is optional. The script handles submission without replacing your styling.'));
|
|
413
|
+
console.log(brand.muted(' Read data-feedbackbasket-state for loading, success, and error UI.'));
|
|
365
414
|
console.log();
|
|
366
415
|
}
|
|
367
416
|
function renderWidgetSettings(projectName, settings) {
|
|
@@ -369,6 +418,7 @@ function renderWidgetSettings(projectName, settings) {
|
|
|
369
418
|
console.log(divider(40));
|
|
370
419
|
console.log();
|
|
371
420
|
const display = [
|
|
421
|
+
['Capture Mode', String(settings.captureMode ?? 'feedback')],
|
|
372
422
|
['Button Color', String(settings.buttonColor ?? '')],
|
|
373
423
|
['Button Label', String(settings.buttonLabel ?? '')],
|
|
374
424
|
['Button Radius', String(settings.buttonRadius ?? '')],
|
|
@@ -389,6 +439,8 @@ function renderWidgetSettings(projectName, settings) {
|
|
|
389
439
|
['Success Message', String(settings.successMessage ?? '')],
|
|
390
440
|
['Z-Index', String(settings.zIndex ?? '')],
|
|
391
441
|
['Show Branding', String(settings.showBranding ?? true)],
|
|
442
|
+
['Share Console Errors', String(settings.allowConsoleErrors ?? false)],
|
|
443
|
+
['Error Tracking', String(settings.errorTrackingEnabled ?? false)],
|
|
392
444
|
];
|
|
393
445
|
for (const [label, value] of display) {
|
|
394
446
|
console.log(` ${brand.label(label.padEnd(18))} ${value}`);
|
package/dist/src/help.js
CHANGED
|
@@ -17,7 +17,7 @@ export function renderRootHelp() {
|
|
|
17
17
|
lines.push(` ${logo()} CLI ${brand.muted(`v${VERSION}`)}`);
|
|
18
18
|
lines.push('');
|
|
19
19
|
lines.push(` ${brand.muted('The command-line interface for FeedbackBasket.')}`);
|
|
20
|
-
lines.push(` ${brand.muted('Manage projects, feedback, widgets, and team from your terminal.')}`);
|
|
20
|
+
lines.push(` ${brand.muted('Manage projects, feedback, mobile apps, waitlists, widgets, and team from your terminal.')}`);
|
|
21
21
|
lines.push('');
|
|
22
22
|
// Core Commands
|
|
23
23
|
lines.push(section(' CORE COMMANDS'));
|
|
@@ -25,6 +25,8 @@ export function renderRootHelp() {
|
|
|
25
25
|
lines.push(cmd('feedback', 'View and manage feedback'));
|
|
26
26
|
lines.push(cmd('bugs', 'View bug reports with severity'));
|
|
27
27
|
lines.push(cmd('widget', 'Manage feedback widget & get embed code'));
|
|
28
|
+
lines.push(cmd('waitlist', 'View and export waitlist signups'));
|
|
29
|
+
lines.push(cmd('mobile', 'Set up and verify mobile app feedback'));
|
|
28
30
|
lines.push(cmd('team', 'Manage organization members'));
|
|
29
31
|
lines.push('');
|
|
30
32
|
// Shortcuts
|
|
@@ -36,6 +38,7 @@ export function renderRootHelp() {
|
|
|
36
38
|
lines.push(section(' SEARCH & EXPORT'));
|
|
37
39
|
lines.push(cmd('feedback search', 'Search feedback across projects'));
|
|
38
40
|
lines.push(cmd('feedback export', 'Export feedback to CSV, Markdown, or JSON'));
|
|
41
|
+
lines.push(cmd('waitlist export', 'Export waitlist signups to CSV'));
|
|
39
42
|
lines.push(cmd('bugs stats', 'Bug statistics summary'));
|
|
40
43
|
lines.push('');
|
|
41
44
|
// Auth & Config
|
|
@@ -62,6 +65,10 @@ export function renderRootHelp() {
|
|
|
62
65
|
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket bugs list --severity high`);
|
|
63
66
|
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket widget script myapp`);
|
|
64
67
|
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket widget settings myapp --display modal`);
|
|
68
|
+
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket widget settings myapp --capture-mode waitlist`);
|
|
69
|
+
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket waitlist list myapp --search "@example.com"`);
|
|
70
|
+
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket mobile setup myapp --bundle-id com.example.app`);
|
|
71
|
+
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket mobile verify myapp --bundle-id com.example.app --wait 120`);
|
|
65
72
|
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket projects create "My App" --url https://myapp.com`);
|
|
66
73
|
lines.push('');
|
|
67
74
|
// Learn More
|
package/dist/src/types.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export type FeedbackStatus = 'OPEN' | 'UNDER_REVIEW' | 'PLANNED' | 'IN_PROGRESS'
|
|
|
2
2
|
export type FeedbackCategory = 'BUG' | 'FEATURE_REQUEST' | 'IMPROVEMENT' | 'QUESTION';
|
|
3
3
|
export type Sentiment = 'POSITIVE' | 'NEGATIVE' | 'NEUTRAL';
|
|
4
4
|
export type Severity = 'high' | 'medium' | 'low';
|
|
5
|
-
export type ReplyDelivery = 'email' | 'widget' | 'both';
|
|
5
|
+
export type ReplyDelivery = 'email' | 'widget' | 'in-app' | 'both';
|
|
6
6
|
export type FeedbackFlowQuestionType = 'text' | 'textarea' | 'single_choice';
|
|
7
7
|
export interface FeedbackFlowQuestion {
|
|
8
8
|
id: string;
|
|
@@ -24,6 +24,7 @@ export interface FeedbackFlowSettings {
|
|
|
24
24
|
types: FeedbackFlowType[];
|
|
25
25
|
}
|
|
26
26
|
export interface WidgetSettings {
|
|
27
|
+
captureMode?: 'feedback' | 'waitlist';
|
|
27
28
|
widgetType?: string;
|
|
28
29
|
triggerMode?: 'floating' | 'inline';
|
|
29
30
|
buttonColor?: string;
|
|
@@ -44,6 +45,8 @@ export interface WidgetSettings {
|
|
|
44
45
|
displayMode?: 'modal' | 'popup';
|
|
45
46
|
zIndex?: number;
|
|
46
47
|
showBranding?: boolean;
|
|
48
|
+
allowConsoleErrors?: boolean;
|
|
49
|
+
errorTrackingEnabled?: boolean;
|
|
47
50
|
feedbackFlow?: FeedbackFlowSettings;
|
|
48
51
|
}
|
|
49
52
|
export interface Project {
|
|
@@ -57,6 +60,32 @@ export interface Project {
|
|
|
57
60
|
byStatus: Record<string, number>;
|
|
58
61
|
byCategory: Record<string, number>;
|
|
59
62
|
}
|
|
63
|
+
export interface MobileIntegrationResponse {
|
|
64
|
+
project: {
|
|
65
|
+
id: string;
|
|
66
|
+
name: string;
|
|
67
|
+
url: string;
|
|
68
|
+
};
|
|
69
|
+
integration: {
|
|
70
|
+
enabled: boolean;
|
|
71
|
+
publishableKey: string;
|
|
72
|
+
publishableKeyIncluded: boolean;
|
|
73
|
+
hostedFormUrl: string | null;
|
|
74
|
+
bundleIds: string[];
|
|
75
|
+
connection: {
|
|
76
|
+
connected: boolean;
|
|
77
|
+
lastSeenAt: string | null;
|
|
78
|
+
platform: string | null;
|
|
79
|
+
sdkVersion: string | null;
|
|
80
|
+
bundleId: string | null;
|
|
81
|
+
};
|
|
82
|
+
} | null;
|
|
83
|
+
sdk: {
|
|
84
|
+
swiftPackageUrl: string;
|
|
85
|
+
minimumIOSVersion: string;
|
|
86
|
+
productionBaseUrl: string;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
60
89
|
export interface Feedback {
|
|
61
90
|
id: string;
|
|
62
91
|
content: string;
|
|
@@ -86,6 +115,7 @@ export interface Feedback {
|
|
|
86
115
|
device?: string | null;
|
|
87
116
|
language?: string | null;
|
|
88
117
|
hasWidgetAccess?: boolean;
|
|
118
|
+
replyChannel?: 'widget' | 'in_app' | null;
|
|
89
119
|
attachments?: FeedbackAttachment[];
|
|
90
120
|
project: {
|
|
91
121
|
id: string;
|
|
@@ -124,6 +154,25 @@ export interface Pagination {
|
|
|
124
154
|
offset: number;
|
|
125
155
|
hasMore: boolean;
|
|
126
156
|
}
|
|
157
|
+
export interface WaitlistEntry {
|
|
158
|
+
id: string;
|
|
159
|
+
email: string;
|
|
160
|
+
name?: string | null;
|
|
161
|
+
pageUrl?: string | null;
|
|
162
|
+
referrerUrl?: string | null;
|
|
163
|
+
createdAt: string;
|
|
164
|
+
updatedAt: string;
|
|
165
|
+
}
|
|
166
|
+
export interface WaitlistResponse {
|
|
167
|
+
project: {
|
|
168
|
+
id: string;
|
|
169
|
+
name: string;
|
|
170
|
+
captureMode: 'feedback' | 'waitlist';
|
|
171
|
+
};
|
|
172
|
+
entries: WaitlistEntry[];
|
|
173
|
+
totalSignups: number;
|
|
174
|
+
pagination: Pagination;
|
|
175
|
+
}
|
|
127
176
|
export interface FeedbackResponse {
|
|
128
177
|
feedback: Feedback[];
|
|
129
178
|
pagination: Pagination;
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
2
|
-
export declare const USER_AGENT = "FeedbackBasket-CLI/0.
|
|
1
|
+
export declare const VERSION = "0.11.0";
|
|
2
|
+
export declare const USER_AGENT = "FeedbackBasket-CLI/0.11.0";
|
package/dist/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.11.0';
|
|
2
2
|
export const USER_AGENT = `FeedbackBasket-CLI/${VERSION}`;
|