@openclaw/lobster 2026.2.24 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/lobster",
3
- "version": "2026.2.24",
3
+ "version": "2026.3.1",
4
4
  "description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)",
5
5
  "type": "module",
6
6
  "openclaw": {
@@ -17,9 +17,13 @@ const spawnState = vi.hoisted(() => ({
17
17
  spawn: vi.fn(),
18
18
  }));
19
19
 
20
- vi.mock("node:child_process", () => ({
21
- spawn: (...args: unknown[]) => spawnState.spawn(...args),
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
 
@@ -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";
@@ -1,5 +1,8 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
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 resolvedExecPath = resolveWindowsExecutablePath(execPath, env);
174
- const ext = path.extname(resolvedExecPath).toLowerCase();
175
- if (ext !== ".cmd" && ext !== ".bat") {
176
- return { command: resolvedExecPath, argv };
177
- }
178
-
179
- const scriptPath =
180
- resolveLobsterScriptFromCmdShim(resolvedExecPath) ??
181
- resolveLobsterScriptFromPackageJson(resolvedExecPath);
182
- if (!scriptPath) {
183
- throw new Error(
184
- `${path.basename(resolvedExecPath)} wrapper resolved, but no Node entrypoint could be resolved without shell execution. Ensure Lobster is installed and runnable on PATH (prefer lobster.exe).`,
185
- );
186
- }
187
-
188
- const entryExt = path.extname(scriptPath).toLowerCase();
189
- if (entryExt === ".exe") {
190
- return { command: scriptPath, argv, windowsHide: true };
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
  }