kx-cli 0.0.4__tar.gz → 0.0.6__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 (86) hide show
  1. kx_cli-0.0.6/PKG-INFO +183 -0
  2. kx_cli-0.0.6/README.md +162 -0
  3. {kx_cli-0.0.4 → kx_cli-0.0.6}/pyproject.toml +3 -2
  4. kx_cli-0.0.6/src/kx/commands/back.py +13 -0
  5. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/describe.py +2 -1
  6. kx_cli-0.0.6/src/kx/commands/diagnostic.py +195 -0
  7. kx_cli-0.0.6/src/kx/commands/drop.py +9 -0
  8. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/edit.py +2 -1
  9. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/exec.py +10 -5
  10. kx_cli-0.0.6/src/kx/commands/forward.py +13 -0
  11. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/get.py +2 -1
  12. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/logs.py +2 -1
  13. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/port_forward.py +4 -1
  14. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/rollout.py +13 -8
  15. kx_cli-0.0.6/src/kx/commands/theme.py +23 -0
  16. kx_cli-0.0.6/src/kx/config.py +82 -0
  17. kx_cli-0.0.6/src/kx/console.py +654 -0
  18. kx_cli-0.0.6/src/kx/diagnostics.py +363 -0
  19. kx_cli-0.0.6/src/kx/errors.py +27 -0
  20. kx_cli-0.0.6/src/kx/graph.py +186 -0
  21. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/index.py +8 -14
  22. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/main.py +214 -85
  23. kx_cli-0.0.6/src/kx/state.py +100 -0
  24. kx_cli-0.0.6/src/kx/themes.py +149 -0
  25. kx_cli-0.0.6/src/kx_cli.egg-info/PKG-INFO +183 -0
  26. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx_cli.egg-info/SOURCES.txt +23 -0
  27. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx_cli.egg-info/requires.txt +2 -1
  28. kx_cli-0.0.6/tests/test_back.py +36 -0
  29. kx_cli-0.0.6/tests/test_cli_help.py +48 -0
  30. kx_cli-0.0.6/tests/test_config.py +183 -0
  31. kx_cli-0.0.6/tests/test_console.py +622 -0
  32. kx_cli-0.0.6/tests/test_delete.py +34 -0
  33. kx_cli-0.0.6/tests/test_diagnostic.py +241 -0
  34. kx_cli-0.0.6/tests/test_diagnostics.py +415 -0
  35. kx_cli-0.0.6/tests/test_drop.py +31 -0
  36. kx_cli-0.0.6/tests/test_error_handling.py +94 -0
  37. kx_cli-0.0.6/tests/test_events.py +69 -0
  38. {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_exec.py +14 -2
  39. kx_cli-0.0.6/tests/test_forward.py +36 -0
  40. kx_cli-0.0.6/tests/test_graph.py +233 -0
  41. {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_index.py +3 -4
  42. kx_cli-0.0.6/tests/test_rollout.py +125 -0
  43. kx_cli-0.0.6/tests/test_state.py +372 -0
  44. kx_cli-0.0.6/tests/test_theme.py +40 -0
  45. kx_cli-0.0.6/tests/test_themes.py +36 -0
  46. kx_cli-0.0.6/tests/test_tree.py +59 -0
  47. kx_cli-0.0.4/PKG-INFO +0 -125
  48. kx_cli-0.0.4/README.md +0 -105
  49. kx_cli-0.0.4/src/kx/console.py +0 -331
  50. kx_cli-0.0.4/src/kx/graph.py +0 -231
  51. kx_cli-0.0.4/src/kx/state.py +0 -38
  52. kx_cli-0.0.4/src/kx_cli.egg-info/PKG-INFO +0 -125
  53. kx_cli-0.0.4/tests/test_console.py +0 -202
  54. kx_cli-0.0.4/tests/test_rollout.py +0 -69
  55. kx_cli-0.0.4/tests/test_state.py +0 -102
  56. {kx_cli-0.0.4 → kx_cli-0.0.6}/LICENSE +0 -0
  57. {kx_cli-0.0.4 → kx_cli-0.0.6}/setup.cfg +0 -0
  58. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/__init__.py +0 -0
  59. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/__init__.py +0 -0
  60. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/delete.py +0 -0
  61. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/events.py +0 -0
  62. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/labels.py +0 -0
  63. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/namespace.py +0 -0
  64. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/scale.py +0 -0
  65. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/state.py +0 -0
  66. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/tree.py +0 -0
  67. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/yaml.py +0 -0
  68. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/events.py +0 -0
  69. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/k8s.py +0 -0
  70. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/kinds.py +0 -0
  71. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/kubectl.py +0 -0
  72. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/types.py +0 -0
  73. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx_cli.egg-info/dependency_links.txt +0 -0
  74. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx_cli.egg-info/entry_points.txt +0 -0
  75. {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx_cli.egg-info/top_level.txt +0 -0
  76. {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_cli_get.py +0 -0
  77. {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_describe.py +0 -0
  78. {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_edit.py +0 -0
  79. {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_get.py +0 -0
  80. {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_kinds.py +0 -0
  81. {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_labels.py +0 -0
  82. {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_logs.py +0 -0
  83. {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_namespace.py +0 -0
  84. {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_port_forward.py +0 -0
  85. {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_scale.py +0 -0
  86. {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_yaml.py +0 -0
kx_cli-0.0.6/PKG-INFO ADDED
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.4
2
+ Name: kx-cli
3
+ Version: 0.0.6
4
+ Summary: kubectl wrapper with index-based resource selection
5
+ Classifier: Programming Language :: Python :: 3
6
+ Classifier: Programming Language :: Python :: 3.11
7
+ Classifier: Programming Language :: Python :: 3.12
8
+ Classifier: Programming Language :: Python :: 3.13
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: typer==0.26.8
13
+ Requires-Dist: kubernetes
14
+ Requires-Dist: rich
15
+ Provides-Extra: dev
16
+ Requires-Dist: ruff; extra == "dev"
17
+ Requires-Dist: pre-commit; extra == "dev"
18
+ Requires-Dist: pytest; extra == "dev"
19
+ Requires-Dist: pytest-cov; extra == "dev"
20
+ Dynamic: license-file
21
+
22
+ <div align="center">
23
+ <img src="https://raw.githubusercontent.com/jzills/kx/main/assets/banner.svg" alt="kx — kubectl, indexed." width="800"/>
24
+ </div>
25
+
26
+ <br>
27
+
28
+ <div align="center">
29
+
30
+ # kubectl, indexed
31
+
32
+ </div>
33
+
34
+ <div align="center">
35
+
36
+ [![PyPI version](https://img.shields.io/pypi/v/kx-cli?style=flat-square&color=3fb950&labelColor=21262d)](https://pypi.org/project/kx-cli/)
37
+ [![Python](https://img.shields.io/pypi/pyversions/kx-cli?style=flat-square&color=3fb950&labelColor=21262d)](https://pypi.org/project/kx-cli/)
38
+ [![License](https://img.shields.io/github/license/jzills/kx?style=flat-square&color=3fb950&labelColor=21262d)](LICENSE)
39
+ [![CI](https://img.shields.io/github/actions/workflow/status/jzills/kx/pr.yml?style=flat-square&color=3fb950&labelColor=21262d&label=CI)](https://github.com/jzills/kx/actions/workflows/pr.yml)
40
+
41
+ </div>
42
+
43
+ `kx` is a kubectl wrapper that adds index-based resource selection. Run `kx get <resource>` once, then reference any result by number instead of typing full resource names.
44
+
45
+ <div align="center">
46
+ <img src="https://raw.githubusercontent.com/jzills/kx/main/demo/demo.gif" alt="kx demo" width="800"/>
47
+ </div>
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ pip install kx-cli
53
+ ```
54
+
55
+ ## Usage
56
+
57
+ ### List resources
58
+
59
+ ```
60
+ kx get <resource> [--match|-m <substring>] [kubectl flags...]
61
+ ```
62
+
63
+ 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).
64
+
65
+ ```
66
+ $ kx get pods
67
+ Pods · default · 3 items
68
+ X NAME READY STATUS RESTARTS AGE
69
+ 1 api-7d9f4b8c6-xkp2q 1/1 Running 0 2d
70
+ 2 worker-6c8b5f7d9-mnt4r 1/1 Running 3 5h
71
+ 3 postgres-0 1/1 Running 0 12d
72
+ ```
73
+
74
+ All subsequent commands reference resources by their `X` index from the last `kx get`.
75
+
76
+ Global flags: `--no-color` disables styled output, `--version` prints the installed version, and `--help` on any command shows usage, examples, and aliases.
77
+
78
+ ### Commands
79
+
80
+ <!-- commands-table-start -->
81
+ | Command | Description |
82
+ |---|---|
83
+ | `kx get <resource> [--match/-m text] [kubectl flags...]` | List resources and assign index numbers for use with other commands. |
84
+ | `kx describe <indexes>... [kubectl flags...]` | Show full kubectl describe output for one or more indexed resources. |
85
+ | `kx events <indexes>...` | Show Kubernetes events for one or more indexed resources. |
86
+ | `kx logs <index> [kubectl flags...]` | Stream logs for an indexed resource; aggregates across pods for Deployments, StatefulSets, DaemonSets, and Services. |
87
+ | `kx labels <indexes>... [--selector/-s]` | Show labels for one or more indexed resources; --selector formats output as a label selector. |
88
+ | `kx yaml <indexes>... [--show text]` | Print the raw YAML manifest for one or more indexed resources; --show filters to specific top-level fields. |
89
+ | `kx delete <indexes>... [--yes/-y]` | Delete one or more indexed resources (prompts for confirmation unless --yes). |
90
+ | `kx edit <index> [kubectl flags...]` | Open an indexed resource in your editor via kubectl edit. |
91
+ | `kx exec <index> [<cmd>...] [kubectl flags...]` | Open an interactive shell in an indexed pod (bash, falling back to sh). |
92
+ | `kx tree <index> [--index/-i]` | Show the ownership graph for an indexed resource; --index assigns indexes to tree nodes. |
93
+ | `kx rollout <action> <index>` | Run a rollout action (status, restart, pause, resume, history, undo) on a Deployment, StatefulSet, or DaemonSet. |
94
+ | `kx scale <index> <replicas>` | Scale an indexed Deployment, StatefulSet, or ReplicaSet to a given replica count. |
95
+ | `kx port-forward <index> <port> [kubectl flags...]` | Forward a local port to an indexed resource (Pod, Deployment, ReplicaSet, StatefulSet, DaemonSet, Service). |
96
+ | `kx diagnostic <index>` | Run read-only health diagnostics on an indexed Deployment, StatefulSet, DaemonSet, or Pod; alias: kx diag. |
97
+ | `kx namespace <index>` | Switch to an indexed namespace; alias: kx ns (run kx get namespaces first). |
98
+ | `kx theme [<name>]` | List available color themes or persist a choice by name or index. |
99
+ | `kx state [<position>] [--all/-a]` | Show current state, jump to a history position, or list all entries with --all. |
100
+ | `kx drop <position>` | Remove a history entry by position (shown in kx state --all). |
101
+ | `kx back` | Navigate to the previous kx get result. |
102
+ | `kx forward` | Navigate to the next kx get result. |
103
+ <!-- commands-table-end -->
104
+
105
+ ### Example workflow
106
+
107
+ ```bash
108
+ # list deployments, pick index 2
109
+ kx get deployments
110
+ kx describe 2
111
+
112
+ # check events on that deployment
113
+ kx events 2
114
+
115
+ # drill into a pod
116
+ kx get pods
117
+ kx logs 1
118
+ kx exec 1 # opens bash/sh
119
+ kx exec 1 -- env # run a specific command
120
+
121
+ # forward local port 8080 to port 80 on a service
122
+ kx get services
123
+ kx port-forward 2 8080:80
124
+
125
+ # navigate history after multiple gets
126
+ kx get pods
127
+ kx get deployments
128
+ kx logs 1 # logs from pod index 1
129
+ kx state --all # review full history
130
+
131
+ # clean up
132
+ kx delete 3
133
+ ```
134
+
135
+ ## State
136
+
137
+ `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.
138
+
139
+ ```
140
+ $ kx get pods # saves a new entry, cursor advances
141
+ $ kx get deployments # saves another entry, cursor advances
142
+ $ kx logs 1 # resolves index 1 from the pods result
143
+ $ kx state --all # lists all history entries and the current position
144
+ ```
145
+
146
+ Use `kx state <position>` to jump directly to any history entry, and `kx drop <position>` to remove one.
147
+
148
+ ## Configuration
149
+
150
+ `kx` reads `~/.kx/config.toml`; environment variables override file settings.
151
+
152
+ | Key | Env var | Default | Description |
153
+ | --- | --- | --- | --- |
154
+ | `max_history` | `KX_MAX_HISTORY` | `10` | Number of `kx get` results kept in history. |
155
+ | `shells` | `KX_SHELLS` (comma-separated) | `["bash", "sh"]` | Shell candidates for `kx exec`. |
156
+ | `no_color` | `KX_NO_COLOR` | `false` | Disable styled output (same as `--no-color`). |
157
+ | `theme` | `KX_THEME` | `"github-dark"` | Color theme for all output. |
158
+
159
+ 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.
160
+
161
+ ### Themes
162
+
163
+ ```bash
164
+ kx theme # list available themes (indexed) with a preview of each
165
+ kx theme nord # persist a theme to ~/.kx/config.toml
166
+ kx theme 3 # same, selecting by index from the kx theme listing
167
+ ```
168
+
169
+ 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).
170
+
171
+ ## Development
172
+
173
+ ```bash
174
+ python -m venv .venv
175
+ source .venv/bin/activate
176
+ pip install -e ".[dev]"
177
+ ```
178
+
179
+ Run the CLI directly:
180
+
181
+ ```bash
182
+ python -m kx.main --help
183
+ ```
kx_cli-0.0.6/README.md ADDED
@@ -0,0 +1,162 @@
1
+ <div align="center">
2
+ <img src="https://raw.githubusercontent.com/jzills/kx/main/assets/banner.svg" alt="kx — kubectl, indexed." width="800"/>
3
+ </div>
4
+
5
+ <br>
6
+
7
+ <div align="center">
8
+
9
+ # kubectl, indexed
10
+
11
+ </div>
12
+
13
+ <div align="center">
14
+
15
+ [![PyPI version](https://img.shields.io/pypi/v/kx-cli?style=flat-square&color=3fb950&labelColor=21262d)](https://pypi.org/project/kx-cli/)
16
+ [![Python](https://img.shields.io/pypi/pyversions/kx-cli?style=flat-square&color=3fb950&labelColor=21262d)](https://pypi.org/project/kx-cli/)
17
+ [![License](https://img.shields.io/github/license/jzills/kx?style=flat-square&color=3fb950&labelColor=21262d)](LICENSE)
18
+ [![CI](https://img.shields.io/github/actions/workflow/status/jzills/kx/pr.yml?style=flat-square&color=3fb950&labelColor=21262d&label=CI)](https://github.com/jzills/kx/actions/workflows/pr.yml)
19
+
20
+ </div>
21
+
22
+ `kx` is a kubectl wrapper that adds index-based resource selection. Run `kx get <resource>` once, then reference any result by number instead of typing full resource names.
23
+
24
+ <div align="center">
25
+ <img src="https://raw.githubusercontent.com/jzills/kx/main/demo/demo.gif" alt="kx demo" width="800"/>
26
+ </div>
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install kx-cli
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ### List resources
37
+
38
+ ```
39
+ kx get <resource> [--match|-m <substring>] [kubectl flags...]
40
+ ```
41
+
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).
43
+
44
+ ```
45
+ $ kx get pods
46
+ Pods · default · 3 items
47
+ X NAME READY STATUS RESTARTS AGE
48
+ 1 api-7d9f4b8c6-xkp2q 1/1 Running 0 2d
49
+ 2 worker-6c8b5f7d9-mnt4r 1/1 Running 3 5h
50
+ 3 postgres-0 1/1 Running 0 12d
51
+ ```
52
+
53
+ All subsequent commands reference resources by their `X` index from the last `kx get`.
54
+
55
+ Global flags: `--no-color` disables styled output, `--version` prints the installed version, and `--help` on any command shows usage, examples, and aliases.
56
+
57
+ ### Commands
58
+
59
+ <!-- commands-table-start -->
60
+ | Command | Description |
61
+ |---|---|
62
+ | `kx get <resource> [--match/-m text] [kubectl flags...]` | List resources and assign index numbers for use with other commands. |
63
+ | `kx describe <indexes>... [kubectl flags...]` | Show full kubectl describe output for one or more indexed resources. |
64
+ | `kx events <indexes>...` | Show Kubernetes events for one or more indexed resources. |
65
+ | `kx logs <index> [kubectl flags...]` | Stream logs for an indexed resource; aggregates across pods for Deployments, StatefulSets, DaemonSets, and Services. |
66
+ | `kx labels <indexes>... [--selector/-s]` | Show labels for one or more indexed resources; --selector formats output as a label selector. |
67
+ | `kx yaml <indexes>... [--show text]` | Print the raw YAML manifest for one or more indexed resources; --show filters to specific top-level fields. |
68
+ | `kx delete <indexes>... [--yes/-y]` | Delete one or more indexed resources (prompts for confirmation unless --yes). |
69
+ | `kx edit <index> [kubectl flags...]` | Open an indexed resource in your editor via kubectl edit. |
70
+ | `kx exec <index> [<cmd>...] [kubectl flags...]` | Open an interactive shell in an indexed pod (bash, falling back to sh). |
71
+ | `kx tree <index> [--index/-i]` | Show the ownership graph for an indexed resource; --index assigns indexes to tree nodes. |
72
+ | `kx rollout <action> <index>` | Run a rollout action (status, restart, pause, resume, history, undo) on a Deployment, StatefulSet, or DaemonSet. |
73
+ | `kx scale <index> <replicas>` | Scale an indexed Deployment, StatefulSet, or ReplicaSet to a given replica count. |
74
+ | `kx port-forward <index> <port> [kubectl flags...]` | Forward a local port to an indexed resource (Pod, Deployment, ReplicaSet, StatefulSet, DaemonSet, Service). |
75
+ | `kx diagnostic <index>` | Run read-only health diagnostics on an indexed Deployment, StatefulSet, DaemonSet, or Pod; alias: kx diag. |
76
+ | `kx namespace <index>` | Switch to an indexed namespace; alias: kx ns (run kx get namespaces first). |
77
+ | `kx theme [<name>]` | List available color themes or persist a choice by name or index. |
78
+ | `kx state [<position>] [--all/-a]` | Show current state, jump to a history position, or list all entries with --all. |
79
+ | `kx drop <position>` | Remove a history entry by position (shown in kx state --all). |
80
+ | `kx back` | Navigate to the previous kx get result. |
81
+ | `kx forward` | Navigate to the next kx get result. |
82
+ <!-- commands-table-end -->
83
+
84
+ ### Example workflow
85
+
86
+ ```bash
87
+ # list deployments, pick index 2
88
+ kx get deployments
89
+ kx describe 2
90
+
91
+ # check events on that deployment
92
+ kx events 2
93
+
94
+ # drill into a pod
95
+ kx get pods
96
+ kx logs 1
97
+ kx exec 1 # opens bash/sh
98
+ kx exec 1 -- env # run a specific command
99
+
100
+ # forward local port 8080 to port 80 on a service
101
+ kx get services
102
+ kx port-forward 2 8080:80
103
+
104
+ # navigate history after multiple gets
105
+ kx get pods
106
+ kx get deployments
107
+ kx logs 1 # logs from pod index 1
108
+ kx state --all # review full history
109
+
110
+ # clean up
111
+ kx delete 3
112
+ ```
113
+
114
+ ## State
115
+
116
+ `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.
117
+
118
+ ```
119
+ $ kx get pods # saves a new entry, cursor advances
120
+ $ kx get deployments # saves another entry, cursor advances
121
+ $ kx logs 1 # resolves index 1 from the pods result
122
+ $ kx state --all # lists all history entries and the current position
123
+ ```
124
+
125
+ Use `kx state <position>` to jump directly to any history entry, and `kx drop <position>` to remove one.
126
+
127
+ ## Configuration
128
+
129
+ `kx` reads `~/.kx/config.toml`; environment variables override file settings.
130
+
131
+ | Key | Env var | Default | Description |
132
+ | --- | --- | --- | --- |
133
+ | `max_history` | `KX_MAX_HISTORY` | `10` | Number of `kx get` results kept in history. |
134
+ | `shells` | `KX_SHELLS` (comma-separated) | `["bash", "sh"]` | Shell candidates for `kx exec`. |
135
+ | `no_color` | `KX_NO_COLOR` | `false` | Disable styled output (same as `--no-color`). |
136
+ | `theme` | `KX_THEME` | `"github-dark"` | Color theme for all output. |
137
+
138
+ 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.
139
+
140
+ ### Themes
141
+
142
+ ```bash
143
+ kx theme # list available themes (indexed) with a preview of each
144
+ kx theme nord # persist a theme to ~/.kx/config.toml
145
+ kx theme 3 # same, selecting by index from the kx theme listing
146
+ ```
147
+
148
+ 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).
149
+
150
+ ## Development
151
+
152
+ ```bash
153
+ python -m venv .venv
154
+ source .venv/bin/activate
155
+ pip install -e ".[dev]"
156
+ ```
157
+
158
+ Run the CLI directly:
159
+
160
+ ```bash
161
+ python -m kx.main --help
162
+ ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kx-cli"
3
- version = "0.0.4"
3
+ version = "0.0.6"
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.7",
14
+ "typer==0.26.8",
15
15
  "kubernetes",
16
16
  "rich",
17
17
  ]
@@ -21,6 +21,7 @@ dev = [
21
21
  "ruff",
22
22
  "pre-commit",
23
23
  "pytest",
24
+ "pytest-cov",
24
25
  ]
25
26
 
26
27
  [project.scripts]
@@ -0,0 +1,13 @@
1
+ from dataclasses import asdict
2
+ import json
3
+
4
+ from kx.state import StateServiceProtocol
5
+
6
+
7
+ class BackCommand:
8
+ def __init__(self, state: StateServiceProtocol):
9
+ self.state = state
10
+
11
+ def execute(self) -> str:
12
+ state = self.state.navigate(-1)
13
+ return json.dumps(asdict(state), indent=2)
@@ -11,7 +11,8 @@ class DescribeCommand:
11
11
  self.state = state
12
12
  self.kubectl = kubectl
13
13
 
14
- def execute(self, index: int, extra_args: list[str] = []) -> None:
14
+ def execute(self, index: int, extra_args: list[str] | None = None) -> None:
15
+ extra_args = extra_args or []
15
16
  name, namespace, kind = self.state.fields(index)
16
17
  self.kubectl.run_interactive(
17
18
  ["describe", kind, name, "-n", namespace, *extra_args]
@@ -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,9 @@
1
+ from kx.state import StateHistory, StateServiceProtocol
2
+
3
+
4
+ class DropCommand:
5
+ def __init__(self, state: StateServiceProtocol):
6
+ self.state = state
7
+
8
+ def execute(self, position: int) -> StateHistory:
9
+ return self.state.drop(position)
@@ -7,6 +7,7 @@ class EditCommand:
7
7
  self.state = state
8
8
  self.kubectl = kubectl
9
9
 
10
- def execute(self, index: int, extra_args: list[str] = []) -> None:
10
+ def execute(self, index: int, extra_args: list[str] | None = None) -> None:
11
+ extra_args = extra_args or []
11
12
  name, namespace, kind = self.state.fields(index)
12
13
  self.kubectl.run_interactive(["edit", kind, name, "-n", namespace, *extra_args])
@@ -4,17 +4,22 @@ from kx.kinds import Kind
4
4
  from kx.kubectl import KubectlServiceProtocol
5
5
  from kx.state import StateServiceProtocol
6
6
 
7
- _SHELLS = ["bash", "sh"]
8
-
9
7
 
10
8
  class ExecCommand:
11
- def __init__(self, state: StateServiceProtocol, kubectl: KubectlServiceProtocol):
9
+ def __init__(
10
+ self,
11
+ state: StateServiceProtocol,
12
+ kubectl: KubectlServiceProtocol,
13
+ shells: tuple[str, ...] = ("bash", "sh"),
14
+ ):
12
15
  self.state = state
13
16
  self.kubectl = kubectl
17
+ self.shells = shells
14
18
 
15
19
  def execute(
16
- self, index: int, cmd: list[str] | None, extra_args: list[str] = []
20
+ self, index: int, cmd: list[str] | None, extra_args: list[str] | None = None
17
21
  ) -> None:
22
+ extra_args = extra_args or []
18
23
  name, namespace, kind = self.state.fields(index)
19
24
  if kind != Kind.Pod:
20
25
  raise ValueError("exec is only supported for pods.")
@@ -26,7 +31,7 @@ class ExecCommand:
26
31
  if rc != 0:
27
32
  raise ValueError(f"Command failed in container (exit {rc}).")
28
33
  else:
29
- for shell in _SHELLS:
34
+ for shell in self.shells:
30
35
  probe_rc = self.kubectl.probe(
31
36
  [
32
37
  "exec",
@@ -0,0 +1,13 @@
1
+ from dataclasses import asdict
2
+ import json
3
+
4
+ from kx.state import StateServiceProtocol
5
+
6
+
7
+ class ForwardCommand:
8
+ def __init__(self, state: StateServiceProtocol):
9
+ self.state = state
10
+
11
+ def execute(self) -> str:
12
+ state = self.state.navigate(+1)
13
+ return json.dumps(asdict(state), indent=2)
@@ -28,8 +28,9 @@ class GetCommand:
28
28
  self,
29
29
  resource: str,
30
30
  filter_term: str | None = None,
31
- extra_args: list[str] = [],
31
+ extra_args: list[str] | None = None,
32
32
  ) -> str:
33
+ extra_args = extra_args or []
33
34
  output = self.kubectl.run(["get", resource, *extra_args])
34
35
  if filter_term:
35
36
  output = self.index.filter(output, filter_term)
@@ -12,7 +12,8 @@ class LogsCommand:
12
12
  self.state = state
13
13
  self.kubectl = kubectl
14
14
 
15
- 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 []
16
17
  name, namespace, kind = self.state.fields(index)
17
18
  if kind == Kind.Pod:
18
19
  self.kubectl.run_interactive(["logs", name, "-n", namespace, *extra_args])