2026-02-27 17:05:05 +00:00
# ! / u s r / b i n / e n v t s x
/ * *
* generate - completions . ts — auto - generates shell completions from the commander . js command tree .
*
* Usage :
* tsx scripts / generate - completions . ts # print generated files to stdout
* tsx scripts / generate - completions . ts -- write # write completions / files
* tsx scripts / generate - completions . ts -- check # exit 0 if files match , 1 if stale
*
* Requires ` pnpm build ` to have run first ( workspace packages must be compiled ) .
* /
import { Command , type Option , type Argument } from 'commander' ;
import { readFileSync , writeFileSync } from 'node:fs' ;
import { join , dirname } from 'node:path' ;
import { fileURLToPath } from 'node:url' ;
const __dirname = dirname ( fileURLToPath ( import . meta . url ) ) ;
const ROOT = join ( __dirname , '..' ) ;
// ============================================================
// Configuration — hints for argument completions
// ============================================================
type CompletionHint =
| 'project_names'
| 'resource_types'
| 'resource_names'
| 'instance_names'
| 'available_servers'
| 'project_servers'
| 'file'
| { static : string [ ] } ;
/ * *
* Maps ` command.argumentName ` → how to complete that argument .
* Anything not listed here gets no special completion ( user types freeform ) .
* /
const COMPLETION_HINTS : Record < string , CompletionHint > = {
'console.project' : 'project_names' ,
'get.resource' : 'resource_types' ,
'get.id' : 'resource_names' ,
'describe.resource' : 'resource_types' ,
'describe.id' : 'resource_names' ,
'delete.resource' : 'resource_types' ,
'delete.id' : 'resource_names' ,
'edit.resource' : { static : [ 'servers' , 'secrets' , 'projects' , 'groups' , 'rbac' , 'prompts' , 'promptrequests' ] } ,
'edit.name-or-id' : 'resource_names' ,
'patch.resource' : 'resource_types' ,
'patch.name' : 'resource_names' ,
'approve.resource' : { static : [ 'promptrequest' ] } ,
'approve.name' : 'resource_names' ,
'logs.name' : 'instance_names' ,
'attach-server.server-name' : 'available_servers' ,
'detach-server.server-name' : 'project_servers' ,
'apply.file' : 'file' ,
} ;
/** Options whose values are file paths (use -rF in fish, -f in bash). */
const FILE_OPTIONS = new Set ( [
'apply.file' ,
'backup.output' ,
'restore.input' ,
'create-prompt.content-file' ,
'create-promptrequest.content-file' ,
] ) ;
/** Commands shown ONLY when --project/-p is on the command line. */
const PROJECT_ONLY_COMMANDS = new Set ( [ 'attach-server' , 'detach-server' ] ) ;
/** Commands that appear in BOTH project and non-project context. */
const PROJECT_SCOPED_COMMANDS = new Set ( [
'get' , 'describe' , 'delete' , 'logs' , 'create' , 'edit' , 'help' ,
] ) ;
/** Completely hidden commands (never shown in completions). */
const NEVER_SHOW_COMMANDS = new Set ( [ 'mcp' ] ) ;
/ * *
* Commands that follow the resource - type → resource - name two - arg pattern .
* Used to generate guard functions .
* /
const RESOURCE_COMMANDS = [ 'get' , 'describe' , 'delete' , 'edit' , 'patch' , 'approve' ] ;
// ============================================================
// Command tree extraction
// ============================================================
interface CmdInfo {
name : string ;
description : string ;
hidden : boolean ;
options : OptInfo [ ] ;
args : ArgInfo [ ] ;
subcommands : CmdInfo [ ] ;
}
interface OptInfo {
short? : string ; // e.g. '-o'
long : string ; // e.g. '--output'
description : string ;
takesValue : boolean ;
choices? : string [ ] ;
negate : boolean ;
}
interface ArgInfo {
name : string ;
description : string ;
required : boolean ;
variadic : boolean ;
choices? : string [ ] ;
}
function extractOption ( opt : Option ) : OptInfo {
return {
short : ( opt as unknown as Record < string , string > ) . short || undefined ,
long : ( opt as unknown as Record < string , string > ) . long ,
description : opt.description ,
takesValue : ( opt as unknown as Record < string , boolean > ) . required || ( opt as unknown as Record < string , boolean > ) . optional || false ,
choices : ( opt as unknown as Record < string , string [ ] | undefined > ) . argChoices || undefined ,
negate : ( opt as unknown as Record < string , boolean > ) . negate || false ,
} ;
}
function extractArgument ( arg : Argument ) : ArgInfo {
return {
name : ( arg as unknown as Record < string , string > ) . _name ? ? arg . name ( ) ,
description : arg.description ,
required : ( arg as unknown as Record < string , boolean > ) . required ,
variadic : ( arg as unknown as Record < string , boolean > ) . variadic ,
choices : ( arg as unknown as Record < string , string [ ] | undefined > ) . _choices || undefined ,
} ;
}
function extractCommand ( cmd : Command ) : CmdInfo {
const options = ( cmd . options as Option [ ] )
. filter ( ( o ) = > {
const long = ( o as unknown as Record < string , string > ) . long ;
// Skip --help and --version (handled globally)
return long !== '--help' && long !== '--version' ;
} )
. map ( extractOption ) ;
const args = ( ( cmd as unknown as Record < string , Argument [ ] > ) . registeredArguments ? ? [ ] )
. map ( extractArgument ) ;
const subcommands = ( cmd . commands as Command [ ] )
. filter ( ( sub ) = > sub . name ( ) !== 'help' ) // skip commander's auto-generated help
. map ( extractCommand )
// Re-add 'help' as a minimal command for completion purposes
if ( ( cmd . commands as Command [ ] ) . some ( ( sub ) = > sub . name ( ) === 'help' ) ) {
subcommands . push ( {
name : 'help' ,
description : 'display help for command' ,
hidden : false ,
options : [ ] ,
args : [ ] ,
subcommands : [ ] ,
} ) ;
}
return {
name : cmd.name ( ) ,
description : cmd.description ( ) ,
hidden : ( cmd as unknown as Record < string , boolean > ) . _hidden ? ? false ,
options ,
args ,
subcommands ,
} ;
}
async function extractTree ( ) : Promise < CmdInfo > {
// createProgram() is safe to call — it reads config from disk (defaults if missing)
// and creates an ApiClient (no network calls).
const { createProgram } = await import ( '../src/cli/src/index.js' ) as { createProgram : ( ) = > Command } ;
const program = createProgram ( ) ;
return extractCommand ( program ) ;
}
// ============================================================
// Resource aliases (mirrors RESOURCE_ALIASES from shared.ts)
// ============================================================
const CANONICAL_RESOURCES = [
'servers' , 'instances' , 'secrets' , 'templates' , 'projects' ,
'users' , 'groups' , 'rbac' , 'prompts' , 'promptrequests' ,
2026-03-03 19:07:39 +00:00
'serverattachments' , 'proxymodels' , 'all' ,
2026-02-27 17:05:05 +00:00
] ;
const ALIAS_ENTRIES : [ string , string ] [ ] = [
[ 'server' , 'servers' ] , [ 'srv' , 'servers' ] ,
[ 'instance' , 'instances' ] , [ 'inst' , 'instances' ] ,
[ 'secret' , 'secrets' ] , [ 'sec' , 'secrets' ] ,
[ 'template' , 'templates' ] , [ 'tpl' , 'templates' ] ,
[ 'project' , 'projects' ] , [ 'proj' , 'projects' ] ,
[ 'user' , 'users' ] ,
[ 'group' , 'groups' ] ,
[ 'rbac' , 'rbac' ] , [ 'rbac-definition' , 'rbac' ] , [ 'rbac-binding' , 'rbac' ] ,
[ 'prompt' , 'prompts' ] , [ 'prompts' , 'prompts' ] ,
[ 'promptrequest' , 'promptrequests' ] , [ 'promptrequests' , 'promptrequests' ] , [ 'pr' , 'promptrequests' ] ,
[ 'serverattachment' , 'serverattachments' ] , [ 'serverattachments' , 'serverattachments' ] , [ 'sa' , 'serverattachments' ] ,
2026-03-03 19:07:39 +00:00
[ 'proxymodel' , 'proxymodels' ] , [ 'proxymodels' , 'proxymodels' ] , [ 'pm' , 'proxymodels' ] ,
2026-02-27 17:05:05 +00:00
[ 'all' , 'all' ] ,
] ;
const ALL_ALIASES = [ . . . CANONICAL_RESOURCES , . . . ALIAS_ENTRIES . map ( ( [ a ] ) = > a ) ] ;
// Deduplicate (some canonicals are also aliases)
const ALL_ALIASES_UNIQUE = [ . . . new Set ( ALL_ALIASES ) ] ;
// ============================================================
// Fish completion generator
// ============================================================
function generateFish ( root : CmdInfo ) : string {
const lines : string [ ] = [ ] ;
const emit = ( s : string ) = > lines . push ( s ) ;
emit ( '# mcpctl fish completions — auto-generated by scripts/generate-completions.ts' ) ;
emit ( '# DO NOT EDIT MANUALLY — run: pnpm completions:generate' ) ;
emit ( '' ) ;
// --- Erase stale completions ---
emit ( '# Erase any stale completions from previous versions' ) ;
emit ( 'complete -c mcpctl -e' ) ;
emit ( '' ) ;
// --- Categorize commands ---
const visibleCmds : CmdInfo [ ] = [ ] ; // shown without --project
const projectCmds : CmdInfo [ ] = [ ] ; // shown with --project
const projectOnlyCmds : CmdInfo [ ] = [ ] ; // ONLY shown with --project
for ( const cmd of root . subcommands ) {
if ( NEVER_SHOW_COMMANDS . has ( cmd . name ) ) continue ;
if ( PROJECT_ONLY_COMMANDS . has ( cmd . name ) ) {
projectOnlyCmds . push ( cmd ) ;
projectCmds . push ( cmd ) ;
} else if ( PROJECT_SCOPED_COMMANDS . has ( cmd . name ) ) {
visibleCmds . push ( cmd ) ;
projectCmds . push ( cmd ) ;
} else if ( ! cmd . hidden ) {
visibleCmds . push ( cmd ) ;
}
}
const visibleNames = visibleCmds . map ( ( c ) = > c . name ) . join ( ' ' ) ;
const projectNames = projectCmds . map ( ( c ) = > c . name ) . join ( ' ' ) ;
emit ( ` set -l commands ${ visibleNames } ` ) ;
emit ( ` set -l project_commands ${ projectNames } ` ) ;
emit ( '' ) ;
// --- Disable file completions by default ---
emit ( '# Disable file completions by default' ) ;
emit ( 'complete -c mcpctl -f' ) ;
emit ( '' ) ;
// --- Global options ---
emit ( '# Global options' ) ;
emit ( "complete -c mcpctl -s v -l version -d 'Show version'" ) ;
for ( const opt of root . options ) {
const parts = [ 'complete -c mcpctl' ] ;
if ( opt . short ) parts . push ( ` -s ${ opt . short . replace ( '-' , '' ) } ` ) ;
parts . push ( ` -l ${ opt . long . replace ( /^--/ , '' ) } ` ) ;
parts . push ( ` -d ' ${ esc ( opt . description ) } ' ` ) ;
if ( opt . long === '--project' ) {
parts . push ( ` -xa '(__mcpctl_project_names)' ` ) ;
} else if ( opt . takesValue ) {
if ( opt . choices ) {
parts . push ( ` -xa ' ${ opt . choices . join ( ' ' ) } ' ` ) ;
} else {
parts . push ( '-x' ) ;
}
}
emit ( parts . join ( ' ' ) ) ;
}
emit ( "complete -c mcpctl -s h -l help -d 'Show help'" ) ;
emit ( '' ) ;
// --- Runtime helper functions ---
emit ( '# ---- Runtime helpers ----' ) ;
emit ( '' ) ;
emitFishHelpers ( emit ) ;
// --- Top-level command completions ---
emit ( '# Top-level commands (without --project)' ) ;
for ( const cmd of visibleCmds ) {
emit ( ` complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $ commands" -a ${ cmd . name } -d ' ${ esc ( cmd . description ) } ' ` ) ;
}
emit ( '' ) ;
emit ( '# Project-scoped commands (with --project)' ) ;
for ( const cmd of projectCmds ) {
emit ( ` complete -c mcpctl -n "__mcpctl_has_project; and not __fish_seen_subcommand_from $ project_commands" -a ${ cmd . name } -d ' ${ esc ( cmd . description ) } ' ` ) ;
}
emit ( '' ) ;
// --- Resource type completions ---
emit ( '# Resource types — only when resource type not yet selected' ) ;
// Group commands by their resource type hint
const resourceTypeCmds : string [ ] = [ ] ;
const staticTypeCmds : { cmds : string [ ] ; types : string [ ] } [ ] = [ ] ;
for ( const cmdName of RESOURCE_COMMANDS ) {
const hint = COMPLETION_HINTS [ ` ${ cmdName } .resource ` ] ? ? COMPLETION_HINTS [ ` ${ cmdName } . ${ getFirstArgName ( root , cmdName ) } ` ] ;
if ( hint === 'resource_types' ) {
resourceTypeCmds . push ( cmdName ) ;
} else if ( hint && typeof hint === 'object' && 'static' in hint ) {
// Check if we already have a group with the same types
const key = hint . static . join ( ' ' ) ;
const existing = staticTypeCmds . find ( ( g ) = > g . types . join ( ' ' ) === key ) ;
if ( existing ) {
existing . cmds . push ( cmdName ) ;
} else {
staticTypeCmds . push ( { cmds : [ cmdName ] , types : hint.static } ) ;
}
}
}
if ( resourceTypeCmds . length > 0 ) {
emit ( ` complete -c mcpctl -n "__fish_seen_subcommand_from ${ resourceTypeCmds . join ( ' ' ) } ; and __mcpctl_needs_resource_type" -a " $ resources" -d 'Resource type' ` ) ;
}
for ( const group of staticTypeCmds ) {
emit ( ` complete -c mcpctl -n "__fish_seen_subcommand_from ${ group . cmds . join ( ' ' ) } ; and __mcpctl_needs_resource_type" -a ' ${ group . types . join ( ' ' ) } ' -d 'Resource type' ` ) ;
}
emit ( '' ) ;
// --- Resource name completions ---
const resourceNameCmds = RESOURCE_COMMANDS . filter ( ( cmdName ) = > {
const cmd = root . subcommands . find ( ( c ) = > c . name === cmdName ) ;
if ( ! cmd ) return false ;
return cmd . args . some ( ( a ) = > {
const hint = COMPLETION_HINTS [ ` ${ cmdName } . ${ a . name } ` ] ;
return hint === 'resource_names' ;
} ) ;
} ) ;
if ( resourceNameCmds . length > 0 ) {
emit ( '# Resource names — after resource type is selected' ) ;
emit ( ` complete -c mcpctl -n "__fish_seen_subcommand_from ${ resourceNameCmds . join ( ' ' ) } ; and not __mcpctl_needs_resource_type" -a '(__mcpctl_resource_names)' -d 'Resource name' ` ) ;
emit ( '' ) ;
}
// --- Subcommand completions (config, create) ---
for ( const cmd of root . subcommands ) {
if ( cmd . subcommands . length === 0 ) continue ;
const subNames = cmd . subcommands . map ( ( s ) = > s . name ) ;
emit ( ` # ${ cmd . name } subcommands ` ) ;
emit ( ` set -l ${ cmd . name } _cmds ${ subNames . join ( ' ' ) } ` ) ;
for ( const sub of cmd . subcommands ) {
emit ( ` complete -c mcpctl -n "__fish_seen_subcommand_from ${ cmd . name } ; and not __fish_seen_subcommand_from $ ${ cmd . name } _cmds" -a ${ sub . name } -d ' ${ esc ( sub . description ) } ' ` ) ;
}
emit ( '' ) ;
// Subcommand options
for ( const sub of cmd . subcommands ) {
if ( sub . options . length === 0 ) continue ;
emit ( ` # ${ cmd . name } ${ sub . name } options ` ) ;
for ( const opt of sub . options ) {
const parts = [ ` complete -c mcpctl -n "__mcpctl_subcmd_active ${ cmd . name } ${ sub . name } " ` ] ;
if ( opt . short ) parts . push ( ` -s ${ opt . short . replace ( '-' , '' ) } ` ) ;
parts . push ( ` -l ${ opt . long . replace ( /^--/ , '' ) } ` ) ;
parts . push ( ` -d ' ${ esc ( opt . description ) } ' ` ) ;
if ( opt . negate ) {
// --no-X flags are boolean, no value
} else if ( opt . takesValue ) {
const fileKey = ` ${ cmd . name } - ${ sub . name } . ${ opt . long . replace ( /^--/ , '' ) } ` ;
if ( FILE_OPTIONS . has ( fileKey ) ) {
parts . push ( '-rF' ) ;
} else if ( opt . choices ) {
parts . push ( ` -xa ' ${ opt . choices . join ( ' ' ) } ' ` ) ;
} else if ( opt . long === '--project' ) {
parts . push ( ` -xa '(__mcpctl_project_names)' ` ) ;
} else {
parts . push ( '-x' ) ;
}
}
emit ( parts . join ( ' ' ) ) ;
}
emit ( '' ) ;
}
}
// --- Per-command option completions (top-level commands without subcommands) ---
for ( const cmd of root . subcommands ) {
if ( cmd . subcommands . length > 0 ) continue ; // Handled above
if ( cmd . options . length === 0 ) continue ;
if ( NEVER_SHOW_COMMANDS . has ( cmd . name ) ) continue ;
emit ( ` # ${ cmd . name } options ` ) ;
for ( const opt of cmd . options ) {
const parts = [ ` complete -c mcpctl -n "__fish_seen_subcommand_from ${ cmd . name } " ` ] ;
if ( opt . short ) parts . push ( ` -s ${ opt . short . replace ( '-' , '' ) } ` ) ;
parts . push ( ` -l ${ opt . long . replace ( /^--/ , '' ) } ` ) ;
parts . push ( ` -d ' ${ esc ( opt . description ) } ' ` ) ;
if ( opt . negate ) {
// boolean flag
} else if ( opt . takesValue ) {
const fileKey = ` ${ cmd . name } . ${ opt . long . replace ( /^--/ , '' ) } ` ;
if ( FILE_OPTIONS . has ( fileKey ) ) {
parts . push ( '-rF' ) ;
} else if ( opt . choices ) {
parts . push ( ` -xa ' ${ opt . choices . join ( ' ' ) } ' ` ) ;
} else if ( opt . long === '--project' ) {
parts . push ( ` -xa '(__mcpctl_project_names)' ` ) ;
} else {
parts . push ( '-x' ) ;
}
}
emit ( parts . join ( ' ' ) ) ;
}
emit ( '' ) ;
}
// --- Special argument completions ---
// logs: instance names
emit ( "# logs: takes a server/instance name" ) ;
emit ( "complete -c mcpctl -n \"__fish_seen_subcommand_from logs; and __mcpctl_needs_arg_for logs\" -a '(__mcpctl_instance_names)' -d 'Server name'" ) ;
emit ( '' ) ;
// console: project name
emit ( "# console: takes a project name" ) ;
emit ( "complete -c mcpctl -n \"__fish_seen_subcommand_from console; and __mcpctl_needs_arg_for console\" -a '(__mcpctl_project_names)' -d 'Project name'" ) ;
emit ( '' ) ;
// attach-server / detach-server
emit ( "# attach-server: show servers NOT in the project (only if no server arg yet)" ) ;
emit ( "complete -c mcpctl -n \"__fish_seen_subcommand_from attach-server; and __mcpctl_needs_server_arg\" -a '(__mcpctl_available_servers)' -d 'Server'" ) ;
emit ( '' ) ;
emit ( "# detach-server: show servers IN the project (only if no server arg yet)" ) ;
emit ( "complete -c mcpctl -n \"__fish_seen_subcommand_from detach-server; and __mcpctl_needs_server_arg\" -a '(__mcpctl_project_servers)' -d 'Server'" ) ;
emit ( '' ) ;
// apply: allow file completions for positional argument
emit ( "# apply: allow file completions for positional argument" ) ;
emit ( "complete -c mcpctl -n \"__fish_seen_subcommand_from apply\" -F" ) ;
emit ( '' ) ;
// help completions
emit ( '# help completions' ) ;
emit ( 'complete -c mcpctl -n "__fish_seen_subcommand_from help" -a "$commands"' ) ;
return lines . join ( '\n' ) + '\n' ;
}
function emitFishHelpers ( emit : ( s : string ) = > void ) : void {
emit ( ` # Helper: check if --project or -p was given
function __mcpctl_has_project
set - l tokens ( commandline - opc )
for i in ( seq ( count $tokens ) )
if test "$tokens[$i]" = "--project" - o "$tokens[$i]" = "-p"
return 0
end
end
return 1
end
` );
const aliasListStr = ALL_ALIASES_UNIQUE . join ( ' ' ) ;
const resCmdStr = RESOURCE_COMMANDS . join ( ' ' ) ;
emit ( ` # Resource type detection
set - l resources $ { CANONICAL_RESOURCES . join ( ' ' ) }
function __mcpctl_needs_resource_type
set - l resource_aliases $ { aliasListStr }
set - l tokens ( commandline - opc )
set - l found_cmd false
for tok in $tokens
if $found_cmd
if contains -- $tok $resource_aliases
return 1 # resource type already present
end
end
if contains -- $tok $ { resCmdStr }
set found_cmd true
end
end
if $found_cmd
return 0 # command found but no resource type yet
end
return 1
end
` );
emit ( ` # Map any resource alias to the canonical plural form for API calls
function __mcpctl_resolve_resource
switch $argv [ 1 ] ` );
// Group aliases by canonical name
const groups = new Map < string , string [ ] > ( ) ;
for ( const canonical of CANONICAL_RESOURCES ) {
groups . set ( canonical , [ ] ) ;
}
for ( const [ alias , canonical ] of ALIAS_ENTRIES ) {
const g = groups . get ( canonical ) ? ? [ ] ;
if ( ! g . includes ( alias ) ) g . push ( alias ) ;
groups . set ( canonical , g ) ;
}
for ( const [ canonical , aliases ] of groups ) {
const allForms = [ . . . new Set ( [ . . . aliases , canonical ] ) ] . join ( ' ' ) ;
emit ( ` case ${ allForms } ; ${ ' ' . repeat ( Math . max ( 1 , 24 - allForms . length ) ) } echo ${ canonical } ` ) ;
}
emit ( ` case '*'; echo $ argv[1] ` ) ;
emit ( ` end ` ) ;
emit ( ` end
` );
emit ( ` function __mcpctl_get_resource_type
set - l resource_aliases $ { aliasListStr }
set - l tokens ( commandline - opc )
set - l found_cmd false
for tok in $tokens
if $found_cmd
if contains -- $tok $resource_aliases
__mcpctl_resolve_resource $tok
return
end
end
if contains -- $tok $ { resCmdStr }
set found_cmd true
end
end
end
` );
emit ( ` # Fetch resource names dynamically from the API
function __mcpctl_resource_names
set - l resource ( __mcpctl_get_resource_type )
if test - z "$resource"
return
end
if test "$resource" = "instances"
mcpctl get instances - o json 2 > / d e v / n u l l | j q - r ' . [ ] [ ] . s e r v e r . n a m e ' 2 > / d e v / n u l l
else if test "$resource" = "prompts" - o "$resource" = "promptrequests"
mcpctl get $resource - A - o json 2 > / d e v / n u l l | j q - r ' . [ ] . n a m e ' 2 > / d e v / n u l l
else
mcpctl get $resource - o json 2 > / d e v / n u l l | j q - r ' . [ ] . n a m e ' 2 > / d e v / n u l l
end
end
` );
emit ( ` # Fetch project names for --project value
function __mcpctl_project_names
mcpctl get projects - o json 2 > / d e v / n u l l | j q - r ' . [ ] . n a m e ' 2 > / d e v / n u l l
end
` );
emit ( ` # Helper: get the --project/-p value from the command line
function __mcpctl_get_project_value
set - l tokens ( commandline - opc )
for i in ( seq ( count $tokens ) )
if test "$tokens[$i]" = "--project" - o "$tokens[$i]" = "-p" ; and test $i - lt ( count $tokens )
echo $tokens [ ( math $i + 1 ) ]
return
end
end
end
` );
emit ( ` # Servers currently attached to the project (for detach-server)
function __mcpctl_project_servers
set - l proj ( __mcpctl_get_project_value )
if test - z "$proj"
return
end
mcpctl -- project $proj get servers - o json 2 > / d e v / n u l l | j q - r ' . [ ] . n a m e ' 2 > / d e v / n u l l
end
` );
emit ( ` # Servers NOT attached to the project (for attach-server)
function __mcpctl_available_servers
set - l proj ( __mcpctl_get_project_value )
if test - z "$proj"
mcpctl get servers - o json 2 > / d e v / n u l l | j q - r ' . [ ] . n a m e ' 2 > / d e v / n u l l
return
end
set - l all ( mcpctl get servers - o json 2 > / d e v / n u l l | j q - r ' . [ ] . n a m e ' 2 > / d e v / n u l l )
set - l attached ( mcpctl -- project $proj get servers - o json 2 > / d e v / n u l l | j q - r ' . [ ] . n a m e ' 2 > / d e v / n u l l )
for s in $all
if not contains -- $s $attached
echo $s
end
end
end
` );
emit ( ` # Instance names for logs
function __mcpctl_instance_names
mcpctl get instances - o json 2 > / d e v / n u l l | j q - r ' . [ ] [ ] . s e r v e r . n a m e ' 2 > / d e v / n u l l
end
` );
emit ( ` # Helper: check if a positional arg has been given for a specific command
function __mcpctl_needs_arg_for
set - l cmd $argv [ 1 ]
set - l tokens ( commandline - opc )
set - l found false
for tok in $tokens
if $found
if not string match - q -- '-*' $tok
return 1 # arg already present
end
end
if test "$tok" = "$cmd"
set found true
end
end
if $found
return 0 # command found but no arg yet
end
return 1
end
` );
emit ( ` # Helper: check if attach-server/detach-server already has a server argument
function __mcpctl_needs_server_arg
set - l tokens ( commandline - opc )
set - l found_cmd false
for tok in $tokens
if $found_cmd
if not string match - q -- '-*' $tok
return 1 # server arg already present
end
end
if contains -- $tok attach - server detach - server
set found_cmd true
end
end
if $found_cmd
return 0
end
return 1
end
` );
emit ( ` # Helper: check if a specific parent-child subcommand pair is active
function __mcpctl_subcmd_active
set - l parent $argv [ 1 ]
set - l child $argv [ 2 ]
set - l tokens ( commandline - opc )
set - l found_parent false
for tok in $tokens
if $found_parent
if test "$tok" = "$child"
return 0
end
if not string match - q -- '-*' $tok
return 1 # different subcommand
end
end
if test "$tok" = "$parent"
set found_parent true
end
end
return 1
end
` );
}
// ============================================================
// Bash completion generator
// ============================================================
function generateBash ( root : CmdInfo ) : string {
const lines : string [ ] = [ ] ;
const emit = ( s : string ) = > lines . push ( s ) ;
emit ( '# mcpctl bash completions — auto-generated by scripts/generate-completions.ts' ) ;
emit ( '# DO NOT EDIT MANUALLY — run: pnpm completions:generate' ) ;
emit ( '' ) ;
// --- Categorize commands ---
const visibleCmds : CmdInfo [ ] = [ ] ;
const projectCmds : CmdInfo [ ] = [ ] ;
for ( const cmd of root . subcommands ) {
if ( NEVER_SHOW_COMMANDS . has ( cmd . name ) ) {
// Include mcp in case handler but not in top-level list
}
if ( PROJECT_ONLY_COMMANDS . has ( cmd . name ) ) {
projectCmds . push ( cmd ) ;
} else if ( PROJECT_SCOPED_COMMANDS . has ( cmd . name ) ) {
visibleCmds . push ( cmd ) ;
projectCmds . push ( cmd ) ;
} else if ( ! cmd . hidden ) {
visibleCmds . push ( cmd ) ;
}
}
const visibleNames = visibleCmds . map ( ( c ) = > c . name ) . join ( ' ' ) ;
const projectNames = projectCmds . map ( ( c ) = > c . name ) . join ( ' ' ) ;
// Build global opts string from the commander tree
const globalOptParts : string [ ] = [ '-v' , '--version' ] ;
for ( const opt of root . options ) {
if ( opt . short ) globalOptParts . push ( opt . short ) ;
globalOptParts . push ( opt . long ) ;
}
// Always include -h/--help
globalOptParts . push ( '-h' , '--help' ) ;
emit ( '_mcpctl() {' ) ;
emit ( ' local cur prev words cword' ) ;
emit ( ' _init_completion || return' ) ;
emit ( '' ) ;
emit ( ` local commands=" ${ visibleNames } " ` ) ;
emit ( ` local project_commands=" ${ projectNames } " ` ) ;
emit ( ` local global_opts=" ${ globalOptParts . join ( ' ' ) } " ` ) ;
emit ( ` local resources=" ${ CANONICAL_RESOURCES . join ( ' ' ) } " ` ) ;
emit ( ` local resource_aliases=" ${ ALL_ALIASES_UNIQUE . join ( ' ' ) } " ` ) ;
emit ( '' ) ;
// --- has_project check ---
emit ( ' # Check if --project/-p was given' ) ;
emit ( ' local has_project=false' ) ;
emit ( ' local i' ) ;
emit ( ' for ((i=1; i < cword; i++)); do' ) ;
emit ( ' if [[ "${words[i]}" == "--project" || "${words[i]}" == "-p" ]]; then' ) ;
emit ( ' has_project=true' ) ;
emit ( ' break' ) ;
emit ( ' fi' ) ;
emit ( ' done' ) ;
emit ( '' ) ;
// --- subcmd detection ---
emit ( ' # Find the first subcommand' ) ;
emit ( ' local subcmd=""' ) ;
emit ( ' local subcmd_pos=0' ) ;
emit ( ' for ((i=1; i < cword; i++)); do' ) ;
emit ( ' if [[ "${words[i]}" == "--project" || "${words[i]}" == "--daemon-url" || "${words[i]}" == "-p" ]]; then' ) ;
emit ( ' ((i++))' ) ;
emit ( ' continue' ) ;
emit ( ' fi' ) ;
emit ( ' if [[ "${words[i]}" != -* ]]; then' ) ;
emit ( ' subcmd="${words[i]}"' ) ;
emit ( ' subcmd_pos=$i' ) ;
emit ( ' break' ) ;
emit ( ' fi' ) ;
emit ( ' done' ) ;
emit ( '' ) ;
// --- resource_type detection ---
emit ( ' # Find the resource type after resource commands' ) ;
emit ( ' local resource_type=""' ) ;
emit ( ' if [[ -n "$subcmd_pos" ]] && [[ $subcmd_pos -gt 0 ]]; then' ) ;
emit ( ' for ((i=subcmd_pos+1; i < cword; i++)); do' ) ;
emit ( ' if [[ "${words[i]}" != -* ]] && [[ " $resource_aliases " == *" ${words[i]} "* ]]; then' ) ;
emit ( ' resource_type="${words[i]}"' ) ;
emit ( ' break' ) ;
emit ( ' fi' ) ;
emit ( ' done' ) ;
emit ( ' fi' ) ;
emit ( '' ) ;
// --- Helper functions ---
emit ( ' # Helper: get --project/-p value' ) ;
emit ( ' _mcpctl_get_project_value() {' ) ;
emit ( ' local i' ) ;
emit ( ' for ((i=1; i < cword; i++)); do' ) ;
emit ( ' if [[ "${words[i]}" == "--project" || "${words[i]}" == "-p" ]] && (( i+1 < cword )); then' ) ;
emit ( ' echo "${words[i+1]}"' ) ;
emit ( ' return' ) ;
emit ( ' fi' ) ;
emit ( ' done' ) ;
emit ( ' }' ) ;
emit ( '' ) ;
emit ( ' # Helper: fetch resource names' ) ;
emit ( ' _mcpctl_resource_names() {' ) ;
emit ( ' local rt="$1"' ) ;
emit ( ' if [[ -n "$rt" ]]; then' ) ;
emit ( ' if [[ "$rt" == "instances" ]]; then' ) ;
emit ( " mcpctl get instances -o json 2>/dev/null | jq -r '.[][].server.name' 2>/dev/null" ) ;
emit ( ' else' ) ;
emit ( " mcpctl get \"$rt\" -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null" ) ;
emit ( ' fi' ) ;
emit ( ' fi' ) ;
emit ( ' }' ) ;
emit ( '' ) ;
emit ( ' # Helper: find sub-subcommand (for config/create)' ) ;
emit ( ' _mcpctl_get_subcmd() {' ) ;
emit ( ' local parent_pos="$1"' ) ;
emit ( ' local i' ) ;
emit ( ' for ((i=parent_pos+1; i < cword; i++)); do' ) ;
emit ( ' if [[ "${words[i]}" != -* ]]; then' ) ;
emit ( ' echo "${words[i]}"' ) ;
emit ( ' return' ) ;
emit ( ' fi' ) ;
emit ( ' done' ) ;
emit ( ' }' ) ;
emit ( '' ) ;
// --- Option value completion ---
emit ( ' # If completing option values' ) ;
emit ( ' if [[ "$prev" == "--project" || "$prev" == "-p" ]]; then' ) ;
emit ( ' local names' ) ;
emit ( " names=$(mcpctl get projects -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null)" ) ;
emit ( ' COMPREPLY=($(compgen -W "$names" -- "$cur"))' ) ;
emit ( ' return' ) ;
emit ( ' fi' ) ;
emit ( '' ) ;
// --- Case statement ---
emit ( ' case "$subcmd" in' ) ;
for ( const cmd of root . subcommands ) {
emitBashCase ( emit , cmd , root ) ;
}
// help command
emit ( ' help)' ) ;
emit ( ' COMPREPLY=($(compgen -W "$commands" -- "$cur"))' ) ;
emit ( ' return ;;' ) ;
emit ( ' esac' ) ;
emit ( '' ) ;
// --- Default (no subcommand) ---
emit ( ' # No subcommand yet — offer commands based on context' ) ;
emit ( ' if [[ -z "$subcmd" ]]; then' ) ;
emit ( ' if $has_project; then' ) ;
emit ( ' COMPREPLY=($(compgen -W "$project_commands $global_opts" -- "$cur"))' ) ;
emit ( ' else' ) ;
emit ( ' COMPREPLY=($(compgen -W "$commands $global_opts" -- "$cur"))' ) ;
emit ( ' fi' ) ;
emit ( ' fi' ) ;
emit ( '}' ) ;
emit ( '' ) ;
emit ( 'complete -F _mcpctl mcpctl' ) ;
return lines . join ( '\n' ) + '\n' ;
}
function emitBashCase ( emit : ( s : string ) = > void , cmd : CmdInfo , root : CmdInfo ) : void {
const name = cmd . name ;
// Collect all option flags for this command
const optFlags = bashOptFlags ( cmd ) ;
if ( cmd . subcommands . length > 0 ) {
// Commands with subcommands (config, create)
const subNames = cmd . subcommands . map ( ( s ) = > s . name ) ;
emit ( ` ${ name } ) ` ) ;
emit ( ` local ${ name } _sub= $ (_mcpctl_get_subcmd $ subcmd_pos) ` ) ;
emit ( ` if [[ -z " $ ${ name } _sub" ]]; then ` ) ;
emit ( ` COMPREPLY=( $ (compgen -W " ${ subNames . join ( ' ' ) } help" -- " $ cur")) ` ) ;
emit ( ' else' ) ;
emit ( ` case " $ ${ name } _sub" in ` ) ;
for ( const sub of cmd . subcommands ) {
const subOpts = bashOptFlags ( sub ) ;
emit ( ` ${ sub . name } ) ` ) ;
emit ( ` COMPREPLY=( $ (compgen -W " ${ subOpts } " -- " $ cur")) ` ) ;
emit ( ' ;;' ) ;
}
emit ( ' *)' ) ;
emit ( ' COMPREPLY=($(compgen -W "-h --help" -- "$cur"))' ) ;
emit ( ' ;;' ) ;
emit ( ' esac' ) ;
emit ( ' fi' ) ;
emit ( ' return ;;' ) ;
return ;
}
// Resource-type commands (get, describe, delete, edit, patch, approve)
if ( RESOURCE_COMMANDS . includes ( name ) ) {
const hint = COMPLETION_HINTS [ ` ${ name } .resource ` ] ? ? COMPLETION_HINTS [ ` ${ name } . ${ getFirstArgName ( root , name ) } ` ] ;
let resourceList = '$resources' ;
if ( hint && typeof hint === 'object' && 'static' in hint ) {
resourceList = hint . static . join ( ' ' ) ;
} else if ( hint === 'resource_types' ) {
resourceList = '$resources' ;
}
emit ( ` ${ name } ) ` ) ;
emit ( ' if [[ -z "$resource_type" ]]; then' ) ;
emit ( ` COMPREPLY=( $ (compgen -W " ${ resourceList } ${ optFlags } " -- " $ cur")) ` ) ;
emit ( ' else' ) ;
emit ( ' local names' ) ;
emit ( ' names=$(_mcpctl_resource_names "$resource_type")' ) ;
emit ( ` COMPREPLY=( $ (compgen -W " $ names ${ optFlags } " -- " $ cur")) ` ) ;
emit ( ' fi' ) ;
emit ( ' return ;;' ) ;
return ;
}
// logs: first arg is instance name
if ( name === 'logs' ) {
emit ( ` ${ name } ) ` ) ;
emit ( ' if [[ $((cword - subcmd_pos)) -eq 1 ]]; then' ) ;
emit ( ' local names' ) ;
emit ( " names=$(mcpctl get instances -o json 2>/dev/null | jq -r '.[][].server.name' 2>/dev/null)" ) ;
emit ( ` COMPREPLY=( $ (compgen -W " $ names ${ optFlags } " -- " $ cur")) ` ) ;
emit ( ' else' ) ;
emit ( ` COMPREPLY=( $ (compgen -W " ${ optFlags } " -- " $ cur")) ` ) ;
emit ( ' fi' ) ;
emit ( ' return ;;' ) ;
return ;
}
// console: first arg is project name
if ( name === 'console' ) {
emit ( ` ${ name } ) ` ) ;
emit ( ' if [[ $((cword - subcmd_pos)) -eq 1 ]]; then' ) ;
emit ( ' local names' ) ;
emit ( " names=$(mcpctl get projects -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null)" ) ;
emit ( ` COMPREPLY=( $ (compgen -W " $ names ${ optFlags } " -- " $ cur")) ` ) ;
emit ( ' else' ) ;
emit ( ` COMPREPLY=( $ (compgen -W " ${ optFlags } " -- " $ cur")) ` ) ;
emit ( ' fi' ) ;
emit ( ' return ;;' ) ;
return ;
}
// attach-server
if ( name === 'attach-server' ) {
emit ( ' attach-server)' ) ;
emit ( ' if [[ $((cword - subcmd_pos)) -ne 1 ]]; then return; fi' ) ;
emit ( ' local proj names all_servers proj_servers' ) ;
emit ( ' proj=$(_mcpctl_get_project_value)' ) ;
emit ( ' if [[ -n "$proj" ]]; then' ) ;
emit ( " all_servers=$(mcpctl get servers -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null)" ) ;
emit ( " proj_servers=$(mcpctl --project \"$proj\" get servers -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null)" ) ;
emit ( ' names=$(comm -23 <(echo "$all_servers" | sort) <(echo "$proj_servers" | sort))' ) ;
emit ( ' else' ) ;
emit ( ' names=$(_mcpctl_resource_names "servers")' ) ;
emit ( ' fi' ) ;
emit ( ' COMPREPLY=($(compgen -W "$names" -- "$cur"))' ) ;
emit ( ' return ;;' ) ;
return ;
}
// detach-server
if ( name === 'detach-server' ) {
emit ( ' detach-server)' ) ;
emit ( ' if [[ $((cword - subcmd_pos)) -ne 1 ]]; then return; fi' ) ;
emit ( ' local proj names' ) ;
emit ( ' proj=$(_mcpctl_get_project_value)' ) ;
emit ( ' if [[ -n "$proj" ]]; then' ) ;
emit ( " names=$(mcpctl --project \"$proj\" get servers -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null)" ) ;
emit ( ' fi' ) ;
emit ( ' COMPREPLY=($(compgen -W "$names" -- "$cur"))' ) ;
emit ( ' return ;;' ) ;
return ;
}
// apply: file completions
if ( name === 'apply' ) {
emit ( ' apply)' ) ;
emit ( ` COMPREPLY=( $ (compgen -f -W " ${ optFlags } " -- " $ cur")) ` ) ;
emit ( ' return ;;' ) ;
return ;
}
// mcp: hidden but still handle
if ( name === 'mcp' ) {
emit ( ' mcp)' ) ;
emit ( ` COMPREPLY=( $ (compgen -W " ${ optFlags } " -- " $ cur")) ` ) ;
emit ( ' return ;;' ) ;
return ;
}
// Generic command with options only
emit ( ` ${ name } ) ` ) ;
emit ( ` COMPREPLY=( $ (compgen -W " ${ optFlags } " -- " $ cur")) ` ) ;
emit ( ' return ;;' ) ;
}
/** Build the option flags string for a command (for bash COMPREPLY). */
function bashOptFlags ( cmd : CmdInfo ) : string {
const parts : string [ ] = [ ] ;
for ( const opt of cmd . options ) {
if ( opt . short ) parts . push ( opt . short ) ;
parts . push ( opt . long ) ;
}
parts . push ( '-h' , '--help' ) ;
return parts . join ( ' ' ) ;
}
// ============================================================
// Utilities
// ============================================================
/** Escape single quotes in fish strings. */
function esc ( s : string ) : string {
return s . replace ( /'/g , "\\'" ) ;
}
/** Get the first argument name for a command. */
function getFirstArgName ( root : CmdInfo , cmdName : string ) : string {
const cmd = root . subcommands . find ( ( c ) = > c . name === cmdName ) ;
return cmd ? . args [ 0 ] ? . name ? ? '' ;
}
// ============================================================
// Main
// ============================================================
async function main ( ) : Promise < void > {
const mode = process . argv [ 2 ] ? ? '' ;
let tree : CmdInfo ;
try {
tree = await extractTree ( ) ;
} catch ( err ) {
console . error ( 'Failed to extract command tree from createProgram().' ) ;
console . error ( 'Make sure workspace packages are built: pnpm build' ) ;
console . error ( err ) ;
process . exit ( 1 ) ;
}
const fishContent = generateFish ( tree ) ;
const bashContent = generateBash ( tree ) ;
const fishPath = join ( ROOT , 'completions' , 'mcpctl.fish' ) ;
const bashPath = join ( ROOT , 'completions' , 'mcpctl.bash' ) ;
if ( mode === '--check' ) {
let stale = false ;
try {
const currentFish = readFileSync ( fishPath , 'utf-8' ) ;
if ( currentFish !== fishContent ) {
console . error ( 'completions/mcpctl.fish is stale' ) ;
stale = true ;
}
} catch {
console . error ( 'completions/mcpctl.fish does not exist' ) ;
stale = true ;
}
try {
const currentBash = readFileSync ( bashPath , 'utf-8' ) ;
if ( currentBash !== bashContent ) {
console . error ( 'completions/mcpctl.bash is stale' ) ;
stale = true ;
}
} catch {
console . error ( 'completions/mcpctl.bash does not exist' ) ;
stale = true ;
}
if ( stale ) {
console . error ( 'Run: pnpm completions:generate' ) ;
process . exit ( 1 ) ;
}
console . log ( 'Completions are up to date.' ) ;
process . exit ( 0 ) ;
}
if ( mode === '--write' ) {
writeFileSync ( fishPath , fishContent ) ;
writeFileSync ( bashPath , bashContent ) ;
console . log ( ` Wrote ${ fishPath } ` ) ;
console . log ( ` Wrote ${ bashPath } ` ) ;
process . exit ( 0 ) ;
}
// Default: print to stdout
console . log ( '=== completions/mcpctl.fish ===' ) ;
console . log ( fishContent ) ;
console . log ( '=== completions/mcpctl.bash ===' ) ;
console . log ( bashContent ) ;
}
main ( ) . catch ( ( err ) = > {
console . error ( err ) ;
process . exit ( 1 ) ;
} ) ;