@@ -7,6 +7,8 @@ import { Result } from "@stackframe/stack-shared/dist/utils/results";
77import { describe , beforeAll , afterAll } from "vitest" ;
88import { it , niceFetch , STACK_BACKEND_BASE_URL , STACK_INTERNAL_PROJECT_CLIENT_KEY , STACK_INTERNAL_PROJECT_SERVER_KEY , STACK_INTERNAL_PROJECT_ADMIN_KEY } from "../helpers" ;
99
10+ const isLocalEmulator = process . env . NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR === "true" ;
11+
1012const CLI_BIN = path . resolve ( "packages/stack-cli/dist/index.js" ) ;
1113const CLI_SRC_BIN = path . resolve ( "packages/stack-cli/src/index.ts" ) ;
1214
@@ -134,6 +136,9 @@ describe("Stack CLI", () => {
134136 } ) ;
135137
136138 it ( "errors when no project ID given" , async ( { expect } ) => {
139+ // Exercise the default (local) path: project-ID resolution happens before
140+ // any emulator I/O, so the missing-ID error fires regardless of whether
141+ // an emulator is running.
137142 const { stderr, exitCode } = await runCli ( [ "exec" , "return 1" ] ) ;
138143 expect ( exitCode ) . toBe ( 1 ) ;
139144 expect ( stderr ) . toContain ( "No project ID" ) ;
@@ -183,7 +188,7 @@ describe("Stack CLI", () => {
183188 it ( "returns basic expression" , async ( { expect } ) => {
184189 expect ( createdProjectId ) . toBeDefined ( ) ;
185190 const { stdout, exitCode } = await runCli (
186- [ "exec" , "return 1+1" ] ,
191+ [ "exec" , "--cloud" , " return 1+1"] ,
187192 { STACK_PROJECT_ID : createdProjectId } ,
188193 ) ;
189194 expect ( exitCode ) . toBe ( 0 ) ;
@@ -192,7 +197,7 @@ describe("Stack CLI", () => {
192197
193198 it ( "has stackServerApp object available" , async ( { expect } ) => {
194199 const { stdout, exitCode } = await runCli (
195- [ "exec" , "return typeof stackServerApp" ] ,
200+ [ "exec" , "--cloud" , " return typeof stackServerApp"] ,
196201 { STACK_PROJECT_ID : createdProjectId } ,
197202 ) ;
198203 expect ( exitCode ) . toBe ( 0 ) ;
@@ -205,15 +210,21 @@ describe("Stack CLI", () => {
205210 expect ( stdout ) . toContain ( "https://docs.stack-auth.com/docs/sdk" ) ;
206211 } ) ;
207212
213+ it ( "exec help mentions --cloud option" , async ( { expect } ) => {
214+ const { stdout, exitCode } = await runCli ( [ "exec" , "--help" ] ) ;
215+ expect ( exitCode ) . toBe ( 0 ) ;
216+ expect ( stdout ) . toContain ( "--cloud" ) ;
217+ } ) ;
218+
208219 it ( "errors when no javascript is provided" , async ( { expect } ) => {
209- const { stderr, exitCode } = await runCli ( [ "exec" ] , { STACK_PROJECT_ID : createdProjectId } ) ;
220+ const { stderr, exitCode } = await runCli ( [ "exec" , "--cloud" ] , { STACK_PROJECT_ID : createdProjectId } ) ;
210221 expect ( exitCode ) . toBe ( 1 ) ;
211222 expect ( stderr ) . toContain ( "Missing JavaScript argument" ) ;
212223 } ) ;
213224
214225 it ( "reports syntax error" , async ( { expect } ) => {
215226 const { stderr, exitCode } = await runCli (
216- [ "exec" , "return @@invalid" ] ,
227+ [ "exec" , "--cloud" , " return @@invalid"] ,
217228 { STACK_PROJECT_ID : createdProjectId } ,
218229 ) ;
219230 expect ( exitCode ) . toBe ( 1 ) ;
@@ -222,7 +233,7 @@ describe("Stack CLI", () => {
222233
223234 it ( "reports runtime error" , async ( { expect } ) => {
224235 const { stderr, exitCode } = await runCli (
225- [ "exec" , "throw new Error('boom')" ] ,
236+ [ "exec" , "--cloud" , " throw new Error('boom')"] ,
226237 { STACK_PROJECT_ID : createdProjectId } ,
227238 ) ;
228239 expect ( exitCode ) . toBe ( 1 ) ;
@@ -231,7 +242,7 @@ describe("Stack CLI", () => {
231242
232243 it ( "reports string runtime error" , async ( { expect } ) => {
233244 const { stderr, exitCode } = await runCli (
234- [ "exec" , "throw 'boom-string'" ] ,
245+ [ "exec" , "--cloud" , " throw 'boom-string'"] ,
235246 { STACK_PROJECT_ID : createdProjectId } ,
236247 ) ;
237248 expect ( exitCode ) . toBe ( 1 ) ;
@@ -240,7 +251,7 @@ describe("Stack CLI", () => {
240251
241252 it ( "reports object runtime error" , async ( { expect } ) => {
242253 const { stderr, exitCode } = await runCli (
243- [ "exec" , "throw { code: 123 }" ] ,
254+ [ "exec" , "--cloud" , " throw { code: 123 }"] ,
244255 { STACK_PROJECT_ID : createdProjectId } ,
245256 ) ;
246257 expect ( exitCode ) . toBe ( 1 ) ;
@@ -249,7 +260,7 @@ describe("Stack CLI", () => {
249260
250261 it ( "reports undefined variable" , async ( { expect } ) => {
251262 const { stderr, exitCode } = await runCli (
252- [ "exec" , "return nonExistentVar" ] ,
263+ [ "exec" , "--cloud" , " return nonExistentVar"] ,
253264 { STACK_PROJECT_ID : createdProjectId } ,
254265 ) ;
255266 expect ( exitCode ) . toBe ( 1 ) ;
@@ -258,7 +269,7 @@ describe("Stack CLI", () => {
258269
259270 it ( "returns undefined for no return value" , async ( { expect } ) => {
260271 const { stdout, exitCode } = await runCli (
261- [ "exec" , "const x = 1" ] ,
272+ [ "exec" , "--cloud" , " const x = 1"] ,
262273 { STACK_PROJECT_ID : createdProjectId } ,
263274 ) ;
264275 expect ( exitCode ) . toBe ( 0 ) ;
@@ -267,7 +278,7 @@ describe("Stack CLI", () => {
267278
268279 it ( "returns complex object as JSON" , async ( { expect } ) => {
269280 const { stdout, exitCode } = await runCli (
270- [ "exec" , "return {a: 1, b: [2, 3]}" ] ,
281+ [ "exec" , "--cloud" , " return {a: 1, b: [2, 3]}"] ,
271282 { STACK_PROJECT_ID : createdProjectId } ,
272283 ) ;
273284 expect ( exitCode ) . toBe ( 0 ) ;
@@ -277,7 +288,7 @@ describe("Stack CLI", () => {
277288
278289 it ( "supports async code" , async ( { expect } ) => {
279290 const { stdout, exitCode } = await runCli (
280- [ "exec" , "return await Promise.resolve(42)" ] ,
291+ [ "exec" , "--cloud" , " return await Promise.resolve(42)"] ,
281292 { STACK_PROJECT_ID : createdProjectId } ,
282293 ) ;
283294 expect ( exitCode ) . toBe ( 0 ) ;
@@ -290,7 +301,7 @@ describe("Stack CLI", () => {
290301 createdUserEmail = `exec-test-${ crypto . randomUUID ( ) } @stack-generated.example.com` ;
291302 const code = `const u = await stackServerApp.createUser({ primaryEmail: "${ createdUserEmail } ", password: "test123456" }); return { id: u.id, email: u.primaryEmail }` ;
292303 const { stdout, exitCode } = await runCli (
293- [ "exec" , code ] ,
304+ [ "exec" , "--cloud" , code ] ,
294305 { STACK_PROJECT_ID : createdProjectId } ,
295306 ) ;
296307 expect ( exitCode ) . toBe ( 0 ) ;
@@ -303,14 +314,89 @@ describe("Stack CLI", () => {
303314 expect ( createdProjectId ) . toBeDefined ( ) ;
304315 expect ( createdUserEmail ) . toBeDefined ( ) ;
305316 const { stdout, exitCode } = await runCli (
306- [ "exec" , "const users = await stackServerApp.listUsers(); return users.length" ] ,
317+ [ "exec" , "--cloud" , " const users = await stackServerApp.listUsers(); return users.length"] ,
307318 { STACK_PROJECT_ID : createdProjectId } ,
308319 ) ;
309320 expect ( exitCode ) . toBe ( 0 ) ;
310321 const count = JSON . parse ( stdout ) ;
311322 expect ( count ) . toBeGreaterThanOrEqual ( 1 ) ;
312323 } ) ;
313324
325+ it ( "local-default exec errors when emulator PCK file is missing" , async ( { expect } ) => {
326+ // Without --cloud, exec defaults to the local emulator. With
327+ // STACK_EMULATOR_HOME pointed at an empty dir, the PCK file lookup fires
328+ // before any network call and we get a clear error. Setting
329+ // STACK_EMULATOR_READY_TIMEOUT_MS=0 disables the boot-race polling window
330+ // so this test fails fast.
331+ const fakeEmulatorHome = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "stack-cli-fake-emulator-" ) ) ;
332+ try {
333+ const { stderr, exitCode } = await runCli (
334+ [ "exec" , "return 1" ] ,
335+ {
336+ STACK_PROJECT_ID : createdProjectId ,
337+ STACK_EMULATOR_HOME : fakeEmulatorHome ,
338+ STACK_EMULATOR_READY_TIMEOUT_MS : "0" ,
339+ } ,
340+ ) ;
341+ expect ( exitCode ) . toBe ( 1 ) ;
342+ expect ( stderr ) . toContain ( "Local emulator publishable client key not found" ) ;
343+ } finally {
344+ fs . rmSync ( fakeEmulatorHome , { recursive : true } ) ;
345+ }
346+ } ) ;
347+
348+ it ( "local-default exec errors when emulator API is unreachable" , async ( { expect } ) => {
349+ // PCK file present (so we get past the file check) but STACK_EMULATOR_API_URL
350+ // points at a port nothing is listening on — fetch fails with a clear error.
351+ // STACK_EMULATOR_READY_TIMEOUT_MS=0 keeps the retry loop from waiting.
352+ const fakeEmulatorHome = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "stack-cli-fake-emulator-" ) ) ;
353+ try {
354+ const pckDir = path . join ( fakeEmulatorHome , "run" , "vm" ) ;
355+ fs . mkdirSync ( pckDir , { recursive : true } ) ;
356+ fs . writeFileSync ( path . join ( pckDir , "internal-pck" ) , "pck_stub_for_test" ) ;
357+ const { stderr, exitCode } = await runCli (
358+ [ "exec" , "return 1" ] ,
359+ {
360+ STACK_PROJECT_ID : createdProjectId ,
361+ STACK_EMULATOR_HOME : fakeEmulatorHome ,
362+ STACK_EMULATOR_API_URL : "http://127.0.0.1:1" ,
363+ STACK_EMULATOR_READY_TIMEOUT_MS : "0" ,
364+ } ,
365+ ) ;
366+ expect ( exitCode ) . toBe ( 1 ) ;
367+ expect ( stderr ) . toContain ( "Cannot reach local emulator" ) ;
368+ } finally {
369+ fs . rmSync ( fakeEmulatorHome , { recursive : true } ) ;
370+ }
371+ } ) ;
372+
373+ // Positive happy-path: only runs when the backend is in local-emulator mode
374+ // (the password sign-in for local-emulator@stack-auth.com only succeeds
375+ // there). Stages a STACK_EMULATOR_HOME with the real internal PCK and
376+ // points STACK_EMULATOR_API_URL at the running backend, so the CLI takes
377+ // the local-default path and signs in as the emulator admin.
378+ it . runIf ( isLocalEmulator ) ( "local-default exec runs against the local emulator backend" , async ( { expect } ) => {
379+ expect ( createdProjectId ) . toBeDefined ( ) ;
380+ const fakeEmulatorHome = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "stack-cli-emu-positive-" ) ) ;
381+ try {
382+ const pckDir = path . join ( fakeEmulatorHome , "run" , "vm" ) ;
383+ fs . mkdirSync ( pckDir , { recursive : true } ) ;
384+ fs . writeFileSync ( path . join ( pckDir , "internal-pck" ) , STACK_INTERNAL_PROJECT_CLIENT_KEY ) ;
385+ const { stdout, exitCode } = await runCli (
386+ [ "exec" , "return 1+1" ] ,
387+ {
388+ STACK_PROJECT_ID : createdProjectId ,
389+ STACK_EMULATOR_HOME : fakeEmulatorHome ,
390+ STACK_EMULATOR_API_URL : STACK_BACKEND_BASE_URL ,
391+ } ,
392+ ) ;
393+ expect ( exitCode ) . toBe ( 0 ) ;
394+ expect ( stdout . trim ( ) ) . toBe ( "2" ) ;
395+ } finally {
396+ fs . rmSync ( fakeEmulatorHome , { recursive : true } ) ;
397+ }
398+ } ) ;
399+
314400 let configTsPath : string ;
315401
316402 it ( "config pull writes a .ts file" , async ( { expect } ) => {
0 commit comments