@openclaw/lobster 2026.3.13 → 2026.5.2-beta.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.
@@ -1,213 +1,216 @@
1
- import { spawn } from "node:child_process";
2
- import path from "node:path";
3
- import { Type } from "@sinclair/typebox";
4
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk/lobster";
5
- import { resolveWindowsLobsterSpawn } from "./windows-spawn.js";
6
-
7
- type LobsterEnvelope =
8
- | {
9
- ok: true;
10
- status: "ok" | "needs_approval" | "cancelled";
11
- output: unknown[];
12
- requiresApproval: null | {
13
- type: "approval_request";
14
- prompt: string;
15
- items: unknown[];
16
- resumeToken?: string;
17
- };
18
- }
1
+ import { Type } from "typebox";
2
+ import type { OpenClawPluginApi } from "../runtime-api.js";
3
+ import {
4
+ createEmbeddedLobsterRunner,
5
+ resolveLobsterCwd,
6
+ type LobsterRunner,
7
+ type LobsterRunnerParams,
8
+ } from "./lobster-runner.js";
9
+ import {
10
+ type ManagedLobsterFlowResult,
11
+ resumeManagedLobsterFlow,
12
+ runManagedLobsterFlow,
13
+ } from "./lobster-taskflow.js";
14
+
15
+ type BoundTaskFlow = ReturnType<
16
+ NonNullable<OpenClawPluginApi["runtime"]>["tasks"]["managedFlows"]["bindSession"]
17
+ >;
18
+
19
+ type JsonLike =
20
+ | null
21
+ | boolean
22
+ | number
23
+ | string
24
+ | JsonLike[]
19
25
  | {
20
- ok: false;
21
- error: { type?: string; message: string };
26
+ [key: string]: JsonLike;
22
27
  };
23
28
 
24
- function normalizeForCwdSandbox(p: string): string {
25
- const normalized = path.normalize(p);
26
- return process.platform === "win32" ? normalized.toLowerCase() : normalized;
29
+ type LobsterToolOptions = {
30
+ runner?: LobsterRunner;
31
+ taskFlow?: BoundTaskFlow;
32
+ };
33
+
34
+ type ManagedFlowRunParams = {
35
+ controllerId: string;
36
+ goal: string;
37
+ currentStep?: string;
38
+ waitingStep?: string;
39
+ stateJson?: JsonLike;
40
+ };
41
+
42
+ type ManagedFlowResumeParams = {
43
+ flowId: string;
44
+ expectedRevision: number;
45
+ currentStep?: string;
46
+ waitingStep?: string;
47
+ };
48
+
49
+ type ManagedFlowSuccessResult = {
50
+ ok: true;
51
+ envelope: unknown;
52
+ flow: unknown;
53
+ mutation: unknown;
54
+ };
55
+
56
+ function readOptionalTrimmedString(value: unknown, fieldName: string): string | undefined {
57
+ if (value === undefined) {
58
+ return undefined;
59
+ }
60
+ if (typeof value !== "string") {
61
+ throw new Error(`${fieldName} must be a string`);
62
+ }
63
+ const trimmed = value.trim();
64
+ return trimmed ? trimmed : undefined;
27
65
  }
28
66
 
29
- function resolveCwd(cwdRaw: unknown): string {
30
- if (typeof cwdRaw !== "string" || !cwdRaw.trim()) {
31
- return process.cwd();
67
+ function readOptionalNumber(value: unknown, fieldName: string): number | undefined {
68
+ if (value === undefined) {
69
+ return undefined;
32
70
  }
33
- const cwd = cwdRaw.trim();
34
- if (path.isAbsolute(cwd)) {
35
- throw new Error("cwd must be a relative path");
71
+ if (typeof value !== "number" || !Number.isInteger(value)) {
72
+ throw new Error(`${fieldName} must be an integer`);
36
73
  }
37
- const base = process.cwd();
38
- const resolved = path.resolve(base, cwd);
74
+ return value;
75
+ }
39
76
 
40
- const rel = path.relative(normalizeForCwdSandbox(base), normalizeForCwdSandbox(resolved));
41
- if (rel === "" || rel === ".") {
42
- return resolved;
77
+ function readOptionalBoolean(value: unknown, fieldName: string): boolean | undefined {
78
+ if (value === undefined) {
79
+ return undefined;
43
80
  }
44
- if (rel.startsWith("..") || path.isAbsolute(rel)) {
45
- throw new Error("cwd must stay within the gateway working directory");
81
+ if (typeof value !== "boolean") {
82
+ throw new Error(`${fieldName} must be a boolean`);
46
83
  }
47
- return resolved;
84
+ return value;
48
85
  }
49
86
 
50
- async function runLobsterSubprocessOnce(params: {
51
- execPath: string;
52
- argv: string[];
53
- cwd: string;
54
- timeoutMs: number;
55
- maxStdoutBytes: number;
56
- }) {
57
- const { execPath, argv, cwd } = params;
58
- const timeoutMs = Math.max(200, params.timeoutMs);
59
- const maxStdoutBytes = Math.max(1024, params.maxStdoutBytes);
60
-
61
- const env = { ...process.env, LOBSTER_MODE: "tool" } as Record<string, string | undefined>;
62
- const nodeOptions = env.NODE_OPTIONS ?? "";
63
- if (nodeOptions.includes("--inspect")) {
64
- delete env.NODE_OPTIONS;
87
+ function parseOptionalFlowStateJson(value: unknown): JsonLike | undefined {
88
+ if (value === undefined) {
89
+ return undefined;
90
+ }
91
+ if (typeof value !== "string") {
92
+ throw new Error("flowStateJson must be a JSON string");
93
+ }
94
+ try {
95
+ return JSON.parse(value) as JsonLike;
96
+ } catch {
97
+ throw new Error("flowStateJson must be valid JSON");
65
98
  }
66
- const spawnTarget =
67
- process.platform === "win32"
68
- ? resolveWindowsLobsterSpawn(execPath, argv, env)
69
- : { command: execPath, argv };
70
-
71
- return await new Promise<{ stdout: string }>((resolve, reject) => {
72
- const child = spawn(spawnTarget.command, spawnTarget.argv, {
73
- cwd,
74
- stdio: ["ignore", "pipe", "pipe"],
75
- env,
76
- windowsHide: spawnTarget.windowsHide,
77
- });
78
-
79
- let stdout = "";
80
- let stdoutBytes = 0;
81
- let stderr = "";
82
- let settled = false;
83
-
84
- const settle = (
85
- result: { ok: true; value: { stdout: string } } | { ok: false; error: Error },
86
- ) => {
87
- if (settled) {
88
- return;
89
- }
90
- settled = true;
91
- clearTimeout(timer);
92
- if (result.ok) {
93
- resolve(result.value);
94
- } else {
95
- reject(result.error);
96
- }
97
- };
98
-
99
- const failAndTerminate = (message: string) => {
100
- try {
101
- child.kill("SIGKILL");
102
- } finally {
103
- settle({ ok: false, error: new Error(message) });
104
- }
105
- };
106
-
107
- child.stdout?.setEncoding("utf8");
108
- child.stderr?.setEncoding("utf8");
109
-
110
- child.stdout?.on("data", (chunk) => {
111
- const str = String(chunk);
112
- stdoutBytes += Buffer.byteLength(str, "utf8");
113
- if (stdoutBytes > maxStdoutBytes) {
114
- failAndTerminate("lobster output exceeded maxStdoutBytes");
115
- return;
116
- }
117
- stdout += str;
118
- });
119
-
120
- child.stderr?.on("data", (chunk) => {
121
- stderr += String(chunk);
122
- });
123
-
124
- const timer = setTimeout(() => {
125
- failAndTerminate("lobster subprocess timed out");
126
- }, timeoutMs);
127
-
128
- child.once("error", (err) => {
129
- settle({ ok: false, error: err });
130
- });
131
-
132
- child.once("exit", (code) => {
133
- if (code !== 0) {
134
- settle({
135
- ok: false,
136
- error: new Error(`lobster failed (${code ?? "?"}): ${stderr.trim() || stdout.trim()}`),
137
- });
138
- return;
139
- }
140
- settle({ ok: true, value: { stdout } });
141
- });
142
- });
143
99
  }
144
100
 
145
- function parseEnvelope(stdout: string): LobsterEnvelope {
146
- const trimmed = stdout.trim();
147
-
148
- const tryParse = (input: string) => {
149
- try {
150
- return JSON.parse(input) as unknown;
151
- } catch {
152
- return undefined;
153
- }
101
+ function parseRunFlowParams(params: Record<string, unknown>): ManagedFlowRunParams | null {
102
+ const controllerId = readOptionalTrimmedString(params.flowControllerId, "flowControllerId");
103
+ const goal = readOptionalTrimmedString(params.flowGoal, "flowGoal");
104
+ const currentStep = readOptionalTrimmedString(params.flowCurrentStep, "flowCurrentStep");
105
+ const waitingStep = readOptionalTrimmedString(params.flowWaitingStep, "flowWaitingStep");
106
+ const stateJson = parseOptionalFlowStateJson(params.flowStateJson);
107
+ const resumeFlowId = readOptionalTrimmedString(params.flowId, "flowId");
108
+ const resumeRevision = readOptionalNumber(params.flowExpectedRevision, "flowExpectedRevision");
109
+
110
+ const hasRunFields =
111
+ controllerId !== undefined ||
112
+ goal !== undefined ||
113
+ currentStep !== undefined ||
114
+ waitingStep !== undefined ||
115
+ stateJson !== undefined;
116
+
117
+ if (!hasRunFields) {
118
+ return null;
119
+ }
120
+ if (resumeFlowId !== undefined || resumeRevision !== undefined) {
121
+ throw new Error("run action does not accept flowId or flowExpectedRevision");
122
+ }
123
+ if (!controllerId) {
124
+ throw new Error("flowControllerId required when using managed TaskFlow run mode");
125
+ }
126
+ if (!goal) {
127
+ throw new Error("flowGoal required when using managed TaskFlow run mode");
128
+ }
129
+ return {
130
+ controllerId,
131
+ goal,
132
+ ...(currentStep ? { currentStep } : {}),
133
+ ...(waitingStep ? { waitingStep } : {}),
134
+ ...(stateJson !== undefined ? { stateJson } : {}),
154
135
  };
136
+ }
155
137
 
156
- let parsed: unknown = tryParse(trimmed);
157
-
158
- // Some environments can leak extra stdout (e.g. warnings/logs) before the
159
- // final JSON envelope. Be tolerant and parse the last JSON-looking suffix.
160
- if (parsed === undefined) {
161
- const suffixMatch = trimmed.match(/({[\s\S]*}|\[[\s\S]*])\s*$/);
162
- if (suffixMatch?.[1]) {
163
- parsed = tryParse(suffixMatch[1]);
164
- }
138
+ function parseResumeFlowParams(params: Record<string, unknown>): ManagedFlowResumeParams | null {
139
+ const flowId = readOptionalTrimmedString(params.flowId, "flowId");
140
+ const expectedRevision = readOptionalNumber(params.flowExpectedRevision, "flowExpectedRevision");
141
+ const currentStep = readOptionalTrimmedString(params.flowCurrentStep, "flowCurrentStep");
142
+ const waitingStep = readOptionalTrimmedString(params.flowWaitingStep, "flowWaitingStep");
143
+ const token = readOptionalTrimmedString(params.token, "token");
144
+ const approvalId = readOptionalTrimmedString(params.approvalId, "approvalId");
145
+ const approve = readOptionalBoolean(params.approve, "approve");
146
+ const runControllerId = readOptionalTrimmedString(params.flowControllerId, "flowControllerId");
147
+ const runGoal = readOptionalTrimmedString(params.flowGoal, "flowGoal");
148
+ const stateJson = params.flowStateJson;
149
+
150
+ const hasResumeFields =
151
+ flowId !== undefined ||
152
+ expectedRevision !== undefined ||
153
+ currentStep !== undefined ||
154
+ waitingStep !== undefined;
155
+
156
+ if (!hasResumeFields) {
157
+ return null;
165
158
  }
166
-
167
- if (parsed === undefined) {
168
- throw new Error("lobster returned invalid JSON");
159
+ if (runControllerId !== undefined || runGoal !== undefined || stateJson !== undefined) {
160
+ throw new Error("resume action does not accept flowControllerId, flowGoal, or flowStateJson");
169
161
  }
170
-
171
- if (!parsed || typeof parsed !== "object") {
172
- throw new Error("lobster returned invalid JSON envelope");
162
+ if (!flowId) {
163
+ throw new Error("flowId required when using managed TaskFlow resume mode");
173
164
  }
174
-
175
- const ok = (parsed as { ok?: unknown }).ok;
176
- if (ok === true || ok === false) {
177
- return parsed as LobsterEnvelope;
165
+ if (expectedRevision === undefined) {
166
+ throw new Error("flowExpectedRevision required when using managed TaskFlow resume mode");
178
167
  }
168
+ if (!token && !approvalId) {
169
+ throw new Error("token or approvalId required when using managed TaskFlow resume mode");
170
+ }
171
+ if (approve === undefined) {
172
+ throw new Error("approve required when using managed TaskFlow resume mode");
173
+ }
174
+ return {
175
+ flowId,
176
+ expectedRevision,
177
+ ...(currentStep ? { currentStep } : {}),
178
+ ...(waitingStep ? { waitingStep } : {}),
179
+ };
180
+ }
179
181
 
180
- throw new Error("lobster returned invalid JSON envelope");
182
+ function formatManagedFlowResult(result: ManagedFlowSuccessResult) {
183
+ const envelope =
184
+ result.envelope && typeof result.envelope === "object" && !Array.isArray(result.envelope)
185
+ ? result.envelope
186
+ : { envelope: result.envelope };
187
+ const details = {
188
+ ...envelope,
189
+ flow: result.flow,
190
+ mutation: result.mutation,
191
+ };
192
+ return {
193
+ content: [{ type: "text", text: JSON.stringify(details, null, 2) }],
194
+ details,
195
+ };
181
196
  }
182
197
 
183
- function buildLobsterArgv(action: string, params: Record<string, unknown>): string[] {
184
- if (action === "run") {
185
- const pipeline = typeof params.pipeline === "string" ? params.pipeline : "";
186
- if (!pipeline.trim()) {
187
- throw new Error("pipeline required");
188
- }
189
- const argv = ["run", "--mode", "tool", pipeline];
190
- const argsJson = typeof params.argsJson === "string" ? params.argsJson : "";
191
- if (argsJson.trim()) {
192
- argv.push("--args-json", argsJson);
193
- }
194
- return argv;
198
+ function requireTaskFlowRuntime(taskFlow: BoundTaskFlow | undefined, action: "run" | "resume") {
199
+ if (!taskFlow) {
200
+ throw new Error(`Managed TaskFlow ${action} mode requires a bound taskFlow runtime`);
195
201
  }
196
- if (action === "resume") {
197
- const token = typeof params.token === "string" ? params.token : "";
198
- if (!token.trim()) {
199
- throw new Error("token required");
200
- }
201
- const approve = params.approve;
202
- if (typeof approve !== "boolean") {
203
- throw new Error("approve required");
204
- }
205
- return ["resume", "--token", token, "--approve", approve ? "yes" : "no"];
202
+ return taskFlow;
203
+ }
204
+
205
+ function resolveManagedFlowToolResult(result: ManagedLobsterFlowResult) {
206
+ if (!result.ok) {
207
+ throw result.error;
206
208
  }
207
- throw new Error(`Unknown action: ${action}`);
209
+ return formatManagedFlowResult(result);
208
210
  }
209
211
 
210
- export function createLobsterTool(api: OpenClawPluginApi) {
212
+ export function createLobsterTool(api: OpenClawPluginApi, options?: LobsterToolOptions) {
213
+ const runner = options?.runner ?? createEmbeddedLobsterRunner();
211
214
  return {
212
215
  name: "lobster",
213
216
  label: "Lobster Workflow",
@@ -219,6 +222,7 @@ export function createLobsterTool(api: OpenClawPluginApi) {
219
222
  pipeline: Type.Optional(Type.String()),
220
223
  argsJson: Type.Optional(Type.String()),
221
224
  token: Type.Optional(Type.String()),
225
+ approvalId: Type.Optional(Type.String()),
222
226
  approve: Type.Optional(Type.Boolean()),
223
227
  cwd: Type.Optional(
224
228
  Type.String({
@@ -228,35 +232,85 @@ export function createLobsterTool(api: OpenClawPluginApi) {
228
232
  ),
229
233
  timeoutMs: Type.Optional(Type.Number()),
230
234
  maxStdoutBytes: Type.Optional(Type.Number()),
235
+ flowControllerId: Type.Optional(Type.String()),
236
+ flowGoal: Type.Optional(Type.String()),
237
+ flowStateJson: Type.Optional(Type.String()),
238
+ flowId: Type.Optional(Type.String()),
239
+ flowExpectedRevision: Type.Optional(Type.Number()),
240
+ flowCurrentStep: Type.Optional(Type.String()),
241
+ flowWaitingStep: Type.Optional(Type.String()),
231
242
  }),
232
243
  async execute(_id: string, params: Record<string, unknown>) {
233
244
  const action = typeof params.action === "string" ? params.action.trim() : "";
234
245
  if (!action) {
235
246
  throw new Error("action required");
236
247
  }
248
+ if (action !== "run" && action !== "resume") {
249
+ throw new Error(`Unknown action: ${action}`);
250
+ }
237
251
 
238
- const execPath = "lobster";
239
- const cwd = resolveCwd(params.cwd);
252
+ const cwd = resolveLobsterCwd(params.cwd);
240
253
  const timeoutMs = typeof params.timeoutMs === "number" ? params.timeoutMs : 20_000;
241
254
  const maxStdoutBytes =
242
255
  typeof params.maxStdoutBytes === "number" ? params.maxStdoutBytes : 512_000;
243
256
 
244
- const argv = buildLobsterArgv(action, params);
245
-
246
257
  if (api.runtime?.version && api.logger?.debug) {
247
258
  api.logger.debug(`lobster plugin runtime=${api.runtime.version}`);
248
259
  }
249
260
 
250
- const { stdout } = await runLobsterSubprocessOnce({
251
- execPath,
252
- argv,
261
+ const runnerParams: LobsterRunnerParams = {
262
+ action,
263
+ ...(typeof params.pipeline === "string" ? { pipeline: params.pipeline } : {}),
264
+ ...(typeof params.argsJson === "string" ? { argsJson: params.argsJson } : {}),
265
+ ...(typeof params.token === "string" ? { token: params.token } : {}),
266
+ ...(typeof params.approvalId === "string" ? { approvalId: params.approvalId } : {}),
267
+ ...(typeof params.approve === "boolean" ? { approve: params.approve } : {}),
253
268
  cwd,
254
269
  timeoutMs,
255
270
  maxStdoutBytes,
256
- });
271
+ };
257
272
 
258
- const envelope = parseEnvelope(stdout);
273
+ const taskFlow = options?.taskFlow;
274
+ if (action === "run") {
275
+ const flowParams = parseRunFlowParams(params);
276
+ if (flowParams) {
277
+ return resolveManagedFlowToolResult(
278
+ await runManagedLobsterFlow({
279
+ taskFlow: requireTaskFlowRuntime(taskFlow, "run"),
280
+ runner,
281
+ runnerParams,
282
+ controllerId: flowParams.controllerId,
283
+ goal: flowParams.goal,
284
+ ...(flowParams.stateJson !== undefined ? { stateJson: flowParams.stateJson } : {}),
285
+ ...(flowParams.currentStep ? { currentStep: flowParams.currentStep } : {}),
286
+ ...(flowParams.waitingStep ? { waitingStep: flowParams.waitingStep } : {}),
287
+ }),
288
+ );
289
+ }
290
+ } else {
291
+ const flowParams = parseResumeFlowParams(params);
292
+ if (flowParams) {
293
+ return resolveManagedFlowToolResult(
294
+ await resumeManagedLobsterFlow({
295
+ taskFlow: requireTaskFlowRuntime(taskFlow, "resume"),
296
+ runner,
297
+ runnerParams: runnerParams as LobsterRunnerParams & {
298
+ action: "resume";
299
+ approve: boolean;
300
+ } & ({ token: string } | { approvalId: string }),
301
+ flowId: flowParams.flowId,
302
+ expectedRevision: flowParams.expectedRevision,
303
+ ...(flowParams.currentStep ? { currentStep: flowParams.currentStep } : {}),
304
+ ...(flowParams.waitingStep ? { waitingStep: flowParams.waitingStep } : {}),
305
+ }),
306
+ );
307
+ }
308
+ }
259
309
 
310
+ const envelope = await runner.run(runnerParams);
311
+ if (!envelope.ok) {
312
+ throw new Error(envelope.error.message);
313
+ }
260
314
  return {
261
315
  content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }],
262
316
  details: envelope,
@@ -0,0 +1,48 @@
1
+ import { vi } from "vitest";
2
+ import type { OpenClawPluginApi } from "../runtime-api.js";
3
+
4
+ type BoundTaskFlow = ReturnType<
5
+ NonNullable<OpenClawPluginApi["runtime"]>["tasks"]["managedFlows"]["bindSession"]
6
+ >;
7
+
8
+ export function createFakeTaskFlow(overrides?: Partial<BoundTaskFlow>): BoundTaskFlow {
9
+ const baseFlow = {
10
+ flowId: "flow-1",
11
+ revision: 1,
12
+ syncMode: "managed" as const,
13
+ controllerId: "tests/lobster",
14
+ ownerKey: "agent:main:main",
15
+ status: "running" as const,
16
+ goal: "Run Lobster workflow",
17
+ };
18
+
19
+ return {
20
+ sessionKey: "agent:main:main",
21
+ createManaged: vi.fn().mockReturnValue(baseFlow),
22
+ get: vi.fn(),
23
+ list: vi.fn().mockReturnValue([]),
24
+ findLatest: vi.fn(),
25
+ resolve: vi.fn(),
26
+ getTaskSummary: vi.fn(),
27
+ setWaiting: vi.fn().mockImplementation((input) => ({
28
+ applied: true,
29
+ flow: { ...baseFlow, revision: input.expectedRevision + 1, status: "waiting" as const },
30
+ })),
31
+ resume: vi.fn().mockImplementation((input) => ({
32
+ applied: true,
33
+ flow: { ...baseFlow, revision: input.expectedRevision + 1, status: "running" as const },
34
+ })),
35
+ finish: vi.fn().mockImplementation((input) => ({
36
+ applied: true,
37
+ flow: { ...baseFlow, revision: input.expectedRevision + 1, status: "completed" as const },
38
+ })),
39
+ fail: vi.fn().mockImplementation((input) => ({
40
+ applied: true,
41
+ flow: { ...baseFlow, revision: input.expectedRevision + 1, status: "failed" as const },
42
+ })),
43
+ requestCancel: vi.fn(),
44
+ cancel: vi.fn(),
45
+ runTask: vi.fn(),
46
+ ...overrides,
47
+ };
48
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "../tsconfig.package-boundary.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "."
5
+ },
6
+ "include": ["./*.ts", "./src/**/*.ts"],
7
+ "exclude": [
8
+ "./**/*.test.ts",
9
+ "./dist/**",
10
+ "./node_modules/**",
11
+ "./src/test-support/**",
12
+ "./src/**/*test-helpers.ts",
13
+ "./src/**/*test-harness.ts",
14
+ "./src/**/*test-support.ts"
15
+ ]
16
+ }
@@ -1,43 +0,0 @@
1
- type PathEnvKey = "PATH" | "Path" | "PATHEXT" | "Pathext";
2
-
3
- const PATH_ENV_KEYS = ["PATH", "Path", "PATHEXT", "Pathext"] as const;
4
-
5
- export type PlatformPathEnvSnapshot = {
6
- platformDescriptor: PropertyDescriptor | undefined;
7
- env: Record<PathEnvKey, string | undefined>;
8
- };
9
-
10
- export function setProcessPlatform(platform: NodeJS.Platform): void {
11
- Object.defineProperty(process, "platform", {
12
- value: platform,
13
- configurable: true,
14
- });
15
- }
16
-
17
- export function snapshotPlatformPathEnv(): PlatformPathEnvSnapshot {
18
- return {
19
- platformDescriptor: Object.getOwnPropertyDescriptor(process, "platform"),
20
- env: {
21
- PATH: process.env.PATH,
22
- Path: process.env.Path,
23
- PATHEXT: process.env.PATHEXT,
24
- Pathext: process.env.Pathext,
25
- },
26
- };
27
- }
28
-
29
- export function restorePlatformPathEnv(snapshot: PlatformPathEnvSnapshot): void {
30
- if (snapshot.platformDescriptor) {
31
- Object.defineProperty(process, "platform", snapshot.platformDescriptor);
32
- }
33
-
34
- for (const key of PATH_ENV_KEYS) {
35
- const value = snapshot.env[key];
36
- if (value === undefined) {
37
- delete process.env[key];
38
- continue;
39
- }
40
- process.env[key] = value;
41
- }
42
- }
43
- export { createWindowsCmdShimFixture } from "../../shared/windows-cmd-shim-test-fixtures.js";