@userreason/wizard 0.2.1 → 0.3.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 +95 -0
- package/dist/agent-note.js +7 -6
- package/dist/agent-skill.js +6 -5
- package/dist/analytics.js +9 -0
- package/dist/bin.js +222 -51
- package/dist/chunk-4INLEQE2.js +196 -0
- package/dist/{chunk-GGARVSPT.js → chunk-7AAH6C4L.js} +9198 -5670
- package/dist/{chunk-ULHKVBPD.js → chunk-7O7H3PIM.js} +7 -7
- package/dist/chunk-CSZW2ULJ.js +152 -0
- package/dist/{chunk-3RDJORRS.js → chunk-DMXUXEQI.js} +1 -1
- package/dist/{chunk-SJVLAYGG.js → chunk-EVNEX5N5.js} +8 -1
- package/dist/chunk-KF37D5WL.js +44 -0
- package/dist/{chunk-5SC2K42Q.js → chunk-LEJFE4U5.js} +2 -2
- package/dist/{chunk-OEGNGGZ2.js → chunk-NMHTFC5Q.js} +10 -3
- package/dist/{chunk-6CJ4ANMN.js → chunk-NNPYUATH.js} +1 -1
- package/dist/chunk-PJQOSDYS.js +50 -0
- package/dist/chunk-Q5U5UOQN.js +288 -0
- package/dist/{chunk-5Z5J6PY5.js → chunk-SB3AYQWG.js} +6 -6
- package/dist/{chunk-YMSGDC4B.js → chunk-TOID3NYP.js} +40 -11
- package/dist/{chunk-JD5U4U26.js → chunk-TRBSH5G7.js} +6 -6
- package/dist/{chunk-ZGZOBW2B.js → chunk-XASPGFYK.js} +3 -3
- package/dist/{chunk-HACTNBV3.js → chunk-XPFKXLIZ.js} +3 -3
- package/dist/chunk-ZHJV247H.js +31 -0
- package/dist/cli-access.js +3 -3
- package/dist/connection.js +2 -2
- package/dist/first-survey.js +11 -0
- package/dist/footer.js +17 -0
- package/dist/global-cli.js +6 -5
- package/dist/install-diagnostics.js +9 -0
- package/dist/integrate.js +5 -4
- package/dist/project.js +8 -3
- package/dist/registry.js +5 -3
- package/dist/setup-flow.js +25 -0
- package/dist/setup.js +33 -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 +6 -5
- package/dist/ui.js +2 -2
- package/package.json +1 -2
|
@@ -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-LEJFE4U5.js";
|
|
5
|
+
import {
|
|
6
|
+
require_typescript
|
|
7
|
+
} from "./chunk-7AAH6C4L.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
|
+
};
|