@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.
@@ -0,0 +1,258 @@
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
+ cliCommand
7
+ } from "./chunk-YMSGDC4B.js";
8
+ import {
9
+ require_dist
10
+ } from "./chunk-OEGNGGZ2.js";
11
+ import {
12
+ __toESM
13
+ } from "./chunk-WL3NOIM6.js";
14
+
15
+ // src/agent-skill.ts
16
+ var import_yaml = __toESM(require_dist(), 1);
17
+ import { constants } from "node:fs";
18
+ import { createHash } from "node:crypto";
19
+ import { lstat, readFile, readdir, mkdir, open, unlink, rmdir } from "node:fs/promises";
20
+ import { join, relative } from "node:path";
21
+ var directories = [".agents", ".cursor", ".claude", ".codex"];
22
+ var files = ["references/management.md", "SKILL.md"];
23
+ var LEGACY_MANIFEST = ".userreason.json";
24
+ var VERSION_KEY = "userreason-version";
25
+ var FINGERPRINT_KEY = "userreason-content-sha256";
26
+ var EARLIER = [
27
+ { "SKILL.md": "ac7d0e8a407e7bfb8fd734ba38aa548df52df3251ac1826208e142a2dc96b348", "references/management.md": "613981e7ad7eb1f92c2fda73396df532783b3622384c46c0ff608ae9ba79d8d1" }
28
+ ];
29
+ var hash = (content) => createHash("sha256").update(content).digest("hex");
30
+ var same = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([path, value]) => b[path] === value);
31
+ var fingerprint = (files2) => hash(JSON.stringify(Object.entries(files2).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)));
32
+ function frontmatter(content) {
33
+ const match = /^(---\r?\n)([\s\S]*?\r?\n)---(?=\r?\n|$)/.exec(content);
34
+ if (!match) return null;
35
+ const document = (0, import_yaml.parseDocument)(match[2]);
36
+ if (document.errors.length || !(0, import_yaml.isMap)(document.contents)) return null;
37
+ return { document, start: match[1].length, end: match[1].length + match[2].length };
38
+ }
39
+ function trackedSkill(content) {
40
+ const parsed = frontmatter(content);
41
+ const metadata = parsed?.document.get("metadata", true);
42
+ if (!parsed || !(0, import_yaml.isMap)(metadata)) return null;
43
+ const version = metadata.get(VERSION_KEY, true), digest = metadata.get(FINGERPRINT_KEY, true);
44
+ if (!(0, import_yaml.isScalar)(version) || typeof version.value !== "string" || !(0, import_yaml.isScalar)(digest) || typeof digest.value !== "string" || !/^[a-f0-9]{64}$/.test(digest.value) || !digest.range) return null;
45
+ const [start, end] = digest.range;
46
+ return {
47
+ fingerprint: digest.value,
48
+ content: content.slice(0, parsed.start + start) + '""' + content.slice(parsed.start + end)
49
+ };
50
+ }
51
+ async function installed(path) {
52
+ const current = {}, snapshot = {}, normalized = {};
53
+ let recorded = null;
54
+ let expectedFingerprint = null;
55
+ for (const folder of ["", "references"]) {
56
+ let entries;
57
+ try {
58
+ entries = await readdir(join(path, folder), { withFileTypes: true });
59
+ } catch (error) {
60
+ if (folder && error.code === "ENOENT") continue;
61
+ throw error;
62
+ }
63
+ for (const entry of entries) {
64
+ if (!folder && entry.name === "references" && entry.isDirectory() && !entry.isSymbolicLink()) continue;
65
+ const name = folder ? `${folder}/${entry.name}` : entry.name, metadata = await lstat(join(path, name));
66
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1 || metadata.size > 1e6) return null;
67
+ const content = await readFile(join(path, name));
68
+ snapshot[name] = hash(content);
69
+ if (name !== LEGACY_MANIFEST) {
70
+ current[name] = snapshot[name];
71
+ const tracked = name === "SKILL.md" ? trackedSkill(content.toString("utf8")) : null;
72
+ normalized[name] = tracked ? hash(tracked.content) : current[name];
73
+ if (tracked) expectedFingerprint = tracked.fingerprint;
74
+ continue;
75
+ }
76
+ try {
77
+ const parsed = JSON.parse(content.toString("utf8"));
78
+ if (!parsed.files || typeof parsed.files !== "object" || Object.values(parsed.files).some((value) => typeof value !== "string")) return null;
79
+ recorded = parsed.files;
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+ }
85
+ return {
86
+ current,
87
+ recorded,
88
+ snapshot,
89
+ managed: expectedFingerprint !== null ? fingerprint(normalized) === expectedFingerprint : recorded !== null ? same(current, recorded) : EARLIER.some((earlier) => same(current, earlier))
90
+ };
91
+ }
92
+ async function safeDirectory(root, parts, create = false) {
93
+ let path = root;
94
+ for (const part of parts) {
95
+ path = join(path, part);
96
+ let metadata;
97
+ try {
98
+ metadata = await lstat(path);
99
+ } catch (error) {
100
+ if (error.code !== "ENOENT") throw error;
101
+ if (!create) return false;
102
+ try {
103
+ await mkdir(path);
104
+ } catch (error2) {
105
+ if (error2.code !== "EEXIST") throw error2;
106
+ }
107
+ metadata = await lstat(path);
108
+ }
109
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw new Error("The skill directory needs review.");
110
+ }
111
+ return true;
112
+ }
113
+ async function packaged(project) {
114
+ const assets = await Promise.all(files.map(async (path) => ({
115
+ path,
116
+ content: (await readFile(new URL(`./skills/userreason/${path}`, import.meta.url), "utf8")).replaceAll("npx --no-install userreason", cliCommand(project))
117
+ })));
118
+ const skill = assets.find((file) => file.path === "SKILL.md");
119
+ const parsed = frontmatter(skill.content);
120
+ if (!parsed) throw new Error("The packaged skill needs valid YAML frontmatter.");
121
+ const { version } = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
122
+ if (typeof version !== "string") throw new Error("The wizard package needs a version.");
123
+ parsed.document.setIn(["metadata", VERSION_KEY], version);
124
+ parsed.document.setIn(["metadata", FINGERPRINT_KEY], "");
125
+ skill.content = skill.content.slice(0, parsed.start) + parsed.document.toString({ lineWidth: 0 }) + skill.content.slice(parsed.end);
126
+ const digest = fingerprint(Object.fromEntries(assets.map((file) => [file.path, hash(file.content)])));
127
+ const ready = frontmatter(skill.content);
128
+ const scalar = ready.document.getIn(["metadata", FINGERPRINT_KEY], true);
129
+ if (!(0, import_yaml.isScalar)(scalar) || !scalar.range) throw new Error("The packaged skill needs tracking metadata.");
130
+ skill.content = skill.content.slice(0, ready.start + scalar.range[0]) + JSON.stringify(digest) + skill.content.slice(ready.start + scalar.range[1]);
131
+ return assets;
132
+ }
133
+ async function updateAgentSkill(plan) {
134
+ const { folder, snapshot } = plan.update;
135
+ await safeDirectory(plan.directory, [folder, "skills", "userreason"]);
136
+ const now = await installed(plan.path);
137
+ if (!now || !same(now.snapshot, snapshot)) throw new Error("The skill changed after the preview.");
138
+ await safeDirectory(plan.directory, [folder, "skills", "userreason", "references"], true);
139
+ const write = async (name, content) => {
140
+ const handle = await open(join(plan.path, name), constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | (constants.O_NOFOLLOW ?? 0), 420);
141
+ try {
142
+ if (!(await handle.stat()).isFile()) throw new Error("The skill directory needs review.");
143
+ await handle.writeFile(content, "utf8");
144
+ } finally {
145
+ await handle.close();
146
+ }
147
+ };
148
+ for (const file of plan.files) await write(file.path, file.content);
149
+ for (const name of Object.keys(now.current)) if (!plan.files.some((file) => file.path === name)) await unlink(join(plan.path, name));
150
+ if (Object.hasOwn(now.snapshot, LEGACY_MANIFEST)) await unlink(join(plan.path, LEGACY_MANIFEST));
151
+ }
152
+ async function planAgentSkill(project) {
153
+ const host = { ...project, packageManager: project.packageManager ?? "npm" };
154
+ for (const folder of directories) {
155
+ if (!await safeDirectory(project.directory, [folder, "skills"])) continue;
156
+ const path = join(project.directory, folder, "skills", "userreason");
157
+ let metadata;
158
+ try {
159
+ metadata = await lstat(path);
160
+ } catch (error) {
161
+ if (error.code !== "ENOENT") throw error;
162
+ continue;
163
+ }
164
+ const preserved = {
165
+ status: metadata.isDirectory() && !metadata.isSymbolicLink() ? "already_present" : "skipped",
166
+ path: relative(project.directory, path),
167
+ reason: "existing_skill_preserved"
168
+ };
169
+ if (preserved.status !== "already_present") return preserved;
170
+ const found = await installed(path), assets2 = await packaged(host);
171
+ if (!found?.managed) return preserved;
172
+ if (!Object.hasOwn(found.snapshot, LEGACY_MANIFEST) && same(found.current, Object.fromEntries(assets2.map((file) => [file.path, hash(file.content)])))) return { ...preserved, reason: "up_to_date" };
173
+ return { directory: project.directory, path, files: assets2, update: { folder, snapshot: found.snapshot } };
174
+ }
175
+ const assets = await packaged(host);
176
+ return { directory: project.directory, path: join(project.directory, ".agents/skills/userreason"), files: assets };
177
+ }
178
+ async function applyAgentSkill(plan) {
179
+ if (plan.update) return updateAgentSkill(plan);
180
+ await safeDirectory(plan.directory, [".agents", "skills"], true);
181
+ await mkdir(plan.path);
182
+ const metadata = await lstat(plan.path);
183
+ const created = [];
184
+ try {
185
+ await safeDirectory(plan.directory, [".agents", "skills", "userreason", "references"], true);
186
+ for (const file of plan.files) {
187
+ await safeDirectory(plan.directory, [".agents", "skills", "userreason", ...file.path.split("/").slice(0, -1)]);
188
+ const current = await lstat(plan.path);
189
+ if (current.dev !== metadata.dev || current.ino !== metadata.ino) throw new Error("The skill directory changed after creation.");
190
+ const path = join(plan.path, file.path);
191
+ const handle = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 420);
192
+ try {
193
+ const info = await handle.stat();
194
+ created.push({ path, dev: info.dev, ino: info.ino });
195
+ await handle.writeFile(file.content, "utf8");
196
+ } finally {
197
+ await handle.close();
198
+ }
199
+ }
200
+ } catch (error) {
201
+ const current = await lstat(plan.path).catch(() => void 0);
202
+ if (current?.isDirectory() && !current.isSymbolicLink() && current.dev === metadata.dev && current.ino === metadata.ino) {
203
+ for (const file of created.reverse()) {
204
+ const info = await lstat(file.path).catch(() => void 0);
205
+ if (info?.isFile() && !info.isSymbolicLink() && info.dev === file.dev && info.ino === file.ino)
206
+ await unlink(file.path).catch(() => {
207
+ });
208
+ }
209
+ await rmdir(join(plan.path, "references")).catch(() => {
210
+ });
211
+ await rmdir(plan.path).catch(() => {
212
+ });
213
+ }
214
+ throw error;
215
+ }
216
+ }
217
+ async function offerAgentSkill(project, terminal, options) {
218
+ if (!options.installed || options.skip || !terminal.interactive && !options.requested)
219
+ return { status: "skipped", reason: options.installed ? "not_requested" : "installation_pending" };
220
+ try {
221
+ const plan = await planAgentSkill(project);
222
+ if (!("files" in plan)) {
223
+ terminal.info(plan.reason === "up_to_date" ? `UserReason agent skill is up to date at ${plan.path}` : `Existing UserReason agent skill preserved at ${plan.path}`);
224
+ return plan;
225
+ }
226
+ if (plan.update) {
227
+ const where = relative(plan.directory, plan.path);
228
+ if (options.preview !== false) terminal.note(`The UserReason skill in ${where} is an earlier version that nobody edited.
229
+ This replaces its files with the current version.`, "Optional skill update");
230
+ if (!options.requested && !await terminal.confirm("Update the UserReason feedback skill?", true, { recommended: true }))
231
+ return { status: "declined", path: where };
232
+ await applyAgentSkill(plan);
233
+ terminal.success(`Feedback skill updated in ${where}`);
234
+ return { status: "updated", path: where };
235
+ }
236
+ const entry = plan.files.find((file) => file.path === "SKILL.md").content;
237
+ const description = /^description: (.+)$/m.exec(entry)[1];
238
+ if (options.preview !== false) terminal.note(`${description}
239
+
240
+ Adds .agents/skills/userreason/SKILL.md and references/management.md.
241
+ Agents can propose feedback investigations, read the current documentation and use authenticated management when requested.`, "Optional project agent skill");
242
+ if (!options.requested && !await terminal.confirm("Install the UserReason feedback skill for your agents?", true, { recommended: true }))
243
+ return { status: "declined" };
244
+ await applyAgentSkill(plan);
245
+ terminal.success("Feedback skill installed in .agents/skills/userreason");
246
+ return { status: "added", path: ".agents/skills/userreason" };
247
+ } catch (error) {
248
+ if (error instanceof Cancelled) return { status: "declined" };
249
+ terminal.warn("Installation is complete. The optional agent skill could not be installed; the skill directory needs review.");
250
+ return { status: "failed", reason: "skill_directory_needs_review" };
251
+ }
252
+ }
253
+
254
+ export {
255
+ planAgentSkill,
256
+ applyAgentSkill,
257
+ offerAgentSkill
258
+ };
@@ -0,0 +1,70 @@
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
+ currentPackageProfile
4
+ } from "./chunk-GGARVSPT.js";
5
+
6
+ // src/startup.ts
7
+ import { readFile } from "node:fs/promises";
8
+ import { spawn } from "node:child_process";
9
+ import { constants } from "node:os";
10
+ var HANDOFF = "USERREASON_WIZARD_HANDOFF";
11
+ async function wizardVersion() {
12
+ const { version } = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
13
+ if (typeof version !== "string") throw new Error("The wizard package needs a version.");
14
+ return version;
15
+ }
16
+ async function ensureCurrentWizard(embedded, version, options = {}) {
17
+ const profile = await currentPackageProfile(embedded, options.fetcher);
18
+ if (profile.kind !== "local-development") return { profile };
19
+ if (version === profile.wizardVersion && embedded.wizardVersion === version && embedded.buildId === profile.buildId && embedded.sdkVersion === profile.sdkVersion && embedded.cliVersion === profile.cliVersion)
20
+ return { profile };
21
+ const env = options.env ?? process.env;
22
+ if (env[HANDOFF]) throw new Error("The relaunched wizard is not the current local build. No setup was performed. Check dev:packages and rerun the command.");
23
+ const args = options.args ?? process.argv.slice(2);
24
+ const nodeOptions = [env.NODE_OPTIONS, ...(options.execArgv ?? process.execArgv).map((arg) => JSON.stringify(arg))].filter(Boolean).join(" ");
25
+ const npmArgs = [
26
+ "-y",
27
+ "--prefer-online",
28
+ `--registry=${profile.registry}`,
29
+ `--@userreason:registry=${profile.registry}`,
30
+ ...nodeOptions ? [`--node-options=${nodeOptions}`] : [],
31
+ `@userreason/wizard@${profile.wizardVersion}`,
32
+ ...args
33
+ ];
34
+ const socket = Boolean(env.SOCKET_CLI_API_BASE_URL);
35
+ options.onRestart?.(`Wizard ${version} is out of date. Starting local wizard ${profile.wizardVersion}\u2026`);
36
+ const exitCode = await new Promise((resolve, reject) => {
37
+ const child = spawn(socket ? "socket" : "npx", socket ? ["npx", ...npmArgs] : npmArgs, {
38
+ cwd: process.cwd(),
39
+ shell: false,
40
+ stdio: "inherit",
41
+ env: { ...env, [HANDOFF]: "1", ...nodeOptions ? { NODE_OPTIONS: nodeOptions } : {} }
42
+ });
43
+ const interrupt = () => {
44
+ child.kill("SIGINT");
45
+ };
46
+ const terminate = () => {
47
+ child.kill("SIGTERM");
48
+ };
49
+ const cleanup = () => {
50
+ process.off("SIGINT", interrupt);
51
+ process.off("SIGTERM", terminate);
52
+ };
53
+ process.on("SIGINT", interrupt);
54
+ process.on("SIGTERM", terminate);
55
+ child.once("error", () => {
56
+ cleanup();
57
+ reject(new Error("Unable to start the current local wizard. No setup was performed. Check your package launcher and its permissions."));
58
+ });
59
+ child.once("close", (code, signal) => {
60
+ cleanup();
61
+ resolve(code ?? (signal ? 128 + constants.signals[signal] : 1));
62
+ });
63
+ });
64
+ return { profile, exitCode };
65
+ }
66
+
67
+ export {
68
+ wizardVersion,
69
+ ensureCurrentWizard
70
+ };