helixevo 0.2.15 → 0.2.17
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.
|
@@ -1,55 +1,126 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import {
|
|
2
|
+
import { spawn, type ChildProcess } from 'child_process'
|
|
3
3
|
|
|
4
4
|
export const dynamic = 'force-dynamic'
|
|
5
5
|
|
|
6
6
|
// Allowed commands — whitelist to prevent arbitrary execution
|
|
7
|
-
const ALLOWED_COMMANDS: Record<string, { cmd: string; timeout: number }> = {
|
|
8
|
-
'status': { cmd: 'helixevo status',
|
|
9
|
-
'health': { cmd: 'helixevo health --verbose',
|
|
10
|
-
'metrics': { cmd: 'helixevo metrics --verbose',
|
|
11
|
-
'evolve': { cmd: 'helixevo evolve --verbose',
|
|
12
|
-
'evolve-dry': { cmd: 'helixevo evolve --dry-run --verbose', timeout: 300000 },
|
|
13
|
-
'generalize': { cmd: 'helixevo generalize --verbose',
|
|
14
|
-
'generalize-dry': { cmd: 'helixevo generalize --dry-run --verbose', timeout: 300000 },
|
|
15
|
-
'graph-rebuild': { cmd: 'helixevo graph --rebuild --verbose',
|
|
16
|
-
'graph-optimize': { cmd: 'helixevo graph --optimize --verbose', timeout: 300000 },
|
|
17
|
-
'research': { cmd: 'helixevo research --verbose',
|
|
18
|
-
'research-dry': { cmd: 'helixevo research --dry-run --verbose', timeout: 300000 },
|
|
19
|
-
'report': { cmd: 'helixevo report --days 7',
|
|
7
|
+
const ALLOWED_COMMANDS: Record<string, { cmd: string; args: string[]; timeout: number }> = {
|
|
8
|
+
'status': { cmd: 'helixevo', args: ['status'], timeout: 15000 },
|
|
9
|
+
'health': { cmd: 'helixevo', args: ['health', '--verbose'], timeout: 120000 },
|
|
10
|
+
'metrics': { cmd: 'helixevo', args: ['metrics', '--verbose'], timeout: 15000 },
|
|
11
|
+
'evolve': { cmd: 'helixevo', args: ['evolve', '--verbose'], timeout: 300000 },
|
|
12
|
+
'evolve-dry': { cmd: 'helixevo', args: ['evolve', '--dry-run', '--verbose'], timeout: 300000 },
|
|
13
|
+
'generalize': { cmd: 'helixevo', args: ['generalize', '--verbose'], timeout: 300000 },
|
|
14
|
+
'generalize-dry': { cmd: 'helixevo', args: ['generalize', '--dry-run', '--verbose'], timeout: 300000 },
|
|
15
|
+
'graph-rebuild': { cmd: 'helixevo', args: ['graph', '--rebuild', '--verbose'], timeout: 300000 },
|
|
16
|
+
'graph-optimize': { cmd: 'helixevo', args: ['graph', '--optimize', '--verbose'], timeout: 300000 },
|
|
17
|
+
'research': { cmd: 'helixevo', args: ['research', '--verbose'], timeout: 300000 },
|
|
18
|
+
'research-dry': { cmd: 'helixevo', args: ['research', '--dry-run', '--verbose'], timeout: 300000 },
|
|
19
|
+
'report': { cmd: 'helixevo', args: ['report', '--days', '7'], timeout: 30000 },
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// Track the currently running process so it can be stopped
|
|
23
|
+
let activeProcess: ChildProcess | null = null
|
|
24
|
+
let activeCommand: string | null = null
|
|
25
|
+
|
|
22
26
|
export async function POST(request: Request) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const { command } = body as { command: string }
|
|
27
|
+
const body = await request.json()
|
|
28
|
+
const { command } = body as { command: string }
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
const entry = ALLOWED_COMMANDS[command]
|
|
31
|
+
if (!entry) {
|
|
32
|
+
return NextResponse.json({
|
|
33
|
+
success: false,
|
|
34
|
+
error: `Unknown command: ${command}`,
|
|
35
|
+
}, { status: 400 })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Kill any existing process before starting a new one
|
|
39
|
+
if (activeProcess && !activeProcess.killed) {
|
|
40
|
+
activeProcess.kill('SIGTERM')
|
|
41
|
+
activeProcess = null
|
|
42
|
+
activeCommand = null
|
|
43
|
+
}
|
|
34
44
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
return new Promise<Response>((resolve) => {
|
|
46
|
+
let output = ''
|
|
47
|
+
let timedOut = false
|
|
48
|
+
|
|
49
|
+
const child = spawn(entry.cmd, entry.args, {
|
|
38
50
|
env: { ...process.env },
|
|
51
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
39
52
|
})
|
|
40
53
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
activeProcess = child
|
|
55
|
+
activeCommand = command
|
|
56
|
+
|
|
57
|
+
const timeout = setTimeout(() => {
|
|
58
|
+
timedOut = true
|
|
59
|
+
child.kill('SIGTERM')
|
|
60
|
+
}, entry.timeout)
|
|
61
|
+
|
|
62
|
+
child.stdout?.on('data', (data: Buffer) => {
|
|
63
|
+
output += data.toString()
|
|
45
64
|
})
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
|
|
66
|
+
child.stderr?.on('data', (data: Buffer) => {
|
|
67
|
+
output += data.toString()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
child.on('close', (code, signal) => {
|
|
71
|
+
clearTimeout(timeout)
|
|
72
|
+
activeProcess = null
|
|
73
|
+
activeCommand = null
|
|
74
|
+
|
|
75
|
+
if (signal === 'SIGTERM' && !timedOut) {
|
|
76
|
+
// Stopped by user
|
|
77
|
+
resolve(NextResponse.json({
|
|
78
|
+
success: false,
|
|
79
|
+
stopped: true,
|
|
80
|
+
output: output + '\n\n[Stopped by user]',
|
|
81
|
+
}))
|
|
82
|
+
} else if (timedOut) {
|
|
83
|
+
resolve(NextResponse.json({
|
|
84
|
+
success: false,
|
|
85
|
+
output: output + '\n\n[Timed out]',
|
|
86
|
+
}, { status: 500 }))
|
|
87
|
+
} else {
|
|
88
|
+
resolve(NextResponse.json({
|
|
89
|
+
success: code === 0,
|
|
90
|
+
command: `${entry.cmd} ${entry.args.join(' ')}`,
|
|
91
|
+
output: output || 'No output',
|
|
92
|
+
}, code === 0 ? {} : { status: 500 }))
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
child.on('error', (err) => {
|
|
97
|
+
clearTimeout(timeout)
|
|
98
|
+
activeProcess = null
|
|
99
|
+
activeCommand = null
|
|
100
|
+
resolve(NextResponse.json({
|
|
101
|
+
success: false,
|
|
102
|
+
output: err.message,
|
|
103
|
+
}, { status: 500 }))
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// DELETE = stop the running command
|
|
109
|
+
export async function DELETE() {
|
|
110
|
+
if (activeProcess && !activeProcess.killed) {
|
|
111
|
+
const cmd = activeCommand
|
|
112
|
+
activeProcess.kill('SIGTERM')
|
|
113
|
+
activeProcess = null
|
|
114
|
+
activeCommand = null
|
|
115
|
+
return NextResponse.json({ stopped: true, command: cmd })
|
|
54
116
|
}
|
|
117
|
+
return NextResponse.json({ stopped: false, message: 'No running command' })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// GET = check if something is running
|
|
121
|
+
export async function GET() {
|
|
122
|
+
return NextResponse.json({
|
|
123
|
+
running: activeProcess !== null && !activeProcess.killed,
|
|
124
|
+
command: activeCommand,
|
|
125
|
+
})
|
|
55
126
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react'
|
|
3
|
+
import { useState, useRef } from 'react'
|
|
4
4
|
|
|
5
5
|
interface Action {
|
|
6
6
|
id: string
|
|
@@ -17,12 +17,13 @@ interface QuickActionsProps {
|
|
|
17
17
|
actions: Action[]
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
type RunState = 'idle' | 'running' | 'success' | 'error'
|
|
20
|
+
type RunState = 'idle' | 'running' | 'success' | 'error' | 'stopped'
|
|
21
21
|
|
|
22
22
|
export function QuickActions({ actions }: QuickActionsProps) {
|
|
23
23
|
const [running, setRunning] = useState<string | null>(null)
|
|
24
24
|
const [state, setState] = useState<RunState>('idle')
|
|
25
25
|
const [output, setOutput] = useState<string | null>(null)
|
|
26
|
+
const abortRef = useRef<AbortController | null>(null)
|
|
26
27
|
|
|
27
28
|
const handleRun = async (action: Action) => {
|
|
28
29
|
if (action.disabled) return
|
|
@@ -30,19 +31,48 @@ export function QuickActions({ actions }: QuickActionsProps) {
|
|
|
30
31
|
setState('running')
|
|
31
32
|
setOutput(null)
|
|
32
33
|
|
|
34
|
+
const controller = new AbortController()
|
|
35
|
+
abortRef.current = controller
|
|
36
|
+
|
|
33
37
|
try {
|
|
34
38
|
const res = await fetch('/api/run', {
|
|
35
39
|
method: 'POST',
|
|
36
40
|
headers: { 'Content-Type': 'application/json' },
|
|
37
41
|
body: JSON.stringify({ command: action.command }),
|
|
42
|
+
signal: controller.signal,
|
|
38
43
|
})
|
|
39
44
|
const data = await res.json()
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
if (data.stopped) {
|
|
46
|
+
setOutput(data.output ?? 'Stopped')
|
|
47
|
+
setState('stopped')
|
|
48
|
+
} else {
|
|
49
|
+
setOutput(data.output ?? data.error ?? 'No output')
|
|
50
|
+
setState(data.success ? 'success' : 'error')
|
|
51
|
+
}
|
|
52
|
+
} catch (err: unknown) {
|
|
53
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
54
|
+
// Client-side abort — also kill server-side process
|
|
55
|
+
try { await fetch('/api/run', { method: 'DELETE' }) } catch {}
|
|
56
|
+
setOutput('Stopped by user')
|
|
57
|
+
setState('stopped')
|
|
58
|
+
} else {
|
|
59
|
+
setOutput('Network error')
|
|
60
|
+
setState('error')
|
|
61
|
+
}
|
|
62
|
+
} finally {
|
|
63
|
+
abortRef.current = null
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const handleStop = async () => {
|
|
68
|
+
// Abort the fetch request
|
|
69
|
+
if (abortRef.current) {
|
|
70
|
+
abortRef.current.abort()
|
|
45
71
|
}
|
|
72
|
+
// Also kill server-side process
|
|
73
|
+
try {
|
|
74
|
+
await fetch('/api/run', { method: 'DELETE' })
|
|
75
|
+
} catch {}
|
|
46
76
|
}
|
|
47
77
|
|
|
48
78
|
const handleClose = () => {
|
|
@@ -51,6 +81,21 @@ export function QuickActions({ actions }: QuickActionsProps) {
|
|
|
51
81
|
setOutput(null)
|
|
52
82
|
}
|
|
53
83
|
|
|
84
|
+
const stateColor = state === 'success' ? 'var(--green)'
|
|
85
|
+
: state === 'error' ? 'var(--red)'
|
|
86
|
+
: state === 'stopped' ? 'var(--yellow)'
|
|
87
|
+
: 'var(--text-secondary)'
|
|
88
|
+
|
|
89
|
+
const stateBorderColor = state === 'success' ? 'var(--green-border)'
|
|
90
|
+
: state === 'error' ? 'var(--red-border)'
|
|
91
|
+
: state === 'stopped' ? 'var(--yellow-border)'
|
|
92
|
+
: 'var(--border)'
|
|
93
|
+
|
|
94
|
+
const stateBgColor = state === 'success' ? 'var(--green-light)'
|
|
95
|
+
: state === 'error' ? 'var(--red-light)'
|
|
96
|
+
: state === 'stopped' ? 'var(--yellow-light)'
|
|
97
|
+
: 'var(--bg-section)'
|
|
98
|
+
|
|
54
99
|
return (
|
|
55
100
|
<>
|
|
56
101
|
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
|
@@ -93,12 +138,12 @@ export function QuickActions({ actions }: QuickActionsProps) {
|
|
|
93
138
|
))}
|
|
94
139
|
</div>
|
|
95
140
|
|
|
96
|
-
{/* Output
|
|
141
|
+
{/* Output panel */}
|
|
97
142
|
{running && state !== 'idle' && (
|
|
98
143
|
<div style={{
|
|
99
144
|
marginTop: 12,
|
|
100
145
|
background: 'var(--bg-card)',
|
|
101
|
-
border: `1px solid ${
|
|
146
|
+
border: `1px solid ${stateBorderColor}`,
|
|
102
147
|
borderRadius: 'var(--radius-lg)',
|
|
103
148
|
overflow: 'hidden',
|
|
104
149
|
}}>
|
|
@@ -106,9 +151,7 @@ export function QuickActions({ actions }: QuickActionsProps) {
|
|
|
106
151
|
<div style={{
|
|
107
152
|
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
|
108
153
|
padding: '10px 14px',
|
|
109
|
-
background:
|
|
110
|
-
: state === 'success' ? 'var(--green-light)'
|
|
111
|
-
: 'var(--red-light)',
|
|
154
|
+
background: stateBgColor,
|
|
112
155
|
borderBottom: '1px solid var(--border)',
|
|
113
156
|
}}>
|
|
114
157
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, fontWeight: 600 }}>
|
|
@@ -121,32 +164,46 @@ export function QuickActions({ actions }: QuickActionsProps) {
|
|
|
121
164
|
)}
|
|
122
165
|
{state === 'success' && <span style={{ color: 'var(--green)' }}>✓</span>}
|
|
123
166
|
{state === 'error' && <span style={{ color: 'var(--red)' }}>✗</span>}
|
|
124
|
-
<span style={{
|
|
125
|
-
|
|
126
|
-
}}>
|
|
167
|
+
{state === 'stopped' && <span style={{ color: 'var(--yellow)' }}>■</span>}
|
|
168
|
+
<span style={{ color: stateColor }}>
|
|
127
169
|
{state === 'running' ? `Running ${actions.find(a => a.id === running)?.label}...`
|
|
128
170
|
: state === 'success' ? 'Completed'
|
|
171
|
+
: state === 'stopped' ? 'Stopped'
|
|
129
172
|
: 'Failed'}
|
|
130
173
|
</span>
|
|
131
174
|
</div>
|
|
132
|
-
{
|
|
133
|
-
|
|
134
|
-
<button onClick={
|
|
135
|
-
background: '
|
|
136
|
-
padding: '3px
|
|
137
|
-
|
|
138
|
-
}}>
|
|
139
|
-
Refresh
|
|
140
|
-
</button>
|
|
141
|
-
<button onClick={handleClose} style={{
|
|
142
|
-
background: 'none', border: '1px solid var(--border)', borderRadius: 'var(--radius)',
|
|
143
|
-
padding: '3px 10px', fontSize: 11, fontWeight: 600, cursor: 'pointer',
|
|
144
|
-
color: 'var(--text-secondary)',
|
|
175
|
+
<div style={{ display: 'flex', gap: 6 }}>
|
|
176
|
+
{state === 'running' && (
|
|
177
|
+
<button onClick={handleStop} style={{
|
|
178
|
+
background: 'var(--red)', color: '#fff', border: 'none', borderRadius: 'var(--radius)',
|
|
179
|
+
padding: '3px 12px', fontSize: 11, fontWeight: 600, cursor: 'pointer',
|
|
180
|
+
display: 'flex', alignItems: 'center', gap: 4,
|
|
145
181
|
}}>
|
|
146
|
-
|
|
182
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
|
183
|
+
<rect x="4" y="4" width="16" height="16" rx="2" />
|
|
184
|
+
</svg>
|
|
185
|
+
Stop
|
|
147
186
|
</button>
|
|
148
|
-
|
|
149
|
-
|
|
187
|
+
)}
|
|
188
|
+
{state !== 'running' && (
|
|
189
|
+
<>
|
|
190
|
+
<button onClick={() => { window.location.reload() }} style={{
|
|
191
|
+
background: 'none', border: '1px solid var(--border)', borderRadius: 'var(--radius)',
|
|
192
|
+
padding: '3px 10px', fontSize: 11, fontWeight: 600, cursor: 'pointer',
|
|
193
|
+
color: 'var(--text-secondary)',
|
|
194
|
+
}}>
|
|
195
|
+
Refresh
|
|
196
|
+
</button>
|
|
197
|
+
<button onClick={handleClose} style={{
|
|
198
|
+
background: 'none', border: '1px solid var(--border)', borderRadius: 'var(--radius)',
|
|
199
|
+
padding: '3px 10px', fontSize: 11, fontWeight: 600, cursor: 'pointer',
|
|
200
|
+
color: 'var(--text-secondary)',
|
|
201
|
+
}}>
|
|
202
|
+
Close
|
|
203
|
+
</button>
|
|
204
|
+
</>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
150
207
|
</div>
|
|
151
208
|
|
|
152
209
|
{/* Output */}
|
|
@@ -56,23 +56,47 @@ export function UpdateBanner({ currentVersion }: { currentVersion: string }) {
|
|
|
56
56
|
if (data.success) {
|
|
57
57
|
setState('success')
|
|
58
58
|
setNewVersion(data.version)
|
|
59
|
-
|
|
59
|
+
|
|
60
|
+
// Step 1: Tell server to restart (exits with code 75)
|
|
60
61
|
setTimeout(async () => {
|
|
61
62
|
try {
|
|
62
63
|
await fetch('/api/restart', { method: 'POST' })
|
|
63
64
|
} catch {}
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
|
|
66
|
+
// Step 2: Wait for old server to go DOWN (fetch should fail)
|
|
67
|
+
let serverDown = false
|
|
68
|
+
for (let i = 0; i < 20; i++) {
|
|
69
|
+
await new Promise(r => setTimeout(r, 1000))
|
|
70
|
+
try {
|
|
71
|
+
await fetch('/api/restart', { method: 'GET', signal: AbortSignal.timeout(1000) })
|
|
72
|
+
// Still up — keep waiting
|
|
73
|
+
} catch {
|
|
74
|
+
serverDown = true
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Step 3: Wait for new server to come UP
|
|
80
|
+
if (serverDown) {
|
|
81
|
+
await new Promise(r => setTimeout(r, 2000)) // give it time to start
|
|
82
|
+
}
|
|
83
|
+
for (let i = 0; i < 40; i++) {
|
|
84
|
+
await new Promise(r => setTimeout(r, 1500))
|
|
66
85
|
try {
|
|
67
|
-
const res = await fetch('
|
|
86
|
+
const res = await fetch('/?_t=' + Date.now(), {
|
|
87
|
+
cache: 'no-store',
|
|
88
|
+
signal: AbortSignal.timeout(2000),
|
|
89
|
+
})
|
|
68
90
|
if (res.ok) {
|
|
69
|
-
|
|
70
|
-
window.location.
|
|
91
|
+
// Step 4: Hard reload with cache busting
|
|
92
|
+
window.location.href = window.location.pathname + '?updated=' + Date.now()
|
|
93
|
+
return
|
|
71
94
|
}
|
|
72
95
|
} catch {}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Fallback: force reload anyway
|
|
99
|
+
window.location.href = window.location.pathname + '?updated=' + Date.now()
|
|
76
100
|
}, 1500)
|
|
77
101
|
} else {
|
|
78
102
|
setState('error')
|
package/dist/cli.js
CHANGED
|
@@ -12405,15 +12405,22 @@ function launchDashboard(dir, openBrowser) {
|
|
|
12405
12405
|
console.log(`
|
|
12406
12406
|
\uD83D\uDD04 Restarting dashboard after update...
|
|
12407
12407
|
`);
|
|
12408
|
-
|
|
12409
|
-
|
|
12410
|
-
|
|
12411
|
-
|
|
12412
|
-
|
|
12413
|
-
|
|
12414
|
-
|
|
12408
|
+
setTimeout(() => {
|
|
12409
|
+
const nextCache = join15(dir, ".next");
|
|
12410
|
+
if (existsSync10(nextCache)) {
|
|
12411
|
+
try {
|
|
12412
|
+
rmSync2(nextCache, { recursive: true });
|
|
12413
|
+
} catch {}
|
|
12414
|
+
}
|
|
12415
|
+
ensureSkillGraph();
|
|
12416
|
+
let updatedVersion = currentVersion;
|
|
12417
|
+
try {
|
|
12418
|
+
updatedVersion = execSync2("helixevo --version 2>/dev/null", { encoding: "utf-8" }).trim() || currentVersion;
|
|
12419
|
+
} catch {}
|
|
12420
|
+
console.log(` \uD83C\uDF10 HelixEvo Dashboard v${updatedVersion} at http://localhost:3847
|
|
12415
12421
|
`);
|
|
12416
|
-
|
|
12422
|
+
launchDashboard(dir, false);
|
|
12423
|
+
}, 2000);
|
|
12417
12424
|
} else {
|
|
12418
12425
|
process.exit(code ?? 0);
|
|
12419
12426
|
}
|
package/package.json
CHANGED