runmark 0.2.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.
- runmark/__init__.py +7 -0
- runmark/__main__.py +8 -0
- runmark/cli/__init__.py +5 -0
- runmark/cli/app.py +74 -0
- runmark/cli/commands/__init__.py +27 -0
- runmark/cli/commands/check.py +54 -0
- runmark/cli/commands/contract.py +236 -0
- runmark/cli/commands/diff.py +90 -0
- runmark/cli/commands/doctor.py +73 -0
- runmark/cli/commands/history.py +25 -0
- runmark/cli/commands/init.py +65 -0
- runmark/cli/commands/scan.py +25 -0
- runmark/cli/commands/share.py +104 -0
- runmark/cli/commands/snapshot.py +39 -0
- runmark/cli/commands/verify.py +75 -0
- runmark/cli/commands/version.py +36 -0
- runmark/contracts/__init__.py +60 -0
- runmark/contracts/canonicalizer.py +119 -0
- runmark/contracts/diff.py +390 -0
- runmark/contracts/discovery.py +33 -0
- runmark/contracts/evaluator.py +724 -0
- runmark/contracts/evidence.py +448 -0
- runmark/contracts/generator.py +107 -0
- runmark/contracts/parser.py +60 -0
- runmark/contracts/security.py +11 -0
- runmark/contracts/validator.py +157 -0
- runmark/contracts/version_constraints.py +234 -0
- runmark/core/__init__.py +32 -0
- runmark/core/contract_check.py +66 -0
- runmark/core/contract_diff.py +65 -0
- runmark/core/contract_init.py +80 -0
- runmark/core/diff.py +522 -0
- runmark/core/doctor.py +240 -0
- runmark/core/reporter.py +163 -0
- runmark/core/scanner.py +173 -0
- runmark/core/snapshot.py +34 -0
- runmark/core/verifier.py +83 -0
- runmark/detectors/__init__.py +86 -0
- runmark/detectors/base.py +66 -0
- runmark/detectors/containers/__init__.py +5 -0
- runmark/detectors/containers/compose.py +82 -0
- runmark/detectors/dependencies/__init__.py +9 -0
- runmark/detectors/dependencies/node.py +185 -0
- runmark/detectors/dependencies/python.py +162 -0
- runmark/detectors/environment/__init__.py +5 -0
- runmark/detectors/environment/env.py +89 -0
- runmark/detectors/git/__init__.py +5 -0
- runmark/detectors/git/git.py +81 -0
- runmark/detectors/network/__init__.py +5 -0
- runmark/detectors/network/ports.py +90 -0
- runmark/detectors/project/__init__.py +11 -0
- runmark/detectors/project/docker.py +48 -0
- runmark/detectors/project/node.py +95 -0
- runmark/detectors/project/python.py +97 -0
- runmark/detectors/registry.py +39 -0
- runmark/detectors/runtimes/__init__.py +13 -0
- runmark/detectors/runtimes/docker.py +59 -0
- runmark/detectors/runtimes/git.py +59 -0
- runmark/detectors/runtimes/node.py +59 -0
- runmark/detectors/runtimes/python.py +62 -0
- runmark/detectors/services/__init__.py +9 -0
- runmark/detectors/services/postgres.py +104 -0
- runmark/detectors/services/redis.py +104 -0
- runmark/detectors/system/__init__.py +5 -0
- runmark/detectors/system/system.py +40 -0
- runmark/models/__init__.py +81 -0
- runmark/models/common.py +36 -0
- runmark/models/container.py +14 -0
- runmark/models/contract.py +122 -0
- runmark/models/contract_result.py +84 -0
- runmark/models/dependency.py +19 -0
- runmark/models/diagnostic.py +133 -0
- runmark/models/environment.py +32 -0
- runmark/models/git.py +16 -0
- runmark/models/network.py +17 -0
- runmark/models/project.py +20 -0
- runmark/models/runmark.py +58 -0
- runmark/models/runtime.py +19 -0
- runmark/models/service.py +21 -0
- runmark/models/system.py +13 -0
- runmark/output/__init__.py +34 -0
- runmark/output/contract.py +337 -0
- runmark/output/json.py +16 -0
- runmark/output/markdown.py +172 -0
- runmark/output/tables.py +300 -0
- runmark/output/terminal.py +70 -0
- runmark/security/__init__.py +29 -0
- runmark/security/contract_sanitizer.py +38 -0
- runmark/security/export_sanitizer.py +142 -0
- runmark/security/redactor.py +75 -0
- runmark/security/sanitizer.py +105 -0
- runmark/security/secret_patterns.py +116 -0
- runmark/storage/__init__.py +6 -0
- runmark/storage/filesystem.py +133 -0
- runmark/storage/paths.py +60 -0
- runmark/utils/__init__.py +20 -0
- runmark/utils/commands.py +117 -0
- runmark/utils/hashing.py +240 -0
- runmark/utils/platform.py +36 -0
- runmark-0.2.1.dist-info/METADATA +149 -0
- runmark-0.2.1.dist-info/RECORD +105 -0
- runmark-0.2.1.dist-info/WHEEL +5 -0
- runmark-0.2.1.dist-info/entry_points.txt +2 -0
- runmark-0.2.1.dist-info/licenses/LICENSE +21 -0
- runmark-0.2.1.dist-info/top_level.txt +1 -0
runmark/__init__.py
ADDED
runmark/__main__.py
ADDED
runmark/cli/__init__.py
ADDED
runmark/cli/app.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Runmark CLI application entrypoint."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from runmark.cli.commands.check import check_command
|
|
8
|
+
from runmark.cli.commands.contract import contract_app
|
|
9
|
+
from runmark.cli.commands.diff import diff_command
|
|
10
|
+
from runmark.cli.commands.doctor import doctor_command
|
|
11
|
+
from runmark.cli.commands.history import history_command
|
|
12
|
+
from runmark.cli.commands.init import init_command
|
|
13
|
+
from runmark.cli.commands.scan import scan_command
|
|
14
|
+
from runmark.cli.commands.share import share_command
|
|
15
|
+
from runmark.cli.commands.snapshot import snapshot_command
|
|
16
|
+
from runmark.cli.commands.verify import verify_command
|
|
17
|
+
from runmark.cli.commands.version import version_command
|
|
18
|
+
from runmark.output.terminal import term
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(
|
|
21
|
+
name="runmark",
|
|
22
|
+
help="Runmark — Know what makes your code run.\n\nGit tracks your code. Runmark tracks what makes your code run.",
|
|
23
|
+
no_args_is_help=True,
|
|
24
|
+
add_completion=False,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Register subcommands
|
|
28
|
+
app.command("init", help="Initialize Runmark tracking in the current project.")(init_command)
|
|
29
|
+
app.command(
|
|
30
|
+
"scan", help="Inspect and display the complete runtime, dependency, and service state."
|
|
31
|
+
)(scan_command)
|
|
32
|
+
app.command(
|
|
33
|
+
"check",
|
|
34
|
+
help="Evaluate whether the host environment satisfies the project environment contract (runmark.json).",
|
|
35
|
+
)(check_command)
|
|
36
|
+
app.add_typer(contract_app)
|
|
37
|
+
app.command(
|
|
38
|
+
"snapshot",
|
|
39
|
+
help="Capture and persist current environment state into an immutable baseline snapshot.",
|
|
40
|
+
)(snapshot_command)
|
|
41
|
+
app.command(
|
|
42
|
+
"diff", help="Compare environment state between snapshots or against live environment."
|
|
43
|
+
)(diff_command)
|
|
44
|
+
app.command("verify", help="Verify current machine environment against a baseline snapshot.")(
|
|
45
|
+
verify_command
|
|
46
|
+
)
|
|
47
|
+
app.command(
|
|
48
|
+
"doctor", help="Diagnose environment discrepancies and get actionable remediation advice."
|
|
49
|
+
)(doctor_command)
|
|
50
|
+
app.command(
|
|
51
|
+
"share",
|
|
52
|
+
help="Generate a sanitized, portable diagnostic report for sharing with teammates or issues.",
|
|
53
|
+
)(share_command)
|
|
54
|
+
app.command("version", help="Display version and platform diagnostics.")(version_command)
|
|
55
|
+
app.command("history", help="List snapshot history.")(history_command)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def main() -> int:
|
|
59
|
+
"""Main CLI entrypoint."""
|
|
60
|
+
try:
|
|
61
|
+
app()
|
|
62
|
+
return 0
|
|
63
|
+
except typer.Exit as e:
|
|
64
|
+
return e.exit_code
|
|
65
|
+
except KeyboardInterrupt:
|
|
66
|
+
term.print("\n[dim]Operation cancelled by user.[/dim]")
|
|
67
|
+
return 130
|
|
68
|
+
except Exception as exc:
|
|
69
|
+
term.print_error(f"Unexpected error: {exc}")
|
|
70
|
+
return 3
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
sys.exit(main())
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""CLI commands package."""
|
|
2
|
+
|
|
3
|
+
from runmark.cli.commands.check import check_command
|
|
4
|
+
from runmark.cli.commands.contract import contract_app
|
|
5
|
+
from runmark.cli.commands.diff import diff_command
|
|
6
|
+
from runmark.cli.commands.doctor import doctor_command
|
|
7
|
+
from runmark.cli.commands.history import history_command
|
|
8
|
+
from runmark.cli.commands.init import init_command
|
|
9
|
+
from runmark.cli.commands.scan import scan_command
|
|
10
|
+
from runmark.cli.commands.share import share_command
|
|
11
|
+
from runmark.cli.commands.snapshot import snapshot_command
|
|
12
|
+
from runmark.cli.commands.verify import verify_command
|
|
13
|
+
from runmark.cli.commands.version import version_command
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"check_command",
|
|
17
|
+
"contract_app",
|
|
18
|
+
"diff_command",
|
|
19
|
+
"doctor_command",
|
|
20
|
+
"history_command",
|
|
21
|
+
"init_command",
|
|
22
|
+
"scan_command",
|
|
23
|
+
"share_command",
|
|
24
|
+
"snapshot_command",
|
|
25
|
+
"verify_command",
|
|
26
|
+
"version_command",
|
|
27
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""CLI command: runmark check."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from runmark.contracts.parser import ContractParseError
|
|
8
|
+
from runmark.contracts.security import ContractSecurityError
|
|
9
|
+
from runmark.contracts.validator import ContractValidationError
|
|
10
|
+
from runmark.core.contract_check import ContractCheckService
|
|
11
|
+
from runmark.output.contract import render_contract_check
|
|
12
|
+
from runmark.output.terminal import term
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def check_command(
|
|
16
|
+
path: Path | None = typer.Option(
|
|
17
|
+
None,
|
|
18
|
+
"--path",
|
|
19
|
+
"-p",
|
|
20
|
+
help="Target project root directory (defaults to current directory)",
|
|
21
|
+
),
|
|
22
|
+
json_output: bool = typer.Option(
|
|
23
|
+
False,
|
|
24
|
+
"--json",
|
|
25
|
+
help="Output evaluation results as machine-readable JSON",
|
|
26
|
+
),
|
|
27
|
+
explain: bool = typer.Option(
|
|
28
|
+
False,
|
|
29
|
+
"--explain",
|
|
30
|
+
help="Display detailed diagnostic breakdown and suggested actions for unsatisfied requirements",
|
|
31
|
+
),
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Evaluate whether current host environment satisfies the project environment contract (runmark.json)."""
|
|
34
|
+
try:
|
|
35
|
+
contract, result = ContractCheckService.check_environment(project_path=path)
|
|
36
|
+
except ContractSecurityError as sec_err:
|
|
37
|
+
term.print_error(str(sec_err))
|
|
38
|
+
raise typer.Exit(code=4) from None
|
|
39
|
+
except (FileNotFoundError, ContractParseError, ContractValidationError) as err:
|
|
40
|
+
term.print_error(str(err))
|
|
41
|
+
raise typer.Exit(code=2) from None
|
|
42
|
+
except Exception as exc:
|
|
43
|
+
term.print_error(f"Internal error during contract evaluation: {exc}")
|
|
44
|
+
raise typer.Exit(code=3) from None
|
|
45
|
+
|
|
46
|
+
if json_output:
|
|
47
|
+
term.print_json(result)
|
|
48
|
+
else:
|
|
49
|
+
render_contract_check(result, explain=explain)
|
|
50
|
+
|
|
51
|
+
if result.is_passed:
|
|
52
|
+
raise typer.Exit(code=0)
|
|
53
|
+
else:
|
|
54
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""CLI command group: runmark contract."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from runmark.contracts.parser import ContractParseError
|
|
8
|
+
from runmark.contracts.security import ContractSecurityError
|
|
9
|
+
from runmark.contracts.validator import ContractValidationError
|
|
10
|
+
from runmark.core.contract_check import ContractCheckService
|
|
11
|
+
from runmark.core.contract_diff import ContractDiffService
|
|
12
|
+
from runmark.core.contract_init import ContractInitService
|
|
13
|
+
from runmark.output.contract import (
|
|
14
|
+
render_contract_diff,
|
|
15
|
+
render_contract_preview,
|
|
16
|
+
render_contract_show,
|
|
17
|
+
)
|
|
18
|
+
from runmark.output.terminal import term
|
|
19
|
+
|
|
20
|
+
contract_app = typer.Typer(
|
|
21
|
+
name="contract",
|
|
22
|
+
help="Inspect, validate, initialize, and display project environment contracts (runmark.json).",
|
|
23
|
+
no_args_is_help=True,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@contract_app.command(
|
|
28
|
+
"init", help="Bootstrap a runmark.json contract from discovered project evidence."
|
|
29
|
+
)
|
|
30
|
+
def init_contract_command(
|
|
31
|
+
path: Path | None = typer.Option(
|
|
32
|
+
None,
|
|
33
|
+
"--path",
|
|
34
|
+
"-p",
|
|
35
|
+
help="Target project root directory (defaults to current directory)",
|
|
36
|
+
),
|
|
37
|
+
yes: bool = typer.Option(
|
|
38
|
+
False,
|
|
39
|
+
"--yes",
|
|
40
|
+
"-y",
|
|
41
|
+
help="Skip interactive confirmation and generate contract directly",
|
|
42
|
+
),
|
|
43
|
+
dry_run: bool = typer.Option(
|
|
44
|
+
False,
|
|
45
|
+
"--dry-run",
|
|
46
|
+
help="Preview generated contract without modifying any files",
|
|
47
|
+
),
|
|
48
|
+
force: bool = typer.Option(
|
|
49
|
+
False,
|
|
50
|
+
"--force",
|
|
51
|
+
"-f",
|
|
52
|
+
help="Overwrite existing runmark.json contract",
|
|
53
|
+
),
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Bootstrap an environment contract from project evidence."""
|
|
56
|
+
target_root = path.resolve() if path else Path.cwd().resolve()
|
|
57
|
+
target_file = target_root / "runmark.json"
|
|
58
|
+
|
|
59
|
+
# Pre-check for existing contract if not using force/dry-run
|
|
60
|
+
if target_file.exists() and not force and not dry_run:
|
|
61
|
+
term.print_error(
|
|
62
|
+
f"Environment contract 'runmark.json' already exists in '{target_root}'.\n"
|
|
63
|
+
"Use --force to replace the existing contract."
|
|
64
|
+
)
|
|
65
|
+
raise typer.Exit(code=2)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
contract, evidence, _ = ContractInitService.init_contract(
|
|
69
|
+
project_path=path,
|
|
70
|
+
force=force,
|
|
71
|
+
dry_run=True, # Always generate in dry-run first for preview and validation
|
|
72
|
+
)
|
|
73
|
+
except ContractSecurityError as sec_err:
|
|
74
|
+
term.print_error(str(sec_err))
|
|
75
|
+
raise typer.Exit(code=4) from None
|
|
76
|
+
except FileExistsError as fe_err:
|
|
77
|
+
term.print_error(str(fe_err))
|
|
78
|
+
raise typer.Exit(code=2) from None
|
|
79
|
+
except (FileNotFoundError, ContractParseError, ContractValidationError) as err:
|
|
80
|
+
term.print_error(str(err))
|
|
81
|
+
raise typer.Exit(code=2) from None
|
|
82
|
+
except Exception as exc:
|
|
83
|
+
term.print_error(f"Internal error generating contract: {exc}")
|
|
84
|
+
raise typer.Exit(code=3) from None
|
|
85
|
+
|
|
86
|
+
# Render Preview
|
|
87
|
+
render_contract_preview(contract, evidence)
|
|
88
|
+
|
|
89
|
+
if dry_run:
|
|
90
|
+
term.print_warning("Dry-run mode enabled. No files were modified.")
|
|
91
|
+
raise typer.Exit(code=0)
|
|
92
|
+
|
|
93
|
+
# Interactive confirmation if not skipped with --yes
|
|
94
|
+
if not yes:
|
|
95
|
+
confirmed = typer.confirm("Create runmark.json?", default=True)
|
|
96
|
+
if not confirmed:
|
|
97
|
+
term.print("Aborted. No files were modified.")
|
|
98
|
+
raise typer.Exit(code=0)
|
|
99
|
+
|
|
100
|
+
# Persist contract atomically
|
|
101
|
+
try:
|
|
102
|
+
ContractInitService.init_contract(
|
|
103
|
+
project_path=path,
|
|
104
|
+
force=force,
|
|
105
|
+
dry_run=False,
|
|
106
|
+
)
|
|
107
|
+
except ContractSecurityError as sec_err:
|
|
108
|
+
term.print_error(str(sec_err))
|
|
109
|
+
raise typer.Exit(code=4) from None
|
|
110
|
+
except Exception as exc:
|
|
111
|
+
term.print_error(f"Failed to persist contract: {exc}")
|
|
112
|
+
raise typer.Exit(code=3) from None
|
|
113
|
+
|
|
114
|
+
term.print_success("Environment contract 'runmark.json' created successfully.")
|
|
115
|
+
raise typer.Exit(code=0)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@contract_app.command(
|
|
119
|
+
"diff", help="Show semantic differences between working contract and baseline."
|
|
120
|
+
)
|
|
121
|
+
def diff_contract_command(
|
|
122
|
+
path: Path | None = typer.Option(
|
|
123
|
+
None,
|
|
124
|
+
"--path",
|
|
125
|
+
"-p",
|
|
126
|
+
help="Target project root directory (defaults to current directory)",
|
|
127
|
+
),
|
|
128
|
+
json_output: bool = typer.Option(
|
|
129
|
+
False,
|
|
130
|
+
"--json",
|
|
131
|
+
help="Output semantic contract diff as machine-readable JSON",
|
|
132
|
+
),
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Compare working runmark.json against Git repository baseline."""
|
|
135
|
+
try:
|
|
136
|
+
diff_result = ContractDiffService.diff_contracts(project_path=path)
|
|
137
|
+
except ContractSecurityError as sec_err:
|
|
138
|
+
term.print_error(str(sec_err))
|
|
139
|
+
raise typer.Exit(code=4) from None
|
|
140
|
+
except (FileNotFoundError, ContractParseError, ContractValidationError) as err:
|
|
141
|
+
term.print_error(str(err))
|
|
142
|
+
raise typer.Exit(code=2) from None
|
|
143
|
+
except Exception as exc:
|
|
144
|
+
term.print_error(f"Internal error diffing contracts: {exc}")
|
|
145
|
+
raise typer.Exit(code=3) from None
|
|
146
|
+
|
|
147
|
+
if json_output:
|
|
148
|
+
term.print_json(diff_result)
|
|
149
|
+
else:
|
|
150
|
+
render_contract_diff(diff_result)
|
|
151
|
+
raise typer.Exit(code=0)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@contract_app.command(
|
|
155
|
+
"validate", help="Validate runmark.json syntax, schema, domain semantics, and security."
|
|
156
|
+
)
|
|
157
|
+
def validate_contract_command(
|
|
158
|
+
path: Path | None = typer.Option(
|
|
159
|
+
None,
|
|
160
|
+
"--path",
|
|
161
|
+
"-p",
|
|
162
|
+
help="Target project root directory (defaults to current directory)",
|
|
163
|
+
),
|
|
164
|
+
json_output: bool = typer.Option(
|
|
165
|
+
False,
|
|
166
|
+
"--json",
|
|
167
|
+
help="Output validation status as machine-readable JSON",
|
|
168
|
+
),
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Validate that runmark.json is structurally, semantically, and securely valid."""
|
|
171
|
+
try:
|
|
172
|
+
contract = ContractCheckService.validate_contract(project_path=path)
|
|
173
|
+
except ContractSecurityError as sec_err:
|
|
174
|
+
term.print_error(str(sec_err))
|
|
175
|
+
raise typer.Exit(code=4) from None
|
|
176
|
+
except (FileNotFoundError, ContractParseError, ContractValidationError) as err:
|
|
177
|
+
term.print_error(str(err))
|
|
178
|
+
raise typer.Exit(code=2) from None
|
|
179
|
+
except Exception as exc:
|
|
180
|
+
term.print_error(f"Internal error validating contract: {exc}")
|
|
181
|
+
raise typer.Exit(code=3) from None
|
|
182
|
+
|
|
183
|
+
if json_output:
|
|
184
|
+
term.print_json(
|
|
185
|
+
{
|
|
186
|
+
"status": "valid",
|
|
187
|
+
"version": contract.version,
|
|
188
|
+
"project": contract.project.name,
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
term.print_success(
|
|
193
|
+
f"Environment contract 'runmark.json' is valid (version {contract.version})."
|
|
194
|
+
)
|
|
195
|
+
raise typer.Exit(code=0)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@contract_app.command(
|
|
199
|
+
"show", help="Display the canonical / normalized interpretation of runmark.json."
|
|
200
|
+
)
|
|
201
|
+
def show_contract_command(
|
|
202
|
+
path: Path | None = typer.Option(
|
|
203
|
+
None,
|
|
204
|
+
"--path",
|
|
205
|
+
"-p",
|
|
206
|
+
help="Target project root directory (defaults to current directory)",
|
|
207
|
+
),
|
|
208
|
+
json_output: bool = typer.Option(
|
|
209
|
+
False,
|
|
210
|
+
"--json",
|
|
211
|
+
help="Output canonical contract definition as machine-readable JSON",
|
|
212
|
+
),
|
|
213
|
+
) -> None:
|
|
214
|
+
"""Display normalized contract specifications and canonical fingerprint."""
|
|
215
|
+
try:
|
|
216
|
+
contract, canonical, fingerprint = ContractCheckService.show_contract(project_path=path)
|
|
217
|
+
except ContractSecurityError as sec_err:
|
|
218
|
+
term.print_error(str(sec_err))
|
|
219
|
+
raise typer.Exit(code=4) from None
|
|
220
|
+
except (FileNotFoundError, ContractParseError, ContractValidationError) as err:
|
|
221
|
+
term.print_error(str(err))
|
|
222
|
+
raise typer.Exit(code=2) from None
|
|
223
|
+
except Exception as exc:
|
|
224
|
+
term.print_error(f"Internal error displaying contract: {exc}")
|
|
225
|
+
raise typer.Exit(code=3) from None
|
|
226
|
+
|
|
227
|
+
if json_output:
|
|
228
|
+
term.print_json(
|
|
229
|
+
{
|
|
230
|
+
"contract": canonical,
|
|
231
|
+
"fingerprint": fingerprint,
|
|
232
|
+
}
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
render_contract_show(contract, canonical, fingerprint)
|
|
236
|
+
raise typer.Exit(code=0)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""CLI command: runmark diff."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from runmark.core.diff import DiffEngine
|
|
8
|
+
from runmark.core.scanner import Scanner
|
|
9
|
+
from runmark.core.snapshot import SnapshotManager
|
|
10
|
+
from runmark.models.runmark import RunmarkState
|
|
11
|
+
from runmark.output.tables import render_diff
|
|
12
|
+
from runmark.output.terminal import term
|
|
13
|
+
from runmark.storage.paths import find_project_root
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def diff_command(
|
|
17
|
+
from_snapshot: str | None = typer.Option(
|
|
18
|
+
None,
|
|
19
|
+
"--from",
|
|
20
|
+
help="Baseline snapshot ID (defaults to current active snapshot)",
|
|
21
|
+
),
|
|
22
|
+
to_snapshot: str | None = typer.Option(
|
|
23
|
+
None,
|
|
24
|
+
"--to",
|
|
25
|
+
help="Target snapshot ID (defaults to active live scan of current environment)",
|
|
26
|
+
),
|
|
27
|
+
path: Path = typer.Option(None, "--path", "-p", help="Target project root directory"),
|
|
28
|
+
json_output: bool = typer.Option(False, "--json", help="Output result as JSON"),
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Compare environment state between snapshots or against current live environment."""
|
|
31
|
+
root = find_project_root(path)
|
|
32
|
+
mgr = SnapshotManager(root)
|
|
33
|
+
|
|
34
|
+
# 1. Load baseline state
|
|
35
|
+
base_state: RunmarkState
|
|
36
|
+
if from_snapshot:
|
|
37
|
+
try:
|
|
38
|
+
base_state = mgr.get_snapshot(from_snapshot)
|
|
39
|
+
except FileNotFoundError:
|
|
40
|
+
term.print_error(f"Snapshot '{from_snapshot}' not found.")
|
|
41
|
+
raise typer.Exit(code=2) from None
|
|
42
|
+
else:
|
|
43
|
+
loaded_base = mgr.get_current()
|
|
44
|
+
if not loaded_base:
|
|
45
|
+
term.print_error(
|
|
46
|
+
"No baseline snapshot found. Run 'runmark snapshot' first to create a baseline."
|
|
47
|
+
)
|
|
48
|
+
raise typer.Exit(code=2)
|
|
49
|
+
base_state = loaded_base
|
|
50
|
+
|
|
51
|
+
# 2. Load target state
|
|
52
|
+
target_state: RunmarkState
|
|
53
|
+
if to_snapshot:
|
|
54
|
+
try:
|
|
55
|
+
target_state = mgr.get_snapshot(to_snapshot)
|
|
56
|
+
except FileNotFoundError:
|
|
57
|
+
term.print_error(f"Snapshot '{to_snapshot}' not found.")
|
|
58
|
+
raise typer.Exit(code=2) from None
|
|
59
|
+
else:
|
|
60
|
+
scanner = Scanner(root)
|
|
61
|
+
target_state = scanner.scan()
|
|
62
|
+
|
|
63
|
+
# 3. Compute semantic diff
|
|
64
|
+
diff = DiffEngine.compare(base_state, target_state)
|
|
65
|
+
|
|
66
|
+
if json_output:
|
|
67
|
+
term.print_json(
|
|
68
|
+
{
|
|
69
|
+
"from_id": diff.from_id,
|
|
70
|
+
"to_id": diff.to_id,
|
|
71
|
+
"from_fingerprint": diff.from_fingerprint,
|
|
72
|
+
"to_fingerprint": diff.to_fingerprint,
|
|
73
|
+
"is_identical": diff.is_identical,
|
|
74
|
+
"items": [
|
|
75
|
+
{
|
|
76
|
+
"category": item.category,
|
|
77
|
+
"item_name": item.item_name,
|
|
78
|
+
"classification": item.classification.value,
|
|
79
|
+
"severity": item.severity.value,
|
|
80
|
+
"old_value": item.old_value,
|
|
81
|
+
"new_value": item.new_value,
|
|
82
|
+
"description": item.description,
|
|
83
|
+
"is_source_revision": item.is_source_revision,
|
|
84
|
+
}
|
|
85
|
+
for item in diff.items
|
|
86
|
+
],
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
else:
|
|
90
|
+
render_diff(diff, term.console)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""CLI command: runmark doctor."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from runmark.core.diff import DiffEngine
|
|
8
|
+
from runmark.core.doctor import Doctor
|
|
9
|
+
from runmark.core.scanner import Scanner
|
|
10
|
+
from runmark.core.snapshot import SnapshotManager
|
|
11
|
+
from runmark.output.tables import render_doctor
|
|
12
|
+
from runmark.output.terminal import term
|
|
13
|
+
from runmark.storage.paths import find_project_root
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def doctor_command(
|
|
17
|
+
snapshot: str | None = typer.Option(
|
|
18
|
+
None,
|
|
19
|
+
"--snapshot",
|
|
20
|
+
"-s",
|
|
21
|
+
help="Compare current machine against a baseline snapshot",
|
|
22
|
+
),
|
|
23
|
+
path: Path = typer.Option(None, "--path", "-p", help="Target project root directory"),
|
|
24
|
+
json_output: bool = typer.Option(False, "--json", help="Output result as JSON"),
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Diagnose development environment issues, missing dependencies, or baseline drift."""
|
|
27
|
+
root = find_project_root(path)
|
|
28
|
+
scanner = Scanner(root)
|
|
29
|
+
current_state = scanner.scan()
|
|
30
|
+
|
|
31
|
+
if snapshot:
|
|
32
|
+
mgr = SnapshotManager(root)
|
|
33
|
+
try:
|
|
34
|
+
expected_state = mgr.get_snapshot(snapshot)
|
|
35
|
+
diff = DiffEngine.compare(expected_state, current_state)
|
|
36
|
+
report = Doctor.diagnose_diff(diff)
|
|
37
|
+
except FileNotFoundError:
|
|
38
|
+
term.print_error(f"Snapshot '{snapshot}' not found.")
|
|
39
|
+
raise typer.Exit(code=2) from None
|
|
40
|
+
else:
|
|
41
|
+
# Check against baseline snapshot if exists, or diagnose current scan directly
|
|
42
|
+
mgr = SnapshotManager(root)
|
|
43
|
+
baseline = mgr.get_current()
|
|
44
|
+
if baseline:
|
|
45
|
+
diff = DiffEngine.compare(baseline, current_state)
|
|
46
|
+
report = Doctor.diagnose_diff(diff)
|
|
47
|
+
# If diff is clean, also do standalone check for missing required env vars in current state
|
|
48
|
+
if report.is_healthy:
|
|
49
|
+
report = Doctor.diagnose_state(current_state)
|
|
50
|
+
else:
|
|
51
|
+
report = Doctor.diagnose_state(current_state)
|
|
52
|
+
|
|
53
|
+
if json_output:
|
|
54
|
+
term.print_json(
|
|
55
|
+
{
|
|
56
|
+
"project_name": report.project_name,
|
|
57
|
+
"fingerprint": report.fingerprint,
|
|
58
|
+
"is_healthy": report.is_healthy,
|
|
59
|
+
"issues": [
|
|
60
|
+
{
|
|
61
|
+
"code": issue.code,
|
|
62
|
+
"severity": issue.severity.value,
|
|
63
|
+
"title": issue.title,
|
|
64
|
+
"evidence": issue.evidence,
|
|
65
|
+
"explanation": issue.explanation,
|
|
66
|
+
"suggested_action": issue.suggested_action,
|
|
67
|
+
}
|
|
68
|
+
for issue in report.issues
|
|
69
|
+
],
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
render_doctor(report, term.console)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""CLI command: runmark history."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from runmark.core.snapshot import SnapshotManager
|
|
8
|
+
from runmark.output.tables import render_history
|
|
9
|
+
from runmark.output.terminal import term
|
|
10
|
+
from runmark.storage.paths import find_project_root
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def history_command(
|
|
14
|
+
path: Path = typer.Option(None, "--path", "-p", help="Target project root directory"),
|
|
15
|
+
json_output: bool = typer.Option(False, "--json", help="Output result as JSON"),
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Display chronological history of captured environment snapshots."""
|
|
18
|
+
root = find_project_root(path)
|
|
19
|
+
mgr = SnapshotManager(root)
|
|
20
|
+
snapshots = mgr.list_history()
|
|
21
|
+
|
|
22
|
+
if json_output:
|
|
23
|
+
term.print_json([s.model_dump(mode="json") for s in snapshots])
|
|
24
|
+
else:
|
|
25
|
+
render_history(snapshots, term.console)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""CLI command: runmark init."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
|
|
8
|
+
from runmark.output.terminal import term
|
|
9
|
+
from runmark.storage.filesystem import FilesystemStorage
|
|
10
|
+
from runmark.storage.paths import find_project_root
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def init_command(
|
|
14
|
+
path: Path = typer.Option(None, "--path", "-p", help="Target project root directory"),
|
|
15
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing configuration"),
|
|
16
|
+
json_output: bool = typer.Option(False, "--json", help="Output result as JSON"),
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Initialize Runmark configuration in the current project."""
|
|
19
|
+
root = find_project_root(path)
|
|
20
|
+
storage = FilesystemStorage(root)
|
|
21
|
+
|
|
22
|
+
already_initialized = storage.paths.is_initialized()
|
|
23
|
+
if already_initialized and not force:
|
|
24
|
+
if json_output:
|
|
25
|
+
term.print_json(
|
|
26
|
+
{
|
|
27
|
+
"status": "already_initialized",
|
|
28
|
+
"project_root": str(root),
|
|
29
|
+
"runmark_dir": str(storage.paths.runmark_dir),
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
else:
|
|
33
|
+
term.print(
|
|
34
|
+
Panel(
|
|
35
|
+
f"[yellow]Runmark is already initialized in:[/yellow] [bold]{root}[/bold]\n"
|
|
36
|
+
f"Configuration: [dim]{storage.paths.config_file}[/dim]\n"
|
|
37
|
+
f"Use [cyan]--force[/cyan] to re-initialize.",
|
|
38
|
+
border_style="yellow",
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
storage.initialize(force=force)
|
|
44
|
+
|
|
45
|
+
if json_output:
|
|
46
|
+
term.print_json(
|
|
47
|
+
{
|
|
48
|
+
"status": "initialized",
|
|
49
|
+
"project_root": str(root),
|
|
50
|
+
"runmark_dir": str(storage.paths.runmark_dir),
|
|
51
|
+
"config_file": str(storage.paths.config_file),
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
else:
|
|
55
|
+
term.print(
|
|
56
|
+
Panel(
|
|
57
|
+
f"[bold green]✓ Initialized Runmark in:[/bold green] [bold]{root}[/bold]\n\n"
|
|
58
|
+
f"• Created configuration: [dim]{storage.paths.config_file}[/dim]\n"
|
|
59
|
+
f"• Created snapshots dir: [dim]{storage.paths.snapshots_dir}[/dim]\n\n"
|
|
60
|
+
f"Next steps:\n"
|
|
61
|
+
f" 1. Run [bold cyan]runmark scan[/bold cyan] to inspect your environment.\n"
|
|
62
|
+
f" 2. Run [bold cyan]runmark snapshot[/bold cyan] to create your first baseline.",
|
|
63
|
+
border_style="green",
|
|
64
|
+
)
|
|
65
|
+
)
|