helixevo 0.2.11 → 0.2.12

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.
@@ -0,0 +1,37 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { execSync } from 'child_process'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ export async function POST() {
7
+ try {
8
+ // Run the upgrade command
9
+ const output = execSync('npm install -g helixevo@latest 2>&1', {
10
+ encoding: 'utf-8',
11
+ timeout: 120000, // 2 minute timeout
12
+ env: { ...process.env },
13
+ })
14
+
15
+ // Get the new version
16
+ let newVersion = ''
17
+ try {
18
+ newVersion = execSync('helixevo --version 2>/dev/null', { encoding: 'utf-8' }).trim()
19
+ } catch {
20
+ // Try parsing from output
21
+ const match = output.match(/helixevo@(\d+\.\d+\.\d+)/)
22
+ newVersion = match?.[1] ?? 'unknown'
23
+ }
24
+
25
+ return NextResponse.json({
26
+ success: true,
27
+ version: newVersion,
28
+ output: output.slice(-500), // last 500 chars
29
+ })
30
+ } catch (err: unknown) {
31
+ const message = err instanceof Error ? err.message : String(err)
32
+ return NextResponse.json({
33
+ success: false,
34
+ error: message.slice(-500),
35
+ }, { status: 500 })
36
+ }
37
+ }
@@ -15,10 +15,14 @@ function compareVersions(current: string, latest: string): boolean {
15
15
  return false
16
16
  }
17
17
 
18
+ type UpdateState = 'idle' | 'updating' | 'success' | 'error'
19
+
18
20
  export function UpdateBanner({ currentVersion }: { currentVersion: string }) {
19
21
  const [latestVersion, setLatestVersion] = useState<string | null>(null)
20
22
  const [dismissed, setDismissed] = useState(false)
21
- const [copied, setCopied] = useState(false)
23
+ const [state, setState] = useState<UpdateState>('idle')
24
+ const [newVersion, setNewVersion] = useState<string | null>(null)
25
+ const [errorMsg, setErrorMsg] = useState<string | null>(null)
22
26
 
23
27
  useEffect(() => {
24
28
  let mounted = true
@@ -41,14 +45,29 @@ export function UpdateBanner({ currentVersion }: { currentVersion: string }) {
41
45
 
42
46
  if (!latestVersion || dismissed) return null
43
47
 
44
- const command = 'npm install -g helixevo@latest'
48
+ const handleUpdate = async () => {
49
+ setState('updating')
50
+ setErrorMsg(null)
45
51
 
46
- const handleCopy = async () => {
47
52
  try {
48
- await navigator.clipboard.writeText(command)
49
- setCopied(true)
50
- setTimeout(() => setCopied(false), 2000)
51
- } catch {}
53
+ const res = await fetch('/api/upgrade', { method: 'POST' })
54
+ const data = await res.json()
55
+
56
+ if (data.success) {
57
+ setState('success')
58
+ setNewVersion(data.version)
59
+ // Reload after a brief delay to show success state
60
+ setTimeout(() => {
61
+ window.location.reload()
62
+ }, 2000)
63
+ } else {
64
+ setState('error')
65
+ setErrorMsg(data.error ?? 'Update failed')
66
+ }
67
+ } catch (err) {
68
+ setState('error')
69
+ setErrorMsg('Network error — try running: npm install -g helixevo@latest')
70
+ }
52
71
  }
53
72
 
54
73
  return (
@@ -58,7 +77,7 @@ export function UpdateBanner({ currentVersion }: { currentVersion: string }) {
58
77
  right: 20,
59
78
  width: 320,
60
79
  background: 'var(--bg-card)',
61
- border: '1px solid var(--purple-border)',
80
+ border: `1px solid ${state === 'success' ? 'var(--green-border)' : state === 'error' ? 'var(--red-border)' : 'var(--purple-border)'}`,
62
81
  borderRadius: 'var(--radius-lg)',
63
82
  boxShadow: 'var(--shadow-xl)',
64
83
  padding: '16px 18px',
@@ -66,83 +85,168 @@ export function UpdateBanner({ currentVersion }: { currentVersion: string }) {
66
85
  animation: 'updateSlideIn 0.4s ease-out',
67
86
  }}>
68
87
  {/* Close button */}
69
- <button
70
- onClick={() => setDismissed(true)}
71
- style={{
72
- position: 'absolute', top: 8, right: 10,
73
- background: 'none', border: 'none', cursor: 'pointer',
74
- color: 'var(--text-dim)', fontSize: 18, lineHeight: 1,
75
- padding: '2px 4px',
76
- }}
77
- aria-label="Dismiss"
78
- >
79
- &times;
80
- </button>
88
+ {state !== 'updating' && (
89
+ <button
90
+ onClick={() => setDismissed(true)}
91
+ style={{
92
+ position: 'absolute', top: 8, right: 10,
93
+ background: 'none', border: 'none', cursor: 'pointer',
94
+ color: 'var(--text-dim)', fontSize: 18, lineHeight: 1,
95
+ padding: '2px 4px',
96
+ }}
97
+ aria-label="Dismiss"
98
+ >
99
+ &times;
100
+ </button>
101
+ )}
81
102
 
82
103
  {/* Header */}
83
- <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
104
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
84
105
  <div style={{
85
106
  width: 28, height: 28, borderRadius: '50%',
86
- background: 'var(--purple-light)', display: 'flex',
107
+ background: state === 'success' ? 'var(--green-light)'
108
+ : state === 'error' ? 'var(--red-light)'
109
+ : 'var(--purple-light)',
110
+ display: 'flex',
87
111
  alignItems: 'center', justifyContent: 'center',
88
112
  }}>
89
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--purple)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
90
- <path d="M12 19V5m-7 7l7-7 7 7" />
91
- </svg>
113
+ {state === 'updating' ? (
114
+ <div style={{
115
+ width: 14, height: 14, border: '2px solid var(--purple-border)',
116
+ borderTopColor: 'var(--purple)', borderRadius: '50%',
117
+ animation: 'updateSpin 0.8s linear infinite',
118
+ }} />
119
+ ) : state === 'success' ? (
120
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--green)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
121
+ <path d="M20 6L9 17l-5-5" />
122
+ </svg>
123
+ ) : state === 'error' ? (
124
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--red)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
125
+ <path d="M18 6L6 18M6 6l12 12" />
126
+ </svg>
127
+ ) : (
128
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--purple)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
129
+ <path d="M12 19V5m-7 7l7-7 7 7" />
130
+ </svg>
131
+ )}
92
132
  </div>
93
133
  <div>
94
134
  <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)', letterSpacing: -0.2 }}>
95
- Update Available
135
+ {state === 'updating' ? 'Updating...'
136
+ : state === 'success' ? 'Updated!'
137
+ : state === 'error' ? 'Update Failed'
138
+ : 'Update Available'}
96
139
  </div>
97
140
  <div style={{ fontSize: 11, color: 'var(--text-dim)' }}>
98
- v{currentVersion} &rarr; <span style={{ color: 'var(--green)', fontWeight: 600 }}>v{latestVersion}</span>
141
+ {state === 'success'
142
+ ? <>Now on <span style={{ color: 'var(--green)', fontWeight: 600 }}>v{newVersion}</span> — restarting...</>
143
+ : <>v{currentVersion} &rarr; <span style={{ color: 'var(--green)', fontWeight: 600 }}>v{latestVersion}</span></>
144
+ }
99
145
  </div>
100
146
  </div>
101
147
  </div>
102
148
 
103
- {/* Command */}
104
- <div
105
- onClick={handleCopy}
106
- style={{
149
+ {/* Action area */}
150
+ {state === 'idle' && (
151
+ <button
152
+ onClick={handleUpdate}
153
+ style={{
154
+ width: '100%',
155
+ padding: '9px 16px',
156
+ background: 'var(--purple)',
157
+ color: '#fff',
158
+ border: 'none',
159
+ borderRadius: 'var(--radius)',
160
+ fontSize: 13,
161
+ fontWeight: 600,
162
+ cursor: 'pointer',
163
+ display: 'flex',
164
+ alignItems: 'center',
165
+ justifyContent: 'center',
166
+ gap: 6,
167
+ transition: 'opacity 0.15s',
168
+ }}
169
+ onMouseOver={e => (e.currentTarget.style.opacity = '0.9')}
170
+ onMouseOut={e => (e.currentTarget.style.opacity = '1')}
171
+ >
172
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
173
+ <path d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9" />
174
+ </svg>
175
+ Update Now
176
+ </button>
177
+ )}
178
+
179
+ {state === 'updating' && (
180
+ <div style={{
181
+ width: '100%',
182
+ padding: '9px 16px',
107
183
  background: 'var(--bg-section)',
108
- border: '1px solid var(--border)',
109
184
  borderRadius: 'var(--radius)',
110
- padding: '8px 12px',
111
- fontFamily: 'var(--font-mono)',
112
- fontSize: 11,
185
+ fontSize: 12,
113
186
  color: 'var(--text-secondary)',
114
- cursor: 'pointer',
115
- display: 'flex',
116
- alignItems: 'center',
117
- justifyContent: 'space-between',
118
- gap: 8,
119
- transition: 'border-color 0.15s',
120
- }}
121
- title="Click to copy"
122
- >
123
- <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
124
- $ {command}
125
- </span>
126
- <span style={{
127
- fontSize: 10, fontFamily: 'var(--font)', fontWeight: 600,
128
- color: copied ? 'var(--green)' : 'var(--purple)',
129
- whiteSpace: 'nowrap',
130
- flexShrink: 0,
187
+ textAlign: 'center',
131
188
  }}>
132
- {copied ? 'Copied!' : 'Copy'}
133
- </span>
134
- </div>
189
+ Installing helixevo@latest...
190
+ </div>
191
+ )}
135
192
 
136
- {/* Subtle hint */}
137
- <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 8, textAlign: 'center' }}>
138
- Run in terminal, then refresh dashboard
139
- </div>
193
+ {state === 'success' && (
194
+ <div style={{
195
+ width: '100%',
196
+ padding: '9px 16px',
197
+ background: 'var(--green-light)',
198
+ borderRadius: 'var(--radius)',
199
+ fontSize: 12,
200
+ color: 'var(--green)',
201
+ textAlign: 'center',
202
+ fontWeight: 600,
203
+ }}>
204
+ Restarting dashboard...
205
+ </div>
206
+ )}
207
+
208
+ {state === 'error' && (
209
+ <>
210
+ <div style={{
211
+ width: '100%',
212
+ padding: '8px 12px',
213
+ background: 'var(--red-light)',
214
+ borderRadius: 'var(--radius)',
215
+ fontSize: 11,
216
+ color: 'var(--red)',
217
+ marginBottom: 8,
218
+ maxHeight: 60,
219
+ overflow: 'auto',
220
+ }}>
221
+ {errorMsg}
222
+ </div>
223
+ <button
224
+ onClick={handleUpdate}
225
+ style={{
226
+ width: '100%',
227
+ padding: '7px 12px',
228
+ background: 'var(--bg-section)',
229
+ color: 'var(--text-secondary)',
230
+ border: '1px solid var(--border)',
231
+ borderRadius: 'var(--radius)',
232
+ fontSize: 12,
233
+ fontWeight: 600,
234
+ cursor: 'pointer',
235
+ }}
236
+ >
237
+ Retry
238
+ </button>
239
+ </>
240
+ )}
140
241
 
141
242
  <style>{`
142
243
  @keyframes updateSlideIn {
143
244
  from { opacity: 0; transform: translateY(16px) scale(0.97); }
144
245
  to { opacity: 1; transform: translateY(0) scale(1); }
145
246
  }
247
+ @keyframes updateSpin {
248
+ to { transform: rotate(360deg); }
249
+ }
146
250
  `}</style>
147
251
  </div>
148
252
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helixevo",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
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": {