@chrysb/alphaclaw 0.2.0 → 0.2.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/bin/alphaclaw.js +16 -7
- package/lib/public/css/shell.css +96 -1
- package/lib/public/css/theme.css +26 -0
- package/lib/public/js/app.js +70 -22
- package/lib/public/js/components/channels.js +1 -1
- package/lib/public/js/components/confirm-dialog.js +66 -0
- package/lib/public/js/components/credentials-modal.js +1 -1
- package/lib/public/js/components/gateway.js +141 -36
- package/lib/public/js/components/google.js +15 -7
- package/lib/public/js/components/onboarding/welcome-form-step.js +5 -2
- package/lib/public/js/components/onboarding/welcome-setup-step.js +2 -2
- package/lib/public/js/components/update-action-button.js +66 -0
- package/lib/public/js/components/welcome.js +23 -1
- package/lib/public/js/lib/api.js +9 -0
- package/lib/server/onboarding/github.js +96 -30
- package/lib/server/onboarding/index.js +82 -31
- package/lib/server/routes/onboarding.js +36 -0
- package/lib/setup/core-prompts/TOOLS.md +5 -5
- package/lib/setup/gitignore +2 -0
- package/package.json +2 -2
package/bin/alphaclaw.js
CHANGED
|
@@ -252,13 +252,17 @@ const runGitSync = () => {
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
const originUrl = `https://github.com/${githubRepo}.git`;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
255
|
+
let branch = "main";
|
|
256
|
+
try {
|
|
257
|
+
branch =
|
|
258
|
+
String(
|
|
259
|
+
execSync("git symbolic-ref --short HEAD", {
|
|
260
|
+
cwd: openclawDir,
|
|
261
|
+
encoding: "utf8",
|
|
262
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
263
|
+
}),
|
|
264
|
+
).trim() || "main";
|
|
265
|
+
} catch {}
|
|
262
266
|
const askPassPath = path.join(
|
|
263
267
|
os.tmpdir(),
|
|
264
268
|
`alphaclaw-git-askpass-${process.pid}.sh`,
|
|
@@ -294,6 +298,8 @@ const runGitSync = () => {
|
|
|
294
298
|
);
|
|
295
299
|
|
|
296
300
|
runGit(`remote set-url origin ${quoteArg(originUrl)}`);
|
|
301
|
+
runGit(`config user.name ${quoteArg("AlphaClaw Agent")}`);
|
|
302
|
+
runGit(`config user.email ${quoteArg("agent@alphaclaw.md")}`);
|
|
297
303
|
try {
|
|
298
304
|
runGit(`ls-remote --exit-code --heads origin ${quoteArg(branch)}`, {
|
|
299
305
|
withAuth: true,
|
|
@@ -316,6 +322,9 @@ const runGitSync = () => {
|
|
|
316
322
|
runGit(`push origin ${quoteArg(branch)}`, { withAuth: true });
|
|
317
323
|
const hash = String(runGit("rev-parse --short HEAD")).trim();
|
|
318
324
|
console.log(`[alphaclaw] Git sync complete (${hash})`);
|
|
325
|
+
console.log(
|
|
326
|
+
`[alphaclaw] Commit URL: https://github.com/${githubRepo}/commit/${hash}`,
|
|
327
|
+
);
|
|
319
328
|
return 0;
|
|
320
329
|
} catch (e) {
|
|
321
330
|
const details = String(e.stderr || e.stdout || e.message || "").trim();
|
package/lib/public/css/shell.css
CHANGED
|
@@ -175,9 +175,104 @@
|
|
|
175
175
|
|
|
176
176
|
.statusbar-left, .statusbar-right { display: flex; align-items: center; gap: 16px; margin-left: 2px; }
|
|
177
177
|
|
|
178
|
+
.mobile-topbar {
|
|
179
|
+
display: none;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.mobile-topbar-menu {
|
|
183
|
+
display: inline-flex;
|
|
184
|
+
align-items: center;
|
|
185
|
+
justify-content: center;
|
|
186
|
+
width: 32px;
|
|
187
|
+
height: 32px;
|
|
188
|
+
border: 1px solid var(--border-strong);
|
|
189
|
+
border-radius: 8px;
|
|
190
|
+
background: var(--bg-hover);
|
|
191
|
+
color: var(--text);
|
|
192
|
+
cursor: pointer;
|
|
193
|
+
}
|
|
194
|
+
.mobile-topbar-menu:hover { background: var(--bg-active); }
|
|
195
|
+
|
|
196
|
+
.mobile-topbar-title {
|
|
197
|
+
display: inline-flex;
|
|
198
|
+
align-items: center;
|
|
199
|
+
justify-content: center;
|
|
200
|
+
width: 100%;
|
|
201
|
+
text-align: center;
|
|
202
|
+
font-size: 14px;
|
|
203
|
+
letter-spacing: 0.03em;
|
|
204
|
+
color: var(--text-muted);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.mobile-sidebar-overlay {
|
|
208
|
+
display: none;
|
|
209
|
+
}
|
|
210
|
+
|
|
178
211
|
/* ── Responsive ────────────────────────────────── */
|
|
179
212
|
|
|
180
213
|
@media (max-width: 768px) {
|
|
181
214
|
.app-shell { grid-template-columns: 1fr; }
|
|
182
|
-
.app-
|
|
215
|
+
.app-content {
|
|
216
|
+
padding: 0 14px 12px;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.mobile-topbar {
|
|
220
|
+
display: flex;
|
|
221
|
+
align-items: center;
|
|
222
|
+
justify-content: center;
|
|
223
|
+
position: sticky;
|
|
224
|
+
top: 0;
|
|
225
|
+
z-index: 15;
|
|
226
|
+
background: var(--panel-bg-contrast);
|
|
227
|
+
border: 0;
|
|
228
|
+
border-bottom: 1px solid var(--panel-border-contrast);
|
|
229
|
+
border-radius: 0;
|
|
230
|
+
min-height: 52px;
|
|
231
|
+
padding: 8px 14px;
|
|
232
|
+
margin: 0 -14px 10px;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.mobile-topbar.is-scrolled {
|
|
236
|
+
background: var(--bg-content);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.mobile-topbar-menu {
|
|
240
|
+
position: absolute;
|
|
241
|
+
left: 14px;
|
|
242
|
+
top: 50%;
|
|
243
|
+
transform: translateY(-50%);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.app-sidebar {
|
|
247
|
+
display: flex;
|
|
248
|
+
position: fixed;
|
|
249
|
+
top: 0;
|
|
250
|
+
left: 0;
|
|
251
|
+
bottom: 24px;
|
|
252
|
+
width: min(260px, 82vw);
|
|
253
|
+
z-index: 30;
|
|
254
|
+
transform: translateX(-100%);
|
|
255
|
+
transition: transform 0.18s ease;
|
|
256
|
+
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.app-sidebar.mobile-open {
|
|
260
|
+
transform: translateX(0);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.mobile-sidebar-overlay {
|
|
264
|
+
display: block;
|
|
265
|
+
position: fixed;
|
|
266
|
+
inset: 0 0 24px 0;
|
|
267
|
+
background: rgba(0, 0, 0, 0.45);
|
|
268
|
+
opacity: 0;
|
|
269
|
+
pointer-events: none;
|
|
270
|
+
transition: opacity 0.18s ease;
|
|
271
|
+
z-index: 20;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.mobile-sidebar-overlay.active {
|
|
275
|
+
opacity: 1;
|
|
276
|
+
pointer-events: auto;
|
|
277
|
+
}
|
|
183
278
|
}
|
package/lib/public/css/theme.css
CHANGED
|
@@ -156,6 +156,32 @@ textarea:focus {
|
|
|
156
156
|
background: rgba(99, 235, 255, 0.08);
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
.ac-btn-secondary {
|
|
160
|
+
border: 1px solid var(--panel-border-contrast);
|
|
161
|
+
color: #d1d5db;
|
|
162
|
+
background: rgba(255, 255, 255, 0.03);
|
|
163
|
+
transition:
|
|
164
|
+
border-color 0.15s ease,
|
|
165
|
+
color 0.15s ease,
|
|
166
|
+
background 0.15s ease,
|
|
167
|
+
transform 0.15s ease;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.ac-btn-secondary:hover:not(:disabled) {
|
|
171
|
+
border-color: rgba(255, 255, 255, 0.35);
|
|
172
|
+
color: #f3f4f6;
|
|
173
|
+
background: rgba(255, 255, 255, 0.06);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.ac-btn-secondary:active:not(:disabled) {
|
|
177
|
+
transform: translateY(1px);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.ac-btn-secondary:disabled {
|
|
181
|
+
opacity: 0.5;
|
|
182
|
+
cursor: not-allowed;
|
|
183
|
+
}
|
|
184
|
+
|
|
159
185
|
.ac-btn-green {
|
|
160
186
|
border: 1px solid rgba(34, 197, 94, 0.45);
|
|
161
187
|
background: linear-gradient(
|
package/lib/public/js/app.js
CHANGED
|
@@ -30,6 +30,7 @@ import { Envars } from "./components/envars.js";
|
|
|
30
30
|
import { ToastContainer, showToast } from "./components/toast.js";
|
|
31
31
|
import { TelegramWorkspace } from "./components/telegram-workspace.js";
|
|
32
32
|
import { ChevronDownIcon } from "./components/icons.js";
|
|
33
|
+
import { UpdateActionButton } from "./components/update-action-button.js";
|
|
33
34
|
const html = htm.bind(h);
|
|
34
35
|
const kUiTabs = ["general", "providers", "envars"];
|
|
35
36
|
const kSubScreens = ["telegram"];
|
|
@@ -236,8 +237,8 @@ const GeneralTab = ({ onSwitchTab, onNavigate, isActive }) => {
|
|
|
236
237
|
<div>
|
|
237
238
|
<h2 class="font-semibold text-sm">OpenClaw Gateway Dashboard</h2>
|
|
238
239
|
</div>
|
|
239
|
-
|
|
240
|
-
|
|
240
|
+
<${UpdateActionButton}
|
|
241
|
+
onClick=${async () => {
|
|
241
242
|
if (dashboardLoading) return;
|
|
242
243
|
setDashboardLoading(true);
|
|
243
244
|
try {
|
|
@@ -250,13 +251,11 @@ const GeneralTab = ({ onSwitchTab, onNavigate, isActive }) => {
|
|
|
250
251
|
}
|
|
251
252
|
setDashboardLoading(false);
|
|
252
253
|
}}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
${dashboardLoading ? "Opening..." : "Open"}
|
|
259
|
-
</button>
|
|
254
|
+
loading=${dashboardLoading}
|
|
255
|
+
warning=${false}
|
|
256
|
+
idleLabel="Open"
|
|
257
|
+
loadingLabel="Opening..."
|
|
258
|
+
/>
|
|
260
259
|
</div>
|
|
261
260
|
<${DevicePairings}
|
|
262
261
|
pending=${devicePending}
|
|
@@ -298,6 +297,8 @@ function App() {
|
|
|
298
297
|
const [acDismissed, setAcDismissed] = useState(false);
|
|
299
298
|
const [authEnabled, setAuthEnabled] = useState(false);
|
|
300
299
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
300
|
+
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
|
301
|
+
const [mobileTopbarScrolled, setMobileTopbarScrolled] = useState(false);
|
|
301
302
|
const menuRef = useRef(null);
|
|
302
303
|
|
|
303
304
|
const closeMenu = useCallback((e) => {
|
|
@@ -326,6 +327,15 @@ function App() {
|
|
|
326
327
|
history.replaceState(null, "", `#${subScreen || tab}`);
|
|
327
328
|
}, [tab, subScreen]);
|
|
328
329
|
|
|
330
|
+
useEffect(() => {
|
|
331
|
+
if (!mobileSidebarOpen) return;
|
|
332
|
+
const previousOverflow = document.body.style.overflow;
|
|
333
|
+
document.body.style.overflow = "hidden";
|
|
334
|
+
return () => {
|
|
335
|
+
document.body.style.overflow = previousOverflow;
|
|
336
|
+
};
|
|
337
|
+
}, [mobileSidebarOpen]);
|
|
338
|
+
|
|
329
339
|
useEffect(() => {
|
|
330
340
|
if (!onboarded) return;
|
|
331
341
|
let active = true;
|
|
@@ -363,7 +373,6 @@ function App() {
|
|
|
363
373
|
setAcUpdating(false);
|
|
364
374
|
}
|
|
365
375
|
};
|
|
366
|
-
|
|
367
376
|
// Still loading onboard status
|
|
368
377
|
if (onboarded === null) {
|
|
369
378
|
return html`
|
|
@@ -408,8 +417,20 @@ function App() {
|
|
|
408
417
|
`;
|
|
409
418
|
}
|
|
410
419
|
|
|
411
|
-
const navigateToSubScreen = (screen) =>
|
|
412
|
-
|
|
420
|
+
const navigateToSubScreen = (screen) => {
|
|
421
|
+
setSubScreen(screen);
|
|
422
|
+
setMobileSidebarOpen(false);
|
|
423
|
+
};
|
|
424
|
+
const exitSubScreen = () => {
|
|
425
|
+
setSubScreen(null);
|
|
426
|
+
setMobileSidebarOpen(false);
|
|
427
|
+
};
|
|
428
|
+
const handleAppContentScroll = (e) => {
|
|
429
|
+
const nextScrolled = e.currentTarget.scrollTop > 0;
|
|
430
|
+
setMobileTopbarScrolled((currentScrolled) =>
|
|
431
|
+
currentScrolled === nextScrolled ? currentScrolled : nextScrolled,
|
|
432
|
+
);
|
|
433
|
+
};
|
|
413
434
|
|
|
414
435
|
const kNavItems = [
|
|
415
436
|
{ id: "general", label: "General" },
|
|
@@ -419,7 +440,7 @@ function App() {
|
|
|
419
440
|
|
|
420
441
|
return html`
|
|
421
442
|
<div class="app-shell">
|
|
422
|
-
<div class
|
|
443
|
+
<div class=${`app-sidebar ${mobileSidebarOpen ? "mobile-open" : ""}`}>
|
|
423
444
|
<div class="sidebar-brand">
|
|
424
445
|
<img src="./img/logo.svg" alt="" width="20" height="20" />
|
|
425
446
|
<span><span style="color: var(--accent)">alpha</span>claw</span>
|
|
@@ -458,7 +479,11 @@ function App() {
|
|
|
458
479
|
(item) => html`
|
|
459
480
|
<a
|
|
460
481
|
class=${tab === item.id && !subScreen ? "active" : ""}
|
|
461
|
-
onclick=${() => {
|
|
482
|
+
onclick=${() => {
|
|
483
|
+
setSubScreen(null);
|
|
484
|
+
setTab(item.id);
|
|
485
|
+
setMobileSidebarOpen(false);
|
|
486
|
+
}}
|
|
462
487
|
>
|
|
463
488
|
${item.label}
|
|
464
489
|
</a>
|
|
@@ -468,19 +493,42 @@ function App() {
|
|
|
468
493
|
<div class="sidebar-footer">
|
|
469
494
|
${acHasUpdate && acLatest && !acDismissed
|
|
470
495
|
? html`
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
496
|
+
<${UpdateActionButton}
|
|
497
|
+
onClick=${handleAcUpdate}
|
|
498
|
+
loading=${acUpdating}
|
|
499
|
+
warning=${true}
|
|
500
|
+
idleLabel=${`Update to v${acLatest}`}
|
|
501
|
+
loadingLabel="Updating..."
|
|
502
|
+
className="w-full justify-center"
|
|
503
|
+
/>
|
|
478
504
|
`
|
|
479
505
|
: null}
|
|
480
506
|
</div>
|
|
481
507
|
</div>
|
|
482
508
|
|
|
483
|
-
<div
|
|
509
|
+
<div
|
|
510
|
+
class=${`mobile-sidebar-overlay ${mobileSidebarOpen ? "active" : ""}`}
|
|
511
|
+
onclick=${() => setMobileSidebarOpen(false)}
|
|
512
|
+
/>
|
|
513
|
+
|
|
514
|
+
<div class="app-content" onscroll=${handleAppContentScroll}>
|
|
515
|
+
<div class=${`mobile-topbar ${mobileTopbarScrolled ? "is-scrolled" : ""}`}>
|
|
516
|
+
<button
|
|
517
|
+
class="mobile-topbar-menu"
|
|
518
|
+
onclick=${() => setMobileSidebarOpen((open) => !open)}
|
|
519
|
+
aria-label="Open menu"
|
|
520
|
+
aria-expanded=${mobileSidebarOpen ? "true" : "false"}
|
|
521
|
+
>
|
|
522
|
+
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor">
|
|
523
|
+
<path
|
|
524
|
+
d="M2 3.75a.75.75 0 01.75-.75h10.5a.75.75 0 010 1.5H2.75A.75.75 0 012 3.75zm0 4.25a.75.75 0 01.75-.75h10.5a.75.75 0 010 1.5H2.75A.75.75 0 012 8zm0 4.25a.75.75 0 01.75-.75h10.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75z"
|
|
525
|
+
/>
|
|
526
|
+
</svg>
|
|
527
|
+
</button>
|
|
528
|
+
<span class="mobile-topbar-title">
|
|
529
|
+
<span style="color: var(--accent)">alpha</span>claw
|
|
530
|
+
</span>
|
|
531
|
+
</div>
|
|
484
532
|
<div class="max-w-2xl w-full mx-auto">
|
|
485
533
|
${subScreen === "telegram"
|
|
486
534
|
? html`
|
|
@@ -17,7 +17,7 @@ export function Channels({ channels, onSwitchTab, onNavigate }) {
|
|
|
17
17
|
${channels ? ALL_CHANNELS.map(ch => {
|
|
18
18
|
const info = channels[ch];
|
|
19
19
|
const channelMeta = kChannelMeta[ch] || { label: ch.charAt(0).toUpperCase() + ch.slice(1), iconSrc: '' };
|
|
20
|
-
const isClickable = ch === 'telegram' && info && onNavigate;
|
|
20
|
+
const isClickable = ch === 'telegram' && info?.status === 'paired' && onNavigate;
|
|
21
21
|
let badge;
|
|
22
22
|
if (!info) {
|
|
23
23
|
badge = html`<a
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useEffect } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
|
|
5
|
+
const html = htm.bind(h);
|
|
6
|
+
|
|
7
|
+
export const ConfirmDialog = ({
|
|
8
|
+
visible = false,
|
|
9
|
+
title = "Confirm action",
|
|
10
|
+
message = "Are you sure you want to continue?",
|
|
11
|
+
confirmLabel = "Confirm",
|
|
12
|
+
cancelLabel = "Cancel",
|
|
13
|
+
onConfirm,
|
|
14
|
+
onCancel,
|
|
15
|
+
confirmTone = "primary",
|
|
16
|
+
}) => {
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!visible) return;
|
|
19
|
+
|
|
20
|
+
const handleKeydown = (event) => {
|
|
21
|
+
if (event.key === "Escape") {
|
|
22
|
+
onCancel?.();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
window.addEventListener("keydown", handleKeydown);
|
|
27
|
+
return () => window.removeEventListener("keydown", handleKeydown);
|
|
28
|
+
}, [visible, onCancel]);
|
|
29
|
+
|
|
30
|
+
if (!visible) return null;
|
|
31
|
+
|
|
32
|
+
const confirmClass =
|
|
33
|
+
confirmTone === "warning"
|
|
34
|
+
? "border border-yellow-500/45 text-yellow-300 bg-[linear-gradient(180deg,rgba(234,179,8,0.22)_0%,rgba(234,179,8,0.12)_100%)] shadow-[inset_0_0_0_1px_rgba(234,179,8,0.18)] hover:border-yellow-300/75 hover:text-yellow-200 hover:bg-[linear-gradient(180deg,rgba(234,179,8,0.3)_0%,rgba(234,179,8,0.16)_100%)] hover:shadow-[inset_0_0_0_1px_rgba(234,179,8,0.26),0_0_12px_rgba(234,179,8,0.16)]"
|
|
35
|
+
: "ac-btn-cyan";
|
|
36
|
+
|
|
37
|
+
return html`
|
|
38
|
+
<div
|
|
39
|
+
class="fixed inset-0 bg-black/70 flex items-center justify-center p-4 z-50"
|
|
40
|
+
onclick=${(event) => {
|
|
41
|
+
if (event.target === event.currentTarget) onCancel?.();
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<div class="bg-modal border border-border rounded-xl p-5 max-w-md w-full space-y-3">
|
|
45
|
+
<h2 class="text-base font-semibold">${title}</h2>
|
|
46
|
+
<p class="text-sm text-gray-400">${message}</p>
|
|
47
|
+
<div class="pt-1 flex items-center justify-end gap-2">
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
onclick=${onCancel}
|
|
51
|
+
class="px-4 py-2 rounded-lg text-sm ac-btn-secondary"
|
|
52
|
+
>
|
|
53
|
+
${cancelLabel}
|
|
54
|
+
</button>
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
onclick=${onConfirm}
|
|
58
|
+
class="px-4 py-2 rounded-lg text-sm transition-all ${confirmClass}"
|
|
59
|
+
>
|
|
60
|
+
${confirmLabel}
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
`;
|
|
66
|
+
};
|
|
@@ -328,7 +328,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
|
328
328
|
</button>
|
|
329
329
|
<button
|
|
330
330
|
onclick=${onClose}
|
|
331
|
-
class="px-4 py-2 rounded-lg text-sm ac-btn-
|
|
331
|
+
class="px-4 py-2 rounded-lg text-sm ac-btn-secondary"
|
|
332
332
|
>
|
|
333
333
|
Cancel
|
|
334
334
|
</button>
|
|
@@ -7,14 +7,39 @@ import {
|
|
|
7
7
|
updateOpenclaw,
|
|
8
8
|
} from "../lib/api.js";
|
|
9
9
|
import { showToast } from "./toast.js";
|
|
10
|
+
import { UpdateActionButton } from "./update-action-button.js";
|
|
11
|
+
import { ConfirmDialog } from "./confirm-dialog.js";
|
|
10
12
|
const html = htm.bind(h);
|
|
11
13
|
|
|
12
|
-
function VersionRow({ label, currentVersion, fetchVersion, applyUpdate
|
|
14
|
+
function VersionRow({ label, currentVersion, fetchVersion, applyUpdate }) {
|
|
13
15
|
const [checking, setChecking] = useState(false);
|
|
14
16
|
const [version, setVersion] = useState(currentVersion || null);
|
|
15
17
|
const [latestVersion, setLatestVersion] = useState(null);
|
|
16
18
|
const [hasUpdate, setHasUpdate] = useState(false);
|
|
17
19
|
const [error, setError] = useState("");
|
|
20
|
+
const [hasViewedChangelog, setHasViewedChangelog] = useState(false);
|
|
21
|
+
const [confirmWithoutChangelogOpen, setConfirmWithoutChangelogOpen] = useState(false);
|
|
22
|
+
const simulateUpdate = (() => {
|
|
23
|
+
try {
|
|
24
|
+
const params = new URLSearchParams(window.location.search);
|
|
25
|
+
return params.get("simulateUpdate") === "1";
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
})();
|
|
30
|
+
const simulatedVersion = (() => {
|
|
31
|
+
if (!simulateUpdate) return null;
|
|
32
|
+
try {
|
|
33
|
+
const params = new URLSearchParams(window.location.search);
|
|
34
|
+
return params.get("simulateVersion") || "v0.0.0-preview";
|
|
35
|
+
} catch {
|
|
36
|
+
return "v0.0.0-preview";
|
|
37
|
+
}
|
|
38
|
+
})();
|
|
39
|
+
const effectiveHasUpdate = simulateUpdate || hasUpdate;
|
|
40
|
+
const effectiveLatestVersion = simulatedVersion || latestVersion;
|
|
41
|
+
const changelogUrl = "https://github.com/openclaw/openclaw/tags";
|
|
42
|
+
const showMobileUpdateRow = effectiveHasUpdate && effectiveLatestVersion;
|
|
18
43
|
|
|
19
44
|
useEffect(() => {
|
|
20
45
|
setVersion(currentVersion || null);
|
|
@@ -36,22 +61,30 @@ function VersionRow({ label, currentVersion, fetchVersion, applyUpdate, tagsUrl
|
|
|
36
61
|
}
|
|
37
62
|
};
|
|
38
63
|
load();
|
|
39
|
-
return () => {
|
|
64
|
+
return () => {
|
|
65
|
+
active = false;
|
|
66
|
+
};
|
|
40
67
|
}, []);
|
|
41
68
|
|
|
42
|
-
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!effectiveHasUpdate || !effectiveLatestVersion) {
|
|
71
|
+
setHasViewedChangelog(false);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
setHasViewedChangelog(false);
|
|
75
|
+
}, [effectiveHasUpdate, effectiveLatestVersion]);
|
|
76
|
+
|
|
77
|
+
const runAction = async () => {
|
|
43
78
|
if (checking) return;
|
|
44
79
|
setChecking(true);
|
|
45
80
|
setError("");
|
|
46
81
|
try {
|
|
47
|
-
const data =
|
|
48
|
-
? await applyUpdate()
|
|
49
|
-
: await fetchVersion(true);
|
|
82
|
+
const data = effectiveHasUpdate ? await applyUpdate() : await fetchVersion(true);
|
|
50
83
|
setVersion(data.currentVersion || version);
|
|
51
84
|
setLatestVersion(data.latestVersion || null);
|
|
52
85
|
setHasUpdate(!!data.hasUpdate);
|
|
53
86
|
setError(data.ok ? "" : data.error || "");
|
|
54
|
-
if (
|
|
87
|
+
if (effectiveHasUpdate) {
|
|
55
88
|
if (!data.ok) {
|
|
56
89
|
showToast(data.error || `${label} update failed`, "error");
|
|
57
90
|
} else if (data.updated || data.restarting) {
|
|
@@ -65,42 +98,117 @@ function VersionRow({ label, currentVersion, fetchVersion, applyUpdate, tagsUrl
|
|
|
65
98
|
showToast(`Already at latest ${label} version`, "success");
|
|
66
99
|
}
|
|
67
100
|
} else if (data.hasUpdate && data.latestVersion) {
|
|
68
|
-
showToast(
|
|
101
|
+
showToast(
|
|
102
|
+
`${label} update available: ${data.latestVersion}`,
|
|
103
|
+
"warning",
|
|
104
|
+
);
|
|
69
105
|
} else {
|
|
70
106
|
showToast(`${label} is up to date`, "success");
|
|
71
107
|
}
|
|
72
108
|
} catch (err) {
|
|
73
|
-
setError(
|
|
74
|
-
|
|
109
|
+
setError(
|
|
110
|
+
err.message ||
|
|
111
|
+
(effectiveHasUpdate ? `Could not update ${label}` : "Could not check updates"),
|
|
112
|
+
);
|
|
113
|
+
showToast(
|
|
114
|
+
effectiveHasUpdate ? `Could not update ${label}` : "Could not check updates",
|
|
115
|
+
"error",
|
|
116
|
+
);
|
|
75
117
|
}
|
|
76
118
|
setChecking(false);
|
|
77
119
|
};
|
|
78
120
|
|
|
121
|
+
const handleAction = () => {
|
|
122
|
+
if (checking) return;
|
|
123
|
+
if (effectiveHasUpdate && effectiveLatestVersion && !hasViewedChangelog) {
|
|
124
|
+
setConfirmWithoutChangelogOpen(true);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
runAction();
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const handleConfirmWithoutChangelog = () => {
|
|
131
|
+
setConfirmWithoutChangelogOpen(false);
|
|
132
|
+
runAction();
|
|
133
|
+
};
|
|
134
|
+
|
|
79
135
|
return html`
|
|
80
136
|
<div class="flex items-center justify-between gap-3">
|
|
81
137
|
<div class="min-w-0">
|
|
82
|
-
<p class="text-
|
|
83
|
-
<span class="text-gray-500">${label}</span>${" "}${version
|
|
138
|
+
<p class="text-xs text-gray-300 truncate">
|
|
139
|
+
<span class="text-gray-500">${label}</span>${" "}${version
|
|
140
|
+
? `${version}`
|
|
141
|
+
: "..."}
|
|
84
142
|
</p>
|
|
85
143
|
${error && html`<p class="text-xs text-yellow-500 mt-1">${error}</p>`}
|
|
86
144
|
</div>
|
|
87
145
|
<div class="flex items-center gap-2 shrink-0">
|
|
88
|
-
${
|
|
89
|
-
<a
|
|
90
|
-
|
|
91
|
-
|
|
146
|
+
${effectiveHasUpdate && effectiveLatestVersion && html`
|
|
147
|
+
<a
|
|
148
|
+
href=${changelogUrl}
|
|
149
|
+
target="_blank"
|
|
150
|
+
rel="noreferrer"
|
|
151
|
+
onclick=${() => setHasViewedChangelog(true)}
|
|
152
|
+
class="hidden md:inline text-xs text-gray-500 hover:text-gray-300 transition-colors"
|
|
153
|
+
>View changelog</a
|
|
154
|
+
>
|
|
92
155
|
`}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
156
|
+
${showMobileUpdateRow
|
|
157
|
+
? html`
|
|
158
|
+
<${UpdateActionButton}
|
|
159
|
+
onClick=${handleAction}
|
|
160
|
+
loading=${checking}
|
|
161
|
+
warning=${effectiveHasUpdate}
|
|
162
|
+
idleLabel=${effectiveHasUpdate
|
|
163
|
+
? `Update to ${effectiveLatestVersion || "latest"}`
|
|
164
|
+
: "Check updates"}
|
|
165
|
+
loadingLabel=${effectiveHasUpdate ? "Updating..." : "Checking..."}
|
|
166
|
+
className="hidden md:inline-flex"
|
|
167
|
+
/>
|
|
168
|
+
`
|
|
169
|
+
: html`
|
|
170
|
+
<${UpdateActionButton}
|
|
171
|
+
onClick=${handleAction}
|
|
172
|
+
loading=${checking}
|
|
173
|
+
warning=${effectiveHasUpdate}
|
|
174
|
+
idleLabel=${effectiveHasUpdate
|
|
175
|
+
? `Update to ${effectiveLatestVersion || "latest"}`
|
|
176
|
+
: "Check updates"}
|
|
177
|
+
loadingLabel=${effectiveHasUpdate ? "Updating..." : "Checking..."}
|
|
178
|
+
/>
|
|
179
|
+
`}
|
|
102
180
|
</div>
|
|
103
181
|
</div>
|
|
182
|
+
${showMobileUpdateRow && html`
|
|
183
|
+
<div class="mt-2 md:hidden flex items-center gap-2">
|
|
184
|
+
<a
|
|
185
|
+
href=${changelogUrl}
|
|
186
|
+
target="_blank"
|
|
187
|
+
rel="noreferrer"
|
|
188
|
+
onclick=${() => setHasViewedChangelog(true)}
|
|
189
|
+
class="inline-flex items-center justify-center flex-1 h-9 text-xs rounded-lg border border-border text-gray-400 hover:text-gray-200 hover:border-gray-500 transition-colors"
|
|
190
|
+
>View changelog</a
|
|
191
|
+
>
|
|
192
|
+
<${UpdateActionButton}
|
|
193
|
+
onClick=${handleAction}
|
|
194
|
+
loading=${checking}
|
|
195
|
+
warning=${effectiveHasUpdate}
|
|
196
|
+
idleLabel=${`Update to ${effectiveLatestVersion || "latest"}`}
|
|
197
|
+
loadingLabel="Updating..."
|
|
198
|
+
className="flex-1 h-9 px-3"
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
`}
|
|
202
|
+
<${ConfirmDialog}
|
|
203
|
+
visible=${confirmWithoutChangelogOpen}
|
|
204
|
+
title="Update without changelog?"
|
|
205
|
+
message="Are you sure you want to update without viewing the changelog?"
|
|
206
|
+
confirmLabel=${`Update to ${effectiveLatestVersion || "latest"}`}
|
|
207
|
+
cancelLabel="Cancel"
|
|
208
|
+
confirmTone="warning"
|
|
209
|
+
onCancel=${() => setConfirmWithoutChangelogOpen(false)}
|
|
210
|
+
onConfirm=${handleConfirmWithoutChangelog}
|
|
211
|
+
/>
|
|
104
212
|
`;
|
|
105
213
|
}
|
|
106
214
|
|
|
@@ -134,16 +242,14 @@ export function Gateway({ status, openclawVersion }) {
|
|
|
134
242
|
>
|
|
135
243
|
</div>
|
|
136
244
|
</div>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
disabled=${
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
Restart
|
|
146
|
-
</button>
|
|
245
|
+
<${UpdateActionButton}
|
|
246
|
+
onClick=${handleRestart}
|
|
247
|
+
disabled=${!status}
|
|
248
|
+
loading=${restarting}
|
|
249
|
+
warning=${false}
|
|
250
|
+
idleLabel="Restart"
|
|
251
|
+
loadingLabel="On it..."
|
|
252
|
+
/>
|
|
147
253
|
</div>
|
|
148
254
|
<div class="mt-3 pt-3 border-t border-border">
|
|
149
255
|
<${VersionRow}
|
|
@@ -151,7 +257,6 @@ export function Gateway({ status, openclawVersion }) {
|
|
|
151
257
|
currentVersion=${openclawVersion}
|
|
152
258
|
fetchVersion=${fetchOpenclawVersion}
|
|
153
259
|
applyUpdate=${updateOpenclaw}
|
|
154
|
-
tagsUrl="https://github.com/openclaw/openclaw/tags"
|
|
155
260
|
/>
|
|
156
261
|
</div>
|
|
157
262
|
</div>`;
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getDefaultScopes,
|
|
13
13
|
} from "./scope-picker.js";
|
|
14
14
|
import { CredentialsModal } from "./credentials-modal.js";
|
|
15
|
+
import { ConfirmDialog } from "./confirm-dialog.js";
|
|
15
16
|
import { showToast } from "./toast.js";
|
|
16
17
|
const html = htm.bind(h);
|
|
17
18
|
|
|
@@ -22,6 +23,7 @@ export function Google({ gatewayStatus }) {
|
|
|
22
23
|
const [apiStatus, setApiStatus] = useState({});
|
|
23
24
|
const [checkingApis, setCheckingApis] = useState(false);
|
|
24
25
|
const [modalOpen, setModalOpen] = useState(false);
|
|
26
|
+
const [disconnectDialogOpen, setDisconnectDialogOpen] = useState(false);
|
|
25
27
|
|
|
26
28
|
const runApiCheck = useCallback(async () => {
|
|
27
29
|
setApiStatus({});
|
|
@@ -98,12 +100,6 @@ export function Google({ gatewayStatus }) {
|
|
|
98
100
|
const handleCheckApis = () => runApiCheck();
|
|
99
101
|
|
|
100
102
|
const handleDisconnect = async () => {
|
|
101
|
-
if (
|
|
102
|
-
!confirm(
|
|
103
|
-
"Disconnect Google account? Your agent will lose access to Gmail, Calendar, etc.",
|
|
104
|
-
)
|
|
105
|
-
)
|
|
106
|
-
return;
|
|
107
103
|
const data = await apiDisconnect();
|
|
108
104
|
if (data.ok) {
|
|
109
105
|
setGoogle({
|
|
@@ -188,7 +184,7 @@ export function Google({ gatewayStatus }) {
|
|
|
188
184
|
</button>
|
|
189
185
|
</div>
|
|
190
186
|
<button
|
|
191
|
-
onclick=${
|
|
187
|
+
onclick=${() => setDisconnectDialogOpen(true)}
|
|
192
188
|
class="text-xs text-red-400/60 hover:text-red-400"
|
|
193
189
|
>
|
|
194
190
|
Disconnect
|
|
@@ -216,5 +212,17 @@ export function Google({ gatewayStatus }) {
|
|
|
216
212
|
onClose=${() => setModalOpen(false)}
|
|
217
213
|
onSaved=${refresh}
|
|
218
214
|
/>
|
|
215
|
+
<${ConfirmDialog}
|
|
216
|
+
visible=${disconnectDialogOpen}
|
|
217
|
+
title="Disconnect Google account?"
|
|
218
|
+
message="Your agent will lose access to Gmail, Calendar, and other Google Workspace services until you reconnect."
|
|
219
|
+
confirmLabel="Disconnect"
|
|
220
|
+
cancelLabel="Cancel"
|
|
221
|
+
onCancel=${() => setDisconnectDialogOpen(false)}
|
|
222
|
+
onConfirm=${async () => {
|
|
223
|
+
setDisconnectDialogOpen(false);
|
|
224
|
+
await handleDisconnect();
|
|
225
|
+
}}
|
|
226
|
+
/>
|
|
219
227
|
`;
|
|
220
228
|
}
|
|
@@ -36,6 +36,7 @@ export const WelcomeFormStep = ({
|
|
|
36
36
|
goBack,
|
|
37
37
|
goNext,
|
|
38
38
|
loading,
|
|
39
|
+
githubStepLoading,
|
|
39
40
|
allValid,
|
|
40
41
|
handleSubmit,
|
|
41
42
|
}) => {
|
|
@@ -276,10 +277,12 @@ export const WelcomeFormStep = ({
|
|
|
276
277
|
: html`<div class="w-full"></div>`}
|
|
277
278
|
<button
|
|
278
279
|
onclick=${goNext}
|
|
279
|
-
disabled=${!currentGroupValid}
|
|
280
|
+
disabled=${!currentGroupValid || githubStepLoading}
|
|
280
281
|
class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all ac-btn-cyan"
|
|
281
282
|
>
|
|
282
|
-
|
|
283
|
+
${activeGroup.id === "github" && githubStepLoading
|
|
284
|
+
? "Checking..."
|
|
285
|
+
: "Next"}
|
|
283
286
|
</button>
|
|
284
287
|
`
|
|
285
288
|
: html`
|
|
@@ -78,7 +78,7 @@ export const WelcomeSetupStep = ({ error, loading, onRetry, onBack }) => {
|
|
|
78
78
|
const currentTip = kSetupTips[tipIndex];
|
|
79
79
|
|
|
80
80
|
return html`
|
|
81
|
-
<div class="min-h-[320px]
|
|
81
|
+
<div class="relative min-h-[320px] pt-4 pb-20 flex">
|
|
82
82
|
<div
|
|
83
83
|
class="flex-1 flex flex-col items-center justify-center text-center gap-4"
|
|
84
84
|
>
|
|
@@ -107,7 +107,7 @@ export const WelcomeSetupStep = ({ error, loading, onRetry, onBack }) => {
|
|
|
107
107
|
<p class="text-sm text-gray-500">This could take 10-15 seconds</p>
|
|
108
108
|
</div>
|
|
109
109
|
<div
|
|
110
|
-
class="
|
|
110
|
+
class="absolute bottom-3 left-3 right-3 bg-black/20 border border-border rounded-lg px-3 py-2 text-xs text-gray-500"
|
|
111
111
|
>
|
|
112
112
|
<span class="text-gray-400">${currentTip.label}: </span>
|
|
113
113
|
${currentTip.text}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
|
|
4
|
+
const html = htm.bind(h);
|
|
5
|
+
|
|
6
|
+
export const UpdateActionButton = ({
|
|
7
|
+
onClick,
|
|
8
|
+
disabled = false,
|
|
9
|
+
loading = false,
|
|
10
|
+
warning = false,
|
|
11
|
+
idleLabel = "Check updates",
|
|
12
|
+
loadingLabel = "Checking...",
|
|
13
|
+
className = "",
|
|
14
|
+
}) => {
|
|
15
|
+
const isInteractive = !loading && !disabled;
|
|
16
|
+
const toneClass = warning
|
|
17
|
+
? isInteractive
|
|
18
|
+
? "border-yellow-500/35 text-yellow-400 bg-yellow-500/10 hover:border-yellow-400/60 hover:text-yellow-300 hover:bg-yellow-500/15"
|
|
19
|
+
: "border-yellow-500/35 text-yellow-400 bg-yellow-500/10"
|
|
20
|
+
: isInteractive
|
|
21
|
+
? "border-border text-gray-500 hover:text-gray-300 hover:border-gray-500"
|
|
22
|
+
: "border-border text-gray-500";
|
|
23
|
+
const loadingClass = loading
|
|
24
|
+
? `cursor-not-allowed ${warning
|
|
25
|
+
? "opacity-90 animate-pulse shadow-[0_0_0_1px_rgba(234,179,8,0.22),0_0_18px_rgba(234,179,8,0.12)]"
|
|
26
|
+
: "opacity-80"}`
|
|
27
|
+
: "";
|
|
28
|
+
|
|
29
|
+
return html`
|
|
30
|
+
<button
|
|
31
|
+
onclick=${onClick}
|
|
32
|
+
disabled=${disabled || loading}
|
|
33
|
+
class="inline-flex items-center justify-center h-7 text-xs leading-none px-2.5 py-1 rounded-lg border transition-colors whitespace-nowrap ${toneClass} ${loadingClass} ${className}"
|
|
34
|
+
>
|
|
35
|
+
${loading
|
|
36
|
+
? html`
|
|
37
|
+
<span class="inline-flex items-center gap-1.5 leading-none">
|
|
38
|
+
<svg
|
|
39
|
+
class="animate-spin"
|
|
40
|
+
width="12"
|
|
41
|
+
height="12"
|
|
42
|
+
viewBox="0 0 24 24"
|
|
43
|
+
fill="none"
|
|
44
|
+
aria-hidden="true"
|
|
45
|
+
>
|
|
46
|
+
<circle
|
|
47
|
+
class="opacity-30"
|
|
48
|
+
cx="12"
|
|
49
|
+
cy="12"
|
|
50
|
+
r="9"
|
|
51
|
+
stroke="currentColor"
|
|
52
|
+
stroke-width="3"
|
|
53
|
+
/>
|
|
54
|
+
<path
|
|
55
|
+
class="opacity-90"
|
|
56
|
+
fill="currentColor"
|
|
57
|
+
d="M12 3a9 9 0 0 1 9 9h-3a6 6 0 0 0-6-6V3z"
|
|
58
|
+
/>
|
|
59
|
+
</svg>
|
|
60
|
+
${loadingLabel}
|
|
61
|
+
</span>
|
|
62
|
+
`
|
|
63
|
+
: idleLabel}
|
|
64
|
+
</button>
|
|
65
|
+
`;
|
|
66
|
+
};
|
|
@@ -3,6 +3,7 @@ import { useState, useEffect, useRef } from "https://esm.sh/preact/hooks";
|
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import {
|
|
5
5
|
runOnboard,
|
|
6
|
+
verifyGithubOnboardingRepo,
|
|
6
7
|
fetchModels,
|
|
7
8
|
fetchCodexStatus,
|
|
8
9
|
disconnectCodex,
|
|
@@ -58,6 +59,7 @@ export const Welcome = ({ onComplete }) => {
|
|
|
58
59
|
const [codexAuthStarted, setCodexAuthStarted] = useState(false);
|
|
59
60
|
const [codexAuthWaiting, setCodexAuthWaiting] = useState(false);
|
|
60
61
|
const [loading, setLoading] = useState(false);
|
|
62
|
+
const [githubStepLoading, setGithubStepLoading] = useState(false);
|
|
61
63
|
const [error, setError] = useState(null);
|
|
62
64
|
const codexPopupPollRef = useRef(null);
|
|
63
65
|
|
|
@@ -360,8 +362,27 @@ export const Welcome = ({ onComplete }) => {
|
|
|
360
362
|
setStep(kWelcomeGroups.length - 1);
|
|
361
363
|
};
|
|
362
364
|
|
|
363
|
-
const goNext = () => {
|
|
365
|
+
const goNext = async () => {
|
|
364
366
|
if (!activeGroup || !currentGroupValid) return;
|
|
367
|
+
if (activeGroup.id === "github") {
|
|
368
|
+
setGithubStepLoading(true);
|
|
369
|
+
setError(null);
|
|
370
|
+
try {
|
|
371
|
+
const result = await verifyGithubOnboardingRepo(
|
|
372
|
+
vals.GITHUB_WORKSPACE_REPO,
|
|
373
|
+
vals.GITHUB_TOKEN,
|
|
374
|
+
);
|
|
375
|
+
if (!result?.ok) {
|
|
376
|
+
setError(result?.error || "GitHub verification failed");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
} catch (err) {
|
|
380
|
+
setError(err?.message || "GitHub verification failed");
|
|
381
|
+
return;
|
|
382
|
+
} finally {
|
|
383
|
+
setGithubStepLoading(false);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
365
386
|
setStep((prev) => Math.min(kWelcomeGroups.length - 1, prev + 1));
|
|
366
387
|
};
|
|
367
388
|
|
|
@@ -438,6 +459,7 @@ export const Welcome = ({ onComplete }) => {
|
|
|
438
459
|
goBack=${goBack}
|
|
439
460
|
goNext=${goNext}
|
|
440
461
|
loading=${loading}
|
|
462
|
+
githubStepLoading=${githubStepLoading}
|
|
441
463
|
allValid=${allValid}
|
|
442
464
|
handleSubmit=${handleSubmit}
|
|
443
465
|
/>
|
package/lib/public/js/lib/api.js
CHANGED
|
@@ -167,6 +167,15 @@ export async function runOnboard(vars, modelKey) {
|
|
|
167
167
|
return res.json();
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
export async function verifyGithubOnboardingRepo(repo, token) {
|
|
171
|
+
const res = await authFetch('/api/onboard/github/verify', {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: { 'Content-Type': 'application/json' },
|
|
174
|
+
body: JSON.stringify({ repo, token }),
|
|
175
|
+
});
|
|
176
|
+
return res.json();
|
|
177
|
+
}
|
|
178
|
+
|
|
170
179
|
export const fetchModels = async () => {
|
|
171
180
|
const res = await authFetch('/api/models');
|
|
172
181
|
return res.json();
|
|
@@ -1,48 +1,114 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
};
|
|
1
|
+
const buildGithubHeaders = (githubToken) => ({
|
|
2
|
+
Authorization: `token ${githubToken}`,
|
|
3
|
+
"User-Agent": "openclaw-railway",
|
|
4
|
+
Accept: "application/vnd.github+json",
|
|
5
|
+
});
|
|
7
6
|
|
|
7
|
+
const parseGithubErrorMessage = async (response) => {
|
|
8
8
|
try {
|
|
9
|
-
const
|
|
9
|
+
const payload = await response.json();
|
|
10
|
+
if (typeof payload?.message === "string" && payload.message.trim()) {
|
|
11
|
+
return payload.message.trim();
|
|
12
|
+
}
|
|
13
|
+
} catch {}
|
|
14
|
+
return response.statusText || `HTTP ${response.status}`;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const verifyGithubRepoForOnboarding = async ({ repoUrl, githubToken }) => {
|
|
18
|
+
const ghHeaders = buildGithubHeaders(githubToken);
|
|
19
|
+
const [repoOwner] = String(repoUrl || "").split("/", 1);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const userRes = await fetch("https://api.github.com/user", {
|
|
10
23
|
headers: ghHeaders,
|
|
11
24
|
});
|
|
25
|
+
if (!userRes.ok) {
|
|
26
|
+
const details = await parseGithubErrorMessage(userRes);
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
status: 400,
|
|
30
|
+
error: `Cannot verify GitHub token: ${details}`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const authedUser = await userRes.json().catch(() => ({}));
|
|
34
|
+
const authedLogin = String(authedUser?.login || "").trim();
|
|
35
|
+
if (
|
|
36
|
+
repoOwner &&
|
|
37
|
+
authedLogin &&
|
|
38
|
+
repoOwner.toLowerCase() !== authedLogin.toLowerCase()
|
|
39
|
+
) {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
status: 400,
|
|
43
|
+
error: `Workspace repo owner must match your token user "${authedLogin}"`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
12
46
|
|
|
47
|
+
const checkRes = await fetch(`https://api.github.com/repos/${repoUrl}`, {
|
|
48
|
+
headers: ghHeaders,
|
|
49
|
+
});
|
|
13
50
|
if (checkRes.status === 404) {
|
|
14
|
-
console.log(`[onboard] Creating repo ${repoUrl}...`);
|
|
15
|
-
const createRes = await fetch("https://api.github.com/user/repos", {
|
|
16
|
-
method: "POST",
|
|
17
|
-
headers: { ...ghHeaders, "Content-Type": "application/json" },
|
|
18
|
-
body: JSON.stringify({
|
|
19
|
-
name: repoName,
|
|
20
|
-
private: true,
|
|
21
|
-
auto_init: false,
|
|
22
|
-
}),
|
|
23
|
-
});
|
|
24
|
-
if (!createRes.ok) {
|
|
25
|
-
const err = await createRes.json().catch(() => ({}));
|
|
26
|
-
return {
|
|
27
|
-
ok: false,
|
|
28
|
-
status: 400,
|
|
29
|
-
error: `Failed to create repo: ${err.message || createRes.statusText}`,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
console.log(`[onboard] Repo ${repoUrl} created`);
|
|
33
51
|
return { ok: true };
|
|
34
52
|
}
|
|
53
|
+
if (checkRes.ok) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
status: 400,
|
|
57
|
+
error: `Repository "${repoUrl}" already exists.`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
35
60
|
|
|
36
|
-
|
|
37
|
-
|
|
61
|
+
const details = await parseGithubErrorMessage(checkRes);
|
|
38
62
|
return {
|
|
39
63
|
ok: false,
|
|
40
64
|
status: 400,
|
|
41
|
-
error: `Cannot
|
|
65
|
+
error: `Cannot verify repo "${repoUrl}": ${details}`,
|
|
42
66
|
};
|
|
67
|
+
} catch (e) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
status: 400,
|
|
71
|
+
error: `GitHub verification error: ${e.message}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const ensureGithubRepoAccessible = async ({
|
|
77
|
+
repoUrl,
|
|
78
|
+
repoName,
|
|
79
|
+
githubToken,
|
|
80
|
+
}) => {
|
|
81
|
+
const ghHeaders = buildGithubHeaders(githubToken);
|
|
82
|
+
const verification = await verifyGithubRepoForOnboarding({
|
|
83
|
+
repoUrl,
|
|
84
|
+
githubToken,
|
|
85
|
+
});
|
|
86
|
+
if (!verification.ok) return verification;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
console.log(`[onboard] Creating repo ${repoUrl}...`);
|
|
90
|
+
const createRes = await fetch("https://api.github.com/user/repos", {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: { ...ghHeaders, "Content-Type": "application/json" },
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
name: repoName,
|
|
95
|
+
private: true,
|
|
96
|
+
auto_init: false,
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
if (!createRes.ok) {
|
|
100
|
+
const details = await parseGithubErrorMessage(createRes);
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
status: 400,
|
|
104
|
+
error: `Failed to create repo: ${details}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
console.log(`[onboard] Repo ${repoUrl} created`);
|
|
108
|
+
return { ok: true };
|
|
43
109
|
} catch (e) {
|
|
44
110
|
return { ok: false, status: 400, error: `GitHub error: ${e.message}` };
|
|
45
111
|
}
|
|
46
112
|
};
|
|
47
113
|
|
|
48
|
-
module.exports = { ensureGithubRepoAccessible };
|
|
114
|
+
module.exports = { ensureGithubRepoAccessible, verifyGithubRepoForOnboarding };
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
2
|
const { kSetupDir, kRootDir } = require("../constants");
|
|
3
3
|
const { validateOnboardingInput } = require("./validation");
|
|
4
|
-
const {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
const {
|
|
5
|
+
ensureGithubRepoAccessible,
|
|
6
|
+
verifyGithubRepoForOnboarding,
|
|
7
|
+
} = require("./github");
|
|
8
|
+
const {
|
|
9
|
+
buildOnboardArgs,
|
|
10
|
+
writeSanitizedOpenclawConfig,
|
|
11
|
+
} = require("./openclaw");
|
|
12
|
+
const {
|
|
13
|
+
installControlUiSkill,
|
|
14
|
+
syncBootstrapPromptFiles,
|
|
15
|
+
} = require("./workspace");
|
|
16
|
+
const {
|
|
17
|
+
installHourlyGitSyncScript,
|
|
18
|
+
installHourlyGitSyncCron,
|
|
19
|
+
} = require("./cron");
|
|
8
20
|
|
|
9
21
|
const createOnboardingService = ({
|
|
10
22
|
fs,
|
|
@@ -22,6 +34,15 @@ const createOnboardingService = ({
|
|
|
22
34
|
}) => {
|
|
23
35
|
const { OPENCLAW_DIR, WORKSPACE_DIR } = constants;
|
|
24
36
|
|
|
37
|
+
const verifyGithubSetup = async ({
|
|
38
|
+
githubRepoInput,
|
|
39
|
+
githubToken,
|
|
40
|
+
resolveGithubRepoUrl,
|
|
41
|
+
}) => {
|
|
42
|
+
const repoUrl = resolveGithubRepoUrl(githubRepoInput);
|
|
43
|
+
return verifyGithubRepoForOnboarding({ repoUrl, githubToken });
|
|
44
|
+
};
|
|
45
|
+
|
|
25
46
|
const completeOnboarding = async ({ req, vars, modelKey }) => {
|
|
26
47
|
const validation = validateOnboardingInput({
|
|
27
48
|
vars,
|
|
@@ -30,14 +51,24 @@ const createOnboardingService = ({
|
|
|
30
51
|
hasCodexOauthProfile,
|
|
31
52
|
});
|
|
32
53
|
if (!validation.ok) {
|
|
33
|
-
return {
|
|
54
|
+
return {
|
|
55
|
+
status: validation.status,
|
|
56
|
+
body: { ok: false, error: validation.error },
|
|
57
|
+
};
|
|
34
58
|
}
|
|
35
59
|
|
|
36
|
-
const {
|
|
37
|
-
|
|
60
|
+
const {
|
|
61
|
+
varMap,
|
|
62
|
+
githubToken,
|
|
63
|
+
githubRepoInput,
|
|
64
|
+
selectedProvider,
|
|
65
|
+
hasCodexOauth,
|
|
66
|
+
} = validation.data;
|
|
38
67
|
|
|
39
68
|
const repoUrl = resolveGithubRepoUrl(githubRepoInput);
|
|
40
|
-
const varsToSave = [
|
|
69
|
+
const varsToSave = [
|
|
70
|
+
...vars.filter((v) => v.value && v.key !== "GITHUB_WORKSPACE_REPO"),
|
|
71
|
+
];
|
|
41
72
|
varsToSave.push({ key: "GITHUB_WORKSPACE_REPO", value: repoUrl });
|
|
42
73
|
writeEnvFile(varsToSave);
|
|
43
74
|
reloadEnv();
|
|
@@ -50,22 +81,32 @@ const createOnboardingService = ({
|
|
|
50
81
|
githubToken,
|
|
51
82
|
});
|
|
52
83
|
if (!repoCheck.ok) {
|
|
53
|
-
return {
|
|
84
|
+
return {
|
|
85
|
+
status: repoCheck.status,
|
|
86
|
+
body: { ok: false, error: repoCheck.error },
|
|
87
|
+
};
|
|
54
88
|
}
|
|
55
89
|
|
|
56
90
|
fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
|
|
57
91
|
fs.mkdirSync(WORKSPACE_DIR, { recursive: true });
|
|
58
|
-
syncBootstrapPromptFiles({
|
|
92
|
+
syncBootstrapPromptFiles({
|
|
93
|
+
fs,
|
|
94
|
+
workspaceDir: WORKSPACE_DIR,
|
|
95
|
+
baseUrl: getBaseUrl(req),
|
|
96
|
+
});
|
|
59
97
|
|
|
60
98
|
if (!fs.existsSync(`${OPENCLAW_DIR}/.git`)) {
|
|
61
99
|
await shellCmd(
|
|
62
|
-
`cd ${OPENCLAW_DIR} && git init -b main && git remote add origin "${remoteUrl}" && git config user.email "agent@
|
|
100
|
+
`cd ${OPENCLAW_DIR} && git init -b main && git remote add origin "${remoteUrl}" && git config user.email "agent@alphaclaw.md" && git config user.name "AlphaClaw Agent"`,
|
|
63
101
|
);
|
|
64
102
|
console.log("[onboard] Git initialized");
|
|
65
103
|
}
|
|
66
104
|
|
|
67
105
|
if (!fs.existsSync(`${OPENCLAW_DIR}/.gitignore`)) {
|
|
68
|
-
fs.copyFileSync(
|
|
106
|
+
fs.copyFileSync(
|
|
107
|
+
path.join(kSetupDir, "gitignore"),
|
|
108
|
+
`${OPENCLAW_DIR}/.gitignore`,
|
|
109
|
+
);
|
|
69
110
|
}
|
|
70
111
|
|
|
71
112
|
const onboardArgs = buildOnboardArgs({
|
|
@@ -74,14 +115,17 @@ const createOnboardingService = ({
|
|
|
74
115
|
hasCodexOauth,
|
|
75
116
|
workspaceDir: WORKSPACE_DIR,
|
|
76
117
|
});
|
|
77
|
-
await shellCmd(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
118
|
+
await shellCmd(
|
|
119
|
+
`openclaw onboard ${onboardArgs.map((a) => `"${a}"`).join(" ")}`,
|
|
120
|
+
{
|
|
121
|
+
env: {
|
|
122
|
+
...process.env,
|
|
123
|
+
OPENCLAW_HOME: kRootDir,
|
|
124
|
+
OPENCLAW_CONFIG_PATH: `${OPENCLAW_DIR}/openclaw.json`,
|
|
125
|
+
},
|
|
126
|
+
timeout: 120000,
|
|
82
127
|
},
|
|
83
|
-
|
|
84
|
-
});
|
|
128
|
+
);
|
|
85
129
|
console.log("[onboard] Onboard complete");
|
|
86
130
|
|
|
87
131
|
await shellCmd(`openclaw models set "${modelKey}"`, {
|
|
@@ -89,7 +133,9 @@ const createOnboardingService = ({
|
|
|
89
133
|
timeout: 30000,
|
|
90
134
|
}).catch((e) => {
|
|
91
135
|
console.error("[onboard] Failed to set model:", e.message);
|
|
92
|
-
throw new Error(
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Onboarding completed but failed to set model "${modelKey}"`,
|
|
138
|
+
);
|
|
93
139
|
});
|
|
94
140
|
|
|
95
141
|
try {
|
|
@@ -99,28 +145,33 @@ const createOnboardingService = ({
|
|
|
99
145
|
writeSanitizedOpenclawConfig({ fs, openclawDir: OPENCLAW_DIR, varMap });
|
|
100
146
|
ensureGatewayProxyConfig(getBaseUrl(req));
|
|
101
147
|
|
|
102
|
-
installControlUiSkill({
|
|
148
|
+
installControlUiSkill({
|
|
149
|
+
fs,
|
|
150
|
+
openclawDir: OPENCLAW_DIR,
|
|
151
|
+
baseUrl: getBaseUrl(req),
|
|
152
|
+
});
|
|
103
153
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
154
|
+
installHourlyGitSyncScript({ fs, openclawDir: OPENCLAW_DIR });
|
|
155
|
+
await installHourlyGitSyncCron({ fs, openclawDir: OPENCLAW_DIR });
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
await shellCmd(`alphaclaw git-sync -m "initial setup"`, {
|
|
107
159
|
timeout: 30000,
|
|
108
160
|
env: {
|
|
109
161
|
...process.env,
|
|
110
162
|
GITHUB_TOKEN: githubToken,
|
|
111
163
|
},
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
await installHourlyGitSyncCron({ fs, openclawDir: OPENCLAW_DIR });
|
|
164
|
+
});
|
|
165
|
+
console.log("[onboard] Initial state committed and pushed");
|
|
166
|
+
} catch (e) {
|
|
167
|
+
console.error("[onboard] Git push error:", e.message);
|
|
168
|
+
}
|
|
118
169
|
|
|
119
170
|
startGateway();
|
|
120
171
|
return { status: 200, body: { ok: true } };
|
|
121
172
|
};
|
|
122
173
|
|
|
123
|
-
return { completeOnboarding };
|
|
174
|
+
return { completeOnboarding, verifyGithubSetup };
|
|
124
175
|
};
|
|
125
176
|
|
|
126
177
|
module.exports = { createOnboardingService };
|
|
@@ -111,6 +111,42 @@ const registerOnboardingRoutes = ({
|
|
|
111
111
|
res.status(500).json({ ok: false, error: sanitizeOnboardingError(err) });
|
|
112
112
|
}
|
|
113
113
|
});
|
|
114
|
+
|
|
115
|
+
app.post("/api/onboard/github/verify", async (req, res) => {
|
|
116
|
+
if (isOnboarded()) {
|
|
117
|
+
return res.json({ ok: false, error: "Already onboarded" });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const githubRepoInput = String(req.body?.repo || "").trim();
|
|
122
|
+
const githubToken = String(req.body?.token || "").trim();
|
|
123
|
+
if (!githubRepoInput || !githubToken) {
|
|
124
|
+
return res
|
|
125
|
+
.status(400)
|
|
126
|
+
.json({
|
|
127
|
+
ok: false,
|
|
128
|
+
error: "GitHub token and workspace repo are required",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const result = await onboardingService.verifyGithubSetup({
|
|
133
|
+
githubRepoInput,
|
|
134
|
+
githubToken,
|
|
135
|
+
resolveGithubRepoUrl,
|
|
136
|
+
});
|
|
137
|
+
if (!result.ok) {
|
|
138
|
+
return res
|
|
139
|
+
.status(result.status || 400)
|
|
140
|
+
.json({ ok: false, error: result.error });
|
|
141
|
+
}
|
|
142
|
+
return res.json({ ok: true });
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.error("[onboard] GitHub verify error:", err);
|
|
145
|
+
return res
|
|
146
|
+
.status(500)
|
|
147
|
+
.json({ ok: false, error: sanitizeOnboardingError(err) });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
114
150
|
};
|
|
115
151
|
|
|
116
152
|
module.exports = { registerOnboardingRoutes };
|
|
@@ -6,11 +6,11 @@ AlphaClaw UI: `{{SETUP_UI_URL}}`
|
|
|
6
6
|
|
|
7
7
|
### Tabs
|
|
8
8
|
|
|
9
|
-
| Tab
|
|
10
|
-
|
|
|
11
|
-
| General
|
|
12
|
-
| Providers | `{{SETUP_UI_URL}}#providers` | AI provider credentials (Anthropic, OpenAI, Gemini, Mistral, Voyage, Groq, Deepgram), feature capabilities, Codex OAuth
|
|
13
|
-
| Envars
|
|
9
|
+
| Tab | URL | What it manages |
|
|
10
|
+
| --------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
11
|
+
| General | `{{SETUP_UI_URL}}#general` | Gateway status & restart, channel health (Telegram/Discord), pending pairings, feature health (Embeddings/Audio), Google Workspace connection, repo auto-sync schedule, OpenClaw dashboard |
|
|
12
|
+
| Providers | `{{SETUP_UI_URL}}#providers` | AI provider credentials (Anthropic, OpenAI, Gemini, Mistral, Voyage, Groq, Deepgram), feature capabilities, Codex OAuth |
|
|
13
|
+
| Envars | `{{SETUP_UI_URL}}#envars` | View/edit/add environment variables (saved to `/data/.env`), gateway restart to apply changes |
|
|
14
14
|
|
|
15
15
|
### Environment variables
|
|
16
16
|
|
package/lib/setup/gitignore
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrysb/alphaclaw",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
7
|
"description": "Setup UI, gateway manager, and onboarding wrapper for OpenClaw",
|
|
8
8
|
"bin": {
|
|
9
|
-
"alphaclaw": "
|
|
9
|
+
"alphaclaw": "bin/alphaclaw.js"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"bin/",
|