Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/early-falcons-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@design-intelligence/ghost": patch
---

Flush stdout and stderr before exit so piped command output is never truncated.
10 changes: 5 additions & 5 deletions packages/ghost/src/commands/checks-command.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { CAC } from "cac";
import { resolveGhostPackage } from "../package.js";
import { addChecksDir } from "../scan/check-scaffold.js";
import { failFromError } from "./errors.js";
import { exitCli, failFromError } from "./errors.js";

/**
* `ghost checks <action>` — manage the flat `.ghost/checks/` directory of
Expand All @@ -21,12 +21,12 @@ export function registerChecksCommand(cli: CAC): void {
try {
if (opts.format !== "cli" && opts.format !== "json") {
console.error("Error: --format must be 'cli' or 'json'");
process.exit(2);
await exitCli(2);
return;
}
if (action !== "init") {
console.error("Error: ghost checks supports `init`");
process.exit(2);
await exitCli(2);
return;
}

Expand All @@ -53,9 +53,9 @@ export function registerChecksCommand(cli: CAC): void {
process.stdout.write(` skipped ${file}\n`);
}
}
process.exit(0);
await exitCli(0);
} catch (err) {
failFromError(err);
await failFromError(err);
}
});
}
18 changes: 15 additions & 3 deletions packages/ghost/src/commands/errors.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import { EXIT } from "#ghost-core";

function flushStream(stream: NodeJS.WriteStream): Promise<void> {
return new Promise((resolve) => {
stream.write("", () => resolve());
});
}

export async function exitCli(code: number): Promise<never> {
await flushStream(process.stdout);
await flushStream(process.stderr);
process.exit(code);
}

/**
* Report a thrown error and exit. A `UsageError` (or anything carrying a numeric
* `exitCode`) exits with that code; everything else is an unexpected crash and
* exits `1`. Pass `stream` to match a command's existing output channel.
*/
export function failFromError(
export async function failFromError(
err: unknown,
stream: "stderr" | "stdout" = "stderr",
): never {
): Promise<never> {
const message = err instanceof Error ? err.message : String(err);
const line = `Error: ${message}\n`;
if (stream === "stdout") process.stdout.write(line);
Expand All @@ -18,5 +30,5 @@ export function failFromError(
typeof (err as { exitCode?: unknown })?.exitCode === "number"
? (err as { exitCode: number }).exitCode
: EXIT.failure;
process.exit(exitCode);
return exitCli(exitCode);
}
10 changes: 5 additions & 5 deletions packages/ghost/src/commands/export-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
} from "../scan/fingerprint-package.js";
import { resolveGitRoot } from "../scan/package-paths.js";
import { defaultArchiveName, writeDirectoryTarball } from "../scan/tarball.js";
import { failFromError } from "./errors.js";
import { exitCli, failFromError } from "./errors.js";

interface ExportAuditTravelingLocator {
nodeId: string;
Expand Down Expand Up @@ -66,7 +66,7 @@ export function registerExportCommand(cli: CAC): void {
try {
if (opts.format !== "markdown" && opts.format !== "json") {
console.error("Error: --format must be 'markdown' or 'json'");
process.exit(2);
await exitCli(2);
return;
}

Expand All @@ -76,7 +76,7 @@ export function registerExportCommand(cli: CAC): void {
console.error(
"Error: ghost package has validation errors. Run `ghost validate` and fix them before exporting.",
);
process.exit(2);
await exitCli(2);
return;
}
const loaded = await loadGhostPackage(paths);
Expand Down Expand Up @@ -137,9 +137,9 @@ export function registerExportCommand(cli: CAC): void {
);
}

process.exit(opts.strict && audit.stranded.length > 0 ? 2 : 0);
await exitCli(opts.strict && audit.stranded.length > 0 ? 2 : 0);
} catch (err) {
failFromError(err);
await failFromError(err);
}
});
}
Expand Down
8 changes: 4 additions & 4 deletions packages/ghost/src/commands/fingerprint-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
resolveGhostPackage,
} from "../package.js";
import { detectFileKind, lintDetectedFileKind } from "../scan/file-kind.js";
import { failFromError } from "./errors.js";
import { exitCli, failFromError } from "./errors.js";
import { registerInitCommand } from "./init-command.js";

/**
Expand Down Expand Up @@ -39,7 +39,7 @@ export function registerFingerprintCommands(cli: CAC): void {
if (path === undefined || (await isDirectory(target))) {
report = await lintGhostPackage(packagePath, process.cwd());
writeLintReport(report, opts.format);
process.exit(report.errors > 0 ? 1 : 0);
await exitCli(report.errors > 0 ? 1 : 0);
return;
}

Expand All @@ -50,9 +50,9 @@ export function registerFingerprintCommands(cli: CAC): void {

writeLintReport(report, opts.format);

process.exit(report.errors > 0 ? 1 : 0);
await exitCli(report.errors > 0 ? 1 : 0);
} catch (err) {
failFromError(err);
await failFromError(err);
}
});

Expand Down
8 changes: 4 additions & 4 deletions packages/ghost/src/commands/gather-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { GhostGatherResult } from "../embed/index.js";
import { gatherGhostPackage, loadGhostSnapshot } from "../embed/index.js";
import { appendGhostEvent, resolveRunId } from "../observability-events.js";
import { resolveGhostPackage } from "../package.js";
import { failFromError } from "./errors.js";
import { exitCli, failFromError } from "./errors.js";

export function registerGatherCommand(cli: CAC): void {
cli
Expand All @@ -27,7 +27,7 @@ export function registerGatherCommand(cli: CAC): void {
try {
if (opts.format !== "markdown" && opts.format !== "json") {
console.error("Error: --format must be 'markdown' or 'json'");
process.exit(2);
await exitCli(2);
return;
}

Expand All @@ -53,9 +53,9 @@ export function registerGatherCommand(cli: CAC): void {
} else {
process.stdout.write(formatMenuMarkdown(menu));
}
process.exit(0);
await exitCli(0);
} catch (err) {
failFromError(err);
await failFromError(err);
}
});
}
Expand Down
8 changes: 4 additions & 4 deletions packages/ghost/src/commands/init-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { UsageError } from "#ghost-core";
import { initGhostPackage } from "../package.js";
import { addChecksDir } from "../scan/check-scaffold.js";
import { getInitBody } from "../scan/templates.js";
import { failFromError } from "./errors.js";
import { exitCli, failFromError } from "./errors.js";

export function registerInitCommand(cli: CAC): void {
cli
Expand All @@ -29,7 +29,7 @@ export function registerInitCommand(cli: CAC): void {
console.error(
"Error: ghost init no longer accepts a positional directory. Use --package <dir> for an exact package directory.",
);
process.exit(2);
await exitCli(2);
return;
}
const exactPackage =
Expand Down Expand Up @@ -97,9 +97,9 @@ export function registerInitCommand(cli: CAC): void {
}
}
}
process.exit(0);
await exitCli(0);
} catch (err) {
failFromError(err);
await failFromError(err);
}
});
}
Expand Down
10 changes: 5 additions & 5 deletions packages/ghost/src/commands/manifest-command.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { CAC } from "cac";
import { buildCliManifest } from "./command-discovery.js";
import { failFromError } from "./errors.js";
import { exitCli, failFromError } from "./errors.js";

/**
* Emit a self-describing manifest of the CLI: every command, its curated
Expand All @@ -15,11 +15,11 @@ export function registerManifestCommand(cli: CAC): void {
"Emit a self-describing JSON manifest of every command and flag.",
)
.option("--format <fmt>", "Output format: json", { default: "json" })
.action((opts) => {
.action(async (opts) => {
try {
if (opts.format !== "json") {
console.error("Error: ghost manifest supports only --format json");
process.exit(2);
await exitCli(2);
return;
}
const manifest = {
Expand All @@ -28,9 +28,9 @@ export function registerManifestCommand(cli: CAC): void {
data: buildCliManifest(cli, cli.name),
};
process.stdout.write(`${JSON.stringify(manifest, null, 2)}\n`);
process.exit(0);
await exitCli(0);
} catch (err) {
failFromError(err);
await failFromError(err);
}
});
}
12 changes: 6 additions & 6 deletions packages/ghost/src/commands/pull-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { appendGhostEvent, resolveRunId } from "../observability-events.js";
import { resolveGhostPackage } from "../package.js";
import { GHOST_EVENTS_FILENAME } from "../scan/constants.js";
import { resolveGitRoot } from "../scan/package-paths.js";
import { failFromError } from "./errors.js";
import { exitCli, failFromError } from "./errors.js";

export function registerPullCommand(cli: CAC): void {
cli
Expand Down Expand Up @@ -37,12 +37,12 @@ export function registerPullCommand(cli: CAC): void {
try {
if (opts.format !== "markdown" && opts.format !== "json") {
console.error("Error: --format must be 'markdown' or 'json'");
process.exit(2);
await exitCli(2);
return;
}
if (opts.order !== "steering" && opts.order !== "given") {
console.error("Error: --order must be 'steering' or 'given'");
process.exit(2);
await exitCli(2);
return;
}

Expand Down Expand Up @@ -80,7 +80,7 @@ export function registerPullCommand(cli: CAC): void {
}

if (result.ids.length === 0) {
process.exit(2);
await exitCli(2);
return;
}

Expand All @@ -91,9 +91,9 @@ export function registerPullCommand(cli: CAC): void {
} else {
process.stdout.write(formatPullMarkdown(result));
}
process.exit(0);
await exitCli(0);
} catch (err) {
failFromError(err);
await failFromError(err);
}
});
}
Expand Down
8 changes: 4 additions & 4 deletions packages/ghost/src/commands/pulse-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from "../observability-events.js";
import { resolveGhostPackage } from "../package.js";
import { loadGhostPackage } from "../scan/fingerprint-package.js";
import { failFromError } from "./errors.js";
import { exitCli, failFromError } from "./errors.js";

export function registerPulseCommand(cli: CAC): void {
cli
Expand All @@ -23,7 +23,7 @@ export function registerPulseCommand(cli: CAC): void {
try {
if (opts.format !== "markdown" && opts.format !== "json") {
console.error("Error: --format must be 'markdown' or 'json'");
process.exit(2);
await exitCli(2);
return;
}

Expand All @@ -38,9 +38,9 @@ export function registerPulseCommand(cli: CAC): void {
} else {
process.stdout.write(formatPulseMarkdown(report));
}
process.exit(0);
await exitCli(0);
} catch (err) {
failFromError(err);
await failFromError(err);
}
});
}
Expand Down
10 changes: 5 additions & 5 deletions packages/ghost/src/commands/review-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
formatReviewPacket,
} from "../review/review-packet.js";
import { loadGhostPackage } from "../scan/fingerprint-package.js";
import { failFromError } from "./errors.js";
import { exitCli, failFromError } from "./errors.js";

const execFileAsync = promisify(execFile);

Expand All @@ -35,7 +35,7 @@ export function registerReviewCommand(cli: CAC): void {
const format = opts.json ? "json" : opts.format;
if (format !== "markdown" && format !== "json") {
console.error("Error: --format must be 'markdown' or 'json'");
process.exit(2);
await exitCli(2);
return;
}

Expand All @@ -45,7 +45,7 @@ export function registerReviewCommand(cli: CAC): void {
console.error(
"No checks directory. Run `ghost checks init` to add review assertions.",
);
process.exit(2);
await exitCli(2);
return;
}
const diffText = await resolveDiff({
Expand All @@ -62,9 +62,9 @@ export function registerReviewCommand(cli: CAC): void {
? `${JSON.stringify(packet, null, 2)}\n`
: formatReviewPacket(packet),
);
process.exit(0);
await exitCli(0);
} catch (err) {
failFromError(err);
await failFromError(err);
}
});
}
Expand Down
10 changes: 5 additions & 5 deletions packages/ghost/src/commands/skill-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import type { CAC } from "cac";
import { loadSkillBundle, UsageError } from "#ghost-core";
import { failFromError } from "./errors.js";
import { exitCli, failFromError } from "./errors.js";

// The bundle assets are copied to `dist/skill-bundle` (sibling of `commands/`).
const SKILL_BUNDLE_ROOT = fileURLToPath(
Expand Down Expand Up @@ -37,7 +37,7 @@ export function registerSkillCommand(cli: CAC): void {
try {
if (action !== "install") {
console.error("Error: ghost skill currently supports only `install`");
process.exit(2);
await exitCli(2);
return;
}

Expand All @@ -53,7 +53,7 @@ export function registerSkillCommand(cli: CAC): void {
console.error(
`Error: ${outDir} already contains SKILL.md. Pass --force to reinstall.`,
);
process.exit(3);
await exitCli(3);
return;
}

Expand All @@ -70,9 +70,9 @@ export function registerSkillCommand(cli: CAC): void {
`Wrote ${written.length} file${written.length === 1 ? "" : "s"} to ${outDir}:\n`,
);
for (const file of written) process.stdout.write(` ${file}\n`);
process.exit(0);
await exitCli(0);
} catch (err) {
failFromError(err);
await failFromError(err);
}
});
}
Expand Down
Loading
Loading