shellscripts 0.23.1__py3-none-any.whl
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.
- shell_scripts/__init__.py +5 -0
- shell_scripts/__main__.py +5 -0
- shell_scripts/commands/__init__.py +81 -0
- shell_scripts/commands/_dc_common.py +138 -0
- shell_scripts/commands/ai_install.py +453 -0
- shell_scripts/commands/bin_links.py +78 -0
- shell_scripts/commands/claude.py +56 -0
- shell_scripts/commands/clean.py +98 -0
- shell_scripts/commands/codex.py +107 -0
- shell_scripts/commands/copilot.py +54 -0
- shell_scripts/commands/dicom2jpg.py +98 -0
- shell_scripts/commands/dicomviewer.py +83 -0
- shell_scripts/commands/diff_cmd.py +50 -0
- shell_scripts/commands/doxygen_cmd.py +159 -0
- shell_scripts/commands/edit_cmd.py +50 -0
- shell_scripts/commands/gemini.py +54 -0
- shell_scripts/commands/kiro.py +54 -0
- shell_scripts/commands/opencode.py +54 -0
- shell_scripts/commands/pdf_crop.py +520 -0
- shell_scripts/commands/pdf_merge.py +180 -0
- shell_scripts/commands/pdf_split_by_format.py +243 -0
- shell_scripts/commands/pdf_split_by_toc.py +197 -0
- shell_scripts/commands/pdf_tiler_090.py +61 -0
- shell_scripts/commands/pdf_tiler_100.py +61 -0
- shell_scripts/commands/pdf_toc_clean.py +175 -0
- shell_scripts/commands/pi.py +61 -0
- shell_scripts/commands/req_cmd.py +362 -0
- shell_scripts/commands/tests_cmd.py +87 -0
- shell_scripts/commands/ubuntu_dark_theme.py +64 -0
- shell_scripts/commands/venv_cmd.py +82 -0
- shell_scripts/commands/video2h264.py +89 -0
- shell_scripts/commands/video2h265.py +87 -0
- shell_scripts/commands/view_cmd.py +50 -0
- shell_scripts/commands/vscode_cmd.py +42 -0
- shell_scripts/commands/vsinsider_cmd.py +42 -0
- shell_scripts/config.py +328 -0
- shell_scripts/core.py +324 -0
- shell_scripts/utils.py +455 -0
- shell_scripts/version_check.py +323 -0
- shellscripts-0.23.1.dist-info/METADATA +310 -0
- shellscripts-0.23.1.dist-info/RECORD +45 -0
- shellscripts-0.23.1.dist-info/WHEEL +5 -0
- shellscripts-0.23.1.dist-info/entry_points.txt +3 -0
- shellscripts-0.23.1.dist-info/licenses/LICENSE +674 -0
- shellscripts-0.23.1.dist-info/top_level.txt +1 -0
shell_scripts/config.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""@brief Centralized runtime configuration loader and accessor surface.
|
|
3
|
+
|
|
4
|
+
@details Stores hardcoded defaults for configurable CLI behaviors and provides
|
|
5
|
+
deterministic runtime loading from `~/.config/shellScripts/config.json` with
|
|
6
|
+
recursive merge semantics. Missing files/keys preserve defaults; invalid value
|
|
7
|
+
types are ignored at accessor boundaries. Time complexity for load is O(N) over
|
|
8
|
+
JSON node count.
|
|
9
|
+
@satisfies DES-011, DES-012, REQ-004, REQ-005, REQ-024, REQ-045, REQ-046, REQ-050
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import copy
|
|
15
|
+
import json
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from shell_scripts.utils import print_warn
|
|
20
|
+
|
|
21
|
+
## @var DEFAULT_RUNTIME_CONFIG
|
|
22
|
+
# @brief Hardcoded default runtime configuration payload.
|
|
23
|
+
# @details Defines management command templates and dispatch command profiles
|
|
24
|
+
# for `diff`, `edit`, and `view`. Serialized by `write_default_runtime_config`.
|
|
25
|
+
# @satisfies DES-011, DES-012
|
|
26
|
+
DEFAULT_RUNTIME_CONFIG: dict[str, Any] = {
|
|
27
|
+
"management": {
|
|
28
|
+
"upgrade": "uv tool install shellscripts --force",
|
|
29
|
+
"upgrade_develop": (
|
|
30
|
+
"uv tool install shellscripts --force "
|
|
31
|
+
"--from git+https://github.com/Ogekuri/shellScripts.git@{tag}"
|
|
32
|
+
),
|
|
33
|
+
"uninstall": "uv tool uninstall shellscripts",
|
|
34
|
+
},
|
|
35
|
+
"dispatch": {
|
|
36
|
+
"diff": {
|
|
37
|
+
"categories": {
|
|
38
|
+
"image": ["bcompare"],
|
|
39
|
+
"pdf": ["bcompare"],
|
|
40
|
+
"text": ["bcompare"],
|
|
41
|
+
"code": ["bcompare"],
|
|
42
|
+
"html": ["bcompare"],
|
|
43
|
+
"markdown": ["bcompare"],
|
|
44
|
+
},
|
|
45
|
+
"fallback": ["bcompare"],
|
|
46
|
+
},
|
|
47
|
+
"edit": {
|
|
48
|
+
"categories": {
|
|
49
|
+
"image": ["gimp"],
|
|
50
|
+
"pdf": ["okular"],
|
|
51
|
+
"text": ["/opt/sublime_text/sublime_text", "--launch-or-new-window", "-wait"],
|
|
52
|
+
"code": ["/opt/sublime_text/sublime_text", "--launch-or-new-window", "-wait"],
|
|
53
|
+
"html": ["/opt/sublime_text/sublime_text", "--launch-or-new-window", "-wait"],
|
|
54
|
+
"markdown": ["/opt/sublime_text/sublime_text", "--launch-or-new-window", "-wait"],
|
|
55
|
+
},
|
|
56
|
+
"fallback": ["/opt/sublime_text/sublime_text", "-n", "-wait"],
|
|
57
|
+
},
|
|
58
|
+
"view": {
|
|
59
|
+
"categories": {
|
|
60
|
+
"image": ["sushi"],
|
|
61
|
+
"pdf": ["sushi"],
|
|
62
|
+
"text": ["sushi"],
|
|
63
|
+
"code": ["sushi"],
|
|
64
|
+
"html": ["sushi"],
|
|
65
|
+
"markdown": ["typora"],
|
|
66
|
+
},
|
|
67
|
+
"fallback": ["sushi"],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
"req": {
|
|
71
|
+
"providers": [
|
|
72
|
+
"claude:prompts",
|
|
73
|
+
"github:skills",
|
|
74
|
+
"codex:skills",
|
|
75
|
+
"opencode:prompts",
|
|
76
|
+
"gemini:prompts",
|
|
77
|
+
"kiro:agents",
|
|
78
|
+
"pi:prompts",
|
|
79
|
+
],
|
|
80
|
+
"static_checks": [
|
|
81
|
+
"C=Command,cppcheck,--error-exitcode=1,\"--enable=warning,style,performance,portability\",--std=c11",
|
|
82
|
+
"C=Command,clang-format,--dry-run,--Werror",
|
|
83
|
+
"C++=Command,cppcheck,--error-exitcode=1,\"--enable=warning,style,performance,portability\",--std=c++20",
|
|
84
|
+
"C++=Command,clang-format,--dry-run,--Werror",
|
|
85
|
+
"Python=Pylance",
|
|
86
|
+
"Python=Ruff",
|
|
87
|
+
"JavaScript=Command,node,--check",
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
## @var _runtime_config
|
|
93
|
+
# @brief In-memory runtime configuration snapshot.
|
|
94
|
+
# @details Initialized from defaults; updated only by `load_runtime_config`.
|
|
95
|
+
# @satisfies DES-011, REQ-045
|
|
96
|
+
_runtime_config: dict[str, Any] = copy.deepcopy(DEFAULT_RUNTIME_CONFIG)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_config_path() -> Path:
|
|
100
|
+
"""@brief Return canonical runtime config location.
|
|
101
|
+
|
|
102
|
+
@details Resolves path as `$HOME/.config/shellScripts/config.json` using
|
|
103
|
+
`Path.home()` for user directory abstraction.
|
|
104
|
+
@return {Path} Absolute config file path.
|
|
105
|
+
@satisfies DES-011, DES-012, REQ-045, REQ-046
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
return Path.home() / ".config" / "shellScripts" / "config.json"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_default_runtime_config() -> dict[str, Any]:
|
|
112
|
+
"""@brief Return deep-copied default configuration payload.
|
|
113
|
+
|
|
114
|
+
@details Produces an isolated copy to avoid external mutation of the global
|
|
115
|
+
defaults constant and to keep write/load operations deterministic.
|
|
116
|
+
@return {dict[str, Any]} Fresh deep copy of `DEFAULT_RUNTIME_CONFIG`.
|
|
117
|
+
@satisfies DES-011, DES-012
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
return copy.deepcopy(DEFAULT_RUNTIME_CONFIG)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _deep_merge_dict(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
|
|
124
|
+
"""@brief Recursively merge nested mapping values.
|
|
125
|
+
|
|
126
|
+
@details For keys where both base and override values are dictionaries,
|
|
127
|
+
recursively merges child keys; otherwise replaces base value with override.
|
|
128
|
+
Time complexity O(N) over override node count.
|
|
129
|
+
@param base {dict[str, Any]} Target mapping mutated in place.
|
|
130
|
+
@param override {dict[str, Any]} Source mapping with overriding values.
|
|
131
|
+
@return {dict[str, Any]} The mutated `base` reference.
|
|
132
|
+
@satisfies DES-011, REQ-045
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
for key, value in override.items():
|
|
136
|
+
if isinstance(base.get(key), dict) and isinstance(value, dict):
|
|
137
|
+
_deep_merge_dict(base[key], value)
|
|
138
|
+
continue
|
|
139
|
+
base[key] = value
|
|
140
|
+
return base
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _normalize_command_vector(value: Any) -> list[str] | None:
|
|
144
|
+
"""@brief Validate and normalize an executable argv vector.
|
|
145
|
+
|
|
146
|
+
@details Accepts only non-empty lists of non-empty strings and returns a
|
|
147
|
+
cloned list for defensive immutability.
|
|
148
|
+
@param value {Any} Candidate command vector.
|
|
149
|
+
@return {list[str]|None} Sanitized vector or `None` if invalid.
|
|
150
|
+
@satisfies DES-011, REQ-045
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
if not isinstance(value, list) or not value:
|
|
154
|
+
return None
|
|
155
|
+
if not all(isinstance(item, str) and item for item in value):
|
|
156
|
+
return None
|
|
157
|
+
return [item for item in value]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _normalize_string_list(value: Any) -> list[str] | None:
|
|
161
|
+
"""@brief Validate and normalize a list of non-empty strings.
|
|
162
|
+
|
|
163
|
+
@details Accepts only list payloads containing non-empty string elements and
|
|
164
|
+
returns a cloned list for defensive immutability. Empty lists are valid.
|
|
165
|
+
@param value {Any} Candidate list payload.
|
|
166
|
+
@return {list[str]|None} Sanitized list or `None` if invalid.
|
|
167
|
+
@satisfies DES-011, REQ-045, REQ-050
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
if not isinstance(value, list):
|
|
171
|
+
return None
|
|
172
|
+
if not all(isinstance(item, str) and item for item in value):
|
|
173
|
+
return None
|
|
174
|
+
return [item for item in value]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _normalize_categories(value: Any) -> dict[str, list[str]] | None:
|
|
178
|
+
"""@brief Validate category-to-command mapping payload.
|
|
179
|
+
|
|
180
|
+
@details Keeps only entries with string keys and valid command vectors.
|
|
181
|
+
Invalid entries are dropped and can trigger fallback usage upstream.
|
|
182
|
+
@param value {Any} Candidate category map payload.
|
|
183
|
+
@return {dict[str, list[str]]|None} Sanitized category map or `None`.
|
|
184
|
+
@satisfies DES-011, REQ-024, REQ-045
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
if not isinstance(value, dict):
|
|
188
|
+
return None
|
|
189
|
+
normalized: dict[str, list[str]] = {}
|
|
190
|
+
for key, command in value.items():
|
|
191
|
+
if not isinstance(key, str):
|
|
192
|
+
continue
|
|
193
|
+
command_vector = _normalize_command_vector(command)
|
|
194
|
+
if command_vector is None:
|
|
195
|
+
continue
|
|
196
|
+
normalized[key] = command_vector
|
|
197
|
+
return normalized
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def load_runtime_config(path: Path | None = None) -> dict[str, Any]:
|
|
201
|
+
"""@brief Load runtime configuration file and merge into defaults.
|
|
202
|
+
|
|
203
|
+
@details Resets in-memory state to defaults for each call, then attempts to
|
|
204
|
+
read and parse JSON payload from target path and recursively merge override
|
|
205
|
+
keys. Missing file, invalid JSON, non-object root, or read errors preserve
|
|
206
|
+
defaults and emit warnings.
|
|
207
|
+
@param path {Path|None} Optional override path; default is canonical path.
|
|
208
|
+
@return {dict[str, Any]} Active in-memory runtime configuration snapshot.
|
|
209
|
+
@exception {json.JSONDecodeError} Handled internally and downgraded to warn.
|
|
210
|
+
@exception {OSError} Handled internally and downgraded to warn.
|
|
211
|
+
@satisfies DES-011, REQ-045
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
global _runtime_config
|
|
215
|
+
|
|
216
|
+
target = path or get_config_path()
|
|
217
|
+
_runtime_config = get_default_runtime_config()
|
|
218
|
+
if not target.exists():
|
|
219
|
+
return _runtime_config
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
payload = json.loads(target.read_text(encoding="utf-8"))
|
|
223
|
+
except OSError as err:
|
|
224
|
+
print_warn(f"Cannot read runtime config '{target}': {err}. Using defaults.")
|
|
225
|
+
return _runtime_config
|
|
226
|
+
except json.JSONDecodeError as err:
|
|
227
|
+
print_warn(f"Invalid JSON in runtime config '{target}': {err}. Using defaults.")
|
|
228
|
+
return _runtime_config
|
|
229
|
+
|
|
230
|
+
if not isinstance(payload, dict):
|
|
231
|
+
print_warn(
|
|
232
|
+
f"Runtime config '{target}' must be a JSON object. Using defaults."
|
|
233
|
+
)
|
|
234
|
+
return _runtime_config
|
|
235
|
+
|
|
236
|
+
_deep_merge_dict(_runtime_config, payload)
|
|
237
|
+
return _runtime_config
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def get_management_command(name: str) -> str:
|
|
241
|
+
"""@brief Resolve management command string with safe default fallback.
|
|
242
|
+
|
|
243
|
+
@details Reads runtime key under `management.<name>`; returns default value
|
|
244
|
+
when key is absent or not a non-empty string.
|
|
245
|
+
@param name {str} Management operation key (`upgrade` or `uninstall`).
|
|
246
|
+
@return {str} Shell command string to execute.
|
|
247
|
+
@satisfies REQ-004, REQ-005, REQ-045
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
runtime_value = _runtime_config.get("management", {}).get(name)
|
|
251
|
+
if isinstance(runtime_value, str) and runtime_value.strip():
|
|
252
|
+
return runtime_value
|
|
253
|
+
default_value = DEFAULT_RUNTIME_CONFIG["management"][name]
|
|
254
|
+
return default_value
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def get_dispatch_profile(name: str) -> tuple[dict[str, list[str]], list[str]]:
|
|
258
|
+
"""@brief Resolve dispatch profile for diff/edit/view command wrappers.
|
|
259
|
+
|
|
260
|
+
@details Builds profile from `dispatch.<name>` runtime payload with typed
|
|
261
|
+
normalization and per-section fallback to hardcoded defaults for missing or
|
|
262
|
+
invalid values.
|
|
263
|
+
@param name {str} Dispatch command key (`diff`, `edit`, or `view`).
|
|
264
|
+
@return {tuple[dict[str, list[str]], list[str]]} `(categories, fallback)`.
|
|
265
|
+
@satisfies DES-007, REQ-024, REQ-045
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
default_profile = DEFAULT_RUNTIME_CONFIG["dispatch"][name]
|
|
269
|
+
runtime_profile = _runtime_config.get("dispatch", {}).get(name, {})
|
|
270
|
+
|
|
271
|
+
categories = _normalize_categories(runtime_profile.get("categories"))
|
|
272
|
+
if categories is None:
|
|
273
|
+
categories = _normalize_categories(default_profile["categories"])
|
|
274
|
+
fallback = _normalize_command_vector(runtime_profile.get("fallback"))
|
|
275
|
+
if fallback is None:
|
|
276
|
+
fallback = _normalize_command_vector(default_profile["fallback"])
|
|
277
|
+
|
|
278
|
+
if categories is None or fallback is None:
|
|
279
|
+
raise ValueError("Default dispatch profile is invalid.")
|
|
280
|
+
return categories, fallback
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def get_req_profile() -> tuple[list[str], list[str]]:
|
|
284
|
+
"""@brief Resolve `req` providers and static checks from runtime config.
|
|
285
|
+
|
|
286
|
+
@details Builds profile from `req.providers` and `req.static_checks` runtime
|
|
287
|
+
payload with typed normalization and per-section fallback to hardcoded
|
|
288
|
+
defaults for missing or invalid values.
|
|
289
|
+
@return {tuple[list[str], list[str]]} `(providers, static_checks)`.
|
|
290
|
+
@satisfies DES-011, REQ-045, REQ-050
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
default_profile = DEFAULT_RUNTIME_CONFIG["req"]
|
|
294
|
+
runtime_profile = _runtime_config.get("req", {})
|
|
295
|
+
if not isinstance(runtime_profile, dict):
|
|
296
|
+
runtime_profile = {}
|
|
297
|
+
|
|
298
|
+
providers = _normalize_string_list(runtime_profile.get("providers"))
|
|
299
|
+
if providers is None:
|
|
300
|
+
providers = _normalize_string_list(default_profile["providers"])
|
|
301
|
+
|
|
302
|
+
static_checks = _normalize_string_list(runtime_profile.get("static_checks"))
|
|
303
|
+
if static_checks is None:
|
|
304
|
+
static_checks = _normalize_string_list(default_profile["static_checks"])
|
|
305
|
+
|
|
306
|
+
if providers is None or static_checks is None:
|
|
307
|
+
raise ValueError("Default req profile is invalid.")
|
|
308
|
+
return providers, static_checks
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def write_default_runtime_config(path: Path | None = None) -> Path:
|
|
312
|
+
"""@brief Write default runtime configuration file to disk.
|
|
313
|
+
|
|
314
|
+
@details Creates parent directories when missing and writes canonical JSON
|
|
315
|
+
payload using sorted keys and indentation for deterministic content.
|
|
316
|
+
@param path {Path|None} Optional override path; default is canonical path.
|
|
317
|
+
@return {Path} Path where the file has been written.
|
|
318
|
+
@exception {OSError} Propagated when filesystem write fails.
|
|
319
|
+
@satisfies DES-012, REQ-046
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
target = path or get_config_path()
|
|
323
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
324
|
+
target.write_text(
|
|
325
|
+
f"{json.dumps(DEFAULT_RUNTIME_CONFIG, indent=2, sort_keys=True)}\n",
|
|
326
|
+
encoding="utf-8",
|
|
327
|
+
)
|
|
328
|
+
return target
|
shell_scripts/core.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""@brief Main shellscripts CLI dispatcher and management operation handler.
|
|
3
|
+
|
|
4
|
+
@details Provides global help rendering, management command execution
|
|
5
|
+
(`--version`, `--ver`, `--upgrade`, `--uninstall`, `--write-config`,
|
|
6
|
+
`--develop`/`--dev` upgrade selector), runtime
|
|
7
|
+
configuration bootstrap, and subcommand delegation to lazily imported modules.
|
|
8
|
+
@satisfies PRJ-001, PRJ-002, PRJ-003, REQ-001, REQ-002, REQ-003, REQ-004, REQ-005, REQ-045, REQ-046, REQ-048, REQ-049, REQ-050, REQ-051, REQ-052, REQ-053, REQ-054, REQ-056, REQ-064, REQ-065, REQ-066, REQ-068, REQ-069, REQ-075, REQ-076, REQ-077, REQ-078
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
import subprocess
|
|
13
|
+
|
|
14
|
+
from shell_scripts import __version__
|
|
15
|
+
from shell_scripts.config import (
|
|
16
|
+
get_management_command,
|
|
17
|
+
load_runtime_config,
|
|
18
|
+
write_default_runtime_config,
|
|
19
|
+
)
|
|
20
|
+
from shell_scripts.version_check import check_for_updates, fetch_github_latest_release, print_remote_latest_versions
|
|
21
|
+
from shell_scripts.commands import get_command
|
|
22
|
+
from shell_scripts.utils import (
|
|
23
|
+
capture_terminal_state,
|
|
24
|
+
detect_runtime_os,
|
|
25
|
+
is_linux,
|
|
26
|
+
print_error,
|
|
27
|
+
print_info,
|
|
28
|
+
reset_terminal_state,
|
|
29
|
+
require_shell_command_executables,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
PROGRAM = "shellscripts"
|
|
33
|
+
## @var HELP_COMMAND_COLUMN_WIDTH
|
|
34
|
+
# @brief Fixed command-name column width for grouped help rendering.
|
|
35
|
+
# @details Forces deterministic spacing for all command rows across grouped
|
|
36
|
+
# global help sections.
|
|
37
|
+
# @satisfies REQ-066
|
|
38
|
+
HELP_COMMAND_COLUMN_WIDTH = 16
|
|
39
|
+
|
|
40
|
+
## @var HELP_SECTION_COMMANDS
|
|
41
|
+
# @brief Ordered command groups for global help rendering.
|
|
42
|
+
# @details Defines deterministic section order and command-token order used by
|
|
43
|
+
# `print_help` when emitting grouped command help. Ordering is normative and
|
|
44
|
+
# maps directly to REQ-066 output expectations.
|
|
45
|
+
# @satisfies DES-014, REQ-066
|
|
46
|
+
HELP_SECTION_COMMANDS = (
|
|
47
|
+
(
|
|
48
|
+
"Edit/View Commands",
|
|
49
|
+
(
|
|
50
|
+
"edit",
|
|
51
|
+
"view",
|
|
52
|
+
"diff",
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
(
|
|
56
|
+
"PDF Commands",
|
|
57
|
+
(
|
|
58
|
+
"pdf-crop",
|
|
59
|
+
"pdf-merge",
|
|
60
|
+
"pdf-split-by-format",
|
|
61
|
+
"pdf-split-by-toc",
|
|
62
|
+
"pdf-tiler-090",
|
|
63
|
+
"pdf-tiler-100",
|
|
64
|
+
"pdf-toc-clean",
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
(
|
|
68
|
+
"AI Commands",
|
|
69
|
+
(
|
|
70
|
+
"ai-install",
|
|
71
|
+
"claude",
|
|
72
|
+
"codex",
|
|
73
|
+
"copilot",
|
|
74
|
+
"gemini",
|
|
75
|
+
"kiro",
|
|
76
|
+
"opencode",
|
|
77
|
+
"pi",
|
|
78
|
+
),
|
|
79
|
+
),
|
|
80
|
+
(
|
|
81
|
+
"Develop Commands",
|
|
82
|
+
(
|
|
83
|
+
"req",
|
|
84
|
+
"venv",
|
|
85
|
+
"clean",
|
|
86
|
+
"doxygen",
|
|
87
|
+
"tests",
|
|
88
|
+
"vscode",
|
|
89
|
+
"vsinsider",
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
(
|
|
93
|
+
"Image Commands",
|
|
94
|
+
(
|
|
95
|
+
"dicom2jpg",
|
|
96
|
+
"dicomviewer",
|
|
97
|
+
),
|
|
98
|
+
),
|
|
99
|
+
(
|
|
100
|
+
"Video Commands",
|
|
101
|
+
(
|
|
102
|
+
"video2h264",
|
|
103
|
+
"video2h265",
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
(
|
|
107
|
+
"OS Commands",
|
|
108
|
+
("ubuntu-dark-theme",),
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def print_help(command_name=None):
|
|
114
|
+
"""@brief Print global or command-specific help text.
|
|
115
|
+
|
|
116
|
+
@details Renders command module help for known command names; otherwise exits
|
|
117
|
+
with explicit unknown-command error. Global help includes management options
|
|
118
|
+
and grouped command sections using deterministic section and command order,
|
|
119
|
+
with descriptions sourced from each command module `DESCRIPTION` constant.
|
|
120
|
+
@param command_name {str|None} Optional command token for scoped help.
|
|
121
|
+
@return {None} Writes to stdout/stderr; may terminate process on invalid command.
|
|
122
|
+
@throws {SystemExit} Raised when unknown command name is requested.
|
|
123
|
+
@satisfies PRJ-002, DES-014, REQ-001, REQ-002, REQ-066, REQ-068, REQ-069
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
if command_name:
|
|
127
|
+
cmd = get_command(command_name)
|
|
128
|
+
if cmd:
|
|
129
|
+
cmd.print_help(__version__)
|
|
130
|
+
else:
|
|
131
|
+
print_error(f"Unknown command: {command_name}")
|
|
132
|
+
sys.exit(1)
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
print(f"Usage: {PROGRAM} [command] [options] ({__version__})")
|
|
136
|
+
print()
|
|
137
|
+
print("Management Commands:")
|
|
138
|
+
print(
|
|
139
|
+
f" --upgrade - Reinstall {PROGRAM} on Linux; print manual command elsewhere."
|
|
140
|
+
)
|
|
141
|
+
print(
|
|
142
|
+
" --dev - With --upgrade: upgrade from the latest GitHub release tag"
|
|
143
|
+
)
|
|
144
|
+
print(" instead of PyPI (alias: --develop).")
|
|
145
|
+
print(
|
|
146
|
+
f" --uninstall - Uninstall {PROGRAM} on Linux; print manual command elsewhere."
|
|
147
|
+
)
|
|
148
|
+
print(" --write-config - Write default runtime config to user config directory.")
|
|
149
|
+
print(
|
|
150
|
+
f" --ver - Print the {PROGRAM} version plus latest PyPI and GitHub versions."
|
|
151
|
+
)
|
|
152
|
+
print(
|
|
153
|
+
f" --version - Print the {PROGRAM} version plus latest PyPI and GitHub versions."
|
|
154
|
+
)
|
|
155
|
+
print(
|
|
156
|
+
" --help - Print the full help screen or the help text of a specific command."
|
|
157
|
+
)
|
|
158
|
+
print()
|
|
159
|
+
for section_name, command_names in HELP_SECTION_COMMANDS:
|
|
160
|
+
print()
|
|
161
|
+
print(section_name)
|
|
162
|
+
for command_name in command_names:
|
|
163
|
+
command_module = get_command(command_name)
|
|
164
|
+
description = getattr(command_module, "DESCRIPTION", "")
|
|
165
|
+
print(f" {command_name:<{HELP_COMMAND_COLUMN_WIDTH}} - {description}")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _build_develop_upgrade_command(latest_tag: str) -> str:
|
|
169
|
+
"""@brief Compose the develop upgrade command for one GitHub release tag.
|
|
170
|
+
|
|
171
|
+
@details Resolves the `management.upgrade_develop` runtime-config template,
|
|
172
|
+
substitutes the `{tag}` placeholder with the supplied tag, and returns the
|
|
173
|
+
shell command that installs the package from the GitHub tag archive.
|
|
174
|
+
Complexity O(n) over template length.
|
|
175
|
+
@param latest_tag {str} GitHub release tag token preserving its `v` prefix.
|
|
176
|
+
@return {str} Fully substituted shell upgrade command.
|
|
177
|
+
@satisfies REQ-076, DES-019
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
template = get_management_command("upgrade_develop")
|
|
181
|
+
return template.replace("{tag}", latest_tag)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def do_upgrade(develop: bool = False) -> int:
|
|
185
|
+
"""@brief Execute Linux-only upgrade command resolved from runtime config.
|
|
186
|
+
|
|
187
|
+
@details Reads management command string from runtime config key
|
|
188
|
+
`management.upgrade` (PyPI default) or, when `develop` is set, resolves
|
|
189
|
+
the latest GitHub release tag and composes the tag-pinned upgrade command;
|
|
190
|
+
executes it on Linux via shell invocation and prints the manual fallback
|
|
191
|
+
command on non-Linux systems.
|
|
192
|
+
@param develop {bool} Force upgrade from the latest GitHub release tag.
|
|
193
|
+
@return {int} `1` when develop tag resolution fails; subprocess return code
|
|
194
|
+
on Linux; `0` on non-Linux fallback.
|
|
195
|
+
@satisfies REQ-004, REQ-076, REQ-078, REQ-045, REQ-056
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
if develop:
|
|
199
|
+
try:
|
|
200
|
+
latest_tag = fetch_github_latest_release(__version__)
|
|
201
|
+
except Exception as error:
|
|
202
|
+
print_error(f"GitHub latest release resolution failed: {error}.")
|
|
203
|
+
return 1
|
|
204
|
+
install_cmd = _build_develop_upgrade_command(latest_tag)
|
|
205
|
+
else:
|
|
206
|
+
install_cmd = get_management_command("upgrade")
|
|
207
|
+
if is_linux():
|
|
208
|
+
require_shell_command_executables(install_cmd)
|
|
209
|
+
print(f"Upgrading {PROGRAM}...")
|
|
210
|
+
result = subprocess.run(install_cmd, shell=True)
|
|
211
|
+
return result.returncode
|
|
212
|
+
else:
|
|
213
|
+
print(f"{PROGRAM} automatic upgrade is only supported on Linux.")
|
|
214
|
+
print("Run this command manually:")
|
|
215
|
+
print(f" {install_cmd}")
|
|
216
|
+
return 0
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def do_uninstall():
|
|
220
|
+
"""@brief Execute Linux-only uninstall command resolved from runtime config.
|
|
221
|
+
|
|
222
|
+
@details Reads management command string from runtime config key
|
|
223
|
+
`management.uninstall`, executes it on Linux via shell invocation, and
|
|
224
|
+
prints manual fallback command on non-Linux systems.
|
|
225
|
+
@return {int} Subprocess return code on Linux; `0` on non-Linux fallback.
|
|
226
|
+
@satisfies REQ-005, REQ-045, REQ-056
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
uninstall_cmd = get_management_command("uninstall")
|
|
230
|
+
if is_linux():
|
|
231
|
+
require_shell_command_executables(uninstall_cmd)
|
|
232
|
+
print(f"Uninstalling {PROGRAM}...")
|
|
233
|
+
result = subprocess.run(uninstall_cmd, shell=True)
|
|
234
|
+
return result.returncode
|
|
235
|
+
else:
|
|
236
|
+
print(f"{PROGRAM} automatic uninstall is only supported on Linux.")
|
|
237
|
+
print("Run this command manually:")
|
|
238
|
+
print(f" {uninstall_cmd}")
|
|
239
|
+
return 0
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def do_write_config():
|
|
243
|
+
"""@brief Persist default runtime configuration file to disk.
|
|
244
|
+
|
|
245
|
+
@details Writes canonical config JSON to
|
|
246
|
+
`$HOME/.config/shellScripts/config.json` and logs destination path.
|
|
247
|
+
@return {int} `0` on successful write.
|
|
248
|
+
@throws {OSError} Propagated on filesystem write failure.
|
|
249
|
+
@satisfies REQ-046
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
target = write_default_runtime_config()
|
|
253
|
+
target_text = target.as_posix() if hasattr(target, "as_posix") else str(target)
|
|
254
|
+
print_info(f"Default runtime config written to: {target_text}")
|
|
255
|
+
return 0
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def main():
|
|
259
|
+
"""@brief Entrypoint for shellscripts argument dispatch.
|
|
260
|
+
|
|
261
|
+
@details Performs runtime OS detection, update check, runtime configuration
|
|
262
|
+
load, and argument dispatch through management flags (`--version` prints
|
|
263
|
+
installed plus PyPI/GitHub latest versions; `--upgrade` upgrades from PyPI
|
|
264
|
+
or, with `--develop`/`--dev`, from the latest GitHub release tag) and
|
|
265
|
+
subcommands, then restores terminal raw/cbreak and disables legacy+xterm
|
|
266
|
+
mouse-tracking modes (`?9l`, `?1000l`, `?1001l`, `?1002l`, `?1003l`,
|
|
267
|
+
`?1004l`, `?1005l`, `?1006l`, `?1007l`, `?1015l`, `?1016l`) before exit.
|
|
268
|
+
@return {int} Process-compatible return code for caller (`sys.exit`).
|
|
269
|
+
@satisfies PRJ-001, REQ-001, REQ-002, REQ-003, REQ-004, REQ-005, REQ-045, REQ-046, REQ-047, REQ-048, REQ-049, REQ-050, REQ-051, REQ-052, REQ-053, REQ-054, REQ-064, REQ-065, REQ-066, REQ-068, REQ-069, REQ-075, REQ-076, REQ-077, REQ-078
|
|
270
|
+
"""
|
|
271
|
+
saved_tty = capture_terminal_state()
|
|
272
|
+
try:
|
|
273
|
+
detect_runtime_os()
|
|
274
|
+
check_for_updates(__version__)
|
|
275
|
+
load_runtime_config()
|
|
276
|
+
|
|
277
|
+
args = sys.argv[1:]
|
|
278
|
+
|
|
279
|
+
if not args:
|
|
280
|
+
print_help()
|
|
281
|
+
return 0
|
|
282
|
+
|
|
283
|
+
first_arg = args[0]
|
|
284
|
+
|
|
285
|
+
if first_arg in ("--version", "--ver"):
|
|
286
|
+
print(__version__)
|
|
287
|
+
print_remote_latest_versions(__version__)
|
|
288
|
+
return 0
|
|
289
|
+
|
|
290
|
+
if first_arg == "--upgrade":
|
|
291
|
+
develop = any(arg in ("--develop", "--dev") for arg in args[1:])
|
|
292
|
+
return do_upgrade(develop=develop)
|
|
293
|
+
|
|
294
|
+
if first_arg in ("--develop", "--dev"):
|
|
295
|
+
print_error("--develop/--dev must be used together with --upgrade.")
|
|
296
|
+
return 1
|
|
297
|
+
|
|
298
|
+
if first_arg == "--uninstall":
|
|
299
|
+
return do_uninstall()
|
|
300
|
+
|
|
301
|
+
if first_arg == "--write-config":
|
|
302
|
+
return do_write_config()
|
|
303
|
+
|
|
304
|
+
if first_arg == "--help":
|
|
305
|
+
if len(args) > 1:
|
|
306
|
+
print_help(args[1])
|
|
307
|
+
else:
|
|
308
|
+
print_help()
|
|
309
|
+
return 0
|
|
310
|
+
|
|
311
|
+
cmd = get_command(first_arg)
|
|
312
|
+
if cmd:
|
|
313
|
+
cmd_args = args[1:]
|
|
314
|
+
if "--help" in cmd_args:
|
|
315
|
+
cmd.print_help(__version__)
|
|
316
|
+
return 0
|
|
317
|
+
return cmd.run(cmd_args) or 0
|
|
318
|
+
|
|
319
|
+
print_error(f"Unknown command: {first_arg}")
|
|
320
|
+
print()
|
|
321
|
+
print_help()
|
|
322
|
+
return 1
|
|
323
|
+
finally:
|
|
324
|
+
reset_terminal_state(saved_tty)
|