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.
@@ -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,3 @@
1
+ import { Command } from 'commander';
2
+ import { type OutputWriter } from '../output/writer.js';
3
+ export declare function createWaitlistCommand(getWriter: () => OutputWriter): Command;
@@ -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
- renderInlineTriggerHelp(settingsResult.settings);
271
+ if (settingsResult.settings.captureMode === 'waitlist') {
272
+ renderWaitlistHelp();
273
+ }
274
+ else {
275
+ renderInlineTriggerHelp(settingsResult.settings);
276
+ }
257
277
  }
258
- writer.ok(result, {
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
@@ -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;
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.9.3";
2
- export declare const USER_AGENT = "FeedbackBasket-CLI/0.9.3";
1
+ export declare const VERSION = "0.11.0";
2
+ export declare const USER_AGENT = "FeedbackBasket-CLI/0.11.0";
@@ -1,2 +1,2 @@
1
- export const VERSION = '0.9.3';
1
+ export const VERSION = '0.11.0';
2
2
  export const USER_AGENT = `FeedbackBasket-CLI/${VERSION}`;