Files
openchamber/packages/web/bin/lib/cli-args.js
T

727 lines
27 KiB
JavaScript

import { TunnelCliError, EXIT_CODE } from './cli-errors.js';
const DEFAULT_PORT = 3000;
const DEFAULT_TAIL_LINES = 200;
function levenshteinDistance(a, b) {
const m = a.length;
const n = b.length;
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
for (let i = 0; i <= m; i++) dp[i][0] = i;
for (let j = 0; j <= n; j++) dp[0][j] = j;
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
dp[i][j] = a[i - 1] === b[j - 1]
? dp[i - 1][j - 1]
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
}
}
return dp[m][n];
}
function findClosestMatch(input, candidates, maxDistance = 3) {
if (typeof input !== 'string' || input.length === 0 || !Array.isArray(candidates)) {
return null;
}
const normalized = input.toLowerCase();
let bestCandidate = null;
let bestDistance = maxDistance + 1;
for (const candidate of candidates) {
const distance = levenshteinDistance(normalized, candidate.toLowerCase());
if (distance < bestDistance) {
bestDistance = distance;
bestCandidate = candidate;
}
}
return bestDistance <= maxDistance ? bestCandidate : null;
}
function splitOptionToken(arg) {
if (!arg.startsWith('-')) return null;
if (arg.startsWith('--')) {
const eqIndex = arg.indexOf('=');
return {
name: eqIndex >= 0 ? arg.slice(2, eqIndex) : arg.slice(2),
inlineValue: eqIndex >= 0 ? arg.slice(eqIndex + 1) : undefined,
long: true,
};
}
return {
name: arg.slice(1),
inlineValue: undefined,
long: false,
};
}
function parseArgs(argv = process.argv.slice(2)) {
const args = Array.isArray(argv) ? [...argv] : [];
const options = {
port: DEFAULT_PORT,
host: undefined,
uiPassword: process.env.OPENCHAMBER_UI_PASSWORD || undefined,
json: false,
all: false,
follow: true,
lines: DEFAULT_TAIL_LINES,
provider: undefined,
mode: undefined,
profile: undefined,
name: undefined,
configPath: undefined,
token: undefined,
tokenFile: undefined,
tokenStdin: false,
hostname: undefined,
server: undefined,
connectTtl: undefined,
sessionTtl: undefined,
qr: false,
explicitQr: false,
force: false,
showSecrets: false,
dryRun: false,
plain: false,
quiet: false,
explicitPort: false,
explicitUiPassword: false,
envSnapshot: true,
foreground: false,
lan: false,
apiOnly: false,
};
const removedFlagErrors = [];
const positional = [];
let helpRequested = false;
let versionRequested = false;
const consumeValue = (index, inlineValue) => {
if (typeof inlineValue === 'string' && inlineValue.length > 0) {
return { value: inlineValue, nextIndex: index };
}
const candidate = args[index + 1];
if (typeof candidate === 'string' && !candidate.startsWith('-')) {
return { value: candidate, nextIndex: index + 1 };
}
return { value: undefined, nextIndex: index };
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
const parsedToken = splitOptionToken(arg);
if (!parsedToken) {
positional.push(arg);
continue;
}
const { name, inlineValue, long } = parsedToken;
switch (name) {
case 'port':
case 'p': {
const { value: consumedValue, nextIndex: consumedIndex } = consumeValue(i, inlineValue);
let value = consumedValue;
let nextIndex = consumedIndex;
// Support explicit negative numeric values like `-p -1` so we can report
// a clear range validation error instead of "Unknown option".
if (value === undefined && typeof inlineValue !== 'string') {
const candidate = args[i + 1];
if (typeof candidate === 'string' && /^-\d+$/.test(candidate)) {
value = candidate;
nextIndex = i + 1;
}
}
i = nextIndex;
if (typeof value !== 'string' || value.trim().length === 0) {
throw new TunnelCliError('Missing value for --port.', EXIT_CODE.USAGE_ERROR);
}
if (!/^-?\d+$/.test(value.trim())) {
throw new TunnelCliError(`Invalid port value: ${value}`, EXIT_CODE.USAGE_ERROR);
}
const parsed = parseInt(value, 10);
if (parsed < 1 || parsed > 65535) {
throw new TunnelCliError(`Invalid port value: ${parsed}`, EXIT_CODE.USAGE_ERROR);
}
options.port = parsed;
options.explicitPort = true;
break;
}
case 'host': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
if (typeof value !== 'string' || value.trim().length === 0) {
throw new TunnelCliError('Missing value for --host.', EXIT_CODE.USAGE_ERROR);
}
options.host = value.trim();
break;
}
case 'lan':
options.lan = true;
break;
case 'ui-password': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.uiPassword = typeof value === 'string' ? value : '';
options.explicitUiPassword = true;
break;
}
case 'provider': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.provider = typeof value === 'string' ? value : options.provider;
break;
}
case 'mode': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.mode = typeof value === 'string' ? value : options.mode;
break;
}
case 'profile': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.profile = typeof value === 'string' ? value : options.profile;
break;
}
case 'name': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.name = typeof value === 'string' ? value : options.name;
break;
}
case 'config': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.configPath = typeof value === 'string' ? value : null;
break;
}
case 'token': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.token = typeof value === 'string' ? value : options.token;
break;
}
case 'token-file': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.tokenFile = typeof value === 'string' ? value : options.tokenFile;
break;
}
case 'token-stdin':
options.tokenStdin = true;
break;
case 'hostname': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.hostname = typeof value === 'string' ? value : options.hostname;
break;
}
case 'server':
case 'server-url': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
if (typeof value !== 'string' || value.trim().length === 0) {
throw new TunnelCliError('Missing value for --server.', EXIT_CODE.USAGE_ERROR);
}
options.server = value.trim();
break;
}
case 'connect-ttl': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.connectTtl = typeof value === 'string' ? value : options.connectTtl;
break;
}
case 'session-ttl': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.sessionTtl = typeof value === 'string' ? value : options.sessionTtl;
break;
}
case 'json':
options.json = true;
break;
case 'all':
options.all = true;
break;
case 'no-follow':
options.follow = false;
break;
case 'no-env-snapshot':
options.envSnapshot = false;
break;
case 'lines': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
const parsed = parseInt(value ?? '', 10);
if (Number.isFinite(parsed) && parsed > 0) {
options.lines = parsed;
}
break;
}
case 'qr':
options.qr = true;
options.explicitQr = true;
break;
case 'no-qr':
options.qr = false;
options.explicitQr = true;
break;
case 'force':
options.force = true;
break;
case 'show-secrets':
options.showSecrets = true;
break;
case 'dry-run':
options.dryRun = true;
break;
case 'plain':
options.plain = true;
break;
case 'quiet':
case 'q':
options.quiet = true;
break;
case 'help':
case 'h':
helpRequested = true;
break;
case 'version':
case 'v':
versionRequested = true;
break;
case 'foreground':
case 'no-daemon':
options.foreground = true;
break;
case 'api-only':
options.apiOnly = true;
break;
case 'daemon':
case 'd':
// Legacy no-op: daemon mode is already the default, but older clients
// may still pass this when starting a remote server.
break;
case 'try-cf-tunnel':
removedFlagErrors.push('`--try-cf-tunnel` was removed. Use: openchamber tunnel start --provider cloudflare --mode quick');
break;
case 'tunnel-qr':
removedFlagErrors.push('`--tunnel-qr` was removed. Use: openchamber tunnel start ... --qr');
break;
case 'tunnel-password-url':
removedFlagErrors.push('`--tunnel-password-url` was removed. Use UI password auth directly after tunnel start.');
break;
case 'tunnel-provider':
case 'tunnel-mode':
case 'tunnel-config':
case 'tunnel-token':
case 'tunnel-hostname':
case 'tunnel':
removedFlagErrors.push(`\`--${name}\` was removed from top-level serve flow. Use: openchamber tunnel start ...`);
break;
default:
if (!long && name.length === 1) {
removedFlagErrors.push(`Unknown option: -${name}`);
} else {
removedFlagErrors.push(`Unknown option: --${name}`);
}
break;
}
}
const command = positional[0] || 'serve';
const subcommand = command === 'tunnel' ? (positional[1] || 'help') : null;
const tunnelAction = command === 'tunnel' ? (positional[2] || null) : null;
const startupAction = command === 'startup' ? (positional[1] || 'status') : null;
if (options.lan && typeof options.host !== 'string') {
options.host = '0.0.0.0';
}
if (command !== 'tunnel' && typeof options.hostname === 'string' && typeof options.host !== 'string') {
options.host = options.hostname;
}
return {
command,
subcommand,
tunnelAction,
startupAction,
options,
removedFlagErrors,
helpRequested,
versionRequested,
};
}
function showHelp() {
console.log(`
OpenChamber - Web interface for the OpenCode AI coding agent
USAGE:
openchamber [COMMAND] [OPTIONS]
COMMANDS:
serve Start the web server (daemon default)
stop Stop running instance(s)
restart Stop and start the server
status Show server status
tunnel Tunnel lifecycle commands
startup Manage launch at system startup
logs Tail OpenChamber logs
connect-url Generate URL/QR for connecting another client
update Check for and install updates
OPTIONS:
-p, --port Web server port (default: ${DEFAULT_PORT})
--host Bind address (default: 127.0.0.1)
--hostname Alias for --host outside tunnel commands
--lan Bind to 0.0.0.0 for LAN access
--server <url> Public/server URL for connect-url links
--ui-password Protect browser UI with single password
--api-only Start API routes only, without serving browser UI assets
--foreground Run server in foreground (use with systemd/process managers)
--no-daemon Alias for --foreground
-h, --help Show help
-v, --version Show version
ENVIRONMENT:
OPENCHAMBER_HOST Bind address (e.g. 0.0.0.0 for all interfaces)
OPENCHAMBER_UI_PASSWORD Alternative to --ui-password flag
OPENCHAMBER_API_ONLY Set to true/1 to start API routes only
OPENCHAMBER_DATA_DIR Override OpenChamber data directory
OPENCODE_HOST External OpenCode server base URL, e.g. http://hostname:4096
OPENCODE_PORT Port of external OpenCode server to connect to
OPENCODE_SKIP_START Skip starting OpenCode, use external server
OPENCHAMBER_OPENCODE_HOSTNAME Bind hostname for managed OpenCode server (default: 127.0.0.1)
EXAMPLES:
openchamber # Start in daemon mode on default port 3000 (or free port)
openchamber --port 8080 # Start on port 8080 (daemon)
openchamber --lan --port 3002 # Start on LAN at 0.0.0.0:3002
openchamber serve --foreground # Start in foreground (for systemd Type=simple)
openchamber connect-url --port 3000 --qr
openchamber connect-url --server https://openchamber.example.com
openchamber startup enable # Start OpenChamber at user login
openchamber tunnel help # Show tunnel lifecycle help
openchamber logs # Follow logs for latest running instance
`);
}
function showStartupHelp() {
console.log(`
OpenChamber Startup Commands
USAGE:
openchamber startup <SUBCOMMAND> [OPTIONS]
SUBCOMMANDS:
status Show startup integration status
enable Install and start native user startup integration
disable Stop and remove native user startup integration
OPTIONS:
-p, --port Web server port used by startup service
--host Bind address used by startup service
--ui-password Protect browser UI with single password
--api-only Start API routes only, without serving browser UI assets
--no-env-snapshot Do not save current environment for startup service
--json Output machine-readable JSON
-q, --quiet Suppress non-essential output
EXAMPLES:
openchamber startup enable
openchamber startup enable --port 3000
openchamber startup enable --port 3000 --api-only --host 0.0.0.0
openchamber startup status --json
`);
}
function showConnectUrlHelp() {
console.log(`
OpenChamber Connect URL
USAGE:
openchamber connect-url [OPTIONS]
DESCRIPTION:
Generate an openchamber:// connection link for adding this server to another
OpenChamber app. If no server is running on the selected port, it starts one.
OPTIONS:
-p, --port <port> Server port to use or start (default: ${DEFAULT_PORT})
--host <address> Bind address when starting the server
--hostname <address> Alias for --host
--lan Bind to 0.0.0.0 for LAN access when starting
--server <url> Public URL saved into the connection link
--server-url <url> Alias for --server
--name <label> Label saved with the remote client token
--ui-password <value> Protect browser access when UI routes are enabled
--api-only Start in headless/API-only mode when starting
--qr Print a QR code for the connection link
--json Output machine-readable JSON
-q, --quiet Print only the connection link
-h, --help Show this help
EXAMPLES:
openchamber connect-url --port 3000 --qr
openchamber connect-url --port 3000 --api-only --lan --server http://workstation.local:3000 --qr
openchamber connect-url --server https://openchamber.example.com --name Workstation
`);
}
function showTunnelHelp() {
console.log(`
Tunnel Lifecycle Commands
USAGE:
openchamber tunnel <SUBCOMMAND> [OPTIONS]
SUBCOMMANDS:
help Show this tunnel help
providers Show available tunnel providers and capabilities
ready Check tunnel readiness for a provider
doctor Run deep tunnel diagnostics
status Show tunnel status
start Start a tunnel
stop Stop active tunnel (keep server running)
profile Manage saved managed-remote profiles
COMMON OPTIONS:
-p, --port Target OpenChamber instance port
--host Bind address when auto-starting an instance
--lan Bind to 0.0.0.0 when auto-starting an instance
--ui-password Protect browser UI when auto-starting an instance
--api-only Start API routes only when auto-starting an instance
--json Output machine-readable JSON
--all Apply to all running instances (doctor default, stop)
START OPTIONS:
--provider <id> Tunnel provider id (default: cloudflare)
--mode <id> Tunnel mode (default: quick)
--profile <name> Start tunnel from saved profile name
--config [path] Managed-local config path (optional)
--token <token> Managed-remote token (visible in process list)
--token-file <path> Read token from file (recommended)
--token-stdin Read token from stdin
--hostname <hostname> Managed-remote hostname
--connect-ttl <value> Connect-link TTL (e.g. 30m, 24h, 1d)
--session-ttl <value> Session TTL (e.g. 8h, 24h, 1d)
--qr Print QR code for resulting tunnel URL
--no-qr Disable QR output
--dry-run Validate inputs without applying changes
OUTPUT OPTIONS:
--show-secrets Show full tokens in output (default: redacted)
--plain Disable colors and decorations
-q, --quiet Suppress non-essential output
--json Output machine-readable JSON
BEHAVIOR NOTES:
- One active tunnel per OpenChamber instance.
- Starting a different mode/provider replaces the current tunnel and revokes old connect links/sessions.
- Connect links are one-time; generating a new link revokes the previous unused link.
PROFILE USAGE:
openchamber tunnel profile list [--provider <id>] [--json]
openchamber tunnel profile show --name <name> [--provider <id>] [--json]
openchamber tunnel profile add --provider <id> --mode managed-remote --name <name> --hostname <host> --token <token> [--force] [--json]
openchamber tunnel profile add --provider <id> --mode managed-remote --name <name> --hostname <host> --token-file <path> [--force] [--json]
openchamber tunnel profile remove --name <name> [--provider <id>] [--json]
SHELL COMPLETION:
openchamber tunnel completion bash Generate Bash completion script
openchamber tunnel completion zsh Generate Zsh completion script
openchamber tunnel completion fish Generate Fish completion script
EXAMPLES:
openchamber tunnel providers
openchamber tunnel ready --provider cloudflare
openchamber tunnel doctor --provider cloudflare
openchamber tunnel status
openchamber tunnel start --qr
openchamber tunnel start --profile prod-main
openchamber tunnel start --provider cloudflare --mode managed-remote --token-file ~/.secrets/cf-token --hostname app.example.com
openchamber tunnel start --provider cloudflare --mode managed-local --config ~/.cloudflared/config.yml
openchamber tunnel start --dry-run --provider cloudflare --mode managed-remote --token-file ~/.secrets/cf-token --hostname app.example.com
echo "$TOKEN" | openchamber tunnel profile add --provider cloudflare --mode managed-remote --name prod-main --hostname app.example.com --token-stdin
openchamber tunnel profile list --provider cloudflare
openchamber tunnel profile list --json --show-secrets
openchamber tunnel stop --port 3000
`);
}
function generateCompletionScript(shell) {
const normalized = typeof shell === 'string' ? shell.trim().toLowerCase() : '';
if (normalized === 'bash') {
return `# Bash completion for openchamber tunnel
# Add to ~/.bashrc: eval "$(openchamber tunnel completion bash)"
_openchamber_tunnel() {
local cur prev commands tunnel_commands profile_commands common_flags start_flags
COMPREPLY=()
cur="\${COMP_WORDS[COMP_CWORD]}"
prev="\${COMP_WORDS[COMP_CWORD-1]}"
commands="serve stop restart status tunnel logs update"
tunnel_commands="help providers ready doctor status start stop profile completion"
profile_commands="list show add remove"
common_flags="--port --foreground --no-daemon --json --all --help --version --plain --quiet"
start_flags="--provider --mode --profile --config --token --token-file --token-stdin --hostname --connect-ttl --session-ttl --qr --no-qr --dry-run --show-secrets"
if [[ \${COMP_CWORD} -eq 1 ]]; then
COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
return 0
fi
if [[ "\${COMP_WORDS[1]}" == "tunnel" ]]; then
if [[ \${COMP_CWORD} -eq 2 ]]; then
COMPREPLY=( $(compgen -W "\${tunnel_commands}" -- "\${cur}") )
return 0
fi
if [[ "\${COMP_WORDS[2]}" == "profile" && \${COMP_CWORD} -eq 3 ]]; then
COMPREPLY=( $(compgen -W "\${profile_commands}" -- "\${cur}") )
return 0
fi
if [[ "\${COMP_WORDS[2]}" == "completion" && \${COMP_CWORD} -eq 3 ]]; then
COMPREPLY=( $(compgen -W "bash zsh fish" -- "\${cur}") )
return 0
fi
if [[ "\${COMP_WORDS[2]}" == "start" ]]; then
COMPREPLY=( $(compgen -W "\${start_flags} \${common_flags}" -- "\${cur}") )
return 0
fi
COMPREPLY=( $(compgen -W "\${common_flags}" -- "\${cur}") )
return 0
fi
COMPREPLY=( $(compgen -W "\${common_flags}" -- "\${cur}") )
return 0
}
complete -F _openchamber_tunnel openchamber
`;
}
if (normalized === 'zsh') {
return `#compdef openchamber
# Zsh completion for openchamber tunnel
# Add to ~/.zshrc: eval "$(openchamber tunnel completion zsh)"
_openchamber() {
local -a commands tunnel_commands profile_commands
commands=(
'serve:Start the web server'
'stop:Stop running instance(s)'
'restart:Stop and start the server'
'status:Show server status'
'tunnel:Tunnel lifecycle commands'
'logs:Tail OpenChamber logs'
'update:Check for and install updates'
)
tunnel_commands=(
'help:Show tunnel help'
'providers:Show available providers'
'ready:Check tunnel readiness'
'doctor:Run tunnel diagnostics'
'status:Show tunnel status'
'start:Start a tunnel'
'stop:Stop active tunnel'
'profile:Manage saved profiles'
'completion:Generate shell completion'
)
profile_commands=(
'list:List profiles'
'show:Show profile details'
'add:Add a profile'
'remove:Remove a profile'
)
_arguments -C \\
'1:command:->command' \\
'*::arg:->args'
case \$state in
command)
_describe 'command' commands
;;
args)
case \$words[1] in
tunnel)
if (( CURRENT == 2 )); then
_describe 'tunnel command' tunnel_commands
elif [[ \$words[2] == "profile" ]] && (( CURRENT == 3 )); then
_describe 'profile action' profile_commands
elif [[ \$words[2] == "completion" ]] && (( CURRENT == 3 )); then
_values 'shell' bash zsh fish
fi
;;
esac
;;
esac
}
compdef _openchamber openchamber
`;
}
if (normalized === 'fish') {
return `# Fish completion for openchamber tunnel
# Save to ~/.config/fish/completions/openchamber.fish
complete -c openchamber -n '__fish_use_subcommand' -a 'serve' -d 'Start the web server'
complete -c openchamber -n '__fish_seen_subcommand_from serve' -l foreground -d 'Run in foreground (for systemd/process managers)'
complete -c openchamber -n '__fish_seen_subcommand_from serve' -l no-daemon -d 'Run in foreground (alias for --foreground)'
complete -c openchamber -n '__fish_use_subcommand' -a 'stop' -d 'Stop running instance(s)'
complete -c openchamber -n '__fish_use_subcommand' -a 'restart' -d 'Stop and start the server'
complete -c openchamber -n '__fish_use_subcommand' -a 'status' -d 'Show server status'
complete -c openchamber -n '__fish_use_subcommand' -a 'tunnel' -d 'Tunnel lifecycle commands'
complete -c openchamber -n '__fish_use_subcommand' -a 'logs' -d 'Tail logs'
complete -c openchamber -n '__fish_use_subcommand' -a 'update' -d 'Check for updates'
complete -c openchamber -n '__fish_seen_subcommand_from tunnel; and not __fish_seen_subcommand_from help providers ready doctor status start stop profile completion' -a 'help' -d 'Show tunnel help'
complete -c openchamber -n '__fish_seen_subcommand_from tunnel; and not __fish_seen_subcommand_from help providers ready doctor status start stop profile completion' -a 'providers' -d 'Show providers'
complete -c openchamber -n '__fish_seen_subcommand_from tunnel; and not __fish_seen_subcommand_from help providers ready doctor status start stop profile completion' -a 'ready' -d 'Check readiness'
complete -c openchamber -n '__fish_seen_subcommand_from tunnel; and not __fish_seen_subcommand_from help providers ready doctor status start stop profile completion' -a 'doctor' -d 'Run diagnostics'
complete -c openchamber -n '__fish_seen_subcommand_from tunnel; and not __fish_seen_subcommand_from help providers ready doctor status start stop profile completion' -a 'status' -d 'Show tunnel status'
complete -c openchamber -n '__fish_seen_subcommand_from tunnel; and not __fish_seen_subcommand_from help providers ready doctor status start stop profile completion' -a 'start' -d 'Start a tunnel'
complete -c openchamber -n '__fish_seen_subcommand_from tunnel; and not __fish_seen_subcommand_from help providers ready doctor status start stop profile completion' -a 'stop' -d 'Stop tunnel'
complete -c openchamber -n '__fish_seen_subcommand_from tunnel; and not __fish_seen_subcommand_from help providers ready doctor status start stop profile completion' -a 'profile' -d 'Manage profiles'
complete -c openchamber -n '__fish_seen_subcommand_from tunnel; and not __fish_seen_subcommand_from help providers ready doctor status start stop profile completion' -a 'completion' -d 'Generate completions'
complete -c openchamber -n '__fish_seen_subcommand_from tunnel; and __fish_seen_subcommand_from start' -l provider -d 'Provider id'
complete -c openchamber -n '__fish_seen_subcommand_from tunnel; and __fish_seen_subcommand_from start' -l mode -d 'Tunnel mode'
complete -c openchamber -n '__fish_seen_subcommand_from tunnel; and __fish_seen_subcommand_from start' -l profile -d 'Profile name'
complete -c openchamber -n '__fish_seen_subcommand_from tunnel; and __fish_seen_subcommand_from start' -l config -d 'Config path'
complete -c openchamber -n '__fish_seen_subcommand_from tunnel; and __fish_seen_subcommand_from start' -l token -d 'Token'
complete -c openchamber -n '__fish_seen_subcommand_from tunnel; and __fish_seen_subcommand_from start' -l token-file -d 'Token file path'
complete -c openchamber -n '__fish_seen_subcommand_from tunnel; and __fish_seen_subcommand_from start' -l token-stdin -d 'Read token from stdin'
complete -c openchamber -n '__fish_seen_subcommand_from tunnel; and __fish_seen_subcommand_from start' -l hostname -d 'Hostname'
complete -c openchamber -n '__fish_seen_subcommand_from tunnel; and __fish_seen_subcommand_from start' -l dry-run -d 'Validate without applying'
complete -c openchamber -n '__fish_seen_subcommand_from tunnel; and __fish_seen_subcommand_from start' -l qr -d 'Show QR code'
`;
}
return null;
}
export {
DEFAULT_PORT,
parseArgs,
showHelp,
showStartupHelp,
showConnectUrlHelp,
showTunnelHelp,
generateCompletionScript,
findClosestMatch,
};