Skip to content

Commit 8a029ba

Browse files
Mossakaclaude
andcommitted
feat(cli): organize help text with logical option groups
Group CLI flags into Domain Filtering, Image Management, Container Configuration, Network & Security, API Proxy, and Logging & Debug sections with visual section headers in --help output. Fixes #500 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 85d8ad0 commit 8a029ba

File tree

1 file changed

+158
-71
lines changed

1 file changed

+158
-71
lines changed

src/cli.ts

Lines changed: 158 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -677,54 +677,129 @@ export function parseVolumeMounts(mounts: string[]): ParseVolumeMountsResult | P
677677
return { success: true, mounts: result };
678678
}
679679

680+
function formatItem(
681+
term: string,
682+
description: string,
683+
termWidth: number,
684+
indent: number,
685+
sep: number,
686+
_helpWidth: number
687+
): string {
688+
const indentStr = ' '.repeat(indent);
689+
const fullWidth = termWidth + sep;
690+
if (description) {
691+
if (term.length < fullWidth - sep) {
692+
return `${indentStr}${term.padEnd(fullWidth)}${description}`;
693+
}
694+
return `${indentStr}${term}\n${' '.repeat(indent + fullWidth)}${description}`;
695+
}
696+
return `${indentStr}${term}`;
697+
}
698+
680699
const program = new Command();
681700

701+
// Option group markers used by the custom help formatter to insert section headers.
702+
// Each key is the long flag name of the first option in a group.
703+
const optionGroupHeaders: Record<string, string> = {
704+
'allow-domains': 'Domain Filtering:',
705+
'build-local': 'Image Management:',
706+
'env': 'Container Configuration:',
707+
'dns-servers': 'Network & Security:',
708+
'enable-api-proxy': 'API Proxy:',
709+
'log-level': 'Logging & Debug:',
710+
};
711+
682712
program
683713
.name('awf')
684714
.description('Network firewall for agentic workflows with domain whitelisting')
685715
.version(version)
716+
.configureHelp({
717+
formatHelp(cmd, helper): string {
718+
const termWidth = helper.padWidth(cmd, helper);
719+
const helpWidth = (helper as unknown as { helpWidth?: number }).helpWidth ?? 80;
720+
const itemIndent = 2;
721+
const itemSep = 2;
722+
723+
const output: string[] = [];
724+
725+
// Usage line
726+
const usage = helper.commandUsage(cmd);
727+
output.push(`Usage: ${usage}`);
728+
729+
const desc = helper.commandDescription(cmd);
730+
if (desc) {
731+
output.push('');
732+
output.push(desc);
733+
}
734+
735+
// Arguments
736+
const args = helper.visibleArguments(cmd);
737+
if (args.length > 0) {
738+
output.push('');
739+
output.push('Arguments:');
740+
for (const arg of args) {
741+
const term = helper.argumentTerm(arg);
742+
const argDesc = helper.argumentDescription(arg);
743+
output.push(formatItem(term, argDesc, termWidth, itemIndent, itemSep, helpWidth));
744+
}
745+
}
746+
747+
// Options with group headers
748+
const options = helper.visibleOptions(cmd);
749+
if (options.length > 0) {
750+
output.push('');
751+
output.push('Options:');
752+
for (const opt of options) {
753+
const flags = helper.optionTerm(opt);
754+
const optDesc = helper.optionDescription(opt);
755+
const longFlag = opt.long?.replace(/^--/, '');
756+
if (longFlag && optionGroupHeaders[longFlag]) {
757+
output.push('');
758+
output.push(` ${optionGroupHeaders[longFlag]}`);
759+
}
760+
output.push(formatItem(flags, optDesc, termWidth, itemIndent + 2, itemSep, helpWidth));
761+
}
762+
}
763+
764+
return output.join('\n') + '\n';
765+
}
766+
})
767+
768+
// -- Domain Filtering --
686769
.option(
687770
'-d, --allow-domains <domains>',
688771
'Comma-separated list of allowed domains. Supports wildcards and protocol prefixes:\n' +
689-
' github.com - exact domain + subdomains (HTTP & HTTPS)\n' +
690-
' *.github.com - any subdomain of github.com\n' +
691-
' api-*.example.com - api-* subdomains\n' +
692-
' https://secure.com - HTTPS only\n' +
693-
' http://legacy.com - HTTP only\n' +
694-
' localhost - auto-configure for local testing (Playwright, etc.)'
772+
' github.com - exact domain + subdomains (HTTP & HTTPS)\n' +
773+
' *.github.com - any subdomain of github.com\n' +
774+
' api-*.example.com - api-* subdomains\n' +
775+
' https://secure.com - HTTPS only\n' +
776+
' http://legacy.com - HTTP only\n' +
777+
' localhost - auto-configure for local testing (Playwright, etc.)'
695778
)
696779
.option(
697780
'--allow-domains-file <path>',
698-
'Path to file containing allowed domains (one per line or comma-separated, supports # comments)'
781+
'Path to file with allowed domains (one per line, supports # comments)'
699782
)
700783
.option(
701784
'--block-domains <domains>',
702-
'Comma-separated list of blocked domains (takes precedence over allowed domains). Supports wildcards.'
785+
'Comma-separated blocked domains (overrides allow list). Supports wildcards.'
703786
)
704787
.option(
705788
'--block-domains-file <path>',
706-
'Path to file containing blocked domains (one per line or comma-separated, supports # comments)'
789+
'Path to file with blocked domains (one per line, supports # comments)'
707790
)
708791
.option(
709-
'--log-level <level>',
710-
'Log level: debug, info, warn, error',
711-
'info'
712-
)
713-
.option(
714-
'-k, --keep-containers',
715-
'Keep containers running after command exits',
716-
false
717-
)
718-
.option(
719-
'--tty',
720-
'Allocate a pseudo-TTY for the container (required for interactive tools like Claude Code)',
792+
'--ssl-bump',
793+
'Enable SSL Bump for HTTPS content inspection (allows URL path filtering)',
721794
false
722795
)
723796
.option(
724-
'--work-dir <dir>',
725-
'Working directory for temporary files',
726-
path.join(os.tmpdir(), `awf-${Date.now()}`)
797+
'--allow-urls <urls>',
798+
'Comma-separated allowed URL patterns for HTTPS (requires --ssl-bump).\n' +
799+
' Supports wildcards: https://github.com/myorg/*'
727800
)
801+
802+
// -- Image Management --
728803
.option(
729804
'-b, --build-local',
730805
'Build containers locally instead of using GHCR images',
@@ -733,13 +808,13 @@ program
733808
.option(
734809
'--agent-image <value>',
735810
'Agent container image (default: "default")\n' +
736-
' Presets (pre-built, fast):\n' +
737-
' default - Minimal ubuntu:22.04 (~200MB)\n' +
738-
' act - GitHub Actions parity (~2GB)\n' +
739-
' Custom base images (requires --build-local):\n' +
740-
' ubuntu:XX.XX\n' +
741-
' ghcr.io/catthehacker/ubuntu:runner-XX.XX\n' +
742-
' ghcr.io/catthehacker/ubuntu:full-XX.XX'
811+
' Presets (pre-built, fast):\n' +
812+
' default - Minimal ubuntu:22.04 (~200MB)\n' +
813+
' act - GitHub Actions parity (~2GB)\n' +
814+
' Custom base images (requires --build-local):\n' +
815+
' ubuntu:XX.XX\n' +
816+
' ghcr.io/catthehacker/ubuntu:runner-XX.XX\n' +
817+
' ghcr.io/catthehacker/ubuntu:full-XX.XX'
743818
)
744819
.option(
745820
'--image-registry <registry>',
@@ -749,20 +824,22 @@ program
749824
.option(
750825
'--image-tag <tag>',
751826
'Container image tag (applies to both squid and agent images)\n' +
752-
' Image name varies by --agent-image preset:\n' +
753-
' default → agent:<tag>\n' +
754-
' act → agent-act:<tag>',
827+
' Image name varies by --agent-image preset:\n' +
828+
' default → agent:<tag>\n' +
829+
' act → agent-act:<tag>',
755830
'latest'
756831
)
757832
.option(
758833
'--skip-pull',
759-
'Use local images without pulling from registry (requires images to be pre-downloaded)',
834+
'Use local images without pulling from registry (requires pre-downloaded images)',
760835
false
761836
)
837+
838+
// -- Container Configuration --
762839
.option(
763840
'-e, --env <KEY=VALUE>',
764-
'Additional environment variables to pass to container (can be specified multiple times)',
765-
(value, previous: string[] = []) => [...previous, value],
841+
'Environment variable for the container (repeatable)',
842+
(value: string, previous: string[] = []) => [...previous, value],
766843
[]
767844
)
768845
.option(
@@ -772,74 +849,84 @@ program
772849
)
773850
.option(
774851
'-v, --mount <host_path:container_path[:mode]>',
775-
'Volume mount (can be specified multiple times). Format: host_path:container_path[:ro|rw]',
776-
(value, previous: string[] = []) => [...previous, value],
852+
'Volume mount (repeatable). Format: host_path:container_path[:ro|rw]',
853+
(value: string, previous: string[] = []) => [...previous, value],
777854
[]
778855
)
779856
.option(
780857
'--container-workdir <dir>',
781-
'Working directory inside the container (should match GITHUB_WORKSPACE for path consistency)'
858+
'Working directory inside the container'
782859
)
783860
.option(
784-
'--dns-servers <servers>',
785-
'Comma-separated list of trusted DNS servers. DNS traffic is ONLY allowed to these servers (default: 8.8.8.8,8.8.4.4)',
786-
'8.8.8.8,8.8.4.4'
861+
'--tty',
862+
'Allocate a pseudo-TTY (required for interactive tools like Claude Code)',
863+
false
787864
)
865+
866+
// -- Network & Security --
788867
.option(
789-
'--proxy-logs-dir <path>',
790-
'Directory to save Squid proxy logs to (writes access.log directly to this directory)'
868+
'--dns-servers <servers>',
869+
'Comma-separated trusted DNS servers',
870+
'8.8.8.8,8.8.4.4'
791871
)
792872
.option(
793873
'--enable-host-access',
794-
'Enable access to host services via host.docker.internal. ' +
795-
'Security warning: When combined with --allow-domains host.docker.internal, ' +
796-
'containers can access ANY service on the host machine.',
874+
'Enable access to host services via host.docker.internal',
797875
false
798876
)
799877
.option(
800878
'--allow-host-ports <ports>',
801-
'Comma-separated list of ports or port ranges to allow when using --enable-host-access. ' +
802-
'By default, only ports 80 and 443 are allowed. ' +
803-
'Example: --allow-host-ports 3000 or --allow-host-ports 3000,8080 or --allow-host-ports 3000-3010,8000-8090'
804-
)
805-
.option(
806-
'--ssl-bump',
807-
'Enable SSL Bump for HTTPS content inspection (allows URL path filtering for HTTPS)',
808-
false
809-
)
810-
.option(
811-
'--allow-urls <urls>',
812-
'Comma-separated list of allowed URL patterns for HTTPS (requires --ssl-bump).\n' +
813-
' Supports wildcards: https://github.com/myorg/*'
879+
'Ports/ranges to allow with --enable-host-access (default: 80,443).\n' +
880+
' Example: 3000,8080 or 3000-3010,8000-8090'
814881
)
882+
883+
// -- API Proxy --
815884
.option(
816885
'--enable-api-proxy',
817-
'Enable API proxy sidecar for holding authentication credentials.\n' +
818-
' Deploys a Node.js proxy that injects API keys securely.\n' +
819-
' Supports OpenAI (Codex) and Anthropic (Claude) APIs.',
886+
'Enable API proxy sidecar for secure credential injection.\n' +
887+
' Supports OpenAI (Codex) and Anthropic (Claude) APIs.',
820888
false
821889
)
822890
.option(
823891
'--copilot-api-target <host>',
824-
'Target hostname for GitHub Copilot API requests in the api-proxy sidecar.\n' +
825-
' Defaults to api.githubcopilot.com. Useful for GHES deployments.\n' +
826-
' Can also be set via COPILOT_API_TARGET env var.',
892+
'Target hostname for Copilot API requests (default: api.githubcopilot.com)',
827893
)
828894
.option(
829895
'--rate-limit-rpm <n>',
830-
'Enable rate limiting: max requests per minute per provider (requires --enable-api-proxy)',
896+
'Max requests per minute per provider (requires --enable-api-proxy)',
831897
)
832898
.option(
833899
'--rate-limit-rph <n>',
834-
'Enable rate limiting: max requests per hour per provider (requires --enable-api-proxy)',
900+
'Max requests per hour per provider (requires --enable-api-proxy)',
835901
)
836902
.option(
837903
'--rate-limit-bytes-pm <n>',
838-
'Enable rate limiting: max request bytes per minute per provider (requires --enable-api-proxy)',
904+
'Max request bytes per minute per provider (requires --enable-api-proxy)',
839905
)
840906
.option(
841907
'--no-rate-limit',
842-
'Explicitly disable rate limiting in the API proxy (requires --enable-api-proxy)',
908+
'Disable rate limiting in the API proxy (requires --enable-api-proxy)',
909+
)
910+
911+
// -- Logging & Debug --
912+
.option(
913+
'--log-level <level>',
914+
'Log level: debug, info, warn, error',
915+
'info'
916+
)
917+
.option(
918+
'-k, --keep-containers',
919+
'Keep containers running after command exits',
920+
false
921+
)
922+
.option(
923+
'--work-dir <dir>',
924+
'Working directory for temporary files',
925+
path.join(os.tmpdir(), `awf-${Date.now()}`)
926+
)
927+
.option(
928+
'--proxy-logs-dir <path>',
929+
'Directory to save Squid proxy access.log'
843930
)
844931
.argument('[args...]', 'Command and arguments to execute (use -- to separate from options)')
845932
.action(async (args: string[], options) => {

0 commit comments

Comments
 (0)