From b6d829f517378577cc62be91629128148db27b8b Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Fri, 24 Oct 2025 13:56:50 -0700 Subject: [PATCH 1/4] Init script now installs MCP servers --- packages/init-stack/package.json | 14 +- packages/init-stack/src/index.ts | 516 ++++++++++++++++++++++++++++--- packages/init-stack/src/mcp.ts | 405 ++++++++++++++++++++++++ packages/init-stack/src/util.ts | 83 +++++ 4 files changed, 962 insertions(+), 56 deletions(-) create mode 100644 packages/init-stack/src/mcp.ts create mode 100644 packages/init-stack/src/util.ts diff --git a/packages/init-stack/package.json b/packages/init-stack/package.json index 1f13a15192..5687385de1 100644 --- a/packages/init-stack/package.json +++ b/packages/init-stack/package.json @@ -18,16 +18,16 @@ "ensure-neon": "grep -q '\"@neondatabase/serverless\"' ./test-run-output/package.json && echo 'Initialized Neon successfully!'", "test-run-neon": "pnpm run test-run-node --neon && pnpm run ensure-neon", "test-run-neon:manual": "pnpm run test-run-node:manual --neon && pnpm run ensure-neon", - "test-run-no-browser": "rimraf test-run-output && mkdir test-run-output && cd test-run-output && npm init --init-author-name example-author --init-license UNLICENSED --init-author-url http://example.com --init-module test-run-output --init-version 1.0.0 -y && cd .. && pnpm run init-stack:local test-run-output --on-question error --js --server --npm --no-browser", + "test-run-no-browser": "rimraf test-run-output && mkdir test-run-output && cd test-run-output && npm init --init-author-name example-author --init-license UNLICENSED --init-author-url http://example.com --init-module test-run-output --init-version 1.0.0 -y && cd .. && pnpm run init-stack:local test-run-output --on-question error --no-warn-uncommitted-changes --js --server --npm --no-browser", "test-run-node:manual": "rimraf test-run-output && mkdir test-run-output && cd test-run-output && npm init && cd .. && pnpm run init-stack:local test-run-output", - "test-run-node": "rimraf test-run-output && mkdir test-run-output && cd test-run-output && npm init --init-author-name example-author --init-license UNLICENSED --init-author-url http://example.com --init-module test-run-output --init-version 1.0.0 -y && cd .. && pnpm run init-stack:local test-run-output --on-question error --js --server --npm --no-browser", + "test-run-node": "rimraf test-run-output && mkdir test-run-output && cd test-run-output && npm init --init-author-name example-author --init-license UNLICENSED --init-author-url http://example.com --init-module test-run-output --init-version 1.0.0 -y && cd .. && pnpm run init-stack:local test-run-output --on-question error --no-warn-uncommitted-changes --js --server --npm --no-browser", "test-run-js:manual": "rimraf test-run-output && npx -y sv create test-run-output --no-install && pnpm run init-stack:local test-run-output", - "test-run-js": "rimraf test-run-output && npx -y sv create test-run-output --template minimal --types ts --no-add-ons --no-install && pnpm run init-stack:local test-run-output --on-question error --js --client --npm --no-browser", + "test-run-js": "rimraf test-run-output && npx -y sv create test-run-output --template minimal --types ts --no-add-ons --no-install && pnpm run init-stack:local test-run-output --on-question error --no-warn-uncommitted-changes --js --client --npm --no-browser", "test-run-next:manual": "rimraf test-run-output && npx -y create-next-app@latest test-run-output && pnpm run init-stack:local test-run-output", - "test-run-next": "rimraf test-run-output && npx -y create-next-app@latest test-run-output --app --ts --no-src-dir --tailwind --use-npm --eslint --import-alias '##@#/*' --turbopack && pnpm run init-stack:local test-run-output --on-question error --no-browser", - "test-run-keys-next": "rimraf test-run-output && npx -y create-next-app@latest test-run-output --app --ts --no-src-dir --tailwind --use-npm --eslint --import-alias '##@#/*' --turbopack && pnpm run init-stack:local test-run-output --on-question error --project-id my-project-id --publishable-client-key my-publishable-client-key", - "test-run-keys-js": "rimraf test-run-output && npx -y sv create test-run-output --template minimal --types ts --no-add-ons --no-install && pnpm run init-stack:local test-run-output --on-question error --js --client --npm --project-id my-project-id --publishable-client-key my-publishable-client-key", - "test-run-react": "rimraf test-run-output && npx -y create-vite@latest test-run-output --template react-ts && pnpm run init-stack:local test-run-output --on-question error --no-browser --npm", + "test-run-next": "rimraf test-run-output && npx -y create-next-app@latest test-run-output --app --ts --no-src-dir --tailwind --use-npm --eslint --import-alias '##@#/*' --turbopack && pnpm run init-stack:local test-run-output --on-question error --no-warn-uncommitted-changes --no-browser", + "test-run-keys-next": "rimraf test-run-output && npx -y create-next-app@latest test-run-output --app --ts --no-src-dir --tailwind --use-npm --eslint --import-alias '##@#/*' --turbopack && pnpm run init-stack:local test-run-output --on-question error --no-warn-uncommitted-changes --project-id my-project-id --publishable-client-key my-publishable-client-key", + "test-run-keys-js": "rimraf test-run-output && npx -y sv create test-run-output --template minimal --types ts --no-add-ons --no-install && pnpm run init-stack:local test-run-output --on-question error --no-warn-uncommitted-changes --js --client --npm --project-id my-project-id --publishable-client-key my-publishable-client-key", + "test-run-react": "rimraf test-run-output && npx -y create-vite@latest test-run-output --template react-ts && pnpm run init-stack:local test-run-output --on-question error --no-warn-uncommitted-changes --no-browser --npm", "test-run-react:manual": "rimraf test-run-output && npx -y create-vite@latest test-run-output --template react-ts && pnpm run init-stack:local test-run-output --react" }, "files": [ diff --git a/packages/init-stack/src/index.ts b/packages/init-stack/src/index.ts index 756878b693..96bbb01854 100644 --- a/packages/init-stack/src/index.ts +++ b/packages/init-stack/src/index.ts @@ -8,6 +8,10 @@ import * as os from 'os'; import * as path from "path"; import { PostHog } from 'posthog-node'; import packageJson from '../package.json'; +import { scheduleMcpConfiguration } from "./mcp"; +import { Colorize, configureVerboseLogging, logVerbose, templateIdentity } from "./util"; + +export { templateIdentity } from "./util"; const jsLikeFileExtensions: string[] = [ "mtsx", @@ -92,6 +96,7 @@ program .option("--publishable-client-key ", "Publishable client key to use in setup") .option("--no-browser", "Don't open browser for environment variable setup") .option("--on-question ", "How to handle interactive questions: ask | guess | error | default", "default") + .option("--no-warn-uncommitted-changes", "Don't warn about uncommitted changes in the Git repository") .addHelpText('after', ` For more information, please visit https://docs.stack-auth.com/getting-started/setup`); @@ -101,7 +106,13 @@ const options = program.opts(); // Keep existing variables but assign from Commander let savedProjectPath: string | undefined = program.args[0] || undefined; -const isDryRun: boolean = options.dryRun || false; +const verboseEnvRaw = process.env.STACK_VERBOSE; +const parsedVerboseLevel = typeof verboseEnvRaw === "string" && verboseEnvRaw.trim().length > 0 + ? Number.parseInt(verboseEnvRaw.trim(), 10) + : 0; +const verboseLevel: number = Number.isFinite(parsedVerboseLevel) ? Math.max(0, parsedVerboseLevel) : 0; +const isVerbose: boolean = verboseLevel > 0; +const isDryRun: boolean = options.dryRun || isTruthyEnv("STACK_DRY_RUN") || false; const isNeon: boolean = options.neon || false; const typeFromArgs: "js" | "next" | "react" | undefined = options.js ? "js" : options.next ? "next" : options.react ? "react" : undefined; const packageManagerFromArgs: string | undefined = options.npm ? "npm" : options.yarn ? "yarn" : options.pnpm ? "pnpm" : options.bun ? "bun" : undefined; @@ -110,6 +121,7 @@ const isServer: boolean = options.server || false; const projectIdFromArgs: string | undefined = options.projectId; const publishableClientKeyFromArgs: string | undefined = options.publishableClientKey; const onQuestionMode: OnQuestionMode = resolveOnQuestionMode(options.onQuestion); +const warnUncommittedChanges: boolean = options.warnUncommittedChanges ?? true; // Commander negates the boolean options with prefix `--no-` // so `--no-browser` becomes `browser: false` @@ -134,16 +146,6 @@ const ansis: Ansis = { bold: "\x1b[1m", }; -type TemplateFunction = (strings: TemplateStringsArray, ...values: any[]) => string; - -type Colorize = { - red: TemplateFunction, - blue: TemplateFunction, - green: TemplateFunction, - yellow: TemplateFunction, - bold: TemplateFunction, -}; - const colorize: Colorize = { red: (strings, ...values) => ansis.red + templateIdentity(strings, ...values) + ansis.clear, blue: (strings, ...values) => ansis.blue + templateIdentity(strings, ...values) + ansis.clear, @@ -152,12 +154,18 @@ const colorize: Colorize = { bold: (strings, ...values) => ansis.bold + templateIdentity(strings, ...values) + ansis.clear, }; +configureVerboseLogging({ + level: verboseLevel, + formatter: (message) => colorize.blue`[verbose] ${message}`, +}); + const filesCreated: string[] = []; const filesModified: string[] = []; const commandsExecuted: string[] = []; const packagesToInstall: string[] = []; const writeFileHandlers: Array<() => Promise> = []; +const deferredCommandHandlers: Array<() => Promise> = []; const nextSteps: string[] = [ `Create an account and Stack Auth API key for your project on https://app.stack-auth.com`, ]; @@ -174,6 +182,7 @@ const distinctId = crypto.randomUUID(); async function capture(event: string, properties: Record) { + logVerbose("capture event", { event, properties }); ph_client.capture({ event: `${EVENT_PREFIX}${event}`, distinctId, @@ -201,6 +210,26 @@ async function main(): Promise { `); console.log(); + logVerbose("Initialization run metadata", { + version: packageJson.version, + cwd: process.cwd(), + args: program.args, + options: { + isDryRun, + isVerbose, + isNeon, + typeFromArgs, + packageManagerFromArgs, + isClient, + isServer, + projectIdFromArgs: Boolean(projectIdFromArgs), + publishableClientKeyFromArgs: Boolean(publishableClientKeyFromArgs), + noBrowser, + onQuestionMode, + verboseLevel, + }, + }); + await capture("start", { version: packageJson.version, isDryRun, @@ -222,11 +251,34 @@ async function main(): Promise { // Prepare some stuff await clearStdin(); const projectPath = await getProjectPath(); + await ensureGitWorkspaceIsReady(projectPath); + logVerbose("Project path prepared", { projectPath, isDryRun, isVerbose }); + scheduleMcpConfiguration({ + projectPath, + isDryRun, + colorize, + registerWriteHandler: (handler) => writeFileHandlers.push(handler), + registerCommandHandler: (handler) => deferredCommandHandlers.push(handler), + recordFileChange, + runScheduledCommand, + }); + nextSteps.push("Restart your MCP-enabled editors so they pick up the Stack Auth MCP."); + logVerbose("MCP configuration scheduled", { + writeHandlers: writeFileHandlers.length, + deferredCommands: deferredCommandHandlers.length, + }); // Steps const { packageJson: projectPackageJson } = await Steps.getProject(); const type = await Steps.getProjectType({ packageJson: projectPackageJson }); + logVerbose("Project inspection complete", { + detectedType: type, + dependencies: { + hasReact: Boolean(projectPackageJson.dependencies?.["react"]), + hasNext: Boolean(projectPackageJson.dependencies?.["next"]), + }, + }); await capture("project-type-selected", { type, @@ -240,6 +292,7 @@ async function main(): Promise { const convexIntegration = await Steps.maybeInstallConvexIntegration({ packageJson: projectPackageJson, type }); if (convexIntegration) { nextSteps.push(...convexIntegration.instructions); + logVerbose("Convex integration detected", convexIntegration); } if (type === "next") { @@ -282,8 +335,10 @@ async function main(): Promise { `Follow the instructions on how to use Stack Auth's vanilla SDK at http://docs.stack-auth.com/others/js-client`, ); } + logVerbose("Primary integration steps completed", { type, nextStepsCount: nextSteps.length }); const { packageManager } = await Steps.getPackageManager(); + logVerbose("Package manager determined", { packageManager }); await capture(`package-manager-selected`, { packageManager, @@ -309,6 +364,10 @@ async function main(): Promise { shell: true, cwd: projectPath, }); + logVerbose("Dependency installation finished", { + packageManager, + packages: packagesToInstall, + }); await capture(`dependencies-installed`, { packageManager, @@ -319,11 +378,15 @@ async function main(): Promise { console.log(); console.log(colorize.bold`Writing files...`); console.log(); - for (const writeFileHandler of writeFileHandlers) { + for (let i = 0; i < writeFileHandlers.length; i++) { + const writeFileHandler = writeFileHandlers[i]; + logVerbose("Executing write handler", { index: i }); await writeFileHandler(); } console.log(`${colorize.green`√`} Done writing files`); + await runDeferredCommands(); + console.log('\n\n\n'); console.log(colorize.bold`${colorize.green`Installation succeeded!`}`); console.log(); @@ -332,6 +395,9 @@ async function main(): Promise { console.log(` ${colorize.blue`${command}`}`); } console.log(); + console.log("MCP servers installed:"); + console.log(` ${colorize.green`https://mcp.stack-auth.com`}`); + console.log(); console.log("Files written:"); for (const file of filesModified) { console.log(` ${colorize.yellow`${file}`}`); @@ -451,6 +517,7 @@ type ConvexIntegrationResult = { const Steps = { async getProject(): Promise<{ packageJson: PackageJson }> { let projectPath = await getProjectPath(); + logVerbose("Steps.getProject invoked", { projectPath }); if (!fs.existsSync(projectPath)) { throw new UserError(`The project path ${projectPath} does not exist`); } @@ -470,20 +537,40 @@ const Steps = { throw new UserError(`package.json file is not valid JSON: ${e}`); } + logVerbose("Steps.getProject completed", { + packageJsonPath, + dependencyCounts: { + dependencies: Object.keys(packageJson.dependencies ?? {}).length, + devDependencies: Object.keys(packageJson.devDependencies ?? {}).length, + }, + }); + return { packageJson }; }, async getProjectType({ packageJson }: { packageJson: PackageJson }): Promise<"js" | "next" | "react"> { - if (typeFromArgs) return typeFromArgs; + if (typeFromArgs) { + logVerbose("Steps.getProjectType using CLI override", { typeFromArgs }); + return typeFromArgs; + } + + logVerbose("Steps.getProjectType attempting autodetect", { + hasNext: Boolean(packageJson.dependencies?.["next"] || packageJson.devDependencies?.["next"]), + hasReact: Boolean(packageJson.dependencies?.["react"] || packageJson.dependencies?.["react-dom"]), + onQuestionMode, + }); const maybeNextProject = await Steps.maybeGetNextProjectInfo({ packageJson }); if (!("error" in maybeNextProject)) { + logVerbose("Steps.getProjectType resolved to Next.js project"); return "next"; } if (packageJson.dependencies?.["react"] || packageJson.dependencies?.["react-dom"]) { + logVerbose("Steps.getProjectType resolved to React project"); return "react"; } if (onQuestionMode === "guess") { + logVerbose("Steps.getProjectType defaulting to JS due to --on-question=guess"); return "js"; } if (onQuestionMode === "error") { @@ -496,13 +583,15 @@ const Steps = { name: "type", message: "Which integration would you like to install?", choices: [ - { name: "None (vanilla JS, Node.js, etc)", value: "js" }, + { name: "Vanilla JS (other/no framework)", value: "js" }, + { name: "Node.js", value: "js" }, { name: "React", value: "react" }, { name: "Next.js", value: "next" }, ] } ]); + logVerbose("Steps.getProjectType received user selection", { type }); return type; }, @@ -512,24 +601,35 @@ const Steps = { next: (install && process.env.STACK_NEXT_INSTALL_PACKAGE_NAME_OVERRIDE) || "@stackframe/stack", react: (install && process.env.STACK_REACT_INSTALL_PACKAGE_NAME_OVERRIDE) || "@stackframe/react", } as const; - return mapping[type]; + const packageName = mapping[type]; + logVerbose("Steps.getStackPackageName resolved", { type, install, packageName }); + return packageName; }, async addStackPackage(type: "js" | "next" | "react"): Promise { - packagesToInstall.push(await Steps.getStackPackageName(type, true)); + const pkgName = await Steps.getStackPackageName(type, true); + logVerbose("Steps.addStackPackage scheduling install", { pkgName }); + packagesToInstall.push(pkgName); }, async getNextProjectInfo({ packageJson }: { packageJson: PackageJson }): Promise { + logVerbose("Steps.getNextProjectInfo invoked"); const maybe = await Steps.maybeGetNextProjectInfo({ packageJson }); - if ("error" in maybe) throw new UserError(maybe.error); + if ("error" in maybe) { + logVerbose("Steps.getNextProjectInfo failed validation", maybe); + throw new UserError(maybe.error); + } + logVerbose("Steps.getNextProjectInfo resolved", maybe); return maybe; }, async maybeGetNextProjectInfo({ packageJson }: { packageJson: PackageJson }): Promise { const projectPath = await getProjectPath(); + logVerbose("Steps.maybeGetNextProjectInfo evaluating Next.js eligibility", { projectPath }); const nextVersionInPackageJson = packageJson.dependencies?.["next"] ?? packageJson.devDependencies?.["next"]; if (!nextVersionInPackageJson) { + logVerbose("Steps.maybeGetNextProjectInfo missing Next dependency"); 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 ( @@ -537,6 +637,7 @@ const Steps = { !nextVersionInPackageJson.includes("15") && nextVersionInPackageJson !== "latest" ) { + logVerbose("Steps.maybeGetNextProjectInfo found unsupported Next version", { version: nextVersionInPackageJson }); 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` }; } @@ -544,6 +645,7 @@ const Steps = { const srcPath = path.join(projectPath, hasSrcAppFolder ? "src" : ""); const appPath = path.join(srcPath, "app"); if (!fs.existsSync(appPath)) { + logVerbose("Steps.maybeGetNextProjectInfo missing Next app directory", { appPath }); return { error: `The app path ${appPath} does not exist. Only the Next.js App router is supported — are you maybe on the Pages router instead?` }; } @@ -554,11 +656,18 @@ const Steps = { const nextConfigPath = nextConfigPathWithoutExtension + "." + (nextConfigFileExtension ?? "js"); if (!fs.existsSync(nextConfigPath)) { + logVerbose("Steps.maybeGetNextProjectInfo missing next.config file", { nextConfigPath }); return { error: `Expected file at ${nextConfigPath} for Next.js projects.` }; } const dryUpdateNextLayoutFileResult = await Steps.dryUpdateNextLayoutFile({ appPath, defaultExtension: "jsx" }); + logVerbose("Steps.maybeGetNextProjectInfo success", { + projectPath, + srcPath, + appPath, + detectedExtension: dryUpdateNextLayoutFileResult.fileExtension, + }); return { type: "next", srcPath, @@ -570,9 +679,13 @@ const Steps = { async writeEnvVars(type: "js" | "next" | "react"): Promise { const projectPath = await getProjectPath(); + logVerbose("Steps.writeEnvVars invoked", { type, projectPath }); // TODO: in non-Next environments, ask the user what method they prefer for envvars - if (type !== "next") return false; + if (type !== "next") { + logVerbose("Steps.writeEnvVars skipped", { reason: "non-next-project" }); + return false; + } const envLocalPath = path.join(projectPath, ".env.local"); @@ -601,32 +714,38 @@ const Steps = { "STACK_SECRET_SERVER_KEY=\n"; laterWriteFile(envLocalPath, envContent); + logVerbose("Steps.writeEnvVars scheduled .env.local creation", { envLocalPath }); return true; } + logVerbose("Steps.writeEnvVars found existing env files", { potentialEnvLocations }); return false; }, async maybeInstallConvexIntegration({ packageJson, type }: { packageJson: PackageJson, type: "js" | "next" | "react" }): Promise { const hasConvexDependency = Boolean(packageJson.dependencies?.["convex"] || packageJson.devDependencies?.["convex"]); if (!hasConvexDependency) { + logVerbose("Steps.maybeInstallConvexIntegration skipped", { reason: "no-convex-dependency" }); return null; } const projectPath = await getProjectPath(); const convexDir = path.join(projectPath, "convex"); if (!fs.existsSync(convexDir)) { + logVerbose("Steps.maybeInstallConvexIntegration skipped", { reason: "missing-convex-dir", convexDir }); return null; } const stackPackageName = await Steps.getStackPackageName(type); const instructions: string[] = []; + logVerbose("Steps.maybeInstallConvexIntegration configuring", { convexDir, stackPackageName }); const authConfigPath = path.join(convexDir, "auth.config.ts"); const desiredAuthConfig = createConvexAuthConfigContent({ stackPackageName, type }); const existingAuthConfig = await readFile(authConfigPath); if (!existingAuthConfig || (!existingAuthConfig.includes("getConvexProvidersConfig") && !existingAuthConfig.includes("@stackframe/"))) { laterWriteFile(authConfigPath, desiredAuthConfig); + logVerbose("Steps.maybeInstallConvexIntegration scheduled auth.config.ts update", { authConfigPath }); } const convexConfigPath = path.join(convexDir, "convex.config.ts"); @@ -636,16 +755,21 @@ const Steps = { if (!existingConvexConfig) { laterWriteFile(convexConfigPath, desiredConvexConfig); + logVerbose("Steps.maybeInstallConvexIntegration writing convex.config.ts from template", { convexConfigPath }); } else if (existingConvexConfig.includes("app.use(stackAuthComponent") && existingConvexConfig.includes("/convex.config") && existingConvexConfig.includes("stackframe")) { // already integrated + logVerbose("Steps.maybeInstallConvexIntegration detected existing convex.config.ts integration", { convexConfigPath }); } else { const integratedContent = integrateConvexConfig(existingConvexConfig, stackPackageName); if (integratedContent) { laterWriteFile(convexConfigPath, integratedContent); + logVerbose("Steps.maybeInstallConvexIntegration merged convex.config.ts content", { convexConfigPath }); } else if (isSimpleConvexConfig(existingConvexConfig)) { laterWriteFile(convexConfigPath, desiredConvexConfig); + logVerbose("Steps.maybeInstallConvexIntegration replaced simple convex.config.ts", { convexConfigPath }); } else { needsManualConvexConfig = true; + logVerbose("Steps.maybeInstallConvexIntegration requiring manual convex.config.ts review", { convexConfigPath }); } } @@ -657,12 +781,14 @@ const Steps = { if (convexClientUpdateResult.skippedFiles.length > 0) { instructions.push("Review your Convex client setup and call stackClientApp.getConvexClientAuth({}) or stackServerApp.getConvexClientAuth({}) manually where needed."); } + logVerbose("Steps.maybeInstallConvexIntegration client update summary", convexClientUpdateResult); instructions.push( "Set the Stack Auth environment variables in Convex (Deployment → Settings → Environment Variables).", "Verify your Convex clients call stackClientApp.getConvexClientAuth({}) or stackServerApp.getConvexClientAuth({}) so they share authentication with Stack Auth." ); + logVerbose("Steps.maybeInstallConvexIntegration completed", { instructions }); return { instructions }; }, @@ -701,12 +827,15 @@ const Steps = { fileExtension: string, indentation: string, }> { + logVerbose("Steps.updateNextLayoutFile invoked", projectInfo); const res = await Steps.dryUpdateNextLayoutFile(projectInfo); laterWriteFile(res.path, res.updatedContent); + logVerbose("Steps.updateNextLayoutFile scheduled write", { path: res.path }); return res; }, async writeStackAppFile({ type, srcPath, defaultExtension, indentation }: StackAppFileOptions, clientOrServer: "server" | "client", alsoHasClient: boolean): Promise { + logVerbose("Steps.writeStackAppFile invoked", { type, srcPath, clientOrServer, alsoHasClient }); const packageName = await Steps.getStackPackageName(type); const clientOrServerCap = { @@ -722,6 +851,7 @@ const Steps = { stackAppPathWithoutExtension + "." + stackAppFileExtension; const stackAppContent = await readFile(stackAppPath); if (stackAppContent) { + logVerbose("Steps.writeStackAppFile found existing file", { stackAppPath }); if (!stackAppContent.includes("@stackframe/")) { throw new UserError( `A file at the path ${stackAppPath} already exists. Stack uses the stack/${clientOrServer}.ts file to initialize the Stack SDK. Please remove the existing file and try again.` @@ -764,10 +894,12 @@ ${shouldInheritFromClient ? `${indentation}inheritsFrom: stackClientApp,` : `${i }); `.trim() + "\n" ); + logVerbose("Steps.writeStackAppFile scheduled creation", { stackAppPath, inheritsFromClient: shouldInheritFromClient }); return { fileName: stackAppPath }; }, async writeReactClientFile({ srcPath, defaultExtension, indentation, hasReactRouterDom }: { srcPath: string, defaultExtension: string, indentation: string, hasReactRouterDom: boolean }): Promise { + logVerbose("Steps.writeReactClientFile invoked", { srcPath, hasReactRouterDom }); const packageName = await Steps.getStackPackageName("react"); const relativeStackAppPath = `stack/client`; const stackAppPathWithoutExtension = path.join(srcPath, relativeStackAppPath); @@ -775,6 +907,7 @@ ${shouldInheritFromClient ? `${indentation}inheritsFrom: stackClientApp,` : `${i const stackAppPath = stackAppPathWithoutExtension + "." + stackAppFileExtension; const stackAppContent = await readFile(stackAppPath); if (stackAppContent) { + logVerbose("Steps.writeReactClientFile found existing file", { stackAppPath }); if (!stackAppContent.includes("@stackframe/")) { throw new UserError(`A file at the path ${stackAppPath} already exists. Stack uses the stack/client.ts file to initialize the Stack SDK. Please remove the existing file and try again.`); } @@ -796,10 +929,12 @@ ${shouldInheritFromClient ? `${indentation}inheritsFrom: stackClientApp,` : `${i stackAppPath, `${imports}export const stackClientApp = new StackClientApp({ \n${indentation}tokenStore: "cookie", \n${indentation}projectId: ${projectIdWrite}, \n${indentation}publishableClientKey: ${publishableClientKeyWrite}${redirectMethod}, \n}); \n` ); + logVerbose("Steps.writeReactClientFile scheduled creation", { stackAppPath }); return { fileName: stackAppPath }; }, async writeNextHandlerFile(projectInfo: ProjectInfo): Promise { + logVerbose("Steps.writeNextHandlerFile invoked", projectInfo); const handlerPathWithoutExtension = path.join( projectInfo.appPath, "handler/[...stack]/page" @@ -809,6 +944,7 @@ ${shouldInheritFromClient ? `${indentation}inheritsFrom: stackClientApp,` : `${i const handlerPath = handlerPathWithoutExtension + "." + handlerFileExtension; const handlerContent = await readFile(handlerPath); if (handlerContent && !handlerContent.includes("@stackframe/")) { + logVerbose("Steps.writeNextHandlerFile found conflicting file", { handlerPath }); 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.` ); @@ -821,6 +957,7 @@ ${shouldInheritFromClient ? `${indentation}inheritsFrom: stackClientApp,` : `${i }, async writeNextLoadingFile(projectInfo: ProjectInfo): Promise { + logVerbose("Steps.writeNextLoadingFile invoked", projectInfo); let loadingPathWithoutExtension = path.join(projectInfo.appPath, "loading"); const loadingFileExtension = (await findJsExtension(loadingPathWithoutExtension)) ?? projectInfo.defaultExtension; @@ -833,9 +970,13 @@ ${shouldInheritFromClient ? `${indentation}inheritsFrom: stackClientApp,` : `${i }, async getPackageManager(): Promise<{ packageManager: string }> { - if (packageManagerFromArgs) return { packageManager: packageManagerFromArgs }; + if (packageManagerFromArgs) { + logVerbose("Steps.getPackageManager using CLI override", { packageManager: packageManagerFromArgs }); + return { packageManager: packageManagerFromArgs }; + } const packageManager = await promptPackageManager(); const versionCommand = `${packageManager} --version`; + logVerbose("Steps.getPackageManager checking binary availability", { packageManager }); try { await shellNicelyFormatted(versionCommand, { shell: true, quiet: true }); @@ -846,6 +987,7 @@ ${shouldInheritFromClient ? `${indentation}inheritsFrom: stackClientApp,` : `${i ); } + logVerbose("Steps.getPackageManager resolved", { packageManager }); return { packageManager }; }, @@ -869,39 +1011,57 @@ ${shouldInheritFromClient ? `${indentation}inheritsFrom: stackClientApp,` : `${i if (!isReady) { throw new UserError("Installation aborted."); } + logVerbose("Steps.ensureReady confirmed", { type, projectPath, isReady }); }, async getServerOrClientOrBoth(): Promise> { - if (isClient && isServer) return ["server", "client"]; - if (isServer) return ["server"]; - if (isClient) return ["client"]; + logVerbose("Steps.getServerOrClientOrBoth invoked", { isClientFlag: isClient, isServerFlag: isServer, onQuestionMode }); + if (isClient && isServer) { + logVerbose("Steps.getServerOrClientOrBoth using CLI flags", { selection: ["server", "client"] }); + return ["server", "client"]; + } + if (isServer) { + logVerbose("Steps.getServerOrClientOrBoth using server flag"); + return ["server"]; + } + if (isClient) { + logVerbose("Steps.getServerOrClientOrBoth using client flag"); + return ["client"]; + } - if (onQuestionMode === "guess") return ["server", "client"]; + if (onQuestionMode === "guess") { + logVerbose("Steps.getServerOrClientOrBoth defaulting to both"); + return ["server", "client"]; + } if (onQuestionMode === "error") { throw new UnansweredQuestionError("Ambiguous installation type. Re-run with --server, --client, or both."); } - return (await inquirer.prompt([{ + const selection = (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"] } + { name: "Both (eg. Next.js)", value: ["server", "client"] } ] }])).type; + logVerbose("Steps.getServerOrClientOrBoth received user selection", { selection }); + return selection; }, /** * 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(): Promise { const projectPath = await getProjectPath(); const hasTsConfig = fs.existsSync( path.join(projectPath, "tsconfig.json") ); - return hasTsConfig ? "ts" : "js"; + const extension = hasTsConfig ? "ts" : "js"; + logVerbose("Steps.guessDefaultFileExtension result", { projectPath, hasTsConfig, extension }); + return extension; }, /** @@ -913,7 +1073,9 @@ ${shouldInheritFromClient ? `${indentation}inheritsFrom: stackClientApp,` : `${i const hasSrcFolder = fs.existsSync( path.join(projectPath, "src") ); - return hasSrcFolder ? potentialSrcPath : projectPath; + const resolvedPath = hasSrcFolder ? potentialSrcPath : projectPath; + logVerbose("Steps.guessSrcPath result", { hasSrcFolder, resolvedPath }); + return resolvedPath; }, @@ -926,6 +1088,7 @@ type LayoutResult = { } async function getUpdatedLayout(originalLayout: string): Promise { + logVerbose("getUpdatedLayout invoked", { length: originalLayout.length }); let layout = originalLayout; const indentation = guessIndentation(originalLayout); @@ -943,11 +1106,13 @@ async function getUpdatedLayout(originalLayout: string): Promise]*>/.exec(layout); const bodyCloseTag = /<\s*\/\s*body[^>]*>/.exec(layout); if (!bodyOpenTag || !bodyCloseTag) { + logVerbose("getUpdatedLayout missing body tag"); return undefined; } const bodyOpenEndIndex = bodyOpenTag.index + bodyOpenTag[0].length; const bodyCloseStartIndex = bodyCloseTag.index; if (bodyCloseStartIndex <= bodyOpenEndIndex) { + logVerbose("getUpdatedLayout invalid body indices", { bodyOpenEndIndex, bodyCloseStartIndex }); return undefined; } @@ -973,6 +1138,7 @@ async function getUpdatedLayout(originalLayout: string): Promise { + logVerbose("getProjectPath invoked", { savedProjectPath }); if (savedProjectPath === undefined) { savedProjectPath = process.cwd(); @@ -1017,6 +1184,7 @@ async function getProjectPath(): Promise { path.join(savedProjectPath, "package.json") ); if (askForPathModification) { + logVerbose("getProjectPath did not find package.json in cwd", { cwd: savedProjectPath }); if (onQuestionMode === "guess" || onQuestionMode === "error") { throw new UserError(`No package.json file found in ${savedProjectPath}. Re-run providing the project path argument (e.g. 'init-stack ').`); } @@ -1030,18 +1198,125 @@ async function getProjectPath(): Promise { }, ]) ).newPath; + logVerbose("getProjectPath received manual input", { savedProjectPath }); } } + logVerbose("getProjectPath resolved", { savedProjectPath }); return savedProjectPath as string; } +async function ensureGitWorkspaceIsReady(projectPath: string): Promise { + if (!warnUncommittedChanges) { + logVerbose("ensureGitWorkspaceIsReady skipped as requested by user"); + return; + } + + logVerbose("ensureGitWorkspaceIsReady invoked", { projectPath }); + let isGitRepo = false; + try { + const gitRepoResult = child_process.spawnSync( + "git", + ["rev-parse", "--is-inside-work-tree"], + { + shell: true, + cwd: projectPath, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + } + ); + isGitRepo = gitRepoResult.status === 0 && gitRepoResult.stdout.trim() === "true"; + } catch (e) { + logVerbose("ensureGitWorkspaceIsReady failed to detect git repository", { error: e }); + return; + } + if (!isGitRepo) { + logVerbose("ensureGitWorkspaceIsReady skipping", { reason: "not-a-git-repo" }); + return; + } + + const statusResult = child_process.spawnSync( + "git", + ["status", "--porcelain"], + { + shell: true, + cwd: projectPath, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + } + ); + if (statusResult.error || statusResult.status !== 0) { + logVerbose("ensureGitWorkspaceIsReady git status failed", { status: statusResult.status, error: statusResult.error }); + return; + } + + const lines = statusResult.stdout + .split("\n") + .map((line) => line.replace(/\r$/, "")) + .filter((line) => line.length > 0); + + const unstagedLines = lines.filter((line) => { + if (line.startsWith("!!")) return false; + if (line.startsWith("??")) return true; + if (line.length < 2) return false; + const workingTreeStatus = line[1]; + return Boolean(workingTreeStatus && workingTreeStatus !== " "); + }); + + if (unstagedLines.length === 0) { + logVerbose("ensureGitWorkspaceIsReady clean working tree"); + return; + } + + const changedFiles = unstagedLines.map((line) => { + const filePath = line.slice(3).trim(); + return filePath.length > 0 ? filePath : line; + }); + + console.log(); + console.log(colorize.yellow`Detected unstaged/uncommitted changes in your Git repository:`); + const filesToShow = changedFiles.slice(0, 10); + for (const file of filesToShow) { + console.log(` - ${file}`); + } + if (changedFiles.length > filesToShow.length) { + console.log(` - ...and ${changedFiles.length - filesToShow.length} more`); + } + console.log(colorize.yellow`You may want to stage and commit these changes before installing Stack Auth, so you can review the changes afterwards.`); + console.log(); + + if (onQuestionMode === "guess") { + console.log(colorize.yellow`Continuing because --on-question=guess.`); + return; + } + if (onQuestionMode === "error") { + throw new UnansweredQuestionError("Unstaged changes detected in the project directory"); + } + + const { proceed } = await inquirer.prompt([ + { + type: "confirm", + name: "proceed", + message: "Continue with Stack initialization anyway?", + default: false, + }, + ]); + + if (!proceed) { + throw new UserError("Aborting Stack initialization to avoid overwriting unstaged changes."); + } + logVerbose("ensureGitWorkspaceIsReady user confirmed proceed despite unstaged changes"); +} + async function findJsExtension(fullPathWithoutExtension: string): Promise { + logVerbose("findJsExtension invoked", { fullPathWithoutExtension }); for (const ext of jsLikeFileExtensions) { const fullPath = fullPathWithoutExtension + "." + ext; if (fs.existsSync(fullPath)) { + logVerbose("findJsExtension found file", { fullPath, ext }); return ext; } } + logVerbose("findJsExtension no matching file", { fullPathWithoutExtension }); return null; } @@ -1052,17 +1327,26 @@ async function promptPackageManager(): Promise { const npmLock = fs.existsSync(path.join(projectPath, "package-lock.json")); const bunLock = fs.existsSync(path.join(projectPath, "bun.lockb")) || fs.existsSync(path.join(projectPath, "bun.lock")); + logVerbose("promptPackageManager inspecting lockfiles", { yarnLock, pnpmLock, npmLock, bunLock }); + if (yarnLock && !pnpmLock && !npmLock && !bunLock) { + logVerbose("promptPackageManager auto-selected yarn"); return "yarn"; } else if (!yarnLock && pnpmLock && !npmLock && !bunLock) { + logVerbose("promptPackageManager auto-selected pnpm"); return "pnpm"; } else if (!yarnLock && !pnpmLock && npmLock && !bunLock) { + logVerbose("promptPackageManager auto-selected npm"); return "npm"; } else if (!yarnLock && !pnpmLock && !npmLock && bunLock) { + logVerbose("promptPackageManager auto-selected bun"); return "bun"; } - if (onQuestionMode === "guess") return "npm"; + if (onQuestionMode === "guess") { + logVerbose("promptPackageManager defaulting to npm due to guess mode"); + return "npm"; + } if (onQuestionMode === "error") { throw new UnansweredQuestionError("Unable to determine the package manager. Re-run with one of: --npm, --yarn, --pnpm, or --bun."); } @@ -1075,6 +1359,7 @@ async function promptPackageManager(): Promise { choices: ["npm", "yarn", "pnpm", "bun"], }, ]); + logVerbose("promptPackageManager user selected", { packageManager: answers.packageManager }); return answers.packageManager; } @@ -1086,6 +1371,7 @@ type ShellOptions = { } async function shellNicelyFormatted(command: string, { quiet, ...options }: ShellOptions): Promise { + logVerbose("shellNicelyFormatted invoked", { command, options: { ...options, quiet } }); let ui: any; let interval: NodeJS.Timeout | undefined; if (!quiet) { @@ -1107,6 +1393,7 @@ async function shellNicelyFormatted(command: string, { quiet, ...options }: Shel try { if (!isDryRun) { const child = child_process.spawn(command, options); + logVerbose("shellNicelyFormatted spawned process", { pid: child.pid, command }); if (!quiet) { child.stdout.pipe(ui.log); child.stderr.pipe(ui.log); @@ -1117,12 +1404,14 @@ async function shellNicelyFormatted(command: string, { quiet, ...options }: Shel if (code === 0) { resolve(); } else { + logVerbose("shellNicelyFormatted command failed", { code }); reject(new Error(`Command ${command} failed with code ${code}`)); } }); }); } else { console.log(`[DRY-RUN] Would have run: ${command}`); + logVerbose("shellNicelyFormatted skipped due to dry run", { command }); } if (!quiet) { @@ -1131,7 +1420,9 @@ async function shellNicelyFormatted(command: string, { quiet, ...options }: Shel `${colorize.green`√`} Command ${command} succeeded\n` ); } + logVerbose("shellNicelyFormatted completed", { command }); } catch (e) { + logVerbose("shellNicelyFormatted encountered error", { command, error: e instanceof Error ? { message: e.message, stack: e.stack } : e }); if (!quiet) { ui.updateBottomBar( `${colorize.red`X`} Command ${command} failed\n` @@ -1149,26 +1440,35 @@ async function shellNicelyFormatted(command: string, { quiet, ...options }: Shel } async function readFile(fullPath: string): Promise { + logVerbose("readFile invoked", { fullPath, isDryRun }); try { if (!isDryRun) { - return fs.readFileSync(fullPath, "utf-8"); + const content = fs.readFileSync(fullPath, "utf-8"); + logVerbose("readFile succeeded", { fullPath, length: content.length }); + return content; } + logVerbose("readFile skipped due to dry run", { fullPath }); return null; } catch (err: any) { if (err.code === "ENOENT") { + logVerbose("readFile file missing", { fullPath }); return null; } + logVerbose("readFile errored", { fullPath, error: err instanceof Error ? { message: err.message, stack: err.stack } : err }); throw err; } } async function writeFile(fullPath: string, content: string): Promise { + logVerbose("writeFile invoked", { fullPath, length: content.length, isDryRun }); let create = !fs.existsSync(fullPath); if (!isDryRun) { fs.mkdirSync(path.dirname(fullPath), { recursive: true }); fs.writeFileSync(fullPath, content); + logVerbose("writeFile wrote to disk", { fullPath, created: create }); } else { console.log(`[DRY-RUN] Would have written to ${fullPath}`); + logVerbose("writeFile skipped due to dry run", { fullPath }); } const relativeToProjectPath = path.relative(await getProjectPath(), fullPath); if (!create) { @@ -1176,9 +1476,11 @@ async function writeFile(fullPath: string, content: string): Promise { } else { filesCreated.push(relativeToProjectPath); } + logVerbose("writeFile recorded change", { relativeToProjectPath, created: create }); } function laterWriteFile(fullPath: string, content: string): void { + logVerbose("laterWriteFile scheduled", { fullPath, length: content.length }); writeFileHandlers.push(async () => { await writeFile(fullPath, content); }); @@ -1186,16 +1488,87 @@ function laterWriteFile(fullPath: string, content: string): void { async function writeFileIfNotExists(fullPath: string, content: string): Promise { if (!fs.existsSync(fullPath)) { + logVerbose("writeFileIfNotExists writing new file", { fullPath }); await writeFile(fullPath, content); + } else { + logVerbose("writeFileIfNotExists skipped", { fullPath }); } } function laterWriteFileIfNotExists(fullPath: string, content: string): void { + logVerbose("laterWriteFileIfNotExists scheduled", { fullPath }); writeFileHandlers.push(async () => { await writeFileIfNotExists(fullPath, content); }); } +async function runDeferredCommands(): Promise { + if (!deferredCommandHandlers.length) { + logVerbose("runDeferredCommands skipped", { reason: "no-handlers" }); + return; + } + logVerbose("runDeferredCommands executing handlers", { count: deferredCommandHandlers.length }); + for (let index = 0; index < deferredCommandHandlers.length; index++) { + logVerbose("runDeferredCommands executing handler", { index }); + const handler = deferredCommandHandlers[index]; + await handler(); + } + logVerbose("runDeferredCommands completed"); +} + +type RunScheduledCommandMetadata = { + recordInCommandsExecuted?: boolean, +}; + +async function runScheduledCommand( + command: string, + args: string[], + options: child_process.SpawnSyncOptions = {}, + metadata: RunScheduledCommandMetadata = {}, +): Promise { + logVerbose("runScheduledCommand invoked", { command, args, options, isDryRun }); + const display = [command, ...args].join(" "); + if (isDryRun) { + console.log(`[DRY-RUN] Would run: ${display}`); + logVerbose("runScheduledCommand skipped due to dry run", { display }); + return; + } + + const result = child_process.spawnSync(command, args, { + stdio: "pipe", + ...options, + }); + const recordInCommandsExecuted = metadata.recordInCommandsExecuted; + if (recordInCommandsExecuted && !commandsExecuted.includes(display)) { + commandsExecuted.push(display); + } + if (result.status === 0) { + console.log(`${colorize.green`√`} ${display}`); + logVerbose("runScheduledCommand succeeded", { display }); + } else { + logVerbose("runScheduledCommand failed", { display, result, stderr: result.stderr.toString(), stdout: result.stdout.toString() }); + throw new Error(`Command ${display} failed with status ${result.status}: ${result.stderr.toString()}`); + } +} + +async function recordFileChange(fullPath: string, existed: boolean): Promise { + logVerbose("recordFileChange invoked", { fullPath, existed }); + const projectRoot = path.resolve(await getProjectPath()); + const relative = path.relative(projectRoot, fullPath); + const insideProject = relative && !relative.startsWith("..") && !path.isAbsolute(relative); + const entry = insideProject ? relative : fullPath; + + if (existed) { + if (!filesModified.includes(entry)) { + filesModified.push(entry); + } + logVerbose("recordFileChange marked modified", { entry }); + } else if (!filesCreated.includes(entry)) { + filesCreated.push(entry); + logVerbose("recordFileChange marked created", { entry }); + } +} + function createConvexAuthConfigContent(options: { stackPackageName: string, type: "js" | "next" | "react" }): string { const envVarName = getPublicProjectEnvVarName(options.type); return `import { getConvexProvidersConfig } from ${JSON.stringify(options.stackPackageName)}; @@ -1323,15 +1696,24 @@ type AddSetAuthResult = { async function updateConvexClients({ projectPath, type }: { projectPath: string, type: "js" | "next" | "react" }): Promise { const files = collectConvexClientCandidateFiles(projectPath); + logVerbose("updateConvexClients collected files", { projectPath, count: files.length }); const updatedFiles: string[] = []; const skippedFiles: string[] = []; for (const filePath of files) { + logVerbose("updateConvexClients inspecting file", { filePath }); const fileContent = await readFile(filePath); - if (!fileContent) continue; - if (!/new\s+Convex(?:React|Http)?Client\b/.test(fileContent)) continue; + if (!fileContent) { + logVerbose("updateConvexClients skipped file (no content)", { filePath }); + continue; + } + if (!/new\s+Convex(?:React|Http)?Client\b/.test(fileContent)) { + logVerbose("updateConvexClients skipped file (no Convex client)", { filePath }); + continue; + } const addResult = addSetAuthToConvexClients(fileContent, type); + logVerbose("updateConvexClients processed file", { filePath, addResult }); if (!addResult.changed) { if (addResult.instantiationCount > 0 && addResult.skippedHttpCount > 0) { skippedFiles.push(filePath); @@ -1341,18 +1723,22 @@ async function updateConvexClients({ projectPath, type }: { projectPath: string, let finalContent = addResult.updatedContent; if (addResult.usedClientApp) { + logVerbose("updateConvexClients ensuring client import", { filePath }); finalContent = await ensureStackAppImport(finalContent, filePath, "client"); } if (addResult.usedServerApp) { + logVerbose("updateConvexClients ensuring server import", { filePath }); finalContent = await ensureStackAppImport(finalContent, filePath, "server"); } if (finalContent !== fileContent) { laterWriteFile(filePath, finalContent); updatedFiles.push(filePath); + logVerbose("updateConvexClients scheduled update", { filePath }); } } + logVerbose("updateConvexClients finished", { updatedFiles, skippedFiles }); return { updatedFiles, skippedFiles, @@ -1362,8 +1748,10 @@ async function updateConvexClients({ projectPath, type }: { projectPath: string, type StackAppKind = "client" | "server"; async function ensureStackAppImport(content: string, filePath: string, kind: StackAppKind): Promise { + logVerbose("ensureStackAppImport invoked", { filePath, kind }); const identifier = kind === "client" ? "stackClientApp" : "stackServerApp"; if (new RegExp(`import\\s+[^;]*\\b${identifier}\\b`).test(content)) { + logVerbose("ensureStackAppImport found existing import", { filePath, identifier }); return content; } @@ -1398,6 +1786,7 @@ async function ensureStackAppImport(content: string, filePath: string, kind: Sta lines.splice(insertIndex + 1, 0, ""); } + logVerbose("ensureStackAppImport added import", { filePath, importLine }); return lines.join(newline); } @@ -1411,10 +1800,13 @@ function convertToModuleSpecifier(relativePath: string): string { async function getStackAppBasePath(kind: StackAppKind): Promise { const srcPath = await Steps.guessSrcPath(); - return path.join(srcPath, "stack", kind); + const basePath = path.join(srcPath, "stack", kind); + logVerbose("getStackAppBasePath resolved", { kind, basePath }); + return basePath; } function addSetAuthToConvexClients(content: string, type: "js" | "next" | "react"): AddSetAuthResult { + logVerbose("addSetAuthToConvexClients invoked", { type, length: content.length }); const newline = content.includes("\r\n") ? "\r\n" : "\n"; const instantiationRegex = /^[ \t]*(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*new\s+(Convex(?:React|Http)?Client)\b([\s\S]*?);/gm; const replacements: Array<{ start: number, end: number, text: string }> = []; @@ -1432,12 +1824,14 @@ function addSetAuthToConvexClients(content: string, type: "js" | "next" | "react if (className === "ConvexHttpClient") { skippedHttpCount += 1; + logVerbose("addSetAuthToConvexClients skipping ConvexHttpClient", { variableName, fileLength: content.length }); continue; } const remainder = content.slice(match.index + fullMatch.length); const setAuthRegex = new RegExp(`\\b${escapeRegExp(variableName)}\\s*\\.setAuth\\s*\\(`); if (setAuthRegex.test(remainder)) { + logVerbose("addSetAuthToConvexClients found existing setAuth", { variableName }); continue; } @@ -1456,9 +1850,11 @@ function addSetAuthToConvexClients(content: string, type: "js" | "next" | "react end: match.index + fullMatch.length, text: replacementText, }); + logVerbose("addSetAuthToConvexClients queued replacement", { variableName, authCall }); } if (replacements.length === 0) { + logVerbose("addSetAuthToConvexClients no replacements", { instantiationCount, skippedHttpCount }); return { updatedContent: content, changed: false, @@ -1475,6 +1871,8 @@ function addSetAuthToConvexClients(content: string, type: "js" | "next" | "react updatedContent = `${updatedContent.slice(0, replacement.start)}${replacement.text}${updatedContent.slice(replacement.end)}`; } + logVerbose("addSetAuthToConvexClients completed replacements", { replacements: replacements.length }); + logVerbose("addSetAuthToConvexClients result", { changed: true, instantiationCount, skippedHttpCount, usedClientApp, usedServerApp }); return { updatedContent, changed: true, @@ -1488,27 +1886,39 @@ function addSetAuthToConvexClients(content: string, type: "js" | "next" | "react function determineAuthCallExpression({ type, className, content }: { type: "js" | "next" | "react", className: string, content: string }): { expression: string, identifier: "stackClientApp" | "stackServerApp" } { const hasClientAppReference = /\bstackClientApp\b/.test(content); const hasServerAppReference = /\bstackServerApp\b/.test(content); + logVerbose("determineAuthCallExpression context", { type, className, hasClientAppReference, hasServerAppReference }); if (type === "js") { - return { expression: "stackServerApp.getConvexClientAuth({})", identifier: "stackServerApp" }; + const result = { expression: "stackServerApp.getConvexClientAuth({})", identifier: "stackServerApp" as const }; + logVerbose("determineAuthCallExpression returning for JS", result); + return result; } if (hasClientAppReference) { - return { expression: getClientAuthCall(type), identifier: "stackClientApp" }; + const result = { expression: getClientAuthCall(type), identifier: "stackClientApp" as const }; + logVerbose("determineAuthCallExpression using client reference", result); + return result; } if (hasServerAppReference && className !== "ConvexReactClient") { - return { expression: "stackServerApp.getConvexClientAuth({})", identifier: "stackServerApp" }; + const result = { expression: "stackServerApp.getConvexClientAuth({})", identifier: "stackServerApp" as const }; + logVerbose("determineAuthCallExpression using server reference", result); + return result; } - return { expression: getClientAuthCall(type), identifier: "stackClientApp" }; + const fallback = { expression: getClientAuthCall(type), identifier: "stackClientApp" as const }; + logVerbose("determineAuthCallExpression fallback", fallback); + return fallback; } function getClientAuthCall(type: "js" | "next" | "react"): string { + logVerbose("getClientAuthCall invoked", { type }); return "stackClientApp.getConvexClientAuth({})"; } function collectConvexClientCandidateFiles(projectPath: string): string[] { + logVerbose("collectConvexClientCandidateFiles invoked", { projectPath }); const roots = getConvexSearchRoots(projectPath); + logVerbose("collectConvexClientCandidateFiles roots", { roots }); const files = new Set(); const visited = new Set(); @@ -1516,7 +1926,9 @@ function collectConvexClientCandidateFiles(projectPath: string): string[] { walkDirectory(root, files, visited); } - return Array.from(files); + const result = Array.from(files); + logVerbose("collectConvexClientCandidateFiles result", { count: result.length }); + return result; } function getConvexSearchRoots(projectPath: string): string[] { @@ -1531,8 +1943,10 @@ function getConvexSearchRoots(projectPath: string): string[] { } }); if (existing.length > 0) { + logVerbose("getConvexSearchRoots using existing directories", { existing }); return existing; } + logVerbose("getConvexSearchRoots defaulting to project root", { projectPath }); return [projectPath]; } @@ -1562,6 +1976,7 @@ function walkDirectory(currentDir: string, files: Set, visited: Set, visited: Set