epi-recorder 2.1.0__py3-none-any.whl → 2.1.2__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.
- epi_cli/__main__.py +6 -1
- epi_cli/main.py +46 -10
- epi_core/__init__.py +1 -1
- epi_core/container.py +10 -2
- epi_core/schemas.py +6 -1
- epi_core/serialize.py +38 -9
- epi_core/trust.py +10 -0
- epi_postinstall.py +197 -0
- epi_recorder/__init__.py +1 -1
- epi_recorder-2.1.2.dist-info/METADATA +574 -0
- {epi_recorder-2.1.0.dist-info → epi_recorder-2.1.2.dist-info}/RECORD +17 -15
- {epi_recorder-2.1.0.dist-info → epi_recorder-2.1.2.dist-info}/top_level.txt +1 -0
- epi_viewer_static/app.js +54 -20
- epi_viewer_static/crypto.js +517 -0
- epi_recorder-2.1.0.dist-info/METADATA +0 -159
- {epi_recorder-2.1.0.dist-info → epi_recorder-2.1.2.dist-info}/WHEEL +0 -0
- {epi_recorder-2.1.0.dist-info → epi_recorder-2.1.2.dist-info}/entry_points.txt +0 -0
- {epi_recorder-2.1.0.dist-info → epi_recorder-2.1.2.dist-info}/licenses/LICENSE +0 -0
epi_cli/__main__.py
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
"""
|
|
2
|
-
EPI CLI entry point for python -m epi_cli
|
|
2
|
+
EPI CLI - Main entry point for python -m epi_cli
|
|
3
|
+
|
|
4
|
+
This allows users to run the CLI even if 'epi' is not in PATH:
|
|
5
|
+
python -m epi_cli run script.py
|
|
6
|
+
python -m epi_cli view recording.epi
|
|
7
|
+
etc.
|
|
3
8
|
"""
|
|
4
9
|
from epi_cli.main import cli_main
|
|
5
10
|
|
epi_cli/main.py
CHANGED
|
@@ -231,43 +231,79 @@ def doctor():
|
|
|
231
231
|
"""
|
|
232
232
|
[Doctor] Self-healing doctor. Fixes common issues silently.
|
|
233
233
|
"""
|
|
234
|
-
console.print("\n[bold blue]EPI Doctor[/bold blue]\n")
|
|
234
|
+
console.print("\n[bold blue]EPI Doctor - System Health Check[/bold blue]\n")
|
|
235
235
|
|
|
236
236
|
issues = 0
|
|
237
|
+
fixed = 0
|
|
237
238
|
|
|
238
239
|
# Check 1: Keys
|
|
239
240
|
console.print("1. Security Keys: ", end="")
|
|
240
241
|
from epi_cli.keys import generate_default_keypair_if_missing
|
|
241
242
|
if generate_default_keypair_if_missing(console_output=False):
|
|
242
|
-
console.print("[green]FIXED (Generated)[/green]")
|
|
243
|
-
|
|
243
|
+
console.print("[green][OK] FIXED (Generated)[/green]")
|
|
244
|
+
fixed += 1
|
|
244
245
|
else:
|
|
245
|
-
console.print("[green]OK[/green]")
|
|
246
|
+
console.print("[green][OK][/green]")
|
|
246
247
|
|
|
247
248
|
# Check 2: Command on PATH
|
|
248
249
|
console.print("2. 'epi' command: ", end="")
|
|
249
250
|
import shutil
|
|
250
251
|
if shutil.which("epi"):
|
|
251
|
-
console.print("[green]OK[/green]")
|
|
252
|
+
console.print("[green][OK][/green]")
|
|
252
253
|
else:
|
|
253
|
-
console.print("[red]
|
|
254
|
-
console.print(" [yellow]Run: python epi_setup.py[/yellow]")
|
|
254
|
+
console.print("[red][X] NOT IN PATH[/red]")
|
|
255
255
|
issues += 1
|
|
256
|
+
|
|
257
|
+
# Try to auto-fix on Windows
|
|
258
|
+
import platform
|
|
259
|
+
if platform.system() == "Windows":
|
|
260
|
+
console.print(" [cyan]→ Attempting automatic PATH fix...[/cyan]")
|
|
261
|
+
try:
|
|
262
|
+
import epi_postinstall
|
|
263
|
+
from pathlib import Path
|
|
264
|
+
|
|
265
|
+
scripts_dir = epi_postinstall.get_scripts_dir()
|
|
266
|
+
if scripts_dir and scripts_dir.exists():
|
|
267
|
+
console.print(f" [dim]Scripts directory: {scripts_dir}[/dim]")
|
|
268
|
+
|
|
269
|
+
if epi_postinstall.add_to_user_path_windows(scripts_dir):
|
|
270
|
+
console.print(" [green][OK] PATH updated successfully![/green]")
|
|
271
|
+
console.print(" [yellow][!] Please restart your terminal for changes to take effect[/yellow]")
|
|
272
|
+
fixed += 1
|
|
273
|
+
else:
|
|
274
|
+
console.print(" [yellow][!] Could not update PATH automatically[/yellow]")
|
|
275
|
+
console.print(" [dim]Manual fix: Use 'python -m epi_cli' instead[/dim]")
|
|
276
|
+
else:
|
|
277
|
+
console.print(" [red][X] Could not locate Scripts directory[/red]")
|
|
278
|
+
except Exception as e:
|
|
279
|
+
console.print(f" [red][X] Auto-fix failed: {e}[/red]")
|
|
280
|
+
console.print(" [dim]Workaround: Use 'python -m epi_cli' instead[/dim]")
|
|
281
|
+
else:
|
|
282
|
+
console.print(" [dim]Workaround: Use 'python -m epi_cli' instead[/dim]")
|
|
256
283
|
|
|
257
284
|
# Check 3: Browser
|
|
258
285
|
console.print("3. Browser Check: ", end="")
|
|
259
286
|
try:
|
|
260
287
|
import webbrowser
|
|
261
288
|
webbrowser.get()
|
|
262
|
-
console.print("[green]OK[/green]")
|
|
289
|
+
console.print("[green][OK][/green]")
|
|
263
290
|
except:
|
|
264
|
-
console.print("[yellow]WARNING (Headless?)[/yellow]")
|
|
291
|
+
console.print("[yellow][!] WARNING (Headless?)[/yellow]")
|
|
265
292
|
|
|
293
|
+
# Summary
|
|
266
294
|
print()
|
|
295
|
+
console.print("[bold]" + "="*70 + "[/bold]")
|
|
267
296
|
if issues == 0:
|
|
268
297
|
console.print("[bold green][OK] System Healthy![/bold green]")
|
|
269
298
|
else:
|
|
270
|
-
|
|
299
|
+
if fixed > 0:
|
|
300
|
+
console.print(f"[bold yellow][!] Fixed {fixed}/{issues} issues[/bold yellow]")
|
|
301
|
+
if fixed < issues:
|
|
302
|
+
console.print("[dim]Some issues require manual attention (see above)[/dim]")
|
|
303
|
+
else:
|
|
304
|
+
console.print(f"[bold yellow][!] Found {issues} issues[/bold yellow]")
|
|
305
|
+
console.print("[dim]See suggestions above[/dim]")
|
|
306
|
+
console.print("[bold]" + "="*70 + "[/bold]\n")
|
|
271
307
|
|
|
272
308
|
|
|
273
309
|
# Entry point for CLI
|
epi_core/__init__.py
CHANGED
epi_core/container.py
CHANGED
|
@@ -80,6 +80,8 @@ class EPIContainer:
|
|
|
80
80
|
# Read template and assets
|
|
81
81
|
template_html = template_path.read_text(encoding="utf-8")
|
|
82
82
|
app_js = app_js_path.read_text(encoding="utf-8") if app_js_path.exists() else ""
|
|
83
|
+
crypto_js_path = viewer_static_dir / "crypto.js"
|
|
84
|
+
crypto_js = crypto_js_path.read_text(encoding="utf-8") if crypto_js_path.exists() else ""
|
|
83
85
|
css_styles = css_path.read_text(encoding="utf-8") if css_path.exists() else ""
|
|
84
86
|
|
|
85
87
|
# Read steps from steps.jsonl
|
|
@@ -112,10 +114,16 @@ class EPIContainer:
|
|
|
112
114
|
f'<style>{css_styles}</style>'
|
|
113
115
|
)
|
|
114
116
|
|
|
115
|
-
# Inline app.js
|
|
117
|
+
# Inline crypto.js and app.js
|
|
118
|
+
js_content = ""
|
|
119
|
+
if crypto_js:
|
|
120
|
+
js_content += f"<script>{crypto_js}</script>\n"
|
|
121
|
+
if app_js:
|
|
122
|
+
js_content += f"<script>{app_js}</script>"
|
|
123
|
+
|
|
116
124
|
html_with_js = html_with_css.replace(
|
|
117
125
|
'<script src="app.js"></script>',
|
|
118
|
-
|
|
126
|
+
js_content
|
|
119
127
|
)
|
|
120
128
|
|
|
121
129
|
return html_with_js
|
epi_core/schemas.py
CHANGED
|
@@ -18,7 +18,7 @@ class ManifestModel(BaseModel):
|
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
spec_version: str = Field(
|
|
21
|
-
default="1.
|
|
21
|
+
default="1.1-json",
|
|
22
22
|
description="EPI specification version"
|
|
23
23
|
)
|
|
24
24
|
|
|
@@ -47,6 +47,11 @@ class ManifestModel(BaseModel):
|
|
|
47
47
|
description="Mapping of file paths to their SHA-256 hashes for integrity verification"
|
|
48
48
|
)
|
|
49
49
|
|
|
50
|
+
public_key: Optional[str] = Field(
|
|
51
|
+
default=None,
|
|
52
|
+
description="Hex-encoded public key used for verification"
|
|
53
|
+
)
|
|
54
|
+
|
|
50
55
|
signature: Optional[str] = Field(
|
|
51
56
|
default=None,
|
|
52
57
|
description="Ed25519 signature of the canonical CBOR hash of this manifest (excluding signature field)"
|
epi_core/serialize.py
CHANGED
|
@@ -87,27 +87,56 @@ def get_canonical_hash(model: BaseModel, exclude_fields: set[str] | None = None)
|
|
|
87
87
|
else:
|
|
88
88
|
return value
|
|
89
89
|
|
|
90
|
+
# Normalize datetime and UUID fields to strings
|
|
90
91
|
model_dict = normalize_value(model_dict)
|
|
91
92
|
|
|
92
93
|
if exclude_fields:
|
|
93
94
|
for field in exclude_fields:
|
|
94
95
|
model_dict.pop(field, None)
|
|
95
|
-
|
|
96
|
+
|
|
97
|
+
# JSON Canonicalization for Spec v1.1+
|
|
98
|
+
# Check if model has spec_version and if it indicates JSON usage
|
|
99
|
+
# We default to CBOR for backward compatibility
|
|
100
|
+
|
|
101
|
+
use_json = False
|
|
102
|
+
|
|
103
|
+
# Check spec_version in model or dict
|
|
104
|
+
spec_version = model_dict.get("spec_version")
|
|
105
|
+
if spec_version and (spec_version.startswith("1.1") or "json" in spec_version):
|
|
106
|
+
use_json = True
|
|
107
|
+
|
|
108
|
+
if use_json:
|
|
109
|
+
return _get_json_canonical_hash(model_dict)
|
|
110
|
+
else:
|
|
111
|
+
return _get_cbor_canonical_hash(model_dict)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _get_json_canonical_hash(data: Any) -> str:
|
|
115
|
+
"""Compute canonical SHA-256 hash using JSON (RFC 8785 style)."""
|
|
116
|
+
import json
|
|
117
|
+
|
|
118
|
+
# Dump to JSON with sorted keys and no whitespace
|
|
119
|
+
json_bytes = json.dumps(
|
|
120
|
+
data,
|
|
121
|
+
sort_keys=True,
|
|
122
|
+
separators=(',', ':'),
|
|
123
|
+
ensure_ascii=False
|
|
124
|
+
).encode("utf-8")
|
|
125
|
+
|
|
126
|
+
return hashlib.sha256(json_bytes).hexdigest()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _get_cbor_canonical_hash(data: Any) -> str:
|
|
130
|
+
"""Compute canonical SHA-256 hash using CBOR (Legacy v1.0)."""
|
|
96
131
|
# Encode to canonical CBOR
|
|
97
|
-
# canonical=True ensures:
|
|
98
|
-
# - Keys are sorted lexicographically
|
|
99
|
-
# - Minimal encoding is used
|
|
100
|
-
# - Deterministic representation
|
|
101
132
|
cbor_bytes = cbor2.dumps(
|
|
102
|
-
|
|
133
|
+
data,
|
|
103
134
|
canonical=True,
|
|
104
135
|
default=_cbor_default_encoder
|
|
105
136
|
)
|
|
106
137
|
|
|
107
138
|
# Compute SHA-256 hash
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return hash_obj.hexdigest()
|
|
139
|
+
return hashlib.sha256(cbor_bytes).hexdigest()
|
|
111
140
|
|
|
112
141
|
|
|
113
142
|
def verify_hash(model: BaseModel, expected_hash: str, exclude_fields: set[str] | None = None) -> bool:
|
epi_core/trust.py
CHANGED
|
@@ -53,6 +53,16 @@ def sign_manifest(
|
|
|
53
53
|
SigningError: If signing fails
|
|
54
54
|
"""
|
|
55
55
|
try:
|
|
56
|
+
# Derive public key and add to manifest
|
|
57
|
+
public_key_obj = private_key.public_key()
|
|
58
|
+
public_key_hex = public_key_obj.public_bytes(
|
|
59
|
+
encoding=serialization.Encoding.Raw,
|
|
60
|
+
format=serialization.PublicFormat.Raw
|
|
61
|
+
).hex()
|
|
62
|
+
|
|
63
|
+
# We must update the manifest BEFORE hashing so the public key is signed
|
|
64
|
+
manifest.public_key = public_key_hex
|
|
65
|
+
|
|
56
66
|
# Compute canonical hash (excluding signature field)
|
|
57
67
|
manifest_hash = get_canonical_hash(manifest, exclude_fields={"signature"})
|
|
58
68
|
hash_bytes = bytes.fromhex(manifest_hash)
|
epi_postinstall.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Post-installation script for EPI Recorder
|
|
3
|
+
Automatically fixes PATH issues on Windows for better UX
|
|
4
|
+
"""
|
|
5
|
+
import sys
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_scripts_dir():
|
|
13
|
+
"""Get the Scripts directory where pip installs executables"""
|
|
14
|
+
if platform.system() == "Windows":
|
|
15
|
+
# Get the site-packages directory
|
|
16
|
+
import site
|
|
17
|
+
user_site = site.getusersitepackages()
|
|
18
|
+
if user_site:
|
|
19
|
+
# Scripts is typically ../Scripts relative to site-packages
|
|
20
|
+
scripts_dir = Path(user_site).parent / "Scripts"
|
|
21
|
+
if scripts_dir.exists():
|
|
22
|
+
return scripts_dir
|
|
23
|
+
|
|
24
|
+
# Fallback: try to find it from pip
|
|
25
|
+
try:
|
|
26
|
+
result = subprocess.run(
|
|
27
|
+
[sys.executable, "-m", "pip", "show", "epi-recorder"],
|
|
28
|
+
capture_output=True,
|
|
29
|
+
text=True
|
|
30
|
+
)
|
|
31
|
+
if result.returncode == 0:
|
|
32
|
+
for line in result.stdout.split('\n'):
|
|
33
|
+
if line.startswith('Location:'):
|
|
34
|
+
location = line.split(':', 1)[1].strip()
|
|
35
|
+
scripts_dir = Path(location).parent / "Scripts"
|
|
36
|
+
if scripts_dir.exists():
|
|
37
|
+
return scripts_dir
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_in_path(directory):
|
|
45
|
+
"""Check if directory is in PATH"""
|
|
46
|
+
path_env = os.environ.get('PATH', '')
|
|
47
|
+
path_dirs = path_env.split(os.pathsep)
|
|
48
|
+
dir_str = str(directory)
|
|
49
|
+
return any(os.path.normcase(p) == os.path.normcase(dir_str) for p in path_dirs)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def add_to_user_path_windows(directory):
|
|
53
|
+
"""Add directory to user PATH on Windows"""
|
|
54
|
+
try:
|
|
55
|
+
import winreg
|
|
56
|
+
|
|
57
|
+
# Open the user environment variables key
|
|
58
|
+
key = winreg.OpenKey(
|
|
59
|
+
winreg.HKEY_CURRENT_USER,
|
|
60
|
+
'Environment',
|
|
61
|
+
0,
|
|
62
|
+
winreg.KEY_READ | winreg.KEY_WRITE
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
# Get current PATH
|
|
67
|
+
current_path, _ = winreg.QueryValueEx(key, 'Path')
|
|
68
|
+
except WindowsError:
|
|
69
|
+
current_path = ''
|
|
70
|
+
|
|
71
|
+
# Add our directory if not already there
|
|
72
|
+
path_parts = current_path.split(os.pathsep)
|
|
73
|
+
dir_str = str(directory)
|
|
74
|
+
|
|
75
|
+
if not any(os.path.normcase(p) == os.path.normcase(dir_str) for p in path_parts):
|
|
76
|
+
new_path = current_path + os.pathsep + dir_str
|
|
77
|
+
winreg.SetValueEx(key, 'Path', 0, winreg.REG_EXPAND_SZ, new_path)
|
|
78
|
+
winreg.CloseKey(key)
|
|
79
|
+
|
|
80
|
+
# Broadcast WM_SETTINGCHANGE to notify the system
|
|
81
|
+
try:
|
|
82
|
+
import ctypes
|
|
83
|
+
HWND_BROADCAST = 0xFFFF
|
|
84
|
+
WM_SETTINGCHANGE = 0x1A
|
|
85
|
+
ctypes.windll.user32.SendMessageW(
|
|
86
|
+
HWND_BROADCAST, WM_SETTINGCHANGE, 0, 'Environment'
|
|
87
|
+
)
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
winreg.CloseKey(key)
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
print(f"Warning: Could not modify PATH: {e}")
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def check_epi_command():
|
|
102
|
+
"""Check if 'epi' command is accessible"""
|
|
103
|
+
try:
|
|
104
|
+
result = subprocess.run(
|
|
105
|
+
['epi', '--version'],
|
|
106
|
+
capture_output=True,
|
|
107
|
+
timeout=2
|
|
108
|
+
)
|
|
109
|
+
return result.returncode == 0
|
|
110
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def post_install():
|
|
115
|
+
"""Main post-install function"""
|
|
116
|
+
print("\n" + "="*70)
|
|
117
|
+
print("🎉 EPI Recorder Installation Complete!")
|
|
118
|
+
print("="*70)
|
|
119
|
+
|
|
120
|
+
# Check if epi command works
|
|
121
|
+
if check_epi_command():
|
|
122
|
+
print("\n✅ The 'epi' command is ready to use!")
|
|
123
|
+
print("\nTry it now:")
|
|
124
|
+
print(" epi --help")
|
|
125
|
+
print(" epi init")
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
# If not, try to fix it (Windows only for now)
|
|
129
|
+
if platform.system() == "Windows":
|
|
130
|
+
print("\n⚠️ 'epi' command not found in PATH")
|
|
131
|
+
print("🔧 Attempting automatic fix...")
|
|
132
|
+
|
|
133
|
+
scripts_dir = get_scripts_dir()
|
|
134
|
+
|
|
135
|
+
if scripts_dir and scripts_dir.exists():
|
|
136
|
+
print(f"📁 Found Scripts directory: {scripts_dir}")
|
|
137
|
+
|
|
138
|
+
if not is_in_path(scripts_dir):
|
|
139
|
+
print("➕ Adding to your user PATH...")
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
if add_to_user_path_windows(scripts_dir):
|
|
143
|
+
print("\n✅ SUCCESS! PATH updated.")
|
|
144
|
+
print("\n⚠️ IMPORTANT: You must restart your terminal for changes to take effect!")
|
|
145
|
+
print("\nAfter restarting your terminal, try:")
|
|
146
|
+
print(" epi --help")
|
|
147
|
+
print(" epi init")
|
|
148
|
+
else:
|
|
149
|
+
print("\n⚠️ Scripts directory already in PATH, but 'epi' not found.")
|
|
150
|
+
print("This might require a terminal restart.")
|
|
151
|
+
print("\nIf 'epi' still doesn't work after restarting, use:")
|
|
152
|
+
print(f" python -m epi_cli")
|
|
153
|
+
except Exception as e:
|
|
154
|
+
print(f"\n❌ Automatic fix failed: {e}")
|
|
155
|
+
show_manual_instructions(scripts_dir)
|
|
156
|
+
else:
|
|
157
|
+
print("✅ Scripts directory is in PATH")
|
|
158
|
+
print("⚠️ You may need to restart your terminal for the command to work.")
|
|
159
|
+
else:
|
|
160
|
+
print("❌ Could not locate Scripts directory")
|
|
161
|
+
show_fallback_instructions()
|
|
162
|
+
else:
|
|
163
|
+
# Linux/Mac
|
|
164
|
+
print("\n⚠️ 'epi' command not found in PATH")
|
|
165
|
+
print("\nIf 'epi' doesn't work, use:")
|
|
166
|
+
print(" python -m epi_cli")
|
|
167
|
+
print("\nOr add pip's user base bin directory to PATH:")
|
|
168
|
+
print(" export PATH=$PATH:$(python -m site --user-base)/bin")
|
|
169
|
+
|
|
170
|
+
print("\n" + "="*70 + "\n")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def show_manual_instructions(scripts_dir):
|
|
174
|
+
"""Show manual PATH update instructions"""
|
|
175
|
+
print("\n📖 MANUAL FIX REQUIRED:")
|
|
176
|
+
print("\nOption 1: Update PATH (Permanent)")
|
|
177
|
+
print(" 1. Press Win + R, type: sysdm.cpl")
|
|
178
|
+
print(" 2. Advanced → Environment Variables")
|
|
179
|
+
print(" 3. Under 'User variables', select 'Path' → Edit")
|
|
180
|
+
print(" 4. Click 'New' and add:")
|
|
181
|
+
print(f" {scripts_dir}")
|
|
182
|
+
print(" 5. Click OK, restart your terminal")
|
|
183
|
+
print("\nOption 2: Use python -m (Always works)")
|
|
184
|
+
print(" python -m epi_cli run script.py")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def show_fallback_instructions():
|
|
188
|
+
"""Show fallback instructions if automatic fix fails"""
|
|
189
|
+
print("\n📖 WORKAROUND:")
|
|
190
|
+
print("\nUse 'python -m epi_cli' instead of 'epi':")
|
|
191
|
+
print(" python -m epi_cli --help")
|
|
192
|
+
print(" python -m epi_cli run script.py")
|
|
193
|
+
print(" python -m epi_cli view recording.epi")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
if __name__ == "__main__":
|
|
197
|
+
post_install()
|
epi_recorder/__init__.py
CHANGED