@chrysb/alphaclaw 0.8.6 → 0.8.7-beta.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.
@@ -1,6 +1,4 @@
1
- const childProcess = require("child_process");
2
1
  const fs = require("fs");
3
- const os = require("os");
4
2
  const path = require("path");
5
3
  const https = require("https");
6
4
  const http = require("http");
@@ -8,7 +6,6 @@ const {
8
6
  kLatestVersionCacheTtlMs,
9
7
  kAlphaclawRegistryUrl,
10
8
  kNpmPackageRoot,
11
- kOpenclawUpdateCopyTimeoutMs,
12
9
  kRootDir,
13
10
  } = require("./constants");
14
11
 
@@ -26,6 +23,9 @@ const isNewerVersion = (latest, current) => {
26
23
  return l.patch > c.patch;
27
24
  };
28
25
 
26
+ const buildAlphaclawInstallSpec = (version = "latest") =>
27
+ `@chrysb/alphaclaw@${String(version || "").trim() || "latest"}`;
28
+
29
29
  const createAlphaclawVersionService = () => {
30
30
  let kUpdateStatusCache = {
31
31
  latestVersion: null,
@@ -108,120 +108,6 @@ const createAlphaclawVersionService = () => {
108
108
  return { latestVersion, hasUpdate };
109
109
  };
110
110
 
111
- const findInstallDir = () => {
112
- // Walk up from kNpmPackageRoot to find the consuming project's directory
113
- // (the one with node_modules/@chrysb/alphaclaw). In Docker this is /app.
114
- let dir = kNpmPackageRoot;
115
- while (dir !== path.dirname(dir)) {
116
- const parent = path.dirname(dir);
117
- if (
118
- path.basename(parent) === "node_modules" ||
119
- parent.includes(`${path.sep}node_modules${path.sep}`)
120
- ) {
121
- dir = parent;
122
- continue;
123
- }
124
- const pkgPath = path.join(parent, "package.json");
125
- if (fs.existsSync(pkgPath)) {
126
- try {
127
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
128
- if (
129
- pkg.dependencies?.["@chrysb/alphaclaw"] ||
130
- pkg.devDependencies?.["@chrysb/alphaclaw"] ||
131
- pkg.optionalDependencies?.["@chrysb/alphaclaw"]
132
- ) {
133
- return parent;
134
- }
135
- } catch {}
136
- }
137
- dir = parent;
138
- }
139
- // Fallback: if running directly (not from node_modules), use kNpmPackageRoot
140
- return kNpmPackageRoot;
141
- };
142
-
143
- const installLatestAlphaclaw = () =>
144
- new Promise((resolve, reject) => {
145
- const installDir = findInstallDir();
146
- const tmpDir = fs.mkdtempSync(
147
- path.join(os.tmpdir(), "alphaclaw-update-"),
148
- );
149
-
150
- const cleanup = () => {
151
- try {
152
- fs.rmSync(tmpDir, { recursive: true, force: true });
153
- } catch {}
154
- };
155
-
156
- fs.writeFileSync(
157
- path.join(tmpDir, "package.json"),
158
- JSON.stringify({
159
- private: true,
160
- dependencies: { "@chrysb/alphaclaw": "latest" },
161
- }),
162
- );
163
-
164
- const npmEnv = {
165
- ...process.env,
166
- npm_config_update_notifier: "false",
167
- npm_config_fund: "false",
168
- npm_config_audit: "false",
169
- };
170
-
171
- console.log(
172
- `[alphaclaw] Running: npm install @chrysb/alphaclaw@latest in temp dir (target: ${installDir})`,
173
- );
174
- childProcess.exec(
175
- "npm install --omit=dev --prefer-online --package-lock=false",
176
- {
177
- cwd: tmpDir,
178
- env: npmEnv,
179
- timeout: 180000,
180
- },
181
- (err, stdout, stderr) => {
182
- if (err) {
183
- const message = String(stderr || err.message || "").trim();
184
- console.log(
185
- `[alphaclaw] alphaclaw install error: ${message.slice(0, 200)}`,
186
- );
187
- cleanup();
188
- return reject(
189
- new Error(
190
- message || "Failed to install @chrysb/alphaclaw@latest",
191
- ),
192
- );
193
- }
194
- if (stdout?.trim()) {
195
- console.log(
196
- `[alphaclaw] alphaclaw install stdout: ${stdout.trim().slice(0, 300)}`,
197
- );
198
- }
199
-
200
- const src = path.join(tmpDir, "node_modules");
201
- const dest = path.join(installDir, "node_modules");
202
- childProcess.exec(
203
- `cp -af "${src}/." "${dest}/"`,
204
- { timeout: kOpenclawUpdateCopyTimeoutMs },
205
- (copyErr) => {
206
- cleanup();
207
- if (copyErr) {
208
- console.log(
209
- `[alphaclaw] alphaclaw copy error: ${(copyErr.message || "").slice(0, 200)}`,
210
- );
211
- return reject(
212
- new Error(
213
- `Failed to copy updated AlphaClaw files: ${copyErr.message}`,
214
- ),
215
- );
216
- }
217
- console.log("[alphaclaw] alphaclaw install completed");
218
- resolve({ stdout: stdout?.trim(), stderr: stderr?.trim() });
219
- },
220
- );
221
- },
222
- );
223
- });
224
-
225
111
  const isContainer = () =>
226
112
  process.env.RAILWAY_ENVIRONMENT ||
227
113
  process.env.RENDER ||
@@ -277,18 +163,33 @@ const createAlphaclawVersionService = () => {
277
163
  kUpdateInProgress = true;
278
164
  const previousVersion = readAlphaclawVersion();
279
165
  try {
280
- await installLatestAlphaclaw();
281
- // Write marker to persistent volume so the update survives container recreation
282
- const markerPath = path.join(kRootDir, ".alphaclaw-update-pending");
166
+ let targetVersion = "latest";
283
167
  try {
284
- fs.writeFileSync(
285
- markerPath,
286
- JSON.stringify({ from: previousVersion, ts: Date.now() }),
168
+ const updateStatus = await readAlphaclawUpdateStatus({ refresh: true });
169
+ if (updateStatus.latestVersion) {
170
+ targetVersion = updateStatus.latestVersion;
171
+ }
172
+ } catch (error) {
173
+ console.log(
174
+ `[alphaclaw] Could not resolve exact AlphaClaw version before restart: ${error.message || "unknown error"}`,
287
175
  );
288
- console.log(`[alphaclaw] Update marker written to ${markerPath}`);
289
- } catch (e) {
290
- console.log(`[alphaclaw] Could not write update marker: ${e.message}`);
291
176
  }
177
+
178
+ const spec = buildAlphaclawInstallSpec(targetVersion);
179
+ // Write marker to persistent volume so the update survives container recreation
180
+ const markerPath = path.join(kRootDir, ".alphaclaw-update-pending");
181
+ fs.writeFileSync(
182
+ markerPath,
183
+ JSON.stringify({
184
+ from: previousVersion,
185
+ to: targetVersion,
186
+ spec,
187
+ ts: Date.now(),
188
+ }),
189
+ );
190
+ console.log(
191
+ `[alphaclaw] Update marker written to ${markerPath} for ${spec}`,
192
+ );
292
193
  kUpdateStatusCache = {
293
194
  latestVersion: null,
294
195
  hasUpdate: false,
@@ -299,15 +200,17 @@ const createAlphaclawVersionService = () => {
299
200
  body: {
300
201
  ok: true,
301
202
  previousVersion,
203
+ targetVersion: targetVersion === "latest" ? null : targetVersion,
302
204
  restarting: true,
303
205
  },
304
206
  };
305
207
  } catch (err) {
306
- kUpdateInProgress = false;
307
208
  return {
308
209
  status: 500,
309
210
  body: { ok: false, error: err.message || "Failed to update AlphaClaw" },
310
211
  };
212
+ } finally {
213
+ kUpdateInProgress = false;
311
214
  }
312
215
  };
313
216
 
@@ -1,12 +1,10 @@
1
- const { exec, execSync } = require("child_process");
1
+ const { execSync } = require("child_process");
2
2
  const fs = require("fs");
3
- const os = require("os");
4
3
  const path = require("path");
5
4
  const {
6
5
  kVersionCacheTtlMs,
7
6
  kLatestVersionCacheTtlMs,
8
- kNpmPackageRoot,
9
- kOpenclawUpdateCopyTimeoutMs,
7
+ kRootDir,
10
8
  } = require("./constants");
11
9
  const { normalizeOpenclawVersion } = require("./helpers");
12
10
  const { parseJsonObjectFromNoisyOutput } = require("./utils/json");
@@ -24,6 +22,9 @@ const createOpenclawVersionService = ({
24
22
  };
25
23
  let kOpenclawUpdateInProgress = false;
26
24
 
25
+ const buildOpenclawInstallSpec = (version = "latest") =>
26
+ `openclaw@${String(version || "").trim() || "latest"}`;
27
+
27
28
  const readOpenclawVersion = () => {
28
29
  const now = Date.now();
29
30
  if (
@@ -87,118 +88,6 @@ const createOpenclawVersionService = ({
87
88
  }
88
89
  };
89
90
 
90
- const findInstallDir = () => {
91
- // Resolve the consumer app root (for example /app in Docker), not this package directory.
92
- let dir = kNpmPackageRoot;
93
- while (dir !== path.dirname(dir)) {
94
- const parent = path.dirname(dir);
95
- if (
96
- path.basename(parent) === "node_modules" ||
97
- parent.includes(`${path.sep}node_modules${path.sep}`)
98
- ) {
99
- dir = parent;
100
- continue;
101
- }
102
- const pkgPath = path.join(parent, "package.json");
103
- if (fs.existsSync(pkgPath)) {
104
- try {
105
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
106
- if (
107
- pkg.dependencies?.["@chrysb/alphaclaw"] ||
108
- pkg.devDependencies?.["@chrysb/alphaclaw"] ||
109
- pkg.optionalDependencies?.["@chrysb/alphaclaw"]
110
- ) {
111
- return parent;
112
- }
113
- } catch {}
114
- }
115
- dir = parent;
116
- }
117
- return kNpmPackageRoot;
118
- };
119
-
120
- // Install to a temp directory, then copy into the real node_modules.
121
- // Running `npm install` directly in the app dir causes EBUSY on Docker
122
- // because npm tries to rename directories that the running process holds open.
123
- // Copying individual files (cp -af) avoids the rename syscall entirely.
124
- const installLatestOpenclaw = () =>
125
- new Promise((resolve, reject) => {
126
- const installDir = findInstallDir();
127
- const tmpDir = fs.mkdtempSync(
128
- path.join(os.tmpdir(), "openclaw-update-"),
129
- );
130
- const cleanup = () => {
131
- try {
132
- fs.rmSync(tmpDir, { recursive: true, force: true });
133
- } catch {}
134
- };
135
-
136
- fs.writeFileSync(
137
- path.join(tmpDir, "package.json"),
138
- JSON.stringify({
139
- private: true,
140
- dependencies: { openclaw: "latest" },
141
- }),
142
- );
143
-
144
- const npmEnv = {
145
- ...process.env,
146
- npm_config_update_notifier: "false",
147
- npm_config_fund: "false",
148
- npm_config_audit: "false",
149
- };
150
-
151
- console.log(
152
- `[alphaclaw] Running: npm install openclaw@latest in temp dir (target: ${installDir})`,
153
- );
154
- exec(
155
- "npm install --omit=dev --prefer-online --package-lock=false",
156
- { cwd: tmpDir, env: npmEnv, timeout: 180000 },
157
- (installErr, stdout, stderr) => {
158
- if (installErr) {
159
- const message = String(stderr || installErr.message || "").trim();
160
- console.log(
161
- `[alphaclaw] openclaw install error: ${message.slice(0, 200)}`,
162
- );
163
- cleanup();
164
- return reject(
165
- new Error(message || "Failed to install openclaw@latest"),
166
- );
167
- }
168
- if (stdout?.trim()) {
169
- console.log(
170
- `[alphaclaw] openclaw install stdout: ${stdout.trim().slice(0, 300)}`,
171
- );
172
- }
173
-
174
- const src = path.join(tmpDir, "node_modules");
175
- const dest = path.join(installDir, "node_modules");
176
- exec(
177
- `cp -af "${src}/." "${dest}/"`,
178
- { timeout: kOpenclawUpdateCopyTimeoutMs },
179
- (cpErr) => {
180
- cleanup();
181
- if (cpErr) {
182
- console.log(
183
- `[alphaclaw] openclaw copy error: ${(cpErr.message || "").slice(0, 200)}`,
184
- );
185
- return reject(
186
- new Error(
187
- `Failed to copy updated openclaw files: ${cpErr.message}`,
188
- ),
189
- );
190
- }
191
- console.log("[alphaclaw] openclaw install completed");
192
- resolve({
193
- stdout: stdout?.trim() || "",
194
- stderr: stderr?.trim() || "",
195
- });
196
- },
197
- );
198
- },
199
- );
200
- });
201
-
202
91
  const getVersionStatus = async (refresh) => {
203
92
  const currentVersion = readOpenclawVersion();
204
93
  try {
@@ -228,27 +117,67 @@ const createOpenclawVersionService = ({
228
117
  kOpenclawUpdateInProgress = true;
229
118
  const previousVersion = readOpenclawVersion();
230
119
  try {
231
- await installLatestOpenclaw();
232
- kOpenclawVersionCache = { value: null, fetchedAt: 0 };
233
- const currentVersion = readOpenclawVersion();
234
- const { latestVersion, hasUpdate } = readOpenclawUpdateStatus({
235
- refresh: true,
236
- });
237
- let restarted = false;
238
- if (isOnboarded()) {
239
- restartGateway();
240
- restarted = true;
120
+ let latestVersion = null;
121
+ let hasUpdate = false;
122
+ try {
123
+ const updateStatus = readOpenclawUpdateStatus({ refresh: true });
124
+ latestVersion = updateStatus.latestVersion || null;
125
+ hasUpdate = !!updateStatus.hasUpdate;
126
+ } catch (error) {
127
+ console.log(
128
+ `[alphaclaw] Could not resolve exact OpenClaw version before restart: ${error.message || "unknown error"}`,
129
+ );
241
130
  }
131
+
132
+ if (!hasUpdate && latestVersion && latestVersion === previousVersion) {
133
+ return {
134
+ status: 200,
135
+ body: {
136
+ ok: true,
137
+ previousVersion,
138
+ currentVersion: previousVersion,
139
+ latestVersion,
140
+ hasUpdate: false,
141
+ restarted: false,
142
+ restarting: false,
143
+ updated: false,
144
+ },
145
+ };
146
+ }
147
+
148
+ const targetVersion = latestVersion || "latest";
149
+ const spec = buildOpenclawInstallSpec(targetVersion);
150
+ const markerPath = path.join(kRootDir, ".openclaw-update-pending");
151
+ fs.writeFileSync(
152
+ markerPath,
153
+ JSON.stringify({
154
+ from: previousVersion,
155
+ to: targetVersion,
156
+ spec,
157
+ ts: Date.now(),
158
+ }),
159
+ );
160
+ console.log(
161
+ `[alphaclaw] OpenClaw update marker written to ${markerPath} for ${spec}`,
162
+ );
163
+ kOpenclawVersionCache = { value: previousVersion, fetchedAt: 0 };
164
+ kOpenclawUpdateStatusCache = {
165
+ latestVersion,
166
+ hasUpdate,
167
+ fetchedAt: 0,
168
+ };
242
169
  return {
243
170
  status: 200,
244
171
  body: {
245
172
  ok: true,
246
173
  previousVersion,
247
- currentVersion,
174
+ currentVersion: previousVersion,
175
+ targetVersion: targetVersion === "latest" ? null : targetVersion,
248
176
  latestVersion,
249
- hasUpdate,
250
- restarted,
251
- updated: previousVersion !== currentVersion,
177
+ hasUpdate: true,
178
+ restarted: false,
179
+ restarting: true,
180
+ updated: previousVersion !== targetVersion,
252
181
  },
253
182
  };
254
183
  } catch (err) {
@@ -0,0 +1,71 @@
1
+ const buildPendingAlphaclawInstallSpec = (marker = {}) => {
2
+ const explicitSpec = String(marker?.spec || "").trim();
3
+ if (explicitSpec) {
4
+ return explicitSpec;
5
+ }
6
+ const targetVersion = String(marker?.to || "").trim() || "latest";
7
+ return `@chrysb/alphaclaw@${targetVersion}`;
8
+ };
9
+
10
+ const shellQuote = (value) =>
11
+ `'${String(value || "").replace(/'/g, `'\"'\"'`)}'`;
12
+
13
+ const applyPendingAlphaclawUpdate = ({
14
+ execSyncImpl,
15
+ fsModule,
16
+ installDir,
17
+ logger = console,
18
+ markerPath,
19
+ }) => {
20
+ if (!fsModule.existsSync(markerPath)) {
21
+ return {
22
+ attempted: false,
23
+ installed: false,
24
+ spec: "",
25
+ };
26
+ }
27
+
28
+ let marker = {};
29
+ try {
30
+ marker = JSON.parse(fsModule.readFileSync(markerPath, "utf8"));
31
+ } catch {
32
+ marker = {};
33
+ }
34
+
35
+ const spec = buildPendingAlphaclawInstallSpec(marker);
36
+ logger.log(`[alphaclaw] Pending update detected, installing ${spec}...`);
37
+
38
+ try {
39
+ execSyncImpl(
40
+ `npm install ${shellQuote(spec)} --omit=dev --no-save --save=false --package-lock=false --prefer-online`,
41
+ {
42
+ cwd: installDir,
43
+ stdio: "inherit",
44
+ timeout: 180000,
45
+ },
46
+ );
47
+ fsModule.unlinkSync(markerPath);
48
+ logger.log("[alphaclaw] Update applied successfully");
49
+ return {
50
+ attempted: true,
51
+ installed: true,
52
+ spec,
53
+ };
54
+ } catch (error) {
55
+ logger.log(`[alphaclaw] Update install failed: ${error.message}`);
56
+ try {
57
+ fsModule.unlinkSync(markerPath);
58
+ } catch {}
59
+ return {
60
+ attempted: true,
61
+ installed: false,
62
+ spec,
63
+ error,
64
+ };
65
+ }
66
+ };
67
+
68
+ module.exports = {
69
+ applyPendingAlphaclawUpdate,
70
+ buildPendingAlphaclawInstallSpec,
71
+ };
@@ -0,0 +1,71 @@
1
+ const buildPendingOpenclawInstallSpec = (marker = {}) => {
2
+ const explicitSpec = String(marker?.spec || "").trim();
3
+ if (explicitSpec) {
4
+ return explicitSpec;
5
+ }
6
+ const targetVersion = String(marker?.to || "").trim() || "latest";
7
+ return `openclaw@${targetVersion}`;
8
+ };
9
+
10
+ const shellQuote = (value) =>
11
+ `'${String(value || "").replace(/'/g, `'\"'\"'`)}'`;
12
+
13
+ const applyPendingOpenclawUpdate = ({
14
+ execSyncImpl,
15
+ fsModule,
16
+ installDir,
17
+ logger = console,
18
+ markerPath,
19
+ }) => {
20
+ if (!fsModule.existsSync(markerPath)) {
21
+ return {
22
+ attempted: false,
23
+ installed: false,
24
+ spec: "",
25
+ };
26
+ }
27
+
28
+ let marker = {};
29
+ try {
30
+ marker = JSON.parse(fsModule.readFileSync(markerPath, "utf8"));
31
+ } catch {
32
+ marker = {};
33
+ }
34
+
35
+ const spec = buildPendingOpenclawInstallSpec(marker);
36
+ logger.log(`[alphaclaw] Pending OpenClaw update detected, installing ${spec}...`);
37
+
38
+ try {
39
+ execSyncImpl(
40
+ `npm install ${shellQuote(spec)} --omit=dev --no-save --save=false --package-lock=false --prefer-online`,
41
+ {
42
+ cwd: installDir,
43
+ stdio: "inherit",
44
+ timeout: 180000,
45
+ },
46
+ );
47
+ fsModule.unlinkSync(markerPath);
48
+ logger.log("[alphaclaw] OpenClaw update applied successfully");
49
+ return {
50
+ attempted: true,
51
+ installed: true,
52
+ spec,
53
+ };
54
+ } catch (error) {
55
+ logger.log(`[alphaclaw] OpenClaw update install failed: ${error.message}`);
56
+ try {
57
+ fsModule.unlinkSync(markerPath);
58
+ } catch {}
59
+ return {
60
+ attempted: true,
61
+ installed: false,
62
+ spec,
63
+ error,
64
+ };
65
+ }
66
+ };
67
+
68
+ module.exports = {
69
+ applyPendingOpenclawUpdate,
70
+ buildPendingOpenclawInstallSpec,
71
+ };
@@ -588,7 +588,12 @@ const registerSystemRoutes = ({
588
588
  console.log(
589
589
  `[alphaclaw] /api/openclaw/update result: status=${result.status} ok=${result.body?.ok === true}`,
590
590
  );
591
- res.status(result.status).json(result.body);
591
+ if (result.status === 200 && result.body?.ok && result.body?.restarting) {
592
+ res.json(result.body);
593
+ setTimeout(() => alphaclawVersionService.restartProcess(), 1000);
594
+ } else {
595
+ res.status(result.status).json(result.body);
596
+ }
592
597
  });
593
598
 
594
599
  app.get("/api/alphaclaw/version", async (req, res) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.8.6",
3
+ "version": "0.8.7-beta.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },