@@ -14,7 +14,7 @@ import {
14
14
setDeployConfig
15
15
} from "./observableApiConfig.js" ;
16
16
import { Telemetry } from "./telemetry.js" ;
17
- import { blue } from "./tty.js" ;
17
+ import { blue , bold , hangingIndentLog , magenta , yellow } from "./tty.js" ;
18
18
19
19
export interface DeployOptions {
20
20
config : Config ;
@@ -28,6 +28,7 @@ export interface DeployEffects {
28
28
logger : Logger ;
29
29
input : NodeJS . ReadableStream ;
30
30
output : NodeJS . WritableStream ;
31
+ outputColumns : number ;
31
32
}
32
33
33
34
const defaultEffects : DeployEffects = {
@@ -37,7 +38,8 @@ const defaultEffects: DeployEffects = {
37
38
isTty : isatty ( process . stdin . fd ) ,
38
39
logger : console ,
39
40
input : process . stdin ,
40
- output : process . stdout
41
+ output : process . stdout ,
42
+ outputColumns : process . stdout . columns ?? 80
41
43
} ;
42
44
43
45
/** Deploy a project to ObservableHQ */
@@ -84,56 +86,78 @@ export async function deploy({config}: DeployOptions, effects = defaultEffects):
84
86
}
85
87
}
86
88
89
+ const deployConfig = await effects . getDeployConfig ( config . root ) ;
87
90
if ( projectId ) {
88
91
// Check last deployed state. If it's not the same project, ask the user if
89
92
// they want to continue anyways. In non-interactive mode just cancel.
90
- const deployConfig = await effects . getDeployConfig ( config . root ) ;
91
93
const previousProjectId = deployConfig ?. projectId ;
92
94
if ( previousProjectId && previousProjectId !== projectId ) {
93
- logger . log (
94
- `This project was last deployed to a workspace/slug different from @${ config . deploy . workspace } /${ config . deploy . project } .`
95
+ const { indent} = hangingIndentLog (
96
+ effects ,
97
+ magenta ( "Attention:" ) ,
98
+ `This project was last deployed to a different project on Observable Cloud from ${ bold (
99
+ `@${ config . deploy . workspace } /${ config . deploy . project } `
100
+ ) } .`
95
101
) ;
96
102
if ( effects . isTty ) {
97
- const choice = await promptUserForInput (
98
- effects . input ,
99
- effects . output ,
100
- `Do you want to deploy to @${ config . deploy . workspace } /${ config . deploy . project } anyway? [y/N]`
101
- ) ;
102
- if ( choice . trim ( ) . toLowerCase ( ) . charAt ( 0 ) !== "y" ) {
103
- throw new CliError ( "User cancelled deploy." , { print : false , exitCode : 2 } ) ;
103
+ const choice = await promptConfirm ( effects , `${ indent } Do you want to deploy anyway?` , { default : false } ) ;
104
+ if ( ! choice ) {
105
+ throw new CliError ( "User cancelled deploy" , { print : false , exitCode : 0 } ) ;
104
106
}
105
107
} else {
106
108
throw new CliError ( "Cancelling deploy due to misconfiguration." ) ;
107
109
}
110
+ } else if ( ! previousProjectId ) {
111
+ const { indent} = hangingIndentLog (
112
+ effects ,
113
+ yellow ( "Warning:" ) ,
114
+ `There is an existing project on Observable Cloud named ${ bold (
115
+ `@${ config . deploy . workspace } /${ config . deploy . project } `
116
+ ) } that is not associated with this repository. If you continue, you'll overwrite the existing content of the project.`
117
+ ) ;
118
+
119
+ if ( ! ( await promptConfirm ( effects , `${ indent } Do you want to continue?` , { default : false } ) ) ) {
120
+ if ( effects . isTty ) {
121
+ throw new CliError ( "Running non-interactively, cancelling deploy" , { print : true , exitCode : 1 } ) ;
122
+ } else {
123
+ throw new CliError ( "User cancelled deploy" , { print : true , exitCode : 0 } ) ;
124
+ }
125
+ }
108
126
}
109
127
} else {
110
128
// Project doesn't exist, so ask the user if they want to create it.
111
- // In non-interactive mode just cancel.
129
+ const { indent} = hangingIndentLog (
130
+ effects ,
131
+ magenta ( "Attention:" ) ,
132
+ `There is no project on the Observable Cloud named ${ bold (
133
+ `@${ config . deploy . workspace } /${ config . deploy . project } `
134
+ ) } `
135
+ ) ;
112
136
if ( effects . isTty ) {
113
- const choice = await promptUserForInput ( effects . input , effects . output , "No project exists. Create it now? [y/N]" ) ;
114
- if ( choice . trim ( ) . toLowerCase ( ) . charAt ( 0 ) !== "y" ) {
115
- throw new CliError ( "User cancelled deploy." , { print : false , exitCode : 2 } ) ;
116
- }
117
137
if ( ! config . title ) {
118
138
throw new CliError ( "You haven't configured a project title. Please set title in your configuration." ) ;
119
139
}
120
- const currentUserResponse = await apiClient . getCurrentUser ( ) ;
121
- const workspace = currentUserResponse . workspaces . find ( ( w ) => w . login === config . deploy ?. workspace ) ;
122
- if ( ! workspace ) {
123
- const availableWorkspaces = currentUserResponse . workspaces . map ( ( w ) => w . login ) . join ( ", " ) ;
124
- throw new CliError (
125
- `Workspace ${ config . deploy ?. workspace } not found. Available workspaces: ${ availableWorkspaces } .`
126
- ) ;
140
+ if ( ! ( await promptConfirm ( effects , `${ indent } Do you want to create it now?` , { default : false } ) ) ) {
141
+ throw new CliError ( "User cancelled deploy." , { print : false , exitCode : 0 } ) ;
127
142
}
128
- const project = await apiClient . postProject ( {
129
- slug : config . deploy . project ,
130
- title : config . title ,
131
- workspaceId : workspace . id
132
- } ) ;
133
- projectId = project . id ;
134
143
} else {
135
144
throw new CliError ( "Cancelling deploy due to non-existent project." ) ;
136
145
}
146
+
147
+ const currentUserResponse = await apiClient . getCurrentUser ( ) ;
148
+ const workspace = currentUserResponse . workspaces . find ( ( w ) => w . login === config . deploy ?. workspace ) ;
149
+ if ( ! workspace ) {
150
+ const availableWorkspaces = currentUserResponse . workspaces . map ( ( w ) => w . login ) . join ( ", " ) ;
151
+ throw new CliError (
152
+ `Workspace ${ config . deploy ?. workspace } not found. Available workspaces: ${ availableWorkspaces } .`
153
+ ) ;
154
+ }
155
+ const project = await apiClient . postProject ( {
156
+ slug : config . deploy . project ,
157
+ title : config . title ,
158
+ workspaceId : workspace . id
159
+ } ) ;
160
+ projectId = project . id ;
137
161
}
138
162
139
163
await effects . setDeployConfig ( config . root , { projectId} ) ;
@@ -170,6 +194,27 @@ async function promptUserForInput(
170
194
}
171
195
}
172
196
197
+ export async function promptConfirm (
198
+ { input, output} : DeployEffects ,
199
+ question : string ,
200
+ opts : { default : boolean }
201
+ ) : Promise < boolean > {
202
+ const rl = readline . createInterface ( { input, output} ) ;
203
+ const choices = opts . default ? "[Y/n]" : "[y/N]" ;
204
+ try {
205
+ let value : string | null = null ;
206
+ while ( true ) {
207
+ value = ( await rl . question ( `${ question } ${ choices } ` ) ) . toLowerCase ( ) ;
208
+ if ( value === "" ) return opts . default ;
209
+ if ( value . startsWith ( "y" ) ) return true ;
210
+ if ( value . startsWith ( "n" ) ) return false ;
211
+ rl . write ( 'Please answer "y" or "n".\n' ) ;
212
+ }
213
+ } finally {
214
+ rl . close ( ) ;
215
+ }
216
+ }
217
+
173
218
class DeployBuildEffects implements BuildEffects {
174
219
readonly logger : Logger ;
175
220
readonly output : Writer ;
0 commit comments