@userreason/wizard 0.0.0-stage → 0.2.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/bin.js ADDED
@@ -0,0 +1,764 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as createWizardRequire } from 'node:module'; import { fileURLToPath as wizardFileURLToPath } from 'node:url'; import { dirname as wizardDirname } from 'node:path'; const require = createWizardRequire(import.meta.url); const __filename = wizardFileURLToPath(import.meta.url); const __dirname = wizardDirname(__filename);
3
+ import {
4
+ offerAgentNote,
5
+ planAgentNote
6
+ } from "./chunk-KV6GSBTQ.js";
7
+ import {
8
+ offerAgentSkill,
9
+ planAgentSkill
10
+ } from "./chunk-5Z5J6PY5.js";
11
+ import {
12
+ offerCliAccess
13
+ } from "./chunk-HACTNBV3.js";
14
+ import {
15
+ offerGlobalCli
16
+ } from "./chunk-JD5U4U26.js";
17
+ import {
18
+ ensureCurrentWizard,
19
+ wizardVersion
20
+ } from "./chunk-6CJ4ANMN.js";
21
+ import {
22
+ Cancelled,
23
+ Terminal,
24
+ approvalStatus,
25
+ clean,
26
+ normalizeOrigin,
27
+ openBrowser
28
+ } from "./chunk-SJVLAYGG.js";
29
+ import {
30
+ BrowserConnectionUnavailable,
31
+ compatibilityCredentials,
32
+ connectInstallation,
33
+ memoryCredentials
34
+ } from "./chunk-3RDJORRS.js";
35
+ import {
36
+ clearInstallation,
37
+ resolveInstallation,
38
+ saveInstallation,
39
+ savedInstallation
40
+ } from "./chunk-FC7PZNYW.js";
41
+ import {
42
+ applyIntegration,
43
+ detectInstallation,
44
+ integrationAdditions,
45
+ integrationPreview,
46
+ planIntegration
47
+ } from "./chunk-6DCCEVPD.js";
48
+ import {
49
+ SdkInstallError,
50
+ cliCommand,
51
+ cliInstallCommand,
52
+ devCommand,
53
+ hasCompatibleCli,
54
+ hasCompatibleSdk,
55
+ inspectProject,
56
+ installCli,
57
+ installSdk,
58
+ sdkInstallCommand,
59
+ verifyConfiguration,
60
+ writeIntegration
61
+ } from "./chunk-YMSGDC4B.js";
62
+ import "./chunk-OEGNGGZ2.js";
63
+ import {
64
+ ApiError,
65
+ AuthenticationError,
66
+ UserReasonClient,
67
+ apiUrl,
68
+ credentialStoreKind,
69
+ detectAgentHarness,
70
+ login,
71
+ packageProfile,
72
+ resolveConfig,
73
+ resolveCredentials,
74
+ saveConfig,
75
+ uuid
76
+ } from "./chunk-GGARVSPT.js";
77
+ import "./chunk-WL3NOIM6.js";
78
+
79
+ // src/bin.ts
80
+ import { parseArgs } from "node:util";
81
+ import { randomUUID } from "node:crypto";
82
+ var help = `UserReason installation wizard
83
+
84
+ userreason-wizard [--directory PATH] [--origin URL]
85
+
86
+ --workspace UUID / --workspace-name NAME
87
+ --site UUID / --site-name NAME
88
+ --environment development|production (customer site environment)
89
+ --api-environment development|production (UserReason API/login; default ${packageProfile().apiEnvironment})
90
+ --framework react|next|vue|vanilla
91
+ --skip-install (advanced: prepare without installing SDK/CLI packages)
92
+ --install (force an SDK/CLI reinstall)
93
+ --refresh (update SDK/CLI packages without browser onboarding)
94
+ --integrate / --skip-integrate
95
+ --global-cli / --skip-global-cli (optional global CLI install; also supported with --refresh)
96
+ --cli-access / --skip-cli-access (optional ongoing CLI authorization)
97
+ --agent-note / --skip-agent-note (optional note in existing AGENTS.md or CLAUDE.md)
98
+ --agent-skill / --skip-agent-skill (optional feedback skill in .agents/skills/userreason)
99
+ --yes (accept installation/root edits; global CLI, access and agent guidance need explicit flags; browser approval remains required)
100
+ --dry-run (print the plan, as JSON with --json; nothing is signed in, written or installed)
101
+ --diff (show the full patch for the application edit and the proposed guidance text)
102
+ --interactive (ask questions even when an agent harness or CI is detected)
103
+ --no-color (also NO_COLOR)
104
+ --no-open (print the browser link without opening it)
105
+ --session-only (keep compatibility login only in memory; do not persist credentials)
106
+ --force (replace differing generated files)
107
+ --debug (show package-manager logs), --json (structured result only on stdout)
108
+ --dev (compatibility alias for --api-environment development)
109
+ --credential-store file|keychain (default file; UR_CREDENTIAL_STORE also supported)
110
+ --client-id CLIENT_ID, --api-url ORIGIN
111
+
112
+ The SDK and project-local CLI install automatically, unless compatible versions are already installed.
113
+ Domains are unrestricted by default; --origin optionally adds an exact restriction.
114
+ Bare hostnames are accepted; explicit HTTP localhost restrictions require --environment development.
115
+ Reruns detect SDK initialization and resume unfinished installations from local wizard state. Legacy userreason.config.json is still readable.
116
+ Prompts are off when stdin or stdout is not a terminal, with --yes or --json, in CI, and in agent harnesses (--interactive overrides the last two).
117
+ `;
118
+ var nameError = (value) => !value.trim() || value.trim().length > 240 ? "Enter a name between 1 and 240 characters." : void 0;
119
+ var currentStep = "Reading wizard options";
120
+ var activeTerminal;
121
+ var frameworkLabel = (framework) => framework === "next" ? "Next.js" : framework;
122
+ var lowerFirst = (value) => value.charAt(0).toLowerCase() + value.slice(1);
123
+ async function main() {
124
+ const { values } = parseArgs({
125
+ allowPositionals: false,
126
+ options: {
127
+ help: { type: "boolean", short: "h" },
128
+ version: { type: "boolean", short: "v" },
129
+ dev: { type: "boolean" },
130
+ "api-environment": { type: "string" },
131
+ yes: { type: "boolean" },
132
+ install: { type: "boolean" },
133
+ refresh: { type: "boolean" },
134
+ "skip-install": { type: "boolean" },
135
+ integrate: { type: "boolean" },
136
+ "skip-integrate": { type: "boolean" },
137
+ "agent-note": { type: "boolean" },
138
+ "skip-agent-note": { type: "boolean" },
139
+ "agent-skill": { type: "boolean" },
140
+ "skip-agent-skill": { type: "boolean" },
141
+ "global-cli": { type: "boolean" },
142
+ "skip-global-cli": { type: "boolean" },
143
+ "cli-access": { type: "boolean" },
144
+ "skip-cli-access": { type: "boolean" },
145
+ "dry-run": { type: "boolean" },
146
+ diff: { type: "boolean" },
147
+ interactive: { type: "boolean" },
148
+ "no-color": { type: "boolean" },
149
+ "no-open": { type: "boolean" },
150
+ "session-only": { type: "boolean" },
151
+ force: { type: "boolean" },
152
+ debug: { type: "boolean" },
153
+ json: { type: "boolean" },
154
+ directory: { type: "string" },
155
+ origin: { type: "string" },
156
+ workspace: { type: "string" },
157
+ "workspace-name": { type: "string" },
158
+ site: { type: "string" },
159
+ "site-name": { type: "string" },
160
+ environment: { type: "string" },
161
+ framework: { type: "string" },
162
+ "credential-store": { type: "string" },
163
+ "client-id": { type: "string" },
164
+ "api-url": { type: "string" }
165
+ }
166
+ });
167
+ const version = await wizardVersion();
168
+ if (values.version) {
169
+ process.stdout.write(version + "\n");
170
+ return;
171
+ }
172
+ if (values.help) {
173
+ process.stdout.write(help);
174
+ return;
175
+ }
176
+ if (values["credential-store"] !== void 0) credentialStoreKind(values["credential-store"]);
177
+ if (values["api-environment"] !== void 0 && !["production", "development"].includes(values["api-environment"]))
178
+ throw new Error("API environment must be development or production");
179
+ if (values.dev && values["api-environment"] === "production")
180
+ throw new Error("Conflicting API environments: --dev selects development. Remove --dev or use --api-environment development.");
181
+ const embeddedProfile = packageProfile();
182
+ const apiEnvironment = values["api-environment"] ?? (values.dev ? "development" : embeddedProfile.apiEnvironment);
183
+ const developmentApi = apiEnvironment === "development";
184
+ const newSiteEnvironment = values.environment ?? apiEnvironment;
185
+ if (values["global-cli"] && values["skip-global-cli"]) throw new Error("Choose --global-cli or --skip-global-cli");
186
+ if (values["global-cli"] && values["skip-install"]) throw new Error("--global-cli requires project package installation; omit --skip-install");
187
+ if (values["cli-access"] && values["skip-cli-access"]) throw new Error("Choose --cli-access or --skip-cli-access");
188
+ if (values["cli-access"] && values["session-only"]) throw new Error("Ongoing CLI access requires persistent credentials; omit --session-only");
189
+ if (values.install && values["skip-install"]) throw new Error("Choose either --install or --skip-install");
190
+ if (values["dry-run"] && values.refresh) throw new Error("Choose either --dry-run or --refresh");
191
+ if (values.refresh && (values["skip-install"] || values.workspace || values.site || values["workspace-name"] || values["site-name"]))
192
+ throw new Error("--refresh updates packages in an existing integration; omit setup/resource flags and --skip-install");
193
+ if (values.integrate && values["skip-integrate"])
194
+ throw new Error("Choose either --integrate or --skip-integrate");
195
+ if (values["agent-note"] && values["skip-agent-note"])
196
+ throw new Error("Choose either --agent-note or --skip-agent-note");
197
+ if (values["agent-skill"] && values["skip-agent-skill"])
198
+ throw new Error("Choose either --agent-skill or --skip-agent-skill");
199
+ if (values.workspace && values["workspace-name"])
200
+ throw new Error("Choose an existing workspace or a new workspace name");
201
+ if (values.site && values["site-name"]) throw new Error("Choose an existing site or a new site name");
202
+ if (values.framework && !["react", "next", "vue", "vanilla"].includes(values.framework))
203
+ throw new Error("Unsupported framework");
204
+ if (values.environment && !["production", "development"].includes(values.environment))
205
+ throw new Error("Site environment must be development or production");
206
+ if (values.origin !== void 0)
207
+ normalizeOrigin(values.origin, values.environment === "production" ? "production" : "development");
208
+ for (const value of [values["workspace-name"], values["site-name"]])
209
+ if (value !== void 0 && nameError(value)) throw new Error(nameError(value));
210
+ currentStep = "Checking the current wizard build";
211
+ const startup = await ensureCurrentWizard(embeddedProfile, version, { onRestart: (message) => process.stderr.write(message + "\n") });
212
+ if (startup.exitCode !== void 0) {
213
+ process.exitCode = startup.exitCode;
214
+ return;
215
+ }
216
+ const profile = startup.profile;
217
+ const harness = detectAgentHarness();
218
+ const tty = Boolean(process.stdin.isTTY && process.stdout.isTTY && process.env.TERM !== "dumb");
219
+ const explicitlyQuiet = Boolean(values.yes || values.json);
220
+ const interactive = tty && !explicitlyQuiet && (!harness || Boolean(values.interactive));
221
+ const terminal = new Terminal(interactive, Boolean(values.json), { color: !values["no-color"] });
222
+ activeTerminal = terminal;
223
+ const { c, S } = terminal.kit;
224
+ const dot = ` ${S.dot} `;
225
+ const environmentNote = developmentApi ? `development API${newSiteEnvironment !== apiEnvironment ? `, new sites are ${newSiteEnvironment}` : ""}` : newSiteEnvironment !== apiEnvironment ? `new sites are ${newSiteEnvironment}` : void 0;
226
+ terminal.header({
227
+ mode: values.refresh ? "refresh" : "setup",
228
+ version,
229
+ detail: [
230
+ ...profile.kind === "local-development" ? [`dev build ${profile.buildId.slice(0, 12)} (sdk ${profile.sdkVersion}, cli ${profile.cliVersion})`] : [],
231
+ ...environmentNote ? [environmentNote] : []
232
+ ]
233
+ });
234
+ if (!interactive) terminal.info(`API environment: ${apiEnvironment}${dot}Site environment for new sites: ${newSiteEnvironment}`);
235
+ if (tty && harness && !explicitlyQuiet && !values.interactive)
236
+ terminal.info(`Agent session detected (${harness})${dot}${c.muted("prompts are off, flags decide")}`);
237
+ currentStep = "Inspecting the project";
238
+ const project = await inspectProject(
239
+ values.directory ?? process.cwd(),
240
+ values.framework
241
+ );
242
+ terminal.info(`${clean(project.name)} ${c.muted(`(${frameworkLabel(project.framework)}, ${project.packageManager})`)}`);
243
+ if (values.refresh) {
244
+ currentStep = "Refreshing UserReason packages";
245
+ const existing = await detectInstallation(project);
246
+ if (!existing) throw new Error("No UserReason initialization found. Run the wizard without --refresh to set up this application.");
247
+ const expectedApi = developmentApi ? "https://api-dev.userreason.com" : "https://api.userreason.com";
248
+ if (existing.apiBaseUrl !== expectedApi && !values["api-url"])
249
+ throw new Error("The existing integration uses a different API. Select its --api-environment or pass its --api-url before refreshing.");
250
+ if (values["api-url"] && existing.apiBaseUrl !== apiUrl(values["api-url"])) throw new Error("The existing integration uses a different API URL.");
251
+ const options = { profile, verbose: values.debug, onWarning: (message) => terminal.warn(message) };
252
+ const updated = [];
253
+ const started = Date.now();
254
+ await terminal.task("Updating UserReason packages", async () => {
255
+ if (!await hasCompatibleSdk(project, profile) || values.install) {
256
+ await installSdk(project, options);
257
+ updated.push("@userreason/js");
258
+ }
259
+ if (!await hasCompatibleCli(project, profile) || values.install) {
260
+ await installCli(project, options);
261
+ updated.push("@userreason/cli");
262
+ }
263
+ }, () => updated.length ? `Updated ${updated.map((name) => c.code(name)).join(", ")} ${c.muted(`(${project.packageManager}, ${Math.max(1, Math.round((Date.now() - started) / 1e3))}s)`)}` : "UserReason packages are already current");
264
+ const globalCli2 = await offerGlobalCli(project, terminal, { installed: true, requested: values["global-cli"], skip: !values["global-cli"], force: values.install, verbose: values.debug, profile, preview: Boolean(values.diff) });
265
+ const agentNote2 = await offerAgentNote(project, terminal, { installed: true, requested: values["agent-note"], skip: !values["agent-note"], preview: Boolean(values.diff) });
266
+ const agentSkill2 = await offerAgentSkill(project, terminal, { installed: true, requested: values["agent-skill"], skip: !values["agent-skill"], preview: Boolean(values.diff) });
267
+ const cliAccess2 = values["cli-access"] ? await offerCliAccess(await resolveConfig({ development: developmentApi, apiUrl: existing.apiBaseUrl, clientId: values["client-id"], credentialStore: values["credential-store"] }), terminal, { installed: true, requested: true, noOpen: values["no-open"], development: developmentApi, preview: Boolean(values.diff) }) : { status: "skipped" };
268
+ const untouched = [!values["global-cli"] && ["global command", "--global-cli"], !values["agent-note"] && ["AGENTS.md note", "--agent-note"], !values["agent-skill"] && ["agent skill", "--agent-skill"], !values["cli-access"] && ["CLI access", "--cli-access"]].filter(Boolean);
269
+ terminal.block("Ready", [
270
+ c.muted("Reload or restart your app to load the updated SDK."),
271
+ ...untouched.length ? [c.muted(`Not touched (${untouched.map(([name]) => name).join(", ")})`), `${c.muted("Add")} ${c.code(untouched.map(([, flag]) => flag).join(" "))} ${c.muted("to include them")}`] : []
272
+ ], { kind: "done" });
273
+ if (values.json) process.stdout.write(JSON.stringify({
274
+ global_cli: globalCli2,
275
+ cli_access: cliAccess2,
276
+ refreshed: true,
277
+ sdk_installed: true,
278
+ cli_installed: true,
279
+ api_environment: apiEnvironment,
280
+ wizard_version: version,
281
+ sdk_required_version: profile.sdkVersion,
282
+ cli_required_version: profile.cliVersion,
283
+ build_id: profile.buildId,
284
+ agent_note: agentNote2,
285
+ agent_skill: agentSkill2
286
+ }) + "\n");
287
+ return;
288
+ }
289
+ currentStep = "Preparing the application integration";
290
+ const plan = values["skip-integrate"] ? void 0 : await planIntegration(project);
291
+ if (plan && plan.status !== "ready") terminal.info(plan.reason);
292
+ const sdkCurrent = await hasCompatibleSdk(project, profile), cliCurrent = await hasCompatibleCli(project, profile);
293
+ const packages = [!sdkCurrent || values.install ? "@userreason/js" : "", !cliCurrent || values.install ? "@userreason/cli" : ""].filter(Boolean);
294
+ if (values["dry-run"]) {
295
+ currentStep = "Planning without changes";
296
+ const additions2 = plan ? integrationAdditions(plan) : void 0;
297
+ const note = values["skip-agent-note"] ? void 0 : await planAgentNote(project, { beforeInstall: true }).catch(() => void 0);
298
+ const skill = values["skip-agent-skill"] ? void 0 : await planAgentSkill(project).catch(() => void 0);
299
+ const steps = [
300
+ { action: "approve", needs: "browser" },
301
+ ...packages.length ? [{ action: "install", packages }] : [],
302
+ ...additions2 ? [{ action: "edit", file: additions2.path, status: "ready", added_lines: additions2.added.length, flag: "--integrate", recommended: true }] : [],
303
+ ...note && "path" in note ? [{ action: "write", file: note.file, flag: "--agent-note", status: "ready", recommended: true }] : [],
304
+ ...skill && "files" in skill ? [{ action: "write", file: ".agents/skills/userreason", flag: "--agent-skill", status: "ready", recommended: true }] : [],
305
+ ...!values["skip-cli-access"] && !values["session-only"] ? [{ action: "authorize", flag: "--cli-access", needs: "browser", recommended: true }] : [],
306
+ ...!values["skip-global-cli"] && !values["skip-install"] ? [{ action: "install_global", flag: "--global-cli", recommended: false }] : []
307
+ ];
308
+ const launcher = profile.kind === "local-development" ? `npx -y --prefer-online --registry=${profile.registry} @userreason/wizard@latest` : "npx -y @userreason/wizard@latest";
309
+ const run = [launcher, "--yes", ...steps.filter((step) => "recommended" in step && step.recommended && "flag" in step && step.flag && step.action !== "authorize").map((step) => step.flag)].join(" ");
310
+ if (values.json) {
311
+ process.stdout.write(JSON.stringify({
312
+ dry_run: true,
313
+ project: { framework: project.framework, package_manager: project.packageManager, name: project.name },
314
+ steps,
315
+ site: "Pass --site UUID to use an existing site, or --site-name NAME to create one.",
316
+ run
317
+ }, null, 2) + "\n");
318
+ } else {
319
+ terminal.info(`Dry run${dot}${c.muted("nothing is signed in, written or installed")}`);
320
+ terminal.block("Plan", terminal.kit.cols(steps.map((step) => {
321
+ const what = "file" in step ? String(step.file) : "packages" in step && step.packages ? step.packages.join(" ") : "needs" in step ? String(step.needs) : "";
322
+ const flag = "flag" in step && step.flag ? `(${step.flag})` : "";
323
+ const label = "recommended" in step ? step.recommended ? "recommended" : "optional" : "";
324
+ return [c.code(step.action), [[what, flag].filter(Boolean).join(" "), label].filter(Boolean).join(dot)];
325
+ })), { kind: "next", each: true });
326
+ terminal.block("Run it", [c.code(run)], { kind: "next", each: true });
327
+ }
328
+ return;
329
+ }
330
+ currentStep = "Discovering authentication configuration";
331
+ const config = await resolveConfig({
332
+ development: developmentApi,
333
+ apiUrl: values["api-url"],
334
+ clientId: values["client-id"],
335
+ credentialStore: values["credential-store"]
336
+ });
337
+ let resumeStorageBlocked = false;
338
+ const warnResumeStorage = () => {
339
+ if (resumeStorageBlocked) return;
340
+ resumeStorageBlocked = true;
341
+ terminal.warn(`Resume details can't be saved
342
+ ${c.muted("If setup is interrupted, choose the same workspace and site on rerun.")}`);
343
+ };
344
+ currentStep = "Reading saved installation details";
345
+ const saved = !values["workspace-name"] && !values["site-name"] && !(values.workspace && values.site) ? await savedInstallation(project, config.apiUrl, warnResumeStorage) : void 0;
346
+ if (saved && saved.apiBaseUrl !== config.apiUrl && !values.force)
347
+ throw new Error(
348
+ "Saved installation belongs to a different API. Review the configuration and use --force to replace it."
349
+ );
350
+ let workspaceChoice = values.workspace ? uuid(values.workspace) : saved?.workspaceId;
351
+ const siteChoice = values.site ? uuid(values.site) : saved?.siteId;
352
+ const resumed = Boolean(saved && !values.workspace && !values.site);
353
+ if (siteChoice && !workspaceChoice) {
354
+ currentStep = "Resolving the existing site";
355
+ const origin = values.origin ? normalizeOrigin(values.origin, newSiteEnvironment === "development" ? "development" : "production") : void 0;
356
+ workspaceChoice = (await resolveInstallation({ siteId: siteChoice, apiBaseUrl: config.apiUrl }, origin)).workspaceId;
357
+ }
358
+ let credentials = resolveCredentials(config), client = new UserReasonClient(config, credentials), connection;
359
+ const open = async (url) => !values["no-open"] && await openBrowser(url);
360
+ const approvalWait = async (label, url, note) => {
361
+ const opened = await open(url);
362
+ if (opened && values.debug) terminal.info(url);
363
+ const text = opened ? label : "Open this link to approve";
364
+ return terminal.waiting(note ? `${text}${dot}${note}` : text, opened ? {} : { link: url });
365
+ };
366
+ const deviceSignIn = async () => {
367
+ let waiting2;
368
+ await login(config, credentials, async (device) => {
369
+ waiting2 = await approvalWait("Sign in in the browser", device.verificationUriComplete, `${c.muted("code")} ${c.code(device.userCode)}`);
370
+ });
371
+ waiting2?.stop("Signed in");
372
+ };
373
+ let waiting;
374
+ let unrestrictedOrigins = false;
375
+ currentStep = "Connecting browser sign-in and approval";
376
+ try {
377
+ connection = await connectInstallation(
378
+ config,
379
+ {
380
+ projectName: project.name.slice(0, 240),
381
+ workspaceId: workspaceChoice,
382
+ workspaceName: values["workspace-name"],
383
+ siteId: siteChoice
384
+ },
385
+ async (url, code) => {
386
+ const note = code ? `${c.muted("code")} ${c.code(code)}` : void 0;
387
+ const opened = await open(url);
388
+ if (opened && values.debug) terminal.info(url);
389
+ const label = opened ? "Approve in the browser" : "Open this link to approve";
390
+ waiting = terminal.waiting(note ? `${label}${dot}${note}` : label, opened ? {} : { link: url });
391
+ const progressNote = note ? `${dot}${note}` : "";
392
+ approvalProgress = (value) => waiting?.update(`${value.phase === "waiting" ? label : approvalStatus(value)}${progressNote}`);
393
+ },
394
+ {
395
+ onCapabilities: (value) => {
396
+ unrestrictedOrigins = value.unrestrictedOrigins;
397
+ },
398
+ onProgress: (value) => approvalProgress?.(value)
399
+ }
400
+ );
401
+ waiting?.stop();
402
+ credentials = memoryCredentials(connection.session);
403
+ client = new UserReasonClient(config, credentials, {
404
+ lock: (operation) => operation()
405
+ });
406
+ } catch (error) {
407
+ waiting?.stop();
408
+ if (!(error instanceof BrowserConnectionUnavailable)) throw error;
409
+ terminal.info(`This API uses the earlier sign-in flow${dot}${c.muted("installation approval opens after sign-in")}`);
410
+ const compatibility = await compatibilityCredentials(
411
+ credentials,
412
+ Boolean(values["session-only"]),
413
+ async (error2) => {
414
+ terminal.warn(error2.message);
415
+ if (!terminal.interactive)
416
+ throw new Error(
417
+ "The selected credential store is unavailable. Rerun with --session-only to keep login only in memory."
418
+ );
419
+ return terminal.confirm("Keep this session in memory only? No credentials will be saved.");
420
+ }
421
+ );
422
+ credentials = compatibility.credentials;
423
+ client = new UserReasonClient(
424
+ config,
425
+ credentials,
426
+ compatibility.sessionOnly ? { lock: (operation) => operation() } : {}
427
+ );
428
+ if (compatibility.sessionOnly) terminal.info("Login is kept only in memory for this run.");
429
+ if (!compatibility.session) {
430
+ await deviceSignIn();
431
+ if (!compatibility.sessionOnly) await saveConfig(config, developmentApi);
432
+ }
433
+ }
434
+ const approve = async (workspaceId, creation = true) => {
435
+ const request = await client.requestGrant({
436
+ workspaceId,
437
+ scopes: [
438
+ ...!workspaceId ? ["workspace:create"] : [],
439
+ "workspace:read",
440
+ "site:read",
441
+ ...creation ? ["site:create"] : []
442
+ ]
443
+ });
444
+ if (request.status === "approved") return request;
445
+ const pending = request.approval_url ? await approvalWait("Approve installation in the browser", request.approval_url) : terminal.waiting("Waiting for installation approval");
446
+ try {
447
+ const granted = await client.waitForGrant(request.id);
448
+ pending.stop("Installation approved");
449
+ return granted;
450
+ } catch (error) {
451
+ pending.stop();
452
+ throw error;
453
+ }
454
+ };
455
+ const authorized = async (operation, workspaceId) => {
456
+ try {
457
+ return await operation();
458
+ } catch (error) {
459
+ if (!(error instanceof ApiError) || error.status !== 403 || !["authorization_required", "grant_required"].includes(error.code) || connection)
460
+ throw error;
461
+ await approve(workspaceId, !siteChoice);
462
+ return operation();
463
+ }
464
+ };
465
+ currentStep = "Loading the approved account";
466
+ let account;
467
+ try {
468
+ account = await client.account();
469
+ } catch (error) {
470
+ if (!(error instanceof AuthenticationError) || error.code !== "invalid_grant") throw error;
471
+ await deviceSignIn();
472
+ account = await client.account();
473
+ }
474
+ const who = connection ? clean(connection.session.email) : void 0;
475
+ let selectedWorkspace;
476
+ const approvedWorkspace = connection?.workspaceId ?? workspaceChoice;
477
+ if (approvedWorkspace) {
478
+ selectedWorkspace = account.workspaces.find((item) => item.id === approvedWorkspace);
479
+ if (!selectedWorkspace && !connection) {
480
+ await approve(approvedWorkspace, !siteChoice);
481
+ account = await client.account();
482
+ selectedWorkspace = account.workspaces.find((item) => item.id === approvedWorkspace);
483
+ }
484
+ if (!selectedWorkspace) throw new Error("The selected workspace is not available to this installation");
485
+ if (who) terminal.success(`Signed in as ${who} ${c.muted(`(workspace ${clean(selectedWorkspace.name)})`)}`);
486
+ } else {
487
+ if (who) terminal.success(`Signed in as ${who}`);
488
+ if (!connection?.workspaceName && !values["workspace-name"] && account.workspaces.length) {
489
+ const selected = await terminal.select(
490
+ "Workspace",
491
+ [
492
+ ...account.workspaces.map((item) => ({
493
+ value: item.id,
494
+ label: item.name
495
+ })),
496
+ { value: "new", label: "New workspace" }
497
+ ],
498
+ account.workspaces[0]?.id
499
+ );
500
+ selectedWorkspace = account.workspaces.find((item) => item.id === selected);
501
+ }
502
+ }
503
+ if (!who && selectedWorkspace) terminal.success(`Workspace: ${clean(selectedWorkspace.name)}`);
504
+ let selectedSite;
505
+ currentStep = "Selecting the workspace and site";
506
+ const siteDetail = (item) => `${item.environment}, ${item.allowed_origins.map(clean).join(", ") || "any domain"}`;
507
+ if (selectedWorkspace) {
508
+ const sites = await authorized(() => client.sites(selectedWorkspace.id), selectedWorkspace.id);
509
+ if (siteChoice) {
510
+ selectedSite = sites.find((item) => item.id === siteChoice);
511
+ if (!selectedSite) throw new Error("The selected site is not available in this workspace");
512
+ terminal.info(resumed ? `Resuming an interrupted setup${dot}${c.muted(`${clean(selectedWorkspace.name)} ${S.arrow} ${clean(selectedSite.name)}`)}` : `Site ${c.code(clean(selectedSite.name))} ${c.muted(`(${siteDetail(selectedSite)})`)}`);
513
+ } else if (!values["site-name"] && sites.length) {
514
+ const width = Math.max(...sites.map((item) => clean(item.name).length));
515
+ const selected = await terminal.select(
516
+ "Site",
517
+ [
518
+ ...sites.map((item) => ({
519
+ value: item.id,
520
+ label: `${clean(item.name).padEnd(width)} ${c.muted(siteDetail(item))}`
521
+ })),
522
+ { value: "new", label: "New site" }
523
+ ],
524
+ sites[0]?.id
525
+ );
526
+ selectedSite = sites.find((item) => item.id === selected);
527
+ }
528
+ }
529
+ const environment = selectedSite?.environment ?? newSiteEnvironment;
530
+ if (environment !== "production" && environment !== "development")
531
+ throw new Error("Invalid site environment");
532
+ const validateOrigin = (value) => {
533
+ try {
534
+ const normalized = normalizeOrigin(value, environment);
535
+ if (selectedSite?.allowed_origins.length && !selectedSite.allowed_origins.includes(normalized))
536
+ return `This site allows ${selectedSite.allowed_origins.join(", ")}. Choose one or add your origin through the dashboard.`;
537
+ } catch (error) {
538
+ return error instanceof Error ? error.message : "Enter a valid app origin";
539
+ }
540
+ };
541
+ const rawOrigin = values.origin ?? selectedSite?.allowed_origins[0];
542
+ const invalidOrigin = rawOrigin ? validateOrigin(rawOrigin) : void 0;
543
+ if (invalidOrigin) throw new Error(invalidOrigin);
544
+ const hostOrigin = rawOrigin ? normalizeOrigin(rawOrigin, environment) : void 0;
545
+ if (!selectedSite && !hostOrigin && !unrestrictedOrigins)
546
+ throw new Error("This API has not enabled domain-free installation yet. Deploy the updated UserReason Worker, or use --origin to configure an explicit restriction. No workspace or site was created.");
547
+ const siteName = selectedSite?.name ?? values["site-name"] ?? await terminal.text("Site name", project.name.slice(0, 240), nameError);
548
+ const workspaceName = selectedWorkspace?.name ?? connection?.workspaceName ?? values["workspace-name"] ?? await terminal.text("New workspace name", void 0, nameError);
549
+ if (nameError(siteName) || nameError(workspaceName))
550
+ throw new Error("Workspace and site names must contain 1\u2013240 characters");
551
+ const workspaceIds = selectedWorkspace ? void 0 : { requestId: randomUUID(), workspaceId: randomUUID() };
552
+ const siteIds = selectedSite ? void 0 : { requestId: randomUUID(), siteId: randomUUID() };
553
+ const installation = {
554
+ workspaceId: selectedWorkspace?.id ?? workspaceIds.workspaceId,
555
+ siteId: selectedSite?.id ?? siteIds.siteId,
556
+ apiBaseUrl: config.apiUrl
557
+ };
558
+ currentStep = "Preparing the application integration";
559
+ const reviewed = values["skip-integrate"] ? void 0 : await planIntegration(project, installation);
560
+ const additions = reviewed ? integrationAdditions(reviewed) : void 0;
561
+ const legacyNext = project.framework === "next" && !project.nextInstrumentation && plan?.status !== "integrated";
562
+ const noteProbe = values["skip-agent-note"] ? void 0 : await planAgentNote(project, { beforeInstall: true }).catch(() => void 0);
563
+ const skillProbe = values["skip-agent-skill"] ? void 0 : await planAgentSkill(project).catch(() => void 0);
564
+ const inPlace = [
565
+ noteProbe && !("path" in noteProbe) && noteProbe.status === "already_present" ? `${noteProbe.file ?? "AGENTS.md"} note` : "",
566
+ skillProbe && !("files" in skillProbe) && skillProbe.status === "already_present" ? "Agent skill" : ""
567
+ ].filter(Boolean);
568
+ if (inPlace.length) terminal.info(`Already in place${dot}${c.muted(inPlace.join(", "))}`);
569
+ const installRow = packages.length ? `${c.muted("install")} ${c.code(packages.join(" "))}` : c.muted(`nothing to install${dot}@userreason/js and @userreason/cli are current`);
570
+ if (reviewed?.status === "ready" && additions) {
571
+ if (values.diff) terminal.note(integrationPreview(reviewed), `Diff${dot}${additions.path}`);
572
+ else terminal.block("Changes", [
573
+ additions.created ? `${c.code(additions.path)} ${c.muted("(new file)")}` : `${c.code(additions.path)}${c.muted(`:${additions.line}`)}`,
574
+ ...additions.added.map((line) => `${c.good("+")} ${clean(line)}`),
575
+ ...legacyNext ? [`${c.code(`userreason.client.${project.typescript ? "tsx" : "jsx"}`)} ${c.muted("(new file, generated)")}`] : [],
576
+ installRow
577
+ ]);
578
+ } else terminal.block("Changes", [reviewed?.status === "integrated" && !packages.length ? c.muted(`nothing to edit${dot}@userreason/js and @userreason/cli are current`) : installRow]);
579
+ const extras = [];
580
+ if (reviewed?.status === "ready" && additions) extras.push({ value: "edit", label: `${additions.created ? "Create" : "Edit"} ${additions.path}`, hint: "add UserReason.init", initial: true });
581
+ if (noteProbe && "path" in noteProbe) extras.push({ value: "note", label: `${noteProbe.file} ${noteProbe.replacement ? "note update" : "note"}`, hint: "user-feedback guidance", initial: true });
582
+ if (skillProbe && "files" in skillProbe) extras.push({ value: "skill", label: skillProbe.update ? "Agent skill update" : "Agent skill", hint: ".agents/skills/userreason", initial: true });
583
+ if (!values["skip-cli-access"] && !values["session-only"]) extras.push({ value: "access", label: "Ongoing CLI access", hint: "second approval (90 days)", initial: true });
584
+ if (!values["skip-global-cli"] && !values["skip-install"]) extras.push({ value: "global", label: "Global userreason command", hint: `optional${dot}npm i -g, replaces the current global CLI`, initial: Boolean(values["global-cli"]) });
585
+ const picked = interactive ? new Set(extras.length ? await terminal.multiselect(`Set up ${c.muted(`${dot.trim()} recommended steps are selected`)}`, extras, extras.filter((item) => item.initial).map((item) => item.value)) : []) : void 0;
586
+ const integrate = Boolean(reviewed?.status === "ready" && (picked ? picked.has("edit") : values.integrate || values.yes));
587
+ const choice = (key, flag, skipFlag) => picked ? { requested: picked.has(key), skip: !picked.has(key) } : { requested: Boolean(values[flag]), skip: skipFlag };
588
+ const noteChoice = choice("note", "agent-note", Boolean(values["skip-agent-note"]));
589
+ const skillChoice = choice("skill", "agent-skill", Boolean(values["skip-agent-skill"]));
590
+ const globalChoice = choice("global", "global-cli", Boolean(values["skip-global-cli"] || values["skip-install"]));
591
+ const accessChoice = choice("access", "cli-access", Boolean(values["skip-cli-access"] || values["session-only"]));
592
+ terminal.cancelState = "some steps are done; rerun the wizard to continue";
593
+ if (!selectedWorkspace) {
594
+ currentStep = "Creating the workspace";
595
+ try {
596
+ selectedWorkspace = await client.createWorkspace(workspaceName, workspaceIds);
597
+ } catch (error) {
598
+ if (!(error instanceof ApiError) || error.status !== 403 || connection || !["authorization_required", "grant_required"].includes(error.code))
599
+ throw error;
600
+ const granted = await approve();
601
+ account = await client.account();
602
+ const grant = account.grants.find(
603
+ (item) => item && typeof item === "object" && "id" in item && item.id === granted.grant_id
604
+ );
605
+ if (grant?.workspace_id)
606
+ throw new Error(
607
+ "An existing workspace was selected in the browser. Rerun the wizard to choose its site."
608
+ );
609
+ selectedWorkspace = await client.createWorkspace(workspaceName, workspaceIds);
610
+ }
611
+ terminal.success(`Created workspace ${c.code(clean(selectedWorkspace.name))}`);
612
+ }
613
+ if (!selectedSite) {
614
+ currentStep = "Creating the site";
615
+ selectedSite = await authorized(
616
+ () => client.createSite({
617
+ workspaceId: selectedWorkspace.id,
618
+ name: siteName,
619
+ environment,
620
+ origins: hostOrigin ? [hostOrigin] : [],
621
+ requestId: siteIds.requestId,
622
+ siteId: siteIds.siteId
623
+ }),
624
+ selectedWorkspace.id
625
+ );
626
+ terminal.success(`Created site ${c.code(clean(selectedSite.name))}`);
627
+ }
628
+ currentStep = "Saving optional installation resume details";
629
+ if (!await saveInstallation(project.directory, installation)) warnResumeStorage();
630
+ currentStep = "Preparing initialization files";
631
+ const generated = await writeIntegration(project, installation, values.force, legacyNext);
632
+ const existingPlan = plan?.status === "integrated" ? reviewed : plan;
633
+ if (existingPlan?.status === "manual") terminal.warn(existingPlan.reason);
634
+ let sdkInstalled = sdkCurrent;
635
+ let cliInstalled = cliCurrent;
636
+ const shouldInstall = !values["skip-install"] && Boolean(values.install || !sdkInstalled);
637
+ const shouldInstallCli = !values["skip-install"] && Boolean(values.install || !cliInstalled);
638
+ let installFailed = false;
639
+ if (shouldInstall || shouldInstallCli) {
640
+ currentStep = "Installing the SDK and CLI";
641
+ const warnings = [];
642
+ const failures = [];
643
+ const started = Date.now();
644
+ const installed = [];
645
+ await terminal.task("Installing @userreason/js, @userreason/cli", async () => {
646
+ if (shouldInstall) {
647
+ try {
648
+ await installSdk(project, { profile, verbose: values.debug, onWarning: (message) => {
649
+ warnings.push(message);
650
+ } });
651
+ sdkInstalled = true;
652
+ installed.push("@userreason/js");
653
+ } catch (error) {
654
+ if (!(error instanceof SdkInstallError)) throw error;
655
+ failures.push({ error, command: sdkInstallCommand(project), title: "Install SDK manually" });
656
+ return;
657
+ }
658
+ }
659
+ if (shouldInstallCli) {
660
+ try {
661
+ await installCli(project, { profile, verbose: values.debug, onWarning: (message) => {
662
+ warnings.push(message);
663
+ } });
664
+ cliInstalled = true;
665
+ installed.push("@userreason/cli");
666
+ } catch (error) {
667
+ if (!(error instanceof SdkInstallError)) throw error;
668
+ failures.push({ error, command: cliInstallCommand(project, profile), title: "Install CLI manually" });
669
+ }
670
+ }
671
+ }, () => installed.length ? `Installed ${installed.map((name) => c.code(name)).join(", ")} ${c.muted(`(${project.packageManager}${Date.now() - started >= 2e3 ? `, ${Math.round((Date.now() - started) / 1e3)}s` : ""})`)}` : void 0);
672
+ for (const message of warnings) terminal.warn(message);
673
+ for (const failure of failures) {
674
+ installFailed = true;
675
+ process.exitCode = 1;
676
+ terminal.error(failure.error.message);
677
+ terminal.note(`${failure.command}
678
+
679
+ Then rerun the wizard. Your saved workspace and site will be reused.`, failure.title);
680
+ }
681
+ } else if (sdkInstalled) terminal.success(`UserReason packages already installed ${c.muted("(compatible versions)")}`);
682
+ if (integrate && reviewed && !installFailed) {
683
+ currentStep = "Applying the application initialization";
684
+ const concrete = await planIntegration(project, installation);
685
+ if (reviewed.status !== "ready" || concrete.status !== "ready" || concrete.path !== reviewed.path || concrete.before !== reviewed.before || concrete.after !== reviewed.after)
686
+ throw new Error("The application entry changed after the preview. Rerun to review it again.");
687
+ await applyIntegration(concrete);
688
+ if (concrete.before === null) generated.files.push(concrete.path);
689
+ terminal.success(`${concrete.before === null ? "Created" : "Edited"} ${c.code(additions?.path ?? "")}`);
690
+ }
691
+ const integrated = Boolean(integrate && !installFailed || existingPlan?.status === "integrated");
692
+ if (!integrated && !installFailed) terminal.note(generated.instruction, "Application integration");
693
+ currentStep = "Checking site configuration";
694
+ const verification = await terminal.task(
695
+ "Checking site configuration",
696
+ () => verifyConfiguration(installation, hostOrigin ?? "https://installation.userreason.invalid"),
697
+ "Site configuration reachable"
698
+ );
699
+ if (integrated && sdkInstalled && !installFailed) {
700
+ currentStep = "Clearing optional installation resume details";
701
+ if (!await clearInstallation(project.directory, config.apiUrl) && !resumeStorageBlocked)
702
+ terminal.warn("Setup is complete, but optional resume storage could not be cleared. The installed application initialization takes precedence on reruns.");
703
+ }
704
+ const completed = integrated && sdkInstalled && cliInstalled && !installFailed;
705
+ if (picked && !completed && !installFailed && (picked.has("note") || picked.has("skill")))
706
+ terminal.warn("The note and skill wait for an initialized SDK. Rerun with the entry edit selected, or apply the initialization yourself first.");
707
+ currentStep = "Offering the optional user feedback note";
708
+ const agentNote = await offerAgentNote(project, terminal, { installed: completed, ...noteChoice, preview: Boolean(values.diff) });
709
+ currentStep = "Offering the optional feedback agent skill";
710
+ const agentSkill = await offerAgentSkill(project, terminal, { installed: completed, ...skillChoice, preview: Boolean(values.diff) });
711
+ currentStep = "Offering the optional global CLI";
712
+ const globalCli = await offerGlobalCli(project, terminal, { installed: cliInstalled && !installFailed, ...globalChoice, force: values.install, verbose: values.debug, profile, preview: Boolean(values.diff) });
713
+ currentStep = "Offering ongoing CLI access";
714
+ const cliAccess = await offerCliAccess(config, terminal, { installed: cliInstalled && !installFailed, ...accessChoice, noOpen: values["no-open"], development: developmentApi, preview: Boolean(values.diff) });
715
+ const nextStep = installFailed ? `Run ${sdkInstalled ? cliInstallCommand(project, profile) : sdkInstallCommand(project, profile)} in your normal terminal, then rerun the wizard to finish setup.` : integrated ? "Start your app and open it in the browser to connect the SDK." : generated.instruction;
716
+ const siteLine = `${clean(selectedWorkspace.name)} ${S.arrow} ${clean(selectedSite.name)} ${c.muted(`(${siteDetail(selectedSite)})`)}`;
717
+ terminal.block(installFailed ? "Saved" : "Ready", [c.muted(installFailed ? "SDK installation is pending. After installing, rerun the wizard; your site is reused." : "SDK not connected yet. Start your app to connect it.")], { suffix: siteLine, kind: installFailed ? "warn" : "done" });
718
+ const start = devCommand(project);
719
+ const nextRows = installFailed ? [[c.code(sdkInstalled ? cliInstallCommand(project, profile) : sdkInstallCommand(project, profile)), "then rerun the wizard"]] : [
720
+ ...start ? [[c.code(start), "start your app"]] : [],
721
+ ...cliInstalled ? [[c.code(`${cliCommand(project)} prompt list`), "surveys for this site"], [c.code(`${cliCommand(project)} login --open`), `connect a teammate (after ${project.packageManager} install)`]] : []
722
+ ];
723
+ if (nextRows.length) terminal.block("Next steps", terminal.kit.cols(nextRows), { kind: "next", each: true });
724
+ if (values.json)
725
+ process.stdout.write(
726
+ JSON.stringify({
727
+ global_cli: globalCli,
728
+ cli_access: cliAccess,
729
+ workspace: selectedWorkspace,
730
+ site: selectedSite,
731
+ framework: project.framework,
732
+ api_environment: apiEnvironment,
733
+ files: generated.files,
734
+ sdk_installed: sdkInstalled,
735
+ cli_installed: cliInstalled,
736
+ ...installFailed ? { sdk_install_command: sdkInstallCommand(project, profile), cli_install_command: cliInstallCommand(project, profile), installation_pending: true } : {},
737
+ wizard_version: version,
738
+ sdk_required_version: profile.sdkVersion,
739
+ cli_required_version: profile.cliVersion,
740
+ integrated,
741
+ agent_note: agentNote,
742
+ agent_skill: agentSkill,
743
+ verification,
744
+ next_step: nextStep
745
+ }) + "\n"
746
+ );
747
+ }
748
+ var approvalProgress;
749
+ main().catch((error) => {
750
+ const detail = error instanceof Error ? error.message : "Installation failed";
751
+ if (error instanceof Cancelled) {
752
+ if (activeTerminal) activeTerminal.cancelled();
753
+ process.stderr.write(detail + "\n");
754
+ process.exitCode = 130;
755
+ return;
756
+ }
757
+ const message = `${currentStep}: ${detail}`;
758
+ if (process.argv.includes("--json"))
759
+ process.stdout.write(JSON.stringify({ error: "wizard_failed", step: currentStep, message }) + "\n");
760
+ else if (activeTerminal?.interactive)
761
+ activeTerminal.error(clean(detail), [`while ${lowerFirst(currentStep)}`]);
762
+ else process.stderr.write(message + "\n");
763
+ process.exitCode = 1;
764
+ });