Skip to content

Commit 6bf092a

Browse files
BilalG1N2D4
andauthored
init script: fix stack client app options , add --on-question, remove --agent-mode
<!-- Make sure you've read the CONTRIBUTING.md guidelines: https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md --> <!-- RECURSEML_SUMMARY:START --> ## High-level PR Summary This PR enhances the initialization script for Stack Auth by adding support for `projectId` and `publishableClientKey` (pck) parameters when initializing a Next.js client application. The changes include: 1) Adding logic to pass these parameters from environment variables or directly from arguments to the Next.js client configuration, 2) Updating the layout template to use the client-side Stack app instead of the server-side app in the provider component, and 3) Improving Bun lock file detection by checking for both `bun.lockb` and `bun.lock` formats. These changes ensure proper configuration of Next.js client applications with the required Stack Auth credentials. ⏱️ Estimated Review Time: 5-15 minutes <details> <summary>💡 Review Order Suggestion</summary> | Order | File Path | |-------|-----------| | 1 | `packages/init-stack/src/index.ts` | </details> <!-- RECURSEML_SUMMARY:END --> <!-- ELLIPSIS_HIDDEN --> ---- > [!IMPORTANT] > Enhances Stack Auth init script with `projectId` and `publishableClientKey` support, updates Next.js layout, and improves Bun detection. > > - **Behavior**: > - Adds `projectId` and `publishableClientKey` support to Next.js client configuration in `index.ts`. > - Updates layout template to use client-side Stack app in `getUpdatedLayout()`. > - Improves Bun lock file detection in `promptPackageManager()`. > - **Options**: > - Introduces `--on-question` option to control interactive prompts in `index.ts`. > - **Scripts**: > - Standardizes test scripts to use `--on-question error` in `package.json`. > > <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral)<sup> for 67a98f5. You can [customize](https://app.ellipsis.dev/stack-auth/settings/summaries) this summary. It will automatically update as commits are pushed.</sup> ---- <!-- ELLIPSIS_HIDDEN --> <!-- RECURSEML_ANALYSIS:START --> ## Review by RecurseML _🔍 Review performed on [7a0bf86..0b443e4](stack-auth/stack-auth@7a0bf86...0b443e460f653209cc237cdcec8ccca6a9a8f604)_ ✨ No bugs found, your code is sparkling clean <details> <summary>✅ Files analyzed, no issues (1)</summary> • `packages/init-stack/src/index.ts` </details> [![Need help? Join our Discord](https://img.shields.io/badge/Need%20help%3F%20Join%20our%20Discord-5865F2?style=plastic&logo=discord&logoColor=white)](https://discord.gg/n3SsVDAW6U) <!-- RECURSEML_ANALYSIS:END --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a new --on-question option to control interactive prompt behavior; client-oriented layout generation now emits client-side config (public env keys supported). * **Bug Fixes** * Improved Bun detection (recognizes both lockfile variants) and clearer, mode-aware guidance for ambiguous project/package-manager detection. * **Chores** * Test/init scripts updated to use --on-question error for deterministic non-interactive runs. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
1 parent e256be8 commit 6bf092a

2 files changed

Lines changed: 84 additions & 30 deletions

File tree

packages/init-stack/package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,16 @@
1818
"ensure-neon": "grep -q '\"@neondatabase/serverless\"' ./test-run-output/package.json && echo 'Initialized Neon successfully!'",
1919
"test-run-neon": "pnpm run test-run-node --neon && pnpm run ensure-neon",
2020
"test-run-neon:manual": "pnpm run test-run-node:manual --neon && pnpm run ensure-neon",
21-
"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 --agent-mode --js --server --npm --no-browser",
21+
"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",
2222
"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",
23-
"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 --agent-mode --js --server --npm --no-browser",
23+
"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",
2424
"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",
25-
"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 --agent-mode --js --client --npm --no-browser",
25+
"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",
2626
"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",
27-
"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 --agent-mode --no-browser",
28-
"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 --agent-mode --project-id my-project-id --publishable-client-key my-publishable-client-key",
29-
"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 --agent-mode --js --client --npm --project-id my-project-id --publishable-client-key my-publishable-client-key",
30-
"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 --agent-mode --no-browser --npm",
27+
"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",
28+
"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",
29+
"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",
30+
"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",
3131
"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"
3232
},
3333
"files": [

packages/init-stack/src/index.ts

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,51 @@ const jsLikeFileExtensions: string[] = [
2424
"js",
2525
];
2626

27+
class UserError extends Error {
28+
constructor(message: string) {
29+
super(message);
30+
this.name = "UserError";
31+
}
32+
}
33+
34+
class UnansweredQuestionError extends UserError {
35+
constructor(message: string) {
36+
super(message + ", or use --on-question <guess|ask> to answer questions automatically or interactively");
37+
this.name = "UnansweredQuestionError";
38+
}
39+
}
40+
41+
type OnQuestionMode = "ask" | "guess" | "error";
42+
43+
function isTruthyEnv(name: string): boolean {
44+
const v = process.env[name];
45+
if (!v) return false;
46+
const s = String(v).toLowerCase();
47+
return s === "1" || s === "true" || s === "yes";
48+
}
49+
50+
function isNonInteractiveEnv(): boolean {
51+
if (isTruthyEnv("CI")) return true;
52+
if (isTruthyEnv("GITHUB_ACTIONS")) return true;
53+
if (isTruthyEnv("NONINTERACTIVE")) return true;
54+
if (isTruthyEnv("NO_INTERACTIVE")) return true;
55+
if (isTruthyEnv("PNPM_NON_INTERACTIVE")) return true;
56+
if (isTruthyEnv("YARN_ENABLE_NON_INTERACTIVE")) return true;
57+
if (isTruthyEnv("CURSOR_AGENT")) return true;
58+
if (isTruthyEnv("CLAUDECODE")) return true;
59+
return false;
60+
}
61+
62+
function resolveOnQuestionMode(opt: string): OnQuestionMode {
63+
if (!opt || opt === "default") {
64+
return isNonInteractiveEnv() ? "error" : "ask";
65+
}
66+
if (opt === "ask" || opt === "guess" || opt === "error") {
67+
return opt;
68+
}
69+
throw new UserError(`Invalid argument for --on-question: "${opt}". Valid modes are: "ask", "guess", "error", "default".`);
70+
}
71+
2772
// Setup command line parsing
2873
const program = new Command();
2974
program
@@ -46,7 +91,7 @@ program
4691
.option("--project-id <project-id>", "Project ID to use in setup")
4792
.option("--publishable-client-key <publishable-client-key>", "Publishable client key to use in setup")
4893
.option("--no-browser", "Don't open browser for environment variable setup")
49-
.option("--agent-mode", "Run without prompting for any input")
94+
.option("--on-question <mode>", "How to handle interactive questions: ask | guess | error | default", "default")
5095
.addHelpText('after', `
5196
For more information, please visit https://docs.stack-auth.com/getting-started/setup`);
5297

@@ -64,18 +109,12 @@ const isClient: boolean = options.client || false;
64109
const isServer: boolean = options.server || false;
65110
const projectIdFromArgs: string | undefined = options.projectId;
66111
const publishableClientKeyFromArgs: string | undefined = options.publishableClientKey;
67-
const agentMode = !!options.agentMode;
112+
const onQuestionMode: OnQuestionMode = resolveOnQuestionMode(options.onQuestion);
113+
68114
// Commander negates the boolean options with prefix `--no-`
69115
// so `--no-browser` becomes `browser: false`
70116
const noBrowser: boolean = !options.browser;
71117

72-
class UserError extends Error {
73-
constructor(message: string) {
74-
super(message);
75-
this.name = "UserError";
76-
}
77-
}
78-
79118
type Ansis = {
80119
red: string,
81120
blue: string,
@@ -436,9 +475,12 @@ const Steps = {
436475
if (packageJson.dependencies?.["react"] || packageJson.dependencies?.["react-dom"]) {
437476
return "react";
438477
}
439-
if (agentMode) {
478+
if (onQuestionMode === "guess") {
440479
return "js";
441480
}
481+
if (onQuestionMode === "error") {
482+
throw new UnansweredQuestionError("Unable to auto-detect project type (checked for Next.js and React dependencies). Re-run with one of: --js, --react, or --next.");
483+
}
442484

443485
const { type } = await inquirer.prompt([
444486
{
@@ -625,15 +667,25 @@ const Steps = {
625667

626668
const tokenStore = type === "next" ? '"nextjs-cookie"' : (clientOrServer === "client" ? '"cookie"' : '"memory"');
627669
const publishableClientKeyWrite = clientOrServer === "server"
628-
? `process.env.STACK_PUBLISHABLE_CLIENT_KEY ${publishableClientKeyFromArgs ? `|| '${publishableClientKeyFromArgs}'` : ""}`
670+
? `process.env.STACK_PUBLISHABLE_CLIENT_KEY ${publishableClientKeyFromArgs ? `|| ${JSON.stringify(publishableClientKeyFromArgs)}` : ""}`
629671
: `'${publishableClientKeyFromArgs ?? 'INSERT_YOUR_PUBLISHABLE_CLIENT_KEY_HERE'}'`;
630672
const jsOptions = type === "js" ? [
631673
`\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)` : ""}`,
632-
`${projectIdFromArgs ? `${indentation}projectId: '${projectIdFromArgs}',` : ""}`,
674+
`${projectIdFromArgs ? `${indentation}projectId: ${JSON.stringify(projectIdFromArgs)},` : ""}`,
633675
`${indentation}publishableClientKey: ${publishableClientKeyWrite},`,
634676
`${clientOrServer === "server" ? `${indentation}secretServerKey: process.env.STACK_SECRET_SERVER_KEY,` : ""}`,
635677
].filter(Boolean).join("\n") : "";
636678

679+
const nextClientOptions = (type === "next" && clientOrServer === "client")
680+
? (() => {
681+
const lines = [
682+
projectIdFromArgs ? `${indentation}projectId: process.env.NEXT_PUBLIC_STACK_PROJECT_ID ?? ${JSON.stringify(projectIdFromArgs)},` : "",
683+
publishableClientKeyFromArgs ? `${indentation}publishableClientKey: process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY ?? ${JSON.stringify(publishableClientKeyFromArgs)},` : "",
684+
].filter(Boolean).join("\n");
685+
return lines ? `\n${lines}` : "";
686+
})()
687+
: "";
688+
637689

638690
laterWriteFileIfNotExists(
639691
stackAppPath,
@@ -643,7 +695,7 @@ ${type === "next" && clientOrServer === "server" ? `import "server-only";` : ""}
643695
import { Stack${clientOrServerCap}App } from ${JSON.stringify(packageName)};
644696
645697
export const stack${clientOrServerCap}App = new Stack${clientOrServerCap}App({
646-
${indentation}tokenStore: ${tokenStore},${jsOptions}
698+
${indentation}tokenStore: ${tokenStore},${jsOptions}${nextClientOptions}
647699
});
648700
`.trim() + "\n"
649701
);
@@ -741,7 +793,7 @@ ${indentation}tokenStore: ${tokenStore},${jsOptions}
741793
react: "React",
742794
} as const;
743795
const typeString = typeStringMap[type];
744-
const isReady = agentMode || (await inquirer.prompt([
796+
const isReady = (onQuestionMode !== "ask") || (await inquirer.prompt([
745797
{
746798
type: "confirm",
747799
name: "ready",
@@ -759,8 +811,9 @@ ${indentation}tokenStore: ${tokenStore},${jsOptions}
759811
if (isServer) return ["server"];
760812
if (isClient) return ["client"];
761813

762-
if (agentMode) {
763-
throw new UserError("Please specify the installation type using the --server or --client argument.");
814+
if (onQuestionMode === "guess") return ["server", "client"];
815+
if (onQuestionMode === "error") {
816+
throw new UnansweredQuestionError("Ambiguous installation type. Re-run with --server, --client, or both.");
764817
}
765818

766819
return (await inquirer.prompt([{
@@ -816,7 +869,7 @@ async function getUpdatedLayout(originalLayout: string): Promise<LayoutResult |
816869
const importInsertLocationM1 =
817870
firstImportLocationM1 ?? (hasStringAsFirstLine ? layout.indexOf("\n") : -1);
818871
const importInsertLocation = importInsertLocationM1 + 1;
819-
const importStatement = `import { StackProvider, StackTheme } from "@stackframe/stack";\nimport { stackServerApp } from "../stack/server";\n`;
872+
const importStatement = `import { StackProvider, StackTheme } from "@stackframe/stack";\nimport { stackClientApp } from "../stack/client";\n`;
820873
layout =
821874
layout.slice(0, importInsertLocation) +
822875
importStatement +
@@ -843,7 +896,7 @@ async function getUpdatedLayout(originalLayout: string): Promise<LayoutResult |
843896
bodyCloseStartIndex
844897
);
845898

846-
const insertOpen = "<StackProvider app={stackServerApp}><StackTheme>";
899+
const insertOpen = "<StackProvider app={stackClientApp}><StackTheme>";
847900
const insertClose = "</StackTheme></StackProvider>";
848901

849902
layout =
@@ -899,8 +952,8 @@ async function getProjectPath(): Promise<string> {
899952
path.join(savedProjectPath, "package.json")
900953
);
901954
if (askForPathModification) {
902-
if (agentMode) {
903-
throw new UserError(`No package.json file found in the project directory ${savedProjectPath}. Please specify the correct project path using the --project-path argument, or create a new project before running the wizard.`);
955+
if (onQuestionMode === "guess" || onQuestionMode === "error") {
956+
throw new UserError(`No package.json file found in ${savedProjectPath}. Re-run providing the project path argument (e.g. 'init-stack <project-path>').`);
904957
}
905958
savedProjectPath = (
906959
await inquirer.prompt([
@@ -932,7 +985,7 @@ async function promptPackageManager(): Promise<string> {
932985
const yarnLock = fs.existsSync(path.join(projectPath, "yarn.lock"));
933986
const pnpmLock = fs.existsSync(path.join(projectPath, "pnpm-lock.yaml"));
934987
const npmLock = fs.existsSync(path.join(projectPath, "package-lock.json"));
935-
const bunLock = fs.existsSync(path.join(projectPath, "bun.lockb"));
988+
const bunLock = fs.existsSync(path.join(projectPath, "bun.lockb")) || fs.existsSync(path.join(projectPath, "bun.lock"));
936989

937990
if (yarnLock && !pnpmLock && !npmLock && !bunLock) {
938991
return "yarn";
@@ -944,8 +997,9 @@ async function promptPackageManager(): Promise<string> {
944997
return "bun";
945998
}
946999

947-
if (agentMode) {
948-
throw new UserError("Unable to determine which package manager to use. Please rerun the init command and specify the package manager using exactly one of the following arguments: --npm, --yarn, --pnpm, or --bun.");
1000+
if (onQuestionMode === "guess") return "npm";
1001+
if (onQuestionMode === "error") {
1002+
throw new UnansweredQuestionError("Unable to determine the package manager. Re-run with one of: --npm, --yarn, --pnpm, or --bun.");
9491003
}
9501004

9511005
const answers = await inquirer.prompt([

0 commit comments

Comments
 (0)