From fb0c0efe558a4c77ed649700106601f3d0e8ce31 Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Wed, 25 Feb 2026 17:02:32 -0800 Subject: [PATCH 01/13] base cli --- packages/stack-cli/.eslintrc.cjs | 6 ++ packages/stack-cli/package.json | 40 +++++++++ .../stack-cli/src/commands/config-file.ts | 74 ++++++++++++++++ packages/stack-cli/src/commands/exec.ts | 23 +++++ packages/stack-cli/src/commands/init.ts | 16 ++++ packages/stack-cli/src/commands/login.ts | 36 ++++++++ packages/stack-cli/src/commands/logout.ts | 12 +++ packages/stack-cli/src/commands/project.ts | 84 +++++++++++++++++++ packages/stack-cli/src/commands/update.ts | 13 +++ packages/stack-cli/src/index.ts | 54 ++++++++++++ packages/stack-cli/src/lib/app.ts | 34 ++++++++ packages/stack-cli/src/lib/auth.ts | 77 +++++++++++++++++ packages/stack-cli/src/lib/config.ts | 61 ++++++++++++++ packages/stack-cli/src/lib/errors.ts | 13 +++ packages/stack-cli/src/lib/interactive.ts | 8 ++ packages/stack-cli/tsconfig.json | 22 +++++ packages/stack-cli/tsup.config.ts | 15 ++++ pnpm-lock.yaml | 81 ++++++++++++------ 18 files changed, 642 insertions(+), 27 deletions(-) create mode 100644 packages/stack-cli/.eslintrc.cjs create mode 100644 packages/stack-cli/package.json create mode 100644 packages/stack-cli/src/commands/config-file.ts create mode 100644 packages/stack-cli/src/commands/exec.ts create mode 100644 packages/stack-cli/src/commands/init.ts create mode 100644 packages/stack-cli/src/commands/login.ts create mode 100644 packages/stack-cli/src/commands/logout.ts create mode 100644 packages/stack-cli/src/commands/project.ts create mode 100644 packages/stack-cli/src/commands/update.ts create mode 100644 packages/stack-cli/src/index.ts create mode 100644 packages/stack-cli/src/lib/app.ts create mode 100644 packages/stack-cli/src/lib/auth.ts create mode 100644 packages/stack-cli/src/lib/config.ts create mode 100644 packages/stack-cli/src/lib/errors.ts create mode 100644 packages/stack-cli/src/lib/interactive.ts create mode 100644 packages/stack-cli/tsconfig.json create mode 100644 packages/stack-cli/tsup.config.ts diff --git a/packages/stack-cli/.eslintrc.cjs b/packages/stack-cli/.eslintrc.cjs new file mode 100644 index 0000000000..4994c78632 --- /dev/null +++ b/packages/stack-cli/.eslintrc.cjs @@ -0,0 +1,6 @@ +module.exports = { + "extends": [ + "../../configs/eslint/defaults.js", + ], + "ignorePatterns": ['/*', '!/src'], +}; diff --git a/packages/stack-cli/package.json b/packages/stack-cli/package.json new file mode 100644 index 0000000000..9ad4da307e --- /dev/null +++ b/packages/stack-cli/package.json @@ -0,0 +1,40 @@ +{ + "name": "@stackframe/stack-cli", + "version": "2.8.71", + "repository": "https://github.com/stack-auth/stack-auth", + "description": "The CLI for Stack Auth. https://stack-auth.com", + "main": "dist/index.js", + "type": "module", + "bin": { + "stack": "./dist/index.js" + }, + "scripts": { + "clean": "rimraf node_modules && rimraf dist", + "build": "tsup", + "dev": "tsup --watch", + "lint": "eslint --ext .tsx,.ts .", + "typecheck": "tsc --noEmit" + }, + "files": [ + "README.md", + "dist", + "CHANGELOG.md", + "LICENSE" + ], + "homepage": "https://stack-auth.com", + "keywords": [], + "author": "", + "license": "MIT", + "dependencies": { + "@stackframe/js": "workspace:*", + "commander": "^13.1.0", + "jiti": "^2.4.2" + }, + "devDependencies": { + "@types/node": "20.17.6", + "rimraf": "^6.0.1", + "tsup": "^8.4.0", + "typescript": "5.3.3" + }, + "packageManager": "pnpm@10.23.0" +} diff --git a/packages/stack-cli/src/commands/config-file.ts b/packages/stack-cli/src/commands/config-file.ts new file mode 100644 index 0000000000..99a2e06bb5 --- /dev/null +++ b/packages/stack-cli/src/commands/config-file.ts @@ -0,0 +1,74 @@ +import { Command } from "commander"; +import * as path from "path"; +import * as fs from "fs"; +import { resolveAuth } from "../lib/auth.js"; +import { getAdminProject } from "../lib/app.js"; +import { CliError } from "../lib/errors.js"; + +export function registerConfigCommand(program: Command) { + const config = program + .command("config") + .description("Manage project configuration files"); + + config + .command("pull") + .description("Pull branch config to a local file") + .requiredOption("--config-file ", "Path to write config file (.js or .ts)") + .action(async (opts) => { + const flags = program.opts(); + const auth = resolveAuth(flags); + const project = await getAdminProject(auth); + + const configOverride = await project.getConfigOverride("branch"); + const filePath = path.resolve(opts.configFile); + const ext = path.extname(filePath); + + if (ext !== ".js" && ext !== ".ts") { + throw new CliError("Config file must have a .js or .ts extension."); + } + + const json = JSON.stringify(configOverride, null, 2); + const content = ext === ".ts" + ? `export const config = ${json} as const;\n` + : `export const config = ${json};\n`; + + fs.writeFileSync(filePath, content); + console.log(`Config written to ${filePath}`); + }); + + config + .command("push") + .description("Push a local config file to branch config") + .requiredOption("--config-file ", "Path to config file (.js or .ts)") + .action(async (opts) => { + const flags = program.opts(); + const auth = resolveAuth(flags); + const project = await getAdminProject(auth); + + const filePath = path.resolve(opts.configFile); + const ext = path.extname(filePath); + + if (!fs.existsSync(filePath)) { + throw new CliError(`Config file not found: ${filePath}`); + } + + let configModule: Record; + if (ext === ".ts") { + const { createJiti } = await import("jiti"); + const jiti = createJiti(import.meta.url); + configModule = await jiti.import(filePath) as Record; + } else if (ext === ".js") { + configModule = await import(filePath); + } else { + throw new CliError("Config file must have a .js or .ts extension."); + } + + const config = configModule.config; + if (!config || typeof config !== "object") { + throw new CliError("Config file must export a `config` object. Example: export const config = { ... };"); + } + + await project.replaceConfigOverride("branch", config as Record); + console.log("Config pushed successfully."); + }); +} diff --git a/packages/stack-cli/src/commands/exec.ts b/packages/stack-cli/src/commands/exec.ts new file mode 100644 index 0000000000..268166c88f --- /dev/null +++ b/packages/stack-cli/src/commands/exec.ts @@ -0,0 +1,23 @@ +import { Command } from "commander"; +import { resolveAuth } from "../lib/auth.js"; +import { getAdminProject } from "../lib/app.js"; + +export function registerExecCommand(program: Command) { + program + .command("exec ") + .description("Execute JavaScript with a pre-configured StackServerApp as `app`") + .action(async (javascript: string) => { + const flags = program.opts(); + const auth = resolveAuth(flags); + const project = await getAdminProject(auth); + + // eslint-disable-next-line @typescript-eslint/no-implied-eval + const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor; + const fn = new AsyncFunction("app", javascript); + const result = await fn(project.app); + + if (result !== undefined) { + console.log(JSON.stringify(result, null, 2)); + } + }); +} diff --git a/packages/stack-cli/src/commands/init.ts b/packages/stack-cli/src/commands/init.ts new file mode 100644 index 0000000000..2ebf03baaa --- /dev/null +++ b/packages/stack-cli/src/commands/init.ts @@ -0,0 +1,16 @@ +import { Command } from "commander"; +import { execFileSync } from "child_process"; + +export function registerInitCommand(program: Command) { + program + .command("init") + .description("Initialize Stack Auth in your project (delegates to @stackframe/init-stack)") + .allowUnknownOption(true) + .helpOption(false) + .action((_opts, cmd) => { + const args = cmd.args as string[]; + execFileSync("npx", ["@stackframe/init-stack@latest", ...args], { + stdio: "inherit", + }); + }); +} diff --git a/packages/stack-cli/src/commands/login.ts b/packages/stack-cli/src/commands/login.ts new file mode 100644 index 0000000000..cc01e34ac3 --- /dev/null +++ b/packages/stack-cli/src/commands/login.ts @@ -0,0 +1,36 @@ +import { Command } from "commander"; +import { StackClientApp } from "@stackframe/js"; +import { resolveLoginConfig, DEFAULT_PUBLISHABLE_CLIENT_KEY } from "../lib/auth.js"; +import { writeConfigValue } from "../lib/config.js"; +import { CliError } from "../lib/errors.js"; + +export function registerLoginCommand(program: Command) { + program + .command("login") + .description("Log in to Stack Auth via browser") + .action(async () => { + const flags = program.opts(); + const config = resolveLoginConfig(flags); + + const app = new StackClientApp({ + projectId: "internal", + publishableClientKey: DEFAULT_PUBLISHABLE_CLIENT_KEY, + baseUrl: config.apiUrl, + tokenStore: "memory", + noAutomaticPrefetch: true, + }); + + console.log("Waiting for browser authentication..."); + + const result = await app.promptCliLogin({ + appUrl: config.dashboardUrl, + }); + + if (result.status === "error") { + throw new CliError(`Login failed: ${result.error.message}`); + } + + writeConfigValue("STACK_CLI_REFRESH_TOKEN", result.data); + console.log("Login successful!"); + }); +} diff --git a/packages/stack-cli/src/commands/logout.ts b/packages/stack-cli/src/commands/logout.ts new file mode 100644 index 0000000000..81457df600 --- /dev/null +++ b/packages/stack-cli/src/commands/logout.ts @@ -0,0 +1,12 @@ +import { Command } from "commander"; +import { removeConfigValue } from "../lib/config.js"; + +export function registerLogoutCommand(program: Command) { + program + .command("logout") + .description("Log out of Stack Auth") + .action(() => { + removeConfigValue("STACK_CLI_REFRESH_TOKEN"); + console.log("Logged out successfully."); + }); +} diff --git a/packages/stack-cli/src/commands/project.ts b/packages/stack-cli/src/commands/project.ts new file mode 100644 index 0000000000..7ecb8f91b9 --- /dev/null +++ b/packages/stack-cli/src/commands/project.ts @@ -0,0 +1,84 @@ +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); + }); + }); +} + +export function registerProjectCommand(program: Command) { + const project = program + .command("project") + .description("Manage projects"); + + project + .command("list") + .description("List your owned projects") + .action(async () => { + const flags = program.opts(); + const auth = resolveSessionAuth(flags); + const user = await getInternalUser(auth); + const projects = await user.listOwnedProjects(); + + if (program.opts().json) { + console.log(JSON.stringify(projects.map((p) => ({ id: p.id, displayName: p.displayName })), null, 2)); + } else { + if (projects.length === 0) { + console.log("No projects found."); + return; + } + for (const p of projects) { + console.log(`${p.id}\t${p.displayName}`); + } + } + }); + + project + .command("create") + .description("Create a new project") + .option("--display-name ", "Project display name") + .action(async (opts) => { + const flags = program.opts(); + 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, + }); + + if (program.opts().json) { + console.log(JSON.stringify({ id: newProject.id, displayName: newProject.displayName })); + } else { + console.log(`Project created: ${newProject.id} (${newProject.displayName})`); + } + }); +} diff --git a/packages/stack-cli/src/commands/update.ts b/packages/stack-cli/src/commands/update.ts new file mode 100644 index 0000000000..8f9ca38157 --- /dev/null +++ b/packages/stack-cli/src/commands/update.ts @@ -0,0 +1,13 @@ +import { Command } from "commander"; + +export function registerUpdateCommand(program: Command) { + program + .command("update") + .description("Show version information") + .action(() => { + const version = program.version(); + console.log(`stack-cli version: ${version}`); + console.log("\nWhen using npx @stackframe/stack-cli, you always get the latest version."); + console.log("No manual update is needed."); + }); +} diff --git a/packages/stack-cli/src/index.ts b/packages/stack-cli/src/index.ts new file mode 100644 index 0000000000..4df1426da3 --- /dev/null +++ b/packages/stack-cli/src/index.ts @@ -0,0 +1,54 @@ +import { Command } from "commander"; +import { readFileSync } from "fs"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; +import { AuthError, CliError } from "./lib/errors.js"; +import { registerLoginCommand } from "./commands/login.js"; +import { registerLogoutCommand } from "./commands/logout.js"; +import { registerExecCommand } from "./commands/exec.js"; +import { registerConfigCommand } from "./commands/config-file.js"; +import { registerInitCommand } from "./commands/init.js"; +import { registerProjectCommand } from "./commands/project.js"; +import { registerUpdateCommand } from "./commands/update.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8")); + +const program = new Command(); + +program + .name("stack") + .description("Stack Auth CLI") + .version(pkg.version) + .option("--project-id ", "Project ID") + .option("--api-url ", "Stack Auth API URL") + .option("--dashboard-url ", "Stack Auth Dashboard URL") + .option("--json", "Output in JSON format"); + +registerLoginCommand(program); +registerLogoutCommand(program); +registerExecCommand(program); +registerConfigCommand(program); +registerInitCommand(program); +registerProjectCommand(program); +registerUpdateCommand(program); + +async function main() { + try { + await program.parseAsync(process.argv); + } catch (err) { + if (err instanceof AuthError) { + console.error(`Auth error: ${err.message}`); + process.exit(1); + } + if (err instanceof CliError) { + console.error(`Error: ${err.message}`); + process.exit(1); + } + throw err; + } +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +main(); diff --git a/packages/stack-cli/src/lib/app.ts b/packages/stack-cli/src/lib/app.ts new file mode 100644 index 0000000000..56e042ee7d --- /dev/null +++ b/packages/stack-cli/src/lib/app.ts @@ -0,0 +1,34 @@ +import { StackClientApp } from "@stackframe/js"; +import type { CurrentInternalUser, AdminOwnedProject } from "@stackframe/js"; +import { AuthError } from "./errors.js"; +import { DEFAULT_PUBLISHABLE_CLIENT_KEY } from "./auth.js"; +import type { SessionAuth, ProjectAuth } from "./auth.js"; + +export function getInternalApp(auth: SessionAuth): StackClientApp { + return new StackClientApp({ + projectId: "internal", + publishableClientKey: DEFAULT_PUBLISHABLE_CLIENT_KEY, + baseUrl: auth.apiUrl, + tokenStore: { + accessToken: "", + refreshToken: auth.refreshToken, + }, + noAutomaticPrefetch: true, + }); +} + +export async function getInternalUser(auth: SessionAuth): Promise { + const app = getInternalApp(auth); + const user = await app.getUser({ or: "throw" }); + return user as CurrentInternalUser; +} + +export async function getAdminProject(auth: ProjectAuth): Promise { + const user = await getInternalUser(auth); + const projects = await user.listOwnedProjects(); + const project = projects.find((p) => p.id === auth.projectId); + if (!project) { + throw new AuthError(`Project '${auth.projectId}' not found. Make sure you own this project.`); + } + return project; +} diff --git a/packages/stack-cli/src/lib/auth.ts b/packages/stack-cli/src/lib/auth.ts new file mode 100644 index 0000000000..d9112c86d2 --- /dev/null +++ b/packages/stack-cli/src/lib/auth.ts @@ -0,0 +1,77 @@ +import { readConfigValue } from "./config.js"; +import { AuthError } from "./errors.js"; + +export const DEFAULT_API_URL = "https://api.stack-auth.com"; +export const DEFAULT_DASHBOARD_URL = "https://app.stack-auth.com"; +export const DEFAULT_PUBLISHABLE_CLIENT_KEY = "pck_6ypt981excjnk24dmgx5703my25k2f3y2z3qjhbykz3q0"; + +type Flags = { + projectId?: string, + apiUrl?: string, + dashboardUrl?: string, +}; + +export type LoginConfig = { + apiUrl: string, + dashboardUrl: string, +}; + +export type SessionAuth = LoginConfig & { + refreshToken: string, +}; + +export type ProjectAuth = SessionAuth & { + projectId: string, +}; + +function resolveApiUrl(flags: Flags): string { + return flags.apiUrl + ?? process.env.STACK_API_URL + ?? readConfigValue("STACK_API_URL") + ?? DEFAULT_API_URL; +} + +function resolveDashboardUrl(flags: Flags): string { + return flags.dashboardUrl + ?? process.env.STACK_DASHBOARD_URL + ?? readConfigValue("STACK_DASHBOARD_URL") + ?? DEFAULT_DASHBOARD_URL; +} + +function resolveRefreshToken(): string { + const token = process.env.STACK_CLI_REFRESH_TOKEN + ?? readConfigValue("STACK_CLI_REFRESH_TOKEN"); + if (!token) { + throw new AuthError("Not logged in. Run `stack login` first."); + } + return token; +} + +function resolveProjectId(flags: Flags): string { + const projectId = flags.projectId ?? process.env.STACK_PROJECT_ID; + if (!projectId) { + throw new AuthError("No project ID specified. Use --project-id or set STACK_PROJECT_ID."); + } + return projectId; +} + +export function resolveLoginConfig(flags: Flags): LoginConfig { + return { + apiUrl: resolveApiUrl(flags), + dashboardUrl: resolveDashboardUrl(flags), + }; +} + +export function resolveSessionAuth(flags: Flags): SessionAuth { + return { + ...resolveLoginConfig(flags), + refreshToken: resolveRefreshToken(), + }; +} + +export function resolveAuth(flags: Flags): ProjectAuth { + return { + ...resolveSessionAuth(flags), + projectId: resolveProjectId(flags), + }; +} diff --git a/packages/stack-cli/src/lib/config.ts b/packages/stack-cli/src/lib/config.ts new file mode 100644 index 0000000000..9deee2e7e3 --- /dev/null +++ b/packages/stack-cli/src/lib/config.ts @@ -0,0 +1,61 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; + +const CONFIG_PATH = path.join(os.homedir(), ".stackrc"); + +type ConfigKey = "STACK_CLI_REFRESH_TOKEN" | "STACK_API_URL" | "STACK_DASHBOARD_URL"; + +function parseConfig(content: string): Record { + const result: Record = {}; + for (const line of content.split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) { + continue; + } + const eqIndex = trimmed.indexOf("="); + if (eqIndex === -1) { + continue; + } + const key = trimmed.slice(0, eqIndex).trim(); + const value = trimmed.slice(eqIndex + 1).trim(); + result[key] = value; + } + return result; +} + +function serializeConfig(config: Record): string { + return Object.entries(config) + .map(([key, value]) => `${key}=${value}`) + .join("\n") + "\n"; +} + +function readConfigFile(): Record { + try { + const content = fs.readFileSync(CONFIG_PATH, "utf-8"); + return parseConfig(content); + } catch { + return {}; + } +} + +function writeConfigFile(config: Record): void { + fs.writeFileSync(CONFIG_PATH, serializeConfig(config), { mode: 0o600 }); +} + +export function readConfigValue(key: ConfigKey): string | undefined { + const config = readConfigFile(); + return config[key]; +} + +export function writeConfigValue(key: ConfigKey, value: string): void { + const config = readConfigFile(); + config[key] = value; + writeConfigFile(config); +} + +export function removeConfigValue(key: ConfigKey): void { + const config = readConfigFile(); + delete config[key]; + writeConfigFile(config); +} diff --git a/packages/stack-cli/src/lib/errors.ts b/packages/stack-cli/src/lib/errors.ts new file mode 100644 index 0000000000..973b1ceca5 --- /dev/null +++ b/packages/stack-cli/src/lib/errors.ts @@ -0,0 +1,13 @@ +export class CliError extends Error { + constructor(message: string) { + super(message); + this.name = "CliError"; + } +} + +export class AuthError extends CliError { + constructor(message: string) { + super(message); + this.name = "AuthError"; + } +} diff --git a/packages/stack-cli/src/lib/interactive.ts b/packages/stack-cli/src/lib/interactive.ts new file mode 100644 index 0000000000..3b0b0701f0 --- /dev/null +++ b/packages/stack-cli/src/lib/interactive.ts @@ -0,0 +1,8 @@ +export function isNonInteractiveEnv(): boolean { + return !!( + process.env.CI + || process.env.GITHUB_ACTIONS + || process.env.NONINTERACTIVE + || !process.stdin.isTTY + ); +} diff --git a/packages/stack-cli/tsconfig.json b/packages/stack-cli/tsconfig.json new file mode 100644 index 0000000000..5237fd312d --- /dev/null +++ b/packages/stack-cli/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "target": "ES2021", + "lib": ["ES2021", "ES2022.Error"], + "module": "ES2020", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "noErrorTruncation": true, + "skipLibCheck": true, + "strict": true, + "sourceMap": true, + "declarationMap": true, + "types": [ + "vitest/importMeta" + ] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/stack-cli/tsup.config.ts b/packages/stack-cli/tsup.config.ts new file mode 100644 index 0000000000..6792e69817 --- /dev/null +++ b/packages/stack-cli/tsup.config.ts @@ -0,0 +1,15 @@ +import { defineConfig, Options } from 'tsup'; + +const config: Options = { + entryPoints: ['src/index.ts'], + sourcemap: true, + clean: false, + dts: true, + outDir: 'dist', + format: ['esm'], + banner: { + js: '#!/usr/bin/env node', + }, +}; + +export default defineConfig(config); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49f85999d4..a8990d1cd2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1843,6 +1843,31 @@ importers: specifier: ^8.0.2 version: 8.4.0(@swc/core@1.3.101(@swc/helpers@0.5.15))(jiti@2.6.1)(postcss@8.4.47)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.0) + packages/stack-cli: + dependencies: + '@stackframe/js': + specifier: workspace:* + version: link:../js + commander: + specifier: ^13.1.0 + version: 13.1.0 + jiti: + specifier: ^2.4.2 + version: 2.6.1 + devDependencies: + '@types/node': + specifier: 20.17.6 + version: 20.17.6 + rimraf: + specifier: ^6.0.1 + version: 6.1.2 + tsup: + specifier: ^8.4.0 + version: 8.4.0(@swc/core@1.3.101(@swc/helpers@0.5.15))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.3.3)(yaml@2.8.0) + typescript: + specifier: 5.3.3 + version: 5.3.3 + packages/stack-sc: dependencies: '@types/react': @@ -11580,6 +11605,7 @@ packages: glob@10.3.10: resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} engines: {node: '>=16 || 14 >=14.17'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@10.4.1: @@ -11602,11 +11628,12 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@9.3.5: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} engines: {node: '>=16 || 14 >=14.17'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} @@ -24235,7 +24262,7 @@ snapshots: '@ts-morph/common@0.27.0': dependencies: fast-glob: 3.3.3 - minimatch: 10.0.1 + minimatch: 10.1.1 path-browserify: 1.0.1 '@turf/boolean-point-in-polygon@7.1.0': @@ -24263,7 +24290,7 @@ snapshots: '@types/accepts@1.3.7': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.17.6 '@types/aria-query@5.0.4': {} @@ -24296,16 +24323,16 @@ snapshots: '@types/bn.js@5.1.5': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.17.6 '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.19.0 + '@types/node': 20.17.6 '@types/bunyan@1.8.11': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.17.6 '@types/canvas-confetti@1.6.4': {} @@ -24321,7 +24348,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.17.6 '@types/content-disposition@0.5.8': {} @@ -24334,7 +24361,7 @@ snapshots: '@types/connect': 3.4.38 '@types/express': 5.0.0 '@types/keygrip': 1.0.6 - '@types/node': 22.19.0 + '@types/node': 20.17.6 '@types/css-font-loading-module@0.0.7': {} @@ -24485,7 +24512,7 @@ snapshots: '@types/express-serve-static-core@5.0.6': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.17.6 '@types/qs': 6.9.15 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -24543,7 +24570,7 @@ snapshots: '@types/http-errors': 2.0.4 '@types/keygrip': 1.0.6 '@types/koa-compose': 3.2.8 - '@types/node': 22.19.0 + '@types/node': 20.17.6 '@types/lodash@4.17.5': {} @@ -24555,7 +24582,7 @@ snapshots: '@types/memcached@2.2.10': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.17.6 '@types/methods@1.1.4': {} @@ -24567,7 +24594,7 @@ snapshots: '@types/mysql@2.15.27': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.17.6 '@types/node@12.20.55': {} @@ -24599,7 +24626,7 @@ snapshots: '@types/oracledb@6.5.2': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.17.6 '@types/parse-json@4.0.2': {} @@ -24609,13 +24636,13 @@ snapshots: '@types/pg@8.15.4': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.17.6 pg-protocol: 1.10.3 pg-types: 2.2.0 '@types/pg@8.15.6': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.17.6 pg-protocol: 1.10.3 pg-types: 2.2.0 @@ -24659,12 +24686,12 @@ snapshots: '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 22.19.0 + '@types/node': 20.17.6 '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 22.19.0 + '@types/node': 20.17.6 '@types/send': 0.17.4 '@types/shimmer@1.2.0': {} @@ -24675,7 +24702,7 @@ snapshots: dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 22.19.0 + '@types/node': 20.17.6 form-data: 4.0.1 '@types/supertest@6.0.2': @@ -24687,7 +24714,7 @@ snapshots: '@types/tedious@4.0.14': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.17.6 '@types/three@0.169.0': dependencies: @@ -24700,7 +24727,7 @@ snapshots: '@types/through@0.0.33': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.17.6 '@types/trusted-types@2.0.7': optional: true @@ -24715,7 +24742,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 22.19.0 + '@types/node': 20.17.6 '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.30.0)(typescript@5.3.3))(eslint@8.30.0)(typescript@5.3.3)': dependencies: @@ -31406,7 +31433,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 22.19.0 + '@types/node': 20.17.6 long: 5.2.3 protobufjs@7.5.4: @@ -31421,7 +31448,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 22.19.0 + '@types/node': 20.17.6 long: 5.2.3 proxy-addr@2.0.7: @@ -33406,7 +33433,7 @@ snapshots: picocolors: 1.1.1 postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.4.47)(tsx@4.21.0)(yaml@2.8.0) resolve-from: 5.0.0 - rollup: 4.50.1 + rollup: 4.57.1 source-map: 0.8.0-beta.0 sucrase: 3.35.0 tinyexec: 0.3.2 @@ -33434,7 +33461,7 @@ snapshots: picocolors: 1.1.1 postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.2)(tsx@4.21.0)(yaml@2.8.0) resolve-from: 5.0.0 - rollup: 4.50.1 + rollup: 4.57.1 source-map: 0.8.0-beta.0 sucrase: 3.35.0 tinyexec: 0.3.2 @@ -33462,7 +33489,7 @@ snapshots: picocolors: 1.1.1 postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.15.5)(yaml@2.4.5) resolve-from: 5.0.0 - rollup: 4.50.1 + rollup: 4.57.1 source-map: 0.8.0-beta.0 sucrase: 3.35.0 tinyexec: 0.3.2 @@ -33490,7 +33517,7 @@ snapshots: picocolors: 1.1.1 postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.0) resolve-from: 5.0.0 - rollup: 4.50.1 + rollup: 4.57.1 source-map: 0.8.0-beta.0 sucrase: 3.35.0 tinyexec: 0.3.2 From fa2a57651b97f43358b6e35a57806d4d63d607da Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Wed, 25 Feb 2026 18:01:17 -0800 Subject: [PATCH 02/13] more tests and catch exec errors --- packages/stack-cli/src/commands/exec.ts | 15 +++++++++++++-- packages/stack-cli/src/lib/auth.ts | 2 +- packages/stack-cli/src/lib/config.ts | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/stack-cli/src/commands/exec.ts b/packages/stack-cli/src/commands/exec.ts index 268166c88f..c1db95f997 100644 --- a/packages/stack-cli/src/commands/exec.ts +++ b/packages/stack-cli/src/commands/exec.ts @@ -1,6 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../lib/auth.js"; import { getAdminProject } from "../lib/app.js"; +import { CliError } from "../lib/errors.js"; export function registerExecCommand(program: Command) { program @@ -13,8 +14,18 @@ export function registerExecCommand(program: Command) { // eslint-disable-next-line @typescript-eslint/no-implied-eval const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor; - const fn = new AsyncFunction("app", javascript); - const result = await fn(project.app); + let fn; + try { + fn = new AsyncFunction("app", javascript); + } catch (err: any) { + throw new CliError(`Syntax error in exec code: ${err.message}`); + } + let result; + try { + result = await fn(project.app); + } catch (err: any) { + throw new CliError(`Exec error: ${err.message}`); + } if (result !== undefined) { console.log(JSON.stringify(result, null, 2)); diff --git a/packages/stack-cli/src/lib/auth.ts b/packages/stack-cli/src/lib/auth.ts index d9112c86d2..8f84b7945b 100644 --- a/packages/stack-cli/src/lib/auth.ts +++ b/packages/stack-cli/src/lib/auth.ts @@ -3,7 +3,7 @@ import { AuthError } from "./errors.js"; export const DEFAULT_API_URL = "https://api.stack-auth.com"; export const DEFAULT_DASHBOARD_URL = "https://app.stack-auth.com"; -export const DEFAULT_PUBLISHABLE_CLIENT_KEY = "pck_6ypt981excjnk24dmgx5703my25k2f3y2z3qjhbykz3q0"; +export const DEFAULT_PUBLISHABLE_CLIENT_KEY = process.env.STACK_CLI_PUBLISHABLE_CLIENT_KEY ?? "pck_6ypt981excjnk24dmgx5703my25k2f3y2z3qjhbykz3q0"; type Flags = { projectId?: string, diff --git a/packages/stack-cli/src/lib/config.ts b/packages/stack-cli/src/lib/config.ts index 9deee2e7e3..a3e3ebd5ff 100644 --- a/packages/stack-cli/src/lib/config.ts +++ b/packages/stack-cli/src/lib/config.ts @@ -2,7 +2,7 @@ import * as fs from "fs"; import * as path from "path"; import * as os from "os"; -const CONFIG_PATH = path.join(os.homedir(), ".stackrc"); +const CONFIG_PATH = process.env.STACK_CLI_CONFIG_PATH ?? path.join(os.homedir(), ".stackrc"); type ConfigKey = "STACK_CLI_REFRESH_TOKEN" | "STACK_API_URL" | "STACK_DASHBOARD_URL"; From b63f6dc5d203f8a5eb2f415413b7b9605e7bb9e6 Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Wed, 25 Feb 2026 18:01:24 -0800 Subject: [PATCH 03/13] test file --- apps/e2e/tests/general/cli.test.ts | 308 +++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 apps/e2e/tests/general/cli.test.ts diff --git a/apps/e2e/tests/general/cli.test.ts b/apps/e2e/tests/general/cli.test.ts new file mode 100644 index 0000000000..018cba64f7 --- /dev/null +++ b/apps/e2e/tests/general/cli.test.ts @@ -0,0 +1,308 @@ +import { execFile } from "child_process"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { StackAdminApp } from "@stackframe/js"; +import { Result } from "@stackframe/stack-shared/dist/utils/results"; +import { describe, beforeAll, afterAll } from "vitest"; +import { it, niceFetch, STACK_BACKEND_BASE_URL, STACK_INTERNAL_PROJECT_CLIENT_KEY, STACK_INTERNAL_PROJECT_SERVER_KEY, STACK_INTERNAL_PROJECT_ADMIN_KEY } from "../helpers"; + +const CLI_BIN = path.resolve("packages/stack-cli/dist/index.js"); + +function runCli( + args: string[], + envOverrides?: Record, +): Promise<{ stdout: string, stderr: string, exitCode: number | null }> { + return new Promise((resolve) => { + execFile("node", [CLI_BIN, ...args], { + env: { ...baseEnv, ...envOverrides }, + timeout: 30_000, + }, (error, stdout, stderr) => { + resolve({ + stdout: stdout.toString(), + stderr: stderr.toString(), + exitCode: error ? 1 : 0, + }); + }); + }); +} + +let baseEnv: Record; +let tmpDir: string; +let configFilePath: string; +let refreshToken: string; + +describe("Stack CLI", () => { + beforeAll(async () => { + // Check CLI is built + if (!fs.existsSync(CLI_BIN)) { + throw new Error("CLI not built. Run `pnpm --filter @stackframe/stack-cli run build` first."); + } + + // Create temp dir for config file + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "stack-cli-test-")); + configFilePath = path.join(tmpDir, ".stackrc"); + + // Create test user on internal project (auto-creates team) + const internalApp = new StackAdminApp({ + projectId: "internal", + baseUrl: STACK_BACKEND_BASE_URL, + publishableClientKey: STACK_INTERNAL_PROJECT_CLIENT_KEY, + secretServerKey: STACK_INTERNAL_PROJECT_SERVER_KEY, + superSecretAdminKey: STACK_INTERNAL_PROJECT_ADMIN_KEY, + tokenStore: "memory", + }); + + const fakeEmail = `cli-test-${crypto.randomUUID()}@stack-generated.example.com`; + Result.orThrow(await internalApp.signUpWithCredential({ + email: fakeEmail, + password: "test-password-123", + verificationCallbackUrl: "http://localhost:3000", + })); + + const user = await internalApp.getUser({ or: "throw" }); + + // Create a session to get a refresh token + const sessionRes = await niceFetch(`${STACK_BACKEND_BASE_URL}/api/v1/auth/sessions`, { + method: "POST", + headers: { + "content-type": "application/json", + "x-stack-access-type": "server", + "x-stack-project-id": "internal", + "x-stack-publishable-client-key": STACK_INTERNAL_PROJECT_CLIENT_KEY, + "x-stack-secret-server-key": STACK_INTERNAL_PROJECT_SERVER_KEY, + }, + body: JSON.stringify({ + user_id: user.id, + expires_in_millis: 1000 * 60 * 60 * 24, + is_impersonation: false, + }), + }); + + if (sessionRes.status !== 200) { + throw new Error(`Failed to create session: ${sessionRes.status} ${JSON.stringify(sessionRes.body)}`); + } + refreshToken = sessionRes.body.refresh_token; + + // Set base env for CLI + baseEnv = { + PATH: process.env.PATH ?? "", + HOME: process.env.HOME ?? "", + STACK_API_URL: STACK_BACKEND_BASE_URL, + STACK_CLI_REFRESH_TOKEN: refreshToken, + STACK_CLI_PUBLISHABLE_CLIENT_KEY: STACK_INTERNAL_PROJECT_CLIENT_KEY, + STACK_CLI_CONFIG_PATH: configFilePath, + CI: "1", + }; + }, 120_000); + + afterAll(() => { + if (tmpDir && fs.existsSync(tmpDir)) { + fs.rmSync(tmpDir, { recursive: true }); + } + }); + + it("shows help output", async ({ expect }) => { + const { stdout, exitCode } = await runCli(["--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Stack Auth CLI"); + }); + + it("shows version output", async ({ expect }) => { + const pkg = JSON.parse(fs.readFileSync(path.resolve("packages/stack-cli/package.json"), "utf-8")); + const { stdout, exitCode } = await runCli(["--version"]); + expect(exitCode).toBe(0); + expect(stdout.trim()).toBe(pkg.version); + }); + + it("shows update info", async ({ expect }) => { + const { stdout, exitCode } = await runCli(["update"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("stack-cli version:"); + }); + + it("errors when not logged in", async ({ expect }) => { + const { stderr, exitCode } = await runCli(["project", "list"], { + STACK_CLI_REFRESH_TOKEN: "", + }); + expect(exitCode).not.toBe(0); + expect(stderr).toContain("Not logged in"); + }); + + it("errors when no project ID given", async ({ expect }) => { + const { stderr, exitCode } = await runCli(["exec", "return 1"]); + expect(exitCode).not.toBe(0); + expect(stderr).toContain("No project ID"); + }); + + it("logout clears config", async ({ expect }) => { + // Write a fake token to the config file + fs.writeFileSync(configFilePath, "STACK_CLI_REFRESH_TOKEN=fake-token\n", { mode: 0o600 }); + + const { stdout, exitCode } = await runCli(["logout"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Logged out"); + + const content = fs.readFileSync(configFilePath, "utf-8"); + expect(content).not.toContain("fake-token"); + }); + + let createdProjectId: string; + + it("lists projects as empty JSON array", async ({ expect }) => { + const { stdout, exitCode } = await runCli(["--json", "project", "list"]); + expect(exitCode).toBe(0); + const projects = JSON.parse(stdout); + expect(Array.isArray(projects)).toBe(true); + }); + + it("creates a project", async ({ expect }) => { + const { stdout, exitCode } = await runCli(["--json", "project", "create", "--display-name", "CLI Test"]); + expect(exitCode).toBe(0); + const project = JSON.parse(stdout); + expect(project).toHaveProperty("id"); + expect(project).toHaveProperty("displayName"); + expect(project.displayName).toBe("CLI Test"); + createdProjectId = project.id; + }); + + it("lists projects including created one", async ({ expect }) => { + const { stdout, exitCode } = await runCli(["--json", "project", "list"]); + expect(exitCode).toBe(0); + const projects = JSON.parse(stdout); + const found = projects.find((p: any) => p.id === createdProjectId); + expect(found).toBeDefined(); + expect(found.displayName).toBe("CLI Test"); + }); + + it("returns basic expression", async ({ expect }) => { + const { stdout, exitCode } = await runCli( + ["exec", "return 1+1"], + { STACK_PROJECT_ID: createdProjectId }, + ); + expect(exitCode).toBe(0); + expect(stdout.trim()).toBe("2"); + }); + + it("has app object available", async ({ expect }) => { + const { stdout, exitCode } = await runCli( + ["exec", "return typeof app"], + { STACK_PROJECT_ID: createdProjectId }, + ); + expect(exitCode).toBe(0); + expect(stdout.trim()).toBe('"object"'); + }); + + it("reports syntax error", async ({ expect }) => { + const { stderr, exitCode } = await runCli( + ["exec", "return @@invalid"], + { STACK_PROJECT_ID: createdProjectId }, + ); + expect(exitCode).not.toBe(0); + expect(stderr).toContain("Syntax error"); + }); + + it("reports runtime error", async ({ expect }) => { + const { stderr, exitCode } = await runCli( + ["exec", "throw new Error('boom')"], + { STACK_PROJECT_ID: createdProjectId }, + ); + expect(exitCode).not.toBe(0); + expect(stderr).toContain("boom"); + }); + + it("reports undefined variable", async ({ expect }) => { + const { stderr, exitCode } = await runCli( + ["exec", "return nonExistentVar"], + { STACK_PROJECT_ID: createdProjectId }, + ); + expect(exitCode).not.toBe(0); + expect(stderr).toContain("nonExistentVar"); + }); + + it("returns undefined for no return value", async ({ expect }) => { + const { stdout, exitCode } = await runCli( + ["exec", "const x = 1"], + { STACK_PROJECT_ID: createdProjectId }, + ); + expect(exitCode).toBe(0); + expect(stdout.trim()).toBe(""); + }); + + it("returns complex object as JSON", async ({ expect }) => { + const { stdout, exitCode } = await runCli( + ["exec", "return {a: 1, b: [2, 3]}"], + { STACK_PROJECT_ID: createdProjectId }, + ); + expect(exitCode).toBe(0); + const parsed = JSON.parse(stdout); + expect(parsed).toEqual({ a: 1, b: [2, 3] }); + }); + + it("supports async code", async ({ expect }) => { + const { stdout, exitCode } = await runCli( + ["exec", "return await Promise.resolve(42)"], + { STACK_PROJECT_ID: createdProjectId }, + ); + expect(exitCode).toBe(0); + expect(stdout.trim()).toBe("42"); + }); + + let createdUserEmail: string; + + it("can create user with app", async ({ expect }) => { + createdUserEmail = `exec-test-${crypto.randomUUID()}@stack-generated.example.com`; + const code = `const u = await app.createUser({ primaryEmail: "${createdUserEmail}", password: "test123456" }); return { id: u.id, email: u.primaryEmail }`; + const { stdout, exitCode } = await runCli( + ["exec", code], + { STACK_PROJECT_ID: createdProjectId }, + ); + expect(exitCode).toBe(0); + const parsed = JSON.parse(stdout); + expect(parsed).toHaveProperty("id"); + expect(parsed.email).toBe(createdUserEmail); + }); + + it("can list users with app", async ({ expect }) => { + const { stdout, exitCode } = await runCli( + ["exec", "const users = await app.listUsers(); return users.length"], + { STACK_PROJECT_ID: createdProjectId }, + ); + expect(exitCode).toBe(0); + const count = JSON.parse(stdout); + expect(count).toBeGreaterThanOrEqual(1); + }); + + let configTsPath: string; + + it("config pull writes a .ts file", async ({ expect }) => { + configTsPath = path.join(tmpDir, "config.ts"); + const { stdout, exitCode } = await runCli( + ["config", "pull", "--config-file", configTsPath], + { STACK_PROJECT_ID: createdProjectId }, + ); + expect(exitCode).toBe(0); + expect(stdout).toContain("Config written to"); + const content = fs.readFileSync(configTsPath, "utf-8"); + expect(content).toContain("export const config"); + }); + + it("config push succeeds", async ({ expect }) => { + const { stdout, exitCode } = await runCli( + ["config", "push", "--config-file", configTsPath], + { STACK_PROJECT_ID: createdProjectId }, + ); + expect(exitCode).toBe(0); + expect(stdout).toContain("Config pushed successfully"); + }); + + it("config pull rejects bad extension", async ({ expect }) => { + const badPath = path.join(tmpDir, "config.json"); + const { stderr, exitCode } = await runCli( + ["config", "pull", "--config-file", badPath], + { STACK_PROJECT_ID: createdProjectId }, + ); + expect(exitCode).not.toBe(0); + expect(stderr).toContain(".js or .ts"); + }); +}); From 184ceac1f551c93b5a4be613a6201f33671783df Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Thu, 26 Feb 2026 09:37:27 -0800 Subject: [PATCH 04/13] improve cli exec, better tests --- apps/e2e/tests/general/cli.test.ts | 75 ++++++++++++++++--- packages/stack-cli/package.json | 1 + .../stack-cli/src/commands/config-file.ts | 18 +++-- packages/stack-cli/src/commands/exec.ts | 48 ++++++++++-- packages/stack-cli/tsconfig.json | 1 + 5 files changed, 118 insertions(+), 25 deletions(-) diff --git a/apps/e2e/tests/general/cli.test.ts b/apps/e2e/tests/general/cli.test.ts index 018cba64f7..273c033c2d 100644 --- a/apps/e2e/tests/general/cli.test.ts +++ b/apps/e2e/tests/general/cli.test.ts @@ -125,13 +125,13 @@ describe("Stack CLI", () => { const { stderr, exitCode } = await runCli(["project", "list"], { STACK_CLI_REFRESH_TOKEN: "", }); - expect(exitCode).not.toBe(0); + expect(exitCode).toBe(1); expect(stderr).toContain("Not logged in"); }); it("errors when no project ID given", async ({ expect }) => { const { stderr, exitCode } = await runCli(["exec", "return 1"]); - expect(exitCode).not.toBe(0); + expect(exitCode).toBe(1); expect(stderr).toContain("No project ID"); }); @@ -184,21 +184,43 @@ describe("Stack CLI", () => { expect(stdout.trim()).toBe("2"); }); - it("has app object available", async ({ expect }) => { + it("has stackServerApp object available", async ({ expect }) => { const { stdout, exitCode } = await runCli( - ["exec", "return typeof app"], + ["exec", "return typeof stackServerApp"], { STACK_PROJECT_ID: createdProjectId }, ); expect(exitCode).toBe(0); expect(stdout.trim()).toBe('"object"'); }); + it("lists available exec API methods", async ({ expect }) => { + const { stdout, exitCode } = await runCli(["exec", "--list-api"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("StackServerApp methods"); + expect(stdout).toContain("StackClientApp methods"); + expect(stdout).toContain("createUser("); + expect(stdout).toContain("signInWithCredential("); + expect(stdout).not.toContain("createInternalApiKey("); + }); + + it("errors when combining --list-api and javascript", async ({ expect }) => { + const { stderr, exitCode } = await runCli(["exec", "--list-api", "return 1"]); + expect(exitCode).toBe(1); + expect(stderr).toContain("Cannot pass JavaScript when using --list-api"); + }); + + it("errors when no javascript is provided", async ({ expect }) => { + const { stderr, exitCode } = await runCli(["exec"], { STACK_PROJECT_ID: createdProjectId }); + expect(exitCode).toBe(1); + expect(stderr).toContain("Missing JavaScript argument"); + }); + it("reports syntax error", async ({ expect }) => { const { stderr, exitCode } = await runCli( ["exec", "return @@invalid"], { STACK_PROJECT_ID: createdProjectId }, ); - expect(exitCode).not.toBe(0); + expect(exitCode).toBe(1); expect(stderr).toContain("Syntax error"); }); @@ -207,16 +229,34 @@ describe("Stack CLI", () => { ["exec", "throw new Error('boom')"], { STACK_PROJECT_ID: createdProjectId }, ); - expect(exitCode).not.toBe(0); + expect(exitCode).toBe(1); expect(stderr).toContain("boom"); }); + it("reports string runtime error", async ({ expect }) => { + const { stderr, exitCode } = await runCli( + ["exec", "throw 'boom-string'"], + { STACK_PROJECT_ID: createdProjectId }, + ); + expect(exitCode).toBe(1); + expect(stderr).toContain("boom-string"); + }); + + it("reports object runtime error", async ({ expect }) => { + const { stderr, exitCode } = await runCli( + ["exec", "throw { code: 123 }"], + { STACK_PROJECT_ID: createdProjectId }, + ); + expect(exitCode).toBe(1); + expect(stderr).toContain('{"code":123}'); + }); + it("reports undefined variable", async ({ expect }) => { const { stderr, exitCode } = await runCli( ["exec", "return nonExistentVar"], { STACK_PROJECT_ID: createdProjectId }, ); - expect(exitCode).not.toBe(0); + expect(exitCode).toBe(1); expect(stderr).toContain("nonExistentVar"); }); @@ -250,9 +290,9 @@ describe("Stack CLI", () => { let createdUserEmail: string; - it("can create user with app", async ({ expect }) => { + it("can create user with stackServerApp", async ({ expect }) => { createdUserEmail = `exec-test-${crypto.randomUUID()}@stack-generated.example.com`; - const code = `const u = await app.createUser({ primaryEmail: "${createdUserEmail}", password: "test123456" }); return { id: u.id, email: u.primaryEmail }`; + const code = `const u = await stackServerApp.createUser({ primaryEmail: "${createdUserEmail}", password: "test123456" }); return { id: u.id, email: u.primaryEmail }`; const { stdout, exitCode } = await runCli( ["exec", code], { STACK_PROJECT_ID: createdProjectId }, @@ -263,9 +303,9 @@ describe("Stack CLI", () => { expect(parsed.email).toBe(createdUserEmail); }); - it("can list users with app", async ({ expect }) => { + it("can list users with stackServerApp", async ({ expect }) => { const { stdout, exitCode } = await runCli( - ["exec", "const users = await app.listUsers(); return users.length"], + ["exec", "const users = await stackServerApp.listUsers(); return users.length"], { STACK_PROJECT_ID: createdProjectId }, ); expect(exitCode).toBe(0); @@ -302,7 +342,18 @@ describe("Stack CLI", () => { ["config", "pull", "--config-file", badPath], { STACK_PROJECT_ID: createdProjectId }, ); - expect(exitCode).not.toBe(0); + expect(exitCode).toBe(1); expect(stderr).toContain(".js or .ts"); }); + + it("config push rejects array config export", async ({ expect }) => { + const badConfigPath = path.join(tmpDir, "config-array.ts"); + fs.writeFileSync(badConfigPath, "export const config = [];\n"); + const { stderr, exitCode } = await runCli( + ["config", "push", "--config-file", badConfigPath], + { STACK_PROJECT_ID: createdProjectId }, + ); + expect(exitCode).toBe(1); + expect(stderr).toContain("plain `config` object"); + }); }); diff --git a/packages/stack-cli/package.json b/packages/stack-cli/package.json index 9ad4da307e..9b2624e09c 100644 --- a/packages/stack-cli/package.json +++ b/packages/stack-cli/package.json @@ -12,6 +12,7 @@ "clean": "rimraf node_modules && rimraf dist", "build": "tsup", "dev": "tsup --watch", + "generate:exec-api-metadata": "pnpx --package=tsx tsx ./scripts/generate-exec-api-metadata.ts", "lint": "eslint --ext .tsx,.ts .", "typecheck": "tsc --noEmit" }, diff --git a/packages/stack-cli/src/commands/config-file.ts b/packages/stack-cli/src/commands/config-file.ts index 99a2e06bb5..03636ce0bc 100644 --- a/packages/stack-cli/src/commands/config-file.ts +++ b/packages/stack-cli/src/commands/config-file.ts @@ -5,6 +5,14 @@ import { resolveAuth } from "../lib/auth.js"; import { getAdminProject } from "../lib/app.js"; import { CliError } from "../lib/errors.js"; +function isPlainObject(value: unknown): value is Record { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + return false; + } + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} + export function registerConfigCommand(program: Command) { const config = program .command("config") @@ -52,11 +60,11 @@ export function registerConfigCommand(program: Command) { throw new CliError(`Config file not found: ${filePath}`); } - let configModule: Record; + let configModule: { config?: unknown }; if (ext === ".ts") { const { createJiti } = await import("jiti"); const jiti = createJiti(import.meta.url); - configModule = await jiti.import(filePath) as Record; + configModule = await jiti.import(filePath); } else if (ext === ".js") { configModule = await import(filePath); } else { @@ -64,11 +72,11 @@ export function registerConfigCommand(program: Command) { } const config = configModule.config; - if (!config || typeof config !== "object") { - throw new CliError("Config file must export a `config` object. Example: export const config = { ... };"); + if (!isPlainObject(config)) { + throw new CliError("Config file must export a plain `config` object. Example: export const config = { ... };"); } - await project.replaceConfigOverride("branch", config as Record); + await project.replaceConfigOverride("branch", config); console.log("Config pushed successfully."); }); } diff --git a/packages/stack-cli/src/commands/exec.ts b/packages/stack-cli/src/commands/exec.ts index c1db95f997..ba73f33503 100644 --- a/packages/stack-cli/src/commands/exec.ts +++ b/packages/stack-cli/src/commands/exec.ts @@ -2,12 +2,44 @@ import { Command } from "commander"; import { resolveAuth } from "../lib/auth.js"; import { getAdminProject } from "../lib/app.js"; import { CliError } from "../lib/errors.js"; +import { formatExecApiHelp } from "../lib/exec-api.js"; + +function getErrorMessage(err: unknown): string { + if (err instanceof Error) { + return err.message; + } + if (typeof err === "string") { + return err; + } + try { + return JSON.stringify(err); + } catch { + return String(err); + } +} export function registerExecCommand(program: Command) { program - .command("exec ") - .description("Execute JavaScript with a pre-configured StackServerApp as `app`") - .action(async (javascript: string) => { + .command("exec [javascript]") + .description("Execute JavaScript with a pre-configured StackServerApp as `stackServerApp`") + .option("--list-api", "List callable methods available on the `stackServerApp` object") + .action(async (javascript: string | undefined, options: { listApi?: boolean }) => { + const { listApi } = options; + if (listApi === true && javascript !== undefined) { + throw new CliError("Cannot pass JavaScript when using --list-api."); + } + if (listApi === true) { + try { + console.log(formatExecApiHelp()); + return; + } catch (err: unknown) { + throw new CliError(`Failed to load exec API metadata. Run \`pnpm --filter @stackframe/stack-cli run generate:exec-api-metadata\` and rebuild the CLI. Root cause: ${getErrorMessage(err)}`); + } + } + if (javascript === undefined) { + throw new CliError("Missing JavaScript argument. Use `stack exec \"\"` or `stack exec --list-api`."); + } + const flags = program.opts(); const auth = resolveAuth(flags); const project = await getAdminProject(auth); @@ -16,15 +48,15 @@ export function registerExecCommand(program: Command) { const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor; let fn; try { - fn = new AsyncFunction("app", javascript); - } catch (err: any) { - throw new CliError(`Syntax error in exec code: ${err.message}`); + fn = new AsyncFunction("stackServerApp", javascript); + } catch (err: unknown) { + throw new CliError(`Syntax error in exec code: ${getErrorMessage(err)}`); } let result; try { result = await fn(project.app); - } catch (err: any) { - throw new CliError(`Exec error: ${err.message}`); + } catch (err: unknown) { + throw new CliError(`Exec error: ${getErrorMessage(err)}`); } if (result !== undefined) { diff --git a/packages/stack-cli/tsconfig.json b/packages/stack-cli/tsconfig.json index 5237fd312d..bfc4edf998 100644 --- a/packages/stack-cli/tsconfig.json +++ b/packages/stack-cli/tsconfig.json @@ -7,6 +7,7 @@ "lib": ["ES2021", "ES2022.Error"], "module": "ES2020", "moduleResolution": "Bundler", + "resolveJsonModule": true, "esModuleInterop": true, "noErrorTruncation": true, "skipLibCheck": true, From 02773d860bb4ffd3b7d3a7930992ae394253231d Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Thu, 26 Feb 2026 09:37:36 -0800 Subject: [PATCH 05/13] exec api metadata --- .../scripts/generate-exec-api-metadata.ts | 145 +++++++ .../src/generated/exec-api-metadata.json | 381 ++++++++++++++++++ packages/stack-cli/src/lib/exec-api.ts | 39 ++ 3 files changed, 565 insertions(+) create mode 100644 packages/stack-cli/scripts/generate-exec-api-metadata.ts create mode 100644 packages/stack-cli/src/generated/exec-api-metadata.json create mode 100644 packages/stack-cli/src/lib/exec-api.ts diff --git a/packages/stack-cli/scripts/generate-exec-api-metadata.ts b/packages/stack-cli/scripts/generate-exec-api-metadata.ts new file mode 100644 index 0000000000..c87b11a7b8 --- /dev/null +++ b/packages/stack-cli/scripts/generate-exec-api-metadata.ts @@ -0,0 +1,145 @@ +#!/usr/bin/env node + +import * as fs from "fs"; +import * as path from "path"; +import ts from "typescript"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const stackCliRoot = path.resolve(__dirname, ".."); +const jsDtsPath = path.resolve(stackCliRoot, "../js/dist/index.d.ts"); +const outputPath = path.resolve(stackCliRoot, "src/generated/exec-api-metadata.json"); + +const MAX_TYPE_LENGTH = 120; + +type MethodMetadata = { + name: string, + signatures: string[], +}; + +function simplifyTypeText(text: string): string { + const withoutImportPaths = text.replace(/import\("[^"]+"\)\./g, ""); + const compact = withoutImportPaths.replace(/\s+/g, " ").trim(); + if (compact.length <= MAX_TYPE_LENGTH) { + return compact; + } + return `${compact.slice(0, MAX_TYPE_LENGTH - 3)}...`; +} + +function getTypeAliasDeclaration(sourceFile: ts.SourceFile, name: string): ts.TypeAliasDeclaration { + for (const statement of sourceFile.statements) { + if (ts.isTypeAliasDeclaration(statement) && statement.name.text === name) { + return statement; + } + } + throw new Error(`Could not find type alias ${name} in ${sourceFile.fileName}`); +} + +function formatSignature( + checker: ts.TypeChecker, + context: ts.Node, + methodName: string, + signature: ts.Signature, +): string { + const parameterParts = signature.getParameters().map((parameterSymbol) => { + const declaration = parameterSymbol.valueDeclaration; + const type = checker.getTypeOfSymbolAtLocation(parameterSymbol, declaration ?? context); + const typeText = simplifyTypeText( + checker.typeToString(type, declaration ?? context, ts.TypeFormatFlags.NoTruncation), + ); + const isOptional = ts.isParameter(declaration) && declaration.questionToken != null; + const isRest = ts.isParameter(declaration) && declaration.dotDotDotToken != null; + const rawName = parameterSymbol.getName(); + const parameterName = rawName === "__namedParameters" ? "options" : rawName; + return `${isRest ? "..." : ""}${parameterName}${isOptional ? "?" : ""}: ${typeText}`; + }); + + const returnTypeText = simplifyTypeText( + checker.typeToString(signature.getReturnType(), context, ts.TypeFormatFlags.NoTruncation), + ); + + return `${methodName}(${parameterParts.join(", ")}): ${returnTypeText}`; +} + +function collectMethods( + checker: ts.TypeChecker, + sourceFile: ts.SourceFile, + typeAliasName: string, +): MethodMetadata[] { + const declaration = getTypeAliasDeclaration(sourceFile, typeAliasName); + const symbol = checker.getSymbolAtLocation(declaration.name); + if (symbol == null) { + throw new Error(`Could not resolve symbol for ${typeAliasName}`); + } + const type = checker.getDeclaredTypeOfSymbol(symbol); + const methods: MethodMetadata[] = []; + + for (const property of checker.getPropertiesOfType(type)) { + const propertyName = property.getName(); + if (propertyName.startsWith("__@")) { + continue; + } + + const propertyType = checker.getTypeOfSymbolAtLocation(property, declaration.name); + const callSignatures = propertyType.getCallSignatures(); + if (callSignatures.length === 0) { + continue; + } + + const signatures = Array.from(new Set( + callSignatures.map((signature) => formatSignature(checker, declaration.name, propertyName, signature)), + )); + methods.push({ + name: propertyName, + signatures, + }); + } + + methods.sort((a, b) => a.name.localeCompare(b.name)); + return methods; +} + +if (!fs.existsSync(jsDtsPath)) { + throw new Error(`Could not find SDK declarations at ${jsDtsPath}. Build @stackframe/js first.`); +} + +const program = ts.createProgram([jsDtsPath], { + target: ts.ScriptTarget.ES2021, + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.Bundler, + skipLibCheck: true, + strict: true, +}); + +const sourceFile = program.getSourceFile(jsDtsPath); +if (sourceFile == null) { + throw new Error(`Could not load source file ${jsDtsPath}`); +} + +const checker = program.getTypeChecker(); +const stackClientApp = collectMethods(checker, sourceFile, "StackClientApp"); +const clientSignaturesByMethod = new Map(stackClientApp.map((method) => [method.name, new Set(method.signatures)])); +const stackServerApp = collectMethods(checker, sourceFile, "StackServerApp") + .map((method) => { + const clientSignatures = clientSignaturesByMethod.get(method.name); + if (clientSignatures == null) { + return method; + } + const uniqueSignatures = method.signatures.filter((signature) => !clientSignatures.has(signature)); + return { + name: method.name, + signatures: uniqueSignatures, + }; + }) + .filter((method) => method.signatures.length > 0); + +const output = { + schemaVersion: 1, + stackClientApp, + stackServerApp, +}; + +fs.mkdirSync(path.dirname(outputPath), { recursive: true }); +fs.writeFileSync(outputPath, `${JSON.stringify(output, null, 2)}\n`); +console.log(`Wrote ${outputPath}`); diff --git a/packages/stack-cli/src/generated/exec-api-metadata.json b/packages/stack-cli/src/generated/exec-api-metadata.json new file mode 100644 index 0000000000..06caf61e51 --- /dev/null +++ b/packages/stack-cli/src/generated/exec-api-metadata.json @@ -0,0 +1,381 @@ +{ + "schemaVersion": 1, + "stackClientApp": [ + { + "name": "acceptTeamInvitation", + "signatures": [ + "acceptTeamInvitation(code: string): Promise" + ] + }, + { + "name": "cancelSubscription", + "signatures": [ + "cancelSubscription(options: { productId: string; subscriptionId?: string | undefined; } | { productId: string; subscriptionId?: string | undefine...): Promise" + ] + }, + { + "name": "getAccessToken", + "signatures": [ + "getAccessToken(options?: ({} & (HasTokenStore extends false ? { tokenStore: \"cookie\" | \"nextjs-cookie\" | \"memory\" | RequestLike | { accessToke...): Promise" + ] + }, + { + "name": "getAuthHeaders", + "signatures": [ + "getAuthHeaders(options?: ({} & (HasTokenStore extends false ? { tokenStore: \"cookie\" | \"nextjs-cookie\" | \"memory\" | RequestLike | { accessToke...): Promise<{ \"x-stack-auth\": string; }>" + ] + }, + { + "name": "getAuthJson", + "signatures": [ + "getAuthJson(options?: ({} & (HasTokenStore extends false ? { tokenStore: \"cookie\" | \"nextjs-cookie\" | \"memory\" | RequestLike | { accessToke...): Promise<{ accessToken: string | null; refreshToken: string | null; }>" + ] + }, + { + "name": "getConvexClientAuth", + "signatures": [ + "getConvexClientAuth(options: HasTokenStore extends false ? { tokenStore: TokenStoreInit; } : { tokenStore?: \"cookie\" | \"nextjs-cookie\" | \"memory\" ...): (args: { forceRefreshToken: boolean; }) => Promise" + ] + }, + { + "name": "getConvexHttpClientAuth", + "signatures": [ + "getConvexHttpClientAuth(options: { tokenStore: TokenStoreInit; }): Promise" + ] + }, + { + "name": "getItem", + "signatures": [ + "getItem(...args: [{ itemId: string; userId: string; } | { itemId: string; teamId: string; } | { itemId: string; customCustomerId: stri...): Promise" + ] + }, + { + "name": "getPartialUser", + "signatures": [ + "getPartialUser(options: { or?: \"return-null\" | \"anonymous\" | undefined; tokenStore?: \"cookie\" | \"nextjs-cookie\" | \"memory\" | RequestLike | { ...): Promise", + "getPartialUser(options: GetCurrentPartialUserOptions): Promise" + ] + }, + { + "name": "getProject", + "signatures": [ + "getProject(...args: []): Promise" + ] + }, + { + "name": "getRefreshToken", + "signatures": [ + "getRefreshToken(options?: ({} & (HasTokenStore extends false ? { tokenStore: \"cookie\" | \"nextjs-cookie\" | \"memory\" | RequestLike | { accessToke...): Promise" + ] + }, + { + "name": "getTeamInvitationDetails", + "signatures": [ + "getTeamInvitationDetails(code: string): Promise>", + "getUser(options?: GetCurrentUserOptions | undefined): Promise | null>" + ] + }, + { + "name": "listInvoices", + "signatures": [ + "listInvoices(...args: [options: CustomerInvoicesRequestOptions]): Promise" + ] + }, + { + "name": "listProducts", + "signatures": [ + "listProducts(...args: [options: CustomerProductsRequestOptions]): Promise" + ] + }, + { + "name": "promptCliLogin", + "signatures": [ + "promptCliLogin(options: { appUrl: string; expiresInMillis?: number | undefined; }): Promise" + ] + }, + { + "name": "redirectToAfterSignIn", + "signatures": [ + "redirectToAfterSignIn(options?: RedirectToOptions | undefined): Promise" + ] + }, + { + "name": "redirectToAfterSignOut", + "signatures": [ + "redirectToAfterSignOut(options?: RedirectToOptions | undefined): Promise" + ] + }, + { + "name": "redirectToAfterSignUp", + "signatures": [ + "redirectToAfterSignUp(options?: RedirectToOptions | undefined): Promise" + ] + }, + { + "name": "redirectToEmailVerification", + "signatures": [ + "redirectToEmailVerification(options?: RedirectToOptions | undefined): Promise" + ] + }, + { + "name": "redirectToError", + "signatures": [ + "redirectToError(options?: RedirectToOptions | undefined): Promise" + ] + }, + { + "name": "redirectToForgotPassword", + "signatures": [ + "redirectToForgotPassword(options?: RedirectToOptions | undefined): Promise" + ] + }, + { + "name": "redirectToHome", + "signatures": [ + "redirectToHome(options?: RedirectToOptions | undefined): Promise" + ] + }, + { + "name": "redirectToMagicLinkCallback", + "signatures": [ + "redirectToMagicLinkCallback(options?: RedirectToOptions | undefined): Promise" + ] + }, + { + "name": "redirectToMfa", + "signatures": [ + "redirectToMfa(options?: RedirectToOptions | undefined): Promise" + ] + }, + { + "name": "redirectToOAuthCallback", + "signatures": [ + "redirectToOAuthCallback(): Promise" + ] + }, + { + "name": "redirectToOnboarding", + "signatures": [ + "redirectToOnboarding(options?: RedirectToOptions | undefined): Promise" + ] + }, + { + "name": "redirectToPasswordReset", + "signatures": [ + "redirectToPasswordReset(options?: RedirectToOptions | undefined): Promise" + ] + }, + { + "name": "redirectToSignIn", + "signatures": [ + "redirectToSignIn(options?: RedirectToOptions | undefined): Promise" + ] + }, + { + "name": "redirectToSignOut", + "signatures": [ + "redirectToSignOut(options?: RedirectToOptions | undefined): Promise" + ] + }, + { + "name": "redirectToSignUp", + "signatures": [ + "redirectToSignUp(options?: RedirectToOptions | undefined): Promise" + ] + }, + { + "name": "redirectToTeamInvitation", + "signatures": [ + "redirectToTeamInvitation(options?: RedirectToOptions | undefined): Promise" + ] + }, + { + "name": "resetPassword", + "signatures": [ + "resetPassword(options: { code: string; password: string; }): Promise" + ] + }, + { + "name": "signInWithPasskey", + "signatures": [ + "signInWithPasskey(): Promise", + "signOut(options?: { redirectUrl?: string | URL | undefined; } | undefined): Promise" + ] + }, + { + "name": "signUpWithCredential", + "signatures": [ + "signUpWithCredential(options: { email: string; password: string; noRedirect?: boolean | undefined; } & ({ noVerificationCallback: true; } | { noVer...): Promise" + ] + }, + { + "name": "createUser", + "signatures": [ + "createUser(options: ServerUserCreateOptions): Promise" + ] + }, + { + "name": "getDataVaultStore", + "signatures": [ + "getDataVaultStore(...args: [id: string]): Promise" + ] + }, + { + "name": "getEmailDeliveryStats", + "signatures": [ + "getEmailDeliveryStats(): Promise" + ] + }, + { + "name": "getItem", + "signatures": [ + "getItem(...args: [{ itemId: string; userId: string; } | { itemId: string; teamId: string; } | { itemId: string; customCustomerId: stri...): Promise" + ] + }, + { + "name": "getPartialUser", + "signatures": [ + "getPartialUser(options: GetCurrentPartialUserOptions): Promise" + ] + }, + { + "name": "getServerUser", + "signatures": [ + "getServerUser(): Promise | null>" + ] + }, + { + "name": "getTeam", + "signatures": [ + "getTeam(id: string): Promise", + "getTeam(options: { apiKey: string; }): Promise" + ] + }, + { + "name": "getUser", + "signatures": [ + "getUser(options: { or?: \"redirect\" | \"throw\" | \"return-null\" | \"anonymous\" | \"anonymous-if-exists[deprecated]\" | undefined; includeRes...): Promise>", + "getUser(options?: GetCurrentUserOptions | undefined): Promise | null>", + "getUser(id: string): Promise", + "getUser(options: { apiKey: string; or?: \"return-null\" | \"anonymous\" | undefined; }): Promise", + "getUser(options: { from: \"convex\"; ctx: GenericQueryCtx; or?: \"return-null\" | \"anonymous\" | undefined; }): Promise" + ] + }, + { + "name": "grantProduct", + "signatures": [ + "grantProduct(options: ({ userId: string; } | { teamId: string; } | { customCustomerId: string; }) & ({ productId: string; } | { product: In...): Promise" + ] + }, + { + "name": "listTeams", + "signatures": [ + "listTeams(...args: []): Promise" + ] + }, + { + "name": "listUsers", + "signatures": [ + "listUsers(options?: ServerListUsersOptions | undefined): Promise" + ] + }, + { + "name": "sendEmail", + "signatures": [ + "sendEmail(options: SendEmailOptions): Promise" + ] + } + ] +} diff --git a/packages/stack-cli/src/lib/exec-api.ts b/packages/stack-cli/src/lib/exec-api.ts new file mode 100644 index 0000000000..0251ca3656 --- /dev/null +++ b/packages/stack-cli/src/lib/exec-api.ts @@ -0,0 +1,39 @@ +import metadata from "../generated/exec-api-metadata.json"; + +type MethodEntry = { + name: string, + signatures: string[], +}; + +export type ExecApiMetadata = { + schemaVersion: 1, + stackClientApp: MethodEntry[], + stackServerApp: MethodEntry[], +}; + +function section(title: string, methods: MethodEntry[]): string[] { + const lines = [title]; + if (methods.length === 0) { + lines.push(" (none)"); + return lines; + } + for (const method of methods) { + for (const signature of method.signatures) { + lines.push(` - ${signature}`); + } + } + return lines; +} + +export function formatExecApiHelp(): string { + if (metadata.schemaVersion !== 1) { + throw new Error("Unsupported exec API metadata schema version"); + } + const lines: string[] = []; + lines.push("Available methods on `app`:"); + lines.push(""); + lines.push(...section("StackServerApp methods", metadata.stackServerApp)); + lines.push(""); + lines.push(...section("StackClientApp methods", metadata.stackClientApp)); + return lines.join("\n"); +} From 8f1709b2aa311c77b85f4028214101624140eca6 Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Thu, 26 Feb 2026 12:41:14 -0800 Subject: [PATCH 06/13] small quality fixes --- apps/e2e/tests/general/cli.test.ts | 7 +- .../stack-cli/src/commands/config-file.ts | 8 +- packages/stack-cli/src/commands/project.ts | 2 +- packages/stack-cli/src/lib/config.ts | 84 +++++++++++-------- 4 files changed, 59 insertions(+), 42 deletions(-) diff --git a/apps/e2e/tests/general/cli.test.ts b/apps/e2e/tests/general/cli.test.ts index 273c033c2d..a45a5483a5 100644 --- a/apps/e2e/tests/general/cli.test.ts +++ b/apps/e2e/tests/general/cli.test.ts @@ -21,7 +21,7 @@ function runCli( resolve({ stdout: stdout.toString(), stderr: stderr.toString(), - exitCode: error ? 1 : 0, + exitCode: error ? (error as any).code ?? 1 : 0, }); }); }); @@ -167,6 +167,7 @@ describe("Stack CLI", () => { }); it("lists projects including created one", async ({ expect }) => { + expect(createdProjectId).toBeDefined(); const { stdout, exitCode } = await runCli(["--json", "project", "list"]); expect(exitCode).toBe(0); const projects = JSON.parse(stdout); @@ -176,6 +177,7 @@ describe("Stack CLI", () => { }); it("returns basic expression", async ({ expect }) => { + expect(createdProjectId).toBeDefined(); const { stdout, exitCode } = await runCli( ["exec", "return 1+1"], { STACK_PROJECT_ID: createdProjectId }, @@ -304,6 +306,8 @@ describe("Stack CLI", () => { }); it("can list users with stackServerApp", async ({ expect }) => { + expect(createdProjectId).toBeDefined(); + expect(createdUserEmail).toBeDefined(); const { stdout, exitCode } = await runCli( ["exec", "const users = await stackServerApp.listUsers(); return users.length"], { STACK_PROJECT_ID: createdProjectId }, @@ -328,6 +332,7 @@ describe("Stack CLI", () => { }); it("config push succeeds", async ({ expect }) => { + expect(configTsPath).toBeDefined(); const { stdout, exitCode } = await runCli( ["config", "push", "--config-file", configTsPath], { STACK_PROJECT_ID: createdProjectId }, diff --git a/packages/stack-cli/src/commands/config-file.ts b/packages/stack-cli/src/commands/config-file.ts index 03636ce0bc..8632a512b7 100644 --- a/packages/stack-cli/src/commands/config-file.ts +++ b/packages/stack-cli/src/commands/config-file.ts @@ -56,6 +56,10 @@ export function registerConfigCommand(program: Command) { const filePath = path.resolve(opts.configFile); const ext = path.extname(filePath); + if (ext !== ".js" && ext !== ".ts") { + throw new CliError("Config file must have a .js or .ts extension."); + } + if (!fs.existsSync(filePath)) { throw new CliError(`Config file not found: ${filePath}`); } @@ -65,10 +69,8 @@ export function registerConfigCommand(program: Command) { const { createJiti } = await import("jiti"); const jiti = createJiti(import.meta.url); configModule = await jiti.import(filePath); - } else if (ext === ".js") { - configModule = await import(filePath); } else { - throw new CliError("Config file must have a .js or .ts extension."); + configModule = await import(filePath); } const config = configModule.config; diff --git a/packages/stack-cli/src/commands/project.ts b/packages/stack-cli/src/commands/project.ts index 7ecb8f91b9..16dbfc4e62 100644 --- a/packages/stack-cli/src/commands/project.ts +++ b/packages/stack-cli/src/commands/project.ts @@ -76,7 +76,7 @@ export function registerProjectCommand(program: Command) { }); if (program.opts().json) { - console.log(JSON.stringify({ id: newProject.id, displayName: newProject.displayName })); + console.log(JSON.stringify({ id: newProject.id, displayName: newProject.displayName }, null, 2)); } else { console.log(`Project created: ${newProject.id} (${newProject.displayName})`); } diff --git a/packages/stack-cli/src/lib/config.ts b/packages/stack-cli/src/lib/config.ts index a3e3ebd5ff..461e41f3b9 100644 --- a/packages/stack-cli/src/lib/config.ts +++ b/packages/stack-cli/src/lib/config.ts @@ -6,9 +6,17 @@ const CONFIG_PATH = process.env.STACK_CLI_CONFIG_PATH ?? path.join(os.homedir(), type ConfigKey = "STACK_CLI_REFRESH_TOKEN" | "STACK_API_URL" | "STACK_DASHBOARD_URL"; -function parseConfig(content: string): Record { - const result: Record = {}; - for (const line of content.split("\n")) { +function readConfigFileRaw(): string[] { + try { + return fs.readFileSync(CONFIG_PATH, "utf-8").split("\n"); + } catch { + return []; + } +} + +export function readConfigValue(key: ConfigKey): string | undefined { + const lines = readConfigFileRaw(); + for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) { continue; @@ -17,45 +25,47 @@ function parseConfig(content: string): Record { if (eqIndex === -1) { continue; } - const key = trimmed.slice(0, eqIndex).trim(); - const value = trimmed.slice(eqIndex + 1).trim(); - result[key] = value; - } - return result; -} - -function serializeConfig(config: Record): string { - return Object.entries(config) - .map(([key, value]) => `${key}=${value}`) - .join("\n") + "\n"; -} - -function readConfigFile(): Record { - try { - const content = fs.readFileSync(CONFIG_PATH, "utf-8"); - return parseConfig(content); - } catch { - return {}; + if (trimmed.slice(0, eqIndex).trim() === key) { + return trimmed.slice(eqIndex + 1).trim(); + } } -} - -function writeConfigFile(config: Record): void { - fs.writeFileSync(CONFIG_PATH, serializeConfig(config), { mode: 0o600 }); -} - -export function readConfigValue(key: ConfigKey): string | undefined { - const config = readConfigFile(); - return config[key]; + return undefined; } export function writeConfigValue(key: ConfigKey, value: string): void { - const config = readConfigFile(); - config[key] = value; - writeConfigFile(config); + const lines = readConfigFileRaw(); + const newLine = `${key}=${value}`; + let found = false; + const result = lines.map((line) => { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) { + return line; + } + const eqIndex = trimmed.indexOf("="); + if (eqIndex !== -1 && trimmed.slice(0, eqIndex).trim() === key) { + found = true; + return newLine; + } + return line; + }); + if (!found) { + result.push(newLine); + } + fs.writeFileSync(CONFIG_PATH, result.join("\n"), { mode: 0o600 }); } export function removeConfigValue(key: ConfigKey): void { - const config = readConfigFile(); - delete config[key]; - writeConfigFile(config); + const lines = readConfigFileRaw(); + const result = lines.filter((line) => { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) { + return true; + } + const eqIndex = trimmed.indexOf("="); + if (eqIndex !== -1 && trimmed.slice(0, eqIndex).trim() === key) { + return false; + } + return true; + }); + fs.writeFileSync(CONFIG_PATH, result.join("\n"), { mode: 0o600 }); } From 9c552fb64d04260b234aee6c91fbd3838e46ff7d Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Thu, 26 Feb 2026 15:14:43 -0800 Subject: [PATCH 07/13] lint fix --- packages/stack-cli/src/lib/config.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/stack-cli/src/lib/config.ts b/packages/stack-cli/src/lib/config.ts index 461e41f3b9..9c3a46032b 100644 --- a/packages/stack-cli/src/lib/config.ts +++ b/packages/stack-cli/src/lib/config.ts @@ -36,18 +36,21 @@ export function writeConfigValue(key: ConfigKey, value: string): void { const lines = readConfigFileRaw(); const newLine = `${key}=${value}`; let found = false; - const result = lines.map((line) => { + const result: string[] = []; + for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) { - return line; + result.push(line); + continue; } const eqIndex = trimmed.indexOf("="); if (eqIndex !== -1 && trimmed.slice(0, eqIndex).trim() === key) { found = true; - return newLine; + result.push(newLine); + } else { + result.push(line); } - return line; - }); + } if (!found) { result.push(newLine); } From 1e4bbe70f4c9db1b24536a51502f637906a3dc18 Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Thu, 26 Feb 2026 16:46:57 -0800 Subject: [PATCH 08/13] fix comamnds --- apps/e2e/tests/general/cli.test.ts | 6 ------ packages/stack-cli/src/commands/update.ts | 13 ------------- packages/stack-cli/src/index.ts | 4 ---- packages/stack-cli/src/lib/auth.ts | 16 ++++++---------- 4 files changed, 6 insertions(+), 33 deletions(-) delete mode 100644 packages/stack-cli/src/commands/update.ts diff --git a/apps/e2e/tests/general/cli.test.ts b/apps/e2e/tests/general/cli.test.ts index a45a5483a5..1a37258d38 100644 --- a/apps/e2e/tests/general/cli.test.ts +++ b/apps/e2e/tests/general/cli.test.ts @@ -115,12 +115,6 @@ describe("Stack CLI", () => { expect(stdout.trim()).toBe(pkg.version); }); - it("shows update info", async ({ expect }) => { - const { stdout, exitCode } = await runCli(["update"]); - expect(exitCode).toBe(0); - expect(stdout).toContain("stack-cli version:"); - }); - it("errors when not logged in", async ({ expect }) => { const { stderr, exitCode } = await runCli(["project", "list"], { STACK_CLI_REFRESH_TOKEN: "", diff --git a/packages/stack-cli/src/commands/update.ts b/packages/stack-cli/src/commands/update.ts deleted file mode 100644 index 8f9ca38157..0000000000 --- a/packages/stack-cli/src/commands/update.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Command } from "commander"; - -export function registerUpdateCommand(program: Command) { - program - .command("update") - .description("Show version information") - .action(() => { - const version = program.version(); - console.log(`stack-cli version: ${version}`); - console.log("\nWhen using npx @stackframe/stack-cli, you always get the latest version."); - console.log("No manual update is needed."); - }); -} diff --git a/packages/stack-cli/src/index.ts b/packages/stack-cli/src/index.ts index 4df1426da3..08af5837ed 100644 --- a/packages/stack-cli/src/index.ts +++ b/packages/stack-cli/src/index.ts @@ -9,7 +9,6 @@ import { registerExecCommand } from "./commands/exec.js"; import { registerConfigCommand } from "./commands/config-file.js"; import { registerInitCommand } from "./commands/init.js"; import { registerProjectCommand } from "./commands/project.js"; -import { registerUpdateCommand } from "./commands/update.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -22,8 +21,6 @@ program .description("Stack Auth CLI") .version(pkg.version) .option("--project-id ", "Project ID") - .option("--api-url ", "Stack Auth API URL") - .option("--dashboard-url ", "Stack Auth Dashboard URL") .option("--json", "Output in JSON format"); registerLoginCommand(program); @@ -32,7 +29,6 @@ registerExecCommand(program); registerConfigCommand(program); registerInitCommand(program); registerProjectCommand(program); -registerUpdateCommand(program); async function main() { try { diff --git a/packages/stack-cli/src/lib/auth.ts b/packages/stack-cli/src/lib/auth.ts index 8f84b7945b..7e593acb7b 100644 --- a/packages/stack-cli/src/lib/auth.ts +++ b/packages/stack-cli/src/lib/auth.ts @@ -7,8 +7,6 @@ export const DEFAULT_PUBLISHABLE_CLIENT_KEY = process.env.STACK_CLI_PUBLISHABLE_ type Flags = { projectId?: string, - apiUrl?: string, - dashboardUrl?: string, }; export type LoginConfig = { @@ -24,16 +22,14 @@ export type ProjectAuth = SessionAuth & { projectId: string, }; -function resolveApiUrl(flags: Flags): string { - return flags.apiUrl - ?? process.env.STACK_API_URL +function resolveApiUrl(): string { + return process.env.STACK_API_URL ?? readConfigValue("STACK_API_URL") ?? DEFAULT_API_URL; } -function resolveDashboardUrl(flags: Flags): string { - return flags.dashboardUrl - ?? process.env.STACK_DASHBOARD_URL +function resolveDashboardUrl(): string { + return process.env.STACK_DASHBOARD_URL ?? readConfigValue("STACK_DASHBOARD_URL") ?? DEFAULT_DASHBOARD_URL; } @@ -57,8 +53,8 @@ function resolveProjectId(flags: Flags): string { export function resolveLoginConfig(flags: Flags): LoginConfig { return { - apiUrl: resolveApiUrl(flags), - dashboardUrl: resolveDashboardUrl(flags), + apiUrl: resolveApiUrl(), + dashboardUrl: resolveDashboardUrl(), }; } From 1fd438c2165a67f05d8a5201f404e2023e93e97a Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Thu, 26 Feb 2026 17:52:21 -0800 Subject: [PATCH 09/13] remove extra client app fields --- packages/stack-cli/src/lib/exec-api.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/stack-cli/src/lib/exec-api.ts b/packages/stack-cli/src/lib/exec-api.ts index 0251ca3656..3c8a48cee3 100644 --- a/packages/stack-cli/src/lib/exec-api.ts +++ b/packages/stack-cli/src/lib/exec-api.ts @@ -33,7 +33,5 @@ export function formatExecApiHelp(): string { lines.push("Available methods on `app`:"); lines.push(""); lines.push(...section("StackServerApp methods", metadata.stackServerApp)); - lines.push(""); - lines.push(...section("StackClientApp methods", metadata.stackClientApp)); return lines.join("\n"); } From 17ff4eddba23e2048a3428b04466e92c239e2f2a Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Thu, 26 Feb 2026 17:54:08 -0800 Subject: [PATCH 10/13] pnpm lock --- pnpm-lock.yaml | 236 +++++++++++++++++++------------------------------ 1 file changed, 93 insertions(+), 143 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35032ce338..aac5320abc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1860,7 +1860,7 @@ importers: version: 6.1.2 tsup: specifier: ^8.4.0 - version: 8.4.0(@swc/core@1.3.101(@swc/helpers@0.5.15))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.3.3)(yaml@2.8.0) + version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.3.3)(yaml@2.8.0) typescript: specifier: 5.3.3 version: 5.3.3 @@ -9569,6 +9569,12 @@ packages: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -12244,6 +12250,10 @@ packages: jose@6.1.3: resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + js-beautify@1.15.4: resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} engines: {node: '>=14'} @@ -12510,6 +12520,10 @@ packages: resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} engines: {node: '>=4'} + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + loader-runner@4.3.1: resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} engines: {node: '>=6.11.5'} @@ -12546,6 +12560,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} @@ -14777,6 +14794,11 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions + space-separated-tokens@1.1.5: resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} @@ -15179,6 +15201,9 @@ packages: tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -15221,6 +15246,9 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + tr46@5.0.0: resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} engines: {node: '>=18'} @@ -15302,6 +15330,25 @@ packages: resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} engines: {node: '>=0.6.x'} + tsup@8.4.0: + resolution: {integrity: sha512-b+eZbPCjz10fRryaAA7C8xlIHnf8VnsaRqydheLIqwG/Mcpfk8Z5zp3HayX7GaTygkigHl5cBUs+IhcySiIexQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + tsx@4.15.5: resolution: {integrity: sha512-iKi8jQ2VBmZ2kU/FkGkL2OSHBHsazsUzsdC/W/RwhKIEsIoZ1alCclZHP5jGfNHEaEWUJFM1GquzCf+4db3b0w==} engines: {node: '>=18.0.0'} @@ -15912,6 +15959,9 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -15952,6 +16002,9 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} @@ -25695,6 +25748,11 @@ snapshots: dependencies: run-applescript: 7.0.0 + bundle-require@5.1.0(esbuild@0.25.11): + dependencies: + esbuild: 0.25.11 + load-tsconfig: 0.2.5 + busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -29207,6 +29265,8 @@ snapshots: jose@6.1.3: {} + joycon@3.1.1: {} + js-beautify@1.15.4: dependencies: config-chain: 1.1.13 @@ -29473,6 +29533,8 @@ snapshots: pify: 3.0.0 strip-bom: 3.0.0 + load-tsconfig@0.2.5: {} + loader-runner@4.3.1: {} local-pkg@0.5.0: @@ -29507,6 +29569,8 @@ snapshots: lodash.merge@4.6.2: {} + lodash.sortby@4.7.0: {} + lodash.startcase@4.4.0: {} lodash.throttle@4.1.1: {} @@ -31098,6 +31162,15 @@ snapshots: tsx: 4.21.0 yaml: 2.8.0 + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.0): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 2.6.1 + postcss: 8.5.6 + tsx: 4.21.0 + yaml: 2.8.0 + postcss-nested@6.0.1(postcss@8.5.3): dependencies: postcss: 8.5.3 @@ -32586,6 +32659,10 @@ snapshots: source-map@0.7.6: {} + source-map@0.8.0-beta.0: + dependencies: + whatwg-url: 7.1.0 + space-separated-tokens@1.1.5: {} space-separated-tokens@2.0.2: {} @@ -33151,6 +33228,8 @@ snapshots: tinycolor2@1.6.0: {} + tinyexec@0.3.2: {} + tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -33187,6 +33266,10 @@ snapshots: tr46@0.0.3: {} + tr46@1.0.1: + dependencies: + punycode: 2.3.1 + tr46@5.0.0: dependencies: punycode: 2.3.1 @@ -33287,147 +33370,7 @@ snapshots: tsscmp@1.0.6: {} - tsup@8.3.5(@swc/core@1.3.101(@swc/helpers@0.5.15))(jiti@2.6.1)(postcss@8.4.47)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.0): - dependencies: - bundle-require: 5.0.0(esbuild@0.24.2) - cac: 6.7.14 - chokidar: 4.0.1 - consola: 3.2.3 - debug: 4.3.7 - esbuild: 0.24.2 - joycon: 3.1.1 - picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.4.47)(tsx@4.21.0)(yaml@2.8.0) - resolve-from: 5.0.0 - rollup: 4.24.4 - source-map: 0.8.0-beta.0 - sucrase: 3.35.0 - tinyexec: 0.3.1 - tinyglobby: 0.2.10 - tree-kill: 1.2.2 - optionalDependencies: - '@swc/core': 1.3.101(@swc/helpers@0.5.15) - postcss: 8.4.47 - typescript: 5.8.3 - transitivePeerDependencies: - - jiti - - supports-color - - tsx - - yaml - - tsup@8.3.5(@swc/core@1.3.101(@swc/helpers@0.5.15))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.3.3)(yaml@2.6.0): - dependencies: - bundle-require: 5.0.0(esbuild@0.24.2) - cac: 6.7.14 - chokidar: 4.0.1 - consola: 3.2.3 - debug: 4.3.7 - esbuild: 0.24.2 - joycon: 3.1.1 - picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(yaml@2.6.0) - resolve-from: 5.0.0 - rollup: 4.24.4 - source-map: 0.8.0-beta.0 - sucrase: 3.35.0 - tinyexec: 0.3.1 - tinyglobby: 0.2.10 - tree-kill: 1.2.2 - optionalDependencies: - '@swc/core': 1.3.101(@swc/helpers@0.5.15) - postcss: 8.5.6 - typescript: 5.3.3 - transitivePeerDependencies: - - jiti - - supports-color - - tsx - - yaml - - tsup@8.4.0(@swc/core@1.3.101(@swc/helpers@0.5.15))(jiti@2.6.1)(postcss@8.4.47)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.0): - dependencies: - bundle-require: 5.1.0(esbuild@0.25.11) - cac: 6.7.14 - chokidar: 4.0.3 - consola: 3.4.2 - debug: 4.4.3 - esbuild: 0.25.11 - joycon: 3.1.1 - picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.4.47)(tsx@4.21.0)(yaml@2.8.0) - resolve-from: 5.0.0 - rollup: 4.57.1 - source-map: 0.8.0-beta.0 - sucrase: 3.35.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tree-kill: 1.2.2 - optionalDependencies: - '@swc/core': 1.3.101(@swc/helpers@0.5.15) - postcss: 8.4.47 - typescript: 5.8.3 - transitivePeerDependencies: - - jiti - - supports-color - - tsx - - yaml - - tsup@8.4.0(@swc/core@1.3.101(@swc/helpers@0.5.15))(jiti@2.6.1)(postcss@8.5.2)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.0): - dependencies: - bundle-require: 5.1.0(esbuild@0.25.11) - cac: 6.7.14 - chokidar: 4.0.3 - consola: 3.4.2 - debug: 4.4.3 - esbuild: 0.25.11 - joycon: 3.1.1 - picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.2)(tsx@4.21.0)(yaml@2.8.0) - resolve-from: 5.0.0 - rollup: 4.57.1 - source-map: 0.8.0-beta.0 - sucrase: 3.35.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tree-kill: 1.2.2 - optionalDependencies: - '@swc/core': 1.3.101(@swc/helpers@0.5.15) - postcss: 8.5.2 - typescript: 5.8.3 - transitivePeerDependencies: - - jiti - - supports-color - - tsx - - yaml - - tsup@8.4.0(@swc/core@1.3.101(@swc/helpers@0.5.15))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.15.5)(typescript@5.8.3)(yaml@2.4.5): - dependencies: - bundle-require: 5.1.0(esbuild@0.25.11) - cac: 6.7.14 - chokidar: 4.0.3 - consola: 3.4.2 - debug: 4.4.3 - esbuild: 0.25.11 - joycon: 3.1.1 - picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.15.5)(yaml@2.4.5) - resolve-from: 5.0.0 - rollup: 4.57.1 - source-map: 0.8.0-beta.0 - sucrase: 3.35.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tree-kill: 1.2.2 - optionalDependencies: - '@swc/core': 1.3.101(@swc/helpers@0.5.15) - postcss: 8.5.6 - typescript: 5.8.3 - transitivePeerDependencies: - - jiti - - supports-color - - tsx - - yaml - - tsup@8.4.0(@swc/core@1.3.101(@swc/helpers@0.5.15))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.3.3)(yaml@2.8.0): + tsup@8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.3.3)(yaml@2.8.0): dependencies: bundle-require: 5.1.0(esbuild@0.25.11) cac: 6.7.14 @@ -33446,7 +33389,6 @@ snapshots: tinyglobby: 0.2.15 tree-kill: 1.2.2 optionalDependencies: - '@swc/core': 1.3.101(@swc/helpers@0.5.15) postcss: 8.5.6 typescript: 5.3.3 transitivePeerDependencies: @@ -34097,6 +34039,8 @@ snapshots: webidl-conversions@3.0.1: {} + webidl-conversions@4.0.2: {} + webidl-conversions@7.0.0: {} webpack-sources@3.3.3: {} @@ -34152,6 +34096,12 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + whatwg-url@7.1.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + which-boxed-primitive@1.0.2: dependencies: is-bigint: 1.0.4 From ac45adaa06a83844672b0525b281dad2f9a41baa Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Fri, 27 Feb 2026 09:29:05 -0800 Subject: [PATCH 11/13] fix cli test --- apps/e2e/tests/general/cli.test.ts | 2 -- pnpm-lock.yaml | 7 +++++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/e2e/tests/general/cli.test.ts b/apps/e2e/tests/general/cli.test.ts index 1a37258d38..5c0243ff91 100644 --- a/apps/e2e/tests/general/cli.test.ts +++ b/apps/e2e/tests/general/cli.test.ts @@ -193,9 +193,7 @@ describe("Stack CLI", () => { const { stdout, exitCode } = await runCli(["exec", "--list-api"]); expect(exitCode).toBe(0); expect(stdout).toContain("StackServerApp methods"); - expect(stdout).toContain("StackClientApp methods"); expect(stdout).toContain("createUser("); - expect(stdout).toContain("signInWithCredential("); expect(stdout).not.toContain("createInternalApiKey("); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce2cf7ecad..fe9374cb1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15465,6 +15465,11 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + engines: {node: '>=14.17'} + hasBin: true + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -33329,6 +33334,8 @@ snapshots: transitivePeerDependencies: - supports-color + typescript@5.3.3: {} + typescript@5.9.3: {} ufo@1.5.4: {} From dd0db201e0454be9bc3dcdfdbcff7f29790f3333 Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Fri, 27 Feb 2026 10:23:09 -0800 Subject: [PATCH 12/13] stack cli tsdown --- packages/stack-cli/package.json | 8 +- packages/stack-cli/tsdown.config.ts | 19 ++++ packages/stack-cli/tsup.config.ts | 15 ---- pnpm-lock.yaml | 135 ++-------------------------- 4 files changed, 28 insertions(+), 149 deletions(-) create mode 100644 packages/stack-cli/tsdown.config.ts delete mode 100644 packages/stack-cli/tsup.config.ts diff --git a/packages/stack-cli/package.json b/packages/stack-cli/package.json index 9b2624e09c..307ee5853b 100644 --- a/packages/stack-cli/package.json +++ b/packages/stack-cli/package.json @@ -10,8 +10,8 @@ }, "scripts": { "clean": "rimraf node_modules && rimraf dist", - "build": "tsup", - "dev": "tsup --watch", + "build": "tsdown", + "dev": "tsdown --watch", "generate:exec-api-metadata": "pnpx --package=tsx tsx ./scripts/generate-exec-api-metadata.ts", "lint": "eslint --ext .tsx,.ts .", "typecheck": "tsc --noEmit" @@ -34,8 +34,8 @@ "devDependencies": { "@types/node": "20.17.6", "rimraf": "^6.0.1", - "tsup": "^8.4.0", - "typescript": "5.3.3" + "tsdown": "^0.20.3", + "typescript": "5.9.3" }, "packageManager": "pnpm@10.23.0" } diff --git a/packages/stack-cli/tsdown.config.ts b/packages/stack-cli/tsdown.config.ts new file mode 100644 index 0000000000..a9b8bd3f94 --- /dev/null +++ b/packages/stack-cli/tsdown.config.ts @@ -0,0 +1,19 @@ +import { defineConfig, type UserConfig } from 'tsdown'; + +const config: UserConfig = { + entry: ['src/index.ts'], + sourcemap: true, + clean: false, + dts: true, + outDir: 'dist', + format: { + esm: { + outExtensions: () => ({ js: '.js', dts: '.d.ts' }), + }, + }, + banner: { + js: '#!/usr/bin/env node', + }, +}; + +export default defineConfig(config); diff --git a/packages/stack-cli/tsup.config.ts b/packages/stack-cli/tsup.config.ts deleted file mode 100644 index 6792e69817..0000000000 --- a/packages/stack-cli/tsup.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { defineConfig, Options } from 'tsup'; - -const config: Options = { - entryPoints: ['src/index.ts'], - sourcemap: true, - clean: false, - dts: true, - outDir: 'dist', - format: ['esm'], - banner: { - js: '#!/usr/bin/env node', - }, -}; - -export default defineConfig(config); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fe9374cb1e..72b11da78e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1858,12 +1858,12 @@ importers: rimraf: specifier: ^6.0.1 version: 6.1.2 - tsup: - specifier: ^8.4.0 - version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.3.3)(yaml@2.8.0) + tsdown: + specifier: ^0.20.3 + version: 0.20.3(typescript@5.9.3) typescript: - specifier: 5.3.3 - version: 5.3.3 + specifier: 5.9.3 + version: 5.9.3 packages/stack-sc: dependencies: @@ -9569,12 +9569,6 @@ packages: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} - bundle-require@5.1.0: - resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - peerDependencies: - esbuild: '>=0.18' - busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -12250,10 +12244,6 @@ packages: jose@6.1.3: resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} - joycon@3.1.1: - resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} - engines: {node: '>=10'} - js-beautify@1.15.4: resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} engines: {node: '>=14'} @@ -12520,10 +12510,6 @@ packages: resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} engines: {node: '>=4'} - load-tsconfig@0.2.5: - resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - loader-runner@4.3.1: resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} engines: {node: '>=6.11.5'} @@ -12560,9 +12546,6 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.sortby@4.7.0: - resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} - lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} @@ -14794,11 +14777,6 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} - source-map@0.8.0-beta.0: - resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} - engines: {node: '>= 8'} - deprecated: The work that was done in this beta branch won't be included in future versions - space-separated-tokens@1.1.5: resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} @@ -15201,9 +15179,6 @@ packages: tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -15246,9 +15221,6 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tr46@1.0.1: - resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} - tr46@5.0.0: resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} engines: {node: '>=18'} @@ -15330,25 +15302,6 @@ packages: resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} engines: {node: '>=0.6.x'} - tsup@8.4.0: - resolution: {integrity: sha512-b+eZbPCjz10fRryaAA7C8xlIHnf8VnsaRqydheLIqwG/Mcpfk8Z5zp3HayX7GaTygkigHl5cBUs+IhcySiIexQ==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - '@microsoft/api-extractor': ^7.36.0 - '@swc/core': ^1 - postcss: ^8.4.12 - typescript: '>=4.5.0' - peerDependenciesMeta: - '@microsoft/api-extractor': - optional: true - '@swc/core': - optional: true - postcss: - optional: true - typescript: - optional: true - tsx@4.15.5: resolution: {integrity: sha512-iKi8jQ2VBmZ2kU/FkGkL2OSHBHsazsUzsdC/W/RwhKIEsIoZ1alCclZHP5jGfNHEaEWUJFM1GquzCf+4db3b0w==} engines: {node: '>=18.0.0'} @@ -15465,11 +15418,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - typescript@5.3.3: - resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} - engines: {node: '>=14.17'} - hasBin: true - typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -15959,9 +15907,6 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webidl-conversions@4.0.2: - resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} - webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -16002,9 +15947,6 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - whatwg-url@7.1.0: - resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} - which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} @@ -25674,11 +25616,6 @@ snapshots: dependencies: run-applescript: 7.0.0 - bundle-require@5.1.0(esbuild@0.25.11): - dependencies: - esbuild: 0.25.11 - load-tsconfig: 0.2.5 - busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -29123,8 +29060,6 @@ snapshots: jose@6.1.3: {} - joycon@3.1.1: {} - js-beautify@1.15.4: dependencies: config-chain: 1.1.13 @@ -29391,8 +29326,6 @@ snapshots: pify: 3.0.0 strip-bom: 3.0.0 - load-tsconfig@0.2.5: {} - loader-runner@4.3.1: {} local-pkg@0.5.0: @@ -29427,8 +29360,6 @@ snapshots: lodash.merge@4.6.2: {} - lodash.sortby@4.7.0: {} - lodash.startcase@4.4.0: {} lodash.throttle@4.1.1: {} @@ -31020,15 +30951,6 @@ snapshots: tsx: 4.21.0 yaml: 2.8.0 - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.0): - dependencies: - lilconfig: 3.1.3 - optionalDependencies: - jiti: 2.6.1 - postcss: 8.5.6 - tsx: 4.21.0 - yaml: 2.8.0 - postcss-nested@6.0.1(postcss@8.5.3): dependencies: postcss: 8.5.3 @@ -32500,10 +32422,6 @@ snapshots: source-map@0.7.6: {} - source-map@0.8.0-beta.0: - dependencies: - whatwg-url: 7.1.0 - space-separated-tokens@1.1.5: {} space-separated-tokens@2.0.2: {} @@ -33069,8 +32987,6 @@ snapshots: tinycolor2@1.6.0: {} - tinyexec@0.3.2: {} - tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -33107,10 +33023,6 @@ snapshots: tr46@0.0.3: {} - tr46@1.0.1: - dependencies: - punycode: 2.3.1 - tr46@5.0.0: dependencies: punycode: 2.3.1 @@ -33180,33 +33092,6 @@ snapshots: tsscmp@1.0.6: {} - tsup@8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.3.3)(yaml@2.8.0): - dependencies: - bundle-require: 5.1.0(esbuild@0.25.11) - cac: 6.7.14 - chokidar: 4.0.3 - consola: 3.4.2 - debug: 4.4.3 - esbuild: 0.25.11 - joycon: 3.1.1 - picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.0) - resolve-from: 5.0.0 - rollup: 4.57.1 - source-map: 0.8.0-beta.0 - sucrase: 3.35.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tree-kill: 1.2.2 - optionalDependencies: - postcss: 8.5.6 - typescript: 5.3.3 - transitivePeerDependencies: - - jiti - - supports-color - - tsx - - yaml - tsx@4.15.5: dependencies: esbuild: 0.21.5 @@ -33334,8 +33219,6 @@ snapshots: transitivePeerDependencies: - supports-color - typescript@5.3.3: {} - typescript@5.9.3: {} ufo@1.5.4: {} @@ -33849,8 +33732,6 @@ snapshots: webidl-conversions@3.0.1: {} - webidl-conversions@4.0.2: {} - webidl-conversions@7.0.0: {} webpack-sources@3.3.3: {} @@ -33906,12 +33787,6 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 - whatwg-url@7.1.0: - dependencies: - lodash.sortby: 4.7.0 - tr46: 1.0.1 - webidl-conversions: 4.0.2 - which-boxed-primitive@1.0.2: dependencies: is-bigint: 1.0.4 From 319f7ac7df81fc033c6f9b4f93af99bb03ff4945 Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Mon, 2 Mar 2026 16:15:23 -0800 Subject: [PATCH 13/13] stack cli fixes --- apps/e2e/tests/general/cli.test.ts | 18 +- packages/stack-cli/package.json | 1 - .../scripts/generate-exec-api-metadata.ts | 145 ------- packages/stack-cli/src/commands/exec.ts | 19 +- .../src/generated/exec-api-metadata.json | 381 ------------------ packages/stack-cli/src/lib/config.ts | 71 +--- packages/stack-cli/src/lib/exec-api.ts | 37 -- 7 files changed, 25 insertions(+), 647 deletions(-) delete mode 100644 packages/stack-cli/scripts/generate-exec-api-metadata.ts delete mode 100644 packages/stack-cli/src/generated/exec-api-metadata.json delete mode 100644 packages/stack-cli/src/lib/exec-api.ts diff --git a/apps/e2e/tests/general/cli.test.ts b/apps/e2e/tests/general/cli.test.ts index 5c0243ff91..f5e8cebf49 100644 --- a/apps/e2e/tests/general/cli.test.ts +++ b/apps/e2e/tests/general/cli.test.ts @@ -41,7 +41,7 @@ describe("Stack CLI", () => { // Create temp dir for config file tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "stack-cli-test-")); - configFilePath = path.join(tmpDir, ".stackrc"); + configFilePath = path.join(tmpDir, "credentials.json"); // Create test user on internal project (auto-creates team) const internalApp = new StackAdminApp({ @@ -131,7 +131,7 @@ describe("Stack CLI", () => { it("logout clears config", async ({ expect }) => { // Write a fake token to the config file - fs.writeFileSync(configFilePath, "STACK_CLI_REFRESH_TOKEN=fake-token\n", { mode: 0o600 }); + fs.writeFileSync(configFilePath, JSON.stringify({ STACK_CLI_REFRESH_TOKEN: "fake-token" }), { mode: 0o600 }); const { stdout, exitCode } = await runCli(["logout"]); expect(exitCode).toBe(0); @@ -189,18 +189,10 @@ describe("Stack CLI", () => { expect(stdout.trim()).toBe('"object"'); }); - it("lists available exec API methods", async ({ expect }) => { - const { stdout, exitCode } = await runCli(["exec", "--list-api"]); + it("exec help mentions docs URL", async ({ expect }) => { + const { stdout, exitCode } = await runCli(["exec", "--help"]); expect(exitCode).toBe(0); - expect(stdout).toContain("StackServerApp methods"); - expect(stdout).toContain("createUser("); - expect(stdout).not.toContain("createInternalApiKey("); - }); - - it("errors when combining --list-api and javascript", async ({ expect }) => { - const { stderr, exitCode } = await runCli(["exec", "--list-api", "return 1"]); - expect(exitCode).toBe(1); - expect(stderr).toContain("Cannot pass JavaScript when using --list-api"); + expect(stdout).toContain("https://docs.stack-auth.com/docs/sdk"); }); it("errors when no javascript is provided", async ({ expect }) => { diff --git a/packages/stack-cli/package.json b/packages/stack-cli/package.json index 307ee5853b..fd74645acb 100644 --- a/packages/stack-cli/package.json +++ b/packages/stack-cli/package.json @@ -12,7 +12,6 @@ "clean": "rimraf node_modules && rimraf dist", "build": "tsdown", "dev": "tsdown --watch", - "generate:exec-api-metadata": "pnpx --package=tsx tsx ./scripts/generate-exec-api-metadata.ts", "lint": "eslint --ext .tsx,.ts .", "typecheck": "tsc --noEmit" }, diff --git a/packages/stack-cli/scripts/generate-exec-api-metadata.ts b/packages/stack-cli/scripts/generate-exec-api-metadata.ts deleted file mode 100644 index c87b11a7b8..0000000000 --- a/packages/stack-cli/scripts/generate-exec-api-metadata.ts +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env node - -import * as fs from "fs"; -import * as path from "path"; -import ts from "typescript"; -import { fileURLToPath } from "url"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const stackCliRoot = path.resolve(__dirname, ".."); -const jsDtsPath = path.resolve(stackCliRoot, "../js/dist/index.d.ts"); -const outputPath = path.resolve(stackCliRoot, "src/generated/exec-api-metadata.json"); - -const MAX_TYPE_LENGTH = 120; - -type MethodMetadata = { - name: string, - signatures: string[], -}; - -function simplifyTypeText(text: string): string { - const withoutImportPaths = text.replace(/import\("[^"]+"\)\./g, ""); - const compact = withoutImportPaths.replace(/\s+/g, " ").trim(); - if (compact.length <= MAX_TYPE_LENGTH) { - return compact; - } - return `${compact.slice(0, MAX_TYPE_LENGTH - 3)}...`; -} - -function getTypeAliasDeclaration(sourceFile: ts.SourceFile, name: string): ts.TypeAliasDeclaration { - for (const statement of sourceFile.statements) { - if (ts.isTypeAliasDeclaration(statement) && statement.name.text === name) { - return statement; - } - } - throw new Error(`Could not find type alias ${name} in ${sourceFile.fileName}`); -} - -function formatSignature( - checker: ts.TypeChecker, - context: ts.Node, - methodName: string, - signature: ts.Signature, -): string { - const parameterParts = signature.getParameters().map((parameterSymbol) => { - const declaration = parameterSymbol.valueDeclaration; - const type = checker.getTypeOfSymbolAtLocation(parameterSymbol, declaration ?? context); - const typeText = simplifyTypeText( - checker.typeToString(type, declaration ?? context, ts.TypeFormatFlags.NoTruncation), - ); - const isOptional = ts.isParameter(declaration) && declaration.questionToken != null; - const isRest = ts.isParameter(declaration) && declaration.dotDotDotToken != null; - const rawName = parameterSymbol.getName(); - const parameterName = rawName === "__namedParameters" ? "options" : rawName; - return `${isRest ? "..." : ""}${parameterName}${isOptional ? "?" : ""}: ${typeText}`; - }); - - const returnTypeText = simplifyTypeText( - checker.typeToString(signature.getReturnType(), context, ts.TypeFormatFlags.NoTruncation), - ); - - return `${methodName}(${parameterParts.join(", ")}): ${returnTypeText}`; -} - -function collectMethods( - checker: ts.TypeChecker, - sourceFile: ts.SourceFile, - typeAliasName: string, -): MethodMetadata[] { - const declaration = getTypeAliasDeclaration(sourceFile, typeAliasName); - const symbol = checker.getSymbolAtLocation(declaration.name); - if (symbol == null) { - throw new Error(`Could not resolve symbol for ${typeAliasName}`); - } - const type = checker.getDeclaredTypeOfSymbol(symbol); - const methods: MethodMetadata[] = []; - - for (const property of checker.getPropertiesOfType(type)) { - const propertyName = property.getName(); - if (propertyName.startsWith("__@")) { - continue; - } - - const propertyType = checker.getTypeOfSymbolAtLocation(property, declaration.name); - const callSignatures = propertyType.getCallSignatures(); - if (callSignatures.length === 0) { - continue; - } - - const signatures = Array.from(new Set( - callSignatures.map((signature) => formatSignature(checker, declaration.name, propertyName, signature)), - )); - methods.push({ - name: propertyName, - signatures, - }); - } - - methods.sort((a, b) => a.name.localeCompare(b.name)); - return methods; -} - -if (!fs.existsSync(jsDtsPath)) { - throw new Error(`Could not find SDK declarations at ${jsDtsPath}. Build @stackframe/js first.`); -} - -const program = ts.createProgram([jsDtsPath], { - target: ts.ScriptTarget.ES2021, - module: ts.ModuleKind.ESNext, - moduleResolution: ts.ModuleResolutionKind.Bundler, - skipLibCheck: true, - strict: true, -}); - -const sourceFile = program.getSourceFile(jsDtsPath); -if (sourceFile == null) { - throw new Error(`Could not load source file ${jsDtsPath}`); -} - -const checker = program.getTypeChecker(); -const stackClientApp = collectMethods(checker, sourceFile, "StackClientApp"); -const clientSignaturesByMethod = new Map(stackClientApp.map((method) => [method.name, new Set(method.signatures)])); -const stackServerApp = collectMethods(checker, sourceFile, "StackServerApp") - .map((method) => { - const clientSignatures = clientSignaturesByMethod.get(method.name); - if (clientSignatures == null) { - return method; - } - const uniqueSignatures = method.signatures.filter((signature) => !clientSignatures.has(signature)); - return { - name: method.name, - signatures: uniqueSignatures, - }; - }) - .filter((method) => method.signatures.length > 0); - -const output = { - schemaVersion: 1, - stackClientApp, - stackServerApp, -}; - -fs.mkdirSync(path.dirname(outputPath), { recursive: true }); -fs.writeFileSync(outputPath, `${JSON.stringify(output, null, 2)}\n`); -console.log(`Wrote ${outputPath}`); diff --git a/packages/stack-cli/src/commands/exec.ts b/packages/stack-cli/src/commands/exec.ts index ba73f33503..44d177db2f 100644 --- a/packages/stack-cli/src/commands/exec.ts +++ b/packages/stack-cli/src/commands/exec.ts @@ -2,7 +2,6 @@ import { Command } from "commander"; import { resolveAuth } from "../lib/auth.js"; import { getAdminProject } from "../lib/app.js"; import { CliError } from "../lib/errors.js"; -import { formatExecApiHelp } from "../lib/exec-api.js"; function getErrorMessage(err: unknown): string { if (err instanceof Error) { @@ -22,22 +21,10 @@ export function registerExecCommand(program: Command) { program .command("exec [javascript]") .description("Execute JavaScript with a pre-configured StackServerApp as `stackServerApp`") - .option("--list-api", "List callable methods available on the `stackServerApp` object") - .action(async (javascript: string | undefined, options: { listApi?: boolean }) => { - const { listApi } = options; - if (listApi === true && javascript !== undefined) { - throw new CliError("Cannot pass JavaScript when using --list-api."); - } - if (listApi === true) { - try { - console.log(formatExecApiHelp()); - return; - } catch (err: unknown) { - throw new CliError(`Failed to load exec API metadata. Run \`pnpm --filter @stackframe/stack-cli run generate:exec-api-metadata\` and rebuild the CLI. Root cause: ${getErrorMessage(err)}`); - } - } + .addHelpText("after", "\nFor available API methods, see: https://docs.stack-auth.com/docs/sdk") + .action(async (javascript: string | undefined) => { if (javascript === undefined) { - throw new CliError("Missing JavaScript argument. Use `stack exec \"\"` or `stack exec --list-api`."); + throw new CliError("Missing JavaScript argument. Use `stack exec \"\"` or `stack exec --help`."); } const flags = program.opts(); diff --git a/packages/stack-cli/src/generated/exec-api-metadata.json b/packages/stack-cli/src/generated/exec-api-metadata.json deleted file mode 100644 index 06caf61e51..0000000000 --- a/packages/stack-cli/src/generated/exec-api-metadata.json +++ /dev/null @@ -1,381 +0,0 @@ -{ - "schemaVersion": 1, - "stackClientApp": [ - { - "name": "acceptTeamInvitation", - "signatures": [ - "acceptTeamInvitation(code: string): Promise" - ] - }, - { - "name": "cancelSubscription", - "signatures": [ - "cancelSubscription(options: { productId: string; subscriptionId?: string | undefined; } | { productId: string; subscriptionId?: string | undefine...): Promise" - ] - }, - { - "name": "getAccessToken", - "signatures": [ - "getAccessToken(options?: ({} & (HasTokenStore extends false ? { tokenStore: \"cookie\" | \"nextjs-cookie\" | \"memory\" | RequestLike | { accessToke...): Promise" - ] - }, - { - "name": "getAuthHeaders", - "signatures": [ - "getAuthHeaders(options?: ({} & (HasTokenStore extends false ? { tokenStore: \"cookie\" | \"nextjs-cookie\" | \"memory\" | RequestLike | { accessToke...): Promise<{ \"x-stack-auth\": string; }>" - ] - }, - { - "name": "getAuthJson", - "signatures": [ - "getAuthJson(options?: ({} & (HasTokenStore extends false ? { tokenStore: \"cookie\" | \"nextjs-cookie\" | \"memory\" | RequestLike | { accessToke...): Promise<{ accessToken: string | null; refreshToken: string | null; }>" - ] - }, - { - "name": "getConvexClientAuth", - "signatures": [ - "getConvexClientAuth(options: HasTokenStore extends false ? { tokenStore: TokenStoreInit; } : { tokenStore?: \"cookie\" | \"nextjs-cookie\" | \"memory\" ...): (args: { forceRefreshToken: boolean; }) => Promise" - ] - }, - { - "name": "getConvexHttpClientAuth", - "signatures": [ - "getConvexHttpClientAuth(options: { tokenStore: TokenStoreInit; }): Promise" - ] - }, - { - "name": "getItem", - "signatures": [ - "getItem(...args: [{ itemId: string; userId: string; } | { itemId: string; teamId: string; } | { itemId: string; customCustomerId: stri...): Promise" - ] - }, - { - "name": "getPartialUser", - "signatures": [ - "getPartialUser(options: { or?: \"return-null\" | \"anonymous\" | undefined; tokenStore?: \"cookie\" | \"nextjs-cookie\" | \"memory\" | RequestLike | { ...): Promise", - "getPartialUser(options: GetCurrentPartialUserOptions): Promise" - ] - }, - { - "name": "getProject", - "signatures": [ - "getProject(...args: []): Promise" - ] - }, - { - "name": "getRefreshToken", - "signatures": [ - "getRefreshToken(options?: ({} & (HasTokenStore extends false ? { tokenStore: \"cookie\" | \"nextjs-cookie\" | \"memory\" | RequestLike | { accessToke...): Promise" - ] - }, - { - "name": "getTeamInvitationDetails", - "signatures": [ - "getTeamInvitationDetails(code: string): Promise>", - "getUser(options?: GetCurrentUserOptions | undefined): Promise | null>" - ] - }, - { - "name": "listInvoices", - "signatures": [ - "listInvoices(...args: [options: CustomerInvoicesRequestOptions]): Promise" - ] - }, - { - "name": "listProducts", - "signatures": [ - "listProducts(...args: [options: CustomerProductsRequestOptions]): Promise" - ] - }, - { - "name": "promptCliLogin", - "signatures": [ - "promptCliLogin(options: { appUrl: string; expiresInMillis?: number | undefined; }): Promise" - ] - }, - { - "name": "redirectToAfterSignIn", - "signatures": [ - "redirectToAfterSignIn(options?: RedirectToOptions | undefined): Promise" - ] - }, - { - "name": "redirectToAfterSignOut", - "signatures": [ - "redirectToAfterSignOut(options?: RedirectToOptions | undefined): Promise" - ] - }, - { - "name": "redirectToAfterSignUp", - "signatures": [ - "redirectToAfterSignUp(options?: RedirectToOptions | undefined): Promise" - ] - }, - { - "name": "redirectToEmailVerification", - "signatures": [ - "redirectToEmailVerification(options?: RedirectToOptions | undefined): Promise" - ] - }, - { - "name": "redirectToError", - "signatures": [ - "redirectToError(options?: RedirectToOptions | undefined): Promise" - ] - }, - { - "name": "redirectToForgotPassword", - "signatures": [ - "redirectToForgotPassword(options?: RedirectToOptions | undefined): Promise" - ] - }, - { - "name": "redirectToHome", - "signatures": [ - "redirectToHome(options?: RedirectToOptions | undefined): Promise" - ] - }, - { - "name": "redirectToMagicLinkCallback", - "signatures": [ - "redirectToMagicLinkCallback(options?: RedirectToOptions | undefined): Promise" - ] - }, - { - "name": "redirectToMfa", - "signatures": [ - "redirectToMfa(options?: RedirectToOptions | undefined): Promise" - ] - }, - { - "name": "redirectToOAuthCallback", - "signatures": [ - "redirectToOAuthCallback(): Promise" - ] - }, - { - "name": "redirectToOnboarding", - "signatures": [ - "redirectToOnboarding(options?: RedirectToOptions | undefined): Promise" - ] - }, - { - "name": "redirectToPasswordReset", - "signatures": [ - "redirectToPasswordReset(options?: RedirectToOptions | undefined): Promise" - ] - }, - { - "name": "redirectToSignIn", - "signatures": [ - "redirectToSignIn(options?: RedirectToOptions | undefined): Promise" - ] - }, - { - "name": "redirectToSignOut", - "signatures": [ - "redirectToSignOut(options?: RedirectToOptions | undefined): Promise" - ] - }, - { - "name": "redirectToSignUp", - "signatures": [ - "redirectToSignUp(options?: RedirectToOptions | undefined): Promise" - ] - }, - { - "name": "redirectToTeamInvitation", - "signatures": [ - "redirectToTeamInvitation(options?: RedirectToOptions | undefined): Promise" - ] - }, - { - "name": "resetPassword", - "signatures": [ - "resetPassword(options: { code: string; password: string; }): Promise" - ] - }, - { - "name": "signInWithPasskey", - "signatures": [ - "signInWithPasskey(): Promise", - "signOut(options?: { redirectUrl?: string | URL | undefined; } | undefined): Promise" - ] - }, - { - "name": "signUpWithCredential", - "signatures": [ - "signUpWithCredential(options: { email: string; password: string; noRedirect?: boolean | undefined; } & ({ noVerificationCallback: true; } | { noVer...): Promise" - ] - }, - { - "name": "createUser", - "signatures": [ - "createUser(options: ServerUserCreateOptions): Promise" - ] - }, - { - "name": "getDataVaultStore", - "signatures": [ - "getDataVaultStore(...args: [id: string]): Promise" - ] - }, - { - "name": "getEmailDeliveryStats", - "signatures": [ - "getEmailDeliveryStats(): Promise" - ] - }, - { - "name": "getItem", - "signatures": [ - "getItem(...args: [{ itemId: string; userId: string; } | { itemId: string; teamId: string; } | { itemId: string; customCustomerId: stri...): Promise" - ] - }, - { - "name": "getPartialUser", - "signatures": [ - "getPartialUser(options: GetCurrentPartialUserOptions): Promise" - ] - }, - { - "name": "getServerUser", - "signatures": [ - "getServerUser(): Promise | null>" - ] - }, - { - "name": "getTeam", - "signatures": [ - "getTeam(id: string): Promise", - "getTeam(options: { apiKey: string; }): Promise" - ] - }, - { - "name": "getUser", - "signatures": [ - "getUser(options: { or?: \"redirect\" | \"throw\" | \"return-null\" | \"anonymous\" | \"anonymous-if-exists[deprecated]\" | undefined; includeRes...): Promise>", - "getUser(options?: GetCurrentUserOptions | undefined): Promise | null>", - "getUser(id: string): Promise", - "getUser(options: { apiKey: string; or?: \"return-null\" | \"anonymous\" | undefined; }): Promise", - "getUser(options: { from: \"convex\"; ctx: GenericQueryCtx; or?: \"return-null\" | \"anonymous\" | undefined; }): Promise" - ] - }, - { - "name": "grantProduct", - "signatures": [ - "grantProduct(options: ({ userId: string; } | { teamId: string; } | { customCustomerId: string; }) & ({ productId: string; } | { product: In...): Promise" - ] - }, - { - "name": "listTeams", - "signatures": [ - "listTeams(...args: []): Promise" - ] - }, - { - "name": "listUsers", - "signatures": [ - "listUsers(options?: ServerListUsersOptions | undefined): Promise" - ] - }, - { - "name": "sendEmail", - "signatures": [ - "sendEmail(options: SendEmailOptions): Promise" - ] - } - ] -} diff --git a/packages/stack-cli/src/lib/config.ts b/packages/stack-cli/src/lib/config.ts index 9c3a46032b..30a232d9ae 100644 --- a/packages/stack-cli/src/lib/config.ts +++ b/packages/stack-cli/src/lib/config.ts @@ -2,73 +2,36 @@ import * as fs from "fs"; import * as path from "path"; import * as os from "os"; -const CONFIG_PATH = process.env.STACK_CLI_CONFIG_PATH ?? path.join(os.homedir(), ".stackrc"); +const CONFIG_PATH = process.env.STACK_CLI_CONFIG_PATH ?? path.join(os.homedir(), ".config", "stack-auth", "credentials.json"); type ConfigKey = "STACK_CLI_REFRESH_TOKEN" | "STACK_API_URL" | "STACK_DASHBOARD_URL"; -function readConfigFileRaw(): string[] { +function readConfigJson(): Record { try { - return fs.readFileSync(CONFIG_PATH, "utf-8").split("\n"); + return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8")); } catch { - return []; + return {}; } } +function writeConfigJson(data: Record): void { + fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true }); + fs.writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 }); +} + export function readConfigValue(key: ConfigKey): string | undefined { - const lines = readConfigFileRaw(); - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) { - continue; - } - const eqIndex = trimmed.indexOf("="); - if (eqIndex === -1) { - continue; - } - if (trimmed.slice(0, eqIndex).trim() === key) { - return trimmed.slice(eqIndex + 1).trim(); - } - } - return undefined; + const config = readConfigJson(); + return config[key]; } export function writeConfigValue(key: ConfigKey, value: string): void { - const lines = readConfigFileRaw(); - const newLine = `${key}=${value}`; - let found = false; - const result: string[] = []; - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) { - result.push(line); - continue; - } - const eqIndex = trimmed.indexOf("="); - if (eqIndex !== -1 && trimmed.slice(0, eqIndex).trim() === key) { - found = true; - result.push(newLine); - } else { - result.push(line); - } - } - if (!found) { - result.push(newLine); - } - fs.writeFileSync(CONFIG_PATH, result.join("\n"), { mode: 0o600 }); + const config = readConfigJson(); + config[key] = value; + writeConfigJson(config); } export function removeConfigValue(key: ConfigKey): void { - const lines = readConfigFileRaw(); - const result = lines.filter((line) => { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) { - return true; - } - const eqIndex = trimmed.indexOf("="); - if (eqIndex !== -1 && trimmed.slice(0, eqIndex).trim() === key) { - return false; - } - return true; - }); - fs.writeFileSync(CONFIG_PATH, result.join("\n"), { mode: 0o600 }); + const config = readConfigJson(); + delete config[key]; + writeConfigJson(config); } diff --git a/packages/stack-cli/src/lib/exec-api.ts b/packages/stack-cli/src/lib/exec-api.ts deleted file mode 100644 index 3c8a48cee3..0000000000 --- a/packages/stack-cli/src/lib/exec-api.ts +++ /dev/null @@ -1,37 +0,0 @@ -import metadata from "../generated/exec-api-metadata.json"; - -type MethodEntry = { - name: string, - signatures: string[], -}; - -export type ExecApiMetadata = { - schemaVersion: 1, - stackClientApp: MethodEntry[], - stackServerApp: MethodEntry[], -}; - -function section(title: string, methods: MethodEntry[]): string[] { - const lines = [title]; - if (methods.length === 0) { - lines.push(" (none)"); - return lines; - } - for (const method of methods) { - for (const signature of method.signatures) { - lines.push(` - ${signature}`); - } - } - return lines; -} - -export function formatExecApiHelp(): string { - if (metadata.schemaVersion !== 1) { - throw new Error("Unsupported exec API metadata schema version"); - } - const lines: string[] = []; - lines.push("Available methods on `app`:"); - lines.push(""); - lines.push(...section("StackServerApp methods", metadata.stackServerApp)); - return lines.join("\n"); -}