helixevo 0.2.11 → 0.2.13
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.
- package/dashboard/app/api/run/route.ts +55 -0
- package/dashboard/app/api/upgrade/route.ts +37 -0
- package/dashboard/app/commands/page.tsx +432 -0
- package/dashboard/app/layout.tsx +1 -0
- package/dashboard/app/page.tsx +15 -1
- package/dashboard/components/overview-actions.tsx +74 -0
- package/dashboard/components/quick-actions.tsx +177 -0
- package/dashboard/components/update-banner.tsx +163 -59
- package/package.json +1 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { execSync } from 'child_process'
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
5
|
+
|
|
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 },
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function POST(request: Request) {
|
|
23
|
+
try {
|
|
24
|
+
const body = await request.json()
|
|
25
|
+
const { command } = body as { command: string }
|
|
26
|
+
|
|
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
|
+
}
|
|
34
|
+
|
|
35
|
+
const output = execSync(`${entry.cmd} 2>&1`, {
|
|
36
|
+
encoding: 'utf-8',
|
|
37
|
+
timeout: entry.timeout,
|
|
38
|
+
env: { ...process.env },
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
return NextResponse.json({
|
|
42
|
+
success: true,
|
|
43
|
+
command: entry.cmd,
|
|
44
|
+
output,
|
|
45
|
+
})
|
|
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 })
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { QuickActions } from '@/components/quick-actions'
|
|
5
|
+
|
|
6
|
+
interface CommandInfo {
|
|
7
|
+
name: string
|
|
8
|
+
description: string
|
|
9
|
+
usage: string
|
|
10
|
+
examples: { cmd: string; desc: string }[]
|
|
11
|
+
options?: { flag: string; desc: string }[]
|
|
12
|
+
category: 'core' | 'evolution' | 'network' | 'analysis' | 'system'
|
|
13
|
+
runnable?: { command: string; label: string; icon: string; color: string }
|
|
14
|
+
needsLLM: boolean
|
|
15
|
+
note?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const COMMANDS: CommandInfo[] = [
|
|
19
|
+
{
|
|
20
|
+
name: 'watch',
|
|
21
|
+
description: 'Always-on learning mode. Watches for corrections in real-time, captures failures automatically, and triggers evolution when enough failures accumulate. This is the primary way to use HelixEvo day-to-day.',
|
|
22
|
+
usage: 'helixevo watch [options]',
|
|
23
|
+
examples: [
|
|
24
|
+
{ cmd: 'helixevo watch', desc: 'Start watching with default settings' },
|
|
25
|
+
{ cmd: 'helixevo watch --project myapp', desc: 'Watch with project context for specialized learning' },
|
|
26
|
+
{ cmd: 'helixevo watch --no-evolve', desc: 'Capture corrections only, skip auto-evolution' },
|
|
27
|
+
{ cmd: 'helixevo watch --verbose', desc: 'Show detailed capture and metrics output' },
|
|
28
|
+
],
|
|
29
|
+
options: [
|
|
30
|
+
{ flag: '--project <name>', desc: 'Associate captured failures with a specific project' },
|
|
31
|
+
{ flag: '--events <path>', desc: 'Path to events.jsonl (default: ./events.jsonl)' },
|
|
32
|
+
{ flag: '--verbose', desc: 'Show detailed capture and metrics' },
|
|
33
|
+
{ flag: '--no-evolve', desc: 'Disable auto-evolution (capture only)' },
|
|
34
|
+
],
|
|
35
|
+
category: 'core',
|
|
36
|
+
needsLLM: true,
|
|
37
|
+
note: 'This is a long-running process. It monitors your events.jsonl for new corrections.',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'init',
|
|
41
|
+
description: 'Import existing skills from ~/.agents/skills and Craft Agent workspaces. Generates skill tests and builds an initial skill graph for the dashboard.',
|
|
42
|
+
usage: 'helixevo init [options]',
|
|
43
|
+
examples: [
|
|
44
|
+
{ cmd: 'helixevo init', desc: 'Import from default paths and generate skill tests' },
|
|
45
|
+
{ cmd: 'helixevo init --skip-tests', desc: 'Import skills without generating tests (faster)' },
|
|
46
|
+
{ cmd: 'helixevo init --skills-paths ~/my-skills', desc: 'Import from a custom directory' },
|
|
47
|
+
],
|
|
48
|
+
options: [
|
|
49
|
+
{ flag: '--skills-paths <paths...>', desc: 'Custom paths to scan for existing skills' },
|
|
50
|
+
{ flag: '--skip-tests', desc: 'Skip skill test generation (faster but no regression testing)' },
|
|
51
|
+
],
|
|
52
|
+
category: 'core',
|
|
53
|
+
needsLLM: true,
|
|
54
|
+
note: 'Run this once after installing HelixEvo. Re-running is safe — existing skills are skipped.',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'evolve',
|
|
58
|
+
description: 'The core evolution loop. Analyzes captured failures, proposes skill improvements, evaluates them with multi-judge consensus, runs regression tests, and applies winning changes to the Pareto frontier.',
|
|
59
|
+
usage: 'helixevo evolve [options]',
|
|
60
|
+
examples: [
|
|
61
|
+
{ cmd: 'helixevo evolve', desc: 'Run evolution with default settings' },
|
|
62
|
+
{ cmd: 'helixevo evolve --dry-run', desc: 'Preview proposals without applying changes' },
|
|
63
|
+
{ cmd: 'helixevo evolve --verbose', desc: 'Show detailed LLM interactions and judge reasoning' },
|
|
64
|
+
{ cmd: 'helixevo evolve --max-proposals 3', desc: 'Limit to 3 proposals per run' },
|
|
65
|
+
],
|
|
66
|
+
options: [
|
|
67
|
+
{ flag: '--dry-run', desc: 'Show proposals without applying them' },
|
|
68
|
+
{ flag: '--verbose', desc: 'Show detailed LLM interactions' },
|
|
69
|
+
{ flag: '--max-proposals <n>', desc: 'Maximum proposals per run (default: 5)' },
|
|
70
|
+
],
|
|
71
|
+
category: 'evolution',
|
|
72
|
+
needsLLM: true,
|
|
73
|
+
runnable: { command: 'evolve', label: 'Run Evolve', icon: 'M13 7h8m0 0v8m0-8l-8 8-4-4-6 6', color: 'var(--green)' },
|
|
74
|
+
note: 'Requires at least 5 captured failures. Use "watch" or "capture" to collect them first.',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'generalize',
|
|
78
|
+
description: 'Promotes cross-skill patterns to a higher abstraction layer. Detects when multiple skills share common patterns and creates parent skills that capture the shared knowledge. This is how skills "level up".',
|
|
79
|
+
usage: 'helixevo generalize [options]',
|
|
80
|
+
examples: [
|
|
81
|
+
{ cmd: 'helixevo generalize', desc: 'Detect and promote cross-skill patterns' },
|
|
82
|
+
{ cmd: 'helixevo generalize --dry-run', desc: 'Preview candidates without applying' },
|
|
83
|
+
{ cmd: 'helixevo generalize --verbose', desc: 'Show detailed pattern analysis' },
|
|
84
|
+
],
|
|
85
|
+
options: [
|
|
86
|
+
{ flag: '--dry-run', desc: 'Show candidates without applying' },
|
|
87
|
+
{ flag: '--verbose', desc: 'Show detailed analysis' },
|
|
88
|
+
],
|
|
89
|
+
category: 'evolution',
|
|
90
|
+
needsLLM: true,
|
|
91
|
+
runnable: { command: 'generalize', label: 'Run Generalize', icon: 'M5 10l7-7m0 0l7 7m-7-7v18', color: 'var(--blue)' },
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'specialize',
|
|
95
|
+
description: 'Creates project-specific skills from general ones. Takes a project context and creates specialized variants of general skills tailored to that project\'s patterns and conventions.',
|
|
96
|
+
usage: 'helixevo specialize --project <name> [options]',
|
|
97
|
+
examples: [
|
|
98
|
+
{ cmd: 'helixevo specialize --project myapp', desc: 'Create project-specific skills for myapp' },
|
|
99
|
+
{ cmd: 'helixevo specialize --project myapp --dry-run', desc: 'Preview without creating skills' },
|
|
100
|
+
],
|
|
101
|
+
options: [
|
|
102
|
+
{ flag: '--project <name>', desc: 'Required. Project name for specialization' },
|
|
103
|
+
{ flag: '--dry-run', desc: 'Show candidates without applying' },
|
|
104
|
+
{ flag: '--verbose', desc: 'Show detailed analysis' },
|
|
105
|
+
],
|
|
106
|
+
category: 'evolution',
|
|
107
|
+
needsLLM: true,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'graph',
|
|
111
|
+
description: 'Visualize and manage the skill network graph. Shows relationships between skills (depends, enhances, conflicts, co-evolves). Can render as TUI, Mermaid diagram, or sync to Obsidian.',
|
|
112
|
+
usage: 'helixevo graph [options]',
|
|
113
|
+
examples: [
|
|
114
|
+
{ cmd: 'helixevo graph', desc: 'Show skill network in terminal (instant)' },
|
|
115
|
+
{ cmd: 'helixevo graph --rebuild', desc: 'Re-infer all relationships via LLM' },
|
|
116
|
+
{ cmd: 'helixevo graph --optimize', desc: 'Detect merge/split/conflict opportunities' },
|
|
117
|
+
{ cmd: 'helixevo graph --mermaid', desc: 'Open interactive Mermaid diagram in browser' },
|
|
118
|
+
{ cmd: 'helixevo graph --obsidian ~/vault', desc: 'Sync skill graph to an Obsidian vault' },
|
|
119
|
+
],
|
|
120
|
+
options: [
|
|
121
|
+
{ flag: '--rebuild', desc: 'Force rebuild — re-infer relationships via LLM call' },
|
|
122
|
+
{ flag: '--optimize', desc: 'Run network optimization (merge/split/conflict detection)' },
|
|
123
|
+
{ flag: '--mermaid', desc: 'Render as Mermaid diagram in browser' },
|
|
124
|
+
{ flag: '--obsidian <path>', desc: 'Sync to Obsidian vault at the given path' },
|
|
125
|
+
{ flag: '--verbose', desc: 'Show detailed analysis' },
|
|
126
|
+
],
|
|
127
|
+
category: 'network',
|
|
128
|
+
needsLLM: true,
|
|
129
|
+
runnable: { command: 'graph-rebuild', label: 'Rebuild Graph', icon: 'M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1', color: 'var(--purple)' },
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'research',
|
|
133
|
+
description: 'Proactive skill discovery via web research. Identifies gaps in your skill network, generates hypotheses, searches the web for solutions, and creates draft skills from discoveries.',
|
|
134
|
+
usage: 'helixevo research [options]',
|
|
135
|
+
examples: [
|
|
136
|
+
{ cmd: 'helixevo research', desc: 'Run proactive research' },
|
|
137
|
+
{ cmd: 'helixevo research --project ~/myapp', desc: 'Research with project context for targeted discovery' },
|
|
138
|
+
{ cmd: 'helixevo research --dry-run', desc: 'Show discoveries without creating skills' },
|
|
139
|
+
],
|
|
140
|
+
options: [
|
|
141
|
+
{ flag: '--project <path>', desc: 'Project path for goal extraction' },
|
|
142
|
+
{ flag: '--dry-run', desc: 'Show discoveries without creating skills' },
|
|
143
|
+
{ flag: '--verbose', desc: 'Show detailed research steps' },
|
|
144
|
+
{ flag: '--max-hypotheses <n>', desc: 'Max hypotheses to test (default: 3)' },
|
|
145
|
+
],
|
|
146
|
+
category: 'network',
|
|
147
|
+
needsLLM: true,
|
|
148
|
+
runnable: { command: 'research', label: 'Run Research', icon: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z', color: 'var(--purple)' },
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'health',
|
|
152
|
+
description: 'Comprehensive assessment of your skill network. Evaluates cohesion (how well skills connect), coverage (gaps in knowledge), balance (over/under-developed areas), and cross-project transfer potential.',
|
|
153
|
+
usage: 'helixevo health [options]',
|
|
154
|
+
examples: [
|
|
155
|
+
{ cmd: 'helixevo health', desc: 'Run health assessment' },
|
|
156
|
+
{ cmd: 'helixevo health --verbose', desc: 'Show detailed per-metric analysis' },
|
|
157
|
+
],
|
|
158
|
+
options: [
|
|
159
|
+
{ flag: '--verbose', desc: 'Show detailed analysis' },
|
|
160
|
+
],
|
|
161
|
+
category: 'analysis',
|
|
162
|
+
needsLLM: true,
|
|
163
|
+
runnable: { command: 'health', label: 'Run Health Check', icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z', color: 'var(--yellow)' },
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: 'metrics',
|
|
167
|
+
description: 'Show correction rates, skill improvement trends, and evolution impact over time. Helps you understand how your skills are improving and where attention is needed.',
|
|
168
|
+
usage: 'helixevo metrics [options]',
|
|
169
|
+
examples: [
|
|
170
|
+
{ cmd: 'helixevo metrics', desc: 'Show summary metrics' },
|
|
171
|
+
{ cmd: 'helixevo metrics --verbose', desc: 'Show detailed per-skill breakdown' },
|
|
172
|
+
],
|
|
173
|
+
options: [
|
|
174
|
+
{ flag: '--verbose', desc: 'Show detailed per-skill breakdown' },
|
|
175
|
+
],
|
|
176
|
+
category: 'analysis',
|
|
177
|
+
needsLLM: false,
|
|
178
|
+
runnable: { command: 'metrics', label: 'Show Metrics', icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z', color: 'var(--text-secondary)' },
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'status',
|
|
182
|
+
description: 'Quick overview of system state: total skills, frontier size, failure count, skill tests, and network health. Like a health check but without LLM analysis.',
|
|
183
|
+
usage: 'helixevo status',
|
|
184
|
+
examples: [
|
|
185
|
+
{ cmd: 'helixevo status', desc: 'Show system status' },
|
|
186
|
+
],
|
|
187
|
+
category: 'analysis',
|
|
188
|
+
needsLLM: false,
|
|
189
|
+
runnable: { command: 'status', label: 'Show Status', icon: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z', color: 'var(--text-secondary)' },
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'report',
|
|
193
|
+
description: 'Generate an evolution report summarizing changes over a time period. Shows proposals, accepted/rejected changes, skill improvements, and failure resolution.',
|
|
194
|
+
usage: 'helixevo report [options]',
|
|
195
|
+
examples: [
|
|
196
|
+
{ cmd: 'helixevo report', desc: 'Generate report for the last day' },
|
|
197
|
+
{ cmd: 'helixevo report --days 7', desc: 'Weekly evolution report' },
|
|
198
|
+
{ cmd: 'helixevo report --output report.md', desc: 'Save report to a file' },
|
|
199
|
+
],
|
|
200
|
+
options: [
|
|
201
|
+
{ flag: '--days <n>', desc: 'Report period in days (default: 1)' },
|
|
202
|
+
{ flag: '--output <path>', desc: 'Output path for report' },
|
|
203
|
+
],
|
|
204
|
+
category: 'analysis',
|
|
205
|
+
needsLLM: false,
|
|
206
|
+
runnable: { command: 'report', label: 'Generate Report', icon: 'M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', color: 'var(--blue)' },
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: 'capture',
|
|
210
|
+
description: 'Extract failures from a Craft Agent session file. Parses conversation history to identify user corrections and converts them into structured failure records for evolution.',
|
|
211
|
+
usage: 'helixevo capture <sessionPath> [options]',
|
|
212
|
+
examples: [
|
|
213
|
+
{ cmd: 'helixevo capture session.json', desc: 'Extract failures from a session file' },
|
|
214
|
+
{ cmd: 'helixevo capture session.json --project myapp', desc: 'Capture with project context' },
|
|
215
|
+
],
|
|
216
|
+
options: [
|
|
217
|
+
{ flag: '--project <name>', desc: 'Project name override' },
|
|
218
|
+
],
|
|
219
|
+
category: 'system',
|
|
220
|
+
needsLLM: true,
|
|
221
|
+
note: 'Typically not needed if using "watch" mode, which captures automatically.',
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'dashboard',
|
|
225
|
+
description: 'Launch the web dashboard at http://localhost:3847. Provides a visual interface for monitoring skills, evolution, frontier, and network health.',
|
|
226
|
+
usage: 'helixevo dashboard',
|
|
227
|
+
examples: [
|
|
228
|
+
{ cmd: 'helixevo dashboard', desc: 'Open the dashboard in your browser' },
|
|
229
|
+
],
|
|
230
|
+
category: 'system',
|
|
231
|
+
needsLLM: false,
|
|
232
|
+
note: 'You are currently using the dashboard!',
|
|
233
|
+
},
|
|
234
|
+
]
|
|
235
|
+
|
|
236
|
+
const CATEGORIES: { id: string; label: string; color: string }[] = [
|
|
237
|
+
{ id: 'core', label: 'Core Workflow', color: 'var(--purple)' },
|
|
238
|
+
{ id: 'evolution', label: 'Evolution', color: 'var(--green)' },
|
|
239
|
+
{ id: 'network', label: 'Network & Research', color: 'var(--blue)' },
|
|
240
|
+
{ id: 'analysis', label: 'Analysis & Reports', color: 'var(--yellow)' },
|
|
241
|
+
{ id: 'system', label: 'System', color: 'var(--text-secondary)' },
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
export default function CommandsPage() {
|
|
245
|
+
const [expandedCmd, setExpandedCmd] = useState<string | null>(null)
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<div>
|
|
249
|
+
<div className="page-header">
|
|
250
|
+
<h1 className="page-title">Commands</h1>
|
|
251
|
+
<p className="page-desc">
|
|
252
|
+
All CLI commands available in HelixEvo — click any command for details and examples, or run directly from here
|
|
253
|
+
</p>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
{/* Typical workflow */}
|
|
257
|
+
<div className="card" style={{ marginBottom: 24, padding: '16px 20px' }}>
|
|
258
|
+
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)', marginBottom: 8 }}>Typical Workflow</div>
|
|
259
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap', fontSize: 12 }}>
|
|
260
|
+
{[
|
|
261
|
+
{ label: 'init', desc: 'Import skills' },
|
|
262
|
+
{ label: 'watch', desc: 'Capture corrections' },
|
|
263
|
+
{ label: 'evolve', desc: 'Improve skills' },
|
|
264
|
+
{ label: 'generalize', desc: 'Abstract patterns' },
|
|
265
|
+
{ label: 'graph --rebuild', desc: 'Map relationships' },
|
|
266
|
+
{ label: 'health', desc: 'Assess quality' },
|
|
267
|
+
].map((step, i) => (
|
|
268
|
+
<span key={step.label} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
269
|
+
<span style={{
|
|
270
|
+
background: 'var(--purple-light)', color: 'var(--purple)',
|
|
271
|
+
padding: '4px 10px', borderRadius: 'var(--radius)',
|
|
272
|
+
fontFamily: 'var(--font-mono)', fontWeight: 600,
|
|
273
|
+
}}>
|
|
274
|
+
{step.label}
|
|
275
|
+
</span>
|
|
276
|
+
<span style={{ color: 'var(--text-dim)' }}>{step.desc}</span>
|
|
277
|
+
{i < 5 && <span style={{ color: 'var(--text-muted)', margin: '0 4px' }}>→</span>}
|
|
278
|
+
</span>
|
|
279
|
+
))}
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
{/* Commands by category */}
|
|
284
|
+
{CATEGORIES.map(cat => {
|
|
285
|
+
const cmds = COMMANDS.filter(c => c.category === cat.id)
|
|
286
|
+
if (cmds.length === 0) return null
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<div key={cat.id} style={{ marginBottom: 24 }}>
|
|
290
|
+
<div style={{
|
|
291
|
+
fontSize: 11, fontWeight: 700, color: cat.color,
|
|
292
|
+
textTransform: 'uppercase', letterSpacing: 1.2, marginBottom: 10,
|
|
293
|
+
}}>
|
|
294
|
+
{cat.label}
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
298
|
+
{cmds.map(cmd => {
|
|
299
|
+
const isExpanded = expandedCmd === cmd.name
|
|
300
|
+
return (
|
|
301
|
+
<div key={cmd.name} className="card" style={{
|
|
302
|
+
overflow: 'hidden',
|
|
303
|
+
border: isExpanded ? `1px solid ${cat.color}40` : undefined,
|
|
304
|
+
}}>
|
|
305
|
+
{/* Command header — clickable */}
|
|
306
|
+
<div
|
|
307
|
+
onClick={() => setExpandedCmd(isExpanded ? null : cmd.name)}
|
|
308
|
+
style={{
|
|
309
|
+
padding: '12px 16px',
|
|
310
|
+
cursor: 'pointer',
|
|
311
|
+
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
|
312
|
+
}}
|
|
313
|
+
>
|
|
314
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
315
|
+
<code style={{
|
|
316
|
+
fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 700,
|
|
317
|
+
color: cat.color,
|
|
318
|
+
}}>
|
|
319
|
+
{cmd.name}
|
|
320
|
+
</code>
|
|
321
|
+
<span style={{ fontSize: 12, color: 'var(--text-secondary)' }}>
|
|
322
|
+
{cmd.description.split('.')[0]}
|
|
323
|
+
</span>
|
|
324
|
+
{cmd.needsLLM && (
|
|
325
|
+
<span style={{
|
|
326
|
+
fontSize: 9, fontWeight: 600, padding: '2px 6px',
|
|
327
|
+
background: 'var(--purple-light)', color: 'var(--purple)',
|
|
328
|
+
borderRadius: 4,
|
|
329
|
+
}}>LLM</span>
|
|
330
|
+
)}
|
|
331
|
+
</div>
|
|
332
|
+
<span style={{
|
|
333
|
+
color: 'var(--text-muted)', fontSize: 12,
|
|
334
|
+
transform: isExpanded ? 'rotate(180deg)' : 'none',
|
|
335
|
+
transition: 'transform 0.2s',
|
|
336
|
+
}}>▼</span>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
{/* Expanded details */}
|
|
340
|
+
{isExpanded && (
|
|
341
|
+
<div style={{
|
|
342
|
+
padding: '0 16px 16px',
|
|
343
|
+
borderTop: '1px solid var(--border-subtle)',
|
|
344
|
+
}}>
|
|
345
|
+
{/* Description */}
|
|
346
|
+
<p style={{ fontSize: 12, color: 'var(--text-secondary)', lineHeight: 1.6, marginTop: 12, marginBottom: 14 }}>
|
|
347
|
+
{cmd.description}
|
|
348
|
+
</p>
|
|
349
|
+
|
|
350
|
+
{/* Usage */}
|
|
351
|
+
<div style={{ marginBottom: 14 }}>
|
|
352
|
+
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 1, marginBottom: 6 }}>Usage</div>
|
|
353
|
+
<code style={{
|
|
354
|
+
display: 'block', padding: '8px 12px',
|
|
355
|
+
background: 'var(--bg-section)', borderRadius: 'var(--radius)',
|
|
356
|
+
fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--text)',
|
|
357
|
+
}}>
|
|
358
|
+
$ {cmd.usage}
|
|
359
|
+
</code>
|
|
360
|
+
</div>
|
|
361
|
+
|
|
362
|
+
{/* Options */}
|
|
363
|
+
{cmd.options && cmd.options.length > 0 && (
|
|
364
|
+
<div style={{ marginBottom: 14 }}>
|
|
365
|
+
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 1, marginBottom: 6 }}>Options</div>
|
|
366
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
367
|
+
{cmd.options.map(opt => (
|
|
368
|
+
<div key={opt.flag} style={{ display: 'flex', gap: 12, fontSize: 12, padding: '4px 0' }}>
|
|
369
|
+
<code style={{
|
|
370
|
+
fontFamily: 'var(--font-mono)', color: 'var(--purple)',
|
|
371
|
+
fontWeight: 600, minWidth: 180, flexShrink: 0,
|
|
372
|
+
}}>{opt.flag}</code>
|
|
373
|
+
<span style={{ color: 'var(--text-dim)' }}>{opt.desc}</span>
|
|
374
|
+
</div>
|
|
375
|
+
))}
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
)}
|
|
379
|
+
|
|
380
|
+
{/* Examples */}
|
|
381
|
+
<div style={{ marginBottom: cmd.runnable ? 14 : 0 }}>
|
|
382
|
+
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 1, marginBottom: 6 }}>Examples</div>
|
|
383
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
384
|
+
{cmd.examples.map((ex, i) => (
|
|
385
|
+
<div key={i} style={{
|
|
386
|
+
padding: '8px 12px',
|
|
387
|
+
background: 'var(--bg-section)', borderRadius: 'var(--radius)',
|
|
388
|
+
}}>
|
|
389
|
+
<code style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text)', fontWeight: 600 }}>
|
|
390
|
+
$ {ex.cmd}
|
|
391
|
+
</code>
|
|
392
|
+
<div style={{ fontSize: 11, color: 'var(--text-dim)', marginTop: 2 }}>{ex.desc}</div>
|
|
393
|
+
</div>
|
|
394
|
+
))}
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
|
|
398
|
+
{/* Note */}
|
|
399
|
+
{cmd.note && (
|
|
400
|
+
<div style={{
|
|
401
|
+
padding: '8px 12px', marginBottom: cmd.runnable ? 14 : 0,
|
|
402
|
+
background: 'var(--yellow-light)', borderRadius: 'var(--radius)',
|
|
403
|
+
fontSize: 11, color: 'var(--yellow)', fontWeight: 500,
|
|
404
|
+
borderLeft: '3px solid var(--yellow)',
|
|
405
|
+
}}>
|
|
406
|
+
{cmd.note}
|
|
407
|
+
</div>
|
|
408
|
+
)}
|
|
409
|
+
|
|
410
|
+
{/* Run button */}
|
|
411
|
+
{cmd.runnable && (
|
|
412
|
+
<QuickActions actions={[{
|
|
413
|
+
id: cmd.runnable.command,
|
|
414
|
+
label: cmd.runnable.label,
|
|
415
|
+
command: cmd.runnable.command,
|
|
416
|
+
icon: cmd.runnable.icon,
|
|
417
|
+
color: cmd.runnable.color,
|
|
418
|
+
description: `Run: helixevo ${cmd.name}`,
|
|
419
|
+
}]} />
|
|
420
|
+
)}
|
|
421
|
+
</div>
|
|
422
|
+
)}
|
|
423
|
+
</div>
|
|
424
|
+
)
|
|
425
|
+
})}
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
)
|
|
429
|
+
})}
|
|
430
|
+
</div>
|
|
431
|
+
)
|
|
432
|
+
}
|
package/dashboard/app/layout.tsx
CHANGED
|
@@ -15,6 +15,7 @@ const NAV = [
|
|
|
15
15
|
{ href: '/evolution', label: 'Evolution', icon: 'M13 7h8m0 0v8m0-8l-8 8-4-4-6 6' },
|
|
16
16
|
{ href: '/research', label: 'Research', icon: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z' },
|
|
17
17
|
{ href: '/frontier', label: 'Frontier', icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z' },
|
|
18
|
+
{ href: '/commands', label: 'Commands', icon: 'M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z' },
|
|
18
19
|
{ href: '/guide', label: 'Guide', icon: 'M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253' },
|
|
19
20
|
]
|
|
20
21
|
|
package/dashboard/app/page.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { getDashboardSummary, loadFrontier, loadHistory, loadGraph } from '@/lib/data'
|
|
1
|
+
import { getDashboardSummary, loadFrontier, loadHistory, loadGraph, loadFailures } from '@/lib/data'
|
|
2
|
+
import { OverviewActions } from '@/components/overview-actions'
|
|
2
3
|
|
|
3
4
|
export const dynamic = 'force-dynamic'
|
|
4
5
|
|
|
@@ -28,6 +29,7 @@ export default function Overview() {
|
|
|
28
29
|
const frontier = loadFrontier()
|
|
29
30
|
const history = loadHistory()
|
|
30
31
|
const graph = loadGraph()
|
|
32
|
+
const failures = loadFailures()
|
|
31
33
|
const recent = history.iterations.slice(-5).reverse()
|
|
32
34
|
|
|
33
35
|
return (
|
|
@@ -39,6 +41,18 @@ export default function Overview() {
|
|
|
39
41
|
</p>
|
|
40
42
|
</div>
|
|
41
43
|
|
|
44
|
+
{/* Quick Actions */}
|
|
45
|
+
<div className="card" style={{ marginBottom: 20, padding: '14px 18px' }}>
|
|
46
|
+
<div style={{ fontSize: 9, fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 10 }}>
|
|
47
|
+
Quick Actions
|
|
48
|
+
</div>
|
|
49
|
+
<OverviewActions
|
|
50
|
+
hasSkills={graph.nodes.length > 0}
|
|
51
|
+
hasFailures={failures.length > 0}
|
|
52
|
+
hasEdges={graph.edges.length > 0}
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
42
56
|
{/* Stats */}
|
|
43
57
|
<div className="grid-5" style={{ marginBottom: 28 }}>
|
|
44
58
|
<StatCard value={s.skills.total} label="Total Skills" color="var(--purple)" sub={`${s.skills.evolved} evolved`} accent="◆" />
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { QuickActions } from './quick-actions'
|
|
4
|
+
|
|
5
|
+
interface OverviewActionsProps {
|
|
6
|
+
hasFailures: boolean
|
|
7
|
+
hasSkills: boolean
|
|
8
|
+
hasEdges: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function OverviewActions({ hasFailures, hasSkills, hasEdges }: OverviewActionsProps) {
|
|
12
|
+
const actions = [
|
|
13
|
+
{
|
|
14
|
+
id: 'graph-rebuild',
|
|
15
|
+
label: 'Organize Skills',
|
|
16
|
+
command: 'graph-rebuild',
|
|
17
|
+
icon: 'M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1',
|
|
18
|
+
color: 'var(--purple)',
|
|
19
|
+
description: 'Use LLM to infer relationships between skills (depends, enhances, conflicts)',
|
|
20
|
+
disabled: !hasSkills,
|
|
21
|
+
disabledReason: 'No skills imported yet — run helixevo init first',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'generalize',
|
|
25
|
+
label: 'Generalize',
|
|
26
|
+
command: 'generalize',
|
|
27
|
+
icon: 'M5 10l7-7m0 0l7 7m-7-7v18',
|
|
28
|
+
color: 'var(--blue)',
|
|
29
|
+
description: 'Detect patterns across skills and promote to abstract parent skills',
|
|
30
|
+
disabled: !hasSkills,
|
|
31
|
+
disabledReason: 'No skills imported yet',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'evolve',
|
|
35
|
+
label: 'Evolve',
|
|
36
|
+
command: 'evolve',
|
|
37
|
+
icon: 'M13 7h8m0 0v8m0-8l-8 8-4-4-6 6',
|
|
38
|
+
color: 'var(--green)',
|
|
39
|
+
description: 'Evolve skills based on captured failures',
|
|
40
|
+
disabled: !hasFailures,
|
|
41
|
+
disabledReason: 'No failures captured yet — use Craft Agent and corrections will be captured automatically',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'health',
|
|
45
|
+
label: 'Health Check',
|
|
46
|
+
command: 'health',
|
|
47
|
+
icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
48
|
+
color: 'var(--yellow)',
|
|
49
|
+
description: 'Assess network cohesion, coverage, balance, and cross-project transfer',
|
|
50
|
+
disabled: !hasSkills,
|
|
51
|
+
disabledReason: 'No skills imported yet',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'graph-optimize',
|
|
55
|
+
label: 'Optimize',
|
|
56
|
+
command: 'graph-optimize',
|
|
57
|
+
icon: 'M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15',
|
|
58
|
+
color: 'var(--text-secondary)',
|
|
59
|
+
description: 'Detect merge/split/conflict opportunities in the skill network',
|
|
60
|
+
disabled: !hasEdges,
|
|
61
|
+
disabledReason: 'Run "Organize Skills" first to build relationships',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'research',
|
|
65
|
+
label: 'Research',
|
|
66
|
+
command: 'research',
|
|
67
|
+
icon: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z',
|
|
68
|
+
color: 'var(--purple)',
|
|
69
|
+
description: 'Proactive web research to discover new skill opportunities',
|
|
70
|
+
},
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
return <QuickActions actions={actions} />
|
|
74
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
|
|
5
|
+
interface Action {
|
|
6
|
+
id: string
|
|
7
|
+
label: string
|
|
8
|
+
command: string
|
|
9
|
+
icon: string
|
|
10
|
+
color: string
|
|
11
|
+
description: string
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
disabledReason?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface QuickActionsProps {
|
|
17
|
+
actions: Action[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type RunState = 'idle' | 'running' | 'success' | 'error'
|
|
21
|
+
|
|
22
|
+
export function QuickActions({ actions }: QuickActionsProps) {
|
|
23
|
+
const [running, setRunning] = useState<string | null>(null)
|
|
24
|
+
const [state, setState] = useState<RunState>('idle')
|
|
25
|
+
const [output, setOutput] = useState<string | null>(null)
|
|
26
|
+
|
|
27
|
+
const handleRun = async (action: Action) => {
|
|
28
|
+
if (action.disabled) return
|
|
29
|
+
setRunning(action.id)
|
|
30
|
+
setState('running')
|
|
31
|
+
setOutput(null)
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch('/api/run', {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { 'Content-Type': 'application/json' },
|
|
37
|
+
body: JSON.stringify({ command: action.command }),
|
|
38
|
+
})
|
|
39
|
+
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
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const handleClose = () => {
|
|
49
|
+
setRunning(null)
|
|
50
|
+
setState('idle')
|
|
51
|
+
setOutput(null)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<>
|
|
56
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
|
57
|
+
{actions.map(action => (
|
|
58
|
+
<button
|
|
59
|
+
key={action.id}
|
|
60
|
+
onClick={() => handleRun(action)}
|
|
61
|
+
disabled={action.disabled || (running !== null && running !== action.id)}
|
|
62
|
+
title={action.disabled ? action.disabledReason : action.description}
|
|
63
|
+
style={{
|
|
64
|
+
display: 'flex', alignItems: 'center', gap: 6,
|
|
65
|
+
padding: '8px 14px',
|
|
66
|
+
background: action.disabled ? 'var(--bg-section)' : 'var(--bg-card)',
|
|
67
|
+
border: `1px solid ${running === action.id && state === 'running' ? action.color : 'var(--border)'}`,
|
|
68
|
+
borderRadius: 'var(--radius)',
|
|
69
|
+
fontSize: 12,
|
|
70
|
+
fontWeight: 600,
|
|
71
|
+
color: action.disabled ? 'var(--text-muted)' : 'var(--text-secondary)',
|
|
72
|
+
cursor: action.disabled ? 'not-allowed' : 'pointer',
|
|
73
|
+
opacity: (running !== null && running !== action.id) ? 0.5 : 1,
|
|
74
|
+
transition: 'all 0.15s',
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
{running === action.id && state === 'running' ? (
|
|
78
|
+
<span style={{
|
|
79
|
+
width: 14, height: 14, border: '2px solid var(--border)',
|
|
80
|
+
borderTopColor: action.color, borderRadius: '50%',
|
|
81
|
+
animation: 'actionSpin 0.8s linear infinite',
|
|
82
|
+
flexShrink: 0,
|
|
83
|
+
}} />
|
|
84
|
+
) : (
|
|
85
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none"
|
|
86
|
+
stroke={action.disabled ? 'var(--text-muted)' : action.color}
|
|
87
|
+
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
88
|
+
<path d={action.icon} />
|
|
89
|
+
</svg>
|
|
90
|
+
)}
|
|
91
|
+
{action.label}
|
|
92
|
+
</button>
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{/* Output modal */}
|
|
97
|
+
{running && state !== 'idle' && (
|
|
98
|
+
<div style={{
|
|
99
|
+
marginTop: 12,
|
|
100
|
+
background: 'var(--bg-card)',
|
|
101
|
+
border: `1px solid ${state === 'success' ? 'var(--green-border)' : state === 'error' ? 'var(--red-border)' : 'var(--border)'}`,
|
|
102
|
+
borderRadius: 'var(--radius-lg)',
|
|
103
|
+
overflow: 'hidden',
|
|
104
|
+
}}>
|
|
105
|
+
{/* Header */}
|
|
106
|
+
<div style={{
|
|
107
|
+
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
|
108
|
+
padding: '10px 14px',
|
|
109
|
+
background: state === 'running' ? 'var(--bg-section)'
|
|
110
|
+
: state === 'success' ? 'var(--green-light)'
|
|
111
|
+
: 'var(--red-light)',
|
|
112
|
+
borderBottom: '1px solid var(--border)',
|
|
113
|
+
}}>
|
|
114
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, fontWeight: 600 }}>
|
|
115
|
+
{state === 'running' && (
|
|
116
|
+
<span style={{
|
|
117
|
+
width: 12, height: 12, border: '2px solid var(--border)',
|
|
118
|
+
borderTopColor: 'var(--purple)', borderRadius: '50%',
|
|
119
|
+
animation: 'actionSpin 0.8s linear infinite',
|
|
120
|
+
}} />
|
|
121
|
+
)}
|
|
122
|
+
{state === 'success' && <span style={{ color: 'var(--green)' }}>✓</span>}
|
|
123
|
+
{state === 'error' && <span style={{ color: 'var(--red)' }}>✗</span>}
|
|
124
|
+
<span style={{
|
|
125
|
+
color: state === 'success' ? 'var(--green)' : state === 'error' ? 'var(--red)' : 'var(--text-secondary)',
|
|
126
|
+
}}>
|
|
127
|
+
{state === 'running' ? `Running ${actions.find(a => a.id === running)?.label}...`
|
|
128
|
+
: state === 'success' ? 'Completed'
|
|
129
|
+
: 'Failed'}
|
|
130
|
+
</span>
|
|
131
|
+
</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)',
|
|
145
|
+
}}>
|
|
146
|
+
Close
|
|
147
|
+
</button>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
{/* Output */}
|
|
153
|
+
<pre style={{
|
|
154
|
+
padding: '12px 14px',
|
|
155
|
+
margin: 0,
|
|
156
|
+
fontSize: 11,
|
|
157
|
+
lineHeight: 1.5,
|
|
158
|
+
fontFamily: 'var(--font-mono)',
|
|
159
|
+
color: 'var(--text-secondary)',
|
|
160
|
+
maxHeight: 400,
|
|
161
|
+
overflow: 'auto',
|
|
162
|
+
whiteSpace: 'pre-wrap',
|
|
163
|
+
wordBreak: 'break-word',
|
|
164
|
+
}}>
|
|
165
|
+
{output ?? 'Waiting for output...'}
|
|
166
|
+
</pre>
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
<style>{`
|
|
171
|
+
@keyframes actionSpin {
|
|
172
|
+
to { transform: rotate(360deg); }
|
|
173
|
+
}
|
|
174
|
+
`}</style>
|
|
175
|
+
</>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
@@ -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