@chrysb/alphaclaw 0.1.2 → 0.1.4
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/lib/public/js/app.js +68 -1
- package/lib/public/js/components/gateway.js +86 -91
- package/lib/public/js/lib/api.js +11 -0
- package/lib/server/alphaclaw-version.js +190 -0
- package/lib/server/constants.js +2 -0
- package/lib/server/routes/system.js +21 -0
- package/lib/server.js +3 -0
- package/package.json +1 -1
package/lib/public/js/app.js
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
fetchOnboardStatus,
|
|
13
13
|
fetchDashboardUrl,
|
|
14
14
|
updateSyncCron,
|
|
15
|
+
fetchAlphaclawVersion,
|
|
16
|
+
updateAlphaclaw,
|
|
15
17
|
} from "./lib/api.js";
|
|
16
18
|
import { usePolling } from "./hooks/usePolling.js";
|
|
17
19
|
import { Gateway } from "./components/gateway.js";
|
|
@@ -240,6 +242,11 @@ const GeneralTab = ({ onSwitchTab }) => {
|
|
|
240
242
|
function App() {
|
|
241
243
|
const [onboarded, setOnboarded] = useState(null);
|
|
242
244
|
const [tab, setTab] = useState("general");
|
|
245
|
+
const [acVersion, setAcVersion] = useState(null);
|
|
246
|
+
const [acLatest, setAcLatest] = useState(null);
|
|
247
|
+
const [acHasUpdate, setAcHasUpdate] = useState(false);
|
|
248
|
+
const [acUpdating, setAcUpdating] = useState(false);
|
|
249
|
+
const [acDismissed, setAcDismissed] = useState(false);
|
|
243
250
|
|
|
244
251
|
useEffect(() => {
|
|
245
252
|
fetchOnboardStatus()
|
|
@@ -247,6 +254,40 @@ function App() {
|
|
|
247
254
|
.catch(() => setOnboarded(false));
|
|
248
255
|
}, []);
|
|
249
256
|
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
if (!onboarded) return;
|
|
259
|
+
let active = true;
|
|
260
|
+
const check = async () => {
|
|
261
|
+
try {
|
|
262
|
+
const data = await fetchAlphaclawVersion(false);
|
|
263
|
+
if (!active) return;
|
|
264
|
+
setAcVersion(data.currentVersion || null);
|
|
265
|
+
setAcLatest(data.latestVersion || null);
|
|
266
|
+
setAcHasUpdate(!!data.hasUpdate);
|
|
267
|
+
} catch {}
|
|
268
|
+
};
|
|
269
|
+
check();
|
|
270
|
+
const id = setInterval(check, 5 * 60 * 1000);
|
|
271
|
+
return () => { active = false; clearInterval(id); };
|
|
272
|
+
}, [onboarded]);
|
|
273
|
+
|
|
274
|
+
const handleAcUpdate = async () => {
|
|
275
|
+
if (acUpdating) return;
|
|
276
|
+
setAcUpdating(true);
|
|
277
|
+
try {
|
|
278
|
+
const data = await updateAlphaclaw();
|
|
279
|
+
if (data.ok) {
|
|
280
|
+
showToast("AlphaClaw updated — restarting...", "success");
|
|
281
|
+
} else {
|
|
282
|
+
showToast(data.error || "AlphaClaw update failed", "error");
|
|
283
|
+
setAcUpdating(false);
|
|
284
|
+
}
|
|
285
|
+
} catch (err) {
|
|
286
|
+
showToast(err.message || "Could not update AlphaClaw", "error");
|
|
287
|
+
setAcUpdating(false);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
250
291
|
// Still loading onboard status
|
|
251
292
|
if (onboarded === null) {
|
|
252
293
|
return html`
|
|
@@ -288,11 +329,37 @@ function App() {
|
|
|
288
329
|
<div class="flex items-center gap-3 pb-3">
|
|
289
330
|
<div class="text-4xl">🦞</div>
|
|
290
331
|
<div>
|
|
291
|
-
<h1 class="text-2xl font-semibold">OpenClaw Setup</h1>
|
|
332
|
+
<h1 class="text-2xl font-semibold">OpenClaw Setup${acVersion ? html`${" "}<span class="text-base font-normal text-gray-600">v${acVersion}</span>` : ""}</h1>
|
|
292
333
|
<p class="text-gray-500 text-sm">This should be easy, right?</p>
|
|
293
334
|
</div>
|
|
294
335
|
</div>
|
|
295
336
|
|
|
337
|
+
${acHasUpdate && acLatest && !acDismissed && html`
|
|
338
|
+
<div class="mb-3 flex items-center justify-between gap-3 rounded-lg border border-yellow-500/30 bg-yellow-500/10 px-3 py-2">
|
|
339
|
+
<p class="text-sm text-yellow-400">
|
|
340
|
+
AlphaClaw <span class="font-medium">v${acLatest}</span> is available
|
|
341
|
+
</p>
|
|
342
|
+
<div class="flex items-center gap-2 shrink-0">
|
|
343
|
+
<button
|
|
344
|
+
onclick=${handleAcUpdate}
|
|
345
|
+
disabled=${acUpdating}
|
|
346
|
+
class="text-xs font-medium px-3 py-1 rounded-lg bg-yellow-500/20 text-yellow-300 hover:bg-yellow-500/30 transition-colors ${acUpdating ? "opacity-50 cursor-not-allowed" : ""}"
|
|
347
|
+
>
|
|
348
|
+
${acUpdating ? "Updating..." : "Update now"}
|
|
349
|
+
</button>
|
|
350
|
+
<button
|
|
351
|
+
onclick=${() => setAcDismissed(true)}
|
|
352
|
+
class="text-yellow-500/60 hover:text-yellow-400 transition-colors"
|
|
353
|
+
title="Dismiss"
|
|
354
|
+
>
|
|
355
|
+
<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor">
|
|
356
|
+
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"/>
|
|
357
|
+
</svg>
|
|
358
|
+
</button>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
`}
|
|
362
|
+
|
|
296
363
|
<div class="flex gap-1 border-b border-border">
|
|
297
364
|
${["general", "models", "envars"].map(
|
|
298
365
|
(t) => html`
|
|
@@ -9,96 +9,118 @@ import {
|
|
|
9
9
|
import { showToast } from "./toast.js";
|
|
10
10
|
const html = htm.bind(h);
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
const [
|
|
14
|
-
const [
|
|
15
|
-
const [currentVersion, setCurrentVersion] = useState(openclawVersion || null);
|
|
12
|
+
function VersionRow({ label, currentVersion, fetchVersion, applyUpdate, tagsUrl }) {
|
|
13
|
+
const [checking, setChecking] = useState(false);
|
|
14
|
+
const [version, setVersion] = useState(currentVersion || null);
|
|
16
15
|
const [latestVersion, setLatestVersion] = useState(null);
|
|
17
16
|
const [hasUpdate, setHasUpdate] = useState(false);
|
|
18
|
-
const [
|
|
19
|
-
const isRunning = status === "running" && !restarting;
|
|
20
|
-
const dotClass = isRunning
|
|
21
|
-
? "w-2 h-2 rounded-full bg-green-500"
|
|
22
|
-
: "w-2 h-2 rounded-full bg-yellow-500 animate-pulse";
|
|
17
|
+
const [error, setError] = useState("");
|
|
23
18
|
|
|
24
19
|
useEffect(() => {
|
|
25
|
-
|
|
26
|
-
}, [
|
|
20
|
+
setVersion(currentVersion || null);
|
|
21
|
+
}, [currentVersion]);
|
|
27
22
|
|
|
28
23
|
useEffect(() => {
|
|
29
24
|
let active = true;
|
|
30
|
-
const
|
|
25
|
+
const load = async () => {
|
|
31
26
|
try {
|
|
32
|
-
const data = await
|
|
27
|
+
const data = await fetchVersion(false);
|
|
33
28
|
if (!active) return;
|
|
34
|
-
|
|
29
|
+
setVersion(data.currentVersion || currentVersion || null);
|
|
35
30
|
setLatestVersion(data.latestVersion || null);
|
|
36
31
|
setHasUpdate(!!data.hasUpdate);
|
|
37
|
-
|
|
32
|
+
setError(data.ok ? "" : data.error || "");
|
|
38
33
|
} catch (err) {
|
|
39
34
|
if (!active) return;
|
|
40
|
-
|
|
35
|
+
setError(err.message || "Could not check updates");
|
|
41
36
|
}
|
|
42
37
|
};
|
|
43
|
-
|
|
44
|
-
return () => {
|
|
45
|
-
active = false;
|
|
46
|
-
};
|
|
38
|
+
load();
|
|
39
|
+
return () => { active = false; };
|
|
47
40
|
}, []);
|
|
48
41
|
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
await restartGateway();
|
|
54
|
-
showToast("Gateway restarted", "success");
|
|
55
|
-
} catch (err) {
|
|
56
|
-
showToast("Restart failed: " + err.message, "error");
|
|
57
|
-
}
|
|
58
|
-
setRestarting(false);
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const handleUpdate = async () => {
|
|
62
|
-
if (checkingUpdate) return;
|
|
63
|
-
setCheckingUpdate(true);
|
|
64
|
-
setUpdateError("");
|
|
42
|
+
const handleAction = async () => {
|
|
43
|
+
if (checking) return;
|
|
44
|
+
setChecking(true);
|
|
45
|
+
setError("");
|
|
65
46
|
try {
|
|
66
47
|
const data = hasUpdate
|
|
67
|
-
? await
|
|
68
|
-
: await
|
|
69
|
-
|
|
48
|
+
? await applyUpdate()
|
|
49
|
+
: await fetchVersion(true);
|
|
50
|
+
setVersion(data.currentVersion || version);
|
|
70
51
|
setLatestVersion(data.latestVersion || null);
|
|
71
52
|
setHasUpdate(!!data.hasUpdate);
|
|
72
|
-
|
|
53
|
+
setError(data.ok ? "" : data.error || "");
|
|
73
54
|
if (hasUpdate) {
|
|
74
55
|
if (!data.ok) {
|
|
75
|
-
showToast(data.error ||
|
|
76
|
-
} else if (data.updated) {
|
|
56
|
+
showToast(data.error || `${label} update failed`, "error");
|
|
57
|
+
} else if (data.updated || data.restarting) {
|
|
77
58
|
showToast(
|
|
78
|
-
data.
|
|
79
|
-
?
|
|
80
|
-
: `Updated to ${data.currentVersion}`,
|
|
59
|
+
data.restarting
|
|
60
|
+
? `${label} updated — restarting...`
|
|
61
|
+
: `Updated ${label} to ${data.currentVersion}`,
|
|
81
62
|
"success",
|
|
82
63
|
);
|
|
83
64
|
} else {
|
|
84
|
-
showToast(
|
|
65
|
+
showToast(`Already at latest ${label} version`, "success");
|
|
85
66
|
}
|
|
86
67
|
} else if (data.hasUpdate && data.latestVersion) {
|
|
87
|
-
showToast(
|
|
68
|
+
showToast(`${label} update available: ${data.latestVersion}`, "warning");
|
|
88
69
|
} else {
|
|
89
|
-
showToast(
|
|
70
|
+
showToast(`${label} is up to date`, "success");
|
|
90
71
|
}
|
|
91
72
|
} catch (err) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
73
|
+
setError(err.message || (hasUpdate ? `Could not update ${label}` : "Could not check updates"));
|
|
74
|
+
showToast(hasUpdate ? `Could not update ${label}` : "Could not check updates", "error");
|
|
75
|
+
}
|
|
76
|
+
setChecking(false);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return html`
|
|
80
|
+
<div class="flex items-center justify-between gap-3">
|
|
81
|
+
<div class="min-w-0">
|
|
82
|
+
<p class="text-sm text-gray-300 truncate">
|
|
83
|
+
<span class="text-gray-500">${label}</span>${" "}v${version || "unknown"}
|
|
84
|
+
</p>
|
|
85
|
+
${error && html`<p class="text-xs text-yellow-500 mt-1">${error}</p>`}
|
|
86
|
+
</div>
|
|
87
|
+
<div class="flex items-center gap-2 shrink-0">
|
|
88
|
+
${hasUpdate && latestVersion && tagsUrl && html`
|
|
89
|
+
<a href=${tagsUrl} target="_blank"
|
|
90
|
+
class="text-xs text-yellow-500 hover:text-yellow-300 transition-colors"
|
|
91
|
+
>${latestVersion} available</a>
|
|
92
|
+
`}
|
|
93
|
+
<button
|
|
94
|
+
onclick=${handleAction}
|
|
95
|
+
disabled=${checking}
|
|
96
|
+
class="text-xs px-2.5 py-1 rounded-lg border border-border text-gray-500 hover:text-gray-300 hover:border-gray-500 transition-colors ${checking ? "opacity-50 cursor-not-allowed" : ""}"
|
|
97
|
+
>
|
|
98
|
+
${checking
|
|
99
|
+
? hasUpdate ? "Updating..." : "Checking..."
|
|
100
|
+
: hasUpdate ? "Update" : "Check updates"}
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function Gateway({ status, openclawVersion }) {
|
|
108
|
+
const [restarting, setRestarting] = useState(false);
|
|
109
|
+
const isRunning = status === "running" && !restarting;
|
|
110
|
+
const dotClass = isRunning
|
|
111
|
+
? "w-2 h-2 rounded-full bg-green-500"
|
|
112
|
+
: "w-2 h-2 rounded-full bg-yellow-500 animate-pulse";
|
|
113
|
+
|
|
114
|
+
const handleRestart = async () => {
|
|
115
|
+
if (restarting) return;
|
|
116
|
+
setRestarting(true);
|
|
117
|
+
try {
|
|
118
|
+
await restartGateway();
|
|
119
|
+
showToast("Gateway restarted", "success");
|
|
120
|
+
} catch (err) {
|
|
121
|
+
showToast("Restart failed: " + err.message, "error");
|
|
100
122
|
}
|
|
101
|
-
|
|
123
|
+
setRestarting(false);
|
|
102
124
|
};
|
|
103
125
|
|
|
104
126
|
return html` <div class="bg-surface border border-border rounded-xl p-4">
|
|
@@ -124,40 +146,13 @@ export function Gateway({ status, openclawVersion }) {
|
|
|
124
146
|
</button>
|
|
125
147
|
</div>
|
|
126
148
|
<div class="mt-3 pt-3 border-t border-border">
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
</div>
|
|
135
|
-
<div class="flex items-center gap-2 shrink-0">
|
|
136
|
-
${hasUpdate &&
|
|
137
|
-
latestVersion &&
|
|
138
|
-
html`<a
|
|
139
|
-
href="https://github.com/openclaw/openclaw/tags"
|
|
140
|
-
target="_blank"
|
|
141
|
-
class="text-xs text-yellow-500 hover:text-yellow-300 transition-colors"
|
|
142
|
-
>${latestVersion} available</a
|
|
143
|
-
>`}
|
|
144
|
-
<button
|
|
145
|
-
onclick=${handleUpdate}
|
|
146
|
-
disabled=${checkingUpdate}
|
|
147
|
-
class="text-xs px-2.5 py-1 rounded-lg border border-border text-gray-500 hover:text-gray-300 hover:border-gray-500 transition-colors ${checkingUpdate
|
|
148
|
-
? "opacity-50 cursor-not-allowed"
|
|
149
|
-
: ""}"
|
|
150
|
-
>
|
|
151
|
-
${checkingUpdate
|
|
152
|
-
? hasUpdate
|
|
153
|
-
? "Updating..."
|
|
154
|
-
: "Checking..."
|
|
155
|
-
: hasUpdate
|
|
156
|
-
? "Update"
|
|
157
|
-
: "Check updates"}
|
|
158
|
-
</button>
|
|
159
|
-
</div>
|
|
160
|
-
</div>
|
|
149
|
+
<${VersionRow}
|
|
150
|
+
label="OpenClaw"
|
|
151
|
+
currentVersion=${openclawVersion}
|
|
152
|
+
fetchVersion=${fetchOpenclawVersion}
|
|
153
|
+
applyUpdate=${updateOpenclaw}
|
|
154
|
+
tagsUrl="https://github.com/openclaw/openclaw/tags"
|
|
155
|
+
/>
|
|
161
156
|
</div>
|
|
162
157
|
</div>`;
|
|
163
158
|
}
|
package/lib/public/js/lib/api.js
CHANGED
|
@@ -80,6 +80,17 @@ export async function updateOpenclaw() {
|
|
|
80
80
|
return res.json();
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
export async function fetchAlphaclawVersion(refresh = false) {
|
|
84
|
+
const query = refresh ? '?refresh=1' : '';
|
|
85
|
+
const res = await authFetch(`/api/alphaclaw/version${query}`);
|
|
86
|
+
return res.json();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function updateAlphaclaw() {
|
|
90
|
+
const res = await authFetch('/api/alphaclaw/update', { method: 'POST' });
|
|
91
|
+
return res.json();
|
|
92
|
+
}
|
|
93
|
+
|
|
83
94
|
export async function fetchSyncCron() {
|
|
84
95
|
const res = await authFetch('/api/sync-cron');
|
|
85
96
|
const text = await res.text();
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
const { exec } = require("child_process");
|
|
2
|
+
const { spawn } = require("child_process");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const https = require("https");
|
|
6
|
+
const http = require("http");
|
|
7
|
+
const {
|
|
8
|
+
kLatestVersionCacheTtlMs,
|
|
9
|
+
kAlphaclawRegistryUrl,
|
|
10
|
+
kPackageRoot,
|
|
11
|
+
} = require("./constants");
|
|
12
|
+
|
|
13
|
+
// kPackageRoot is lib/ — the actual npm package root (with package.json) is one level up
|
|
14
|
+
const kNpmPackageRoot = path.resolve(kPackageRoot, "..");
|
|
15
|
+
|
|
16
|
+
const createAlphaclawVersionService = () => {
|
|
17
|
+
let kUpdateStatusCache = { latestVersion: null, hasUpdate: false, fetchedAt: 0 };
|
|
18
|
+
let kUpdateInProgress = false;
|
|
19
|
+
|
|
20
|
+
const readAlphaclawVersion = () => {
|
|
21
|
+
try {
|
|
22
|
+
const pkg = JSON.parse(
|
|
23
|
+
fs.readFileSync(path.join(kNpmPackageRoot, "package.json"), "utf8"),
|
|
24
|
+
);
|
|
25
|
+
return pkg.version || null;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const fetchLatestVersionFromRegistry = () =>
|
|
32
|
+
new Promise((resolve, reject) => {
|
|
33
|
+
const get = kAlphaclawRegistryUrl.startsWith("https") ? https.get : http.get;
|
|
34
|
+
get(kAlphaclawRegistryUrl, { headers: { Accept: "application/json" } }, (res) => {
|
|
35
|
+
let data = "";
|
|
36
|
+
res.on("data", (chunk) => { data += chunk; });
|
|
37
|
+
res.on("end", () => {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(data);
|
|
40
|
+
resolve(parsed["dist-tags"]?.latest || null);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
reject(new Error("Failed to parse registry response"));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}).on("error", reject);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const readAlphaclawUpdateStatus = async ({ refresh = false } = {}) => {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
if (
|
|
51
|
+
!refresh &&
|
|
52
|
+
kUpdateStatusCache.fetchedAt &&
|
|
53
|
+
now - kUpdateStatusCache.fetchedAt < kLatestVersionCacheTtlMs
|
|
54
|
+
) {
|
|
55
|
+
return {
|
|
56
|
+
latestVersion: kUpdateStatusCache.latestVersion,
|
|
57
|
+
hasUpdate: kUpdateStatusCache.hasUpdate,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const currentVersion = readAlphaclawVersion();
|
|
61
|
+
const latestVersion = await fetchLatestVersionFromRegistry();
|
|
62
|
+
const hasUpdate = !!(currentVersion && latestVersion && latestVersion !== currentVersion);
|
|
63
|
+
kUpdateStatusCache = { latestVersion, hasUpdate, fetchedAt: Date.now() };
|
|
64
|
+
console.log(
|
|
65
|
+
`[alphaclaw] alphaclaw update status: hasUpdate=${hasUpdate} current=${currentVersion} latest=${latestVersion || "unknown"}`,
|
|
66
|
+
);
|
|
67
|
+
return { latestVersion, hasUpdate };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const findInstallDir = () => {
|
|
71
|
+
// Walk up from kNpmPackageRoot to find the consuming project's directory
|
|
72
|
+
// (the one with node_modules/@chrysb/alphaclaw). In Docker this is /app.
|
|
73
|
+
let dir = kNpmPackageRoot;
|
|
74
|
+
while (dir !== path.dirname(dir)) {
|
|
75
|
+
const parent = path.dirname(dir);
|
|
76
|
+
if (path.basename(parent) === "node_modules" || parent.includes("node_modules")) {
|
|
77
|
+
dir = parent;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const pkgPath = path.join(parent, "package.json");
|
|
81
|
+
if (fs.existsSync(pkgPath)) {
|
|
82
|
+
try {
|
|
83
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
84
|
+
if (pkg.dependencies?.["@chrysb/alphaclaw"]) {
|
|
85
|
+
return parent;
|
|
86
|
+
}
|
|
87
|
+
} catch {}
|
|
88
|
+
}
|
|
89
|
+
dir = parent;
|
|
90
|
+
}
|
|
91
|
+
// Fallback: if running directly (not from node_modules), use kNpmPackageRoot
|
|
92
|
+
return kNpmPackageRoot;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const installLatestAlphaclaw = () =>
|
|
96
|
+
new Promise((resolve, reject) => {
|
|
97
|
+
const installDir = findInstallDir();
|
|
98
|
+
console.log(`[alphaclaw] Running: npm install @chrysb/alphaclaw@latest (cwd: ${installDir})`);
|
|
99
|
+
exec(
|
|
100
|
+
"npm install @chrysb/alphaclaw@latest --omit=dev --no-save --package-lock=false",
|
|
101
|
+
{
|
|
102
|
+
cwd: installDir,
|
|
103
|
+
env: {
|
|
104
|
+
...process.env,
|
|
105
|
+
npm_config_update_notifier: "false",
|
|
106
|
+
npm_config_fund: "false",
|
|
107
|
+
npm_config_audit: "false",
|
|
108
|
+
},
|
|
109
|
+
timeout: 180000,
|
|
110
|
+
},
|
|
111
|
+
(err, stdout, stderr) => {
|
|
112
|
+
if (err) {
|
|
113
|
+
const message = String(stderr || err.message || "").trim();
|
|
114
|
+
console.log(`[alphaclaw] alphaclaw install error: ${message.slice(0, 200)}`);
|
|
115
|
+
return reject(new Error(message || "Failed to install @chrysb/alphaclaw@latest"));
|
|
116
|
+
}
|
|
117
|
+
if (stdout?.trim()) {
|
|
118
|
+
console.log(`[alphaclaw] alphaclaw install stdout: ${stdout.trim().slice(0, 300)}`);
|
|
119
|
+
}
|
|
120
|
+
console.log("[alphaclaw] alphaclaw install completed");
|
|
121
|
+
resolve({ stdout: stdout?.trim(), stderr: stderr?.trim() });
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const restartProcess = () => {
|
|
127
|
+
console.log("[alphaclaw] Restarting process...");
|
|
128
|
+
const child = spawn(process.argv[0], process.argv.slice(1), {
|
|
129
|
+
detached: true,
|
|
130
|
+
stdio: "inherit",
|
|
131
|
+
});
|
|
132
|
+
child.unref();
|
|
133
|
+
process.exit(0);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const getVersionStatus = async (refresh) => {
|
|
137
|
+
const currentVersion = readAlphaclawVersion();
|
|
138
|
+
try {
|
|
139
|
+
const { latestVersion, hasUpdate } = await readAlphaclawUpdateStatus({ refresh });
|
|
140
|
+
return { ok: true, currentVersion, latestVersion, hasUpdate };
|
|
141
|
+
} catch (err) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
currentVersion,
|
|
145
|
+
latestVersion: kUpdateStatusCache.latestVersion,
|
|
146
|
+
hasUpdate: kUpdateStatusCache.hasUpdate,
|
|
147
|
+
error: err.message || "Failed to fetch latest AlphaClaw version",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const updateAlphaclaw = async () => {
|
|
153
|
+
if (kUpdateInProgress) {
|
|
154
|
+
return {
|
|
155
|
+
status: 409,
|
|
156
|
+
body: { ok: false, error: "AlphaClaw update already in progress" },
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
kUpdateInProgress = true;
|
|
161
|
+
const previousVersion = readAlphaclawVersion();
|
|
162
|
+
try {
|
|
163
|
+
await installLatestAlphaclaw();
|
|
164
|
+
kUpdateStatusCache = { latestVersion: null, hasUpdate: false, fetchedAt: 0 };
|
|
165
|
+
return {
|
|
166
|
+
status: 200,
|
|
167
|
+
body: {
|
|
168
|
+
ok: true,
|
|
169
|
+
previousVersion,
|
|
170
|
+
restarting: true,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
} catch (err) {
|
|
174
|
+
kUpdateInProgress = false;
|
|
175
|
+
return {
|
|
176
|
+
status: 500,
|
|
177
|
+
body: { ok: false, error: err.message || "Failed to update AlphaClaw" },
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
readAlphaclawVersion,
|
|
184
|
+
getVersionStatus,
|
|
185
|
+
updateAlphaclaw,
|
|
186
|
+
restartProcess,
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
module.exports = { createAlphaclawVersionService };
|
package/lib/server/constants.js
CHANGED
|
@@ -106,6 +106,7 @@ const kFallbackOnboardingModels = [
|
|
|
106
106
|
const kVersionCacheTtlMs = 60 * 1000;
|
|
107
107
|
const kLatestVersionCacheTtlMs = 10 * 60 * 1000;
|
|
108
108
|
const kOpenclawRegistryUrl = "https://registry.npmjs.org/openclaw";
|
|
109
|
+
const kAlphaclawRegistryUrl = "https://registry.npmjs.org/@chrysb%2falphaclaw";
|
|
109
110
|
const kAppDir = kPackageRoot;
|
|
110
111
|
|
|
111
112
|
const kSystemVars = new Set([
|
|
@@ -265,6 +266,7 @@ module.exports = {
|
|
|
265
266
|
kVersionCacheTtlMs,
|
|
266
267
|
kLatestVersionCacheTtlMs,
|
|
267
268
|
kOpenclawRegistryUrl,
|
|
269
|
+
kAlphaclawRegistryUrl,
|
|
268
270
|
kAppDir,
|
|
269
271
|
kSystemVars,
|
|
270
272
|
kKnownVars,
|
|
@@ -12,6 +12,7 @@ const registerSystemRoutes = ({
|
|
|
12
12
|
isOnboarded,
|
|
13
13
|
getChannelStatus,
|
|
14
14
|
openclawVersionService,
|
|
15
|
+
alphaclawVersionService,
|
|
15
16
|
clawCmd,
|
|
16
17
|
restartGateway,
|
|
17
18
|
OPENCLAW_DIR,
|
|
@@ -183,6 +184,26 @@ const registerSystemRoutes = ({
|
|
|
183
184
|
res.status(result.status).json(result.body);
|
|
184
185
|
});
|
|
185
186
|
|
|
187
|
+
app.get("/api/alphaclaw/version", async (req, res) => {
|
|
188
|
+
const refresh = String(req.query.refresh || "") === "1";
|
|
189
|
+
const status = await alphaclawVersionService.getVersionStatus(refresh);
|
|
190
|
+
res.json(status);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
app.post("/api/alphaclaw/update", async (req, res) => {
|
|
194
|
+
console.log("[alphaclaw] /api/alphaclaw/update requested");
|
|
195
|
+
const result = await alphaclawVersionService.updateAlphaclaw();
|
|
196
|
+
console.log(
|
|
197
|
+
`[alphaclaw] /api/alphaclaw/update result: status=${result.status} ok=${result.body?.ok === true}`,
|
|
198
|
+
);
|
|
199
|
+
if (result.status === 200 && result.body?.ok) {
|
|
200
|
+
res.json(result.body);
|
|
201
|
+
setTimeout(() => alphaclawVersionService.restartProcess(), 1000);
|
|
202
|
+
} else {
|
|
203
|
+
res.status(result.status).json(result.body);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
186
207
|
app.get("/api/gateway-status", async (req, res) => {
|
|
187
208
|
const result = await clawCmd("status");
|
|
188
209
|
res.json(result);
|
package/lib/server.js
CHANGED
|
@@ -34,6 +34,7 @@ const { createCommands } = require("./server/commands");
|
|
|
34
34
|
const { createAuthProfiles } = require("./server/auth-profiles");
|
|
35
35
|
const { createLoginThrottle } = require("./server/login-throttle");
|
|
36
36
|
const { createOpenclawVersionService } = require("./server/openclaw-version");
|
|
37
|
+
const { createAlphaclawVersionService } = require("./server/alphaclaw-version");
|
|
37
38
|
const { syncBootstrapPromptFiles } = require("./server/onboarding/workspace");
|
|
38
39
|
|
|
39
40
|
const { registerAuthRoutes } = require("./server/routes/auth");
|
|
@@ -76,6 +77,7 @@ const openclawVersionService = createOpenclawVersionService({
|
|
|
76
77
|
restartGateway,
|
|
77
78
|
isOnboarded,
|
|
78
79
|
});
|
|
80
|
+
const alphaclawVersionService = createAlphaclawVersionService();
|
|
79
81
|
|
|
80
82
|
const { requireAuth } = registerAuthRoutes({ app, loginThrottle });
|
|
81
83
|
app.use(express.static(path.join(__dirname, "public")));
|
|
@@ -118,6 +120,7 @@ registerSystemRoutes({
|
|
|
118
120
|
isOnboarded,
|
|
119
121
|
getChannelStatus,
|
|
120
122
|
openclawVersionService,
|
|
123
|
+
alphaclawVersionService,
|
|
121
124
|
clawCmd,
|
|
122
125
|
restartGateway,
|
|
123
126
|
OPENCLAW_DIR: constants.OPENCLAW_DIR,
|