fix(install): honor redirected setup diagnostics

Require terminal stderr before launching the automatic setup wizard, matching the CLI's interactive admission rule. Print the manual setup command when diagnostics are redirected.

Exercise a real PTY with stderr redirected to a fixture file and verify the installed command is not launched for setup. Installer lane: 33 tests passed; targeted regression, typecheck/lint and npm pack dry-run passed. Full package verifier was not run under the requested focused-check resource policy.
This commit is contained in:
morluto 2026-10-13 12:59:52 +08:00
parent 10935b7b11
commit 8b181716ac
2 changed files with 68 additions and 2 deletions

View File

@ -167,7 +167,7 @@ main() {
[ "$installed_version" = "$version" ] || fail "installed version $installed_version does not match $version. Reinstall the requested version, then retry."
printf 'REA %s is installed.\n' "$version"
if [ "$start_setup" = true ] && [ -r /dev/tty ] && [ -w /dev/tty ] && [ -t 1 ]; then
if [ "$start_setup" = true ] && [ -r /dev/tty ] && [ -w /dev/tty ] && [ -t 1 ] && [ -t 2 ]; then
printf 'Starting REA setup...\n'
"$install_bin" setup </dev/tty >/dev/tty
else

View File

@ -11,6 +11,7 @@ import {
import { join } from "node:path";
import { promisify } from "node:util";
import { spawn as spawnPty } from "@lydell/node-pty";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
@ -185,6 +186,69 @@ describe("curl installer scenarios", { timeout: 20_000 }, () => {
});
});
describe("installer TTY fallback", { timeout: 20_000 }, () => {
it("skips automatic setup when stderr is redirected", async () => {
const fixture = await createFixture();
const script = join(process.cwd(), "install.sh");
const wrapper = `
const { spawnSync } = require("node:child_process");
const { openSync, closeSync } = require("node:fs");
const stderr = openSync(process.argv[2], "w");
const child = spawnSync("/bin/bash", [process.argv[1], ...process.argv.slice(3)], {
stdio: ["inherit", "inherit", stderr],
env: process.env,
});
closeSync(stderr);
process.exit(child.status ?? 1);
`;
let output = "";
const terminal = spawnPty(
process.execPath,
["-e", wrapper, script, fixture.installerStderrLog, "--version", "0.3.0"],
{
cwd: process.cwd(),
env: {
...process.env,
HOME: fixture.home,
TMPDIR: fixture.temporary,
PATH: `${fixture.bin}:/usr/bin:/bin`,
FAKE_NPM_LOG: fixture.npmLog,
FAKE_REA_LOG: fixture.reaLog,
FAKE_REA_SOURCE: join(fixture.bin, "rea-source"),
FAKE_NPM_PREFIX: join(fixture.home, ".npm-global"),
TERM: "xterm-256color",
},
name: "xterm-256color",
cols: 120,
rows: 40,
},
);
let timedOut = false;
const exitCode = await new Promise<number>((resolve, reject) => {
const timeout = setTimeout(() => {
timedOut = true;
terminal.kill();
}, 10_000);
terminal.onData((data) => {
output += data;
});
terminal.onExit(({ exitCode: code }) => {
clearTimeout(timeout);
if (timedOut)
reject(new Error(`Installer PTY timed out after output: ${output}`));
else resolve(code);
});
});
expect(exitCode).toBe(0);
expect(output).toContain(
`Run ${join(fixture.home, ".local/bin/rea")} setup to configure Hopper and agents.`,
);
expect(await readFile(fixture.reaLog, "utf8")).toBe("--version\n");
expect(await readFile(fixture.installerStderrLog, "utf8")).toBe("");
});
});
describe("truncated installer download", { timeout: 20_000 }, () => {
it("runs nothing from a download truncated after the npm install line", async () => {
const fixture = await createFixture();
@ -433,6 +497,7 @@ interface InstallerFixture {
readonly temporary: string;
readonly npmLog: string;
readonly reaLog: string;
readonly installerStderrLog: string;
}
const getProcessOutput = (
@ -456,6 +521,7 @@ const createFixture = async (): Promise<InstallerFixture> => {
const temporary = join(root, "tmp");
const npmLog = join(root, "npm.log");
const reaLog = join(root, "rea.log");
const installerStderrLog = join(root, "installer.stderr.log");
await Promise.all([mkdir(home), mkdir(bin), mkdir(temporary)]);
await Promise.all([
executable(
@ -507,7 +573,7 @@ exit 0
symlink(`/usr/bin/${command}`, join(bin, command)),
),
]);
return { home, bin, temporary, npmLog, reaLog };
return { home, bin, temporary, npmLog, reaLog, installerStderrLog };
};
const executable = async (path: string, source: string): Promise<void> => {