feat: ESLint, shell completions, Docker, nfpm packaging, CI/CD
- ESLint with typescript-eslint + prettier (eslint.config.js)
- Shell completions for bash and fish (scripts/generate-completions.ts)
- Multi-stage Dockerfile for bastion (fedora:43 + dnsmasq + node)
- nfpm.yaml for RPM/DEB packaging with bun-compiled binary
- Build scripts: build-rpm.sh, build-bastion.sh, publish-rpm/deb.sh
- Gitea Actions CI/CD: lint, typecheck, test, build, publish
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 21:51:01 +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 , mkdirSync } 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 , '..' ) ;
// ============================================================
// Command tree extraction
// ============================================================
interface CmdInfo {
name : string ;
description : string ;
hidden : boolean ;
options : OptInfo [ ] ;
args : ArgInfo [ ] ;
subcommands : CmdInfo [ ] ;
}
interface OptInfo {
short? : string ;
long : string ;
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 ;
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' )
. map ( extractCommand ) ;
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 > {
const { createProgram } = await import ( '../src/cli/src/index.js' ) as { createProgram : ( ) = > Command } ;
const program = createProgram ( ) ;
return extractCommand ( program ) ;
}
// ============================================================
// Utilities
// ============================================================
function esc ( s : string ) : string {
return s . replace ( /'/g , "\\'" ) ;
}
/** Collect all commands recursively with their full path. */
function collectCommands ( cmd : CmdInfo , prefix : string [ ] = [ ] ) : { path : string [ ] ; cmd : CmdInfo } [ ] {
const result : { path : string [ ] ; cmd : CmdInfo } [ ] = [ ] ;
for ( const sub of cmd . subcommands ) {
const fullPath = [ . . . prefix , sub . name ] ;
result . push ( { path : fullPath , cmd : sub } ) ;
result . push ( . . . collectCommands ( sub , fullPath ) ) ;
}
return result ;
}
// ============================================================
// Fish completion generator
// ============================================================
function generateFish ( root : CmdInfo ) : string {
const lines : string [ ] = [ ] ;
const emit = ( s : string ) : void = > { lines . push ( s ) ; } ;
const BIN = root . name ;
emit ( ` # ${ BIN } fish completions -- auto-generated by scripts/generate-completions.ts ` ) ;
emit ( '# DO NOT EDIT MANUALLY -- run: pnpm completions:generate' ) ;
emit ( '' ) ;
emit ( ` complete -c ${ BIN } -e ` ) ;
emit ( ` complete -c ${ BIN } -f ` ) ;
emit ( '' ) ;
// Global options
emit ( '# Global options' ) ;
emit ( ` complete -c ${ BIN } -s v -l version -d 'Show version' ` ) ;
emit ( ` complete -c ${ BIN } -s h -l help -d 'Show help' ` ) ;
emit ( '' ) ;
const allCmds = collectCommands ( root ) ;
feat: install logging, error trapping, PXE/ISO integration tests
Kickstart installs on real hardware failed silently — no error reporting,
only 3 progress callbacks, zero log streaming. This overhaul makes every
install fully observable.
Kickstart improvements:
- Error trapping in %pre and %post (trap ERR sends failure details to bastion)
- 12+ granular progress stages (was 3): SSH, hostname, k3s prep, EFI boot, metadata
- Background log streamer: tails %post output and batch-sends to /api/log
- bastion_log() function for explicit log lines from kickstart scripts
Bastion API:
- POST /api/log — receives raw log lines from kickstart (single or batch)
- InstallLogBuffer — per-MAC ring buffer (2000 lines) + file persistence
- GET /api/logs/:mac — now returns log_lines + log_total alongside stages
- SSE /api/logs/:mac/follow — uses named events (event: stage vs event: log)
- Progress events forwarded to labd via bastion-progress WebSocket message
- Post-provision k3s logs routed through progressBus (was console-only)
dnsmasq fixes found during VM testing:
- HTTP Boot filename: ipxe-real.efi → ipxe.efi (leftover from old 2-stage approach)
- pxe-service directives: only in proxy mode (breaks OVMF PXE in full mode)
- PXEClient vendor class echo for UEFI firmware compatibility
Integration tests:
- PXE boot test: blank UEFI VM → dnsmasq → HTTP Boot → iPXE → bastion → install
- ISO boot test: blank VM boots from bastion-generated ISO → same flow
- Shared helpers: pxe-network (no DHCP, nftables fix), pxe-vm (UEFI + ISO boot)
- test-provision.sh: runs both PXE + ISO tests with prerequisite checks
- 250GB sparse QCOW2 disk (LVM layout needs ~204GB)
201 unit tests passing (11 new).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 22:26:33 +00:00
// Helper: test if EXACTLY the given subcommand chain is present (for subcommand suggestions)
emit ( '# Helper: test if exactly a subcommand chain is active (no extra positional args)' ) ;
feat: ESLint, shell completions, Docker, nfpm packaging, CI/CD
- ESLint with typescript-eslint + prettier (eslint.config.js)
- Shell completions for bash and fish (scripts/generate-completions.ts)
- Multi-stage Dockerfile for bastion (fedora:43 + dnsmasq + node)
- nfpm.yaml for RPM/DEB packaging with bun-compiled binary
- Build scripts: build-rpm.sh, build-bastion.sh, publish-rpm/deb.sh
- Gitea Actions CI/CD: lint, typecheck, test, build, publish
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 21:51:01 +00:00
emit ( ` function __ ${ BIN } _using_cmd ` ) ;
emit ( ' set -l tokens (commandline -opc)' ) ;
emit ( ' set -l expected $argv' ) ;
emit ( ' set -l depth (count $expected)' ) ;
emit ( ' set -l found 0' ) ;
emit ( ' set -l i 1' ) ;
emit ( ' for tok in $tokens[2..]' ) ;
emit ( ' if string match -q -- "-*" $tok' ) ;
emit ( ' continue' ) ;
emit ( ' end' ) ;
emit ( ' set i (math $i + 1)' ) ;
emit ( ' set -l idx (math $i - 1)' ) ;
emit ( ' if test $idx -le $depth' ) ;
emit ( ' if test "$tok" != "$expected[$idx]"' ) ;
emit ( ' return 1' ) ;
emit ( ' end' ) ;
emit ( ' set found (math $found + 1)' ) ;
emit ( ' else' ) ;
emit ( ' return 1' ) ;
emit ( ' end' ) ;
emit ( ' end' ) ;
emit ( ' test $found -eq $depth' ) ;
emit ( 'end' ) ;
emit ( '' ) ;
feat: install logging, error trapping, PXE/ISO integration tests
Kickstart installs on real hardware failed silently — no error reporting,
only 3 progress callbacks, zero log streaming. This overhaul makes every
install fully observable.
Kickstart improvements:
- Error trapping in %pre and %post (trap ERR sends failure details to bastion)
- 12+ granular progress stages (was 3): SSH, hostname, k3s prep, EFI boot, metadata
- Background log streamer: tails %post output and batch-sends to /api/log
- bastion_log() function for explicit log lines from kickstart scripts
Bastion API:
- POST /api/log — receives raw log lines from kickstart (single or batch)
- InstallLogBuffer — per-MAC ring buffer (2000 lines) + file persistence
- GET /api/logs/:mac — now returns log_lines + log_total alongside stages
- SSE /api/logs/:mac/follow — uses named events (event: stage vs event: log)
- Progress events forwarded to labd via bastion-progress WebSocket message
- Post-provision k3s logs routed through progressBus (was console-only)
dnsmasq fixes found during VM testing:
- HTTP Boot filename: ipxe-real.efi → ipxe.efi (leftover from old 2-stage approach)
- pxe-service directives: only in proxy mode (breaks OVMF PXE in full mode)
- PXEClient vendor class echo for UEFI firmware compatibility
Integration tests:
- PXE boot test: blank UEFI VM → dnsmasq → HTTP Boot → iPXE → bastion → install
- ISO boot test: blank VM boots from bastion-generated ISO → same flow
- Shared helpers: pxe-network (no DHCP, nftables fix), pxe-vm (UEFI + ISO boot)
- test-provision.sh: runs both PXE + ISO tests with prerequisite checks
- 250GB sparse QCOW2 disk (LVM layout needs ~204GB)
201 unit tests passing (11 new).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 22:26:33 +00:00
// Helper: test if command chain STARTS WITH the given prefix (for options that apply after args)
emit ( '# Helper: test if command starts with a subcommand chain (options still apply after args)' ) ;
emit ( ` function __ ${ BIN } _in_cmd ` ) ;
emit ( ' set -l tokens (commandline -opc)' ) ;
emit ( ' set -l expected $argv' ) ;
emit ( ' set -l depth (count $expected)' ) ;
emit ( ' set -l found 0' ) ;
emit ( ' for tok in $tokens[2..]' ) ;
emit ( ' if string match -q -- "-*" $tok' ) ;
emit ( ' continue' ) ;
emit ( ' end' ) ;
emit ( ' set found (math $found + 1)' ) ;
emit ( ' if test $found -le $depth' ) ;
emit ( ' if test "$tok" != "$expected[$found]"' ) ;
emit ( ' return 1' ) ;
emit ( ' end' ) ;
emit ( ' end' ) ;
emit ( ' end' ) ;
emit ( ' test $found -ge $depth' ) ;
emit ( 'end' ) ;
emit ( '' ) ;
// Dynamic completions: fetch machine data from bastion API
emit ( '# Dynamic: fetch machine hostnames from bastion (installed + queued)' ) ;
emit ( ` function __ ${ BIN } _installed_hosts ` ) ;
emit ( ' curl -s http://localhost:8080/api/machines 2>/dev/null | ' ) ;
emit ( " python3 -c 'import sys,json; d=json.load(sys.stdin); hosts=[v.get(\"hostname\",\"\") for v in {**d.get(\"install_queue\",{}), **d.get(\"installed\",{})}.values() if v.get(\"hostname\")]; [print(h) for h in set(hosts)]' 2>/dev/null" ) ;
emit ( 'end' ) ;
emit ( '' ) ;
emit ( '# Dynamic: fetch all known MAC addresses (discovered + queue + installed)' ) ;
emit ( ` function __ ${ BIN } _known_macs ` ) ;
emit ( ' curl -s http://localhost:8080/api/machines 2>/dev/null | ' ) ;
emit ( " python3 -c 'import sys,json; d=json.load(sys.stdin); [print(k) for k in {**d.get(\"discovered\",{}), **d.get(\"install_queue\",{}), **d.get(\"installed\",{})}]' 2>/dev/null" ) ;
emit ( 'end' ) ;
emit ( '' ) ;
emit ( '# Dynamic: fetch hostnames and MACs from all states' ) ;
emit ( ` function __ ${ BIN } _hosts_and_macs ` ) ;
emit ( ' curl -s http://localhost:8080/api/machines 2>/dev/null | ' ) ;
emit ( " python3 -c 'import sys,json; d=json.load(sys.stdin); a={**d.get(\"discovered\",{}), **d.get(\"install_queue\",{}), **d.get(\"installed\",{})}; macs=list(a.keys()); hosts=[v.get(\"hostname\",\"\") for v in {**d.get(\"install_queue\",{}), **d.get(\"installed\",{})}.values() if v.get(\"hostname\")]; [print(x) for x in set(macs+hosts)]' 2>/dev/null" ) ;
emit ( 'end' ) ;
emit ( '' ) ;
// Target completions for commands that accept hostname/IP/MAC
emit ( '# Target argument completions' ) ;
// app k3s — takes hostname/IP
emit ( ` complete -c ${ BIN } -n "__ ${ BIN } _using_cmd app k3s install" -a "(__ ${ BIN } _installed_hosts)" -d 'installed host' ` ) ;
emit ( ` complete -c ${ BIN } -n "__ ${ BIN } _using_cmd app k3s health" -a "(__ ${ BIN } _installed_hosts)" -d 'installed host' ` ) ;
emit ( ` complete -c ${ BIN } -n "__ ${ BIN } _using_cmd app labcontroller deploy" -a "(__ ${ BIN } _installed_hosts)" -d 'installed host' ` ) ;
emit ( ` complete -c ${ BIN } -n "__ ${ BIN } _using_cmd app labcontroller status" -a "(__ ${ BIN } _installed_hosts)" -d 'installed host' ` ) ;
// provision install — takes MAC then hostname
emit ( ` complete -c ${ BIN } -n "__ ${ BIN } _using_cmd provision install" -a "(__ ${ BIN } _known_macs)" -d 'MAC address' ` ) ;
// provision reprovision/forget/logs — takes MAC or hostname
emit ( ` complete -c ${ BIN } -n "__ ${ BIN } _using_cmd provision reprovision" -a "(__ ${ BIN } _hosts_and_macs)" -d 'host or MAC' ` ) ;
emit ( ` complete -c ${ BIN } -n "__ ${ BIN } _using_cmd provision forget" -a "(__ ${ BIN } _hosts_and_macs)" -d 'host or MAC' ` ) ;
emit ( ` complete -c ${ BIN } -n "__ ${ BIN } _using_cmd provision logs" -a "(__ ${ BIN } _hosts_and_macs)" -d 'host or MAC' ` ) ;
emit ( '' ) ;
feat: ESLint, shell completions, Docker, nfpm packaging, CI/CD
- ESLint with typescript-eslint + prettier (eslint.config.js)
- Shell completions for bash and fish (scripts/generate-completions.ts)
- Multi-stage Dockerfile for bastion (fedora:43 + dnsmasq + node)
- nfpm.yaml for RPM/DEB packaging with bun-compiled binary
- Build scripts: build-rpm.sh, build-bastion.sh, publish-rpm/deb.sh
- Gitea Actions CI/CD: lint, typecheck, test, build, publish
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 21:51:01 +00:00
// Top-level commands
const topCmds = root . subcommands . filter ( ( c ) = > ! c . hidden ) ;
emit ( '# Top-level commands' ) ;
for ( const cmd of topCmds ) {
emit ( ` complete -c ${ BIN } -n "not __fish_seen_subcommand_from ${ topCmds . map ( ( c ) = > c . name ) . join ( ' ' ) } " -a ${ cmd . name } -d ' ${ esc ( cmd . description ) } ' ` ) ;
}
emit ( '' ) ;
// Subcommands and options at each level
for ( const { path , cmd } of allCmds ) {
if ( cmd . hidden ) continue ;
// If this command has subcommands, offer them
const visibleSubs = cmd . subcommands . filter ( ( s ) = > ! s . hidden ) ;
if ( visibleSubs . length > 0 ) {
const parentCondition = ` __ ${ BIN } _using_cmd ${ path . join ( ' ' ) } ` ;
emit ( ` # ${ path . join ( ' ' ) } subcommands ` ) ;
for ( const sub of visibleSubs ) {
emit ( ` complete -c ${ BIN } -n " ${ parentCondition } " -a ${ sub . name } -d ' ${ esc ( sub . description ) } ' ` ) ;
}
emit ( '' ) ;
}
feat: install logging, error trapping, PXE/ISO integration tests
Kickstart installs on real hardware failed silently — no error reporting,
only 3 progress callbacks, zero log streaming. This overhaul makes every
install fully observable.
Kickstart improvements:
- Error trapping in %pre and %post (trap ERR sends failure details to bastion)
- 12+ granular progress stages (was 3): SSH, hostname, k3s prep, EFI boot, metadata
- Background log streamer: tails %post output and batch-sends to /api/log
- bastion_log() function for explicit log lines from kickstart scripts
Bastion API:
- POST /api/log — receives raw log lines from kickstart (single or batch)
- InstallLogBuffer — per-MAC ring buffer (2000 lines) + file persistence
- GET /api/logs/:mac — now returns log_lines + log_total alongside stages
- SSE /api/logs/:mac/follow — uses named events (event: stage vs event: log)
- Progress events forwarded to labd via bastion-progress WebSocket message
- Post-provision k3s logs routed through progressBus (was console-only)
dnsmasq fixes found during VM testing:
- HTTP Boot filename: ipxe-real.efi → ipxe.efi (leftover from old 2-stage approach)
- pxe-service directives: only in proxy mode (breaks OVMF PXE in full mode)
- PXEClient vendor class echo for UEFI firmware compatibility
Integration tests:
- PXE boot test: blank UEFI VM → dnsmasq → HTTP Boot → iPXE → bastion → install
- ISO boot test: blank VM boots from bastion-generated ISO → same flow
- Shared helpers: pxe-network (no DHCP, nftables fix), pxe-vm (UEFI + ISO boot)
- test-provision.sh: runs both PXE + ISO tests with prerequisite checks
- 250GB sparse QCOW2 disk (LVM layout needs ~204GB)
201 unit tests passing (11 new).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 22:26:33 +00:00
// Options for this command (use __in_cmd so options complete even after positional args)
feat: ESLint, shell completions, Docker, nfpm packaging, CI/CD
- ESLint with typescript-eslint + prettier (eslint.config.js)
- Shell completions for bash and fish (scripts/generate-completions.ts)
- Multi-stage Dockerfile for bastion (fedora:43 + dnsmasq + node)
- nfpm.yaml for RPM/DEB packaging with bun-compiled binary
- Build scripts: build-rpm.sh, build-bastion.sh, publish-rpm/deb.sh
- Gitea Actions CI/CD: lint, typecheck, test, build, publish
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 21:51:01 +00:00
if ( cmd . options . length > 0 ) {
feat: install logging, error trapping, PXE/ISO integration tests
Kickstart installs on real hardware failed silently — no error reporting,
only 3 progress callbacks, zero log streaming. This overhaul makes every
install fully observable.
Kickstart improvements:
- Error trapping in %pre and %post (trap ERR sends failure details to bastion)
- 12+ granular progress stages (was 3): SSH, hostname, k3s prep, EFI boot, metadata
- Background log streamer: tails %post output and batch-sends to /api/log
- bastion_log() function for explicit log lines from kickstart scripts
Bastion API:
- POST /api/log — receives raw log lines from kickstart (single or batch)
- InstallLogBuffer — per-MAC ring buffer (2000 lines) + file persistence
- GET /api/logs/:mac — now returns log_lines + log_total alongside stages
- SSE /api/logs/:mac/follow — uses named events (event: stage vs event: log)
- Progress events forwarded to labd via bastion-progress WebSocket message
- Post-provision k3s logs routed through progressBus (was console-only)
dnsmasq fixes found during VM testing:
- HTTP Boot filename: ipxe-real.efi → ipxe.efi (leftover from old 2-stage approach)
- pxe-service directives: only in proxy mode (breaks OVMF PXE in full mode)
- PXEClient vendor class echo for UEFI firmware compatibility
Integration tests:
- PXE boot test: blank UEFI VM → dnsmasq → HTTP Boot → iPXE → bastion → install
- ISO boot test: blank VM boots from bastion-generated ISO → same flow
- Shared helpers: pxe-network (no DHCP, nftables fix), pxe-vm (UEFI + ISO boot)
- test-provision.sh: runs both PXE + ISO tests with prerequisite checks
- 250GB sparse QCOW2 disk (LVM layout needs ~204GB)
201 unit tests passing (11 new).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 22:26:33 +00:00
const condition = ` __ ${ BIN } _in_cmd ${ path . join ( ' ' ) } ` ;
feat: ESLint, shell completions, Docker, nfpm packaging, CI/CD
- ESLint with typescript-eslint + prettier (eslint.config.js)
- Shell completions for bash and fish (scripts/generate-completions.ts)
- Multi-stage Dockerfile for bastion (fedora:43 + dnsmasq + node)
- nfpm.yaml for RPM/DEB packaging with bun-compiled binary
- Build scripts: build-rpm.sh, build-bastion.sh, publish-rpm/deb.sh
- Gitea Actions CI/CD: lint, typecheck, test, build, publish
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 21:51:01 +00:00
emit ( ` # ${ path . join ( ' ' ) } options ` ) ;
for ( const opt of cmd . options ) {
const parts = [ ` complete -c ${ BIN } -n " ${ condition } " ` ] ;
if ( opt . short ) parts . push ( ` -s ${ opt . short . replace ( '-' , '' ) } ` ) ;
parts . push ( ` -l ${ opt . long . replace ( /^--/ , '' ) } ` ) ;
parts . push ( ` -d ' ${ esc ( opt . description ) } ' ` ) ;
if ( opt . takesValue ) {
if ( opt . choices ) {
parts . push ( ` -xa ' ${ opt . choices . join ( ' ' ) } ' ` ) ;
} else {
parts . push ( '-x' ) ;
}
}
emit ( parts . join ( ' ' ) ) ;
}
emit ( '' ) ;
}
}
return lines . join ( '\n' ) + '\n' ;
}
// ============================================================
// Bash completion generator
// ============================================================
function generateBash ( root : CmdInfo ) : string {
const lines : string [ ] = [ ] ;
const emit = ( s : string ) : void = > { lines . push ( s ) ; } ;
const BIN = root . name ;
emit ( ` # ${ BIN } bash completions -- auto-generated by scripts/generate-completions.ts ` ) ;
emit ( '# DO NOT EDIT MANUALLY -- run: pnpm completions:generate' ) ;
emit ( '' ) ;
const allCmds = collectCommands ( root ) ;
const topCmds = root . subcommands . filter ( ( c ) = > ! c . hidden ) . map ( ( c ) = > c . name ) ;
emit ( ` _ ${ BIN } () { ` ) ;
emit ( ' local cur prev words cword' ) ;
emit ( ' _init_completion || return' ) ;
emit ( '' ) ;
emit ( ` local top_commands=" ${ topCmds . join ( ' ' ) } " ` ) ;
emit ( '' ) ;
// Build chain of subcommands from command line
emit ( ' # Extract the subcommand chain (skip options and their values)' ) ;
emit ( ' local -a subcmd_chain=()' ) ;
emit ( ' local i skip_next=false' ) ;
emit ( ' for ((i=1; i < cword; i++)); do' ) ;
emit ( ' if $skip_next; then skip_next=false; continue; fi' ) ;
emit ( ' case "${words[i]}" in' ) ;
emit ( ' -*) ;; # skip options' ) ;
emit ( ' *) subcmd_chain+=("${words[i]}") ;;' ) ;
emit ( ' esac' ) ;
emit ( ' done' ) ;
emit ( '' ) ;
emit ( ' local chain_len=${#subcmd_chain[@]}' ) ;
emit ( ' local chain_str="${subcmd_chain[*]}"' ) ;
emit ( '' ) ;
// Build case statement for each command path
emit ( ' case "$chain_str" in' ) ;
// Start with the deepest paths first to match longest
const sortedCmds = [ . . . allCmds ] . sort ( ( a , b ) = > b . path . length - a . path . length ) ;
for ( const { path , cmd } of sortedCmds ) {
if ( cmd . hidden ) continue ;
const pathStr = path . join ( ' ' ) ;
const visibleSubs = cmd . subcommands . filter ( ( s ) = > ! s . hidden ) . map ( ( s ) = > s . name ) ;
const optFlags : string [ ] = [ ] ;
for ( const opt of cmd . options ) {
if ( opt . short ) optFlags . push ( opt . short ) ;
optFlags . push ( opt . long ) ;
}
optFlags . push ( '-h' , '--help' ) ;
const completions = [ . . . visibleSubs , . . . optFlags ] . join ( ' ' ) ;
emit ( ` " ${ pathStr } ") ` ) ;
emit ( ` COMPREPLY=( $ (compgen -W " ${ completions } " -- " $ cur")) ` ) ;
emit ( ' return ;;' ) ;
}
// Top-level (no subcommand yet)
emit ( ' "")' ) ;
emit ( ` COMPREPLY=( $ (compgen -W " $ top_commands -h --help -v --version" -- " $ cur")) ` ) ;
emit ( ' return ;;' ) ;
// Default
emit ( ' *)' ) ;
emit ( ' COMPREPLY=($(compgen -W "-h --help" -- "$cur"))' ) ;
emit ( ' return ;;' ) ;
emit ( ' esac' ) ;
emit ( '}' ) ;
emit ( '' ) ;
emit ( ` complete -F _ ${ BIN } ${ BIN } ` ) ;
return lines . join ( '\n' ) + '\n' ;
}
// ============================================================
// 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 completionsDir = join ( ROOT , 'completions' ) ;
2026-03-18 00:07:17 +00:00
const fishPath = join ( completionsDir , 'labctl.fish' ) ;
const bashPath = join ( completionsDir , 'labctl.bash' ) ;
feat: ESLint, shell completions, Docker, nfpm packaging, CI/CD
- ESLint with typescript-eslint + prettier (eslint.config.js)
- Shell completions for bash and fish (scripts/generate-completions.ts)
- Multi-stage Dockerfile for bastion (fedora:43 + dnsmasq + node)
- nfpm.yaml for RPM/DEB packaging with bun-compiled binary
- Build scripts: build-rpm.sh, build-bastion.sh, publish-rpm/deb.sh
- Gitea Actions CI/CD: lint, typecheck, test, build, publish
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 21:51:01 +00:00
if ( mode === '--check' ) {
let stale = false ;
try {
const currentFish = readFileSync ( fishPath , 'utf-8' ) ;
if ( currentFish !== fishContent ) {
2026-03-18 00:07:17 +00:00
console . error ( 'completions/labctl.fish is stale' ) ;
feat: ESLint, shell completions, Docker, nfpm packaging, CI/CD
- ESLint with typescript-eslint + prettier (eslint.config.js)
- Shell completions for bash and fish (scripts/generate-completions.ts)
- Multi-stage Dockerfile for bastion (fedora:43 + dnsmasq + node)
- nfpm.yaml for RPM/DEB packaging with bun-compiled binary
- Build scripts: build-rpm.sh, build-bastion.sh, publish-rpm/deb.sh
- Gitea Actions CI/CD: lint, typecheck, test, build, publish
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 21:51:01 +00:00
stale = true ;
}
} catch {
2026-03-18 00:07:17 +00:00
console . error ( 'completions/labctl.fish does not exist' ) ;
feat: ESLint, shell completions, Docker, nfpm packaging, CI/CD
- ESLint with typescript-eslint + prettier (eslint.config.js)
- Shell completions for bash and fish (scripts/generate-completions.ts)
- Multi-stage Dockerfile for bastion (fedora:43 + dnsmasq + node)
- nfpm.yaml for RPM/DEB packaging with bun-compiled binary
- Build scripts: build-rpm.sh, build-bastion.sh, publish-rpm/deb.sh
- Gitea Actions CI/CD: lint, typecheck, test, build, publish
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 21:51:01 +00:00
stale = true ;
}
try {
const currentBash = readFileSync ( bashPath , 'utf-8' ) ;
if ( currentBash !== bashContent ) {
2026-03-18 00:07:17 +00:00
console . error ( 'completions/labctl.bash is stale' ) ;
feat: ESLint, shell completions, Docker, nfpm packaging, CI/CD
- ESLint with typescript-eslint + prettier (eslint.config.js)
- Shell completions for bash and fish (scripts/generate-completions.ts)
- Multi-stage Dockerfile for bastion (fedora:43 + dnsmasq + node)
- nfpm.yaml for RPM/DEB packaging with bun-compiled binary
- Build scripts: build-rpm.sh, build-bastion.sh, publish-rpm/deb.sh
- Gitea Actions CI/CD: lint, typecheck, test, build, publish
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 21:51:01 +00:00
stale = true ;
}
} catch {
2026-03-18 00:07:17 +00:00
console . error ( 'completions/labctl.bash does not exist' ) ;
feat: ESLint, shell completions, Docker, nfpm packaging, CI/CD
- ESLint with typescript-eslint + prettier (eslint.config.js)
- Shell completions for bash and fish (scripts/generate-completions.ts)
- Multi-stage Dockerfile for bastion (fedora:43 + dnsmasq + node)
- nfpm.yaml for RPM/DEB packaging with bun-compiled binary
- Build scripts: build-rpm.sh, build-bastion.sh, publish-rpm/deb.sh
- Gitea Actions CI/CD: lint, typecheck, test, build, publish
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 21:51:01 +00:00
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' ) {
mkdirSync ( completionsDir , { recursive : true } ) ;
writeFileSync ( fishPath , fishContent ) ;
writeFileSync ( bashPath , bashContent ) ;
console . log ( ` Wrote ${ fishPath } ` ) ;
console . log ( ` Wrote ${ bashPath } ` ) ;
process . exit ( 0 ) ;
}
// Default: print to stdout
2026-03-18 00:07:17 +00:00
console . log ( '=== completions/labctl.fish ===' ) ;
feat: ESLint, shell completions, Docker, nfpm packaging, CI/CD
- ESLint with typescript-eslint + prettier (eslint.config.js)
- Shell completions for bash and fish (scripts/generate-completions.ts)
- Multi-stage Dockerfile for bastion (fedora:43 + dnsmasq + node)
- nfpm.yaml for RPM/DEB packaging with bun-compiled binary
- Build scripts: build-rpm.sh, build-bastion.sh, publish-rpm/deb.sh
- Gitea Actions CI/CD: lint, typecheck, test, build, publish
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 21:51:01 +00:00
console . log ( fishContent ) ;
2026-03-18 00:07:17 +00:00
console . log ( '=== completions/labctl.bash ===' ) ;
feat: ESLint, shell completions, Docker, nfpm packaging, CI/CD
- ESLint with typescript-eslint + prettier (eslint.config.js)
- Shell completions for bash and fish (scripts/generate-completions.ts)
- Multi-stage Dockerfile for bastion (fedora:43 + dnsmasq + node)
- nfpm.yaml for RPM/DEB packaging with bun-compiled binary
- Build scripts: build-rpm.sh, build-bastion.sh, publish-rpm/deb.sh
- Gitea Actions CI/CD: lint, typecheck, test, build, publish
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 21:51:01 +00:00
console . log ( bashContent ) ;
}
main ( ) . catch ( ( err ) = > {
console . error ( err ) ;
process . exit ( 1 ) ;
} ) ;