askeroo
Version:
A modern CLI prompt library with flow control, history navigation, and conditional prompts
196 lines • 8.43 kB
JavaScript
import { ask, note, stream, spawnWithColors } from "../src/index.js";
import { spawn } from "child_process";
/**
* Real-world example: npm install with streaming output
*
* This example demonstrates:
* - Real npm command execution with child_process spawn
* - Streaming output where only last N lines are visible
* - Label updates on complete/error (not new lines!)
* - How write() handles chunked data from npm
*
* IMPORTANT: NPM Output Buffering
* --------------------------------
* npm buffers output when it detects it's not running in an interactive terminal.
* This means output may appear all at once at the end instead of streaming in real-time.
*
* Solutions:
* 1. Environment variables (used in npm example) - helps but not always perfect
* 2. Use spawnWithColors (see pnpm example) - auto-uses node-pty if available
* 3. Install node-pty for true unbuffered output: `npm install node-pty`
* 4. Use --loglevel=verbose (npm) or --reporter=append-only (pnpm) flags
* 5. For guaranteed streaming, see stream-real-time-demo.ts with bash commands
*/
/**
* Install packages using npm with streamed output
* Only shows the last 10 lines of output to keep terminal clean
*/
async function installPackagesWithNpm(packages) {
// Create stream with label and maxLines
const output = await stream(`Installing ${packages.join(", ")}...`, {
maxLines: 10, // Only show last 10 lines - npm can be verbose!
showLineNumbers: true,
});
return new Promise((resolve, reject) => {
// Spawn npm install command with environment variables to force unbuffered output
// Using --loglevel=verbose generates more output which streams better
const npm = spawn("npm", ["install", "--save", "--loglevel=verbose", ...packages], {
env: {
...process.env,
// Force npm to think it's in a TTY to get real-time output
FORCE_COLOR: "1",
NPM_CONFIG_COLOR: "always",
NPM_CONFIG_PROGRESS: "true",
},
});
// Capture stdout - npm sends installation progress here
npm.stdout.on("data", (data) => {
// write() handles chunked data and splits into lines automatically
output.write(data.toString());
});
// Capture stderr - npm also sends progress info to stderr
npm.stderr.on("data", (data) => {
output.write(data.toString());
});
// Handle spawn errors (e.g., npm not found)
npm.on("error", (err) => {
// error() updates the label with red X - no new line!
output.error(`Failed to start npm: ${err.message}`);
reject(err);
});
// Handle process completion
npm.on("close", (code) => {
if (code === 0) {
// complete() updates the label with green checkmark - no new line!
output.complete(`Successfully installed ${packages.length} package(s)!`);
resolve();
}
else {
// error() updates the label with red X - no new line!
output.error(`Installation failed (exit code ${code})`);
reject(new Error(`npm install failed with code ${code}`));
}
});
});
}
/**
* Install packages using pnpm with colored output
* Shows how spawnWithColors preserves ANSI colors
*/
async function installPackagesWithPnpm(packages) {
const output = await stream(`Installing ${packages.join(", ")} with pnpm...`, {
maxLines: 15, // Show last 15 lines
showLineNumbers: false, // No line numbers for cleaner output
prefixSymbol: "│", // Add a nice prefix
});
return new Promise((resolve, reject) => {
// Use spawnWithColors to preserve terminal colors
// Add --reporter=append-only to get better streaming output
const pnpm = spawnWithColors("pnpm", [
"add",
"--reporter=append-only",
...packages,
]);
let hasOutput = false;
pnpm.stdout.on("data", (data) => {
hasOutput = true;
output.write(data.toString());
});
pnpm.stderr.on("data", (data) => {
hasOutput = true;
output.write(data.toString());
});
pnpm.on("error", (err) => {
output.error(`Failed to start pnpm: ${err.message}`);
reject(err);
});
pnpm.on("close", async (code) => {
// If no output was captured, it might have been buffered or package already installed
if (!hasOutput) {
await output.writeLine("(No output captured - package may already be installed)");
}
if (code === 0) {
await output.complete(`Installed ${packages.length} package(s) with pnpm!`);
resolve();
}
else {
await output.error(`pnpm failed (exit code ${code})`);
reject(new Error(`pnpm add failed with code ${code}`));
}
});
});
}
/**
* Example: Simulate verbose npm output showing line limiting
* This demonstrates how maxLines keeps only the most recent output visible
*/
async function simulateVerboseNpmOutput() {
const output = await stream("Simulating verbose npm output...", {
maxLines: 10, // Only last 10 lines visible
showLineNumbers: true,
});
// Simulate npm outputting many lines during installation
// (Real npm can output 50+ lines for a single package install)
await output.writeLine("npm info using npm@10.2.3");
await output.writeLine("npm info using node@v20.10.0");
await new Promise((r) => setTimeout(r, 100));
await output.writeLine("npm http fetch GET 200 https://registry.npmjs.org/chalk 145ms");
await new Promise((r) => setTimeout(r, 100));
// Simulate many dependency resolution lines
for (let i = 1; i <= 25; i++) {
await output.writeLine(`npm http fetch GET 200 https://registry.npmjs.org/package-${i} ${50 + i * 2}ms`);
await new Promise((r) => setTimeout(r, 80));
}
await output.writeLine("");
await output.writeLine("added 25 packages, and audited 26 packages in 3s");
await output.writeLine("found 0 vulnerabilities");
// Label updates to show completion - no new line added!
await output.complete("✓ Installation complete!");
}
/**
* Example showing error handling
*/
async function simulateNpmError() {
const output = await stream("Installing non-existent package...", {
maxLines: 8,
});
await output.writeLine("npm info using npm@10.2.3");
await new Promise((r) => setTimeout(r, 100));
await output.writeLine("npm http fetch GET 404 https://registry.npmjs.org/this-package-definitely-does-not-exist 234ms");
await new Promise((r) => setTimeout(r, 300));
await output.writeLine("npm ERR! code E404");
await output.writeLine("npm ERR! 404 Not Found - GET https://registry.npmjs.org/this-package-definitely-does-not-exist");
await output.writeLine("npm ERR! 404");
await output.writeLine("npm ERR! 404 'this-package-definitely-does-not-exist@latest' is not in this registry.");
// Label updates with error message - no new line added!
await output.error("✗ Package not found!");
}
// Run examples
const flow = async () => {
// Example 1: Simulated verbose npm output (safe to run)
// await note(
// "=== Example 1: Verbose npm output (only last 10 lines shown) ===\n"
// );
// await simulateVerboseNpmOutput();
// await new Promise((r) => setTimeout(r, 500));
// // Example 2: Simulated error
// await note("\n\n=== Example 2: npm error handling ===\n");
// await simulateNpmError();
// Example 3: Real npm install (commented out for safety)
// Uncomment to test with real package installation
await note("\n\n=== Example 3: Real npm install ===\n");
await installPackagesWithNpm(["chalk"]);
// Example 4: Real pnpm install (commented out for safety)
await note("\n\n=== Example 4: Real pnpm install ===\n");
await installPackagesWithPnpm(["commander"]);
};
(async () => {
try {
const result = await ask(flow);
}
catch (error) {
console.error("Error:", error);
process.exit(1);
}
})();
//# sourceMappingURL=stream-npm-real-example.js.map