88} from "../workers.output.ts" ;
99import { LegacyTelemetryState } from "../../../../telemetry/legacy-telemetry-state.service.ts" ;
1010import { RuntimeInfo } from "../../../../../shared/runtime/runtime-info.service.ts" ;
11+ import { Tty } from "../../../../../shared/runtime/tty.service.ts" ;
1112import {
1213 commitWorkerEntry ,
1314 planWorkerEntry ,
@@ -33,18 +34,22 @@ import {
3334} from "../../../../../shared/workers/worker-runtimes.ts" ;
3435import { WORKER_STACKS } from "../../../../../shared/workers/worker-stacks.ts" ;
3536import {
36- InvalidWorkerNameError ,
37+ MissingWorkerNameError ,
3738 WorkerDirectoryExistsError ,
3839} from "../../../../../shared/workers/workers.errors.ts" ;
39- import { legacyLoadWorkersProjectForEntryWrite } from "../workers.shared.ts" ;
40+ import {
41+ legacyLoadWorkersProjectForEntryWrite ,
42+ legacyValidateWorkerName ,
43+ type LegacyWorkersProject ,
44+ } from "../workers.shared.ts" ;
4045import type { LegacyWorkersNewFlags } from "./new.command.ts" ;
4146
4247/**
43- * `supabase experimental workers new < name> ` — scaffold `supabase/workers/<name>/` from the
48+ * `supabase experimental workers new [ name] ` — scaffold `supabase/workers/<name>/` from the
4449 * chosen runtime's starter files and record the choice in `config.toml`.
4550 * Nothing is deployed; this is entirely local-disk work.
4651 *
47- * The runtime and size are resolved *before* anything is written, so a
52+ * The name, runtime and size are all resolved *before* anything is written, so a
4853 * cancelled prompt leaves nothing behind for this worker at all.
4954 */
5055
@@ -53,19 +58,82 @@ function defaultFirst<T>(values: ReadonlyArray<T>, defaultValue: T): Array<T> {
5358 return [ defaultValue , ...values . filter ( ( value ) => value !== defaultValue ) ] ;
5459}
5560
61+ /**
62+ * Whether this run has a terminal to ask on.
63+ *
64+ * `-o json|yaml|toml|env` leaves `output.format` as `text`, and the prompts go
65+ * through Clack, which writes its terminal UI to stdout with no stream
66+ * override — so a machine format is as non-interactive as a redirected stdout,
67+ * whichever flag asked for it.
68+ *
69+ * `output.interactive` only tracks *stdout*, so on its own it still let
70+ * `printf 'api\n' | supabase experimental workers new` feed the pipe straight
71+ * into the name prompt instead of taking the documented non-interactive path. A
72+ * prompt is only answerable from a keyboard, so stdin has to be a terminal too
73+ * — the same pair `workers delete` guards its confirmation with.
74+ */
75+ const canPromptFor = Effect . fnUntraced ( function * ( machineOutput : boolean ) {
76+ const output = yield * Output ;
77+ const tty = yield * Tty ;
78+ return output . format === "text" && output . interactive && ! machineOutput && tty . stdinIsTty ;
79+ } ) ;
80+
81+ /**
82+ * The worker name, asked for when the command line did not carry one.
83+ *
84+ * The name is the one input here that cannot be defaulted — it is the
85+ * directory, the `config.toml` key and the hostname — so a bare
86+ * `supabase experimental workers new` asks rather than failing the parse. The
87+ * prompt validates against everything the command would otherwise refuse a
88+ * moment later, so a mistyped or already-recorded name is corrected in place
89+ * instead of ending the run.
90+ */
91+ const resolveName = Effect . fnUntraced ( function * ( options : {
92+ readonly explicit : Option . Option < string > ;
93+ /** Whether there is a terminal to ask on — see `canPromptFor`. */
94+ readonly canPrompt : boolean ;
95+ readonly project : LegacyWorkersProject ;
96+ } ) {
97+ if ( Option . isSome ( options . explicit ) ) {
98+ return options . explicit . value ;
99+ }
100+
101+ if ( options . canPrompt ) {
102+ const output = yield * Output ;
103+ return yield * output . promptText ( "What should this worker be called?" , {
104+ validate : ( value ) => {
105+ const invalid = validateWorkerNameMessage ( value ) ;
106+ if ( invalid !== undefined ) {
107+ return invalid ;
108+ }
109+ return options . project . section . workers [ value ] === undefined
110+ ? undefined
111+ : `"${ value } " is already configured in ${ options . project . configPath } .` ;
112+ } ,
113+ } ) ;
114+ }
115+
116+ return yield * Effect . fail (
117+ new MissingWorkerNameError ( {
118+ detail : "Worker name is required in non-interactive mode." ,
119+ suggestion : "Pass a worker name, for example `supabase experimental workers new api`." ,
120+ } ) ,
121+ ) ;
122+ } ) ;
123+
56124const resolveRuntime = Effect . fnUntraced ( function * ( options : {
57125 readonly explicit : Option . Option < WorkerRuntime > ;
58- /** `-o json|yaml|toml|env` — stdout belongs to the payload, so do not prompt . */
59- readonly machineOutput : boolean ;
126+ /** Whether there is a terminal to ask on — see `canPromptFor` . */
127+ readonly canPrompt : boolean ;
60128} ) {
61129 // `--runtime` is a choice flag, so the parser has already rejected anything
62130 // outside the catalog by the time it gets here.
63131 if ( Option . isSome ( options . explicit ) ) {
64132 return options . explicit . value ;
65133 }
66134
67- const output = yield * Output ;
68- if ( output . format === "text" && output . interactive && ! options . machineOutput ) {
135+ if ( options . canPrompt ) {
136+ const output = yield * Output ;
69137 const selected = yield * output . promptSelect (
70138 "Which runtime should this worker use?" ,
71139 defaultFirst ( [ ...WORKER_RUNTIMES ] , DEFAULT_WORKER_RUNTIME ) . map ( ( runtime ) => ( {
@@ -82,15 +150,15 @@ const resolveRuntime = Effect.fnUntraced(function* (options: {
82150
83151const resolveSize = Effect . fnUntraced ( function * ( options : {
84152 readonly explicit : Option . Option < WorkerSize > ;
85- /** `-o json|yaml|toml|env` — stdout belongs to the payload, so do not prompt . */
86- readonly machineOutput : boolean ;
153+ /** Whether there is a terminal to ask on — see `canPromptFor` . */
154+ readonly canPrompt : boolean ;
87155} ) {
88156 if ( Option . isSome ( options . explicit ) ) {
89157 return options . explicit . value ;
90158 }
91159
92- const output = yield * Output ;
93- if ( output . format === "text" && output . interactive && ! options . machineOutput ) {
160+ if ( options . canPrompt ) {
161+ const output = yield * Output ;
94162 const selected = yield * output . promptSelect (
95163 "Which instance size should this worker use?" ,
96164 defaultFirst ( [ ...WORKER_SIZES ] , DEFAULT_WORKER_SIZE ) . map ( ( size ) => ( {
@@ -134,21 +202,19 @@ export const legacyWorkersNew = Effect.fn("legacy.experimental.workers.new")(fun
134202 yield * Effect . gen ( function * ( ) {
135203 const project = yield * legacyLoadWorkersProjectForEntryWrite ( ) ;
136204
137- const name = flags . name ;
138- const invalid = validateWorkerNameMessage ( name ) ;
139- if ( invalid !== undefined ) {
140- return yield * Effect . fail (
141- new InvalidWorkerNameError ( {
142- detail : `"${ name } " is not a valid worker name. ${ invalid } ` ,
143- suggestion : "Worker names become hostnames, so they must be DNS labels." ,
144- } ) ,
145- ) ;
146- }
205+ // Decided once, before the first prompt rather than beside the last, since
206+ // the name is now asked for too — every prompt below shares the answer.
207+ const machineOutput = yield * legacyWorkersMachineOutputRequested ( ) ;
208+ const canPrompt = yield * canPromptFor ( machineOutput ) ;
209+
210+ const name = yield * resolveName ( { explicit : flags . name , canPrompt, project } ) ;
211+ yield * legacyValidateWorkerName ( name ) ;
147212
148213 // Refused before anything is asked or written. `new` creates a worker;
149214 // changing one that already exists is a `config.toml` edit, and the file is
150215 // the user's. Checking here rather than only in `planWorkerEntry` means the
151- // prompts never run for a name that was going to be refused anyway.
216+ // runtime and size prompts never run for a name that was going to be
217+ // refused anyway; the name prompt rejects it up front for the same reason.
152218 if ( project . section . workers [ name ] !== undefined ) {
153219 return yield * Effect . fail (
154220 new WorkerAlreadyConfiguredError ( {
@@ -159,14 +225,10 @@ export const legacyWorkersNew = Effect.fn("legacy.experimental.workers.new")(fun
159225 }
160226
161227 // Resolved before anything is written, so cancelling either prompt leaves
162- // nothing behind — the name included.
163- // `-o` leaves `output.format` as `text`, and `promptSelect` goes through
164- // Clack, which writes its terminal UI to stdout with no stream override — so
165- // a prompt would land in front of the payload just as the notices did. With a
166- // machine format requested there is nowhere to ask, so the defaults stand.
167- const machineOutput = yield * legacyWorkersMachineOutputRequested ( ) ;
168- const runtime = yield * resolveRuntime ( { explicit : flags . runtime , machineOutput } ) ;
169- const size = yield * resolveSize ( { explicit : flags . size , machineOutput } ) ;
228+ // nothing behind — the name included. With nowhere to ask, the defaults
229+ // stand; only the name has nothing to fall back to.
230+ const runtime = yield * resolveRuntime ( { explicit : flags . runtime , canPrompt } ) ;
231+ const size = yield * resolveSize ( { explicit : flags . size , canPrompt } ) ;
170232
171233 // Validated before anything is written: this is the directory the starter
172234 // files land in, so a value naming the project root, `supabase/`, or
0 commit comments