diff --git a/README.md b/README.md index 4e974a1..4bef434 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,14 @@ AI-powered talent profile search using natural language. CLI tool with interacti - [Bun](https://bun.sh/) v1.3+ -### Install from npm +### Install and run ```bash -npm install -g talent-agent +npm install -g talent-agent && talent-agent ``` +This installs the CLI globally and launches the interactive TUI. If Bun is not installed, you will be prompted to install it. + Or run without installing: ```bash diff --git a/scripts/postinstall.js b/scripts/postinstall.js index 66e2682..5187000 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -10,6 +10,27 @@ */ import { execFileSync } from "node:child_process"; +import { openSync, writeSync, closeSync } from "node:fs"; + +// npm v7+ suppresses all lifecycle script stdout/stderr for successful +// scripts. Writing directly to /dev/tty bypasses that capture and +// prints straight to the user's terminal. Falls back to stderr when +// /dev/tty is unavailable (CI, Docker, non-interactive shells). +let ttyFd; +try { + ttyFd = openSync("/dev/tty", "w"); +} catch { + // /dev/tty not available -- fall back to stderr +} + +const log = (msg = "") => { + const line = msg + "\n"; + if (ttyFd !== undefined) { + writeSync(ttyFd, line); + } else { + process.stderr.write(line); + } +}; let hasBun = false; try { @@ -19,19 +40,23 @@ try { // bun not found -- warn below } -console.log(""); -console.log(" talent-agent installed successfully!"); -console.log(""); +log(); +log(" talent-agent installed successfully!"); +log(); if (!hasBun) { - console.log(" Bun runtime is required but was not found."); - console.log(" Install it: curl -fsSL https://bun.sh/install | bash"); - console.log(" Then restart your terminal (or run: source ~/.bashrc)"); - console.log(""); + log(" Bun runtime is required but was not found."); + log(" Install it: curl -fsSL https://bun.sh/install | bash"); + log(" Then restart your terminal (or run: source ~/.bashrc)"); + log(); } -console.log(" Get started:"); -console.log(" talent-agent login Authenticate"); -console.log(' talent-agent "Find devs" Search for talent'); -console.log(" talent-agent --help Show all options"); -console.log(""); +log(" Get started:"); +log(" talent-agent login Authenticate"); +log(' talent-agent "Find devs" Search for talent'); +log(" talent-agent --help Show all options"); +log(); + +if (ttyFd !== undefined) { + closeSync(ttyFd); +} diff --git a/test/postinstall.test.ts b/test/postinstall.test.ts index ecdab5a..5e732ed 100644 --- a/test/postinstall.test.ts +++ b/test/postinstall.test.ts @@ -4,7 +4,7 @@ * Runs the script via `node` as a subprocess to verify its output, * matching how npm would execute it after `npm install`. */ -import { execFileSync } from "node:child_process"; +import { execFileSync, spawnSync } from "node:child_process"; import { dirname, resolve } from "node:path"; import { describe, expect, it } from "vitest"; @@ -25,29 +25,33 @@ function pathWithoutBun(): string { /** * Helper to run the postinstall script and capture output. + * + * The script writes to /dev/tty (bypasses npm's stdio suppression) with + * a fallback to stderr. In tests we force the fallback by setting + * FORCE_STDERR=1, which doesn't exist in the script -- instead we + * redirect /dev/tty writes by running without a controlling terminal. + * The simplest reliable approach: pipe the child's stdio so /dev/tty + * open fails, triggering the stderr fallback that we CAN capture. */ function runPostinstall(env?: Record): { - stdout: string; - stderr: string; + output: string; exitCode: number; } { - try { - const stdout = execFileSync("node", [SCRIPT_PATH], { - env: { - ...process.env, - ...env, - }, - encoding: "utf-8", - timeout: 10000, - }); - return { stdout, stderr: "", exitCode: 0 }; - } catch (error: any) { - return { - stdout: error.stdout ?? "", - stderr: error.stderr ?? "", - exitCode: error.status ?? 1, - }; - } + const result = spawnSync("node", [SCRIPT_PATH], { + env: { + ...process.env, + ...env, + }, + encoding: "utf-8", + timeout: 10000, + // Piping stdio detaches the child from the controlling terminal, + // so openSync("/dev/tty") falls back to stderr which we capture. + stdio: ["pipe", "pipe", "pipe"], + }); + return { + output: (result.stdout ?? "") + (result.stderr ?? ""), + exitCode: result.status ?? 1, + }; } describe("scripts/postinstall.js", () => { @@ -58,31 +62,31 @@ describe("scripts/postinstall.js", () => { }); it("prints the success message", () => { - const { stdout } = runPostinstall(); + const { output } = runPostinstall(); - expect(stdout).toContain("talent-agent installed successfully!"); + expect(output).toContain("talent-agent installed successfully!"); }); it("prints getting-started instructions", () => { - const { stdout } = runPostinstall(); + const { output } = runPostinstall(); - expect(stdout).toContain("talent-agent login"); - expect(stdout).toContain("talent-agent --help"); - expect(stdout).toContain("Get started:"); + expect(output).toContain("talent-agent login"); + expect(output).toContain("talent-agent --help"); + expect(output).toContain("Get started:"); }); it("does not show the bun warning when bun is available", () => { - const { stdout } = runPostinstall(); + const { output } = runPostinstall(); // bun is available in this dev environment - expect(stdout).not.toContain("Bun runtime is required but was not found"); + expect(output).not.toContain("Bun runtime is required but was not found"); }); it("shows the bun warning when bun is not on PATH", () => { - const { stdout } = runPostinstall({ PATH: pathWithoutBun() }); + const { output } = runPostinstall({ PATH: pathWithoutBun() }); - expect(stdout).toContain("Bun runtime is required but was not found"); - expect(stdout).toContain("curl -fsSL https://bun.sh/install | bash"); - expect(stdout).toContain("restart your terminal"); + expect(output).toContain("Bun runtime is required but was not found"); + expect(output).toContain("curl -fsSL https://bun.sh/install | bash"); + expect(output).toContain("restart your terminal"); }); });