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
56 changes: 56 additions & 0 deletions .agents/skills/adding-cli-commands/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
name: adding-cli-commands
description: "Add new CLI commands to clawctl. Use when creating a new command, subcommand group, or modifying the CLI surface. Ensures all touchpoints are updated: handler, exports, commander wiring, README, shell completions, and tests."
---

# Adding CLI Commands

When adding a new command to clawctl, **7 places** need updating.
Missing any of them causes broken completions, missing docs, or test failures.

## Checklist

1. **Command handler** — `packages/cli/src/commands/<name>.ts`
Create `export async function runCommandName(...)`.
For instance-targeting commands, use `requireInstance(opts)`.

2. **Barrel export** — `packages/cli/src/commands/index.ts`
Add `export { runCommandName } from "./<name>.js"`

3. **Commander wiring** — `packages/cli/bin/cli.tsx`
Add `.command()` block with description, options, action handler.
Import the handler from the barrel export.

4. **README** — `README.md` (commands table, around line 66)
Add a row with command syntax and description.

5. **Bash completions** — `packages/templates/src/completions/bash.ts`
- Add to `local commands="..."` string (line 88)
- Add case in `case "$cmd" in` block with command-specific options

6. **Zsh completions** — `packages/templates/src/completions/zsh.ts`
- Add to `commands=(...)` array (around line 70)
- Add case in `case ${words[1]} in` block with `_arguments`

7. **Completion tests** — `packages/templates/src/completions/completions.test.ts`
Add to `ALL_COMMANDS` array (line 6)

## Conventions

See [references/cli-conventions.md](references/cli-conventions.md) for
instance resolution, positional argument rules, and subcommand patterns.

## Subcommand groups

Parent commands like `mount` and `daemon` need special handling:

- **Commander**: Create parent with `.command("name")`, add `.action(() => cmd.help())` for bare invocation, nest children under it
- **Completions**: Add the parent to the top-level commands list. Add a case that completes subcommand names. Add nested cases for each subcommand's options.
- **README**: List each subcommand as its own row
- **Tests**: Add the parent command name to `ALL_COMMANDS`

## Update check skip list

If the command shouldn't trigger the auto-update check (infrastructure commands
like `update`, `daemon`, `completions`), add it to `SKIP_UPDATE_COMMANDS` in
`cli.tsx`.
65 changes: 65 additions & 0 deletions .agents/skills/adding-cli-commands/references/cli-conventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# CLI Command Conventions

## Instance resolution

Every command that targets an instance uses `requireInstance(opts)` from
host-core. It resolves the instance in this order:

1. Explicit `-i <name>` / `--instance <name>` flag
2. Local `.clawctl` context file (set by `clawctl use`)
3. Global context (`~/.config/clawctl/context.json`)
4. Error if none found

## Positional `[name]` argument

Commands that **only** target an instance (no other positional args)
offer `[name]` as a convenience positional:

```
clawctl status [name] # OK — no other positionals
clawctl start [name] # OK
clawctl mount list [name] # OK
```

Commands that have **other required positional arguments** must NOT use
`[name]` — Commander consumes the first positional as the optional name,
swallowing the real argument. Use `-i` or context resolution instead:

```
clawctl mount add <host-path> <guest-path> # No [name] — would eat <host-path>
clawctl mount remove <guest-path> # No [name] — would eat <guest-path>
```

## Subcommand groups

Parent commands (`mount`, `daemon`, `completions`) use Commander's nested
command pattern:

```typescript
const parentCmd = program
.command("parent")
.description("Description")
.action(() => {
parentCmd.help(); // Show help when called bare
});

parentCmd
.command("child")
.description("...")
.action(async () => { ... });
```

For subcommands with required positional args, add `.showHelpAfterError(true)`
so missing arguments show usage instead of a cryptic error.

## Naming conventions

- Command handlers: `runCommandName` (e.g., `runMountList`, `runDaemonStart`)
- File names: kebab-case matching the command (e.g., `mount.ts`, `daemon.ts`)
- One file per command or command group

## Error handling

- Use `process.exit(1)` for user errors (not found, invalid args)
- Let exceptions propagate for unexpected errors
- `requireInstance()` handles its own error output and exits
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ For automated setups, pass a JSON config file and skip the prompts — see [Head
| `clawctl shell [name]` | Interactive shell into the VM |
| `clawctl shell [name] -- <cmd...>` | Run a command in the VM |
| `clawctl openclaw <subcommand...>` | Run an `openclaw` command in the VM (alias: `oc`) |
| `clawctl mount list [name]` | List all mounts for an instance |
| `clawctl mount add <host> <guest>` | Add a host directory mount (`--writable` for rw) |
| `clawctl mount remove <guest-path>` | Remove a user-added mount |
| `clawctl use [name] [--global]` | Set or show the current instance context |
| `clawctl register <name> --project <path>` | Register an existing (pre-registry) instance |
| `clawctl completions <shell>` | Generate shell completion script (bash or zsh) |
Expand Down
33 changes: 4 additions & 29 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,35 +241,10 @@ The eventual goal is to keep Ink active during onboarding by embedding the subpr

## CLI Command Conventions

### Instance resolution

Every command that targets an instance uses `requireInstance(opts)` from
host-core. It resolves the instance in this order:

1. Explicit `-i <name>` / `--instance <name>` flag
2. Local `.clawctl` context file (set by `clawctl use`)
3. Global context (`~/.config/clawctl/context.json`)
4. Error if none found

### Positional `[name]` argument

Commands that **only** target an instance (no other positional args)
offer `[name]` as a convenience positional:

```
clawctl status [name] # OK — no other positionals
clawctl start [name] # OK
clawctl mount list [name] # OK
```

Commands that have **other required positional arguments** must NOT use
`[name]` — Commander consumes the first positional as the optional name,
swallowing the real argument. Use `-i` or context resolution instead:

```
clawctl mount add <host-path> <guest-path> # No [name] — would eat <host-path>
clawctl mount remove <guest-path> # No [name] — would eat <guest-path>
```
See `.agents/skills/adding-cli-commands/` for the full checklist of
files to update when adding a command, and
`.agents/skills/adding-cli-commands/references/cli-conventions.md` for
instance resolution, positional argument rules, and subcommand patterns.

## Error Handling

Expand Down
31 changes: 30 additions & 1 deletion packages/templates/src/completions/bash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export function generateBashCompletion(binName: string): string {
fi
done

local commands="create list status start stop restart delete shell register openclaw oc use"
local commands="create list status start stop restart delete shell register openclaw oc use mount"

# Complete command name at position 1
if [[ \$COMP_CWORD -eq 1 ]]; then
Expand Down Expand Up @@ -164,6 +164,35 @@ export function generateBashCompletion(binName: string): string {
;;
esac
;;
mount)
if [[ \$COMP_CWORD -eq 2 ]]; then
COMPREPLY=( $(compgen -W "list add remove --help" -- "$cur") )
else
local sub="\${COMP_WORDS[2]}"
case "$sub" in
list)
case "$prev" in
-i|--instance)
COMPREPLY=( $(compgen -W "$(_${fnName}_instances)" -- "$cur") )
;;
*)
if [[ "$cur" == -* ]]; then
COMPREPLY=( $(compgen -W "-i --instance --help" -- "$cur") )
else
COMPREPLY=( $(compgen -W "$(_${fnName}_instances)" -- "$cur") )
fi
;;
esac
;;
add)
COMPREPLY=( $(compgen -W "-i --instance --writable --no-restart --help" -- "$cur") )
;;
remove)
COMPREPLY=( $(compgen -W "-i --instance --no-restart --help" -- "$cur") )
;;
esac
fi
;;
completions)
COMPREPLY=( $(compgen -W "bash zsh update-oc --help" -- "$cur") )
;;
Expand Down
1 change: 1 addition & 0 deletions packages/templates/src/completions/completions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const ALL_COMMANDS = [
"openclaw",
"oc",
"use",
"mount",
];

// -- Bash completion ----------------------------------------------------------
Expand Down
37 changes: 37 additions & 0 deletions packages/templates/src/completions/zsh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export function generateZshCompletion(binName: string): string {
'openclaw:Run an openclaw command in the VM'
'oc:Run an openclaw command in the VM (alias)'
'use:Set or show the current instance context'
'mount:Manage VM mount points'
)

local state
Expand Down Expand Up @@ -146,6 +147,42 @@ export function generateZshCompletion(binName: string): string {
'--global[Set global context instead of local .clawctl file]' ${BS}
'--help[Show help]'
;;
mount)
local -a mount_commands
mount_commands=(
'list:List all mounts for an instance'
'add:Add a host directory mount to the VM'
'remove:Remove a mount from the VM'
)
if (( CURRENT == 2 )); then
_describe 'mount command' mount_commands
else
case \${words[2]} in
list)
_arguments ${BS}
'1:instance name:_${fnName}_instances' ${BS}
'(-i --instance)'{-i,--instance}'[Instance to target]:instance name:_${fnName}_instances' ${BS}
'--help[Show help]'
;;
add)
_arguments ${BS}
'1:host path:_directories' ${BS}
'2:guest path:' ${BS}
'(-i --instance)'{-i,--instance}'[Instance to target]:instance name:_${fnName}_instances' ${BS}
'--writable[Mount as read-write]' ${BS}
'--no-restart[Update config but do not restart the VM]' ${BS}
'--help[Show help]'
;;
remove)
_arguments ${BS}
'1:guest path:' ${BS}
'(-i --instance)'{-i,--instance}'[Instance to target]:instance name:_${fnName}_instances' ${BS}
'--no-restart[Update config but do not restart the VM]' ${BS}
'--help[Show help]'
;;
esac
fi
;;
completions)
_arguments '1:shell:(bash zsh)' '--help[Show help]'
;;
Expand Down
24 changes: 24 additions & 0 deletions tasks/2026-04-01_1133_cli-command-skill/TASK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# CLI command skill + fix missing mount completions/README

## Status: Resolved

## Scope

1. Create a skill that guides adding new CLI commands — ensures all touchpoints are updated
2. Move CLI command conventions from `docs/architecture.md` into the skill
3. Add `mount` to README commands table, shell completions, and completion tests

## Context

When the `clawctl mount` command was added, the README and shell completions were missed. There are 7 places that need updating when adding a CLI command, and no single reference that lists them all. A skill solves this by being loaded whenever someone is working on CLI commands.

## Plan

Create `.agents/skills/adding-cli-commands/` with a SKILL.md that contains the full checklist and conventions (instance resolution, positional args, subcommand groups). Move the CLI conventions section from `docs/architecture.md` into the skill's reference doc, and link back. Then fix the `mount` command gaps.

## Steps

- [x] Create the skill with checklist and conventions
- [x] Add `mount` to README commands table
- [x] Add `mount` to bash/zsh completions and test
- [x] Lint, format, test, commit
Loading