opencodespaces 1.1.7 → 1.2.0

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/dist/index.js CHANGED
@@ -38,3 +38,4 @@ sshCommand(program);
38
38
  program.parse();
39
39
  // test
40
40
  // v2
41
+ // Workflow test 2026-01-18
package/dist/lib/auth.js CHANGED
@@ -15,6 +15,522 @@ import { saveCredentials, loadCredentials, deleteCredentials, getServerUrl, } fr
15
15
  // Port range to try for local OAuth callback server
16
16
  const PORT_RANGE_START = 9876;
17
17
  const PORT_RANGE_END = 9900;
18
+ /**
19
+ * Generate the terminal-style success page HTML
20
+ */
21
+ function generateSuccessPage(name, email, initials, avatarUrl) {
22
+ return `<!DOCTYPE html>
23
+ <html lang="en">
24
+ <head>
25
+ <meta charset="UTF-8">
26
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
27
+ <title>OpenCodeSpaces - CLI Login</title>
28
+ <style>
29
+ * { box-sizing: border-box; margin: 0; padding: 0; }
30
+
31
+ body {
32
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
33
+ min-height: 100vh;
34
+ display: flex;
35
+ flex-direction: column;
36
+ align-items: center;
37
+ justify-content: center;
38
+ background: linear-gradient(180deg, hsl(258, 77%, 12%), hsl(258, 77%, 5%));
39
+ color: #f1f5f9;
40
+ padding: 1rem;
41
+ }
42
+
43
+ .logo {
44
+ margin-bottom: 2rem;
45
+ }
46
+
47
+ .logo img {
48
+ height: 43px;
49
+ width: auto;
50
+ }
51
+
52
+ .terminal {
53
+ width: 100%;
54
+ max-width: 32rem;
55
+ background: rgba(12, 10, 29, 0.9);
56
+ backdrop-filter: blur(12px);
57
+ border: 1px solid rgba(139, 92, 246, 0.3);
58
+ border-radius: 0.5rem;
59
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
60
+ overflow: hidden;
61
+ }
62
+
63
+ .terminal-header {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 0.5rem;
67
+ padding: 0.75rem 1rem;
68
+ border-bottom: 1px solid rgba(139, 92, 246, 0.3);
69
+ background: rgba(88, 28, 135, 0.3);
70
+ }
71
+
72
+ .dots {
73
+ display: flex;
74
+ gap: 0.375rem;
75
+ }
76
+
77
+ .dot {
78
+ width: 0.75rem;
79
+ height: 0.75rem;
80
+ border-radius: 50%;
81
+ }
82
+
83
+ .dot-red { background: rgba(239, 68, 68, 0.8); }
84
+ .dot-yellow { background: rgba(234, 179, 8, 0.8); }
85
+ .dot-green { background: rgba(34, 197, 94, 0.8); }
86
+
87
+ .terminal-title {
88
+ margin-left: 0.5rem;
89
+ font-size: 0.75rem;
90
+ color: #6b7280;
91
+ }
92
+
93
+ .terminal-content {
94
+ padding: 1rem;
95
+ }
96
+
97
+ .line {
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 0.5rem;
101
+ font-size: 0.875rem;
102
+ margin-bottom: 0.75rem;
103
+ opacity: 0;
104
+ transform: translateX(-8px);
105
+ animation: slideIn 0.3s ease forwards;
106
+ }
107
+
108
+ .line:nth-child(1) { animation-delay: 0.1s; }
109
+ .line:nth-child(2) { animation-delay: 0.4s; }
110
+ .line:nth-child(3) { animation-delay: 0.7s; }
111
+ .line:nth-child(4) { animation-delay: 1.0s; }
112
+ .line:nth-child(5) { animation-delay: 1.3s; }
113
+ .line:nth-child(6) { animation-delay: 1.6s; }
114
+ .line:nth-child(7) { animation-delay: 1.9s; }
115
+
116
+ @keyframes slideIn {
117
+ to {
118
+ opacity: 1;
119
+ transform: translateX(0);
120
+ }
121
+ }
122
+
123
+ .prompt {
124
+ color: #a78bfa;
125
+ font-weight: 600;
126
+ }
127
+
128
+ .text {
129
+ color: #f1f5f9;
130
+ }
131
+
132
+ .check {
133
+ color: #22c55e;
134
+ margin-left: auto;
135
+ animation: zoomIn 0.2s ease forwards;
136
+ }
137
+
138
+ @keyframes zoomIn {
139
+ from {
140
+ transform: scale(0);
141
+ opacity: 0;
142
+ }
143
+ to {
144
+ transform: scale(1);
145
+ opacity: 1;
146
+ }
147
+ }
148
+
149
+ .user-info {
150
+ display: flex;
151
+ align-items: center;
152
+ gap: 0.75rem;
153
+ padding: 0.75rem 0;
154
+ margin-bottom: 0.75rem;
155
+ opacity: 0;
156
+ transform: translateY(8px);
157
+ animation: fadeUp 0.5s ease forwards;
158
+ animation-delay: 1.3s;
159
+ }
160
+
161
+ @keyframes fadeUp {
162
+ to {
163
+ opacity: 1;
164
+ transform: translateY(0);
165
+ }
166
+ }
167
+
168
+ .avatar {
169
+ width: 2.5rem;
170
+ height: 2.5rem;
171
+ border-radius: 50%;
172
+ background: linear-gradient(135deg, #a78bfa, #14b8a6);
173
+ display: flex;
174
+ align-items: center;
175
+ justify-content: center;
176
+ font-size: 0.875rem;
177
+ font-weight: 600;
178
+ overflow: hidden;
179
+ border: 2px solid rgba(139, 92, 246, 0.5);
180
+ }
181
+
182
+ .avatar img {
183
+ width: 100%;
184
+ height: 100%;
185
+ object-fit: cover;
186
+ }
187
+
188
+ .user-details {
189
+ display: flex;
190
+ flex-direction: column;
191
+ }
192
+
193
+ .user-name {
194
+ font-weight: 600;
195
+ color: #f1f5f9;
196
+ }
197
+
198
+ .user-email {
199
+ font-size: 0.875rem;
200
+ color: #6b7280;
201
+ }
202
+
203
+ .progress-section {
204
+ margin-top: 1rem;
205
+ padding-top: 1rem;
206
+ border-top: 1px solid rgba(139, 92, 246, 0.2);
207
+ opacity: 0;
208
+ animation: fadeIn 0.5s ease forwards;
209
+ animation-delay: 2.2s;
210
+ }
211
+
212
+ @keyframes fadeIn {
213
+ to { opacity: 1; }
214
+ }
215
+
216
+ .progress-bar {
217
+ height: 0.375rem;
218
+ background: rgba(88, 28, 135, 0.5);
219
+ border-radius: 9999px;
220
+ overflow: hidden;
221
+ margin-bottom: 0.5rem;
222
+ }
223
+
224
+ .progress-fill {
225
+ height: 100%;
226
+ background: linear-gradient(to right, #a78bfa, #14b8a6);
227
+ border-radius: 9999px;
228
+ width: 0%;
229
+ animation: fillProgress 5s linear forwards;
230
+ animation-delay: 2.5s;
231
+ }
232
+
233
+ @keyframes fillProgress {
234
+ to { width: 100%; }
235
+ }
236
+
237
+ .countdown {
238
+ text-align: center;
239
+ font-size: 0.75rem;
240
+ color: #6b7280;
241
+ }
242
+
243
+ .hint {
244
+ margin-top: 1.5rem;
245
+ text-align: center;
246
+ font-size: 0.75rem;
247
+ color: #4b5563;
248
+ max-width: 28rem;
249
+ line-height: 1.5;
250
+ }
251
+ </style>
252
+ </head>
253
+ <body>
254
+ <div class="logo">
255
+ <img src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEyOTggMjAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiIHhtbG5zOnNlcmlmPSJodHRwOi8vd3d3LnNlcmlmLmNvbS8iIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZDtjbGlwLXJ1bGU6ZXZlbm9kZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6MjsiPgogICAgPGcgdHJhbnNmb3JtPSJtYXRyaXgoMSwwLDAsMSwtMC41ODA3NjEsLTAuMDA1NDU1KSI+CiAgICAgICAgPGcgdHJhbnNmb3JtPSJtYXRyaXgoMC42NTg4NTcsMCwwLDAuNjU4ODU3LC0xMDIuOTY2MTQ2LC01NzQuNjM3NjA4KSI+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0xNTguMDEyLDg3Mi4zMDRMMjYwLjI5Miw4NzIuNjI5QzMxMS44NDcsODcyLjc1OCAzNTUuMzQzLDg2Ni4zNjcgNDAwLjc0LDg5OC4wMzJDNDc5LjEzNSw5NTIuNzEyIDQ4OC4wNzEsMTA2My4zNyA0MjEuMzYxLDExMzIuMDdMNDIwLjUzNywxMTMyLjlDNDAyLjE5MSwxMTUxLjE5IDM3OS4xNzUsMTE2NC4wOSAzNTMuOTk5LDExNzAuMkMzMjguODUzLDExNzYuNDIgMjkxLjI5MiwxMTc0LjY0IDI2NC41MTUsMTE3NC42NkwxNTguMDI5LDExNzQuNzlMMTU3Ljc1Myw5NjMuOTI5TDE1Ny4zMjIsOTA2LjAyNUMxNTcuMjQ3LDg5Ny43NDYgMTU2Ljc0Niw4NzkuODY2IDE1OC4wMTIsODcyLjMwNFoiIHN0eWxlPSJmaWxsOnJnYig5OCwxMTEsMjQwKTtmaWxsLXJ1bGU6bm9uemVybzsiLz4KICAgICAgICA8L2c+CiAgICAgICAgPGcgdHJhbnNmb3JtPSJtYXRyaXgoMC42NTg4NTcsMCwwLDAuNjU4ODU3LC0xMDIuOTY2MTQ2LC01NzQuNjM3NjA4KSI+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMTYuMzYxLDk1Mi4yNjNMMzg3Ljg0NSw5NTIuNDg5TDM4Ny45ODEsOTg5LjI3NUwyMTYuNDc0LDk4OS4yMjJMMjE2LjM2MSw5NTIuMjYzWiIgc3R5bGU9ImZpbGw6cmdiKDMxLDE0LDcxKTtmaWxsLXJ1bGU6bm9uemVybzsiLz4KICAgICAgICA8L2c+CiAgICAgICAgPGcgdHJhbnNmb3JtPSJtYXRyaXgoMC42NTg4NTcsMCwwLDAuNjU4ODU3LC0xMDIuOTY2MTQ2LC01NzQuNjM3NjA4KSI+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yOTYuNjEsMTA2MS4zN0MzMTEuMzA3LDEwNTguMjUgMzI1Ljc3NiwxMDY3LjU1IDMyOS4wMzQsMTA4Mi4yMkMzMzIuMjkzLDEwOTYuODggMzIzLjEyNCwxMTExLjQ0IDMwOC40ODcsMTExNC44M0MyOTMuNjU5LDExMTguMjcgMjc4Ljg3NSwxMTA4Ljk1IDI3NS41NzMsMTA5NC4wOUMyNzIuMjcyLDEwNzkuMjQgMjgxLjcyMiwxMDY0LjU0IDI5Ni42MSwxMDYxLjM3WiIgc3R5bGU9ImZpbGw6cmdiKDMxLDE0LDcxKTtmaWxsLXJ1bGU6bm9uemVybzsiLz4KICAgICAgICA8L2c+CiAgICAgICAgPGcgdHJhbnNmb3JtPSJtYXRyaXgoOS4yNTk4NTUsMCwwLDkuMjU5ODU1LC0yMjgzLjQxNTc3OCwtMzQwNi4xMzM4MjEpIj4KICAgICAgICAgICAgPHRleHQgeD0iMjc1LjA5cHgiIHk9IjM4MS45NThweCIgc3R5bGU9ImZvbnQtZmFtaWx5OidNb250c2VycmF0LUJvbGQnLCAnTW9udHNlcnJhdCc7Zm9udC13ZWlnaHQ6NzAwO2ZvbnQtc2l6ZToxMnB4O2ZpbGw6d2hpdGU7Ij5PcGVuQzx0c3BhbiB4PSIzMTcuOTNweCAiIHk9IjM4MS45NThweCAiPm88L3RzcGFuPmRlPC90ZXh0PgogICAgICAgICAgICA8ZyB0cmFuc2Zvcm09Im1hdHJpeCgxMiwwLDAsMTIsMzg3LjE1ODM2NywzODEuOTU4MjkyKSI+CiAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgPHRleHQgeD0iMzQ0LjgxcHgiIHk9IjM4MS45NThweCIgc3R5bGU9ImZvbnQtZmFtaWx5OidNb250c2VycmF0LVJlZ3VsYXInLCAnTW9udHNlcnJhdCc7Zm9udC1zaXplOjEycHg7ZmlsbDp3aGl0ZTsiPlNwYWM8dHNwYW4geD0iMzc0LjA0MnB4ICIgeT0iMzgxLjk1OHB4ICI+ZTwvdHNwYW4+czwvdGV4dD4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPgo=" alt="OpenCode Spaces" />
256
+ </div>
257
+
258
+ <div class="terminal">
259
+ <div class="terminal-header">
260
+ <div class="dots">
261
+ <div class="dot dot-red"></div>
262
+ <div class="dot dot-yellow"></div>
263
+ <div class="dot dot-green"></div>
264
+ </div>
265
+ <span class="terminal-title">opencodespaces — cli auth</span>
266
+ </div>
267
+
268
+ <div class="terminal-content">
269
+ <div class="line">
270
+ <span class="prompt">$</span>
271
+ <span class="text">opencodespaces login</span>
272
+ </div>
273
+ <div class="line">
274
+ <span class="prompt">></span>
275
+ <span class="text">Opening browser for authentication...</span>
276
+ <span class="check">✓</span>
277
+ </div>
278
+ <div class="line">
279
+ <span class="prompt">></span>
280
+ <span class="text">Google OAuth completed</span>
281
+ <span class="check">✓</span>
282
+ </div>
283
+ <div class="line">
284
+ <span class="prompt">></span>
285
+ <span class="text">Token received from server</span>
286
+ <span class="check">✓</span>
287
+ </div>
288
+
289
+ <div class="user-info">
290
+ <div class="avatar">${avatarUrl ? `<img src="${avatarUrl}" alt="${name}" />` : initials}</div>
291
+ <div class="user-details">
292
+ <span class="user-name">Welcome, ${name}!</span>
293
+ <span class="user-email">${email}</span>
294
+ </div>
295
+ </div>
296
+
297
+ <div class="line" style="animation-delay: 1.6s;">
298
+ <span class="prompt">></span>
299
+ <span class="text">Authentication successful!</span>
300
+ </div>
301
+ <div class="line" style="animation-delay: 1.9s;">
302
+ <span class="prompt">></span>
303
+ <span class="text">Credentials saved to ~/.opencodespaces/credentials</span>
304
+ </div>
305
+
306
+ <div class="progress-section">
307
+ <div class="progress-bar">
308
+ <div class="progress-fill"></div>
309
+ </div>
310
+ <p class="countdown" id="countdown">This window will close in 5s</p>
311
+ </div>
312
+ </div>
313
+ </div>
314
+
315
+ <p class="hint">
316
+ You can close this window and return to your terminal.<br>
317
+ The CLI has received your credentials.
318
+ </p>
319
+
320
+ <script>
321
+ let seconds = 5;
322
+ const countdownEl = document.getElementById('countdown');
323
+
324
+ const interval = setInterval(() => {
325
+ seconds--;
326
+ if (seconds <= 0) {
327
+ clearInterval(interval);
328
+ countdownEl.textContent = '✓ Complete — you can close this tab';
329
+ } else {
330
+ countdownEl.textContent = 'Completing in ' + seconds + 's...';
331
+ }
332
+ }, 1000);
333
+ </script>
334
+ </body>
335
+ </html>`;
336
+ }
337
+ /**
338
+ * Generate the terminal-style error page HTML
339
+ */
340
+ function generateErrorPage(errorMessage) {
341
+ const errorMessages = {
342
+ 'oauth_failed': 'Google sign-in failed',
343
+ 'token_exchange_failed': 'Authentication failed',
344
+ 'userinfo_failed': 'Could not get user info',
345
+ 'no_code': 'Invalid OAuth response',
346
+ 'oauth_not_configured': 'OAuth not configured on server',
347
+ 'domain_not_allowed': 'Email domain not allowed',
348
+ };
349
+ const displayError = errorMessages[errorMessage] || errorMessage;
350
+ return `<!DOCTYPE html>
351
+ <html lang="en">
352
+ <head>
353
+ <meta charset="UTF-8">
354
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
355
+ <title>OpenCodeSpaces - CLI Login Failed</title>
356
+ <style>
357
+ * { box-sizing: border-box; margin: 0; padding: 0; }
358
+
359
+ body {
360
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
361
+ min-height: 100vh;
362
+ display: flex;
363
+ flex-direction: column;
364
+ align-items: center;
365
+ justify-content: center;
366
+ background: linear-gradient(180deg, hsl(258, 77%, 12%), hsl(258, 77%, 5%));
367
+ color: #f1f5f9;
368
+ padding: 1rem;
369
+ }
370
+
371
+ .logo {
372
+ margin-bottom: 2rem;
373
+ }
374
+
375
+ .logo img {
376
+ height: 43px;
377
+ width: auto;
378
+ }
379
+
380
+ .terminal {
381
+ width: 100%;
382
+ max-width: 32rem;
383
+ background: rgba(12, 10, 29, 0.9);
384
+ backdrop-filter: blur(12px);
385
+ border: 1px solid rgba(139, 92, 246, 0.3);
386
+ border-radius: 0.5rem;
387
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
388
+ overflow: hidden;
389
+ }
390
+
391
+ .terminal-header {
392
+ display: flex;
393
+ align-items: center;
394
+ gap: 0.5rem;
395
+ padding: 0.75rem 1rem;
396
+ border-bottom: 1px solid rgba(139, 92, 246, 0.3);
397
+ background: rgba(88, 28, 135, 0.3);
398
+ }
399
+
400
+ .dots {
401
+ display: flex;
402
+ gap: 0.375rem;
403
+ }
404
+
405
+ .dot {
406
+ width: 0.75rem;
407
+ height: 0.75rem;
408
+ border-radius: 50%;
409
+ }
410
+
411
+ .dot-red { background: rgba(239, 68, 68, 0.8); }
412
+ .dot-yellow { background: rgba(234, 179, 8, 0.8); }
413
+ .dot-green { background: rgba(34, 197, 94, 0.8); }
414
+
415
+ .terminal-title {
416
+ margin-left: 0.5rem;
417
+ font-size: 0.75rem;
418
+ color: #6b7280;
419
+ }
420
+
421
+ .terminal-content {
422
+ padding: 1rem;
423
+ }
424
+
425
+ .line {
426
+ display: flex;
427
+ align-items: center;
428
+ gap: 0.5rem;
429
+ font-size: 0.875rem;
430
+ margin-bottom: 0.75rem;
431
+ opacity: 0;
432
+ transform: translateX(-8px);
433
+ animation: slideIn 0.3s ease forwards;
434
+ }
435
+
436
+ .line:nth-child(1) { animation-delay: 0.1s; }
437
+ .line:nth-child(2) { animation-delay: 0.4s; }
438
+ .line:nth-child(3) { animation-delay: 0.7s; }
439
+
440
+ @keyframes slideIn {
441
+ to {
442
+ opacity: 1;
443
+ transform: translateX(0);
444
+ }
445
+ }
446
+
447
+ .prompt { color: #a78bfa; font-weight: 600; }
448
+ .text { color: #f1f5f9; }
449
+ .check { color: #22c55e; margin-left: auto; }
450
+ .x { color: #ef4444; margin-left: auto; }
451
+
452
+ .error-section {
453
+ margin-top: 1rem;
454
+ padding: 1rem;
455
+ background: rgba(239, 68, 68, 0.1);
456
+ border: 1px solid rgba(239, 68, 68, 0.3);
457
+ border-radius: 0.375rem;
458
+ opacity: 0;
459
+ animation: fadeIn 0.3s ease forwards;
460
+ animation-delay: 1s;
461
+ }
462
+
463
+ @keyframes fadeIn {
464
+ to { opacity: 1; }
465
+ }
466
+
467
+ .error-title {
468
+ display: flex;
469
+ align-items: center;
470
+ gap: 0.5rem;
471
+ color: #f87171;
472
+ font-weight: 600;
473
+ margin-bottom: 0.5rem;
474
+ }
475
+
476
+ .error-message {
477
+ color: #9ca3af;
478
+ font-size: 0.875rem;
479
+ }
480
+
481
+ .hint {
482
+ margin-top: 1.5rem;
483
+ text-align: center;
484
+ font-size: 0.75rem;
485
+ color: #4b5563;
486
+ }
487
+ </style>
488
+ </head>
489
+ <body>
490
+ <div class="logo">
491
+ <img src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEyOTggMjAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiIHhtbG5zOnNlcmlmPSJodHRwOi8vd3d3LnNlcmlmLmNvbS8iIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZDtjbGlwLXJ1bGU6ZXZlbm9kZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6MjsiPgogICAgPGcgdHJhbnNmb3JtPSJtYXRyaXgoMSwwLDAsMSwtMC41ODA3NjEsLTAuMDA1NDU1KSI+CiAgICAgICAgPGcgdHJhbnNmb3JtPSJtYXRyaXgoMC42NTg4NTcsMCwwLDAuNjU4ODU3LC0xMDIuOTY2MTQ2LC01NzQuNjM3NjA4KSI+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0xNTguMDEyLDg3Mi4zMDRMMjYwLjI5Miw4NzIuNjI5QzMxMS44NDcsODcyLjc1OCAzNTUuMzQzLDg2Ni4zNjcgNDAwLjc0LDg5OC4wMzJDNDc5LjEzNSw5NTIuNzEyIDQ4OC4wNzEsMTA2My4zNyA0MjEuMzYxLDExMzIuMDdMNDIwLjUzNywxMTMyLjlDNDAyLjE5MSwxMTUxLjE5IDM3OS4xNzUsMTE2NC4wOSAzNTMuOTk5LDExNzAuMkMzMjguODUzLDExNzYuNDIgMjkxLjI5MiwxMTc0LjY0IDI2NC41MTUsMTE3NC42NkwxNTguMDI5LDExNzQuNzlMMTU3Ljc1Myw5NjMuOTI5TDE1Ny4zMjIsOTA2LjAyNUMxNTcuMjQ3LDg5Ny43NDYgMTU2Ljc0Niw4NzkuODY2IDE1OC4wMTIsODcyLjMwNFoiIHN0eWxlPSJmaWxsOnJnYig5OCwxMTEsMjQwKTtmaWxsLXJ1bGU6bm9uemVybzsiLz4KICAgICAgICA8L2c+CiAgICAgICAgPGcgdHJhbnNmb3JtPSJtYXRyaXgoMC42NTg4NTcsMCwwLDAuNjU4ODU3LC0xMDIuOTY2MTQ2LC01NzQuNjM3NjA4KSI+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMTYuMzYxLDk1Mi4yNjNMMzg3Ljg0NSw5NTIuNDg5TDM4Ny45ODEsOTg5LjI3NUwyMTYuNDc0LDk4OS4yMjJMMjE2LjM2MSw5NTIuMjYzWiIgc3R5bGU9ImZpbGw6cmdiKDMxLDE0LDcxKTtmaWxsLXJ1bGU6bm9uemVybzsiLz4KICAgICAgICA8L2c+CiAgICAgICAgPGcgdHJhbnNmb3JtPSJtYXRyaXgoMC42NTg4NTcsMCwwLDAuNjU4ODU3LC0xMDIuOTY2MTQ2LC01NzQuNjM3NjA4KSI+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yOTYuNjEsMTA2MS4zN0MzMTEuMzA3LDEwNTguMjUgMzI1Ljc3NiwxMDY3LjU1IDMyOS4wMzQsMTA4Mi4yMkMzMzIuMjkzLDEwOTYuODggMzIzLjEyNCwxMTExLjQ0IDMwOC40ODcsMTExNC44M0MyOTMuNjU5LDExMTguMjcgMjc4Ljg3NSwxMTA4Ljk1IDI3NS41NzMsMTA5NC4wOUMyNzIuMjcyLDEwNzkuMjQgMjgxLjcyMiwxMDY0LjU0IDI5Ni42MSwxMDYxLjM3WiIgc3R5bGU9ImZpbGw6cmdiKDMxLDE0LDcxKTtmaWxsLXJ1bGU6bm9uemVybzsiLz4KICAgICAgICA8L2c+CiAgICAgICAgPGcgdHJhbnNmb3JtPSJtYXRyaXgoOS4yNTk4NTUsMCwwLDkuMjU5ODU1LC0yMjgzLjQxNTc3OCwtMzQwNi4xMzM4MjEpIj4KICAgICAgICAgICAgPHRleHQgeD0iMjc1LjA5cHgiIHk9IjM4MS45NThweCIgc3R5bGU9ImZvbnQtZmFtaWx5OidNb250c2VycmF0LUJvbGQnLCAnTW9udHNlcnJhdCc7Zm9udC13ZWlnaHQ6NzAwO2ZvbnQtc2l6ZToxMnB4O2ZpbGw6d2hpdGU7Ij5PcGVuQzx0c3BhbiB4PSIzMTcuOTNweCAiIHk9IjM4MS45NThweCAiPm88L3RzcGFuPmRlPC90ZXh0PgogICAgICAgICAgICA8ZyB0cmFuc2Zvcm09Im1hdHJpeCgxMiwwLDAsMTIsMzg3LjE1ODM2NywzODEuOTU4MjkyKSI+CiAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgPHRleHQgeD0iMzQ0LjgxcHgiIHk9IjM4MS45NThweCIgc3R5bGU9ImZvbnQtZmFtaWx5OidNb250c2VycmF0LVJlZ3VsYXInLCAnTW9udHNlcnJhdCc7Zm9udC1zaXplOjEycHg7ZmlsbDp3aGl0ZTsiPlNwYWM8dHNwYW4geD0iMzc0LjA0MnB4ICIgeT0iMzgxLjk1OHB4ICI+ZTwvdHNwYW4+czwvdGV4dD4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPgo=" alt="OpenCode Spaces" />
492
+ </div>
493
+
494
+ <div class="terminal">
495
+ <div class="terminal-header">
496
+ <div class="dots">
497
+ <div class="dot dot-red"></div>
498
+ <div class="dot dot-yellow"></div>
499
+ <div class="dot dot-green"></div>
500
+ </div>
501
+ <span class="terminal-title">opencodespaces — cli auth</span>
502
+ </div>
503
+
504
+ <div class="terminal-content">
505
+ <div class="line">
506
+ <span class="prompt">$</span>
507
+ <span class="text">opencodespaces login</span>
508
+ </div>
509
+ <div class="line">
510
+ <span class="prompt">></span>
511
+ <span class="text">Opening browser for authentication...</span>
512
+ <span class="check">✓</span>
513
+ </div>
514
+ <div class="line">
515
+ <span class="prompt">></span>
516
+ <span class="text">Verifying credentials...</span>
517
+ <span class="x">✗</span>
518
+ </div>
519
+
520
+ <div class="error-section">
521
+ <div class="error-title">
522
+ <span>⚠</span>
523
+ <span>${displayError}</span>
524
+ </div>
525
+ <p class="error-message">Could not complete authentication. Please try again in your terminal.</p>
526
+ </div>
527
+ </div>
528
+ </div>
529
+
530
+ <p class="hint">Close this window and run <code>opencodespaces login</code> again.</p>
531
+ </body>
532
+ </html>`;
533
+ }
18
534
  /**
19
535
  * Find an available port in the range
20
536
  */
@@ -59,56 +575,26 @@ function startCallbackServer(port) {
59
575
  const token = url.searchParams.get('token');
60
576
  const email = url.searchParams.get('email');
61
577
  const name = url.searchParams.get('name');
578
+ const avatar = url.searchParams.get('avatar');
62
579
  const error = url.searchParams.get('error');
63
580
  // Send response to browser
64
581
  res.writeHead(200, { 'Content-Type': 'text/html' });
65
582
  if (token) {
66
- res.end(`
67
- <!DOCTYPE html>
68
- <html>
69
- <head>
70
- <title>OpenCodeSpaces - Login Successful</title>
71
- <style>
72
- body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0f172a; color: #f1f5f9; }
73
- .container { text-align: center; padding: 2rem; }
74
- h1 { color: #22c55e; margin-bottom: 0.5rem; }
75
- p { color: #94a3b8; }
76
- </style>
77
- </head>
78
- <body>
79
- <div class="container">
80
- <h1>✓ Login Successful</h1>
81
- <p>You can close this window and return to your terminal.</p>
82
- </div>
83
- </body>
84
- </html>
85
- `);
86
- // Close server after short delay to ensure response is sent
583
+ // Get user initials for avatar fallback
584
+ const initials = (name || 'U')
585
+ .split(' ')
586
+ .map((n) => n[0])
587
+ .join('')
588
+ .toUpperCase()
589
+ .slice(0, 2);
590
+ res.end(generateSuccessPage(name || 'User', email || '', initials, avatar || undefined));
591
+ // Close server after delay to ensure page renders
87
592
  setTimeout(() => {
88
593
  cleanup({ success: true, token, email: email || undefined, name: name || undefined });
89
- }, 100);
594
+ }, 500);
90
595
  }
91
596
  else {
92
- res.end(`
93
- <!DOCTYPE html>
94
- <html>
95
- <head>
96
- <title>OpenCodeSpaces - Login Failed</title>
97
- <style>
98
- body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0f172a; color: #f1f5f9; }
99
- .container { text-align: center; padding: 2rem; }
100
- h1 { color: #ef4444; margin-bottom: 0.5rem; }
101
- p { color: #94a3b8; }
102
- </style>
103
- </head>
104
- <body>
105
- <div class="container">
106
- <h1>✗ Login Failed</h1>
107
- <p>${error || 'Unknown error occurred'}</p>
108
- </div>
109
- </body>
110
- </html>
111
- `);
597
+ res.end(generateErrorPage(error || 'Unknown error'));
112
598
  setTimeout(() => {
113
599
  cleanup({ success: false, error: error || 'Login failed' });
114
600
  }, 100);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencodespaces",
3
- "version": "1.1.7",
3
+ "version": "1.2.0",
4
4
  "description": "CLI for OpenCodeSpaces - Connect your local IDE to cloud sessions",
5
5
  "type": "module",
6
6
  "bin": {