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 -->
60 lines
2.0 KiB
TypeScript
60 lines
2.0 KiB
TypeScript
import * as fs from "fs";
|
|
import * as os from "os";
|
|
import * as path from "path";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import { internalPckPath, pollInternalPck } from "./emulator-paths.js";
|
|
|
|
describe("pollInternalPck", () => {
|
|
const SAVED_HOME = process.env.STACK_EMULATOR_HOME;
|
|
let tmpHome: string;
|
|
|
|
beforeEach(() => {
|
|
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "stack-cli-poll-pck-"));
|
|
process.env.STACK_EMULATOR_HOME = tmpHome;
|
|
});
|
|
afterEach(() => {
|
|
if (SAVED_HOME === undefined) delete process.env.STACK_EMULATOR_HOME;
|
|
else process.env.STACK_EMULATOR_HOME = SAVED_HOME;
|
|
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
});
|
|
|
|
function writePck(contents: string): void {
|
|
const pckPath = internalPckPath();
|
|
fs.mkdirSync(path.dirname(pckPath), { recursive: true });
|
|
fs.writeFileSync(pckPath, contents);
|
|
}
|
|
|
|
it("returns trimmed contents when the file already exists", async () => {
|
|
writePck(" pck_existing \n");
|
|
const result = await pollInternalPck(50);
|
|
expect(result).toBe("pck_existing");
|
|
});
|
|
|
|
it("returns null when the deadline elapses with no file", async () => {
|
|
const start = Date.now();
|
|
const result = await pollInternalPck(0);
|
|
expect(result).toBeNull();
|
|
// 0ms budget should resolve almost instantly.
|
|
expect(Date.now() - start).toBeLessThan(500);
|
|
});
|
|
|
|
it("treats an empty/whitespace-only file as not-yet-ready and times out null", async () => {
|
|
writePck(" \n");
|
|
const result = await pollInternalPck(0);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("picks up the file if it appears mid-poll", async () => {
|
|
setTimeout(() => writePck("pck_appears_late"), 80);
|
|
const result = await pollInternalPck(2000);
|
|
expect(result).toBe("pck_appears_late");
|
|
});
|
|
|
|
it("propagates non-ENOENT read errors", async () => {
|
|
// Create a directory at the PCK path so readFileSync throws EISDIR.
|
|
const pckPath = internalPckPath();
|
|
fs.mkdirSync(pckPath, { recursive: true });
|
|
await expect(pollInternalPck(50)).rejects.toThrow();
|
|
});
|
|
});
|