gcli-control 0.1.0__tar.gz → 0.2.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.
- {gcli_control-0.1.0 → gcli_control-0.2.0}/PKG-INFO +1 -1
- {gcli_control-0.1.0 → gcli_control-0.2.0}/gcli/__init__.py +1 -1
- gcli_control-0.2.0/gcli/aliases.py +512 -0
- gcli_control-0.2.0/gcli/client.py +598 -0
- gcli_control-0.2.0/gcli/clipboard.py +496 -0
- gcli_control-0.2.0/gcli/fileops.py +483 -0
- {gcli_control-0.1.0 → gcli_control-0.2.0}/gcli/host.py +94 -0
- gcli_control-0.2.0/gcli/monitoring.py +907 -0
- gcli_control-0.2.0/gcli/netutils.py +481 -0
- gcli_control-0.2.0/gcli/output.py +633 -0
- gcli_control-0.2.0/gcli/processes.py +632 -0
- gcli_control-0.2.0/gcli/security.py +245 -0
- gcli_control-0.2.0/gcli/session.py +404 -0
- {gcli_control-0.1.0 → gcli_control-0.2.0}/gcli_control.egg-info/PKG-INFO +1 -1
- {gcli_control-0.1.0 → gcli_control-0.2.0}/gcli_control.egg-info/SOURCES.txt +9 -0
- {gcli_control-0.1.0 → gcli_control-0.2.0}/pyproject.toml +1 -1
- gcli_control-0.1.0/gcli/client.py +0 -362
- {gcli_control-0.1.0 → gcli_control-0.2.0}/README.md +0 -0
- {gcli_control-0.1.0 → gcli_control-0.2.0}/gcli/__main__.py +0 -0
- {gcli_control-0.1.0 → gcli_control-0.2.0}/gcli/colors.py +0 -0
- {gcli_control-0.1.0 → gcli_control-0.2.0}/gcli/crypto.py +0 -0
- {gcli_control-0.1.0 → gcli_control-0.2.0}/gcli/npoint.py +0 -0
- {gcli_control-0.1.0 → gcli_control-0.2.0}/gcli/protocol.py +0 -0
- {gcli_control-0.1.0 → gcli_control-0.2.0}/gcli/utils.py +0 -0
- {gcli_control-0.1.0 → gcli_control-0.2.0}/gcli_control.egg-info/dependency_links.txt +0 -0
- {gcli_control-0.1.0 → gcli_control-0.2.0}/gcli_control.egg-info/entry_points.txt +0 -0
- {gcli_control-0.1.0 → gcli_control-0.2.0}/gcli_control.egg-info/requires.txt +0 -0
- {gcli_control-0.1.0 → gcli_control-0.2.0}/gcli_control.egg-info/top_level.txt +0 -0
- {gcli_control-0.1.0 → gcli_control-0.2.0}/setup.cfg +0 -0
- {gcli_control-0.1.0 → gcli_control-0.2.0}/tests/test_integration.py +0 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command aliases and scripting for the gcli remote access tool.
|
|
3
|
+
|
|
4
|
+
Provides named command aliases (create, remove, list, expand), batch
|
|
5
|
+
script execution, and persistent alias storage via JSON files.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
from gcli.aliases import dispatch, CommandAliases
|
|
10
|
+
|
|
11
|
+
# Remote command dispatch
|
|
12
|
+
result = dispatch({"type": "alias", "name": "ll", "command": "ls -la"})
|
|
13
|
+
|
|
14
|
+
# Local expansion
|
|
15
|
+
ca = CommandAliases()
|
|
16
|
+
ca.alias("ll", "ls -la")
|
|
17
|
+
expanded = ca.expand("ll -h") # -> "ls -la -h"
|
|
18
|
+
"""
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
import platform
|
|
23
|
+
import logging
|
|
24
|
+
import tempfile
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger("gcli.aliases")
|
|
29
|
+
|
|
30
|
+
MAX_EXPANSION_DEPTH = 5
|
|
31
|
+
|
|
32
|
+
_WINDOWS = platform.system() == "Windows"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _default_aliases() -> Dict[str, str]:
|
|
36
|
+
"""Return the built-in alias set appropriate for the current OS."""
|
|
37
|
+
if _WINDOWS:
|
|
38
|
+
return {
|
|
39
|
+
"ll": "dir",
|
|
40
|
+
"la": "dir /a",
|
|
41
|
+
"cls": "cls",
|
|
42
|
+
"h": "doskey /history",
|
|
43
|
+
"..": "cd ..",
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
"ll": "ls -la",
|
|
47
|
+
"la": "ls -a",
|
|
48
|
+
"cls": "clear",
|
|
49
|
+
"h": "history 20",
|
|
50
|
+
"..": "cd ..",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CommandAliases:
|
|
55
|
+
"""Manages command aliases, expansion, persistence, and batch execution."""
|
|
56
|
+
|
|
57
|
+
def __init__(self) -> None:
|
|
58
|
+
self._aliases: Dict[str, str] = _default_aliases()
|
|
59
|
+
|
|
60
|
+
# ------------------------------------------------------------------
|
|
61
|
+
# Core alias management
|
|
62
|
+
# ------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
def alias(self, name: str, command: str) -> dict:
|
|
65
|
+
"""Create or overwrite a named alias.
|
|
66
|
+
|
|
67
|
+
Returns ``{"ok": True, "name": ..., "command": ...}`` on success.
|
|
68
|
+
"""
|
|
69
|
+
if not name or not isinstance(name, str):
|
|
70
|
+
return {"ok": False, "error": "Alias name must be a non-empty string"}
|
|
71
|
+
if not command or not isinstance(command, str):
|
|
72
|
+
return {"ok": False, "error": "Alias command must be a non-empty string"}
|
|
73
|
+
self._aliases[name] = command
|
|
74
|
+
logger.debug("Alias set: %s -> %s", name, command)
|
|
75
|
+
return {"ok": True, "name": name, "command": command}
|
|
76
|
+
|
|
77
|
+
def unalias(self, name: str) -> dict:
|
|
78
|
+
"""Remove a named alias.
|
|
79
|
+
|
|
80
|
+
Returns ``{"ok": True, "removed": name}`` on success.
|
|
81
|
+
"""
|
|
82
|
+
if name not in self._aliases:
|
|
83
|
+
return {"ok": False, "error": f"Alias not found: {name!r}"}
|
|
84
|
+
del self._aliases[name]
|
|
85
|
+
logger.debug("Alias removed: %s", name)
|
|
86
|
+
return {"ok": True, "removed": name}
|
|
87
|
+
|
|
88
|
+
def list_aliases(self) -> dict:
|
|
89
|
+
"""Return all current aliases.
|
|
90
|
+
|
|
91
|
+
Returns ``{"ok": True, "aliases": {...}, "count": n}``.
|
|
92
|
+
"""
|
|
93
|
+
snapshot = dict(self._aliases)
|
|
94
|
+
return {"ok": True, "aliases": snapshot, "count": len(snapshot)}
|
|
95
|
+
|
|
96
|
+
# ------------------------------------------------------------------
|
|
97
|
+
# Expansion
|
|
98
|
+
# ------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
def expand(self, command: str) -> str:
|
|
101
|
+
"""Expand the first word of *command* if it matches an alias.
|
|
102
|
+
|
|
103
|
+
Follows alias references up to ``MAX_EXPANSION_DEPTH`` levels to
|
|
104
|
+
prevent infinite recursion. Non-alias leading words are returned
|
|
105
|
+
unchanged.
|
|
106
|
+
|
|
107
|
+
>>> ca = CommandAliases()
|
|
108
|
+
>>> ca.alias("ll", "ls -la")
|
|
109
|
+
>>> ca.expand("ll -h")
|
|
110
|
+
'ls -la -h'
|
|
111
|
+
"""
|
|
112
|
+
if not command:
|
|
113
|
+
return command
|
|
114
|
+
|
|
115
|
+
parts = command.split(None, 1)
|
|
116
|
+
if not parts:
|
|
117
|
+
return command
|
|
118
|
+
|
|
119
|
+
word = parts[0]
|
|
120
|
+
rest = (" " + parts[1]) if len(parts) > 1 else ""
|
|
121
|
+
|
|
122
|
+
visited: set = set()
|
|
123
|
+
for _ in range(MAX_EXPANSION_DEPTH):
|
|
124
|
+
if word not in self._aliases:
|
|
125
|
+
break
|
|
126
|
+
if word in visited:
|
|
127
|
+
logger.warning("Alias recursion detected for %r, stopping", word)
|
|
128
|
+
break
|
|
129
|
+
visited.add(word)
|
|
130
|
+
expansion = self._aliases[word]
|
|
131
|
+
# Re-split: the expansion may itself have arguments baked in,
|
|
132
|
+
# but we only track the leading word for the next hop.
|
|
133
|
+
exp_parts = expansion.split(None, 1)
|
|
134
|
+
word = exp_parts[0]
|
|
135
|
+
tail = (" " + exp_parts[1]) if len(exp_parts) > 1 else ""
|
|
136
|
+
# The trailing arguments from the original call always come last
|
|
137
|
+
rest = tail + rest
|
|
138
|
+
|
|
139
|
+
return word + rest
|
|
140
|
+
|
|
141
|
+
# ------------------------------------------------------------------
|
|
142
|
+
# Batch script execution
|
|
143
|
+
# ------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
def run_script(self, commands: List[str],
|
|
146
|
+
stop_on_error: bool = False) -> dict:
|
|
147
|
+
"""Execute a list of shell commands sequentially.
|
|
148
|
+
|
|
149
|
+
Each command is expanded for aliases before execution. Results are
|
|
150
|
+
returned in order. When *stop_on_error* is ``True`` the script
|
|
151
|
+
halts on the first command whose ``ok`` field is ``False``.
|
|
152
|
+
|
|
153
|
+
Returns::
|
|
154
|
+
|
|
155
|
+
{"ok": True, "results": [...], "total": n, "succeeded": m, "failed": f}
|
|
156
|
+
"""
|
|
157
|
+
if not isinstance(commands, list):
|
|
158
|
+
return {"ok": False, "error": "commands must be a list"}
|
|
159
|
+
|
|
160
|
+
from .utils import run_command # local import to avoid circular deps at module level
|
|
161
|
+
|
|
162
|
+
results: List[dict] = []
|
|
163
|
+
succeeded = 0
|
|
164
|
+
failed = 0
|
|
165
|
+
|
|
166
|
+
for raw_cmd in commands:
|
|
167
|
+
if not isinstance(raw_cmd, str) or not raw_cmd.strip():
|
|
168
|
+
entry = {"cmd": str(raw_cmd), "ok": False, "output": "", "error": "empty command"}
|
|
169
|
+
results.append(entry)
|
|
170
|
+
failed += 1
|
|
171
|
+
if stop_on_error:
|
|
172
|
+
break
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
expanded = self.expand(raw_cmd.strip())
|
|
176
|
+
rc = run_command(expanded)
|
|
177
|
+
ok = rc.get("ok", False)
|
|
178
|
+
output = rc.get("stdout", "")
|
|
179
|
+
stderr = rc.get("stderr", "")
|
|
180
|
+
if stderr:
|
|
181
|
+
output = (output + "\n" + stderr).strip() if output else stderr
|
|
182
|
+
|
|
183
|
+
entry = {"cmd": raw_cmd, "ok": ok, "output": output}
|
|
184
|
+
results.append(entry)
|
|
185
|
+
|
|
186
|
+
if ok:
|
|
187
|
+
succeeded += 1
|
|
188
|
+
else:
|
|
189
|
+
failed += 1
|
|
190
|
+
if stop_on_error:
|
|
191
|
+
break
|
|
192
|
+
|
|
193
|
+
total = succeeded + failed
|
|
194
|
+
return {
|
|
195
|
+
"ok": True,
|
|
196
|
+
"results": results,
|
|
197
|
+
"total": total,
|
|
198
|
+
"succeeded": succeeded,
|
|
199
|
+
"failed": failed,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# ------------------------------------------------------------------
|
|
203
|
+
# Persistence
|
|
204
|
+
# ------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
def save_aliases(self, path: str) -> dict:
|
|
207
|
+
"""Persist the current alias dictionary to a JSON file.
|
|
208
|
+
|
|
209
|
+
Returns ``{"ok": True, "path": ..., "count": n}``.
|
|
210
|
+
"""
|
|
211
|
+
try:
|
|
212
|
+
p = Path(path)
|
|
213
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
214
|
+
p.write_text(json.dumps(self._aliases, indent=2, sort_keys=True),
|
|
215
|
+
encoding="utf-8")
|
|
216
|
+
logger.debug("Saved %d aliases to %s", len(self._aliases), path)
|
|
217
|
+
return {"ok": True, "path": str(p.resolve()), "count": len(self._aliases)}
|
|
218
|
+
except Exception as exc:
|
|
219
|
+
return {"ok": False, "error": str(exc)}
|
|
220
|
+
|
|
221
|
+
def load_aliases(self, path: str) -> dict:
|
|
222
|
+
"""Load aliases from a JSON file and merge with current set.
|
|
223
|
+
|
|
224
|
+
Existing aliases are overwritten by any conflicting keys in the file.
|
|
225
|
+
|
|
226
|
+
Returns ``{"ok": True, "loaded": n, "total": m}``.
|
|
227
|
+
"""
|
|
228
|
+
try:
|
|
229
|
+
p = Path(path)
|
|
230
|
+
if not p.exists():
|
|
231
|
+
return {"ok": False, "error": f"File not found: {path}"}
|
|
232
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
233
|
+
if not isinstance(data, dict):
|
|
234
|
+
return {"ok": False, "error": "Alias file must contain a JSON object"}
|
|
235
|
+
loaded = 0
|
|
236
|
+
for k, v in data.items():
|
|
237
|
+
if isinstance(k, str) and isinstance(v, str) and k and v:
|
|
238
|
+
self._aliases[k] = v
|
|
239
|
+
loaded += 1
|
|
240
|
+
logger.debug("Loaded %d aliases from %s", loaded, path)
|
|
241
|
+
return {"ok": True, "loaded": loaded, "total": len(self._aliases)}
|
|
242
|
+
except json.JSONDecodeError as exc:
|
|
243
|
+
return {"ok": False, "error": f"Invalid JSON: {exc}"}
|
|
244
|
+
except Exception as exc:
|
|
245
|
+
return {"ok": False, "error": str(exc)}
|
|
246
|
+
|
|
247
|
+
# ------------------------------------------------------------------
|
|
248
|
+
# Direct accessors (useful for integration)
|
|
249
|
+
# ------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def aliases(self) -> Dict[str, str]:
|
|
253
|
+
"""Read-only snapshot of the alias dictionary."""
|
|
254
|
+
return dict(self._aliases)
|
|
255
|
+
|
|
256
|
+
def clear(self) -> None:
|
|
257
|
+
"""Remove all aliases (including built-ins)."""
|
|
258
|
+
self._aliases.clear()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
# Singleton for dispatch
|
|
263
|
+
# ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
_instance: Optional[CommandAliases] = None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _get_instance() -> CommandAliases:
|
|
269
|
+
global _instance
|
|
270
|
+
if _instance is None:
|
|
271
|
+
_instance = CommandAliases()
|
|
272
|
+
return _instance
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# ---------------------------------------------------------------------------
|
|
276
|
+
# Remote command dispatcher
|
|
277
|
+
# ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
_HANDLERS: Dict[str, Callable[..., dict]] = {}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _register(name: str):
|
|
283
|
+
"""Decorator that registers a handler function for a command type."""
|
|
284
|
+
def decorator(fn: Callable[..., dict]) -> Callable[..., dict]:
|
|
285
|
+
_HANDLERS[name] = fn
|
|
286
|
+
return fn
|
|
287
|
+
return decorator
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@_register("alias")
|
|
291
|
+
def _handle_alias(payload: dict) -> dict:
|
|
292
|
+
name = payload.get("name", "")
|
|
293
|
+
command = payload.get("command", "")
|
|
294
|
+
return _get_instance().alias(name, command)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@_register("unalias")
|
|
298
|
+
def _handle_unalias(payload: dict) -> dict:
|
|
299
|
+
name = payload.get("name", "")
|
|
300
|
+
return _get_instance().unalias(name)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@_register("list_aliases")
|
|
304
|
+
def _handle_list(payload: dict) -> dict:
|
|
305
|
+
return _get_instance().list_aliases()
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@_register("run_script")
|
|
309
|
+
def _handle_run_script(payload: dict) -> dict:
|
|
310
|
+
commands = payload.get("commands", [])
|
|
311
|
+
stop_on_error = payload.get("stop_on_error", False)
|
|
312
|
+
return _get_instance().run_script(commands, stop_on_error)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@_register("save_aliases")
|
|
316
|
+
def _handle_save(payload: dict) -> dict:
|
|
317
|
+
path = payload.get("path", "")
|
|
318
|
+
if not path:
|
|
319
|
+
return {"ok": False, "error": "No path specified"}
|
|
320
|
+
return _get_instance().save_aliases(path)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@_register("load_aliases")
|
|
324
|
+
def _handle_load(payload: dict) -> dict:
|
|
325
|
+
path = payload.get("path", "")
|
|
326
|
+
if not path:
|
|
327
|
+
return {"ok": False, "error": "No path specified"}
|
|
328
|
+
return _get_instance().load_aliases(path)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def dispatch(payload: dict) -> dict:
|
|
332
|
+
"""Route an alias-related command payload to the correct handler.
|
|
333
|
+
|
|
334
|
+
Parameters
|
|
335
|
+
----------
|
|
336
|
+
payload:
|
|
337
|
+
A dict with at least a ``"type"`` key matching one of the supported
|
|
338
|
+
command names (alias, unalias, list_aliases, run_script,
|
|
339
|
+
save_aliases, load_aliases).
|
|
340
|
+
|
|
341
|
+
Returns
|
|
342
|
+
-------
|
|
343
|
+
dict
|
|
344
|
+
``{"ok": True, ...}`` on success, or ``{"ok": False, "error": ...}``
|
|
345
|
+
on failure.
|
|
346
|
+
"""
|
|
347
|
+
cmd_type = payload.get("type", "")
|
|
348
|
+
handler = _HANDLERS.get(cmd_type)
|
|
349
|
+
if handler is None:
|
|
350
|
+
return {"ok": False, "error": f"Unknown alias command: {cmd_type!r}"}
|
|
351
|
+
return handler(payload)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
# ---------------------------------------------------------------------------
|
|
355
|
+
# Self-test
|
|
356
|
+
# ---------------------------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
def test_aliases() -> None:
|
|
359
|
+
"""Exercise every capability of the aliases module."""
|
|
360
|
+
passed = 0
|
|
361
|
+
failed = 0
|
|
362
|
+
|
|
363
|
+
def check(desc: str, condition: bool) -> None:
|
|
364
|
+
nonlocal passed, failed
|
|
365
|
+
if condition:
|
|
366
|
+
passed += 1
|
|
367
|
+
else:
|
|
368
|
+
failed += 1
|
|
369
|
+
print(f" FAIL: {desc}")
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
# -- built-in aliases loaded --
|
|
373
|
+
ca = CommandAliases()
|
|
374
|
+
r = ca.list_aliases()
|
|
375
|
+
check("list_aliases ok", r["ok"])
|
|
376
|
+
check("ll built-in present", "ll" in r["aliases"])
|
|
377
|
+
check("la built-in present", "la" in r["aliases"])
|
|
378
|
+
check("cls built-in present", "cls" in r["aliases"])
|
|
379
|
+
check("h built-in present", "h" in r["aliases"])
|
|
380
|
+
check(".. built-in present", ".." in r["aliases"])
|
|
381
|
+
|
|
382
|
+
# -- create alias --
|
|
383
|
+
r = ca.alias("greet", "echo hello")
|
|
384
|
+
check("alias create ok", r["ok"])
|
|
385
|
+
check("alias name returned", r["name"] == "greet")
|
|
386
|
+
check("alias command returned", r["command"] == "echo hello")
|
|
387
|
+
|
|
388
|
+
# -- create alias validation --
|
|
389
|
+
r = ca.alias("", "echo hi")
|
|
390
|
+
check("alias empty name fails", not r["ok"])
|
|
391
|
+
r = ca.alias("x", "")
|
|
392
|
+
check("alias empty command fails", not r["ok"])
|
|
393
|
+
|
|
394
|
+
# -- list includes new alias --
|
|
395
|
+
r = ca.list_aliases()
|
|
396
|
+
check("new alias in list", "greet" in r["aliases"])
|
|
397
|
+
check("list count correct", r["count"] == len(r["aliases"]))
|
|
398
|
+
|
|
399
|
+
# -- expand --
|
|
400
|
+
result = ca.expand("greet world")
|
|
401
|
+
check("expand greet", result == "echo hello world")
|
|
402
|
+
|
|
403
|
+
result = ca.expand("ll -la")
|
|
404
|
+
check("expand ll", result.endswith("-la"))
|
|
405
|
+
check("expand ll has ls", result.startswith("ls") or result.startswith("dir"))
|
|
406
|
+
|
|
407
|
+
result = ca.expand("unknown_cmd arg")
|
|
408
|
+
check("expand unknown unchanged", result == "unknown_cmd arg")
|
|
409
|
+
|
|
410
|
+
result = ca.expand("")
|
|
411
|
+
check("expand empty unchanged", result == "")
|
|
412
|
+
|
|
413
|
+
# -- alias chaining (one level) --
|
|
414
|
+
ca.alias("g1", "greet there")
|
|
415
|
+
result = ca.expand("g1")
|
|
416
|
+
check("chain g1 -> greet", result == "echo hello there")
|
|
417
|
+
|
|
418
|
+
# -- alias recursion prevention --
|
|
419
|
+
ca.alias("a", "b extra")
|
|
420
|
+
ca.alias("b", "a more")
|
|
421
|
+
result = ca.expand("a")
|
|
422
|
+
check("recursion stops", "a" in result or "b" in result)
|
|
423
|
+
|
|
424
|
+
# -- unalias --
|
|
425
|
+
r = ca.unalias("greet")
|
|
426
|
+
check("unalias ok", r["ok"])
|
|
427
|
+
check("unalias removed name", r["removed"] == "greet")
|
|
428
|
+
|
|
429
|
+
r = ca.unalias("greet")
|
|
430
|
+
check("unalias missing fails", not r["ok"])
|
|
431
|
+
|
|
432
|
+
# -- run_script --
|
|
433
|
+
r = ca.run_script(["echo first", "echo second", "echo third"])
|
|
434
|
+
check("run_script ok", r["ok"])
|
|
435
|
+
check("run_script total", r["total"] == 3)
|
|
436
|
+
check("run_script succeeded", r["succeeded"] == 3)
|
|
437
|
+
check("run_script failed", r["failed"] == 0)
|
|
438
|
+
check("run_script results len", len(r["results"]) == 3)
|
|
439
|
+
|
|
440
|
+
# -- run_script with alias expansion --
|
|
441
|
+
ca.alias("greet", "echo hello")
|
|
442
|
+
r = ca.run_script(["greet"])
|
|
443
|
+
check("run_script alias expand", r["ok"])
|
|
444
|
+
check("run_script alias output", "hello" in r["results"][0].get("output", ""))
|
|
445
|
+
|
|
446
|
+
# -- run_script with bad command and stop_on_error --
|
|
447
|
+
r = ca.run_script(["echo ok", "false_command_xyz_999", "echo never"],
|
|
448
|
+
stop_on_error=True)
|
|
449
|
+
check("run_script stop_on_error ok", r["ok"])
|
|
450
|
+
check("run_script stopped early", r["total"] < 3)
|
|
451
|
+
|
|
452
|
+
# -- run_script invalid input --
|
|
453
|
+
r = ca.run_script("not a list")
|
|
454
|
+
check("run_script non-list fails", not r["ok"])
|
|
455
|
+
|
|
456
|
+
# -- save / load round-trip --
|
|
457
|
+
tmp = Path(tempfile.mkdtemp(prefix="gcli_alias_test_"))
|
|
458
|
+
save_path = str(tmp / "aliases.json")
|
|
459
|
+
r = ca.save_aliases(save_path)
|
|
460
|
+
check("save_aliases ok", r["ok"])
|
|
461
|
+
check("save_aliases count", r["count"] > 0)
|
|
462
|
+
|
|
463
|
+
# Create a fresh instance and load
|
|
464
|
+
ca2 = CommandAliases()
|
|
465
|
+
ca2.clear()
|
|
466
|
+
r = ca2.load_aliases(save_path)
|
|
467
|
+
check("load_aliases ok", r["ok"])
|
|
468
|
+
check("load_aliases loaded", r["loaded"] > 0)
|
|
469
|
+
check("load_aliases preserves greet", "greet" in ca2.aliases)
|
|
470
|
+
|
|
471
|
+
# -- load missing file --
|
|
472
|
+
r = ca2.load_aliases("/nonexistent/path/xyz.json")
|
|
473
|
+
check("load missing file fails", not r["ok"])
|
|
474
|
+
|
|
475
|
+
# -- load invalid JSON --
|
|
476
|
+
bad_path = str(tmp / "bad.json")
|
|
477
|
+
Path(bad_path).write_text("{invalid json", encoding="utf-8")
|
|
478
|
+
r = ca2.load_aliases(bad_path)
|
|
479
|
+
check("load bad json fails", not r["ok"])
|
|
480
|
+
|
|
481
|
+
# -- save to nonexistent parent --
|
|
482
|
+
r = ca.save_aliases(str(tmp / "deep" / "nest" / "aliases.json"))
|
|
483
|
+
check("save creates parents", r["ok"])
|
|
484
|
+
|
|
485
|
+
# -- dispatch routing --
|
|
486
|
+
r = dispatch({"type": "alias", "name": "test1", "command": "echo test"})
|
|
487
|
+
check("dispatch alias", r["ok"])
|
|
488
|
+
r = dispatch({"type": "unalias", "name": "test1"})
|
|
489
|
+
check("dispatch unalias", r["ok"])
|
|
490
|
+
r = dispatch({"type": "list_aliases"})
|
|
491
|
+
check("dispatch list_aliases", r["ok"])
|
|
492
|
+
r = dispatch({"type": "run_script", "commands": ["echo x"]})
|
|
493
|
+
check("dispatch run_script", r["ok"])
|
|
494
|
+
r = dispatch({"type": "bogus"})
|
|
495
|
+
check("dispatch unknown fails", not r["ok"])
|
|
496
|
+
|
|
497
|
+
except Exception as exc:
|
|
498
|
+
failed += 1
|
|
499
|
+
print(f" EXCEPTION: {exc}")
|
|
500
|
+
finally:
|
|
501
|
+
# Cleanup temp dir
|
|
502
|
+
try:
|
|
503
|
+
import shutil
|
|
504
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
505
|
+
except Exception:
|
|
506
|
+
pass
|
|
507
|
+
|
|
508
|
+
print(f"aliases tests: {passed} passed, {failed} failed")
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
if __name__ == "__main__":
|
|
512
|
+
test_aliases()
|