rea-agents 1.0.0 → 1.1.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 +28 -16
- package/dist/application/Doctor.js +4 -4
- package/dist/application/EvidenceBundleFiles.js +4 -3
- package/dist/application/MacHopper.js +175 -0
- package/dist/application/Setup.js +83 -54
- package/dist/cli.js +32 -3
- package/dist/domain/runtimeVersion.js +8 -0
- package/install.sh +79 -164
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
[](https://www.npmjs.com/package/rea-agents)
|
|
12
12
|
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
|
13
13
|
[](#68-tools-for-investigation)
|
|
14
|
-
[](https://nodejs.org/)
|
|
15
15
|
[](LICENSE)
|
|
16
16
|
|
|
17
17
|
[Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [68 tools](#68-tools-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works)
|
|
18
18
|
|
|
19
19
|
<br />
|
|
20
20
|
|
|
21
|
-
<code>
|
|
21
|
+
<code>npm install --global rea-agents && rea setup</code>
|
|
22
22
|
|
|
23
23
|
</div>
|
|
24
24
|
|
|
@@ -81,15 +81,24 @@ REA shows how it reached its conclusions. It does not claim to recover original
|
|
|
81
81
|
|
|
82
82
|
## Quick start
|
|
83
83
|
|
|
84
|
-
###
|
|
84
|
+
### Install the CLI — recommended
|
|
85
85
|
|
|
86
86
|
```bash
|
|
87
|
-
|
|
87
|
+
npm install --global rea-agents
|
|
88
|
+
rea setup
|
|
88
89
|
```
|
|
89
90
|
|
|
90
|
-
|
|
91
|
+
Installing the CLI does not update Homebrew, Node.js, npm, Hopper, or coding-agent configuration. `rea setup` detects what is already present, prints every proposed change, and asks before applying it.
|
|
92
|
+
|
|
93
|
+
REA detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, and Devin. Registrations are additive, backup-first, and read back after writing. You can safely rerun setup.
|
|
91
94
|
|
|
92
|
-
|
|
95
|
+
An optional curl wrapper installs the same CLI package and starts setup only when a terminal is available:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Pass installer options after `bash -s --`, for example `--dry-run`, `--no-setup`, or `--version 1.0.0`. The curl wrapper never installs prerequisites or configures integrations itself. See [Installation and setup](docs/installation.md) for its exact mutation boundary.
|
|
93
102
|
|
|
94
103
|
### With a coding agent — recommended
|
|
95
104
|
|
|
@@ -99,23 +108,23 @@ npx skills add morluto/rea
|
|
|
99
108
|
|
|
100
109
|
Ask your agent to set up REA. It will check your Mac, explain anything it needs to install, ask for approval, and guide you through system prompts. After setup, restart the agent if it asks you to load the full REA toolset.
|
|
101
110
|
|
|
102
|
-
|
|
111
|
+
Review the setup plan, approve it if appropriate, complete Hopper's one-time activation when prompted, then describe the app or feature you want to understand.
|
|
103
112
|
|
|
104
113
|
### From Terminal — no installation
|
|
105
114
|
|
|
106
115
|
```bash
|
|
107
|
-
npx -y rea-agents setup
|
|
116
|
+
npx -y rea-agents setup
|
|
108
117
|
npx -y rea-agents doctor
|
|
109
118
|
npx -y rea-agents analyze /Applications/Notes.app
|
|
110
119
|
```
|
|
111
120
|
|
|
112
|
-
|
|
121
|
+
Review the setup plan before confirming it. Restart a configured coding agent so it loads REA.
|
|
113
122
|
|
|
114
123
|
### From Terminal — install the `rea` command
|
|
115
124
|
|
|
116
125
|
```bash
|
|
117
126
|
npm install --global rea-agents
|
|
118
|
-
rea setup
|
|
127
|
+
rea setup
|
|
119
128
|
rea doctor
|
|
120
129
|
rea analyze /Applications/Notes.app
|
|
121
130
|
```
|
|
@@ -137,9 +146,10 @@ Choose either the no-install commands or the global installation. You do not nee
|
|
|
137
146
|
|
|
138
147
|
- macOS 12 or newer
|
|
139
148
|
- Ubuntu 24.04+, Fedora 41+, or 64-bit Arch Linux
|
|
140
|
-
- Node.js 24.
|
|
149
|
+
- Node.js 22.19+ or 24.11+ (including newer releases)
|
|
150
|
+
- npm; REA does not require or install a particular npm version
|
|
141
151
|
|
|
142
|
-
|
|
152
|
+
Deep binary analysis currently uses [Hopper](https://www.hopperapp.com/), a separate desktop application with its own license. Setup reuses an existing installation. If Hopper is missing, interactive setup proposes the official package and includes it in the confirmation plan. Unattended installation requires `rea setup --yes --install-hopper`.
|
|
143
153
|
|
|
144
154
|
If something is not working, run:
|
|
145
155
|
|
|
@@ -151,7 +161,9 @@ npx -y rea-agents doctor
|
|
|
151
161
|
|
|
152
162
|
### Linux installation and troubleshooting
|
|
153
163
|
|
|
154
|
-
On
|
|
164
|
+
On macOS, approved setup downloads Hopper's official DMG, verifies it, and installs the app into `~/Applications` without Homebrew or administrator privileges. It opens Hopper once for activation; no manual drag-and-drop is required.
|
|
165
|
+
|
|
166
|
+
On Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux, approved setup downloads the matching official Hopper package, restricts downloads to Hopper's public origin, verifies the published size and checksum, and invokes `apt-get`, `dnf`, or `pacman`. When REA is not already running as root, `pkexec` presents the system authorization prompt. REA never invokes `sudo`.
|
|
155
167
|
|
|
156
168
|
The normal Linux launcher is `/opt/hopper/bin/Hopper`. If Hopper was installed elsewhere:
|
|
157
169
|
|
|
@@ -166,7 +178,7 @@ If doctor reports a missing analysis engine even though the file exists, inspect
|
|
|
166
178
|
ldd /opt/hopper/bin/Hopper | grep 'not found'
|
|
167
179
|
```
|
|
168
180
|
|
|
169
|
-
Install the missing distribution packages and rerun `rea setup
|
|
181
|
+
Install the missing distribution packages and rerun `rea setup`. Hopper is a desktop application: real analysis requires an active `DISPLAY` or `WAYLAND_DISPLAY`, plus one-time license activation. The curl installer places the `rea` command in `~/.local/bin` on Linux; add that directory to future shell `PATH` values if it is not already present.
|
|
170
182
|
|
|
171
183
|
REA defaults `HOPPER_LAUNCHER_PATH` to `/Applications/Hopper Disassembler.app/Contents/MacOS/hopper` on macOS and `/opt/hopper/bin/Hopper` on Linux. Explicit configuration always takes precedence.
|
|
172
184
|
|
|
@@ -177,7 +189,7 @@ rea uninstall
|
|
|
177
189
|
rea uninstall --purge-data # also removes only ~/.rea/cache and ~/.rea/state
|
|
178
190
|
```
|
|
179
191
|
|
|
180
|
-
Uninstall preserves Hopper,
|
|
192
|
+
Uninstall preserves Hopper, Node.js, evidence, captures, external evidence roots, unrelated skills, and other MCP servers. It refuses malformed client configuration and never follows purge-data symlinks.
|
|
181
193
|
|
|
182
194
|
### CLI or coding agent?
|
|
183
195
|
|
|
@@ -286,7 +298,7 @@ REA is growing into a toolkit for understanding software across static artifacts
|
|
|
286
298
|
7. **More targets and platforms** — Windows-native providers and ConPTY verification, Linux parity, websites and APIs, mobile artifacts, firmware, document formats, and other software-defined systems.
|
|
287
299
|
8. **Differential reconstruction** — compare artifacts, functions, bundles, protocols, UIs, and process captures; track residual unknowns; verify a reconstruction against observed behavior.
|
|
288
300
|
|
|
289
|
-
Roadmap items describe direction, not shipped support. New providers must produce the same evidence and safety metadata as existing capabilities before they become part of the public workflow.
|
|
301
|
+
Roadmap items describe direction, not shipped support. New providers must produce the same evidence and safety metadata as existing capabilities before they become part of the public workflow. Once REA has multiple optional toolchains, setup can become capability-selective; the consent rules for that future work are recorded in the [installation roadmap](docs/roadmap.md).
|
|
290
302
|
|
|
291
303
|
See the [static-analysis provider evaluation](docs/provider-evaluation.md) for the current research matrix and admission gate.
|
|
292
304
|
|
|
@@ -6,6 +6,7 @@ import { join } from "node:path";
|
|
|
6
6
|
import { execFile } from "node:child_process";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
import { parseBinaryTarget } from "../domain/binaryTarget.js";
|
|
9
|
+
import { supportsNodeVersion } from "../domain/runtimeVersion.js";
|
|
9
10
|
import { probeHomebrew } from "./homebrew.js";
|
|
10
11
|
import { linuxHopperLauncherPath, linuxSharedLibrariesAvailable, readLinuxDistribution, } from "./LinuxHopper.js";
|
|
11
12
|
const execFileAsync = promisify(execFile);
|
|
@@ -18,9 +19,8 @@ const SYSTEM_LINUX_HOPPER = "/opt/hopper/bin/Hopper";
|
|
|
18
19
|
*/
|
|
19
20
|
export const runDoctor = async (target, host = systemDoctorHost()) => {
|
|
20
21
|
const checks = [];
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
remediation: "Install Node.js 24.18 or newer.",
|
|
22
|
+
checks.push(check("node", supportsNodeVersion(host.nodeVersion), host.nodeVersion, {
|
|
23
|
+
remediation: "Install Node.js 22.19+ or 24.11+.",
|
|
24
24
|
classification: "missing_dependency",
|
|
25
25
|
}));
|
|
26
26
|
const macosVersion = host.platform === "darwin" ? await host.macosVersion() : undefined;
|
|
@@ -50,7 +50,7 @@ export const runDoctor = async (target, host = systemDoctorHost()) => {
|
|
|
50
50
|
break;
|
|
51
51
|
}
|
|
52
52
|
checks.push(check("hopper", hopperPath !== undefined, hopperPath, {
|
|
53
|
-
remediation: "Run rea setup
|
|
53
|
+
remediation: "Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH.",
|
|
54
54
|
classification: "missing_analysis_engine",
|
|
55
55
|
}));
|
|
56
56
|
if (target !== undefined)
|
|
@@ -60,8 +60,9 @@ export const writeEvidenceBundle = async (bundle, path, overwrite, policy) => {
|
|
|
60
60
|
if (bytes > policy.maxBytes)
|
|
61
61
|
return err(new EvidenceFileError("write", "too-large"));
|
|
62
62
|
try {
|
|
63
|
-
const
|
|
64
|
-
const
|
|
63
|
+
const requestedPath = resolve(path);
|
|
64
|
+
const canonicalParent = await realpath(dirname(requestedPath));
|
|
65
|
+
const destination = resolve(canonicalParent, basename(requestedPath));
|
|
65
66
|
if (!(await isApproved(destination, policy.roots)))
|
|
66
67
|
return err(new EvidenceFileError("write", "outside-root"));
|
|
67
68
|
const existing = await lstat(destination).catch((cause) => {
|
|
@@ -80,7 +81,7 @@ export const writeEvidenceBundle = async (bundle, path, overwrite, policy) => {
|
|
|
80
81
|
mode: 0o600,
|
|
81
82
|
fsync: true,
|
|
82
83
|
});
|
|
83
|
-
return ok({ path:
|
|
84
|
+
return ok({ path: requestedPath, bytes });
|
|
84
85
|
}
|
|
85
86
|
catch (cause) {
|
|
86
87
|
return err(new EvidenceFileError("write", "io", { cause }));
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { access, mkdir, mkdtemp, readdir, rename, rm, writeFile, } from "node:fs/promises";
|
|
4
|
+
import { homedir, tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
const RELEASES_URL = "https://www.hopperapp.com/include/files-api.php?request=releases&public=true";
|
|
10
|
+
const DOWNLOAD_PREFIX = "https://www.hopperapp.com:443/downloader/public/";
|
|
11
|
+
const MAX_PACKAGE_BYTES = 100_000_000;
|
|
12
|
+
const BUNDLE_IDENTIFIER = "com.cryptic-apps.hopper-web-4";
|
|
13
|
+
/** Download, verify, and install Hopper into the current user's Applications directory. */
|
|
14
|
+
export const installMacHopper = async (host = systemMacHopperInstallHost()) => {
|
|
15
|
+
let temporary;
|
|
16
|
+
let mounted = false;
|
|
17
|
+
try {
|
|
18
|
+
const metadata = await host.download(RELEASES_URL);
|
|
19
|
+
if (!metadata.ok)
|
|
20
|
+
return { status: "failed", reason: "release_metadata" };
|
|
21
|
+
const parsed = releasesSchema.safeParse(parseJson(metadata.bytes));
|
|
22
|
+
if (!parsed.success)
|
|
23
|
+
return { status: "failed", reason: "release_metadata" };
|
|
24
|
+
const release = parsed.data["OS X"];
|
|
25
|
+
if (!release.filename.startsWith(DOWNLOAD_PREFIX))
|
|
26
|
+
return { status: "failed", reason: "release_metadata" };
|
|
27
|
+
const archive = await host.download(release.filename);
|
|
28
|
+
if (!archive.ok)
|
|
29
|
+
return { status: "failed", reason: "download" };
|
|
30
|
+
if (!integrityMatches(archive.bytes, release))
|
|
31
|
+
return { status: "failed", reason: "integrity" };
|
|
32
|
+
const destination = host.destination();
|
|
33
|
+
if (await host.destinationExists(destination))
|
|
34
|
+
return { status: "failed", reason: "destination_exists" };
|
|
35
|
+
temporary = await host.createTemporaryDirectory();
|
|
36
|
+
const packagePath = join(temporary, "hopper.dmg");
|
|
37
|
+
const mountPath = join(temporary, "mounted");
|
|
38
|
+
await host.createMountDirectory(mountPath);
|
|
39
|
+
await host.writePackage(packagePath, archive.bytes);
|
|
40
|
+
if (!(await host.mount(packagePath, mountPath)))
|
|
41
|
+
return { status: "failed", reason: "mount" };
|
|
42
|
+
mounted = true;
|
|
43
|
+
const source = await host.appBundle(mountPath);
|
|
44
|
+
if (source === undefined || !(await host.validBundle(source)))
|
|
45
|
+
return { status: "failed", reason: "bundle" };
|
|
46
|
+
const stage = `${destination}.rea-stage`;
|
|
47
|
+
if (!(await host.installBundle(source, destination, stage)))
|
|
48
|
+
return { status: "failed", reason: "copy" };
|
|
49
|
+
const launcherPath = join(destination, "Contents/MacOS/hopper");
|
|
50
|
+
if (!(await host.launcherReady(launcherPath)))
|
|
51
|
+
return { status: "failed", reason: "launcher_missing" };
|
|
52
|
+
await host.openApplication(destination);
|
|
53
|
+
return { status: "installed", launcherPath };
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return { status: "failed", reason: "download" };
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
if (temporary !== undefined) {
|
|
60
|
+
const mountPath = join(temporary, "mounted");
|
|
61
|
+
if (mounted)
|
|
62
|
+
await host.unmount(mountPath);
|
|
63
|
+
await host.cleanup(temporary);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const systemMacHopperInstallHost = () => ({
|
|
68
|
+
async download(url) {
|
|
69
|
+
const response = await fetch(url, {
|
|
70
|
+
headers: { accept: "application/json", "user-agent": "rea-installer" },
|
|
71
|
+
signal: AbortSignal.timeout(120_000),
|
|
72
|
+
});
|
|
73
|
+
const length = Number(response.headers.get("content-length") ?? "0");
|
|
74
|
+
if (length > MAX_PACKAGE_BYTES)
|
|
75
|
+
return { ok: false, bytes: new Uint8Array() };
|
|
76
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
77
|
+
return { ok: response.ok && bytes.byteLength <= MAX_PACKAGE_BYTES, bytes };
|
|
78
|
+
},
|
|
79
|
+
createTemporaryDirectory: () => mkdtemp(join(tmpdir(), "rea-hopper-mac-")),
|
|
80
|
+
createMountDirectory: (path) => mkdir(path),
|
|
81
|
+
writePackage: (path, bytes) => writeFile(path, bytes, { mode: 0o600 }),
|
|
82
|
+
async mount(packagePath, mountPath) {
|
|
83
|
+
return commandSucceeds("hdiutil", [
|
|
84
|
+
"attach",
|
|
85
|
+
"-readonly",
|
|
86
|
+
"-nobrowse",
|
|
87
|
+
"-mountpoint",
|
|
88
|
+
mountPath,
|
|
89
|
+
packagePath,
|
|
90
|
+
]);
|
|
91
|
+
},
|
|
92
|
+
async unmount(mountPath) {
|
|
93
|
+
await commandSucceeds("hdiutil", ["detach", mountPath, "-force"]);
|
|
94
|
+
},
|
|
95
|
+
async appBundle(mountPath) {
|
|
96
|
+
const entries = await readdir(mountPath, { withFileTypes: true });
|
|
97
|
+
const app = entries.find((entry) => entry.isDirectory() && /^Hopper.*\.app$/iu.test(entry.name));
|
|
98
|
+
return app === undefined ? undefined : join(mountPath, app.name);
|
|
99
|
+
},
|
|
100
|
+
async validBundle(appPath) {
|
|
101
|
+
if (!(await commandSucceeds("codesign", [
|
|
102
|
+
"--verify",
|
|
103
|
+
"--deep",
|
|
104
|
+
"--strict",
|
|
105
|
+
appPath,
|
|
106
|
+
])))
|
|
107
|
+
return false;
|
|
108
|
+
try {
|
|
109
|
+
const identifier = (await execFileAsync("/usr/libexec/PlistBuddy", [
|
|
110
|
+
"-c",
|
|
111
|
+
"Print :CFBundleIdentifier",
|
|
112
|
+
join(appPath, "Contents/Info.plist"),
|
|
113
|
+
])).stdout.trim();
|
|
114
|
+
return identifier === BUNDLE_IDENTIFIER;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
async destinationExists(path) {
|
|
121
|
+
try {
|
|
122
|
+
await access(path);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
async installBundle(source, destination, stage) {
|
|
130
|
+
try {
|
|
131
|
+
await mkdir(join(homedir(), "Applications"), { recursive: true });
|
|
132
|
+
await rm(stage, { recursive: true, force: true });
|
|
133
|
+
await execFileAsync("ditto", [source, stage]);
|
|
134
|
+
await rename(stage, destination);
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
await rm(stage, { recursive: true, force: true });
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
async launcherReady(path) {
|
|
143
|
+
try {
|
|
144
|
+
await access(path);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
async openApplication(path) {
|
|
152
|
+
await execFileAsync("open", [path]);
|
|
153
|
+
},
|
|
154
|
+
cleanup: (path) => rm(path, { recursive: true, force: true }),
|
|
155
|
+
destination: () => join(homedir(), "Applications/Hopper Disassembler.app"),
|
|
156
|
+
});
|
|
157
|
+
const parseJson = (bytes) => JSON.parse(new TextDecoder().decode(bytes));
|
|
158
|
+
const integrityMatches = (bytes, release) => bytes.byteLength === Number(release.file_length) &&
|
|
159
|
+
bytes.byteLength <= MAX_PACKAGE_BYTES &&
|
|
160
|
+
createHash("sha1").update(bytes).digest("hex") === release.file_hash;
|
|
161
|
+
const commandSucceeds = async (command, args) => {
|
|
162
|
+
try {
|
|
163
|
+
await execFileAsync(command, args);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
const releaseSchema = z.object({
|
|
171
|
+
filename: z.string().url(),
|
|
172
|
+
file_length: z.string().regex(/^\d+$/u),
|
|
173
|
+
file_hash: z.string().regex(/^[a-f0-9]{40}$/u),
|
|
174
|
+
});
|
|
175
|
+
const releasesSchema = z.object({ "OS X": releaseSchema });
|
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
import { access, copyFile, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
import { execFile } from "node:child_process";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
4
|
import { z } from "zod";
|
|
7
5
|
import writeFileAtomic from "write-file-atomic";
|
|
8
6
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
9
7
|
import { PRODUCT_IDENTITY } from "../identity.js";
|
|
8
|
+
import { supportsNodeVersion } from "../domain/runtimeVersion.js";
|
|
10
9
|
import { runDoctor, systemDoctorHost } from "./Doctor.js";
|
|
11
|
-
import { probeHomebrew } from "./homebrew.js";
|
|
12
10
|
import { installLinuxHopper, readLinuxDistribution, } from "./LinuxHopper.js";
|
|
13
|
-
|
|
11
|
+
import { installMacHopper } from "./MacHopper.js";
|
|
14
12
|
const registrationCommand = () => process.env.npm_command === "exec"
|
|
15
13
|
? ["npx", "-y", PRODUCT_IDENTITY.packageName, "mcp"]
|
|
16
14
|
: [resolve(process.argv[1] ?? PRODUCT_IDENTITY.cliBinary), "mcp"];
|
|
17
15
|
/**
|
|
18
16
|
* Install prerequisites and configure detected clients idempotently.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* Discovery always precedes mutation. Interactive confirmation or explicit
|
|
18
|
+
* unattended options authorize only the actions represented in the result.
|
|
21
19
|
*/
|
|
22
|
-
export const runSetup = async (
|
|
23
|
-
const
|
|
20
|
+
export const runSetup = async (options, host = systemSetupHost(), confirm) => {
|
|
21
|
+
const appliedActions = [];
|
|
22
|
+
let plannedActions = [];
|
|
24
23
|
const clients = {};
|
|
25
24
|
const fail = async (remediation) => ({
|
|
26
25
|
status: "needs_human",
|
|
27
|
-
|
|
26
|
+
plannedActions,
|
|
27
|
+
appliedActions,
|
|
28
28
|
clients,
|
|
29
29
|
doctor: await host.doctor(),
|
|
30
30
|
remediation,
|
|
@@ -32,43 +32,85 @@ export const runSetup = async (yes, host = systemSetupHost()) => {
|
|
|
32
32
|
const unsupported = await hostRemediation(host);
|
|
33
33
|
if (unsupported !== undefined)
|
|
34
34
|
return fail(unsupported);
|
|
35
|
-
if (host.platform === "darwin" && !(await host.hasHomebrew())) {
|
|
36
|
-
if (!yes)
|
|
37
|
-
return fail("Re-run with --yes to install Homebrew.");
|
|
38
|
-
if (!(await host.installHomebrew()))
|
|
39
|
-
return fail("Homebrew installation was interrupted; resolve the installer prompt and re-run setup.");
|
|
40
|
-
actions.push("installed_homebrew");
|
|
41
|
-
}
|
|
42
35
|
let hopperPath = await host.hopperPath();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
36
|
+
const detectedClients = await host.detectedClients();
|
|
37
|
+
plannedActions = [
|
|
38
|
+
...(hopperPath === undefined
|
|
39
|
+
? [
|
|
40
|
+
{
|
|
41
|
+
kind: "install_hopper",
|
|
42
|
+
target: host.platform === "darwin"
|
|
43
|
+
? "~/Applications/Hopper Disassembler.app"
|
|
44
|
+
: "system package manager",
|
|
45
|
+
detail: "Download the official Hopper package, verify it, install it, and open Hopper for activation.",
|
|
46
|
+
external: true,
|
|
47
|
+
},
|
|
48
|
+
]
|
|
49
|
+
: []),
|
|
50
|
+
...detectedClients
|
|
51
|
+
.filter(({ format }) => format !== "unsupported")
|
|
52
|
+
.map((client) => ({
|
|
53
|
+
kind: "configure_client",
|
|
54
|
+
target: client.configPath,
|
|
55
|
+
detail: `Add the REA MCP registration for ${client.name}; preserve unrelated configuration.`,
|
|
56
|
+
external: false,
|
|
57
|
+
})),
|
|
58
|
+
{
|
|
59
|
+
kind: "install_skill",
|
|
60
|
+
target: "~/.agents/skills/rea-analysis/SKILL.md",
|
|
61
|
+
detail: "Install or update the bundled REA analysis skill.",
|
|
62
|
+
external: false,
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
let approved = options.approved;
|
|
66
|
+
let interactiveApproval = false;
|
|
67
|
+
if (!approved && confirm !== undefined && !options.structured) {
|
|
68
|
+
interactiveApproval = await confirm(plannedActions);
|
|
69
|
+
approved = interactiveApproval;
|
|
70
|
+
}
|
|
71
|
+
if (!approved)
|
|
72
|
+
return {
|
|
73
|
+
status: confirm === undefined ? "needs_confirmation" : "planned",
|
|
74
|
+
plannedActions,
|
|
75
|
+
appliedActions,
|
|
76
|
+
clients,
|
|
77
|
+
doctor: await host.doctor(),
|
|
78
|
+
remediation: "Review the setup plan, then rerun interactively or with --yes.",
|
|
79
|
+
};
|
|
80
|
+
if (hopperPath === undefined &&
|
|
81
|
+
(interactiveApproval || options.installHopper)) {
|
|
82
|
+
hopperPath = await host.installHopper();
|
|
50
83
|
if (hopperPath === undefined)
|
|
51
|
-
return
|
|
84
|
+
return {
|
|
85
|
+
status: "needs_human",
|
|
86
|
+
plannedActions,
|
|
87
|
+
appliedActions,
|
|
88
|
+
clients,
|
|
89
|
+
doctor: await host.doctor(),
|
|
90
|
+
remediation: "Hopper installation failed; install Hopper manually or rerun setup after resolving the reported system error.",
|
|
91
|
+
};
|
|
92
|
+
appliedActions.push("installed_hopper");
|
|
52
93
|
}
|
|
53
|
-
for (const client of
|
|
94
|
+
for (const client of detectedClients) {
|
|
54
95
|
const result = await host.configureClient(client, hopperPath, registrationCommand());
|
|
55
96
|
clients[client.name] = result;
|
|
56
97
|
if (result.status === "failed")
|
|
57
98
|
return fail(`${client.name} configuration ${result.reason} verification failed; no successful configuration was reported.`);
|
|
58
99
|
if (result.status === "configured")
|
|
59
|
-
|
|
100
|
+
appliedActions.push(`configured_${client.name}`);
|
|
60
101
|
}
|
|
61
102
|
const skill = await host.installSkill();
|
|
62
103
|
if (skill === "failed")
|
|
63
104
|
return fail("Agent skill installation or readback failed.");
|
|
64
105
|
if (skill === "installed")
|
|
65
|
-
|
|
106
|
+
appliedActions.push("installed_skill");
|
|
66
107
|
const doctor = await host.doctor();
|
|
67
|
-
const activationRequired =
|
|
108
|
+
const activationRequired = appliedActions.includes("installed_hopper");
|
|
68
109
|
const ready = doctor.healthy && !activationRequired;
|
|
69
110
|
return {
|
|
70
111
|
status: ready ? "ready" : "needs_human",
|
|
71
|
-
|
|
112
|
+
plannedActions,
|
|
113
|
+
appliedActions,
|
|
72
114
|
clients,
|
|
73
115
|
doctor,
|
|
74
116
|
...(ready
|
|
@@ -76,15 +118,17 @@ export const runSetup = async (yes, host = systemSetupHost()) => {
|
|
|
76
118
|
: {
|
|
77
119
|
remediation: activationRequired
|
|
78
120
|
? "Open Hopper, complete its one-time activation, then rerun rea doctor --json."
|
|
79
|
-
:
|
|
121
|
+
: hopperPath === undefined
|
|
122
|
+
? "Hopper is optional for non-Hopper providers. Rerun with --yes --install-hopper for deep native analysis."
|
|
123
|
+
: "Run rea doctor and apply each reported remediation.",
|
|
80
124
|
}),
|
|
81
125
|
};
|
|
82
126
|
};
|
|
83
127
|
const hostRemediation = async (host) => {
|
|
84
128
|
if (host.platform !== "darwin" && host.platform !== "linux")
|
|
85
129
|
return "REA supports Hopper on macOS and selected 64-bit Linux distributions.";
|
|
86
|
-
if (
|
|
87
|
-
return "Install Node.js
|
|
130
|
+
if (!supportsNodeVersion(host.nodeVersion))
|
|
131
|
+
return "Install Node.js 22.19+ or 24.11+ and rerun setup.";
|
|
88
132
|
if (host.platform === "darwin") {
|
|
89
133
|
const version = await host.macosVersion();
|
|
90
134
|
return version === undefined || major(version) < 12
|
|
@@ -95,7 +139,7 @@ const hostRemediation = async (host) => {
|
|
|
95
139
|
? undefined
|
|
96
140
|
: "REA supports Hopper on Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux.";
|
|
97
141
|
};
|
|
98
|
-
/** Production setup effects for
|
|
142
|
+
/** Production setup effects for Hopper, agent configuration, and the canonical skill directory. */
|
|
99
143
|
const systemSetupHost = () => {
|
|
100
144
|
const doctorHost = systemDoctorHost();
|
|
101
145
|
return {
|
|
@@ -103,15 +147,13 @@ const systemSetupHost = () => {
|
|
|
103
147
|
nodeVersion: process.versions.node,
|
|
104
148
|
macosVersion: () => doctorHost.macosVersion(),
|
|
105
149
|
linuxDistribution: readLinuxDistribution,
|
|
106
|
-
hasHomebrew: () => brewSucceeds(["--version"]),
|
|
107
|
-
installHomebrew: () => commandSucceeds("/bin/bash", [
|
|
108
|
-
"-c",
|
|
109
|
-
"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)",
|
|
110
|
-
], { ...process.env, NONINTERACTIVE: "1" }),
|
|
111
150
|
hopperPath: async () => (await runDoctor(undefined, doctorHost)).hopperPath,
|
|
112
|
-
installHopper: () =>
|
|
113
|
-
|
|
114
|
-
|
|
151
|
+
installHopper: async () => {
|
|
152
|
+
const result = process.platform === "linux"
|
|
153
|
+
? await installLinuxHopper()
|
|
154
|
+
: await installMacHopper();
|
|
155
|
+
return result.status === "installed" ? result.launcherPath : undefined;
|
|
156
|
+
},
|
|
115
157
|
detectedClients: () => detectClients(homedir()),
|
|
116
158
|
configureClient: (client, hopperPath, command) => client.format === "unsupported"
|
|
117
159
|
? Promise.resolve({ status: "skipped" })
|
|
@@ -351,19 +393,6 @@ export const installCanonicalSkill = async (home) => {
|
|
|
351
393
|
return "failed";
|
|
352
394
|
}
|
|
353
395
|
};
|
|
354
|
-
const commandSucceeds = async (command, args, env) => {
|
|
355
|
-
try {
|
|
356
|
-
await execFileAsync(command, args, env === undefined ? {} : { env });
|
|
357
|
-
return true;
|
|
358
|
-
}
|
|
359
|
-
catch {
|
|
360
|
-
return false;
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
const brewSucceeds = async (args) => {
|
|
364
|
-
const result = await probeHomebrew(async (command) => (await commandSucceeds(command, args)) ? true : undefined);
|
|
365
|
-
return result === true;
|
|
366
|
-
};
|
|
367
396
|
const major = (version) => Number.parseInt(version.split(".")[0] ?? "0", 10);
|
|
368
397
|
const exists = async (path) => {
|
|
369
398
|
try {
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Cli, z } from "incur";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
3
4
|
import { runDoctor } from "./application/Doctor.js";
|
|
4
5
|
import { runDirectAnalysis, runProviderAnalysis, runSessionStatus, } from "./application/DirectAnalysis.js";
|
|
5
6
|
import { runSetup } from "./application/Setup.js";
|
|
@@ -60,10 +61,20 @@ const registerCoreCommands = (cli, logger) => {
|
|
|
60
61
|
yes: z
|
|
61
62
|
.boolean()
|
|
62
63
|
.default(false)
|
|
63
|
-
.describe("Approve
|
|
64
|
+
.describe("Approve user-owned setup actions without prompting"),
|
|
65
|
+
installHopper: z
|
|
66
|
+
.boolean()
|
|
67
|
+
.default(false)
|
|
68
|
+
.describe("Also approve Hopper installation with --yes"),
|
|
64
69
|
}),
|
|
65
|
-
alias: { yes: "y" },
|
|
66
|
-
run: ({ options }) => logCliCommand(logger, "setup", () => runSetup(
|
|
70
|
+
alias: { yes: "y", installHopper: "install-hopper" },
|
|
71
|
+
run: ({ options, formatExplicit }) => logCliCommand(logger, "setup", () => runSetup({
|
|
72
|
+
approved: options.yes,
|
|
73
|
+
installHopper: options.installHopper,
|
|
74
|
+
structured: formatExplicit,
|
|
75
|
+
}, undefined, options.yes || formatExplicit || process.stdin.isTTY !== true
|
|
76
|
+
? undefined
|
|
77
|
+
: confirmSetup)),
|
|
67
78
|
});
|
|
68
79
|
cli.command("doctor", {
|
|
69
80
|
description: "Check whether REA is ready",
|
|
@@ -112,6 +123,24 @@ const registerCoreCommands = (cli, logger) => {
|
|
|
112
123
|
run: ({ args }) => logCliCommand(logger, "decompile", () => runDirectAnalysis(args.path, "procedure_pseudo_code", { procedure: args.address }, logger)),
|
|
113
124
|
});
|
|
114
125
|
};
|
|
126
|
+
const confirmSetup = async (actions) => {
|
|
127
|
+
process.stdout.write("\nREA setup plan\n");
|
|
128
|
+
for (const action of actions)
|
|
129
|
+
process.stdout.write(` - ${action.detail}\n ${action.target}${action.external ? " (external software)" : ""}\n`);
|
|
130
|
+
const prompt = createInterface({
|
|
131
|
+
input: process.stdin,
|
|
132
|
+
output: process.stdout,
|
|
133
|
+
});
|
|
134
|
+
try {
|
|
135
|
+
const answer = (await prompt.question("Continue? [Y/n] "))
|
|
136
|
+
.trim()
|
|
137
|
+
.toLowerCase();
|
|
138
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
prompt.close();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
115
144
|
const registerXrefsCommand = (cli, logger) => {
|
|
116
145
|
cli.command("xrefs", {
|
|
117
146
|
description: "List bounded references to an analyzed address",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Return whether a Node.js version satisfies REA's tested runtime families. */
|
|
2
|
+
export const supportsNodeVersion = (version) => {
|
|
3
|
+
const [majorText = "", minorText = ""] = version.split(".");
|
|
4
|
+
const major = Number.parseInt(majorText, 10);
|
|
5
|
+
const minor = Number.parseInt(minorText, 10);
|
|
6
|
+
return ((major === 22 && minor >= 19) ||
|
|
7
|
+
(major >= 24 && (major !== 24 || minor >= 11)));
|
|
8
|
+
};
|
package/install.sh
CHANGED
|
@@ -3,196 +3,111 @@ set -euo pipefail
|
|
|
3
3
|
|
|
4
4
|
PACKAGE="rea-agents"
|
|
5
5
|
REPOSITORY="morluto/rea"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
node_stage=""
|
|
11
|
-
node_backup=""
|
|
12
|
-
node_version=""
|
|
13
|
-
|
|
14
|
-
cleanup() {
|
|
15
|
-
if [[ -n "$temporary_directory" && -d "$temporary_directory" ]]; then
|
|
16
|
-
rm -rf -- "$temporary_directory"
|
|
17
|
-
fi
|
|
18
|
-
if [[ -n "$node_stage" && -e "$node_stage" ]]; then
|
|
19
|
-
rm -rf -- "$node_stage"
|
|
20
|
-
fi
|
|
21
|
-
if [[ -n "$node_backup" && -e "$node_backup" && ! -e "$node_destination" ]]; then
|
|
22
|
-
mv -- "$node_backup" "$node_destination"
|
|
23
|
-
fi
|
|
24
|
-
}
|
|
25
|
-
trap cleanup EXIT HUP INT TERM
|
|
6
|
+
version="${REA_VERSION:-}"
|
|
7
|
+
dry_run=false
|
|
8
|
+
start_setup=true
|
|
9
|
+
verbose=false
|
|
26
10
|
|
|
27
11
|
fail() {
|
|
28
12
|
printf 'REA installation failed: %s\n' "$1" >&2
|
|
29
13
|
exit 1
|
|
30
14
|
}
|
|
31
15
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
[[ "$major" =~ ^[0-9]+$ && "$minor" =~ ^[0-9]+$ ]] || return 1
|
|
39
|
-
((major > required_major || (major == required_major && minor >= required_minor)))
|
|
16
|
+
usage() {
|
|
17
|
+
cat <<'EOF'
|
|
18
|
+
Usage: install.sh [--version VERSION] [--dry-run] [--no-setup] [--no-prompt] [--verbose]
|
|
19
|
+
|
|
20
|
+
Installs only the REA CLI. Run `rea setup` separately to configure Hopper and coding agents.
|
|
21
|
+
EOF
|
|
40
22
|
}
|
|
41
23
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
24
|
+
while (($# > 0)); do
|
|
25
|
+
case "$1" in
|
|
26
|
+
--version)
|
|
27
|
+
(($# >= 2)) || fail "--version requires a value."
|
|
28
|
+
version="$2"
|
|
29
|
+
shift 2
|
|
30
|
+
;;
|
|
31
|
+
--dry-run)
|
|
32
|
+
dry_run=true
|
|
33
|
+
shift
|
|
34
|
+
;;
|
|
35
|
+
--no-setup|--no-prompt)
|
|
36
|
+
start_setup=false
|
|
37
|
+
shift
|
|
38
|
+
;;
|
|
39
|
+
--verbose)
|
|
40
|
+
verbose=true
|
|
41
|
+
shift
|
|
42
|
+
;;
|
|
43
|
+
--help|-h)
|
|
44
|
+
usage
|
|
45
|
+
exit 0
|
|
46
|
+
;;
|
|
47
|
+
*) fail "unknown option: $1" ;;
|
|
57
48
|
esac
|
|
58
|
-
|
|
59
|
-
fail "REA supports macOS and selected 64-bit Linux distributions."
|
|
60
|
-
fi
|
|
61
|
-
command -v curl >/dev/null 2>&1 || fail "curl is required."
|
|
62
|
-
command -v shasum >/dev/null 2>&1 || command -v sha256sum >/dev/null 2>&1 || fail "shasum or sha256sum is required."
|
|
49
|
+
done
|
|
63
50
|
|
|
64
|
-
if [[ "$
|
|
65
|
-
|
|
66
|
-
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || fail "Homebrew installation did not complete."
|
|
67
|
-
if [[ -x /opt/homebrew/bin/brew ]]; then
|
|
68
|
-
PATH="/opt/homebrew/bin:$PATH"
|
|
69
|
-
elif [[ -x /usr/local/bin/brew ]]; then
|
|
70
|
-
PATH="/usr/local/bin:$PATH"
|
|
71
|
-
fi
|
|
72
|
-
fi
|
|
73
|
-
if [[ "$platform" == "Darwin" ]]; then
|
|
74
|
-
command -v brew >/dev/null 2>&1 || fail "Homebrew is not available after installation."
|
|
51
|
+
if [[ "$verbose" == true ]]; then
|
|
52
|
+
set -x
|
|
75
53
|
fi
|
|
76
54
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
fi
|
|
84
|
-
if [[ "$node_compatible" != true ]]; then
|
|
85
|
-
if [[ "$platform" == "Darwin" ]]; then
|
|
86
|
-
brew install "$NODE_FORMULA" || fail "Node.js 24 installation failed."
|
|
87
|
-
node_prefix="$(brew --prefix "$NODE_FORMULA")" || fail "could not locate Node.js 24."
|
|
88
|
-
PATH="$node_prefix/bin:$PATH"
|
|
89
|
-
else
|
|
90
|
-
machine="$(uname -m)"
|
|
91
|
-
node_arch="$([[ "$machine" == "x86_64" ]] && printf 'x64' || printf 'arm64')"
|
|
92
|
-
node_archive="node-v${NODE_VERSION}-linux-${node_arch}.tar.xz"
|
|
93
|
-
node_base="https://nodejs.org/dist/v${NODE_VERSION}"
|
|
94
|
-
temporary_directory="$(mktemp -d "${TMPDIR:-/tmp}/rea-install.XXXXXX")" || fail "could not create a temporary directory."
|
|
95
|
-
curl -fsSL "$node_base/$node_archive" -o "$temporary_directory/$node_archive" || fail "Node.js download failed."
|
|
96
|
-
curl -fsSL "$node_base/SHASUMS256.txt" -o "$temporary_directory/SHASUMS256.txt" || fail "Node.js checksum download failed."
|
|
97
|
-
expected_checksum="$(awk -v archive="$node_archive" '$2 == archive { print $1 }' "$temporary_directory/SHASUMS256.txt")"
|
|
98
|
-
[[ "$expected_checksum" =~ ^[a-f0-9]{64}$ ]] || fail "Node.js checksum metadata was malformed."
|
|
99
|
-
if command -v shasum >/dev/null 2>&1; then
|
|
100
|
-
actual_checksum="$(shasum -a 256 "$temporary_directory/$node_archive" | awk '{ print $1 }')"
|
|
101
|
-
else
|
|
102
|
-
actual_checksum="$(sha256sum "$temporary_directory/$node_archive" | awk '{ print $1 }')"
|
|
103
|
-
fi
|
|
104
|
-
[[ "$actual_checksum" == "$expected_checksum" ]] || fail "Node.js checksum verification failed."
|
|
105
|
-
node_destination="$HOME/.local/share/rea/node"
|
|
106
|
-
node_stage="$HOME/.local/share/rea/node.new.$$"
|
|
107
|
-
node_backup="$HOME/.local/share/rea/node.backup.$$"
|
|
108
|
-
[[ ! -L "$node_destination" ]] || fail "refusing to replace a symlinked Node.js installation."
|
|
109
|
-
mkdir -p "$HOME/.local/share/rea"
|
|
110
|
-
rm -rf -- "$node_stage" "$node_backup"
|
|
111
|
-
mkdir "$node_stage"
|
|
112
|
-
tar -xJf "$temporary_directory/$node_archive" --strip-components=1 -C "$node_stage" || fail "Node.js extraction failed."
|
|
113
|
-
if [[ -e "$node_destination" ]]; then
|
|
114
|
-
mv -- "$node_destination" "$node_backup"
|
|
115
|
-
fi
|
|
116
|
-
if ! mv -- "$node_stage" "$node_destination"; then
|
|
117
|
-
[[ ! -e "$node_backup" ]] || mv -- "$node_backup" "$node_destination"
|
|
118
|
-
fail "Node.js installation replacement failed."
|
|
119
|
-
fi
|
|
120
|
-
node_stage=""
|
|
121
|
-
rm -rf -- "$node_backup"
|
|
122
|
-
node_backup=""
|
|
123
|
-
PATH="$node_destination/bin:$PATH"
|
|
124
|
-
fi
|
|
125
|
-
fi
|
|
55
|
+
platform="$(uname -s)"
|
|
56
|
+
[[ "$platform" == "Darwin" || "$platform" == "Linux" ]] || fail "REA supports macOS and selected 64-bit Linux distributions."
|
|
57
|
+
command -v curl >/dev/null 2>&1 || fail "curl is required."
|
|
58
|
+
command -v node >/dev/null 2>&1 || fail "Node.js 22.19+ or 24.11+ is required; install it from https://nodejs.org and retry."
|
|
59
|
+
command -v npm >/dev/null 2>&1 || fail "npm is required; install it with Node.js and retry."
|
|
60
|
+
|
|
126
61
|
node_version="$(node -p 'process.versions.node' 2>/dev/null || true)"
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
npm_version="$(npm --version)"
|
|
132
|
-
if [[ ! "$npm_version" =~ ^11\. ]] || ! version_at_least "$npm_version" 11 16; then
|
|
133
|
-
if [[ "$platform" == "Linux" ]]; then
|
|
134
|
-
npm install --global --prefix "$HOME/.local" npm@11 || fail "npm 11 installation failed."
|
|
135
|
-
else
|
|
136
|
-
npm install --global npm@11 || fail "npm 11 installation failed."
|
|
137
|
-
fi
|
|
138
|
-
fi
|
|
139
|
-
npm_version="$(npm --version)"
|
|
140
|
-
if [[ ! "$npm_version" =~ ^11\. ]] || ! version_at_least "$npm_version" 11 16; then
|
|
141
|
-
fail "npm 11.16 or newer is not active on PATH."
|
|
62
|
+
IFS=. read -r node_major node_minor _node_patch <<<"$node_version"
|
|
63
|
+
[[ "$node_major" =~ ^[0-9]+$ && "$node_minor" =~ ^[0-9]+$ ]] || fail "could not determine the active Node.js version."
|
|
64
|
+
if ! ((node_major == 22 && node_minor >= 19 || node_major >= 24)); then
|
|
65
|
+
fail "Node.js $node_version is unsupported; use Node.js 22.19+ or 24.11+."
|
|
142
66
|
fi
|
|
143
|
-
if
|
|
144
|
-
|
|
145
|
-
temporary_directory=""
|
|
67
|
+
if ((node_major == 24 && node_minor < 11)); then
|
|
68
|
+
fail "Node.js $node_version is unsupported; use Node.js 24.11 or newer."
|
|
146
69
|
fi
|
|
147
70
|
|
|
148
|
-
if [[ -n "$
|
|
149
|
-
|
|
150
|
-
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] || fail "REA_VERSION must be an exact semantic version."
|
|
71
|
+
if [[ -n "$version" ]]; then
|
|
72
|
+
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] || fail "version must be an exact semantic version."
|
|
151
73
|
else
|
|
152
|
-
release_json="$(curl -fsSL -H 'Accept: application/vnd.github+json' "https://api.github.com/repos/$REPOSITORY/releases/latest")" || fail "could not resolve the latest GitHub release."
|
|
74
|
+
release_json="$(curl -fsSL --proto '=https' --tlsv1.2 --retry 3 -H 'Accept: application/vnd.github+json' "https://api.github.com/repos/$REPOSITORY/releases/latest")" || fail "could not resolve the latest GitHub release."
|
|
153
75
|
tag="$(printf '%s' "$release_json" | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{const v=JSON.parse(s).tag_name;if(typeof v!=="string")process.exit(1);process.stdout.write(v)})')" || fail "GitHub returned malformed release metadata."
|
|
154
76
|
[[ "$tag" =~ ^rea-agents-([0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?)$ ]] || fail "latest release tag is not a valid rea-agents release."
|
|
155
77
|
version="${BASH_REMATCH[1]}"
|
|
156
78
|
fi
|
|
157
79
|
|
|
158
|
-
|
|
80
|
+
prefix_args=()
|
|
159
81
|
if [[ "$platform" == "Linux" ]]; then
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
npm install --global --prefix "$HOME/.local" "$PACKAGE@$version" || fail "npm package installation failed."
|
|
82
|
+
prefix_args=(--prefix "$HOME/.local")
|
|
83
|
+
install_bin="$HOME/.local/bin/rea"
|
|
163
84
|
else
|
|
164
|
-
npm
|
|
85
|
+
npm_prefix="$(npm prefix --global)" || fail "could not determine the npm global prefix."
|
|
86
|
+
install_bin="$npm_prefix/bin/rea"
|
|
165
87
|
fi
|
|
166
|
-
command -v rea >/dev/null 2>&1 || fail "the rea command is not available after installation."
|
|
167
|
-
installed_version="$(rea --version 2>/dev/null | tr -d '[:space:]')" || fail "could not read the installed REA version."
|
|
168
|
-
[[ "$installed_version" == "$version" ]] || fail "installed version $installed_version does not match $version."
|
|
169
88
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const fs = require("node:fs");
|
|
177
|
-
const [setupPath, doctorPath] = process.argv.slice(2);
|
|
178
|
-
try {
|
|
179
|
-
const setup = JSON.parse(fs.readFileSync(setupPath, "utf8"));
|
|
180
|
-
const doctor = JSON.parse(fs.readFileSync(doctorPath, "utf8"));
|
|
181
|
-
if (!Array.isArray(doctor.checks) || typeof doctor.healthy !== "boolean") process.exit(2);
|
|
182
|
-
if (setup.status !== "ready" && setup.status !== "needs_human") process.exit(2);
|
|
183
|
-
const activation = setup.actions.includes("installed_hopper");
|
|
184
|
-
if (!doctor.healthy && !activation) process.exit(3);
|
|
185
|
-
process.stdout.write(activation ? "activation" : "ready");
|
|
186
|
-
} catch { process.exit(2); }
|
|
187
|
-
NODE
|
|
188
|
-
)" || fail "setup or doctor reported an unhealthy required component or malformed output."
|
|
89
|
+
printf 'REA install plan\n'
|
|
90
|
+
printf ' Version: %s\n' "$version"
|
|
91
|
+
printf ' Runtime: Node.js %s\n' "$node_version"
|
|
92
|
+
printf ' Command: npm install --global %s@%s\n' "$PACKAGE" "$version"
|
|
93
|
+
printf ' Binary: %s\n' "$install_bin"
|
|
94
|
+
printf ' Setup: %s\n' "$([[ "$start_setup" == true ]] && printf 'start when a terminal is available' || printf 'skipped')"
|
|
189
95
|
|
|
190
|
-
if [[ "$
|
|
191
|
-
printf '
|
|
192
|
-
|
|
193
|
-
else
|
|
194
|
-
printf 'REA %s is installed and ready. Run rea doctor --json at any time.\n' "$version"
|
|
96
|
+
if [[ "$dry_run" == true ]]; then
|
|
97
|
+
printf 'Dry run complete; no changes made.\n'
|
|
98
|
+
exit 0
|
|
195
99
|
fi
|
|
196
|
-
|
|
197
|
-
|
|
100
|
+
|
|
101
|
+
printf 'Installing %s@%s...\n' "$PACKAGE" "$version"
|
|
102
|
+
npm install --global "${prefix_args[@]}" "$PACKAGE@$version" || fail "npm package installation failed."
|
|
103
|
+
[[ -x "$install_bin" ]] || fail "the rea command was not installed at $install_bin."
|
|
104
|
+
installed_version="$("$install_bin" --version 2>/dev/null | tr -d '[:space:]')" || fail "could not read the installed REA version."
|
|
105
|
+
[[ "$installed_version" == "$version" ]] || fail "installed version $installed_version does not match $version."
|
|
106
|
+
|
|
107
|
+
printf 'REA %s is installed.\n' "$version"
|
|
108
|
+
if [[ "$start_setup" == true && -r /dev/tty && -w /dev/tty && -t 1 ]]; then
|
|
109
|
+
printf 'Starting REA setup...\n'
|
|
110
|
+
"$install_bin" setup </dev/tty >/dev/tty
|
|
111
|
+
else
|
|
112
|
+
printf 'Run %s setup to configure Hopper and coding agents.\n' "$install_bin"
|
|
198
113
|
fi
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rea-agents",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Reverse engineer anything from your terminal or coding agent with one CLI and MCP server.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,8 +33,7 @@
|
|
|
33
33
|
"rea": "scripts/rea.mjs"
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|
|
36
|
-
"node": ">=24.
|
|
37
|
-
"npm": ">=11.16.0 <12"
|
|
36
|
+
"node": "^22.19.0 || >=24.11.0"
|
|
38
37
|
},
|
|
39
38
|
"packageManager": "npm@11.16.0",
|
|
40
39
|
"files": [
|
|
@@ -99,7 +98,7 @@
|
|
|
99
98
|
"pino": "9.14.0",
|
|
100
99
|
"plist": "5.0.0",
|
|
101
100
|
"smol-toml": "^1.7.0",
|
|
102
|
-
"write-file-atomic": "
|
|
101
|
+
"write-file-atomic": "7.0.0",
|
|
103
102
|
"ws": "8.21.0",
|
|
104
103
|
"zod": "4.4.3"
|
|
105
104
|
},
|