vcf-super-cli 0.2.0__tar.gz → 0.3.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 (88) hide show
  1. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/CHANGELOG.md +36 -0
  2. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/PKG-INFO +3 -2
  3. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/README.md +2 -1
  4. vcf_super_cli-0.3.0/docs/commands.md +112 -0
  5. vcf_super_cli-0.3.0/docs/superpowers/specs/2026-06-05-vcf-super-cli-v0.3-ergonomics-design.md +232 -0
  6. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/docs/usage.md +19 -0
  7. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/pyproject.toml +15 -1
  8. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/tests/test_builder.py +1 -1
  9. vcf_super_cli-0.3.0/tests/test_complete.py +100 -0
  10. vcf_super_cli-0.3.0/tests/test_filter_pagination_cli.py +174 -0
  11. vcf_super_cli-0.3.0/tests/test_filters.py +66 -0
  12. vcf_super_cli-0.3.0/tests/test_paginate.py +54 -0
  13. vcf_super_cli-0.3.0/tests/test_pyvmomi_events.py +138 -0
  14. vcf_super_cli-0.3.0/tests/test_pyvmomi_inventory.py +96 -0
  15. vcf_super_cli-0.3.0/tests/test_pyvmomi_perf.py +138 -0
  16. vcf_super_cli-0.3.0/tests/test_pyvmomi_tasks.py +62 -0
  17. vcf_super_cli-0.3.0/tests/test_vmomi.py +131 -0
  18. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/uv.lock +1 -1
  19. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/_version.py +1 -1
  20. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/cli/app.py +14 -1
  21. vcf_super_cli-0.3.0/vsc/connect/vmomi.py +102 -0
  22. vcf_super_cli-0.3.0/vsc/gen/builder.py +417 -0
  23. vcf_super_cli-0.3.0/vsc/gen/complete.py +62 -0
  24. vcf_super_cli-0.3.0/vsc/gen/filters.py +88 -0
  25. vcf_super_cli-0.3.0/vsc/gen/paginate.py +40 -0
  26. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/output/errors.py +26 -0
  27. vcf_super_cli-0.3.0/vsc/pyvmomi/__init__.py +7 -0
  28. vcf_super_cli-0.3.0/vsc/pyvmomi/events.py +98 -0
  29. vcf_super_cli-0.3.0/vsc/pyvmomi/inventory.py +91 -0
  30. vcf_super_cli-0.3.0/vsc/pyvmomi/perf.py +118 -0
  31. vcf_super_cli-0.3.0/vsc/pyvmomi/runner.py +51 -0
  32. vcf_super_cli-0.3.0/vsc/pyvmomi/tasks.py +41 -0
  33. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/skill/assets/SKILL.md +15 -0
  34. vcf_super_cli-0.2.0/docs/commands.md +0 -64
  35. vcf_super_cli-0.2.0/vsc/gen/builder.py +0 -243
  36. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/.github/workflows/docs.yml +0 -0
  37. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/.github/workflows/e2e.yml +0 -0
  38. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/.github/workflows/lint.yml +0 -0
  39. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/.github/workflows/release.yml +0 -0
  40. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/.github/workflows/test.yml +0 -0
  41. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/.gitignore +0 -0
  42. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/AGENTS.md +0 -0
  43. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/LICENSE +0 -0
  44. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/docs/design.md +0 -0
  45. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/docs/index.md +0 -0
  46. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/docs/install.md +0 -0
  47. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/docs/profiles.md +0 -0
  48. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/docs/superpowers/specs/2026-06-05-vcf-super-cli-design.md +0 -0
  49. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/docs/superpowers/specs/2026-06-05-vcf-super-cli-v0.2-writes-design.md +0 -0
  50. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/docs/writes.md +0 -0
  51. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/mkdocs.yml +0 -0
  52. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/tests/__init__.py +0 -0
  53. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/tests/e2e/README.md +0 -0
  54. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/tests/e2e/__init__.py +0 -0
  55. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/tests/e2e/conftest.py +0 -0
  56. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/tests/e2e/test_read_smoke.py +0 -0
  57. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/tests/test_cli.py +0 -0
  58. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/tests/test_config.py +0 -0
  59. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/tests/test_discover.py +0 -0
  60. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/tests/test_errors.py +0 -0
  61. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/tests/test_params.py +0 -0
  62. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/tests/test_preview.py +0 -0
  63. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/tests/test_render.py +0 -0
  64. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/tests/test_session.py +0 -0
  65. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/tests/test_skill.py +0 -0
  66. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/tests/test_version.py +0 -0
  67. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/tests/test_write_errors.py +0 -0
  68. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/__init__.py +0 -0
  69. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/cli/__init__.py +0 -0
  70. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/cli/profiles.py +0 -0
  71. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/cli/skill.py +0 -0
  72. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/config/__init__.py +0 -0
  73. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/config/schema.py +0 -0
  74. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/config/store.py +0 -0
  75. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/connect/__init__.py +0 -0
  76. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/connect/session.py +0 -0
  77. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/connect/targets.py +0 -0
  78. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/gen/__init__.py +0 -0
  79. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/gen/discover.py +0 -0
  80. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/gen/model.py +0 -0
  81. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/gen/params.py +0 -0
  82. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/gen/preview.py +0 -0
  83. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/logging_config.py +0 -0
  84. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/output/__init__.py +0 -0
  85. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/output/exit_codes.py +0 -0
  86. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/output/render.py +0 -0
  87. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/skill/__init__.py +0 -0
  88. {vcf_super_cli-0.2.0 → vcf_super_cli-0.3.0}/vsc/skill/export.py +0 -0
@@ -7,6 +7,42 @@ versions may include breaking changes.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## v0.3.0 — 2026-06-05
11
+
12
+ Ergonomics: friendlier filtering and paging, offline shell completion, and a
13
+ pyVmomi fallback surface for gaps the REST/vAPI layer doesn't cover. The
14
+ agent-facing contract is unchanged — stable JSON, error envelope, exit codes,
15
+ and writes still dry-run by default.
16
+
17
+ ### Added
18
+
19
+ - **Static shell completion** (#32), fully offline — never opens a connection.
20
+ Completes enum option choices, output formats, configured profile names, and
21
+ `list` filter enum values. Install with `vsc --install-completion`. (Live
22
+ resource-id completion is intentionally deferred to a later release.)
23
+ - **Per-field filter flags** (#33). `list` commands flatten the SDK filter spec
24
+ into typed `--<field>` options (repeatable for list/set fields; enum choices
25
+ validated and completed), e.g. `vsc vsphere vm list --power-states POWERED_ON
26
+ --names web-1`. The raw `--filter '<json>'` blob stays as a base layer that
27
+ per-field flags override.
28
+ - **Pagination helpers** (#33): `--all` follows the NSX cursor across pages;
29
+ `--max-items N` caps the total; `--limit N` is a client-side cap for
30
+ non-paginated (vSphere) lists. Without `--all`, a paginated `list` returns one
31
+ page and surfaces its `cursor` for manual paging.
32
+ - **pyVmomi fallback commands** (read-only) under `vsc vsphere`, for areas the
33
+ REST/vAPI surface lacks — same JSON / error-envelope / exit-code contract:
34
+ - `perf vm|host --metric <group.name>` — performance counters via the
35
+ PerformanceManager (#34).
36
+ - `events list [--vm|--host] [--since 1h]` and `tasks list` — recent events
37
+ and recent/running tasks (#35).
38
+ - `inventory vm|host [--props <path>]…` — a PropertyCollector property walk
39
+ (#36).
40
+
41
+ ### Changed
42
+
43
+ - Documentation and the bundled agent `SKILL.md` describe completion, the filter
44
+ and paging flags, and the pyVmomi fallback surface.
45
+
10
46
  ## v0.2.0 — 2026-06-05
11
47
 
12
48
  First PyPI release. Adds the **write** surface on top of the v0.1 read-only
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vcf-super-cli
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Modern, agent-friendly CLI for VMware Cloud Foundation 9, with a command tree generated dynamically from the vcf-sdk vAPI bindings.
5
5
  Project-URL: Homepage, https://github.com/thomaschristory/vcf-super-cli
6
6
  Project-URL: Documentation, https://thomaschristory.github.io/vcf-super-cli/
@@ -73,7 +73,8 @@ $ vsc vsphere power stop vm-42 --apply # writes are dry-run without --app
73
73
  | vSphere / vCenter read (`vsc vsphere …`) | ✅ v0.1 |
74
74
  | NSX **Policy API** read (`vsc nsx …`) | ✅ v0.1 |
75
75
  | Writes — dry-run by default + `--apply` (`vsc vsphere …` / `vsc nsx …`) | ✅ v0.2 |
76
- | Dynamic shell completion, per-field filter flags, pyVmomi fallback | v0.3 (planned) |
76
+ | Ergonomics — offline shell completion, per-field filter flags + paging, pyVmomi fallback (`perf`/`events`/`tasks`/`inventory`) | v0.3 |
77
+ | Live resource-id completion | planned |
77
78
  | NSX Manager / Global-Manager, SDDC Manager, Operations, LCM | deferred |
78
79
 
79
80
  ## Install
@@ -41,7 +41,8 @@ $ vsc vsphere power stop vm-42 --apply # writes are dry-run without --app
41
41
  | vSphere / vCenter read (`vsc vsphere …`) | ✅ v0.1 |
42
42
  | NSX **Policy API** read (`vsc nsx …`) | ✅ v0.1 |
43
43
  | Writes — dry-run by default + `--apply` (`vsc vsphere …` / `vsc nsx …`) | ✅ v0.2 |
44
- | Dynamic shell completion, per-field filter flags, pyVmomi fallback | v0.3 (planned) |
44
+ | Ergonomics — offline shell completion, per-field filter flags + paging, pyVmomi fallback (`perf`/`events`/`tasks`/`inventory`) | v0.3 |
45
+ | Live resource-id completion | planned |
45
46
  | NSX Manager / Global-Manager, SDDC Manager, Operations, LCM | deferred |
46
47
 
47
48
  ## Install
@@ -0,0 +1,112 @@
1
+ # Commands
2
+
3
+ The `vsphere` and `nsx` command trees are **generated from the `vcf-sdk` vAPI
4
+ bindings**, so `vsc --help` (and each sub-`--help`) is always the authoritative,
5
+ version-accurate reference. This page is an overview.
6
+
7
+ Generated leaves expose both **read** and **write** verbs:
8
+
9
+ - `list` — list resources (optional `--filter '<json>'`)
10
+ - `get <id>` — fetch one resource by id, **where the SDK provides a by-id GET**
11
+ (e.g. `vm`, `cluster`, `datacenter`, `datastore`; some leaves such as `host`,
12
+ `folder`, and `network` are `list`-only)
13
+ - writes — `create`, `delete`, `set` (PUT upsert), `patch`, and action verbs
14
+ (e.g. `start`/`stop`/`reset` under `power`). **Writes are dry-run by default and
15
+ require `--apply`** — see [Writes](writes.md).
16
+
17
+ ## `vsc vsphere …` (vCenter)
18
+
19
+ Generated from `com.vmware.vcenter`:
20
+
21
+ | Group | Examples |
22
+ |-------|----------|
23
+ | `vm` | `vsc vsphere vm list`, `vsc vsphere vm get <vm>`, `vsc vsphere vm delete <vm> --apply` |
24
+ | `host` | `vsc vsphere host list`, `vsc vsphere host disconnect <host> --apply` |
25
+ | `cluster` | `vsc vsphere cluster list` |
26
+ | `datacenter` | `vsc vsphere datacenter list` |
27
+ | `datastore` | `vsc vsphere datastore list` |
28
+ | `folder` | `vsc vsphere folder list` |
29
+ | `network` | `vsc vsphere network list` |
30
+ | `resource-pool` | `vsc vsphere resource-pool create --spec '<json>' --apply` |
31
+ | `power` | `vsc vsphere power start\|stop\|reset\|suspend <vm> --apply` |
32
+ | `cpu` / `memory` / `disk` / `ethernet` | VM hardware reads + writes, e.g. `vsc vsphere cpu update <vm> --spec '<json>' --apply` |
33
+ | `perf` / `events` / `tasks` / `inventory` | **pyVmomi fallback** (read-only) — perf counters, recent events, recent/running tasks, and a property walk the REST/vAPI surface lacks (see below) |
34
+
35
+ ### pyVmomi fallback commands
36
+
37
+ A few inventory/performance areas are only reachable over the older pyVmomi SOAP
38
+ API. Those commands live under `vsc vsphere` alongside the generated ones, are
39
+ **read-only** (no `--apply`), and emit the same JSON / error envelope / exit codes.
40
+
41
+ - `vsc vsphere perf vm <vm> [--metric group.name]… [--max-samples N]` — performance
42
+ counters via the PerformanceManager. Metrics are `group.name` (e.g. `cpu.usage`)
43
+ or `group.name.rollup` (e.g. `cpu.usage.average`); repeat `--metric` for several.
44
+ - `vsc vsphere perf host <host> …` — the same for an ESXi host.
45
+ - `vsc vsphere events list [--vm <vm> | --host <host>] [--since 1h] [--max-count N]`
46
+ — recent events via the EventManager. `--since` takes a duration (`30s`/`15m`/
47
+ `2h`/`1d`); scope to at most one entity.
48
+ - `vsc vsphere tasks list [--max-count N]` — recent and running tasks via the
49
+ TaskManager.
50
+ - `vsc vsphere inventory vm <vm> [--props path]…` / `inventory host <host> …` —
51
+ managed-object properties the REST list ops omit (device tree, custom attributes),
52
+ via the PropertyCollector. `--props` is repeatable (e.g. `--props config.hardware`);
53
+ with none, a small per-type summary set is returned.
54
+
55
+ ## `vsc nsx …` (NSX Policy)
56
+
57
+ Generated from `vcf.nsx.policy`:
58
+
59
+ | Group | Examples |
60
+ |-------|----------|
61
+ | `segments` | `vsc nsx segments list`, `vsc nsx segments set <id> --segment '<json>' --apply` |
62
+ | `tier0s` / `tier1s` | `vsc nsx tier1s list`, `vsc nsx tier1s set <id> --tier1 '<json>' --apply` |
63
+ | `services` | `vsc nsx services list` |
64
+ | `groups` | `vsc nsx groups list`, `vsc nsx groups delete <domain> <group> --apply` |
65
+ | `security-policies` | `vsc nsx security-policies list` |
66
+ | `gateway-policies` | `vsc nsx gateway-policies list` |
67
+ | `ip-pools` | `vsc nsx ip-pools set <id> --ip-address-pool '<json>' --apply` |
68
+ | `dhcp-server-configs` / `dhcp-relay-configs` | DHCP config reads + writes |
69
+ | `locale-services` | Tier-1 locale services reads + writes |
70
+
71
+ ## Curated commands
72
+
73
+ - `vsc profiles …` — manage connection profiles (see [Profiles](profiles.md))
74
+ - `vsc skill export <dir> [--apply]` — export the bundled agent Skill
75
+ - `vsc --version`
76
+
77
+ ## Filtering
78
+
79
+ `list` commands expose each field of the SDK filter spec as its own typed flag.
80
+ List-valued fields are repeatable; enum fields validate their choices and
81
+ tab-complete:
82
+
83
+ ```sh
84
+ vsc --profile prod vsphere vm list --power-states POWERED_ON --names web-1 --names web-2
85
+ ```
86
+
87
+ The raw JSON spec is still accepted as a base layer / escape hatch; per-field
88
+ flags merge **over** it (a flag wins over the same key in the blob):
89
+
90
+ ```sh
91
+ vsc --profile prod vsphere vm list --filter '{"clusters": ["domain-c1"]}' --power-states POWERED_ON
92
+ ```
93
+
94
+ ## Pagination
95
+
96
+ `list` commands accept paging flags:
97
+
98
+ | Flag | Effect |
99
+ |------|--------|
100
+ | `--all` | follow the cursor and return **every** page (paginated backends, e.g. NSX) |
101
+ | `--max-items N` | cap the total number of items returned |
102
+ | `--limit N` | client-side cap for non-paginated (vSphere) lists |
103
+
104
+ ```sh
105
+ vsc --profile prod nsx segments list --all # every page, concatenated
106
+ vsc --profile prod nsx segments list --page-size 50 # one page (cursor surfaced for manual paging)
107
+ vsc --profile prod vsphere vm list --limit 20 # first 20 only
108
+ ```
109
+
110
+ Without `--all`, a paginated `list` returns one page and surfaces the `cursor` so
111
+ an agent can drive pagination itself. On non-paginated backends (vSphere) `--all`
112
+ is a no-op — the output stays a plain array.
@@ -0,0 +1,232 @@
1
+ # vcf-super-cli v0.3 — Ergonomics (design)
2
+
3
+ **Status:** approved 2026-06-05. Milestone: `v0.3 — ergonomics` (#3).
4
+ **Predecessors:** [v0.1 design](2026-06-05-vcf-super-cli-design.md), [v0.2 writes design](2026-06-05-vcf-super-cli-v0.2-writes-design.md).
5
+
6
+ ## Goal
7
+
8
+ Make the dynamically-generated CLI pleasant to drive by hand and still trivially
9
+ scriptable by an agent. Three independent feature areas, each shipped as its own
10
+ issue/PR under milestone #3:
11
+
12
+ 1. **Static shell completion** — offline tab-completion of enums, output formats,
13
+ profile names, and per-field filter choices.
14
+ 2. **Filter & pagination helpers** — per-field filter flags (augmenting today's
15
+ `--filter '<json>'` blob) and friendlier paging (`--all`, `--max-items`,
16
+ `--limit`).
17
+ 3. **pyVmomi fallback commands** — hand-written read commands (perf, events/tasks,
18
+ inventory walk) for gaps the vAPI/REST surface doesn't cover.
19
+
20
+ The agent contract is unchanged and non-negotiable: stable JSON output, the
21
+ documented error envelope, the frozen exit codes, and **writes stay dry-run by
22
+ default**. The pyVmomi additions are all reads, so they need no `--apply` gate.
23
+
24
+ ## Locked decisions (from brainstorm, 2026-06-05)
25
+
26
+ - **Completion is static/offline only.** No feature in v0.3 hits the API during
27
+ `<TAB>`. Live resource-ID completion (e.g. completing `<vm>` from a live list)
28
+ is explicitly **deferred to v0.4**; the param model already carries
29
+ `resource_types` so it remains a clean future extension.
30
+ - **Filter flags augment, never replace, `--filter`.** Raw `--filter '<json>'`
31
+ stays as the base layer / escape hatch; generated per-field flags merge over it.
32
+ - **Pagination:** `--all` auto-follows the NSX cursor; `--max-items` caps total;
33
+ `--limit` is a client-side cap for non-cursor (vSphere) lists. NSX
34
+ `--page-size`/`--cursor` already exist as generated query options.
35
+ - **pyVmomi covers all three families** (perf, events/tasks, inventory walk),
36
+ built on a small shared `SmartConnect` foundation.
37
+
38
+ ---
39
+
40
+ ## Feature 1 — Static shell completion
41
+
42
+ ### Behaviour
43
+
44
+ Completion values are derived entirely from the introspected `Param` model plus
45
+ local config — never from a network call. `--help` and completion both stay fully
46
+ offline.
47
+
48
+ Completed surfaces:
49
+
50
+ | Surface | Source | Example |
51
+ |---------|--------|---------|
52
+ | enum option | `param.enum_values` | `--power-state POWERED_<TAB>` → `POWERED_ON`, `POWERED_OFF` |
53
+ | `--output` / `-o` | `OutputFormat` members | `-o <TAB>` → `json`, `table` |
54
+ | global `--profile` / `-p` | profile names from `load_config()` (offline file read) | `-p <TAB>` → `prod`, `lab` |
55
+ | per-field filter enum flags | reuse enum completer (feature 2) | `--filter-power-state <TAB>` |
56
+
57
+ Resource-ID args (the `<vm>` positionals) are **not** completed in v0.3.
58
+
59
+ ### Implementation
60
+
61
+ - New module **`vsc/gen/complete.py`** — pure completer factories:
62
+ - `enum_completer(values: list[str]) -> Callable[[str], list[str]]`
63
+ - `profile_completer() -> Callable[[str], list[str]]`
64
+ Each takes the Click `incomplete` string and returns the prefix-filtered
65
+ candidate list. No `ctx`/`param` dependency → unit-testable without a shell.
66
+ - **`vsc/gen/builder.py`** `_build_signature`: when a non-path option is built for
67
+ an `ENUM` param, pass `autocompletion=enum_completer(param.enum_values)` to
68
+ `typer.Option`. When building `--output`, attach an `OutputFormat` completer.
69
+ - **`vsc/cli/app.py`** main callback: attach `profile_completer()` to `--profile`.
70
+ - Root app already sets `add_completion=True`; document `vsc --install-completion`
71
+ and `vsc --show-completion` in `docs/usage.md`.
72
+
73
+ ### Tests
74
+
75
+ `tests/test_complete.py` — completer factories return correctly prefix-filtered
76
+ lists, empty `incomplete` returns all, no match returns `[]`. A smoke test asserts
77
+ generated enum options carry an `autocompletion` callback.
78
+
79
+ ---
80
+
81
+ ## Feature 2 — Filter & pagination helpers
82
+
83
+ ### Per-field filter flags
84
+
85
+ vCenter list operations take a single `filter` parameter that is a `STRUCT`
86
+ (`VM.FilterSpec` etc.). Today the user must pass it as `--filter '<json>'`. v0.3
87
+ additionally flattens that struct's fields into typed options.
88
+
89
+ - **Detection:** on a read/list op, a parameter that is `ParamKind.STRUCT` **and
90
+ named `filter`** is flattened. Write-body structs are *not* flattened — they
91
+ stay JSON (`--spec`, `--segment`, …).
92
+ - **Generated flags:** each struct field → `--<field>` (kebab-cased). `list`/`set`
93
+ fields are **repeatable** (`list[str]` annotation, multiple uses accumulate).
94
+ `enum` fields carry choices in help + the enum completer.
95
+ - **Precedence / merge:** raw `--filter '<json>'` provides the base dict; per-field
96
+ flags merge **over** it (a flag wins over the same key in the blob). The merged
97
+ dict is coerced into the FilterSpec via the existing
98
+ `coerce_struct`/`coerce_value` path — no new coercion logic.
99
+ - **Collisions:** a flattened field whose flag would collide with a reserved option
100
+ (`--output`, `--apply`) or another option is suffixed using the existing
101
+ `_sig_name` mechanism.
102
+
103
+ New helper **`vsc/gen/filters.py`**:
104
+ - `flatten_filter(param: Param) -> list[Param]` — child params for each struct field
105
+ (built with `param_from_type` on `struct_type.get_field(name)`).
106
+ - `assemble_filter(base_json, field_values, param) -> Any` — merge base + per-field
107
+ values and coerce into the struct. Pure; unit-testable.
108
+
109
+ `builder.py` consumes these: `_build_signature` emits the child options (tracking
110
+ them so `_collect_kwargs` knows to reassemble rather than pass them through), and
111
+ `_collect_kwargs` calls `assemble_filter` to rebuild the single `filter` kwarg.
112
+
113
+ ### Pagination
114
+
115
+ NSX Policy list ops return a `*ListResult` struct (`.results`, `.cursor`,
116
+ `.result_count`) and already expose `cursor`/`page_size` as generated query
117
+ options. vSphere REST list ops return a plain list and do not paginate.
118
+
119
+ Injected options on **list verbs** (`op.cli_verb == "list"`):
120
+
121
+ | Flag | Applies to | Effect |
122
+ |------|-----------|--------|
123
+ | `--all` | cursor lists (NSX) | follow `.cursor` across pages, concatenate `.results`, stop at `--max-items` |
124
+ | `--max-items N` | all lists | hard cap on returned items (post-fetch for vSphere, loop-stop for `--all`) |
125
+ | `--limit N` | non-cursor lists (vSphere) | client-side slice of the returned list |
126
+
127
+ Without `--all`, a cursor list returns one page **including** its `cursor`, so an
128
+ agent can paginate manually. `--all` and `--max-items` interact: the follow loop
129
+ stops once `--max-items` items are collected.
130
+
131
+ New helper **`vsc/gen/paginate.py`**:
132
+ - `follow_cursor(fetch_page, *, max_items) -> list` — `fetch_page(cursor) ->
133
+ (results, next_cursor)`; loops until `next_cursor` is empty/repeated or the cap
134
+ is hit. Pure given the `fetch_page` callable (the callable closes over the SDK
135
+ method in `builder.py`); unit-testable with a fake pager.
136
+
137
+ `builder.py` `make_command` detects a cursor-bearing result and, when `--all` is
138
+ set, drives `follow_cursor`; otherwise applies `--limit`/`--max-items` slicing
139
+ before `emit()`.
140
+
141
+ ### Tests
142
+
143
+ `tests/test_filters.py` — flatten produces one child per field with correct
144
+ kind/required/repeatable; assemble merges blob + flags with flags winning; enum
145
+ field carries choices. `tests/test_paginate.py` — `follow_cursor` concatenates,
146
+ respects `max_items`, terminates on empty/duplicate cursor. Builder integration
147
+ tests assert a vCenter list op grows `--<field>` options and an NSX list op grows
148
+ `--all`/`--max-items`.
149
+
150
+ ---
151
+
152
+ ## Feature 3 — pyVmomi fallback commands
153
+
154
+ Hand-written read commands for gaps the vAPI/REST surface doesn't cover, mounted
155
+ into the existing `vsc vsphere` tree and emitted through the existing
156
+ `emit()`/error-envelope contract.
157
+
158
+ ### Foundation
159
+
160
+ - New module **`vsc/connect/vmomi.py`**:
161
+ - `connect_vmomi() -> ServiceInstance` via `pyVim.connect.SmartConnect`, reusing
162
+ `resolve_target("vsphere")` for server/user/password and an `ssl` context that
163
+ honours `verify` (unverified context when `insecure`). Cached like the vAPI
164
+ connections (`reset_cache()` clears it); `Disconnect` registered at process
165
+ exit.
166
+ - `vmomi_jsonable(obj) -> Any` — convert managed-object / data-object trees into
167
+ plain JSON-able dicts (`moref` → `{"type", "value"}`, data objects → field
168
+ dicts, leave scalars/datetimes alone) so `emit()` renders them like any other
169
+ result.
170
+ - New package **`vsc/pyvmomi/`**, one module per family, each exposing a
171
+ `typer.Typer`. `vsc/cli/app.py` adds them onto the vsphere group
172
+ (`vsphere_group.add_typer(perf_app, name="perf")`, …). pyVmomi errors
173
+ (`vim.fault.*`, connection failures) map into the existing envelope/exit codes
174
+ (auth→3, not-found→4, connection→5) via a small adapter reusing
175
+ `vsc/output/errors.py` patterns.
176
+
177
+ ### Commands
178
+
179
+ - **`vsc vsphere perf`** — real-time/historical counters via `PerformanceManager`.
180
+ e.g. `perf vm <vm> --metric cpu.usage [--interval 20s]`,
181
+ `perf host <host> --metric mem.usage`. Resolves the counter id, queries
182
+ `QueryPerf`, emits timestamped samples.
183
+ - **`vsc vsphere events`** — recent events via `EventManager` with `--since`/entity
184
+ filters. **`vsc vsphere tasks`** — running + recent tasks via `TaskManager`.
185
+ - **`vsc vsphere inventory`** — `PropertyCollector` walk for properties the REST
186
+ list ops omit (device tree, custom attributes, relationships), e.g.
187
+ `inventory vm <vm> --props config.hardware`.
188
+
189
+ All are reads → no `--apply`. They accept the same `--output json|table` and obey
190
+ `--profile`.
191
+
192
+ ### Tests
193
+
194
+ pyVmomi has no offline-introspection trick like the vAPI bindings, so tests mock
195
+ `SmartConnect`/`ServiceInstance` and the relevant managers. `tests/test_vmomi.py`
196
+ covers `vmomi_jsonable` conversion (moref/data-object/scalar) purely;
197
+ `tests/test_pyvmomi_*.py` drive each command against a fake `ServiceInstance`
198
+ asserting the emitted JSON shape and error mapping. No live vCenter required.
199
+
200
+ ---
201
+
202
+ ## Cross-cutting
203
+
204
+ - Each feature PR updates the relevant `docs/` page(s) and the bundled
205
+ `vsc/skill/assets/SKILL.md` for any new surface.
206
+ - `ruff`, `mypy --strict`, `pytest`, and `mkdocs --strict` stay green; CI green
207
+ before merge.
208
+ - Every PR gets an adversarial **refute-before-accept** review pass; findings are
209
+ fixed before merge.
210
+
211
+ ## Issues (milestone #3)
212
+
213
+ | Issue | Title | Depends on |
214
+ |-------|-------|-----------|
215
+ | Epic | v0.3 — ergonomics | — |
216
+ | A | Static shell completion (enums, `--output`, `--profile`, filter choices) | — |
217
+ | B | Filter & pagination helpers (per-field flags + `--all`/`--max-items`/`--limit`) | — |
218
+ | C | pyVmomi fallback foundation + `vsc vsphere perf` | — |
219
+ | D | pyVmomi events & tasks | C |
220
+ | E | pyVmomi inventory walk | C |
221
+ | F | v0.3 docs/SKILL roundup + release prep | A–E |
222
+
223
+ Build order: A and B in parallel; C → (D, E); F last. The release tag (`vX.Y.Z`)
224
+ is pushed **manually by the maintainer** after F merges — `release.yml` keeps **no
225
+ `environment:` block** (the PyPI trusted publisher is registered with a blank
226
+ environment; they must stay matched).
227
+
228
+ ## Out of scope (v0.3)
229
+
230
+ - Live resource-ID completion (hits the API) — v0.4 candidate.
231
+ - pyVmomi *writes* — the fallback surface is read-only this milestone.
232
+ - NSX Manager / Global-Manager APIs (still Policy-only).
@@ -14,6 +14,25 @@ vsc --profile prod vsphere vm list -o table # table
14
14
 
15
15
  Only `json` and `table` are accepted; anything else is rejected with exit code `2`.
16
16
 
17
+ ## Shell completion
18
+
19
+ Install completion for your shell once:
20
+
21
+ ```sh
22
+ vsc --install-completion # bash, zsh, fish, PowerShell
23
+ vsc --show-completion # print the script instead of installing
24
+ ```
25
+
26
+ Completion is **fully offline** — it never opens a connection. It suggests:
27
+
28
+ - enum option choices (e.g. `--power-states <TAB>` → `POWERED_ON`, `POWERED_OFF`),
29
+ - output formats (`-o <TAB>` → `json`, `table`),
30
+ - configured profile names (`--profile <TAB>`),
31
+ - and per-field `list` filter enum choices.
32
+
33
+ Completing a live resource id (e.g. `<vm>` from a real inventory) would require a
34
+ network call and is deliberately **not** done in this release.
35
+
17
36
  ## Error envelope
18
37
 
19
38
  Errors are written to **stderr** as a stable JSON object:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "vcf-super-cli"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Modern, agent-friendly CLI for VMware Cloud Foundation 9, with a command tree generated dynamically from the vcf-sdk vAPI bindings."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -89,10 +89,16 @@ ignore = [
89
89
  "vsc/cli/**" = [
90
90
  "B008", # typer.Option/Argument in defaults is the Typer idiom
91
91
  ]
92
+ "vsc/pyvmomi/**" = [
93
+ "B008", # typer.Option/Argument in defaults is the Typer idiom
94
+ ]
92
95
  "vsc/gen/**" = [
93
96
  "PLR0911", # type dispatch tables legitimately have many returns
94
97
  "PLR0912", # ...and many branches
95
98
  ]
99
+ "vsc/connect/vmomi.py" = [
100
+ "PLR0911", # vmomi_jsonable is a type-dispatch table (many returns)
101
+ ]
96
102
  "tests/**" = [
97
103
  "PLR2004", # magic values in tests are fine
98
104
  "PLR0915", # lifecycle tests legitimately need many statements
@@ -112,6 +118,14 @@ plugins = ["pydantic.mypy"]
112
118
  module = ["com", "com.*", "vcf", "vcf.*", "vmware", "vmware.*", "pyVmomi.*", "pyVim.*"]
113
119
  ignore_missing_imports = true
114
120
 
121
+ [[tool.mypy.overrides]]
122
+ # pyVmomi/pyVim are dynamically typed (attributes synthesized at runtime); skipping
123
+ # their analysis treats their surface as Any rather than flagging every vim.* access
124
+ # and untyped SmartConnect/Disconnect call.
125
+ module = ["pyVmomi", "pyVmomi.*", "pyVim", "pyVim.*"]
126
+ follow_imports = "skip"
127
+ follow_imports_for_stubs = true
128
+
115
129
  [tool.pytest.ini_options]
116
130
  addopts = "-ra --strict-markers --strict-config --ignore=tests/e2e"
117
131
  testpaths = ["tests"]
@@ -278,7 +278,7 @@ def _synthetic_write_with_param(param_name: str) -> Operation:
278
278
  def test_injected_flags_do_not_collide_with_reserved_param_names() -> None:
279
279
  # A write op with a body param literally named 'apply' must not produce two
280
280
  # --apply option declarations; the user param is renamed.
281
- sig, _spec = _build_signature(_synthetic_write_with_param("apply"))
281
+ sig, _spec, _fp = _build_signature(_synthetic_write_with_param("apply"))
282
282
  decls: list[str] = []
283
283
  for p in sig.parameters.values():
284
284
  info = p.default
@@ -0,0 +1,100 @@
1
+ """Static (offline) shell-completion helpers and their wiring into commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from typing import ClassVar
7
+
8
+ import pytest
9
+
10
+ from vsc.gen.builder import _OUTPUT_PARAM, _build_signature
11
+ from vsc.gen.complete import enum_completer, output_format_completer, profile_completer
12
+ from vsc.gen.model import Operation, Param, ParamKind
13
+
14
+
15
+ def test_enum_completer_prefix_filters() -> None:
16
+ complete = enum_completer(["POWERED_ON", "POWERED_OFF", "SUSPENDED"])
17
+ assert complete("POWERED_") == ["POWERED_ON", "POWERED_OFF"]
18
+
19
+
20
+ def test_enum_completer_empty_incomplete_returns_all() -> None:
21
+ values = ["A", "B", "C"]
22
+ assert enum_completer(values)("") == values
23
+
24
+
25
+ def test_enum_completer_no_match_returns_empty() -> None:
26
+ assert enum_completer(["A", "B"])("Z") == []
27
+
28
+
29
+ def test_enum_completer_none_incomplete_returns_all() -> None:
30
+ # Typer's wrapper types incomplete as ``str | None``; a None must not crash
31
+ # completion — treat it like an empty prefix.
32
+ assert enum_completer(["A", "B"])(None) == ["A", "B"] # type: ignore[arg-type]
33
+
34
+
35
+ def test_output_format_completer_offers_formats() -> None:
36
+ assert output_format_completer()("") == ["json", "table"]
37
+ assert output_format_completer()("t") == ["table"]
38
+
39
+
40
+ def test_profile_completer_filters_names(monkeypatch: pytest.MonkeyPatch) -> None:
41
+ class _Cfg:
42
+ profiles: ClassVar[dict[str, object]] = {"prod": 1, "prod-eu": 2, "lab": 3}
43
+
44
+ monkeypatch.setattr("vsc.gen.complete.load_config", _Cfg)
45
+ assert profile_completer()("prod") == ["prod", "prod-eu"]
46
+
47
+
48
+ def test_profile_completer_is_failsoft(monkeypatch: pytest.MonkeyPatch) -> None:
49
+ def _boom() -> object:
50
+ raise RuntimeError("no config readable")
51
+
52
+ monkeypatch.setattr("vsc.gen.complete.load_config", _boom)
53
+ assert profile_completer()("anything") == [] # never raises during <TAB>
54
+
55
+
56
+ # --------------------------------------------------------------------------- #
57
+ # Wiring: generated options carry the right completer
58
+ # --------------------------------------------------------------------------- #
59
+
60
+
61
+ def _op_with(param: Param) -> Operation:
62
+ return Operation(
63
+ backend="vsphere",
64
+ service_cls=object,
65
+ iface_id="com.vmware.vcenter.thing",
66
+ op_id="get",
67
+ method_name="get",
68
+ cli_verb="get",
69
+ http_method="GET",
70
+ url_template="/vcenter/thing",
71
+ path_vars=[],
72
+ path_var_map={},
73
+ params=[param],
74
+ )
75
+
76
+
77
+ def _option_for(sig: inspect.Signature, sig_name: str) -> object:
78
+ return sig.parameters[sig_name].default
79
+
80
+
81
+ def test_enum_option_gets_autocompletion() -> None:
82
+ enum_param = Param(name="state", kind=ParamKind.ENUM, required=False, enum_values=["ON", "OFF"])
83
+ sig, _spec, _fp = _build_signature(_op_with(enum_param))
84
+ opt = _option_for(sig, "state")
85
+ assert opt.autocompletion is not None
86
+ assert opt.autocompletion("O") == ["ON", "OFF"]
87
+
88
+
89
+ def test_non_enum_option_has_no_autocompletion() -> None:
90
+ string_param = Param(name="name", kind=ParamKind.STRING, required=False)
91
+ sig, _spec, _fp = _build_signature(_op_with(string_param))
92
+ assert _option_for(sig, "name").autocompletion is None
93
+
94
+
95
+ def test_output_option_gets_format_completion() -> None:
96
+ string_param = Param(name="name", kind=ParamKind.STRING, required=False)
97
+ sig, _spec, _fp = _build_signature(_op_with(string_param))
98
+ opt = _option_for(sig, _OUTPUT_PARAM)
99
+ assert opt.autocompletion is not None
100
+ assert opt.autocompletion("") == ["json", "table"]