groove-dev 0.12.5 → 0.12.7
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/node_modules/@groove-dev/daemon/src/api.js +47 -1
- package/node_modules/@groove-dev/daemon/src/firstrun.js +25 -13
- package/node_modules/@groove-dev/gui/dist/assets/index-D4-7VM-v.js +73 -0
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/src/components/DirPicker.jsx +209 -0
- package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +53 -46
- package/package.json +1 -1
- package/packages/daemon/src/api.js +47 -1
- package/packages/daemon/src/firstrun.js +25 -13
- package/packages/gui/dist/assets/index-D4-7VM-v.js +73 -0
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/src/components/DirPicker.jsx +209 -0
- package/packages/gui/src/components/SpawnPanel.jsx +53 -46
- package/node_modules/@groove-dev/gui/dist/assets/index-B49YqEXS.js +0 -73
- package/packages/gui/dist/assets/index-B49YqEXS.js +0 -73
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>GROOVE</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-D4-7VM-v.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-Gfb8Zxy9.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// GROOVE GUI — Directory Picker
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
|
|
4
|
+
import React, { useState, useEffect } from 'react';
|
|
5
|
+
|
|
6
|
+
export default function DirPicker({ onSelect, onClose, initial }) {
|
|
7
|
+
const [currentPath, setCurrentPath] = useState(initial || '');
|
|
8
|
+
const [dirs, setDirs] = useState([]);
|
|
9
|
+
const [parent, setParent] = useState(null);
|
|
10
|
+
const [loading, setLoading] = useState(true);
|
|
11
|
+
const [error, setError] = useState('');
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
browse(currentPath);
|
|
15
|
+
}, [currentPath]);
|
|
16
|
+
|
|
17
|
+
async function browse(path) {
|
|
18
|
+
setLoading(true);
|
|
19
|
+
setError('');
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch(`/api/browse?path=${encodeURIComponent(path)}`);
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
const err = await res.json().catch(() => ({}));
|
|
24
|
+
setError(err.error || 'Failed to browse');
|
|
25
|
+
setDirs([]);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const data = await res.json();
|
|
29
|
+
setDirs(data.dirs);
|
|
30
|
+
setParent(data.parent);
|
|
31
|
+
} catch {
|
|
32
|
+
setError('Failed to connect');
|
|
33
|
+
} finally {
|
|
34
|
+
setLoading(false);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const breadcrumbs = currentPath
|
|
39
|
+
? ['root', ...currentPath.split('/')]
|
|
40
|
+
: ['root'];
|
|
41
|
+
|
|
42
|
+
function handleBreadcrumbClick(index) {
|
|
43
|
+
if (index === 0) {
|
|
44
|
+
setCurrentPath('');
|
|
45
|
+
} else {
|
|
46
|
+
const parts = currentPath.split('/');
|
|
47
|
+
setCurrentPath(parts.slice(0, index).join('/'));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div style={styles.overlay} onClick={onClose}>
|
|
53
|
+
<div style={styles.modal} onClick={(e) => e.stopPropagation()}>
|
|
54
|
+
<div style={styles.header}>
|
|
55
|
+
<span style={styles.title}>SELECT DIRECTORY</span>
|
|
56
|
+
<button onClick={onClose} style={styles.closeBtn}>x</button>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{/* Breadcrumb */}
|
|
60
|
+
<div style={styles.breadcrumb}>
|
|
61
|
+
{breadcrumbs.map((part, i) => (
|
|
62
|
+
<span key={i}>
|
|
63
|
+
{i > 0 && <span style={styles.breadSep}>/</span>}
|
|
64
|
+
<button
|
|
65
|
+
onClick={() => handleBreadcrumbClick(i)}
|
|
66
|
+
style={{
|
|
67
|
+
...styles.breadPart,
|
|
68
|
+
color: i === breadcrumbs.length - 1 ? 'var(--text-bright)' : 'var(--text-dim)',
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
{part}
|
|
72
|
+
</button>
|
|
73
|
+
</span>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{/* Directory list */}
|
|
78
|
+
<div style={styles.list}>
|
|
79
|
+
{parent !== null && (
|
|
80
|
+
<button
|
|
81
|
+
onClick={() => setCurrentPath(parent)}
|
|
82
|
+
style={styles.dirRow}
|
|
83
|
+
>
|
|
84
|
+
<span style={styles.dirIcon}>..</span>
|
|
85
|
+
<span style={styles.dirName}>parent directory</span>
|
|
86
|
+
</button>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{loading && <div style={styles.empty}>loading...</div>}
|
|
90
|
+
{error && <div style={styles.errorText}>{error}</div>}
|
|
91
|
+
|
|
92
|
+
{!loading && !error && dirs.length === 0 && (
|
|
93
|
+
<div style={styles.empty}>no subdirectories</div>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
{dirs.map((dir) => (
|
|
97
|
+
<button
|
|
98
|
+
key={dir.path}
|
|
99
|
+
onClick={() => setCurrentPath(dir.path)}
|
|
100
|
+
style={styles.dirRow}
|
|
101
|
+
>
|
|
102
|
+
<span style={styles.dirIcon}>{dir.hasChildren ? '+' : ' '}</span>
|
|
103
|
+
<span style={styles.dirName}>{dir.name}</span>
|
|
104
|
+
</button>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{/* Footer — select current */}
|
|
109
|
+
<div style={styles.footer}>
|
|
110
|
+
<div style={styles.selectedPath}>
|
|
111
|
+
{currentPath || '(project root)'}
|
|
112
|
+
</div>
|
|
113
|
+
<button
|
|
114
|
+
onClick={() => { onSelect(currentPath); onClose(); }}
|
|
115
|
+
style={styles.selectBtn}
|
|
116
|
+
>
|
|
117
|
+
Select
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const styles = {
|
|
126
|
+
overlay: {
|
|
127
|
+
position: 'fixed', inset: 0, zIndex: 200,
|
|
128
|
+
background: 'rgba(0, 0, 0, 0.6)',
|
|
129
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
130
|
+
},
|
|
131
|
+
modal: {
|
|
132
|
+
width: 360, maxHeight: '70vh',
|
|
133
|
+
background: 'var(--bg-chrome)',
|
|
134
|
+
border: '1px solid var(--border)',
|
|
135
|
+
borderRadius: 4,
|
|
136
|
+
display: 'flex', flexDirection: 'column',
|
|
137
|
+
fontFamily: 'var(--font)',
|
|
138
|
+
},
|
|
139
|
+
header: {
|
|
140
|
+
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
|
141
|
+
padding: '10px 12px',
|
|
142
|
+
borderBottom: '1px solid var(--border)',
|
|
143
|
+
},
|
|
144
|
+
title: {
|
|
145
|
+
fontSize: 11, fontWeight: 600, color: 'var(--text-dim)',
|
|
146
|
+
textTransform: 'uppercase', letterSpacing: 1.5,
|
|
147
|
+
},
|
|
148
|
+
closeBtn: {
|
|
149
|
+
background: 'none', border: 'none', color: 'var(--text-dim)',
|
|
150
|
+
fontSize: 14, cursor: 'pointer', fontFamily: 'var(--font)',
|
|
151
|
+
padding: '0 4px',
|
|
152
|
+
},
|
|
153
|
+
breadcrumb: {
|
|
154
|
+
padding: '8px 12px',
|
|
155
|
+
borderBottom: '1px solid var(--border)',
|
|
156
|
+
fontSize: 11,
|
|
157
|
+
},
|
|
158
|
+
breadSep: {
|
|
159
|
+
color: 'var(--text-muted)', margin: '0 2px',
|
|
160
|
+
},
|
|
161
|
+
breadPart: {
|
|
162
|
+
background: 'none', border: 'none', cursor: 'pointer',
|
|
163
|
+
fontFamily: 'var(--font)', fontSize: 11,
|
|
164
|
+
padding: 0,
|
|
165
|
+
},
|
|
166
|
+
list: {
|
|
167
|
+
flex: 1, overflowY: 'auto',
|
|
168
|
+
padding: '4px 0',
|
|
169
|
+
minHeight: 120, maxHeight: 300,
|
|
170
|
+
},
|
|
171
|
+
dirRow: {
|
|
172
|
+
display: 'flex', alignItems: 'center', gap: 6,
|
|
173
|
+
width: '100%', padding: '6px 12px',
|
|
174
|
+
background: 'none', border: 'none',
|
|
175
|
+
color: 'var(--text-primary)', fontSize: 12,
|
|
176
|
+
fontFamily: 'var(--font)', cursor: 'pointer',
|
|
177
|
+
textAlign: 'left',
|
|
178
|
+
},
|
|
179
|
+
dirIcon: {
|
|
180
|
+
color: 'var(--accent)', fontSize: 11, width: 12, textAlign: 'center',
|
|
181
|
+
flexShrink: 0,
|
|
182
|
+
},
|
|
183
|
+
dirName: {
|
|
184
|
+
flex: 1,
|
|
185
|
+
},
|
|
186
|
+
empty: {
|
|
187
|
+
padding: '16px 12px', color: 'var(--text-dim)', fontSize: 11,
|
|
188
|
+
textAlign: 'center',
|
|
189
|
+
},
|
|
190
|
+
errorText: {
|
|
191
|
+
padding: '16px 12px', color: 'var(--red)', fontSize: 11,
|
|
192
|
+
textAlign: 'center',
|
|
193
|
+
},
|
|
194
|
+
footer: {
|
|
195
|
+
display: 'flex', alignItems: 'center', gap: 8,
|
|
196
|
+
padding: '10px 12px',
|
|
197
|
+
borderTop: '1px solid var(--border)',
|
|
198
|
+
},
|
|
199
|
+
selectedPath: {
|
|
200
|
+
flex: 1, fontSize: 11, color: 'var(--text-dim)',
|
|
201
|
+
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
|
202
|
+
},
|
|
203
|
+
selectBtn: {
|
|
204
|
+
padding: '5px 14px',
|
|
205
|
+
background: 'transparent', border: '1px solid var(--accent)',
|
|
206
|
+
borderRadius: 2, color: 'var(--accent)', fontSize: 11, fontWeight: 600,
|
|
207
|
+
fontFamily: 'var(--font)', cursor: 'pointer', flexShrink: 0,
|
|
208
|
+
},
|
|
209
|
+
};
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import React, { useState, useEffect } from 'react';
|
|
5
5
|
import { useGrooveStore } from '../stores/groove';
|
|
6
|
+
import DirPicker from './DirPicker';
|
|
6
7
|
|
|
7
8
|
const ROLE_PRESETS = [
|
|
8
9
|
{ id: 'backend', label: 'Backend', desc: 'APIs, server logic, database', scope: ['src/api/**', 'src/server/**', 'src/lib/**', 'src/db/**'] },
|
|
@@ -39,6 +40,7 @@ export default function SpawnPanel() {
|
|
|
39
40
|
const [connectingProvider, setConnectingProvider] = useState(null);
|
|
40
41
|
const [apiKeyInput, setApiKeyInput] = useState('');
|
|
41
42
|
const [keySaving, setKeySaving] = useState(false);
|
|
43
|
+
const [showDirPicker, setShowDirPicker] = useState(false);
|
|
42
44
|
|
|
43
45
|
useEffect(() => {
|
|
44
46
|
fetchProviders();
|
|
@@ -210,40 +212,51 @@ export default function SpawnPanel() {
|
|
|
210
212
|
rows={3}
|
|
211
213
|
/>
|
|
212
214
|
|
|
213
|
-
{/*
|
|
214
|
-
{
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
{
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
215
|
+
{/* Directory picker */}
|
|
216
|
+
<div style={styles.label}>DIRECTORY</div>
|
|
217
|
+
<div style={styles.wsRow}>
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
onClick={() => setWorkingDir('')}
|
|
221
|
+
style={{
|
|
222
|
+
...styles.wsBtn,
|
|
223
|
+
...(!workingDir ? { borderColor: 'var(--accent)', color: 'var(--text-bright)' } : {}),
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
project root
|
|
227
|
+
</button>
|
|
228
|
+
{workspaces.map((ws) => (
|
|
229
|
+
<button
|
|
230
|
+
key={ws.path}
|
|
231
|
+
type="button"
|
|
232
|
+
onClick={() => setWorkingDir(ws.path)}
|
|
233
|
+
style={{
|
|
234
|
+
...styles.wsBtn,
|
|
235
|
+
...(workingDir === ws.path ? { borderColor: 'var(--accent)', color: 'var(--text-bright)' } : {}),
|
|
236
|
+
}}
|
|
237
|
+
title={`${ws.name} (${ws.files} files)`}
|
|
238
|
+
>
|
|
239
|
+
{ws.path}
|
|
240
|
+
</button>
|
|
241
|
+
))}
|
|
242
|
+
<button
|
|
243
|
+
type="button"
|
|
244
|
+
onClick={() => setShowDirPicker(true)}
|
|
245
|
+
style={styles.browseBtn}
|
|
246
|
+
>
|
|
247
|
+
Browse...
|
|
248
|
+
</button>
|
|
249
|
+
</div>
|
|
250
|
+
{workingDir && (
|
|
251
|
+
<div style={styles.hint}>{workingDir}</div>
|
|
252
|
+
)}
|
|
253
|
+
|
|
254
|
+
{showDirPicker && (
|
|
255
|
+
<DirPicker
|
|
256
|
+
initial={workingDir}
|
|
257
|
+
onSelect={(path) => setWorkingDir(path)}
|
|
258
|
+
onClose={() => setShowDirPicker(false)}
|
|
259
|
+
/>
|
|
247
260
|
)}
|
|
248
261
|
|
|
249
262
|
{/* Permissions */}
|
|
@@ -279,18 +292,6 @@ export default function SpawnPanel() {
|
|
|
279
292
|
|
|
280
293
|
{showAdvanced && (
|
|
281
294
|
<>
|
|
282
|
-
{/* Working directory — manual input for custom paths */}
|
|
283
|
-
<div style={styles.label}>WORKING DIRECTORY</div>
|
|
284
|
-
<input
|
|
285
|
-
style={styles.input}
|
|
286
|
-
placeholder="e.g. packages/frontend (default: project root)"
|
|
287
|
-
value={workingDir}
|
|
288
|
-
onChange={(e) => setWorkingDir(e.target.value)}
|
|
289
|
-
/>
|
|
290
|
-
<div style={styles.hint}>
|
|
291
|
-
Relative path — or use the directory buttons above
|
|
292
|
-
</div>
|
|
293
|
-
|
|
294
295
|
{/* Provider selector with connection flow */}
|
|
295
296
|
<div style={styles.label}>PROVIDER</div>
|
|
296
297
|
{providerList.map((p) => {
|
|
@@ -571,6 +572,12 @@ const styles = {
|
|
|
571
572
|
fontFamily: 'var(--font)',
|
|
572
573
|
transition: 'color 0.1s, border-color 0.1s',
|
|
573
574
|
},
|
|
575
|
+
browseBtn: {
|
|
576
|
+
background: 'none', border: '1px dashed var(--border)',
|
|
577
|
+
borderRadius: 2, padding: '3px 8px',
|
|
578
|
+
color: 'var(--text-dim)', fontSize: 10, cursor: 'pointer',
|
|
579
|
+
fontFamily: 'var(--font)',
|
|
580
|
+
},
|
|
574
581
|
error: {
|
|
575
582
|
color: 'var(--red)', fontSize: 11, marginTop: 8,
|
|
576
583
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.7",
|
|
4
4
|
"description": "Open-source agent orchestration layer for AI coding tools. GUI dashboard, multi-agent coordination, zero cold-start (Journalist), infinite sessions (adaptive context rotation), AI Project Manager, Quick Launch. Works with Claude Code, Codex, Gemini CLI, Ollama.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import express from 'express';
|
|
5
5
|
import { resolve, dirname } from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
|
-
import { existsSync, readFileSync } from 'fs';
|
|
7
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
8
8
|
import { listProviders } from './providers/index.js';
|
|
9
9
|
import { validateAgentConfig } from './validate.js';
|
|
10
10
|
|
|
@@ -365,6 +365,52 @@ export function createApi(app, daemon) {
|
|
|
365
365
|
res.json(daemon.adaptive.getAllProfiles());
|
|
366
366
|
});
|
|
367
367
|
|
|
368
|
+
// --- Directory Browser ---
|
|
369
|
+
|
|
370
|
+
app.get('/api/browse', (req, res) => {
|
|
371
|
+
const relPath = req.query.path || '';
|
|
372
|
+
|
|
373
|
+
// Security: no absolute paths, no traversal
|
|
374
|
+
if (relPath.startsWith('/') || relPath.includes('..') || relPath.includes('\0')) {
|
|
375
|
+
return res.status(400).json({ error: 'Invalid path' });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const fullPath = relPath ? resolve(daemon.projectDir, relPath) : daemon.projectDir;
|
|
379
|
+
|
|
380
|
+
// Must stay within project directory
|
|
381
|
+
if (!fullPath.startsWith(daemon.projectDir)) {
|
|
382
|
+
return res.status(400).json({ error: 'Path outside project' });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (!existsSync(fullPath)) {
|
|
386
|
+
return res.status(404).json({ error: 'Directory not found' });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
const entries = readdirSync(fullPath, { withFileTypes: true })
|
|
391
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules')
|
|
392
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
393
|
+
.map((e) => {
|
|
394
|
+
const childPath = relPath ? `${relPath}/${e.name}` : e.name;
|
|
395
|
+
// Check if this dir has subdirectories (for expand arrow)
|
|
396
|
+
let hasChildren = false;
|
|
397
|
+
try {
|
|
398
|
+
hasChildren = readdirSync(resolve(fullPath, e.name), { withFileTypes: true })
|
|
399
|
+
.some((c) => c.isDirectory() && !c.name.startsWith('.') && c.name !== 'node_modules');
|
|
400
|
+
} catch { /* unreadable */ }
|
|
401
|
+
return { name: e.name, path: childPath, hasChildren };
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
res.json({
|
|
405
|
+
current: relPath || '.',
|
|
406
|
+
parent: relPath ? relPath.split('/').slice(0, -1).join('/') : null,
|
|
407
|
+
dirs: entries,
|
|
408
|
+
});
|
|
409
|
+
} catch (err) {
|
|
410
|
+
res.status(500).json({ error: err.message });
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
368
414
|
// --- Codebase Indexer ---
|
|
369
415
|
|
|
370
416
|
app.get('/api/indexer', (req, res) => {
|
|
@@ -25,9 +25,33 @@ export function isFirstRun(grooveDir) {
|
|
|
25
25
|
export function printWelcome(port, host = '127.0.0.1', firstRun = false) {
|
|
26
26
|
const providers = listProviders();
|
|
27
27
|
const installed = providers.filter((p) => p.installed);
|
|
28
|
+
const notInstalled = providers.filter((p) => !p.installed);
|
|
28
29
|
|
|
29
30
|
console.log('');
|
|
30
|
-
console.log('
|
|
31
|
+
console.log(' ┌─────────────────────────────────────┐');
|
|
32
|
+
console.log(' │ Welcome to GROOVE │');
|
|
33
|
+
console.log(' │ Agent orchestration for AI coding │');
|
|
34
|
+
console.log(' └─────────────────────────────────────┘');
|
|
35
|
+
console.log('');
|
|
36
|
+
|
|
37
|
+
if (installed.length > 0) {
|
|
38
|
+
console.log(` Providers (${installed.length} ready):`);
|
|
39
|
+
for (const p of installed) {
|
|
40
|
+
console.log(` ✓ ${p.name}`);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
console.log(' No AI providers detected.');
|
|
44
|
+
console.log(' Install at least one: npm i -g @anthropic-ai/claude-code');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (notInstalled.length > 0) {
|
|
48
|
+
console.log('');
|
|
49
|
+
console.log(' Available to install:');
|
|
50
|
+
for (const p of notInstalled) {
|
|
51
|
+
console.log(` · ${p.name.padEnd(18)} ${p.installCommand}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
31
55
|
console.log('');
|
|
32
56
|
|
|
33
57
|
const isRemote = host !== '127.0.0.1';
|
|
@@ -72,18 +96,6 @@ export function printWelcome(port, host = '127.0.0.1', firstRun = false) {
|
|
|
72
96
|
|
|
73
97
|
console.log(` Stop: groove stop (or Ctrl+C)`);
|
|
74
98
|
console.log(` Docs: https://docs.groovedev.ai`);
|
|
75
|
-
|
|
76
|
-
// Show providers only on first run or if none installed
|
|
77
|
-
if (firstRun || installed.length === 0) {
|
|
78
|
-
console.log('');
|
|
79
|
-
if (installed.length > 0) {
|
|
80
|
-
console.log(` Providers: ${installed.map((p) => p.name).join(', ')}`);
|
|
81
|
-
} else {
|
|
82
|
-
console.log(' No AI providers detected.');
|
|
83
|
-
console.log(' Install one: npm i -g @anthropic-ai/claude-code');
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
99
|
console.log('');
|
|
88
100
|
}
|
|
89
101
|
|