Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
282 changes: 211 additions & 71 deletions packages/quickli-docs/docs/concepts/application.md
Original file line number Diff line number Diff line change
@@ -1,131 +1,271 @@
---
sidebar_position: 2
description: Application is the root container of every quiCkLI CLI.
keywords: [quickli, application, entrypoint, command registration, run]
description: Application is the root container and execution manager of every quiCkLI CLI.
keywords: [quickli, application, entrypoint, command registration, run, main, global options, shell completion]
---

# Application

`Application` is the root CLI container in `quickli`. It sits at the top of the concept
hierarchy: everything else — commands, arguments, options, and plugins — is registered
against an `Application` instance.
`Application` is the root CLI container and execution manager in `quickli`. It sits at the top of the concept hierarchy: everything else — commands, global options, plugins, and execution options — is registered against or configured within an `Application` instance.

```
Application ← you are here
├── Command
├── Command
├── global_options
├── Command / Subcommand
├── Entrypoint (fallback / single-action)
└── Plugin
```

## What it owns
## What Application Owns

- command registration
- optional root entrypoint registration
- application-level global options
- command dispatch from input tokens
- application and command help rendering
- **Command Registry**: Manages registered **[Command](./command.md)** objects and prevents duplicate command names.
- **Root Entrypoint**: Supports single-action CLIs via `@app.entrypoint`.
- **Global Options**: Defines application-wide **[Option](./option.md)** flags/values available to all commands.
- **Command Dispatch**: Parses `argv` tokens, matches commands/subcommands, and binds arguments and options to handlers.
- **Help Rendering**: Automatically generates structured help text for the application and all registered commands.
- **Shell Completion**: Optionally generates tab-completion scripts for `bash`, `zsh`, and `powershell`.
- **Execution Lifecycles**: Provides `run()` for library/testing use and `main()` for executable CLIs.

## Execution model
## Application Construction Parameters

`Application.run()` dispatches the selected command and returns the handler result (or
generated help text).
When instantiating `Application`, you can customize several core behaviors:

- It reads `sys.argv[1:]` **by default** when called without arguments.
- Pass an explicit list to override: `app.run(["greet", "Ada"])`.
- Set `auto_sys_argv=False` at construction to always use an empty list instead.
- It does **not** print output by default.
- It does **not** choose process exit codes.
```python
from quickli import Application, Option

app = Application(
name="mytool",
description="A multi-purpose developer CLI.",
global_options=[
Option("verbose", short_name="v", is_flag=True, help_text="Enable verbose output."),
],
shell_completion=True,
auto_sys_argv=True,
error_handler=None,
)
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `name` | `str` | `"app"` | The binary/application name used in help text and usage lines. |
| `description` | `str \| None` | `None` | Short description shown at the top of application help output. |
| `global_options` | `list[Option] \| None` | `None` | List of global **[Option](./option.md)** definitions available to all commands. |
| `shell_completion` | `bool` | `False` | When `True`, automatically registers a built-in `shell-completion` command. |
| `auto_sys_argv` | `bool` | `True` | When `True`, `run()` reads `sys.argv[1:]` by default when `argv` is `None`. |
| `error_handler` | `Callable \| None` | `None` | Optional error callback that can inspect or adapt exceptions before `main()` renders them. |

`Application.main(argv=None)` adds the standard executable shell on top of `run()`.
## Command Registration Methods

- It reads `sys.argv[1:]` when `argv` is omitted.
- It prints normal command results.
- It converts runtime failures into structured quickli errors.
- It returns process-friendly exit codes.
`Application` supports four distinct ways to author and register commands:

That split keeps library use explicit while still giving executable applications a simple
default runtime.
### 1. Decorator Registration (`@app.command`)

## Registration API
The most common way to register named commands in multi-command tools.

```python
from quickli import Application, Argument, Option

app = Application(name="demo")

@app.command(
name="greet",
help_text="Greet a user by name.",
arguments=[Argument("name")],
options=[Option("shout", is_flag=True)],
)
def greet_user(name: str, shout: bool = False) -> str:
msg = f"Hello, {name}!"
return msg.upper() if shout else msg
```

`Application` offers decorator APIs for command registration:
### 2. Imperative Command Registration (`app.register_command`)

- `@app.command(...)` for named commands in a multi-command CLI
- `@app.entrypoint(...)` for a commandless root flow
Useful when commands are constructed dynamically or created in separate modules using the `Command` class.

When both exist, command names take precedence, and the entrypoint acts as fallback.
```python
from quickli import Application, Command, Argument

app = Application(name="demo")

build_cmd = Command(
name="build",
help_text="Build the target artefact.",
arguments=[Argument("target")],
handler=lambda target: f"building {target}…",
)

app.register_command(build_cmd)
```

## Single-action tool example
### 3. Root Entrypoint Registration (`@app.entrypoint`)

Use `@app.entrypoint` when your tool does exactly one thing and does not need named
subcommands.
Used for single-action tools (like `cat` or `head`) that do not require command names.

```python
from quickli import Application, Argument

app = Application(name="greet")
app = Application(name="quickhead")

@app.entrypoint(
arguments=[Argument("file_path")],
)
def main_action(file_path: str) -> str:
return f"Reading top lines of {file_path}"

@app.entrypoint(arguments=[Argument("name")])
def main(name: str) -> str:
return f"Hello, {name}!"
print(app.run(["notes.txt"]))
```

:::note[Command Precedence with Entrypoints]
When an `Application` defines both commands and a root entrypoint, input tokens matching a registered command name dispatch to that command. If no command name matches, the tokens fallback to the root entrypoint.
:::

### 4. Plugin Registration (`app.load_plugin`)

print(app.run(["Alice"])) # Hello, Alice!
Used to attach external command modules via the **[Plugin](./plugin.md)** contract.

```python
from quickli import Application, Plugin

class AuditPlugin(Plugin):
@property
def name(self) -> str:
return "audit"

@property
def description(self) -> str:
return "Security audit commands"

def register(self, application: Application) -> None:
@application.command(help_text="Run security check.")
def check() -> str:
return "audit passed"

app = Application(name="demo")
app.load_plugin(AuditPlugin())
```

## Multi-command tool example
## Global Options

Use `@app.command` when your tool exposes several distinct actions, such as `build`,
`deploy`, and `clean`.
Global options apply across the entire application and can be passed before or after the command name:

```python
from quickli import Application
app = Application(
name="demo",
global_options=[
Option("config", short_name="c", help_text="Path to configuration file."),
Option("verbose", short_name="v", is_flag=True, help_text="Verbose logging."),
],
)

@app.command(help_text="Perform deployment.")
def deploy(config: str | None = None, verbose: bool = False) -> str:
return f"deploying (config={config}, verbose={verbose})"

# Both token orders are valid:
print(app.run(["--verbose", "deploy", "-c", "app.toml"]))
print(app.run(["deploy", "-c", "app.toml", "--verbose"]))
```

:::info[Global Option Availability]
Global options are automatically injected into handler signatures when the handler accepts parameters matching the global option names. See **[Option](./option.md#global-and-local-options)** for more details.
:::

app = Application(name="mytool")
## Execution Models: `run()` vs `main()`

`Application` explicitly separates library-level command execution from executable entrypoints:

@app.command(help_text="Build the project.")
def build() -> str:
return "building…"
```
┌──────────────────────┐
│ sys.argv / input │
└──────────┬───────────┘
│
┌────────────────┴────────────────┐
▼ ▼
app.run(argv) app.main(argv, format)
┌──────────────────────┐ ┌───────────────────────────┐
│ - Pure execution │ │ - Calls run() │
│ - Returns string/text│ │ - Prints stdout │
│ - No print or exit │ │ - Handles exit codes │
│ - Ideal for testing │ │ - Supports JSON output │
└──────────────────────┘ └───────────────────────────┘
```

### `Application.run(argv=None)`

@app.command(help_text="Clean build artefacts.")
def clean() -> str:
return "cleaning…"
Runs the command dispatch loop and returns the string output or help text.

- **Arguments**: `argv` (`list[str] | None`). If `None` and `auto_sys_argv=True`, reads `sys.argv[1:]`.
- **Side effects**: None (does not print to stdout/stderr and does not terminate the process).
- **Return value**: The return value of the matched command handler (coerced to string) or generated help text.

print(app.run(["build"])) # building…
```python
# Pure string execution — ideal for unit tests:
output = app.run(["greet", "Alice"])
assert output == "Hello, Alice!"
```

## Tips
### `Application.main(argv=None, output_format="text")`

:::note[Single action vs. multiple commands]
Provides an executable wrapper suitable for CLI binaries (`if __name__ == "__main__":`).

Use `@app.entrypoint` for a single-action tool (like `cat` or `head`) and `@app.command`
for a multi-action tool (like `git` or `kubectl`). You can always add commands later — the
entrypoint acts as a fallback when no command name matches.
- **Output handling**: Prints successful results directly to standard output.
- **Exit codes**: Returns `0` on success, or non-zero error exit codes on failure.
- **Machine-readable output**: Pass `output_format="json"` to emit JSON payloads for automated agents or scripts.
- **Error transformation**: Converts unhandled exceptions into structured `UserCodeError` or `InternalCLIError` instances.

:::
```python
if __name__ == "__main__":
app.main()
```

:::tip[Wrapping `run()` in an executable]
#### JSON Output Format Example

`Application.run()` reads `sys.argv[1:]` by default and returns a string result.
Printing and exit-code handling belong to your `main()` wrapper so that the application
stays independently testable.
```python
app.main(argv=["greet", "Alice"], output_format="json")
# Outputs JSON payload:
# {"status": "success", "result": "Hello, Alice!"}
```

## Built-in Shell Completion

When constructed with `shell_completion=True`, the `Application` automatically registers a `shell-completion` command:

```python
if __name__ == "__main__":
print(app.run())
app = Application(name="mycli", shell_completion=True)

# Generate completion script programmatically:
bash_script = app.generate_completion("bash")
zsh_script = app.generate_completion("zsh")
ps_script = app.generate_completion("powershell")
```

Pass an explicit list to override the default: `app.run(["greet", "Ada"])`. Set
`auto_sys_argv=False` at construction to disable automatic reading entirely.
Users can generate shell completion scripts directly from the CLI:

```bash
mycli shell-completion bash > /etc/bash_completion.d/mycli
```

:::tip[Testing Shell Completion]
The standalone completion generators `generate_bash_completion`, `generate_zsh_completion`, and `generate_powershell_completion` are also available from top-level `quickli`.
:::

## Where to go next
## Custom Error Handlers

You can pass a custom `error_handler` callback to `Application` to process or format exceptions before `main()` outputs them:

```python
def log_and_format_error(error: Exception) -> str | None:
print(f"[LOG] CLI error encountered: {error}")
return f"Custom Error Message: {error}"

app = Application(name="demo", error_handler=log_and_format_error)
```

## Where to Go Next

- Define named actions using **[Command](./command.md)** and **[Subcommand](./command.md#nested-subcommands)**.
- Accept positional arguments using **[Argument](./argument.md)**.
- Accept flags and options using **[Option](./option.md)**.
- Persist settings across runs with **[Config](./config.md)**.
- Modularize your application with **[Plugin](./plugin.md)**.

- Add **[Commands](./command.md)** to give your application named actions.
- Add **[Arguments](./argument.md)** and **[Options](./option.md)** to accept input.
- Use **[Plugins](./plugin.md)** to load reusable command sets without modifying the core.
Loading