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.
Files changed (45) hide show
  1. shell_scripts/__init__.py +5 -0
  2. shell_scripts/__main__.py +5 -0
  3. shell_scripts/commands/__init__.py +81 -0
  4. shell_scripts/commands/_dc_common.py +138 -0
  5. shell_scripts/commands/ai_install.py +453 -0
  6. shell_scripts/commands/bin_links.py +78 -0
  7. shell_scripts/commands/claude.py +56 -0
  8. shell_scripts/commands/clean.py +98 -0
  9. shell_scripts/commands/codex.py +107 -0
  10. shell_scripts/commands/copilot.py +54 -0
  11. shell_scripts/commands/dicom2jpg.py +98 -0
  12. shell_scripts/commands/dicomviewer.py +83 -0
  13. shell_scripts/commands/diff_cmd.py +50 -0
  14. shell_scripts/commands/doxygen_cmd.py +159 -0
  15. shell_scripts/commands/edit_cmd.py +50 -0
  16. shell_scripts/commands/gemini.py +54 -0
  17. shell_scripts/commands/kiro.py +54 -0
  18. shell_scripts/commands/opencode.py +54 -0
  19. shell_scripts/commands/pdf_crop.py +520 -0
  20. shell_scripts/commands/pdf_merge.py +180 -0
  21. shell_scripts/commands/pdf_split_by_format.py +243 -0
  22. shell_scripts/commands/pdf_split_by_toc.py +197 -0
  23. shell_scripts/commands/pdf_tiler_090.py +61 -0
  24. shell_scripts/commands/pdf_tiler_100.py +61 -0
  25. shell_scripts/commands/pdf_toc_clean.py +175 -0
  26. shell_scripts/commands/pi.py +61 -0
  27. shell_scripts/commands/req_cmd.py +362 -0
  28. shell_scripts/commands/tests_cmd.py +87 -0
  29. shell_scripts/commands/ubuntu_dark_theme.py +64 -0
  30. shell_scripts/commands/venv_cmd.py +82 -0
  31. shell_scripts/commands/video2h264.py +89 -0
  32. shell_scripts/commands/video2h265.py +87 -0
  33. shell_scripts/commands/view_cmd.py +50 -0
  34. shell_scripts/commands/vscode_cmd.py +42 -0
  35. shell_scripts/commands/vsinsider_cmd.py +42 -0
  36. shell_scripts/config.py +328 -0
  37. shell_scripts/core.py +324 -0
  38. shell_scripts/utils.py +455 -0
  39. shell_scripts/version_check.py +323 -0
  40. shellscripts-0.23.1.dist-info/METADATA +310 -0
  41. shellscripts-0.23.1.dist-info/RECORD +45 -0
  42. shellscripts-0.23.1.dist-info/WHEEL +5 -0
  43. shellscripts-0.23.1.dist-info/entry_points.txt +3 -0
  44. shellscripts-0.23.1.dist-info/licenses/LICENSE +674 -0
  45. shellscripts-0.23.1.dist-info/top_level.txt +1 -0
@@ -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)