development-engine-vector 0.3.0__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.
- dev/__init__.py +3 -0
- dev/__main__.py +5 -0
- dev/cli/__init__.py +0 -0
- dev/cli/cli.py +674 -0
- dev/kernel/__init__.py +0 -0
- dev/kernel/config.py +46 -0
- dev/kernel/db.py +129 -0
- dev/kernel/paths.py +85 -0
- dev/kernel/pmf_kernel.py +432 -0
- dev/kernel/selfcheck.py +137 -0
- dev/ui/__init__.py +0 -0
- dev/ui/actions.py +47 -0
- dev/ui/db/__init__.py +26 -0
- dev/ui/db/base.py +75 -0
- dev/ui/db/checks.py +16 -0
- dev/ui/db/events.py +18 -0
- dev/ui/db/health.py +34 -0
- dev/ui/db/identity.py +12 -0
- dev/ui/db/manifest.py +35 -0
- dev/ui/db/overview.py +47 -0
- dev/ui/db/runs.py +47 -0
- dev/ui/routes.py +114 -0
- dev/ui/static/__init__.py +0 -0
- dev/ui/static/web.css +722 -0
- dev/ui/static/web.js +528 -0
- dev/ui/templates.py +231 -0
- dev/ui/web.py +28 -0
- dev/utils.py +93 -0
- dev/vcs/__init__.py +0 -0
- dev/vcs/git.py +107 -0
- dev/vcs/github.py +77 -0
- dev/workflow/__init__.py +0 -0
- dev/workflow/cda.py +143 -0
- dev/workflow/changelog.py +102 -0
- dev/workflow/preflight.py +307 -0
- dev/workflow/release.py +217 -0
- dev/workflow/versioning.py +87 -0
- development_engine_vector-0.3.0.dist-info/METADATA +252 -0
- development_engine_vector-0.3.0.dist-info/RECORD +41 -0
- development_engine_vector-0.3.0.dist-info/WHEEL +4 -0
- development_engine_vector-0.3.0.dist-info/entry_points.txt +2 -0
dev/ui/templates.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
INDEX_HTML = """
|
|
4
|
+
<!DOCTYPE html>
|
|
5
|
+
<html>
|
|
6
|
+
<head>
|
|
7
|
+
<meta charset="utf-8">
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
9
|
+
<title>dev — Development Engine</title>
|
|
10
|
+
<style>{{STYLE_CSS}}</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="root">
|
|
14
|
+
<div class="sidebar">
|
|
15
|
+
<div class="sidebar-header">
|
|
16
|
+
<div class="sidebar-title">dev</div>
|
|
17
|
+
<div style="font-size: 11px; color: var(--text-tertiary); margin-top: 5px;">
|
|
18
|
+
Development Engine Vector
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="nav-group">
|
|
23
|
+
<div class="nav-group-title">Core</div>
|
|
24
|
+
<div class="nav-item active" data-page="dashboard"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="3" width="7" height="8" rx="1"/><rect x="14" y="3" width="7" height="5" rx="1"/><rect x="14" y="12" width="7" height="9" rx="1"/><rect x="3" y="14" width="7" height="6" rx="1"/></svg>Dashboard</div>
|
|
25
|
+
<div class="nav-item" data-page="runs"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>Runs</div>
|
|
26
|
+
<div class="nav-item" data-page="health"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>Health</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="nav-group">
|
|
30
|
+
<div class="nav-group-title">Config</div>
|
|
31
|
+
<div class="nav-item" data-page="checks"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>Checks</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="nav-group">
|
|
35
|
+
<div class="nav-group-title">Inventory</div>
|
|
36
|
+
<div class="nav-item" data-page="manifest"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2Z"/></svg>Manifest</div>
|
|
37
|
+
<div class="nav-item" data-page="identity"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/></svg>Identity</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="nav-group">
|
|
41
|
+
<div class="nav-group-title">Logs</div>
|
|
42
|
+
<div class="nav-item" data-page="events"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>Events</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="nav-group">
|
|
46
|
+
<div class="nav-group-title">System</div>
|
|
47
|
+
<div class="nav-item" data-page="query"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m8 9 4-4 4 4"/><path d="m8 15 4-4 4 4"/></svg>Raw Query</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="content" id="main-content">
|
|
52
|
+
<!-- Pages rendered here -->
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div id="detail-drawer" class="drawer">
|
|
57
|
+
<div class="drawer-backdrop" onclick="closeRunDrawer()"></div>
|
|
58
|
+
<div class="drawer-panel">
|
|
59
|
+
<div class="drawer-header">
|
|
60
|
+
<div class="drawer-title">
|
|
61
|
+
<div class="title" id="drawer-run-title">Run Details</div>
|
|
62
|
+
<div class="subtitle" id="drawer-run-subtitle">Full run metadata and per-check output.</div>
|
|
63
|
+
</div>
|
|
64
|
+
<button class="drawer-close" onclick="closeRunDrawer()" aria-label="Close run details">×</button>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="drawer-tabs" id="drawer-tabs">
|
|
67
|
+
<div class="drawer-tab active" data-tab="overview" onclick="switchRunTab('overview')">Overview</div>
|
|
68
|
+
<div class="drawer-tab" data-tab="output" onclick="switchRunTab('output')">Output</div>
|
|
69
|
+
<div class="drawer-tab" data-tab="raw" onclick="switchRunTab('raw')">Raw</div>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="drawer-body" id="drawer-body">
|
|
72
|
+
<div class="spinner"></div>
|
|
73
|
+
Loading run details...
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<script>{{APP_JS}}</script>
|
|
79
|
+
</body>
|
|
80
|
+
</html>
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def render_dashboard():
|
|
85
|
+
return """
|
|
86
|
+
<div class="page-header">
|
|
87
|
+
<div>
|
|
88
|
+
<div class="page-title">Dashboard</div>
|
|
89
|
+
<div class="page-subtitle">Vet run history, check pass rates, and system status.</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
<div id="dashboard-content" class="loading">
|
|
93
|
+
<div class="spinner"></div>
|
|
94
|
+
Loading overview...
|
|
95
|
+
</div>
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def render_runs():
|
|
100
|
+
return """
|
|
101
|
+
<div class="page-header">
|
|
102
|
+
<div>
|
|
103
|
+
<div class="page-title">Runs</div>
|
|
104
|
+
<div class="page-subtitle">All vet executions with status and notes.</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
<div id="runs-content" class="loading">
|
|
108
|
+
<div class="spinner"></div>
|
|
109
|
+
Loading runs...
|
|
110
|
+
</div>
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def render_health():
|
|
115
|
+
return """
|
|
116
|
+
<div class="page-header">
|
|
117
|
+
<div>
|
|
118
|
+
<div class="page-title">Health</div>
|
|
119
|
+
<div class="page-subtitle">Per-check results across all vet runs.</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
<div id="health-content" class="loading">
|
|
123
|
+
<div class="spinner"></div>
|
|
124
|
+
Loading health records...
|
|
125
|
+
</div>
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def render_checks():
|
|
130
|
+
return """
|
|
131
|
+
<div class="page-header">
|
|
132
|
+
<div>
|
|
133
|
+
<div class="page-title">Checks</div>
|
|
134
|
+
<div class="page-subtitle">Defined vet checks — name, kind, command, and timeout.</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
<div id="checks-content" class="loading">
|
|
138
|
+
<div class="spinner"></div>
|
|
139
|
+
Loading checks...
|
|
140
|
+
</div>
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def render_manifest():
|
|
145
|
+
return """
|
|
146
|
+
<div class="page-header">
|
|
147
|
+
<div>
|
|
148
|
+
<div class="page-title">Manifest</div>
|
|
149
|
+
<div class="page-subtitle">File inventory of the source tree.</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="card mb-20">
|
|
153
|
+
<div class="form-group">
|
|
154
|
+
<label class="form-label">Filter</label>
|
|
155
|
+
<input type="text" id="manifest-search" class="form-input" placeholder="Filter by path or filename...">
|
|
156
|
+
</div>
|
|
157
|
+
<button class="button button-primary" onclick="loadManifest(document.getElementById('manifest-search').value)">Search</button>
|
|
158
|
+
</div>
|
|
159
|
+
<div id="manifest-content" class="loading">
|
|
160
|
+
<div class="spinner"></div>
|
|
161
|
+
Loading manifest...
|
|
162
|
+
</div>
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def render_identity():
|
|
167
|
+
return """
|
|
168
|
+
<div class="page-header">
|
|
169
|
+
<div>
|
|
170
|
+
<div class="page-title">Identity</div>
|
|
171
|
+
<div class="page-subtitle">System identity key-value records.</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
<div id="identity-content" class="loading">
|
|
175
|
+
<div class="spinner"></div>
|
|
176
|
+
Loading identity...
|
|
177
|
+
</div>
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def render_events():
|
|
182
|
+
return """
|
|
183
|
+
<div class="page-header">
|
|
184
|
+
<div>
|
|
185
|
+
<div class="page-title">Events</div>
|
|
186
|
+
<div class="page-subtitle">System event log.</div>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
<div id="events-content" class="loading">
|
|
190
|
+
<div class="spinner"></div>
|
|
191
|
+
Loading events...
|
|
192
|
+
</div>
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def render_query():
|
|
197
|
+
return """
|
|
198
|
+
<div class="page-header">
|
|
199
|
+
<div>
|
|
200
|
+
<div class="page-title">Raw Query</div>
|
|
201
|
+
<div class="page-subtitle">Execute SQL directly against the control database.</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
<div class="card mb-20">
|
|
205
|
+
<div class="form-group">
|
|
206
|
+
<label class="form-label">SQL Query</label>
|
|
207
|
+
<textarea id="query-input" class="form-textarea" placeholder="SELECT * FROM runs ORDER BY id DESC LIMIT 10"></textarea>
|
|
208
|
+
</div>
|
|
209
|
+
<button class="button button-primary" onclick="executeQuery()">Execute</button>
|
|
210
|
+
</div>
|
|
211
|
+
<div id="query-results" class="hidden"></div>
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
_PAGE_MAP = {
|
|
216
|
+
'dashboard': render_dashboard,
|
|
217
|
+
'runs': render_runs,
|
|
218
|
+
'health': render_health,
|
|
219
|
+
'checks': render_checks,
|
|
220
|
+
'manifest': render_manifest,
|
|
221
|
+
'identity': render_identity,
|
|
222
|
+
'events': render_events,
|
|
223
|
+
'query': render_query,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
PAGE_REGISTRY_JS = "const PAGE_REGISTRY = {\n"
|
|
227
|
+
PAGE_REGISTRY_JS += ",\n".join(
|
|
228
|
+
f" '{name}': () => {json.dumps(fn())}"
|
|
229
|
+
for name, fn in _PAGE_MAP.items()
|
|
230
|
+
)
|
|
231
|
+
PAGE_REGISTRY_JS += "\n};\n\n"
|
dev/ui/web.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
dev — Development Engine Vector
|
|
4
|
+
Embedded web UI entry point.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import socket
|
|
8
|
+
from wsgiref.simple_server import make_server, WSGIServer
|
|
9
|
+
|
|
10
|
+
from dev.ui.routes import application # noqa: F401
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def start_server(host="127.0.0.1", port=9001):
|
|
14
|
+
"""Start the embedded WSGI server."""
|
|
15
|
+
print(f"Starting dev UI at http://{host}:{port}")
|
|
16
|
+
print("Press Ctrl+C to stop.")
|
|
17
|
+
|
|
18
|
+
class ReusableTCPServer(WSGIServer):
|
|
19
|
+
def server_bind(self):
|
|
20
|
+
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
21
|
+
super().server_bind()
|
|
22
|
+
|
|
23
|
+
httpd = make_server(host, port, application, server_class=ReusableTCPServer)
|
|
24
|
+
httpd.serve_forever()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
start_server()
|
dev/utils.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Shared utilities and helpers."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Colors:
|
|
8
|
+
"""ANSI color codes for terminal output."""
|
|
9
|
+
|
|
10
|
+
RESET = "\033[0m"
|
|
11
|
+
BOLD = "\033[1m"
|
|
12
|
+
DIM = "\033[2m"
|
|
13
|
+
RED = "\033[91m"
|
|
14
|
+
GREEN = "\033[92m"
|
|
15
|
+
YELLOW = "\033[93m"
|
|
16
|
+
BLUE = "\033[94m"
|
|
17
|
+
CYAN = "\033[96m"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def bold(text):
|
|
21
|
+
return f"{Colors.BOLD}{text}{Colors.RESET}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def dim(text):
|
|
25
|
+
return f"{Colors.DIM}{text}{Colors.RESET}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def green(text):
|
|
29
|
+
return f"{Colors.GREEN}{text}{Colors.RESET}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def yellow(text):
|
|
33
|
+
return f"{Colors.YELLOW}{text}{Colors.RESET}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def red(text):
|
|
37
|
+
return f"{Colors.RED}{text}{Colors.RESET}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def cyan(text):
|
|
41
|
+
return f"{Colors.CYAN}{text}{Colors.RESET}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def blue(text):
|
|
45
|
+
return f"{Colors.BLUE}{text}{Colors.RESET}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def hr(width=80):
|
|
49
|
+
"""Return a horizontal rule."""
|
|
50
|
+
return dim("─" * width)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def header(text):
|
|
54
|
+
"""Print a formatted header."""
|
|
55
|
+
print(f"\n{bold(text)}")
|
|
56
|
+
print(hr())
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def success(text):
|
|
60
|
+
"""Print success message."""
|
|
61
|
+
print(green(f"✓ {text}"))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def error(text):
|
|
65
|
+
"""Print error message and exit."""
|
|
66
|
+
print(red(f"✗ {text}"))
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def warn(text):
|
|
71
|
+
"""Print warning message."""
|
|
72
|
+
print(yellow(f"⚠ {text}"))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def info(text):
|
|
76
|
+
"""Print info message."""
|
|
77
|
+
print(f" {text}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def run_command(command, cwd=None, env=None, check=True, capture_output=False):
|
|
81
|
+
"""Run a subprocess command and surface output on failure."""
|
|
82
|
+
try:
|
|
83
|
+
result = subprocess.run(
|
|
84
|
+
command,
|
|
85
|
+
cwd=cwd,
|
|
86
|
+
env=env,
|
|
87
|
+
check=check,
|
|
88
|
+
capture_output=capture_output,
|
|
89
|
+
text=True,
|
|
90
|
+
)
|
|
91
|
+
return result
|
|
92
|
+
except subprocess.CalledProcessError as exc:
|
|
93
|
+
error(f"Command failed: {' '.join(command)}\n{exc.stderr or exc.stdout or exc}")
|
dev/vcs/__init__.py
ADDED
|
File without changes
|
dev/vcs/git.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""dev.vcs.git — git operations for the Development Engine Vector."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from dev.utils import error, success, info
|
|
7
|
+
from dev.vcs.github import create_github_repo
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GitOps:
|
|
11
|
+
"""Encapsulates git operations."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, project_dir):
|
|
14
|
+
self.project_dir = Path(project_dir).resolve()
|
|
15
|
+
if not (self.project_dir / ".git").exists():
|
|
16
|
+
error(f"Not a git repository: {self.project_dir}")
|
|
17
|
+
|
|
18
|
+
def run_git(self, args, check=True):
|
|
19
|
+
"""Execute a git command."""
|
|
20
|
+
try:
|
|
21
|
+
result = subprocess.run(
|
|
22
|
+
["git"] + args,
|
|
23
|
+
cwd=self.project_dir,
|
|
24
|
+
capture_output=True,
|
|
25
|
+
text=True,
|
|
26
|
+
check=check,
|
|
27
|
+
)
|
|
28
|
+
return result.stdout.strip()
|
|
29
|
+
except subprocess.CalledProcessError as e:
|
|
30
|
+
if check:
|
|
31
|
+
error(f"Git command failed: {' '.join(args)}\n{e.stderr}")
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
def get_current_branch(self):
|
|
35
|
+
"""Get current branch name."""
|
|
36
|
+
return self.run_git(["rev-parse", "--abbrev-ref", "HEAD"])
|
|
37
|
+
|
|
38
|
+
def get_last_tag(self):
|
|
39
|
+
"""Get most recent tag."""
|
|
40
|
+
result = self.run_git(["describe", "--tags", "--abbrev=0"], check=False)
|
|
41
|
+
return result if result else None
|
|
42
|
+
|
|
43
|
+
def has_uncommitted_changes(self):
|
|
44
|
+
"""Check for uncommitted changes."""
|
|
45
|
+
output = self.run_git(["status", "--porcelain"], check=False)
|
|
46
|
+
return bool(output and output.strip())
|
|
47
|
+
|
|
48
|
+
def stage_files(self, patterns):
|
|
49
|
+
"""Stage files matching patterns."""
|
|
50
|
+
for pattern in patterns:
|
|
51
|
+
self.run_git(["add", pattern])
|
|
52
|
+
|
|
53
|
+
def commit(self, message):
|
|
54
|
+
"""Create a commit."""
|
|
55
|
+
self.run_git(["commit", "-m", message])
|
|
56
|
+
info(f"Committed: {message}")
|
|
57
|
+
|
|
58
|
+
def create_tag(self, version, message=None):
|
|
59
|
+
"""Create annotated tag."""
|
|
60
|
+
tag_name = f"v{version}"
|
|
61
|
+
msg = message or f"Release {version}"
|
|
62
|
+
self.run_git(["tag", "-a", tag_name, "-m", msg])
|
|
63
|
+
success(f"Tagged: {tag_name}")
|
|
64
|
+
|
|
65
|
+
def get_commits_since_tag(self, tag=None):
|
|
66
|
+
"""Get commits since last tag."""
|
|
67
|
+
if not tag:
|
|
68
|
+
tag = self.get_last_tag()
|
|
69
|
+
if not tag:
|
|
70
|
+
return self.run_git(["log", "--oneline"]).split("\n")
|
|
71
|
+
return self.run_git(["log", f"{tag}..HEAD", "--oneline"]).split("\n")
|
|
72
|
+
|
|
73
|
+
def get_remote_url(self, remote="origin"):
|
|
74
|
+
"""Return the URL of a remote, or None if not set."""
|
|
75
|
+
result = subprocess.run(
|
|
76
|
+
["git", "remote", "get-url", remote],
|
|
77
|
+
cwd=self.project_dir,
|
|
78
|
+
capture_output=True,
|
|
79
|
+
text=True,
|
|
80
|
+
)
|
|
81
|
+
return result.stdout.strip() if result.returncode == 0 else None
|
|
82
|
+
|
|
83
|
+
def ensure_remote(self, org, repo_name, private=True, remote="origin"):
|
|
84
|
+
"""Ensure the git remote exists, creating the GitHub repo if needed."""
|
|
85
|
+
existing = self.get_remote_url(remote)
|
|
86
|
+
if existing:
|
|
87
|
+
info(f"Remote '{remote}' already set: {existing}")
|
|
88
|
+
return existing
|
|
89
|
+
|
|
90
|
+
ssh_url = create_github_repo(org, repo_name, private=private)
|
|
91
|
+
if not ssh_url:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
self.run_git(["remote", "add", remote, ssh_url])
|
|
95
|
+
success(f"Added remote '{remote}': {ssh_url}")
|
|
96
|
+
return ssh_url
|
|
97
|
+
|
|
98
|
+
def push_main(self, push_tags=False, set_upstream=False):
|
|
99
|
+
"""Push main branch to origin."""
|
|
100
|
+
args = ["push", "origin", "main"]
|
|
101
|
+
if set_upstream:
|
|
102
|
+
args = ["push", "-u", "origin", "main"]
|
|
103
|
+
self.run_git(args)
|
|
104
|
+
success("Pushed main branch")
|
|
105
|
+
if push_tags:
|
|
106
|
+
self.run_git(["push", "origin", "--tags"])
|
|
107
|
+
success("Pushed tags")
|
dev/vcs/github.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""dev.vcs.github — GitHub API integration for the Development Engine Vector.
|
|
2
|
+
|
|
3
|
+
Token resolution order:
|
|
4
|
+
1. GITHUB_TOKEN environment variable
|
|
5
|
+
2. GITHIUB_TOKEN environment variable (legacy typo — kept for compatibility)
|
|
6
|
+
3. /Volumes/intel/systems/audit/.env file
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from typing import Optional
|
|
12
|
+
import urllib.error
|
|
13
|
+
import urllib.request
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from dev.utils import error, success, info
|
|
17
|
+
|
|
18
|
+
# Path from this file up to /Volumes/intel, then into audit/.env
|
|
19
|
+
_ENV_FILE = (
|
|
20
|
+
Path(__file__).parent.parent.parent.parent.parent.parent
|
|
21
|
+
/ "systems" / "audit" / ".env"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _load_github_token() -> Optional[str]:
|
|
26
|
+
"""Load GitHub token from environment or audit-engine/.env."""
|
|
27
|
+
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GITHIUB_TOKEN")
|
|
28
|
+
if token:
|
|
29
|
+
return token
|
|
30
|
+
if _ENV_FILE.exists():
|
|
31
|
+
for line in _ENV_FILE.read_text().splitlines():
|
|
32
|
+
if line.startswith("GITHIUB_TOKEN=") or line.startswith("GITHUB_TOKEN="):
|
|
33
|
+
return line.split("=", 1)[1].strip()
|
|
34
|
+
error("GitHub token not found in environment or audit-engine/.env")
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _github_api(method: str, path: str, body: Optional[dict] = None) -> dict:
|
|
39
|
+
"""Make a GitHub API request. Returns parsed JSON response."""
|
|
40
|
+
token = _load_github_token()
|
|
41
|
+
url = f"https://api.github.com{path}"
|
|
42
|
+
data = json.dumps(body).encode() if body else None
|
|
43
|
+
req = urllib.request.Request(
|
|
44
|
+
url,
|
|
45
|
+
data=data,
|
|
46
|
+
method=method,
|
|
47
|
+
headers={
|
|
48
|
+
"Authorization": f"token {token}",
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
"Accept": "application/vnd.github.v3+json",
|
|
51
|
+
},
|
|
52
|
+
)
|
|
53
|
+
try:
|
|
54
|
+
with urllib.request.urlopen(req) as resp:
|
|
55
|
+
return json.loads(resp.read())
|
|
56
|
+
except urllib.error.HTTPError as exc:
|
|
57
|
+
return json.loads(exc.read())
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def create_github_repo(org: str, name: str, private: bool = True, description: str = "") -> Optional[str]:
|
|
61
|
+
"""Create a GitHub repo under org. Returns ssh_url or None."""
|
|
62
|
+
info(f"Creating GitHub repo {org}/{name} ...")
|
|
63
|
+
resp = _github_api(
|
|
64
|
+
"POST",
|
|
65
|
+
f"/orgs/{org}/repos",
|
|
66
|
+
{"name": name, "private": private, "description": description},
|
|
67
|
+
)
|
|
68
|
+
if "ssh_url" in resp:
|
|
69
|
+
success(f"Created: {resp['ssh_url']}")
|
|
70
|
+
return resp["ssh_url"]
|
|
71
|
+
msg = resp.get("message", str(resp))
|
|
72
|
+
if "already exists" in msg or "name already exists" in msg:
|
|
73
|
+
ssh_url = f"git@github.com:{org}/{name}.git"
|
|
74
|
+
info(f"Repo already exists: {ssh_url}")
|
|
75
|
+
return ssh_url
|
|
76
|
+
error(f"Failed to create repo: {msg}")
|
|
77
|
+
return None
|
dev/workflow/__init__.py
ADDED
|
File without changes
|
dev/workflow/cda.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cda_preflight — project-specific preflight checks for vscode-ark (cda).
|
|
3
|
+
|
|
4
|
+
Checks:
|
|
5
|
+
cli_smoke — `cda --help` exits 0
|
|
6
|
+
test_suite — pytest tests/ passes with no failures
|
|
7
|
+
db_accessible — data/vscode-ark.db opens as a valid SQLite database
|
|
8
|
+
no_stale_pid — no *.pid files present on disk
|
|
9
|
+
data_gitignored — data/ directory is gitignored
|
|
10
|
+
install_path — editable install of vscode_ark resolves to this project dir
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import shutil
|
|
14
|
+
import sqlite3
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _record(name, passed, message, details=None):
|
|
21
|
+
item = {"name": name, "passed": passed, "message": message}
|
|
22
|
+
if details is not None:
|
|
23
|
+
item["details"] = details
|
|
24
|
+
return item
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _check_cli_smoke(project_dir):
|
|
28
|
+
cda_bin = shutil.which("cda")
|
|
29
|
+
if not cda_bin:
|
|
30
|
+
return _record("cli_smoke", False, "cda command not found in PATH")
|
|
31
|
+
try:
|
|
32
|
+
result = subprocess.run(
|
|
33
|
+
[cda_bin, "--help"],
|
|
34
|
+
capture_output=True,
|
|
35
|
+
text=True,
|
|
36
|
+
timeout=10,
|
|
37
|
+
)
|
|
38
|
+
if result.returncode == 0:
|
|
39
|
+
return _record("cli_smoke", True, f"cda --help succeeded ({cda_bin})")
|
|
40
|
+
return _record("cli_smoke", False, f"cda --help exited {result.returncode}", result.stderr)
|
|
41
|
+
except subprocess.TimeoutExpired:
|
|
42
|
+
return _record("cli_smoke", False, "cda --help timed out after 10s")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _check_test_suite(project_dir):
|
|
46
|
+
tests_dir = project_dir / "tests"
|
|
47
|
+
if not tests_dir.is_dir():
|
|
48
|
+
return _record("test_suite", False, "tests/ directory not found")
|
|
49
|
+
try:
|
|
50
|
+
result = subprocess.run(
|
|
51
|
+
[sys.executable, "-m", "pytest", str(tests_dir), "-q", "--tb=short"],
|
|
52
|
+
cwd=project_dir,
|
|
53
|
+
capture_output=True,
|
|
54
|
+
text=True,
|
|
55
|
+
timeout=120,
|
|
56
|
+
)
|
|
57
|
+
if result.returncode == 0:
|
|
58
|
+
# Parse summary line (e.g. "5 passed in 0.12s")
|
|
59
|
+
last = [line for line in result.stdout.splitlines() if line.strip()]
|
|
60
|
+
summary = last[-1] if last else "passed"
|
|
61
|
+
return _record("test_suite", True, f"All tests passed: {summary}")
|
|
62
|
+
details = (result.stdout + result.stderr).strip()
|
|
63
|
+
return _record("test_suite", False, "Test suite has failures", details[-2000:])
|
|
64
|
+
except subprocess.TimeoutExpired:
|
|
65
|
+
return _record("test_suite", False, "Test suite timed out after 120s")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _check_db_accessible(project_dir):
|
|
69
|
+
db_path = project_dir / "data" / "vscode-ark.db"
|
|
70
|
+
if not db_path.exists():
|
|
71
|
+
return _record("db_accessible", False, f"vscode-ark.db not found at {db_path}")
|
|
72
|
+
try:
|
|
73
|
+
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
|
74
|
+
conn.execute("SELECT 1")
|
|
75
|
+
conn.close()
|
|
76
|
+
size_mb = db_path.stat().st_size / (1024 * 1024)
|
|
77
|
+
return _record("db_accessible", True, f"vscode-ark.db is accessible ({size_mb:.0f} MB)")
|
|
78
|
+
except sqlite3.DatabaseError as exc:
|
|
79
|
+
return _record("db_accessible", False, f"vscode-ark.db is corrupt or unreadable: {exc}")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _check_no_stale_pid(project_dir):
|
|
83
|
+
pid_files = list(project_dir.glob("*.pid"))
|
|
84
|
+
if pid_files:
|
|
85
|
+
names = ", ".join(p.name for p in pid_files)
|
|
86
|
+
return _record("no_stale_pid", False, f"Stale PID files present: {names}")
|
|
87
|
+
return _record("no_stale_pid", True, "No stale PID files present")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _check_data_gitignored(project_dir):
|
|
91
|
+
result = subprocess.run(
|
|
92
|
+
["git", "check-ignore", "-q", "data"],
|
|
93
|
+
cwd=project_dir,
|
|
94
|
+
capture_output=True,
|
|
95
|
+
)
|
|
96
|
+
if result.returncode == 0:
|
|
97
|
+
return _record("data_gitignored", True, "data/ directory is gitignored")
|
|
98
|
+
return _record("data_gitignored", False, "data/ is not gitignored — sensitive data may be committed")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _check_install_path(project_dir):
|
|
102
|
+
try:
|
|
103
|
+
result = subprocess.run(
|
|
104
|
+
[
|
|
105
|
+
sys.executable, "-c",
|
|
106
|
+
"import vscode_ark, pathlib; "
|
|
107
|
+
"print(pathlib.Path(vscode_ark.__file__).parent.parent.resolve())",
|
|
108
|
+
],
|
|
109
|
+
capture_output=True,
|
|
110
|
+
text=True,
|
|
111
|
+
)
|
|
112
|
+
if result.returncode != 0:
|
|
113
|
+
return _record("install_path", False, "vscode_ark not importable — editable install missing or broken")
|
|
114
|
+
install_dir = Path(result.stdout.strip()).resolve()
|
|
115
|
+
if install_dir == project_dir.resolve():
|
|
116
|
+
return _record("install_path", True, f"editable install points to {install_dir}")
|
|
117
|
+
return _record(
|
|
118
|
+
"install_path", False,
|
|
119
|
+
f"editable install points to wrong path: {install_dir} (expected {project_dir})"
|
|
120
|
+
)
|
|
121
|
+
except Exception as exc:
|
|
122
|
+
return _record("install_path", False, f"install_path check error: {exc}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class CdaPreflightModule:
|
|
126
|
+
name = "cda_preflight"
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def is_applicable(cls, project_dir):
|
|
130
|
+
"""Only run on the vscode-ark (cda) project."""
|
|
131
|
+
return (Path(project_dir).resolve() / "vscode_ark").is_dir()
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def run(cls, project_dir):
|
|
135
|
+
project_dir = Path(project_dir).resolve()
|
|
136
|
+
return [
|
|
137
|
+
_check_cli_smoke(project_dir),
|
|
138
|
+
_check_test_suite(project_dir),
|
|
139
|
+
_check_db_accessible(project_dir),
|
|
140
|
+
_check_no_stale_pid(project_dir),
|
|
141
|
+
_check_data_gitignored(project_dir),
|
|
142
|
+
_check_install_path(project_dir),
|
|
143
|
+
]
|