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 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
+ }
@@ -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
  ]
@@ -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)' }}>&rarr;</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 }}>&rarr;</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 &amp; 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
+ }
@@ -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.36",
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": {