@@ -16,6 +16,7 @@ import { createIntegrationsAddAction } from './commands/integrations/add-integra
1616import { createIntegrationsEditAction } from './commands/integrations/edit-integration'
1717import { createLintAction } from './commands/lint'
1818import { createOpenAction } from './commands/open'
19+ import { createPublishAction } from './commands/publish'
1920import { createRunAction } from './commands/run'
2021import { createScheduleAction } from './commands/schedule'
2122import { createSplitAction } from './commands/split'
@@ -57,8 +58,8 @@ export function createProgram(): Command {
5758 } )
5859 . exitOverride ( err => {
5960 // Map Commander errors to appropriate exit codes
60- // InvalidArgumentError (e.g., invalid --type value) should exit with InvalidUsage (2)
61- if ( err . code === 'commander.invalidArgument' ) {
61+ // Invalid values and missing required options are both invalid usage (2).
62+ if ( err . code === 'commander.invalidArgument' || err . code === 'commander.missingMandatoryOptionValue' ) {
6263 process . exit ( ExitCode . InvalidUsage )
6364 }
6465 // For other Commander errors, use the default exit code
@@ -563,6 +564,56 @@ ${c.bold('Exit Codes:')}
563564 } )
564565 . action ( createSyncAction ( program ) )
565566
567+ // Publish command - publish a local app directory to Deepnote
568+ program
569+ . command ( 'publish' )
570+ . description ( 'Publish a local app directory to a Deepnote project' )
571+ . argument ( '<dir>' , 'Directory containing the app files to publish' )
572+ . requiredOption ( '--project-id <uuid>' , 'Deepnote project ID to publish to' )
573+ . option ( '--url <url>' , 'API base URL' , DEFAULT_API_URL )
574+ . option ( '--token <token>' , `Bearer token for the Deepnote API (or use ${ DEEPNOTE_TOKEN_ENV } env var)` )
575+ . option ( '--path <prefix>' , 'Target directory under _deepnote_static' , '_deepnote_static' )
576+ . addOption (
577+ new Option ( '--api-access <state>' , 'Allow the published app to call Deepnote APIs' ) . choices ( [
578+ 'enabled' ,
579+ 'disabled' ,
580+ ] )
581+ )
582+ . option ( '--prune' , 'Delete remote files below --path that are absent locally' )
583+ . addHelpText ( 'after' , ( ) => {
584+ const c = getChalk ( )
585+ return `
586+ ${ c . bold ( 'Description:' ) }
587+ Replaces matching files in ${ c . dim ( '_deepnote_static/' ) } and enables static website sharing
588+ after every upload succeeds. API access is left unchanged unless explicitly set.
589+
590+ ${ c . bold ( 'Examples:' ) }
591+ ${ c . dim ( '# Publish a build directory to a project' ) }
592+ $ deepnote publish ./dist --project-id 0f1e2d3c-4b5a-6789-abcd-ef0123456789
593+
594+ ${ c . dim ( '# Publish with an explicit token' ) }
595+ $ deepnote publish ./build --project-id <uuid> --token <token>
596+
597+ ${ c . dim ( '# Publish to a custom path prefix' ) }
598+ $ deepnote publish ./out --project-id <uuid> --path _deepnote_static/v2
599+
600+ ${ c . dim ( '# Let the published app call Deepnote APIs' ) }
601+ $ deepnote publish ./dist --project-id <uuid> --api-access enabled
602+
603+ ${ c . dim ( '# Remove remote files that are no longer in the local build' ) }
604+ $ deepnote publish ./dist --project-id <uuid> --prune
605+
606+ ${ c . dim ( '# Quiet mode (no progress output)' ) }
607+ $ deepnote publish ./dist --project-id <uuid> -q
608+
609+ ${ c . bold ( 'Exit Codes:' ) }
610+ ${ c . dim ( '0' ) } Files uploaded and website sharing enabled
611+ ${ c . dim ( '1' ) } Upload, pruning, or settings update failed
612+ ${ c . dim ( '2' ) } Invalid usage (bad path, directory not found, missing token)
613+ `
614+ } )
615+ . action ( createPublishAction ( program ) )
616+
566617 // Convert command - convert between notebook formats
567618 program
568619 . command ( 'convert' )
0 commit comments