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
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ feedbackbasket feedback list --category BUG --agent
|
|
|
39
39
|
feedbackbasket feedback create "Login button is broken" --content "Clicking Log in does nothing in Safari." --project myapp --type bug --agent
|
|
40
40
|
feedbackbasket feedback update <id> --status PLANNED --agent
|
|
41
41
|
feedbackbasket widget script myproject --agent
|
|
42
|
+
feedbackbasket mobile setup myproject --bundle-id com.example.app --include-publishable-key --agent
|
|
42
43
|
```
|
|
43
44
|
|
|
44
45
|
When installing or configuring a widget for the current app, agents should not rely on the CLI default project. First run `feedbackbasket projects list --agent`, match the current app by its real website URL or clearly matching project name, and only create a new project after confirming no existing project belongs to this app. If the only known URL is `localhost`, ask for the production, staging, preview, or intended public URL before creating the project.
|
|
@@ -100,6 +101,7 @@ feedbackbasket feedback update <id> --status PLANNED # Update status
|
|
|
100
101
|
feedbackbasket feedback update <id> --category BUG # Update category
|
|
101
102
|
feedbackbasket feedback reply <id> "Thanks!" --delivery email --reply-to support@example.com
|
|
102
103
|
feedbackbasket feedback reply <id> "Thanks!" --delivery widget
|
|
104
|
+
feedbackbasket feedback reply <id> "Thanks!" --delivery in-app
|
|
103
105
|
feedbackbasket feedback reply <id> "Thanks!" --delivery both --reply-to support@example.com
|
|
104
106
|
feedbackbasket feedback replies <id> # List sent replies
|
|
105
107
|
feedbackbasket feedback note <id> "Investigating this..." # Add internal note
|
|
@@ -129,12 +131,15 @@ feedbackbasket bugs stats --project myapp # Per-project stats
|
|
|
129
131
|
feedbackbasket widget settings myapp
|
|
130
132
|
|
|
131
133
|
# Update widget configuration
|
|
134
|
+
feedbackbasket widget settings myapp --capture-mode waitlist
|
|
135
|
+
feedbackbasket widget settings myapp --capture-mode feedback
|
|
132
136
|
feedbackbasket widget settings myapp --color "#22c55e" --label "Send Feedback"
|
|
133
137
|
feedbackbasket widget settings myapp --position bottom-left --display modal
|
|
134
138
|
feedbackbasket widget settings myapp --email-required --intro "How can we improve?"
|
|
135
139
|
feedbackbasket widget settings myapp --button-radius 10 --button-size regular
|
|
136
140
|
feedbackbasket widget settings myapp --show-email --allow-attachments
|
|
137
141
|
feedbackbasket widget settings myapp --email-read-only --hide-email-when-prefilled
|
|
142
|
+
feedbackbasket widget settings myapp --error-tracking --allow-console-errors
|
|
138
143
|
|
|
139
144
|
# Configure guided feedback types and follow-up questions
|
|
140
145
|
feedbackbasket widget flow myapp
|
|
@@ -146,6 +151,19 @@ feedbackbasket widget flow myapp --config ./feedback-flow.json
|
|
|
146
151
|
feedbackbasket widget script myapp
|
|
147
152
|
```
|
|
148
153
|
|
|
154
|
+
Waitlist mode uses the same project script. Add `data-feedbackbasket-waitlist` to your own form, with a required `email` field and optional `name` field. The CLI's `widget script` output shows a starter form when waitlist mode is active.
|
|
155
|
+
|
|
156
|
+
### Waitlist
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
feedbackbasket waitlist list myapp
|
|
160
|
+
feedbackbasket waitlist list myapp --search "@example.com" --limit 50 --offset 0
|
|
161
|
+
feedbackbasket waitlist list myapp --agent
|
|
162
|
+
feedbackbasket waitlist export myapp
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Waitlist listing returns emails, optional names, source pages, total counts, the active capture mode, and pagination. Export prints the dashboard-compatible CSV to stdout.
|
|
166
|
+
|
|
149
167
|
For inline trigger mode, load the widget once and call the public API from your own button:
|
|
150
168
|
|
|
151
169
|
```html
|
|
@@ -156,6 +174,8 @@ For inline trigger mode, load the widget once and call the public API from your
|
|
|
156
174
|
|
|
157
175
|
Passing the trigger element lets popup mode open beside your custom button. Calling `window.FeedbackWidget.openFeedbackForm()` with no arguments still uses the configured widget position.
|
|
158
176
|
|
|
177
|
+
Use only the public `openFeedbackForm()` API from the snippet. Do not call internal or undocumented methods such as `open()` or `openModal()`.
|
|
178
|
+
|
|
159
179
|
Use `--email-read-only` and `--hide-email-when-prefilled` with runtime `userEmail` values from your app. These settings do not store visitor emails in FeedbackBasket widget settings.
|
|
160
180
|
|
|
161
181
|
The default widget experience is a basic modal. Only switch to popup mode or enable guided feedback when you intentionally want that flow.
|
|
@@ -184,6 +204,34 @@ The default widget experience is a basic modal. Only switch to popup mode or ena
|
|
|
184
204
|
}
|
|
185
205
|
```
|
|
186
206
|
|
|
207
|
+
### Mobile Apps
|
|
208
|
+
|
|
209
|
+
Mobile setup is additive and does not change the website widget. The `fb_mobile_` value is a publishable, write-only project identifier designed to ship in an app; it is not a CLI token or private API key. Mobile commands mask it unless `--include-publishable-key` is explicitly supplied.
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
# Enable mobile feedback and add allowed iOS bundle IDs
|
|
213
|
+
feedbackbasket mobile setup myapp --bundle-id com.example.app
|
|
214
|
+
|
|
215
|
+
# Return the publishable key and hosted form URL for an authorized app setup
|
|
216
|
+
feedbackbasket mobile setup myapp --bundle-id com.example.app --include-publishable-key --agent
|
|
217
|
+
|
|
218
|
+
# Inspect and verify the SDK heartbeat
|
|
219
|
+
feedbackbasket mobile status myapp
|
|
220
|
+
feedbackbasket mobile verify myapp --bundle-id com.example.app --wait 120
|
|
221
|
+
|
|
222
|
+
# Add or remove bundle IDs without replacing the others
|
|
223
|
+
feedbackbasket mobile bundle-ids myapp --add com.example.app.beta
|
|
224
|
+
feedbackbasket mobile bundle-ids myapp --remove com.example.app.beta
|
|
225
|
+
|
|
226
|
+
# Actions that can interrupt installed apps require explicit confirmation
|
|
227
|
+
feedbackbasket mobile disable myapp --yes
|
|
228
|
+
feedbackbasket mobile rotate-key myapp --yes --include-publishable-key
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Agents should never repeat the full publishable key in their final response. They must never place `fb_cli_` or `fb_key_` credentials in a mobile app. Key rotation invalidates the previous key and therefore requires explicit user authorization.
|
|
232
|
+
|
|
233
|
+
The native Swift SDK securely stores reply-thread credentials in the app Keychain and shows team replies in the same feedback sheet. Host apps do not need to build an inbox or manage reply tokens. Hosted-form integrations remain email-only.
|
|
234
|
+
|
|
187
235
|
### Team
|
|
188
236
|
|
|
189
237
|
```bash
|
package/dist/src/cli.js
CHANGED
|
@@ -11,6 +11,8 @@ import { createDoctorCommand } from './commands/doctor.js';
|
|
|
11
11
|
import { createSetupCommand } from './commands/setup.js';
|
|
12
12
|
import { createWidgetCommand } from './commands/widget.js';
|
|
13
13
|
import { createTeamCommand } from './commands/team.js';
|
|
14
|
+
import { createWaitlistCommand } from './commands/waitlist.js';
|
|
15
|
+
import { createMobileCommand } from './commands/mobile.js';
|
|
14
16
|
import { renderRootHelp } from './help.js';
|
|
15
17
|
let writer;
|
|
16
18
|
function resolveFormat(opts) {
|
|
@@ -62,6 +64,8 @@ export function run() {
|
|
|
62
64
|
program.addCommand(createFeedbackCommand(getWriter));
|
|
63
65
|
program.addCommand(createBugsCommand(getWriter));
|
|
64
66
|
program.addCommand(createWidgetCommand(getWriter));
|
|
67
|
+
program.addCommand(createWaitlistCommand(getWriter));
|
|
68
|
+
program.addCommand(createMobileCommand(getWriter));
|
|
65
69
|
program.addCommand(createTeamCommand(getWriter));
|
|
66
70
|
program.addCommand(createDoctorCommand(getWriter));
|
|
67
71
|
program.addCommand(createSetupCommand(getWriter));
|
package/dist/src/client.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { ProjectsResponse, FeedbackResponse, BugReportsResponse, FeedbackParams, FeedbackCreateInput, FeedbackCreateResponse, FeedbackReplyResponse, BugReportParams, UserProfile, Project, Feedback, WidgetSettings } from './types.js';
|
|
1
|
+
import type { ProjectsResponse, FeedbackResponse, BugReportsResponse, FeedbackParams, FeedbackCreateInput, FeedbackCreateResponse, FeedbackReplyResponse, BugReportParams, UserProfile, Project, Feedback, WidgetSettings, WaitlistResponse, MobileIntegrationResponse } from './types.js';
|
|
2
2
|
export declare class FeedbackBasketClient {
|
|
3
|
-
private
|
|
3
|
+
private readonly apiBaseUrl;
|
|
4
|
+
private readonly token;
|
|
4
5
|
constructor(token: string, baseUrl: string);
|
|
5
6
|
me(): Promise<UserProfile>;
|
|
6
7
|
listProjects(): Promise<ProjectsResponse>;
|
|
@@ -48,9 +49,23 @@ export declare class FeedbackBasketClient {
|
|
|
48
49
|
getWidgetScript(projectId: string): Promise<{
|
|
49
50
|
projectId: string;
|
|
50
51
|
projectName: string;
|
|
52
|
+
captureMode: 'feedback' | 'waitlist';
|
|
51
53
|
embedCode: string;
|
|
52
54
|
scriptUrl: string;
|
|
53
55
|
}>;
|
|
56
|
+
getMobileIntegration(projectId: string, includePublishableKey?: boolean): Promise<MobileIntegrationResponse>;
|
|
57
|
+
updateMobileIntegration(projectId: string, data: {
|
|
58
|
+
enabled?: boolean;
|
|
59
|
+
addBundleIds?: string[];
|
|
60
|
+
removeBundleIds?: string[];
|
|
61
|
+
}, includePublishableKey?: boolean): Promise<MobileIntegrationResponse>;
|
|
62
|
+
rotateMobileProjectKey(projectId: string, includePublishableKey?: boolean): Promise<MobileIntegrationResponse>;
|
|
63
|
+
getWaitlist(projectId: string, params?: {
|
|
64
|
+
search?: string;
|
|
65
|
+
limit?: number;
|
|
66
|
+
offset?: number;
|
|
67
|
+
}): Promise<WaitlistResponse>;
|
|
68
|
+
exportWaitlist(projectId: string): Promise<string>;
|
|
54
69
|
updateFeedback(id: string, data: {
|
|
55
70
|
status?: string;
|
|
56
71
|
category?: string;
|
|
@@ -118,5 +133,4 @@ export declare class FeedbackBasketClient {
|
|
|
118
133
|
email: string;
|
|
119
134
|
}>;
|
|
120
135
|
private request;
|
|
121
|
-
private handleError;
|
|
122
136
|
}
|
package/dist/src/client.js
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
import axios, { AxiosError } from 'axios';
|
|
2
1
|
import { USER_AGENT } from './version.js';
|
|
3
|
-
import { errAuth, errForbidden, errRateLimit, errNetwork, errAPI } from './output/errors.js';
|
|
2
|
+
import { CLIError, errAuth, errForbidden, errRateLimit, errNetwork, errAPI } from './output/errors.js';
|
|
4
3
|
export class FeedbackBasketClient {
|
|
5
|
-
|
|
4
|
+
apiBaseUrl;
|
|
5
|
+
token;
|
|
6
6
|
constructor(token, baseUrl) {
|
|
7
|
-
this.
|
|
8
|
-
|
|
9
|
-
timeout: 30_000,
|
|
10
|
-
headers: {
|
|
11
|
-
'Authorization': `Bearer ${token}`,
|
|
12
|
-
'Content-Type': 'application/json',
|
|
13
|
-
'User-Agent': USER_AGENT,
|
|
14
|
-
},
|
|
15
|
-
});
|
|
7
|
+
this.apiBaseUrl = `${baseUrl.replace(/\/$/, '')}/api/v1`;
|
|
8
|
+
this.token = token;
|
|
16
9
|
}
|
|
17
10
|
async me() {
|
|
18
11
|
return this.request('GET', '/auth/me');
|
|
@@ -63,6 +56,27 @@ export class FeedbackBasketClient {
|
|
|
63
56
|
async getWidgetScript(projectId) {
|
|
64
57
|
return this.request('GET', `/projects/${encodeURIComponent(projectId)}/widget-script`);
|
|
65
58
|
}
|
|
59
|
+
// Mobile feedback
|
|
60
|
+
async getMobileIntegration(projectId, includePublishableKey = false) {
|
|
61
|
+
const query = includePublishableKey ? '?includePublishableKey=true' : '';
|
|
62
|
+
return this.request('GET', `/projects/${encodeURIComponent(projectId)}/mobile${query}`);
|
|
63
|
+
}
|
|
64
|
+
async updateMobileIntegration(projectId, data, includePublishableKey = false) {
|
|
65
|
+
const query = includePublishableKey ? '?includePublishableKey=true' : '';
|
|
66
|
+
return this.request('PATCH', `/projects/${encodeURIComponent(projectId)}/mobile${query}`, data);
|
|
67
|
+
}
|
|
68
|
+
async rotateMobileProjectKey(projectId, includePublishableKey = false) {
|
|
69
|
+
const query = includePublishableKey ? '?includePublishableKey=true' : '';
|
|
70
|
+
return this.request('POST', `/projects/${encodeURIComponent(projectId)}/mobile/rotate-key${query}`);
|
|
71
|
+
}
|
|
72
|
+
async getWaitlist(projectId, params = {}) {
|
|
73
|
+
const query = buildQuery(params);
|
|
74
|
+
return this.request('GET', `/projects/${encodeURIComponent(projectId)}/waitlist${query}`);
|
|
75
|
+
}
|
|
76
|
+
async exportWaitlist(projectId) {
|
|
77
|
+
const data = await this.request('GET', `/projects/${encodeURIComponent(projectId)}/waitlist/export`);
|
|
78
|
+
return typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
|
79
|
+
}
|
|
66
80
|
// Write operations
|
|
67
81
|
async updateFeedback(id, data) {
|
|
68
82
|
return this.request('PATCH', `/feedback/${encodeURIComponent(id)}`, data);
|
|
@@ -95,8 +109,8 @@ export class FeedbackBasketClient {
|
|
|
95
109
|
return this.request('DELETE', `/feedback/${encodeURIComponent(feedbackId)}/notes/${encodeURIComponent(noteId)}`);
|
|
96
110
|
}
|
|
97
111
|
async exportFeedback(projectId, format = 'csv') {
|
|
98
|
-
const
|
|
99
|
-
return typeof
|
|
112
|
+
const data = await this.request('GET', `/projects/${encodeURIComponent(projectId)}/export?format=${format}`);
|
|
113
|
+
return typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
|
100
114
|
}
|
|
101
115
|
// Team
|
|
102
116
|
async listTeam() {
|
|
@@ -109,34 +123,58 @@ export class FeedbackBasketClient {
|
|
|
109
123
|
return this.request('DELETE', `/team/${encodeURIComponent(memberId)}`);
|
|
110
124
|
}
|
|
111
125
|
async request(method, path, data) {
|
|
126
|
+
const controller = new AbortController();
|
|
127
|
+
const timeout = setTimeout(() => controller.abort(), 30_000);
|
|
112
128
|
try {
|
|
113
|
-
const response = await this.
|
|
114
|
-
|
|
129
|
+
const response = await fetch(`${this.apiBaseUrl}${path}`, {
|
|
130
|
+
method,
|
|
131
|
+
signal: controller.signal,
|
|
132
|
+
headers: {
|
|
133
|
+
'Authorization': `Bearer ${this.token}`,
|
|
134
|
+
'Content-Type': 'application/json',
|
|
135
|
+
'User-Agent': USER_AGENT,
|
|
136
|
+
},
|
|
137
|
+
body: data === undefined ? undefined : JSON.stringify(data),
|
|
138
|
+
});
|
|
139
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
140
|
+
const payload = contentType.includes('application/json')
|
|
141
|
+
? await response.json().catch(() => null)
|
|
142
|
+
: await response.text();
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
const message = getErrorMessage(payload, response.statusText);
|
|
145
|
+
switch (response.status) {
|
|
146
|
+
case 401: throw errAuth(message);
|
|
147
|
+
case 403: throw errForbidden(message);
|
|
148
|
+
case 404: throw errAPI(404, message);
|
|
149
|
+
case 429: throw errRateLimit();
|
|
150
|
+
default: throw errAPI(response.status, message);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return payload;
|
|
115
154
|
}
|
|
116
155
|
catch (error) {
|
|
117
|
-
|
|
156
|
+
if (error instanceof CLIError)
|
|
157
|
+
throw error;
|
|
158
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
159
|
+
throw errNetwork(cause);
|
|
118
160
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (error instanceof AxiosError) {
|
|
122
|
-
const status = error.response?.status;
|
|
123
|
-
const message = error.response?.data?.error
|
|
124
|
-
?? error.response?.data?.message
|
|
125
|
-
?? error.message;
|
|
126
|
-
if (!error.response) {
|
|
127
|
-
return errNetwork(error);
|
|
128
|
-
}
|
|
129
|
-
switch (status) {
|
|
130
|
-
case 401: return errAuth(message);
|
|
131
|
-
case 403: return errForbidden(message);
|
|
132
|
-
case 404: return errAPI(404, message);
|
|
133
|
-
case 429: return errRateLimit();
|
|
134
|
-
default: return errAPI(status ?? 500, message);
|
|
135
|
-
}
|
|
161
|
+
finally {
|
|
162
|
+
clearTimeout(timeout);
|
|
136
163
|
}
|
|
137
|
-
return error instanceof Error ? error : new Error(String(error));
|
|
138
164
|
}
|
|
139
165
|
}
|
|
166
|
+
function getErrorMessage(payload, fallback) {
|
|
167
|
+
if (payload && typeof payload === 'object') {
|
|
168
|
+
const value = payload;
|
|
169
|
+
if (typeof value.error === 'string')
|
|
170
|
+
return value.error;
|
|
171
|
+
if (typeof value.message === 'string')
|
|
172
|
+
return value.message;
|
|
173
|
+
}
|
|
174
|
+
if (typeof payload === 'string' && payload.trim())
|
|
175
|
+
return payload;
|
|
176
|
+
return fallback || 'Request failed';
|
|
177
|
+
}
|
|
140
178
|
function buildQuery(params) {
|
|
141
179
|
const parts = [];
|
|
142
180
|
for (const [key, value] of Object.entries(params)) {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import type { OutputWriter } from '../output/writer.js';
|
|
3
|
+
import type { ReplyDelivery } from '../types.js';
|
|
3
4
|
export declare function createFeedbackReplyCommand(getWriter: () => OutputWriter): Command;
|
|
4
5
|
export declare function createFeedbackRepliesCommand(getWriter: () => OutputWriter): Command;
|
|
6
|
+
export declare function replyDestinations(delivery: ReplyDelivery): Array<'email' | 'widget'>;
|
|
@@ -5,14 +5,14 @@ import { loadConfig } from '../config/config.js';
|
|
|
5
5
|
import { errAuth, errUsage } from '../output/errors.js';
|
|
6
6
|
import { brand } from '../output/theme.js';
|
|
7
7
|
import { ask } from '../prompt.js';
|
|
8
|
-
const deliveryOptions = new Set(['email', 'widget', 'both']);
|
|
8
|
+
const deliveryOptions = new Set(['email', 'widget', 'in-app', 'both']);
|
|
9
9
|
export function createFeedbackReplyCommand(getWriter) {
|
|
10
10
|
return new Command('reply')
|
|
11
11
|
.argument('<id>', 'Feedback ID to reply to')
|
|
12
12
|
.argument('[content]', 'Reply content (or use --content)')
|
|
13
|
-
.description('Reply to feedback by email, widget thread, or both')
|
|
13
|
+
.description('Reply to feedback by email, widget/in-app thread, or both')
|
|
14
14
|
.option('--content <text>', 'Reply content (alternative to positional argument)')
|
|
15
|
-
.option('--delivery <delivery>', 'Reply delivery (email, widget, both)', 'email')
|
|
15
|
+
.option('--delivery <delivery>', 'Reply delivery (email, widget, in-app, both)', 'email')
|
|
16
16
|
.option('--reply-to <email>', 'Reply-to email for email delivery')
|
|
17
17
|
.action(async (id, contentArg, opts) => {
|
|
18
18
|
const writer = getWriter();
|
|
@@ -22,19 +22,22 @@ export function createFeedbackReplyCommand(getWriter) {
|
|
|
22
22
|
throw errUsage('Reply content is required', 'Example: feedbackbasket feedback reply <id> "Thanks for reporting this!" --delivery widget');
|
|
23
23
|
}
|
|
24
24
|
if (!deliveryOptions.has(delivery)) {
|
|
25
|
-
throw errUsage('Delivery must be email, widget, or both', 'Example: feedbackbasket feedback reply <id> "Thanks!" --delivery both --reply-to support@example.com');
|
|
25
|
+
throw errUsage('Delivery must be email, widget, in-app, or both', 'Example: feedbackbasket feedback reply <id> "Thanks!" --delivery both --reply-to support@example.com');
|
|
26
26
|
}
|
|
27
27
|
const client = requireClient();
|
|
28
28
|
const feedback = await client.getFeedbackById(id);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
if (delivery === 'in-app' && feedback.replyChannel !== 'in_app') {
|
|
30
|
+
throw errUsage('This feedback does not have an in-app reply thread.', 'Use --delivery widget for website-widget feedback or --delivery email when an email address is available.');
|
|
31
|
+
}
|
|
32
|
+
const destinations = replyDestinations(delivery);
|
|
32
33
|
const sendsEmail = destinations.includes('email');
|
|
33
34
|
const sendsWidget = destinations.includes('widget');
|
|
34
35
|
let replyTo = opts.replyTo ?? feedback.project.replyToEmail ?? undefined;
|
|
35
36
|
if (sendsEmail) {
|
|
36
37
|
if (!feedback.email) {
|
|
37
|
-
throw errUsage(
|
|
38
|
+
throw errUsage(feedback.replyChannel === 'in_app'
|
|
39
|
+
? 'This feedback has no email address; use --delivery in-app.'
|
|
40
|
+
: 'This feedback has no email address; use --delivery widget if it has a widget thread.');
|
|
38
41
|
}
|
|
39
42
|
if (!replyTo) {
|
|
40
43
|
const isInteractive = !writer.isMachineOutput() && process.stdin.isTTY;
|
|
@@ -56,7 +59,7 @@ export function createFeedbackReplyCommand(getWriter) {
|
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
if (sendsWidget && !feedback.hasWidgetAccess) {
|
|
59
|
-
throw errUsage('This feedback
|
|
62
|
+
throw errUsage('This feedback has no in-app or widget reply thread.', 'Use --delivery email for feedback with an email address, or ask the human how they want to respond.');
|
|
60
63
|
}
|
|
61
64
|
const result = await client.sendReply(id, content, {
|
|
62
65
|
replyToEmail: replyTo,
|
|
@@ -71,16 +74,16 @@ export function createFeedbackReplyCommand(getWriter) {
|
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
if (result.message) {
|
|
74
|
-
console.log(` ${brand.success('[OK]')} Widget reply posted`);
|
|
77
|
+
console.log(` ${brand.success('[OK]')} ${feedback.replyChannel === 'in_app' ? 'In-app' : 'Widget'} reply posted`);
|
|
75
78
|
console.log(` ${brand.muted('By:')} ${result.message.sentByName ?? 'CLI'}`);
|
|
76
79
|
}
|
|
77
80
|
console.log();
|
|
78
81
|
}
|
|
79
82
|
writer.ok(result, {
|
|
80
83
|
summary: delivery === 'both'
|
|
81
|
-
?
|
|
82
|
-
:
|
|
83
|
-
? 'Widget reply posted
|
|
84
|
+
? `Reply sent by email and ${feedback.replyChannel === 'in_app' ? 'in-app' : 'widget'}`
|
|
85
|
+
: sendsWidget
|
|
86
|
+
? `${feedback.replyChannel === 'in_app' ? 'In-app' : 'Widget'} reply posted`
|
|
84
87
|
: `Reply sent to ${result.sentTo}`,
|
|
85
88
|
breadcrumbs: [
|
|
86
89
|
{ action: 'View replies', cmd: `feedbackbasket feedback replies ${id}` },
|
|
@@ -139,6 +142,13 @@ export function createFeedbackRepliesCommand(getWriter) {
|
|
|
139
142
|
});
|
|
140
143
|
});
|
|
141
144
|
}
|
|
145
|
+
export function replyDestinations(delivery) {
|
|
146
|
+
if (delivery === 'both')
|
|
147
|
+
return ['email', 'widget'];
|
|
148
|
+
if (delivery === 'in-app')
|
|
149
|
+
return ['widget'];
|
|
150
|
+
return [delivery];
|
|
151
|
+
}
|
|
142
152
|
function requireClient() {
|
|
143
153
|
const manager = new AuthManager();
|
|
144
154
|
const token = manager.resolveToken();
|
|
@@ -186,7 +186,7 @@ function renderFeedbackDetail(item) {
|
|
|
186
186
|
['OS', item.os],
|
|
187
187
|
['Device', item.device],
|
|
188
188
|
['Language', item.language],
|
|
189
|
-
['
|
|
189
|
+
['Reply Channel', item.replyChannel === 'in_app' ? 'In-app' : item.hasWidgetAccess ? 'Widget' : null],
|
|
190
190
|
['Created', item.createdAt],
|
|
191
191
|
];
|
|
192
192
|
for (const [label, value] of fields) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import type { MobileIntegrationResponse } from '../types.js';
|
|
3
|
+
import type { OutputWriter } from '../output/writer.js';
|
|
4
|
+
export declare function createMobileCommand(getWriter: () => OutputWriter): Command;
|
|
5
|
+
export declare function projectRef(projectArg: string | undefined, projectId: string): string;
|
|
6
|
+
export declare function validateBundleIds(bundleIds: string[]): string[];
|
|
7
|
+
export declare function isMobileConnectionVerified(result: MobileIntegrationResponse, expectedBundleId?: string): boolean;
|