@settlemint/sdk-utils
Version:
Shared utilities and helper functions for SettleMint SDK modules
546 lines (533 loc) • 16.4 kB
JavaScript
import { greenBright, inverse, magentaBright, redBright, whiteBright, yellowBright } from "yoctocolors";
import { spawn } from "node:child_process";
import isInCi from "is-in-ci";
import yoctoSpinner from "yocto-spinner";
import { Table } from "console-table-printer";
//#region src/terminal/should-print.ts
/**
* Determines whether terminal output should be printed based on environment variables.
*
* **Environment Variable Precedence:**
* 1. `SETTLEMINT_DISABLE_TERMINAL="true"` - Completely disables all terminal output (highest priority)
* 2. `CLAUDECODE`, `REPL_ID`, or `AGENT` (any truthy value) - Enables quiet mode, suppressing info/debug/status messages
*
* **Quiet Mode Behavior:**
* When quiet mode is active (Claude Code environments), this function returns `false` to suppress
* informational output. However, warnings and errors are always displayed regardless of quiet mode,
* as they are handled separately in the `note()` function with level-based filtering.
*
* @returns `true` if terminal output should be printed, `false` if suppressed
*/
function shouldPrint() {
if (process.env.SETTLEMINT_DISABLE_TERMINAL === "true") {
return false;
}
if (process.env.CLAUDECODE || process.env.REPL_ID || process.env.AGENT) {
return false;
}
return true;
}
//#endregion
//#region src/terminal/ascii.ts
/**
* Prints the SettleMint ASCII art logo to the console in magenta color.
* Used for CLI branding and visual identification.
*
* @example
* import { ascii } from "@settlemint/sdk-utils/terminal";
*
* // Prints the SettleMint logo
* ascii();
*/
const ascii = () => {
if (!shouldPrint()) {
return;
}
console.log(magentaBright(`
_________ __ __ .__ _____ .__ __
/ _____/ _____/ |__/ |_| | ____ / \\ |__| _____/ |_
\\_____ \\_/ __ \\ __\\ __\\ | _/ __ \\ / \\ / \\| |/ \\ __\\
/ \\ ___/| | | | | |_\\ ___// Y \\ | | \\ |
/_________/\\_____>__| |__| |____/\\_____>____|____/__|___|__/__|
`));
};
//#endregion
//#region src/logging/mask-tokens.ts
/**
* Masks sensitive SettleMint tokens in output text by replacing them with asterisks.
* Handles personal access tokens (PAT), application access tokens (AAT), and service account tokens (SAT).
*
* @param output - The text string that may contain sensitive tokens
* @returns The text with any sensitive tokens masked with asterisks
* @example
* import { maskTokens } from "@settlemint/sdk-utils/terminal";
*
* // Masks a token in text
* const masked = maskTokens("Token: sm_pat_****"); // "Token: ***"
*/
const maskTokens = (output) => {
return output.replace(/sm_(pat|aat|sat)_[0-9a-zA-Z]+/g, "***");
};
//#endregion
//#region src/terminal/cancel.ts
/**
* Error class used to indicate that the operation was cancelled.
* This error is used to signal that the operation should be aborted.
*/
var CancelError = class extends Error {};
/**
* Displays an error message in red inverse text and throws a CancelError.
* Used to terminate execution with a visible error message.
* Any sensitive tokens in the message are masked before display.
*
* @param msg - The error message to display
* @returns never - Function does not return as it throws an error
* @example
* import { cancel } from "@settlemint/sdk-utils/terminal";
*
* // Exits process with error message
* cancel("An error occurred");
*/
const cancel = (msg) => {
console.log("");
console.log(inverse(redBright(maskTokens(msg))));
console.log("");
throw new CancelError(msg);
};
//#endregion
//#region src/terminal/execute-command.ts
/**
* Error class for command execution errors
* @extends Error
*/
var CommandError = class extends Error {
/**
* Constructs a new CommandError
* @param message - The error message
* @param code - The exit code of the command
* @param output - The output of the command
*/
constructor(message, code, output) {
super(message);
this.code = code;
this.output = output;
}
};
/**
* Checks if we're in quiet mode (Claude Code environment)
*/
function isQuietMode() {
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
* @param options - Options for customizing command execution
* @returns Array of output strings from stdout and stderr
* @throws {CommandError} If the process fails to start or exits with non-zero code
* @example
* import { executeCommand } from "@settlemint/sdk-utils/terminal";
*
* // Execute git clone
* await executeCommand("git", ["clone", "repo-url"]);
*
* // Execute silently
* await executeCommand("npm", ["install"], { silent: true });
*/
async function executeCommand(command, args, options) {
const { silent,...spawnOptions } = options ?? {};
const quietMode = isQuietMode();
const shouldSuppressOutput = quietMode ? silent !== false : !!silent;
const child = spawn(command, args, {
...spawnOptions,
env: {
...process.env,
...options?.env
}
});
process.stdin.pipe(child.stdin);
const output = [];
const stdoutOutput = [];
const stderrOutput = [];
return new Promise((resolve, reject) => {
child.stdout.on("data", (data) => {
const maskedData = maskTokens(data.toString());
if (!shouldSuppressOutput) {
process.stdout.write(maskedData);
}
output.push(maskedData);
stdoutOutput.push(maskedData);
});
child.stderr.on("data", (data) => {
const maskedData = maskTokens(data.toString());
if (!shouldSuppressOutput) {
process.stderr.write(maskedData);
}
output.push(maskedData);
stderrOutput.push(maskedData);
});
const showErrorOutput = () => {
if (quietMode && shouldSuppressOutput && output.length > 0) {
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) => {
process.stdin.unpipe(child.stdin);
if (code === 0 || code === null || code === 143) {
resolve(output);
return;
}
showErrorOutput();
reject(new CommandError(`Command "${command}" exited with code ${code}`, code, output));
});
});
}
//#endregion
//#region src/terminal/intro.ts
/**
* Displays an introductory message in magenta text with padding.
* Any sensitive tokens in the message are masked before display.
*
* @param msg - The message to display as introduction
* @example
* import { intro } from "@settlemint/sdk-utils/terminal";
*
* // Display intro message
* intro("Starting deployment...");
*/
const intro = (msg) => {
if (!shouldPrint()) {
return;
}
console.log("");
console.log(magentaBright(maskTokens(msg)));
console.log("");
};
//#endregion
//#region src/terminal/note.ts
/**
* Applies color to a message if not already colored.
* @param msg - The message to colorize
* @param level - The severity level determining the color
* @returns Colorized message (yellow for warnings, red for errors, unchanged for info)
*/
function colorize(msg, level) {
if (msg.includes("\x1B[")) {
return msg;
}
if (level === "warn") {
return yellowBright(msg);
}
if (level === "error") {
return redBright(msg);
}
return msg;
}
/**
* Determines whether a message should be printed based on its level and quiet mode.
* @param level - The severity level of the message
* @returns true if the message should be printed, false otherwise
*/
function canPrint(level) {
if (level !== "info") {
return true;
}
return shouldPrint();
}
/**
* Prepares a message for display by converting Error objects and masking tokens.
* @param value - The message string or Error object
* @param level - The severity level (stack traces are included for errors)
* @returns Masked message text, optionally with stack trace
*/
function prepareMessage(value, level) {
let text;
if (value instanceof Error) {
text = value.message;
if (level === "error" && value.stack) {
text = `${text}\n\n${value.stack}`;
}
} else {
text = value;
}
return maskTokens(text);
}
/**
* 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. Can be either:
* - A string: Displayed directly with appropriate styling
* - An Error object: The error message is displayed, and for level "error", the stack trace is automatically included
* @param level - The note level: "info" (default), "warn" for warning styling, or "error" for error styling
* @example
* import { note } from "@settlemint/sdk-utils/terminal";
*
* // Display info note
* note("Operation completed successfully");
*
* // Display warning note
* note("Low disk space remaining", "warn");
*
* // Display error note (string)
* note("Operation failed", "error");
*
* // Display error with stack trace automatically (Error object)
* try {
* // some operation
* } catch (error) {
* // If error is an Error object and level is "error", stack trace is included automatically
* note(error, "error");
* }
*/
const note = (message, level = "info") => {
if (!canPrint(level)) {
return;
}
const msg = prepareMessage(message, level);
console.log("");
if (level === "warn") {
console.warn(colorize(msg, level));
} else if (level === "error") {
console.error(colorize(msg, level));
} else {
console.log(msg);
}
};
//#endregion
//#region src/terminal/list.ts
/**
* Displays a list of items in a formatted manner, supporting nested items.
*
* @param title - The title of the list
* @param items - The items to display, can be strings or arrays for nested items
* @returns The formatted list
* @example
* import { list } from "@settlemint/sdk-utils/terminal";
*
* // Simple list
* list("Use cases", ["use case 1", "use case 2", "use case 3"]);
*
* // Nested list
* list("Providers", [
* "AWS",
* ["us-east-1", "eu-west-1"],
* "Azure",
* ["eastus", "westeurope"]
* ]);
*/
function list(title, items) {
const formatItems = (items$1) => {
return items$1.map((item) => {
if (Array.isArray(item)) {
return item.map((subItem) => ` • ${subItem}`).join("\n");
}
return ` • ${item}`;
}).join("\n");
};
return note(`${title}:\n\n${formatItems(items)}`);
}
//#endregion
//#region src/terminal/outro.ts
/**
* Displays a closing message in green inverted text with padding.
* Any sensitive tokens in the message are masked before display.
*
* @param msg - The message to display as conclusion
* @example
* import { outro } from "@settlemint/sdk-utils/terminal";
*
* // Display outro message
* outro("Deployment completed successfully!");
*/
const outro = (msg) => {
if (!shouldPrint()) {
return;
}
console.log("");
console.log(inverse(greenBright(maskTokens(msg))));
console.log("");
};
//#endregion
//#region src/terminal/spinner.ts
/**
* Error class used to indicate that the spinner operation failed.
* This error is used to signal that the operation should be aborted.
*/
var SpinnerError = class extends Error {
constructor(message, originalError) {
super(message);
this.originalError = originalError;
this.name = "SpinnerError";
}
};
/**
* Displays a loading spinner while executing an async task.
* Shows progress with start/stop messages and handles errors.
* Spinner is disabled in CI environments.
*
* @param options - Configuration options for the spinner
* @returns The result from the executed task
* @throws Will exit process with code 1 if task fails
* @example
* import { spinner } from "@settlemint/sdk-utils/terminal";
*
* // Show spinner during async task
* const result = await spinner({
* startMessage: "Deploying...",
* task: async () => {
* // Async work here
* return "success";
* },
* stopMessage: "Deployed successfully!"
* });
*/
const spinner = async (options) => {
const handleError = (error) => {
note(error, "error");
throw new SpinnerError(error.message, error);
};
if (isInCi || !shouldPrint()) {
try {
return await options.task();
} catch (err) {
return handleError(err);
}
}
const spinner$1 = yoctoSpinner({ stream: process.stdout }).start(options.startMessage);
try {
const result = await options.task(spinner$1);
spinner$1.success(options.stopMessage);
await new Promise((resolve) => process.nextTick(resolve));
return result;
} catch (err) {
spinner$1.error(redBright(`${options.startMessage} --> Error!`));
return handleError(err);
}
};
//#endregion
//#region src/string.ts
/**
* Capitalizes the first letter of a string.
*
* @param val - The string to capitalize
* @returns The input string with its first letter capitalized
*
* @example
* import { capitalizeFirstLetter } from "@settlemint/sdk-utils";
*
* const capitalized = capitalizeFirstLetter("hello");
* // Returns: "Hello"
*/
function capitalizeFirstLetter(val) {
return String(val).charAt(0).toUpperCase() + String(val).slice(1);
}
/**
* Converts a camelCase string to a human-readable string.
*
* @param s - The camelCase string to convert
* @returns The human-readable string
*
* @example
* import { camelCaseToWords } from "@settlemint/sdk-utils";
*
* const words = camelCaseToWords("camelCaseString");
* // Returns: "Camel Case String"
*/
function camelCaseToWords(s) {
const result = s.replace(/([a-z])([A-Z])/g, "$1 $2");
const withSpaces = result.replace(/([A-Z])([a-z])/g, " $1$2");
const capitalized = capitalizeFirstLetter(withSpaces);
return capitalized.replace(/\s+/g, " ").trim();
}
/**
* Replaces underscores and hyphens with spaces.
*
* @param s - The string to replace underscores and hyphens with spaces
* @returns The input string with underscores and hyphens replaced with spaces
*
* @example
* import { replaceUnderscoresAndHyphensWithSpaces } from "@settlemint/sdk-utils";
*
* const result = replaceUnderscoresAndHyphensWithSpaces("Already_Spaced-Second");
* // Returns: "Already Spaced Second"
*/
function replaceUnderscoresAndHyphensWithSpaces(s) {
return s.replace(/[-_]/g, " ");
}
/**
* Truncates a string to a maximum length and appends "..." if it is longer.
*
* @param value - The string to truncate
* @param maxLength - The maximum length of the string
* @returns The truncated string or the original string if it is shorter than the maximum length
*
* @example
* import { truncate } from "@settlemint/sdk-utils";
*
* const truncated = truncate("Hello, world!", 10);
* // Returns: "Hello, wor..."
*/
function truncate(value, maxLength) {
if (value.length <= maxLength) {
return value;
}
return `${value.slice(0, maxLength)}...`;
}
//#endregion
//#region src/terminal/table.ts
/**
* Displays data in a formatted table in the terminal.
*
* @param title - Title to display above the table
* @param data - Array of objects to display in table format
* @example
* import { table } from "@settlemint/sdk-utils/terminal";
*
* const data = [
* { name: "Item 1", value: 100 },
* { name: "Item 2", value: 200 }
* ];
*
* table("My Table", data);
*/
function table(title, data) {
if (!shouldPrint()) {
return;
}
note(title);
if (!data || data.length === 0) {
note("No data to display");
return;
}
const columnKeys = Object.keys(data[0]);
const table$1 = new Table({ columns: columnKeys.map((key) => ({
name: key,
title: whiteBright(camelCaseToWords(key)),
alignment: "left"
})) });
table$1.addRows(data);
table$1.printTable();
}
//#endregion
export { CancelError, CommandError, SpinnerError, ascii, cancel, executeCommand, intro, list, maskTokens, note, outro, spinner, table };
//# sourceMappingURL=terminal.js.map