#!/usr/bin/env node import * as child_process from "child_process"; import * as fs from "fs"; import inquirer from "inquirer"; import open from "open"; import * as path from "path"; const jsLikeFileExtensions = [ "mtsx", "ctsx", "tsx", "mts", "cts", "ts", "mjsx", "cjsx", "jsx", "mjs", "cjs", "js", ]; class UserError extends Error { constructor(message) { super(message); this.name = "UserError"; } } let savedProjectPath = process.argv[2] || undefined; const isDryRun = process.argv.includes("--dry-run"); const isNeon = process.argv.includes("--neon"); const typeFromArgs = ["js", "next"].find(s => process.argv.includes(`--${s}`)); const packageManagerFromArgs = ["npm", "yarn", "pnpm", "bun"].find(s => process.argv.includes(`--${s}`)); const isClient = process.argv.includes("--client"); const isServer = process.argv.includes("--server"); const ansis = { red: "\x1b[31m", blue: "\x1b[34m", green: "\x1b[32m", yellow: "\x1b[33m", clear: "\x1b[0m", bold: "\x1b[1m", }; const colorize = { red: (strings, ...values) => ansis.red + templateIdentity(strings, ...values) + ansis.clear, blue: (strings, ...values) => ansis.blue + templateIdentity(strings, ...values) + ansis.clear, green: (strings, ...values) => ansis.green + templateIdentity(strings, ...values) + ansis.clear, yellow: (strings, ...values) => ansis.yellow + templateIdentity(strings, ...values) + ansis.clear, bold: (strings, ...values) => ansis.bold + templateIdentity(strings, ...values) + ansis.clear, } const filesCreated = []; const filesModified = []; const commandsExecuted = []; const packagesToInstall = []; const writeFileHandlers = []; const nextSteps = [ `Create an account and Stack Auth API key for your project on https://app.stack-auth.com`, ]; async function main() { // Welcome message console.log(); console.log(` ██████ ██████████████ ████████████████████ ████████████████████ WELCOME TO █████████████████ ╔═╗╔╦╗╔═╗╔═╗╦╔═ ┌─┐┬ ┬┌┬┐┬ ┬ █████████████ ╚═╗ ║ ╠═╣║ ╠╩╗ ├─┤│ │ │ ├─┤ █████████████ ████ ╚═╝ ╩ ╩ ╩╚═╝╩ ╩ ┴ ┴└─┘ ┴ ┴ ┴ █████████████████ ██████ ██ ████ ████ █████ █████ ██████ `); console.log(); // Wait just briefly so we can use `Steps` in here (it's defined only after the call to `main()`) await new Promise((resolve) => resolve()); // Prepare some stuff await clearStdin(); const projectPath = await getProjectPath(); // Steps const { packageJson } = await Steps.getProject(); const type = await Steps.getProjectType({ packageJson }); await Steps.addStackPackage(type); if (isNeon) packagesToInstall.push('@neondatabase/serverless'); await Steps.writeEnvVars(type); if (type === "next") { const projectInfo = await Steps.getNextProjectInfo({ packageJson }); await Steps.updateNextLayoutFile(projectInfo); await Steps.writeStackAppFile(projectInfo, "server"); await Steps.writeNextHandlerFile(projectInfo); await Steps.writeNextLoadingFile(projectInfo); nextSteps.push(`Copy the environment variables from the new API key into your .env.local file`) } else if (type === "js") { const defaultExtension = await Steps.guessDefaultFileExtension(); const where = await Steps.getServerOrClientOrBoth(); const srcPath = await Steps.guessSrcPath(); const appFiles = []; for (const w of where) { const { fileName } = await Steps.writeStackAppFile({ type, defaultExtension, indentation: " ", srcPath, }, w); appFiles.push(fileName); } nextSteps.push( `Copy the environment variables from the new API key into your own environment and reference them in ${appFiles.join(" and ")}`, `Follow the instructions on how to use Stack Auth's vanilla SDK at http://docs.stack-auth.com/others/js-client`, ); } else { throw new Error("Unknown type: " + type); } const { packageManager } = await Steps.getPackageManager(); await Steps.ensureReady(type); // Install dependencies console.log(); console.log(colorize.bold`Installing dependencies...`); const installCommand = packageManager === "yarn" ? "yarn add" : `${packageManager} install`; await shellNicelyFormatted(`${installCommand} ${packagesToInstall.join(' ')}`, { shell: true, cwd: projectPath, }); // Write files console.log(); console.log(colorize.bold`Writing files...`); console.log(); for (const writeFileHandler of writeFileHandlers) { await writeFileHandler(); } console.log(`${colorize.green`√`} Done writing files`); console.log('\n\n\n'); console.log(colorize.bold`${colorize.green`Installation succeeded!`}`); console.log(); console.log("Commands executed:"); for (const command of commandsExecuted) { console.log(` ${colorize.blue`${command}`}`); } console.log(); console.log("Files written:"); for (const file of filesModified) { console.log(` ${colorize.yellow`${file}`}`); } for (const file of filesCreated) { console.log(` ${colorize.green`${file}`}`); } console.log(); // Success! console.log(` ${colorize.green`===============================================`} ${colorize.green`Successfully installed Stack! 🚀🚀🚀`} Next steps: ${[...nextSteps.entries()].map(([index, step]) => `${index + 1}. ${step}`).join("\n")} ${type === "next" ? `Then, you will be able to access your sign-in page on http://your-website.example.com/handler/sign-in. That's it!` : "That's it!"} ${colorize.green`===============================================`} For more information, please visit https://docs.stack-auth.com/getting-started/setup `.trim()); if (!process.env.STACK_DISABLE_INTERACTIVE) { await open("https://app.stack-auth.com/wizard-congrats"); } } main() .catch((err) => { if (!(err instanceof UserError)) { console.error(err); } console.error('\n\n\n\n'); console.log(colorize.red`===============================================`); console.error(); if (err instanceof UserError) { console.error(`${colorize.red`ERROR!`} ${err.message}`); } else { console.error("An error occurred during the initialization process."); } console.error(); console.log(colorize.red`===============================================`); console.error(); console.error( "If you need assistance, please try installing Stack manually as described in https://docs.stack-auth.com/getting-started/setup or join our Discord where we're happy to help: https://discord.stack-auth.com" ); if (!(err instanceof UserError)) { console.error(""); console.error(`Error message: ${err.message}`); } console.error(); process.exit(1); }); const Steps = { async getProject() { let projectPath = await getProjectPath(); if (!fs.existsSync(projectPath)) { throw new UserError(`The project path ${projectPath} does not exist`); } const packageJsonPath = path.join(projectPath, "package.json"); if (!fs.existsSync(packageJsonPath)) { throw new UserError( `The package.json file does not exist in the project path ${projectPath}. You must initialize a new project first before installing Stack.` ); } const packageJsonText = fs.readFileSync(packageJsonPath, "utf-8"); let packageJson; try { packageJson = JSON.parse(packageJsonText); } catch (e) { throw new UserError(`package.json file is not valid JSON: ${e}`); } return { packageJson }; }, async getProjectType({ packageJson }) { if (typeFromArgs) return typeFromArgs; const maybeNextProject = await Steps.maybeGetNextProjectInfo({ packageJson }); if (!("error" in maybeNextProject)) return "next"; const { type } = assertInteractive() && await inquirer.prompt([ { type: "list", name: "type", message: "Which integration would you like to install?", choices: [ { name: "None (vanilla JS, Node.js, etc)", value: "js" }, { name: "Next.js", value: "next" }, ] } ]); return type; }, async getStackPackageName(type, install = false) { return { "js": (install && process.env.STACK_JS_INSTALL_PACKAGE_NAME_OVERRIDE) || "@stackframe/js", "next": (install && process.env.STACK_NEXT_INSTALL_PACKAGE_NAME_OVERRIDE) || "@stackframe/stack", }[type] ?? throwErr("Unknown type in addStackPackage: " + type); }, async addStackPackage(type) { packagesToInstall.push(await Steps.getStackPackageName(type, true)); }, async getNextProjectInfo({ packageJson }) { const maybe = await Steps.maybeGetNextProjectInfo({ packageJson }); if ("error" in maybe) throw new UserError(maybe.error); return maybe; }, async maybeGetNextProjectInfo({ packageJson }) { const projectPath = await getProjectPath(); const nextVersionInPackageJson = packageJson?.dependencies?.["next"] ?? packageJson?.devDependencies?.["next"]; if (!nextVersionInPackageJson) { return { error: `The project at ${projectPath} does not appear to be a Next.js project, or does not have 'next' installed as a dependency.` }; } if ( !nextVersionInPackageJson.includes("14") && !nextVersionInPackageJson.includes("15") && nextVersionInPackageJson !== "latest" ) { return { error: `The project at ${projectPath} is using an unsupported version of Next.js (found ${nextVersionInPackageJson}).\n\nOnly Next.js 14 & 15 projects are currently supported. See Next's upgrade guide: https://nextjs.org/docs/app/building-your-application/upgrading/version-14` }; } const nextConfigPathWithoutExtension = path.join(projectPath, "next.config"); const nextConfigFileExtension = await findJsExtension( nextConfigPathWithoutExtension ); const nextConfigPath = nextConfigPathWithoutExtension + "." + (nextConfigFileExtension ?? "js"); if (!fs.existsSync(nextConfigPath)) { return { error: `Expected file at ${nextConfigPath}. Only Next.js projects are currently supported.` }; } const hasSrcAppFolder = fs.existsSync(path.join(projectPath, "src/app")); const srcPath = path.join(projectPath, hasSrcAppFolder ? "src" : ""); const appPath = path.join(srcPath, "app"); if (!fs.existsSync(appPath)) { return { error: `The app path ${appPath} does not exist. Only the Next.js app router is supported.` }; } const dryUpdateNextLayoutFileResult = await Steps.dryUpdateNextLayoutFile({ appPath, defaultExtension: "jsx" }); return { type: "next", srcPath, appPath, defaultExtension: dryUpdateNextLayoutFileResult.fileExtension, indentation: dryUpdateNextLayoutFileResult.indentation, }; }, async writeEnvVars(type) { const projectPath = await getProjectPath(); // TODO: in non-Next environments, ask the user what method they prefer for envvars if (type !== "next") return false; const envLocalPath = path.join(projectPath, ".env.local"); const potentialEnvLocations = [ path.join(projectPath, ".env"), path.join(projectPath, ".env.development"), path.join(projectPath, ".env.default"), path.join(projectPath, ".env.defaults"), path.join(projectPath, ".env.example"), envLocalPath, ]; if (potentialEnvLocations.every((p) => !fs.existsSync(p))) { laterWriteFile( envLocalPath, "# Stack Auth keys\n# Get these variables by creating a project on https://app.stack-auth.com.\nNEXT_PUBLIC_STACK_PROJECT_ID=\nNEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=\nSTACK_SECRET_SERVER_KEY=\n" ); return true; } return false; }, async dryUpdateNextLayoutFile({ appPath, defaultExtension }) { const layoutPathWithoutExtension = path.join(appPath, "layout"); const layoutFileExtension = (await findJsExtension(layoutPathWithoutExtension)) ?? defaultExtension; const layoutPath = layoutPathWithoutExtension + "." + layoutFileExtension; const layoutContent = (await readFile(layoutPath)) ?? throwErr( `The layout file at ${layoutPath} does not exist. Stack requires a layout file to be present in the /app folder.` ); const updatedLayoutResult = (await getUpdatedLayout(layoutContent)) ?? throwErr( "Unable to parse root layout file. Make sure it contains a tag. If it still doesn't work, you may need to manually install Stack. See: https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#root-layout-required" ); const updatedLayoutContent = updatedLayoutResult.content; return { path: layoutPath, updatedContent: updatedLayoutContent, fileExtension: layoutFileExtension, indentation: updatedLayoutResult.indentation }; }, async updateNextLayoutFile(projectInfo) { const res = await Steps.dryUpdateNextLayoutFile(projectInfo); laterWriteFile(res.path, res.updatedContent); return res; }, async writeStackAppFile({ type, srcPath, defaultExtension, indentation }, clientOrServer) { const packageName = await Steps.getStackPackageName(type); const clientOrServerCap = { client: "Client", server: "Server", }[clientOrServer] ?? throwErr("unknown clientOrServer " + clientOrServer); const relativeStackAppPath = { js: `stack/${clientOrServer}`, next: "stack", }[type] ?? throwErr("unknown type"); const stackAppPathWithoutExtension = path.join(srcPath, relativeStackAppPath); const stackAppFileExtension = (await findJsExtension(stackAppPathWithoutExtension)) ?? defaultExtension; const stackAppPath = stackAppPathWithoutExtension + "." + stackAppFileExtension; const stackAppContent = await readFile(stackAppPath); if (stackAppContent) { if (!stackAppContent.includes("@stackframe/")) { throw new UserError( `A file at the path ${stackAppPath} already exists. Stack uses the stack.ts file to initialize the Stack SDK. Please remove the existing file and try again.` ); } throw new UserError( `It seems that you already installed Stack in this project.` ); } laterWriteFileIfNotExists( stackAppPath, ` ${type === "next" ? `import "server-only";` : ""} import { Stack${clientOrServerCap}App } from ${JSON.stringify(packageName)}; export const stack${clientOrServerCap}App = new Stack${clientOrServerCap}App({ ${indentation}tokenStore: ${type === "next" ? '"nextjs-cookie"' : (clientOrServer === "client" ? '"cookie"' : '"memory"')},${ type === "js" ? `\n\n${indentation}// get your Stack Auth API keys from https://app.stack-auth.com${clientOrServer === "client" ? ` and store them in a safe place (eg. environment variables)` : ""}` : ""}${ type === "js" ? `\n${indentation}publishableClientKey: ${clientOrServer === "server" ? 'process.env.STACK_PUBLISHABLE_CLIENT_KEY' : 'INSERT_YOUR_PUBLISHABLE_CLIENT_KEY_HERE'},` : ""}${ type === "js" && clientOrServer === "server" ? `\n${indentation}secretServerKey: process.env.STACK_SECRET_SERVER_KEY,` : ""} }); `.trim() + "\n" ); return { fileName: stackAppPath }; }, async writeNextHandlerFile(projectInfo) { const handlerPathWithoutExtension = path.join( projectInfo.appPath, "handler/[...stack]/page" ); const handlerFileExtension = (await findJsExtension(handlerPathWithoutExtension)) ?? projectInfo.defaultExtension; const handlerPath = handlerPathWithoutExtension + "." + handlerFileExtension; const handlerContent = await readFile(handlerPath); if (handlerContent && !handlerContent.includes("@stackframe/")) { throw new UserError( `A file at the path ${handlerPath} already exists. Stack uses the /handler path to handle incoming requests. Please remove the existing file and try again.` ); } laterWriteFileIfNotExists( handlerPath, `import { StackHandler } from "@stackframe/stack";\nimport { stackServerApp } from "../../../stack";\n\nexport default function Handler(props${ handlerFileExtension.includes("ts") ? ": unknown" : "" }) {\n${projectInfo.indentation}return ;\n}\n` ); }, async writeNextLoadingFile(projectInfo) { let loadingPathWithoutExtension = path.join(projectInfo.appPath, "loading"); const loadingFileExtension = (await findJsExtension(loadingPathWithoutExtension)) ?? projectInfo.defaultExtension; const loadingPath = loadingPathWithoutExtension + "." + loadingFileExtension; laterWriteFileIfNotExists( loadingPath, `export default function Loading() {\n${projectInfo.indentation}// Stack uses React Suspense, which will render this page while user data is being fetched.\n${projectInfo.indentation}// See: https://nextjs.org/docs/app/api-reference/file-conventions/loading\n${projectInfo.indentation}return <>;\n}\n` ); }, async getPackageManager() { if (packageManagerFromArgs) return { packageManager: packageManagerFromArgs }; const packageManager = await promptPackageManager(); const versionCommand = `${packageManager} --version`; try { await shellNicelyFormatted(versionCommand, { shell: true, quiet: true }); } catch (err) { console.error(err); throw new UserError( `Could not run the package manager command '${versionCommand}'. Please make sure ${packageManager} is installed on your system.` ); } return { packageManager }; }, async ensureReady(type) { const projectPath = await getProjectPath(); const typeString = { js: "JavaScript", next: "Next.js" }[type] ?? throwErr("unknown type"); const isReady = !!process.env.STACK_DISABLE_INTERACTIVE || (await inquirer.prompt([ { type: "confirm", name: "ready", message: `Found a ${typeString} project at ${projectPath} — ready to install Stack Auth?`, default: true, }, ])).ready; if (!isReady) { throw new UserError("Installation aborted."); } }, async getServerOrClientOrBoth() { if (isClient && isServer) return ["server", "client"]; if (isServer) return ["server"]; if (isClient) return ["client"]; return (await inquirer.prompt([{ type: "list", name: "type", message: "Do you want to use Stack Auth on the server, or on the client?", choices: [ { name: "Client (eg. Vite, HTML)", value: ["client"] }, { name: "Server (eg. Node.js)", value: ["server"] }, { name: "Both", value: ["server", "client"] } ] }])).type; }, /** * note: this is a heuristic, specific frameworks may have better heuristics (eg. the Next.js code uses the extension of the global layout file) */ async guessDefaultFileExtension() { const projectPath = await getProjectPath(); const hasTsConfig = fs.existsSync( path.join(projectPath, "tsconfig.json") ); return hasTsConfig ? "ts" : "js"; }, /** * note: this is a heuristic, specific frameworks may have better heuristics (eg. the Next.js code uses the location of the app folder) */ async guessSrcPath() { const projectPath = await getProjectPath(); const potentialSrcPath = path.join(projectPath, "src"); const hasSrcFolder = fs.existsSync( path.join(projectPath, "src") ); return hasSrcFolder ? potentialSrcPath : projectPath; }, }; async function getUpdatedLayout(originalLayout) { let layout = originalLayout; const indentation = guessIndentation(originalLayout); const firstImportLocationM1 = /\simport\s/.exec(layout)?.index; const hasStringAsFirstLine = layout.startsWith('"') || layout.startsWith("'"); const importInsertLocationM1 = firstImportLocationM1 ?? (hasStringAsFirstLine ? layout.indexOf("\n") : -1); const importInsertLocation = importInsertLocationM1 + 1; const importStatement = `import { StackProvider, StackTheme } from "@stackframe/stack";\nimport { stackServerApp } from "../stack";\n`; layout = layout.slice(0, importInsertLocation) + importStatement + layout.slice(importInsertLocation); const bodyOpenTag = /<\s*body[^>]*>/.exec(layout); const bodyCloseTag = /<\s*\/\s*body[^>]*>/.exec(layout); if (!bodyOpenTag || !bodyCloseTag) { return undefined; } const bodyOpenEndIndex = bodyOpenTag.index + bodyOpenTag[0].length; const bodyCloseStartIndex = bodyCloseTag.index; if (bodyCloseStartIndex <= bodyOpenEndIndex) { return undefined; } const lines = layout.split("\n"); const [bodyOpenEndLine, bodyOpenEndIndexInLine] = getLineIndex( lines, bodyOpenEndIndex ); const [bodyCloseStartLine, bodyCloseStartIndexInLine] = getLineIndex( lines, bodyCloseStartIndex ); const insertOpen = ""; const insertClose = ""; layout = layout.slice(0, bodyCloseStartIndex) + insertClose + layout.slice(bodyCloseStartIndex); layout = layout.slice(0, bodyOpenEndIndex) + insertOpen + layout.slice(bodyOpenEndIndex); return { content: `${layout}`, indentation, }; } function guessIndentation(str) { const lines = str.split("\n"); const linesLeadingWhitespaces = lines .map((line) => line.match(/^\s*/)[0]) .filter((ws) => ws.length > 0); const isMostlyTabs = linesLeadingWhitespaces.filter((ws) => ws.includes("\t")).length >= (linesLeadingWhitespaces.length * 2) / 3; if (isMostlyTabs) return "\t"; const linesLeadingWhitespacesCount = linesLeadingWhitespaces.map( (ws) => ws.length ); const min = Math.min(Infinity, ...linesLeadingWhitespacesCount); return Number.isFinite(min) ? " ".repeat(Math.max(2, min)) : " "; } function getLineIndex(lines, stringIndex) { let lineIndex = 0; for (let l = 0; l < lines.length; l++) { const line = lines[l]; if (stringIndex < lineIndex + line.length) { return [l, stringIndex - lineIndex]; } lineIndex += line.length + 1; } throw new Error( `Index ${stringIndex} is out of bounds for lines ${JSON.stringify(lines)}` ); } async function getProjectPath() { if (savedProjectPath === undefined) { savedProjectPath = process.cwd(); const askForPathModification = !fs.existsSync( path.join(savedProjectPath, "package.json") ); if (askForPathModification) { savedProjectPath = ( assertInteractive() && await inquirer.prompt([ { type: "input", name: "newPath", message: "Please enter the path to your project:", default: ".", }, ]) ).newPath; } } return savedProjectPath; } async function findJsExtension(fullPathWithoutExtension) { for (const ext of jsLikeFileExtensions) { const fullPath = fullPathWithoutExtension + "." + ext; if (fs.existsSync(fullPath)) { return ext; } } return null; } async function promptPackageManager() { const projectPath = await getProjectPath(); const yarnLock = fs.existsSync(path.join(projectPath, "yarn.lock")); const pnpmLock = fs.existsSync(path.join(projectPath, "pnpm-lock.yaml")); const npmLock = fs.existsSync(path.join(projectPath, "package-lock.json")); const bunLock = fs.existsSync(path.join(projectPath, "bun.lockb")); if (yarnLock && !pnpmLock && !npmLock && !bunLock) { return "yarn"; } else if (!yarnLock && pnpmLock && !npmLock && !bunLock) { return "pnpm"; } else if (!yarnLock && !pnpmLock && npmLock && !bunLock) { return "npm"; } else if (!yarnLock && !pnpmLock && !npmLock && bunLock) { return "bun"; } const answers = assertInteractive() && await inquirer.prompt([ { type: "list", name: "packageManager", message: "Which package manager are you using for this project?", choices: ["npm", "yarn", "pnpm", "bun"], }, ]); return answers.packageManager; } async function shellNicelyFormatted(command, { quiet, ...options }) { let ui, interval; if (!quiet) { console.log(); ui = new inquirer.ui.BottomBar(); let dots = 4; ui.updateBottomBar( colorize.blue`Running command: ${command}...` ); interval = setInterval(() => { if (!isDryRun) { ui.updateBottomBar( colorize.blue`Running command: ${command}${".".repeat(dots++ % 5)}` ); } }, 700); } try { if (!isDryRun) { const child = child_process.spawn(command, options); if (!quiet) { child.stdout.pipe(ui.log); child.stderr.pipe(ui.log); } await new Promise((resolve, reject) => { child.on("exit", (code) => { if (code === 0) { resolve(); } else { reject(new Error(`Command ${command} failed with code ${code}`)); } }); }); } else { console.log(`[DRY-RUN] Would have run: ${command}`); } if (!quiet) { commandsExecuted.push(command); ui.updateBottomBar( `${colorize.green`√`} Command ${command} succeeded\n` ); } } catch (e) { if (!quiet) { ui.updateBottomBar( `${colorize.red`X`} Command ${command} failed\n` ); } throw e; } finally { clearTimeout(interval); if (!quiet) { ui.close(); } } } async function readFile(fullPath) { try { if (!isDryRun) { return fs.readFileSync(fullPath, "utf-8"); } } catch (err) { if (err.code === "ENOENT") { return null; } throw err; } } async function writeFile(fullPath, content) { let create = !fs.existsSync(fullPath); if (!isDryRun) { fs.mkdirSync(path.dirname(fullPath), { recursive: true }); fs.writeFileSync(fullPath, content); } else { console.log(`[DRY-RUN] Would have written to ${fullPath}`); } const relativeToProjectPath = path.relative(await getProjectPath(), fullPath); if (!create) { filesModified.push(relativeToProjectPath); } else { filesCreated.push(relativeToProjectPath); } } function laterWriteFile(...args) { writeFileHandlers.push(async () => { await writeFile(...args); }) } async function writeFileIfNotExists(fullPath, content) { if (!fs.existsSync(fullPath)) { await writeFile(fullPath, content); } } function laterWriteFileIfNotExists(...args) { writeFileHandlers.push(async () => { await writeFileIfNotExists(...args); }) } function assertInteractive() { if (process.env.STACK_DISABLE_INTERACTIVE) { throw new UserError("STACK_DISABLE_INTERACTIVE is set, but wizard requires interactivity to complete. Make sure you supplied all required command line arguments!"); } return true; } function throwErr(message) { throw new Error(message); } // TODO import this function from stack-shared instead (but that would require us to fix the build to let us import it) export function templateIdentity(strings, ...values) { if (strings.length === 0) return ""; if (values.length !== strings.length - 1) throw new Error("Invalid number of values; must be one less than strings"); return strings.slice(1).reduce((result, string, i) => `${result}${values[i] ?? "n/a"}${string}`, strings[0]); } async function clearStdin() { await new Promise((resolve) => { if (process.stdin.isTTY) { process.stdin.setRawMode(true); } process.stdin.resume(); process.stdin.removeAllListeners('data'); const flush = () => { while (process.stdin.read() !== null) {} if (process.stdin.isTTY) { process.stdin.setRawMode(false); } resolve(); }; // Add a small delay to allow any buffered input to clear setTimeout(flush, 10); }); }