vcf-super-cli 0.3.0__tar.gz → 0.5.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 (97) hide show
  1. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/CHANGELOG.md +48 -0
  2. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/PKG-INFO +3 -2
  3. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/README.md +2 -1
  4. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/docs/commands.md +11 -0
  5. vcf_super_cli-0.5.0/docs/design.md +93 -0
  6. vcf_super_cli-0.5.0/docs/index.md +48 -0
  7. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/docs/profiles.md +4 -0
  8. vcf_super_cli-0.5.0/docs/superpowers/specs/2026-06-08-vcf-super-cli-v0.5-find-vms-design.md +78 -0
  9. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/docs/usage.md +29 -2
  10. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/pyproject.toml +1 -1
  11. vcf_super_cli-0.5.0/tests/test_complete_cache.py +107 -0
  12. vcf_super_cli-0.5.0/tests/test_complete_dynamic.py +205 -0
  13. vcf_super_cli-0.5.0/tests/test_pyvmomi_find.py +342 -0
  14. vcf_super_cli-0.5.0/tests/test_resources.py +115 -0
  15. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/uv.lock +1 -1
  16. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/_version.py +1 -1
  17. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/gen/builder.py +20 -3
  18. vcf_super_cli-0.5.0/vsc/gen/complete_cache.py +128 -0
  19. vcf_super_cli-0.5.0/vsc/gen/complete_dynamic.py +145 -0
  20. vcf_super_cli-0.5.0/vsc/gen/resources.py +135 -0
  21. vcf_super_cli-0.5.0/vsc/pyvmomi/find.py +187 -0
  22. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/pyvmomi/inventory.py +60 -0
  23. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/skill/assets/SKILL.md +11 -3
  24. vcf_super_cli-0.3.0/docs/design.md +0 -31
  25. vcf_super_cli-0.3.0/docs/index.md +0 -38
  26. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/.github/workflows/docs.yml +0 -0
  27. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/.github/workflows/e2e.yml +0 -0
  28. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/.github/workflows/lint.yml +0 -0
  29. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/.github/workflows/release.yml +0 -0
  30. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/.github/workflows/test.yml +0 -0
  31. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/.gitignore +0 -0
  32. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/AGENTS.md +0 -0
  33. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/LICENSE +0 -0
  34. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/docs/install.md +0 -0
  35. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/docs/superpowers/specs/2026-06-05-vcf-super-cli-design.md +0 -0
  36. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/docs/superpowers/specs/2026-06-05-vcf-super-cli-v0.2-writes-design.md +0 -0
  37. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/docs/superpowers/specs/2026-06-05-vcf-super-cli-v0.3-ergonomics-design.md +0 -0
  38. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/docs/writes.md +0 -0
  39. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/mkdocs.yml +0 -0
  40. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/__init__.py +0 -0
  41. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/e2e/README.md +0 -0
  42. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/e2e/__init__.py +0 -0
  43. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/e2e/conftest.py +0 -0
  44. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/e2e/test_read_smoke.py +0 -0
  45. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_builder.py +0 -0
  46. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_cli.py +0 -0
  47. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_complete.py +0 -0
  48. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_config.py +0 -0
  49. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_discover.py +0 -0
  50. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_errors.py +0 -0
  51. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_filter_pagination_cli.py +0 -0
  52. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_filters.py +0 -0
  53. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_paginate.py +0 -0
  54. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_params.py +0 -0
  55. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_preview.py +0 -0
  56. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_pyvmomi_events.py +0 -0
  57. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_pyvmomi_inventory.py +0 -0
  58. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_pyvmomi_perf.py +0 -0
  59. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_pyvmomi_tasks.py +0 -0
  60. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_render.py +0 -0
  61. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_session.py +0 -0
  62. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_skill.py +0 -0
  63. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_version.py +0 -0
  64. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_vmomi.py +0 -0
  65. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/tests/test_write_errors.py +0 -0
  66. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/__init__.py +0 -0
  67. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/cli/__init__.py +0 -0
  68. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/cli/app.py +0 -0
  69. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/cli/profiles.py +0 -0
  70. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/cli/skill.py +0 -0
  71. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/config/__init__.py +0 -0
  72. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/config/schema.py +0 -0
  73. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/config/store.py +0 -0
  74. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/connect/__init__.py +0 -0
  75. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/connect/session.py +0 -0
  76. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/connect/targets.py +0 -0
  77. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/connect/vmomi.py +0 -0
  78. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/gen/__init__.py +0 -0
  79. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/gen/complete.py +0 -0
  80. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/gen/discover.py +0 -0
  81. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/gen/filters.py +0 -0
  82. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/gen/model.py +0 -0
  83. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/gen/paginate.py +0 -0
  84. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/gen/params.py +0 -0
  85. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/gen/preview.py +0 -0
  86. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/logging_config.py +0 -0
  87. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/output/__init__.py +0 -0
  88. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/output/errors.py +0 -0
  89. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/output/exit_codes.py +0 -0
  90. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/output/render.py +0 -0
  91. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/pyvmomi/__init__.py +0 -0
  92. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/pyvmomi/events.py +0 -0
  93. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/pyvmomi/perf.py +0 -0
  94. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/pyvmomi/runner.py +0 -0
  95. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/pyvmomi/tasks.py +0 -0
  96. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/skill/__init__.py +0 -0
  97. {vcf_super_cli-0.3.0 → vcf_super_cli-0.5.0}/vsc/skill/export.py +0 -0
@@ -7,6 +7,54 @@ versions may include breaking changes.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## v0.5.0 — 2026-06-08
11
+
12
+ Find VMs by attribute. Answers the everyday *"which VM has `10.20.3.41`?"* — a
13
+ reverse lookup the REST `vm list` filter can't do, because guest networking lives
14
+ only on the pyVmomi `guest.*` properties.
15
+
16
+ ### Added
17
+
18
+ - **`vsc vsphere inventory find`** (#54) — locate VMs by guest/runtime attribute
19
+ without knowing the moid: `--ip` (exact or CIDR, IPv4/IPv6, across the primary and
20
+ every NIC address), `--name`, `--hostname`, `--guest-os` (case-insensitive
21
+ substring or glob), `--mac` (exact), and `--power-state`. Flags **AND** together; a
22
+ repeated flag **ORs** within its field; at least one match flag is required (it
23
+ refuses to dump the whole inventory). `--props` (repeatable) widens each hit's
24
+ output only — never a match criterion. One PropertyCollector round-trip over a
25
+ container view (always destroyed); a pure, pyVmomi-free matcher does the filtering.
26
+ Reads only — no `--apply` — and emits the same JSON / error envelope / exit codes
27
+ as the other pyVmomi fallback commands.
28
+
29
+ ## v0.4.0 — 2026-06-08
30
+
31
+ Live resource-id completion. Pressing `<TAB>` on an id-typed argument can now
32
+ suggest **real ids** from the live inventory — strictly opt-in, cached, and
33
+ fail-soft. The agent-facing contract is unchanged: stable JSON, error envelope,
34
+ exit codes, dry-run-by-default writes, and `--help` stays fully offline.
35
+
36
+ ### Added
37
+
38
+ - **Live resource-id completion** (#44), opt-in via `VSC_COMPLETE_DYNAMIC=1` and
39
+ off by default. When enabled, `<TAB>` on an id arg/option suggests live ids for
40
+ the resource type (VMs, hosts, clusters, datacenters, datastores, resource
41
+ pools), with each resource's name shown as completion help. Built on:
42
+ - a resource-type → list-op **registry** derived purely by introspecting the
43
+ SDK metadata (#45), no network;
44
+ - a **TTL cache** under the platform cache dir, keyed by
45
+ profile/backend/resource-type (default 60s, `VSC_COMPLETE_TTL` override) (#46);
46
+ - a **dynamic completer** whose fetch is time-bounded (`VSC_COMPLETE_TIMEOUT`,
47
+ default 2s) and blanket fail-soft — any error, missing auth, or timeout
48
+ yields no suggestions and is never cached (#47).
49
+ - Static/offline completion (enums, output formats, profiles, filter enums) is
50
+ unchanged and remains the default. `--help` and command execution never open a
51
+ connection for completion.
52
+
53
+ ### Notes
54
+
55
+ - Live completion is a human convenience only; agents should keep using `list`
56
+ to discover ids and must not depend on completion for correctness (#48).
57
+
10
58
  ## v0.3.0 — 2026-06-05
11
59
 
12
60
  Ergonomics: friendlier filtering and paging, offline shell completion, and a
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vcf-super-cli
3
- Version: 0.3.0
3
+ Version: 0.5.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/
@@ -74,7 +74,8 @@ $ vsc vsphere power stop vm-42 --apply # writes are dry-run without --app
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
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
+ | Live resource-id completion | v0.4 |
78
+ | Find VMs by attribute — IP / hostname / MAC / guest OS / power (`vsc vsphere inventory find`) | ✅ v0.5 |
78
79
  | NSX Manager / Global-Manager, SDDC Manager, Operations, LCM | deferred |
79
80
 
80
81
  ## Install
@@ -42,7 +42,8 @@ $ vsc vsphere power stop vm-42 --apply # writes are dry-run without --app
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
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
+ | Live resource-id completion | v0.4 |
46
+ | Find VMs by attribute — IP / hostname / MAC / guest OS / power (`vsc vsphere inventory find`) | ✅ v0.5 |
46
47
  | NSX Manager / Global-Manager, SDDC Manager, Operations, LCM | deferred |
47
48
 
48
49
  ## Install
@@ -51,6 +51,17 @@ API. Those commands live under `vsc vsphere` alongside the generated ones, are
51
51
  managed-object properties the REST list ops omit (device tree, custom attributes),
52
52
  via the PropertyCollector. `--props` is repeatable (e.g. `--props config.hardware`);
53
53
  with none, a small per-type summary set is returned.
54
+ - `vsc vsphere inventory find [--ip …] [--name …] [--hostname …] [--mac …] [--guest-os …] [--power-state …] [--props …]`
55
+ — **find VMs by guest/runtime attribute without knowing the moid.** This is the
56
+ answer to *"which VM has `10.20.3.41`?"*: guest networking isn't in the REST
57
+ `vm list` filter, so this sweeps every VM's `guest.*` properties in one round-trip.
58
+ `--ip` accepts an exact address or a CIDR (`10.20.3.0/24`, IPv4 or IPv6) and matches
59
+ across the primary and every NIC address; `--name`/`--hostname`/`--guest-os` are
60
+ case-insensitive substring or glob; `--mac` is exact; `--power-state` is one of
61
+ `poweredOn`/`poweredOff`/`suspended`. Flags **AND** together, a repeated flag **ORs**
62
+ within its field, and at least one match flag is required. `--props` (repeatable)
63
+ widens each hit's output only — it never affects matching. Powered-off VMs and those
64
+ without VMware Tools report no guest IP and won't match `--ip`.
54
65
 
55
66
  ## `vsc nsx …` (NSX Policy)
56
67
 
@@ -0,0 +1,93 @@
1
+ # Design
2
+
3
+ `vsc` builds its command tree by **statically introspecting the installed
4
+ `vcf-sdk` vAPI bindings**. Both `vmware-vcenter` and `vcf-nsx` are vAPI stub
5
+ libraries on the shared `vmware-vapi-runtime`:
6
+
7
+ - Each service is a `VapiInterface` subclass (`VM`, `Host`, `Cluster`,
8
+ `Datastore`, `Network`, …).
9
+ - Every operation carries `OperationRestMetadata` (`http_method`,
10
+ `path_variables`, `query_parameters`).
11
+ - Parameters are typed via `StructType` binding types.
12
+
13
+ A single generator walks those classes and emits one Typer command per
14
+ operation. `http_method == "GET"` ⇒ a read command; other verbs ⇒ writes
15
+ (dry-run by default + `--apply`). Because the metadata ships inside the SDK, the
16
+ tree builds **offline** — no server needed to render `--help`.
17
+
18
+ Top-level grouping:
19
+
20
+ - `vsc vsphere …` → `com.vmware.vcenter` (+ selected `com.vmware.esx`)
21
+ - `vsc nsx …` → `vcf.nsx.policy` (NSX Policy API)
22
+
23
+ The introspected intermediate representation is a list of `Operation` objects,
24
+ each carrying typed `Param`s (`vsc/gen/model.py`). Everything below is built from
25
+ that one model.
26
+
27
+ ## Ergonomics (v0.3)
28
+
29
+ These build on the introspected `Param` model without changing the agent
30
+ contract.
31
+
32
+ ### Offline shell completion
33
+
34
+ Completion values come entirely from the model and local config — **never a
35
+ network call**, so `<TAB>` stays fast and `--help` stays offline. Enum options
36
+ complete from their fixed choices, `--output` from the output formats, and
37
+ `--profile` from configured profile names (`vsc/gen/complete.py`). Completing a
38
+ live resource id would require a connection and is deferred to a later release.
39
+
40
+ ### Per-field filter flags
41
+
42
+ A `list` operation takes a single `filter` parameter that is a struct
43
+ (`VM.FilterSpec` etc.). The generator flattens that struct into typed
44
+ `--<field>` options (repeatable for list/set fields; enum fields validate and
45
+ complete their choices). The raw `--filter '<json>'` blob remains as a base
46
+ layer that per-field flags merge **over** (`vsc/gen/filters.py`).
47
+
48
+ ### Pagination
49
+
50
+ `list` commands gain `--all` (follow the NSX cursor across pages), `--max-items`
51
+ (cap the total), and `--limit` (client-side cap for non-paginated vSphere
52
+ lists). Without `--all`, a paginated list returns one page and surfaces its
53
+ `cursor` for manual paging; on non-cursor backends `--all` is a no-op and the
54
+ output stays a plain array (`vsc/gen/paginate.py`).
55
+
56
+ ## pyVmomi fallback (v0.3)
57
+
58
+ A few inventory/performance areas are only reachable through the older pyVmomi
59
+ SOAP API. Those are **hand-written read-only commands** mounted under
60
+ `vsc vsphere` alongside the generated ones, sharing the same output contract:
61
+
62
+ - a separate connection path, `connect_vmomi()` via `SmartConnect`, reusing the
63
+ resolved vSphere credentials and honouring `--insecure` like the REST path
64
+ (`vsc/connect/vmomi.py`);
65
+ - `vmomi_jsonable()` collapses managed objects to their moref and data objects to
66
+ dicts so results render like any other;
67
+ - a shared `run_read()` runner maps pyVmomi faults onto the same error envelope
68
+ and exit codes (`vsc/pyvmomi/runner.py`).
69
+
70
+ Commands: `perf` (PerformanceManager counters), `events` / `tasks`
71
+ (Event/Task managers), and `inventory` — a PropertyCollector property walk
72
+ (`inventory vm`/`host`) plus `inventory find`, which sweeps every VM's `guest.*`
73
+ properties in one round-trip to locate a VM by IP / name / hostname / MAC / guest
74
+ OS / power state (the fields the REST `vm list` filter can't reach). All are
75
+ reads — no `--apply`.
76
+
77
+ ## Design specs
78
+
79
+ The authoritative, milestone-by-milestone designs live in the repository:
80
+
81
+ - [v0.1 — dynamic read-only CLI](https://github.com/thomaschristory/vcf-super-cli/blob/main/docs/superpowers/specs/2026-06-05-vcf-super-cli-design.md)
82
+ - [v0.2 — writes](https://github.com/thomaschristory/vcf-super-cli/blob/main/docs/superpowers/specs/2026-06-05-vcf-super-cli-v0.2-writes-design.md)
83
+ - [v0.3 — ergonomics](https://github.com/thomaschristory/vcf-super-cli/blob/main/docs/superpowers/specs/2026-06-05-vcf-super-cli-v0.3-ergonomics-design.md)
84
+ - [v0.5 — find VMs by attribute](https://github.com/thomaschristory/vcf-super-cli/blob/main/docs/superpowers/specs/2026-06-08-vcf-super-cli-v0.5-find-vms-design.md)
85
+
86
+ ## Contracts
87
+
88
+ - **Output:** JSON by default; `--output table` for humans.
89
+ - **Errors:** stable envelope `{ "error": { code, message, kind, details } }`.
90
+ - **Exit codes:** documented `IntEnum` (`0` ok, `1` generic, `2` usage, `3` auth,
91
+ `4` not-found, `5` connection, `6` config, `7` conflict, `8` unavailable).
92
+ - **Writes:** dry-run by default; `--apply` is the only gate and a dry-run opens
93
+ no connection.
@@ -0,0 +1,48 @@
1
+ # vcf-super-cli
2
+
3
+ A modern, agent-friendly CLI for **VMware Cloud Foundation 9** whose command tree
4
+ is **generated dynamically** from the official [`vcf-sdk`](https://pypi.org/project/vcf-sdk/)
5
+ vAPI bindings.
6
+
7
+ ```console
8
+ $ vsc vsphere vm list --power-states POWERED_ON --profile prod
9
+ $ vsc nsx segments list --all --output table
10
+ $ vsc vsphere perf vm vm-42 --metric cpu.usage
11
+ ```
12
+
13
+ !!! warning "Pre-1.0"
14
+ Reads and writes are both available. **Writes are dry-run by default** —
15
+ nothing changes without `--apply`. See [Writes](writes.md). While on `0.x`,
16
+ minor versions may include breaking changes.
17
+
18
+ ## Highlights
19
+
20
+ - **Mirrors the real API** — commands come from the SDK's vAPI metadata, covering
21
+ vCenter and NSX from one generator.
22
+ - **Ergonomic** — tab-completion (enums, formats, profiles, filter choices, and
23
+ opt-in [live resource ids](usage.md#live-resource-id-completion-opt-in)),
24
+ per-field `--<field>` filter flags, and paging (`--all` / `--max-items` /
25
+ `--limit`).
26
+ - **pyVmomi fallback** — read-only `perf`, `events`, `tasks`, and `inventory`
27
+ commands for areas the REST/vAPI surface doesn't cover, including
28
+ `inventory find` to locate a VM by IP / hostname / MAC / guest OS.
29
+ - **Safe by default** — writes are dry-run unless `--apply`; a dry-run never connects.
30
+ - **Agent-friendly** — JSON output, stable error envelope, documented exit codes,
31
+ bundled agent Skill.
32
+
33
+ See the [Design](design.md) for how dynamic generation works, and
34
+ [Commands](commands.md) for the full surface.
35
+
36
+ ## Install
37
+
38
+ ```sh
39
+ uv tool install vcf-super-cli # or: pip install vcf-super-cli
40
+ vsc --install-completion # optional: offline shell completion
41
+ ```
42
+
43
+ From source:
44
+
45
+ ```sh
46
+ uv sync
47
+ uv run vsc --help
48
+ ```
@@ -56,6 +56,10 @@ VSC_PROFILE=prod vsc nsx segments list
56
56
  | `VSC_VSPHERE_INSECURE` / `VSC_NSX_INSECURE` | `1`/`true` to skip TLS verification (lab/self-signed) |
57
57
  | `VSC_CONFIG_FILE` | Override the config file location |
58
58
  | `VSC_LOG_LEVEL` | Log level on stderr (default `WARNING`) |
59
+ | `VSC_COMPLETE_DYNAMIC` | `1`/`true` to enable [live resource-id completion](usage.md#live-resource-id-completion-opt-in) (off by default) |
60
+ | `VSC_COMPLETE_TTL` | Live-completion cache TTL in seconds (default `60`) |
61
+ | `VSC_COMPLETE_TIMEOUT` | Hard timeout for a live-completion fetch in seconds (default `2`) |
62
+ | `VSC_CACHE_DIR` | Override the cache directory (where live-completion results are cached) |
59
63
 
60
64
  !!! warning "TLS"
61
65
  Verification is **on by default**. Only set `VSC_*_INSECURE` (or
@@ -0,0 +1,78 @@
1
+ # v0.5 — find VMs by attribute
2
+
3
+ **Milestone:** v0.5 — find VMs · **Issue:** #54 · **Date:** 2026-06-08
4
+
5
+ ## Problem
6
+
7
+ You can `vsc vsphere vm list` and filter on what the vAPI `VM.FilterSpec` exposes
8
+ (names, power states, hosts, clusters, datacenters, folders, resource pools).
9
+ None of the **guest/runtime network fields** are filterable there, so the everyday
10
+ question *"which VM has `10.20.3.41`?"* is unanswerable from the CLI. Guest
11
+ networking lives only on the pyVmomi `guest.*` properties; the per-VM REST guest
12
+ API needs the moid first, useless for a reverse lookup.
13
+
14
+ ## Command
15
+
16
+ ```
17
+ vsc vsphere inventory find [--ip ...] [--name ...] [--hostname ...] [--mac ...]
18
+ [--guest-os ...] [--power-state ...] [--props ...] [-o ...]
19
+ ```
20
+
21
+ Third command under `inventory_app`, next to `vm`/`host`. VM-focused for v1.
22
+
23
+ ### Match semantics
24
+
25
+ - Flags **AND** together; a repeated flag **ORs** within that field.
26
+ - `--ip` matches any address across `guest.ipAddress` + every `guest.net[].ipAddress`,
27
+ as an **exact IP or CIDR** via stdlib `ipaddress` (IPv4 and IPv6). An exact IP is
28
+ just a `/32` (or `/128`) network, so one code path covers both.
29
+ - `--name`, `--hostname`, `--guest-os` are case-insensitive **substring**, or
30
+ **glob** when the pattern has `*?[`. `--name` is fully client-side (one matcher).
31
+ - `--mac` exact, case-insensitive, against `guest.net[].macAddress`.
32
+ - `--power-state` ∈ {poweredOn, poweredOff, suspended} against `runtime.powerState`.
33
+ - **No match flag → usage error** (exit 2); we refuse to dump the whole inventory.
34
+ `--props`/`-o` alone do not count.
35
+
36
+ ### `--props` passthrough (output only)
37
+
38
+ Repeatable; mirrors `inventory vm --props`. Appends arbitrary property paths to the
39
+ single PropertyCollector retrieve so each **matched** VM also surfaces them under a
40
+ `properties` sub-dict — bridging search→inspect in one call. Never a match criterion.
41
+
42
+ ## Implementation
43
+
44
+ `vsc/pyvmomi/find.py`:
45
+
46
+ - **Pure matcher** (`Criteria`, `matches`, `summarize`, `validate_criteria`, plus
47
+ `_ip_match`/`_text_match`/`_addresses`/`_macs`) — operates only on a plain
48
+ props dict, no pyVmomi import in the hot path, so it unit-tests in isolation.
49
+ - **One round-trip** (`find_matches` → `_retrieve_all_vms`): a
50
+ `CreateContainerView(rootFolder, [vim.VirtualMachine], recursive=True)` plus a
51
+ single `RetrieveContents` with a `TraversalSpec` pulling the fixed search paths
52
+ **plus** any `--props`. The container view is **always destroyed** in a `finally`.
53
+
54
+ `vsc/pyvmomi/inventory.py` adds the thin `find` command: builds `Criteria` from the
55
+ flags, rejects an empty criteria set and malformed `--ip`/`--power-state` as usage
56
+ errors, and emits through the shared `run_read` runner (same JSON / error envelope /
57
+ exit codes as the other pyVmomi reads).
58
+
59
+ ### Per-hit shape
60
+
61
+ ```json
62
+ { "obj": {"type": "VirtualMachine", "value": "vm-101"},
63
+ "name": "web-1", "power_state": "poweredOn",
64
+ "ip_addresses": ["10.20.3.41"], "hostname": "web-1.corp",
65
+ "guest_os": "Ubuntu Linux (64-bit)",
66
+ "properties": { "config.version": "vmx-19" } } // only with --props
67
+ ```
68
+
69
+ No matches → empty array, exit 0.
70
+
71
+ ## Scope / non-goals (v1)
72
+
73
+ - vSphere/pyVmomi only; NSX has its own search surface.
74
+ - VMs only (no host/network/datastore search yet).
75
+ - Single `RetrieveContents` (no paging). Very large inventories may later want
76
+ `RetrievePropertiesEx` + `maxObjects` paging — a scaling follow-up.
77
+ - VMs without VMware Tools / powered-off report no (or stale) guest IP and won't
78
+ match `--ip`; expected, noted in the docs.
@@ -30,8 +30,35 @@ Completion is **fully offline** — it never opens a connection. It suggests:
30
30
  - configured profile names (`--profile <TAB>`),
31
31
  - and per-field `list` filter enum choices.
32
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.
33
+ ### Live resource-id completion (opt-in)
34
+
35
+ Completing a real id (e.g. `<vm>` from the live inventory) does require a network
36
+ call, so it is **opt-in** and off by default:
37
+
38
+ ```sh
39
+ export VSC_COMPLETE_DYNAMIC=1
40
+ vsc vsphere vm get <TAB> # → vm-101 vm-102 … (real ids, names as help)
41
+ ```
42
+
43
+ When enabled, pressing `<TAB>` on an id-typed argument or option suggests live
44
+ ids for that resource type (VMs, hosts, clusters, datacenters, datastores,
45
+ resource pools), showing each resource's name as completion help.
46
+
47
+ It is built to never get in your way:
48
+
49
+ - **Off by default.** Without `VSC_COMPLETE_DYNAMIC`, `<TAB>` stays fully offline
50
+ (the suggestions above are all you get).
51
+ - **Cached.** Results are cached per profile/backend/resource-type under the
52
+ platform cache dir with a short TTL (default 60s; override with
53
+ `VSC_COMPLETE_TTL=<seconds>`), so repeated presses don't re-hit the API.
54
+ - **Bounded and fail-soft.** The fetch is abandoned after a short timeout
55
+ (`VSC_COMPLETE_TIMEOUT`, default 2s); any error, missing auth, or timeout
56
+ yields no suggestions — `<TAB>` never hangs or prints a traceback.
57
+ - **`--help` is always offline.** Only the shell-completion subprocess ever
58
+ fetches; `--help` and command execution are unaffected.
59
+
60
+ This is a convenience only. The agent contract is unchanged: don't rely on
61
+ completion for correctness — list commands remain the source of truth for ids.
35
62
 
36
63
  ## Error envelope
37
64
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "vcf-super-cli"
3
- version = "0.3.0"
3
+ version = "0.5.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"
@@ -0,0 +1,107 @@
1
+ """On-disk TTL cache for live completion candidates.
2
+
3
+ The cache keeps ``<TAB>`` fast and stops repeated completions from re-hitting the
4
+ API. It is fail-soft by contract: a missing, corrupt or unwritable cache is a
5
+ miss, never an exception — completion must never error or hang.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from pathlib import Path
12
+
13
+ import pytest
14
+
15
+ from vsc.gen.complete_cache import cache_dir, cache_ttl, get_or_fetch
16
+
17
+
18
+ @pytest.fixture(autouse=True)
19
+ def _tmp_cache(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
20
+ monkeypatch.setenv("VSC_CACHE_DIR", str(tmp_path))
21
+ monkeypatch.delenv("VSC_COMPLETE_TTL", raising=False)
22
+
23
+
24
+ def test_hit_within_ttl_skips_fetch() -> None:
25
+ calls = 0
26
+
27
+ def fetch() -> list[tuple[str, str]]:
28
+ nonlocal calls
29
+ calls += 1
30
+ return [("vm-1", "web"), ("vm-2", "db")]
31
+
32
+ first = get_or_fetch("k", ttl=60.0, fetch_fn=fetch, now=1000.0)
33
+ second = get_or_fetch("k", ttl=60.0, fetch_fn=fetch, now=1059.0)
34
+ assert first == [("vm-1", "web"), ("vm-2", "db")]
35
+ assert second == first
36
+ assert calls == 1 # served from cache the second time
37
+
38
+
39
+ def test_expired_entry_refetches() -> None:
40
+ calls = 0
41
+
42
+ def fetch() -> list[tuple[str, str]]:
43
+ nonlocal calls
44
+ calls += 1
45
+ return [(f"vm-{calls}", "x")]
46
+
47
+ get_or_fetch("k", ttl=60.0, fetch_fn=fetch, now=1000.0)
48
+ again = get_or_fetch("k", ttl=60.0, fetch_fn=fetch, now=1061.0)
49
+ assert calls == 2
50
+ assert again == [("vm-2", "x")]
51
+
52
+
53
+ def test_corrupt_file_is_a_miss() -> None:
54
+ # Pre-create a garbage cache file at the key's path; get_or_fetch must treat
55
+ # it as a miss and never raise.
56
+ get_or_fetch("k", ttl=60.0, fetch_fn=lambda: [("a", "a")], now=0.0)
57
+ files = list(cache_dir().glob("*.json"))
58
+ assert files
59
+ files[0].write_text("}{ not json", encoding="utf-8")
60
+ out = get_or_fetch("k", ttl=60.0, fetch_fn=lambda: [("b", "b")], now=1.0)
61
+ assert out == [("b", "b")]
62
+
63
+
64
+ def test_cache_dir_created_lazily() -> None:
65
+ assert not cache_dir().exists()
66
+ get_or_fetch("k", ttl=60.0, fetch_fn=lambda: [("a", "a")], now=0.0)
67
+ assert cache_dir().exists()
68
+
69
+
70
+ def test_write_failure_is_failsoft(monkeypatch: pytest.MonkeyPatch) -> None:
71
+ def boom(*_a: object, **_k: object) -> None:
72
+ raise OSError("disk full")
73
+
74
+ monkeypatch.setattr("vsc.gen.complete_cache.os.replace", boom)
75
+ out = get_or_fetch("k", ttl=60.0, fetch_fn=lambda: [("a", "n")], now=0.0)
76
+ assert out == [("a", "n")] # fetched value still returned despite write failure
77
+
78
+
79
+ def test_items_round_trip_as_tuples() -> None:
80
+ get_or_fetch("k", ttl=60.0, fetch_fn=lambda: [("vm-1", "web")], now=0.0)
81
+ out = get_or_fetch("k", ttl=60.0, fetch_fn=lambda: [("zzz", "zzz")], now=1.0)
82
+ assert out == [("vm-1", "web")]
83
+ assert all(isinstance(item, tuple) and len(item) == 2 for item in out)
84
+
85
+
86
+ def test_malformed_entries_in_payload_are_dropped() -> None:
87
+ get_or_fetch("k", ttl=60.0, fetch_fn=lambda: [("a", "n")], now=0.0)
88
+ path = next(iter(cache_dir().glob("*.json")))
89
+ path.write_text(
90
+ json.dumps({"ts": 0.0, "items": [["ok", "name"], "bad", ["x"], [1, 2, 3]]}),
91
+ encoding="utf-8",
92
+ )
93
+ out = get_or_fetch("k", ttl=60.0, fetch_fn=lambda: [("z", "z")], now=1.0)
94
+ assert out == [("ok", "name")]
95
+
96
+
97
+ def test_cache_ttl_env_override(monkeypatch: pytest.MonkeyPatch) -> None:
98
+ assert cache_ttl() == 60.0
99
+ monkeypatch.setenv("VSC_COMPLETE_TTL", "5")
100
+ assert cache_ttl() == 5.0
101
+ monkeypatch.setenv("VSC_COMPLETE_TTL", "not-a-number")
102
+ assert cache_ttl() == 60.0 # invalid -> default, never raises
103
+
104
+
105
+ def test_cache_dir_under_override() -> None:
106
+ assert cache_dir().parent == Path(__import__("os").environ["VSC_CACHE_DIR"])
107
+ assert cache_dir().name == "completion"