@chrysb/alphaclaw 0.9.0-beta.7 → 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.
Files changed (66) hide show
  1. package/bin/alphaclaw.js +25 -25
  2. package/lib/cli/git-runtime.js +97 -0
  3. package/lib/public/css/chat.css +0 -12
  4. package/lib/public/css/explorer.css +48 -0
  5. package/lib/public/css/shell.css +149 -0
  6. package/lib/public/css/tailwind.generated.css +1 -1
  7. package/lib/public/css/theme.css +265 -0
  8. package/lib/public/dist/app.bundle.js +2770 -2762
  9. package/lib/public/js/app.js +26 -14
  10. package/lib/public/js/components/agents-tab/create-channel-modal.js +259 -59
  11. package/lib/public/js/components/gateway.js +0 -286
  12. package/lib/public/js/components/general/index.js +0 -7
  13. package/lib/public/js/components/icons.js +26 -25
  14. package/lib/public/js/components/modal-shell.js +1 -1
  15. package/lib/public/js/components/models-tab/provider-auth-card.js +60 -49
  16. package/lib/public/js/components/models-tab/use-models.js +74 -9
  17. package/lib/public/js/components/models.js +52 -37
  18. package/lib/public/js/components/onboarding/use-welcome-codex.js +34 -24
  19. package/lib/public/js/components/onboarding/welcome-config.js +76 -10
  20. package/lib/public/js/components/onboarding/welcome-form-step.js +2 -7
  21. package/lib/public/js/components/onboarding/welcome-header.js +12 -14
  22. package/lib/public/js/components/onboarding/welcome-setup-step.js +3 -3
  23. package/lib/public/js/components/providers.js +53 -42
  24. package/lib/public/js/components/routes/chat-route.js +2 -9
  25. package/lib/public/js/components/routes/general-route.js +0 -6
  26. package/lib/public/js/components/routes/index.js +0 -1
  27. package/lib/public/js/components/routes/watchdog-route.js +0 -6
  28. package/lib/public/js/components/sidebar.js +21 -7
  29. package/lib/public/js/components/theme-toggle.js +113 -0
  30. package/lib/public/js/components/update-modal.js +174 -51
  31. package/lib/public/js/components/watchdog-tab/index.js +0 -6
  32. package/lib/public/js/components/welcome/index.js +0 -2
  33. package/lib/public/js/components/welcome/use-welcome.js +101 -36
  34. package/lib/public/js/hooks/use-app-shell-controller.js +16 -33
  35. package/lib/public/js/lib/api.js +0 -28
  36. package/lib/public/js/lib/app-navigation.js +0 -2
  37. package/lib/public/js/lib/channel-provider-availability.js +1 -2
  38. package/lib/public/js/lib/codex-oauth-window.js +22 -0
  39. package/lib/public/js/lib/model-catalog.js +20 -0
  40. package/lib/public/js/lib/storage-keys.js +1 -1
  41. package/lib/public/login.html +8 -4
  42. package/lib/public/setup.html +9 -0
  43. package/lib/scripts/git +47 -1
  44. package/lib/server/agents/channels.js +1 -4
  45. package/lib/server/alphaclaw-version.js +590 -132
  46. package/lib/server/constants.js +5 -0
  47. package/lib/server/db/webhooks/index.js +48 -8
  48. package/lib/server/exec-defaults-config.js +163 -0
  49. package/lib/server/init/register-server-routes.js +0 -8
  50. package/lib/server/init/server-lifecycle.js +2 -0
  51. package/lib/server/model-catalog-cache.js +251 -0
  52. package/lib/server/onboarding/index.js +5 -0
  53. package/lib/server/routes/models.js +14 -23
  54. package/lib/server/routes/nodes.js +9 -23
  55. package/lib/server/routes/system.js +3 -16
  56. package/lib/server/routes/webhooks.js +12 -1
  57. package/lib/server/startup.js +8 -0
  58. package/lib/server/watchdog-notify.js +172 -55
  59. package/lib/server.js +17 -2
  60. package/package.json +2 -2
  61. package/patches/openclaw+2026.4.9.patch +13 -0
  62. package/lib/public/js/components/mcp-tab/index.js +0 -237
  63. package/lib/public/js/components/routes/mcp-route.js +0 -7
  64. package/lib/server/mcp-bridge.js +0 -158
  65. package/lib/server/routes/mcp.js +0 -292
  66. 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 = (v) => {
16
- const [core] = String(v).replace(/^v/, "").split("-");
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 { major: parts[0] || 0, minor: parts[1] || 0, patch: parts[2] || 0 };
34
+ return {
35
+ major: parts[0] || 0,
36
+ minor: parts[1] || 0,
37
+ patch: parts[2] || 0,
38
+ prerelease,
39
+ };
19
40
  };
20
- const l = parse(latest);
21
- const c = parse(current);
22
- if (l.major !== c.major) return l.major > c.major;
23
- if (l.minor !== c.minor) return l.minor > c.minor;
24
- return l.patch > c.patch;
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 createAlphaclawVersionService = () => {
28
- let kUpdateStatusCache = {
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
- fs.readFileSync(path.join(kNpmPackageRoot, "package.json"), "utf8"),
359
+ fsImpl.readFileSync(path.join(kNpmPackageRoot, "package.json"), "utf8"),
39
360
  );
40
- return pkg.version || null;
361
+ return normalizeVersion(pkg.version);
41
362
  } catch {
42
363
  return null;
43
364
  }
44
365
  };
45
366
 
46
- const fetchLatestVersionFromRegistry = () =>
47
- new Promise((resolve, reject) => {
48
- const doGet = (url, redirects = 0) => {
49
- if (redirects > 3) return reject(new Error("Too many redirects"));
50
- const get = url.startsWith("https") ? https.get : http.get;
51
- get(
52
- url,
53
- { headers: { Accept: "application/vnd.npm.install-v1+json" } },
54
- (res) => {
55
- if (
56
- res.statusCode >= 300 &&
57
- res.statusCode < 400 &&
58
- res.headers.location
59
- ) {
60
- res.resume();
61
- return doGet(res.headers.location, redirects + 1);
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 readAlphaclawUpdateStatus = async ({ refresh = false } = {}) => {
390
+ const readRegistryStatus = async ({ refresh = false } = {}) => {
86
391
  const now = Date.now();
87
392
  if (
88
393
  !refresh &&
89
- kUpdateStatusCache.fetchedAt &&
90
- now - kUpdateStatusCache.fetchedAt < kLatestVersionCacheTtlMs
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
- return { latestVersion, hasUpdate };
399
+ const latestVersion = await fetchLatestVersionFromRegistry({ fetchImpl });
400
+ kRegistryStatusCache = {
401
+ latestVersion,
402
+ fetchedAt: Date.now(),
403
+ };
404
+ return kRegistryStatusCache;
107
405
  };
108
406
 
109
- const findInstallDir = () => {
110
- // Walk up from kNpmPackageRoot to find the consuming project's directory
111
- // (the one with node_modules/@chrysb/alphaclaw). In Docker this is /app.
112
- let dir = kNpmPackageRoot;
113
- while (dir !== path.dirname(dir)) {
114
- const parent = path.dirname(dir);
115
- if (
116
- path.basename(parent) === "node_modules" ||
117
- parent.includes("node_modules")
118
- ) {
119
- dir = parent;
120
- continue;
121
- }
122
- const pkgPath = path.join(parent, "package.json");
123
- if (fs.existsSync(pkgPath)) {
124
- try {
125
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
126
- if (pkg.dependencies?.["@chrysb/alphaclaw"]) {
127
- return parent;
128
- }
129
- } catch {}
130
- }
131
- dir = parent;
132
- }
133
- // Fallback: if running directly (not from node_modules), use kNpmPackageRoot
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 (cwd: ${installDir})`,
461
+ `[alphaclaw] Running: npm install @chrysb/alphaclaw@latest in temp dir (target: ${installDir})`,
142
462
  );
143
463
  childProcess.exec(
144
- "npm install @chrysb/alphaclaw@latest --omit=dev --no-save --save=false --package-lock=false --prefer-online",
464
+ "npm install --omit=dev --prefer-online --package-lock=false",
145
465
  {
146
- cwd: installDir,
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
- console.log("[alphaclaw] alphaclaw install completed");
173
- resolve({ stdout: stdout?.trim(), stderr: stderr?.trim() });
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 isContainer = () =>
179
- process.env.RAILWAY_ENVIRONMENT ||
180
- process.env.RENDER ||
181
- process.env.FLY_APP_NAME ||
182
- fs.existsSync("/.dockerenv");
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 (isContainer()) {
186
- // In containers, exit with code 1 so the orchestrator (Railway, Docker
187
- // restart policy, etc.) treats it as a crash and restarts the service.
188
- // Spawning a child doesn't work because killing PID 1 tears down the
189
- // entire container along with any children.
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 currentVersion = readAlphaclawVersion();
599
+ const strategy = detectUpdateStrategy({ env, fsImpl });
206
600
  try {
207
- const { latestVersion, hasUpdate } = await readAlphaclawUpdateStatus({
208
- refresh,
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
- return {
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
- fs.writeFileSync(
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
- kUpdateStatusCache = {
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
- module.exports = { createAlphaclawVersionService };
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
+ };