groove-dev 0.17.0 → 0.17.2
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/integrations-registry.json +105 -9
- package/node_modules/@groove-dev/daemon/src/api.js +52 -0
- package/node_modules/@groove-dev/daemon/src/integrations.js +106 -8
- package/node_modules/@groove-dev/daemon/src/process.js +5 -2
- package/node_modules/@groove-dev/gui/dist/assets/{index-C5k-qSwi.js → index-CEf7nLM2.js} +38 -35
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/src/views/IntegrationsStore.jsx +251 -34
- package/package.json +1 -1
- package/packages/daemon/integrations-registry.json +105 -9
- package/packages/daemon/src/api.js +52 -0
- package/packages/daemon/src/integrations.js +106 -8
- package/packages/daemon/src/process.js +5 -2
- package/packages/gui/dist/assets/{index-C5k-qSwi.js → index-CEf7nLM2.js} +38 -35
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/src/views/IntegrationsStore.jsx +251 -34
|
@@ -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-CEf7nLM2.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-BhjOFLBc.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
@@ -75,16 +75,31 @@ function sortIntegrations(items, sortBy) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
// -- Credential Setup Modal --
|
|
79
|
-
function CredentialModal({ integration,
|
|
78
|
+
// -- Credential Setup Modal (Guided Wizard) --
|
|
79
|
+
function CredentialModal({ integration, onClose }) {
|
|
80
80
|
const [values, setValues] = useState({});
|
|
81
81
|
const [saving, setSaving] = useState(false);
|
|
82
82
|
const [saved, setSaved] = useState({});
|
|
83
|
+
const [oauthStatus, setOauthStatus] = useState(null); // null, 'checking', 'not-configured', 'ready', 'connecting'
|
|
84
|
+
const [googleClientId, setGoogleClientId] = useState('');
|
|
85
|
+
const [googleClientSecret, setGoogleClientSecret] = useState('');
|
|
86
|
+
const [showGoogleSetup, setShowGoogleSetup] = useState(false);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (integration?.authType === 'oauth-google') {
|
|
90
|
+
setOauthStatus('checking');
|
|
91
|
+
fetch('/api/integrations/google-oauth/status')
|
|
92
|
+
.then((r) => r.json())
|
|
93
|
+
.then((data) => setOauthStatus(data.configured ? 'ready' : 'not-configured'))
|
|
94
|
+
.catch(() => setOauthStatus('not-configured'));
|
|
95
|
+
}
|
|
96
|
+
}, [integration]);
|
|
83
97
|
|
|
84
98
|
if (!integration) return null;
|
|
85
99
|
|
|
86
|
-
const
|
|
87
|
-
|
|
100
|
+
const isOAuth = integration.authType === 'oauth-google';
|
|
101
|
+
const envKeys = (integration.envKeys || []).filter((ek) => !ek.hidden);
|
|
102
|
+
const setupSteps = integration.setupSteps || [];
|
|
88
103
|
|
|
89
104
|
async function handleSave(key) {
|
|
90
105
|
if (!values[key]) return;
|
|
@@ -103,54 +118,256 @@ function CredentialModal({ integration, onSave, onClose }) {
|
|
|
103
118
|
setSaving(false);
|
|
104
119
|
}
|
|
105
120
|
|
|
121
|
+
async function handleGoogleSetup() {
|
|
122
|
+
if (!googleClientId || !googleClientSecret) return;
|
|
123
|
+
setSaving(true);
|
|
124
|
+
try {
|
|
125
|
+
await fetch('/api/integrations/google-oauth/setup', {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: { 'Content-Type': 'application/json' },
|
|
128
|
+
body: JSON.stringify({ clientId: googleClientId, clientSecret: googleClientSecret }),
|
|
129
|
+
});
|
|
130
|
+
setOauthStatus('ready');
|
|
131
|
+
setShowGoogleSetup(false);
|
|
132
|
+
} catch { /* ignore */ }
|
|
133
|
+
setSaving(false);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function handleOAuthConnect() {
|
|
137
|
+
setOauthStatus('connecting');
|
|
138
|
+
try {
|
|
139
|
+
const res = await fetch(`/api/integrations/${integration.id}/oauth/start`, { method: 'POST' });
|
|
140
|
+
const data = await res.json();
|
|
141
|
+
if (data.url) {
|
|
142
|
+
window.open(data.url, '_blank', 'width=600,height=700');
|
|
143
|
+
// Poll for completion
|
|
144
|
+
const poll = setInterval(async () => {
|
|
145
|
+
try {
|
|
146
|
+
const statusRes = await fetch(`/api/integrations/${integration.id}/status`);
|
|
147
|
+
const status = await statusRes.json();
|
|
148
|
+
if (status.configured) {
|
|
149
|
+
clearInterval(poll);
|
|
150
|
+
setOauthStatus('ready');
|
|
151
|
+
onClose();
|
|
152
|
+
}
|
|
153
|
+
} catch { /* ignore */ }
|
|
154
|
+
}, 2000);
|
|
155
|
+
// Stop polling after 5 minutes
|
|
156
|
+
setTimeout(() => clearInterval(poll), 300000);
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
setOauthStatus('ready');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
106
163
|
return (
|
|
107
164
|
<div style={modal.overlay} onClick={onClose}>
|
|
108
165
|
<div style={modal.container} onClick={(e) => e.stopPropagation()}>
|
|
109
166
|
<div style={modal.topBar}>
|
|
110
167
|
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-bright)' }}>
|
|
111
|
-
|
|
168
|
+
Connect {integration.name}
|
|
112
169
|
</span>
|
|
113
170
|
<button onClick={onClose} style={modal.closeBtn}>×</button>
|
|
114
171
|
</div>
|
|
115
172
|
|
|
116
173
|
<div style={{ padding: '16px 0' }}>
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
174
|
+
{/* Setup guide steps */}
|
|
175
|
+
{setupSteps.length > 0 && (
|
|
176
|
+
<div style={{ marginBottom: 20 }}>
|
|
177
|
+
<div style={{
|
|
178
|
+
fontSize: 10, fontWeight: 700, color: 'var(--text-muted)',
|
|
179
|
+
textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 10,
|
|
180
|
+
}}>
|
|
181
|
+
Setup Guide
|
|
182
|
+
</div>
|
|
183
|
+
{setupSteps.map((step, i) => (
|
|
184
|
+
<div key={i} style={{
|
|
185
|
+
display: 'flex', gap: 10, marginBottom: 8,
|
|
186
|
+
fontSize: 11, color: 'var(--text-primary)', lineHeight: 1.5,
|
|
187
|
+
}}>
|
|
188
|
+
<span style={{
|
|
189
|
+
width: 20, height: 20, borderRadius: '50%', flexShrink: 0,
|
|
190
|
+
background: 'var(--bg-active)', color: 'var(--accent)',
|
|
191
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
192
|
+
fontSize: 10, fontWeight: 700,
|
|
193
|
+
}}>
|
|
194
|
+
{i + 1}
|
|
129
195
|
</span>
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
196
|
+
<span>{step}</span>
|
|
197
|
+
</div>
|
|
198
|
+
))}
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
{/* Open setup page button for API key integrations */}
|
|
203
|
+
{integration.setupUrl && !isOAuth && (
|
|
204
|
+
<a
|
|
205
|
+
href={integration.setupUrl}
|
|
206
|
+
target="_blank"
|
|
207
|
+
rel="noopener noreferrer"
|
|
208
|
+
style={{
|
|
209
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
|
|
210
|
+
padding: '10px 16px', marginBottom: 16,
|
|
211
|
+
background: 'var(--bg-active)', color: 'var(--accent)',
|
|
212
|
+
border: '1px solid var(--accent)', borderRadius: 6,
|
|
213
|
+
fontSize: 12, fontWeight: 600, textDecoration: 'none',
|
|
214
|
+
cursor: 'pointer',
|
|
215
|
+
}}
|
|
216
|
+
>
|
|
217
|
+
Open {integration.name} Settings {'\u2197'}
|
|
218
|
+
</a>
|
|
219
|
+
)}
|
|
220
|
+
|
|
221
|
+
{/* OAuth flow for Google integrations */}
|
|
222
|
+
{isOAuth && (
|
|
223
|
+
<div style={{ marginBottom: 16 }}>
|
|
224
|
+
{oauthStatus === 'checking' && (
|
|
225
|
+
<div style={{ fontSize: 11, color: 'var(--text-muted)', textAlign: 'center', padding: 16 }}>
|
|
226
|
+
Checking Google OAuth setup...
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
{oauthStatus === 'not-configured' && !showGoogleSetup && (
|
|
231
|
+
<div style={{
|
|
232
|
+
padding: 16, borderRadius: 8,
|
|
233
|
+
background: 'rgba(229, 192, 123, 0.06)', border: '1px solid var(--amber)',
|
|
234
|
+
}}>
|
|
235
|
+
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--amber)', marginBottom: 8 }}>
|
|
236
|
+
One-time Google setup needed
|
|
237
|
+
</div>
|
|
238
|
+
<div style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.5, marginBottom: 12 }}>
|
|
239
|
+
To connect Google services, you need a Google Cloud project with OAuth credentials.
|
|
240
|
+
This is a one-time setup that works for Gmail, Calendar, and Drive.
|
|
241
|
+
</div>
|
|
242
|
+
<a
|
|
243
|
+
href="https://console.cloud.google.com/apis/credentials"
|
|
244
|
+
target="_blank"
|
|
245
|
+
rel="noopener noreferrer"
|
|
246
|
+
style={{
|
|
247
|
+
display: 'inline-flex', alignItems: 'center', gap: 4,
|
|
248
|
+
fontSize: 11, color: 'var(--accent)', textDecoration: 'none',
|
|
249
|
+
marginBottom: 12,
|
|
250
|
+
}}
|
|
251
|
+
>
|
|
252
|
+
Open Google Cloud Console {'\u2197'}
|
|
253
|
+
</a>
|
|
254
|
+
<div style={{ fontSize: 10, color: 'var(--text-muted)', lineHeight: 1.6, marginBottom: 12 }}>
|
|
255
|
+
1. Create a project (or select existing){'\n'}
|
|
256
|
+
2. Configure OAuth consent screen (External, add your email as test user){'\n'}
|
|
257
|
+
3. Create OAuth Client ID (Desktop app){'\n'}
|
|
258
|
+
4. Copy the Client ID and Client Secret below
|
|
259
|
+
</div>
|
|
260
|
+
<button
|
|
261
|
+
onClick={() => setShowGoogleSetup(true)}
|
|
262
|
+
style={{ ...modal.saveBtn, width: '100%' }}
|
|
263
|
+
>
|
|
264
|
+
I have my Client ID and Secret
|
|
265
|
+
</button>
|
|
266
|
+
</div>
|
|
267
|
+
)}
|
|
268
|
+
|
|
269
|
+
{oauthStatus === 'not-configured' && showGoogleSetup && (
|
|
270
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
271
|
+
<div>
|
|
272
|
+
<label style={modal.label}>Google OAuth Client ID</label>
|
|
273
|
+
<input
|
|
274
|
+
value={googleClientId}
|
|
275
|
+
onChange={(e) => setGoogleClientId(e.target.value)}
|
|
276
|
+
placeholder="123456789.apps.googleusercontent.com"
|
|
277
|
+
style={modal.input}
|
|
278
|
+
/>
|
|
279
|
+
</div>
|
|
280
|
+
<div>
|
|
281
|
+
<label style={modal.label}>Google OAuth Client Secret</label>
|
|
282
|
+
<input
|
|
283
|
+
type="password"
|
|
284
|
+
value={googleClientSecret}
|
|
285
|
+
onChange={(e) => setGoogleClientSecret(e.target.value)}
|
|
286
|
+
placeholder="GOCSPX-..."
|
|
287
|
+
style={modal.input}
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
290
|
+
<button
|
|
291
|
+
onClick={handleGoogleSetup}
|
|
292
|
+
disabled={saving || !googleClientId || !googleClientSecret}
|
|
293
|
+
style={{
|
|
294
|
+
...modal.saveBtn, width: '100%',
|
|
295
|
+
opacity: saving || !googleClientId || !googleClientSecret ? 0.4 : 1,
|
|
296
|
+
}}
|
|
297
|
+
>
|
|
298
|
+
{saving ? 'Saving...' : 'Save Google OAuth Credentials'}
|
|
299
|
+
</button>
|
|
300
|
+
<div style={{ fontSize: 9, color: 'var(--text-muted)', textAlign: 'center' }}>
|
|
301
|
+
Stored encrypted, only on this machine. One-time setup for all Google integrations.
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
|
|
306
|
+
{(oauthStatus === 'ready' || oauthStatus === 'connecting') && (
|
|
141
307
|
<button
|
|
142
|
-
onClick={
|
|
143
|
-
disabled={
|
|
308
|
+
onClick={handleOAuthConnect}
|
|
309
|
+
disabled={oauthStatus === 'connecting'}
|
|
144
310
|
style={{
|
|
145
|
-
|
|
146
|
-
|
|
311
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
|
|
312
|
+
width: '100%', padding: '12px 16px',
|
|
313
|
+
background: oauthStatus === 'connecting' ? 'var(--bg-active)' : '#4285f4',
|
|
314
|
+
color: '#fff', border: 'none', borderRadius: 6,
|
|
315
|
+
fontSize: 13, fontWeight: 600, cursor: 'pointer',
|
|
316
|
+
fontFamily: 'var(--font)',
|
|
147
317
|
}}
|
|
148
318
|
>
|
|
149
|
-
|
|
319
|
+
{oauthStatus === 'connecting'
|
|
320
|
+
? 'Waiting for authorization...'
|
|
321
|
+
: `Connect with Google`}
|
|
150
322
|
</button>
|
|
323
|
+
)}
|
|
324
|
+
</div>
|
|
325
|
+
)}
|
|
326
|
+
|
|
327
|
+
{/* API key inputs (only show non-hidden keys) */}
|
|
328
|
+
{envKeys.length > 0 && (
|
|
329
|
+
<div>
|
|
330
|
+
<div style={{
|
|
331
|
+
fontSize: 11, color: 'var(--text-dim)', marginBottom: 12, lineHeight: 1.5,
|
|
332
|
+
}}>
|
|
333
|
+
Paste your credentials below. Values are encrypted and stored locally on this machine only.
|
|
151
334
|
</div>
|
|
335
|
+
|
|
336
|
+
{envKeys.map((ek) => (
|
|
337
|
+
<div key={ek.key} style={{ marginBottom: 14 }}>
|
|
338
|
+
<label style={modal.label}>
|
|
339
|
+
{ek.label || ek.key}
|
|
340
|
+
{ek.required && <span style={{ color: 'var(--red)', marginLeft: 4 }}>*</span>}
|
|
341
|
+
{saved[ek.key] && (
|
|
342
|
+
<span style={{ color: 'var(--green)', marginLeft: 8, fontSize: 10, fontWeight: 500 }}>
|
|
343
|
+
{'\u2713'} saved
|
|
344
|
+
</span>
|
|
345
|
+
)}
|
|
346
|
+
</label>
|
|
347
|
+
<div style={{ display: 'flex', gap: 6 }}>
|
|
348
|
+
<input
|
|
349
|
+
type="password"
|
|
350
|
+
value={values[ek.key] || ''}
|
|
351
|
+
placeholder={ek.placeholder || ek.key}
|
|
352
|
+
onChange={(e) => setValues((prev) => ({ ...prev, [ek.key]: e.target.value }))}
|
|
353
|
+
onKeyDown={(e) => e.key === 'Enter' && handleSave(ek.key)}
|
|
354
|
+
style={modal.input}
|
|
355
|
+
/>
|
|
356
|
+
<button
|
|
357
|
+
onClick={() => handleSave(ek.key)}
|
|
358
|
+
disabled={saving || !values[ek.key]}
|
|
359
|
+
style={{
|
|
360
|
+
...modal.saveBtn,
|
|
361
|
+
opacity: saving || !values[ek.key] ? 0.4 : 1,
|
|
362
|
+
}}
|
|
363
|
+
>
|
|
364
|
+
Save
|
|
365
|
+
</button>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
))}
|
|
152
369
|
</div>
|
|
153
|
-
)
|
|
370
|
+
)}
|
|
154
371
|
</div>
|
|
155
372
|
</div>
|
|
156
373
|
</div>
|