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");
});
});
61 changes: 50 additions & 11 deletions sdk/utils/src/terminal/note.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { maskTokens } from "@/logging/mask-tokens.js";
import { yellowBright } from "yoctocolors";
import { redBright, yellowBright } from "yoctocolors";
import { shouldPrint } from "./should-print.js";

/**
* Displays a note message with optional warning level formatting.
* Regular notes are displayed in normal text, while warnings are shown in yellow.
* Displays a note message with optional warning or error level formatting.
* Regular notes are displayed in normal text, warnings are shown in yellow, and errors in red.
* Any sensitive tokens in the message are masked before display.
* Warnings and errors are always displayed, even in quiet mode (when CLAUDECODE, REPL_ID, or AGENT env vars are set).
* When an Error object is provided with level "error", the stack trace is automatically included.
*
* @param message - The message to display as a note
* @param level - The note level: "info" (default) or "warn" for warning styling
* @param message - The message to display as a note, or an Error object
* @param level - The note level: "info" (default), "warn" for warning styling, or "error" for error styling
* @example
* import { note } from "@settlemint/sdk-utils/terminal";
*
Expand All @@ -17,18 +19,55 @@ import { shouldPrint } from "./should-print.js";
*
* // Display warning note
* note("Low disk space remaining", "warn");
*
* // Display error note
* note("Operation failed", "error");
*
* // Display error with stack trace automatically
* try {
* // some operation
* } catch (error) {
* note(error, "error");
* }
*/
export const note = (message: string, level: "info" | "warn" = "info"): void => {
if (!shouldPrint()) {
export const note = (message: string | Error, level: "info" | "warn" | "error" = "info"): void => {
let messageText: string;
let _error: Error | undefined;

if (message instanceof Error) {
_error = message;
messageText = message.message;
// For errors, automatically include stack trace
if (level === "error" && message.stack) {
messageText = `${messageText}\n\n${message.stack}`;
}
} else {
messageText = message;
}

const maskedMessage = maskTokens(messageText);
const _isQuietMode = process.env.CLAUDECODE || process.env.REPL_ID || process.env.AGENT;

// Always print warnings and errors, even in quiet mode
if (level === "warn" || level === "error") {
console.log("");
if (level === "warn") {
// Apply yellow color if not already colored (check if message contains ANSI codes)
const coloredMessage = maskedMessage.includes("\u001b[") ? maskedMessage : yellowBright(maskedMessage);
console.warn(coloredMessage);
} else {
// Apply red color if not already colored (check if message contains ANSI codes)
const coloredMessage = maskedMessage.includes("\u001b[") ? maskedMessage : redBright(maskedMessage);
console.error(coloredMessage);
}
return;
}
const maskedMessage = maskTokens(message);

console.log("");
if (level === "warn") {
console.warn(yellowBright(maskedMessage));
// For info messages, check if we should print
if (!shouldPrint()) {
return;
}

console.log("");
console.log(maskedMessage);
};
12 changes: 11 additions & 1 deletion sdk/utils/src/terminal/should-print.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
/**
* Returns true if the terminal should print, false otherwise.
* When CLAUDECODE, REPL_ID, or AGENT env vars are set, suppresses info/debug output
* but warnings and errors will still be displayed.
* @returns true if the terminal should print, false otherwise.
*/
export function shouldPrint() {
return process.env.SETTLEMINT_DISABLE_TERMINAL !== "true";
if (process.env.SETTLEMINT_DISABLE_TERMINAL === "true") {
return false;
}
// In quiet mode (Claude Code), suppress info/debug/status messages
// Warnings and errors will still be displayed via note() with appropriate levels
if (process.env.CLAUDECODE || process.env.REPL_ID || process.env.AGENT) {
return false;
}
return true;
}
2 changes: 1 addition & 1 deletion sdk/utils/src/terminal/spinner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ export interface SpinnerOptions<R> {
*/
export const spinner = async <R>(options: SpinnerOptions<R>): Promise<R> => {
const handleError = (error: Error) => {
note(error, "error");
const errorMessage = maskTokens(error.message);
note(redBright(`${errorMessage}\n\n${error.stack}`));
throw new SpinnerError(errorMessage, error);
};
if (isInCi || !shouldPrint()) {
Expand Down
Loading