@chrysb/alphaclaw 0.8.7 → 0.8.8

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 (33) hide show
  1. package/bin/alphaclaw.js +174 -43
  2. package/lib/public/css/tailwind.generated.css +1 -1
  3. package/lib/public/dist/app.bundle.js +2109 -2089
  4. package/lib/public/js/app.js +3 -0
  5. package/lib/public/js/components/gateway.js +6 -3
  6. package/lib/public/js/components/general/index.js +2 -0
  7. package/lib/public/js/components/onboarding/welcome-form-step.js +29 -4
  8. package/lib/public/js/components/routes/general-route.js +2 -0
  9. package/lib/public/js/components/routes/watchdog-route.js +2 -0
  10. package/lib/public/js/components/sidebar.js +20 -7
  11. package/lib/public/js/components/update-modal-helpers.js +12 -0
  12. package/lib/public/js/components/update-modal.js +2 -1
  13. package/lib/public/js/components/watchdog-tab/index.js +2 -0
  14. package/lib/public/js/components/welcome/index.js +1 -0
  15. package/lib/public/js/components/welcome/use-welcome.js +52 -2
  16. package/lib/public/js/hooks/use-app-shell-controller.js +37 -9
  17. package/lib/public/js/lib/api.js +36 -0
  18. package/lib/release/managed-release.js +180 -0
  19. package/lib/server/alphaclaw-runtime.js +294 -0
  20. package/lib/server/alphaclaw-version.js +37 -128
  21. package/lib/server/gateway.js +32 -14
  22. package/lib/server/init/register-server-routes.js +7 -1
  23. package/lib/server/openclaw-runtime.js +428 -0
  24. package/lib/server/openclaw-version.js +76 -136
  25. package/lib/server/package-fingerprint.js +274 -0
  26. package/lib/server/pending-alphaclaw-update.js +85 -0
  27. package/lib/server/pending-openclaw-update.js +86 -0
  28. package/lib/server/routes/pages.js +9 -1
  29. package/lib/server/routes/system.js +6 -1
  30. package/lib/server/usage-tracker-config.js +27 -3
  31. package/package.json +3 -2
  32. package/patches/openclaw+2026.4.9.patch +13 -0
  33. package/patches/openclaw+2026.4.5.patch +0 -13
@@ -1,14 +1,13 @@
1
- const { exec, execSync } = require("child_process");
1
+ const { execFileSync } = 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");
10
+ const { getManagedOpenclawBinPath } = require("./openclaw-runtime");
12
11
  const { parseJsonObjectFromNoisyOutput } = require("./utils/json");
13
12
 
14
13
  const createOpenclawVersionService = ({
@@ -24,6 +23,14 @@ const createOpenclawVersionService = ({
24
23
  };
25
24
  let kOpenclawUpdateInProgress = false;
26
25
 
26
+ const buildOpenclawInstallSpec = (version = "latest") =>
27
+ `openclaw@${String(version || "").trim() || "latest"}`;
28
+
29
+ const getOpenclawCommandPath = () => {
30
+ const managedBinPath = getManagedOpenclawBinPath();
31
+ return fs.existsSync(managedBinPath) ? managedBinPath : "openclaw";
32
+ };
33
+
27
34
  const readOpenclawVersion = () => {
28
35
  const now = Date.now();
29
36
  if (
@@ -33,9 +40,9 @@ const createOpenclawVersionService = ({
33
40
  return kOpenclawVersionCache.value;
34
41
  }
35
42
  try {
36
- const raw = execSync("openclaw --version", {
43
+ const raw = execFileSync(getOpenclawCommandPath(), ["--version"], {
37
44
  env: gatewayEnv(),
38
- timeout: 5000,
45
+ timeout: 10000,
39
46
  encoding: "utf8",
40
47
  }).trim();
41
48
  const version = normalizeOpenclawVersion(raw);
@@ -59,11 +66,16 @@ const createOpenclawVersionService = ({
59
66
  };
60
67
  }
61
68
  try {
62
- const raw = execSync("openclaw update status --json", {
69
+ const raw = execFileSync(
70
+ getOpenclawCommandPath(),
71
+ ["update", "status", "--json"],
72
+ {
63
73
  env: gatewayEnv(),
64
- timeout: 8000,
65
- encoding: "utf8",
66
- }).trim();
74
+ timeout: 30000,
75
+ maxBuffer: 4 * 1024 * 1024,
76
+ encoding: "utf8",
77
+ },
78
+ ).trim();
67
79
  const parsed = parseJsonObjectFromNoisyOutput(raw);
68
80
  if (!parsed) {
69
81
  throw new Error("openclaw update status returned invalid JSON payload");
@@ -87,118 +99,6 @@ const createOpenclawVersionService = ({
87
99
  }
88
100
  };
89
101
 
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
102
  const getVersionStatus = async (refresh) => {
203
103
  const currentVersion = readOpenclawVersion();
204
104
  try {
@@ -228,27 +128,67 @@ const createOpenclawVersionService = ({
228
128
  kOpenclawUpdateInProgress = true;
229
129
  const previousVersion = readOpenclawVersion();
230
130
  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;
131
+ let latestVersion = null;
132
+ let hasUpdate = false;
133
+ try {
134
+ const updateStatus = readOpenclawUpdateStatus({ refresh: true });
135
+ latestVersion = updateStatus.latestVersion || null;
136
+ hasUpdate = !!updateStatus.hasUpdate;
137
+ } catch (error) {
138
+ console.log(
139
+ `[alphaclaw] Could not resolve exact OpenClaw version before restart: ${error.message || "unknown error"}`,
140
+ );
241
141
  }
142
+
143
+ if (!hasUpdate && latestVersion && latestVersion === previousVersion) {
144
+ return {
145
+ status: 200,
146
+ body: {
147
+ ok: true,
148
+ previousVersion,
149
+ currentVersion: previousVersion,
150
+ latestVersion,
151
+ hasUpdate: false,
152
+ restarted: false,
153
+ restarting: false,
154
+ updated: false,
155
+ },
156
+ };
157
+ }
158
+
159
+ const targetVersion = latestVersion || "latest";
160
+ const spec = buildOpenclawInstallSpec(targetVersion);
161
+ const markerPath = path.join(kRootDir, ".openclaw-update-pending");
162
+ fs.writeFileSync(
163
+ markerPath,
164
+ JSON.stringify({
165
+ from: previousVersion,
166
+ to: targetVersion,
167
+ spec,
168
+ ts: Date.now(),
169
+ }),
170
+ );
171
+ console.log(
172
+ `[alphaclaw] OpenClaw update marker written to ${markerPath} for ${spec}`,
173
+ );
174
+ kOpenclawVersionCache = { value: previousVersion, fetchedAt: 0 };
175
+ kOpenclawUpdateStatusCache = {
176
+ latestVersion,
177
+ hasUpdate,
178
+ fetchedAt: 0,
179
+ };
242
180
  return {
243
181
  status: 200,
244
182
  body: {
245
183
  ok: true,
246
184
  previousVersion,
247
- currentVersion,
185
+ currentVersion: previousVersion,
186
+ targetVersion: targetVersion === "latest" ? null : targetVersion,
248
187
  latestVersion,
249
- hasUpdate,
250
- restarted,
251
- updated: previousVersion !== currentVersion,
188
+ hasUpdate: true,
189
+ restarted: false,
190
+ restarting: true,
191
+ updated: previousVersion !== targetVersion,
252
192
  },
253
193
  };
254
194
  } catch (err) {
@@ -0,0 +1,274 @@
1
+ const crypto = require("crypto");
2
+ const fs = require("fs");
3
+ const os = require("os");
4
+ const path = require("path");
5
+
6
+ const kIgnoredDirectoryNames = new Set([".git", "node_modules"]);
7
+
8
+ const normalizeRelativePath = (packageRoot, absolutePath) =>
9
+ path.relative(packageRoot, absolutePath).split(path.sep).join("/");
10
+
11
+ const addIncludedPath = ({ includeSet, value }) => {
12
+ const normalizedValue = String(value || "").trim();
13
+ if (!normalizedValue) return;
14
+ includeSet.add(normalizedValue.replace(/\/+$/, ""));
15
+ };
16
+
17
+ const collectIncludedPaths = ({ packageJson = {} } = {}) => {
18
+ const includeSet = new Set(["package.json"]);
19
+
20
+ if (Array.isArray(packageJson.files)) {
21
+ for (const entry of packageJson.files) {
22
+ addIncludedPath({ includeSet, value: entry });
23
+ }
24
+ }
25
+
26
+ if (typeof packageJson.bin === "string") {
27
+ addIncludedPath({ includeSet, value: packageJson.bin });
28
+ } else if (packageJson.bin && typeof packageJson.bin === "object") {
29
+ for (const entry of Object.values(packageJson.bin)) {
30
+ addIncludedPath({ includeSet, value: entry });
31
+ }
32
+ }
33
+
34
+ return Array.from(includeSet).sort((left, right) => left.localeCompare(right));
35
+ };
36
+
37
+ const walkIncludedFiles = ({
38
+ fsModule = fs,
39
+ packageRoot,
40
+ absolutePath,
41
+ files,
42
+ }) => {
43
+ if (!fsModule.existsSync(absolutePath)) return;
44
+ const relativePath = normalizeRelativePath(packageRoot, absolutePath);
45
+ if (!relativePath || relativePath.startsWith("..")) return;
46
+
47
+ const stat = fsModule.lstatSync(absolutePath);
48
+ if (stat.isSymbolicLink()) {
49
+ files.push({
50
+ relativePath,
51
+ hash: `symlink:${fsModule.readlinkSync(absolutePath)}`,
52
+ });
53
+ return;
54
+ }
55
+ if (stat.isFile()) {
56
+ files.push({
57
+ relativePath,
58
+ hash: crypto
59
+ .createHash("sha256")
60
+ .update(fsModule.readFileSync(absolutePath))
61
+ .digest("hex"),
62
+ });
63
+ return;
64
+ }
65
+ if (!stat.isDirectory()) return;
66
+
67
+ const entries = fsModule
68
+ .readdirSync(absolutePath, { withFileTypes: true })
69
+ .sort((left, right) => left.name.localeCompare(right.name));
70
+
71
+ for (const entry of entries) {
72
+ if (entry.isDirectory() && kIgnoredDirectoryNames.has(entry.name)) continue;
73
+ walkIncludedFiles({
74
+ fsModule,
75
+ packageRoot,
76
+ absolutePath: path.join(absolutePath, entry.name),
77
+ files,
78
+ });
79
+ }
80
+ };
81
+
82
+ const computePackageFingerprint = ({
83
+ fsModule = fs,
84
+ packageRoot,
85
+ packageJsonPath = path.join(packageRoot, "package.json"),
86
+ } = {}) => {
87
+ const resolvedPackageRoot = path.resolve(String(packageRoot || ""));
88
+ if (!resolvedPackageRoot || !fsModule.existsSync(packageJsonPath)) return null;
89
+
90
+ let packageJson;
91
+ try {
92
+ packageJson = JSON.parse(fsModule.readFileSync(packageJsonPath, "utf8"));
93
+ } catch {
94
+ return null;
95
+ }
96
+
97
+ const files = [];
98
+ for (const includePath of collectIncludedPaths({ packageJson })) {
99
+ walkIncludedFiles({
100
+ fsModule,
101
+ packageRoot: resolvedPackageRoot,
102
+ absolutePath: path.resolve(resolvedPackageRoot, includePath),
103
+ files,
104
+ });
105
+ }
106
+
107
+ const hash = crypto.createHash("sha256");
108
+ hash.update("package-fingerprint-v1");
109
+ for (const entry of files.sort((left, right) => left.relativePath.localeCompare(right.relativePath))) {
110
+ hash.update(entry.relativePath);
111
+ hash.update("\0");
112
+ hash.update(entry.hash);
113
+ hash.update("\0");
114
+ }
115
+ return hash.digest("hex");
116
+ };
117
+
118
+ const isPackageRootSymlink = ({
119
+ fsModule = fs,
120
+ packageRoot,
121
+ } = {}) => {
122
+ const resolvedPackageRoot = path.resolve(String(packageRoot || ""));
123
+ if (!resolvedPackageRoot || !fsModule.existsSync(resolvedPackageRoot)) return false;
124
+ try {
125
+ return fsModule.lstatSync(resolvedPackageRoot).isSymbolicLink();
126
+ } catch {
127
+ return false;
128
+ }
129
+ };
130
+
131
+ const resolvePackageRootFromEntryPath = ({
132
+ fsModule = fs,
133
+ entryPath,
134
+ } = {}) => {
135
+ let cursor = path.dirname(path.resolve(String(entryPath || "")));
136
+ while (cursor && cursor !== path.dirname(cursor)) {
137
+ if (fsModule.existsSync(path.join(cursor, "package.json"))) {
138
+ return cursor;
139
+ }
140
+ cursor = path.dirname(cursor);
141
+ }
142
+ return null;
143
+ };
144
+
145
+ const resolveInstallRootFromPackageRoot = ({ packageRoot } = {}) => {
146
+ const resolvedPackageRoot = path.resolve(String(packageRoot || ""));
147
+ if (!resolvedPackageRoot) return "";
148
+ const nodeModulesSegment = `${path.sep}node_modules${path.sep}`;
149
+ const nodeModulesIndex = resolvedPackageRoot.lastIndexOf(nodeModulesSegment);
150
+ if (nodeModulesIndex < 0) {
151
+ return resolvedPackageRoot;
152
+ }
153
+ return resolvedPackageRoot.slice(0, nodeModulesIndex);
154
+ };
155
+
156
+ const seedRuntimeFromBundledInstall = ({
157
+ fsModule = fs,
158
+ packageRoot,
159
+ runtimeDir,
160
+ runtimePackageJson,
161
+ } = {}) => {
162
+ const installRoot = resolveInstallRootFromPackageRoot({ packageRoot });
163
+ const bundledNodeModulesPath = path.join(installRoot, "node_modules");
164
+ if (!installRoot || !fsModule.existsSync(bundledNodeModulesPath)) {
165
+ return {
166
+ seeded: false,
167
+ installRoot,
168
+ bundledNodeModulesPath,
169
+ };
170
+ }
171
+
172
+ const resolvedRuntimeDir = path.resolve(String(runtimeDir || ""));
173
+ const runtimeParentDir = path.dirname(resolvedRuntimeDir);
174
+ fsModule.mkdirSync(runtimeParentDir, { recursive: true });
175
+ const tempRuntimeDir = fsModule.mkdtempSync(
176
+ path.join(runtimeParentDir, `${path.basename(resolvedRuntimeDir)}-seed-`),
177
+ );
178
+ let seeded = false;
179
+ try {
180
+ if (runtimePackageJson) {
181
+ fsModule.writeFileSync(
182
+ path.join(tempRuntimeDir, "package.json"),
183
+ JSON.stringify(runtimePackageJson, null, 2),
184
+ );
185
+ }
186
+ fsModule.cpSync(
187
+ bundledNodeModulesPath,
188
+ path.join(tempRuntimeDir, "node_modules"),
189
+ {
190
+ recursive: true,
191
+ dereference: true,
192
+ preserveTimestamps: true,
193
+ },
194
+ );
195
+ try {
196
+ fsModule.rmSync(resolvedRuntimeDir, { recursive: true, force: true });
197
+ } catch {}
198
+ fsModule.renameSync(tempRuntimeDir, resolvedRuntimeDir);
199
+ seeded = true;
200
+ return {
201
+ seeded: true,
202
+ installRoot,
203
+ bundledNodeModulesPath,
204
+ runtimeDir: resolvedRuntimeDir,
205
+ };
206
+ } finally {
207
+ if (!seeded) {
208
+ try {
209
+ fsModule.rmSync(tempRuntimeDir, { recursive: true, force: true });
210
+ } catch {}
211
+ }
212
+ }
213
+ };
214
+
215
+ const packLocalPackageForInstall = ({
216
+ execSyncImpl,
217
+ fsModule = fs,
218
+ packageRoot,
219
+ tempDirPrefix = "alphaclaw-package-pack-",
220
+ } = {}) => {
221
+ const resolvedPackageRoot = path.resolve(String(packageRoot || ""));
222
+ const packDir = fsModule.mkdtempSync(path.join(os.tmpdir(), tempDirPrefix));
223
+ try {
224
+ const packStdout = String(
225
+ execSyncImpl(
226
+ `npm pack ${shellQuote(resolvedPackageRoot)} --quiet --ignore-scripts --pack-destination ${shellQuote(packDir)}`,
227
+ {
228
+ encoding: "utf8",
229
+ stdio: ["ignore", "pipe", "inherit"],
230
+ timeout: 180000,
231
+ },
232
+ ) || "",
233
+ )
234
+ .trim()
235
+ .split(/\r?\n/)
236
+ .map((entry) => entry.trim())
237
+ .filter(Boolean);
238
+ const packFileName =
239
+ packStdout.at(-1) ||
240
+ fsModule.readdirSync(packDir).find((entry) => entry.endsWith(".tgz"));
241
+ if (!packFileName) {
242
+ throw new Error(`npm pack did not produce a tarball for ${resolvedPackageRoot}`);
243
+ }
244
+ const tarballPath = path.join(packDir, packFileName);
245
+ if (!fsModule.existsSync(tarballPath)) {
246
+ throw new Error(`Packed tarball missing at ${tarballPath}`);
247
+ }
248
+ return {
249
+ tarballPath,
250
+ cleanup: () => {
251
+ try {
252
+ fsModule.rmSync(packDir, { recursive: true, force: true });
253
+ } catch {}
254
+ },
255
+ };
256
+ } catch (error) {
257
+ try {
258
+ fsModule.rmSync(packDir, { recursive: true, force: true });
259
+ } catch {}
260
+ throw error;
261
+ }
262
+ };
263
+
264
+ const shellQuote = (value) =>
265
+ `'${String(value || "").replace(/'/g, `'\"'\"'`)}'`;
266
+
267
+ module.exports = {
268
+ computePackageFingerprint,
269
+ isPackageRootSymlink,
270
+ packLocalPackageForInstall,
271
+ resolveInstallRootFromPackageRoot,
272
+ resolvePackageRootFromEntryPath,
273
+ seedRuntimeFromBundledInstall,
274
+ };
@@ -0,0 +1,85 @@
1
+ const path = require("path");
2
+
3
+ const {
4
+ installManagedAlphaclawRuntime,
5
+ } = require("./alphaclaw-runtime");
6
+
7
+ const buildPendingAlphaclawInstallSpec = (marker = {}) => {
8
+ const explicitSpec = String(marker?.spec || "").trim();
9
+ if (explicitSpec) {
10
+ return explicitSpec;
11
+ }
12
+ const targetVersion = String(marker?.to || "").trim() || "latest";
13
+ return `@chrysb/alphaclaw@${targetVersion}`;
14
+ };
15
+
16
+ const applyPendingAlphaclawUpdate = ({
17
+ execSyncImpl,
18
+ fsModule,
19
+ installDir,
20
+ logger = console,
21
+ markerPath,
22
+ }) => {
23
+ if (!fsModule.existsSync(markerPath)) {
24
+ return {
25
+ attempted: false,
26
+ installed: false,
27
+ spec: "",
28
+ };
29
+ }
30
+
31
+ let marker = {};
32
+ try {
33
+ marker = JSON.parse(fsModule.readFileSync(markerPath, "utf8"));
34
+ } catch {
35
+ marker = {};
36
+ }
37
+
38
+ const spec = buildPendingAlphaclawInstallSpec(marker);
39
+ logger.log(`[alphaclaw] Pending update detected, installing ${spec}...`);
40
+
41
+ const resolvedInstallDir = path.resolve(String(installDir || ""));
42
+ const installParentDir = path.dirname(resolvedInstallDir);
43
+ const tempInstallDir = fsModule.mkdtempSync(
44
+ path.join(installParentDir, `${path.basename(resolvedInstallDir)}-pending-`),
45
+ );
46
+
47
+ try {
48
+ installManagedAlphaclawRuntime({
49
+ execSyncImpl,
50
+ fsModule,
51
+ runtimeDir: tempInstallDir,
52
+ spec,
53
+ });
54
+ try {
55
+ fsModule.rmSync(resolvedInstallDir, { recursive: true, force: true });
56
+ } catch {}
57
+ fsModule.renameSync(tempInstallDir, resolvedInstallDir);
58
+ fsModule.unlinkSync(markerPath);
59
+ logger.log("[alphaclaw] Update applied successfully");
60
+ return {
61
+ attempted: true,
62
+ installed: true,
63
+ spec,
64
+ };
65
+ } catch (error) {
66
+ logger.log(`[alphaclaw] Update install failed: ${error.message}`);
67
+ try {
68
+ fsModule.rmSync(tempInstallDir, { recursive: true, force: true });
69
+ } catch {}
70
+ try {
71
+ fsModule.unlinkSync(markerPath);
72
+ } catch {}
73
+ return {
74
+ attempted: true,
75
+ installed: false,
76
+ spec,
77
+ error,
78
+ };
79
+ }
80
+ };
81
+
82
+ module.exports = {
83
+ applyPendingAlphaclawUpdate,
84
+ buildPendingAlphaclawInstallSpec,
85
+ };
@@ -0,0 +1,86 @@
1
+ const path = require("path");
2
+
3
+ const {
4
+ installManagedOpenclawRuntime,
5
+ } = require("./openclaw-runtime");
6
+
7
+ const buildPendingOpenclawInstallSpec = (marker = {}) => {
8
+ const explicitSpec = String(marker?.spec || "").trim();
9
+ if (explicitSpec) {
10
+ return explicitSpec;
11
+ }
12
+ const targetVersion = String(marker?.to || "").trim() || "latest";
13
+ return `openclaw@${targetVersion}`;
14
+ };
15
+
16
+ const applyPendingOpenclawUpdate = ({
17
+ execSyncImpl,
18
+ fsModule,
19
+ installDir,
20
+ logger = console,
21
+ markerPath,
22
+ }) => {
23
+ if (!fsModule.existsSync(markerPath)) {
24
+ return {
25
+ attempted: false,
26
+ installed: false,
27
+ spec: "",
28
+ };
29
+ }
30
+
31
+ let marker = {};
32
+ try {
33
+ marker = JSON.parse(fsModule.readFileSync(markerPath, "utf8"));
34
+ } catch {
35
+ marker = {};
36
+ }
37
+
38
+ const spec = buildPendingOpenclawInstallSpec(marker);
39
+ logger.log(`[alphaclaw] Pending OpenClaw update detected, installing ${spec}...`);
40
+
41
+ const resolvedInstallDir = path.resolve(String(installDir || ""));
42
+ const installParentDir = path.dirname(resolvedInstallDir);
43
+ const tempInstallDir = fsModule.mkdtempSync(
44
+ path.join(installParentDir, `${path.basename(resolvedInstallDir)}-pending-`),
45
+ );
46
+
47
+ try {
48
+ installManagedOpenclawRuntime({
49
+ execSyncImpl,
50
+ fsModule,
51
+ logger,
52
+ runtimeDir: tempInstallDir,
53
+ spec,
54
+ });
55
+ try {
56
+ fsModule.rmSync(resolvedInstallDir, { recursive: true, force: true });
57
+ } catch {}
58
+ fsModule.renameSync(tempInstallDir, resolvedInstallDir);
59
+ fsModule.unlinkSync(markerPath);
60
+ logger.log("[alphaclaw] OpenClaw update applied successfully");
61
+ return {
62
+ attempted: true,
63
+ installed: true,
64
+ spec,
65
+ };
66
+ } catch (error) {
67
+ logger.log(`[alphaclaw] OpenClaw update install failed: ${error.message}`);
68
+ try {
69
+ fsModule.rmSync(tempInstallDir, { recursive: true, force: true });
70
+ } catch {}
71
+ try {
72
+ fsModule.unlinkSync(markerPath);
73
+ } catch {}
74
+ return {
75
+ attempted: true,
76
+ installed: false,
77
+ spec,
78
+ error,
79
+ };
80
+ }
81
+ };
82
+
83
+ module.exports = {
84
+ applyPendingOpenclawUpdate,
85
+ buildPendingOpenclawInstallSpec,
86
+ };