Skip to content
5 changes: 2 additions & 3 deletions sdk/cli/src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Command, CommanderError } from "@commander-js/extra-typings";
import { AbortPromptError, CancelPromptError, ExitPromptError, ValidationError } from "@inquirer/core";
import { ascii, CancelError, maskTokens, note, SpinnerError } from "@settlemint/sdk-utils/terminal";
import { magentaBright, redBright } from "yoctocolors";
import { magentaBright } from "yoctocolors";
import { telemetry } from "@/utils/telemetry";
import pkg from "../../package.json";
import { getInstalledSdkVersion, validateSdkVersionFromCommand } from "../utils/sdk-version";
Expand Down Expand Up @@ -113,8 +113,7 @@ async function onError(sdkcli: ExtendedCommand, argv: string[], error: Error) {
}

if (!(error instanceof CancelError || error instanceof SpinnerError)) {
const errorMessage = maskTokens(error.message);
note(redBright(`Unknown error: ${errorMessage}\n\n${error.stack}`));
note(error, "error");
}

// Get the command path from the command that threw the error
Expand Down
29 changes: 7 additions & 22 deletions sdk/utils/src/environment/write-env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,7 @@ describe("writeEnv", () => {
});

it("should merge with existing environment variables", async () => {
const existingEnv =
"EXISTING_VAR=existing\nSETTLEMINT_INSTANCE=https://old.example.com";
const existingEnv = "EXISTING_VAR=existing\nSETTLEMINT_INSTANCE=https://old.example.com";
await writeFile(ENV_FILE, existingEnv);

const newEnv = {
Expand All @@ -104,10 +103,7 @@ describe("writeEnv", () => {

it("should handle arrays and objects", async () => {
const env = {
SETTLEMINT_THEGRAPH_SUBGRAPHS_ENDPOINTS: [
"https://graph1.example.com",
"https://graph2.example.com",
],
SETTLEMINT_THEGRAPH_SUBGRAPHS_ENDPOINTS: ["https://graph1.example.com", "https://graph2.example.com"],
};

await writeEnv({
Expand Down Expand Up @@ -137,18 +133,11 @@ describe("writeEnv", () => {
cwd: TEST_DIR,
});
const initialContent = await Bun.file(ENV_FILE).text();
expect(initialContent).toContain(
"SETTLEMINT_INSTANCE=https://dev.example.com",
);
expect(initialContent).toContain(
"SETTLEMINT_CUSTOM_DEPLOYMENT=test-custom-deployment",
);
expect(initialContent).toContain("SETTLEMINT_INSTANCE=https://dev.example.com");
expect(initialContent).toContain("SETTLEMINT_CUSTOM_DEPLOYMENT=test-custom-deployment");
expect(initialContent).toContain("SETTLEMINT_WORKSPACE=test-workspace");
expect(initialContent).toContain("MY_VAR=my-value");
const {
SETTLEMINT_CUSTOM_DEPLOYMENT: _SETTLEMINT_CUSTOM_DEPLOYMENT,
...existingEnv
} = initialEnv;
const { SETTLEMINT_CUSTOM_DEPLOYMENT: _SETTLEMINT_CUSTOM_DEPLOYMENT, ...existingEnv } = initialEnv;

await writeEnv({
prod: false,
Expand All @@ -159,12 +148,8 @@ describe("writeEnv", () => {

const updatedContent = await Bun.file(ENV_FILE).text();
expect(updatedContent).toContain("SETTLEMINT_WORKSPACE=test-workspace");
expect(updatedContent).toContain(
"SETTLEMINT_INSTANCE=https://dev.example.com",
);
expect(updatedContent).not.toContain(
"SETTLEMINT_CUSTOM_DEPLOYMENT=test-custom-deployment",
);
expect(updatedContent).toContain("SETTLEMINT_INSTANCE=https://dev.example.com");
expect(updatedContent).not.toContain("SETTLEMINT_CUSTOM_DEPLOYMENT=test-custom-deployment");
expect(updatedContent).toContain("MY_VAR=my-value");
});
});
136 changes: 136 additions & 0 deletions sdk/utils/src/terminal/execute-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,140 @@ describe("executeCommand", () => {

expect(output.some((line) => line.includes("test"))).toBe(true);
});

test("quiet mode suppresses output on success", async () => {
const originalCLAUDECODE = process.env.CLAUDECODE;
const originalWrite = process.stdout.write;
let stdoutWritten = false;

// biome-ignore lint/suspicious/noExplicitAny: Test mocking requires any
process.stdout.write = mock((_chunk: any) => {
stdoutWritten = true;
return true;
});

try {
process.env.CLAUDECODE = "true";
await executeCommand("echo", ["quiet mode test"]);
expect(stdoutWritten).toBe(false);
} finally {
process.stdout.write = originalWrite;
if (originalCLAUDECODE === undefined) {
delete process.env.CLAUDECODE;
} else {
process.env.CLAUDECODE = originalCLAUDECODE;
}
}
});

test("quiet mode shows output on error", async () => {
const originalCLAUDECODE = process.env.CLAUDECODE;
const originalStdoutWrite = process.stdout.write;
const originalStderrWrite = process.stderr.write;
let outputShown = false;

// biome-ignore lint/suspicious/noExplicitAny: Test mocking requires any
process.stdout.write = mock((_chunk: any) => {
outputShown = true;
return true;
});

// biome-ignore lint/suspicious/noExplicitAny: Test mocking requires any
process.stderr.write = mock((_chunk: any) => {
outputShown = true;
return true;
});

try {
process.env.CLAUDECODE = "true";
await expect(() =>
executeCommand("node", ["-e", "console.log('output'); console.error('error'); process.exit(1);"]),
).toThrow();
// Output should be shown on error even in quiet mode
expect(outputShown).toBe(true);
} finally {
process.stdout.write = originalStdoutWrite;
process.stderr.write = originalStderrWrite;
if (originalCLAUDECODE === undefined) {
delete process.env.CLAUDECODE;
} else {
process.env.CLAUDECODE = originalCLAUDECODE;
}
}
});

test("quiet mode respects silent: false to force output", async () => {
const originalCLAUDECODE = process.env.CLAUDECODE;
const originalWrite = process.stdout.write;
let stdoutWritten = false;

// biome-ignore lint/suspicious/noExplicitAny: Test mocking requires any
process.stdout.write = mock((_chunk: any) => {
stdoutWritten = true;
return true;
});

try {
process.env.CLAUDECODE = "true";
await executeCommand("echo", ["force output in quiet mode"], { silent: false });
expect(stdoutWritten).toBe(true);
} finally {
process.stdout.write = originalWrite;
if (originalCLAUDECODE === undefined) {
delete process.env.CLAUDECODE;
} else {
process.env.CLAUDECODE = originalCLAUDECODE;
}
}
});

test("quiet mode works with REPL_ID environment variable", async () => {
const originalREPL_ID = process.env.REPL_ID;
const originalWrite = process.stdout.write;
let stdoutWritten = false;

// biome-ignore lint/suspicious/noExplicitAny: Test mocking requires any
process.stdout.write = mock((_chunk: any) => {
stdoutWritten = true;
return true;
});

try {
process.env.REPL_ID = "test-repl";
await executeCommand("echo", ["repl quiet test"]);
expect(stdoutWritten).toBe(false);
} finally {
process.stdout.write = originalWrite;
if (originalREPL_ID === undefined) {
delete process.env.REPL_ID;
} else {
process.env.REPL_ID = originalREPL_ID;
}
}
});

test("quiet mode works with AGENT environment variable", async () => {
const originalAGENT = process.env.AGENT;
const originalWrite = process.stdout.write;
let stdoutWritten = false;

// biome-ignore lint/suspicious/noExplicitAny: Test mocking requires any
process.stdout.write = mock((_chunk: any) => {
stdoutWritten = true;
return true;
});

try {
process.env.AGENT = "true";
await executeCommand("echo", ["agent quiet test"]);
expect(stdoutWritten).toBe(false);
} finally {
process.stdout.write = originalWrite;
if (originalAGENT === undefined) {
delete process.env.AGENT;
} else {
process.env.AGENT = originalAGENT;
}
}
});
});
39 changes: 37 additions & 2 deletions sdk/utils/src/terminal/execute-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,19 @@ export class CommandError extends Error {
}
}

/**
* Checks if we're in quiet mode (Claude Code environment)
*/
function isQuietMode(): boolean {
return !!(process.env.CLAUDECODE || process.env.REPL_ID || process.env.AGENT);
}

/**
* Executes a command with the given arguments in a child process.
* Pipes stdin to the child process and captures stdout/stderr output.
* Masks any sensitive tokens in the output before displaying or returning.
* In quiet mode (when CLAUDECODE, REPL_ID, or AGENT env vars are set),
* output is suppressed unless the command errors out.
*
* @param command - The command to execute
* @param args - Array of arguments to pass to the command
Expand All @@ -54,26 +63,50 @@ export async function executeCommand(
options?: ExecuteCommandOptions,
): Promise<string[]> {
const { silent, ...spawnOptions } = options ?? {};
const quietMode = isQuietMode();
// In quiet mode, suppress output unless explicitly overridden with silent: false
const shouldSuppressOutput = quietMode ? silent !== false : !!silent;

const child = spawn(command, args, { ...spawnOptions, env: { ...process.env, ...options?.env } });
process.stdin.pipe(child.stdin);
const output: string[] = [];
const stdoutOutput: string[] = [];
const stderrOutput: string[] = [];

return new Promise((resolve, reject) => {
child.stdout.on("data", (data: Buffer | string) => {
const maskedData = maskTokens(data.toString());
if (!silent) {
if (!shouldSuppressOutput) {
process.stdout.write(maskedData);
}
output.push(maskedData);
stdoutOutput.push(maskedData);
});
child.stderr.on("data", (data: Buffer | string) => {
const maskedData = maskTokens(data.toString());
if (!silent) {
if (!shouldSuppressOutput) {
process.stderr.write(maskedData);
}
output.push(maskedData);
stderrOutput.push(maskedData);
});

const showErrorOutput = () => {
// In quiet mode, show output on error
if (quietMode && shouldSuppressOutput && output.length > 0) {
// Write stdout to stdout and stderr to stderr
if (stdoutOutput.length > 0) {
process.stdout.write(stdoutOutput.join(""));
}
if (stderrOutput.length > 0) {
process.stderr.write(stderrOutput.join(""));
}
}
};

child.on("error", (err) => {
process.stdin.unpipe(child.stdin);
showErrorOutput();
reject(new CommandError(err.message, "code" in err && typeof err.code === "number" ? err.code : 1, output));
});
child.on("close", (code) => {
Expand All @@ -82,6 +115,8 @@ export async function executeCommand(
resolve(output);
return;
}
// In quiet mode, show output on error
showErrorOutput();
reject(new CommandError(`Command "${command}" exited with code ${code}`, code, output));
});
});
Expand Down
Loading
Loading