@@ -15,6 +15,16 @@ import {
1515} from "./lib/deploymentSelection.js" ;
1616import { saveSelectedDeployment } from "./deploymentSelect.js" ;
1717import { deploymentCreate , resolveRegionDetails } from "./deploymentCreate.js" ;
18+ import { ensureBackendBinaryDownloaded } from "./lib/localDeployment/download.js" ;
19+ import {
20+ loadProjectLocalConfig ,
21+ saveDeploymentConfig ,
22+ } from "./lib/localDeployment/filePaths.js" ;
23+ import {
24+ chooseLocalBackendPorts ,
25+ LOCAL_BACKEND_INSTANCE_SECRET ,
26+ } from "./lib/localDeployment/utils.js" ;
27+ import { bigBrainStart } from "./lib/localDeployment/bigBrain.js" ;
1828
1929vi . mock ( "@sentry/node" , ( ) => ( {
2030 captureException : vi . fn ( ) ,
@@ -39,6 +49,24 @@ vi.mock("./deploymentSelect.js", () => ({
3949 saveSelectedDeployment : vi . fn ( ) ,
4050} ) ) ;
4151
52+ vi . mock ( "./lib/localDeployment/download.js" , ( ) => ( {
53+ ensureBackendBinaryDownloaded : vi . fn ( ) ,
54+ } ) ) ;
55+
56+ vi . mock ( "./lib/localDeployment/filePaths.js" , ( ) => ( {
57+ loadProjectLocalConfig : vi . fn ( ) ,
58+ saveDeploymentConfig : vi . fn ( ) ,
59+ } ) ) ;
60+
61+ vi . mock ( "./lib/localDeployment/utils.js" , ( ) => ( {
62+ chooseLocalBackendPorts : vi . fn ( ) ,
63+ LOCAL_BACKEND_INSTANCE_SECRET : "MockSecret123" ,
64+ } ) ) ;
65+
66+ vi . mock ( "./lib/localDeployment/bigBrain.js" , ( ) => ( {
67+ bigBrainStart : vi . fn ( ) ,
68+ } ) ) ;
69+
4270const mockRegions = [
4371 {
4472 name : "aws-us-east-1" as const ,
@@ -132,6 +160,165 @@ describe("non-interactive create flow", () => {
132160 expect . stringContaining ( "--type is required" ) ,
133161 ) ;
134162 } ) ;
163+
164+ test ( "creates a local deployment: downloads binary, chooses ports, registers with Big Brain, saves config" , async ( ) => {
165+ vi . mocked ( getDeploymentSelection ) . mockResolvedValue ( {
166+ kind : "existingDeployment" ,
167+ deploymentToActOn : {
168+ url : "https://joyful-capybara-123.convex.cloud" ,
169+ adminKey : "admin-key" ,
170+ deploymentFields : {
171+ deploymentName : "joyful-capybara-123" ,
172+ deploymentType : "dev" ,
173+ teamSlug : "my-team" ,
174+ projectSlug : "my-project" ,
175+ } ,
176+ source : "deployKey" as const ,
177+ } ,
178+ } ) ;
179+ vi . mocked ( getProjectDetails ) . mockResolvedValue ( fakeProject ) ;
180+ vi . mocked ( loadProjectLocalConfig ) . mockReturnValue ( null ) ;
181+ vi . mocked ( ensureBackendBinaryDownloaded ) . mockResolvedValue ( {
182+ binaryPath : "/path" ,
183+ version : "1.0.0" ,
184+ } ) ;
185+ vi . mocked ( chooseLocalBackendPorts ) . mockResolvedValue ( {
186+ cloudPort : 3210 ,
187+ sitePort : 3211 ,
188+ } ) ;
189+ vi . mocked ( bigBrainStart ) . mockResolvedValue ( {
190+ deploymentName : "local-test-123" ,
191+ adminKey : "test-key" ,
192+ } ) ;
193+
194+ await deploymentCreate . parseAsync ( [ "local" ] , { from : "user" } ) ;
195+
196+ expect ( saveDeploymentConfig ) . toHaveBeenCalledWith (
197+ expect . anything ( ) ,
198+ "local" ,
199+ "local-test-123" ,
200+ {
201+ backendVersion : "1.0.0" ,
202+ ports : { cloud : 3210 , site : 3211 } ,
203+ adminKey : "test-key" ,
204+ instanceSecret : LOCAL_BACKEND_INSTANCE_SECRET ,
205+ } ,
206+ ) ;
207+ expect ( mockPlatformPost ) . not . toHaveBeenCalled ( ) ;
208+ } ) ;
209+
210+ test ( "creates a local deployment with --select and selects it" , async ( ) => {
211+ vi . mocked ( getDeploymentSelection ) . mockResolvedValue ( {
212+ kind : "existingDeployment" ,
213+ deploymentToActOn : {
214+ url : "https://joyful-capybara-123.convex.cloud" ,
215+ adminKey : "admin-key" ,
216+ deploymentFields : {
217+ deploymentName : "joyful-capybara-123" ,
218+ deploymentType : "dev" ,
219+ teamSlug : "my-team" ,
220+ projectSlug : "my-project" ,
221+ } ,
222+ source : "deployKey" as const ,
223+ } ,
224+ } ) ;
225+ vi . mocked ( getProjectDetails ) . mockResolvedValue ( fakeProject ) ;
226+ vi . mocked ( loadProjectLocalConfig ) . mockReturnValue ( null ) ;
227+ vi . mocked ( ensureBackendBinaryDownloaded ) . mockResolvedValue ( {
228+ binaryPath : "/path" ,
229+ version : "1.0.0" ,
230+ } ) ;
231+ vi . mocked ( chooseLocalBackendPorts ) . mockResolvedValue ( {
232+ cloudPort : 3210 ,
233+ sitePort : 3211 ,
234+ } ) ;
235+ vi . mocked ( bigBrainStart ) . mockResolvedValue ( {
236+ deploymentName : "local-test-123" ,
237+ adminKey : "test-key" ,
238+ } ) ;
239+
240+ await deploymentCreate . parseAsync ( [ "local" , "--select" ] , {
241+ from : "user" ,
242+ } ) ;
243+
244+ expect ( saveSelectedDeployment ) . toHaveBeenCalledWith (
245+ expect . anything ( ) ,
246+ "local" ,
247+ {
248+ kind : "deploymentWithinProject" ,
249+ targetProject : {
250+ kind : "deploymentName" ,
251+ deploymentName : "local-test-123" ,
252+ deploymentType : "local" ,
253+ } ,
254+ selectionWithinProject : {
255+ kind : "deploymentSelector" ,
256+ selector : "local" ,
257+ } ,
258+ } ,
259+ null ,
260+ ) ;
261+ } ) ;
262+
263+ test ( "crashes when creating a local deployment with --type" , async ( ) => {
264+ await expect (
265+ deploymentCreate . parseAsync ( [ "local" , "--type" , "dev" ] , {
266+ from : "user" ,
267+ } ) ,
268+ ) . rejects . toThrow ( ) ;
269+ expect ( process . stderr . write ) . toHaveBeenCalledWith (
270+ expect . stringContaining (
271+ "--type cannot be used when creating a local deployment" ,
272+ ) ,
273+ ) ;
274+ expect ( mockPlatformPost ) . not . toHaveBeenCalled ( ) ;
275+ } ) ;
276+
277+ test ( "errors when local deployment already exists" , async ( ) => {
278+ vi . mocked ( getDeploymentSelection ) . mockResolvedValue ( {
279+ kind : "existingDeployment" ,
280+ deploymentToActOn : {
281+ url : "https://joyful-capybara-123.convex.cloud" ,
282+ adminKey : "admin-key" ,
283+ deploymentFields : {
284+ deploymentName : "joyful-capybara-123" ,
285+ deploymentType : "dev" ,
286+ teamSlug : "my-team" ,
287+ projectSlug : "my-project" ,
288+ } ,
289+ source : "deployKey" as const ,
290+ } ,
291+ } ) ;
292+ vi . mocked ( loadProjectLocalConfig ) . mockReturnValue ( {
293+ deploymentName : "existing-local-123" ,
294+ config : { } as any ,
295+ } ) ;
296+
297+ await expect (
298+ deploymentCreate . parseAsync ( [ "local" ] , { from : "user" } ) ,
299+ ) . rejects . toThrow ( ) ;
300+ expect ( process . stderr . write ) . toHaveBeenCalledWith (
301+ expect . stringContaining ( "A local deployment already exists" ) ,
302+ ) ;
303+ } ) ;
304+
305+ test . each ( [ "region" , "default" , "expiration" ] as const ) (
306+ "rejects --%s with local" ,
307+ async ( flag ) => {
308+ const args = [ "local" , `--${ flag } ` ] ;
309+ if ( flag === "region" ) args . push ( "us" ) ;
310+ if ( flag === "expiration" ) args . push ( "none" ) ;
311+
312+ await expect (
313+ deploymentCreate . parseAsync ( args , { from : "user" } ) ,
314+ ) . rejects . toThrow ( ) ;
315+ expect ( process . stderr . write ) . toHaveBeenCalledWith (
316+ expect . stringContaining (
317+ `--${ flag } cannot be used when creating a local deployment` ,
318+ ) ,
319+ ) ;
320+ } ,
321+ ) ;
135322 } ) ;
136323
137324 describe ( "with project configured" , ( ) => {
@@ -662,6 +849,41 @@ describe("interactive create flow", () => {
662849 ) ;
663850 } ) ;
664851
852+ test ( "interactive ref prompt rejects 'local' as a deployment reference" , async ( ) => {
853+ setupDefaultRoutes ( ) ;
854+
855+ const promise = deploymentCreate . parseAsync ( [ "--type" , "dev" ] , {
856+ from : "user" ,
857+ } ) ;
858+
859+ // Ref prompt — enter "local"
860+ await screen . next ( ) ;
861+ expect ( screen . getScreen ( ) ) . toContain ( "How to name this deployment?" ) ;
862+ screen . type ( "local" ) ;
863+ screen . keypress ( "enter" ) ;
864+
865+ // Inline validation error
866+ await screen . next ( ) ;
867+ expect ( screen . getScreen ( ) ) . toContain (
868+ '"local" is not a valid deployment reference' ,
869+ ) ;
870+
871+ // Fix the input
872+ screen . type ( "ization-improvements" ) ;
873+ screen . keypress ( "enter" ) ;
874+
875+ await promise ;
876+
877+ expect ( mockPlatformPost ) . toHaveBeenCalledWith (
878+ "/projects/{project_id}/create_deployment" ,
879+ expect . objectContaining ( {
880+ body : expect . objectContaining ( {
881+ reference : "localization-improvements" ,
882+ } ) ,
883+ } ) ,
884+ ) ;
885+ } ) ;
886+
665887 test ( "--region invalid crashes" , async ( ) => {
666888 setupDefaultRoutes ( ) ;
667889
0 commit comments