@userreason/wizard 0.1.0 → 0.2.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 +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-5Z5J6PY5.js +258 -0
- package/dist/chunk-6CJ4ANMN.js +70 -0
- package/dist/chunk-6DCCEVPD.js +877 -0
- package/dist/chunk-FC7PZNYW.js +115 -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-KV6GSBTQ.js +165 -0
- package/dist/chunk-OEGNGGZ2.js +7403 -0
- package/dist/chunk-SJVLAYGG.js +1477 -0
- package/dist/chunk-WL3NOIM6.js +42 -0
- package/dist/chunk-YMSGDC4B.js +280 -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,153 @@
|
|
|
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
|
+
CredentialStoreError,
|
|
4
|
+
object,
|
|
5
|
+
string,
|
|
6
|
+
uuid
|
|
7
|
+
} from "./chunk-GGARVSPT.js";
|
|
8
|
+
|
|
9
|
+
// src/connection.ts
|
|
10
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
11
|
+
var BrowserConnectionUnavailable = class extends Error {
|
|
12
|
+
};
|
|
13
|
+
function retryableNetworkError(error) {
|
|
14
|
+
if (!(error instanceof Error)) return false;
|
|
15
|
+
const cause = error.cause;
|
|
16
|
+
const rawCode = error.code ?? cause?.code;
|
|
17
|
+
const code = typeof rawCode === "string" ? rawCode : void 0;
|
|
18
|
+
if (code && !["ECONNRESET", "ETIMEDOUT", "EAI_AGAIN", "ECONNREFUSED", "ENETUNREACH"].includes(code)) return false;
|
|
19
|
+
return error.name === "TimeoutError" || error.name === "AbortError" || error instanceof TypeError && error.message === "fetch failed";
|
|
20
|
+
}
|
|
21
|
+
function memoryCredentials(session = null) {
|
|
22
|
+
let value = session;
|
|
23
|
+
return {
|
|
24
|
+
load: async () => value,
|
|
25
|
+
save: async (next) => {
|
|
26
|
+
value = next;
|
|
27
|
+
},
|
|
28
|
+
clear: async () => {
|
|
29
|
+
value = null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
async function compatibilityCredentials(existing, sessionOnly, confirm) {
|
|
34
|
+
if (sessionOnly) return { credentials: memoryCredentials(), session: null, sessionOnly: true };
|
|
35
|
+
try {
|
|
36
|
+
return { credentials: existing, session: await existing.load(), sessionOnly: false };
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (!(error instanceof CredentialStoreError) || error.code === "invalid_session" || !await confirm(error)) throw error;
|
|
39
|
+
return { credentials: memoryCredentials(), session: null, sessionOnly: true };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function connectInstallation(config, input, onBrowser, options = {}) {
|
|
43
|
+
const fetcher = options.fetcher ?? fetch, now = options.now ?? Date.now;
|
|
44
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
45
|
+
const discovery = await fetcher(`${config.apiUrl}/onboarding/v1/auth-config?installation=1`, {
|
|
46
|
+
redirect: "error",
|
|
47
|
+
signal: AbortSignal.timeout(1e4)
|
|
48
|
+
});
|
|
49
|
+
if (!discovery.ok) throw new Error(`Unable to discover installation login (${discovery.status})`);
|
|
50
|
+
const capabilities = object(await discovery.json(), "installation capabilities");
|
|
51
|
+
if (capabilities.client_id !== config.clientId)
|
|
52
|
+
throw new Error("Installation authentication client does not match the configured API");
|
|
53
|
+
options.onCapabilities?.({ unrestrictedOrigins: capabilities.unrestricted_origins === 1 });
|
|
54
|
+
if (capabilities.installation_login !== 1)
|
|
55
|
+
throw new BrowserConnectionUnavailable("This API uses device authentication");
|
|
56
|
+
const verifier = randomBytes(32).toString("hex"), challenge = createHash("sha256").update(verifier).digest("hex");
|
|
57
|
+
async function post(path, body, timeout = 2e4) {
|
|
58
|
+
return fetcher(`${config.apiUrl}/onboarding/v1/installations/${path}`, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: { "Content-Type": "application/json" },
|
|
61
|
+
body: JSON.stringify(body),
|
|
62
|
+
redirect: "error",
|
|
63
|
+
signal: AbortSignal.timeout(timeout)
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const started = await post("start", {
|
|
67
|
+
challenge,
|
|
68
|
+
project_name: input.projectName,
|
|
69
|
+
workspace_id: input.workspaceId ?? null,
|
|
70
|
+
workspace_name: input.workspaceName ?? null,
|
|
71
|
+
site_id: input.siteId ?? null
|
|
72
|
+
});
|
|
73
|
+
if (started.status === 404)
|
|
74
|
+
throw new BrowserConnectionUnavailable("This API does not support browser installation yet");
|
|
75
|
+
if (!started.ok)
|
|
76
|
+
throw new Error(`Unable to start installation (${started.status}). Check the API deployment.`);
|
|
77
|
+
const data = object(await started.json(), "installation request"), id = uuid(string(data.id, "installation ID"));
|
|
78
|
+
const target = new URL(string(data.browser_url, "browser URL"));
|
|
79
|
+
const ticket = string(data.ticket, "installation ticket");
|
|
80
|
+
if (target.protocol !== "https:" && !(target.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(target.hostname)) || target.username || target.password || target.pathname !== "/install" || target.searchParams.get("id") !== id || new URLSearchParams(target.hash.slice(1)).get("request") !== ticket)
|
|
81
|
+
throw new Error("Invalid installation browser URL");
|
|
82
|
+
const expires = Date.parse(string(data.expires_at, "installation expiry"));
|
|
83
|
+
if (!Number.isFinite(expires) || expires <= now() || expires > now() + 901e3 || data.code !== challenge.slice(0, 8).toUpperCase())
|
|
84
|
+
throw new Error("Invalid installation request");
|
|
85
|
+
await onBrowser(target.href, String(data.code));
|
|
86
|
+
const waitingSince = now();
|
|
87
|
+
const deadline = Math.min(expires, waitingSince + 6e5);
|
|
88
|
+
let failures = 0, lastFailure;
|
|
89
|
+
const progress = (phase, httpStatus) => options.onProgress?.({
|
|
90
|
+
phase,
|
|
91
|
+
elapsedSeconds: Math.max(0, Math.floor((now() - waitingSince) / 1e3)),
|
|
92
|
+
consecutiveFailures: failures,
|
|
93
|
+
...httpStatus === void 0 ? {} : { httpStatus }
|
|
94
|
+
});
|
|
95
|
+
progress("waiting");
|
|
96
|
+
while (now() < deadline) {
|
|
97
|
+
await sleep(Math.min(failures ? 5e3 : 3e3, deadline - now()));
|
|
98
|
+
if (now() >= deadline) break;
|
|
99
|
+
let response;
|
|
100
|
+
try {
|
|
101
|
+
response = await post("exchange", { id, ticket, verifier }, Math.max(1, Math.min(2e4, deadline - now())));
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (!retryableNetworkError(error)) throw error;
|
|
104
|
+
failures++;
|
|
105
|
+
lastFailure = "network connection";
|
|
106
|
+
progress("retrying");
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (response.status === 429 || response.status >= 500) {
|
|
110
|
+
failures++;
|
|
111
|
+
lastFailure = `HTTP ${response.status}`;
|
|
112
|
+
progress("retrying", response.status);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (!response.ok)
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Installation connection expired or failed (${response.status}). Run the wizard again.`
|
|
118
|
+
);
|
|
119
|
+
const result = object(await response.json(), "installation connection");
|
|
120
|
+
failures = 0;
|
|
121
|
+
lastFailure = void 0;
|
|
122
|
+
if (result.status === "pending") {
|
|
123
|
+
progress("waiting");
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (result.status !== "approved") throw new Error("Installation was declined.");
|
|
127
|
+
const user = object(result.user, "installation user"), expiry = Date.parse(string(result.expires_at, "access expiry"));
|
|
128
|
+
if (!Number.isFinite(expiry) || expiry > expires || expiry < now() + 6e4)
|
|
129
|
+
throw new Error("Installation access expired. Run the wizard again.");
|
|
130
|
+
return {
|
|
131
|
+
session: {
|
|
132
|
+
...config,
|
|
133
|
+
accessToken: string(result.access_token, "installation token"),
|
|
134
|
+
refreshToken: "",
|
|
135
|
+
expiresAt: expiry,
|
|
136
|
+
userId: uuid(string(user.id, "user ID")),
|
|
137
|
+
email: string(user.email, "email")
|
|
138
|
+
},
|
|
139
|
+
workspaceId: result.workspace_id === null ? null : uuid(string(result.workspace_id, "workspace ID")),
|
|
140
|
+
workspaceName: result.workspace_name === null ? null : string(result.workspace_name, "workspace name"),
|
|
141
|
+
grantId: uuid(string(result.grant_id, "grant ID"))
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (failures) throw new Error(`Unable to confirm browser approval: the API is still failing (${lastFailure}). Run the wizard again when the connection is available.`);
|
|
145
|
+
throw new Error("Browser approval timed out. Finish sign-in and approval in the browser, then run the wizard again.");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export {
|
|
149
|
+
BrowserConnectionUnavailable,
|
|
150
|
+
memoryCredentials,
|
|
151
|
+
compatibilityCredentials,
|
|
152
|
+
connectInstallation
|
|
153
|
+
};
|
|
@@ -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
|
+
};
|