kx-cli 0.0.4__tar.gz → 0.0.5__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 (68) hide show
  1. kx_cli-0.0.5/PKG-INFO +154 -0
  2. kx_cli-0.0.5/README.md +134 -0
  3. {kx_cli-0.0.4 → kx_cli-0.0.5}/pyproject.toml +2 -2
  4. kx_cli-0.0.5/src/kx/commands/back.py +13 -0
  5. kx_cli-0.0.5/src/kx/commands/drop.py +9 -0
  6. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/commands/exec.py +8 -4
  7. kx_cli-0.0.5/src/kx/commands/forward.py +13 -0
  8. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/commands/rollout.py +13 -8
  9. kx_cli-0.0.5/src/kx/config.py +38 -0
  10. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/console.py +38 -1
  11. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/main.py +67 -14
  12. kx_cli-0.0.5/src/kx/state.py +100 -0
  13. kx_cli-0.0.5/src/kx_cli.egg-info/PKG-INFO +154 -0
  14. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx_cli.egg-info/SOURCES.txt +8 -0
  15. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx_cli.egg-info/requires.txt +1 -1
  16. kx_cli-0.0.5/tests/test_back.py +36 -0
  17. kx_cli-0.0.5/tests/test_config.py +87 -0
  18. kx_cli-0.0.5/tests/test_drop.py +31 -0
  19. {kx_cli-0.0.4 → kx_cli-0.0.5}/tests/test_exec.py +14 -2
  20. kx_cli-0.0.5/tests/test_forward.py +36 -0
  21. kx_cli-0.0.5/tests/test_rollout.py +125 -0
  22. kx_cli-0.0.5/tests/test_state.py +373 -0
  23. kx_cli-0.0.4/PKG-INFO +0 -125
  24. kx_cli-0.0.4/README.md +0 -105
  25. kx_cli-0.0.4/src/kx/state.py +0 -38
  26. kx_cli-0.0.4/src/kx_cli.egg-info/PKG-INFO +0 -125
  27. kx_cli-0.0.4/tests/test_rollout.py +0 -69
  28. kx_cli-0.0.4/tests/test_state.py +0 -102
  29. {kx_cli-0.0.4 → kx_cli-0.0.5}/LICENSE +0 -0
  30. {kx_cli-0.0.4 → kx_cli-0.0.5}/setup.cfg +0 -0
  31. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/__init__.py +0 -0
  32. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/commands/__init__.py +0 -0
  33. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/commands/delete.py +0 -0
  34. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/commands/describe.py +0 -0
  35. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/commands/edit.py +0 -0
  36. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/commands/events.py +0 -0
  37. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/commands/get.py +0 -0
  38. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/commands/labels.py +0 -0
  39. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/commands/logs.py +0 -0
  40. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/commands/namespace.py +0 -0
  41. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/commands/port_forward.py +0 -0
  42. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/commands/scale.py +0 -0
  43. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/commands/state.py +0 -0
  44. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/commands/tree.py +0 -0
  45. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/commands/yaml.py +0 -0
  46. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/events.py +0 -0
  47. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/graph.py +0 -0
  48. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/index.py +0 -0
  49. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/k8s.py +0 -0
  50. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/kinds.py +0 -0
  51. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/kubectl.py +0 -0
  52. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx/types.py +0 -0
  53. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx_cli.egg-info/dependency_links.txt +0 -0
  54. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx_cli.egg-info/entry_points.txt +0 -0
  55. {kx_cli-0.0.4 → kx_cli-0.0.5}/src/kx_cli.egg-info/top_level.txt +0 -0
  56. {kx_cli-0.0.4 → kx_cli-0.0.5}/tests/test_cli_get.py +0 -0
  57. {kx_cli-0.0.4 → kx_cli-0.0.5}/tests/test_console.py +0 -0
  58. {kx_cli-0.0.4 → kx_cli-0.0.5}/tests/test_describe.py +0 -0
  59. {kx_cli-0.0.4 → kx_cli-0.0.5}/tests/test_edit.py +0 -0
  60. {kx_cli-0.0.4 → kx_cli-0.0.5}/tests/test_get.py +0 -0
  61. {kx_cli-0.0.4 → kx_cli-0.0.5}/tests/test_index.py +0 -0
  62. {kx_cli-0.0.4 → kx_cli-0.0.5}/tests/test_kinds.py +0 -0
  63. {kx_cli-0.0.4 → kx_cli-0.0.5}/tests/test_labels.py +0 -0
  64. {kx_cli-0.0.4 → kx_cli-0.0.5}/tests/test_logs.py +0 -0
  65. {kx_cli-0.0.4 → kx_cli-0.0.5}/tests/test_namespace.py +0 -0
  66. {kx_cli-0.0.4 → kx_cli-0.0.5}/tests/test_port_forward.py +0 -0
  67. {kx_cli-0.0.4 → kx_cli-0.0.5}/tests/test_scale.py +0 -0
  68. {kx_cli-0.0.4 → kx_cli-0.0.5}/tests/test_yaml.py +0 -0
kx_cli-0.0.5/PKG-INFO ADDED
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.4
2
+ Name: kx-cli
3
+ Version: 0.0.5
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
+ Dynamic: license-file
20
+
21
+ <div align="center">
22
+ <img src="https://raw.githubusercontent.com/jzills/kx/main/assets/banner.svg" alt="kx — kubectl, indexed." width="800"/>
23
+ </div>
24
+
25
+ <br>
26
+
27
+ <div align="center">
28
+
29
+ # kubectl, indexed
30
+
31
+ </div>
32
+
33
+ <div align="center">
34
+
35
+ [![PyPI version](https://img.shields.io/pypi/v/kx-cli?style=flat-square&color=3fb950&labelColor=21262d)](https://pypi.org/project/kx-cli/)
36
+ [![Python](https://img.shields.io/pypi/pyversions/kx-cli?style=flat-square&color=3fb950&labelColor=21262d)](https://pypi.org/project/kx-cli/)
37
+ [![License](https://img.shields.io/github/license/jzills/kx?style=flat-square&color=3fb950&labelColor=21262d)](LICENSE)
38
+ [![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)
39
+
40
+ </div>
41
+
42
+ `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.
43
+
44
+ <div align="center">
45
+ <img src="https://raw.githubusercontent.com/jzills/kx/main/demo/demo.gif" alt="kx demo" width="800"/>
46
+ </div>
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pip install kx-cli
52
+ ```
53
+
54
+ ## Usage
55
+
56
+ ### List resources
57
+
58
+ ```
59
+ kx get <resource> [--match|-m <substring>] [kubectl flags...]
60
+ ```
61
+
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).
63
+
64
+ ```
65
+ $ 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
70
+ ```
71
+
72
+ All subsequent commands reference resources by their `X` index from the last `kx get`.
73
+
74
+ ### Commands
75
+
76
+ <!-- commands-table-start -->
77
+ | Command | Description |
78
+ |---|---|
79
+ | `kx get <resource> [--match/-m text] [kubectl flags...]` | List resources and assign index numbers for use with other commands. |
80
+ | `kx describe <indexes>... [kubectl flags...]` | Show full kubectl describe output for one or more indexed resources. |
81
+ | `kx events <indexes>...` | Show Kubernetes events for one or more indexed resources. |
82
+ | `kx logs <index> [kubectl flags...]` | Stream logs for an indexed resource; aggregates across pods for Deployments, StatefulSets, DaemonSets, and Services. |
83
+ | `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. |
85
+ | `kx delete <indexes>... [--yes/-y]` | Delete one or more indexed resources (prompts for confirmation unless --yes). |
86
+ | `kx edit <index> [kubectl flags...]` | Open an indexed resource in your editor via kubectl edit. |
87
+ | `kx exec <index> [<cmd>...] [kubectl flags...]` | Open an interactive shell in an indexed pod (bash, falling back to sh). |
88
+ | `kx tree <index> [--index/-i]` | Show the ownership graph for an indexed resource; --index assigns indexes to tree nodes. |
89
+ | `kx rollout <action> <index>` | Run a rollout action (status, restart, pause, resume, history, undo) on a Deployment, StatefulSet, or DaemonSet. |
90
+ | `kx scale <index> <replicas>` | Scale an indexed Deployment, StatefulSet, or ReplicaSet to a given replica count. |
91
+ | `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). |
93
+ | `kx state [<position>] [--all/-a]` | Show current state, jump to a history position, or list all entries with --all. |
94
+ | `kx drop <position>` | Remove a history entry by position (shown in kx state --all). |
95
+ | `kx back` | Navigate to the previous kx get result. |
96
+ | `kx forward` | Navigate to the next kx get result. |
97
+ <!-- commands-table-end -->
98
+
99
+ ### Example workflow
100
+
101
+ ```bash
102
+ # list deployments, pick index 2
103
+ kx get deployments
104
+ kx describe 2
105
+
106
+ # check events on that deployment
107
+ kx events 2
108
+
109
+ # drill into a pod
110
+ kx get pods
111
+ kx logs 1
112
+ kx exec 1 # opens bash/sh
113
+ kx exec 1 -- env # run a specific command
114
+
115
+ # forward local port 8080 to port 80 on a service
116
+ kx get services
117
+ kx port-forward 2 8080:80
118
+
119
+ # navigate history after multiple gets
120
+ kx get pods
121
+ kx get deployments
122
+ kx logs 1 # logs from pod index 1
123
+ kx state --all # review full history
124
+
125
+ # clean up
126
+ kx delete 3
127
+ ```
128
+
129
+ ## State
130
+
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.
132
+
133
+ ```
134
+ $ kx get pods # saves a new entry, cursor advances
135
+ $ kx get deployments # saves another entry, cursor advances
136
+ $ kx logs 1 # resolves index 1 from the pods result
137
+ $ kx state --all # lists all history entries and the current position
138
+ ```
139
+
140
+ Use `kx state <position>` to jump directly to any history entry, and `kx drop <position>` to remove one.
141
+
142
+ ## Development
143
+
144
+ ```bash
145
+ python -m venv .venv
146
+ source .venv/bin/activate
147
+ pip install -e ".[dev]"
148
+ ```
149
+
150
+ Run the CLI directly:
151
+
152
+ ```bash
153
+ python -m kx.main --help
154
+ ```
kx_cli-0.0.5/README.md ADDED
@@ -0,0 +1,134 @@
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
+ 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
50
+ ```
51
+
52
+ All subsequent commands reference resources by their `X` index from the last `kx get`.
53
+
54
+ ### Commands
55
+
56
+ <!-- commands-table-start -->
57
+ | Command | Description |
58
+ |---|---|
59
+ | `kx get <resource> [--match/-m text] [kubectl flags...]` | List resources and assign index numbers for use with other commands. |
60
+ | `kx describe <indexes>... [kubectl flags...]` | Show full kubectl describe output for one or more indexed resources. |
61
+ | `kx events <indexes>...` | Show Kubernetes events for one or more indexed resources. |
62
+ | `kx logs <index> [kubectl flags...]` | Stream logs for an indexed resource; aggregates across pods for Deployments, StatefulSets, DaemonSets, and Services. |
63
+ | `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. |
65
+ | `kx delete <indexes>... [--yes/-y]` | Delete one or more indexed resources (prompts for confirmation unless --yes). |
66
+ | `kx edit <index> [kubectl flags...]` | Open an indexed resource in your editor via kubectl edit. |
67
+ | `kx exec <index> [<cmd>...] [kubectl flags...]` | Open an interactive shell in an indexed pod (bash, falling back to sh). |
68
+ | `kx tree <index> [--index/-i]` | Show the ownership graph for an indexed resource; --index assigns indexes to tree nodes. |
69
+ | `kx rollout <action> <index>` | Run a rollout action (status, restart, pause, resume, history, undo) on a Deployment, StatefulSet, or DaemonSet. |
70
+ | `kx scale <index> <replicas>` | Scale an indexed Deployment, StatefulSet, or ReplicaSet to a given replica count. |
71
+ | `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). |
73
+ | `kx state [<position>] [--all/-a]` | Show current state, jump to a history position, or list all entries with --all. |
74
+ | `kx drop <position>` | Remove a history entry by position (shown in kx state --all). |
75
+ | `kx back` | Navigate to the previous kx get result. |
76
+ | `kx forward` | Navigate to the next kx get result. |
77
+ <!-- commands-table-end -->
78
+
79
+ ### Example workflow
80
+
81
+ ```bash
82
+ # list deployments, pick index 2
83
+ kx get deployments
84
+ kx describe 2
85
+
86
+ # check events on that deployment
87
+ kx events 2
88
+
89
+ # drill into a pod
90
+ kx get pods
91
+ kx logs 1
92
+ kx exec 1 # opens bash/sh
93
+ kx exec 1 -- env # run a specific command
94
+
95
+ # forward local port 8080 to port 80 on a service
96
+ kx get services
97
+ kx port-forward 2 8080:80
98
+
99
+ # navigate history after multiple gets
100
+ kx get pods
101
+ kx get deployments
102
+ kx logs 1 # logs from pod index 1
103
+ kx state --all # review full history
104
+
105
+ # clean up
106
+ kx delete 3
107
+ ```
108
+
109
+ ## State
110
+
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.
112
+
113
+ ```
114
+ $ kx get pods # saves a new entry, cursor advances
115
+ $ kx get deployments # saves another entry, cursor advances
116
+ $ kx logs 1 # resolves index 1 from the pods result
117
+ $ kx state --all # lists all history entries and the current position
118
+ ```
119
+
120
+ Use `kx state <position>` to jump directly to any history entry, and `kx drop <position>` to remove one.
121
+
122
+ ## Development
123
+
124
+ ```bash
125
+ python -m venv .venv
126
+ source .venv/bin/activate
127
+ pip install -e ".[dev]"
128
+ ```
129
+
130
+ Run the CLI directly:
131
+
132
+ ```bash
133
+ python -m kx.main --help
134
+ ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kx-cli"
3
- version = "0.0.4"
3
+ version = "0.0.5"
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
  ]
@@ -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)
@@ -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)
@@ -4,13 +4,17 @@ 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
20
  self, index: int, cmd: list[str] | None, extra_args: list[str] = []
@@ -26,7 +30,7 @@ class ExecCommand:
26
30
  if rc != 0:
27
31
  raise ValueError(f"Command failed in container (exit {rc}).")
28
32
  else:
29
- for shell in _SHELLS:
33
+ for shell in self.shells:
30
34
  probe_rc = self.kubectl.probe(
31
35
  [
32
36
  "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)
@@ -5,11 +5,16 @@ from kx.kubectl import KubectlServiceProtocol
5
5
  from kx.state import StateServiceProtocol
6
6
 
7
7
  _SUPPORTED_KINDS = {Kind.Deployment, Kind.StatefulSet, Kind.DaemonSet}
8
+ _INTERACTIVE_ACTIONS = {"status"}
8
9
 
9
10
 
10
11
  class RolloutAction(str, Enum):
11
12
  status = "status"
12
13
  restart = "restart"
14
+ pause = "pause"
15
+ resume = "resume"
16
+ history = "history"
17
+ undo = "undo"
13
18
 
14
19
 
15
20
  class RolloutCommand:
@@ -17,14 +22,14 @@ class RolloutCommand:
17
22
  self.kubectl = kubectl
18
23
  self.state = state
19
24
 
20
- def execute(self, index: int, restart: bool = False) -> str | None:
25
+ def execute(
26
+ self, index: int, action: RolloutAction = RolloutAction.status
27
+ ) -> str | None:
21
28
  name, namespace, kind = self.state.fields(index)
22
29
  if kind not in _SUPPORTED_KINDS:
23
30
  raise ValueError(f"rollout is not supported for '{kind}'.")
24
- if restart:
25
- self.kubectl.run(["rollout", "restart", f"{kind}/{name}", "-n", namespace])
26
- return f"Restarted {kind}/{name}"
27
- self.kubectl.run_interactive(
28
- ["rollout", "status", f"{kind}/{name}", "-n", namespace]
29
- )
30
- return None
31
+ args = ["rollout", action.value, f"{kind}/{name}", "-n", namespace]
32
+ if action.value in _INTERACTIVE_ACTIONS:
33
+ self.kubectl.run_interactive(args)
34
+ return None
35
+ return self.kubectl.run(args)
@@ -0,0 +1,38 @@
1
+ import os
2
+ import tomllib
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ _CONFIG_FILE = Path.home() / ".kx" / "config.toml"
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class Config:
11
+ max_history: int = 10
12
+ shells: tuple[str, ...] = ("bash", "sh")
13
+
14
+
15
+ def load_config() -> Config:
16
+ kwargs: dict = {}
17
+
18
+ if _CONFIG_FILE.exists():
19
+ try:
20
+ with open(_CONFIG_FILE, "rb") as f:
21
+ data = tomllib.load(f)
22
+ except tomllib.TOMLDecodeError as e:
23
+ raise SystemExit(f"kx: error reading {_CONFIG_FILE}: {e}")
24
+ if "max_history" in data:
25
+ kwargs["max_history"] = data["max_history"]
26
+ if "shells" in data:
27
+ kwargs["shells"] = tuple(data["shells"])
28
+
29
+ if "KX_MAX_HISTORY" in os.environ:
30
+ try:
31
+ kwargs["max_history"] = int(os.environ["KX_MAX_HISTORY"])
32
+ except ValueError:
33
+ raise SystemExit("kx: KX_MAX_HISTORY must be an integer")
34
+
35
+ if "KX_SHELLS" in os.environ:
36
+ kwargs["shells"] = tuple(os.environ["KX_SHELLS"].split(","))
37
+
38
+ return Config(**kwargs)
@@ -5,6 +5,7 @@ from rich.console import Console
5
5
  from rich.table import Table
6
6
 
7
7
  from kx.kinds import plural_display
8
+ from kx.state import StateHistory
8
9
 
9
10
  COLOR_HEADER = "#3fb950"
10
11
  COLOR_DIM = "#7d8590"
@@ -262,7 +263,7 @@ _KX_ART = [
262
263
  def print_help(commands: list[tuple[str, str]]) -> None:
263
264
  _console.print()
264
265
  for line in _KX_ART:
265
- _console.print(f"[bold {COLOR_HEADER}]{line}[/bold {COLOR_HEADER}]")
266
+ _console.print(line, style=COLOR_HEADER, markup=False, highlight=False)
266
267
  _console.print(f"[{COLOR_DIM}]kubectl, indexed.[/{COLOR_DIM}]")
267
268
  _console.print()
268
269
  _console.rule(style=COLOR_DIM)
@@ -287,6 +288,42 @@ def print_help(commands: list[tuple[str, str]]) -> None:
287
288
  )
288
289
 
289
290
 
291
+ def render_state_history(history: StateHistory) -> None:
292
+ total = len(history.states)
293
+ label = "entry" if total == 1 else "entries"
294
+ _console.print(f"[{COLOR_DIM}]History · {total} {label}[/{COLOR_DIM}]")
295
+ table = Table(
296
+ show_header=True,
297
+ header_style=f"bold {COLOR_HEADER}",
298
+ box=None,
299
+ padding=(0, 2),
300
+ )
301
+ table.add_column("X", justify="right")
302
+ table.add_column("")
303
+ table.add_column("KIND")
304
+ table.add_column("NAMESPACE")
305
+ table.add_column("ITEMS", justify="right")
306
+ for position, state in enumerate(history.states, start=1):
307
+ is_current = (position - 1) == history.cursor
308
+ marker = f"[bold {COLOR_HEADER}]→[/bold {COLOR_HEADER}]" if is_current else ""
309
+ unique_kinds = set(state.resources.values())
310
+ kind_label = (
311
+ plural_display(next(iter(unique_kinds)))
312
+ if len(unique_kinds) == 1
313
+ else "Mixed"
314
+ )
315
+ count = len(state.resources)
316
+ row_color = COLOR_BODY if is_current else COLOR_DIM
317
+ table.add_row(
318
+ f"[{row_color}]{position}[/{row_color}]",
319
+ marker,
320
+ f"[{row_color}]{kind_label}[/{row_color}]",
321
+ f"[{row_color}]{state.namespace}[/{row_color}]",
322
+ f"[{row_color}]{count}[/{row_color}]",
323
+ )
324
+ _console.print(table)
325
+
326
+
290
327
  def render_labels(labels: dict[str, str]) -> None:
291
328
  if not labels:
292
329
  _console.print(f"[{COLOR_DIM}]No labels.[/{COLOR_DIM}]")