Skip to content

Commit 7d33761

Browse files
committed
Install managed runtimes from tarballs
1 parent aa61f0a commit 7d33761

File tree

5 files changed

+206
-30
lines changed

5 files changed

+206
-30
lines changed

lib/server/alphaclaw-runtime.js

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ const path = require("path");
33

44
const { kRootDir } = require("./constants");
55
const { compareVersionParts } = require("./helpers");
6-
const { computePackageFingerprint } = require("./package-fingerprint");
6+
const {
7+
computePackageFingerprint,
8+
packLocalPackageForInstall,
9+
} = require("./package-fingerprint");
710

811
const getManagedAlphaclawRuntimeDir = ({ rootDir = kRootDir } = {}) =>
912
path.join(rootDir, ".alphaclaw-runtime");
@@ -104,14 +107,30 @@ const installManagedAlphaclawRuntime = ({
104107
fsModule,
105108
runtimeDir,
106109
});
107-
execSyncImpl(
108-
`npm install ${shellQuote(normalizedSpec)} --omit=dev --no-save --save=false --package-lock=false --prefer-online`,
109-
{
110-
cwd: runtimeDir,
111-
stdio: "inherit",
112-
timeout: 180000,
113-
},
114-
);
110+
let packedSource = null;
111+
try {
112+
const installTarget = normalizedSourcePath
113+
? (() => {
114+
packedSource = packLocalPackageForInstall({
115+
execSyncImpl,
116+
fsModule,
117+
packageRoot: normalizedSourcePath,
118+
tempDirPrefix: "alphaclaw-runtime-pack-",
119+
});
120+
return packedSource.tarballPath;
121+
})()
122+
: normalizedSpec;
123+
execSyncImpl(
124+
`npm install ${shellQuote(installTarget)} --omit=dev --no-save --save=false --package-lock=false --prefer-online`,
125+
{
126+
cwd: runtimeDir,
127+
stdio: "inherit",
128+
timeout: 180000,
129+
},
130+
);
131+
} finally {
132+
packedSource?.cleanup?.();
133+
}
115134
return {
116135
spec: normalizedSpec,
117136
version: readManagedAlphaclawRuntimeVersion({

lib/server/openclaw-runtime.js

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,23 @@ const {
66
compareVersionParts,
77
normalizeOpenclawVersion,
88
} = require("./helpers");
9-
const { computePackageFingerprint } = require("./package-fingerprint");
9+
const {
10+
computePackageFingerprint,
11+
packLocalPackageForInstall,
12+
resolvePackageRootFromEntryPath,
13+
} = require("./package-fingerprint");
1014

1115
const getManagedOpenclawRuntimeDir = ({ rootDir = kRootDir } = {}) =>
1216
path.join(rootDir, ".openclaw-runtime");
1317

1418
const getBundledOpenclawPackageRoot = ({
19+
fsModule = fs,
1520
resolveImpl = require.resolve,
16-
} = {}) => path.dirname(resolveImpl("openclaw/package.json"));
21+
} = {}) =>
22+
resolvePackageRootFromEntryPath({
23+
fsModule,
24+
entryPath: resolveImpl("openclaw"),
25+
});
1726

1827
const getManagedOpenclawPackageRoot = ({ runtimeDir } = {}) =>
1928
path.join(
@@ -86,8 +95,14 @@ const readBundledOpenclawVersion = ({
8695
resolveImpl = require.resolve,
8796
} = {}) => {
8897
try {
89-
const pkgPath = resolveImpl("openclaw/package.json");
90-
const pkg = JSON.parse(fsModule.readFileSync(pkgPath, "utf8"));
98+
const packageRoot = getBundledOpenclawPackageRoot({
99+
fsModule,
100+
resolveImpl,
101+
});
102+
if (!packageRoot) return null;
103+
const pkg = JSON.parse(
104+
fsModule.readFileSync(path.join(packageRoot, "package.json"), "utf8"),
105+
);
91106
return normalizeOpenclawVersion(pkg?.version || "");
92107
} catch {
93108
return null;
@@ -157,14 +172,30 @@ const installManagedOpenclawRuntime = ({
157172
fsModule,
158173
runtimeDir,
159174
});
160-
execSyncImpl(
161-
`npm install ${shellQuote(normalizedSpec)} --omit=dev --no-save --save=false --package-lock=false --prefer-online`,
162-
{
163-
cwd: runtimeDir,
164-
stdio: "inherit",
165-
timeout: 180000,
166-
},
167-
);
175+
let packedSource = null;
176+
try {
177+
const installTarget = normalizedSourcePath
178+
? (() => {
179+
packedSource = packLocalPackageForInstall({
180+
execSyncImpl,
181+
fsModule,
182+
packageRoot: normalizedSourcePath,
183+
tempDirPrefix: "openclaw-runtime-pack-",
184+
});
185+
return packedSource.tarballPath;
186+
})()
187+
: normalizedSpec;
188+
execSyncImpl(
189+
`npm install ${shellQuote(installTarget)} --omit=dev --no-save --save=false --package-lock=false --prefer-online`,
190+
{
191+
cwd: runtimeDir,
192+
stdio: "inherit",
193+
timeout: 180000,
194+
},
195+
);
196+
} finally {
197+
packedSource?.cleanup?.();
198+
}
168199
const installedVersion = readManagedOpenclawRuntimeVersion({
169200
fsModule,
170201
runtimeDir,
@@ -191,7 +222,10 @@ const syncManagedOpenclawRuntimeWithBundled = ({
191222
resolveImpl,
192223
alphaclawRoot,
193224
} = {}) => {
194-
const bundledPackageRoot = getBundledOpenclawPackageRoot({ resolveImpl });
225+
const bundledPackageRoot = getBundledOpenclawPackageRoot({
226+
fsModule,
227+
resolveImpl,
228+
});
195229
const bundledVersion = readBundledOpenclawVersion({
196230
fsModule,
197231
resolveImpl,

lib/server/package-fingerprint.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const crypto = require("crypto");
22
const fs = require("fs");
3+
const os = require("os");
34
const path = require("path");
45

56
const kIgnoredDirectoryNames = new Set([".git", "node_modules"]);
@@ -114,6 +115,74 @@ const computePackageFingerprint = ({
114115
return hash.digest("hex");
115116
};
116117

118+
const resolvePackageRootFromEntryPath = ({
119+
fsModule = fs,
120+
entryPath,
121+
} = {}) => {
122+
let cursor = path.dirname(path.resolve(String(entryPath || "")));
123+
while (cursor && cursor !== path.dirname(cursor)) {
124+
if (fsModule.existsSync(path.join(cursor, "package.json"))) {
125+
return cursor;
126+
}
127+
cursor = path.dirname(cursor);
128+
}
129+
return null;
130+
};
131+
132+
const packLocalPackageForInstall = ({
133+
execSyncImpl,
134+
fsModule = fs,
135+
packageRoot,
136+
tempDirPrefix = "alphaclaw-package-pack-",
137+
} = {}) => {
138+
const resolvedPackageRoot = path.resolve(String(packageRoot || ""));
139+
const packDir = fsModule.mkdtempSync(path.join(os.tmpdir(), tempDirPrefix));
140+
try {
141+
const packStdout = String(
142+
execSyncImpl(
143+
`npm pack ${shellQuote(resolvedPackageRoot)} --quiet --ignore-scripts --pack-destination ${shellQuote(packDir)}`,
144+
{
145+
encoding: "utf8",
146+
stdio: ["ignore", "pipe", "inherit"],
147+
timeout: 180000,
148+
},
149+
) || "",
150+
)
151+
.trim()
152+
.split(/\r?\n/)
153+
.map((entry) => entry.trim())
154+
.filter(Boolean);
155+
const packFileName =
156+
packStdout.at(-1) ||
157+
fsModule.readdirSync(packDir).find((entry) => entry.endsWith(".tgz"));
158+
if (!packFileName) {
159+
throw new Error(`npm pack did not produce a tarball for ${resolvedPackageRoot}`);
160+
}
161+
const tarballPath = path.join(packDir, packFileName);
162+
if (!fsModule.existsSync(tarballPath)) {
163+
throw new Error(`Packed tarball missing at ${tarballPath}`);
164+
}
165+
return {
166+
tarballPath,
167+
cleanup: () => {
168+
try {
169+
fsModule.rmSync(packDir, { recursive: true, force: true });
170+
} catch {}
171+
},
172+
};
173+
} catch (error) {
174+
try {
175+
fsModule.rmSync(packDir, { recursive: true, force: true });
176+
} catch {}
177+
throw error;
178+
}
179+
};
180+
181+
const shellQuote = (value) =>
182+
`'${String(value || "").replace(/'/g, `'\"'\"'`)}'`;
183+
117184
module.exports = {
118185
computePackageFingerprint,
186+
packLocalPackageForInstall,
187+
resolvePackageRootFromEntryPath,
119188
};

tests/server/alphaclaw-runtime.test.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ const writeAlphaclawPackage = ({
4646
);
4747
};
4848

49+
const parsePackDestination = (command) => {
50+
const match = String(command || "").match(/--pack-destination '([^']+)'/);
51+
return match ? match[1] : "";
52+
};
53+
4954
describe("server/alphaclaw-runtime", () => {
5055
let tmpDir;
5156

@@ -156,6 +161,13 @@ describe("server/alphaclaw-runtime", () => {
156161
version: "0.8.9",
157162
});
158163
const execSyncImpl = vi.fn((command, options) => {
164+
if (String(command).startsWith("npm pack ")) {
165+
const packDestination = parsePackDestination(command);
166+
const tarballPath = path.join(packDestination, "alphaclaw-runtime.tgz");
167+
fs.mkdirSync(packDestination, { recursive: true });
168+
fs.writeFileSync(tarballPath, "tarball");
169+
return "alphaclaw-runtime.tgz\n";
170+
}
159171
writeAlphaclawPackage({
160172
packageRoot: getManagedAlphaclawPackageRoot({ runtimeDir: options.cwd }),
161173
version: "0.8.9",
@@ -177,8 +189,9 @@ describe("server/alphaclaw-runtime", () => {
177189
bundledVersion: "0.8.9",
178190
runtimeVersion: "0.8.9",
179191
});
192+
expect(execSyncImpl.mock.calls[0][0]).toContain(`npm pack '${bundleDir}'`);
180193
expect(execSyncImpl).toHaveBeenCalledWith(
181-
`npm install '${bundleDir}' --omit=dev --no-save --save=false --package-lock=false --prefer-online`,
194+
expect.stringMatching(/npm install '.*alphaclaw-runtime\.tgz' --omit=dev --no-save --save=false --package-lock=false --prefer-online/),
182195
{
183196
cwd: runtimeDir,
184197
stdio: "inherit",
@@ -202,6 +215,13 @@ describe("server/alphaclaw-runtime", () => {
202215
usageTrackerBody: "module.exports = 'old';\n",
203216
});
204217
const execSyncImpl = vi.fn((command, options) => {
218+
if (String(command).startsWith("npm pack ")) {
219+
const packDestination = parsePackDestination(command);
220+
const tarballPath = path.join(packDestination, "alphaclaw-runtime.tgz");
221+
fs.mkdirSync(packDestination, { recursive: true });
222+
fs.writeFileSync(tarballPath, "tarball");
223+
return "alphaclaw-runtime.tgz\n";
224+
}
205225
writeAlphaclawPackage({
206226
packageRoot: getManagedAlphaclawPackageRoot({ runtimeDir: options.cwd }),
207227
version: "0.8.9",
@@ -224,8 +244,9 @@ describe("server/alphaclaw-runtime", () => {
224244
bundledVersion: "0.8.9",
225245
runtimeVersion: "0.8.9",
226246
});
247+
expect(execSyncImpl.mock.calls[0][0]).toContain(`npm pack '${bundleDir}'`);
227248
expect(execSyncImpl).toHaveBeenCalledWith(
228-
`npm install '${bundleDir}' --omit=dev --no-save --save=false --package-lock=false --prefer-online`,
249+
expect.stringMatching(/npm install '.*alphaclaw-runtime\.tgz' --omit=dev --no-save --save=false --package-lock=false --prefer-online/),
229250
{
230251
cwd: runtimeDir,
231252
stdio: "inherit",

0 commit comments

Comments
 (0)