4-Way CLI Comparison: wst (Workspace Tool)

A comprehensive comparison of implementing the same CLI tool in Python (Typer), TypeScript/Bun, Go (Cobra), and Rust (Clap).

Executive Summary

AspectPython/TyperTypeScript/BunGo/CobraRust/Clap
Ease of Setup⭐⭐⭐⭐⭐ Excellent⭐⭐⭐⭐ Good⭐⭐⭐⭐ Good⭐⭐⭐ Moderate
Learning Curve⭐⭐⭐⭐⭐ Easy⭐⭐⭐⭐ Easy⭐⭐⭐ Moderate⭐⭐ Steep
Build TimeN/A (interpreted)~2s~3s~30s
Startup Time~150ms~50ms~5ms~3ms
Binary SizeN/A~50MB~8MB~4MB
Memory Usage~30MB~20MB~5MB~3MB
Distribution⭐⭐⭐ Script/UV⭐⭐⭐⭐ Binary⭐⭐⭐⭐⭐ Binary⭐⭐⭐⭐⭐ Binary
Ecosystem⭐⭐⭐⭐⭐ Rich⭐⭐⭐⭐ Growing⭐⭐⭐⭐ Mature⭐⭐⭐⭐ Growing
Error Messages⭐⭐⭐⭐⭐ Excellent⭐⭐⭐⭐ Good⭐⭐⭐⭐ Good⭐⭐⭐⭐⭐ Excellent
Type Safety⭐⭐⭐⭐ Runtime⭐⭐⭐⭐ Compile⭐⭐⭐⭐⭐ Compile⭐⭐⭐⭐⭐ Compile
Performance⭐⭐ Slow⭐⭐⭐ Fast⭐⭐⭐⭐ Very Fast⭐⭐⭐⭐⭐ Fastest

1. Installation & Setup

Python (Typer)

Requirements: Python 3.12+, UV (optional but recommended)

# With UV (single-file script)
uv run python/wst.py --help
 
# Or traditional install
pip install typer[all] rich pyyaml
python python/wst.py --help

Pros:

  • No build step required
  • PEP 723 inline dependencies (UV) make distribution simple
  • Can run directly with ./wst.py
  • Easy to modify and test changes

Cons:

  • Requires Python runtime
  • Dependencies must be installed
  • Slower startup time

TypeScript/Bun

Requirements: Bun

# Install dependencies
cd typescript && bun install
 
# Run directly
bun run src/wst.ts --help
 
# Or compile to binary
bun build --compile src/wst.ts --outfile wst
./wst --help

Pros:

  • Fast execution with Bun runtime
  • Can compile to standalone binary
  • Modern JavaScript ecosystem
  • TypeScript provides excellent IDE support

Cons:

  • Bun is relatively new (less mature)
  • Compiled binary is large (~50MB)
  • Requires Bun runtime for non-compiled execution

Go (Cobra)

Requirements: Go 1.23+

# Build
cd go && go build -o wst
 
# Run
./wst --help
 
# Or install globally
go install

Pros:

  • Fast compilation (~3 seconds)
  • Single static binary (no runtime needed)
  • Excellent cross-compilation support
  • Small binary size (~8MB)

Cons:

  • Must compile to run
  • Go modules can be complex for beginners
  • Requires Go toolchain

Rust (Clap)

Requirements: Rust 1.70+

# Build
cd rust && cargo build --release
 
# Run
./target/release/wst --help
 
# Or install globally
cargo install --path .

Pros:

  • Smallest binary (~4MB with optimizations)
  • Fastest runtime performance
  • Memory safe by design
  • Excellent error handling

Cons:

  • Long compilation time (~30 seconds)
  • Steep learning curve
  • Borrow checker can be challenging
  • Requires Rust toolchain

2. Code Structure & Organization

Python (Single File: 270 lines)

python/
└── wst.py          # All code in one file

Structure:

  • Helper functions at top
  • Typer app with subcommands
  • Clear, linear flow
  • Minimal boilerplate

Best Practices:

  • Type hints throughout
  • Rich for beautiful output
  • Exception handling with try/except
  • Pathlib for file operations

TypeScript (Modular: ~400 lines)

typescript/
├── package.json
├── tsconfig.json
└── src/
    └── wst.ts      # All code in one file

Structure:

  • Interfaces and types at top
  • Helper functions
  • Command handlers
  • Commander.js command definitions

Best Practices:

  • Strict TypeScript configuration
  • Interface-driven design
  • Error handling with try/catch
  • Chalk for colored output

Go (Package-based: ~500 lines)

go/
├── go.mod
├── main.go
├── cmd/
│   ├── root.go     # Root command
│   ├── config.go   # Config subcommands
│   ├── attach.go   # Attach command
│   └── env.go      # Env command
└── internal/
    └── config/
        └── config.go   # Config logic

Structure:

  • Clear separation of concerns
  • Each command in separate file
  • Internal package for shared logic
  • Cobra standard pattern

Best Practices:

  • Package-oriented design
  • Cobra + Viper patterns
  • Structured error handling
  • fatih/color for output

Rust (Module-based: ~450 lines)

rust/
├── Cargo.toml
└── src/
    ├── main.rs         # Entry point
    ├── cli.rs          # Clap definitions
    ├── config.rs       # Config logic
    ├── error.rs        # Error helpers
    └── commands/
        ├── mod.rs
        ├── init.rs
        ├── show.rs
        ├── attach.rs
        └── env.rs

Structure:

  • Module-based architecture
  • Each command in separate module
  • Shared config and error handling
  • Clap derive API

Best Practices:

  • Result<T, E> error handling
  • Serde for serialization
  • Anyhow for error context
  • Colored for output

3. CLI Framework Comparison

Python: Typer

@app.command("attach")
def attach(
    path: Optional[str] = typer.Argument(None)
) -> None:
    """Attach a workspace directory."""
    # Implementation

Features:

  • Decorator-based API
  • Automatic help generation
  • Type-based validation
  • Rich integration for output
  • Minimal boilerplate

Strengths:

  • Most intuitive API
  • Excellent error messages
  • Beautiful output by default
  • Fast development

Weaknesses:

  • Runtime type checking only
  • Slower execution
  • Requires Python runtime

TypeScript: Commander.js

program
  .command('attach')
  .description('Attach a workspace directory')
  .argument('[path]', 'Path to workspace')
  .action((path?: string) => {
    // Implementation
  });

Features:

  • Fluent API
  • Automatic help generation
  • Subcommand support
  • Git-style commands

Strengths:

  • Familiar to JS/TS developers
  • Good documentation
  • TypeScript integration
  • Flexible API

Weaknesses:

  • More verbose than Typer
  • Manual validation needed
  • Less type-safe than Clap

Go: Cobra

var attachCmd = &cobra.Command{
    Use:   "attach [path]",
    Short: "Attach a workspace directory",
    Args:  cobra.MaximumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
        // Implementation
    },
}

Features:

  • Command struct-based
  • Powerful flag system
  • Viper integration
  • Industry standard

Strengths:

  • Extremely mature
  • Excellent docs
  • Used by kubectl, docker
  • Powerful flag handling

Weaknesses:

  • Verbose setup
  • Struct-based (more boilerplate)
  • Steeper learning curve

Rust: Clap (Derive API)

#[derive(Subcommand)]
enum Commands {
    /// Attach a workspace directory
    Attach {
        /// Path to workspace directory
        path: Option<String>,
    },
}

Features:

  • Derive macro API
  • Compile-time validation
  • Automatic help generation
  • Powerful parsing

Strengths:

  • Most type-safe
  • Zero-cost abstractions
  • Excellent error messages
  • Compile-time guarantees

Weaknesses:

  • Steepest learning curve
  • Requires understanding macros
  • Longer compile times

4. Config Management

All implementations use YAML for configuration stored at ~/.config/wst/config.yaml.

Python: PyYAML

import yaml
 
config = yaml.safe_load(open(CONFIG_FILE))
yaml.dump(config, open(CONFIG_FILE, 'w'))

Pros: Simple API, batteries included Cons: Security considerations with unsafe load

TypeScript: yaml package

import { parse, stringify } from 'yaml';
 
const config = parse(readFileSync(CONFIG_FILE, 'utf-8'));
writeFileSync(CONFIG_FILE, stringify(config));

Pros: Modern API, good TypeScript support Cons: External dependency

Go: gopkg.in/yaml.v3

import "gopkg.in/yaml.v3"
 
yaml.Unmarshal(data, &config)
yaml.Marshal(config)

Pros: Struct tags for mapping, type-safe Cons: Requires defining structs

Rust: serde_yaml

use serde_yaml;
 
let config: Config = serde_yaml::from_str(&contents)?;
serde_yaml::to_string(&config)?

Pros: Derive-based, type-safe, zero-copy Cons: Compile-time overhead

5. Error Handling Patterns

Python: Exceptions

try:
    config = load_config()
except ConfigError as e:
    console.print(f"[red]Error:[/red] {e}")
    raise typer.Exit(1)

Pattern: Try/catch with typed exceptions Pros: Familiar, flexible Cons: Can forget to handle errors

TypeScript: Try/Catch

try {
    const config = loadConfig();
} catch (error) {
    if (error instanceof ConfigError) {
        printError(error.message);
    }
    process.exit(1);
}

Pattern: Try/catch with error types Pros: Similar to Python Cons: No compile-time checking

Go: Error Returns

config, err := config.LoadConfig()
if err != nil {
    printError(err.Error())
    os.Exit(1)
}

Pattern: Explicit error returns Pros: Clear, explicit Cons: Verbose, repetitive

Rust: Result<T, E>

let config = Config::load()
    .context("Failed to load config")?;

Pattern: Result type with ? operator Pros: Compile-time guarantees, composable Cons: Requires understanding Result/Option

6. Shell Integration

All implementations support eval "$(wst env)" pattern for setting environment variables.

Template Variable Interpolation

Python:

result = value
for key, val in workspace.items():
    placeholder = f"{{{{workspace.{key}}}}}"
    result = result.replace(placeholder, val)

TypeScript:

result = result.replace('{{workspace.path}}', workspace.path);
result = result.replace('{{workspace.name}}', workspace.name);

Go:

result = strings.ReplaceAll(result, "{{workspace.path}}", workspace.Path)
result = strings.ReplaceAll(result, "{{workspace.name}}", workspace.Name)

Rust:

value
    .replace("{{workspace.path}}", &workspace.path)
    .replace("{{workspace.name}}", &workspace.name)

All implementations handle shell-specific syntax (bash vs fish).

7. Performance Benchmarks

Startup Time (100 runs, average)

time for i in {1..100}; do wst --version > /dev/null; done
ImplementationAvg TimeRelative
Rust3ms1x (baseline)
Go5ms1.7x
TypeScript/Bun50ms16.7x
Python150ms50x

Binary Size (release build)

ImplementationSizeStripped
Rust4.2MBYes
Go7.8MBNo
TypeScript/Bun52MBN/A
PythonN/AScript

Build Time (clean build)

ImplementationTimeIncremental
Python0sN/A (script)
TypeScript2s~0.1s
Go3s~0.5s
Rust30s~2s

Memory Usage (RSS during execution)

ImplementationMemoryPeak
Rust2.8MB3.2MB
Go4.5MB5.1MB
TypeScript/Bun18MB22MB
Python28MB32MB

Benchmarks run on Apple M1 Pro, macOS Sonoma

8. Distribution Methods

Python

Methods:

  1. UV Script (Recommended): Single file with inline deps
  2. PyPI Package: pip install wst
  3. PyInstaller: Bundled executable

Best for: Quick scripts, teams with Python already installed

TypeScript/Bun

Methods:

  1. Bun Binary: bun build --compile
  2. npm Package: npm install -g wst
  3. Direct execution: bun run wst.ts

Best for: JavaScript/TypeScript teams, modern tooling

Go

Methods:

  1. Static Binary: Single file, no dependencies
  2. go install: go install github.com/user/wst@latest
  3. Release binaries: GitHub releases per platform

Best for: System tools, DevOps, cross-platform CLIs

Rust

Methods:

  1. Static Binary: Smallest size, fastest execution
  2. cargo install: cargo install wst
  3. Release binaries: GitHub releases per platform

Best for: Performance-critical tools, system utilities

9. CLI Best Practices by Language

Python Best Practices

  1. Use Typer over Click for modern type-based API
  2. Add Rich for beautiful terminal output
  3. PEP 723 inline dependencies for single-file scripts
  4. Type hints throughout for IDE support
  5. Pathlib over os.path for file operations
  6. Click testing for comprehensive test coverage

TypeScript Best Practices

  1. Commander.js is most popular, Cliffy for Deno
  2. Chalk/colors for terminal output
  3. Zod for runtime validation
  4. Strict TypeScript config for safety
  5. Bun for fast execution and compilation
  6. Cosmiconfig for flexible config loading

Go Best Practices

  1. Cobra + Viper is industry standard
  2. Package structure with cmd/ and internal/
  3. fatih/color for colored output
  4. Structured errors with fmt.Errorf
  5. Flag consistency with Cobra conventions
  6. Testing with table-driven tests

Rust Best Practices

  1. Clap derive API for ergonomic CLI definitions
  2. Anyhow for error handling in applications
  3. Serde for config serialization
  4. Result<T, E> throughout for error handling
  5. Colored for terminal output
  6. Integration tests in tests/ directory

10. When to Use Each

Choose Python/Typer When:

  • Rapid prototyping needed
  • Team already uses Python
  • Script-like tool with frequent changes
  • Rich terminal output is important
  • Easy distribution to Python developers
  • Performance is not critical

Best Use Cases:

  • Internal tools
  • Data processing pipelines
  • Dev tools for Python projects
  • Automation scripts

Choose TypeScript/Bun When:

  • Team uses JavaScript/TypeScript
  • Modern tooling preferred
  • Bun ecosystem is acceptable
  • Want option to compile or run directly
  • Cross-platform support needed
  • Moderate performance requirements

Best Use Cases:

  • Node.js project tooling
  • Modern web dev tools
  • Teams transitioning from Node
  • Internal developer tools

Choose Go/Cobra When:

  • Need fast compilation and execution
  • Cross-platform distribution important
  • Industry-standard patterns preferred
  • Team has Go experience
  • System-level tool or DevOps utility
  • Single binary deployment desired

Best Use Cases:

  • DevOps tools
  • System utilities
  • Container/cloud tooling
  • CLI tools for services
  • Cross-platform apps

Choose Rust/Clap When:

  • Maximum performance required
  • Memory safety critical
  • Long-running processes
  • System-level programming
  • Smallest binary size needed
  • Concurrent operations important

Best Use Cases:

  • System utilities
  • Performance-critical tools
  • Embedded systems
  • Security-sensitive apps
  • Replacing C/C++ tools

11. Lessons Learned

Complexity vs Features Trade-off

  • Python: Lowest complexity, quickest to implement
  • TypeScript: Moderate complexity, familiar to web devs
  • Go: Higher complexity due to type system and packages
  • Rust: Highest complexity, but most guarantees

Error Messages Quality

Best: Rust > Python > Go > TypeScript

Rust’s compile-time checks catch errors early. Python’s exceptions are clear. Go’s error messages are straightforward. TypeScript’s runtime errors can be less helpful.

Development Speed

Fastest: Python > TypeScript > Go > Rust

Python’s dynamic nature and Typer’s decorator API enable fastest development. Rust’s borrow checker and compile times slow development.

Production Performance

Best: Rust > Go > TypeScript > Python

Rust and Go are compiled languages with excellent runtime performance. TypeScript/Bun is fast but uses more memory. Python is slowest.

Maintenance Burden

  • Python: Low (simple code, easy to understand)
  • TypeScript: Low (types help, but runtime errors possible)
  • Go: Moderate (verbose, but explicit)
  • Rust: Higher (borrow checker, but safe)

12. Recommendations

For Teams

Team TypeRecommendationWhy
Python ShopPython/TyperNatural fit, easy adoption
Web Dev TeamTypeScript/BunFamiliar syntax and tools
DevOps/CloudGo/CobraIndustry standard, great distribution
Systems TeamRust/ClapPerformance and safety critical
Mixed TeamGo/CobraGood balance of all factors

For Use Cases

Use CaseRecommendationWhy
Quick ScriptPython/TyperFastest development
Internal ToolPython or TypeScriptEasy to modify
Public CLIGo or RustBest distribution
System UtilityRust/ClapPerformance critical
DevOps ToolGo/CobraIndustry standard
Data ProcessingPython/TyperRich ecosystem

13. Conclusion

Each implementation has its strengths:

  • Python/Typer: Best for rapid development and Python teams
  • TypeScript/Bun: Modern, fast, familiar to web developers
  • Go/Cobra: Industry standard, excellent balance
  • Rust/Clap: Maximum performance and safety

The “best” choice depends entirely on your context:

  • Team expertise
  • Performance requirements
  • Distribution needs
  • Maintenance considerations
  • Development timeline

For wst specifically, all four implementations are functionally identical. The differences are in:

  • How fast they start
  • How easy they are to distribute
  • How quickly you can modify them
  • How safe they are to maintain

General Recommendation: Start with what your team knows. Optimize later if needed. Python/Typer for prototypes, Go/Cobra for production tools, Rust/Clap when performance is critical.