@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
|
@@ -0,0 +1,165 @@
|
|
|
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
|
+
Cancelled
|
|
4
|
+
} from "./chunk-SJVLAYGG.js";
|
|
5
|
+
import {
|
|
6
|
+
initializationPath
|
|
7
|
+
} from "./chunk-5SC2K42Q.js";
|
|
8
|
+
import {
|
|
9
|
+
cliCommand
|
|
10
|
+
} from "./chunk-YMSGDC4B.js";
|
|
11
|
+
import {
|
|
12
|
+
packageProfile
|
|
13
|
+
} from "./chunk-GGARVSPT.js";
|
|
14
|
+
|
|
15
|
+
// src/agent-note.ts
|
|
16
|
+
import { constants } from "node:fs";
|
|
17
|
+
import { lstat, stat, readFile, open } from "node:fs/promises";
|
|
18
|
+
import { join, relative } from "node:path";
|
|
19
|
+
var start = "<!-- userreason:feedback:start -->";
|
|
20
|
+
var end = "<!-- userreason:feedback:end -->";
|
|
21
|
+
function note(entry, docs) {
|
|
22
|
+
return `## User feedback
|
|
23
|
+
|
|
24
|
+
When investigating low conversion, drop-off, adoption or unclear user needs,
|
|
25
|
+
consider targeted in-app feedback alongside analytics to understand users' reasons.
|
|
26
|
+
This project collects feedback through \`@userreason/js\`, initialized in \`${entry}\`.
|
|
27
|
+
See [feedback documentation](${docs}).`;
|
|
28
|
+
}
|
|
29
|
+
function earlierNotes(entry) {
|
|
30
|
+
const previousReference = '\nFor SDK methods, prompt settings and CLI commands, read the current documentation instead of relying on memory:\n`npx -y @userreason/cli@latest docs search "<question>"`, or https://docs.userreason.com/llms.txt.';
|
|
31
|
+
return [
|
|
32
|
+
`## User feedback
|
|
33
|
+
|
|
34
|
+
This project uses \`@userreason/js\` to collect in-app user feedback,
|
|
35
|
+
initialized in \`${entry}\`. See [SDK documentation](node_modules/@userreason/js/README.md).`,
|
|
36
|
+
note(entry, "node_modules/@userreason/js/FEEDBACK.md"),
|
|
37
|
+
note(entry, "node_modules/@userreason/js/README.md"),
|
|
38
|
+
...["FEEDBACK.md", "README.md"].map((file) => note(entry, `node_modules/@userreason/js/${file}`) + previousReference)
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
async function planAgentNote(project, options = {}) {
|
|
42
|
+
let path = "", file = "", metadata;
|
|
43
|
+
for (const candidate of ["AGENTS.md", "CLAUDE.md"]) {
|
|
44
|
+
try {
|
|
45
|
+
metadata = await lstat(join(project.directory, candidate));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error.code === "ENOENT") continue;
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
path = join(project.directory, candidate);
|
|
51
|
+
file = candidate;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
if (!metadata) return { status: "skipped", reason: "no_guidance_file" };
|
|
55
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1 || metadata.size > 1e6)
|
|
56
|
+
return { status: "skipped", file, reason: "guidance_file_needs_review" };
|
|
57
|
+
const before = await readFile(path, "utf8");
|
|
58
|
+
const headings = [...before.matchAll(/^## User feedback[ \t]*\r?$/gm)];
|
|
59
|
+
if (headings.length > 1) return { status: "skipped", file, reason: "existing_feedback_section" };
|
|
60
|
+
const heading = headings[0];
|
|
61
|
+
let replacement;
|
|
62
|
+
if (heading) {
|
|
63
|
+
let offset = heading.index;
|
|
64
|
+
const remainder = before.slice(offset + heading[0].length);
|
|
65
|
+
const nextHeading = /^#{1,2}[ \t]/m.exec(remainder);
|
|
66
|
+
const until = nextHeading ? offset + heading[0].length + nextHeading.index : before.length;
|
|
67
|
+
const section = before.slice(offset, until).replaceAll("\r\n", "\n");
|
|
68
|
+
const marked = before.slice(0, offset).endsWith(start + (before.includes("\r\n") ? "\r\n" : "\n"));
|
|
69
|
+
const body = marked ? section.replace(new RegExp(`\\n${end}\\n*$`), "") : section;
|
|
70
|
+
const oldEntry = /initialized in `([^`\n]+)`\./.exec(body)?.[1];
|
|
71
|
+
if (!oldEntry || !earlierNotes(oldEntry).includes(body.trimEnd()) || marked && (before.split(start).length !== 2 || before.split(end).length !== 2) || !marked && (before.includes(start) || before.includes(end)))
|
|
72
|
+
return { status: section.includes("@userreason/js") ? "already_present" : "skipped", file, reason: "existing_feedback_section" };
|
|
73
|
+
if (marked) offset -= start.length + (before.includes("\r\n") ? 2 : 1);
|
|
74
|
+
replacement = { offset, previous: before.slice(offset, until), after: "" };
|
|
75
|
+
} else if (before.includes(start) || before.includes(end)) return { status: "already_present", file };
|
|
76
|
+
if (options.beforeInstall) return { path, file, before, snippet: "", addition: "", dev: metadata.dev, ino: metadata.ino, ...replacement ? { replacement } : {} };
|
|
77
|
+
const source = await initializationPath(project);
|
|
78
|
+
if (!source) return { status: "skipped", file, reason: "no_initialization" };
|
|
79
|
+
const entry = relative(project.directory, source).replaceAll("\\", "/");
|
|
80
|
+
if (!entry || entry.startsWith("../") || /[\r\n`]/.test(entry))
|
|
81
|
+
return { status: "skipped", file, reason: "initialization_path_needs_review" };
|
|
82
|
+
let docs = "node_modules/@userreason/js/FEEDBACK.md";
|
|
83
|
+
try {
|
|
84
|
+
if (!(await stat(join(project.directory, docs))).isFile()) docs = "node_modules/@userreason/js/README.md";
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error.code === "ENOENT") docs = "node_modules/@userreason/js/README.md";
|
|
87
|
+
else throw error;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
if (!(await stat(join(project.directory, docs))).isFile()) return { status: "skipped", file, reason: "docs_unavailable" };
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error.code === "ENOENT") return { status: "skipped", file, reason: "docs_unavailable" };
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
const snippet = note(entry, docs) + `
|
|
96
|
+
For SDK methods, survey settings and CLI commands, read the current documentation instead of relying on memory:
|
|
97
|
+
\`${cliCommand(project)} docs search "<question>"\`, or ${packageProfile().docsUrl}/llms.txt.`;
|
|
98
|
+
const newline = before.includes("\r\n") ? "\r\n" : "\n";
|
|
99
|
+
const separator = before.length ? before.endsWith(newline + newline) ? "" : before.endsWith(newline) ? newline : newline + newline : "";
|
|
100
|
+
const addition = separator + `${snippet}
|
|
101
|
+
`.replaceAll("\n", newline);
|
|
102
|
+
if (replacement) {
|
|
103
|
+
const trailing = /(?:\r?\n)*$/.exec(replacement.previous)[0];
|
|
104
|
+
replacement.after = snippet.replaceAll("\n", newline) + (trailing || newline);
|
|
105
|
+
}
|
|
106
|
+
return { path, file, before, snippet, addition, dev: metadata.dev, ino: metadata.ino, ...replacement ? { replacement } : {} };
|
|
107
|
+
}
|
|
108
|
+
async function applyAgentNote(plan) {
|
|
109
|
+
if ((await lstat(plan.path)).isSymbolicLink()) throw new Error("The guidance file became a symlink.");
|
|
110
|
+
const handle = await open(plan.path, constants.O_RDWR | (plan.replacement ? 0 : constants.O_APPEND) | (constants.O_NOFOLLOW ?? 0));
|
|
111
|
+
try {
|
|
112
|
+
const metadata = await handle.stat();
|
|
113
|
+
if (!metadata.isFile() || metadata.nlink !== 1 || metadata.dev !== plan.dev || metadata.ino !== plan.ino || await handle.readFile("utf8") !== plan.before)
|
|
114
|
+
throw new Error("The guidance file changed after the preview.");
|
|
115
|
+
if (plan.replacement) {
|
|
116
|
+
const { offset, previous, after } = plan.replacement;
|
|
117
|
+
const position = Buffer.byteLength(plan.before.slice(0, offset));
|
|
118
|
+
const tail = Buffer.from(after + plan.before.slice(offset + previous.length));
|
|
119
|
+
let written = 0;
|
|
120
|
+
while (written < tail.length) {
|
|
121
|
+
const result = await handle.write(tail, written, tail.length - written, position + written);
|
|
122
|
+
if (!result.bytesWritten) throw new Error("Unable to update the feedback note.");
|
|
123
|
+
written += result.bytesWritten;
|
|
124
|
+
}
|
|
125
|
+
await handle.truncate(position + tail.length);
|
|
126
|
+
} else await handle.appendFile(plan.addition, "utf8");
|
|
127
|
+
} finally {
|
|
128
|
+
await handle.close();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function offerAgentNote(project, terminal, options) {
|
|
132
|
+
if (!options.installed || options.skip || !terminal.interactive && !options.requested)
|
|
133
|
+
return { status: "skipped", reason: options.installed ? "not_requested" : "installation_pending" };
|
|
134
|
+
try {
|
|
135
|
+
const plan = await planAgentNote(project);
|
|
136
|
+
if (!("path" in plan)) {
|
|
137
|
+
if (plan.status === "already_present") terminal.info(`User feedback note already present in ${plan.file}`);
|
|
138
|
+
else if (plan.file || options.requested) terminal.info("No user feedback note added. An existing guidance file, SDK initialization and local SDK documentation are required; existing feedback sections are preserved.");
|
|
139
|
+
return plan;
|
|
140
|
+
}
|
|
141
|
+
if (options.preview !== false) terminal.note(
|
|
142
|
+
plan.replacement ? `Current note:
|
|
143
|
+
${plan.replacement.previous.trim()}
|
|
144
|
+
|
|
145
|
+
Proposed note:
|
|
146
|
+
${plan.snippet}` : plan.snippet,
|
|
147
|
+
`Optional ${plan.replacement ? "update" : "addition"} to ${plan.file}`
|
|
148
|
+
);
|
|
149
|
+
if (!options.requested && !await terminal.confirm(`${plan.replacement ? "Update" : "Add"} this user feedback note ${plan.replacement ? "in" : "to"} ${plan.file}?`, true, { recommended: true }))
|
|
150
|
+
return { status: "declined", file: plan.file };
|
|
151
|
+
await applyAgentNote(plan);
|
|
152
|
+
terminal.success(`User feedback note ${plan.replacement ? "updated in" : "added to"} ${plan.file}`);
|
|
153
|
+
return { status: plan.replacement ? "updated" : "added", file: plan.file };
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error instanceof Cancelled) return { status: "declined" };
|
|
156
|
+
terminal.warn("Installation is complete. The optional user feedback note could not be added; the guidance file needs review.");
|
|
157
|
+
return { status: "failed", reason: "guidance_file_needs_review" };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export {
|
|
162
|
+
planAgentNote,
|
|
163
|
+
applyAgentNote,
|
|
164
|
+
offerAgentNote
|
|
165
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
9
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
10
|
+
}) : x)(function(x) {
|
|
11
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
12
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
13
|
+
});
|
|
14
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
15
|
+
try {
|
|
16
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
17
|
+
} catch (e) {
|
|
18
|
+
throw mod = 0, e;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var __copyProps = (to, from, except, desc) => {
|
|
22
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
23
|
+
for (let key of __getOwnPropNames(from))
|
|
24
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
25
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
26
|
+
}
|
|
27
|
+
return to;
|
|
28
|
+
};
|
|
29
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
30
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
31
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
32
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
33
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
34
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
35
|
+
mod
|
|
36
|
+
));
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
__require,
|
|
40
|
+
__commonJS,
|
|
41
|
+
__toESM
|
|
42
|
+
};
|
|
@@ -0,0 +1,280 @@
|
|
|
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
|
+
configurePackageRegistry
|
|
4
|
+
} from "./chunk-OEGNGGZ2.js";
|
|
5
|
+
import {
|
|
6
|
+
packageProfile
|
|
7
|
+
} from "./chunk-GGARVSPT.js";
|
|
8
|
+
|
|
9
|
+
// src/project.ts
|
|
10
|
+
import { lstat, readFile, writeFile, access, mkdtemp, realpath, rm } from "node:fs/promises";
|
|
11
|
+
import { join, resolve, dirname } from "node:path";
|
|
12
|
+
import { spawn, execFile } from "node:child_process";
|
|
13
|
+
import { promisify } from "node:util";
|
|
14
|
+
var exists = async (path) => {
|
|
15
|
+
try {
|
|
16
|
+
await access(path);
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
async function inspectProject(directory, framework) {
|
|
23
|
+
directory = resolve(directory);
|
|
24
|
+
const metadata = await lstat(directory);
|
|
25
|
+
if (!metadata.isDirectory()) throw new Error("Project directory must be an existing directory");
|
|
26
|
+
const text = await readFile(join(directory, "package.json"), "utf8");
|
|
27
|
+
if (text.length > 1e6) throw new Error("Project package.json is too large");
|
|
28
|
+
const data = JSON.parse(text);
|
|
29
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
30
|
+
throw new Error("Invalid project package.json");
|
|
31
|
+
const dependencies = { ...data.dependencies, ...data.devDependencies };
|
|
32
|
+
const inferred = dependencies.next ? "next" : dependencies.react ? "react" : dependencies.vue ? "vue" : "vanilla";
|
|
33
|
+
const packageManager = typeof data.packageManager === "string" && /^(pnpm|yarn)@/.test(data.packageManager) ? data.packageManager.split("@")[0] : await exists(join(directory, "pnpm-lock.yaml")) ? "pnpm" : await exists(join(directory, "yarn.lock")) ? "yarn" : "npm";
|
|
34
|
+
const yarnMajor = packageManager === "yarn" ? typeof data.packageManager === "string" && /^yarn@\d+/.test(data.packageManager) ? Number(data.packageManager.slice(5).split(".")[0]) : await exists(join(directory, ".yarnrc.yml")) ? 4 : 1 : void 0;
|
|
35
|
+
const sourceDirectory = await exists(join(directory, "src")) ? join(directory, "src") : directory;
|
|
36
|
+
let nextVersion = dependencies.next;
|
|
37
|
+
if (inferred === "next") {
|
|
38
|
+
try {
|
|
39
|
+
nextVersion = JSON.parse(await readFile(join(directory, "node_modules/next/package.json"), "utf8")).version;
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const version = typeof nextVersion === "string" ? /^[~^]?(\d+)(?:\.(\d+))?/.exec(nextVersion) : null;
|
|
44
|
+
return {
|
|
45
|
+
directory,
|
|
46
|
+
framework: framework ?? inferred,
|
|
47
|
+
sourceDirectory,
|
|
48
|
+
packageManager,
|
|
49
|
+
yarnMajor,
|
|
50
|
+
name: typeof data.name === "string" && data.name ? data.name : "My site",
|
|
51
|
+
vite: Boolean(dependencies.vite),
|
|
52
|
+
devScript: data.scripts && typeof data.scripts === "object" ? typeof data.scripts.dev === "string" ? "dev" : typeof data.scripts.start === "string" ? "start" : void 0 : void 0,
|
|
53
|
+
nextInstrumentation: Boolean(version && (Number(version[1]) > 15 || Number(version[1]) === 15 && Number(version[2]) >= 3)),
|
|
54
|
+
typescript: Boolean(dependencies.typescript) || await exists(join(directory, "tsconfig.json")) || await exists(join(sourceDirectory, "userreason.generated.tsx")) || await exists(join(sourceDirectory, "userreason.generated.ts"))
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
var DEFAULT_API = "https://api.userreason.com";
|
|
58
|
+
function publicOptions(config) {
|
|
59
|
+
return { siteId: config.siteId, ...config.apiBaseUrl !== DEFAULT_API ? { apiBaseUrl: config.apiBaseUrl } : {} };
|
|
60
|
+
}
|
|
61
|
+
function optionsSource(config) {
|
|
62
|
+
return `{
|
|
63
|
+
${Object.entries(publicOptions(config)).map(([key, value]) => ` ${key}: ${JSON.stringify(value)},`).join("\n")}
|
|
64
|
+
}`;
|
|
65
|
+
}
|
|
66
|
+
function initializationSource(config) {
|
|
67
|
+
return `UserReason.init(${optionsSource(config)});
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
function integrationSource(framework, config, typescript = true, legacyNext = false) {
|
|
71
|
+
const options = optionsSource(config);
|
|
72
|
+
const base = `// Generated by the UserReason wizard. Contains public SDK configuration only.
|
|
73
|
+
`;
|
|
74
|
+
if (framework === "next" && legacyNext)
|
|
75
|
+
return {
|
|
76
|
+
filename: `userreason.client.${typescript ? "tsx" : "jsx"}`,
|
|
77
|
+
content: `'use client';
|
|
78
|
+
${base}import { useEffect, useLayoutEffect } from 'react';
|
|
79
|
+
import UserReason from '@userreason/js';
|
|
80
|
+
|
|
81
|
+
const useBrowserLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
|
|
82
|
+
|
|
83
|
+
export function UserReasonInit() {
|
|
84
|
+
useBrowserLayoutEffect(() => {
|
|
85
|
+
UserReason.init(${options});
|
|
86
|
+
return () => UserReason.destroy();
|
|
87
|
+
}, []);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
`,
|
|
91
|
+
instruction: framework === "next" ? "Import UserReasonInit from the generated client file and render <UserReasonInit /> once inside <body> or your Pages Router root." : "Initialize UserReason once before mounting your application."
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
filename: `instrumentation-client.${typescript ? "ts" : "js"}`,
|
|
95
|
+
content: `${base}import UserReason from '@userreason/js';
|
|
96
|
+
|
|
97
|
+
${initializationSource(config)}`,
|
|
98
|
+
instruction: `Add this initialization to ${framework === "next" ? "instrumentation-client.ts/js for Next.js 15.3+, or a browser-only entry for older versions" : "your browser entry before mounting the app"}:
|
|
99
|
+
|
|
100
|
+
import UserReason from '@userreason/js';
|
|
101
|
+
|
|
102
|
+
${initializationSource(config)}`
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async function writeGenerated(path, content, force) {
|
|
106
|
+
try {
|
|
107
|
+
await writeFile(path, content, { flag: "wx", mode: 420 });
|
|
108
|
+
return;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error.code !== "EEXIST") throw error;
|
|
111
|
+
}
|
|
112
|
+
const metadata = await lstat(path);
|
|
113
|
+
if (!metadata.isFile() || metadata.isSymbolicLink())
|
|
114
|
+
throw new Error(`Refusing to overwrite a non-file: ${path}`);
|
|
115
|
+
if (await readFile(path, "utf8") === content) return;
|
|
116
|
+
if (!force)
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Existing file differs: ${path}. Review it and rerun with --force to replace it.`
|
|
119
|
+
);
|
|
120
|
+
await writeFile(path, content, { mode: 420 });
|
|
121
|
+
}
|
|
122
|
+
async function writeIntegration(project, config, force = false, legacyNext = false) {
|
|
123
|
+
const helper = integrationSource(project.framework, config, project.typescript, legacyNext), helperPath = join(project.sourceDirectory, helper.filename);
|
|
124
|
+
const source = await lstat(dirname(helperPath));
|
|
125
|
+
if (!source.isDirectory() || source.isSymbolicLink())
|
|
126
|
+
throw new Error("Source directory must be a real directory inside the project");
|
|
127
|
+
if (legacyNext) await writeGenerated(helperPath, helper.content, force);
|
|
128
|
+
return { files: legacyNext ? [helperPath] : [], instruction: helper.instruction };
|
|
129
|
+
}
|
|
130
|
+
async function installedVersion(project, name) {
|
|
131
|
+
if (project.packageManager === "yarn" && (project.yarnMajor ?? 1) >= 2 && await exists(join(project.directory, ".pnp.cjs"))) {
|
|
132
|
+
const script = "const p=require('pnpapi').resolveToUnqualified(process.argv[1],process.cwd()+'/'); console.log(JSON.parse(require('node:fs').readFileSync(require('node:path').join(p,'package.json'),'utf8')).version)";
|
|
133
|
+
const result = await promisify(execFile)("yarn", ["node", "-e", script, name], { cwd: project.directory, timeout: 15e3 });
|
|
134
|
+
return result.stdout.trim();
|
|
135
|
+
}
|
|
136
|
+
return JSON.parse(await readFile(join(project.directory, "node_modules", name, "package.json"), "utf8")).version;
|
|
137
|
+
}
|
|
138
|
+
var cliCommand = (project) => project.packageManager === "pnpm" ? "pnpm exec userreason" : project.packageManager === "yarn" ? "yarn exec userreason" : "npx --no-install userreason";
|
|
139
|
+
async function hasCompatibleSdk(project, profile = packageProfile()) {
|
|
140
|
+
try {
|
|
141
|
+
const manifest = JSON.parse(await readFile(join(project.directory, "package.json"), "utf8"));
|
|
142
|
+
const version = await installedVersion(project, "@userreason/js");
|
|
143
|
+
return Boolean(manifest.dependencies?.["@userreason/js"] && (profile.kind === "local-development" ? version === profile.sdkVersion : /^0\.4\.\d+$/.test(version)));
|
|
144
|
+
} catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function sdkInstallCommand(project, profile = packageProfile()) {
|
|
149
|
+
return project.packageManager === "npm" ? `npm install --ignore-scripts @userreason/js@${profile.sdkVersion}` : project.packageManager === "pnpm" ? `pnpm add --ignore-scripts @userreason/js@${profile.sdkVersion}` : (project.yarnMajor ?? 1) >= 2 ? `yarn add --mode=skip-build @userreason/js@${profile.sdkVersion}` : `yarn add --ignore-scripts @userreason/js@${profile.sdkVersion}`;
|
|
150
|
+
}
|
|
151
|
+
async function hasCompatibleCli(project, profile = packageProfile()) {
|
|
152
|
+
try {
|
|
153
|
+
const manifest = JSON.parse(await readFile(join(project.directory, "package.json"), "utf8"));
|
|
154
|
+
const version = await installedVersion(project, "@userreason/cli");
|
|
155
|
+
return Boolean(manifest.devDependencies?.["@userreason/cli"] && compatibleCliVersion(version, profile));
|
|
156
|
+
} catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function compatibleCliVersion(version, profile) {
|
|
161
|
+
return profile.kind === "local-development" ? version === profile.cliVersion : /^0\.1\.\d+$/.test(version) && Number(version.split(".")[2]) >= 1;
|
|
162
|
+
}
|
|
163
|
+
function cliInstallCommand(project, profile = packageProfile()) {
|
|
164
|
+
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");
|
|
165
|
+
}
|
|
166
|
+
var SdkInstallError = class extends Error {
|
|
167
|
+
};
|
|
168
|
+
async function installSdk(project, options = {}) {
|
|
169
|
+
return installDependency(project, "sdk", options);
|
|
170
|
+
}
|
|
171
|
+
async function installCli(project, options = {}) {
|
|
172
|
+
return installDependency(project, "cli", options);
|
|
173
|
+
}
|
|
174
|
+
async function installDependency(project, dependency, options) {
|
|
175
|
+
const label = dependency === "sdk" ? "SDK" : "CLI";
|
|
176
|
+
const profile = options.profile ?? packageProfile();
|
|
177
|
+
const spec = dependency === "sdk" ? `@userreason/js@${profile.sdkVersion}` : `@userreason/cli@${profile.cliVersion}`;
|
|
178
|
+
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
|
+
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");
|
|
181
|
+
let cache;
|
|
182
|
+
try {
|
|
183
|
+
await configurePackageRegistry(project, profile);
|
|
184
|
+
if (project.packageManager === "npm") {
|
|
185
|
+
cache = await mkdtemp(join(await realpath(project.directory), ".userreason-npm-"));
|
|
186
|
+
args.push("--cache", cache);
|
|
187
|
+
}
|
|
188
|
+
await new Promise((resolve2, reject) => {
|
|
189
|
+
const child = spawn(project.packageManager, args, {
|
|
190
|
+
cwd: project.directory,
|
|
191
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
192
|
+
shell: false,
|
|
193
|
+
env: {
|
|
194
|
+
...process.env,
|
|
195
|
+
YARN_ENABLE_SCRIPTS: "false",
|
|
196
|
+
npm_config_ignore_scripts: "true",
|
|
197
|
+
...cache ? { npm_config_cache: cache, NPM_CONFIG_CACHE: cache } : {}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
let logs = "";
|
|
201
|
+
const capture = (chunk) => {
|
|
202
|
+
const text = chunk.toString();
|
|
203
|
+
logs = (logs + text).slice(-64e3);
|
|
204
|
+
if (options.verbose) process.stderr.write(text);
|
|
205
|
+
};
|
|
206
|
+
child.stdout?.on("data", capture);
|
|
207
|
+
child.stderr?.on("data", capture);
|
|
208
|
+
child.on("error", () => reject(new SdkInstallError(`Unable to start ${project.packageManager}.`)));
|
|
209
|
+
child.on("close", (code) => {
|
|
210
|
+
if (code === 0) {
|
|
211
|
+
if (/\b[1-9]\d*\s+vulnerabilit(?:y|ies)\b|\bWARN\b|\bwarning\b/i.test(logs))
|
|
212
|
+
options.onWarning?.(
|
|
213
|
+
`The package manager reported warnings. Review its output with --debug and check your project's dependency audit.`
|
|
214
|
+
);
|
|
215
|
+
resolve2();
|
|
216
|
+
} else {
|
|
217
|
+
const restricted = /ERR_ACCESS_DENIED|Access to this API has been restricted/.test(logs);
|
|
218
|
+
reject(
|
|
219
|
+
new SdkInstallError(
|
|
220
|
+
restricted ? `Your Node or package-manager permission policy blocked ${label} installation. Run the manual install command in your normal terminal.` : `${label} installation failed (${project.packageManager} exit ${code}). Rerun with --debug to inspect package-manager output.`
|
|
221
|
+
)
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (error instanceof SdkInstallError) throw error;
|
|
228
|
+
throw new SdkInstallError(
|
|
229
|
+
"Unable to prepare a writable SDK install cache inside the project. Run the manual install command in your normal terminal.",
|
|
230
|
+
{ cause: error }
|
|
231
|
+
);
|
|
232
|
+
} finally {
|
|
233
|
+
if (cache) {
|
|
234
|
+
try {
|
|
235
|
+
await rm(cache, { recursive: true, force: true });
|
|
236
|
+
} catch {
|
|
237
|
+
options.onWarning?.("Unable to remove the temporary .userreason-npm-* cache inside your project.");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async function verifyConfiguration(config, hostOrigin, fetcher = fetch) {
|
|
243
|
+
const response = await fetcher(
|
|
244
|
+
`${config.apiBaseUrl}/v1/sdk/${config.siteId}/config`,
|
|
245
|
+
{ headers: { Origin: hostOrigin }, redirect: "error", signal: AbortSignal.timeout(2e4) }
|
|
246
|
+
);
|
|
247
|
+
if (!response.ok)
|
|
248
|
+
throw new Error(
|
|
249
|
+
`Site configuration check failed (${response.status}); verify its allowed origins and Worker deployment`
|
|
250
|
+
);
|
|
251
|
+
const data = await response.json();
|
|
252
|
+
if (config.workspaceId !== void 0 && data.workspace_id !== config.workspaceId || data.site_id !== config.siteId || !data.payload || typeof data.payload !== "object")
|
|
253
|
+
throw new Error("Worker returned a configuration for a different or invalid site");
|
|
254
|
+
return { configured: true, hostConnectionVerified: false };
|
|
255
|
+
}
|
|
256
|
+
function devCommand(project) {
|
|
257
|
+
if (!project.devScript) return void 0;
|
|
258
|
+
if (project.packageManager === "npm") return project.devScript === "start" ? "npm start" : "npm run dev";
|
|
259
|
+
return `${project.packageManager} ${project.devScript}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export {
|
|
263
|
+
inspectProject,
|
|
264
|
+
DEFAULT_API,
|
|
265
|
+
publicOptions,
|
|
266
|
+
initializationSource,
|
|
267
|
+
integrationSource,
|
|
268
|
+
writeIntegration,
|
|
269
|
+
cliCommand,
|
|
270
|
+
hasCompatibleSdk,
|
|
271
|
+
sdkInstallCommand,
|
|
272
|
+
hasCompatibleCli,
|
|
273
|
+
compatibleCliVersion,
|
|
274
|
+
cliInstallCommand,
|
|
275
|
+
SdkInstallError,
|
|
276
|
+
installSdk,
|
|
277
|
+
installCli,
|
|
278
|
+
verifyConfiguration,
|
|
279
|
+
devCommand
|
|
280
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
detectInstallation
|
|
4
|
+
} from "./chunk-5SC2K42Q.js";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_API
|
|
7
|
+
} from "./chunk-YMSGDC4B.js";
|
|
8
|
+
import {
|
|
9
|
+
apiUrl,
|
|
10
|
+
configDirectory,
|
|
11
|
+
uuid
|
|
12
|
+
} from "./chunk-GGARVSPT.js";
|
|
13
|
+
|
|
14
|
+
// src/state.ts
|
|
15
|
+
import { mkdir, readFile, writeFile, rename, rm, realpath, lstat } from "node:fs/promises";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
18
|
+
function storageBlocked(error) {
|
|
19
|
+
if (!error || typeof error !== "object") return false;
|
|
20
|
+
const value = error;
|
|
21
|
+
return ["ERR_ACCESS_DENIED", "EACCES", "EPERM", "EROFS"].includes(value.code ?? "") || value.cause !== void 0 && storageBlocked(value.cause);
|
|
22
|
+
}
|
|
23
|
+
function parse(value) {
|
|
24
|
+
if (typeof value.siteId !== "string" || value.workspaceId !== void 0 && typeof value.workspaceId !== "string" || value.apiBaseUrl !== void 0 && typeof value.apiBaseUrl !== "string") throw new Error("Invalid saved SDK configuration");
|
|
25
|
+
return {
|
|
26
|
+
siteId: uuid(value.siteId),
|
|
27
|
+
apiBaseUrl: apiUrl(value.apiBaseUrl ?? DEFAULT_API),
|
|
28
|
+
...value.workspaceId !== void 0 ? { workspaceId: uuid(value.workspaceId) } : {}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
async function read(path) {
|
|
32
|
+
try {
|
|
33
|
+
const info = await lstat(path);
|
|
34
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > 4096) throw new Error("Invalid saved installation file");
|
|
35
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
36
|
+
if (value.schemaVersion !== 1) throw new Error("Unsupported saved installation");
|
|
37
|
+
return parse(value);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error.code === "ENOENT") return;
|
|
40
|
+
throw new Error("Unable to read saved UserReason installation. Review it before rerunning.", { cause: error });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function statePath(directory, api) {
|
|
44
|
+
const key = createHash("sha256").update(JSON.stringify([await realpath(directory), api])).digest("hex");
|
|
45
|
+
return join(configDirectory(), "wizard", `${key}.json`);
|
|
46
|
+
}
|
|
47
|
+
async function savedInstallation(project, api, onStorageBlocked) {
|
|
48
|
+
const detected = await detectInstallation(project);
|
|
49
|
+
if (detected) return parse(detected);
|
|
50
|
+
const path = await statePath(project.directory, api);
|
|
51
|
+
let saved;
|
|
52
|
+
try {
|
|
53
|
+
saved = await read(path);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (!storageBlocked(error)) throw error;
|
|
56
|
+
onStorageBlocked?.();
|
|
57
|
+
}
|
|
58
|
+
return saved ?? await read(join(project.directory, "userreason.config.json"));
|
|
59
|
+
}
|
|
60
|
+
async function saveInstallation(directory, config) {
|
|
61
|
+
const path = await statePath(directory, config.apiBaseUrl);
|
|
62
|
+
const value = JSON.stringify({ schemaVersion: 1, ...parse(config) }) + "\n";
|
|
63
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
64
|
+
let created = false;
|
|
65
|
+
try {
|
|
66
|
+
await mkdir(join(configDirectory(), "wizard"), { recursive: true, mode: 448 });
|
|
67
|
+
await writeFile(temporary, value, { flag: "wx", mode: 384 });
|
|
68
|
+
created = true;
|
|
69
|
+
await rename(temporary, path);
|
|
70
|
+
created = false;
|
|
71
|
+
return true;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (storageBlocked(error)) return false;
|
|
74
|
+
throw error;
|
|
75
|
+
} finally {
|
|
76
|
+
if (created) {
|
|
77
|
+
try {
|
|
78
|
+
await rm(temporary, { force: true });
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (!storageBlocked(error)) throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function clearInstallation(directory, api) {
|
|
86
|
+
const path = await statePath(directory, api);
|
|
87
|
+
try {
|
|
88
|
+
await rm(path, { force: true });
|
|
89
|
+
return true;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (storageBlocked(error)) return false;
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function resolveInstallation(config, origin) {
|
|
96
|
+
if (config.workspaceId) return { ...config, workspaceId: config.workspaceId };
|
|
97
|
+
const response = await fetch(`${config.apiBaseUrl}/v1/sdk/${config.siteId}/config`, {
|
|
98
|
+
headers: { Origin: origin ?? "https://installation.userreason.invalid" },
|
|
99
|
+
redirect: "error",
|
|
100
|
+
signal: AbortSignal.timeout(2e4)
|
|
101
|
+
});
|
|
102
|
+
if (!response.ok) throw new Error(`Unable to resolve the existing site (${response.status}). For a restricted site pass --origin, or supply --workspace and --site.`);
|
|
103
|
+
const text = await response.text();
|
|
104
|
+
if (text.length > 2097152) throw new Error("Site configuration is too large");
|
|
105
|
+
const value = JSON.parse(text);
|
|
106
|
+
if (value.site_id !== config.siteId) throw new Error("Site configuration scope mismatch");
|
|
107
|
+
return { ...config, workspaceId: uuid(value.workspace_id) };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export {
|
|
111
|
+
savedInstallation,
|
|
112
|
+
saveInstallation,
|
|
113
|
+
clearInstallation,
|
|
114
|
+
resolveInstallation
|
|
115
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
offerCliAccess
|
|
4
|
+
} from "./chunk-HACTNBV3.js";
|
|
5
|
+
import "./chunk-SJVLAYGG.js";
|
|
6
|
+
import "./chunk-GGARVSPT.js";
|
|
7
|
+
import "./chunk-WL3NOIM6.js";
|
|
8
|
+
export {
|
|
9
|
+
offerCliAccess
|
|
10
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
BrowserConnectionUnavailable,
|
|
4
|
+
compatibilityCredentials,
|
|
5
|
+
connectInstallation,
|
|
6
|
+
memoryCredentials
|
|
7
|
+
} from "./chunk-3RDJORRS.js";
|
|
8
|
+
import {
|
|
9
|
+
CredentialStoreError
|
|
10
|
+
} from "./chunk-GGARVSPT.js";
|
|
11
|
+
import "./chunk-WL3NOIM6.js";
|
|
12
|
+
export {
|
|
13
|
+
BrowserConnectionUnavailable,
|
|
14
|
+
CredentialStoreError,
|
|
15
|
+
compatibilityCredentials,
|
|
16
|
+
connectInstallation,
|
|
17
|
+
memoryCredentials
|
|
18
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
globalCliInstallCommand,
|
|
4
|
+
offerGlobalCli
|
|
5
|
+
} from "./chunk-JD5U4U26.js";
|
|
6
|
+
import "./chunk-SJVLAYGG.js";
|
|
7
|
+
import "./chunk-YMSGDC4B.js";
|
|
8
|
+
import "./chunk-OEGNGGZ2.js";
|
|
9
|
+
import "./chunk-GGARVSPT.js";
|
|
10
|
+
import "./chunk-WL3NOIM6.js";
|
|
11
|
+
export {
|
|
12
|
+
globalCliInstallCommand,
|
|
13
|
+
offerGlobalCli
|
|
14
|
+
};
|