@chrysb/alphaclaw 0.9.0-beta.6 → 0.9.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/bin/alphaclaw.js +25 -25
- package/lib/cli/git-runtime.js +97 -0
- package/lib/public/css/chat.css +0 -12
- package/lib/public/css/explorer.css +48 -0
- package/lib/public/css/shell.css +149 -0
- package/lib/public/css/tailwind.generated.css +1 -1
- package/lib/public/css/theme.css +265 -0
- package/lib/public/dist/app.bundle.js +2770 -2762
- package/lib/public/js/app.js +26 -14
- package/lib/public/js/components/agents-tab/create-channel-modal.js +259 -59
- package/lib/public/js/components/gateway.js +0 -286
- package/lib/public/js/components/general/index.js +0 -7
- package/lib/public/js/components/icons.js +26 -25
- package/lib/public/js/components/modal-shell.js +1 -1
- package/lib/public/js/components/models-tab/provider-auth-card.js +60 -49
- package/lib/public/js/components/models-tab/use-models.js +74 -9
- package/lib/public/js/components/models.js +52 -37
- package/lib/public/js/components/onboarding/use-welcome-codex.js +34 -24
- package/lib/public/js/components/onboarding/welcome-config.js +76 -10
- package/lib/public/js/components/onboarding/welcome-form-step.js +2 -7
- package/lib/public/js/components/onboarding/welcome-header.js +12 -14
- package/lib/public/js/components/onboarding/welcome-setup-step.js +3 -3
- package/lib/public/js/components/providers.js +53 -42
- package/lib/public/js/components/routes/chat-route.js +2 -9
- package/lib/public/js/components/routes/general-route.js +0 -6
- package/lib/public/js/components/routes/index.js +0 -1
- package/lib/public/js/components/routes/watchdog-route.js +0 -6
- package/lib/public/js/components/sidebar.js +21 -7
- package/lib/public/js/components/theme-toggle.js +113 -0
- package/lib/public/js/components/update-modal.js +174 -51
- package/lib/public/js/components/watchdog-tab/index.js +0 -6
- package/lib/public/js/components/welcome/index.js +0 -2
- package/lib/public/js/components/welcome/use-welcome.js +101 -36
- package/lib/public/js/hooks/use-app-shell-controller.js +16 -33
- package/lib/public/js/lib/api.js +0 -28
- package/lib/public/js/lib/app-navigation.js +0 -2
- package/lib/public/js/lib/channel-provider-availability.js +1 -2
- package/lib/public/js/lib/codex-oauth-window.js +22 -0
- package/lib/public/js/lib/model-catalog.js +20 -0
- package/lib/public/js/lib/storage-keys.js +1 -1
- package/lib/public/login.html +8 -4
- package/lib/public/setup.html +9 -0
- package/lib/scripts/git +47 -1
- package/lib/server/agents/channels.js +1 -4
- package/lib/server/alphaclaw-version.js +590 -132
- package/lib/server/constants.js +5 -0
- package/lib/server/db/webhooks/index.js +48 -8
- package/lib/server/exec-defaults-config.js +163 -0
- package/lib/server/init/register-server-routes.js +0 -8
- package/lib/server/init/server-lifecycle.js +2 -0
- package/lib/server/model-catalog-cache.js +251 -0
- package/lib/server/onboarding/index.js +5 -0
- package/lib/server/routes/models.js +14 -23
- package/lib/server/routes/nodes.js +9 -23
- package/lib/server/routes/system.js +3 -16
- package/lib/server/routes/webhooks.js +12 -1
- package/lib/server/startup.js +8 -0
- package/lib/server/watchdog-notify.js +172 -55
- package/lib/server.js +17 -2
- package/package.json +2 -2
- package/patches/openclaw+2026.4.9.patch +13 -0
- package/lib/public/js/components/mcp-tab/index.js +0 -237
- package/lib/public/js/components/routes/mcp-route.js +0 -7
- package/lib/server/mcp-bridge.js +0 -158
- package/lib/server/routes/mcp.js +0 -252
- package/patches/openclaw+2026.3.28.patch +0 -13
|
@@ -1,155 +1,470 @@
|
|
|
1
1
|
const childProcess = require("child_process");
|
|
2
2
|
const fs = require("fs");
|
|
3
|
+
const os = require("os");
|
|
3
4
|
const path = require("path");
|
|
4
|
-
const https = require("https");
|
|
5
|
-
const http = require("http");
|
|
6
5
|
const {
|
|
7
6
|
kLatestVersionCacheTtlMs,
|
|
8
7
|
kAlphaclawRegistryUrl,
|
|
9
8
|
kNpmPackageRoot,
|
|
9
|
+
kOpenclawUpdateCopyTimeoutMs,
|
|
10
10
|
kRootDir,
|
|
11
11
|
} = require("./constants");
|
|
12
|
+
const {
|
|
13
|
+
compareVersionParts,
|
|
14
|
+
normalizeOpenclawVersion,
|
|
15
|
+
resolveGithubRepoUrl,
|
|
16
|
+
} = require("./helpers");
|
|
17
|
+
|
|
18
|
+
const kGithubApiBaseUrl = "https://api.github.com/repos";
|
|
19
|
+
const kGithubRawBaseUrl = "https://raw.githubusercontent.com";
|
|
20
|
+
const kDefaultTemplateBranch = "main";
|
|
21
|
+
const kRailwayTemplateRepoUrl =
|
|
22
|
+
"https://github.com/chrysb/openclaw-railway-template.git";
|
|
23
|
+
const kRenderTemplateRepoUrl =
|
|
24
|
+
"https://github.com/chrysb/openclaw-render-template.git";
|
|
25
|
+
const kApexTemplateRepoUrl =
|
|
26
|
+
"https://github.com/chrysb/openclaw-apex-template.git";
|
|
12
27
|
|
|
13
28
|
const isNewerVersion = (latest, current) => {
|
|
14
29
|
if (!latest || !current) return false;
|
|
15
|
-
const parse = (
|
|
16
|
-
const
|
|
30
|
+
const parse = (value) => {
|
|
31
|
+
const normalized = String(value || "").replace(/^v/, "").trim();
|
|
32
|
+
const [core, prerelease = ""] = normalized.split("-", 2);
|
|
17
33
|
const parts = core.split(".").map(Number);
|
|
18
|
-
return {
|
|
34
|
+
return {
|
|
35
|
+
major: parts[0] || 0,
|
|
36
|
+
minor: parts[1] || 0,
|
|
37
|
+
patch: parts[2] || 0,
|
|
38
|
+
prerelease,
|
|
39
|
+
};
|
|
19
40
|
};
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
|
|
41
|
+
const latestParts = parse(latest);
|
|
42
|
+
const currentParts = parse(current);
|
|
43
|
+
if (latestParts.major !== currentParts.major) {
|
|
44
|
+
return latestParts.major > currentParts.major;
|
|
45
|
+
}
|
|
46
|
+
if (latestParts.minor !== currentParts.minor) {
|
|
47
|
+
return latestParts.minor > currentParts.minor;
|
|
48
|
+
}
|
|
49
|
+
if (latestParts.patch !== currentParts.patch) {
|
|
50
|
+
return latestParts.patch > currentParts.patch;
|
|
51
|
+
}
|
|
52
|
+
if (!latestParts.prerelease && currentParts.prerelease) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const normalizeVersion = (value) => {
|
|
59
|
+
const normalized = String(value || "").trim();
|
|
60
|
+
return normalized || null;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const parseJsonResponse = async (response, fallbackMessage) => {
|
|
64
|
+
const text = await response.text();
|
|
65
|
+
let data = {};
|
|
66
|
+
try {
|
|
67
|
+
data = text ? JSON.parse(text) : {};
|
|
68
|
+
} catch {
|
|
69
|
+
throw new Error(text || fallbackMessage);
|
|
70
|
+
}
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
data?.message ||
|
|
74
|
+
data?.error ||
|
|
75
|
+
text ||
|
|
76
|
+
`${fallbackMessage} (${response.status})`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return data;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const buildGithubHeaders = ({ env = process.env, accept = "application/json" } = {}) => {
|
|
83
|
+
const headers = {
|
|
84
|
+
Accept: accept,
|
|
85
|
+
"User-Agent": "alphaclaw",
|
|
86
|
+
};
|
|
87
|
+
const token = String(env?.GITHUB_TOKEN || env?.GH_TOKEN || "").trim();
|
|
88
|
+
if (token) {
|
|
89
|
+
headers.Authorization = `Bearer ${token}`;
|
|
90
|
+
}
|
|
91
|
+
return headers;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const extractTemplateVersions = (pkg) => ({
|
|
95
|
+
latestVersion: normalizeVersion(pkg?.dependencies?.["@chrysb/alphaclaw"]),
|
|
96
|
+
latestOpenclawVersion: normalizeOpenclawVersion(pkg?.dependencies?.openclaw),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const fetchLatestVersionFromRegistry = async ({ fetchImpl }) => {
|
|
100
|
+
if (typeof fetchImpl !== "function") {
|
|
101
|
+
throw new Error("Fetch is not available for AlphaClaw version checks");
|
|
102
|
+
}
|
|
103
|
+
const response = await fetchImpl(kAlphaclawRegistryUrl, {
|
|
104
|
+
headers: buildGithubHeaders({
|
|
105
|
+
accept: "application/vnd.npm.install-v1+json",
|
|
106
|
+
}),
|
|
107
|
+
});
|
|
108
|
+
const data = await parseJsonResponse(
|
|
109
|
+
response,
|
|
110
|
+
"Failed to fetch latest AlphaClaw version",
|
|
111
|
+
);
|
|
112
|
+
return normalizeVersion(data?.["dist-tags"]?.latest);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const fetchTemplatePackageVersions = async ({
|
|
116
|
+
fetchImpl,
|
|
117
|
+
repoUrl,
|
|
118
|
+
branch = kDefaultTemplateBranch,
|
|
119
|
+
}) => {
|
|
120
|
+
if (typeof fetchImpl !== "function") {
|
|
121
|
+
throw new Error("Fetch is not available for template version checks");
|
|
122
|
+
}
|
|
123
|
+
const repoPath = resolveGithubRepoUrl(repoUrl);
|
|
124
|
+
if (!repoPath) {
|
|
125
|
+
throw new Error("Template repository is not configured");
|
|
126
|
+
}
|
|
127
|
+
const response = await fetchImpl(
|
|
128
|
+
`${kGithubRawBaseUrl}/${repoPath}/${encodeURIComponent(branch)}/package.json`,
|
|
129
|
+
{
|
|
130
|
+
headers: buildGithubHeaders(),
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
const data = await parseJsonResponse(
|
|
134
|
+
response,
|
|
135
|
+
"Could not fetch the deployment template metadata",
|
|
136
|
+
);
|
|
137
|
+
return extractTemplateVersions(data);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const fetchTemplateHeadRef = async ({
|
|
141
|
+
fetchImpl,
|
|
142
|
+
repoUrl,
|
|
143
|
+
branch = kDefaultTemplateBranch,
|
|
144
|
+
env = process.env,
|
|
145
|
+
}) => {
|
|
146
|
+
if (typeof fetchImpl !== "function") {
|
|
147
|
+
throw new Error("Fetch is not available for template update requests");
|
|
148
|
+
}
|
|
149
|
+
const repoPath = resolveGithubRepoUrl(repoUrl);
|
|
150
|
+
if (!repoPath) {
|
|
151
|
+
throw new Error("Template repository is not configured");
|
|
152
|
+
}
|
|
153
|
+
const response = await fetchImpl(
|
|
154
|
+
`${kGithubApiBaseUrl}/${repoPath}/commits/${encodeURIComponent(branch)}`,
|
|
155
|
+
{
|
|
156
|
+
headers: buildGithubHeaders({
|
|
157
|
+
env,
|
|
158
|
+
accept: "application/vnd.github+json",
|
|
159
|
+
}),
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
const data = await parseJsonResponse(
|
|
163
|
+
response,
|
|
164
|
+
"Could not fetch the deployment template metadata",
|
|
165
|
+
);
|
|
166
|
+
return normalizeVersion(data?.sha);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const createUpdateStrategy = ({
|
|
170
|
+
action,
|
|
171
|
+
provider,
|
|
172
|
+
label,
|
|
173
|
+
templateRepoUrl = "",
|
|
174
|
+
templateBranch = kDefaultTemplateBranch,
|
|
175
|
+
description,
|
|
176
|
+
steps = [],
|
|
177
|
+
primaryActionLabel,
|
|
178
|
+
primaryActionUrl = "",
|
|
179
|
+
managedUpdateUrl = "",
|
|
180
|
+
managedUpdateToken = "",
|
|
181
|
+
}) => ({
|
|
182
|
+
action,
|
|
183
|
+
provider,
|
|
184
|
+
label,
|
|
185
|
+
templateRepoUrl,
|
|
186
|
+
templateBranch,
|
|
187
|
+
description: String(description || "").trim(),
|
|
188
|
+
steps: Array.isArray(steps)
|
|
189
|
+
? steps.map((entry) => String(entry || "").trim()).filter(Boolean)
|
|
190
|
+
: [],
|
|
191
|
+
primaryActionLabel: String(primaryActionLabel || "").trim() || "Update now",
|
|
192
|
+
primaryActionUrl: String(primaryActionUrl || "").trim(),
|
|
193
|
+
managedUpdateUrl: String(managedUpdateUrl || "").trim(),
|
|
194
|
+
managedUpdateToken: String(managedUpdateToken || "").trim(),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const buildRailwayDeploymentUrl = (env = process.env) => {
|
|
198
|
+
const projectId = String(env.RAILWAY_PROJECT_ID || "").trim();
|
|
199
|
+
const serviceId = String(env.RAILWAY_SERVICE_ID || "").trim();
|
|
200
|
+
const environmentId = String(env.RAILWAY_ENVIRONMENT_ID || "").trim();
|
|
201
|
+
if (!projectId) return "";
|
|
202
|
+
const baseUrl = serviceId
|
|
203
|
+
? `https://railway.com/project/${projectId}/service/${serviceId}`
|
|
204
|
+
: `https://railway.com/project/${projectId}`;
|
|
205
|
+
return environmentId
|
|
206
|
+
? `${baseUrl}?environmentId=${encodeURIComponent(environmentId)}`
|
|
207
|
+
: baseUrl;
|
|
25
208
|
};
|
|
26
209
|
|
|
27
|
-
const
|
|
28
|
-
|
|
210
|
+
const buildRenderDeploymentUrl = (env = process.env) => {
|
|
211
|
+
const serviceId = String(env.RENDER_SERVICE_ID || "").trim();
|
|
212
|
+
if (!serviceId) return "";
|
|
213
|
+
return `https://dashboard.render.com/web/${encodeURIComponent(serviceId)}`;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const detectUpdateStrategy = ({
|
|
217
|
+
env = process.env,
|
|
218
|
+
fsImpl = fs,
|
|
219
|
+
} = {}) => {
|
|
220
|
+
const deploymentProvider = String(env.ALPHACLAW_DEPLOYMENT_PROVIDER || "")
|
|
221
|
+
.trim()
|
|
222
|
+
.toLowerCase();
|
|
223
|
+
const managedUpdateUrl = String(env.ALPHACLAW_MANAGED_UPDATE_URL || "").trim();
|
|
224
|
+
const managedUpdateToken = String(
|
|
225
|
+
env.ALPHACLAW_MANAGED_UPDATE_TOKEN || "",
|
|
226
|
+
).trim();
|
|
227
|
+
const managedTemplateRepoUrl =
|
|
228
|
+
String(env.ALPHACLAW_TEMPLATE_REPO_URL || "").trim() || kApexTemplateRepoUrl;
|
|
229
|
+
const managedTemplateBranch =
|
|
230
|
+
String(env.ALPHACLAW_TEMPLATE_BRANCH || "").trim() || kDefaultTemplateBranch;
|
|
231
|
+
|
|
232
|
+
if (deploymentProvider === "apex" && managedUpdateUrl && managedUpdateToken) {
|
|
233
|
+
return createUpdateStrategy({
|
|
234
|
+
action: "managed-update",
|
|
235
|
+
provider: "apex",
|
|
236
|
+
label: "Apex",
|
|
237
|
+
templateRepoUrl: managedTemplateRepoUrl,
|
|
238
|
+
templateBranch: managedTemplateBranch,
|
|
239
|
+
primaryActionLabel: "Update now",
|
|
240
|
+
managedUpdateUrl,
|
|
241
|
+
managedUpdateToken,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (deploymentProvider === "apex") {
|
|
246
|
+
return createUpdateStrategy({
|
|
247
|
+
action: "instructions",
|
|
248
|
+
provider: "apex",
|
|
249
|
+
label: "Apex",
|
|
250
|
+
templateRepoUrl: managedTemplateRepoUrl,
|
|
251
|
+
templateBranch: managedTemplateBranch,
|
|
252
|
+
description:
|
|
253
|
+
"This Apex deployment must be migrated to the managed updater before one-click updates are available.",
|
|
254
|
+
primaryActionLabel: "Done",
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (managedUpdateUrl && managedUpdateToken) {
|
|
259
|
+
return createUpdateStrategy({
|
|
260
|
+
action: "managed-update",
|
|
261
|
+
provider: "apex",
|
|
262
|
+
label: "Apex",
|
|
263
|
+
templateRepoUrl: managedTemplateRepoUrl,
|
|
264
|
+
templateBranch: managedTemplateBranch,
|
|
265
|
+
primaryActionLabel: "Update now",
|
|
266
|
+
managedUpdateUrl,
|
|
267
|
+
managedUpdateToken,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (
|
|
272
|
+
env.RAILWAY_ENVIRONMENT ||
|
|
273
|
+
env.RAILWAY_PUBLIC_DOMAIN ||
|
|
274
|
+
env.RAILWAY_STATIC_URL
|
|
275
|
+
) {
|
|
276
|
+
const railwayDeploymentUrl = buildRailwayDeploymentUrl(env);
|
|
277
|
+
return createUpdateStrategy({
|
|
278
|
+
action: "instructions",
|
|
279
|
+
provider: "railway",
|
|
280
|
+
label: "Railway",
|
|
281
|
+
templateRepoUrl: kRailwayTemplateRepoUrl,
|
|
282
|
+
description:
|
|
283
|
+
"Railway deployments update by syncing the latest template repo changes and redeploying the service.",
|
|
284
|
+
steps: [
|
|
285
|
+
"Open your Railway project and select the AlphaClaw service",
|
|
286
|
+
"Update the upstream template/source repo to the latest commit on main",
|
|
287
|
+
"Redeploy the service so AlphaClaw and OpenClaw update together",
|
|
288
|
+
],
|
|
289
|
+
primaryActionLabel: railwayDeploymentUrl ? "Update on Railway" : "Done",
|
|
290
|
+
primaryActionUrl: railwayDeploymentUrl,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (env.RENDER || env.RENDER_EXTERNAL_URL) {
|
|
295
|
+
const renderDeploymentUrl = buildRenderDeploymentUrl(env);
|
|
296
|
+
return createUpdateStrategy({
|
|
297
|
+
action: "instructions",
|
|
298
|
+
provider: "render",
|
|
299
|
+
label: "Render",
|
|
300
|
+
templateRepoUrl: kRenderTemplateRepoUrl,
|
|
301
|
+
description:
|
|
302
|
+
"Render deployments update by deploying the latest template commit.",
|
|
303
|
+
steps: [
|
|
304
|
+
"Open your Render service for this AlphaClaw deployment",
|
|
305
|
+
"Click the arrow next to Manual Deploy",
|
|
306
|
+
'Choose "Deploy latest commit"',
|
|
307
|
+
],
|
|
308
|
+
primaryActionLabel: renderDeploymentUrl ? "Update on Render" : "Done",
|
|
309
|
+
primaryActionUrl: renderDeploymentUrl,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (fsImpl.existsSync("/.dockerenv")) {
|
|
314
|
+
return createUpdateStrategy({
|
|
315
|
+
action: "instructions",
|
|
316
|
+
provider: "container",
|
|
317
|
+
label: "Container",
|
|
318
|
+
description:
|
|
319
|
+
"This AlphaClaw instance is running in a container. Rebuild or redeploy the container from the latest deployment template or image to apply updates.",
|
|
320
|
+
steps: [
|
|
321
|
+
"Pull the latest deployment template or image for this container",
|
|
322
|
+
"Rebuild or redeploy the container with the updated bundle",
|
|
323
|
+
"Restart the service after the new build is ready",
|
|
324
|
+
],
|
|
325
|
+
primaryActionLabel: "Done",
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return createUpdateStrategy({
|
|
330
|
+
action: "self-update",
|
|
331
|
+
provider: "self-hosted",
|
|
332
|
+
label: "This install",
|
|
333
|
+
description:
|
|
334
|
+
"This will install the latest @chrysb/alphaclaw package in place and restart AlphaClaw.",
|
|
335
|
+
steps: [
|
|
336
|
+
"AlphaClaw will install the latest published package in place",
|
|
337
|
+
"The process will restart after the new files are copied into node_modules",
|
|
338
|
+
],
|
|
339
|
+
primaryActionLabel: "Update now",
|
|
340
|
+
});
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const createAlphaclawVersionService = ({
|
|
344
|
+
readOpenclawVersion = () => null,
|
|
345
|
+
env = process.env,
|
|
346
|
+
fsImpl = fs,
|
|
347
|
+
fetchImpl = global.fetch,
|
|
348
|
+
} = {}) => {
|
|
349
|
+
let kRegistryStatusCache = {
|
|
29
350
|
latestVersion: null,
|
|
30
|
-
hasUpdate: false,
|
|
31
351
|
fetchedAt: 0,
|
|
32
352
|
};
|
|
353
|
+
const kTemplateStatusCache = new Map();
|
|
33
354
|
let kUpdateInProgress = false;
|
|
34
355
|
|
|
35
356
|
const readAlphaclawVersion = () => {
|
|
36
357
|
try {
|
|
37
358
|
const pkg = JSON.parse(
|
|
38
|
-
|
|
359
|
+
fsImpl.readFileSync(path.join(kNpmPackageRoot, "package.json"), "utf8"),
|
|
39
360
|
);
|
|
40
|
-
return pkg.version
|
|
361
|
+
return normalizeVersion(pkg.version);
|
|
41
362
|
} catch {
|
|
42
363
|
return null;
|
|
43
364
|
}
|
|
44
365
|
};
|
|
45
366
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
let data = "";
|
|
64
|
-
res.on("data", (chunk) => {
|
|
65
|
-
data += chunk;
|
|
66
|
-
});
|
|
67
|
-
res.on("end", () => {
|
|
68
|
-
try {
|
|
69
|
-
const parsed = JSON.parse(data);
|
|
70
|
-
resolve(parsed["dist-tags"]?.latest || null);
|
|
71
|
-
} catch (e) {
|
|
72
|
-
reject(
|
|
73
|
-
new Error(
|
|
74
|
-
`Failed to parse registry response (status ${res.statusCode})`,
|
|
75
|
-
),
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
},
|
|
80
|
-
).on("error", reject);
|
|
81
|
-
};
|
|
82
|
-
doGet(kAlphaclawRegistryUrl);
|
|
367
|
+
const readTemplateStatus = async ({
|
|
368
|
+
repoUrl,
|
|
369
|
+
branch = kDefaultTemplateBranch,
|
|
370
|
+
refresh = false,
|
|
371
|
+
}) => {
|
|
372
|
+
const cacheKey = `${resolveGithubRepoUrl(repoUrl)}#${branch}`;
|
|
373
|
+
const now = Date.now();
|
|
374
|
+
if (!refresh && kTemplateStatusCache.has(cacheKey)) {
|
|
375
|
+
const cached = kTemplateStatusCache.get(cacheKey);
|
|
376
|
+
if (now - cached.fetchedAt < kLatestVersionCacheTtlMs) {
|
|
377
|
+
return cached;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const payload = await fetchTemplatePackageVersions({
|
|
381
|
+
fetchImpl,
|
|
382
|
+
repoUrl,
|
|
383
|
+
branch,
|
|
83
384
|
});
|
|
385
|
+
const next = { ...payload, fetchedAt: Date.now() };
|
|
386
|
+
kTemplateStatusCache.set(cacheKey, next);
|
|
387
|
+
return next;
|
|
388
|
+
};
|
|
84
389
|
|
|
85
|
-
const
|
|
390
|
+
const readRegistryStatus = async ({ refresh = false } = {}) => {
|
|
86
391
|
const now = Date.now();
|
|
87
392
|
if (
|
|
88
393
|
!refresh &&
|
|
89
|
-
|
|
90
|
-
now -
|
|
394
|
+
kRegistryStatusCache.fetchedAt &&
|
|
395
|
+
now - kRegistryStatusCache.fetchedAt < kLatestVersionCacheTtlMs
|
|
91
396
|
) {
|
|
92
|
-
return
|
|
93
|
-
latestVersion: kUpdateStatusCache.latestVersion,
|
|
94
|
-
hasUpdate: kUpdateStatusCache.hasUpdate,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
const currentVersion = readAlphaclawVersion();
|
|
98
|
-
const latestVersion = await fetchLatestVersionFromRegistry();
|
|
99
|
-
const hasUpdate = isNewerVersion(latestVersion, currentVersion);
|
|
100
|
-
kUpdateStatusCache = { latestVersion, hasUpdate, fetchedAt: Date.now() };
|
|
101
|
-
if (hasUpdate) {
|
|
102
|
-
console.log(
|
|
103
|
-
`[alphaclaw] alphaclaw update available: current=${currentVersion} latest=${latestVersion || "unknown"}`,
|
|
104
|
-
);
|
|
397
|
+
return kRegistryStatusCache;
|
|
105
398
|
}
|
|
106
|
-
|
|
399
|
+
const latestVersion = await fetchLatestVersionFromRegistry({ fetchImpl });
|
|
400
|
+
kRegistryStatusCache = {
|
|
401
|
+
latestVersion,
|
|
402
|
+
fetchedAt: Date.now(),
|
|
403
|
+
};
|
|
404
|
+
return kRegistryStatusCache;
|
|
107
405
|
};
|
|
108
406
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return kNpmPackageRoot;
|
|
407
|
+
const buildVersionStatus = ({
|
|
408
|
+
strategy,
|
|
409
|
+
latestVersion = null,
|
|
410
|
+
latestOpenclawVersion = null,
|
|
411
|
+
ok = true,
|
|
412
|
+
error = "",
|
|
413
|
+
}) => {
|
|
414
|
+
const currentVersion = readAlphaclawVersion();
|
|
415
|
+
const currentOpenclawVersion = normalizeOpenclawVersion(readOpenclawVersion());
|
|
416
|
+
const alphaclawHasUpdate = isNewerVersion(latestVersion, currentVersion);
|
|
417
|
+
const openclawHasUpdate =
|
|
418
|
+
strategy.templateRepoUrl && latestOpenclawVersion
|
|
419
|
+
? !currentOpenclawVersion ||
|
|
420
|
+
compareVersionParts(latestOpenclawVersion, currentOpenclawVersion) > 0
|
|
421
|
+
: false;
|
|
422
|
+
return {
|
|
423
|
+
ok,
|
|
424
|
+
currentVersion,
|
|
425
|
+
currentOpenclawVersion,
|
|
426
|
+
latestVersion: normalizeVersion(latestVersion),
|
|
427
|
+
latestOpenclawVersion: normalizeOpenclawVersion(latestOpenclawVersion),
|
|
428
|
+
hasUpdate: Boolean(alphaclawHasUpdate || openclawHasUpdate),
|
|
429
|
+
updateStrategy: strategy,
|
|
430
|
+
...(error ? { error: String(error || "").trim() } : {}),
|
|
431
|
+
};
|
|
135
432
|
};
|
|
136
433
|
|
|
137
434
|
const installLatestAlphaclaw = () =>
|
|
138
435
|
new Promise((resolve, reject) => {
|
|
139
|
-
const installDir = findInstallDir();
|
|
436
|
+
const installDir = findInstallDir(fsImpl);
|
|
437
|
+
const tmpDir = fsImpl.mkdtempSync(path.join(os.tmpdir(), "alphaclaw-update-"));
|
|
438
|
+
|
|
439
|
+
const cleanup = () => {
|
|
440
|
+
try {
|
|
441
|
+
fsImpl.rmSync(tmpDir, { recursive: true, force: true });
|
|
442
|
+
} catch {}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
fsImpl.writeFileSync(
|
|
446
|
+
path.join(tmpDir, "package.json"),
|
|
447
|
+
JSON.stringify({
|
|
448
|
+
private: true,
|
|
449
|
+
dependencies: { "@chrysb/alphaclaw": "latest" },
|
|
450
|
+
}),
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
const npmEnv = {
|
|
454
|
+
...process.env,
|
|
455
|
+
npm_config_update_notifier: "false",
|
|
456
|
+
npm_config_fund: "false",
|
|
457
|
+
npm_config_audit: "false",
|
|
458
|
+
};
|
|
459
|
+
|
|
140
460
|
console.log(
|
|
141
|
-
`[alphaclaw] Running: npm install @chrysb/alphaclaw@latest (
|
|
461
|
+
`[alphaclaw] Running: npm install @chrysb/alphaclaw@latest in temp dir (target: ${installDir})`,
|
|
142
462
|
);
|
|
143
463
|
childProcess.exec(
|
|
144
|
-
"npm install
|
|
464
|
+
"npm install --omit=dev --prefer-online --package-lock=false",
|
|
145
465
|
{
|
|
146
|
-
cwd:
|
|
147
|
-
env:
|
|
148
|
-
...process.env,
|
|
149
|
-
npm_config_update_notifier: "false",
|
|
150
|
-
npm_config_fund: "false",
|
|
151
|
-
npm_config_audit: "false",
|
|
152
|
-
},
|
|
466
|
+
cwd: tmpDir,
|
|
467
|
+
env: npmEnv,
|
|
153
468
|
timeout: 180000,
|
|
154
469
|
},
|
|
155
470
|
(err, stdout, stderr) => {
|
|
@@ -158,6 +473,7 @@ const createAlphaclawVersionService = () => {
|
|
|
158
473
|
console.log(
|
|
159
474
|
`[alphaclaw] alphaclaw install error: ${message.slice(0, 200)}`,
|
|
160
475
|
);
|
|
476
|
+
cleanup();
|
|
161
477
|
return reject(
|
|
162
478
|
new Error(
|
|
163
479
|
message || "Failed to install @chrysb/alphaclaw@latest",
|
|
@@ -169,28 +485,106 @@ const createAlphaclawVersionService = () => {
|
|
|
169
485
|
`[alphaclaw] alphaclaw install stdout: ${stdout.trim().slice(0, 300)}`,
|
|
170
486
|
);
|
|
171
487
|
}
|
|
172
|
-
|
|
173
|
-
|
|
488
|
+
|
|
489
|
+
const src = path.join(tmpDir, "node_modules");
|
|
490
|
+
const dest = path.join(installDir, "node_modules");
|
|
491
|
+
childProcess.exec(
|
|
492
|
+
`cp -af "${src}/." "${dest}/"`,
|
|
493
|
+
{ timeout: kOpenclawUpdateCopyTimeoutMs },
|
|
494
|
+
(copyErr) => {
|
|
495
|
+
cleanup();
|
|
496
|
+
if (copyErr) {
|
|
497
|
+
console.log(
|
|
498
|
+
`[alphaclaw] alphaclaw copy error: ${(copyErr.message || "").slice(0, 200)}`,
|
|
499
|
+
);
|
|
500
|
+
return reject(
|
|
501
|
+
new Error(
|
|
502
|
+
`Failed to copy updated AlphaClaw files: ${copyErr.message}`,
|
|
503
|
+
),
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
console.log("[alphaclaw] alphaclaw install completed");
|
|
507
|
+
resolve({ stdout: stdout?.trim(), stderr: stderr?.trim() });
|
|
508
|
+
},
|
|
509
|
+
);
|
|
174
510
|
},
|
|
175
511
|
);
|
|
176
512
|
});
|
|
177
513
|
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
514
|
+
const updateManagedDeployment = async (strategy) => {
|
|
515
|
+
try {
|
|
516
|
+
const latestStatus = await readTemplateStatus({
|
|
517
|
+
repoUrl: strategy.templateRepoUrl,
|
|
518
|
+
branch: strategy.templateBranch,
|
|
519
|
+
refresh: true,
|
|
520
|
+
});
|
|
521
|
+
const latestRef = await fetchTemplateHeadRef({
|
|
522
|
+
fetchImpl,
|
|
523
|
+
repoUrl: strategy.templateRepoUrl,
|
|
524
|
+
branch: strategy.templateBranch,
|
|
525
|
+
env,
|
|
526
|
+
});
|
|
527
|
+
const response = await fetchImpl(strategy.managedUpdateUrl, {
|
|
528
|
+
method: "POST",
|
|
529
|
+
headers: {
|
|
530
|
+
"Content-Type": "application/json",
|
|
531
|
+
Authorization: `Bearer ${strategy.managedUpdateToken}`,
|
|
532
|
+
"User-Agent": "alphaclaw",
|
|
533
|
+
},
|
|
534
|
+
body: JSON.stringify({
|
|
535
|
+
repo: strategy.templateRepoUrl,
|
|
536
|
+
ref: latestRef,
|
|
537
|
+
alphaclawVersion: latestStatus.latestVersion || readAlphaclawVersion() || "",
|
|
538
|
+
openclawVersion:
|
|
539
|
+
latestStatus.latestOpenclawVersion ||
|
|
540
|
+
normalizeOpenclawVersion(readOpenclawVersion()) ||
|
|
541
|
+
"",
|
|
542
|
+
}),
|
|
543
|
+
});
|
|
544
|
+
const data = await parseJsonResponse(
|
|
545
|
+
response,
|
|
546
|
+
"Failed to trigger the managed deployment update",
|
|
547
|
+
);
|
|
548
|
+
return {
|
|
549
|
+
status: 200,
|
|
550
|
+
body: {
|
|
551
|
+
ok: true,
|
|
552
|
+
previousVersion: readAlphaclawVersion(),
|
|
553
|
+
currentVersion: latestStatus.latestVersion || readAlphaclawVersion(),
|
|
554
|
+
currentOpenclawVersion: normalizeOpenclawVersion(readOpenclawVersion()),
|
|
555
|
+
latestVersion: latestStatus.latestVersion || readAlphaclawVersion(),
|
|
556
|
+
latestOpenclawVersion:
|
|
557
|
+
latestStatus.latestOpenclawVersion ||
|
|
558
|
+
normalizeOpenclawVersion(readOpenclawVersion()),
|
|
559
|
+
managedUpdate: true,
|
|
560
|
+
restarting: true,
|
|
561
|
+
noop: !!data?.noop,
|
|
562
|
+
phase: String(data?.phase || "").trim(),
|
|
563
|
+
},
|
|
564
|
+
};
|
|
565
|
+
} catch (err) {
|
|
566
|
+
return {
|
|
567
|
+
status: 502,
|
|
568
|
+
body: {
|
|
569
|
+
ok: false,
|
|
570
|
+
error:
|
|
571
|
+
err.message || "Failed to trigger the managed deployment update",
|
|
572
|
+
updateStrategy: strategy,
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
};
|
|
183
577
|
|
|
184
578
|
const restartProcess = () => {
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
579
|
+
if (
|
|
580
|
+
env.RAILWAY_ENVIRONMENT ||
|
|
581
|
+
env.RENDER ||
|
|
582
|
+
env.FLY_APP_NAME ||
|
|
583
|
+
fsImpl.existsSync("/.dockerenv")
|
|
584
|
+
) {
|
|
190
585
|
console.log("[alphaclaw] Restarting via container crash (exit 1)...");
|
|
191
586
|
process.exit(1);
|
|
192
587
|
}
|
|
193
|
-
// On bare metal / Mac / Linux, spawn a replacement process then exit.
|
|
194
588
|
console.log("[alphaclaw] Spawning new process and exiting...");
|
|
195
589
|
const { spawn } = require("child_process");
|
|
196
590
|
const child = spawn(process.argv[0], process.argv.slice(1), {
|
|
@@ -202,39 +596,72 @@ const createAlphaclawVersionService = () => {
|
|
|
202
596
|
};
|
|
203
597
|
|
|
204
598
|
const getVersionStatus = async (refresh) => {
|
|
205
|
-
const
|
|
599
|
+
const strategy = detectUpdateStrategy({ env, fsImpl });
|
|
206
600
|
try {
|
|
207
|
-
|
|
208
|
-
|
|
601
|
+
if (strategy.templateRepoUrl) {
|
|
602
|
+
const status = await readTemplateStatus({
|
|
603
|
+
repoUrl: strategy.templateRepoUrl,
|
|
604
|
+
branch: strategy.templateBranch,
|
|
605
|
+
refresh,
|
|
606
|
+
});
|
|
607
|
+
return buildVersionStatus({
|
|
608
|
+
strategy,
|
|
609
|
+
latestVersion: status.latestVersion,
|
|
610
|
+
latestOpenclawVersion: status.latestOpenclawVersion,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
const status = await readRegistryStatus({ refresh });
|
|
614
|
+
return buildVersionStatus({
|
|
615
|
+
strategy,
|
|
616
|
+
latestVersion: status.latestVersion,
|
|
209
617
|
});
|
|
210
|
-
return { ok: true, currentVersion, latestVersion, hasUpdate };
|
|
211
618
|
} catch (err) {
|
|
212
|
-
|
|
619
|
+
const cachedTemplateStatus = strategy.templateRepoUrl
|
|
620
|
+
? kTemplateStatusCache.get(
|
|
621
|
+
`${resolveGithubRepoUrl(strategy.templateRepoUrl)}#${strategy.templateBranch}`,
|
|
622
|
+
) || {}
|
|
623
|
+
: {};
|
|
624
|
+
return buildVersionStatus({
|
|
625
|
+
strategy,
|
|
626
|
+
latestVersion:
|
|
627
|
+
cachedTemplateStatus.latestVersion || kRegistryStatusCache.latestVersion,
|
|
628
|
+
latestOpenclawVersion: cachedTemplateStatus.latestOpenclawVersion,
|
|
213
629
|
ok: false,
|
|
214
|
-
currentVersion,
|
|
215
|
-
latestVersion: kUpdateStatusCache.latestVersion,
|
|
216
|
-
hasUpdate: kUpdateStatusCache.hasUpdate,
|
|
217
630
|
error: err.message || "Failed to fetch latest AlphaClaw version",
|
|
218
|
-
};
|
|
631
|
+
});
|
|
219
632
|
}
|
|
220
633
|
};
|
|
221
634
|
|
|
222
635
|
const updateAlphaclaw = async () => {
|
|
636
|
+
const strategy = detectUpdateStrategy({ env, fsImpl });
|
|
223
637
|
if (kUpdateInProgress) {
|
|
224
638
|
return {
|
|
225
639
|
status: 409,
|
|
226
640
|
body: { ok: false, error: "AlphaClaw update already in progress" },
|
|
227
641
|
};
|
|
228
642
|
}
|
|
643
|
+
if (strategy.action === "managed-update") {
|
|
644
|
+
return updateManagedDeployment(strategy);
|
|
645
|
+
}
|
|
646
|
+
if (strategy.action !== "self-update") {
|
|
647
|
+
return {
|
|
648
|
+
status: 409,
|
|
649
|
+
body: {
|
|
650
|
+
ok: false,
|
|
651
|
+
error:
|
|
652
|
+
strategy.description || "This deployment is updated outside AlphaClaw.",
|
|
653
|
+
updateStrategy: strategy,
|
|
654
|
+
},
|
|
655
|
+
};
|
|
656
|
+
}
|
|
229
657
|
|
|
230
658
|
kUpdateInProgress = true;
|
|
231
659
|
const previousVersion = readAlphaclawVersion();
|
|
232
660
|
try {
|
|
233
661
|
await installLatestAlphaclaw();
|
|
234
|
-
// Write marker to persistent volume so the update survives container recreation
|
|
235
662
|
const markerPath = path.join(kRootDir, ".alphaclaw-update-pending");
|
|
236
663
|
try {
|
|
237
|
-
|
|
664
|
+
fsImpl.writeFileSync(
|
|
238
665
|
markerPath,
|
|
239
666
|
JSON.stringify({ from: previousVersion, ts: Date.now() }),
|
|
240
667
|
);
|
|
@@ -242,9 +669,8 @@ const createAlphaclawVersionService = () => {
|
|
|
242
669
|
} catch (e) {
|
|
243
670
|
console.log(`[alphaclaw] Could not write update marker: ${e.message}`);
|
|
244
671
|
}
|
|
245
|
-
|
|
672
|
+
kRegistryStatusCache = {
|
|
246
673
|
latestVersion: null,
|
|
247
|
-
hasUpdate: false,
|
|
248
674
|
fetchedAt: 0,
|
|
249
675
|
};
|
|
250
676
|
return {
|
|
@@ -272,4 +698,36 @@ const createAlphaclawVersionService = () => {
|
|
|
272
698
|
};
|
|
273
699
|
};
|
|
274
700
|
|
|
275
|
-
|
|
701
|
+
const findInstallDir = (fsImpl) => {
|
|
702
|
+
let dir = kNpmPackageRoot;
|
|
703
|
+
while (dir !== path.dirname(dir)) {
|
|
704
|
+
const parent = path.dirname(dir);
|
|
705
|
+
if (
|
|
706
|
+
path.basename(parent) === "node_modules" ||
|
|
707
|
+
parent.includes(`${path.sep}node_modules${path.sep}`)
|
|
708
|
+
) {
|
|
709
|
+
dir = parent;
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
const pkgPath = path.join(parent, "package.json");
|
|
713
|
+
if (fsImpl.existsSync(pkgPath)) {
|
|
714
|
+
try {
|
|
715
|
+
const pkg = JSON.parse(fsImpl.readFileSync(pkgPath, "utf8"));
|
|
716
|
+
if (
|
|
717
|
+
pkg.dependencies?.["@chrysb/alphaclaw"] ||
|
|
718
|
+
pkg.devDependencies?.["@chrysb/alphaclaw"] ||
|
|
719
|
+
pkg.optionalDependencies?.["@chrysb/alphaclaw"]
|
|
720
|
+
) {
|
|
721
|
+
return parent;
|
|
722
|
+
}
|
|
723
|
+
} catch {}
|
|
724
|
+
}
|
|
725
|
+
dir = parent;
|
|
726
|
+
}
|
|
727
|
+
return kNpmPackageRoot;
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
module.exports = {
|
|
731
|
+
createAlphaclawVersionService,
|
|
732
|
+
detectUpdateStrategy,
|
|
733
|
+
};
|