helixevo 0.2.14 → 0.2.16

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 { execSync } from 'child_process'
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', timeout: 15000 },
9
- 'health': { cmd: 'helixevo health --verbose', timeout: 120000 },
10
- 'metrics': { cmd: 'helixevo metrics --verbose', timeout: 15000 },
11
- 'evolve': { cmd: 'helixevo evolve --verbose', timeout: 300000 },
12
- 'evolve-dry': { cmd: 'helixevo evolve --dry-run --verbose', timeout: 300000 },
13
- 'generalize': { cmd: 'helixevo generalize --verbose', timeout: 300000 },
14
- 'generalize-dry': { cmd: 'helixevo generalize --dry-run --verbose', timeout: 300000 },
15
- 'graph-rebuild': { cmd: 'helixevo graph --rebuild --verbose', timeout: 300000 },
16
- 'graph-optimize': { cmd: 'helixevo graph --optimize --verbose', timeout: 300000 },
17
- 'research': { cmd: 'helixevo research --verbose', timeout: 300000 },
18
- 'research-dry': { cmd: 'helixevo research --dry-run --verbose', timeout: 300000 },
19
- 'report': { cmd: 'helixevo report --days 7', timeout: 30000 },
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
- try {
24
- const body = await request.json()
25
- const { command } = body as { command: string }
27
+ const body = await request.json()
28
+ const { command } = body as { command: string }
26
29
 
27
- const entry = ALLOWED_COMMANDS[command]
28
- if (!entry) {
29
- return NextResponse.json({
30
- success: false,
31
- error: `Unknown command: ${command}`,
32
- }, { status: 400 })
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
- const output = execSync(`${entry.cmd} 2>&1`, {
36
- encoding: 'utf-8',
37
- timeout: entry.timeout,
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
- return NextResponse.json({
42
- success: true,
43
- command: entry.cmd,
44
- output,
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
- } catch (err: unknown) {
47
- const message = err instanceof Error ? err.message : String(err)
48
- // execSync errors include stdout in the message
49
- const stdout = (err as { stdout?: string })?.stdout ?? ''
50
- return NextResponse.json({
51
- success: false,
52
- output: stdout || message.slice(-2000),
53
- }, { status: 500 })
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
- setOutput(data.output ?? data.error ?? 'No output')
41
- setState(data.success ? 'success' : 'error')
42
- } catch {
43
- setOutput('Network error')
44
- setState('error')
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 modal */}
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 ${state === 'success' ? 'var(--green-border)' : state === 'error' ? 'var(--red-border)' : 'var(--border)'}`,
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: state === 'running' ? 'var(--bg-section)'
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)' }}>&#10003;</span>}
123
166
  {state === 'error' && <span style={{ color: 'var(--red)' }}>&#10007;</span>}
124
- <span style={{
125
- color: state === 'success' ? 'var(--green)' : state === 'error' ? 'var(--red)' : 'var(--text-secondary)',
126
- }}>
167
+ {state === 'stopped' && <span style={{ color: 'var(--yellow)' }}>&#9632;</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
- {state !== 'running' && (
133
- <div style={{ display: 'flex', gap: 6 }}>
134
- <button onClick={() => { window.location.reload() }} style={{
135
- background: 'none', border: '1px solid var(--border)', borderRadius: 'var(--radius)',
136
- padding: '3px 10px', fontSize: 11, fontWeight: 600, cursor: 'pointer',
137
- color: 'var(--text-secondary)',
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
- Close
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
- </div>
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 */}
@@ -1,18 +1,34 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/postinstall.ts
4
- import { createRequire } from "node:module";
4
+ import { readFileSync } from "node:fs";
5
5
  import { join, dirname } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
- var require2 = createRequire(import.meta.url);
8
- var pkg = require2(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"));
7
+ var __dirname = "/Users/tianchichen/Documents/GitHub/helixevo/src";
8
+ var version = "unknown";
9
+ try {
10
+ const candidates = [
11
+ join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"),
12
+ join(process.cwd(), "package.json"),
13
+ join(__dirname, "..", "package.json")
14
+ ];
15
+ for (const p of candidates) {
16
+ try {
17
+ const pkg = JSON.parse(readFileSync(p, "utf-8"));
18
+ if (pkg.name === "helixevo" && pkg.version) {
19
+ version = pkg.version;
20
+ break;
21
+ }
22
+ } catch {}
23
+ }
24
+ } catch {}
9
25
  var green = "\x1B[32m";
10
26
  var cyan = "\x1B[36m";
11
27
  var bold = "\x1B[1m";
12
28
  var dim = "\x1B[2m";
13
29
  var reset = "\x1B[0m";
14
30
  console.log();
15
- console.log(` ${green}✓${reset} ${bold}HelixEvo v${pkg.version}${reset} installed successfully`);
31
+ console.log(` ${green}✓${reset} ${bold}HelixEvo v${version}${reset} installed successfully`);
16
32
  console.log();
17
33
  console.log(` ${dim}Get started:${reset}`);
18
34
  console.log(` ${cyan}helixevo init${reset} Import skills + generate skill tests`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helixevo",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
4
4
  "description": "Self-evolving skill ecosystem for AI agents. Skills and projects co-evolve through multi-judge evaluation and a Pareto frontier.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,7 +24,7 @@
24
24
  "typecheck": "tsc --noEmit",
25
25
  "prepare": "npm run build",
26
26
  "prepublishOnly": "npm run typecheck && npm run build",
27
- "postinstall": "node dist/postinstall.js 2>/dev/null || true"
27
+ "postinstall": "node dist/postinstall.js || true"
28
28
  },
29
29
  "dependencies": {
30
30
  "commander": "^13.1.0",