cli-tools-kit 0.6.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.
- cli_tools_kit-0.6.0/LICENSE +21 -0
- cli_tools_kit-0.6.0/PKG-INFO +505 -0
- cli_tools_kit-0.6.0/README.md +452 -0
- cli_tools_kit-0.6.0/cli_tools_kit/__init__.py +59 -0
- cli_tools_kit-0.6.0/cli_tools_kit/__main__.py +43 -0
- cli_tools_kit-0.6.0/cli_tools_kit/advertise.py +77 -0
- cli_tools_kit-0.6.0/cli_tools_kit/cron_installer.py +159 -0
- cli_tools_kit-0.6.0/cli_tools_kit/gui_installer.py +5928 -0
- cli_tools_kit-0.6.0/cli_tools_kit/host.py +239 -0
- cli_tools_kit-0.6.0/cli_tools_kit/identity.py +229 -0
- cli_tools_kit-0.6.0/cli_tools_kit/onboarding.py +261 -0
- cli_tools_kit-0.6.0/cli_tools_kit/skills.py +97 -0
- cli_tools_kit-0.6.0/cli_tools_kit/sources.py +335 -0
- cli_tools_kit-0.6.0/cli_tools_kit/taxonomy/__init__.py +40 -0
- cli_tools_kit-0.6.0/cli_tools_kit/taxonomy/build.py +171 -0
- cli_tools_kit-0.6.0/cli_tools_kit/taxonomy/capability.py +116 -0
- cli_tools_kit-0.6.0/cli_tools_kit/taxonomy/cluster.py +261 -0
- cli_tools_kit-0.6.0/cli_tools_kit/taxonomy/corpus.py +268 -0
- cli_tools_kit-0.6.0/cli_tools_kit/taxonomy/embedder.py +170 -0
- cli_tools_kit-0.6.0/cli_tools_kit/taxonomy/groups.py +108 -0
- cli_tools_kit-0.6.0/cli_tools_kit/taxonomy/llm_groups.py +748 -0
- cli_tools_kit-0.6.0/cli_tools_kit/tool_installer.py +572 -0
- cli_tools_kit-0.6.0/cli_tools_kit/tui_installer.py +462 -0
- cli_tools_kit-0.6.0/cli_tools_kit.egg-info/PKG-INFO +505 -0
- cli_tools_kit-0.6.0/cli_tools_kit.egg-info/SOURCES.txt +39 -0
- cli_tools_kit-0.6.0/cli_tools_kit.egg-info/dependency_links.txt +1 -0
- cli_tools_kit-0.6.0/cli_tools_kit.egg-info/entry_points.txt +3 -0
- cli_tools_kit-0.6.0/cli_tools_kit.egg-info/requires.txt +10 -0
- cli_tools_kit-0.6.0/cli_tools_kit.egg-info/top_level.txt +1 -0
- cli_tools_kit-0.6.0/pyproject.toml +59 -0
- cli_tools_kit-0.6.0/setup.cfg +4 -0
- cli_tools_kit-0.6.0/tests/test_capability_groups.py +80 -0
- cli_tools_kit-0.6.0/tests/test_cron_installer.py +138 -0
- cli_tools_kit-0.6.0/tests/test_host.py +193 -0
- cli_tools_kit-0.6.0/tests/test_identity.py +372 -0
- cli_tools_kit-0.6.0/tests/test_llm_groups.py +425 -0
- cli_tools_kit-0.6.0/tests/test_skills.py +56 -0
- cli_tools_kit-0.6.0/tests/test_sources.py +350 -0
- cli_tools_kit-0.6.0/tests/test_taxonomy_cluster.py +373 -0
- cli_tools_kit-0.6.0/tests/test_tool_installer.py +279 -0
- cli_tools_kit-0.6.0/tests/test_tui_installer.py +158 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Steffen Probst
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cli-tools-kit
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Installer protocol + helpers for self-installing Python CLI/GUI tools (desktop shortcuts, bash aliases, cron entries), plus reusable tkinter and curses installer screens
|
|
5
|
+
Author: Steffen Probst
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Steffen Probst
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/Probst1nator/cli-tools-kit
|
|
29
|
+
Project-URL: Issues, https://github.com/Probst1nator/cli-tools-kit/issues
|
|
30
|
+
Keywords: installer,desktop,cron,cli,linux,kde,gnome
|
|
31
|
+
Classifier: Development Status :: 4 - Beta
|
|
32
|
+
Classifier: Environment :: Console
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
41
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
42
|
+
Classifier: Topic :: System :: Installation/Setup
|
|
43
|
+
Requires-Python: >=3.10
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
License-File: LICENSE
|
|
46
|
+
Requires-Dist: termcolor>=2.0
|
|
47
|
+
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
48
|
+
Provides-Extra: dev
|
|
49
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
50
|
+
Provides-Extra: gui
|
|
51
|
+
Requires-Dist: Pillow>=9.0; extra == "gui"
|
|
52
|
+
Dynamic: license-file
|
|
53
|
+
|
|
54
|
+
# cli-tools-kit
|
|
55
|
+
|
|
56
|
+
A small library for self-installing Python CLI/GUI tools on Linux and Windows desktops.
|
|
57
|
+
Provides:
|
|
58
|
+
|
|
59
|
+
- **`ToolInstaller`** — install/remove `.desktop` shortcuts or bash aliases
|
|
60
|
+
for a Python script, including auto-sourcing `~/.tools_aliases` from
|
|
61
|
+
`~/.bashrc`.
|
|
62
|
+
- **`CronInstaller`** — idempotent cron-line management with marker comments
|
|
63
|
+
so each tool's entries can be installed/removed without disturbing others.
|
|
64
|
+
- **`advertise()`** — a one-line helper for the `--advertise` JSON probe
|
|
65
|
+
convention that lets parent installers discover and configure your tools.
|
|
66
|
+
- **`skill_status()`** — detect whether a tool's installed Claude Code skill
|
|
67
|
+
(`~/.claude/skills/<name>/`) is `absent`, `current`, or `stale` vs. its
|
|
68
|
+
bundled version, so an installer can suggest updates (`skill_payload_hash`,
|
|
69
|
+
`installed_skill_hash`, `read_installed_skill` alongside).
|
|
70
|
+
- **`gui_installer`** — a full, reusable tkinter GUI installer *engine*: it
|
|
71
|
+
discovers every tool in a project tree that speaks `--advertise`, and offers
|
|
72
|
+
batch install/remove, per-row skill toggles, themes, orphan cleanup, and an
|
|
73
|
+
opt-in login update-check. A thin wrapper points it at its own tree via
|
|
74
|
+
`gui_installer.run(root_dir=..., entry_script=...)`; everything else
|
|
75
|
+
(discovery layout, repo-cache bootstrap, login-check policy, window/desktop
|
|
76
|
+
identities) is configurable. See [§ GUI installer engine](#gui-installer-engine).
|
|
77
|
+
|
|
78
|
+
- **`sources`** — one installer offering tools from several repos. A TOML
|
|
79
|
+
file lists them, the kit clones what is missing over HTTPS and hands the
|
|
80
|
+
engine one discovery root per repo. See
|
|
81
|
+
[§ Sources](#sources-installing-tools-from-several-repos).
|
|
82
|
+
|
|
83
|
+
See [`PROTOCOL.md`](PROTOCOL.md) for the full `--advertise` specification.
|
|
84
|
+
|
|
85
|
+
## Install
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pip install cli-tools-kit
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Or pin in `requirements.txt`:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
cli-tools-kit==0.6.0
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The git URL form still works if you need an unreleased commit:
|
|
98
|
+
`pip install git+https://github.com/Probst1nator/cli-tools-kit.git@v0.6.0`.
|
|
99
|
+
|
|
100
|
+
Requires Python ≥ 3.10. Optional runtime dep: `termcolor` (colored
|
|
101
|
+
install/remove output; falls back to plain text if absent).
|
|
102
|
+
|
|
103
|
+
## Minimal example
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
#!/usr/bin/env python3
|
|
107
|
+
import sys
|
|
108
|
+
from cli_tools_kit import ToolMetadata, ToolInstaller, advertise
|
|
109
|
+
|
|
110
|
+
# MUST come before any heavy imports!
|
|
111
|
+
if "--advertise" in sys.argv:
|
|
112
|
+
advertise(ToolMetadata(
|
|
113
|
+
name="My Tool",
|
|
114
|
+
desktop_file="my_tool.desktop",
|
|
115
|
+
icon="utilities-terminal",
|
|
116
|
+
desc="Does the thing",
|
|
117
|
+
tags=["CLI"],
|
|
118
|
+
alias="mytool",
|
|
119
|
+
))
|
|
120
|
+
|
|
121
|
+
import argparse
|
|
122
|
+
|
|
123
|
+
def main() -> None:
|
|
124
|
+
parser = argparse.ArgumentParser()
|
|
125
|
+
parser.add_argument("--install", action="store_true")
|
|
126
|
+
parser.add_argument("--remove", action="store_true")
|
|
127
|
+
args = parser.parse_args()
|
|
128
|
+
|
|
129
|
+
installer = ToolInstaller(
|
|
130
|
+
script_path=__file__,
|
|
131
|
+
metadata=ToolMetadata(
|
|
132
|
+
name="My Tool",
|
|
133
|
+
desktop_file="my_tool.desktop",
|
|
134
|
+
icon="utilities-terminal",
|
|
135
|
+
desc="Does the thing",
|
|
136
|
+
tags=["CLI"],
|
|
137
|
+
alias="mytool",
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
if args.install:
|
|
141
|
+
installer.install()
|
|
142
|
+
elif args.remove:
|
|
143
|
+
installer.remove()
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
main()
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
After `python my_tool.py --install`, the `mytool` alias is available in new
|
|
150
|
+
shells (run `source ~/.bashrc` to pick it up immediately). On Windows the same
|
|
151
|
+
call writes a `mytool` shim instead — see [§ Windows](#windows).
|
|
152
|
+
|
|
153
|
+
## Windows
|
|
154
|
+
|
|
155
|
+
The kit runs on Windows as well as Linux. Every platform decision lives in
|
|
156
|
+
`cli_tools_kit/host.py`; the differences a user sees are these.
|
|
157
|
+
|
|
158
|
+
- A CLI tool has no bash alias. `--install` writes two launcher scripts into
|
|
159
|
+
`%LOCALAPPDATA%\<slug>\bin`: `<alias>.cmd` for cmd.exe and PowerShell, and
|
|
160
|
+
an extensionless `<alias>` shell script for Git Bash, which is the shell
|
|
161
|
+
Claude Code uses. That directory is added to the user's PATH once, so open a
|
|
162
|
+
new terminal after the first install.
|
|
163
|
+
- A tool tagged `Icon` gets a Start Menu shortcut (`.lnk`) instead of a
|
|
164
|
+
`.desktop` file, and autostart copies that shortcut into the Startup folder.
|
|
165
|
+
Cron-scheduled autostart is not supported on Windows.
|
|
166
|
+
- The tkinter installer window works out of the box. The text screen
|
|
167
|
+
(`--tui`) needs curses, which Python for Windows does not ship:
|
|
168
|
+
`pip install windows-curses`.
|
|
169
|
+
|
|
170
|
+
## Cron entries
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from cli_tools_kit import CronInstaller
|
|
174
|
+
|
|
175
|
+
cron = CronInstaller("my-tool") # unique marker for this tool's entries
|
|
176
|
+
|
|
177
|
+
cron.install([
|
|
178
|
+
f"@reboot cd {SCRIPT_DIR} && python {SCRIPT} --daemon",
|
|
179
|
+
f"0 6 * * * cd {SCRIPT_DIR} && python {SCRIPT} --daily",
|
|
180
|
+
])
|
|
181
|
+
|
|
182
|
+
# Later:
|
|
183
|
+
cron.remove() # strips only lines bearing this marker
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Each managed line gets a trailing `# cli-tool-kit:<marker>` comment. The
|
|
187
|
+
marker keeps the old project spelling so cron lines installed before the
|
|
188
|
+
rename still match.
|
|
189
|
+
Re-installing the same lines is a no-op; other tools' cron entries are
|
|
190
|
+
untouched.
|
|
191
|
+
|
|
192
|
+
## Reusing the installer in your org
|
|
193
|
+
|
|
194
|
+
`cli_tools_kit.gui_installer` is a batteries-included tkinter installer that any
|
|
195
|
+
tool tree can reuse instead of forking. Point it at your tree and it discovers
|
|
196
|
+
every tool that answers `--advertise`, then installs or removes each one's
|
|
197
|
+
desktop entry, shell alias and Claude Code skill.
|
|
198
|
+
|
|
199
|
+
**Start here:**
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
cd /path/to/your/tools
|
|
203
|
+
python3 -m cli_tools_kit
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
That prints a brief you can paste into your coding agent (Claude Code or
|
|
207
|
+
similar); the agent interviews you for the handful of naming decisions and
|
|
208
|
+
writes the wrapper. `--interactive` answers the same questions on the command
|
|
209
|
+
line instead, and `--print-wrapper` just prints the skeleton. A complete
|
|
210
|
+
worked example — wrapper plus a tool — is in
|
|
211
|
+
[`examples/org-installer/`](examples/org-installer/).
|
|
212
|
+
|
|
213
|
+
### Identity: what your installer claims on a host
|
|
214
|
+
|
|
215
|
+
Several organisations' installers can share a machine, so yours needs a name of
|
|
216
|
+
its own. `InstallerIdentity` derives every per-host artifact from one slug:
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
# my-org-tools/installer.py
|
|
220
|
+
import os
|
|
221
|
+
from cli_tools_kit import InstallerIdentity
|
|
222
|
+
from cli_tools_kit.gui_installer import run
|
|
223
|
+
|
|
224
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
225
|
+
|
|
226
|
+
if __name__ == "__main__":
|
|
227
|
+
run(
|
|
228
|
+
identity=InstallerIdentity(slug="acme-tools", title="Acme Tools"),
|
|
229
|
+
root_dir=HERE,
|
|
230
|
+
entry_script=__file__,
|
|
231
|
+
)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
| Derived from `slug="acme-tools"` | Value |
|
|
235
|
+
|---|---|
|
|
236
|
+
| config + icon overrides | `~/.config/acme-tools/` |
|
|
237
|
+
| shell aliases | `~/.acme_tools_aliases` |
|
|
238
|
+
| icon cache | `~/.cache/acme-tools/` |
|
|
239
|
+
| the manager's own shortcut | `acme-tools-installer.desktop` |
|
|
240
|
+
| WM class | `acme_tools_installer` |
|
|
241
|
+
| login-check artifacts | `acme-tools-check.{desktop,log,json}` |
|
|
242
|
+
| `.desktop` marker | `Keywords=acme-tools;ai;tool;` |
|
|
243
|
+
|
|
244
|
+
That last one matters most: it is how the installer's orphan sweeper decides a
|
|
245
|
+
shortcut is *its* shortcut. With distinct markers, two organisations' installers
|
|
246
|
+
never delete each other's entries. Every derived name can be overridden with the
|
|
247
|
+
matching `InstallerIdentity` field (`config_dir`, `aliases_file`, `wm_class`, …).
|
|
248
|
+
|
|
249
|
+
**Passing no identity selects the historical first-party names**, so existing
|
|
250
|
+
installs are untouched by an upgrade. Running the engine bare — no identity, no
|
|
251
|
+
wrapper — stops and offers setup rather than claiming those names.
|
|
252
|
+
|
|
253
|
+
### `run()`
|
|
254
|
+
|
|
255
|
+
Keyword-only; every argument defaults to `None`, meaning "leave the default".
|
|
256
|
+
|
|
257
|
+
| Argument | Default | What it does |
|
|
258
|
+
|---|---|---|
|
|
259
|
+
| `identity` | `LEGACY_IDENTITY` | The names above. The one argument a third party should always pass. |
|
|
260
|
+
| `root_dir` | cwd | The tree to manage. Discovery, `.env` loading and the self-shortcut's `Path=` all anchor here. |
|
|
261
|
+
| `entry_script` | this module | The script the manager shortcut and the login-check autostart entry launch. Pass `__file__` so they re-enter your wrapper, not the bare engine. |
|
|
262
|
+
| `discoverer` | flat + `tools_*/` walk, or the wider walk once `discovery_roots` is set | `callable(root) -> [(entry_point_path, category), …]`. Pass your own for a differently shaped tree. |
|
|
263
|
+
| `prune` | `None` | Extra directory names the default wider walk never enters, on top of the built-in set. Ignored when you pass your own `discoverer`. |
|
|
264
|
+
| `discovery_roots` | `[root_dir]` | Scan these directories instead — for tools that live in a subdirectory or several. |
|
|
265
|
+
| `group_by` | `"capability"` | Which field bands the GUI rows: `"capability"` (the advertised word) or `"category"` (whatever your discoverer assigned). Anything else raises `ValueError`. |
|
|
266
|
+
| `pre_discovery` | `None` | `callable(refresh: bool)` run once before scanning, for side effects like cloning repos into a cache. Skipped on the `--check` path so a login hook never touches the network. |
|
|
267
|
+
| `check_reconcile_shortcuts` | `True` | Whether `--check` also reinstalls drifted shortcuts. Set `False` when your tools' `--install` has side effects unsafe for a login hook, making `--check` skill-only. |
|
|
268
|
+
| `skill_targets` | `[claude_target()]` | Where the text screen can register a skill — see "The text screen" below. |
|
|
269
|
+
| `tui_preselect` | `None` | Initial ticks on the text screen: `None` ticks everything on a host with nothing installed yet and otherwise mirrors the host; `True`/`False` force one or the other. |
|
|
270
|
+
| `window_title` | identity's title | GUI window title. |
|
|
271
|
+
| `self_desktop_file`, `self_desktop_name`, `self_desktop_icon` | identity's | The manager's own shortcut. |
|
|
272
|
+
| `wm_class` | identity's | `StartupWMClass` for window-manager grouping. |
|
|
273
|
+
| `notify_app` | identity's | `notify-send` application label on the `--check` path. |
|
|
274
|
+
| `autostart_check_desktop_name`, `check_log_name`, `check_state_name` | identity's | Login-check artifact filenames. |
|
|
275
|
+
|
|
276
|
+
The identity is applied first and these individual names override it, so you can
|
|
277
|
+
take the whole namespace from a slug and still change one thing.
|
|
278
|
+
|
|
279
|
+
`run()` owns its own `argparse` and consumes `sys.argv`: `--list`, `--check`,
|
|
280
|
+
`--enable-autostart-check`, `--install`, `--update-all`, `--cleanup`, `--tui`,
|
|
281
|
+
`--gui`, and a screen when given none of them. A wrapper that needs its own
|
|
282
|
+
subcommands should skip `run()` and call the primitives (`discover_tools`,
|
|
283
|
+
`install_tool`, `remove_tool`, `cli_check`) after applying an identity with
|
|
284
|
+
`_apply_identity`.
|
|
285
|
+
|
|
286
|
+
### The text screen
|
|
287
|
+
|
|
288
|
+
Without a display (`DISPLAY`/`WAYLAND_DISPLAY` unset: SSH, WSL, a server) or
|
|
289
|
+
without `python3-tk`, `run()` opens a curses screen instead of the tkinter
|
|
290
|
+
window; `--tui` and `--gui` force either. Same rows, same Apply: `Space` ticks
|
|
291
|
+
Install, `s` ticks Skill, `a`/`n` tick all or none, `Enter` applies, `q` quits.
|
|
292
|
+
On a host where none of the tools is installed yet every row starts ticked.
|
|
293
|
+
|
|
294
|
+
A skill can go to more than one place. The default target writes
|
|
295
|
+
`~/.claude/skills/<name>/` through the tool's `--install-skill`; a wrapper adds
|
|
296
|
+
others with `skill_targets`, and the screen lets the user tick which ones
|
|
297
|
+
Apply writes to (keys `1`..`9`):
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
from cli_tools_kit.tui_installer import SkillTarget, claude_target
|
|
301
|
+
|
|
302
|
+
session = SkillTarget(
|
|
303
|
+
key="fauclaude", label="fauclaude session plugin",
|
|
304
|
+
installed=lambda tool: ..., # bool
|
|
305
|
+
install=lambda tool: (True, "..."), # (ok, output)
|
|
306
|
+
uninstall=lambda tool: (True, ""),
|
|
307
|
+
)
|
|
308
|
+
run(identity=IDENTITY, root_dir=HERE, entry_script=__file__,
|
|
309
|
+
skill_targets=[claude_target(), session])
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Without any screen, `--apply NAMES` installs the named tools (aliases, or
|
|
313
|
+
`all`) and their skills, `--skill-target KEYS` says where the skills go
|
|
314
|
+
(`claude`, a wrapper's own keys, or `none`). This is what a coding agent runs
|
|
315
|
+
when it sets a machine up from a pasted prompt:
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
./installer.py --apply xrdlab,cifsearch --skill-target claude,fauclaude
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
The screen calls the engine's `install_tool` / `remove_tool` / skill functions
|
|
322
|
+
by name at run time, so a wrapper that replaced them (to run each tool in its
|
|
323
|
+
own venv, say) is honoured there too.
|
|
324
|
+
|
|
325
|
+
### Discovering your tools
|
|
326
|
+
|
|
327
|
+
With one `root_dir`, the default discoverer accepts two layouts, and a tree may
|
|
328
|
+
mix them:
|
|
329
|
+
|
|
330
|
+
- **flat** — `<root>/<tool>/main.py` (plus `requirements.txt`). Category empty,
|
|
331
|
+
so rows band by each tool's advertised `capability`.
|
|
332
|
+
- **nested** — `<root>/tools_<category>/<tool>/main.py`, where the folder
|
|
333
|
+
supplies the category label.
|
|
334
|
+
|
|
335
|
+
Directories starting with `_` or `.` are skipped.
|
|
336
|
+
|
|
337
|
+
With `discovery_roots` — several repos, each shaped as its authors liked — the
|
|
338
|
+
default is a wider walk of every root. A directory is a tool when it holds
|
|
339
|
+
`requirements.txt` next to `main.py` or `<dirname>.py` with dashes written as
|
|
340
|
+
underscores, which is how a one-tool repo names its script (`manim-kit` ships
|
|
341
|
+
`manim_kit.py`). The root itself counts, so such a repo is one tool. The walk
|
|
342
|
+
goes four levels deep at most and never enters `.venv`, `venv`, `.git`,
|
|
343
|
+
`node_modules`, `__pycache__`, `out`, `cache`, `build`, `dist`, `archive`, a
|
|
344
|
+
name starting with `vendor`, or a dot directory. Pass `prune=[...]` to add more
|
|
345
|
+
names to that list. The category is the tool's parent directory name, empty when
|
|
346
|
+
the parent is the root, and the root's own name when the tool is the root.
|
|
347
|
+
|
|
348
|
+
Anything else: pass a `discoverer`. If an expected tool does not appear, its `--advertise` is the
|
|
349
|
+
thing to fix — it must print JSON and exit *before* any heavy import, or it
|
|
350
|
+
trips the 5-second probe timeout. See [`PROTOCOL.md`](PROTOCOL.md).
|
|
351
|
+
|
|
352
|
+
### Grouping rows by meaning
|
|
353
|
+
|
|
354
|
+
`group_by="capability"` bands rows by the one word each tool advertises. Once a
|
|
355
|
+
tree outgrows that, `cli_tools_kit.taxonomy` reads what the tree already
|
|
356
|
+
documents about itself and produces a small set of named categories:
|
|
357
|
+
|
|
358
|
+
```python
|
|
359
|
+
from cli_tools_kit.taxonomy import ensure_groups
|
|
360
|
+
|
|
361
|
+
def discover(root):
|
|
362
|
+
groups = ensure_groups(root) # {tool_name: band label}
|
|
363
|
+
return [(entry, groups.get(name, "")) for entry, name in my_walk(root)]
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
It fingerprints every `CLAUDE.md`/`README.md` in the tree and rebuilds only when
|
|
367
|
+
one changed (content hashes, not mtimes — a sync checkout restamps mtimes).
|
|
368
|
+
Three tiers, tried in order so it degrades rather than failing: an LLM naming
|
|
369
|
+
and filling the categories (Gemini via `GEMINI_API_KEY`, else a local LM Studio
|
|
370
|
+
/ Ollama server), else the advertised capability words banded into a fixed six,
|
|
371
|
+
else embedding + k-means. Nothing configured means tier two, which is instant
|
|
372
|
+
and needs no network.
|
|
373
|
+
|
|
374
|
+
### The GUI extra
|
|
375
|
+
|
|
376
|
+
Icon thumbnails need Pillow:
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
pip install "cli-tools-kit[gui]==0.6.0"
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Installing the package also exposes a `cli-tool-installer` console script.
|
|
383
|
+
|
|
384
|
+
## Sources: installing tools from several repos
|
|
385
|
+
|
|
386
|
+
An organisation's tools rarely sit in one checkout. `cli_tools_kit.sources` reads
|
|
387
|
+
a list of repos from a TOML file, puts each one on disk, and hands the engine one
|
|
388
|
+
discovery root per repo. The installer that consumes it is a few lines long.
|
|
389
|
+
|
|
390
|
+
`installer.toml` is tracked and shared by everyone:
|
|
391
|
+
|
|
392
|
+
```toml
|
|
393
|
+
[[source]]
|
|
394
|
+
name = "acme/tools"
|
|
395
|
+
path = "." # relative to this file
|
|
396
|
+
|
|
397
|
+
[[source]]
|
|
398
|
+
name = "acme/lab"
|
|
399
|
+
url = "https://github.com/acme/lab-tools" # cloned into <root>/acme/lab
|
|
400
|
+
|
|
401
|
+
[[source]]
|
|
402
|
+
name = "manim-kit"
|
|
403
|
+
url = "https://github.com/AutomatedAlchemy/manim-kit"
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
`installer.local.toml` next to it is optional and belongs to one machine, so keep
|
|
407
|
+
it out of git. It sets the root and replaces a `path` for a source matched by
|
|
408
|
+
`name`:
|
|
409
|
+
|
|
410
|
+
```toml
|
|
411
|
+
root = "/home/me/checkouts"
|
|
412
|
+
|
|
413
|
+
[[source]]
|
|
414
|
+
name = "acme/lab"
|
|
415
|
+
path = "/home/me/work/lab-tools"
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
A source resolves in this order: the path from the local file, then the `path`
|
|
419
|
+
from the tracked file, then an existing `<root>/<name>`, then a clone of `url`
|
|
420
|
+
into `<root>/<name>`. The root is `--root DIR` if given, else the local file's
|
|
421
|
+
`root`, else two levels above the directory the config file sits in.
|
|
422
|
+
|
|
423
|
+
Cloning is deliberately narrow. Only `https://` URLs are cloned, `ext::` and
|
|
424
|
+
`file://` transports and any hook are switched off for the git call, the clone is
|
|
425
|
+
full rather than shallow (a tool that stamps its output with its commit needs the
|
|
426
|
+
history), and nothing is cloned into a root that does not exist or cannot be
|
|
427
|
+
written to. A clone that fails prints one line and that source is dropped, so a
|
|
428
|
+
colleague without access to a private repo still gets everybody else's tools.
|
|
429
|
+
`--refresh` brings the clones up to date with `git pull --ff-only`; a checkout
|
|
430
|
+
given by `path` is never pulled. Cloning happens in the engine's `pre_discovery`
|
|
431
|
+
hook, which `--check` skips, so the login check stays network-free.
|
|
432
|
+
|
|
433
|
+
A repo that is itself an installer tree can carry its own `installer.toml`. Its
|
|
434
|
+
`[[source]]` entries are resolved too, one nested level deep and no further, with
|
|
435
|
+
paths relative to that file and clones under the same root. A path that is
|
|
436
|
+
already resolved is not visited twice, so a file pointing back at its parent
|
|
437
|
+
cannot loop, and duplicates are dropped.
|
|
438
|
+
|
|
439
|
+
The consumer:
|
|
440
|
+
|
|
441
|
+
```python
|
|
442
|
+
#!/usr/bin/env python3
|
|
443
|
+
import os
|
|
444
|
+
from cli_tools_kit import InstallerIdentity
|
|
445
|
+
from cli_tools_kit.sources import run_installer
|
|
446
|
+
|
|
447
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
448
|
+
run_installer(os.path.join(HERE, "installer.toml"),
|
|
449
|
+
identity=InstallerIdentity(slug="acme-tools", title="Acme Tools"),
|
|
450
|
+
entry_script=__file__)
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
`run_installer` takes `--root DIR` for itself and leaves every other flag to the
|
|
454
|
+
engine, so `--list`, `--apply`, `--skill-target`, `--check`, `--refresh`, `--tui`
|
|
455
|
+
and `--gui` work as they do without sources. Every keyword besides `config_path`
|
|
456
|
+
and `argv` goes to `run()`; `discovery_roots` and `pre_discovery` are the
|
|
457
|
+
function's own to set and passing either raises `TypeError`.
|
|
458
|
+
|
|
459
|
+
Without a wrapper, the same thing from the command line:
|
|
460
|
+
|
|
461
|
+
```bash
|
|
462
|
+
python3 -m cli_tools_kit install path/to/installer.toml --list
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
That surface uses the default installer identity, so an organisation that wants
|
|
466
|
+
its own namespace on the host writes the wrapper above and runs that.
|
|
467
|
+
|
|
468
|
+
The two loaders are usable on their own:
|
|
469
|
+
|
|
470
|
+
```python
|
|
471
|
+
from cli_tools_kit.sources import load_sources, resolve_sources
|
|
472
|
+
|
|
473
|
+
sources = load_sources("installer.toml") # [Source(name, url, path), …]
|
|
474
|
+
roots = resolve_sources(sources, root="~/acme-tools", refresh=False)
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
`resolve_sources` returns one `Path` per repo it could resolve and logs a line
|
|
478
|
+
per repo it could not (`log=` takes any callable, `print` by default). Pass
|
|
479
|
+
`clone=False` to resolve from the filesystem alone and never reach the network.
|
|
480
|
+
Reading the TOML needs Python 3.11 or the `tomli` package, which is a dependency
|
|
481
|
+
on 3.10.
|
|
482
|
+
|
|
483
|
+
## Tests
|
|
484
|
+
|
|
485
|
+
```bash
|
|
486
|
+
pip install -e ".[dev]"
|
|
487
|
+
pytest
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
## Used by
|
|
491
|
+
|
|
492
|
+
Consumers, each a self-installing tool that answers `--advertise`:
|
|
493
|
+
|
|
494
|
+
- [`studon-client`](https://github.com/Probst1nator/studon-client) — `ToolInstaller` + `CronInstaller` + a Claude Code skill
|
|
495
|
+
- [`BlogGen`](https://github.com/AutomatedAlchemy/BlogGen) — install machinery falls back gracefully when the kit is absent
|
|
496
|
+
- [`lernclaude`](https://github.com/Probst1nator/lernclaude) — same pattern
|
|
497
|
+
- [`manim-kit`](https://github.com/AutomatedAlchemy/manim-kit) — reports `skill_status`
|
|
498
|
+
|
|
499
|
+
Two parent installers built on `gui_installer` are private (a `tools_*/<tool>/main.py`
|
|
500
|
+
monorepo and a flat tree bootstrapped from a `repos.json` cache); a third walks a
|
|
501
|
+
lab-tools tree and gives each tool its own venv.
|
|
502
|
+
|
|
503
|
+
## License
|
|
504
|
+
|
|
505
|
+
MIT — see [`LICENSE`](LICENSE).
|