@@ -35,11 +35,7 @@ interface CommandOptions {
3535 config ?: string ;
3636}
3737
38- interface DashboardOptions extends CommandOptions {
39- port ?: number ;
40- }
41-
42- interface ServerStartOptions extends CommandOptions {
38+ interface PortedOptions extends CommandOptions {
4339 port ?: number ;
4440}
4541
@@ -362,19 +358,23 @@ export function getDashboardSpawnOptions(port?: number): {
362358}
363359
364360/**
365- * Validate dashboard port option.
366- * @param port - Optional dashboard port.
367- * @returns Validated dashboard port.
368- * @throws {CLIError } If the provided port is not an integer in the 1-65535 range.
361+ * Validate a port option.
362+ * @param port - Optional port number.
363+ * @param label - Label used in the error message (e.g. "dashboard", "server").
364+ * @returns Validated port, or undefined if not provided.
365+ * @throws {CLIError } If the port is not an integer in the 1-65535 range.
369366 */
370- export function validateDashboardPort ( port ?: number ) : number | undefined {
367+ export function validatePort (
368+ port : number | undefined ,
369+ label : string ,
370+ ) : number | undefined {
371371 if ( port === undefined ) {
372372 return undefined ;
373373 }
374374
375375 if ( ! Number . isInteger ( port ) || port < 1 || port > 65_535 ) {
376376 throw new CLIError (
377- " Invalid dashboard port." ,
377+ ` Invalid ${ label } port.` ,
378378 "Use an integer between 1 and 65535, for example `--port 3001`." ,
379379 ) ;
380380 }
@@ -387,9 +387,9 @@ export function validateDashboardPort(port?: number): number | undefined {
387387 * @param options - Dashboard command options.
388388 * @returns Resolves when the dashboard process exits.
389389 */
390- export async function dashboard ( options : DashboardOptions = { } ) : Promise < void > {
390+ export async function dashboard ( options : PortedOptions = { } ) : Promise < void > {
391391 const configPath = options . config ;
392- const port = validateDashboardPort ( options . port ) ;
392+ const port = validatePort ( options . port , "dashboard" ) ;
393393 consola . start ( "Starting dashboard..." ) ;
394394
395395 const { configFile } = await loadConfigWithEnv ( configPath ) ;
@@ -451,21 +451,15 @@ export async function dashboard(options: DashboardOptions = {}): Promise<void> {
451451 } ) ;
452452}
453453
454- export type { ServerStartOptions } ;
455-
456454/**
457455 * openworkflow server start
458- * Start the OpenWorkflow HTTP API server.
459456 * @param options - Server start options.
460457 */
461- export async function serverStart (
462- options : ServerStartOptions = { } ,
463- ) : Promise < void > {
464- const { config : configPath , port : rawPort } = options ;
465- const port = rawPort ?? 3000 ;
458+ export async function serverStart ( options : PortedOptions = { } ) : Promise < void > {
459+ const port = validatePort ( options . port , "server" ) ?? 3000 ;
466460 consola . start ( "Starting server..." ) ;
467461
468- const { configFile, config } = await loadConfigWithEnv ( configPath ) ;
462+ const { configFile, config } = await loadConfigWithEnv ( options . config ) ;
469463 if ( ! configFile ) {
470464 throw new CLIError (
471465 "No config file found." ,
@@ -474,32 +468,32 @@ export async function serverStart(
474468 }
475469 consola . info ( `Using config: ${ configFile } ` ) ;
476470
477- let createServer : typeof import ( "@openworkflow/server" ) . createServer ;
478- let serve : typeof import ( "@openworkflow/server" ) . serve ;
479- try {
480- ( { createServer, serve } = await import ( "@openworkflow/server" ) ) ;
481- } catch {
482- throw new CLIError (
483- "@openworkflow/server is not installed." ,
484- 'Run `npm install @openworkflow/server` to enable the "server start" command.' ,
485- ) ;
486- }
487-
488471 const backend = config . backend ;
489- const server = createServer ( backend , {
490- logRequests : true ,
491- onError : ( error , ctx ) => {
492- consola . error ( `[${ ctx . method } ${ ctx . path } ]` , error ) ;
493- } ,
494- } ) ;
495- const handle = serve ( server , { port } ) ;
496- consola . success ( `Server listening on http://localhost:${ String ( port ) } ` ) ;
497-
498- registerGracefulShutdown ( {
472+ let handle : { close ( ) : Promise < void > } | null = null ;
473+ const gracefulShutdown = registerGracefulShutdown ( {
499474 noun : "server" ,
500- stopApp : ( ) => handle . close ( ) ,
475+ stopApp : async ( ) => {
476+ await handle ?. close ( ) ;
477+ } ,
501478 backend,
502479 } ) ;
480+
481+ try {
482+ // still dynamic to defer hono's ~150KB until `server start` is invoked
483+ const { createServer, serve } = await import ( "@openworkflow/server" ) ;
484+
485+ const server = createServer ( backend , {
486+ logRequests : true ,
487+ onError : ( error , ctx ) => {
488+ consola . error ( `[${ ctx . method } ${ ctx . path } ]` , error ) ;
489+ } ,
490+ } ) ;
491+ handle = serve ( server , { port } ) ;
492+ consola . success ( `Server listening on http://localhost:${ String ( port ) } ` ) ;
493+ } catch ( error ) {
494+ await gracefulShutdown ( ) ;
495+ throw error ;
496+ }
503497}
504498
505499// -----------------------------------------------------------------------------
@@ -514,11 +508,10 @@ interface ShutdownOptions {
514508}
515509
516510/**
517- * Wire SIGINT/SIGTERM to a graceful shutdown. The HTTP handle / worker is
518- * stopped first (so no new work starts), then the backend is stopped even if
519- * the app-level close fails.
511+ * Wire SIGINT/SIGTERM to a graceful shutdown. `stopApp` runs first; the
512+ * backend is stopped even if `stopApp` throws.
520513 * @param options - What to stop on shutdown
521- * @returns The shutdown function (also registered against SIGINT/SIGTERM)
514+ * @returns The shutdown function
522515 */
523516function registerGracefulShutdown (
524517 options : ShutdownOptions ,
0 commit comments