@userreason/wizard 0.1.0 → 0.2.1
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 +116 -28
- package/dist/THIRD_PARTY_NOTICES.txt +488 -0
- package/dist/agent-note.js +17 -0
- package/dist/agent-skill.js +16 -0
- package/dist/bin.js +720 -689
- package/dist/chunk-3RDJORRS.js +153 -0
- package/dist/chunk-5SC2K42Q.js +899 -0
- package/dist/chunk-5Z5J6PY5.js +258 -0
- package/dist/chunk-6CJ4ANMN.js +70 -0
- package/dist/chunk-GGARVSPT.js +211893 -0
- package/dist/chunk-HACTNBV3.js +51 -0
- package/dist/chunk-JD5U4U26.js +125 -0
- package/dist/chunk-OEGNGGZ2.js +7403 -0
- package/dist/chunk-SJVLAYGG.js +1477 -0
- package/dist/chunk-ULHKVBPD.js +165 -0
- package/dist/chunk-WL3NOIM6.js +42 -0
- package/dist/chunk-YMSGDC4B.js +280 -0
- package/dist/chunk-ZGZOBW2B.js +115 -0
- package/dist/cli-access.js +10 -0
- package/dist/connection.js +18 -0
- package/dist/global-cli.js +14 -0
- package/dist/integrate.js +21 -0
- package/dist/project.js +35 -101
- package/dist/registry.js +8 -0
- package/dist/skills/userreason/SKILL.md +55 -0
- package/dist/skills/userreason/references/management.md +61 -0
- package/dist/startup.js +11 -0
- package/dist/state.js +18 -0
- package/dist/ui.js +23 -0
- package/package.json +7 -17
package/dist/bin.js
CHANGED
|
@@ -1,733 +1,764 @@
|
|
|
1
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-ULHKVBPD.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-ZGZOBW2B.js";
|
|
41
|
+
import {
|
|
42
|
+
applyIntegration,
|
|
43
|
+
detectInstallation,
|
|
44
|
+
integrationAdditions,
|
|
45
|
+
integrationPreview,
|
|
46
|
+
planIntegration
|
|
47
|
+
} from "./chunk-5SC2K42Q.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";
|
|
2
78
|
|
|
3
79
|
// src/bin.ts
|
|
4
80
|
import { parseArgs } from "node:util";
|
|
5
|
-
import { randomUUID
|
|
6
|
-
|
|
81
|
+
import { randomUUID } from "node:crypto";
|
|
82
|
+
var help = `UserReason installation wizard
|
|
7
83
|
|
|
8
|
-
|
|
9
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
84
|
+
userreason-wizard [--directory PATH] [--origin URL]
|
|
10
85
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const normalized = apiUrl(value);
|
|
37
|
-
if (environment === "production" && !normalized.startsWith("https://"))
|
|
38
|
-
throw new Error("Production sites require HTTPS origins");
|
|
39
|
-
return normalized;
|
|
40
|
-
}
|
|
41
|
-
function workspace(value) {
|
|
42
|
-
const data = object(value, "workspace");
|
|
43
|
-
return { ...data, id: uuid(string(data.id, "workspace ID")), name: string(data.name, "workspace name") };
|
|
44
|
-
}
|
|
45
|
-
function site(value) {
|
|
46
|
-
const data = object(value, "site");
|
|
47
|
-
if (data.environment !== "development" && data.environment !== "production")
|
|
48
|
-
throw new Error("Invalid site environment");
|
|
49
|
-
if (!Array.isArray(data.allowed_origins) || !data.allowed_origins.every((item) => typeof item === "string"))
|
|
50
|
-
throw new Error("Invalid site origins");
|
|
51
|
-
return { ...data, id: uuid(string(data.id ?? data.site_id, "site ID")), workspace_id: uuid(string(data.workspace_id, "workspace ID")), name: string(data.name, "site name") };
|
|
52
|
-
}
|
|
53
|
-
function grantRequest(value) {
|
|
54
|
-
const data = object(value, "grant request");
|
|
55
|
-
if (!["pending", "approved", "rejected", "denied", "expired"].includes(String(data.status)))
|
|
56
|
-
throw new Error("Invalid grant request status");
|
|
57
|
-
if (data.approval_url !== void 0) {
|
|
58
|
-
const url = new URL(string(data.approval_url, "approval URL"));
|
|
59
|
-
if (url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)))
|
|
60
|
-
throw new Error("Invalid grant approval URL");
|
|
61
|
-
}
|
|
62
|
-
return { ...data, id: uuid(string(data.id, "grant request ID")) };
|
|
63
|
-
}
|
|
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
|
|
64
111
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
var
|
|
74
|
-
var
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
try {
|
|
119
|
-
result = await post("authenticate", { grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code: deviceCode, client_id: config.clientId }, fetcher, AbortSignal.timeout(Math.min(2e4, deadline - now())));
|
|
120
|
-
} catch {
|
|
121
|
-
interval = Math.min(interval * 2, 30);
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
if (result.response.ok) {
|
|
125
|
-
const authenticated = session(result.data, config);
|
|
126
|
-
await credentials.save(authenticated);
|
|
127
|
-
return authenticated;
|
|
128
|
-
}
|
|
129
|
-
const code = result.data.error ?? result.data.code;
|
|
130
|
-
if (code === "authorization_pending")
|
|
131
|
-
continue;
|
|
132
|
-
if (code === "slow_down") {
|
|
133
|
-
interval += 5;
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
if (result.response.status >= 500 || result.response.status === 429) {
|
|
137
|
-
interval = Math.min(interval * 2, 30);
|
|
138
|
-
continue;
|
|
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" }
|
|
139
165
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
166
|
+
});
|
|
167
|
+
const version = await wizardVersion();
|
|
168
|
+
if (values.version) {
|
|
169
|
+
process.stdout.write(version + "\n");
|
|
170
|
+
return;
|
|
145
171
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const result = await post("authenticate", { grant_type: "refresh_token", refresh_token: current.refreshToken, client_id: config.clientId }, fetcher);
|
|
150
|
-
if (!result.response.ok) {
|
|
151
|
-
if (result.data.error === "invalid_grant" || result.data.code === "invalid_grant") {
|
|
152
|
-
await credentials.clear();
|
|
153
|
-
throw new AuthenticationError("invalid_grant", "Your session expired or was revoked; run userreason login again");
|
|
154
|
-
}
|
|
155
|
-
throw new AuthenticationError("refresh_failed", `Unable to refresh your session (${result.response.status}); the stored session was retained`);
|
|
172
|
+
if (values.help) {
|
|
173
|
+
process.stdout.write(help);
|
|
174
|
+
return;
|
|
156
175
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
throw new Error("
|
|
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;
|
|
178
215
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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");
|
|
194
258
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
reader.releaseLock();
|
|
199
|
-
}
|
|
200
|
-
const buffer = new Uint8Array(bytes);
|
|
201
|
-
let offset = 0;
|
|
202
|
-
for (const chunk of chunks) {
|
|
203
|
-
buffer.set(chunk, offset);
|
|
204
|
-
offset += chunk.byteLength;
|
|
205
|
-
}
|
|
206
|
-
let config;
|
|
207
|
-
try {
|
|
208
|
-
config = object(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(buffer)), "authentication configuration");
|
|
209
|
-
} catch {
|
|
210
|
-
throw new Error("Invalid UserReason authentication configuration response");
|
|
211
|
-
}
|
|
212
|
-
if (Object.keys(config).some((key) => !["client_id", "authkit_domain", "environment"].includes(key)) || typeof config.client_id !== "string" || !/^client_[A-Za-z0-9]{1,230}$/.test(config.client_id) || typeof config.authkit_domain !== "string" || config.authkit_domain.length > 2048)
|
|
213
|
-
throw new Error("Invalid UserReason authentication configuration response");
|
|
214
|
-
const domain = apiUrl(config.authkit_domain);
|
|
215
|
-
if (!domain.startsWith("https://") || domain !== config.authkit_domain)
|
|
216
|
-
throw new Error("Invalid WorkOS AuthKit domain in authentication configuration");
|
|
217
|
-
if (config.environment !== "local" && config.environment !== (development ? "development" : "production"))
|
|
218
|
-
throw new Error("API authentication environment does not match the selected CLI environment; use --dev for development");
|
|
219
|
-
return config.client_id;
|
|
220
|
-
}
|
|
221
|
-
async function resolveConfig(options = {}) {
|
|
222
|
-
const environment = options.development ? "development" : "production";
|
|
223
|
-
let saved = {};
|
|
224
|
-
try {
|
|
225
|
-
const root = object(JSON.parse(await readFile(join(configDirectory(), "config.json"), "utf8")), "configuration");
|
|
226
|
-
saved = object(root[environment] ?? {}, "environment configuration");
|
|
227
|
-
} catch (error) {
|
|
228
|
-
if (error.code !== "ENOENT")
|
|
229
|
-
throw new Error("Unable to read UserReason configuration");
|
|
230
|
-
}
|
|
231
|
-
const url = apiUrl(options.apiUrl ?? process.env.UR_API_URL ?? (typeof saved.apiUrl === "string" ? saved.apiUrl : options.development ? "https://api-dev.userreason.com" : "https://api.userreason.com"));
|
|
232
|
-
const configured = options.clientId ?? (options.development ? process.env.UR_WORKOS_DEV_CLIENT_ID : void 0) ?? process.env.UR_WORKOS_CLIENT_ID ?? (typeof saved.clientId === "string" ? saved.clientId : void 0);
|
|
233
|
-
const clientId = configured ?? await discoverAuthConfig(url, options.development);
|
|
234
|
-
if (!/^client_[A-Za-z0-9]+$/.test(clientId))
|
|
235
|
-
throw new Error("Set UR_WORKOS_CLIENT_ID or pass --client-id with the public WorkOS client ID. Never supply a WorkOS API key.");
|
|
236
|
-
return { apiUrl: url, clientId };
|
|
237
|
-
}
|
|
238
|
-
async function saveConfig(config, development = false) {
|
|
239
|
-
await mkdir(configDirectory(), { recursive: true, mode: 448 });
|
|
240
|
-
let existing = {};
|
|
241
|
-
try {
|
|
242
|
-
existing = object(JSON.parse(await readFile(join(configDirectory(), "config.json"), "utf8")), "configuration");
|
|
243
|
-
} catch (error) {
|
|
244
|
-
if (error.code !== "ENOENT")
|
|
245
|
-
throw new Error("Unable to read UserReason configuration");
|
|
246
|
-
}
|
|
247
|
-
existing[development ? "development" : "production"] = config;
|
|
248
|
-
await writeFile(join(configDirectory(), "config.json"), JSON.stringify(existing, null, 2) + "\n", { mode: 384 });
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// ../ur-cli/dist/lock.js
|
|
252
|
-
async function withSessionLock(config, operation) {
|
|
253
|
-
const directory = join2(configDirectory(), "locks");
|
|
254
|
-
await mkdir2(directory, { recursive: true, mode: 448 });
|
|
255
|
-
const path = join2(directory, createHash("sha256").update(`${config.apiUrl}
|
|
256
|
-
${config.clientId}`).digest("hex") + ".lock");
|
|
257
|
-
const id = randomUUID(), deadline = Date.now() + 35e3;
|
|
258
|
-
while (true) {
|
|
259
|
-
try {
|
|
260
|
-
const file = await open(path, "wx", 384);
|
|
261
|
-
await file.writeFile(JSON.stringify({ id, createdAt: Date.now() }));
|
|
262
|
-
await file.close();
|
|
263
|
-
break;
|
|
264
|
-
} catch (error) {
|
|
265
|
-
if (error.code !== "EEXIST")
|
|
266
|
-
throw new Error("Unable to lock the UserReason credential store");
|
|
267
|
-
try {
|
|
268
|
-
const old = JSON.parse(await readFile2(path, "utf8"));
|
|
269
|
-
if (typeof old.createdAt === "number" && Date.now() - old.createdAt > 12e4) {
|
|
270
|
-
await unlink(path);
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
} catch {
|
|
259
|
+
if (!await hasCompatibleCli(project, profile) || values.install) {
|
|
260
|
+
await installCli(project, options);
|
|
261
|
+
updated.push("@userreason/cli");
|
|
274
262
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
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;
|
|
279
288
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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 });
|
|
288
327
|
}
|
|
328
|
+
return;
|
|
289
329
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
config;
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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)}`);
|
|
326
370
|
});
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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)
|
|
343
399
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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.");
|
|
358
420
|
}
|
|
359
|
-
|
|
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);
|
|
360
432
|
}
|
|
361
|
-
throw new ApiError(401, "authentication_required");
|
|
362
|
-
}
|
|
363
|
-
async account() {
|
|
364
|
-
const data = object(await this.request("/onboarding/v1/account"), "account"), user = object(data.user, "user");
|
|
365
|
-
uuid(string(user.id, "user ID"));
|
|
366
|
-
if (!Array.isArray(data.workspaces) || !Array.isArray(data.grants))
|
|
367
|
-
throw new Error("Invalid account memberships");
|
|
368
|
-
return { ...data, user, workspaces: data.workspaces.map(workspace) };
|
|
369
|
-
}
|
|
370
|
-
async createWorkspace(name, options = {}) {
|
|
371
|
-
if (!name.trim() || name.length > 240)
|
|
372
|
-
throw new Error("Workspace name must contain 1\u2013240 characters");
|
|
373
|
-
const value = object(await this.request("/onboarding/v1/workspaces", { request_id: uuid(options.requestId ?? randomUUID2()), workspace_id: uuid(options.workspaceId ?? randomUUID2()), name: name.trim() }), "workspace creation");
|
|
374
|
-
return workspace(value.workspace ?? value);
|
|
375
|
-
}
|
|
376
|
-
async sites(workspaceId) {
|
|
377
|
-
const data = object(await this.request(`/onboarding/v1/workspaces/${uuid(workspaceId)}/sites`), "sites"), items = data.items ?? data.sites;
|
|
378
|
-
if (!Array.isArray(items))
|
|
379
|
-
throw new Error("Invalid site list");
|
|
380
|
-
return items.map(site);
|
|
381
|
-
}
|
|
382
|
-
async createSite(input) {
|
|
383
|
-
if (!input.name.trim() || input.name.length > 240)
|
|
384
|
-
throw new Error("Site name must contain 1\u2013240 characters");
|
|
385
|
-
const value = object(await this.request("/onboarding/v1/sites", { request_id: uuid(input.requestId ?? randomUUID2()), workspace_id: uuid(input.workspaceId), site_id: uuid(input.siteId ?? randomUUID2()), name: input.name.trim(), environment: input.environment, allowed_origins: input.origins }), "site creation");
|
|
386
|
-
return site(value.site ?? value);
|
|
387
|
-
}
|
|
388
|
-
async requestGrant(input) {
|
|
389
|
-
if (!input.scopes.length || input.scopes.length > 7)
|
|
390
|
-
throw new Error("Request at least one supported permission");
|
|
391
|
-
return grantRequest(await this.request("/onboarding/v1/grants/request", { request_id: uuid(input.requestId ?? randomUUID2()), workspace_id: input.workspaceId ? uuid(input.workspaceId) : null, site_ids: input.siteIds?.map((id) => uuid(id)) ?? null, scopes: [...new Set(input.scopes)] }));
|
|
392
433
|
}
|
|
393
|
-
async
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
async query(input) {
|
|
407
|
-
if (!["sites", "surveys", "campaigns", "events", "responses", "audits"].includes(input.resource))
|
|
408
|
-
throw new Error("Unsupported query resource");
|
|
409
|
-
const limit = input.limit ?? 50;
|
|
410
|
-
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
411
|
-
throw new Error("Query limit must be an integer from 1 to 100");
|
|
412
|
-
return this.request("/onboarding/v1/query", { workspace_id: uuid(input.workspaceId), ...input.siteId ? { site_id: uuid(input.siteId) } : {}, resource: input.resource, limit, ...input.after ? { after: uuid(input.after) } : {} });
|
|
413
|
-
}
|
|
414
|
-
async waitForGrant(id, options = {}) {
|
|
415
|
-
const deadline = Date.now() + (options.timeoutMs ?? 3e5);
|
|
416
|
-
do {
|
|
417
|
-
const request = await this.grantStatus(id);
|
|
418
|
-
options.onStatus?.(request);
|
|
419
|
-
if (request.status === "approved")
|
|
420
|
-
return request;
|
|
421
|
-
if (request.status !== "pending")
|
|
422
|
-
throw new Error(`Access request ${request.status}; request access again if needed`);
|
|
423
|
-
if (request.expires_at && Date.parse(request.expires_at) <= Date.now())
|
|
424
|
-
throw new Error("Access request expired; request access again");
|
|
425
|
-
await new Promise((resolve2) => setTimeout(resolve2, options.intervalMs ?? 2e3));
|
|
426
|
-
} while (Date.now() < deadline);
|
|
427
|
-
throw new Error("Access approval timed out; check userreason grant status --id " + id);
|
|
428
|
-
}
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
// ../ur-cli/dist/credentials.js
|
|
432
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
433
|
-
function keychainCredentials(config) {
|
|
434
|
-
const account = createHash2("sha256").update(`${config.apiUrl}
|
|
435
|
-
${config.clientId}`).digest("hex");
|
|
436
|
-
async function entry() {
|
|
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");
|
|
437
446
|
try {
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
return {
|
|
445
|
-
async load() {
|
|
446
|
-
let serialized;
|
|
447
|
-
try {
|
|
448
|
-
serialized = (await entry()).getPassword();
|
|
449
|
-
} catch (error) {
|
|
450
|
-
if (/No entry|NoEntry|not found|not exist/i.test(String(error.message)))
|
|
451
|
-
return null;
|
|
452
|
-
throw new Error("Unable to read the UserReason OS credential-store entry");
|
|
453
|
-
}
|
|
454
|
-
if (!serialized)
|
|
455
|
-
return null;
|
|
456
|
-
try {
|
|
457
|
-
const session2 = JSON.parse(serialized);
|
|
458
|
-
if (session2.apiUrl !== config.apiUrl || session2.clientId !== config.clientId || typeof session2.accessToken !== "string" || typeof session2.refreshToken !== "string" || typeof session2.expiresAt !== "number")
|
|
459
|
-
throw new Error();
|
|
460
|
-
return session2;
|
|
461
|
-
} catch {
|
|
462
|
-
throw new Error("Invalid stored UserReason session; run userreason logout, then login again");
|
|
463
|
-
}
|
|
464
|
-
},
|
|
465
|
-
async save(session2) {
|
|
466
|
-
try {
|
|
467
|
-
(await entry()).setPassword(JSON.stringify(session2));
|
|
468
|
-
} catch {
|
|
469
|
-
throw new Error("Unable to save the UserReason session in the OS credential store");
|
|
470
|
-
}
|
|
471
|
-
},
|
|
472
|
-
async clear() {
|
|
473
|
-
try {
|
|
474
|
-
(await entry()).deletePassword();
|
|
475
|
-
} catch (error) {
|
|
476
|
-
if (!/No entry|NoEntry|not found|not exist/i.test(String(error.message)))
|
|
477
|
-
throw new Error("Unable to remove the UserReason OS credential-store entry");
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// src/project.ts
|
|
484
|
-
import { lstat, readFile as readFile3, writeFile as writeFile2, access, copyFile } from "node:fs/promises";
|
|
485
|
-
import { join as join3, resolve, dirname } from "node:path";
|
|
486
|
-
import { spawn } from "node:child_process";
|
|
487
|
-
var exists = async (path) => {
|
|
488
|
-
try {
|
|
489
|
-
await access(path);
|
|
490
|
-
return true;
|
|
491
|
-
} catch {
|
|
492
|
-
return false;
|
|
493
|
-
}
|
|
494
|
-
};
|
|
495
|
-
async function inspectProject(directory, framework) {
|
|
496
|
-
directory = resolve(directory);
|
|
497
|
-
const metadata = await lstat(directory);
|
|
498
|
-
if (!metadata.isDirectory()) throw new Error("Project directory must be an existing directory");
|
|
499
|
-
const text = await readFile3(join3(directory, "package.json"), "utf8");
|
|
500
|
-
if (text.length > 1e6) throw new Error("Project package.json is too large");
|
|
501
|
-
const data = JSON.parse(text);
|
|
502
|
-
if (!data || typeof data !== "object" || Array.isArray(data)) throw new Error("Invalid project package.json");
|
|
503
|
-
const dependencies = { ...data.dependencies, ...data.devDependencies };
|
|
504
|
-
const inferred = dependencies.next ? "next" : dependencies.react ? "react" : dependencies.vue ? "vue" : "vanilla";
|
|
505
|
-
const packageManager = typeof data.packageManager === "string" && /^(pnpm|yarn)@/.test(data.packageManager) ? data.packageManager.split("@")[0] : await exists(join3(directory, "pnpm-lock.yaml")) ? "pnpm" : await exists(join3(directory, "yarn.lock")) ? "yarn" : "npm";
|
|
506
|
-
const yarnMajor = packageManager === "yarn" ? typeof data.packageManager === "string" && /^yarn@\d+/.test(data.packageManager) ? Number(data.packageManager.slice(5).split(".")[0]) : await exists(join3(directory, ".yarnrc.yml")) ? 4 : 1 : void 0;
|
|
507
|
-
return { directory, framework: framework ?? inferred, sourceDirectory: await exists(join3(directory, "src")) ? join3(directory, "src") : directory, packageManager, yarnMajor, name: typeof data.name === "string" && data.name ? data.name : "My site" };
|
|
508
|
-
}
|
|
509
|
-
function integrationSource(framework, config) {
|
|
510
|
-
const options = JSON.stringify(config, null, 2);
|
|
511
|
-
const base = `// Generated by the UserReason wizard. Contains public SDK configuration only.
|
|
512
|
-
`;
|
|
513
|
-
if (framework === "next" || framework === "react") return {
|
|
514
|
-
filename: "userreason.generated.tsx",
|
|
515
|
-
content: `${framework === "next" ? "'use client';\n" : ""}${base}import { useEffect, useLayoutEffect } from 'react';
|
|
516
|
-
import UserReason from '@userreason/js';
|
|
517
|
-
|
|
518
|
-
// Initialize before descendant passive effects; no effects run during SSR.
|
|
519
|
-
const useBrowserLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
|
|
520
|
-
|
|
521
|
-
export function UserReasonProvider() {
|
|
522
|
-
useBrowserLayoutEffect(() => {
|
|
523
|
-
UserReason.init(${options});
|
|
524
|
-
return () => UserReason.destroy();
|
|
525
|
-
}, []);
|
|
526
|
-
return null;
|
|
527
|
-
}
|
|
528
|
-
`,
|
|
529
|
-
instruction: framework === "next" ? "Import UserReasonProvider into your root layout and render <UserReasonProvider /> once inside <body>." : "Import UserReasonProvider into your root application and render <UserReasonProvider /> once."
|
|
530
|
-
};
|
|
531
|
-
if (framework === "vue") return { filename: "userreason.generated.ts", content: `${base}import { onBeforeMount, onBeforeUnmount } from 'vue';
|
|
532
|
-
import UserReason from '@userreason/js';
|
|
533
|
-
|
|
534
|
-
export function useUserReason() {
|
|
535
|
-
onBeforeMount(() => UserReason.init(${options}));
|
|
536
|
-
onBeforeUnmount(() => UserReason.destroy());
|
|
537
|
-
}
|
|
538
|
-
`, instruction: "Import useUserReason into your root Vue component and call it once inside setup()." };
|
|
539
|
-
return { filename: "userreason.generated.ts", content: `${base}import UserReason from '@userreason/js';
|
|
540
|
-
|
|
541
|
-
export function initializeUserReason() {
|
|
542
|
-
UserReason.init(${options});
|
|
543
|
-
return () => UserReason.destroy();
|
|
544
|
-
}
|
|
545
|
-
`, instruction: "Import initializeUserReason at your application entry and call it once after the document body is available. Call the returned cleanup function on teardown." };
|
|
546
|
-
}
|
|
547
|
-
async function writeGenerated(path, content, force) {
|
|
548
|
-
try {
|
|
549
|
-
await writeFile2(path, content, { flag: "wx", mode: 420 });
|
|
550
|
-
return;
|
|
551
|
-
} catch (error) {
|
|
552
|
-
if (error.code !== "EEXIST") throw error;
|
|
553
|
-
}
|
|
554
|
-
const metadata = await lstat(path);
|
|
555
|
-
if (!metadata.isFile() || metadata.isSymbolicLink()) throw new Error(`Refusing to overwrite a non-file: ${path}`);
|
|
556
|
-
if (await readFile3(path, "utf8") === content) return;
|
|
557
|
-
if (!force) throw new Error(`Existing file differs: ${path}. Review it and rerun with --force to create a backup and replace it.`);
|
|
558
|
-
await copyFile(path, `${path}.backup-${Date.now()}`);
|
|
559
|
-
await writeFile2(path, content, { mode: 420 });
|
|
560
|
-
}
|
|
561
|
-
async function writeIntegration(project, config, force = false) {
|
|
562
|
-
const helper = integrationSource(project.framework, config), configPath = join3(project.directory, "userreason.config.json"), helperPath = join3(project.sourceDirectory, helper.filename);
|
|
563
|
-
const source = await lstat(dirname(helperPath));
|
|
564
|
-
if (!source.isDirectory() || source.isSymbolicLink()) throw new Error("Source directory must be a real directory inside the project");
|
|
565
|
-
await writeGenerated(configPath, JSON.stringify({ schemaVersion: 1, sdkVersion: ">=0.2.0", ...config }, null, 2) + "\n", force);
|
|
566
|
-
await writeGenerated(helperPath, helper.content, force);
|
|
567
|
-
return { files: [configPath, helperPath], instruction: helper.instruction };
|
|
568
|
-
}
|
|
569
|
-
async function installSdk(project) {
|
|
570
|
-
const args = project.packageManager === "npm" ? ["install", "--ignore-scripts", "@userreason/js@^0.2.0"] : project.packageManager === "pnpm" ? ["add", "--ignore-scripts", "@userreason/js@^0.2.0"] : (project.yarnMajor ?? 1) >= 2 ? ["add", "--mode=skip-builds", "@userreason/js@^0.2.0"] : ["add", "--ignore-scripts", "@userreason/js@^0.2.0"];
|
|
571
|
-
await new Promise((resolve2, reject) => {
|
|
572
|
-
const child = spawn(project.packageManager, args, { cwd: project.directory, stdio: ["ignore", process.stderr, process.stderr], shell: false, env: { ...process.env, YARN_ENABLE_SCRIPTS: "false", npm_config_ignore_scripts: "true" } });
|
|
573
|
-
child.on("error", () => reject(new Error(`Unable to start ${project.packageManager}; install @userreason/js@^0.2.0 manually`)));
|
|
574
|
-
child.on("exit", (code) => code === 0 ? resolve2() : reject(new Error("SDK installation failed. Publish/install the remote-compatible @userreason/js >=0.2.0 package, then rerun the wizard using the existing workspace/site.")));
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
async function verifyConfiguration(config, hostOrigin, fetcher = fetch) {
|
|
578
|
-
const response = await fetcher(`${config.apiBaseUrl}/v1/sdk/${config.workspaceId}/${config.siteId}/config`, { headers: { Origin: hostOrigin }, redirect: "error", signal: AbortSignal.timeout(2e4) });
|
|
579
|
-
if (!response.ok) throw new Error(`Site configuration check failed (${response.status}); verify its allowed origins and Worker deployment`);
|
|
580
|
-
const data = await response.json();
|
|
581
|
-
if (data.workspace_id !== config.workspaceId || data.site_id !== config.siteId || !data.payload || typeof data.payload !== "object") throw new Error("Worker returned a configuration for a different or invalid site");
|
|
582
|
-
return { configured: true, hostConnectionVerified: false };
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// src/bin.ts
|
|
586
|
-
var help = `UserReason installation wizard
|
|
587
|
-
|
|
588
|
-
userreason-wizard [--directory PATH] [--dev] [--origin URL]
|
|
589
|
-
|
|
590
|
-
--workspace UUID / --workspace-name NAME
|
|
591
|
-
--site UUID / --site-name NAME
|
|
592
|
-
--environment development|production
|
|
593
|
-
--framework react|next|vue|vanilla
|
|
594
|
-
--install / --skip-install
|
|
595
|
-
--yes (non-interactive; requires workspace/site selections or names and origin)
|
|
596
|
-
--force (back up and replace generated files)
|
|
597
|
-
--client-id CLIENT_ID, --api-url ORIGIN, --json
|
|
598
|
-
|
|
599
|
-
Requires remote-compatible @userreason/js >=0.2.0. Only public configuration is written to your project.
|
|
600
|
-
`;
|
|
601
|
-
async function main() {
|
|
602
|
-
const parsed = parseArgs({ allowPositionals: false, options: { help: { type: "boolean", short: "h" }, dev: { type: "boolean" }, yes: { type: "boolean" }, install: { type: "boolean" }, "skip-install": { type: "boolean" }, force: { type: "boolean" }, json: { type: "boolean" }, directory: { type: "string" }, origin: { type: "string" }, workspace: { type: "string" }, "workspace-name": { type: "string" }, site: { type: "string" }, "site-name": { type: "string" }, environment: { type: "string" }, framework: { type: "string" }, "client-id": { type: "string" }, "api-url": { type: "string" } } });
|
|
603
|
-
const values = parsed.values;
|
|
604
|
-
if (values.help) {
|
|
605
|
-
process.stdout.write(help);
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
if (values.install && values["skip-install"]) throw new Error("Choose either --install or --skip-install");
|
|
609
|
-
const framework = values.framework;
|
|
610
|
-
if (framework && !["react", "next", "vue", "vanilla"].includes(framework)) throw new Error("Unsupported framework");
|
|
611
|
-
const project = await inspectProject(values.directory ?? process.cwd(), framework);
|
|
612
|
-
const config = await resolveConfig({ development: values.dev, apiUrl: values["api-url"], clientId: values["client-id"] });
|
|
613
|
-
const credentials = keychainCredentials(config), client = new UserReasonClient(config, credentials);
|
|
614
|
-
const interactive = !values.yes && process.stdin.isTTY;
|
|
615
|
-
const terminal = interactive ? createInterface({ input: process.stdin, output: process.stderr }) : void 0;
|
|
616
|
-
const ask = async (question, fallback) => {
|
|
617
|
-
if (!terminal) {
|
|
618
|
-
if (fallback !== void 0) return fallback;
|
|
619
|
-
throw new Error(`Missing selection: ${question}. Supply the corresponding command option for non-interactive use.`);
|
|
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;
|
|
620
453
|
}
|
|
621
|
-
return (await terminal.question(question + (fallback ? ` [${fallback}]` : "") + ": ")).trim() || fallback || "";
|
|
622
|
-
};
|
|
623
|
-
const approve = async (workspaceId) => {
|
|
624
|
-
const request = await client.requestGrant({ workspaceId, scopes: workspaceId ? ["workspace:read", "site:read", "site:create", "site:update"] : ["workspace:create", "workspace:read", "site:read", "site:create", "site:update"] });
|
|
625
|
-
if (request.status === "approved") return request;
|
|
626
|
-
if (request.approval_url) process.stderr.write(`Approve this installation in your browser: ${request.approval_url}
|
|
627
|
-
`);
|
|
628
|
-
return client.waitForGrant(request.id);
|
|
629
454
|
};
|
|
630
455
|
const authorized = async (operation, workspaceId) => {
|
|
631
456
|
try {
|
|
632
457
|
return await operation();
|
|
633
458
|
} catch (error) {
|
|
634
|
-
if (!(error instanceof ApiError) || error.status !== 403 || !["authorization_required", "grant_required"].includes(error.code)
|
|
635
|
-
|
|
459
|
+
if (!(error instanceof ApiError) || error.status !== 403 || !["authorization_required", "grant_required"].includes(error.code) || connection)
|
|
460
|
+
throw error;
|
|
461
|
+
await approve(workspaceId, !siteChoice);
|
|
636
462
|
return operation();
|
|
637
463
|
}
|
|
638
464
|
};
|
|
465
|
+
currentStep = "Loading the approved account";
|
|
466
|
+
let account;
|
|
639
467
|
try {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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);
|
|
648
483
|
}
|
|
649
|
-
|
|
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) => {
|
|
650
533
|
try {
|
|
651
|
-
|
|
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);
|
|
652
597
|
} catch (error) {
|
|
653
|
-
if (!(error instanceof
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
Confirm code: ${device.userCode}
|
|
657
|
-
`);
|
|
658
|
-
});
|
|
598
|
+
if (!(error instanceof ApiError) || error.status !== 403 || connection || !["authorization_required", "grant_required"].includes(error.code))
|
|
599
|
+
throw error;
|
|
600
|
+
const granted = await approve();
|
|
659
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);
|
|
660
610
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
+
}
|
|
669
658
|
}
|
|
670
|
-
if (
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
selectedWorkspace = await client.createWorkspace(name, ids);
|
|
682
|
-
} catch (error) {
|
|
683
|
-
if (!(error instanceof ApiError) || error.status !== 403 || !["authorization_required", "grant_required"].includes(error.code)) throw error;
|
|
684
|
-
process.stderr.write("Approve a new workspace or select an existing workspace in the browser.\n");
|
|
685
|
-
const approved = await approve();
|
|
686
|
-
account = await client.account();
|
|
687
|
-
const approvedGrant = account.grants.find((value) => value && typeof value === "object" && "id" in value && value.id === approved.grant_id);
|
|
688
|
-
if (approvedGrant?.workspace_id) {
|
|
689
|
-
selectedWorkspace = account.workspaces.find((item) => item.id === approvedGrant.workspace_id);
|
|
690
|
-
if (!selectedWorkspace) throw new Error("The approved workspace also needs workspace:read permission for installation");
|
|
691
|
-
process.stderr.write(`Continuing in the workspace selected during approval: ${selectedWorkspace.name}
|
|
692
|
-
`);
|
|
693
|
-
} else selectedWorkspace = await client.createWorkspace(name, ids);
|
|
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
|
+
}
|
|
694
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);
|
|
695
680
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
}
|
|
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
|
+
);
|
|
728
747
|
}
|
|
748
|
+
var approvalProgress;
|
|
729
749
|
main().catch((error) => {
|
|
730
|
-
const
|
|
731
|
-
|
|
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");
|
|
732
763
|
process.exitCode = 1;
|
|
733
764
|
});
|