From 99ffc3afa56bc9aedaf7036a5295846e7732e354 Mon Sep 17 00:00:00 2001 From: Aadesh Kheria Date: Thu, 23 Apr 2026 18:56:44 -0700 Subject: [PATCH 1/2] initial commit --- packages/stack-cli/src/commands/init.ts | 26 ++++++++++++-- packages/stack-cli/src/commands/project.ts | 38 ++------------------ packages/stack-cli/src/lib/create-project.ts | 36 +++++++++++++++++++ 3 files changed, 63 insertions(+), 37 deletions(-) create mode 100644 packages/stack-cli/src/lib/create-project.ts diff --git a/packages/stack-cli/src/commands/init.ts b/packages/stack-cli/src/commands/init.ts index 1bfe6643fe..c603d9b52d 100644 --- a/packages/stack-cli/src/commands/init.ts +++ b/packages/stack-cli/src/commands/init.ts @@ -10,6 +10,7 @@ import { writeConfigValue } from "../lib/config.js"; import { CliError, AuthError } from "../lib/errors.js"; import { isNonInteractiveEnv } from "../lib/interactive.js"; import { createInitPrompt } from "../lib/init-prompt.js"; +import { createProjectInteractively } from "../lib/create-project.js"; import { runClaudeAgent } from "../lib/claude-agent.js"; import { detectImportPackageFromDir, renderConfigFileContent } from "@stackframe/stack-shared/dist/config-rendering"; @@ -156,10 +157,29 @@ async function handleLinkFromCloud(flags: Record, opts: InitOpt } const user = await getInternalUser(sessionAuth); - const projects = await user.listOwnedProjects(); + let projects = await user.listOwnedProjects(); + let autoCreatedProjectId: string | null = null; if (projects.length === 0) { - throw new CliError("You don't own any projects. Create one at app.stack-auth.com first."); + if (isNonInteractiveEnv()) { + throw new CliError("No projects found. Run `stack project create --display-name ` first, or set --select-project-id."); + } + + const shouldCreate = await confirm({ + message: "You don't have any Stack Auth projects yet. Would you like to create one?", + default: true, + }); + + if (!shouldCreate) { + throw new CliError("You don't own any projects. Create one at app.stack-auth.com or re-run and choose to create one."); + } + + const newProject = await createProjectInteractively(user, { + defaultDisplayName: path.basename(outputDir), + }); + console.log(`\nCreated project: ${newProject.displayName} (${newProject.id})\n`); + projects = [newProject]; + autoCreatedProjectId = newProject.id; } let projectId: string; @@ -169,6 +189,8 @@ async function handleLinkFromCloud(flags: Record, opts: InitOpt throw new CliError(`Project '${opts.selectProjectId}' not found among your owned projects.`); } projectId = opts.selectProjectId; + } else if (autoCreatedProjectId) { + projectId = autoCreatedProjectId; } else { projectId = await select({ message: "Select a project:", diff --git a/packages/stack-cli/src/commands/project.ts b/packages/stack-cli/src/commands/project.ts index 16dbfc4e62..2b1f49ff16 100644 --- a/packages/stack-cli/src/commands/project.ts +++ b/packages/stack-cli/src/commands/project.ts @@ -1,22 +1,7 @@ import { Command } from "commander"; -import * as readline from "readline"; import { resolveSessionAuth } from "../lib/auth.js"; import { getInternalUser } from "../lib/app.js"; -import { isNonInteractiveEnv } from "../lib/interactive.js"; -import { CliError } from "../lib/errors.js"; - -function prompt(question: string): Promise { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - return new Promise((resolve) => { - rl.question(question, (answer) => { - rl.close(); - resolve(answer); - }); - }); -} +import { createProjectInteractively } from "../lib/create-project.js"; export function registerProjectCommand(program: Command) { const project = program @@ -54,25 +39,8 @@ export function registerProjectCommand(program: Command) { const auth = resolveSessionAuth(flags); const user = await getInternalUser(auth); - let displayName: string = opts.displayName; - if (!displayName) { - if (isNonInteractiveEnv()) { - throw new CliError("--display-name is required in non-interactive environments (CI)."); - } - displayName = await prompt("Project display name: "); - if (!displayName.trim()) { - throw new CliError("Display name cannot be empty."); - } - } - - const teams = await user.listTeams(); - if (teams.length === 0) { - throw new CliError("No teams found. You need a team to create a project."); - } - - const newProject = await user.createProject({ - displayName, - teamId: teams[0].id, + const newProject = await createProjectInteractively(user, { + displayName: opts.displayName, }); if (program.opts().json) { diff --git a/packages/stack-cli/src/lib/create-project.ts b/packages/stack-cli/src/lib/create-project.ts new file mode 100644 index 0000000000..686a2c96ba --- /dev/null +++ b/packages/stack-cli/src/lib/create-project.ts @@ -0,0 +1,36 @@ +import { input } from "@inquirer/prompts"; +import type { CurrentInternalUser } from "@stackframe/js"; +import { CliError } from "./errors.js"; +import { isNonInteractiveEnv } from "./interactive.js"; + +type CreateProjectOptions = { + displayName?: string, + defaultDisplayName?: string, +}; + +export async function createProjectInteractively( + user: CurrentInternalUser, + opts: CreateProjectOptions = {}, +) { + let displayName = opts.displayName; + if (!displayName) { + if (isNonInteractiveEnv()) { + throw new CliError("--display-name is required in non-interactive environments (CI)."); + } + displayName = await input({ + message: "Project display name:", + default: opts.defaultDisplayName, + validate: (v) => v.trim().length > 0 || "Display name cannot be empty.", + }); + } + + const teams = await user.listTeams(); + if (teams.length === 0) { + throw new CliError("No teams found on your account. Create a team at app.stack-auth.com first."); + } + + return await user.createProject({ + displayName: displayName.trim(), + teamId: teams[0].id, + }); +} From c7a92ff8cd501ce4bc1b549ddddfef896fec61c8 Mon Sep 17 00:00:00 2001 From: Aadesh Kheria Date: Thu, 23 Apr 2026 20:27:12 -0700 Subject: [PATCH 2/2] cloud project creation --- packages/stack-cli/src/commands/init.ts | 148 +++++++++++++++--------- 1 file changed, 91 insertions(+), 57 deletions(-) diff --git a/packages/stack-cli/src/commands/init.ts b/packages/stack-cli/src/commands/init.ts index c603d9b52d..50c26372a8 100644 --- a/packages/stack-cli/src/commands/init.ts +++ b/packages/stack-cli/src/commands/init.ts @@ -15,7 +15,7 @@ import { runClaudeAgent } from "../lib/claude-agent.js"; import { detectImportPackageFromDir, renderConfigFileContent } from "@stackframe/stack-shared/dist/config-rendering"; type InitOptions = { - mode?: "create" | "link-config" | "link-cloud", + mode?: "create" | "create-cloud" | "link-config" | "link-cloud", apps?: string, configFile?: string, selectProjectId?: string, @@ -27,7 +27,7 @@ export function registerInitCommand(program: Command) { program .command("init") .description("Initialize Stack Auth in your project") - .option("--mode ", "Mode: create, link-config, or link-cloud (skips interactive prompts)") + .option("--mode ", "Mode: create, create-cloud, link-config, or link-cloud (skips interactive prompts)") .option("--apps ", "Comma-separated app IDs to enable (for create mode)") .option("--config-file ", "Path to existing config file (for link-config mode)") .option("--select-project-id ", "Project ID to link (for link-cloud mode)") @@ -58,13 +58,24 @@ async function runInit(program: Command, opts: InitOptions) { console.log("Welcome to Stack Auth!\n"); - const mode: string = opts.mode ?? await select({ - message: "Would you like to link to an existing project, or create a new one?", - choices: [ - { name: "Link an existing project", value: "link" as const }, - { name: "Create a new project (local emulator)", value: "create" as const }, - ], - }); + let mode: string; + if (opts.mode) { + mode = opts.mode; + } else if (opts.selectProjectId) { + mode = "link-cloud"; + } else if (opts.configFile) { + mode = "link-config"; + } else { + console.log("Creating a new Stack Auth project.\n"); + const location = await select({ + message: "Where would you like to create the project?", + choices: [ + { name: "Stack Auth Cloud", value: "hosted" as const }, + { name: "Local (requires local emulator installation, ~1.3gb storage required)", value: "local" as const }, + ], + }); + mode = location === "local" ? "create" : "create-cloud"; + } let configPath: string | undefined; @@ -74,6 +85,9 @@ async function runInit(program: Command, opts: InitOptions) { } else if (mode === "create") { const result = await handleCreate(opts, outputDir); configPath = result.configPath; + } else if (mode === "create-cloud") { + const result = await handleCreateCloud(flags, opts, outputDir); + configPath = result.configPath; } else { throw new CliError(`Unknown mode: ${mode}`); } @@ -139,10 +153,9 @@ async function handleLinkFromConfigFile(opts: InitOptions): Promise<{ configPath return { configPath }; } -async function handleLinkFromCloud(flags: Record, opts: InitOptions, outputDir: string): Promise<{ configPath?: string }> { - let sessionAuth; +async function ensureLoggedInSession(flags: Record) { try { - sessionAuth = resolveSessionAuth(flags as { projectId?: string }); + return resolveSessionAuth(flags as { projectId?: string }); } catch (e) { if (e instanceof AuthError) { if (isNonInteractiveEnv()) { @@ -150,12 +163,75 @@ async function handleLinkFromCloud(flags: Record, opts: InitOpt } console.log("You need to log in first.\n"); await performLogin(flags); - sessionAuth = resolveSessionAuth(flags as { projectId?: string }); + return resolveSessionAuth(flags as { projectId?: string }); + } + throw e; + } +} + +async function writeProjectKeysToEnv( + project: { id: string, app: { createInternalApiKey: (opts: { description: string, expiresAt: Date, hasPublishableClientKey: boolean, hasSecretServerKey: boolean, hasSuperSecretAdminKey: boolean }) => Promise<{ publishableClientKey?: string | null, secretServerKey?: string | null }> } }, + outputDir: string, +) { + const apiKey = await project.app.createInternalApiKey({ + description: "Created by CLI init script", + expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 200), // 200 years + hasPublishableClientKey: true, + hasSecretServerKey: true, + hasSuperSecretAdminKey: false, + }); + + const envLines = [ + "# Stack Auth", + `NEXT_PUBLIC_STACK_PROJECT_ID=${project.id}`, + `NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=${apiKey.publishableClientKey ?? ""}`, + `STACK_SECRET_SERVER_KEY=${apiKey.secretServerKey ?? ""}`, + ].join("\n"); + + const envPath = path.resolve(outputDir, ".env"); + + if (fs.existsSync(envPath)) { + const existing = fs.readFileSync(envPath, "utf-8"); + const separator = existing.endsWith("\n") ? "\n" : "\n\n"; + + if (isNonInteractiveEnv()) { + fs.appendFileSync(envPath, separator + envLines + "\n"); + console.log("\nAppended Stack Auth keys to .env"); } else { - throw e; + const shouldAppend = await confirm({ + message: `.env file already exists. Append Stack Auth keys?`, + default: true, + }); + + if (shouldAppend) { + fs.appendFileSync(envPath, separator + envLines + "\n"); + console.log("\nAppended Stack Auth keys to .env"); + } else { + console.log("\nHere are your environment variables:\n"); + console.log(envLines); + } } + } else { + fs.writeFileSync(envPath, envLines + "\n"); + console.log("\nCreated .env with Stack Auth keys"); } +} +async function handleCreateCloud(flags: Record, opts: InitOptions, outputDir: string): Promise<{ configPath?: string }> { + const sessionAuth = await ensureLoggedInSession(flags); + const user = await getInternalUser(sessionAuth); + + const newProject = await createProjectInteractively(user, { + defaultDisplayName: path.basename(outputDir), + }); + console.log(`\nCreated project: ${newProject.displayName} (${newProject.id})\n`); + + await writeProjectKeysToEnv(newProject, outputDir); + return {}; +} + +async function handleLinkFromCloud(flags: Record, opts: InitOptions, outputDir: string): Promise<{ configPath?: string }> { + const sessionAuth = await ensureLoggedInSession(flags); const user = await getInternalUser(sessionAuth); let projects = await user.listOwnedProjects(); let autoCreatedProjectId: string | null = null; @@ -202,49 +278,7 @@ async function handleLinkFromCloud(flags: Record, opts: InitOpt } const project = projects.find((p) => p.id === projectId)!; - const apiKey = await project.app.createInternalApiKey({ - description: "Created by CLI init script", - expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 200), // 200 years - hasPublishableClientKey: true, - hasSecretServerKey: true, - hasSuperSecretAdminKey: false, - }); - - const envLines = [ - "# Stack Auth", - `NEXT_PUBLIC_STACK_PROJECT_ID=${projectId}`, - `NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=${apiKey.publishableClientKey ?? ""}`, - `STACK_SECRET_SERVER_KEY=${apiKey.secretServerKey ?? ""}`, - ].join("\n"); - - const envPath = path.resolve(outputDir, ".env"); - - if (fs.existsSync(envPath)) { - const existing = fs.readFileSync(envPath, "utf-8"); - const separator = existing.endsWith("\n") ? "\n" : "\n\n"; - - if (isNonInteractiveEnv()) { - fs.appendFileSync(envPath, separator + envLines + "\n"); - console.log("\nAppended Stack Auth keys to .env"); - } else { - const shouldAppend = await confirm({ - message: `.env file already exists. Append Stack Auth keys?`, - default: true, - }); - - if (shouldAppend) { - fs.appendFileSync(envPath, separator + envLines + "\n"); - console.log("\nAppended Stack Auth keys to .env"); - } else { - console.log("\nHere are your environment variables:\n"); - console.log(envLines); - } - } - } else { - fs.writeFileSync(envPath, envLines + "\n"); - console.log("\nCreated .env with Stack Auth keys"); - } - + await writeProjectKeysToEnv(project, outputDir); return {}; }