@userreason/wizard 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -0
- package/dist/agent-note.js +6 -6
- package/dist/agent-skill.js +5 -5
- package/dist/analytics.js +9 -0
- package/dist/bin.js +201 -34
- package/dist/{chunk-ZGZOBW2B.js → chunk-2UEUVLCB.js} +3 -3
- package/dist/{chunk-5SC2K42Q.js → chunk-5IO5LY22.js} +2 -2
- package/dist/{chunk-GGARVSPT.js → chunk-77SK7TKZ.js} +8894 -5663
- package/dist/{chunk-YMSGDC4B.js → chunk-CADA5WZ3.js} +28 -6
- package/dist/{chunk-HACTNBV3.js → chunk-F4FKAUQN.js} +3 -3
- package/dist/{chunk-SJVLAYGG.js → chunk-GOYBYAUH.js} +8 -1
- package/dist/chunk-HIF6NUNK.js +33 -0
- package/dist/{chunk-6CJ4ANMN.js → chunk-MFGJ2KPT.js} +1 -1
- package/dist/{chunk-OEGNGGZ2.js → chunk-NMHTFC5Q.js} +10 -3
- package/dist/{chunk-5Z5J6PY5.js → chunk-NZ65XDNL.js} +4 -4
- package/dist/{chunk-3RDJORRS.js → chunk-P6DOSGRZ.js} +1 -1
- package/dist/chunk-SFODN2YB.js +196 -0
- package/dist/chunk-UN24JFV2.js +44 -0
- package/dist/chunk-VXWLSQYK.js +130 -0
- package/dist/chunk-YYFC7TM2.js +286 -0
- package/dist/{chunk-ULHKVBPD.js → chunk-Z6ZY7X2K.js} +5 -5
- package/dist/{chunk-JD5U4U26.js → chunk-ZHHIHHLU.js} +4 -4
- package/dist/cli-access.js +3 -3
- package/dist/connection.js +2 -2
- package/dist/first-survey.js +11 -0
- package/dist/footer.js +16 -0
- package/dist/global-cli.js +5 -5
- package/dist/integrate.js +4 -4
- package/dist/project.js +7 -3
- package/dist/registry.js +5 -3
- package/dist/setup-flow.js +24 -0
- package/dist/setup.js +32 -0
- package/dist/skills/userreason/SKILL.md +43 -0
- package/dist/skills/userreason/references/management.md +6 -0
- package/dist/startup.js +2 -2
- package/dist/state.js +5 -5
- package/dist/ui.js +2 -2
- package/package.json +1 -2
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
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);
|
|
2
2
|
import {
|
|
3
3
|
configurePackageRegistry
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-NMHTFC5Q.js";
|
|
5
5
|
import {
|
|
6
6
|
packageProfile
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-77SK7TKZ.js";
|
|
8
8
|
|
|
9
9
|
// src/project.ts
|
|
10
10
|
import { lstat, readFile, writeFile, access, mkdtemp, realpath, rm } from "node:fs/promises";
|
|
@@ -140,7 +140,7 @@ async function hasCompatibleSdk(project, profile = packageProfile()) {
|
|
|
140
140
|
try {
|
|
141
141
|
const manifest = JSON.parse(await readFile(join(project.directory, "package.json"), "utf8"));
|
|
142
142
|
const version = await installedVersion(project, "@userreason/js");
|
|
143
|
-
return Boolean(manifest.dependencies?.["@userreason/js"] && (profile.kind === "local-development" ? version === profile.sdkVersion :
|
|
143
|
+
return Boolean(manifest.dependencies?.["@userreason/js"] && (profile.kind === "local-development" ? version === profile.sdkVersion : compatibleProductionVersion(version, profile.sdkVersion)));
|
|
144
144
|
} catch {
|
|
145
145
|
return false;
|
|
146
146
|
}
|
|
@@ -157,8 +157,13 @@ async function hasCompatibleCli(project, profile = packageProfile()) {
|
|
|
157
157
|
return false;
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
|
+
function compatibleProductionVersion(version, requested) {
|
|
161
|
+
const installed = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
|
|
162
|
+
const target = /^(\d+)\.(\d+)\.(\d+)$/.exec(requested.replace(/^[~^]/, ""));
|
|
163
|
+
return Boolean(installed && target && installed[1] === target[1] && installed[2] === target[2] && Number(installed[3]) >= Number(target[3]));
|
|
164
|
+
}
|
|
160
165
|
function compatibleCliVersion(version, profile) {
|
|
161
|
-
return profile.kind === "local-development" ? version === profile.cliVersion :
|
|
166
|
+
return profile.kind === "local-development" ? version === profile.cliVersion : compatibleProductionVersion(version, profile.cliVersion);
|
|
162
167
|
}
|
|
163
168
|
function cliInstallCommand(project, profile = packageProfile()) {
|
|
164
169
|
return sdkInstallCommand(project, profile).replace("@userreason/js@" + profile.sdkVersion, "--save-dev @userreason/cli@" + profile.cliVersion).replace("yarn add --mode=skip-build --save-dev", "yarn add --mode=skip-build --dev").replace("yarn add --ignore-scripts --save-dev", "yarn add --ignore-scripts --dev");
|
|
@@ -177,10 +182,11 @@ async function installDependency(project, dependency, options) {
|
|
|
177
182
|
const spec = dependency === "sdk" ? `@userreason/js@${profile.sdkVersion}` : `@userreason/cli@${profile.cliVersion}`;
|
|
178
183
|
const args = project.packageManager === "npm" ? ["install", "--ignore-scripts", spec] : project.packageManager === "pnpm" ? ["add", "--ignore-scripts", spec] : (project.yarnMajor ?? 1) >= 2 ? ["add", "--mode=skip-build", spec] : ["add", "--ignore-scripts", spec];
|
|
179
184
|
if (dependency === "cli") args.push(project.packageManager === "yarn" ? "--dev" : "--save-dev");
|
|
180
|
-
if (profile.kind === "local-development") args.push(project.packageManager === "yarn" ? "--exact" : "--save-exact");
|
|
185
|
+
if (profile.kind === "local-development" || options.exact) args.push(project.packageManager === "yarn" ? "--exact" : "--save-exact");
|
|
181
186
|
let cache;
|
|
182
187
|
try {
|
|
183
|
-
|
|
188
|
+
options.signal?.throwIfAborted();
|
|
189
|
+
if (options.configureRegistry !== false) await configurePackageRegistry(project, profile);
|
|
184
190
|
if (project.packageManager === "npm") {
|
|
185
191
|
cache = await mkdtemp(join(await realpath(project.directory), ".userreason-npm-"));
|
|
186
192
|
args.push("--cache", cache);
|
|
@@ -197,6 +203,14 @@ async function installDependency(project, dependency, options) {
|
|
|
197
203
|
...cache ? { npm_config_cache: cache, NPM_CONFIG_CACHE: cache } : {}
|
|
198
204
|
}
|
|
199
205
|
});
|
|
206
|
+
let killTimer;
|
|
207
|
+
const abort = () => {
|
|
208
|
+
child.kill("SIGTERM");
|
|
209
|
+
killTimer = setTimeout(() => child.kill("SIGKILL"), 2e3);
|
|
210
|
+
killTimer.unref();
|
|
211
|
+
};
|
|
212
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
213
|
+
if (options.signal?.aborted) abort();
|
|
200
214
|
let logs = "";
|
|
201
215
|
const capture = (chunk) => {
|
|
202
216
|
const text = chunk.toString();
|
|
@@ -207,6 +221,12 @@ async function installDependency(project, dependency, options) {
|
|
|
207
221
|
child.stderr?.on("data", capture);
|
|
208
222
|
child.on("error", () => reject(new SdkInstallError(`Unable to start ${project.packageManager}.`)));
|
|
209
223
|
child.on("close", (code) => {
|
|
224
|
+
options.signal?.removeEventListener("abort", abort);
|
|
225
|
+
if (killTimer) clearTimeout(killTimer);
|
|
226
|
+
if (options.signal?.aborted) {
|
|
227
|
+
reject(options.signal.reason);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
210
230
|
if (code === 0) {
|
|
211
231
|
if (/\b[1-9]\d*\s+vulnerabilit(?:y|ies)\b|\bWARN\b|\bwarning\b/i.test(logs))
|
|
212
232
|
options.onWarning?.(
|
|
@@ -266,10 +286,12 @@ export {
|
|
|
266
286
|
initializationSource,
|
|
267
287
|
integrationSource,
|
|
268
288
|
writeIntegration,
|
|
289
|
+
installedVersion,
|
|
269
290
|
cliCommand,
|
|
270
291
|
hasCompatibleSdk,
|
|
271
292
|
sdkInstallCommand,
|
|
272
293
|
hasCompatibleCli,
|
|
294
|
+
compatibleProductionVersion,
|
|
273
295
|
compatibleCliVersion,
|
|
274
296
|
cliInstallCommand,
|
|
275
297
|
SdkInstallError,
|
|
@@ -2,14 +2,14 @@ import { createRequire as createWizardRequire } from 'node:module'; import { fil
|
|
|
2
2
|
import {
|
|
3
3
|
Cancelled,
|
|
4
4
|
openBrowser
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-GOYBYAUH.js";
|
|
6
6
|
import {
|
|
7
7
|
authorizeCli,
|
|
8
8
|
grantSummary,
|
|
9
9
|
printable,
|
|
10
10
|
resolveCredentials,
|
|
11
11
|
saveConfig
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-77SK7TKZ.js";
|
|
13
13
|
|
|
14
14
|
// src/cli-access.ts
|
|
15
15
|
async function offerCliAccess(config, terminal, options) {
|
|
@@ -39,7 +39,7 @@ async function offerCliAccess(config, terminal, options) {
|
|
|
39
39
|
return { status: "approved", grant };
|
|
40
40
|
} catch (error) {
|
|
41
41
|
waiting?.stop();
|
|
42
|
-
if (error instanceof Cancelled) return { status: "declined" };
|
|
42
|
+
if (error instanceof Cancelled) return { status: "declined", reason: "user_cancelled" };
|
|
43
43
|
if (error instanceof Error) terminal.warn(printable(error.message));
|
|
44
44
|
terminal.warn("The application is installed, but ongoing CLI access was not connected. Run the project CLI with login --open to finish.");
|
|
45
45
|
return { status: "failed" };
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
plainLine,
|
|
8
8
|
stripAnsi,
|
|
9
9
|
textWidth
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-77SK7TKZ.js";
|
|
11
11
|
import {
|
|
12
12
|
__commonJS,
|
|
13
13
|
__toESM
|
|
@@ -1235,6 +1235,7 @@ function approvalProgressMessage(progress) {
|
|
|
1235
1235
|
var Cancelled = class extends Error {
|
|
1236
1236
|
constructor() {
|
|
1237
1237
|
super("Installation cancelled.");
|
|
1238
|
+
this.name = "Cancelled";
|
|
1238
1239
|
}
|
|
1239
1240
|
};
|
|
1240
1241
|
var clean = (value) => value.replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(/[\x00-\x09\x0b-\x1f\x7f-\x9f]/g, "");
|
|
@@ -1279,6 +1280,7 @@ var Terminal = class {
|
|
|
1279
1280
|
constructor(interactive, json, options = {}) {
|
|
1280
1281
|
this.interactive = interactive;
|
|
1281
1282
|
this.json = json;
|
|
1283
|
+
this.onCancel = options.onCancel;
|
|
1282
1284
|
this.kit = createUiKit({ level: detectColorLevel({ stream: process.stdout, off: options.color === false }) });
|
|
1283
1285
|
}
|
|
1284
1286
|
interactive;
|
|
@@ -1288,6 +1290,7 @@ var Terminal = class {
|
|
|
1288
1290
|
cancelState = "nothing was written";
|
|
1289
1291
|
/** True after output that leaves no guide line behind, so the next event adds one. */
|
|
1290
1292
|
gap = false;
|
|
1293
|
+
onCancel;
|
|
1291
1294
|
header(options) {
|
|
1292
1295
|
const lines = this.kit.header({ ...options, trademark: this.interactive });
|
|
1293
1296
|
if (this.interactive) for (const line of lines) process.stdout.write(cleanStyled(line) + "\n");
|
|
@@ -1373,6 +1376,10 @@ var Terminal = class {
|
|
|
1373
1376
|
cancelled(afterSpinner = false) {
|
|
1374
1377
|
if (this.interactive && afterSpinner) process.stdout.write("\n");
|
|
1375
1378
|
this.error(`Cancelled ${this.kit.c.muted(`${this.kit.S.dot} ${this.cancelState}`)}`);
|
|
1379
|
+
if (this.onCancel) {
|
|
1380
|
+
this.onCancel();
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1376
1383
|
process.exit(130);
|
|
1377
1384
|
}
|
|
1378
1385
|
async text(message, initialValue, validate) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
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);
|
|
2
|
+
import {
|
|
3
|
+
firstSurvey,
|
|
4
|
+
firstSurveyHandoff,
|
|
5
|
+
prepareFirstSurveyLocally,
|
|
6
|
+
recommendedSurveys
|
|
7
|
+
} from "./chunk-77SK7TKZ.js";
|
|
8
|
+
|
|
9
|
+
// src/first-survey.ts
|
|
10
|
+
function beginFirstSurveyPreparation(root, mode, prepare = prepareFirstSurveyLocally) {
|
|
11
|
+
return mode.development && mode.interactive && !mode.preview ? prepare(root) : void 0;
|
|
12
|
+
}
|
|
13
|
+
async function offerFirstSurvey(client, root, workspace, site, ui, mode) {
|
|
14
|
+
if (!mode.interactive || mode.preview) return { state: "skipped" };
|
|
15
|
+
if (mode.development) return recommendedSurveys(client, root, workspace, site, ui, mode);
|
|
16
|
+
const choice = await ui.choose(
|
|
17
|
+
"Find your first useful survey? Nothing changes or publishes automatically.",
|
|
18
|
+
[
|
|
19
|
+
{ value: "guided", label: "Review suggestions" },
|
|
20
|
+
{ value: "agent", label: "Continue with my coding agent" },
|
|
21
|
+
{ value: "finish", label: "Finish setup" }
|
|
22
|
+
],
|
|
23
|
+
"finish"
|
|
24
|
+
);
|
|
25
|
+
if (choice === "agent") ui.show(firstSurveyHandoff(root, client.config.apiUrl, workspace, site));
|
|
26
|
+
if (choice === "guided") return firstSurvey(client, root, workspace, site, ui);
|
|
27
|
+
return { state: "skipped" };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
beginFirstSurveyPreparation,
|
|
32
|
+
offerFirstSurvey
|
|
33
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
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);
|
|
2
2
|
import {
|
|
3
3
|
currentPackageProfile
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-77SK7TKZ.js";
|
|
5
5
|
|
|
6
6
|
// src/startup.ts
|
|
7
7
|
import { readFile } from "node:fs/promises";
|
|
@@ -7342,8 +7342,8 @@ import { lstat, readFile, open } from "node:fs/promises";
|
|
|
7342
7342
|
import { join } from "node:path";
|
|
7343
7343
|
import { execFile } from "node:child_process";
|
|
7344
7344
|
import { promisify } from "node:util";
|
|
7345
|
-
async function
|
|
7346
|
-
if (profile.kind !== "local-development") return;
|
|
7345
|
+
async function planPackageRegistry(project, profile) {
|
|
7346
|
+
if (profile.kind !== "local-development") return null;
|
|
7347
7347
|
const modernYarn = project.packageManager === "yarn" && (project.yarnMajor ?? 1) >= 2;
|
|
7348
7348
|
const path = join(project.directory, modernYarn ? ".yarnrc.yml" : ".npmrc");
|
|
7349
7349
|
let before = "", metadata;
|
|
@@ -7382,7 +7382,13 @@ async function configurePackageRegistry(project, profile) {
|
|
|
7382
7382
|
after = configured === profile.registry ? before : `${before}${before && !before.endsWith("\n") ? "\n" : ""}${line}
|
|
7383
7383
|
`;
|
|
7384
7384
|
}
|
|
7385
|
-
|
|
7385
|
+
return { path, before: metadata ? before : null, after };
|
|
7386
|
+
}
|
|
7387
|
+
async function configurePackageRegistry(project, profile) {
|
|
7388
|
+
const plan = await planPackageRegistry(project, profile);
|
|
7389
|
+
if (!plan || plan.after === plan.before) return;
|
|
7390
|
+
const { path, after } = plan, before = plan.before ?? "";
|
|
7391
|
+
const metadata = plan.before === null ? null : await lstat(path);
|
|
7386
7392
|
const handle = await open(path, metadata ? constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0) : "wx", 420);
|
|
7387
7393
|
try {
|
|
7388
7394
|
if (metadata) {
|
|
@@ -7399,5 +7405,6 @@ async function configurePackageRegistry(project, profile) {
|
|
|
7399
7405
|
|
|
7400
7406
|
export {
|
|
7401
7407
|
require_dist,
|
|
7408
|
+
planPackageRegistry,
|
|
7402
7409
|
configurePackageRegistry
|
|
7403
7410
|
};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
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);
|
|
2
2
|
import {
|
|
3
3
|
Cancelled
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-GOYBYAUH.js";
|
|
5
5
|
import {
|
|
6
6
|
cliCommand
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-CADA5WZ3.js";
|
|
8
8
|
import {
|
|
9
9
|
require_dist
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-NMHTFC5Q.js";
|
|
11
11
|
import {
|
|
12
12
|
__toESM
|
|
13
13
|
} from "./chunk-WL3NOIM6.js";
|
|
@@ -245,7 +245,7 @@ Agents can propose feedback investigations, read the current documentation and u
|
|
|
245
245
|
terminal.success("Feedback skill installed in .agents/skills/userreason");
|
|
246
246
|
return { status: "added", path: ".agents/skills/userreason" };
|
|
247
247
|
} catch (error) {
|
|
248
|
-
if (error instanceof Cancelled) return { status: "declined" };
|
|
248
|
+
if (error instanceof Cancelled) return { status: "declined", reason: "user_cancelled" };
|
|
249
249
|
terminal.warn("Installation is complete. The optional agent skill could not be installed; the skill directory needs review.");
|
|
250
250
|
return { status: "failed", reason: "skill_directory_needs_review" };
|
|
251
251
|
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
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);
|
|
2
|
+
import {
|
|
3
|
+
applyIntegration
|
|
4
|
+
} from "./chunk-5IO5LY22.js";
|
|
5
|
+
import {
|
|
6
|
+
require_typescript
|
|
7
|
+
} from "./chunk-77SK7TKZ.js";
|
|
8
|
+
import {
|
|
9
|
+
__toESM
|
|
10
|
+
} from "./chunk-WL3NOIM6.js";
|
|
11
|
+
|
|
12
|
+
// src/footer.ts
|
|
13
|
+
var import_typescript = __toESM(require_typescript(), 1);
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
16
|
+
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
17
|
+
var FOOTER_LINK = '<a href="https://userreason.com/" data-userreason-footer="v1">Powered by UserReason</a>';
|
|
18
|
+
var digest = (text) => createHash("sha256").update(text).digest("hex");
|
|
19
|
+
async function source(root, path, overlay) {
|
|
20
|
+
const local = relative(root, path);
|
|
21
|
+
if (!local || local.startsWith("..") || local.includes("\\")) return;
|
|
22
|
+
let current = root;
|
|
23
|
+
for (const part of local.split("/")) {
|
|
24
|
+
current = join(current, part);
|
|
25
|
+
let info2;
|
|
26
|
+
try {
|
|
27
|
+
info2 = await lstat(current);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error.code === "ENOENT") return;
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
if (info2.isSymbolicLink()) return;
|
|
33
|
+
}
|
|
34
|
+
const info = await lstat(path);
|
|
35
|
+
if (!info.isFile() || info.nlink !== 1 || info.size > 1e6) return;
|
|
36
|
+
return overlay?.get(path) ?? readFile(path, "utf8");
|
|
37
|
+
}
|
|
38
|
+
function parse(path, text) {
|
|
39
|
+
const ast = import_typescript.default.createSourceFile(path, text, import_typescript.default.ScriptTarget.Latest, true, /\.(?:jsx|tsx|js)$/.test(path) ? import_typescript.default.ScriptKind.TSX : import_typescript.default.ScriptKind.TS);
|
|
40
|
+
return ast.parseDiagnostics?.length ? void 0 : ast;
|
|
41
|
+
}
|
|
42
|
+
function jsxFooters(path, text) {
|
|
43
|
+
const ast = parse(path, text);
|
|
44
|
+
if (!ast) return [];
|
|
45
|
+
const result = [];
|
|
46
|
+
const visit = (node) => {
|
|
47
|
+
if (import_typescript.default.isJsxElement(node) && node.openingElement.tagName.getText(ast) === "footer") {
|
|
48
|
+
result.push({ path, source: text, start: node.getStart(ast), insertion: node.closingElement.getStart(ast), end: node.end });
|
|
49
|
+
}
|
|
50
|
+
import_typescript.default.forEachChild(node, visit);
|
|
51
|
+
};
|
|
52
|
+
visit(ast);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
function htmlFooters(path, text, start = 0, stop = text.length) {
|
|
56
|
+
const result = [], stack = [];
|
|
57
|
+
let at = start;
|
|
58
|
+
while (at < stop) {
|
|
59
|
+
const open = text.indexOf("<", at);
|
|
60
|
+
if (open < 0 || open >= stop) break;
|
|
61
|
+
if (text.startsWith("<!--", open)) {
|
|
62
|
+
const close = text.indexOf("-->", open + 4);
|
|
63
|
+
if (close < 0) return [];
|
|
64
|
+
at = close + 3;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const name = /^<\s*(\/?)\s*([A-Za-z][A-Za-z0-9:-]*)\b/.exec(text.slice(open));
|
|
68
|
+
if (!name) {
|
|
69
|
+
at = open + 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
let end = open + name[0].length, quote = "";
|
|
73
|
+
for (; end < stop; end++) {
|
|
74
|
+
const char = text[end];
|
|
75
|
+
if (quote) {
|
|
76
|
+
if (char === quote) quote = "";
|
|
77
|
+
} else if (char === '"' || char === "'") quote = char;
|
|
78
|
+
else if (char === ">") break;
|
|
79
|
+
}
|
|
80
|
+
if (end >= stop) return [];
|
|
81
|
+
const tag = name[2].toLowerCase(), closing = Boolean(name[1]), self = /\/\s*$/.test(text.slice(open, end));
|
|
82
|
+
if (!closing && ["script", "style", "textarea"].includes(tag)) {
|
|
83
|
+
const tail = new RegExp(`<\\/\\s*${tag}\\s*>`, "ig");
|
|
84
|
+
tail.lastIndex = end + 1;
|
|
85
|
+
const match = tail.exec(text);
|
|
86
|
+
if (!match) return [];
|
|
87
|
+
at = tail.lastIndex;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (tag === "footer") {
|
|
91
|
+
if (self) return [];
|
|
92
|
+
if (!closing) stack.push(open);
|
|
93
|
+
else {
|
|
94
|
+
const begin = stack.pop();
|
|
95
|
+
if (begin === void 0) return [];
|
|
96
|
+
result.push({ path, source: text, start: begin, insertion: open, end: end + 1 });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
at = end + 1;
|
|
100
|
+
}
|
|
101
|
+
return stack.length ? [] : result;
|
|
102
|
+
}
|
|
103
|
+
function vueParts(text) {
|
|
104
|
+
const starts = [...text.matchAll(/<template(?:\s[^>]*|)>/g)];
|
|
105
|
+
const first = starts[0];
|
|
106
|
+
const end = text.lastIndexOf("</template>");
|
|
107
|
+
return {
|
|
108
|
+
template: first && end > first.index ? { start: first.index + first[0].length, end } : void 0,
|
|
109
|
+
scripts: [...text.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script\s*>/g)].map((match) => match[1])
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function imports(root, path, text, overlay) {
|
|
113
|
+
const parts = extname(path) === ".vue" ? vueParts(text) : void 0;
|
|
114
|
+
const script = parts ? parts.scripts.join("\n") : text;
|
|
115
|
+
const ast = parse(path + ".tsx", script);
|
|
116
|
+
if (!ast) return [];
|
|
117
|
+
const result = [];
|
|
118
|
+
for (const statement of ast.statements) {
|
|
119
|
+
if (!import_typescript.default.isImportDeclaration(statement) || !import_typescript.default.isStringLiteral(statement.moduleSpecifier) || !statement.moduleSpecifier.text.startsWith(".")) continue;
|
|
120
|
+
const bindings = [statement.importClause?.name?.text, ...statement.importClause?.namedBindings && import_typescript.default.isNamedImports(statement.importClause.namedBindings) ? statement.importClause.namedBindings.elements.map((item) => item.name.text) : []].filter((name) => Boolean(name));
|
|
121
|
+
const body = script.slice(0, statement.pos) + script.slice(statement.end) + (parts?.template ? text.slice(parts.template.start, parts.template.end) : "");
|
|
122
|
+
if (!bindings.some((name) => new RegExp(`(?:<|\\b)${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[\\s/>.(),])`).test(body))) continue;
|
|
123
|
+
const base = resolve(dirname(path), statement.moduleSpecifier.text);
|
|
124
|
+
for (const candidate of [base, ...[".tsx", ".jsx", ".ts", ".js", ".vue"].map((extension) => base + extension), ...["index.tsx", "index.jsx", "index.ts", "index.js", "index.vue"].map((file) => join(base, file))]) {
|
|
125
|
+
if (await source(root, candidate, overlay) !== void 0) {
|
|
126
|
+
result.push(candidate);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
async function planFooter(project, overlay) {
|
|
134
|
+
const root = resolve(project.directory);
|
|
135
|
+
const entries = project.framework === "vanilla" ? ["index.html"] : project.framework === "next" ? ["app/layout.tsx", "app/layout.jsx", "app/layout.js", "src/app/layout.tsx", "src/app/layout.jsx", "src/app/layout.js", "pages/_app.tsx", "pages/_app.jsx", "src/pages/_app.tsx", "src/pages/_app.jsx"] : project.framework === "vue" ? ["src/main.ts", "src/main.js", "main.ts", "main.js"] : ["src/main.tsx", "src/main.jsx", "src/index.tsx", "src/index.jsx", "src/index.js", "src/main.js", "index.jsx"];
|
|
136
|
+
const queue = [];
|
|
137
|
+
for (const file of entries) {
|
|
138
|
+
const path = join(root, file), text = await source(root, path, overlay);
|
|
139
|
+
if (text !== void 0) queue.push({ path, source: text });
|
|
140
|
+
}
|
|
141
|
+
if (queue.length !== 1) return { status: "manual", code: "unsupported_footer", candidates: [] };
|
|
142
|
+
const seen = /* @__PURE__ */ new Set(), candidates = [];
|
|
143
|
+
while (queue.length) {
|
|
144
|
+
const file = queue.shift();
|
|
145
|
+
if (seen.has(file.path)) continue;
|
|
146
|
+
if (seen.size >= 80) return { status: "manual", code: "unsupported_footer", candidates: [] };
|
|
147
|
+
seen.add(file.path);
|
|
148
|
+
if (extname(file.path) === ".html") candidates.push(...htmlFooters(file.path, file.source));
|
|
149
|
+
else if (extname(file.path) === ".vue") {
|
|
150
|
+
const range = vueParts(file.source).template;
|
|
151
|
+
if (range) candidates.push(...htmlFooters(file.path, file.source, range.start, range.end));
|
|
152
|
+
} else candidates.push(...jsxFooters(file.path, file.source));
|
|
153
|
+
for (const path of await imports(root, file.path, file.source, overlay)) {
|
|
154
|
+
const text = await source(root, path, overlay);
|
|
155
|
+
if (text !== void 0) queue.push({ path, source: text });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const listed = candidates.map((candidate2) => ({ path: relative(root, candidate2.path), line: candidate2.source.slice(0, candidate2.start).split("\n").length }));
|
|
159
|
+
const attribution = /<a\b[^>]*href\s*=\s*["']https:\/\/(?:www\.)?userreason\.com\/?["'][^>]*>\s*Powered by UserReason\s*<\/a\s*>/i;
|
|
160
|
+
const marked = candidates.filter((candidate2) => attribution.test(candidate2.source.slice(candidate2.start, candidate2.end)));
|
|
161
|
+
if (marked.length === 1) {
|
|
162
|
+
const candidate2 = marked[0];
|
|
163
|
+
return { status: "already_present", directory: root, path: candidate2.path, before: candidate2.source, after: candidate2.source, anchorHash: digest(candidate2.source.slice(candidate2.start, candidate2.end)) };
|
|
164
|
+
}
|
|
165
|
+
if (candidates.length !== 1) return { status: "manual", code: candidates.length ? "ambiguous_footer" : "missing_footer", candidates: listed };
|
|
166
|
+
const candidate = candidates[0];
|
|
167
|
+
const existing = candidate.source.slice(candidate.start, candidate.end);
|
|
168
|
+
if (attribution.test(existing))
|
|
169
|
+
return { status: "already_present", directory: root, path: candidate.path, before: candidate.source, after: candidate.source, anchorHash: digest(existing) };
|
|
170
|
+
const line = candidate.source.slice(0, candidate.insertion).split("\n").at(-1) ?? "";
|
|
171
|
+
const indent = /^\s*/.exec(line)?.[0] ?? "";
|
|
172
|
+
const insertion = `
|
|
173
|
+
${indent} ${FOOTER_LINK}
|
|
174
|
+
${indent}`;
|
|
175
|
+
return {
|
|
176
|
+
status: "ready",
|
|
177
|
+
directory: root,
|
|
178
|
+
path: candidate.path,
|
|
179
|
+
before: candidate.source,
|
|
180
|
+
after: candidate.source.slice(0, candidate.insertion) + insertion + candidate.source.slice(candidate.insertion),
|
|
181
|
+
anchorHash: digest(existing)
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
async function applyFooter(plan) {
|
|
185
|
+
if (plan.status === "already_present") {
|
|
186
|
+
if (await source(plan.directory, plan.path) !== plan.before) throw new Error("Footer changed after review");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
await applyIntegration({ ...plan, status: "ready", description: "Add UserReason attribution to the existing page footer" });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export {
|
|
193
|
+
FOOTER_LINK,
|
|
194
|
+
planFooter,
|
|
195
|
+
applyFooter
|
|
196
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
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);
|
|
2
|
+
import {
|
|
3
|
+
TELEMETRY_REASONS,
|
|
4
|
+
ToolTelemetry
|
|
5
|
+
} from "./chunk-77SK7TKZ.js";
|
|
6
|
+
|
|
7
|
+
// src/analytics.ts
|
|
8
|
+
var WizardAnalytics = class extends ToolTelemetry {
|
|
9
|
+
starts = /* @__PURE__ */ new Map();
|
|
10
|
+
begin(step) {
|
|
11
|
+
this.starts.set(step, performance.now());
|
|
12
|
+
this.step(step, "started");
|
|
13
|
+
}
|
|
14
|
+
end(step, phase = "completed") {
|
|
15
|
+
const start = this.starts.get(step);
|
|
16
|
+
this.step(step, phase, start === void 0 ? {} : { duration_ms: performance.now() - start });
|
|
17
|
+
this.starts.delete(step);
|
|
18
|
+
}
|
|
19
|
+
async measure(step, operation, phase = "applied") {
|
|
20
|
+
const start = performance.now();
|
|
21
|
+
this.begin(step);
|
|
22
|
+
await this.checkpoint();
|
|
23
|
+
try {
|
|
24
|
+
const value = await operation();
|
|
25
|
+
const result = value && typeof value === "object" ? value : void 0;
|
|
26
|
+
const outcome = result?.reason === "user_cancelled" ? "cancelled" : result?.status === "already_present" ? "already_present" : result?.status === "declined" ? "declined" : result?.status === "skipped" ? "skipped" : result?.status === "failed" ? "failed" : phase;
|
|
27
|
+
const reason = result?.reason && TELEMETRY_REASONS.includes(result.reason) ? result.reason : result?.reason?.includes("review") ? "validation_failed" : result?.reason?.includes("preserved") || result?.reason === "existing_feedback_section" ? "existing_content_preserved" : outcome === "failed" ? "operation_failed" : outcome === "skipped" ? "unsupported" : void 0;
|
|
28
|
+
this.step(step, outcome, { duration_ms: performance.now() - start, ...reason ? { reason } : {}, ...result?.file === "AGENTS.md" || result?.file === "CLAUDE.md" ? { guidance_file: result.file } : {} });
|
|
29
|
+
this.starts.delete(step);
|
|
30
|
+
return value;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
this.step(step, error instanceof Error && error.name === "Cancelled" ? "cancelled" : "failed", { duration_ms: performance.now() - start, reason: error instanceof Error && error.name === "Cancelled" ? "user_cancelled" : "operation_failed" });
|
|
33
|
+
this.starts.delete(step);
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
finish(cancelled = false, failed = false) {
|
|
38
|
+
for (const step of [...this.starts.keys()].reverse()) this.end(step, cancelled ? "cancelled" : failed ? "failed" : "completed");
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
WizardAnalytics
|
|
44
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
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);
|
|
2
|
+
import {
|
|
3
|
+
connectSetup,
|
|
4
|
+
resumeSetup,
|
|
5
|
+
runSetup
|
|
6
|
+
} from "./chunk-YYFC7TM2.js";
|
|
7
|
+
import {
|
|
8
|
+
clean,
|
|
9
|
+
openBrowser
|
|
10
|
+
} from "./chunk-GOYBYAUH.js";
|
|
11
|
+
import {
|
|
12
|
+
BrowserSetupUnavailable
|
|
13
|
+
} from "./chunk-77SK7TKZ.js";
|
|
14
|
+
|
|
15
|
+
// src/setup-flow.ts
|
|
16
|
+
var shell = (value) => /^[A-Za-z0-9_./:@=-]+$/.test(value) ? value : `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
17
|
+
function setupSampleCommand(setupId, apiOrigin, cliVersion) {
|
|
18
|
+
return ["npx", "-y", `@userreason/cli@${cliVersion}`, "setup", "sample", "--setup", setupId, "--api-url", apiOrigin, "--directory", ".", "--json"].map(shell).join(" ");
|
|
19
|
+
}
|
|
20
|
+
function setupResumeCommand(setupId, apiOrigin, version) {
|
|
21
|
+
return ["npx", "-y", `@userreason/wizard@${version}`, "--resume-setup", setupId, "--api-url", apiOrigin, "--directory", "."].map(shell).join(" ");
|
|
22
|
+
}
|
|
23
|
+
var setupManualGuidance = {
|
|
24
|
+
install_packages: "Review the project dependency and registry settings, install compatible @userreason/js and @userreason/cli versions with the project package manager, then rerun setup. See https://docs.userreason.com/get-started/quickstart/.",
|
|
25
|
+
initialize_sdk: "Identify the existing browser entry and initialize UserReason once with the selected site\u2019s public ID, preserving any existing initialization. See https://docs.userreason.com/sdk/initialization/.",
|
|
26
|
+
install_skill: "Review the existing agent guidance and merge the UserReason skill manually from the installed @userreason/cli skills/userreason directory. Preserve your existing edits and do not overwrite a customized skill.",
|
|
27
|
+
insert_footer: "Identify the intended existing site footer and manually add a Powered by UserReason link to https://userreason.com/. Preserve its content and layout; resolve ambiguous placement before editing."
|
|
28
|
+
};
|
|
29
|
+
function setupSummary(session) {
|
|
30
|
+
return {
|
|
31
|
+
setup_id: session.setup_id,
|
|
32
|
+
phase: session.phase,
|
|
33
|
+
workspace_id: session.workspace_id,
|
|
34
|
+
site_id: session.site_id,
|
|
35
|
+
local_operations: session.commands.map((command) => ({ kind: command.kind, status: command.status, code: command.result?.code ?? null })),
|
|
36
|
+
manual_steps: session.local_plan?.manual_steps ?? [],
|
|
37
|
+
source: session.source?.status ?? "not_started",
|
|
38
|
+
drafts: session.draft_outcomes.map((draft) => ({ candidate_id: draft.candidate_id, prompt_id: draft.prompt_id, status: draft.status, revision: draft.revision })),
|
|
39
|
+
sample: session.sample.status,
|
|
40
|
+
cli_access: "not_requested"
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async function runBrowserWizard(input) {
|
|
44
|
+
const { terminal, project, apiOrigin, signal } = input;
|
|
45
|
+
let connected;
|
|
46
|
+
try {
|
|
47
|
+
connected = input.resumeId ? await resumeSetup(apiOrigin, input.resumeId, project, { signal, measurementDisabled: input.measurementDisabled }) : await connectSetup(apiOrigin, project, async (url, code) => {
|
|
48
|
+
terminal.block("Continue in your browser", [url, `Verification code: ${code}`, "Review the project, local changes and source permissions there."]);
|
|
49
|
+
if (!input.noOpen && !await openBrowser(url)) terminal.info("Open the link above to continue.");
|
|
50
|
+
}, { signal, browserOrigin: input.browserOrigin, measurementDisabled: input.measurementDisabled });
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (!input.resumeId && error instanceof BrowserSetupUnavailable) return false;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
const resume = setupResumeCommand(connected.client.identity.setup_id, apiOrigin, input.version);
|
|
56
|
+
terminal.block("Setup connected", [`Setup: ${connected.client.identity.setup_id}`, "Keep this terminal open. The browser controls approvals, draft choices and cancellation.", `To reconnect from this project: ${resume}`]);
|
|
57
|
+
let status = "", displayedPacket = "";
|
|
58
|
+
const shownManual = /* @__PURE__ */ new Set();
|
|
59
|
+
try {
|
|
60
|
+
const result = await runSetup(connected.client, project, {
|
|
61
|
+
signal,
|
|
62
|
+
onSession(session) {
|
|
63
|
+
for (const step of session.local_plan?.manual_steps ?? []) {
|
|
64
|
+
const key = `${step.kind}:${step.code}`;
|
|
65
|
+
if (shownManual.has(key)) continue;
|
|
66
|
+
shownManual.add(key);
|
|
67
|
+
terminal.block(`Manual step: ${step.kind}`, [setupManualGuidance[step.kind]]);
|
|
68
|
+
}
|
|
69
|
+
const next = `${session.phase}:${session.source?.status ?? ""}:${session.sample.status}`;
|
|
70
|
+
if (next === status) return;
|
|
71
|
+
status = next;
|
|
72
|
+
const messages = {
|
|
73
|
+
configuring: "Choose the workspace, site and installation options in the browser.",
|
|
74
|
+
awaiting_local_approval: "Review and approve the exact local changes in the browser.",
|
|
75
|
+
applying: "Applying browser-approved local changes.",
|
|
76
|
+
ready_for_recommendations: session.source?.status === "pending" ? "Review source files and provider details in the browser before any source upload." : "Continue with recommendations or skip that step in the browser.",
|
|
77
|
+
analyzing: "Analyzing the source packet approved in the browser.",
|
|
78
|
+
selecting_drafts: "Review recommendations and select drafts in the browser.",
|
|
79
|
+
creating_drafts: "Creating only the drafts selected in the browser.",
|
|
80
|
+
agent_test: session.sample.status === "received" ? "Simulated sample received. Finish or skip in the browser." : "Complete or skip the optional agent test in the browser.",
|
|
81
|
+
complete: "Browser setup completed.",
|
|
82
|
+
cancelled: "Browser setup cancelled. Applied changes and created drafts are listed below.",
|
|
83
|
+
expired: "Browser setup expired. Applied changes and created drafts are listed below."
|
|
84
|
+
};
|
|
85
|
+
terminal.info(messages[session.phase]);
|
|
86
|
+
},
|
|
87
|
+
onSourceIssue(code) {
|
|
88
|
+
terminal.warn(`Source analysis needs review (${clean(code)}). Check the browser status, then retry source review or skip it. Changed local source requires fresh approval.`);
|
|
89
|
+
},
|
|
90
|
+
onSourcePrepared(packet) {
|
|
91
|
+
if (displayedPacket === packet.manifest.packet_digest) return;
|
|
92
|
+
displayedPacket = packet.manifest.packet_digest;
|
|
93
|
+
terminal.block("Local source review", [
|
|
94
|
+
"The exact bounded excerpts below stay local until you approve this packet and its providers in the browser.",
|
|
95
|
+
`Packet SHA-256: ${packet.manifest.packet_digest}`,
|
|
96
|
+
...packet.evidence.flatMap((item) => [`${clean(item.path)}:${item.start_line}-${item.end_line} (file SHA-256 ${item.file_hash})`, ...item.excerpt.split("\n").map((line) => `> ${clean(line)}`)]),
|
|
97
|
+
`Omitted files: ${packet.manifest.omitted_files}. Truncated: ${packet.manifest.truncated ? "yes" : "no"}. Git history is excluded.`
|
|
98
|
+
]);
|
|
99
|
+
},
|
|
100
|
+
onSample(session) {
|
|
101
|
+
terminal.block("Try your agent", ["Ask: What do UserReason dummy respondents think?", "Tell your agent to read this setup\u2019s simulated sample, using:", setupSampleCommand(session.setup_id, apiOrigin, input.profile.cliVersion), "This is example data. It does not read production responses or grant ongoing CLI access."]);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
const summary = setupSummary(result);
|
|
105
|
+
terminal.block("Setup result", [
|
|
106
|
+
`Status: ${result.phase}`,
|
|
107
|
+
...summary.local_operations.map((operation) => `${operation.kind}: ${operation.status}${operation.code ? ` (${operation.code})` : ""}`),
|
|
108
|
+
...summary.manual_steps.map((step) => `${step.kind}: manual (${step.code})`),
|
|
109
|
+
...summary.drafts.map((draft) => `Draft ${draft.prompt_id}: ${draft.status}${draft.revision ? ` (revision ${draft.revision})` : ""}`),
|
|
110
|
+
`Simulated sample: ${summary.sample}`,
|
|
111
|
+
"Ongoing CLI access was not requested. Use a separate `userreason login --open` when you want to authorize it."
|
|
112
|
+
]);
|
|
113
|
+
if (input.json) process.stdout.write(`${JSON.stringify(summary)}
|
|
114
|
+
`);
|
|
115
|
+
if (result.phase === "cancelled") process.exitCode = 130;
|
|
116
|
+
else if (result.phase !== "complete") process.exitCode = 1;
|
|
117
|
+
return true;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
terminal.info(`Resume from this project: ${resume}`);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export {
|
|
125
|
+
setupSampleCommand,
|
|
126
|
+
setupResumeCommand,
|
|
127
|
+
setupManualGuidance,
|
|
128
|
+
setupSummary,
|
|
129
|
+
runBrowserWizard
|
|
130
|
+
};
|