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 [
|
|
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
|
|
48
|
+
const handleUpdate = async () => {
|
|
49
|
+
setState('updating')
|
|
50
|
+
setErrorMsg(null)
|
|
45
51
|
|
|
46
|
-
const handleCopy = async () => {
|
|
47
52
|
try {
|
|
48
|
-
await
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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:
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
+
×
|
|
100
|
+
</button>
|
|
101
|
+
)}
|
|
81
102
|
|
|
82
103
|
{/* Header */}
|
|
83
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom:
|
|
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(--
|
|
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
|
-
|
|
90
|
-
<
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
141
|
+
{state === 'success'
|
|
142
|
+
? <>Now on <span style={{ color: 'var(--green)', fontWeight: 600 }}>v{newVersion}</span> — restarting...</>
|
|
143
|
+
: <>v{currentVersion} → <span style={{ color: 'var(--green)', fontWeight: 600 }}>v{latestVersion}</span></>
|
|
144
|
+
}
|
|
99
145
|
</div>
|
|
100
146
|
</div>
|
|
101
147
|
</div>
|
|
102
148
|
|
|
103
|
-
{/*
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
111
|
-
fontFamily: 'var(--font-mono)',
|
|
112
|
-
fontSize: 11,
|
|
185
|
+
fontSize: 12,
|
|
113
186
|
color: 'var(--text-secondary)',
|
|
114
|
-
|
|
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
|
-
|
|
133
|
-
</
|
|
134
|
-
|
|
189
|
+
Installing helixevo@latest...
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
135
192
|
|
|
136
|
-
{
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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