cli-mem 0.4.0__tar.gz → 0.4.1__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_mem-0.4.0 → cli_mem-0.4.1}/PKG-INFO +1 -1
- {cli_mem-0.4.0 → cli_mem-0.4.1}/pyproject.toml +1 -1
- {cli_mem-0.4.0 → cli_mem-0.4.1}/src/mem/__init__.py +1 -1
- {cli_mem-0.4.0 → cli_mem-0.4.1}/src/mem/groups.py +30 -2
- {cli_mem-0.4.0 → cli_mem-0.4.1}/tests/test_groups.py +85 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/.github/workflows/ci.yml +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/.github/workflows/release.yml +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/.gitignore +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/ARCHITECTURE.md +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/LICENSE +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/PHILOSOPHY.md +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/README.md +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/assets/.gitkeep +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/command-groups/databases/postgres-debug.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/command-groups/databases/redis-ops.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/command-groups/devops/docker-cleanup.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/command-groups/devops/incident-response.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/command-groups/devops/k8s-debug.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/command-groups/git/cleanup.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/command-groups/git/forensics.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/command-groups/networking/dns-debug.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/command-groups/networking/network-debug.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/command-groups/performance/system-profile.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/command-groups/security/headers-audit.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/command-groups/security/recon.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/command-groups/security/ssl-audit.json +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/docs/decisions/001-jsonl-over-sqlite.md +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/docs/decisions/002-apple-fm-sdk-for-patterns.md +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/docs/decisions/003-no-daemon.md +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/docs/decisions/004-per-repo-jsonl.md +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/hooks/mem.bash +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/hooks/mem.fish +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/hooks/mem.zsh +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/install.sh +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/src/mem/_generable.py +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/src/mem/capture.py +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/src/mem/cli.py +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/src/mem/models.py +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/src/mem/patterns.py +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/src/mem/search.py +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/src/mem/storage.py +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/src/mem/variables.py +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/tests/conftest.py +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/tests/test_capture.py +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/tests/test_patterns.py +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/tests/test_search.py +0 -0
- {cli_mem-0.4.0 → cli_mem-0.4.1}/tests/test_storage.py +0 -0
|
@@ -24,7 +24,11 @@ import click
|
|
|
24
24
|
from mem import storage
|
|
25
25
|
from mem.capture import get_git_repo
|
|
26
26
|
from mem.models import Group, GroupCommand, GroupFile, SavedCommand, VarDeclaration
|
|
27
|
-
from mem.variables import
|
|
27
|
+
from mem.variables import (
|
|
28
|
+
merge_var_declarations,
|
|
29
|
+
parse_variables,
|
|
30
|
+
process_escapes,
|
|
31
|
+
)
|
|
28
32
|
|
|
29
33
|
GROUP_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9-]*$")
|
|
30
34
|
|
|
@@ -239,6 +243,21 @@ def export_json(group_name: str, group: Group) -> str:
|
|
|
239
243
|
)
|
|
240
244
|
|
|
241
245
|
|
|
246
|
+
def _auto_detect_vars(commands: list[GroupCommand]) -> None:
|
|
247
|
+
"""Populate the vars field on imported commands by scanning for $VAR_NAME tokens.
|
|
248
|
+
|
|
249
|
+
Commands that already have vars declared (e.g., from a full mem export)
|
|
250
|
+
are left untouched. This ensures imported runbooks work with mem run
|
|
251
|
+
variable substitution out of the box.
|
|
252
|
+
"""
|
|
253
|
+
for cmd in commands:
|
|
254
|
+
if cmd.vars is not None:
|
|
255
|
+
continue
|
|
256
|
+
detected = parse_variables(cmd.cmd)
|
|
257
|
+
if detected:
|
|
258
|
+
cmd.vars = [VarDeclaration(name=n) for n in detected]
|
|
259
|
+
|
|
260
|
+
|
|
242
261
|
def import_from_json_str(content: str) -> tuple[str | None, list[GroupCommand]]:
|
|
243
262
|
"""Parse a JSON string and extract commands.
|
|
244
263
|
|
|
@@ -276,10 +295,18 @@ def import_from_json_str(content: str) -> tuple[str | None, list[GroupCommand]]:
|
|
|
276
295
|
raise click.ClickException("Expected 'commands' to be a list.")
|
|
277
296
|
|
|
278
297
|
try:
|
|
279
|
-
commands = [
|
|
298
|
+
commands = [
|
|
299
|
+
GroupCommand(
|
|
300
|
+
cmd=c["cmd"],
|
|
301
|
+
comment=c.get("comment"),
|
|
302
|
+
vars=c.get("vars"),
|
|
303
|
+
)
|
|
304
|
+
for c in cmds
|
|
305
|
+
]
|
|
280
306
|
except (KeyError, TypeError) as e:
|
|
281
307
|
raise click.ClickException(f"Malformed command entry: {e}")
|
|
282
308
|
|
|
309
|
+
_auto_detect_vars(commands)
|
|
283
310
|
return group_name, commands
|
|
284
311
|
|
|
285
312
|
|
|
@@ -338,6 +365,7 @@ def import_from_markdown_str(content: str) -> tuple[str | None, list[GroupComman
|
|
|
338
365
|
if not commands:
|
|
339
366
|
raise click.ClickException("No commands found in markdown table.")
|
|
340
367
|
|
|
368
|
+
_auto_detect_vars(commands)
|
|
341
369
|
return group_name, commands
|
|
342
370
|
|
|
343
371
|
|
|
@@ -2053,6 +2053,91 @@ class TestImportFromMarkdownStr:
|
|
|
2053
2053
|
groups.import_from_markdown_str("# Just text\nNo table here.")
|
|
2054
2054
|
|
|
2055
2055
|
|
|
2056
|
+
class TestImportAutoDetectVars:
|
|
2057
|
+
"""Tests for automatic variable detection during import."""
|
|
2058
|
+
|
|
2059
|
+
def test_json_detects_single_var(self):
|
|
2060
|
+
content = json.dumps({"recon": {"commands": [{"cmd": "dig $DOMAIN A +short"}]}})
|
|
2061
|
+
_, cmds = groups.import_from_json_str(content)
|
|
2062
|
+
assert cmds[0].vars is not None
|
|
2063
|
+
assert len(cmds[0].vars) == 1
|
|
2064
|
+
assert cmds[0].vars[0].name == "DOMAIN"
|
|
2065
|
+
|
|
2066
|
+
def test_json_detects_multiple_vars(self):
|
|
2067
|
+
content = json.dumps(
|
|
2068
|
+
{"commands": [{"cmd": "curl -u $USER_NAME:$API_TOKEN https://$HOST/api"}]}
|
|
2069
|
+
)
|
|
2070
|
+
_, cmds = groups.import_from_json_str(content)
|
|
2071
|
+
var_names = [v.name for v in cmds[0].vars]
|
|
2072
|
+
assert "USER_NAME" in var_names
|
|
2073
|
+
assert "API_TOKEN" in var_names
|
|
2074
|
+
assert "HOST" in var_names
|
|
2075
|
+
|
|
2076
|
+
def test_json_no_vars_leaves_none(self):
|
|
2077
|
+
content = json.dumps({"commands": [{"cmd": "ls -la"}]})
|
|
2078
|
+
_, cmds = groups.import_from_json_str(content)
|
|
2079
|
+
assert cmds[0].vars is None
|
|
2080
|
+
|
|
2081
|
+
def test_json_excludes_shell_vars(self):
|
|
2082
|
+
"""$HOME and $PATH are common shell vars and should not be detected."""
|
|
2083
|
+
content = json.dumps({"commands": [{"cmd": "ls $HOME/.config && echo $PATH"}]})
|
|
2084
|
+
_, cmds = groups.import_from_json_str(content)
|
|
2085
|
+
assert cmds[0].vars is None
|
|
2086
|
+
|
|
2087
|
+
def test_json_preserves_existing_vars(self):
|
|
2088
|
+
"""Commands that already declare vars (from a full export) should not be overwritten."""
|
|
2089
|
+
content = json.dumps(
|
|
2090
|
+
{
|
|
2091
|
+
"commands": [
|
|
2092
|
+
{
|
|
2093
|
+
"cmd": "curl https://$HOST/api",
|
|
2094
|
+
"vars": [{"name": "HOST", "default": "localhost"}],
|
|
2095
|
+
}
|
|
2096
|
+
]
|
|
2097
|
+
}
|
|
2098
|
+
)
|
|
2099
|
+
_, cmds = groups.import_from_json_str(content)
|
|
2100
|
+
assert len(cmds[0].vars) == 1
|
|
2101
|
+
assert cmds[0].vars[0].name == "HOST"
|
|
2102
|
+
assert cmds[0].vars[0].default == "localhost"
|
|
2103
|
+
|
|
2104
|
+
def test_markdown_detects_vars(self):
|
|
2105
|
+
md = (
|
|
2106
|
+
"## ops\n\n"
|
|
2107
|
+
"| Command | Description |\n"
|
|
2108
|
+
"|---|---|\n"
|
|
2109
|
+
"| `kubectl get pods -n $NAMESPACE` | list pods |\n"
|
|
2110
|
+
)
|
|
2111
|
+
_, cmds = groups.import_from_markdown_str(md)
|
|
2112
|
+
assert cmds[0].vars is not None
|
|
2113
|
+
assert cmds[0].vars[0].name == "NAMESPACE"
|
|
2114
|
+
|
|
2115
|
+
def test_markdown_no_vars(self):
|
|
2116
|
+
md = (
|
|
2117
|
+
"## test\n\n"
|
|
2118
|
+
"| Command | Description |\n"
|
|
2119
|
+
"|---|---|\n"
|
|
2120
|
+
"| `echo hello` | greeting |\n"
|
|
2121
|
+
)
|
|
2122
|
+
_, cmds = groups.import_from_markdown_str(md)
|
|
2123
|
+
assert cmds[0].vars is None
|
|
2124
|
+
|
|
2125
|
+
def test_mixed_commands_some_with_vars(self):
|
|
2126
|
+
content = json.dumps(
|
|
2127
|
+
{
|
|
2128
|
+
"commands": [
|
|
2129
|
+
{"cmd": "echo starting"},
|
|
2130
|
+
{"cmd": "curl https://$HOST:$PORT/health"},
|
|
2131
|
+
{"cmd": "echo done"},
|
|
2132
|
+
]
|
|
2133
|
+
}
|
|
2134
|
+
)
|
|
2135
|
+
_, cmds = groups.import_from_json_str(content)
|
|
2136
|
+
assert cmds[0].vars is None
|
|
2137
|
+
assert len(cmds[1].vars) == 2
|
|
2138
|
+
assert cmds[2].vars is None
|
|
2139
|
+
|
|
2140
|
+
|
|
2056
2141
|
class TestImportFromJsonDelegation:
|
|
2057
2142
|
"""Ensure the file-based function still works via delegation."""
|
|
2058
2143
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|