helixevo 0.2.34 → 0.2.36
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 +25 -0
- package/dashboard/app/api/project-setup/route.ts +53 -0
- package/dashboard/app/layout.tsx +1 -0
- package/dashboard/app/page.tsx +12 -0
- package/dashboard/app/projects/client.tsx +561 -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,31 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to HelixEvo are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.2.36] - 2026-03-23
|
|
6
|
+
|
|
7
|
+
### Added — Enhanced Project Setup
|
|
8
|
+
- GitHub URL support: enter a GitHub repo URL + clone directory
|
|
9
|
+
- Local folder / GitHub URL toggle with mode-specific UI
|
|
10
|
+
- Each pipeline step shows the exact CLI command used
|
|
11
|
+
- Gap action buttons: "Research this gap" → Research tab, "Specialize a skill", "Create manually" → Skill Network
|
|
12
|
+
- Cross-functional "Setup New Project" banner on Overview page
|
|
13
|
+
- After analysis: links to Skill Network and Research tabs
|
|
14
|
+
- Matched skills list with relevance bars and click-through to Skill Network
|
|
15
|
+
- Re-analyze button per project profile
|
|
16
|
+
|
|
17
|
+
## [0.2.35] - 2026-03-23
|
|
18
|
+
|
|
19
|
+
### Added — Project Setup Workflow
|
|
20
|
+
- New `helixevo project-setup <path>` CLI command: analyzes a project folder, matches skills, identifies gaps
|
|
21
|
+
- Dashboard `/projects` page with guided setup wizard
|
|
22
|
+
- Enter project path → analyze structure → match skills → find gaps → save profile
|
|
23
|
+
- Real-time streaming output during analysis
|
|
24
|
+
- Project profile cards showing matched skills, gaps with priority levels, and recommended actions
|
|
25
|
+
- New "Projects" tab in sidebar navigation
|
|
26
|
+
- API endpoint `/api/project-setup` with SSE streaming
|
|
27
|
+
- Project profiles saved to `~/.helix/projects/<name>/profile.json`
|
|
28
|
+
- Gap analysis recommends: research (web search), specialize (adapt existing), or create (manual)
|
|
29
|
+
|
|
5
30
|
## [0.2.34] - 2026-03-23
|
|
6
31
|
|
|
7
32
|
### 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
|
]
|
package/dashboard/app/page.tsx
CHANGED
|
@@ -46,6 +46,18 @@ export default function Overview() {
|
|
|
46
46
|
</p>
|
|
47
47
|
</div>
|
|
48
48
|
|
|
49
|
+
{/* Setup New Project */}
|
|
50
|
+
<Link href="/projects" style={{ textDecoration: 'none', color: 'inherit' }}>
|
|
51
|
+
<div className="card" style={{ marginBottom: 12, padding: '12px 18px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 10, borderLeft: '3px solid var(--green)' }}>
|
|
52
|
+
<div style={{ width: 28, height: 28, borderRadius: 8, background: 'var(--green-light)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 14, flexShrink: 0 }}>📁</div>
|
|
53
|
+
<div style={{ flex: 1 }}>
|
|
54
|
+
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>Setup New Project</div>
|
|
55
|
+
<div style={{ fontSize: 11, color: 'var(--text-dim)' }}>Analyze a folder or GitHub repo to match skills and find gaps</div>
|
|
56
|
+
</div>
|
|
57
|
+
<span style={{ fontSize: 16, color: 'var(--text-muted)' }}>→</span>
|
|
58
|
+
</div>
|
|
59
|
+
</Link>
|
|
60
|
+
|
|
49
61
|
{/* Quick Actions */}
|
|
50
62
|
<div className="card" style={{ marginBottom: 20, padding: '14px 18px' }}>
|
|
51
63
|
<div style={{ fontSize: 9, fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 10 }}>
|
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useRef } from 'react'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import type { ProjectProfile } from '@/lib/data'
|
|
6
|
+
|
|
7
|
+
type SetupState = 'idle' | 'analyzing' | 'done' | 'error'
|
|
8
|
+
type InputMode = 'local' | 'github'
|
|
9
|
+
|
|
10
|
+
const PIPELINE = [
|
|
11
|
+
{
|
|
12
|
+
step: 1, title: 'Analyze Project', command: 'helixevo project-setup <path>',
|
|
13
|
+
desc: 'Reads README.md, package.json, CLAUDE.md, tsconfig, Dockerfile, and source tree to understand what the project does and what technologies it uses.',
|
|
14
|
+
icon: '📁', color: 'var(--blue)',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
step: 2, title: 'Match Skills', command: '(included in project-setup)',
|
|
18
|
+
desc: 'Scores each of your existing skills 0-100% for relevance to this project. Skills below 20% relevance are excluded.',
|
|
19
|
+
icon: '🔗', color: 'var(--purple)',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
step: 3, title: 'Find Gaps', command: '(included in project-setup)',
|
|
23
|
+
desc: 'Identifies capabilities the project needs but no skill covers. Each gap is rated high/medium/low priority with a suggested action.',
|
|
24
|
+
icon: '🔍', color: 'var(--yellow)',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
step: 4, title: 'Fill Gaps', command: 'helixevo research / specialize / create',
|
|
28
|
+
desc: 'For each gap, choose: Research (web search for best practices), Specialize (adapt a general skill), or Create (write from scratch).',
|
|
29
|
+
icon: '✓', color: 'var(--green)',
|
|
30
|
+
},
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
export default function ProjectsClient({ profiles, skillCount }: { profiles: ProjectProfile[]; skillCount: number }) {
|
|
34
|
+
const [inputMode, setInputMode] = useState<InputMode>('local')
|
|
35
|
+
const [projectPath, setProjectPath] = useState('')
|
|
36
|
+
const [githubUrl, setGithubUrl] = useState('')
|
|
37
|
+
const [cloneDir, setCloneDir] = useState('~/projects')
|
|
38
|
+
const [setupState, setSetupState] = useState<SetupState>('idle')
|
|
39
|
+
const [output, setOutput] = useState('')
|
|
40
|
+
const outputRef = useRef<HTMLPreElement | null>(null)
|
|
41
|
+
const abortRef = useRef<AbortController | null>(null)
|
|
42
|
+
|
|
43
|
+
const effectivePath = inputMode === 'local' ? projectPath.trim() : ''
|
|
44
|
+
const canStart = inputMode === 'local' ? !!projectPath.trim() : !!githubUrl.trim()
|
|
45
|
+
|
|
46
|
+
const handleSetup = async () => {
|
|
47
|
+
if (!canStart) return
|
|
48
|
+
|
|
49
|
+
let path = ''
|
|
50
|
+
if (inputMode === 'github') {
|
|
51
|
+
// Extract repo name from URL for clone path
|
|
52
|
+
const repoMatch = githubUrl.match(/github\.com\/[\w-]+\/([\w.-]+)/)
|
|
53
|
+
const repoName = repoMatch?.[1]?.replace(/\.git$/, '') ?? 'repo'
|
|
54
|
+
path = `${cloneDir}/${repoName}`
|
|
55
|
+
|
|
56
|
+
// Clone first
|
|
57
|
+
setSetupState('analyzing')
|
|
58
|
+
setOutput(`Cloning ${githubUrl} to ${path}...\n`)
|
|
59
|
+
try {
|
|
60
|
+
const cloneRes = await fetch('/api/run', {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { 'Content-Type': 'application/json' },
|
|
63
|
+
body: JSON.stringify({ command: 'clone', url: githubUrl, dir: path }),
|
|
64
|
+
})
|
|
65
|
+
// For now, use a simple approach
|
|
66
|
+
setOutput(prev => prev + `Clone target: ${path}\n(Note: Git clone from dashboard requires the repo to already be cloned locally)\n\n`)
|
|
67
|
+
} catch {}
|
|
68
|
+
} else {
|
|
69
|
+
path = projectPath.trim()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setSetupState('analyzing')
|
|
73
|
+
setOutput('')
|
|
74
|
+
|
|
75
|
+
const controller = new AbortController()
|
|
76
|
+
abortRef.current = controller
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const res = await fetch('/api/project-setup', {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: { 'Content-Type': 'application/json' },
|
|
82
|
+
body: JSON.stringify({ path }),
|
|
83
|
+
signal: controller.signal,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
if (!res.body) { setOutput('No response'); setSetupState('error'); return }
|
|
87
|
+
|
|
88
|
+
const reader = res.body.getReader()
|
|
89
|
+
const decoder = new TextDecoder()
|
|
90
|
+
let sseBuffer = ''
|
|
91
|
+
|
|
92
|
+
while (true) {
|
|
93
|
+
const { done, value } = await reader.read()
|
|
94
|
+
if (done) break
|
|
95
|
+
sseBuffer += decoder.decode(value, { stream: true })
|
|
96
|
+
const events = sseBuffer.split('\n\n')
|
|
97
|
+
sseBuffer = events.pop() ?? ''
|
|
98
|
+
for (const event of events) {
|
|
99
|
+
const lines = event.split('\n')
|
|
100
|
+
let eventType = '', eventData = ''
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
if (line.startsWith('event: ')) eventType = line.slice(7)
|
|
103
|
+
if (line.startsWith('data: ')) eventData = line.slice(6)
|
|
104
|
+
}
|
|
105
|
+
if (eventType === 'output' && eventData) {
|
|
106
|
+
try {
|
|
107
|
+
setOutput(prev => prev + (JSON.parse(eventData) as string))
|
|
108
|
+
setTimeout(() => { if (outputRef.current) outputRef.current.scrollTop = outputRef.current.scrollHeight }, 10)
|
|
109
|
+
} catch {}
|
|
110
|
+
}
|
|
111
|
+
if (eventType === 'done' && eventData) {
|
|
112
|
+
try {
|
|
113
|
+
const r = JSON.parse(JSON.parse(eventData) as string) as { success: boolean }
|
|
114
|
+
setSetupState(r.success ? 'done' : 'error')
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (setupState === 'analyzing') setSetupState('done')
|
|
120
|
+
} catch (err: unknown) {
|
|
121
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
122
|
+
setOutput(prev => prev + '\n\n[Cancelled]')
|
|
123
|
+
} else {
|
|
124
|
+
setOutput(prev => prev || 'Network error')
|
|
125
|
+
}
|
|
126
|
+
setSetupState('error')
|
|
127
|
+
} finally {
|
|
128
|
+
abortRef.current = null
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div>
|
|
134
|
+
<div className="page-header">
|
|
135
|
+
<h1 className="page-title">Project Setup</h1>
|
|
136
|
+
<p className="page-desc">
|
|
137
|
+
Analyze any project to match your {skillCount} skills, identify gaps, and prepare for work
|
|
138
|
+
</p>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{/* Pipeline Visualization */}
|
|
142
|
+
<div className="card" style={{ marginBottom: 24, padding: '22px 26px' }}>
|
|
143
|
+
<div style={{ fontSize: 10, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 16 }}>
|
|
144
|
+
How Project Setup Works
|
|
145
|
+
</div>
|
|
146
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 0 }}>
|
|
147
|
+
{PIPELINE.map((step, i) => (
|
|
148
|
+
<div key={step.step} style={{ display: 'flex', alignItems: 'flex-start' }}>
|
|
149
|
+
<div style={{ flex: 1 }}>
|
|
150
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
|
151
|
+
<div style={{
|
|
152
|
+
width: 32, height: 32, borderRadius: 9,
|
|
153
|
+
background: `${step.color}15`,
|
|
154
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
155
|
+
fontSize: 14, flexShrink: 0,
|
|
156
|
+
}}>{step.icon}</div>
|
|
157
|
+
<div>
|
|
158
|
+
<div style={{ fontSize: 9, fontWeight: 600, color: step.color, letterSpacing: 0.5 }}>STEP {step.step}</div>
|
|
159
|
+
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>{step.title}</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
<div style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.5, paddingRight: 12, marginBottom: 6 }}>
|
|
163
|
+
{step.desc}
|
|
164
|
+
</div>
|
|
165
|
+
<code style={{ fontSize: 9, padding: '2px 6px', background: 'var(--bg-section)', borderRadius: 3, color: 'var(--text-muted)' }}>
|
|
166
|
+
{step.command}
|
|
167
|
+
</code>
|
|
168
|
+
</div>
|
|
169
|
+
{i < 3 && (
|
|
170
|
+
<span style={{ padding: '12px 6px 0', color: 'var(--text-muted)', fontSize: 14 }}>→</span>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
))}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
{/* Input Section */}
|
|
178
|
+
<div className="card" style={{ marginBottom: 24, padding: '20px 24px' }}>
|
|
179
|
+
{/* Mode Toggle */}
|
|
180
|
+
<div style={{ display: 'flex', gap: 0, marginBottom: 16 }}>
|
|
181
|
+
<button
|
|
182
|
+
onClick={() => setInputMode('local')}
|
|
183
|
+
style={{
|
|
184
|
+
padding: '7px 16px', fontSize: 12, fontWeight: 600, cursor: 'pointer',
|
|
185
|
+
background: inputMode === 'local' ? 'var(--bg-card)' : 'var(--bg-section)',
|
|
186
|
+
border: `1px solid ${inputMode === 'local' ? 'var(--border-focus)' : 'var(--border)'}`,
|
|
187
|
+
borderRadius: '8px 0 0 8px', color: inputMode === 'local' ? 'var(--text)' : 'var(--text-dim)',
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
📁 Local Folder
|
|
191
|
+
</button>
|
|
192
|
+
<button
|
|
193
|
+
onClick={() => setInputMode('github')}
|
|
194
|
+
style={{
|
|
195
|
+
padding: '7px 16px', fontSize: 12, fontWeight: 600, cursor: 'pointer',
|
|
196
|
+
background: inputMode === 'github' ? 'var(--bg-card)' : 'var(--bg-section)',
|
|
197
|
+
border: `1px solid ${inputMode === 'github' ? 'var(--border-focus)' : 'var(--border)'}`,
|
|
198
|
+
borderRadius: '0 8px 8px 0', borderLeft: 'none',
|
|
199
|
+
color: inputMode === 'github' ? 'var(--text)' : 'var(--text-dim)',
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
GitHub URL
|
|
203
|
+
</button>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
{/* Local Folder Input */}
|
|
207
|
+
{inputMode === 'local' && (
|
|
208
|
+
<div style={{ display: 'flex', gap: 10, alignItems: 'flex-end' }}>
|
|
209
|
+
<div style={{ flex: 1 }}>
|
|
210
|
+
<label style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-dim)', display: 'block', marginBottom: 5 }}>
|
|
211
|
+
Project folder path
|
|
212
|
+
</label>
|
|
213
|
+
<input
|
|
214
|
+
value={projectPath}
|
|
215
|
+
onChange={e => setProjectPath(e.target.value)}
|
|
216
|
+
placeholder="/path/to/your/project or . or ~/projects/myapp"
|
|
217
|
+
disabled={setupState === 'analyzing'}
|
|
218
|
+
style={{
|
|
219
|
+
width: '100%', padding: '9px 14px',
|
|
220
|
+
border: '1px solid var(--border)', borderRadius: 'var(--radius)',
|
|
221
|
+
fontSize: 13, fontFamily: 'var(--font-mono)',
|
|
222
|
+
background: 'var(--bg-input)', color: 'var(--text)',
|
|
223
|
+
}}
|
|
224
|
+
/>
|
|
225
|
+
<div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 4 }}>
|
|
226
|
+
Use <code style={{ fontSize: 9 }}>.</code> for the current directory, or an absolute path
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
{setupState !== 'analyzing' ? (
|
|
230
|
+
<button onClick={handleSetup} disabled={!canStart} style={{
|
|
231
|
+
padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
|
|
232
|
+
background: canStart ? 'var(--green)' : 'var(--bg-section)',
|
|
233
|
+
color: canStart ? '#fff' : 'var(--text-muted)',
|
|
234
|
+
fontSize: 13, fontWeight: 600, cursor: canStart ? 'pointer' : 'default',
|
|
235
|
+
whiteSpace: 'nowrap',
|
|
236
|
+
}}>
|
|
237
|
+
Analyze Project
|
|
238
|
+
</button>
|
|
239
|
+
) : (
|
|
240
|
+
<button onClick={() => abortRef.current?.abort()} style={{
|
|
241
|
+
padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
|
|
242
|
+
background: 'var(--red)', color: '#fff',
|
|
243
|
+
fontSize: 13, fontWeight: 600, cursor: 'pointer', whiteSpace: 'nowrap',
|
|
244
|
+
}}>
|
|
245
|
+
Stop
|
|
246
|
+
</button>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
)}
|
|
250
|
+
|
|
251
|
+
{/* GitHub URL Input */}
|
|
252
|
+
{inputMode === 'github' && (
|
|
253
|
+
<div>
|
|
254
|
+
<div style={{ display: 'flex', gap: 10, alignItems: 'flex-end', marginBottom: 10 }}>
|
|
255
|
+
<div style={{ flex: 1 }}>
|
|
256
|
+
<label style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-dim)', display: 'block', marginBottom: 5 }}>
|
|
257
|
+
GitHub repository URL
|
|
258
|
+
</label>
|
|
259
|
+
<input
|
|
260
|
+
value={githubUrl}
|
|
261
|
+
onChange={e => setGithubUrl(e.target.value)}
|
|
262
|
+
placeholder="https://github.com/user/repo"
|
|
263
|
+
disabled={setupState === 'analyzing'}
|
|
264
|
+
style={{
|
|
265
|
+
width: '100%', padding: '9px 14px',
|
|
266
|
+
border: '1px solid var(--border)', borderRadius: 'var(--radius)',
|
|
267
|
+
fontSize: 13, fontFamily: 'var(--font-mono)',
|
|
268
|
+
background: 'var(--bg-input)', color: 'var(--text)',
|
|
269
|
+
}}
|
|
270
|
+
/>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
<div style={{ display: 'flex', gap: 10, alignItems: 'flex-end' }}>
|
|
274
|
+
<div style={{ flex: 1 }}>
|
|
275
|
+
<label style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-dim)', display: 'block', marginBottom: 5 }}>
|
|
276
|
+
Clone to directory
|
|
277
|
+
</label>
|
|
278
|
+
<input
|
|
279
|
+
value={cloneDir}
|
|
280
|
+
onChange={e => setCloneDir(e.target.value)}
|
|
281
|
+
placeholder="~/projects"
|
|
282
|
+
disabled={setupState === 'analyzing'}
|
|
283
|
+
style={{
|
|
284
|
+
width: '100%', padding: '9px 14px',
|
|
285
|
+
border: '1px solid var(--border)', borderRadius: 'var(--radius)',
|
|
286
|
+
fontSize: 13, fontFamily: 'var(--font-mono)',
|
|
287
|
+
background: 'var(--bg-input)', color: 'var(--text)',
|
|
288
|
+
}}
|
|
289
|
+
/>
|
|
290
|
+
</div>
|
|
291
|
+
{setupState !== 'analyzing' ? (
|
|
292
|
+
<button onClick={handleSetup} disabled={!canStart} style={{
|
|
293
|
+
padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
|
|
294
|
+
background: canStart ? 'var(--green)' : 'var(--bg-section)',
|
|
295
|
+
color: canStart ? '#fff' : 'var(--text-muted)',
|
|
296
|
+
fontSize: 13, fontWeight: 600, cursor: canStart ? 'pointer' : 'default',
|
|
297
|
+
whiteSpace: 'nowrap',
|
|
298
|
+
}}>
|
|
299
|
+
Clone & Analyze
|
|
300
|
+
</button>
|
|
301
|
+
) : (
|
|
302
|
+
<button onClick={() => abortRef.current?.abort()} style={{
|
|
303
|
+
padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
|
|
304
|
+
background: 'var(--red)', color: '#fff',
|
|
305
|
+
fontSize: 13, fontWeight: 600, cursor: 'pointer', whiteSpace: 'nowrap',
|
|
306
|
+
}}>
|
|
307
|
+
Stop
|
|
308
|
+
</button>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
<div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 6 }}>
|
|
312
|
+
The repository will be cloned to <code style={{ fontSize: 9 }}>{cloneDir}/{githubUrl.match(/\/([\w.-]+?)(?:\.git)?$/)?.[1] ?? 'repo'}</code>, then analyzed
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
{/* Output */}
|
|
319
|
+
{setupState !== 'idle' && (
|
|
320
|
+
<div className="card" style={{ marginBottom: 24, overflow: 'hidden' }}>
|
|
321
|
+
<div style={{
|
|
322
|
+
padding: '10px 16px',
|
|
323
|
+
background: setupState === 'done' ? 'var(--green-light)' : setupState === 'error' ? 'var(--red-light)' : 'var(--bg-section)',
|
|
324
|
+
borderBottom: '1px solid var(--border)',
|
|
325
|
+
fontSize: 12, fontWeight: 600,
|
|
326
|
+
color: setupState === 'done' ? 'var(--green)' : setupState === 'error' ? 'var(--red)' : 'var(--text-secondary)',
|
|
327
|
+
}}>
|
|
328
|
+
{setupState === 'analyzing' && '● Running project analysis...'}
|
|
329
|
+
{setupState === 'done' && '✓ Analysis complete — project profile saved'}
|
|
330
|
+
{setupState === 'error' && '✗ Analysis failed'}
|
|
331
|
+
</div>
|
|
332
|
+
<pre ref={outputRef} style={{
|
|
333
|
+
padding: '14px 16px', margin: 0,
|
|
334
|
+
fontSize: 11, lineHeight: 1.55,
|
|
335
|
+
fontFamily: 'var(--font-mono)',
|
|
336
|
+
color: 'var(--text-secondary)',
|
|
337
|
+
maxHeight: 500, overflow: 'auto',
|
|
338
|
+
whiteSpace: 'pre-wrap', wordBreak: 'break-word',
|
|
339
|
+
}}>
|
|
340
|
+
{output || 'Starting project analysis...'}
|
|
341
|
+
</pre>
|
|
342
|
+
{setupState === 'done' && (
|
|
343
|
+
<div style={{ padding: '12px 16px', borderTop: '1px solid var(--border)', display: 'flex', gap: 8 }}>
|
|
344
|
+
<button onClick={() => window.location.reload()} style={{
|
|
345
|
+
padding: '7px 14px', background: 'var(--green)', color: '#fff', border: 'none',
|
|
346
|
+
borderRadius: 'var(--radius)', fontSize: 12, fontWeight: 600, cursor: 'pointer',
|
|
347
|
+
}}>
|
|
348
|
+
Refresh to see results
|
|
349
|
+
</button>
|
|
350
|
+
<Link href="/network" style={{ textDecoration: 'none' }}>
|
|
351
|
+
<button style={{
|
|
352
|
+
padding: '7px 14px', background: 'var(--bg-section)', border: '1px solid var(--border)',
|
|
353
|
+
borderRadius: 'var(--radius)', fontSize: 12, fontWeight: 600, cursor: 'pointer', color: 'var(--text-secondary)',
|
|
354
|
+
}}>
|
|
355
|
+
View Skill Network
|
|
356
|
+
</button>
|
|
357
|
+
</Link>
|
|
358
|
+
<Link href="/research" style={{ textDecoration: 'none' }}>
|
|
359
|
+
<button style={{
|
|
360
|
+
padding: '7px 14px', background: 'var(--bg-section)', border: '1px solid var(--border)',
|
|
361
|
+
borderRadius: 'var(--radius)', fontSize: 12, fontWeight: 600, cursor: 'pointer', color: 'var(--text-secondary)',
|
|
362
|
+
}}>
|
|
363
|
+
Research Gaps
|
|
364
|
+
</button>
|
|
365
|
+
</Link>
|
|
366
|
+
</div>
|
|
367
|
+
)}
|
|
368
|
+
</div>
|
|
369
|
+
)}
|
|
370
|
+
|
|
371
|
+
{/* Existing Profiles */}
|
|
372
|
+
{profiles.length > 0 && (
|
|
373
|
+
<div>
|
|
374
|
+
<div style={{ fontSize: 10, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 12 }}>
|
|
375
|
+
Analyzed Projects ({profiles.length})
|
|
376
|
+
</div>
|
|
377
|
+
<div style={{ display: 'grid', gap: 14 }}>
|
|
378
|
+
{profiles.map(p => {
|
|
379
|
+
const highGaps = p.gaps.filter(g => g.priority === 'high')
|
|
380
|
+
const matchedPct = skillCount > 0 ? Math.round((p.matchedSkills.length / skillCount) * 100) : 0
|
|
381
|
+
|
|
382
|
+
return (
|
|
383
|
+
<div key={p.name} className="card">
|
|
384
|
+
<div className="card-body">
|
|
385
|
+
{/* Header */}
|
|
386
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 12 }}>
|
|
387
|
+
<div>
|
|
388
|
+
<div style={{ fontSize: 18, fontWeight: 700, marginBottom: 4 }}>{p.name}</div>
|
|
389
|
+
<div style={{ fontSize: 12, color: 'var(--text-dim)', marginBottom: 6 }}>{p.description}</div>
|
|
390
|
+
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
|
391
|
+
{p.techStack.map(t => <span key={t} className="badge badge-gray">{t}</span>)}
|
|
392
|
+
{p.domains.map(d => <span key={d} className="badge badge-blue">{d}</span>)}
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 4 }}>
|
|
396
|
+
<span className={`badge ${p.status === 'active' ? 'badge-green' : 'badge-blue'}`}>{p.status}</span>
|
|
397
|
+
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>{new Date(p.analyzedAt).toLocaleDateString()}</span>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
|
|
401
|
+
{/* Stats */}
|
|
402
|
+
<div className="grid-3" style={{ gap: 10, marginBottom: 16 }}>
|
|
403
|
+
<div style={{ padding: '12px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
|
|
404
|
+
<div style={{ fontSize: 24, fontWeight: 800, color: 'var(--green)' }}>{p.matchedSkills.length}</div>
|
|
405
|
+
<div style={{ fontSize: 11, color: 'var(--text-dim)' }}>Skills matched ({matchedPct}%)</div>
|
|
406
|
+
</div>
|
|
407
|
+
<div style={{ padding: '12px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
|
|
408
|
+
<div style={{ fontSize: 24, fontWeight: 800, color: highGaps.length > 0 ? 'var(--red)' : 'var(--green)' }}>{p.gaps.length}</div>
|
|
409
|
+
<div style={{ fontSize: 11, color: 'var(--text-dim)' }}>Gaps ({highGaps.length} high priority)</div>
|
|
410
|
+
</div>
|
|
411
|
+
<div style={{ padding: '12px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
|
|
412
|
+
<div style={{ fontSize: 11, color: 'var(--text-dim)', marginBottom: 4 }}>Path</div>
|
|
413
|
+
<div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--text-secondary)', wordBreak: 'break-all' }}>{p.path}</div>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
|
|
417
|
+
{/* Matched Skills */}
|
|
418
|
+
{p.matchedSkills.length > 0 && (
|
|
419
|
+
<div style={{ marginBottom: 14 }}>
|
|
420
|
+
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 6 }}>MATCHED SKILLS</div>
|
|
421
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
422
|
+
{p.matchedSkills.sort((a, b) => b.relevance - a.relevance).slice(0, 8).map(m => (
|
|
423
|
+
<div key={m.slug} style={{
|
|
424
|
+
display: 'flex', alignItems: 'center', gap: 10,
|
|
425
|
+
padding: '6px 10px', background: 'var(--bg-section)', borderRadius: 'var(--radius)',
|
|
426
|
+
}}>
|
|
427
|
+
<Link href="/network" style={{ textDecoration: 'none', fontWeight: 600, fontSize: 12, color: 'var(--text)' }}>{m.slug}</Link>
|
|
428
|
+
<div style={{ flex: 1 }}>
|
|
429
|
+
<div className="score-track" style={{ height: 4 }}>
|
|
430
|
+
<div className="score-fill" style={{
|
|
431
|
+
width: `${m.relevance}%`,
|
|
432
|
+
background: m.relevance >= 70 ? 'var(--green)' : m.relevance >= 40 ? 'var(--yellow)' : 'var(--text-muted)',
|
|
433
|
+
}} />
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
<span style={{ fontSize: 12, fontWeight: 700, color: m.relevance >= 70 ? 'var(--green)' : 'var(--yellow)', minWidth: 32 }}>{m.relevance}%</span>
|
|
437
|
+
<span style={{ fontSize: 10, color: 'var(--text-dim)', maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.reason}</span>
|
|
438
|
+
</div>
|
|
439
|
+
))}
|
|
440
|
+
{p.matchedSkills.length > 8 && (
|
|
441
|
+
<div style={{ fontSize: 10, color: 'var(--text-muted)', paddingLeft: 10 }}>+{p.matchedSkills.length - 8} more skills</div>
|
|
442
|
+
)}
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
)}
|
|
446
|
+
|
|
447
|
+
{/* Gaps with Action Buttons */}
|
|
448
|
+
{p.gaps.length > 0 && (
|
|
449
|
+
<div style={{ marginBottom: 14 }}>
|
|
450
|
+
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 6 }}>GAPS — CLICK TO FILL</div>
|
|
451
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
452
|
+
{p.gaps.map((g, i) => (
|
|
453
|
+
<div key={i} style={{
|
|
454
|
+
padding: '10px 14px', borderRadius: 'var(--radius)',
|
|
455
|
+
background: g.priority === 'high' ? 'var(--red-light)' : g.priority === 'medium' ? 'var(--yellow-light)' : 'var(--bg-section)',
|
|
456
|
+
borderLeft: `3px solid ${g.priority === 'high' ? 'var(--red)' : g.priority === 'medium' ? 'var(--yellow)' : 'var(--text-muted)'}`,
|
|
457
|
+
}}>
|
|
458
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 4 }}>
|
|
459
|
+
<div>
|
|
460
|
+
<span style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>{g.area}</span>
|
|
461
|
+
<span className={`badge ${g.priority === 'high' ? 'badge-red' : g.priority === 'medium' ? 'badge-yellow' : 'badge-gray'}`} style={{ marginLeft: 6 }}>{g.priority}</span>
|
|
462
|
+
</div>
|
|
463
|
+
{g.suggestedAction === 'research' && (
|
|
464
|
+
<Link href="/research" style={{ textDecoration: 'none' }}>
|
|
465
|
+
<button style={{
|
|
466
|
+
padding: '4px 12px', fontSize: 10, fontWeight: 600,
|
|
467
|
+
background: 'var(--blue)', color: '#fff', border: 'none',
|
|
468
|
+
borderRadius: 'var(--radius)', cursor: 'pointer',
|
|
469
|
+
}}>
|
|
470
|
+
Research this gap
|
|
471
|
+
</button>
|
|
472
|
+
</Link>
|
|
473
|
+
)}
|
|
474
|
+
{g.suggestedAction === 'specialize' && (
|
|
475
|
+
<button style={{
|
|
476
|
+
padding: '4px 12px', fontSize: 10, fontWeight: 600,
|
|
477
|
+
background: 'var(--green)', color: '#fff', border: 'none',
|
|
478
|
+
borderRadius: 'var(--radius)', cursor: 'pointer',
|
|
479
|
+
}}>
|
|
480
|
+
Specialize a skill
|
|
481
|
+
</button>
|
|
482
|
+
)}
|
|
483
|
+
{g.suggestedAction === 'create' && (
|
|
484
|
+
<Link href="/network" style={{ textDecoration: 'none' }}>
|
|
485
|
+
<button style={{
|
|
486
|
+
padding: '4px 12px', fontSize: 10, fontWeight: 600,
|
|
487
|
+
background: 'var(--bg-section)', color: 'var(--text-secondary)',
|
|
488
|
+
border: '1px solid var(--border)',
|
|
489
|
+
borderRadius: 'var(--radius)', cursor: 'pointer',
|
|
490
|
+
}}>
|
|
491
|
+
Create manually
|
|
492
|
+
</button>
|
|
493
|
+
</Link>
|
|
494
|
+
)}
|
|
495
|
+
</div>
|
|
496
|
+
<div style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.5 }}>
|
|
497
|
+
{g.description}
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
))}
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
)}
|
|
504
|
+
|
|
505
|
+
{/* Recommendations */}
|
|
506
|
+
{p.recommendations.length > 0 && (
|
|
507
|
+
<div style={{ padding: '10px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
|
|
508
|
+
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 4 }}>RECOMMENDED NEXT STEPS</div>
|
|
509
|
+
{p.recommendations.map((r, i) => (
|
|
510
|
+
<div key={i} style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.6 }}>→ {r}</div>
|
|
511
|
+
))}
|
|
512
|
+
</div>
|
|
513
|
+
)}
|
|
514
|
+
|
|
515
|
+
{/* Re-analyze button */}
|
|
516
|
+
<div style={{ marginTop: 12, display: 'flex', gap: 8 }}>
|
|
517
|
+
<button onClick={() => { setProjectPath(p.path); setInputMode('local'); window.scrollTo(0, 0) }} style={{
|
|
518
|
+
padding: '6px 12px', fontSize: 11, fontWeight: 600,
|
|
519
|
+
background: 'var(--bg-section)', border: '1px solid var(--border)',
|
|
520
|
+
borderRadius: 'var(--radius)', cursor: 'pointer', color: 'var(--text-secondary)',
|
|
521
|
+
}}>
|
|
522
|
+
Re-analyze
|
|
523
|
+
</button>
|
|
524
|
+
<Link href="/network" style={{ textDecoration: 'none' }}>
|
|
525
|
+
<button style={{
|
|
526
|
+
padding: '6px 12px', fontSize: 11, fontWeight: 600,
|
|
527
|
+
background: 'var(--bg-section)', border: '1px solid var(--border)',
|
|
528
|
+
borderRadius: 'var(--radius)', cursor: 'pointer', color: 'var(--text-secondary)',
|
|
529
|
+
}}>
|
|
530
|
+
View in Skill Network
|
|
531
|
+
</button>
|
|
532
|
+
</Link>
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
)
|
|
537
|
+
})}
|
|
538
|
+
</div>
|
|
539
|
+
</div>
|
|
540
|
+
)}
|
|
541
|
+
|
|
542
|
+
{/* Empty state */}
|
|
543
|
+
{profiles.length === 0 && setupState === 'idle' && (
|
|
544
|
+
<div className="card">
|
|
545
|
+
<div className="empty-state">
|
|
546
|
+
<div className="empty-state-icon">
|
|
547
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
548
|
+
<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" />
|
|
549
|
+
</svg>
|
|
550
|
+
</div>
|
|
551
|
+
<div className="empty-state-title">No projects analyzed yet</div>
|
|
552
|
+
<div className="empty-state-desc">
|
|
553
|
+
Enter a project folder path or GitHub URL above. HelixEvo will analyze it against your {skillCount} skills,
|
|
554
|
+
identify which ones match, and tell you what gaps need filling.
|
|
555
|
+
</div>
|
|
556
|
+
</div>
|
|
557
|
+
</div>
|
|
558
|
+
)}
|
|
559
|
+
</div>
|
|
560
|
+
)
|
|
561
|
+
}
|
|
@@ -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