helixevo 0.2.34 → 0.2.35
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/CHANGELOG.md +13 -0
- package/dashboard/app/api/project-setup/route.ts +53 -0
- package/dashboard/app/layout.tsx +1 -0
- package/dashboard/app/projects/client.tsx +326 -0
- package/dashboard/app/projects/page.tsx +12 -0
- package/dashboard/lib/data.ts +30 -0
- package/dist/cli.js +251 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to HelixEvo are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.2.35] - 2026-03-23
|
|
6
|
+
|
|
7
|
+
### Added — Project Setup Workflow
|
|
8
|
+
- New `helixevo project-setup <path>` CLI command: analyzes a project folder, matches skills, identifies gaps
|
|
9
|
+
- Dashboard `/projects` page with guided setup wizard
|
|
10
|
+
- Enter project path → analyze structure → match skills → find gaps → save profile
|
|
11
|
+
- Real-time streaming output during analysis
|
|
12
|
+
- Project profile cards showing matched skills, gaps with priority levels, and recommended actions
|
|
13
|
+
- New "Projects" tab in sidebar navigation
|
|
14
|
+
- API endpoint `/api/project-setup` with SSE streaming
|
|
15
|
+
- Project profiles saved to `~/.helix/projects/<name>/profile.json`
|
|
16
|
+
- Gap analysis recommends: research (web search), specialize (adapt existing), or create (manual)
|
|
17
|
+
|
|
5
18
|
## [0.2.34] - 2026-03-23
|
|
6
19
|
|
|
7
20
|
### Fixed
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { spawn } from 'child_process'
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
5
|
+
|
|
6
|
+
export async function POST(request: Request) {
|
|
7
|
+
const body = await request.json()
|
|
8
|
+
const { path: projectPath } = body as { path: string }
|
|
9
|
+
|
|
10
|
+
if (!projectPath) {
|
|
11
|
+
return NextResponse.json({ success: false, error: 'No project path provided' }, { status: 400 })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Stream the output using SSE
|
|
15
|
+
const encoder = new TextEncoder()
|
|
16
|
+
|
|
17
|
+
const stream = new ReadableStream({
|
|
18
|
+
start(controller) {
|
|
19
|
+
const child = spawn('helixevo', ['project-setup', projectPath, '--verbose'], {
|
|
20
|
+
env: { ...process.env },
|
|
21
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const send = (event: string, data: string) => {
|
|
25
|
+
try {
|
|
26
|
+
controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`))
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
child.stdout?.on('data', (chunk: Buffer) => send('output', chunk.toString()))
|
|
31
|
+
child.stderr?.on('data', (chunk: Buffer) => send('output', chunk.toString()))
|
|
32
|
+
|
|
33
|
+
const timeout = setTimeout(() => child.kill('SIGTERM'), 120000)
|
|
34
|
+
|
|
35
|
+
child.on('close', (code) => {
|
|
36
|
+
clearTimeout(timeout)
|
|
37
|
+
send('done', JSON.stringify({ success: code === 0 }))
|
|
38
|
+
try { controller.close() } catch {}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
child.on('error', (err) => {
|
|
42
|
+
clearTimeout(timeout)
|
|
43
|
+
send('output', `Error: ${err.message}`)
|
|
44
|
+
send('done', JSON.stringify({ success: false }))
|
|
45
|
+
try { controller.close() } catch {}
|
|
46
|
+
})
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return new Response(stream, {
|
|
51
|
+
headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' },
|
|
52
|
+
})
|
|
53
|
+
}
|
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: '/projects', label: 'Projects', icon: 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z' },
|
|
18
19
|
{ 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' },
|
|
19
20
|
{ 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' },
|
|
20
21
|
]
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useRef } from 'react'
|
|
4
|
+
import type { ProjectProfile } from '@/lib/data'
|
|
5
|
+
|
|
6
|
+
type SetupState = 'idle' | 'analyzing' | 'done' | 'error'
|
|
7
|
+
|
|
8
|
+
const STEPS = [
|
|
9
|
+
{ step: 1, title: 'Analyze Project', desc: 'Read project files, tech stack, and structure', icon: '📁' },
|
|
10
|
+
{ step: 2, title: 'Match Skills', desc: 'Score each existing skill for relevance', icon: '🔗' },
|
|
11
|
+
{ step: 3, title: 'Find Gaps', desc: 'Identify missing capabilities', icon: '🔍' },
|
|
12
|
+
{ step: 4, title: 'Save Profile', desc: 'Create project profile for ongoing tracking', icon: '✓' },
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
export default function ProjectsClient({ profiles, skillCount }: { profiles: ProjectProfile[]; skillCount: number }) {
|
|
16
|
+
const [projectPath, setProjectPath] = useState('')
|
|
17
|
+
const [setupState, setSetupState] = useState<SetupState>('idle')
|
|
18
|
+
const [output, setOutput] = useState('')
|
|
19
|
+
const outputRef = useRef<HTMLPreElement | null>(null)
|
|
20
|
+
const abortRef = useRef<AbortController | null>(null)
|
|
21
|
+
|
|
22
|
+
const handleSetup = async () => {
|
|
23
|
+
if (!projectPath.trim()) return
|
|
24
|
+
setSetupState('analyzing')
|
|
25
|
+
setOutput('')
|
|
26
|
+
|
|
27
|
+
const controller = new AbortController()
|
|
28
|
+
abortRef.current = controller
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const res = await fetch('/api/project-setup', {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
body: JSON.stringify({ path: projectPath.trim() }),
|
|
35
|
+
signal: controller.signal,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
if (!res.body) { setOutput('No response'); setSetupState('error'); return }
|
|
39
|
+
|
|
40
|
+
const reader = res.body.getReader()
|
|
41
|
+
const decoder = new TextDecoder()
|
|
42
|
+
let sseBuffer = ''
|
|
43
|
+
|
|
44
|
+
while (true) {
|
|
45
|
+
const { done, value } = await reader.read()
|
|
46
|
+
if (done) break
|
|
47
|
+
sseBuffer += decoder.decode(value, { stream: true })
|
|
48
|
+
const events = sseBuffer.split('\n\n')
|
|
49
|
+
sseBuffer = events.pop() ?? ''
|
|
50
|
+
for (const event of events) {
|
|
51
|
+
const lines = event.split('\n')
|
|
52
|
+
let eventType = '', eventData = ''
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
if (line.startsWith('event: ')) eventType = line.slice(7)
|
|
55
|
+
if (line.startsWith('data: ')) eventData = line.slice(6)
|
|
56
|
+
}
|
|
57
|
+
if (eventType === 'output' && eventData) {
|
|
58
|
+
try {
|
|
59
|
+
setOutput(prev => prev + (JSON.parse(eventData) as string))
|
|
60
|
+
setTimeout(() => { if (outputRef.current) outputRef.current.scrollTop = outputRef.current.scrollHeight }, 10)
|
|
61
|
+
} catch {}
|
|
62
|
+
}
|
|
63
|
+
if (eventType === 'done' && eventData) {
|
|
64
|
+
try {
|
|
65
|
+
const r = JSON.parse(JSON.parse(eventData) as string) as { success: boolean }
|
|
66
|
+
setSetupState(r.success ? 'done' : 'error')
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (setupState === 'analyzing') setSetupState('done')
|
|
72
|
+
} catch (err: unknown) {
|
|
73
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
74
|
+
setOutput(prev => prev + '\n\n[Cancelled]')
|
|
75
|
+
} else {
|
|
76
|
+
setOutput(prev => prev || 'Network error')
|
|
77
|
+
}
|
|
78
|
+
setSetupState('error')
|
|
79
|
+
} finally {
|
|
80
|
+
abortRef.current = null
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const handleStop = () => {
|
|
85
|
+
if (abortRef.current) abortRef.current.abort()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div>
|
|
90
|
+
<div className="page-header">
|
|
91
|
+
<h1 className="page-title">Project Setup</h1>
|
|
92
|
+
<p className="page-desc">
|
|
93
|
+
Analyze a project folder to match your skills, find gaps, and get ready to work
|
|
94
|
+
</p>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* Setup Wizard */}
|
|
98
|
+
<div className="card" style={{ marginBottom: 24, padding: '22px 26px' }}>
|
|
99
|
+
{/* Pipeline */}
|
|
100
|
+
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 0, marginBottom: 24 }}>
|
|
101
|
+
{STEPS.map((step, i) => (
|
|
102
|
+
<div key={step.step} style={{ display: 'flex', alignItems: 'flex-start', flex: 1 }}>
|
|
103
|
+
<div style={{ flex: 1 }}>
|
|
104
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
|
|
105
|
+
<div style={{
|
|
106
|
+
width: 30, height: 30, borderRadius: 8,
|
|
107
|
+
background: 'var(--bg-section)', display: 'flex',
|
|
108
|
+
alignItems: 'center', justifyContent: 'center', fontSize: 14,
|
|
109
|
+
}}>{step.icon}</div>
|
|
110
|
+
<div>
|
|
111
|
+
<div style={{ fontSize: 9, fontWeight: 600, color: 'var(--text-muted)', letterSpacing: 0.5 }}>STEP {step.step}</div>
|
|
112
|
+
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--text)' }}>{step.title}</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
<div style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.4 }}>{step.desc}</div>
|
|
116
|
+
</div>
|
|
117
|
+
{i < 3 && (
|
|
118
|
+
<span style={{ padding: '10px 8px 0', color: 'var(--text-muted)', fontSize: 14 }}>→</span>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
{/* Input */}
|
|
125
|
+
<div style={{ display: 'flex', gap: 10, alignItems: 'flex-end' }}>
|
|
126
|
+
<div style={{ flex: 1 }}>
|
|
127
|
+
<label style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-dim)', display: 'block', marginBottom: 5 }}>
|
|
128
|
+
Project folder path
|
|
129
|
+
</label>
|
|
130
|
+
<input
|
|
131
|
+
value={projectPath}
|
|
132
|
+
onChange={e => setProjectPath(e.target.value)}
|
|
133
|
+
placeholder="/path/to/your/project"
|
|
134
|
+
disabled={setupState === 'analyzing'}
|
|
135
|
+
style={{
|
|
136
|
+
width: '100%', padding: '9px 14px',
|
|
137
|
+
border: '1px solid var(--border)', borderRadius: 'var(--radius)',
|
|
138
|
+
fontSize: 13, fontFamily: 'var(--font-mono)',
|
|
139
|
+
background: 'var(--bg-input)', color: 'var(--text)',
|
|
140
|
+
}}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
{setupState === 'idle' || setupState === 'done' || setupState === 'error' ? (
|
|
144
|
+
<button onClick={handleSetup} disabled={!projectPath.trim()} style={{
|
|
145
|
+
padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
|
|
146
|
+
background: projectPath.trim() ? 'var(--green)' : 'var(--bg-section)',
|
|
147
|
+
color: projectPath.trim() ? '#fff' : 'var(--text-muted)',
|
|
148
|
+
fontSize: 13, fontWeight: 600, cursor: projectPath.trim() ? 'pointer' : 'default',
|
|
149
|
+
whiteSpace: 'nowrap',
|
|
150
|
+
}}>
|
|
151
|
+
{setupState === 'done' ? 'Re-analyze' : 'Analyze Project'}
|
|
152
|
+
</button>
|
|
153
|
+
) : (
|
|
154
|
+
<button onClick={handleStop} style={{
|
|
155
|
+
padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
|
|
156
|
+
background: 'var(--red)', color: '#fff',
|
|
157
|
+
fontSize: 13, fontWeight: 600, cursor: 'pointer', whiteSpace: 'nowrap',
|
|
158
|
+
}}>
|
|
159
|
+
Stop
|
|
160
|
+
</button>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
{/* Output */}
|
|
166
|
+
{setupState !== 'idle' && (
|
|
167
|
+
<div className="card" style={{ marginBottom: 24, overflow: 'hidden' }}>
|
|
168
|
+
<div style={{
|
|
169
|
+
padding: '10px 16px',
|
|
170
|
+
background: setupState === 'done' ? 'var(--green-light)' : setupState === 'error' ? 'var(--red-light)' : 'var(--bg-section)',
|
|
171
|
+
borderBottom: '1px solid var(--border)',
|
|
172
|
+
fontSize: 12, fontWeight: 600,
|
|
173
|
+
color: setupState === 'done' ? 'var(--green)' : setupState === 'error' ? 'var(--red)' : 'var(--text-secondary)',
|
|
174
|
+
}}>
|
|
175
|
+
{setupState === 'analyzing' && '● Analyzing project and matching skills...'}
|
|
176
|
+
{setupState === 'done' && '✓ Analysis complete — project profile saved'}
|
|
177
|
+
{setupState === 'error' && '✗ Analysis failed'}
|
|
178
|
+
</div>
|
|
179
|
+
<pre ref={outputRef} style={{
|
|
180
|
+
padding: '14px 16px', margin: 0,
|
|
181
|
+
fontSize: 11, lineHeight: 1.55,
|
|
182
|
+
fontFamily: 'var(--font-mono)',
|
|
183
|
+
color: 'var(--text-secondary)',
|
|
184
|
+
maxHeight: 500, overflow: 'auto',
|
|
185
|
+
whiteSpace: 'pre-wrap', wordBreak: 'break-word',
|
|
186
|
+
}}>
|
|
187
|
+
{output || 'Starting project analysis...'}
|
|
188
|
+
</pre>
|
|
189
|
+
{setupState === 'done' && (
|
|
190
|
+
<div style={{ padding: '12px 16px', borderTop: '1px solid var(--border)', display: 'flex', gap: 8 }}>
|
|
191
|
+
<button onClick={() => window.location.reload()} style={{
|
|
192
|
+
padding: '7px 14px', background: 'var(--bg-section)', border: '1px solid var(--border)',
|
|
193
|
+
borderRadius: 'var(--radius)', fontSize: 12, fontWeight: 600, cursor: 'pointer', color: 'var(--text-secondary)',
|
|
194
|
+
}}>
|
|
195
|
+
Refresh to see results
|
|
196
|
+
</button>
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
{/* Existing Project Profiles */}
|
|
203
|
+
{profiles.length > 0 && (
|
|
204
|
+
<div style={{ marginBottom: 24 }}>
|
|
205
|
+
<div style={{ fontSize: 10, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 12 }}>
|
|
206
|
+
Analyzed Projects ({profiles.length})
|
|
207
|
+
</div>
|
|
208
|
+
<div style={{ display: 'grid', gap: 12 }}>
|
|
209
|
+
{profiles.map(p => {
|
|
210
|
+
const highGaps = p.gaps.filter(g => g.priority === 'high')
|
|
211
|
+
const matchedPct = skillCount > 0 ? Math.round((p.matchedSkills.length / skillCount) * 100) : 0
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<div key={p.name} className="card">
|
|
215
|
+
<div className="card-body">
|
|
216
|
+
{/* Header */}
|
|
217
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 12 }}>
|
|
218
|
+
<div>
|
|
219
|
+
<div style={{ fontSize: 16, fontWeight: 700, marginBottom: 4 }}>{p.name}</div>
|
|
220
|
+
<div style={{ fontSize: 12, color: 'var(--text-dim)' }}>{p.description}</div>
|
|
221
|
+
</div>
|
|
222
|
+
<span className={`badge ${p.status === 'active' ? 'badge-green' : 'badge-blue'}`}>{p.status}</span>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
{/* Tech + Domains */}
|
|
226
|
+
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 14 }}>
|
|
227
|
+
{p.techStack.map(t => <span key={t} className="badge badge-gray">{t}</span>)}
|
|
228
|
+
{p.domains.map(d => <span key={d} className="badge badge-blue">{d}</span>)}
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
{/* Stats row */}
|
|
232
|
+
<div className="grid-3" style={{ gap: 10, marginBottom: 14 }}>
|
|
233
|
+
<div style={{ padding: '10px 12px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
|
|
234
|
+
<div style={{ fontSize: 20, fontWeight: 800, color: 'var(--green)' }}>{p.matchedSkills.length}</div>
|
|
235
|
+
<div style={{ fontSize: 10, color: 'var(--text-dim)' }}>Skills Matched ({matchedPct}%)</div>
|
|
236
|
+
</div>
|
|
237
|
+
<div style={{ padding: '10px 12px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
|
|
238
|
+
<div style={{ fontSize: 20, fontWeight: 800, color: highGaps.length > 0 ? 'var(--red)' : 'var(--green)' }}>{p.gaps.length}</div>
|
|
239
|
+
<div style={{ fontSize: 10, color: 'var(--text-dim)' }}>Gaps ({highGaps.length} high priority)</div>
|
|
240
|
+
</div>
|
|
241
|
+
<div style={{ padding: '10px 12px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
|
|
242
|
+
<div style={{ fontSize: 10, color: 'var(--text-dim)', marginBottom: 2 }}>Analyzed</div>
|
|
243
|
+
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)' }}>{new Date(p.analyzedAt).toLocaleDateString()}</div>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
{/* Top matched skills */}
|
|
248
|
+
{p.matchedSkills.length > 0 && (
|
|
249
|
+
<div style={{ marginBottom: 14 }}>
|
|
250
|
+
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 6 }}>TOP MATCHES</div>
|
|
251
|
+
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
|
252
|
+
{p.matchedSkills.slice(0, 6).map(m => (
|
|
253
|
+
<div key={m.slug} style={{
|
|
254
|
+
padding: '4px 10px', background: 'var(--bg-section)',
|
|
255
|
+
borderRadius: 'var(--radius)', fontSize: 11, display: 'flex', alignItems: 'center', gap: 6,
|
|
256
|
+
}}>
|
|
257
|
+
<span style={{ fontWeight: 500 }}>{m.slug}</span>
|
|
258
|
+
<span style={{ fontWeight: 700, color: m.relevance >= 70 ? 'var(--green)' : 'var(--yellow)' }}>{m.relevance}%</span>
|
|
259
|
+
</div>
|
|
260
|
+
))}
|
|
261
|
+
{p.matchedSkills.length > 6 && (
|
|
262
|
+
<span style={{ fontSize: 10, color: 'var(--text-muted)', padding: '4px 6px' }}>+{p.matchedSkills.length - 6} more</span>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
)}
|
|
267
|
+
|
|
268
|
+
{/* Gaps */}
|
|
269
|
+
{p.gaps.length > 0 && (
|
|
270
|
+
<div>
|
|
271
|
+
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 6 }}>GAPS</div>
|
|
272
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
273
|
+
{p.gaps.map((g, i) => (
|
|
274
|
+
<div key={i} style={{
|
|
275
|
+
padding: '6px 10px', borderRadius: 'var(--radius)',
|
|
276
|
+
background: g.priority === 'high' ? 'var(--red-light)' : g.priority === 'medium' ? 'var(--yellow-light)' : 'var(--bg-section)',
|
|
277
|
+
borderLeft: `3px solid ${g.priority === 'high' ? 'var(--red)' : g.priority === 'medium' ? 'var(--yellow)' : 'var(--text-muted)'}`,
|
|
278
|
+
fontSize: 11, display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
|
279
|
+
}}>
|
|
280
|
+
<span><strong>{g.area}</strong> — {g.description.slice(0, 60)}</span>
|
|
281
|
+
<span className={`badge ${g.suggestedAction === 'research' ? 'badge-blue' : g.suggestedAction === 'specialize' ? 'badge-green' : 'badge-gray'}`}>
|
|
282
|
+
{g.suggestedAction}
|
|
283
|
+
</span>
|
|
284
|
+
</div>
|
|
285
|
+
))}
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
|
|
290
|
+
{/* Recommendations */}
|
|
291
|
+
{p.recommendations.length > 0 && (
|
|
292
|
+
<div style={{ marginTop: 12, padding: '10px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
|
|
293
|
+
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 4 }}>NEXT STEPS</div>
|
|
294
|
+
{p.recommendations.map((r, i) => (
|
|
295
|
+
<div key={i} style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.5 }}>→ {r}</div>
|
|
296
|
+
))}
|
|
297
|
+
</div>
|
|
298
|
+
)}
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
)
|
|
302
|
+
})}
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
)}
|
|
306
|
+
|
|
307
|
+
{/* Empty state */}
|
|
308
|
+
{profiles.length === 0 && setupState === 'idle' && (
|
|
309
|
+
<div className="card">
|
|
310
|
+
<div className="empty-state">
|
|
311
|
+
<div className="empty-state-icon">
|
|
312
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
313
|
+
<path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
|
314
|
+
</svg>
|
|
315
|
+
</div>
|
|
316
|
+
<div className="empty-state-title">No projects analyzed yet</div>
|
|
317
|
+
<div className="empty-state-desc">
|
|
318
|
+
Enter a project folder path above to analyze it against your {skillCount} skills.
|
|
319
|
+
HelixEvo will identify which skills match and what gaps need filling.
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
</div>
|
|
325
|
+
)
|
|
326
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { loadAllProjectProfiles, loadGraph } from '@/lib/data'
|
|
2
|
+
import ProjectsClient from './client'
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
5
|
+
|
|
6
|
+
export default function ProjectsPage() {
|
|
7
|
+
const profiles = loadAllProjectProfiles()
|
|
8
|
+
const graph = loadGraph()
|
|
9
|
+
const skillCount = graph.nodes.length
|
|
10
|
+
|
|
11
|
+
return <ProjectsClient profiles={profiles} skillCount={skillCount} />
|
|
12
|
+
}
|
package/dashboard/lib/data.ts
CHANGED
|
@@ -121,6 +121,36 @@ export function getProjectSkills(project: string): { failures: Failure[]; skills
|
|
|
121
121
|
return { failures, skills }
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
// ─── Project Profiles ────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
export interface ProjectProfile {
|
|
127
|
+
name: string
|
|
128
|
+
path: string
|
|
129
|
+
description: string
|
|
130
|
+
techStack: string[]
|
|
131
|
+
domains: string[]
|
|
132
|
+
analyzedAt: string
|
|
133
|
+
matchedSkills: { slug: string; relevance: number; reason: string }[]
|
|
134
|
+
gaps: { area: string; description: string; priority: 'high' | 'medium' | 'low'; suggestedAction: 'research' | 'specialize' | 'create' }[]
|
|
135
|
+
recommendations: string[]
|
|
136
|
+
status: 'analyzed' | 'active'
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function loadAllProjectProfiles(): ProjectProfile[] {
|
|
140
|
+
const dir = join(SG_DIR, 'projects')
|
|
141
|
+
if (!existsSync(dir)) return []
|
|
142
|
+
try {
|
|
143
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
144
|
+
.filter(d => d.isDirectory())
|
|
145
|
+
.map(d => {
|
|
146
|
+
const p = join(dir, d.name, 'profile.json')
|
|
147
|
+
if (!existsSync(p)) return null
|
|
148
|
+
try { return JSON.parse(readFileSync(p, 'utf-8')) as ProjectProfile } catch { return null }
|
|
149
|
+
})
|
|
150
|
+
.filter((p): p is ProjectProfile => p !== null)
|
|
151
|
+
} catch { return [] }
|
|
152
|
+
}
|
|
153
|
+
|
|
124
154
|
export function getDashboardSummary() {
|
|
125
155
|
const graph = loadGraph()
|
|
126
156
|
const failures = loadFailures()
|
package/dist/cli.js
CHANGED
|
@@ -13157,28 +13157,269 @@ async function metricsCommand(options) {
|
|
|
13157
13157
|
}
|
|
13158
13158
|
}
|
|
13159
13159
|
|
|
13160
|
-
// src/
|
|
13161
|
-
|
|
13160
|
+
// src/commands/project-setup.ts
|
|
13161
|
+
init_skills();
|
|
13162
|
+
init_llm();
|
|
13162
13163
|
import { join as join18 } from "node:path";
|
|
13164
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11, writeFileSync as writeFileSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync3 } from "node:fs";
|
|
13165
|
+
|
|
13166
|
+
// src/prompts/project-analysis.ts
|
|
13167
|
+
function buildProjectAnalysisPrompt(projectContext, skills) {
|
|
13168
|
+
const skillsList = skills.map((s) => {
|
|
13169
|
+
const tags = s.meta.tags?.join(", ") ?? "";
|
|
13170
|
+
const desc = s.meta.description ?? "";
|
|
13171
|
+
const preview = s.content.slice(0, 150).replace(/\n/g, " ");
|
|
13172
|
+
return `- ${s.slug}: ${desc} [tags: ${tags}] — ${preview}`;
|
|
13173
|
+
}).join(`
|
|
13174
|
+
`);
|
|
13175
|
+
return `You are a project analyst for HelixEvo, a skill ecosystem for AI agents.
|
|
13176
|
+
|
|
13177
|
+
## Task
|
|
13178
|
+
Analyze the following project and determine which existing skills are relevant, what skill gaps exist, and what actions to take.
|
|
13179
|
+
|
|
13180
|
+
## Project Context
|
|
13181
|
+
${projectContext}
|
|
13182
|
+
|
|
13183
|
+
## Available Skills (${skills.length} total)
|
|
13184
|
+
${skillsList}
|
|
13185
|
+
|
|
13186
|
+
## Analysis Requirements
|
|
13187
|
+
|
|
13188
|
+
1. **Match skills**: For each existing skill, score its relevance (0-100%) to this project. Only include skills with relevance >= 20%.
|
|
13189
|
+
|
|
13190
|
+
2. **Identify gaps**: What capabilities does this project need that NO existing skill covers? Be specific.
|
|
13191
|
+
- "high" priority = the project cannot function well without this
|
|
13192
|
+
- "medium" = would significantly improve quality
|
|
13193
|
+
- "low" = nice to have
|
|
13194
|
+
|
|
13195
|
+
3. **Recommend actions**: For each gap, suggest one of:
|
|
13196
|
+
- "research" = search the web for best practices and create a new skill
|
|
13197
|
+
- "specialize" = adapt an existing general skill for this project's specific patterns
|
|
13198
|
+
- "create" = manually create a skill from scratch
|
|
13199
|
+
|
|
13200
|
+
## Output
|
|
13201
|
+
Return JSON:
|
|
13202
|
+
{
|
|
13203
|
+
"name": "project-name (kebab-case)",
|
|
13204
|
+
"description": "one-line description of what this project does",
|
|
13205
|
+
"techStack": ["tech1", "tech2"],
|
|
13206
|
+
"domains": ["domain1", "domain2"],
|
|
13207
|
+
"matchedSkills": [
|
|
13208
|
+
{ "slug": "skill-slug", "relevance": 85, "reason": "why this skill is relevant" }
|
|
13209
|
+
],
|
|
13210
|
+
"gaps": [
|
|
13211
|
+
{ "area": "gap name", "description": "what capability is missing", "priority": "high", "suggestedAction": "research" }
|
|
13212
|
+
],
|
|
13213
|
+
"recommendations": [
|
|
13214
|
+
"Run 'helixevo specialize --project X' to create project-specific skills",
|
|
13215
|
+
"Consider researching X to fill the Y gap"
|
|
13216
|
+
]
|
|
13217
|
+
}
|
|
13218
|
+
|
|
13219
|
+
## Constraints
|
|
13220
|
+
- Be specific about WHY each skill matches or doesn't
|
|
13221
|
+
- Don't suggest gaps for things skills already cover
|
|
13222
|
+
- Focus on practical, actionable gaps — not theoretical ones
|
|
13223
|
+
- Recommendations should be concrete next steps`;
|
|
13224
|
+
}
|
|
13225
|
+
|
|
13226
|
+
// src/commands/project-setup.ts
|
|
13227
|
+
init_config();
|
|
13228
|
+
function readProjectContext(projectPath) {
|
|
13229
|
+
const contextFiles = [];
|
|
13230
|
+
const filesToRead = [
|
|
13231
|
+
"README.md",
|
|
13232
|
+
"package.json",
|
|
13233
|
+
"CLAUDE.md",
|
|
13234
|
+
".agents/AGENTS.md",
|
|
13235
|
+
"tsconfig.json",
|
|
13236
|
+
"Cargo.toml",
|
|
13237
|
+
"pyproject.toml",
|
|
13238
|
+
"go.mod",
|
|
13239
|
+
"Gemfile",
|
|
13240
|
+
"requirements.txt",
|
|
13241
|
+
"docker-compose.yml",
|
|
13242
|
+
"Dockerfile"
|
|
13243
|
+
];
|
|
13244
|
+
for (const f of filesToRead) {
|
|
13245
|
+
const p = join18(projectPath, f);
|
|
13246
|
+
if (existsSync14(p)) {
|
|
13247
|
+
try {
|
|
13248
|
+
const content = readFileSync11(p, "utf-8");
|
|
13249
|
+
contextFiles.push({ name: f, content: content.slice(0, 2000) });
|
|
13250
|
+
} catch {}
|
|
13251
|
+
}
|
|
13252
|
+
}
|
|
13253
|
+
let dirStructure = "";
|
|
13254
|
+
try {
|
|
13255
|
+
const items = readdirSync3(projectPath, { withFileTypes: true }).filter((d) => !d.name.startsWith(".") && d.name !== "node_modules" && d.name !== "__pycache__").slice(0, 30);
|
|
13256
|
+
dirStructure = items.map((d) => `${d.isDirectory() ? "\uD83D\uDCC1" : "\uD83D\uDCC4"} ${d.name}`).join(`
|
|
13257
|
+
`);
|
|
13258
|
+
} catch {}
|
|
13259
|
+
let srcStructure = "";
|
|
13260
|
+
const srcDir = join18(projectPath, "src");
|
|
13261
|
+
if (existsSync14(srcDir)) {
|
|
13262
|
+
try {
|
|
13263
|
+
const scanDir = (dir, prefix, depth) => {
|
|
13264
|
+
if (depth > 2)
|
|
13265
|
+
return [];
|
|
13266
|
+
const lines = [];
|
|
13267
|
+
const items = readdirSync3(dir, { withFileTypes: true }).filter((d) => !d.name.startsWith(".")).slice(0, 20);
|
|
13268
|
+
for (const item of items) {
|
|
13269
|
+
lines.push(`${prefix}${item.isDirectory() ? "\uD83D\uDCC1" : "\uD83D\uDCC4"} ${item.name}`);
|
|
13270
|
+
if (item.isDirectory()) {
|
|
13271
|
+
lines.push(...scanDir(join18(dir, item.name), prefix + " ", depth + 1));
|
|
13272
|
+
}
|
|
13273
|
+
}
|
|
13274
|
+
return lines;
|
|
13275
|
+
};
|
|
13276
|
+
srcStructure = scanDir(srcDir, " ", 0).join(`
|
|
13277
|
+
`);
|
|
13278
|
+
} catch {}
|
|
13279
|
+
}
|
|
13280
|
+
let context = `## Project Path
|
|
13281
|
+
${projectPath}
|
|
13282
|
+
|
|
13283
|
+
`;
|
|
13284
|
+
context += `## Directory Structure
|
|
13285
|
+
${dirStructure}
|
|
13286
|
+
`;
|
|
13287
|
+
if (srcStructure)
|
|
13288
|
+
context += `
|
|
13289
|
+
## Source Structure (src/)
|
|
13290
|
+
${srcStructure}
|
|
13291
|
+
`;
|
|
13292
|
+
context += `
|
|
13293
|
+
`;
|
|
13294
|
+
for (const f of contextFiles) {
|
|
13295
|
+
context += `## ${f.name}
|
|
13296
|
+
\`\`\`
|
|
13297
|
+
${f.content}
|
|
13298
|
+
\`\`\`
|
|
13299
|
+
|
|
13300
|
+
`;
|
|
13301
|
+
}
|
|
13302
|
+
return context;
|
|
13303
|
+
}
|
|
13304
|
+
function getProjectsDir() {
|
|
13305
|
+
return join18(getHelixDir(), "projects");
|
|
13306
|
+
}
|
|
13307
|
+
function saveProjectProfile(profile) {
|
|
13308
|
+
const dir = join18(getProjectsDir(), profile.name);
|
|
13309
|
+
mkdirSync5(dir, { recursive: true });
|
|
13310
|
+
writeFileSync11(join18(dir, "profile.json"), JSON.stringify(profile, null, 2));
|
|
13311
|
+
}
|
|
13312
|
+
async function projectSetupCommand(projectPath, options) {
|
|
13313
|
+
const verbose = options.verbose ?? false;
|
|
13314
|
+
const dryRun = options.dryRun ?? false;
|
|
13315
|
+
const resolvedPath = join18(process.cwd(), projectPath).replace(/\/$/, "");
|
|
13316
|
+
if (!existsSync14(resolvedPath)) {
|
|
13317
|
+
console.error(` ✗ Path not found: ${resolvedPath}`);
|
|
13318
|
+
process.exit(1);
|
|
13319
|
+
}
|
|
13320
|
+
console.log(`\uD83D\uDCE6 HelixEvo Project Setup
|
|
13321
|
+
`);
|
|
13322
|
+
console.log(` Project: ${resolvedPath}
|
|
13323
|
+
`);
|
|
13324
|
+
console.log(" Step 1: Analyzing project structure...");
|
|
13325
|
+
const context = readProjectContext(resolvedPath);
|
|
13326
|
+
if (verbose) {
|
|
13327
|
+
console.log(` Read ${context.split(`
|
|
13328
|
+
`).length} lines of context`);
|
|
13329
|
+
}
|
|
13330
|
+
const skills = loadAllGeneralSkills();
|
|
13331
|
+
console.log(` Step 2: Comparing against ${skills.length} available skills...
|
|
13332
|
+
`);
|
|
13333
|
+
console.log(" Step 3: Running skill-project analysis...");
|
|
13334
|
+
const prompt = buildProjectAnalysisPrompt(context, skills);
|
|
13335
|
+
const analysis = await chatJson({ prompt });
|
|
13336
|
+
console.log(`
|
|
13337
|
+
─── Project: ${analysis.name} ───`);
|
|
13338
|
+
console.log(` ${analysis.description}`);
|
|
13339
|
+
console.log(` Tech: ${analysis.techStack.join(", ")}`);
|
|
13340
|
+
console.log(` Domains: ${analysis.domains.join(", ")}
|
|
13341
|
+
`);
|
|
13342
|
+
const matched = analysis.matchedSkills.sort((a, b) => b.relevance - a.relevance);
|
|
13343
|
+
console.log(` ✓ ${matched.length} skills matched (of ${skills.length} total):
|
|
13344
|
+
`);
|
|
13345
|
+
for (const m of matched) {
|
|
13346
|
+
const bar = "█".repeat(Math.round(m.relevance / 10)) + "░".repeat(10 - Math.round(m.relevance / 10));
|
|
13347
|
+
const color = m.relevance >= 70 ? "\x1B[32m" : m.relevance >= 40 ? "\x1B[33m" : "\x1B[2m";
|
|
13348
|
+
console.log(` ${color}${bar}\x1B[0m ${m.relevance}% ${m.slug}`);
|
|
13349
|
+
if (verbose)
|
|
13350
|
+
console.log(` ${m.reason}`);
|
|
13351
|
+
}
|
|
13352
|
+
const gaps = analysis.gaps;
|
|
13353
|
+
if (gaps.length > 0) {
|
|
13354
|
+
console.log(`
|
|
13355
|
+
○ ${gaps.length} gap(s) identified:
|
|
13356
|
+
`);
|
|
13357
|
+
for (const g of gaps) {
|
|
13358
|
+
const icon = g.priority === "high" ? "\uD83D\uDD34" : g.priority === "medium" ? "\uD83D\uDFE1" : "\uD83D\uDFE2";
|
|
13359
|
+
console.log(` ${icon} [${g.priority}] ${g.area}`);
|
|
13360
|
+
console.log(` ${g.description}`);
|
|
13361
|
+
console.log(` → Action: ${g.suggestedAction}`);
|
|
13362
|
+
}
|
|
13363
|
+
} else {
|
|
13364
|
+
console.log(`
|
|
13365
|
+
✓ No significant gaps found — your skills cover this project well!`);
|
|
13366
|
+
}
|
|
13367
|
+
if (analysis.recommendations.length > 0) {
|
|
13368
|
+
console.log(`
|
|
13369
|
+
─── Recommended Next Steps ───`);
|
|
13370
|
+
for (const r of analysis.recommendations) {
|
|
13371
|
+
console.log(` → ${r}`);
|
|
13372
|
+
}
|
|
13373
|
+
}
|
|
13374
|
+
if (!dryRun) {
|
|
13375
|
+
const profile = {
|
|
13376
|
+
name: analysis.name,
|
|
13377
|
+
path: resolvedPath,
|
|
13378
|
+
description: analysis.description,
|
|
13379
|
+
techStack: analysis.techStack,
|
|
13380
|
+
domains: analysis.domains,
|
|
13381
|
+
analyzedAt: new Date().toISOString(),
|
|
13382
|
+
matchedSkills: analysis.matchedSkills,
|
|
13383
|
+
gaps: analysis.gaps,
|
|
13384
|
+
recommendations: analysis.recommendations,
|
|
13385
|
+
status: "analyzed"
|
|
13386
|
+
};
|
|
13387
|
+
saveProjectProfile(profile);
|
|
13388
|
+
console.log(`
|
|
13389
|
+
✓ Project profile saved to ~/.helix/projects/${analysis.name}/`);
|
|
13390
|
+
console.log(` Next: Run "helixevo specialize --project ${analysis.name}" to create project-specific skills`);
|
|
13391
|
+
if (gaps.filter((g) => g.suggestedAction === "research").length > 0) {
|
|
13392
|
+
console.log(` Or: Run "helixevo research --project ${resolvedPath}" to fill gaps via web research`);
|
|
13393
|
+
}
|
|
13394
|
+
} else {
|
|
13395
|
+
console.log(`
|
|
13396
|
+
[DRY RUN — profile not saved]`);
|
|
13397
|
+
}
|
|
13398
|
+
console.log();
|
|
13399
|
+
}
|
|
13400
|
+
|
|
13401
|
+
// src/utils/update-check.ts
|
|
13402
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync12, existsSync as existsSync15, mkdirSync as mkdirSync6 } from "node:fs";
|
|
13403
|
+
import { join as join19 } from "node:path";
|
|
13163
13404
|
import { homedir as homedir4 } from "node:os";
|
|
13164
|
-
var HELIX_DIR2 =
|
|
13165
|
-
var CACHE_PATH =
|
|
13405
|
+
var HELIX_DIR2 = join19(homedir4(), ".helix");
|
|
13406
|
+
var CACHE_PATH = join19(HELIX_DIR2, "update-check.json");
|
|
13166
13407
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
13167
13408
|
var REGISTRY_URL = "https://registry.npmjs.org/helixevo/latest";
|
|
13168
13409
|
function readCache() {
|
|
13169
13410
|
try {
|
|
13170
|
-
if (!
|
|
13411
|
+
if (!existsSync15(CACHE_PATH))
|
|
13171
13412
|
return null;
|
|
13172
|
-
return JSON.parse(
|
|
13413
|
+
return JSON.parse(readFileSync12(CACHE_PATH, "utf-8"));
|
|
13173
13414
|
} catch {
|
|
13174
13415
|
return null;
|
|
13175
13416
|
}
|
|
13176
13417
|
}
|
|
13177
13418
|
function writeCache(cache) {
|
|
13178
13419
|
try {
|
|
13179
|
-
if (!
|
|
13180
|
-
|
|
13181
|
-
|
|
13420
|
+
if (!existsSync15(HELIX_DIR2))
|
|
13421
|
+
mkdirSync6(HELIX_DIR2, { recursive: true });
|
|
13422
|
+
writeFileSync12(CACHE_PATH, JSON.stringify(cache));
|
|
13182
13423
|
} catch {}
|
|
13183
13424
|
}
|
|
13184
13425
|
function compareVersions(current, latest) {
|
|
@@ -13281,6 +13522,7 @@ program2.command("health").description("Assess network health: cohesion, coverag
|
|
|
13281
13522
|
printHealthReport2(health);
|
|
13282
13523
|
});
|
|
13283
13524
|
program2.command("metrics").description("Show correction rates, skill trends, and evolution impact").option("--verbose", "Show detailed per-skill breakdown").action(metricsCommand);
|
|
13525
|
+
program2.command("project-setup").description("Analyze a project folder and match it against your skill set").argument("<path>", "Path to the project folder").option("--verbose", "Show detailed analysis").option("--dry-run", "Analyze without saving project profile").action(projectSetupCommand);
|
|
13284
13526
|
program2.hook("postAction", () => {
|
|
13285
13527
|
checkForUpdates().catch(() => {});
|
|
13286
13528
|
});
|
package/package.json
CHANGED