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 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
+ }
@@ -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 }}>&rarr;</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
+ }
@@ -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/utils/update-check.ts
13161
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync11, existsSync as existsSync14, mkdirSync as mkdirSync5 } from "node:fs";
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 = join18(homedir4(), ".helix");
13165
- var CACHE_PATH = join18(HELIX_DIR2, "update-check.json");
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 (!existsSync14(CACHE_PATH))
13411
+ if (!existsSync15(CACHE_PATH))
13171
13412
  return null;
13172
- return JSON.parse(readFileSync11(CACHE_PATH, "utf-8"));
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 (!existsSync14(HELIX_DIR2))
13180
- mkdirSync5(HELIX_DIR2, { recursive: true });
13181
- writeFileSync11(CACHE_PATH, JSON.stringify(cache));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helixevo",
3
- "version": "0.2.34",
3
+ "version": "0.2.35",
4
4
  "description": "Self-evolving skill ecosystem for AI agents. Skills and projects co-evolve through multi-judge evaluation and a Pareto frontier.",
5
5
  "type": "module",
6
6
  "bin": {