@openclaw/lobster 2026.2.25 → 2026.3.1
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/package.json +1 -1
- package/src/lobster-tool.test.ts +7 -3
- package/src/test-helpers.ts +1 -14
- package/src/windows-spawn.ts +23 -180
package/package.json
CHANGED
package/src/lobster-tool.test.ts
CHANGED
|
@@ -17,9 +17,13 @@ const spawnState = vi.hoisted(() => ({
|
|
|
17
17
|
spawn: vi.fn(),
|
|
18
18
|
}));
|
|
19
19
|
|
|
20
|
-
vi.mock("node:child_process", () =>
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
vi.mock("node:child_process", async (importOriginal) => {
|
|
21
|
+
const actual = await importOriginal<typeof import("node:child_process")>();
|
|
22
|
+
return {
|
|
23
|
+
...actual,
|
|
24
|
+
spawn: (...args: unknown[]) => spawnState.spawn(...args),
|
|
25
|
+
};
|
|
26
|
+
});
|
|
23
27
|
|
|
24
28
|
let createLobsterTool: typeof import("./lobster-tool.js").createLobsterTool;
|
|
25
29
|
|
package/src/test-helpers.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
1
|
type PathEnvKey = "PATH" | "Path" | "PATHEXT" | "Pathext";
|
|
5
2
|
|
|
6
3
|
const PATH_ENV_KEYS = ["PATH", "Path", "PATHEXT", "Pathext"] as const;
|
|
@@ -43,14 +40,4 @@ export function restorePlatformPathEnv(snapshot: PlatformPathEnvSnapshot): void
|
|
|
43
40
|
process.env[key] = value;
|
|
44
41
|
}
|
|
45
42
|
}
|
|
46
|
-
|
|
47
|
-
export async function createWindowsCmdShimFixture(params: {
|
|
48
|
-
shimPath: string;
|
|
49
|
-
scriptPath: string;
|
|
50
|
-
shimLine: string;
|
|
51
|
-
}): Promise<void> {
|
|
52
|
-
await fs.mkdir(path.dirname(params.scriptPath), { recursive: true });
|
|
53
|
-
await fs.mkdir(path.dirname(params.shimPath), { recursive: true });
|
|
54
|
-
await fs.writeFile(params.scriptPath, "module.exports = {};\n", "utf8");
|
|
55
|
-
await fs.writeFile(params.shimPath, `@echo off\r\n${params.shimLine}\r\n`, "utf8");
|
|
56
|
-
}
|
|
43
|
+
export { createWindowsCmdShimFixture } from "../../shared/windows-cmd-shim-test-fixtures.js";
|
package/src/windows-spawn.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
applyWindowsSpawnProgramPolicy,
|
|
3
|
+
materializeWindowsSpawnProgram,
|
|
4
|
+
resolveWindowsSpawnProgramCandidate,
|
|
5
|
+
} from "openclaw/plugin-sdk";
|
|
3
6
|
|
|
4
7
|
type SpawnTarget = {
|
|
5
8
|
command: string;
|
|
@@ -7,187 +10,27 @@ type SpawnTarget = {
|
|
|
7
10
|
windowsHide?: boolean;
|
|
8
11
|
};
|
|
9
12
|
|
|
10
|
-
function isFilePath(value: string): boolean {
|
|
11
|
-
try {
|
|
12
|
-
const stat = fs.statSync(value);
|
|
13
|
-
return stat.isFile();
|
|
14
|
-
} catch {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function resolveWindowsExecutablePath(execPath: string, env: NodeJS.ProcessEnv): string {
|
|
20
|
-
if (execPath.includes("/") || execPath.includes("\\") || path.isAbsolute(execPath)) {
|
|
21
|
-
return execPath;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const pathValue = env.PATH ?? env.Path ?? process.env.PATH ?? process.env.Path ?? "";
|
|
25
|
-
const pathEntries = pathValue
|
|
26
|
-
.split(";")
|
|
27
|
-
.map((entry) => entry.trim())
|
|
28
|
-
.filter(Boolean);
|
|
29
|
-
|
|
30
|
-
const hasExtension = path.extname(execPath).length > 0;
|
|
31
|
-
const pathExtRaw =
|
|
32
|
-
env.PATHEXT ??
|
|
33
|
-
env.Pathext ??
|
|
34
|
-
process.env.PATHEXT ??
|
|
35
|
-
process.env.Pathext ??
|
|
36
|
-
".EXE;.CMD;.BAT;.COM";
|
|
37
|
-
const pathExt = hasExtension
|
|
38
|
-
? [""]
|
|
39
|
-
: pathExtRaw
|
|
40
|
-
.split(";")
|
|
41
|
-
.map((ext) => ext.trim())
|
|
42
|
-
.filter(Boolean)
|
|
43
|
-
.map((ext) => (ext.startsWith(".") ? ext : `.${ext}`));
|
|
44
|
-
|
|
45
|
-
for (const dir of pathEntries) {
|
|
46
|
-
for (const ext of pathExt) {
|
|
47
|
-
for (const candidateExt of [ext, ext.toLowerCase(), ext.toUpperCase()]) {
|
|
48
|
-
const candidate = path.join(dir, `${execPath}${candidateExt}`);
|
|
49
|
-
if (isFilePath(candidate)) {
|
|
50
|
-
return candidate;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return execPath;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function resolveBinEntry(binField: string | Record<string, string> | undefined): string | null {
|
|
60
|
-
if (typeof binField === "string") {
|
|
61
|
-
const trimmed = binField.trim();
|
|
62
|
-
return trimmed || null;
|
|
63
|
-
}
|
|
64
|
-
if (!binField || typeof binField !== "object") {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const preferred = binField.lobster;
|
|
69
|
-
if (typeof preferred === "string" && preferred.trim()) {
|
|
70
|
-
return preferred.trim();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
for (const value of Object.values(binField)) {
|
|
74
|
-
if (typeof value === "string" && value.trim()) {
|
|
75
|
-
return value.trim();
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function resolveLobsterScriptFromPackageJson(wrapperPath: string): string | null {
|
|
82
|
-
const wrapperDir = path.dirname(wrapperPath);
|
|
83
|
-
const packageDirs = [
|
|
84
|
-
// Local install: <repo>/node_modules/.bin/lobster.cmd -> ../lobster
|
|
85
|
-
path.resolve(wrapperDir, "..", "lobster"),
|
|
86
|
-
// Global npm install: <npm-prefix>/lobster.cmd -> ./node_modules/lobster
|
|
87
|
-
path.resolve(wrapperDir, "node_modules", "lobster"),
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
for (const packageDir of packageDirs) {
|
|
91
|
-
const packageJsonPath = path.join(packageDir, "package.json");
|
|
92
|
-
if (!isFilePath(packageJsonPath)) {
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
|
|
98
|
-
bin?: string | Record<string, string>;
|
|
99
|
-
};
|
|
100
|
-
const scriptRel = resolveBinEntry(packageJson.bin);
|
|
101
|
-
if (!scriptRel) {
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
const scriptPath = path.resolve(packageDir, scriptRel);
|
|
105
|
-
if (isFilePath(scriptPath)) {
|
|
106
|
-
return scriptPath;
|
|
107
|
-
}
|
|
108
|
-
} catch {
|
|
109
|
-
// Ignore malformed package metadata; caller will throw a guided error.
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function resolveLobsterScriptFromCmdShim(wrapperPath: string): string | null {
|
|
117
|
-
if (!isFilePath(wrapperPath)) {
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
const content = fs.readFileSync(wrapperPath, "utf8");
|
|
123
|
-
const candidates: string[] = [];
|
|
124
|
-
const extractRelativeFromToken = (token: string): string | null => {
|
|
125
|
-
const match = token.match(/%~?dp0%\s*[\\/]*(.*)$/i);
|
|
126
|
-
if (!match) {
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
const relative = match[1];
|
|
130
|
-
if (!relative) {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
return relative;
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
const matches = content.matchAll(/"([^"\r\n]*)"/g);
|
|
137
|
-
for (const match of matches) {
|
|
138
|
-
const token = match[1] ?? "";
|
|
139
|
-
const relative = extractRelativeFromToken(token);
|
|
140
|
-
if (!relative) {
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const normalizedRelative = relative
|
|
145
|
-
.trim()
|
|
146
|
-
.replace(/[\\/]+/g, path.sep)
|
|
147
|
-
.replace(/^[\\/]+/, "");
|
|
148
|
-
const candidate = path.resolve(path.dirname(wrapperPath), normalizedRelative);
|
|
149
|
-
if (isFilePath(candidate)) {
|
|
150
|
-
candidates.push(candidate);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const nonNode = candidates.find((candidate) => {
|
|
155
|
-
const base = path.basename(candidate).toLowerCase();
|
|
156
|
-
return base !== "node.exe" && base !== "node";
|
|
157
|
-
});
|
|
158
|
-
if (nonNode) {
|
|
159
|
-
return nonNode;
|
|
160
|
-
}
|
|
161
|
-
} catch {
|
|
162
|
-
// Ignore unreadable shims; caller will throw a guided error.
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
13
|
export function resolveWindowsLobsterSpawn(
|
|
169
14
|
execPath: string,
|
|
170
15
|
argv: string[],
|
|
171
16
|
env: NodeJS.ProcessEnv,
|
|
172
17
|
): SpawnTarget {
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
return { command: process.execPath, argv: [scriptPath, ...argv], windowsHide: true };
|
|
18
|
+
const candidate = resolveWindowsSpawnProgramCandidate({
|
|
19
|
+
command: execPath,
|
|
20
|
+
env,
|
|
21
|
+
packageName: "lobster",
|
|
22
|
+
});
|
|
23
|
+
const program = applyWindowsSpawnProgramPolicy({
|
|
24
|
+
candidate,
|
|
25
|
+
allowShellFallback: false,
|
|
26
|
+
});
|
|
27
|
+
const resolved = materializeWindowsSpawnProgram(program, argv);
|
|
28
|
+
if (resolved.shell) {
|
|
29
|
+
throw new Error("lobster wrapper resolved to shell fallback unexpectedly");
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
command: resolved.command,
|
|
33
|
+
argv: resolved.argv,
|
|
34
|
+
windowsHide: resolved.windowsHide,
|
|
35
|
+
};
|
|
193
36
|
}
|