mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
## Summary Reworks the `stack` CLI surface so the cloud-vs-local choice is **explicit at every invocation**, removing the global `--project-id` / `STACK_PROJECT_ID` env var and the local-default `exec` behavior introduced earlier in this branch. ### `stack exec` - Removes `--cloud`, `STACK_EXEC_DEFAULT_TARGET`, and the implicit local default. The CLI now requires **exactly one** of: - `--cloud-project-id <id>` — run against the Stack Auth cloud API - `--config-file <path>` — run against the local emulator project mapped to that absolute config-file path - The `--config-file` branch resolves the project id by calling the existing `GET /api/latest/internal/local-emulator/project` endpoint and matching `absolute_file_path` client-side. No new backend endpoint introduced. ### `stack config pull` / `stack config push` - Both now take `--cloud-project-id <id>` per-command instead of the global flag / `STACK_PROJECT_ID` env. - `config pull --config-file` is **optional**: when omitted, the CLI uses `./stack.config.ts` from the current directory. If neither flag nor cwd file is present, it exits with a clear hint to pass `--config-file` or `cd` into a directory containing `stack.config.ts`. ### `stack project list` - Default (no flags) lists both **cloud and local emulator** projects. Each entry carries a `target: "cloud" | "dev"` field (text format: `<id>\t<displayName>\t[<target>]`). - `--cloud` / `--dev` filter to a single source (mutually exclusive — passing both errors). - On the default code path, an unreachable local emulator emits a single stderr warning (`warning: skipping dev projects — local emulator not reachable …`) and the command still succeeds with cloud results. With `--dev` explicit, the unreachable case hard-errors. ### `stack project create` - Now requires `--cloud` to make the cloud-vs-local choice explicit. There is no local alternative today; the flag exists to surface the decision so a future local-project create doesn't silently change behavior. ### Backend - Bumps the `LIMIT` on `GET /api/latest/internal/local-emulator/project` from 20 → 100 so `project list --dev` doesn't silently truncate. ### Refactors (from earlier in this branch, unchanged here) - Local-emulator paths/ports/PCK polling live in `packages/stack-cli/src/lib/emulator-paths.ts`. - Shared local-emulator admin credentials live in `packages/stack-shared/src/local-emulator.ts`. - `resolveAuth` / `resolveLocalEmulatorAuth` take an explicit `projectId: string` (no more `Flags` parameter). - New `packages/stack-cli/src/lib/local-emulator-client.ts` encapsulates the GET-and-match flow used by both `exec --config-file` and `project list --dev`. ## Breaking changes **Scripts that relied on any of the following must be updated:** | Removed | Replacement | | --- | --- | | Global `--project-id <id>` flag | Per-command `--cloud-project-id <id>` | | `STACK_PROJECT_ID` env var | Per-command `--cloud-project-id <id>` | | `stack exec --cloud` | `stack exec --cloud-project-id <id>` | | `STACK_EXEC_DEFAULT_TARGET=cloud\|local` | `--cloud-project-id <id>` or `--config-file <path>` | | `stack exec` defaulting to local emulator | Explicit `--config-file <path>` required | | `stack project create` without a flag | `stack project create --cloud …` required | ## Test plan - [x] `pnpm lint` (stack-cli, backend, e2e) — clean - [x] `pnpm --filter @stackframe/stack-cli typecheck` — clean - [x] `pnpm --filter @stackframe/stack-cli exec vitest run` — **72/72 passing** (new unit tests: `parseExecTarget`, `resolveConfigFilePathForPull`, `resolveProjectListSources`, `formatProjectList`) - [x] `pnpm test run apps/e2e/tests/general/cli.test.ts` — **73 passing, 4 skipped, 0 failing**. New e2e cases cover: - `exec` with neither flag → errors with "Specify a target" - `exec` with both flags → errors with "not both" - `exec --config-file` with missing file / missing PCK / unreachable API - `exec --config-file` happy path against a real local-emulator backend (gated on `NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=true`) - `config pull` cwd fallback to `./stack.config.ts` - `config pull` with no `--config-file` and no cwd `stack.config.ts` → errors with `Pass --config-file …` - `project list --cloud --dev` together → errors - `project list` default with unreachable emulator → cloud results + single stderr warning - `project create` without `--cloud` → errors - All previously-`--cloud` exec cases ported to `--cloud-project-id` - [x] Manual smoke: `stack exec --help`, `stack project list --cloud --dev`, `stack project create` all emit the expected friendly errors / help text. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * CLI `exec`, `config`, and `project` commands now require explicit targeting via `--cloud-project-id` (cloud) or `--config-file` (local emulator). * `project list` now supports `--cloud` and `--dev` flags to display projects from both sources with target indicators. * Enhanced environment variable validation for emulator service ports with proper fallback handling. * **Bug Fixes** * `project list` now gracefully handles unreachable emulator with warning fallback instead of failure. * **Tests** * Expanded test coverage for project targeting, config file resolution, and emulator connectivity scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
114 lines
4.2 KiB
TypeScript
114 lines
4.2 KiB
TypeScript
import { Command } from "commander";
|
|
import { getInternalUser } from "../lib/app.js";
|
|
import { resolveLoginConfig, resolveSessionAuth } from "../lib/auth.js";
|
|
import { listLocalEmulatorProjects } from "../lib/local-emulator-client.js";
|
|
import { createProjectInteractively } from "../lib/create-project.js";
|
|
import { CliError } from "../lib/errors.js";
|
|
|
|
export type ProjectTarget = "cloud" | "dev";
|
|
|
|
export type ProjectListEntry = {
|
|
id: string,
|
|
displayName: string,
|
|
target: ProjectTarget,
|
|
};
|
|
|
|
export type ProjectListFlags = {
|
|
cloud?: boolean,
|
|
dev?: boolean,
|
|
};
|
|
|
|
// Returns which sources `project list` should query. Mutually exclusive; with
|
|
// no flags we hit both. Exported for unit tests.
|
|
export function resolveProjectListSources(opts: ProjectListFlags): { cloud: boolean, dev: boolean } {
|
|
if (opts.cloud && opts.dev) {
|
|
throw new CliError("Pass either --cloud or --dev, not both. Omit both flags to list projects from both sources.");
|
|
}
|
|
if (opts.cloud) return { cloud: true, dev: false };
|
|
if (opts.dev) return { cloud: false, dev: true };
|
|
return { cloud: true, dev: true };
|
|
}
|
|
|
|
// Render projects for the human-readable list output. Each line is
|
|
// `<id>\t<displayName>\t[cloud|dev]`. No projects → "No projects found." sentinel.
|
|
export function formatProjectList(projects: ProjectListEntry[]): string {
|
|
if (projects.length === 0) {
|
|
return "No projects found.";
|
|
}
|
|
return projects.map((p) => `${p.id}\t${p.displayName}\t[${p.target}]`).join("\n");
|
|
}
|
|
|
|
export function registerProjectCommand(program: Command) {
|
|
const project = program
|
|
.command("project")
|
|
.description("Manage projects");
|
|
|
|
project
|
|
.command("list")
|
|
.description("List your projects (defaults to both cloud and local emulator)")
|
|
.option("--cloud", "Only list cloud projects")
|
|
.option("--dev", "Only list local emulator (dev) projects")
|
|
.action(async (opts: ProjectListFlags) => {
|
|
const sources = resolveProjectListSources(opts);
|
|
const results: ProjectListEntry[] = [];
|
|
|
|
if (sources.cloud) {
|
|
const auth = resolveSessionAuth();
|
|
const user = await getInternalUser(auth);
|
|
const cloudProjects = await user.listOwnedProjects();
|
|
for (const p of cloudProjects) {
|
|
results.push({ id: p.id, displayName: p.displayName, target: "cloud" });
|
|
}
|
|
}
|
|
|
|
if (sources.dev) {
|
|
try {
|
|
const devProjects = await listLocalEmulatorProjects();
|
|
for (const p of devProjects) {
|
|
results.push({ id: p.projectId, displayName: p.displayName, target: "dev" });
|
|
}
|
|
} catch (err) {
|
|
// When the user did not explicitly request --dev, treat an unreachable
|
|
// emulator as a soft failure: warn on stderr and keep the cloud
|
|
// results. With --dev (sources.cloud === false) we surface the error.
|
|
if (!sources.cloud) {
|
|
throw err;
|
|
}
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
console.error(`warning: skipping dev projects — local emulator not reachable (${message}). Start it with \`stack emulator start\`.`);
|
|
}
|
|
}
|
|
|
|
if (program.opts().json) {
|
|
console.log(JSON.stringify(results, null, 2));
|
|
} else {
|
|
console.log(formatProjectList(results));
|
|
}
|
|
});
|
|
|
|
project
|
|
.command("create")
|
|
.description("Create a new cloud project")
|
|
.option("--cloud", "Confirm that this creates a cloud (not local emulator) project")
|
|
.option("--display-name <name>", "Project display name")
|
|
.action(async (opts) => {
|
|
if (!opts.cloud) {
|
|
throw new CliError("stack project create currently only creates cloud projects. Pass --cloud to confirm.");
|
|
}
|
|
const auth = resolveSessionAuth();
|
|
const user = await getInternalUser(auth);
|
|
const { dashboardUrl } = resolveLoginConfig();
|
|
|
|
const newProject = await createProjectInteractively(user, {
|
|
displayName: opts.displayName,
|
|
dashboardUrl,
|
|
});
|
|
|
|
if (program.opts().json) {
|
|
console.log(JSON.stringify({ id: newProject.id, displayName: newProject.displayName, target: "cloud" }, null, 2));
|
|
} else {
|
|
console.log(`Project created: ${newProject.id} (${newProject.displayName})`);
|
|
}
|
|
});
|
|
}
|