userdispatch 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-JSBRDJBE.js +30 -0
- package/dist/index.js +26 -851
- package/dist/init-DQZBIX7E.js +1007 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__commonJS,
|
|
3
|
+
__toESM
|
|
4
|
+
} from "./chunk-JSBRDJBE.js";
|
|
5
|
+
|
|
6
|
+
// node_modules/picocolors/picocolors.js
|
|
7
|
+
var require_picocolors = __commonJS({
|
|
8
|
+
"node_modules/picocolors/picocolors.js"(exports, module) {
|
|
9
|
+
"use strict";
|
|
10
|
+
var p = process || {};
|
|
11
|
+
var argv = p.argv || [];
|
|
12
|
+
var env = p.env || {};
|
|
13
|
+
var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
|
|
14
|
+
var formatter = (open, close, replace = open) => (input) => {
|
|
15
|
+
let string = "" + input, index = string.indexOf(close, open.length);
|
|
16
|
+
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
17
|
+
};
|
|
18
|
+
var replaceClose = (string, close, replace, index) => {
|
|
19
|
+
let result = "", cursor = 0;
|
|
20
|
+
do {
|
|
21
|
+
result += string.substring(cursor, index) + replace;
|
|
22
|
+
cursor = index + close.length;
|
|
23
|
+
index = string.indexOf(close, cursor);
|
|
24
|
+
} while (~index);
|
|
25
|
+
return result + string.substring(cursor);
|
|
26
|
+
};
|
|
27
|
+
var createColors = (enabled = isColorSupported) => {
|
|
28
|
+
let f = enabled ? formatter : () => String;
|
|
29
|
+
return {
|
|
30
|
+
isColorSupported: enabled,
|
|
31
|
+
reset: f("\x1B[0m", "\x1B[0m"),
|
|
32
|
+
bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
33
|
+
dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
34
|
+
italic: f("\x1B[3m", "\x1B[23m"),
|
|
35
|
+
underline: f("\x1B[4m", "\x1B[24m"),
|
|
36
|
+
inverse: f("\x1B[7m", "\x1B[27m"),
|
|
37
|
+
hidden: f("\x1B[8m", "\x1B[28m"),
|
|
38
|
+
strikethrough: f("\x1B[9m", "\x1B[29m"),
|
|
39
|
+
black: f("\x1B[30m", "\x1B[39m"),
|
|
40
|
+
red: f("\x1B[31m", "\x1B[39m"),
|
|
41
|
+
green: f("\x1B[32m", "\x1B[39m"),
|
|
42
|
+
yellow: f("\x1B[33m", "\x1B[39m"),
|
|
43
|
+
blue: f("\x1B[34m", "\x1B[39m"),
|
|
44
|
+
magenta: f("\x1B[35m", "\x1B[39m"),
|
|
45
|
+
cyan: f("\x1B[36m", "\x1B[39m"),
|
|
46
|
+
white: f("\x1B[37m", "\x1B[39m"),
|
|
47
|
+
gray: f("\x1B[90m", "\x1B[39m"),
|
|
48
|
+
bgBlack: f("\x1B[40m", "\x1B[49m"),
|
|
49
|
+
bgRed: f("\x1B[41m", "\x1B[49m"),
|
|
50
|
+
bgGreen: f("\x1B[42m", "\x1B[49m"),
|
|
51
|
+
bgYellow: f("\x1B[43m", "\x1B[49m"),
|
|
52
|
+
bgBlue: f("\x1B[44m", "\x1B[49m"),
|
|
53
|
+
bgMagenta: f("\x1B[45m", "\x1B[49m"),
|
|
54
|
+
bgCyan: f("\x1B[46m", "\x1B[49m"),
|
|
55
|
+
bgWhite: f("\x1B[47m", "\x1B[49m"),
|
|
56
|
+
blackBright: f("\x1B[90m", "\x1B[39m"),
|
|
57
|
+
redBright: f("\x1B[91m", "\x1B[39m"),
|
|
58
|
+
greenBright: f("\x1B[92m", "\x1B[39m"),
|
|
59
|
+
yellowBright: f("\x1B[93m", "\x1B[39m"),
|
|
60
|
+
blueBright: f("\x1B[94m", "\x1B[39m"),
|
|
61
|
+
magentaBright: f("\x1B[95m", "\x1B[39m"),
|
|
62
|
+
cyanBright: f("\x1B[96m", "\x1B[39m"),
|
|
63
|
+
whiteBright: f("\x1B[97m", "\x1B[39m"),
|
|
64
|
+
bgBlackBright: f("\x1B[100m", "\x1B[49m"),
|
|
65
|
+
bgRedBright: f("\x1B[101m", "\x1B[49m"),
|
|
66
|
+
bgGreenBright: f("\x1B[102m", "\x1B[49m"),
|
|
67
|
+
bgYellowBright: f("\x1B[103m", "\x1B[49m"),
|
|
68
|
+
bgBlueBright: f("\x1B[104m", "\x1B[49m"),
|
|
69
|
+
bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
|
|
70
|
+
bgCyanBright: f("\x1B[106m", "\x1B[49m"),
|
|
71
|
+
bgWhiteBright: f("\x1B[107m", "\x1B[49m")
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
module.exports = createColors();
|
|
75
|
+
module.exports.createColors = createColors;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// src/commands/init.ts
|
|
80
|
+
var import_picocolors7 = __toESM(require_picocolors(), 1);
|
|
81
|
+
import { intro, log as log7 } from "@clack/prompts";
|
|
82
|
+
|
|
83
|
+
// src/steps/auth.ts
|
|
84
|
+
var import_picocolors = __toESM(require_picocolors(), 1);
|
|
85
|
+
import { text, spinner, log, isCancel } from "@clack/prompts";
|
|
86
|
+
import { exec } from "child_process";
|
|
87
|
+
import { platform } from "os";
|
|
88
|
+
|
|
89
|
+
// src/lib/config.ts
|
|
90
|
+
var PROD_URL = process.env.USERDISPATCH_API_URL || "https://userdispatch.com";
|
|
91
|
+
var API_BASE = `${PROD_URL}/api`;
|
|
92
|
+
|
|
93
|
+
// src/lib/api.ts
|
|
94
|
+
var ApiError = class extends Error {
|
|
95
|
+
constructor(status, body) {
|
|
96
|
+
super(body.error || `HTTP ${status}`);
|
|
97
|
+
this.status = status;
|
|
98
|
+
this.body = body;
|
|
99
|
+
this.name = "ApiError";
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
async function request(path, options) {
|
|
103
|
+
const url = `${API_BASE}${path}`;
|
|
104
|
+
const res = await fetch(url, options);
|
|
105
|
+
const body = await res.json().catch(() => ({ error: "Invalid JSON response" }));
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
throw new ApiError(res.status, body);
|
|
108
|
+
}
|
|
109
|
+
return body;
|
|
110
|
+
}
|
|
111
|
+
async function apiGet(path, token) {
|
|
112
|
+
return request(path, {
|
|
113
|
+
method: "GET",
|
|
114
|
+
headers: {
|
|
115
|
+
Authorization: `Bearer ${token}`,
|
|
116
|
+
Accept: "application/json"
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async function apiPost(path, body, token) {
|
|
121
|
+
return request(path, {
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers: {
|
|
124
|
+
Authorization: `Bearer ${token}`,
|
|
125
|
+
"Content-Type": "application/json",
|
|
126
|
+
Accept: "application/json"
|
|
127
|
+
},
|
|
128
|
+
body: JSON.stringify(body)
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
async function apiSubmission(path, body, apiKey) {
|
|
132
|
+
return request(path, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: {
|
|
135
|
+
"X-API-Key": apiKey,
|
|
136
|
+
"Content-Type": "application/json",
|
|
137
|
+
Accept: "application/json"
|
|
138
|
+
},
|
|
139
|
+
body: JSON.stringify(body)
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/steps/auth.ts
|
|
144
|
+
function openBrowser(url) {
|
|
145
|
+
const os = platform();
|
|
146
|
+
const cmd = os === "darwin" ? "open" : os === "win32" ? "start" : "xdg-open";
|
|
147
|
+
exec(`${cmd} "${url}"`, (err) => {
|
|
148
|
+
if (err) {
|
|
149
|
+
log.warn(`Could not open browser. Please visit:
|
|
150
|
+
${url}`);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async function validateToken(token) {
|
|
155
|
+
try {
|
|
156
|
+
const res = await apiGet("/cli/auth", token);
|
|
157
|
+
return { email: res.data.email || "", org: res.data.org };
|
|
158
|
+
} catch (err) {
|
|
159
|
+
if (err instanceof ApiError && err.status === 401) {
|
|
160
|
+
throw new Error("Invalid token. Please try again.");
|
|
161
|
+
}
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function authStep(flags) {
|
|
166
|
+
const s = spinner();
|
|
167
|
+
let token = flags.token;
|
|
168
|
+
if (!token) {
|
|
169
|
+
const authUrl = `${PROD_URL}/cli/auth`;
|
|
170
|
+
log.step(import_picocolors.default.bold("Step 1 of 6 \u2014 Authenticate"));
|
|
171
|
+
log.info("Sign in to link this project to your UserDispatch account. Opening your browser now...");
|
|
172
|
+
openBrowser(authUrl);
|
|
173
|
+
log.info("After signing in, copy the token and paste it below.");
|
|
174
|
+
const result = await text({
|
|
175
|
+
message: "Paste your token here (starts with ud_):",
|
|
176
|
+
placeholder: "ud_...",
|
|
177
|
+
validate(value) {
|
|
178
|
+
if (!value.trim()) return "Token is required.";
|
|
179
|
+
if (!value.trim().startsWith("ud_")) return 'Token should start with "ud_".';
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
if (isCancel(result)) {
|
|
183
|
+
log.warn("Setup cancelled.");
|
|
184
|
+
process.exit(0);
|
|
185
|
+
}
|
|
186
|
+
token = result.trim();
|
|
187
|
+
}
|
|
188
|
+
s.start("Validating token...");
|
|
189
|
+
try {
|
|
190
|
+
const { email, org } = await validateToken(token);
|
|
191
|
+
s.stop("Token validated.");
|
|
192
|
+
if (org) {
|
|
193
|
+
log.info(`Signed in as ${email}`);
|
|
194
|
+
log.info(`Organization: ${org.name} (${org.slug})`);
|
|
195
|
+
} else {
|
|
196
|
+
log.info(`Signed in as ${email}`);
|
|
197
|
+
log.info("No organization yet \u2014 we'll create one in the next step.");
|
|
198
|
+
}
|
|
199
|
+
return { token, email, org };
|
|
200
|
+
} catch (err) {
|
|
201
|
+
s.stop("Token validation failed.");
|
|
202
|
+
if (err instanceof Error) {
|
|
203
|
+
log.error(err.message);
|
|
204
|
+
}
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/steps/setup.ts
|
|
210
|
+
var import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
211
|
+
import { text as text2, select, spinner as spinner2, log as log2, isCancel as isCancel2 } from "@clack/prompts";
|
|
212
|
+
import { basename } from "path";
|
|
213
|
+
function slugify(str) {
|
|
214
|
+
return str.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
215
|
+
}
|
|
216
|
+
function toTitleCase(str) {
|
|
217
|
+
return str.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
218
|
+
}
|
|
219
|
+
async function promptForValue(message, defaultValue, placeholder) {
|
|
220
|
+
const result = await text2({
|
|
221
|
+
message,
|
|
222
|
+
defaultValue,
|
|
223
|
+
placeholder: placeholder || defaultValue,
|
|
224
|
+
validate(value) {
|
|
225
|
+
if (!value.trim() && !defaultValue) return "This field is required.";
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
if (isCancel2(result)) {
|
|
229
|
+
log2.warn("Setup cancelled.");
|
|
230
|
+
process.exit(0);
|
|
231
|
+
}
|
|
232
|
+
return result.trim() || defaultValue;
|
|
233
|
+
}
|
|
234
|
+
async function setupStep(auth, flags = {}) {
|
|
235
|
+
const s = spinner2();
|
|
236
|
+
const hasOrg = Boolean(auth.org);
|
|
237
|
+
const stepTitle = hasOrg ? "Step 2 of 6 \u2014 Create app" : "Step 2 of 6 \u2014 Create organization & app";
|
|
238
|
+
log2.step(import_picocolors2.default.bold(stepTitle));
|
|
239
|
+
let orgName;
|
|
240
|
+
let orgSlug;
|
|
241
|
+
if (!auth.org) {
|
|
242
|
+
if (flags.ci && flags.org) {
|
|
243
|
+
orgName = flags.org;
|
|
244
|
+
orgSlug = slugify(flags.org);
|
|
245
|
+
} else {
|
|
246
|
+
log2.info("Your team or company name. This is the top-level account that holds all your apps.");
|
|
247
|
+
orgName = await promptForValue("Organization name:", "My Organization");
|
|
248
|
+
log2.info("URL-friendly identifier \u2014 your dashboard will be at userdispatch.com/org/<slug>.");
|
|
249
|
+
orgSlug = await promptForValue("Organization slug:", slugify(orgName));
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
log2.info(`Using organization: ${import_picocolors2.default.bold(auth.org.name)}`);
|
|
253
|
+
}
|
|
254
|
+
const defaultAppName = toTitleCase(basename(process.cwd()));
|
|
255
|
+
let appName;
|
|
256
|
+
let appSlug;
|
|
257
|
+
if (flags.ci && flags.app) {
|
|
258
|
+
appName = flags.app;
|
|
259
|
+
appSlug = slugify(flags.app);
|
|
260
|
+
} else {
|
|
261
|
+
log2.info("Name your app \u2014 this label appears in the feedback widget and your dashboard.");
|
|
262
|
+
appName = await promptForValue("App name:", defaultAppName);
|
|
263
|
+
log2.info("URL-friendly identifier \u2014 feedback form will be at userdispatch.com/f/<org>/<app-slug>.");
|
|
264
|
+
appSlug = await promptForValue("App slug:", slugify(appName));
|
|
265
|
+
}
|
|
266
|
+
const MAX_ATTEMPTS = 3;
|
|
267
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
268
|
+
s.start("Creating your setup...");
|
|
269
|
+
try {
|
|
270
|
+
const body = {
|
|
271
|
+
appName,
|
|
272
|
+
appSlug
|
|
273
|
+
};
|
|
274
|
+
if (orgName) body.orgName = orgName;
|
|
275
|
+
if (orgSlug) body.orgSlug = orgSlug;
|
|
276
|
+
const res = await apiPost("/cli/setup", body, auth.token);
|
|
277
|
+
s.stop("Setup complete.");
|
|
278
|
+
const { org, app, isNewOrg } = res.data;
|
|
279
|
+
if (isNewOrg) {
|
|
280
|
+
log2.info(`Created organization: ${org.name} (${org.slug})`);
|
|
281
|
+
}
|
|
282
|
+
log2.info(`Created app: ${app.name} (${app.slug})`);
|
|
283
|
+
return { org, app, isNewOrg };
|
|
284
|
+
} catch (err) {
|
|
285
|
+
s.stop("Setup failed.");
|
|
286
|
+
if (err instanceof ApiError && err.status === 409) {
|
|
287
|
+
const conflictBody = err.body;
|
|
288
|
+
if (conflictBody.existing) {
|
|
289
|
+
const existing = conflictBody.existing;
|
|
290
|
+
const conflictOrg = conflictBody.org;
|
|
291
|
+
if (flags.ci) {
|
|
292
|
+
log2.info(`App "${existing.slug}" already exists \u2014 using it.`);
|
|
293
|
+
const org = conflictOrg || auth.org;
|
|
294
|
+
return {
|
|
295
|
+
org,
|
|
296
|
+
app: { id: existing.id, name: existing.name, slug: existing.slug, apiKey: existing.apiKey },
|
|
297
|
+
isNewOrg: false
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const action = await select({
|
|
301
|
+
message: `App "${existing.slug}" already exists in this organization.`,
|
|
302
|
+
options: [
|
|
303
|
+
{ value: "use", label: "Use existing app", hint: existing.name },
|
|
304
|
+
{ value: "rename", label: "Enter a different name" }
|
|
305
|
+
]
|
|
306
|
+
});
|
|
307
|
+
if (isCancel2(action)) {
|
|
308
|
+
log2.warn("Setup cancelled.");
|
|
309
|
+
process.exit(0);
|
|
310
|
+
}
|
|
311
|
+
if (action === "use") {
|
|
312
|
+
const org = conflictOrg || auth.org;
|
|
313
|
+
return {
|
|
314
|
+
org,
|
|
315
|
+
app: { id: existing.id, name: existing.name, slug: existing.slug, apiKey: existing.apiKey },
|
|
316
|
+
isNewOrg: false
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
appName = await promptForValue("App name:", defaultAppName);
|
|
320
|
+
appSlug = await promptForValue("App slug:", slugify(appName));
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
log2.warn(`${err.message}. Please choose a different slug.`);
|
|
324
|
+
if (orgName) {
|
|
325
|
+
orgSlug = await promptForValue("Organization slug:", slugify(orgName));
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (err instanceof ApiError) {
|
|
330
|
+
log2.error(err.message);
|
|
331
|
+
} else if (err instanceof Error) {
|
|
332
|
+
log2.error(err.message);
|
|
333
|
+
}
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
log2.error("Too many conflicts. Please try again.");
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/steps/widget.ts
|
|
342
|
+
var import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
343
|
+
import { spinner as spinner3, log as log3, confirm, isCancel as isCancel3 } from "@clack/prompts";
|
|
344
|
+
import { execSync } from "child_process";
|
|
345
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
346
|
+
import { join as join2 } from "path";
|
|
347
|
+
|
|
348
|
+
// src/lib/detect.ts
|
|
349
|
+
import { existsSync, readFileSync } from "fs";
|
|
350
|
+
import { join } from "path";
|
|
351
|
+
import { homedir } from "os";
|
|
352
|
+
function detectPackageManager(cwd = process.cwd()) {
|
|
353
|
+
if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) return "bun";
|
|
354
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
355
|
+
if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
|
|
356
|
+
return "npm";
|
|
357
|
+
}
|
|
358
|
+
function detectFramework(cwd = process.cwd()) {
|
|
359
|
+
const pkgPath = join(cwd, "package.json");
|
|
360
|
+
let deps = {};
|
|
361
|
+
if (existsSync(pkgPath)) {
|
|
362
|
+
try {
|
|
363
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
364
|
+
deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
365
|
+
} catch {
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (deps["next"]) {
|
|
369
|
+
if (existsSync(join(cwd, "app/layout.tsx")) || existsSync(join(cwd, "app/layout.jsx"))) {
|
|
370
|
+
return { framework: "next-app", label: "Next.js (App Router)", layoutFile: join(cwd, "app/layout.tsx") };
|
|
371
|
+
}
|
|
372
|
+
if (existsSync(join(cwd, "src/app/layout.tsx")) || existsSync(join(cwd, "src/app/layout.jsx"))) {
|
|
373
|
+
return { framework: "next-app", label: "Next.js (App Router)", layoutFile: join(cwd, "src/app/layout.tsx") };
|
|
374
|
+
}
|
|
375
|
+
if (existsSync(join(cwd, "pages/_document.tsx")) || existsSync(join(cwd, "pages/_document.jsx"))) {
|
|
376
|
+
return { framework: "next-pages", label: "Next.js (Pages Router)", layoutFile: join(cwd, "pages/_document.tsx") };
|
|
377
|
+
}
|
|
378
|
+
return { framework: "next-app", label: "Next.js", layoutFile: void 0 };
|
|
379
|
+
}
|
|
380
|
+
if (deps["vite"]) {
|
|
381
|
+
return { framework: "vite", label: "Vite", layoutFile: join(cwd, "index.html") };
|
|
382
|
+
}
|
|
383
|
+
if (deps["react-scripts"]) {
|
|
384
|
+
return { framework: "cra", label: "Create React App", layoutFile: join(cwd, "public/index.html") };
|
|
385
|
+
}
|
|
386
|
+
if (deps["nuxt"]) {
|
|
387
|
+
return { framework: "nuxt", label: "Nuxt", layoutFile: join(cwd, "nuxt.config.ts") };
|
|
388
|
+
}
|
|
389
|
+
if (deps["@sveltejs/kit"]) {
|
|
390
|
+
return { framework: "sveltekit", label: "SvelteKit", layoutFile: join(cwd, "src/app.html") };
|
|
391
|
+
}
|
|
392
|
+
if (deps["astro"]) {
|
|
393
|
+
return { framework: "astro", label: "Astro", layoutFile: void 0 };
|
|
394
|
+
}
|
|
395
|
+
if (existsSync(join(cwd, "index.html"))) {
|
|
396
|
+
return { framework: "static", label: "Static HTML", layoutFile: join(cwd, "index.html") };
|
|
397
|
+
}
|
|
398
|
+
return { framework: "unknown", label: "Unknown", layoutFile: void 0 };
|
|
399
|
+
}
|
|
400
|
+
function detectAgents(cwd = process.cwd()) {
|
|
401
|
+
const home = homedir();
|
|
402
|
+
const agents = [
|
|
403
|
+
{
|
|
404
|
+
name: "Claude Code",
|
|
405
|
+
id: "claude-code",
|
|
406
|
+
configPath: join(cwd, ".mcp.json"),
|
|
407
|
+
detected: true
|
|
408
|
+
// always offer Claude Code
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
name: "Cursor",
|
|
412
|
+
id: "cursor",
|
|
413
|
+
configPath: join(cwd, ".cursor/mcp.json"),
|
|
414
|
+
detected: existsSync(join(cwd, ".cursor"))
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
name: "Windsurf",
|
|
418
|
+
id: "windsurf",
|
|
419
|
+
configPath: join(home, ".codeium/windsurf/mcp_config.json"),
|
|
420
|
+
detected: existsSync(join(home, ".codeium/windsurf"))
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
name: "VS Code Copilot",
|
|
424
|
+
id: "vscode",
|
|
425
|
+
configPath: join(cwd, ".vscode/mcp.json"),
|
|
426
|
+
detected: existsSync(join(cwd, ".vscode"))
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: "Codex",
|
|
430
|
+
id: "codex",
|
|
431
|
+
configPath: join(cwd, ".codex/config.toml"),
|
|
432
|
+
detected: existsSync(join(cwd, ".codex"))
|
|
433
|
+
}
|
|
434
|
+
];
|
|
435
|
+
return agents;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/steps/widget.ts
|
|
439
|
+
var WIDGET_URL = `${PROD_URL}/widget.js`;
|
|
440
|
+
function installCommand(pm, pkg) {
|
|
441
|
+
switch (pm) {
|
|
442
|
+
case "bun":
|
|
443
|
+
return `bun add ${pkg}`;
|
|
444
|
+
case "pnpm":
|
|
445
|
+
return `pnpm add ${pkg}`;
|
|
446
|
+
case "yarn":
|
|
447
|
+
return `yarn add ${pkg}`;
|
|
448
|
+
default:
|
|
449
|
+
return `npm install ${pkg}`;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function isAlreadyInstalled(cwd) {
|
|
453
|
+
const pkgPath = join2(cwd, "package.json");
|
|
454
|
+
if (!existsSync2(pkgPath)) return false;
|
|
455
|
+
try {
|
|
456
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
457
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
458
|
+
return !!allDeps["@userdispatch/sdk"];
|
|
459
|
+
} catch {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function fileContainsWidget(filePath) {
|
|
464
|
+
if (!existsSync2(filePath)) return false;
|
|
465
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
466
|
+
return content.includes("userdispatch.com/widget.js") || content.includes("userdispatch/widget");
|
|
467
|
+
}
|
|
468
|
+
function injectNextAppRouter(filePath, apiKey) {
|
|
469
|
+
if (!existsSync2(filePath)) return false;
|
|
470
|
+
let content = readFileSync2(filePath, "utf-8");
|
|
471
|
+
if (fileContainsWidget(filePath)) {
|
|
472
|
+
log3.info("Widget already present in layout file \u2014 skipping injection.");
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
if (!content.includes("from 'next/script'") && !content.includes('from "next/script"')) {
|
|
476
|
+
const lastImportIndex = content.lastIndexOf("import ");
|
|
477
|
+
if (lastImportIndex !== -1) {
|
|
478
|
+
const endOfImport = content.indexOf("\n", lastImportIndex);
|
|
479
|
+
const importLine = `
|
|
480
|
+
import Script from "next/script";`;
|
|
481
|
+
content = content.slice(0, endOfImport + 1) + importLine + content.slice(endOfImport + 1);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const scriptTag = ` <Script src="${WIDGET_URL}" data-api-key="${apiKey}" strategy="afterInteractive" />`;
|
|
485
|
+
const bodyCloseIndex = content.lastIndexOf("</body>");
|
|
486
|
+
if (bodyCloseIndex === -1) {
|
|
487
|
+
log3.warn("Could not find </body> tag in layout file.");
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
content = content.slice(0, bodyCloseIndex) + scriptTag + "\n" + content.slice(bodyCloseIndex);
|
|
491
|
+
writeFileSync(filePath, content, "utf-8");
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
function injectHtmlFile(filePath, apiKey) {
|
|
495
|
+
if (!existsSync2(filePath)) return false;
|
|
496
|
+
let content = readFileSync2(filePath, "utf-8");
|
|
497
|
+
if (fileContainsWidget(filePath)) {
|
|
498
|
+
log3.info("Widget already present \u2014 skipping injection.");
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
const scriptTag = ` <script src="${WIDGET_URL}" data-api-key="${apiKey}" defer></script>`;
|
|
502
|
+
const bodyCloseIndex = content.lastIndexOf("</body>");
|
|
503
|
+
if (bodyCloseIndex === -1) {
|
|
504
|
+
log3.warn("Could not find </body> tag in file.");
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
content = content.slice(0, bodyCloseIndex) + scriptTag + "\n" + content.slice(bodyCloseIndex);
|
|
508
|
+
writeFileSync(filePath, content, "utf-8");
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
async function widgetStep(apiKey, flags = {}) {
|
|
512
|
+
const cwd = process.cwd();
|
|
513
|
+
const s = spinner3();
|
|
514
|
+
const pm = detectPackageManager(cwd);
|
|
515
|
+
const detection = detectFramework(cwd);
|
|
516
|
+
log3.step(import_picocolors3.default.bold("Step 3 of 6 \u2014 Install widget"));
|
|
517
|
+
log3.info(`Add the feedback widget to your app.
|
|
518
|
+
Detected framework: ${detection.label}`);
|
|
519
|
+
let installed = false;
|
|
520
|
+
if (!isAlreadyInstalled(cwd)) {
|
|
521
|
+
let shouldInstall;
|
|
522
|
+
if (flags.ci) {
|
|
523
|
+
shouldInstall = true;
|
|
524
|
+
} else {
|
|
525
|
+
shouldInstall = await confirm({
|
|
526
|
+
message: "Install @userdispatch/sdk? (TypeScript SDK for sending feedback from your code)",
|
|
527
|
+
initialValue: true
|
|
528
|
+
});
|
|
529
|
+
if (isCancel3(shouldInstall)) {
|
|
530
|
+
log3.warn("Setup cancelled.");
|
|
531
|
+
process.exit(0);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (shouldInstall) {
|
|
535
|
+
s.start(`Installing @userdispatch/sdk via ${pm}...`);
|
|
536
|
+
try {
|
|
537
|
+
execSync(installCommand(pm, "@userdispatch/sdk"), {
|
|
538
|
+
cwd,
|
|
539
|
+
stdio: "pipe"
|
|
540
|
+
});
|
|
541
|
+
s.stop("SDK installed.");
|
|
542
|
+
installed = true;
|
|
543
|
+
} catch {
|
|
544
|
+
s.stop("SDK installation failed \u2014 you can install it manually later.");
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
} else {
|
|
548
|
+
log3.info("@userdispatch/sdk already installed.");
|
|
549
|
+
installed = true;
|
|
550
|
+
}
|
|
551
|
+
let injected = false;
|
|
552
|
+
const { framework, layoutFile } = detection;
|
|
553
|
+
if (!layoutFile || !existsSync2(layoutFile)) {
|
|
554
|
+
if (framework === "unknown") {
|
|
555
|
+
log3.info(`Could not detect framework. Add the widget manually:
|
|
556
|
+
<script src="${WIDGET_URL}" data-api-key="${apiKey}" defer></script>`);
|
|
557
|
+
} else {
|
|
558
|
+
log3.info(`No layout file found for ${detection.label}. Add the widget manually.`);
|
|
559
|
+
}
|
|
560
|
+
return { installed, injected: false, framework, packageManager: pm };
|
|
561
|
+
}
|
|
562
|
+
if (framework === "next-app") {
|
|
563
|
+
injected = injectNextAppRouter(layoutFile, apiKey);
|
|
564
|
+
} else if (["vite", "cra", "sveltekit", "static"].includes(framework)) {
|
|
565
|
+
injected = injectHtmlFile(layoutFile, apiKey);
|
|
566
|
+
} else if (framework === "nuxt") {
|
|
567
|
+
log3.info(`For Nuxt, add the widget script to your nuxt.config.ts:
|
|
568
|
+
app: { head: { script: [{ src: "${WIDGET_URL}", "data-api-key": "${apiKey}", defer: true }] } }`);
|
|
569
|
+
} else if (framework === "next-pages") {
|
|
570
|
+
injected = injectHtmlFile(layoutFile, apiKey);
|
|
571
|
+
} else {
|
|
572
|
+
log3.info(`Add the widget manually:
|
|
573
|
+
<script src="${WIDGET_URL}" data-api-key="${apiKey}" defer></script>`);
|
|
574
|
+
}
|
|
575
|
+
if (injected) {
|
|
576
|
+
log3.success(`Widget injected into ${layoutFile}`);
|
|
577
|
+
}
|
|
578
|
+
return { installed, injected, framework, file: injected ? layoutFile : void 0, packageManager: pm };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// src/steps/mcp.ts
|
|
582
|
+
var import_picocolors4 = __toESM(require_picocolors(), 1);
|
|
583
|
+
import { multiselect, spinner as spinner4, log as log4, isCancel as isCancel4 } from "@clack/prompts";
|
|
584
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync } from "fs";
|
|
585
|
+
import { dirname, relative } from "path";
|
|
586
|
+
var MCP_URL = `${PROD_URL}/api/mcp`;
|
|
587
|
+
var DOCS_URL = `${PROD_URL}/docs/mcp`;
|
|
588
|
+
function readJsonFile(filePath) {
|
|
589
|
+
if (!existsSync3(filePath)) return {};
|
|
590
|
+
try {
|
|
591
|
+
return JSON.parse(readFileSync3(filePath, "utf-8"));
|
|
592
|
+
} catch {
|
|
593
|
+
return {};
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function writeJsonFile(filePath, data) {
|
|
597
|
+
const dir = dirname(filePath);
|
|
598
|
+
if (!existsSync3(dir)) {
|
|
599
|
+
mkdirSync(dir, { recursive: true });
|
|
600
|
+
}
|
|
601
|
+
writeFileSync2(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
602
|
+
}
|
|
603
|
+
function addToGitignore(cwd, entry) {
|
|
604
|
+
const gitignorePath = `${cwd}/.gitignore`;
|
|
605
|
+
if (!existsSync3(gitignorePath)) return;
|
|
606
|
+
const content = readFileSync3(gitignorePath, "utf-8");
|
|
607
|
+
if (content.includes(entry)) return;
|
|
608
|
+
const newline = content.endsWith("\n") ? "" : "\n";
|
|
609
|
+
writeFileSync2(gitignorePath, content + newline + entry + "\n", "utf-8");
|
|
610
|
+
}
|
|
611
|
+
function configureCodexMcp(agent, token, cwd) {
|
|
612
|
+
const dir = dirname(agent.configPath);
|
|
613
|
+
if (!existsSync3(dir)) {
|
|
614
|
+
mkdirSync(dir, { recursive: true });
|
|
615
|
+
}
|
|
616
|
+
let content = "";
|
|
617
|
+
if (existsSync3(agent.configPath)) {
|
|
618
|
+
content = readFileSync3(agent.configPath, "utf-8");
|
|
619
|
+
}
|
|
620
|
+
const section = `[mcp_servers.userdispatch]
|
|
621
|
+
url = "${MCP_URL}"
|
|
622
|
+
http_headers = { "Authorization" = "Bearer ${token}" }`;
|
|
623
|
+
const sectionRegex = /\[mcp_servers\.userdispatch\][^\[]*/s;
|
|
624
|
+
if (sectionRegex.test(content)) {
|
|
625
|
+
content = content.replace(sectionRegex, section + "\n");
|
|
626
|
+
} else {
|
|
627
|
+
const newline = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
628
|
+
content = content + newline + "\n" + section + "\n";
|
|
629
|
+
}
|
|
630
|
+
writeFileSync2(agent.configPath, content, "utf-8");
|
|
631
|
+
const relPath = relative(cwd, agent.configPath);
|
|
632
|
+
if (!relPath.startsWith("..") && !relPath.startsWith("/")) {
|
|
633
|
+
addToGitignore(cwd, relPath);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function configureMcp(agent, token, cwd) {
|
|
637
|
+
if (agent.id === "codex") {
|
|
638
|
+
configureCodexMcp(agent, token, cwd);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const config = readJsonFile(agent.configPath);
|
|
642
|
+
const mcpServers = config.mcpServers ?? {};
|
|
643
|
+
mcpServers["userdispatch"] = {
|
|
644
|
+
url: MCP_URL,
|
|
645
|
+
headers: {
|
|
646
|
+
Authorization: `Bearer ${token}`
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
config.mcpServers = mcpServers;
|
|
650
|
+
writeJsonFile(agent.configPath, config);
|
|
651
|
+
const relPath = relative(cwd, agent.configPath);
|
|
652
|
+
if (!relPath.startsWith("..") && !relPath.startsWith("/")) {
|
|
653
|
+
addToGitignore(cwd, relPath);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async function mcpStep(token, flags = {}) {
|
|
657
|
+
const cwd = process.cwd();
|
|
658
|
+
const agents = detectAgents(cwd);
|
|
659
|
+
const detected = agents.filter((a) => a.detected);
|
|
660
|
+
log4.step(import_picocolors4.default.bold("Step 4 of 6 \u2014 Connect the DispatchAgent MCP Server"));
|
|
661
|
+
log4.info("The DispatchAgent MCP Server lets your coding agent triage bugs, reply to users, and generate weekly digests.");
|
|
662
|
+
if (detected.length === 0) {
|
|
663
|
+
log4.info(`No coding agents detected. You can configure MCP manually later: ${import_picocolors4.default.underline(DOCS_URL)}`);
|
|
664
|
+
return { configured: [], skipped: true };
|
|
665
|
+
}
|
|
666
|
+
let selected;
|
|
667
|
+
if (flags.ci) {
|
|
668
|
+
selected = detected;
|
|
669
|
+
log4.info(`Configuring MCP for ${detected.map((a) => a.name).join(", ")}...`);
|
|
670
|
+
} else if (detected.length === 1) {
|
|
671
|
+
selected = detected;
|
|
672
|
+
log4.info(`Configuring MCP for ${detected[0].name}...`);
|
|
673
|
+
} else {
|
|
674
|
+
const result = await multiselect({
|
|
675
|
+
message: "Which coding agents should we configure?",
|
|
676
|
+
options: detected.map((a) => ({
|
|
677
|
+
value: a.id,
|
|
678
|
+
label: a.name,
|
|
679
|
+
hint: a.configPath
|
|
680
|
+
})),
|
|
681
|
+
initialValues: detected.map((a) => a.id)
|
|
682
|
+
});
|
|
683
|
+
if (isCancel4(result)) {
|
|
684
|
+
log4.warn("Setup cancelled.");
|
|
685
|
+
process.exit(0);
|
|
686
|
+
}
|
|
687
|
+
selected = detected.filter((a) => result.includes(a.id));
|
|
688
|
+
}
|
|
689
|
+
if (selected.length === 0) {
|
|
690
|
+
log4.info("Skipped MCP configuration.");
|
|
691
|
+
return { configured: [], skipped: true };
|
|
692
|
+
}
|
|
693
|
+
const s = spinner4();
|
|
694
|
+
s.start("Configuring MCP servers...");
|
|
695
|
+
for (const agent of selected) {
|
|
696
|
+
configureMcp(agent, token, cwd);
|
|
697
|
+
}
|
|
698
|
+
s.stop("MCP servers configured.");
|
|
699
|
+
for (const agent of selected) {
|
|
700
|
+
log4.info(` ${agent.name}: ${agent.configPath}`);
|
|
701
|
+
}
|
|
702
|
+
log4.info(`Manual setup & docs: ${import_picocolors4.default.underline(DOCS_URL)}`);
|
|
703
|
+
return { configured: selected, skipped: false };
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/steps/verify.ts
|
|
707
|
+
var import_picocolors5 = __toESM(require_picocolors(), 1);
|
|
708
|
+
import { spinner as spinner5, log as log5 } from "@clack/prompts";
|
|
709
|
+
async function verifyStep(ctx) {
|
|
710
|
+
const s = spinner5();
|
|
711
|
+
log5.step(import_picocolors5.default.bold("Step 5 of 6 \u2014 Verify connection"));
|
|
712
|
+
log5.info("Sending a test submission to confirm everything is wired up.");
|
|
713
|
+
s.start("Sending test submission...");
|
|
714
|
+
const agentNames = ctx.mcp.configured.map((a) => a.name).join(", ") || "none";
|
|
715
|
+
try {
|
|
716
|
+
await apiSubmission(
|
|
717
|
+
"/v1/submissions",
|
|
718
|
+
{
|
|
719
|
+
type: "feedback",
|
|
720
|
+
subject: "Welcome to UserDispatch!",
|
|
721
|
+
message: `Setup complete for ${ctx.setup.app.name} (${ctx.setup.org.name}).
|
|
722
|
+
|
|
723
|
+
Configured agents: ${agentNames}.
|
|
724
|
+
|
|
725
|
+
Try replying to this submission from your dashboard or coding agent to see it in action.`,
|
|
726
|
+
metadata: { source: "cli-init", agents: agentNames, version: "0.3.0" }
|
|
727
|
+
},
|
|
728
|
+
ctx.apiKey
|
|
729
|
+
);
|
|
730
|
+
s.stop("Test submission sent \u2014 check your dashboard to see it.");
|
|
731
|
+
return true;
|
|
732
|
+
} catch (err) {
|
|
733
|
+
s.stop("Test submission failed.");
|
|
734
|
+
if (err instanceof ApiError) {
|
|
735
|
+
log5.warn(`Could not send test submission: ${err.message}`);
|
|
736
|
+
} else if (err instanceof Error) {
|
|
737
|
+
log5.warn(`Could not send test submission: ${err.message}`);
|
|
738
|
+
}
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// src/steps/summary.ts
|
|
744
|
+
var import_picocolors6 = __toESM(require_picocolors(), 1);
|
|
745
|
+
import { note, outro, log as log6 } from "@clack/prompts";
|
|
746
|
+
import { exec as exec2 } from "child_process";
|
|
747
|
+
import { platform as platform2 } from "os";
|
|
748
|
+
|
|
749
|
+
// src/lib/agent-guides.ts
|
|
750
|
+
var guides = {
|
|
751
|
+
"claude-code": {
|
|
752
|
+
name: "Claude Code",
|
|
753
|
+
restart: "Restart Claude Code (or start a new conversation) to load the MCP server.",
|
|
754
|
+
prompts: [
|
|
755
|
+
"Show me my recent submissions",
|
|
756
|
+
"Triage open bugs and suggest priorities",
|
|
757
|
+
"Draft a reply to the latest feedback",
|
|
758
|
+
"Generate a weekly feedback digest"
|
|
759
|
+
],
|
|
760
|
+
tip: "Claude Code can read, reply, and update statuses \u2014 all from your terminal."
|
|
761
|
+
},
|
|
762
|
+
cursor: {
|
|
763
|
+
name: "Cursor",
|
|
764
|
+
restart: "Restart Cursor to load MCP. Check Settings > MCP Servers to confirm.",
|
|
765
|
+
prompts: [
|
|
766
|
+
"@userdispatch Show me my recent submissions",
|
|
767
|
+
"@userdispatch Triage open bugs and suggest priorities",
|
|
768
|
+
"@userdispatch Draft a reply to the latest feedback",
|
|
769
|
+
"@userdispatch Generate a weekly feedback digest"
|
|
770
|
+
],
|
|
771
|
+
tip: "Cursor can read, reply, and update statuses right from the editor."
|
|
772
|
+
},
|
|
773
|
+
windsurf: {
|
|
774
|
+
name: "Windsurf",
|
|
775
|
+
restart: "Restart Windsurf to load the MCP server.",
|
|
776
|
+
prompts: [
|
|
777
|
+
"Show me my recent submissions",
|
|
778
|
+
"Triage open bugs and suggest priorities",
|
|
779
|
+
"Draft a reply to the latest feedback",
|
|
780
|
+
"Generate a weekly feedback digest"
|
|
781
|
+
],
|
|
782
|
+
tip: "Windsurf can read, reply, and update statuses from your editor."
|
|
783
|
+
},
|
|
784
|
+
codex: {
|
|
785
|
+
name: "Codex",
|
|
786
|
+
restart: "Restart Codex or open a new session to load the MCP server.",
|
|
787
|
+
prompts: [
|
|
788
|
+
"Show me my recent submissions",
|
|
789
|
+
"Triage open bugs and suggest priorities",
|
|
790
|
+
"Draft a reply to the latest feedback",
|
|
791
|
+
"Generate a weekly feedback digest"
|
|
792
|
+
],
|
|
793
|
+
tip: "Codex can read, reply, and update statuses from your terminal."
|
|
794
|
+
},
|
|
795
|
+
vscode: {
|
|
796
|
+
name: "VS Code Copilot",
|
|
797
|
+
restart: "Reload VS Code (Cmd+Shift+P > Reload Window) to pick up MCP.",
|
|
798
|
+
prompts: [
|
|
799
|
+
"Show me my recent submissions",
|
|
800
|
+
"Triage open bugs and suggest priorities",
|
|
801
|
+
"Draft a reply to the latest feedback",
|
|
802
|
+
"Generate a weekly feedback digest"
|
|
803
|
+
],
|
|
804
|
+
tip: "VS Code Copilot can read, reply, and update statuses from the editor."
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
function getAgentGuide(agentId) {
|
|
808
|
+
return guides[agentId];
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// src/steps/summary.ts
|
|
812
|
+
function openBrowser2(url) {
|
|
813
|
+
const os = platform2();
|
|
814
|
+
const cmd = os === "darwin" ? "open" : os === "win32" ? "start" : "xdg-open";
|
|
815
|
+
exec2(`${cmd} "${url}"`, () => {
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
function summaryStep(result) {
|
|
819
|
+
const { setup, widget, mcp, verified } = result;
|
|
820
|
+
log6.step(import_picocolors6.default.bold("Step 6 of 6 \u2014 Summary"));
|
|
821
|
+
const maskedKey = setup.app.apiKey ? setup.app.apiKey.slice(0, 7) + "..." + setup.app.apiKey.slice(-4) : "N/A";
|
|
822
|
+
const statusLines = [];
|
|
823
|
+
statusLines.push(`Organization ${setup.org.name} (${setup.org.slug})`);
|
|
824
|
+
statusLines.push(`App ${setup.app.name} (${setup.app.slug})`);
|
|
825
|
+
statusLines.push(`API Key ${maskedKey}`);
|
|
826
|
+
statusLines.push("");
|
|
827
|
+
if (widget.installed) {
|
|
828
|
+
statusLines.push(`${import_picocolors6.default.green("\u2713")} SDK installed via ${widget.packageManager}`);
|
|
829
|
+
}
|
|
830
|
+
if (widget.injected && widget.file) {
|
|
831
|
+
statusLines.push(`${import_picocolors6.default.green("\u2713")} Widget injected into ${widget.file}`);
|
|
832
|
+
} else if (!widget.injected) {
|
|
833
|
+
statusLines.push(`\u25CB Widget: manual setup needed`);
|
|
834
|
+
}
|
|
835
|
+
if (mcp.configured.length > 0) {
|
|
836
|
+
const names = mcp.configured.map((a) => a.name).join(", ");
|
|
837
|
+
statusLines.push(`${import_picocolors6.default.green("\u2713")} MCP configured for ${names}`);
|
|
838
|
+
}
|
|
839
|
+
statusLines.push(`${verified ? import_picocolors6.default.green("\u2713") : "\u25CB"} Connection ${verified ? "verified" : "not verified"}`);
|
|
840
|
+
note(statusLines.join("\n"), "Setup Complete");
|
|
841
|
+
const nextLines = [];
|
|
842
|
+
nextLines.push("Links");
|
|
843
|
+
nextLines.push(` Dashboard: ${PROD_URL}/org/${setup.org.slug}/dashboard`);
|
|
844
|
+
nextLines.push(` Submissions: ${PROD_URL}/org/${setup.org.slug}/submissions`);
|
|
845
|
+
nextLines.push(` Feedback form: ${PROD_URL}/f/${setup.org.slug}/${setup.app.slug}`);
|
|
846
|
+
if (mcp.configured.length > 0) {
|
|
847
|
+
const primaryAgent = mcp.configured[0];
|
|
848
|
+
const guide = getAgentGuide(primaryAgent.id);
|
|
849
|
+
if (guide) {
|
|
850
|
+
nextLines.push("");
|
|
851
|
+
nextLines.push(`Get started with ${guide.name}`);
|
|
852
|
+
nextLines.push("");
|
|
853
|
+
nextLines.push(` 1. ${guide.restart}`);
|
|
854
|
+
nextLines.push(" 2. Try these prompts:");
|
|
855
|
+
nextLines.push("");
|
|
856
|
+
for (const prompt of guide.prompts) {
|
|
857
|
+
nextLines.push(` > "${prompt}"`);
|
|
858
|
+
}
|
|
859
|
+
nextLines.push("");
|
|
860
|
+
nextLines.push(`Tip: ${guide.tip}`);
|
|
861
|
+
}
|
|
862
|
+
} else {
|
|
863
|
+
nextLines.push("");
|
|
864
|
+
nextLines.push("Get started");
|
|
865
|
+
nextLines.push("");
|
|
866
|
+
nextLines.push(" 1. Start your dev server \u2014 the widget appears bottom-right");
|
|
867
|
+
nextLines.push(" 2. Submit feedback through the widget");
|
|
868
|
+
nextLines.push(" 3. Check your dashboard to see submissions");
|
|
869
|
+
}
|
|
870
|
+
note(nextLines.join("\n"), "What's Next");
|
|
871
|
+
if (process.stdin.isTTY) {
|
|
872
|
+
const submissionsUrl = `${PROD_URL}/org/${setup.org.slug}/submissions`;
|
|
873
|
+
openBrowser2(submissionsUrl);
|
|
874
|
+
outro("Submissions page opened in your browser. Happy building!");
|
|
875
|
+
} else {
|
|
876
|
+
outro("Setup complete. Happy building!");
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// src/lib/sentry.ts
|
|
881
|
+
import * as Sentry from "@sentry/node";
|
|
882
|
+
var SENTRY_DSN = "https://9611a3d31c8f0b5d8049c23924d0d4ad@o4510882392899584.ingest.us.sentry.io/4510965084127232";
|
|
883
|
+
var SENSITIVE_PATTERNS = [/ud_[a-zA-Z0-9_-]+/g, /pk_[a-zA-Z0-9_-]+/g, /[^\s@]+@[^\s@]+\.[^\s@]+/g];
|
|
884
|
+
function scrubSensitive(str) {
|
|
885
|
+
let result = str;
|
|
886
|
+
for (const pattern of SENSITIVE_PATTERNS) {
|
|
887
|
+
result = result.replace(pattern, "[REDACTED]");
|
|
888
|
+
}
|
|
889
|
+
return result;
|
|
890
|
+
}
|
|
891
|
+
function initSentry() {
|
|
892
|
+
Sentry.init({
|
|
893
|
+
dsn: SENTRY_DSN,
|
|
894
|
+
environment: process.env.NODE_ENV || "production",
|
|
895
|
+
release: `userdispatch-cli@${process.env.npm_package_version || "unknown"}`,
|
|
896
|
+
beforeSend(event) {
|
|
897
|
+
if (event.exception?.values) {
|
|
898
|
+
for (const ex of event.exception.values) {
|
|
899
|
+
if (ex.value) ex.value = scrubSensitive(ex.value);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
if (event.breadcrumbs) {
|
|
903
|
+
for (const crumb of event.breadcrumbs) {
|
|
904
|
+
if (crumb.message) crumb.message = scrubSensitive(crumb.message);
|
|
905
|
+
if (crumb.data) {
|
|
906
|
+
for (const [key, val] of Object.entries(crumb.data)) {
|
|
907
|
+
if (typeof val === "string") {
|
|
908
|
+
crumb.data[key] = scrubSensitive(val);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return event;
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
function captureCliError(err, context) {
|
|
919
|
+
const safeContext = { ...context };
|
|
920
|
+
delete safeContext.token;
|
|
921
|
+
delete safeContext.apiKey;
|
|
922
|
+
delete safeContext.email;
|
|
923
|
+
Sentry.withScope((scope) => {
|
|
924
|
+
scope.setTag("cli_step", String(safeContext.step || "unknown"));
|
|
925
|
+
scope.setContext("cli", {
|
|
926
|
+
...safeContext,
|
|
927
|
+
platform: process.platform,
|
|
928
|
+
arch: process.arch
|
|
929
|
+
});
|
|
930
|
+
if (err instanceof Error) {
|
|
931
|
+
Sentry.captureException(err);
|
|
932
|
+
} else {
|
|
933
|
+
Sentry.captureMessage(String(err), "error");
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// src/commands/init.ts
|
|
939
|
+
async function initCommand(flags) {
|
|
940
|
+
initSentry();
|
|
941
|
+
console.log(`
|
|
942
|
+
${import_picocolors7.default.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")}
|
|
943
|
+
${import_picocolors7.default.bold("UserDispatch")}
|
|
944
|
+
${import_picocolors7.default.dim("Gather customer feedback in under 60s")}
|
|
945
|
+
${import_picocolors7.default.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")}
|
|
946
|
+
`);
|
|
947
|
+
intro("Setup Wizard");
|
|
948
|
+
log7.info("This wizard will authenticate you, create your app, install the widget, and connect your coding agent through the DispatchAgent MCP Server.");
|
|
949
|
+
let currentStep = "init";
|
|
950
|
+
try {
|
|
951
|
+
currentStep = "auth";
|
|
952
|
+
const auth = await authStep(flags);
|
|
953
|
+
currentStep = "setup";
|
|
954
|
+
const setup = await setupStep(auth, flags);
|
|
955
|
+
const modifiedFiles = [];
|
|
956
|
+
currentStep = "widget";
|
|
957
|
+
let widget;
|
|
958
|
+
try {
|
|
959
|
+
widget = await widgetStep(setup.app.apiKey, flags);
|
|
960
|
+
if (widget.file) modifiedFiles.push(widget.file);
|
|
961
|
+
} catch (err) {
|
|
962
|
+
captureCliError(err, { step: "widget", framework: flags.framework, nodeVersion: process.version });
|
|
963
|
+
log7.warn(`Widget setup encountered an issue: ${err instanceof Error ? err.message : String(err)}`);
|
|
964
|
+
widget = {
|
|
965
|
+
installed: false,
|
|
966
|
+
injected: false,
|
|
967
|
+
framework: "unknown",
|
|
968
|
+
packageManager: "npm"
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
currentStep = "mcp";
|
|
972
|
+
let mcp;
|
|
973
|
+
try {
|
|
974
|
+
mcp = await mcpStep(auth.token, flags);
|
|
975
|
+
for (const agent of mcp.configured) {
|
|
976
|
+
modifiedFiles.push(agent.configPath);
|
|
977
|
+
}
|
|
978
|
+
} catch (err) {
|
|
979
|
+
captureCliError(err, { step: "mcp", nodeVersion: process.version });
|
|
980
|
+
log7.warn(`MCP setup encountered an issue: ${err instanceof Error ? err.message : String(err)}`);
|
|
981
|
+
mcp = { configured: [], skipped: true };
|
|
982
|
+
}
|
|
983
|
+
currentStep = "verify";
|
|
984
|
+
let verified = false;
|
|
985
|
+
try {
|
|
986
|
+
verified = await verifyStep({ apiKey: setup.app.apiKey, setup, mcp });
|
|
987
|
+
} catch (err) {
|
|
988
|
+
captureCliError(err, { step: "verify", nodeVersion: process.version });
|
|
989
|
+
log7.warn(`Verification encountered an issue: ${err instanceof Error ? err.message : String(err)}`);
|
|
990
|
+
}
|
|
991
|
+
currentStep = "summary";
|
|
992
|
+
summaryStep({
|
|
993
|
+
auth,
|
|
994
|
+
setup,
|
|
995
|
+
widget,
|
|
996
|
+
mcp,
|
|
997
|
+
verified,
|
|
998
|
+
modifiedFiles
|
|
999
|
+
});
|
|
1000
|
+
} catch (err) {
|
|
1001
|
+
captureCliError(err, { step: currentStep, nodeVersion: process.version });
|
|
1002
|
+
throw err;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
export {
|
|
1006
|
+
initCommand
|
|
1007
|
+
};
|