kx-cli 0.0.5__tar.gz → 0.0.7__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 (87) hide show
  1. {kx_cli-0.0.5/src/kx_cli.egg-info → kx_cli-0.0.7}/PKG-INFO +76 -11
  2. {kx_cli-0.0.5 → kx_cli-0.0.7}/README.md +71 -9
  3. {kx_cli-0.0.5 → kx_cli-0.0.7}/pyproject.toml +6 -2
  4. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/delete.py +9 -2
  5. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/describe.py +6 -2
  6. kx_cli-0.0.7/src/kx/commands/diagnostic.py +195 -0
  7. kx_cli-0.0.7/src/kx/commands/edit.py +18 -0
  8. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/events.py +12 -1
  9. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/exec.py +5 -1
  10. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/get.py +15 -5
  11. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/logs.py +19 -4
  12. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/port_forward.py +8 -2
  13. kx_cli-0.0.7/src/kx/commands/theme.py +23 -0
  14. kx_cli-0.0.7/src/kx/config.py +82 -0
  15. kx_cli-0.0.7/src/kx/console.py +708 -0
  16. kx_cli-0.0.7/src/kx/diagnostics.py +363 -0
  17. kx_cli-0.0.7/src/kx/errors.py +80 -0
  18. kx_cli-0.0.7/src/kx/graph.py +186 -0
  19. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/index.py +8 -14
  20. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/kinds.py +4 -0
  21. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/main.py +276 -110
  22. kx_cli-0.0.7/src/kx/refresh.py +63 -0
  23. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/state.py +23 -2
  24. kx_cli-0.0.7/src/kx/themes.py +149 -0
  25. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/types.py +2 -0
  26. {kx_cli-0.0.5 → kx_cli-0.0.7/src/kx_cli.egg-info}/PKG-INFO +76 -11
  27. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx_cli.egg-info/SOURCES.txt +18 -0
  28. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx_cli.egg-info/requires.txt +5 -1
  29. {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_cli_get.py +14 -0
  30. kx_cli-0.0.7/tests/test_cli_help.py +60 -0
  31. kx_cli-0.0.7/tests/test_cli_kind_alias.py +155 -0
  32. kx_cli-0.0.7/tests/test_config.py +183 -0
  33. kx_cli-0.0.7/tests/test_console.py +645 -0
  34. kx_cli-0.0.7/tests/test_delete.py +61 -0
  35. {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_describe.py +26 -0
  36. kx_cli-0.0.7/tests/test_diagnostic.py +241 -0
  37. kx_cli-0.0.7/tests/test_diagnostics.py +415 -0
  38. {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_edit.py +26 -0
  39. kx_cli-0.0.7/tests/test_error_handling.py +193 -0
  40. kx_cli-0.0.7/tests/test_events.py +112 -0
  41. {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_exec.py +36 -2
  42. {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_get.py +25 -0
  43. kx_cli-0.0.7/tests/test_graph.py +233 -0
  44. {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_index.py +3 -4
  45. {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_logs.py +49 -0
  46. kx_cli-0.0.7/tests/test_namespace.py +105 -0
  47. {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_port_forward.py +26 -0
  48. kx_cli-0.0.7/tests/test_refresh.py +82 -0
  49. {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_state.py +52 -4
  50. kx_cli-0.0.7/tests/test_theme.py +40 -0
  51. kx_cli-0.0.7/tests/test_themes.py +36 -0
  52. kx_cli-0.0.7/tests/test_tree.py +59 -0
  53. kx_cli-0.0.5/src/kx/commands/edit.py +0 -12
  54. kx_cli-0.0.5/src/kx/config.py +0 -38
  55. kx_cli-0.0.5/src/kx/console.py +0 -368
  56. kx_cli-0.0.5/src/kx/graph.py +0 -231
  57. kx_cli-0.0.5/tests/test_config.py +0 -87
  58. kx_cli-0.0.5/tests/test_console.py +0 -202
  59. kx_cli-0.0.5/tests/test_namespace.py +0 -51
  60. {kx_cli-0.0.5 → kx_cli-0.0.7}/LICENSE +0 -0
  61. {kx_cli-0.0.5 → kx_cli-0.0.7}/setup.cfg +0 -0
  62. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/__init__.py +0 -0
  63. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/__init__.py +0 -0
  64. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/back.py +0 -0
  65. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/drop.py +0 -0
  66. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/forward.py +0 -0
  67. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/labels.py +0 -0
  68. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/namespace.py +0 -0
  69. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/rollout.py +0 -0
  70. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/scale.py +0 -0
  71. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/state.py +0 -0
  72. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/tree.py +0 -0
  73. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/yaml.py +0 -0
  74. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/events.py +0 -0
  75. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/k8s.py +0 -0
  76. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/kubectl.py +0 -0
  77. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx_cli.egg-info/dependency_links.txt +0 -0
  78. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx_cli.egg-info/entry_points.txt +0 -0
  79. {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx_cli.egg-info/top_level.txt +0 -0
  80. {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_back.py +0 -0
  81. {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_drop.py +0 -0
  82. {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_forward.py +0 -0
  83. {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_kinds.py +0 -0
  84. {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_labels.py +0 -0
  85. {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_rollout.py +0 -0
  86. {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_scale.py +0 -0
  87. {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_yaml.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kx-cli
3
- Version: 0.0.5
3
+ Version: 0.0.7
4
4
  Summary: kubectl wrapper with index-based resource selection
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.11
@@ -9,13 +9,16 @@ Classifier: Programming Language :: Python :: 3.13
9
9
  Requires-Python: >=3.11
10
10
  Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
12
- Requires-Dist: typer==0.26.8
12
+ Requires-Dist: typer==0.27.0
13
13
  Requires-Dist: kubernetes
14
14
  Requires-Dist: rich
15
15
  Provides-Extra: dev
16
16
  Requires-Dist: ruff; extra == "dev"
17
17
  Requires-Dist: pre-commit; extra == "dev"
18
18
  Requires-Dist: pytest; extra == "dev"
19
+ Requires-Dist: pytest-cov; extra == "dev"
20
+ Provides-Extra: build
21
+ Requires-Dist: pyinstaller; extra == "build"
19
22
  Dynamic: license-file
20
23
 
21
24
  <div align="center">
@@ -47,10 +50,42 @@ Dynamic: license-file
47
50
 
48
51
  ## Install
49
52
 
53
+ Requires Python 3.11+ and `kubectl` on your PATH.
54
+
55
+ With [uv](https://docs.astral.sh/uv/) (recommended):
56
+
57
+ ```bash
58
+ uv tool install kx-cli
59
+ ```
60
+
61
+ With [pipx](https://pipx.pypa.io/):
62
+
63
+ ```bash
64
+ pipx install kx-cli
65
+ ```
66
+
67
+ With pip:
68
+
50
69
  ```bash
51
70
  pip install kx-cli
52
71
  ```
53
72
 
73
+ As a kubectl plugin via [krew](https://krew.sigs.k8s.io/) (no Python required; pending krew-index acceptance):
74
+
75
+ ```bash
76
+ kubectl krew install kx
77
+ alias kx="kubectl kx"
78
+ ```
79
+
80
+ Standalone binaries (linux/macOS, amd64/arm64, no Python required) are attached to each [GitHub Release](https://github.com/jzills/kx/releases).
81
+
82
+ Or try it without installing (the package is `kx-cli`, the command is `kx`):
83
+
84
+ ```bash
85
+ uvx --from kx-cli kx get pods
86
+ pipx run --spec kx-cli kx get pods
87
+ ```
88
+
54
89
  ## Usage
55
90
 
56
91
  ### List resources
@@ -59,29 +94,34 @@ pip install kx-cli
59
94
  kx get <resource> [--match|-m <substring>] [kubectl flags...]
60
95
  ```
61
96
 
62
- Fetches resources and assigns index numbers. Any extra flags (e.g. `-n <namespace>`, `-A`) are passed through to kubectl. Use `--match`/`-m` to filter results by name (substring, case-insensitive).
97
+ Fetches resources and assigns index numbers. Any extra flags (e.g. `-n <namespace>`, `-A`) are passed through to kubectl. Use `--match`/`-m` to filter results by name (substring, case-insensitive). All-namespace listings (`-A`) are display-only — rows aren't indexed, since names aren't unique across namespaces; scope to a namespace to select.
98
+
99
+ Known kinds can drop the `get`: `kx pods`, `kx deploy -n kube-system`, `kx svc --match api`. This covers the built-in kinds and their kubectl shorthands (`po`, `deploy`, `svc`, `sts`, ...); existing commands take precedence, so `kx ns 3` still switches namespaces (bare `kx ns` lists them). CRDs and other resource types still use `kx get <resource>`. An integer after a kind is an index into the current state: `kx po 3` (or `kx get po 3`) relists just that pod, erroring if index 3 isn't a pod. Multiple indexes work too: `kx po 1 3`.
63
100
 
64
101
  ```
65
102
  $ kx get pods
66
- X NAME READY STATUS RESTARTS AGE
67
- 1 api-7d9f4b8c6-xkp2q 1/1 Running 0 2d
68
- 2 worker-6c8b5f7d9-mnt4r 1/1 Running 3 5h
69
- 3 postgres-0 1/1 Running 0 12d
103
+ Pods · default · 3 items
104
+ X NAME READY STATUS RESTARTS AGE
105
+ 1 api-7d9f4b8c6-xkp2q 1/1 Running 0 2d
106
+ 2 worker-6c8b5f7d9-mnt4r 1/1 Running 3 5h
107
+ 3 postgres-0 1/1 Running 0 12d
70
108
  ```
71
109
 
72
110
  All subsequent commands reference resources by their `X` index from the last `kx get`.
73
111
 
112
+ Global flags: `--no-color` disables styled output, `-v`/`--version` prints the installed version, and `--help` on any command shows usage, examples, and aliases.
113
+
74
114
  ### Commands
75
115
 
76
116
  <!-- commands-table-start -->
77
117
  | Command | Description |
78
118
  |---|---|
79
- | `kx get <resource> [--match/-m text] [kubectl flags...]` | List resources and assign index numbers for use with other commands. |
119
+ | `kx get <resource> [--match/-m str] [kubectl flags...]` | List resources and assign index numbers for use with other commands; shorthand: kx <kind> (e.g. kx pods, kx po 3). |
80
120
  | `kx describe <indexes>... [kubectl flags...]` | Show full kubectl describe output for one or more indexed resources. |
81
121
  | `kx events <indexes>...` | Show Kubernetes events for one or more indexed resources. |
82
122
  | `kx logs <index> [kubectl flags...]` | Stream logs for an indexed resource; aggregates across pods for Deployments, StatefulSets, DaemonSets, and Services. |
83
123
  | `kx labels <indexes>... [--selector/-s]` | Show labels for one or more indexed resources; --selector formats output as a label selector. |
84
- | `kx yaml <indexes>... [--show text]` | Print the raw YAML manifest for one or more indexed resources; --show filters to specific top-level fields. |
124
+ | `kx yaml <indexes>... [--show str]` | Print the raw YAML manifest for one or more indexed resources; --show filters to specific top-level fields. |
85
125
  | `kx delete <indexes>... [--yes/-y]` | Delete one or more indexed resources (prompts for confirmation unless --yes). |
86
126
  | `kx edit <index> [kubectl flags...]` | Open an indexed resource in your editor via kubectl edit. |
87
127
  | `kx exec <index> [<cmd>...] [kubectl flags...]` | Open an interactive shell in an indexed pod (bash, falling back to sh). |
@@ -89,7 +129,9 @@ All subsequent commands reference resources by their `X` index from the last `kx
89
129
  | `kx rollout <action> <index>` | Run a rollout action (status, restart, pause, resume, history, undo) on a Deployment, StatefulSet, or DaemonSet. |
90
130
  | `kx scale <index> <replicas>` | Scale an indexed Deployment, StatefulSet, or ReplicaSet to a given replica count. |
91
131
  | `kx port-forward <index> <port> [kubectl flags...]` | Forward a local port to an indexed resource (Pod, Deployment, ReplicaSet, StatefulSet, DaemonSet, Service). |
92
- | `kx namespace <index>` | Switch to an indexed namespace; alias: kx ns (run kx get namespaces first). |
132
+ | `kx diagnostic <index>` | Run read-only health diagnostics on an indexed Deployment, StatefulSet, DaemonSet, or Pod; alias: kx diag. |
133
+ | `kx namespace [<index>]` | List namespaces, or switch to an indexed one; alias: kx ns. |
134
+ | `kx theme [<name>]` | List available color themes or persist a choice by name or index. |
93
135
  | `kx state [<position>] [--all/-a]` | Show current state, jump to a history position, or list all entries with --all. |
94
136
  | `kx drop <position>` | Remove a history entry by position (shown in kx state --all). |
95
137
  | `kx back` | Navigate to the previous kx get result. |
@@ -128,7 +170,7 @@ kx delete 3
128
170
 
129
171
  ## State
130
172
 
131
- `kx` maintains a history of up to 10 `kx get` results in `~/.kx_state.json`. A cursor tracks your current position; index-based commands always resolve against the entry at the cursor.
173
+ `kx` maintains a history of up to 10 `kx get` results in `~/.kx/state.json`. A cursor tracks your current position; index-based commands always resolve against the entry at the cursor.
132
174
 
133
175
  ```
134
176
  $ kx get pods # saves a new entry, cursor advances
@@ -139,6 +181,29 @@ $ kx state --all # lists all history entries and the current position
139
181
 
140
182
  Use `kx state <position>` to jump directly to any history entry, and `kx drop <position>` to remove one.
141
183
 
184
+ ## Configuration
185
+
186
+ `kx` reads `~/.kx/config.toml`; environment variables override file settings.
187
+
188
+ | Key | Env var | Default | Description |
189
+ | --- | --- | --- | --- |
190
+ | `max_history` | `KX_MAX_HISTORY` | `10` | Number of `kx get` results kept in history. |
191
+ | `shells` | `KX_SHELLS` (comma-separated) | `["bash", "sh"]` | Shell candidates for `kx exec`. |
192
+ | `no_color` | `KX_NO_COLOR` | `false` | Disable styled output (same as `--no-color`). |
193
+ | `theme` | `KX_THEME` | `"github-dark"` | Color theme for all output. |
194
+
195
+ Styled output is emitted only when stdout is a terminal — piped or redirected output is plain text, so `kx get pods | grep worker` stays clean. The [`NO_COLOR`](https://no-color.org/) convention is honored as well.
196
+
197
+ ### Themes
198
+
199
+ ```bash
200
+ kx theme # list available themes (indexed) with a preview of each
201
+ kx theme nord # persist a theme to ~/.kx/config.toml
202
+ kx theme 3 # same, selecting by index from the kx theme listing
203
+ ```
204
+
205
+ Prefab themes: `github-dark` (default), `dracula`, `nord`, `gruvbox`, `solarized-dark`, `catppuccin-mocha`, `tokyo-night`, `rose-pine`, `mono`, `light` (for light terminal backgrounds), and `plain` (no styling at all).
206
+
142
207
  ## Development
143
208
 
144
209
  ```bash
@@ -27,10 +27,42 @@
27
27
 
28
28
  ## Install
29
29
 
30
+ Requires Python 3.11+ and `kubectl` on your PATH.
31
+
32
+ With [uv](https://docs.astral.sh/uv/) (recommended):
33
+
34
+ ```bash
35
+ uv tool install kx-cli
36
+ ```
37
+
38
+ With [pipx](https://pipx.pypa.io/):
39
+
40
+ ```bash
41
+ pipx install kx-cli
42
+ ```
43
+
44
+ With pip:
45
+
30
46
  ```bash
31
47
  pip install kx-cli
32
48
  ```
33
49
 
50
+ As a kubectl plugin via [krew](https://krew.sigs.k8s.io/) (no Python required; pending krew-index acceptance):
51
+
52
+ ```bash
53
+ kubectl krew install kx
54
+ alias kx="kubectl kx"
55
+ ```
56
+
57
+ Standalone binaries (linux/macOS, amd64/arm64, no Python required) are attached to each [GitHub Release](https://github.com/jzills/kx/releases).
58
+
59
+ Or try it without installing (the package is `kx-cli`, the command is `kx`):
60
+
61
+ ```bash
62
+ uvx --from kx-cli kx get pods
63
+ pipx run --spec kx-cli kx get pods
64
+ ```
65
+
34
66
  ## Usage
35
67
 
36
68
  ### List resources
@@ -39,29 +71,34 @@ pip install kx-cli
39
71
  kx get <resource> [--match|-m <substring>] [kubectl flags...]
40
72
  ```
41
73
 
42
- Fetches resources and assigns index numbers. Any extra flags (e.g. `-n <namespace>`, `-A`) are passed through to kubectl. Use `--match`/`-m` to filter results by name (substring, case-insensitive).
74
+ Fetches resources and assigns index numbers. Any extra flags (e.g. `-n <namespace>`, `-A`) are passed through to kubectl. Use `--match`/`-m` to filter results by name (substring, case-insensitive). All-namespace listings (`-A`) are display-only — rows aren't indexed, since names aren't unique across namespaces; scope to a namespace to select.
75
+
76
+ Known kinds can drop the `get`: `kx pods`, `kx deploy -n kube-system`, `kx svc --match api`. This covers the built-in kinds and their kubectl shorthands (`po`, `deploy`, `svc`, `sts`, ...); existing commands take precedence, so `kx ns 3` still switches namespaces (bare `kx ns` lists them). CRDs and other resource types still use `kx get <resource>`. An integer after a kind is an index into the current state: `kx po 3` (or `kx get po 3`) relists just that pod, erroring if index 3 isn't a pod. Multiple indexes work too: `kx po 1 3`.
43
77
 
44
78
  ```
45
79
  $ kx get pods
46
- X NAME READY STATUS RESTARTS AGE
47
- 1 api-7d9f4b8c6-xkp2q 1/1 Running 0 2d
48
- 2 worker-6c8b5f7d9-mnt4r 1/1 Running 3 5h
49
- 3 postgres-0 1/1 Running 0 12d
80
+ Pods · default · 3 items
81
+ X NAME READY STATUS RESTARTS AGE
82
+ 1 api-7d9f4b8c6-xkp2q 1/1 Running 0 2d
83
+ 2 worker-6c8b5f7d9-mnt4r 1/1 Running 3 5h
84
+ 3 postgres-0 1/1 Running 0 12d
50
85
  ```
51
86
 
52
87
  All subsequent commands reference resources by their `X` index from the last `kx get`.
53
88
 
89
+ Global flags: `--no-color` disables styled output, `-v`/`--version` prints the installed version, and `--help` on any command shows usage, examples, and aliases.
90
+
54
91
  ### Commands
55
92
 
56
93
  <!-- commands-table-start -->
57
94
  | Command | Description |
58
95
  |---|---|
59
- | `kx get <resource> [--match/-m text] [kubectl flags...]` | List resources and assign index numbers for use with other commands. |
96
+ | `kx get <resource> [--match/-m str] [kubectl flags...]` | List resources and assign index numbers for use with other commands; shorthand: kx <kind> (e.g. kx pods, kx po 3). |
60
97
  | `kx describe <indexes>... [kubectl flags...]` | Show full kubectl describe output for one or more indexed resources. |
61
98
  | `kx events <indexes>...` | Show Kubernetes events for one or more indexed resources. |
62
99
  | `kx logs <index> [kubectl flags...]` | Stream logs for an indexed resource; aggregates across pods for Deployments, StatefulSets, DaemonSets, and Services. |
63
100
  | `kx labels <indexes>... [--selector/-s]` | Show labels for one or more indexed resources; --selector formats output as a label selector. |
64
- | `kx yaml <indexes>... [--show text]` | Print the raw YAML manifest for one or more indexed resources; --show filters to specific top-level fields. |
101
+ | `kx yaml <indexes>... [--show str]` | Print the raw YAML manifest for one or more indexed resources; --show filters to specific top-level fields. |
65
102
  | `kx delete <indexes>... [--yes/-y]` | Delete one or more indexed resources (prompts for confirmation unless --yes). |
66
103
  | `kx edit <index> [kubectl flags...]` | Open an indexed resource in your editor via kubectl edit. |
67
104
  | `kx exec <index> [<cmd>...] [kubectl flags...]` | Open an interactive shell in an indexed pod (bash, falling back to sh). |
@@ -69,7 +106,9 @@ All subsequent commands reference resources by their `X` index from the last `kx
69
106
  | `kx rollout <action> <index>` | Run a rollout action (status, restart, pause, resume, history, undo) on a Deployment, StatefulSet, or DaemonSet. |
70
107
  | `kx scale <index> <replicas>` | Scale an indexed Deployment, StatefulSet, or ReplicaSet to a given replica count. |
71
108
  | `kx port-forward <index> <port> [kubectl flags...]` | Forward a local port to an indexed resource (Pod, Deployment, ReplicaSet, StatefulSet, DaemonSet, Service). |
72
- | `kx namespace <index>` | Switch to an indexed namespace; alias: kx ns (run kx get namespaces first). |
109
+ | `kx diagnostic <index>` | Run read-only health diagnostics on an indexed Deployment, StatefulSet, DaemonSet, or Pod; alias: kx diag. |
110
+ | `kx namespace [<index>]` | List namespaces, or switch to an indexed one; alias: kx ns. |
111
+ | `kx theme [<name>]` | List available color themes or persist a choice by name or index. |
73
112
  | `kx state [<position>] [--all/-a]` | Show current state, jump to a history position, or list all entries with --all. |
74
113
  | `kx drop <position>` | Remove a history entry by position (shown in kx state --all). |
75
114
  | `kx back` | Navigate to the previous kx get result. |
@@ -108,7 +147,7 @@ kx delete 3
108
147
 
109
148
  ## State
110
149
 
111
- `kx` maintains a history of up to 10 `kx get` results in `~/.kx_state.json`. A cursor tracks your current position; index-based commands always resolve against the entry at the cursor.
150
+ `kx` maintains a history of up to 10 `kx get` results in `~/.kx/state.json`. A cursor tracks your current position; index-based commands always resolve against the entry at the cursor.
112
151
 
113
152
  ```
114
153
  $ kx get pods # saves a new entry, cursor advances
@@ -119,6 +158,29 @@ $ kx state --all # lists all history entries and the current position
119
158
 
120
159
  Use `kx state <position>` to jump directly to any history entry, and `kx drop <position>` to remove one.
121
160
 
161
+ ## Configuration
162
+
163
+ `kx` reads `~/.kx/config.toml`; environment variables override file settings.
164
+
165
+ | Key | Env var | Default | Description |
166
+ | --- | --- | --- | --- |
167
+ | `max_history` | `KX_MAX_HISTORY` | `10` | Number of `kx get` results kept in history. |
168
+ | `shells` | `KX_SHELLS` (comma-separated) | `["bash", "sh"]` | Shell candidates for `kx exec`. |
169
+ | `no_color` | `KX_NO_COLOR` | `false` | Disable styled output (same as `--no-color`). |
170
+ | `theme` | `KX_THEME` | `"github-dark"` | Color theme for all output. |
171
+
172
+ Styled output is emitted only when stdout is a terminal — piped or redirected output is plain text, so `kx get pods | grep worker` stays clean. The [`NO_COLOR`](https://no-color.org/) convention is honored as well.
173
+
174
+ ### Themes
175
+
176
+ ```bash
177
+ kx theme # list available themes (indexed) with a preview of each
178
+ kx theme nord # persist a theme to ~/.kx/config.toml
179
+ kx theme 3 # same, selecting by index from the kx theme listing
180
+ ```
181
+
182
+ Prefab themes: `github-dark` (default), `dracula`, `nord`, `gruvbox`, `solarized-dark`, `catppuccin-mocha`, `tokyo-night`, `rose-pine`, `mono`, `light` (for light terminal backgrounds), and `plain` (no styling at all).
183
+
122
184
  ## Development
123
185
 
124
186
  ```bash
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kx-cli"
3
- version = "0.0.5"
3
+ version = "0.0.7"
4
4
  description = "kubectl wrapper with index-based resource selection"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -11,7 +11,7 @@ classifiers = [
11
11
  "Programming Language :: Python :: 3.13",
12
12
  ]
13
13
  dependencies = [
14
- "typer==0.26.8",
14
+ "typer==0.27.0",
15
15
  "kubernetes",
16
16
  "rich",
17
17
  ]
@@ -21,6 +21,10 @@ dev = [
21
21
  "ruff",
22
22
  "pre-commit",
23
23
  "pytest",
24
+ "pytest-cov",
25
+ ]
26
+ build = [
27
+ "pyinstaller",
24
28
  ]
25
29
 
26
30
  [project.scripts]
@@ -1,6 +1,8 @@
1
+ from contextlib import nullcontext
2
+
1
3
  from kx.kubectl import KubectlServiceProtocol
2
4
  from kx.state import StateServiceProtocol
3
- from kx.types import Confirm
5
+ from kx.types import Confirm, Status
4
6
 
5
7
 
6
8
  class DeleteCommand:
@@ -9,14 +11,19 @@ class DeleteCommand:
9
11
  state: StateServiceProtocol,
10
12
  kubectl: KubectlServiceProtocol,
11
13
  confirm: Confirm,
14
+ status: Status | None = None,
12
15
  ):
13
16
  self.state = state
14
17
  self.kubectl = kubectl
15
18
  self.confirm = confirm
19
+ self.status = status or (lambda _msg: nullcontext())
16
20
 
17
21
  def execute(self, index: int, yes: bool) -> str:
18
22
  name, namespace, kind = self.state.fields(index)
23
+ # The confirm prompt must stay outside the spinner: a prompt inside a
24
+ # Live region breaks input.
19
25
  if not yes:
20
26
  self.confirm(f"Delete {kind}/{name} in {namespace}?")
21
- self.kubectl.run(["delete", kind, name, "-n", namespace])
27
+ with self.status("deleting"):
28
+ self.kubectl.run(["delete", kind, name, "-n", namespace])
22
29
  return f"Deleted {kind}/{name}"
@@ -1,4 +1,5 @@
1
1
  from kx.kubectl import KubectlServiceProtocol
2
+ from kx.refresh import ensure_exists
2
3
  from kx.state import StateServiceProtocol
3
4
 
4
5
 
@@ -11,8 +12,11 @@ class DescribeCommand:
11
12
  self.state = state
12
13
  self.kubectl = kubectl
13
14
 
14
- def execute(self, index: int, extra_args: list[str] = []) -> None:
15
+ def execute(self, index: int, extra_args: list[str] | None = None) -> None:
16
+ extra_args = extra_args or []
15
17
  name, namespace, kind = self.state.fields(index)
16
- self.kubectl.run_interactive(
18
+ rc = self.kubectl.run_interactive(
17
19
  ["describe", kind, name, "-n", namespace, *extra_args]
18
20
  )
21
+ if rc != 0:
22
+ ensure_exists(self.kubectl, kind, name, namespace)
@@ -0,0 +1,195 @@
1
+ from kx.diagnostics import (
2
+ ContainerDiagnostic,
3
+ DiagnosticData,
4
+ DiagnosticReport,
5
+ DiagnosticsServiceProtocol,
6
+ Finding,
7
+ PodDiagnostic,
8
+ ReplicaHealth,
9
+ Severity,
10
+ )
11
+ from kx.kinds import Kind
12
+ from kx.state import StateServiceProtocol
13
+
14
+ _SUPPORTED_KINDS = {Kind.Deployment, Kind.StatefulSet, Kind.DaemonSet, Kind.Pod}
15
+ _RESTART_WARN_THRESHOLD = 5
16
+
17
+ _IMAGE_PULL_REASONS = {"ImagePullBackOff", "ErrImagePull", "InvalidImageName"}
18
+ _CONFIG_ERROR_REASONS = {"CreateContainerConfigError", "CreateContainerError"}
19
+
20
+
21
+ class DiagnosticCommand:
22
+ def __init__(
23
+ self,
24
+ state: StateServiceProtocol,
25
+ diagnostics: DiagnosticsServiceProtocol,
26
+ ):
27
+ self.state = state
28
+ self.diagnostics = diagnostics
29
+
30
+ def execute(self, index: int) -> DiagnosticReport:
31
+ name, namespace, kind = self.state.fields(index)
32
+ if kind not in _SUPPORTED_KINDS:
33
+ raise ValueError(f"diagnostic is not supported for '{kind}'.")
34
+ return build_report(self.diagnostics.gather(kind, name, namespace))
35
+
36
+
37
+ def build_report(data: DiagnosticData) -> DiagnosticReport:
38
+ findings: list[Finding] = []
39
+
40
+ if data.replicas is not None:
41
+ findings.extend(_replica_findings(data.replicas))
42
+ for pod in data.pods:
43
+ findings.extend(_pod_findings(pod))
44
+ findings.extend(_event_findings(data.warning_events))
45
+
46
+ # Prioritize: highest severity first, stable within a severity.
47
+ findings.sort(key=lambda f: f.severity, reverse=True)
48
+ verdict = max((f.severity for f in findings), default=Severity.OK)
49
+
50
+ return DiagnosticReport(
51
+ kind=data.kind,
52
+ name=data.name,
53
+ namespace=data.namespace,
54
+ verdict=verdict,
55
+ findings=findings,
56
+ pods=data.pods,
57
+ warning_events=data.warning_events,
58
+ )
59
+
60
+
61
+ def _replica_findings(replicas: ReplicaHealth) -> list[Finding]:
62
+ findings: list[Finding] = []
63
+ gen = replicas.generation
64
+ observed = replicas.observed_generation
65
+ if gen is not None and observed is not None and observed < gen:
66
+ findings.append(
67
+ Finding(
68
+ Severity.CRITICAL,
69
+ f"Rollout stalled: observed generation {observed} behind spec generation {gen}",
70
+ )
71
+ )
72
+ if replicas.ready < replicas.desired:
73
+ severity = (
74
+ Severity.CRITICAL
75
+ if replicas.ready == 0 and replicas.desired > 0
76
+ else Severity.WARNING
77
+ )
78
+ findings.append(
79
+ Finding(
80
+ severity,
81
+ f"Only {replicas.ready}/{replicas.desired} replicas ready",
82
+ )
83
+ )
84
+ if replicas.available < replicas.desired:
85
+ findings.append(
86
+ Finding(
87
+ Severity.WARNING,
88
+ f"{replicas.available}/{replicas.desired} replicas available",
89
+ )
90
+ )
91
+ if replicas.updated < replicas.desired:
92
+ findings.append(
93
+ Finding(
94
+ Severity.WARNING,
95
+ f"Rollout in progress: {replicas.updated}/{replicas.desired} replicas updated",
96
+ )
97
+ )
98
+ return findings
99
+
100
+
101
+ def _pod_findings(pod: PodDiagnostic) -> list[Finding]:
102
+ findings: list[Finding] = []
103
+
104
+ for container in pod.containers:
105
+ findings.extend(_container_findings(pod.name, container))
106
+
107
+ if pod.phase == "Pending" and not pod.scheduling.schedulable:
108
+ detail = pod.scheduling.message or pod.scheduling.reason or "unschedulable"
109
+ findings.append(
110
+ Finding(Severity.CRITICAL, f"Pod {pod.name} unschedulable: {detail}")
111
+ )
112
+ elif pod.phase == "Pending":
113
+ findings.append(Finding(Severity.WARNING, f"Pod {pod.name} pending"))
114
+ elif pod.phase == "Failed":
115
+ findings.append(Finding(Severity.CRITICAL, f"Pod {pod.name} failed"))
116
+ elif (
117
+ pod.phase == "Running"
118
+ and pod.ready_containers < pod.total_containers
119
+ and not any(c.waiting_reason for c in pod.containers)
120
+ ):
121
+ findings.append(
122
+ Finding(
123
+ Severity.WARNING,
124
+ f"Pod {pod.name}: {pod.ready_containers}/{pod.total_containers} containers ready",
125
+ )
126
+ )
127
+ return findings
128
+
129
+
130
+ def _container_findings(pod_name: str, container: ContainerDiagnostic) -> list[Finding]:
131
+ findings: list[Finding] = []
132
+ reason = container.waiting_reason
133
+
134
+ if reason == "CrashLoopBackOff":
135
+ findings.append(
136
+ Finding(
137
+ Severity.CRITICAL,
138
+ f"CrashLoopBackOff in pod {pod_name} ({container.restart_count} restarts)",
139
+ )
140
+ )
141
+ elif reason in _IMAGE_PULL_REASONS:
142
+ findings.append(
143
+ Finding(
144
+ Severity.CRITICAL,
145
+ f"Image pull failure ({reason}) in pod {pod_name}",
146
+ )
147
+ )
148
+ elif reason in _CONFIG_ERROR_REASONS:
149
+ detail = container.waiting_message or reason
150
+ findings.append(
151
+ Finding(
152
+ Severity.CRITICAL,
153
+ f"Container config error in pod {pod_name}: {detail}",
154
+ )
155
+ )
156
+ elif reason is not None:
157
+ findings.append(
158
+ Finding(
159
+ Severity.WARNING,
160
+ f"Container {container.name} in pod {pod_name} waiting: {reason}",
161
+ )
162
+ )
163
+
164
+ if "OOMKilled" in (container.terminated_reason, container.last_terminated_reason):
165
+ findings.append(Finding(Severity.CRITICAL, f"OOMKilled in pod {pod_name}"))
166
+ elif container.terminated_reason not in (None, "Completed") and container.exit_code:
167
+ findings.append(
168
+ Finding(
169
+ Severity.CRITICAL,
170
+ f"Container {container.name} in pod {pod_name} terminated: "
171
+ f"{container.terminated_reason} (exit {container.exit_code})",
172
+ )
173
+ )
174
+
175
+ if reason is None and container.restart_count >= _RESTART_WARN_THRESHOLD:
176
+ findings.append(
177
+ Finding(
178
+ Severity.WARNING,
179
+ f"Container {container.name} in pod {pod_name} "
180
+ f"restarted {container.restart_count} times",
181
+ )
182
+ )
183
+ return findings
184
+
185
+
186
+ def _event_findings(events) -> list[Finding]:
187
+ # The message is deliberately omitted: the WARNING EVENTS section renders it
188
+ # in full, so repeating it here only bloats the summary.
189
+ return [
190
+ Finding(
191
+ Severity.WARNING,
192
+ f"{event.reason} ×{event.count} on {event.kind}/{event.name}",
193
+ )
194
+ for event in events
195
+ ]
@@ -0,0 +1,18 @@
1
+ from kx.kubectl import KubectlServiceProtocol
2
+ from kx.refresh import ensure_exists
3
+ from kx.state import StateServiceProtocol
4
+
5
+
6
+ class EditCommand:
7
+ def __init__(self, state: StateServiceProtocol, kubectl: KubectlServiceProtocol):
8
+ self.state = state
9
+ self.kubectl = kubectl
10
+
11
+ def execute(self, index: int, extra_args: list[str] | None = None) -> None:
12
+ extra_args = extra_args or []
13
+ name, namespace, kind = self.state.fields(index)
14
+ rc = self.kubectl.run_interactive(
15
+ ["edit", kind, name, "-n", namespace, *extra_args]
16
+ )
17
+ if rc != 0:
18
+ ensure_exists(self.kubectl, kind, name, namespace)
@@ -1,11 +1,19 @@
1
1
  from kx.events import EventsServiceProtocol
2
+ from kx.kubectl import KubectlServiceProtocol
3
+ from kx.refresh import ensure_exists
2
4
  from kx.state import StateServiceProtocol
3
5
 
4
6
 
5
7
  class EventsCommand:
6
- def __init__(self, state: StateServiceProtocol, events: EventsServiceProtocol):
8
+ def __init__(
9
+ self,
10
+ state: StateServiceProtocol,
11
+ events: EventsServiceProtocol,
12
+ kubectl: KubectlServiceProtocol,
13
+ ):
7
14
  self.state = state
8
15
  self.events = events
16
+ self.kubectl = kubectl
9
17
 
10
18
  def execute(self, index: int) -> str:
11
19
  name, namespace, kind = self.state.fields(index)
@@ -13,6 +21,9 @@ class EventsCommand:
13
21
  filtered = self.events.filter(all_events, name, kind)
14
22
 
15
23
  if not filtered:
24
+ # Deleted resources keep their events ~1h; only an empty result
25
+ # warrants checking whether the target itself is gone.
26
+ ensure_exists(self.kubectl, kind, name, namespace)
16
27
  return "No events found"
17
28
 
18
29
  output = []
@@ -2,6 +2,7 @@ import subprocess
2
2
 
3
3
  from kx.kinds import Kind
4
4
  from kx.kubectl import KubectlServiceProtocol
5
+ from kx.refresh import ensure_exists
5
6
  from kx.state import StateServiceProtocol
6
7
 
7
8
 
@@ -17,8 +18,9 @@ class ExecCommand:
17
18
  self.shells = shells
18
19
 
19
20
  def execute(
20
- self, index: int, cmd: list[str] | None, extra_args: list[str] = []
21
+ self, index: int, cmd: list[str] | None, extra_args: list[str] | None = None
21
22
  ) -> None:
23
+ extra_args = extra_args or []
22
24
  name, namespace, kind = self.state.fields(index)
23
25
  if kind != Kind.Pod:
24
26
  raise ValueError("exec is only supported for pods.")
@@ -28,6 +30,7 @@ class ExecCommand:
28
30
  stderr=subprocess.DEVNULL,
29
31
  )
30
32
  if rc != 0:
33
+ ensure_exists(self.kubectl, str(Kind.Pod), name, namespace)
31
34
  raise ValueError(f"Command failed in container (exit {rc}).")
32
35
  else:
33
36
  for shell in self.shells:
@@ -49,6 +52,7 @@ class ExecCommand:
49
52
  ["exec", "-it", name, "-n", namespace, *extra_args, "--", shell]
50
53
  )
51
54
  return
55
+ ensure_exists(self.kubectl, str(Kind.Pod), name, namespace)
52
56
  raise ValueError(
53
57
  "No shell found in container. Provide an explicit command: kx exec <index> -- /path/to/binary"
54
58
  )