proxcli 0.5.0__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. {proxcli-0.5.0 → proxcli-0.7.0}/CHANGELOG.md +17 -0
  2. {proxcli-0.5.0 → proxcli-0.7.0}/PKG-INFO +33 -3
  3. {proxcli-0.5.0 → proxcli-0.7.0}/README.md +32 -2
  4. {proxcli-0.5.0 → proxcli-0.7.0}/TODO.md +12 -11
  5. proxcli-0.7.0/proxmox/cli/completion.py +214 -0
  6. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/cli/main.py +10 -4
  7. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/cli/tasks.py +16 -4
  8. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/client/client.py +78 -0
  9. {proxcli-0.5.0 → proxcli-0.7.0}/pyproject.toml +1 -1
  10. {proxcli-0.5.0 → proxcli-0.7.0}/uv.lock +1 -1
  11. {proxcli-0.5.0 → proxcli-0.7.0}/.env.example +0 -0
  12. {proxcli-0.5.0 → proxcli-0.7.0}/.github/workflows/ci.yml +0 -0
  13. {proxcli-0.5.0 → proxcli-0.7.0}/.gitignore +0 -0
  14. {proxcli-0.5.0 → proxcli-0.7.0}/.python-version +0 -0
  15. {proxcli-0.5.0 → proxcli-0.7.0}/AGENTS.md +0 -0
  16. {proxcli-0.5.0 → proxcli-0.7.0}/PLAN.md +0 -0
  17. {proxcli-0.5.0 → proxcli-0.7.0}/PROJECT.md +0 -0
  18. {proxcli-0.5.0 → proxcli-0.7.0}/PROMPT.md +0 -0
  19. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/__init__.py +0 -0
  20. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/cli/__init__.py +0 -0
  21. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/cli/auth.py +0 -0
  22. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/cli/cluster.py +0 -0
  23. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/cli/container.py +0 -0
  24. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/cli/firewall_helpers.py +0 -0
  25. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/cli/node.py +0 -0
  26. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/cli/pool.py +0 -0
  27. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/cli/storage.py +0 -0
  28. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/cli/vm.py +0 -0
  29. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/client/__init__.py +0 -0
  30. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/client/auth.py +0 -0
  31. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/client/exceptions.py +0 -0
  32. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/config/__init__.py +0 -0
  33. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/config/config.py +0 -0
  34. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/config/models.py +0 -0
  35. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/output/__init__.py +0 -0
  36. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/output/formatter.py +0 -0
  37. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/output/json_fmt.py +0 -0
  38. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/output/table_fmt.py +0 -0
  39. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/output/yaml_fmt.py +0 -0
  40. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/utils/__init__.py +0 -0
  41. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/utils/helpers.py +0 -0
  42. {proxcli-0.5.0 → proxcli-0.7.0}/proxmox/utils/logging.py +0 -0
  43. {proxcli-0.5.0 → proxcli-0.7.0}/tests/__init__.py +0 -0
  44. {proxcli-0.5.0 → proxcli-0.7.0}/tests/conftest.py +0 -0
  45. {proxcli-0.5.0 → proxcli-0.7.0}/tests/test_auth.py +0 -0
  46. {proxcli-0.5.0 → proxcli-0.7.0}/tests/test_cli/__init__.py +0 -0
  47. {proxcli-0.5.0 → proxcli-0.7.0}/tests/test_cli/test_main.py +0 -0
  48. {proxcli-0.5.0 → proxcli-0.7.0}/tests/test_client.py +0 -0
  49. {proxcli-0.5.0 → proxcli-0.7.0}/tests/test_config.py +0 -0
  50. {proxcli-0.5.0 → proxcli-0.7.0}/tests/test_integration/__init__.py +0 -0
  51. {proxcli-0.5.0 → proxcli-0.7.0}/tests/test_output/__init__.py +0 -0
  52. {proxcli-0.5.0 → proxcli-0.7.0}/tests/test_output/test_formatter.py +0 -0
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.0] - 2026-06-20
11
+
12
+ ### Added
13
+ - Task log streaming: `proxmox task log <upid> [--follow]`.
14
+ Without `--follow`, prints available log lines. With `--follow`,
15
+ polls every second until the task exits (like `tail -f`).
16
+ Also added `ProxmoxClient._extract_node_from_upid()` as a static helper.
17
+
18
+ ## [0.6.0] - 2026-06-20
19
+
20
+ ### Added
21
+ - Shell completion support: `proxmox completion bash|zsh|fish`.
22
+ Generated scripts introspect the parser tree and stay in sync
23
+ with all registered resources and actions.
24
+
10
25
  ## [0.5.0] - 2026-06-20
11
26
 
12
27
  ### Added
@@ -67,6 +82,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
67
82
  - CSRF ticket auto-refresh on 401.
68
83
  - AI-agent-friendly: default JSON output, strict exit codes, `--dry-run` mode.
69
84
 
85
+ [0.7.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.7.0
86
+ [0.6.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.6.0
70
87
  [0.5.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.5.0
71
88
  [0.4.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.4.0
72
89
  [0.3.0]: https://github.com/xezpeleta/proxcli/releases/tag/v0.3.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxcli
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: A CLI tool to interact with Proxmox VE nodes and clusters via the REST API
5
5
  Author-email: Xabi Ezpeleta <xezpeleta@gmail.com>
6
6
  License: MIT
@@ -21,7 +21,7 @@ Requires-Dist: pyyaml>=6
21
21
  Requires-Dist: rich>=13
22
22
  Description-Content-Type: text/markdown
23
23
 
24
- # proxmox
24
+ # proxcli
25
25
 
26
26
  A CLI tool to interact with [Proxmox VE](https://www.proxmox.com/) nodes and clusters via the REST API.
27
27
 
@@ -33,7 +33,7 @@ Requires Python 3.10+ and [uv](https://docs.astral.sh/uv/).
33
33
 
34
34
  ```bash
35
35
  # From PyPI
36
- uv tool install proxmox
36
+ uv tool install proxcli
37
37
 
38
38
  # From Git
39
39
  uv tool install git+https://github.com/xezpeleta/proxcli.git
@@ -51,6 +51,11 @@ proxmox auth login --url https://192.168.1.10:8006 --username root@pam --passwor
51
51
  # Or with an API token
52
52
  proxmox auth login --url https://192.168.1.10:8006 --username root@pam --api-token 'root@pam!my-token=deadbeef...'
53
53
 
54
+ # Enable shell completions
55
+ source <(proxmox completion bash) # bash
56
+ source <(proxmox completion zsh) # zsh
57
+ proxmox completion fish | source # fish (or save to ~/.config/fish/completions/proxmox.fish)
58
+
54
59
  # Check auth status
55
60
  proxmox auth status
56
61
 
@@ -129,6 +134,27 @@ proxmox auth status # Show current auth context
129
134
  proxmox auth clear # Remove saved credentials
130
135
  ```
131
136
 
137
+ ### Completion
138
+
139
+ ```bash
140
+ proxmox completion bash # Emit bash completion script
141
+ proxmox completion zsh # Emit zsh completion script
142
+ proxmox completion fish # Emit fish completion script
143
+ ```
144
+
145
+ Add to your shell's rc file:
146
+
147
+ ```bash
148
+ # bash (~/.bashrc)
149
+ source <(proxmox completion bash)
150
+
151
+ # zsh (~/.zshrc)
152
+ source <(proxmox completion zsh)
153
+
154
+ # fish (~/.config/fish/completions/proxmox.fish)
155
+ proxmox completion fish > ~/.config/fish/completions/proxmox.fish
156
+ ```
157
+
132
158
  ### VM (QEMU)
133
159
 
134
160
  ```bash
@@ -250,8 +276,12 @@ proxmox cluster firewall refs [--type alias|ipset|group]
250
276
  ```bash
251
277
  proxmox task list [--node <node>]
252
278
  proxmox task show <upid>
279
+ proxmox task log <upid> [--follow]
253
280
  ```
254
281
 
282
+ `proxmox task log --follow` polls `/nodes/{node}/tasks/{upid}/log` every second
283
+ and streams new lines until the task completes (like `tail -f`).
284
+
255
285
  ## Output Formats
256
286
 
257
287
  ### JSON (default)
@@ -1,4 +1,4 @@
1
- # proxmox
1
+ # proxcli
2
2
 
3
3
  A CLI tool to interact with [Proxmox VE](https://www.proxmox.com/) nodes and clusters via the REST API.
4
4
 
@@ -10,7 +10,7 @@ Requires Python 3.10+ and [uv](https://docs.astral.sh/uv/).
10
10
 
11
11
  ```bash
12
12
  # From PyPI
13
- uv tool install proxmox
13
+ uv tool install proxcli
14
14
 
15
15
  # From Git
16
16
  uv tool install git+https://github.com/xezpeleta/proxcli.git
@@ -28,6 +28,11 @@ proxmox auth login --url https://192.168.1.10:8006 --username root@pam --passwor
28
28
  # Or with an API token
29
29
  proxmox auth login --url https://192.168.1.10:8006 --username root@pam --api-token 'root@pam!my-token=deadbeef...'
30
30
 
31
+ # Enable shell completions
32
+ source <(proxmox completion bash) # bash
33
+ source <(proxmox completion zsh) # zsh
34
+ proxmox completion fish | source # fish (or save to ~/.config/fish/completions/proxmox.fish)
35
+
31
36
  # Check auth status
32
37
  proxmox auth status
33
38
 
@@ -106,6 +111,27 @@ proxmox auth status # Show current auth context
106
111
  proxmox auth clear # Remove saved credentials
107
112
  ```
108
113
 
114
+ ### Completion
115
+
116
+ ```bash
117
+ proxmox completion bash # Emit bash completion script
118
+ proxmox completion zsh # Emit zsh completion script
119
+ proxmox completion fish # Emit fish completion script
120
+ ```
121
+
122
+ Add to your shell's rc file:
123
+
124
+ ```bash
125
+ # bash (~/.bashrc)
126
+ source <(proxmox completion bash)
127
+
128
+ # zsh (~/.zshrc)
129
+ source <(proxmox completion zsh)
130
+
131
+ # fish (~/.config/fish/completions/proxmox.fish)
132
+ proxmox completion fish > ~/.config/fish/completions/proxmox.fish
133
+ ```
134
+
109
135
  ### VM (QEMU)
110
136
 
111
137
  ```bash
@@ -227,8 +253,12 @@ proxmox cluster firewall refs [--type alias|ipset|group]
227
253
  ```bash
228
254
  proxmox task list [--node <node>]
229
255
  proxmox task show <upid>
256
+ proxmox task log <upid> [--follow]
230
257
  ```
231
258
 
259
+ `proxmox task log --follow` polls `/nodes/{node}/tasks/{upid}/log` every second
260
+ and streams new lines until the task completes (like `tail -f`).
261
+
232
262
  ## Output Formats
233
263
 
234
264
  ### JSON (default)
@@ -2,15 +2,20 @@
2
2
 
3
3
  Planned improvements for future releases. Items are roughly ordered by priority.
4
4
 
5
+ Completed items are marked with a check. Implementation notes are preserved for context.
6
+
5
7
  ---
6
8
 
7
- ## v1.1 — Polish & Usability
9
+ ## Done
10
+
11
+ - [x] **Firewall management** — cluster, node, VM, and container. Options, enable/disable, policy, rules (CRUD), aliases (cluster), ipsets with CIDR mgmt (cluster), refs.
12
+ - [x] **Pool management** — `proxmox pool`: list, show, create, update, delete. Wraps `/pools`.
13
+ - [x] **Shell completions** — `proxmox completion bash|zsh|fish`. Dynamic, introspects the parser tree.
8
14
 
9
- - [ ] **Shell completions**
10
- - Add `proxmox completion bash|zsh|fish` subcommand that emits a completion script. Use argparse's built-in completion or a lightweight generator. Makes tab-completion work for all subcommands and flags.
15
+ ## v1.1 Polish & Usability
11
16
 
12
17
  - [ ] **Streaming task logs (`--follow`)**
13
- - `proxmox task log <upid>` that streams task output in real time (like `tail -f`). Requires httpx streaming support (already viable — httpx is the chosen HTTP client).
18
+ - `proxmox task log <upid> --follow` that streams task output in real time (like `tail -f`). Requires httpx streaming.
14
19
 
15
20
  - [ ] **Startup time optimization**
16
21
  - Current `proxmox --help` takes ~350ms. Lazy-load subcommand modules so only the requested resource's code is imported. Move `import rich`, `import yaml` inside formatter functions. Target: <200ms.
@@ -29,17 +34,13 @@ Planned improvements for future releases. Items are roughly ordered by priority.
29
34
  - `proxmox backup` subcommand: `list`, `create`, `show`, `delete`. Wrap `/nodes/{node}/vzdump` and `/nodes/{node}/storage/{storage}/content` for backup files.
30
35
 
31
36
  - [ ] **User & permission management**
32
- - `proxmox user` subcommand: `list`, `show`, `create`, `update`, `delete`. Wraps `/access/users`, `/access/acl`, `/access/roles`.
33
-
34
- - [ ] **Pool management**
35
- - `proxmox pool` subcommand: `list`, `show`, `create`, `update`, `delete`. Wraps `/pools`.
37
+ - `proxmox user` subcommand: `list`, `show`, `create`, `update`, `delete`.
38
+ - `proxmox role` subcommand: `list`, `show`, `create`, `update`, `delete`.
39
+ - `proxmox acl` subcommand: `list`, `show`. Wraps `/access/users`, `/access/roles`, `/access/acl`, `/access/groups`.
36
40
 
37
41
  - [ ] **Network management**
38
42
  - `proxmox network` subcommand: `list`, `show`, `update` for bridges, bonds, VLANs. Wraps `/nodes/{node}/network`.
39
43
 
40
- - [ ] **Firewall management**
41
- - `proxmox firewall` subcommand: `list`, `show`, `enable`, `disable`, `create rule`, `delete rule`. Wraps `/nodes/{node}/firewall`, `/cluster/firewall`.
42
-
43
44
  - [ ] **SDN (Software-Defined Networking)**
44
45
  - `proxmox sdn` subcommand: `zones`, `vnets`, `subnets`. Wraps `/cluster/sdn/*` endpoints.
45
46
 
@@ -0,0 +1,214 @@
1
+ """Shell completion script generation for bash, zsh, and fish."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import textwrap
7
+
8
+ # Each resource has a known list of actions and optional flag completions.
9
+ # We introspect from the parser instead of hardcoding, keeping this DRY.
10
+
11
+ BASH_SCRIPT = textwrap.dedent("""\
12
+ _proxmox_completion() {{
13
+ local cur prev words cword
14
+ _init_completion || return
15
+
16
+ # Global flags
17
+ local global_flags="--url --username --password --password-stdin --api-token --output --dry-run --insecure --timeout --verbose --version --help"
18
+
19
+ # Resources and their actions (autogenerated)
20
+ declare -A _proxmox_resources
21
+ {resource_map}
22
+
23
+ # If we're at the first positional (after global flags), complete resources
24
+ if [[ $cword -eq 1 ]] || [[ "${{words[$((cword-1))]}}" =~ ^(--.*) ]]; then
25
+ COMPREPLY=($(compgen -W "${{!_proxmox_resources[*]}}" -- "$cur"))
26
+ return
27
+ fi
28
+
29
+ # Find the resource word
30
+ local resource=""
31
+ for w in "${{words[@]:1}}"; do
32
+ if [[ -n "${{_proxmox_resources[$w]}}" ]]; then
33
+ resource="$w"
34
+ break
35
+ fi
36
+ done
37
+
38
+ if [[ -n "$resource" ]]; then
39
+ local actions="${{_proxmox_resources[$resource]}}"
40
+ # Complete actions
41
+ COMPREPLY=($(compgen -W "$actions" -- "$cur"))
42
+ fi
43
+ }}
44
+
45
+ complete -F _proxmox_completion proxmox
46
+ """)
47
+
48
+
49
+ ZSH_SCRIPT = textwrap.dedent("""\
50
+ #compdef proxmox
51
+
52
+ _proxmox() {{
53
+ local -a resources
54
+ resources=({resources})
55
+
56
+ local -A resource_actions
57
+ {resource_zsh_map}
58
+
59
+ local curcontext="$curcontext" state line
60
+ typeset -A opt_args
61
+
62
+ _arguments -C \\
63
+ {resource_zsh_flags} \\
64
+ '1:resource:($resources)' \\
65
+ '*::arg:->args'
66
+
67
+ case $state in
68
+ args)
69
+ local resource="$line[1]"
70
+ if [[ -n "${{resource_actions[$resource]}}" ]]; then
71
+ _arguments "*:action:(${{resource_actions[$resource]}})"
72
+ fi
73
+ ;;
74
+ esac
75
+ }}
76
+
77
+ _proxmox "$@"
78
+ """)
79
+
80
+
81
+ FISH_SCRIPT = textwrap.dedent("""\
82
+ function __fish_proxmox_resources
83
+ echo {resources}
84
+ end
85
+
86
+ function __fish_proxmox_actions
87
+ set -l resource (commandline -opc)[1]
88
+ switch $resource
89
+ {fish_cases}
90
+ end
91
+ end
92
+
93
+ # Global flags
94
+ complete -c proxmox -f
95
+ complete -c proxmox -l url -d 'Proxmox API URL'
96
+ complete -c proxmox -l username -d 'Username'
97
+ complete -c proxmox -l password -d 'Password'
98
+ complete -c proxmox -l password-stdin -d 'Read password from stdin'
99
+ complete -c proxmox -l api-token -d 'API token'
100
+ complete -c proxmox -l output -a 'json table yaml' -d 'Output format'
101
+ complete -c proxmox -l dry-run -d 'Print request without executing'
102
+ complete -c proxmox -l insecure -d 'Skip TLS verification'
103
+ complete -c proxmox -l timeout -d 'Request timeout in seconds'
104
+ complete -c proxmox -l verbose -d 'Enable debug output'
105
+ complete -c proxmox -l version -d 'Show version'
106
+ complete -c proxmox -l help -d 'Show help'
107
+
108
+ # Resource completion at position 1
109
+ complete -c proxmox -n 'not __fish_seen_subcommand_from {resources}' \\
110
+ -a '(__fish_proxmox_resources)'
111
+
112
+ # Action completion per resource
113
+ {fish_action_completions}
114
+ """)
115
+
116
+
117
+ def _collect_parser_tree(parser: argparse.ArgumentParser) -> dict[str, list[str]]:
118
+ """Walk the root parser and collect resource -> action mapping."""
119
+ resource_actions: dict[str, list[str]] = {}
120
+
121
+ for action in parser._actions:
122
+ if not isinstance(action, argparse._SubParsersAction):
123
+ continue
124
+ for name, sub_parser in action.choices.items():
125
+ sub_actions: list[str] = []
126
+ for sa in sub_parser._actions:
127
+ if isinstance(sa, argparse._SubParsersAction):
128
+ sub_actions.extend(sa.choices.keys())
129
+ resource_actions[name] = sorted(sub_actions)
130
+
131
+ return resource_actions
132
+
133
+
134
+ def generate_bash(parser: argparse.ArgumentParser) -> str:
135
+ """Generate a bash completion script."""
136
+ resources = _collect_parser_tree(parser)
137
+ lines = []
138
+ for name, actions in sorted(resources.items()):
139
+ lines.append(f' _proxmox_resources[{name}]="{ " ".join(actions) }"')
140
+ return BASH_SCRIPT.format(resource_map="\n".join(lines))
141
+
142
+
143
+ def generate_zsh(parser: argparse.ArgumentParser) -> str:
144
+ """Generate a zsh completion script."""
145
+ resources = _collect_parser_tree(parser)
146
+ resources_list = " ".join(sorted(resources.keys()))
147
+
148
+ zsh_lines = []
149
+ for name, actions in sorted(resources.items()):
150
+ zsh_lines.append(f' resource_actions[{name}]="{ " ".join(actions) }"')
151
+
152
+ return ZSH_SCRIPT.format(
153
+ resources=resources_list,
154
+ resource_zsh_map="\n".join(zsh_lines),
155
+ resource_zsh_flags="",
156
+ )
157
+
158
+
159
+ def generate_fish(parser: argparse.ArgumentParser) -> str:
160
+ """Generate a fish completion script."""
161
+ resources = _collect_parser_tree(parser)
162
+ resources_list = " ".join(sorted(resources.keys()))
163
+
164
+ fish_cases = []
165
+ for name, actions in sorted(resources.items()):
166
+ fish_cases.append(f" case {name}\n echo {' '.join(actions)}")
167
+
168
+ fish_completions = []
169
+ for name, actions in sorted(resources.items()):
170
+ fish_completions.append(
171
+ f"complete -c proxmox -n '__fish_seen_subcommand_from {name}' "
172
+ f"-a '{' '.join(actions)}'"
173
+ )
174
+
175
+ return FISH_SCRIPT.format(
176
+ resources=resources_list,
177
+ fish_cases="\n".join(fish_cases),
178
+ fish_action_completions="\n".join(fish_completions),
179
+ )
180
+
181
+
182
+ # ---------------------------------------------------------------------------
183
+ # CLI registration
184
+ # ---------------------------------------------------------------------------
185
+
186
+
187
+ def register_completion_parser(subparsers: argparse._SubParsersAction) -> None:
188
+ """Register the `proxmox completion` subcommand."""
189
+ comp_parser = subparsers.add_parser(
190
+ "completion", help="Generate shell completion script"
191
+ )
192
+ comp_parser.add_argument(
193
+ "shell",
194
+ choices=["bash", "zsh", "fish"],
195
+ help="Target shell",
196
+ )
197
+ comp_parser.set_defaults(func=_emit_completion)
198
+
199
+
200
+ def _emit_completion(args: argparse.Namespace, _client: object) -> str:
201
+ """Emit the completion script for the requested shell.
202
+
203
+ We need access to the root parser to introspect the subcommand tree,
204
+ so we import the builder here.
205
+ """
206
+ from proxmox.cli.main import build_root_parser
207
+
208
+ parser = build_root_parser()
209
+ generators = {
210
+ "bash": generate_bash,
211
+ "zsh": generate_zsh,
212
+ "fish": generate_fish,
213
+ }
214
+ return generators[args.shell](parser)
@@ -57,6 +57,7 @@ def build_root_parser() -> argparse.ArgumentParser:
57
57
  # Import and register subcommands
58
58
  from proxmox.cli.auth import register_auth_parser
59
59
  from proxmox.cli.cluster import register_cluster_parser
60
+ from proxmox.cli.completion import register_completion_parser
60
61
  from proxmox.cli.container import register_container_parser
61
62
  from proxmox.cli.node import register_node_parser
62
63
  from proxmox.cli.pool import register_pool_parser
@@ -71,6 +72,7 @@ def build_root_parser() -> argparse.ArgumentParser:
71
72
  register_container_parser(subparsers)
72
73
  register_storage_parser(subparsers)
73
74
  register_cluster_parser(subparsers)
75
+ register_completion_parser(subparsers)
74
76
  register_task_parser(subparsers)
75
77
 
76
78
  return parser
@@ -204,13 +206,17 @@ def main(argv: list[str] | None = None) -> None:
204
206
  return
205
207
 
206
208
  try:
207
- # auth status and clear don't need a client
208
- if args.resource == "auth" and args.action in ("status", "clear"):
209
+ # auth status, clear, and completion don't need a client
210
+ if (args.resource == "auth" and args.action in ("status", "clear")) or args.resource == "completion":
209
211
  if hasattr(args, "func"):
210
212
  result = args.func(args, None)
211
213
  if result is not None:
212
- output = format_output(result, args.output)
213
- print(output)
214
+ if args.resource == "completion":
215
+ # Completion scripts are raw shell code, not JSON
216
+ print(result)
217
+ else:
218
+ output = format_output(result, args.output)
219
+ print(output)
214
220
  return
215
221
 
216
222
  _, overrides = _merge_config(args)
@@ -22,13 +22,20 @@ def register_task_parser(subparsers: argparse._SubParsersAction) -> None:
22
22
  task_show.add_argument("upid", help="Task UPID")
23
23
  task_show.set_defaults(func=_task_show)
24
24
 
25
+ # --- task log ---
26
+ task_log = task_sub.add_parser("log", help="Show task log output")
27
+ task_log.add_argument("upid", help="Task UPID")
28
+ task_log.add_argument(
29
+ "--follow", "-f",
30
+ action="store_true",
31
+ help="Follow log output until task completes (like tail -f)",
32
+ )
33
+ task_log.set_defaults(func=_task_log)
34
+
25
35
 
26
36
  def _extract_node_from_upid(upid: str) -> str | None:
27
37
  """Parse node name from a Proxmox UPID string: UPID:{node}:..."""
28
- parts = upid.split(":")
29
- if len(parts) >= 2:
30
- return parts[1]
31
- return None
38
+ return ProxmoxClient._extract_node_from_upid(upid)
32
39
 
33
40
 
34
41
  def _task_list(args: argparse.Namespace, client: ProxmoxClient) -> dict | list:
@@ -63,3 +70,8 @@ def _task_show(args: argparse.Namespace, client: ProxmoxClient) -> dict:
63
70
  if not node:
64
71
  return {"error": f"Could not extract node from UPID: {args.upid}"}
65
72
  return client.get(f"/nodes/{node}/tasks/{args.upid}/status")
73
+
74
+
75
+ def _task_log(args: argparse.Namespace, client: ProxmoxClient) -> None:
76
+ """Stream task log (returns None so main.py skips JSON formatting)."""
77
+ client.stream_task_log(args.upid, follow=args.follow)
@@ -280,3 +280,81 @@ class ProxmoxClient:
280
280
  import sys
281
281
 
282
282
  print(f"[proxmox] {message}", file=sys.stderr)
283
+
284
+ # ------------------------------------------------------------------
285
+ # Task log streaming
286
+ # ------------------------------------------------------------------
287
+
288
+ def stream_task_log(self, upid: str, *, follow: bool = False) -> None:
289
+ """Stream task log lines to stdout.
290
+
291
+ Args:
292
+ upid: Task UPID string (e.g. UPID:pve01:00000010:...).
293
+ follow: If True, keep polling until task exits.
294
+ """
295
+ import sys
296
+
297
+ node = self._extract_node_from_upid(upid)
298
+ if not node:
299
+ print(f"Error: could not extract node from UPID: {upid}", file=sys.stderr)
300
+ return
301
+
302
+ if self._dry_run:
303
+ self._print_dry_run("GET", f"{self._base_url}/api2/json/nodes/{node}/tasks/{upid}/log", None)
304
+ if follow:
305
+ print("[dry-run] --follow would poll /tasks/{upid}/status until stopped")
306
+ return
307
+
308
+ start = 0
309
+ seen_lines: set[int] = set()
310
+
311
+ while True:
312
+ try:
313
+ log_data = self.request("GET", f"/nodes/{node}/tasks/{upid}/log", params={"start": start})
314
+ except ProxmoxAPIError:
315
+ if not follow:
316
+ break
317
+ time.sleep(1)
318
+ continue
319
+
320
+ if isinstance(log_data, dict):
321
+ log_data = log_data.get("data", []) if "data" in log_data else []
322
+
323
+ if isinstance(log_data, list):
324
+ for entry in log_data:
325
+ if not isinstance(entry, dict):
326
+ continue
327
+ n = entry.get("n", 0)
328
+ if n in seen_lines:
329
+ continue
330
+ seen_lines.add(n)
331
+ line = entry.get("t", "")
332
+ print(line)
333
+
334
+ # Check if task is done
335
+ if follow:
336
+ try:
337
+ status_data = self.request("GET", f"/nodes/{node}/tasks/{upid}/status")
338
+ if isinstance(status_data, dict):
339
+ status = status_data.get("status")
340
+ if status == "stopped":
341
+ exit_code = status_data.get("exitstatus")
342
+ if exit_code is not None:
343
+ print(f"\nTask completed with exit code: {exit_code}")
344
+ break
345
+ except ProxmoxAPIError:
346
+ pass
347
+
348
+ if not follow:
349
+ break
350
+
351
+ start = len(seen_lines)
352
+ time.sleep(1)
353
+
354
+ @staticmethod
355
+ def _extract_node_from_upid(upid: str) -> str | None:
356
+ """Parse node name from a Proxmox UPID string: UPID:{node}:..."""
357
+ parts = upid.split(":")
358
+ if len(parts) >= 2:
359
+ return parts[1]
360
+ return None
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "proxcli"
3
- version = "0.5.0"
3
+ version = "0.7.0"
4
4
  description = "A CLI tool to interact with Proxmox VE nodes and clusters via the REST API"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -254,7 +254,7 @@ wheels = [
254
254
 
255
255
  [[package]]
256
256
  name = "proxcli"
257
- version = "0.5.0"
257
+ version = "0.7.0"
258
258
  source = { editable = "." }
259
259
  dependencies = [
260
260
  { name = "httpx" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes