@settlemint/sdk-utils
Version:
Shared utilities and helper functions for SettleMint SDK modules
1,110 lines (1,085 loc) • 35.2 kB
JavaScript
import { dirname, join } from "node:path";
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";
import { ZodError, z } from "zod";
import { config } from "@dotenvx/dotenvx";
import { readFile, stat, writeFile } from "node:fs/promises";
import { findUp } from "find-up";
import { glob } from "glob";
import { deepmerge } from "deepmerge-ts";
//#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
//#region src/validation/validate.ts
/**
* Validates a value against a given Zod schema.
*
* @param schema - The Zod schema to validate against.
* @param value - The value to validate.
* @returns The validated and parsed value.
* @throws Will throw an error if validation fails, with formatted error messages.
*
* @example
* import { validate } from "@settlemint/sdk-utils/validation";
*
* const validatedId = validate(IdSchema, "550e8400-e29b-41d4-a716-446655440000");
*/
function validate(schema, value) {
try {
return schema.parse(value);
} catch (error) {
if (error instanceof ZodError) {
const formattedErrors = error.issues.map((err) => `- ${err.path.join(".")}: ${err.message}`).join("\n");
throw new Error(`Validation error${error.issues.length > 1 ? "s" : ""}:\n${formattedErrors}`);
}
throw error;
}
}
//#endregion
//#region src/validation/access-token.schema.ts
/**
* Schema for validating application access tokens.
* Application access tokens start with 'sm_aat_' prefix.
*/
const ApplicationAccessTokenSchema = z.string().regex(/^sm_aat_.+$/);
/**
* Schema for validating personal access tokens.
* Personal access tokens start with 'sm_pat_' prefix.
*/
const PersonalAccessTokenSchema = z.string().regex(/^sm_pat_.+$/);
/**
* Schema for validating both application and personal access tokens.
* Accepts tokens starting with either 'sm_pat_' or 'sm_aat_' prefix.
*/
const AccessTokenSchema = z.string().regex(/^(sm_pat_.+|sm_aat_.+)$/);
//#endregion
//#region src/json.ts
/**
* Attempts to parse a JSON string into a typed value, returning a default value if parsing fails.
*
* @param value - The JSON string to parse
* @param defaultValue - The value to return if parsing fails or results in null/undefined
* @returns The parsed JSON value as type T, or the default value if parsing fails
*
* @example
* import { tryParseJson } from "@settlemint/sdk-utils";
*
* const config = tryParseJson<{ port: number }>(
* '{"port": 3000}',
* { port: 8080 }
* );
* // Returns: { port: 3000 }
*
* const invalid = tryParseJson<string[]>(
* 'invalid json',
* []
* );
* // Returns: []
*/
function tryParseJson(value, defaultValue = null) {
try {
const parsed = JSON.parse(value);
if (parsed === undefined || parsed === null) {
return defaultValue;
}
return parsed;
} catch (_err) {
return defaultValue;
}
}
/**
* Extracts a JSON object from a string.
*
* @param value - The string to extract the JSON object from
* @returns The parsed JSON object, or null if no JSON object is found
* @throws {Error} If the input string is too long (longer than 5000 characters)
* @example
* import { extractJsonObject } from "@settlemint/sdk-utils";
*
* const json = extractJsonObject<{ port: number }>(
* 'port info: {"port": 3000}',
* );
* // Returns: { port: 3000 }
*/
function extractJsonObject(value) {
if (value.length > 5e3) {
throw new Error("Input too long");
}
const result = /\{([\s\S]*)\}/.exec(value);
if (!result) {
return null;
}
return tryParseJson(result[0]);
}
/**
* Converts a value to a JSON stringifiable format.
*
* @param value - The value to convert
* @returns The JSON stringifiable value
*
* @example
* import { makeJsonStringifiable } from "@settlemint/sdk-utils";
*
* const json = makeJsonStringifiable<{ amount: bigint }>({ amount: BigInt(1000) });
* // Returns: '{"amount":"1000"}'
*/
function makeJsonStringifiable(value) {
if (value === undefined || value === null) {
return value;
}
return tryParseJson(JSON.stringify(value, (_, value$1) => typeof value$1 === "bigint" ? value$1.toString() : value$1));
}
//#endregion
//#region src/validation/unique-name.schema.ts
/**
* Schema for validating unique names used across the SettleMint platform.
* Only accepts lowercase alphanumeric characters and hyphens.
* Used for workspace names, application names, service names etc.
*
* @example
* import { UniqueNameSchema } from "@settlemint/sdk-utils/validation";
*
* // Validate a workspace name
* const isValidName = UniqueNameSchema.safeParse("my-workspace-123").success;
* // true
*
* // Invalid names will fail validation
* const isInvalidName = UniqueNameSchema.safeParse("My Workspace!").success;
* // false
*/
const UniqueNameSchema = z.string().regex(/^[a-z0-9-]+$/);
//#endregion
//#region src/validation/url.schema.ts
/**
* Schema for validating URLs.
*
* @example
* import { UrlSchema } from "@settlemint/sdk-utils/validation";
*
* // Validate a URL
* const isValidUrl = UrlSchema.safeParse("https://console.settlemint.com").success;
* // true
*
* // Invalid URLs will fail validation
* const isInvalidUrl = UrlSchema.safeParse("not-a-url").success;
* // false
*/
const UrlSchema = z.string().url();
/**
* Schema for validating URL paths.
*
* @example
* import { UrlPathSchema } from "@settlemint/sdk-utils/validation";
*
* // Validate a URL path
* const isValidPath = UrlPathSchema.safeParse("/api/v1/users").success;
* // true
*
* // Invalid paths will fail validation
* const isInvalidPath = UrlPathSchema.safeParse("not-a-path").success;
* // false
*/
const UrlPathSchema = z.string().regex(/^\/(?:[a-zA-Z0-9-_]+(?:\/[a-zA-Z0-9-_]+)*\/?)?$/, { message: "Invalid URL path format. Must start with '/' and can contain letters, numbers, hyphens, and underscores." });
/**
* Schema that accepts either a full URL or a URL path.
*
* @example
* import { UrlOrPathSchema } from "@settlemint/sdk-utils/validation";
*
* // Validate a URL
* const isValidUrl = UrlOrPathSchema.safeParse("https://console.settlemint.com").success;
* // true
*
* // Validate a path
* const isValidPath = UrlOrPathSchema.safeParse("/api/v1/users").success;
* // true
*/
const UrlOrPathSchema = z.union([UrlSchema, UrlPathSchema]);
//#endregion
//#region src/validation/dot-env.schema.ts
/**
* Use this value to indicate that the resources are not part of the SettleMint platform.
*/
const STANDALONE_INSTANCE = "standalone";
/**
* Use this value to indicate that the resources are not part of the SettleMint platform.
*/
const LOCAL_INSTANCE = "local";
/**
* Schema for validating environment variables used by the SettleMint SDK.
* Defines validation rules and types for configuration values like URLs,
* access tokens, workspace names, and service endpoints.
*/
const DotEnvSchema = z.object({
SETTLEMINT_INSTANCE: z.union([
UrlSchema,
z.literal(STANDALONE_INSTANCE),
z.literal(LOCAL_INSTANCE)
]).default("https://console.settlemint.com"),
SETTLEMINT_ACCESS_TOKEN: ApplicationAccessTokenSchema.optional(),
SETTLEMINT_PERSONAL_ACCESS_TOKEN: PersonalAccessTokenSchema.optional(),
SETTLEMINT_WORKSPACE: UniqueNameSchema.optional(),
SETTLEMINT_APPLICATION: UniqueNameSchema.optional(),
SETTLEMINT_BLOCKCHAIN_NETWORK: UniqueNameSchema.optional(),
SETTLEMINT_BLOCKCHAIN_NETWORK_CHAIN_ID: z.string().optional(),
SETTLEMINT_BLOCKCHAIN_NODE: UniqueNameSchema.optional(),
SETTLEMINT_BLOCKCHAIN_NODE_JSON_RPC_ENDPOINT: UrlSchema.optional(),
SETTLEMINT_BLOCKCHAIN_NODE_OR_LOAD_BALANCER: UniqueNameSchema.optional(),
SETTLEMINT_BLOCKCHAIN_NODE_OR_LOAD_BALANCER_JSON_RPC_ENDPOINT: UrlSchema.optional(),
SETTLEMINT_HASURA: UniqueNameSchema.optional(),
SETTLEMINT_HASURA_ENDPOINT: UrlSchema.optional(),
SETTLEMINT_HASURA_ADMIN_SECRET: z.string().optional(),
SETTLEMINT_HASURA_DATABASE_URL: z.string().optional(),
SETTLEMINT_THEGRAPH: UniqueNameSchema.optional(),
SETTLEMINT_THEGRAPH_SUBGRAPHS_ENDPOINTS: z.preprocess((value) => tryParseJson(value, []), z.array(UrlSchema).optional()),
SETTLEMINT_THEGRAPH_DEFAULT_SUBGRAPH: z.string().optional(),
SETTLEMINT_PORTAL: UniqueNameSchema.optional(),
SETTLEMINT_PORTAL_GRAPHQL_ENDPOINT: UrlSchema.optional(),
SETTLEMINT_PORTAL_REST_ENDPOINT: UrlSchema.optional(),
SETTLEMINT_PORTAL_WS_ENDPOINT: UrlSchema.optional(),
SETTLEMINT_HD_PRIVATE_KEY: UniqueNameSchema.optional(),
SETTLEMINT_HD_PRIVATE_KEY_FORWARDER_ADDRESS: z.string().optional(),
SETTLEMINT_ACCESSIBLE_PRIVATE_KEY: UniqueNameSchema.optional(),
SETTLEMINT_MINIO: UniqueNameSchema.optional(),
SETTLEMINT_MINIO_ENDPOINT: UrlSchema.optional(),
SETTLEMINT_MINIO_ACCESS_KEY: z.string().optional(),
SETTLEMINT_MINIO_SECRET_KEY: z.string().optional(),
SETTLEMINT_IPFS: UniqueNameSchema.optional(),
SETTLEMINT_IPFS_API_ENDPOINT: UrlSchema.optional(),
SETTLEMINT_IPFS_PINNING_ENDPOINT: UrlSchema.optional(),
SETTLEMINT_IPFS_GATEWAY_ENDPOINT: UrlSchema.optional(),
SETTLEMINT_CUSTOM_DEPLOYMENT: UniqueNameSchema.optional(),
SETTLEMINT_CUSTOM_DEPLOYMENT_ENDPOINT: UrlSchema.optional(),
SETTLEMINT_BLOCKSCOUT: UniqueNameSchema.optional(),
SETTLEMINT_BLOCKSCOUT_GRAPHQL_ENDPOINT: UrlSchema.optional(),
SETTLEMINT_BLOCKSCOUT_UI_ENDPOINT: UrlSchema.optional(),
SETTLEMINT_NEW_PROJECT_NAME: z.string().optional(),
SETTLEMINT_LOG_LEVEL: z.enum([
"debug",
"info",
"warn",
"error",
"none"
]).default("warn")
});
/**
* Partial version of the environment variables schema where all fields are optional.
* Useful for validating incomplete configurations during development or build time.
*/
const DotEnvSchemaPartial = DotEnvSchema.partial();
//#endregion
//#region src/validation/id.schema.ts
/**
* Schema for validating database IDs. Accepts both PostgreSQL UUIDs and MongoDB ObjectIDs.
* PostgreSQL UUIDs are 32 hexadecimal characters with hyphens (e.g. 123e4567-e89b-12d3-a456-426614174000).
* MongoDB ObjectIDs are 24 hexadecimal characters (e.g. 507f1f77bcf86cd799439011).
*
* @example
* import { IdSchema } from "@settlemint/sdk-utils/validation";
*
* // Validate PostgreSQL UUID
* const isValidUuid = IdSchema.safeParse("123e4567-e89b-12d3-a456-426614174000").success;
*
* // Validate MongoDB ObjectID
* const isValidObjectId = IdSchema.safeParse("507f1f77bcf86cd799439011").success;
*/
const IdSchema = z.union([z.string().uuid(), z.string().regex(/^[0-9a-fA-F]{24}$/)]);
//#endregion
//#region src/environment/load-env.ts
/**
* Loads environment variables from .env files.
* To enable encryption with dotenvx (https://www.dotenvx.com/docs) run `bunx dotenvx encrypt`
*
* @param validateEnv - Whether to validate the environment variables against the schema
* @param prod - Whether to load production environment variables
* @param path - Optional path to the directory containing .env files. Defaults to process.cwd()
* @returns A promise that resolves to the validated environment variables
* @throws Will throw an error if validation fails and validateEnv is true
* @example
* import { loadEnv } from '@settlemint/sdk-utils/environment';
*
* // Load and validate environment variables
* const env = await loadEnv(true, false);
* console.log(env.SETTLEMINT_INSTANCE);
*
* // Load without validation
* const rawEnv = await loadEnv(false, false);
*/
async function loadEnv(validateEnv, prod, path = process.cwd()) {
return loadEnvironmentEnv(validateEnv, !!prod, path);
}
async function loadEnvironmentEnv(validateEnv, prod, path = process.cwd()) {
if (prod) {
process.env.NODE_ENV = "production";
}
let { parsed } = config({
convention: "nextjs",
logLevel: "error",
overload: true,
quiet: true,
path: [join(path, ".env"), join(path, ".env.local")]
});
if (!parsed) {
parsed = {};
}
const defaultEnv = Object.fromEntries(Object.entries(process.env).filter(([_, value]) => typeof value === "string" && value !== ""));
try {
return validate(validateEnv ? DotEnvSchema : DotEnvSchemaPartial, {
...parsed,
...defaultEnv
});
} catch (error) {
cancel(error.message);
return {};
}
}
//#endregion
//#region src/filesystem/project-root.ts
/**
* Finds the root directory of the current project by locating the nearest package.json file
*
* @param fallbackToCwd - If true, will return the current working directory if no package.json is found
* @param cwd - The directory to start searching for the package.json file from (defaults to process.cwd())
* @returns Promise that resolves to the absolute path of the project root directory
* @throws Will throw an error if no package.json is found in the directory tree
* @example
* import { projectRoot } from "@settlemint/sdk-utils/filesystem";
*
* // Get project root path
* const rootDir = await projectRoot();
* console.log(`Project root is at: ${rootDir}`);
*/
async function projectRoot(fallbackToCwd = false, cwd) {
const packageJsonPath = await findUp("package.json", { cwd });
if (!packageJsonPath) {
if (fallbackToCwd) {
return process.cwd();
}
throw new Error("Unable to find project root (no package.json found)");
}
return dirname(packageJsonPath);
}
//#endregion
//#region src/filesystem/exists.ts
/**
* Checks if a file or directory exists at the given path
*
* @param path - The file system path to check for existence
* @returns Promise that resolves to true if the path exists, false otherwise
* @example
* import { exists } from "@settlemint/sdk-utils/filesystem";
*
* // Check if file exists before reading
* if (await exists('/path/to/file.txt')) {
* // File exists, safe to read
* }
*/
async function exists(path) {
try {
await stat(path);
return true;
} catch {
return false;
}
}
//#endregion
//#region src/filesystem/mono-repo.ts
/**
* Finds the root directory of a monorepo
*
* @param startDir - The directory to start searching from
* @returns The root directory of the monorepo or null if not found
* @example
* import { findMonoRepoRoot } from "@settlemint/sdk-utils/filesystem";
*
* const root = await findMonoRepoRoot("/path/to/your/project");
* console.log(root); // Output: /path/to/your/project/packages/core
*/
async function findMonoRepoRoot(startDir) {
const lockFilePath = await findUp([
"package-lock.json",
"yarn.lock",
"pnpm-lock.yaml",
"bun.lockb",
"bun.lock"
], { cwd: startDir });
if (lockFilePath) {
const packageJsonPath = join(dirname(lockFilePath), "package.json");
const hasWorkSpaces = await packageJsonHasWorkspaces(packageJsonPath);
return hasWorkSpaces ? dirname(lockFilePath) : null;
}
let currentDir = startDir;
while (currentDir !== "/") {
const packageJsonPath = join(currentDir, "package.json");
if (await packageJsonHasWorkspaces(packageJsonPath)) {
return currentDir;
}
const parentDir = dirname(currentDir);
if (parentDir === currentDir) {
break;
}
currentDir = parentDir;
}
return null;
}
/**
* Finds all packages in a monorepo
*
* @param projectDir - The directory to start searching from
* @returns An array of package directories
* @example
* import { findMonoRepoPackages } from "@settlemint/sdk-utils/filesystem";
*
* const packages = await findMonoRepoPackages("/path/to/your/project");
* console.log(packages); // Output: ["/path/to/your/project/packages/core", "/path/to/your/project/packages/ui"]
*/
async function findMonoRepoPackages(projectDir) {
try {
const monoRepoRoot = await findMonoRepoRoot(projectDir);
if (!monoRepoRoot) {
return [projectDir];
}
const packageJsonPath = join(monoRepoRoot, "package.json");
const packageJson = tryParseJson(await readFile(packageJsonPath, "utf-8"));
const workspaces = packageJson?.workspaces ?? [];
const packagePaths = await Promise.all(workspaces.map(async (workspace) => {
const matches = await glob(join(monoRepoRoot, workspace, "package.json"));
return matches.map((match) => join(match, ".."));
}));
const allPaths = packagePaths.flat();
return allPaths.length === 0 ? [projectDir] : [monoRepoRoot, ...allPaths];
} catch (_error) {
return [projectDir];
}
}
async function packageJsonHasWorkspaces(packageJsonPath) {
if (await exists(packageJsonPath)) {
const packageJson = tryParseJson(await readFile(packageJsonPath, "utf-8"));
if (packageJson?.workspaces && Array.isArray(packageJson?.workspaces) && packageJson?.workspaces.length > 0) {
return true;
}
}
return false;
}
//#endregion
//#region src/environment/write-env.ts
/**
* Writes environment variables to .env files across a project or monorepo
*
* @param options - The options for writing the environment variables
* @param options.prod - Whether to write production environment variables
* @param options.env - The environment variables to write
* @param options.secrets - Whether to write to .env.local files for secrets
* @param options.cwd - The directory to start searching for the package.json file from (defaults to process.cwd())
* @returns Promise that resolves when writing is complete
* @throws Will throw an error if writing fails
* @example
* import { writeEnv } from '@settlemint/sdk-utils/environment';
*
* // Write development environment variables
* await writeEnv({
* prod: false,
* env: {
* SETTLEMINT_INSTANCE: 'https://dev.example.com'
* },
* secrets: false
* });
*
* // Write production secrets
* await writeEnv({
* prod: true,
* env: {
* SETTLEMINT_ACCESS_TOKEN: 'secret-token'
* },
* secrets: true
* });
*/
async function writeEnv({ prod, env, secrets, cwd }) {
const projectDir = await projectRoot(true, cwd);
if (prod) {
process.env.NODE_ENV = "production";
}
const targetDirs = await findMonoRepoPackages(projectDir);
await Promise.all(targetDirs.map(async (dir) => {
const envFile = join(dir, secrets ? `.env${prod ? ".production" : ""}.local` : `.env${prod ? ".production" : ""}`);
let { parsed: currentEnv } = await exists(envFile) ? config({
path: envFile,
logLevel: "error",
quiet: true
}) : { parsed: {} };
if (!currentEnv) {
currentEnv = {};
}
const prunedEnv = pruneCurrentEnv(currentEnv, env);
const mergedEnv = deepmerge(prunedEnv, env);
await writeFile(envFile, stringify(mergedEnv));
}));
}
const quote = /[\s"'#]/;
function stringifyPair([key, val]) {
if (val === undefined) {
return undefined;
}
if (val === null) {
return `${key}=""`;
}
const type = typeof val;
if (type === "string") {
return `${key}=${quote.test(val) ? JSON.stringify(val) : val}`;
}
if (type === "boolean" || type === "number") {
return `${key}=${val}`;
}
if (type === "object") {
return `${key}=${JSON.stringify(val)}`;
}
throw new Error(`Unsupported type for key "${key}": ${type}`);
}
function stringify(obj) {
return Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)).map(stringifyPair).filter((value) => value !== undefined).join("\n");
}
/**
* Prunes the current environment variables from the new environment variables
*
* @param currentEnv - The current environment variables
* @param env - The new environment variables
* @returns The new environment variables with the current environment variables removed
*/
function pruneCurrentEnv(currentEnv, env) {
const dotEnvKeys = Object.keys(DotEnvSchema.shape);
return Object.fromEntries(Object.entries(currentEnv).filter(([key]) => {
if (dotEnvKeys.includes(key) && !env[key]) {
return false;
}
return true;
}));
}
//#endregion
export { loadEnv, writeEnv };
//# sourceMappingURL=environment.js.map