UNPKG

@settlemint/sdk-utils

Version:

Shared utilities and helper functions for SettleMint SDK modules

865 lines (841 loc) 27.2 kB
//#region rolldown:runtime var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion const node_fs_promises = __toESM(require("node:fs/promises")); const node_path = __toESM(require("node:path")); const find_up = __toESM(require("find-up")); const glob = __toESM(require("glob")); const package_manager_detector_detect = __toESM(require("package-manager-detector/detect")); const __antfu_install_pkg = __toESM(require("@antfu/install-pkg")); const yoctocolors = __toESM(require("yoctocolors")); const node_child_process = __toESM(require("node:child_process")); const is_in_ci = __toESM(require("is-in-ci")); const yocto_spinner = __toESM(require("yocto-spinner")); const console_table_printer = __toESM(require("console-table-printer")); const __npmcli_package_json = __toESM(require("@npmcli/package-json")); //#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 (0, find_up.findUp)("package.json", { cwd }); if (!packageJsonPath) { if (fallbackToCwd) { return process.cwd(); } throw new Error("Unable to find project root (no package.json found)"); } return (0, node_path.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 (0, node_fs_promises.stat)(path); return true; } catch { return false; } } //#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/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 (0, find_up.findUp)([ "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb", "bun.lock" ], { cwd: startDir }); if (lockFilePath) { const packageJsonPath = (0, node_path.join)((0, node_path.dirname)(lockFilePath), "package.json"); const hasWorkSpaces = await packageJsonHasWorkspaces(packageJsonPath); return hasWorkSpaces ? (0, node_path.dirname)(lockFilePath) : null; } let currentDir = startDir; while (currentDir !== "/") { const packageJsonPath = (0, node_path.join)(currentDir, "package.json"); if (await packageJsonHasWorkspaces(packageJsonPath)) { return currentDir; } const parentDir = (0, node_path.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 = (0, node_path.join)(monoRepoRoot, "package.json"); const packageJson = tryParseJson(await (0, node_fs_promises.readFile)(packageJsonPath, "utf-8")); const workspaces = packageJson?.workspaces ?? []; const packagePaths = await Promise.all(workspaces.map(async (workspace) => { const matches = await (0, glob.glob)((0, node_path.join)(monoRepoRoot, workspace, "package.json")); return matches.map((match) => (0, node_path.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 (0, node_fs_promises.readFile)(packageJsonPath, "utf-8")); if (packageJson?.workspaces && Array.isArray(packageJson?.workspaces) && packageJson?.workspaces.length > 0) { return true; } } return false; } //#endregion //#region src/package-manager/download-and-extract.ts /** * Formats a directory path by removing trailing slashes and whitespace * * @param targetDir - The directory path to format * @returns The formatted directory path * @example * import { formatTargetDir } from "@settlemint/sdk-utils/package-manager"; * * const formatted = formatTargetDir("/path/to/dir/ "); // "/path/to/dir" */ function formatTargetDir(targetDir) { return targetDir?.trim().replace(/\/+$/g, ""); } /** * Checks if a directory is empty or contains only a .git folder * * @param path - The directory path to check * @returns True if directory is empty or contains only .git, false otherwise * @example * import { isEmpty } from "@settlemint/sdk-utils/package-manager"; * * if (await isEmpty("/path/to/dir")) { * // Directory is empty * } */ async function isEmpty(path) { const files = await (0, node_fs_promises.readdir)(path); return files.length === 0 || files.length === 1 && files[0] === ".git"; } /** * Removes all contents of a directory except the .git folder * * @param dir - The directory path to empty * @example * import { emptyDir } from "@settlemint/sdk-utils/package-manager"; * * await emptyDir("/path/to/dir"); // Removes all contents except .git */ async function emptyDir(dir) { if (!await exists(dir)) return; for (const file of await (0, node_fs_promises.readdir)(dir)) { if (file === ".git") continue; await (0, node_fs_promises.rm)((0, node_path.resolve)(dir, file), { recursive: true, force: true }); } } //#endregion //#region src/package-manager/get-package-manager.ts /** * Detects the package manager used in the current project * * @param targetDir - The directory to check for package manager (optional, defaults to process.cwd()) * @returns The name of the package manager * @example * import { getPackageManager } from "@settlemint/sdk-utils/package-manager"; * * const packageManager = await getPackageManager(); * console.log(`Using ${packageManager}`); */ async function getPackageManager(targetDir) { const packageManager = await (0, package_manager_detector_detect.detect)({ cwd: targetDir || process.cwd() }); return packageManager?.name ?? "npm"; } //#endregion //#region src/package-manager/get-package-manager-executable.ts /** * Retrieves the executable command and arguments for the package manager * * @param targetDir - The directory to check for package manager (optional, defaults to process.cwd()) * @returns An object containing the command and arguments for the package manager * @example * import { getPackageManagerExecutable } from "@settlemint/sdk-utils/package-manager"; * * const { command, args } = await getPackageManagerExecutable(); * console.log(`Using ${command} with args: ${args.join(" ")}`); */ async function getPackageManagerExecutable(targetDir) { const packageManager = await getPackageManager(targetDir ?? process.cwd()); switch (packageManager) { case "pnpm": return { command: "pnpm", args: ["dlx"] }; case "bun": return { command: "bunx", args: [] }; case "yarn": return { command: "yarn", args: ["create"] }; } return { command: "npx", args: [] }; } //#endregion //#region src/terminal/should-print.ts /** * Returns true if the terminal should print, false otherwise. * @returns true if the terminal should print, false otherwise. */ function shouldPrint() { return process.env.SETTLEMINT_DISABLE_TERMINAL !== "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((0, yoctocolors.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((0, yoctocolors.inverse)((0, yoctocolors.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; } }; /** * 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. * * @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 child = (0, node_child_process.spawn)(command, args, { ...spawnOptions, env: { ...process.env, ...options?.env } }); process.stdin.pipe(child.stdin); const output = []; return new Promise((resolve$1, reject) => { child.stdout.on("data", (data) => { const maskedData = maskTokens(data.toString()); if (!silent) { process.stdout.write(maskedData); } output.push(maskedData); }); child.stderr.on("data", (data) => { const maskedData = maskTokens(data.toString()); if (!silent) { process.stderr.write(maskedData); } output.push(maskedData); }); child.on("error", (err) => { process.stdin.unpipe(child.stdin); 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$1(output); return; } 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((0, yoctocolors.magentaBright)(maskTokens(msg))); console.log(""); }; //#endregion //#region src/terminal/note.ts /** * Displays a note message with optional warning level formatting. * Regular notes are displayed in normal text, while warnings are shown in yellow. * Any sensitive tokens in the message are masked before display. * * @param message - The message to display as a note * @param level - The note level: "info" (default) or "warn" for warning 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"); */ const note = (message, level = "info") => { if (!shouldPrint()) { return; } const maskedMessage = maskTokens(message); console.log(""); if (level === "warn") { console.warn((0, yoctocolors.yellowBright)(maskedMessage)); return; } console.log(maskedMessage); }; //#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((0, yoctocolors.inverse)((0, yoctocolors.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) => { const errorMessage = maskTokens(error.message); note((0, yoctocolors.redBright)(`${errorMessage}\n\n${error.stack}`)); throw new SpinnerError(errorMessage, error); }; if (is_in_ci.default || !shouldPrint()) { try { return await options.task(); } catch (err) { return handleError(err); } } const spinner$1 = (0, yocto_spinner.default)({ stream: process.stdout }).start(options.startMessage); try { const result = await options.task(spinner$1); spinner$1.success(options.stopMessage); await new Promise((resolve$1) => process.nextTick(resolve$1)); return result; } catch (err) { spinner$1.error((0, yoctocolors.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 console_table_printer.Table({ columns: columnKeys.map((key) => ({ name: key, title: (0, yoctocolors.whiteBright)(camelCaseToWords(key)), alignment: "left" })) }); table$1.addRows(data); table$1.printTable(); } //#endregion //#region src/package-manager/install-dependencies.ts /** * Installs one or more packages as dependencies using the detected package manager * * @param pkgs - A single package name or array of package names to install * @param cwd - The directory to run the installation in * @returns A promise that resolves when installation is complete * @throws If package installation fails * @example * import { installDependencies } from "@settlemint/sdk-utils/package-manager"; * * // Install a single package * await installDependencies("express"); * * // Install multiple packages * await installDependencies(["express", "cors"]); */ async function installDependencies(pkgs, cwd) { try { await (0, __antfu_install_pkg.installPackage)(pkgs, { silent: true, additionalArgs: ["--exact"], cwd }); } catch (err) { const error = err instanceof Error ? err.message : "Unknown error"; note(`Failed to install ${Array.isArray(pkgs) ? `dependencies '${pkgs.join(", ")}'` : `dependency '${pkgs}'`}: ${error}`, "warn"); } } //#endregion //#region src/package-manager/is-package-installed.ts /** * Checks if a package is installed in the project's dependencies, devDependencies, or peerDependencies. * * @param name - The name of the package to check * @param path - The path to the project root directory. If not provided, will be automatically determined * @returns Whether the package is installed * @throws If unable to read or parse the package.json file * @example * import { isPackageInstalled } from "@settlemint/sdk-utils/package-manager"; * * const isInstalled = await isPackageInstalled("@settlemint/sdk-utils"); * console.log(`@settlemint/sdk-utils is installed: ${isInstalled}`); */ async function isPackageInstalled(name, path) { const pkgJson = await __npmcli_package_json.default.load(path ?? await projectRoot()); const inDependencies = !!pkgJson.content.dependencies?.[name]; const inDevDependencies = !!pkgJson.content.devDependencies?.[name]; const inPeerDependencies = !!pkgJson.content.peerDependencies?.[name]; return inDependencies || inDevDependencies || inPeerDependencies; } //#endregion //#region src/package-manager/set-name.ts /** * Sets the name field in the package.json file * * @param name - The new name to set in the package.json file * @param path - The path to the project root directory. If not provided, will be automatically determined * @returns A promise that resolves when the package.json has been updated * @throws If unable to read, update or save the package.json file * @example * import { setName } from "@settlemint/sdk-utils/package-manager"; * * await setName("my-new-project-name"); */ async function setName(name, path) { const pkgJson = await __npmcli_package_json.default.load(path ?? await projectRoot()); pkgJson.update({ name }); await pkgJson.save(); } //#endregion exports.emptyDir = emptyDir; exports.formatTargetDir = formatTargetDir; exports.getPackageManager = getPackageManager; exports.getPackageManagerExecutable = getPackageManagerExecutable; exports.installDependencies = installDependencies; exports.isEmpty = isEmpty; exports.isPackageInstalled = isPackageInstalled; exports.setName = setName; //# sourceMappingURL=package-manager.cjs.map