railtracks-cli 1.1.22__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.
- railtracks_cli/__init__.py +473 -0
- railtracks_cli/__main__.py +6 -0
- railtracks_cli-1.1.22.dist-info/METADATA +97 -0
- railtracks_cli-1.1.22.dist-info/RECORD +7 -0
- railtracks_cli-1.1.22.dist-info/WHEEL +4 -0
- railtracks_cli-1.1.22.dist-info/entry_points.txt +3 -0
- railtracks_cli-1.1.22.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
railtracks - A Python development server with JSON API
|
|
5
|
+
Usage: railtracks [command]
|
|
6
|
+
|
|
7
|
+
Commands:
|
|
8
|
+
init Initialize railtracks environment (setup directories, download UI)
|
|
9
|
+
viz Start the railtracks development server
|
|
10
|
+
migrate Verify and migrate the structure of .railtracks/ directory
|
|
11
|
+
|
|
12
|
+
- Checks to see if there is a .railtracks directory
|
|
13
|
+
- If not, it creates one (and adds it to the .gitignore)
|
|
14
|
+
- If there is a build directory, it runs the build command
|
|
15
|
+
- If there is a .railtracks directory, it starts the server
|
|
16
|
+
|
|
17
|
+
For testing purposes, you can add `alias railtracks="python railtracks.py"` to your .bashrc or .zshrc
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import shutil
|
|
23
|
+
import socket
|
|
24
|
+
import sys
|
|
25
|
+
import tempfile
|
|
26
|
+
import threading
|
|
27
|
+
import time
|
|
28
|
+
import urllib.request
|
|
29
|
+
import webbrowser
|
|
30
|
+
import zipfile
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from urllib.parse import unquote
|
|
33
|
+
|
|
34
|
+
import uvicorn
|
|
35
|
+
from fastapi import FastAPI
|
|
36
|
+
from fastapi.responses import FileResponse, JSONResponse
|
|
37
|
+
|
|
38
|
+
__version__ = "1.1.22"
|
|
39
|
+
|
|
40
|
+
# TODO: Once we are releasing to PyPi change this to the release asset instead
|
|
41
|
+
latest_ui_url = "https://railtownazureb2c.blob.core.windows.net/cdn/rc-viz/latest.zip"
|
|
42
|
+
|
|
43
|
+
cli_name = "railtracks"
|
|
44
|
+
cli_directory = ".railtracks"
|
|
45
|
+
DEFAULT_PORT = 3030
|
|
46
|
+
|
|
47
|
+
# FastAPI app instance
|
|
48
|
+
app = FastAPI()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_script_directory():
|
|
52
|
+
"""Get the directory where this script is located"""
|
|
53
|
+
return Path(__file__).parent.absolute()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def print_status(message):
|
|
57
|
+
print(f"[{cli_name}] {message}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def print_success(message):
|
|
61
|
+
print(f"[{cli_name}] {message}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def print_warning(message):
|
|
65
|
+
print(f"[{cli_name}] {message}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def print_error(message):
|
|
69
|
+
print(f"[{cli_name}] {message}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_port_in_use(port):
|
|
73
|
+
"""Check if a port is already in use"""
|
|
74
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
75
|
+
try:
|
|
76
|
+
sock.bind(("localhost", port))
|
|
77
|
+
return False # Port is available
|
|
78
|
+
except OSError:
|
|
79
|
+
return True # Port is in use
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def create_railtracks_dir():
|
|
83
|
+
"""Create .railtracks directory if it doesn't exist and add to .gitignore"""
|
|
84
|
+
railtracks_dir = Path(cli_directory)
|
|
85
|
+
if not railtracks_dir.exists():
|
|
86
|
+
print_status(f"Creating {cli_directory} directory...")
|
|
87
|
+
railtracks_dir.mkdir(exist_ok=True)
|
|
88
|
+
print_success(f"Created {cli_directory} directory")
|
|
89
|
+
|
|
90
|
+
# Check if cli_directory is in .gitignore
|
|
91
|
+
gitignore_path = Path(".gitignore")
|
|
92
|
+
if gitignore_path.exists():
|
|
93
|
+
with open(gitignore_path) as f:
|
|
94
|
+
gitignore_content = f.read()
|
|
95
|
+
|
|
96
|
+
if cli_directory not in gitignore_content:
|
|
97
|
+
print_status(f"Adding {cli_directory} to .gitignore...")
|
|
98
|
+
with open(gitignore_path, "a") as f:
|
|
99
|
+
f.write(f"\n{cli_directory}\n")
|
|
100
|
+
print_success(f"Added {cli_directory} to .gitignore")
|
|
101
|
+
else:
|
|
102
|
+
print_status("Creating .gitignore file...")
|
|
103
|
+
with open(gitignore_path, "w") as f:
|
|
104
|
+
f.write(f"{cli_directory}\n")
|
|
105
|
+
print_success(f"Created .gitignore with {cli_directory}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def download_and_extract_ui():
|
|
109
|
+
"""Download the latest frontend UI and extract it to .railtracks/ui"""
|
|
110
|
+
ui_url = latest_ui_url
|
|
111
|
+
ui_dir = Path(f"{cli_directory}/ui")
|
|
112
|
+
|
|
113
|
+
print_status("Downloading latest frontend UI...")
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
# Create temporary file for download
|
|
117
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as temp_file:
|
|
118
|
+
temp_zip_path = temp_file.name
|
|
119
|
+
|
|
120
|
+
# Download the zip file
|
|
121
|
+
print_status(f"Downloading from: {ui_url}")
|
|
122
|
+
urllib.request.urlretrieve(ui_url, temp_zip_path)
|
|
123
|
+
|
|
124
|
+
# Create ui directory if it doesn't exist
|
|
125
|
+
ui_dir.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
|
|
127
|
+
# Extract the zip file
|
|
128
|
+
print_status("Extracting UI files...")
|
|
129
|
+
with zipfile.ZipFile(temp_zip_path, "r") as zip_ref:
|
|
130
|
+
zip_ref.extractall(ui_dir)
|
|
131
|
+
|
|
132
|
+
# Clean up temporary file
|
|
133
|
+
os.unlink(temp_zip_path)
|
|
134
|
+
|
|
135
|
+
print_success("Frontend UI downloaded and extracted successfully")
|
|
136
|
+
print_status(f"UI files available in: {ui_dir}")
|
|
137
|
+
|
|
138
|
+
except urllib.error.URLError as e:
|
|
139
|
+
print_error(f"Failed to download UI: {e}")
|
|
140
|
+
print_error("Please check your internet connection and try again")
|
|
141
|
+
sys.exit(1)
|
|
142
|
+
except zipfile.BadZipFile as e:
|
|
143
|
+
print_error(f"Failed to extract UI zip file: {e}")
|
|
144
|
+
print_error("The downloaded file may be corrupted")
|
|
145
|
+
sys.exit(1)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
print_error(f"Unexpected error during UI download/extraction: {e}")
|
|
148
|
+
sys.exit(1)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def init_railtracks():
|
|
152
|
+
"""Initialize the railtracks environment"""
|
|
153
|
+
print_status("Initializing railtracks environment...")
|
|
154
|
+
|
|
155
|
+
# Setup directories
|
|
156
|
+
create_railtracks_dir()
|
|
157
|
+
|
|
158
|
+
# Download and extract UI
|
|
159
|
+
download_and_extract_ui()
|
|
160
|
+
|
|
161
|
+
print_success("railtracks initialization completed!")
|
|
162
|
+
print_status("You can now run 'railtracks viz' to start the server")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def migrate_railtracks():
|
|
166
|
+
"""Migrate and verify the structure of .railtracks directory"""
|
|
167
|
+
print_status("Verifying .railtracks directory structure...")
|
|
168
|
+
|
|
169
|
+
# Get the .railtracks directory path
|
|
170
|
+
railtracks_dir = Path(cli_directory)
|
|
171
|
+
|
|
172
|
+
# Verify/create .railtracks directory
|
|
173
|
+
if not railtracks_dir.exists():
|
|
174
|
+
print_status(f"Creating {cli_directory} directory...")
|
|
175
|
+
railtracks_dir.mkdir(exist_ok=True)
|
|
176
|
+
print_success(f"Created {cli_directory} directory")
|
|
177
|
+
|
|
178
|
+
# Verify/create .railtracks/data directory
|
|
179
|
+
data_dir = railtracks_dir / "data"
|
|
180
|
+
if not data_dir.exists():
|
|
181
|
+
print_status("Creating .railtracks/data directory...")
|
|
182
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
183
|
+
print_success("Created .railtracks/data directory")
|
|
184
|
+
|
|
185
|
+
# Verify/create .railtracks/data/evaluations directory
|
|
186
|
+
evaluations_dir = data_dir / "evaluations"
|
|
187
|
+
if not evaluations_dir.exists():
|
|
188
|
+
print_status("Creating .railtracks/data/evaluations directory...")
|
|
189
|
+
evaluations_dir.mkdir(parents=True, exist_ok=True)
|
|
190
|
+
print_success("Created .railtracks/data/evaluations directory")
|
|
191
|
+
|
|
192
|
+
# Verify/create .railtracks/data/sessions directory
|
|
193
|
+
sessions_dir = data_dir / "sessions"
|
|
194
|
+
if not sessions_dir.exists():
|
|
195
|
+
print_status("Creating .railtracks/data/sessions directory...")
|
|
196
|
+
sessions_dir.mkdir(parents=True, exist_ok=True)
|
|
197
|
+
print_success("Created .railtracks/data/sessions directory")
|
|
198
|
+
|
|
199
|
+
# Find all JSON files in .railtracks root only (not recursive, not in subdirectories)
|
|
200
|
+
json_files = list(railtracks_dir.glob("*.json"))
|
|
201
|
+
|
|
202
|
+
if json_files:
|
|
203
|
+
print_status(
|
|
204
|
+
f"Found {len(json_files)} JSON file(s) in .railtracks root to migrate..."
|
|
205
|
+
)
|
|
206
|
+
for json_file in json_files:
|
|
207
|
+
destination = sessions_dir / json_file.name
|
|
208
|
+
shutil.move(str(json_file), str(destination))
|
|
209
|
+
print_success(f"Migrated {json_file.name} to .railtracks/data/sessions/")
|
|
210
|
+
print_success(
|
|
211
|
+
f"Migration completed: {len(json_files)} file(s) moved to .railtracks/data/sessions/"
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
print_status("No JSON files found in .railtracks root to migrate")
|
|
215
|
+
|
|
216
|
+
print_success("Directory structure verification and migration completed!")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# FastAPI endpoints
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def get_railtracks_dir():
|
|
223
|
+
"""Get the .railtracks directory path"""
|
|
224
|
+
return Path(cli_directory)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def get_data_dir(subdir):
|
|
228
|
+
"""Get a data subdirectory path (e.g., evaluations, sessions)"""
|
|
229
|
+
return get_railtracks_dir() / "data" / subdir
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@app.get("/api/evaluations")
|
|
233
|
+
async def get_evaluations():
|
|
234
|
+
"""Get all evaluation JSON files from .railtracks/data/evaluations/"""
|
|
235
|
+
evaluations_dir = get_data_dir("evaluations")
|
|
236
|
+
evaluations = []
|
|
237
|
+
|
|
238
|
+
if evaluations_dir.exists():
|
|
239
|
+
for file_path in evaluations_dir.glob("*.json"):
|
|
240
|
+
try:
|
|
241
|
+
with open(file_path, encoding="utf-8") as f:
|
|
242
|
+
content = json.load(f)
|
|
243
|
+
evaluations.append(content)
|
|
244
|
+
except (json.JSONDecodeError, IOError) as e:
|
|
245
|
+
print_error(f"Error reading evaluation file {file_path.name}: {e}")
|
|
246
|
+
|
|
247
|
+
return JSONResponse(content=evaluations)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@app.get("/api/sessions")
|
|
251
|
+
async def get_sessions():
|
|
252
|
+
"""Get all session JSON files from .railtracks/data/sessions/"""
|
|
253
|
+
sessions_dir = get_data_dir("sessions")
|
|
254
|
+
sessions = []
|
|
255
|
+
|
|
256
|
+
if sessions_dir.exists():
|
|
257
|
+
for file_path in sessions_dir.glob("*.json"):
|
|
258
|
+
try:
|
|
259
|
+
with open(file_path, encoding="utf-8") as f:
|
|
260
|
+
content = json.load(f)
|
|
261
|
+
sessions.append(content)
|
|
262
|
+
except (json.JSONDecodeError, IOError) as e:
|
|
263
|
+
print_error(f"Error reading session file {file_path.name}: {e}")
|
|
264
|
+
|
|
265
|
+
return JSONResponse(content=sessions)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@app.get("/api/files")
|
|
269
|
+
async def get_files():
|
|
270
|
+
"""
|
|
271
|
+
DEPRECATED: This endpoint is deprecated and kept for old visualizer compatibility.
|
|
272
|
+
List JSON files in .railtracks directory
|
|
273
|
+
"""
|
|
274
|
+
railtracks_dir = get_railtracks_dir()
|
|
275
|
+
json_files = []
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
if railtracks_dir.exists():
|
|
279
|
+
for file_path in railtracks_dir.glob("*.json"):
|
|
280
|
+
json_files.append(
|
|
281
|
+
{
|
|
282
|
+
"name": file_path.name,
|
|
283
|
+
"size": file_path.stat().st_size,
|
|
284
|
+
"modified": file_path.stat().st_mtime,
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
response = JSONResponse(content=json_files)
|
|
289
|
+
response.headers["Deprecated"] = "true"
|
|
290
|
+
return response
|
|
291
|
+
except Exception as e:
|
|
292
|
+
print_error(f"Error handling /api/files: {e}")
|
|
293
|
+
return JSONResponse(content={"error": "Internal Server Error"}, status_code=500)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@app.get("/api/json/{filename:path}")
|
|
297
|
+
async def get_json_file(filename: str):
|
|
298
|
+
"""
|
|
299
|
+
DEPRECATED: This endpoint is deprecated and kept for old visualizer compatibility.
|
|
300
|
+
Load specific JSON file from .railtracks directory
|
|
301
|
+
"""
|
|
302
|
+
railtracks_dir = get_railtracks_dir()
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
# URL decode the filename to handle spaces and special characters
|
|
306
|
+
filename = unquote(filename)
|
|
307
|
+
if not filename.endswith(".json"):
|
|
308
|
+
filename += ".json"
|
|
309
|
+
|
|
310
|
+
file_path = railtracks_dir / filename
|
|
311
|
+
|
|
312
|
+
if not file_path.exists():
|
|
313
|
+
return JSONResponse(
|
|
314
|
+
content={"error": f"File {filename} not found"}, status_code=404
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
# Read and parse JSON file
|
|
318
|
+
with open(file_path, encoding="utf-8") as f:
|
|
319
|
+
content = f.read()
|
|
320
|
+
# Validate JSON
|
|
321
|
+
json_data = json.loads(content)
|
|
322
|
+
|
|
323
|
+
response = JSONResponse(content=json_data)
|
|
324
|
+
response.headers["Deprecated"] = "true"
|
|
325
|
+
return response
|
|
326
|
+
|
|
327
|
+
except json.JSONDecodeError as e:
|
|
328
|
+
print_error(f"Invalid JSON in {filename}: {e}")
|
|
329
|
+
return JSONResponse(content={"error": f"Invalid JSON: {e}"}, status_code=400)
|
|
330
|
+
except Exception as e:
|
|
331
|
+
print_error(f"Error handling /api/json/{filename}: {e}")
|
|
332
|
+
return JSONResponse(content={"error": "Internal Server Error"}, status_code=500)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@app.post("/api/refresh")
|
|
336
|
+
async def refresh():
|
|
337
|
+
"""
|
|
338
|
+
DEPRECATED: This endpoint is deprecated and kept for old visualizer compatibility.
|
|
339
|
+
Trigger frontend refresh
|
|
340
|
+
"""
|
|
341
|
+
print_status("Frontend refresh triggered")
|
|
342
|
+
response = JSONResponse(content={"status": "refresh_triggered"})
|
|
343
|
+
response.headers["Deprecated"] = "true"
|
|
344
|
+
return response
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@app.get("/{full_path:path}")
|
|
348
|
+
async def serve_ui_or_404(full_path: str):
|
|
349
|
+
"""Serve UI files with SPA routing fallback (catch-all route)"""
|
|
350
|
+
# Skip API routes
|
|
351
|
+
if full_path.startswith("api/"):
|
|
352
|
+
return JSONResponse(content={"error": "Not Found"}, status_code=404)
|
|
353
|
+
|
|
354
|
+
ui_dir = Path(f"{cli_directory}/ui")
|
|
355
|
+
ui_file = ui_dir / full_path
|
|
356
|
+
if ui_file.exists() and ui_file.is_file():
|
|
357
|
+
return FileResponse(str(ui_file))
|
|
358
|
+
# Fallback to index.html for SPA routing
|
|
359
|
+
index_file = ui_dir / "index.html"
|
|
360
|
+
if index_file.exists():
|
|
361
|
+
return FileResponse(str(index_file))
|
|
362
|
+
return JSONResponse(content={"error": "File not found"}, status_code=404)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class RailtracksServer:
|
|
366
|
+
"""Main server class"""
|
|
367
|
+
|
|
368
|
+
def __init__(self, port=DEFAULT_PORT):
|
|
369
|
+
self.port = port
|
|
370
|
+
self.running = False
|
|
371
|
+
self.config = None
|
|
372
|
+
|
|
373
|
+
def start(self):
|
|
374
|
+
"""Start the FastAPI server"""
|
|
375
|
+
self.running = True
|
|
376
|
+
|
|
377
|
+
# Print server info
|
|
378
|
+
print_success(f"🚀 railtracks server running at http://localhost:{self.port}")
|
|
379
|
+
print_status(f"📁 Serving files from: {cli_directory}/ui/")
|
|
380
|
+
print_status("📋 API endpoints:")
|
|
381
|
+
print_status(" GET /api/evaluations - Get all evaluation JSON files")
|
|
382
|
+
print_status(" GET /api/sessions - Get all session JSON files")
|
|
383
|
+
print_status(" GET /api/files - List JSON files (deprecated)")
|
|
384
|
+
print_status(" GET /api/json/{filename} - Load JSON file (deprecated)")
|
|
385
|
+
print_status(" POST /api/refresh - Trigger frontend refresh (deprecated)")
|
|
386
|
+
print_status("Press Ctrl+C to stop the server")
|
|
387
|
+
|
|
388
|
+
# Open browser after a short delay to ensure server is ready
|
|
389
|
+
def open_browser():
|
|
390
|
+
time.sleep(1) # Give server a moment to fully start
|
|
391
|
+
url = f"http://localhost:{self.port}"
|
|
392
|
+
print_status(f"Opening browser to {url}")
|
|
393
|
+
try:
|
|
394
|
+
webbrowser.open(url)
|
|
395
|
+
except Exception as e:
|
|
396
|
+
print_warning(f"Could not open browser automatically: {e}")
|
|
397
|
+
print_status(f"Please manually open: {url}")
|
|
398
|
+
|
|
399
|
+
browser_thread = threading.Thread(target=open_browser)
|
|
400
|
+
browser_thread.daemon = True
|
|
401
|
+
browser_thread.start()
|
|
402
|
+
|
|
403
|
+
# Start uvicorn server
|
|
404
|
+
try:
|
|
405
|
+
config = uvicorn.Config(
|
|
406
|
+
app,
|
|
407
|
+
host="localhost",
|
|
408
|
+
port=self.port,
|
|
409
|
+
log_level="info",
|
|
410
|
+
access_log=False, # We handle our own logging
|
|
411
|
+
)
|
|
412
|
+
server = uvicorn.Server(config)
|
|
413
|
+
self.config = config
|
|
414
|
+
server.run()
|
|
415
|
+
except KeyboardInterrupt:
|
|
416
|
+
self.stop()
|
|
417
|
+
|
|
418
|
+
def stop(self):
|
|
419
|
+
"""Stop the server and cleanup"""
|
|
420
|
+
if self.running:
|
|
421
|
+
print_status("Shutting down railtracks...")
|
|
422
|
+
self.running = False
|
|
423
|
+
|
|
424
|
+
print_success("railtracks stopped.")
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def main():
|
|
428
|
+
"""Main function"""
|
|
429
|
+
if len(sys.argv) < 2:
|
|
430
|
+
print(f"Usage: {cli_name} [command]")
|
|
431
|
+
print("")
|
|
432
|
+
print("Commands:")
|
|
433
|
+
print(
|
|
434
|
+
f" init Initialize {cli_name} environment (setup directories, download portable UI)"
|
|
435
|
+
)
|
|
436
|
+
print(f" viz Start the {cli_name} development server")
|
|
437
|
+
print(f" migrate Verify and migrate the structure of .{cli_name}/ directory")
|
|
438
|
+
print("")
|
|
439
|
+
print("Examples:")
|
|
440
|
+
print(f" {cli_name} init # Initialize development environment")
|
|
441
|
+
print(f" {cli_name} viz # Start visualizer web app")
|
|
442
|
+
print(
|
|
443
|
+
f" {cli_name} migrate # Verify and migrate .{cli_name}/ directory structure"
|
|
444
|
+
)
|
|
445
|
+
sys.exit(1)
|
|
446
|
+
|
|
447
|
+
command = sys.argv[1]
|
|
448
|
+
|
|
449
|
+
if command == "init":
|
|
450
|
+
init_railtracks()
|
|
451
|
+
elif command == "viz":
|
|
452
|
+
# Check if port is already in use
|
|
453
|
+
if is_port_in_use(DEFAULT_PORT):
|
|
454
|
+
print_error(f"Port {DEFAULT_PORT} is already in use!")
|
|
455
|
+
print_status("Please stop the existing server.")
|
|
456
|
+
sys.exit(1)
|
|
457
|
+
|
|
458
|
+
# Setup directories
|
|
459
|
+
create_railtracks_dir()
|
|
460
|
+
|
|
461
|
+
# Start server
|
|
462
|
+
server = RailtracksServer()
|
|
463
|
+
server.start()
|
|
464
|
+
elif command == "migrate":
|
|
465
|
+
migrate_railtracks()
|
|
466
|
+
else:
|
|
467
|
+
print(f"Unknown command: {command}")
|
|
468
|
+
print("Available commands: init, viz, migrate")
|
|
469
|
+
sys.exit(1)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
if __name__ == "__main__":
|
|
473
|
+
main()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: railtracks-cli
|
|
3
|
+
Version: 1.1.22
|
|
4
|
+
Summary: railtracks - A Python development server with JSON API
|
|
5
|
+
Author-email: Logan Underwood <logan@railtown.ai>, Levi Varsanyi <levi@railtown.ai>, Jaime Bueza <jaime@railtown.ai>, Amir Refaee <amir@railtown.ai>, Aryan Ballani <aryan@railtown.ai>, Tristan Brown <tristan@railtown.ai>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Intended Audience :: Science/Research
|
|
18
|
+
Classifier: Natural Language :: English
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
21
|
+
Classifier: Environment :: Console
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: railtracks
|
|
25
|
+
Requires-Dist: fastapi
|
|
26
|
+
Requires-Dist: uvicorn[standard]
|
|
27
|
+
Project-URL: Release Notes, https://github.com/RailtownAI/railtracks/releases
|
|
28
|
+
Project-URL: documentation, https://railtownai.github.io/railtracks/
|
|
29
|
+
Project-URL: home, https://railtracks.org
|
|
30
|
+
Project-URL: repository, https://github.com/RailtownAI/railtracks
|
|
31
|
+
|
|
32
|
+
# Railtracks CLI
|
|
33
|
+
|
|
34
|
+
[](https://github.com/RailtownAI/railtracks/releases)
|
|
35
|
+
[](https://pypi.org/project/railtracks/)
|
|
36
|
+
[](https://opensource.org/licenses/MIT)
|
|
37
|
+
[](https://pypistats.org/packages/railtracks-cli)
|
|
38
|
+
[](https://railtownai.github.io/railtracks/)
|
|
39
|
+
[](https://github.com/RailtownAI/railtracks)
|
|
40
|
+
[](https://discord.gg/h5ZcahDc)
|
|
41
|
+
|
|
42
|
+
A simple CLI to help developers visualize and debug their agents.
|
|
43
|
+
|
|
44
|
+
## What is Railtracks CLI?
|
|
45
|
+
|
|
46
|
+
Railtracks CLI is a development tool that provides:
|
|
47
|
+
|
|
48
|
+
- **Local Development Server**: A web-based visualizer for your railtracks projects
|
|
49
|
+
- **JSON API**: RESTful endpoints to interact with your project data
|
|
50
|
+
- **Modern UI**: A downloadable frontend interface for project visualization
|
|
51
|
+
|
|
52
|
+
## Quick Start
|
|
53
|
+
|
|
54
|
+
### 1. Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install railtracks-cli
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 2. Initialize Your Project
|
|
61
|
+
|
|
62
|
+
First, initialize the railtracks environment in your project directory:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
railtracks init
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This command will:
|
|
69
|
+
|
|
70
|
+
- Create a `.railtracks` directory in your project
|
|
71
|
+
- Add `.railtracks` to your `.gitignore` file
|
|
72
|
+
- Download and extract the latest frontend UI
|
|
73
|
+
|
|
74
|
+
### 3. Start the Development Server
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
railtracks viz
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This starts the development server at `http://localhost:3030` with:
|
|
81
|
+
|
|
82
|
+
- API endpoints for data access
|
|
83
|
+
- Portable Web-based visualizer interface that can be opened in any web environment (web, mobile, vs extension, chrome extension, etc)
|
|
84
|
+
|
|
85
|
+
## Project Structure
|
|
86
|
+
|
|
87
|
+
After initialization, your project will have this structure:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
your-project/
|
|
91
|
+
├── .railtracks/ # Railtracks working directory
|
|
92
|
+
│ ├── ui/ # Frontend interface files
|
|
93
|
+
│ └── *.json # Your project JSON files
|
|
94
|
+
├── .gitignore # Updated to exclude .railtracks
|
|
95
|
+
└── your-source-files/ # Your actual project files
|
|
96
|
+
```
|
|
97
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
railtracks_cli/__init__.py,sha256=UzP2XxVnz5m2tbtPiTrS8-VK3IJEda9M1uAVGhwNpms,15810
|
|
2
|
+
railtracks_cli/__main__.py,sha256=1LnxwDolMoaJ1kuSNMNgsm5DDSOWy6Q8RTh8P-FKGIU,130
|
|
3
|
+
railtracks_cli-1.1.22.dist-info/entry_points.txt,sha256=mc-9r0VsRzZe4DvKJqBTKcAq58SPphRKi-X2ZLun8hE,50
|
|
4
|
+
railtracks_cli-1.1.22.dist-info/licenses/LICENSE,sha256=Y5ir_N69ZJf1coSB9f1xW7CP17TP5gI-YwUSg7xWewM,1078
|
|
5
|
+
railtracks_cli-1.1.22.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
6
|
+
railtracks_cli-1.1.22.dist-info/METADATA,sha256=cwc8LaErS3ZC1hAaXh7fJ72QFKW5STdPTL-UWnCCEOA,3829
|
|
7
|
+
railtracks_cli-1.1.22.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Railtown AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|