@bastani/atomic 0.6.6-0 → 0.6.6
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/.opencode/opencode.json +4 -2
- package/README.md +39 -38
- package/dist/lib/atomic-temp.d.ts +8 -0
- package/dist/lib/atomic-temp.d.ts.map +1 -0
- package/dist/lib/terminal-env.d.ts +9 -0
- package/dist/lib/terminal-env.d.ts.map +1 -0
- package/dist/sdk/providers/claude.d.ts.map +1 -1
- package/dist/sdk/providers/copilot.d.ts +25 -14
- package/dist/sdk/providers/copilot.d.ts.map +1 -1
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/runtime/tmux.d.ts.map +1 -1
- package/package.json +10 -10
- package/src/commands/cli/chat/index.test.ts +194 -2
- package/src/commands/cli/chat/index.ts +89 -28
- package/src/lib/atomic-temp.test.ts +86 -0
- package/src/lib/atomic-temp.ts +62 -0
- package/src/lib/terminal-env.test.ts +343 -0
- package/src/lib/terminal-env.ts +100 -0
- package/src/scripts/clean-dist.test.ts +53 -0
- package/src/scripts/clean-dist.ts +37 -0
- package/src/sdk/providers/claude.ts +42 -20
- package/src/sdk/providers/copilot.test.ts +365 -0
- package/src/sdk/providers/copilot.ts +123 -15
- package/src/sdk/runtime/cc-debounce.ts +2 -2
- package/src/sdk/runtime/executor.test.ts +68 -0
- package/src/sdk/runtime/executor.ts +26 -9
- package/src/sdk/runtime/tmux.ts +6 -2
- package/src/services/system/auth.test.ts +53 -0
- package/src/services/system/auth.ts +31 -28
- package/src/services/system/detect.ts +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* atomic chat -a <agent> [native-args...]
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { join } from "node:path";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { mkdir, writeFile, rm } from "node:fs/promises";
|
|
16
16
|
import { AGENT_CONFIG, type AgentKey } from "../../../services/config/index.ts";
|
|
@@ -19,10 +19,11 @@ import { getCopilotScmDisableFlags } from "../../../services/config/scm-sync.ts"
|
|
|
19
19
|
import {
|
|
20
20
|
resolveAdditionalInstructionsPath,
|
|
21
21
|
} from "../../../services/config/additional-instructions.ts";
|
|
22
|
-
import { dirname } from "node:path";
|
|
23
22
|
import { ensureProjectSetup } from "../init/index.ts";
|
|
24
23
|
import { COLORS } from "../../../theme/colors.ts";
|
|
25
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
getCommandPath,
|
|
26
|
+
} from "../../../services/system/detect.ts";
|
|
26
27
|
import { checkAgentAuth, printAuthError } from "../../../services/system/auth.ts";
|
|
27
28
|
import {
|
|
28
29
|
ensureAtomicGlobalAgentConfigs,
|
|
@@ -42,6 +43,24 @@ import {
|
|
|
42
43
|
} from "../../../sdk/runtime/tmux.ts";
|
|
43
44
|
import { spawnAttachedFooter } from "../../../sdk/runtime/attached-footer.ts";
|
|
44
45
|
import { ensureTmuxInstalled } from "../../../lib/spawn.ts";
|
|
46
|
+
import {
|
|
47
|
+
buildLauncherEnv,
|
|
48
|
+
buildSpawnEnv,
|
|
49
|
+
buildTmuxEnv,
|
|
50
|
+
} from "../../../lib/terminal-env.ts";
|
|
51
|
+
import { atomicTempEnv } from "../../../lib/atomic-temp.ts";
|
|
52
|
+
import {
|
|
53
|
+
type CommandPathResolver,
|
|
54
|
+
resolveCopilotCliPath,
|
|
55
|
+
} from "../../../sdk/providers/copilot.ts";
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
buildLauncherEnv,
|
|
59
|
+
buildSpawnEnv,
|
|
60
|
+
buildTmuxEnv,
|
|
61
|
+
TERMINAL_ENV_KEYS,
|
|
62
|
+
type TerminalEnvKey,
|
|
63
|
+
} from "../../../lib/terminal-env.ts";
|
|
45
64
|
|
|
46
65
|
// ============================================================================
|
|
47
66
|
// Types
|
|
@@ -118,6 +137,18 @@ export function getAdditionalInstructionsDir(
|
|
|
118
137
|
return path ? dirname(path) : undefined;
|
|
119
138
|
}
|
|
120
139
|
|
|
140
|
+
export function resolveChatCommand(
|
|
141
|
+
agentType: AgentType,
|
|
142
|
+
resolveCommandPath: CommandPathResolver = getCommandPath,
|
|
143
|
+
): string | undefined {
|
|
144
|
+
if (agentType === "copilot") {
|
|
145
|
+
return resolveCopilotCliPath(resolveCommandPath);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const config = AGENT_CONFIG[agentType];
|
|
149
|
+
return resolveCommandPath(config.cmd) ?? undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
121
152
|
function generateChatId(): string {
|
|
122
153
|
return crypto.randomUUID().slice(0, 8);
|
|
123
154
|
}
|
|
@@ -132,6 +163,28 @@ function escPwsh(s: string): string {
|
|
|
132
163
|
return s.replace(/[`"$]/g, "`$&");
|
|
133
164
|
}
|
|
134
165
|
|
|
166
|
+
const POSIX_ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
167
|
+
|
|
168
|
+
function assertBashEnvKey(key: string): void {
|
|
169
|
+
if (!POSIX_ENV_KEY_RE.test(key)) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`Invalid Bash env key "${key}": must match /^[A-Za-z_][A-Za-z0-9_]*$/`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function escPwshEnvKey(key: string): string {
|
|
177
|
+
return key.replace(/}/g, "`}");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function removeLauncher(path: string): Promise<void> {
|
|
181
|
+
try {
|
|
182
|
+
await rm(path, { force: true });
|
|
183
|
+
} catch {
|
|
184
|
+
// Cleanup best effort; attach/fallback result should remain authoritative.
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
135
188
|
/**
|
|
136
189
|
* Build a launcher script that preserves cwd and properly quotes args.
|
|
137
190
|
* This avoids shell-injection risks from passthrough args.
|
|
@@ -149,7 +202,7 @@ export function buildLauncherScript(
|
|
|
149
202
|
// PowerShell: use array splatting for safe arg passing
|
|
150
203
|
const argList = args.map((a) => `"${escPwsh(a)}"`).join(", ");
|
|
151
204
|
const envLines = envEntries.map(
|
|
152
|
-
([key, value]) =>
|
|
205
|
+
([key, value]) => `\${env:${escPwshEnvKey(key)}} = "${escPwsh(value)}"`,
|
|
153
206
|
);
|
|
154
207
|
const script = [
|
|
155
208
|
`Set-Location "${escPwsh(projectRoot)}"`,
|
|
@@ -164,18 +217,19 @@ export function buildLauncherScript(
|
|
|
164
217
|
return { script, ext: "ps1" };
|
|
165
218
|
}
|
|
166
219
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
.map((
|
|
170
|
-
|
|
171
|
-
const envLines = envEntries.map(
|
|
172
|
-
(
|
|
173
|
-
|
|
220
|
+
const quotedCommand = [
|
|
221
|
+
`"${escBash(cmd)}"`,
|
|
222
|
+
...args.map((arg) => `"${escBash(arg)}"`),
|
|
223
|
+
].join(" ");
|
|
224
|
+
const envLines = envEntries.map(([key, value]) => {
|
|
225
|
+
assertBashEnvKey(key);
|
|
226
|
+
return `export ${key}="${escBash(value)}"`;
|
|
227
|
+
});
|
|
174
228
|
const script = [
|
|
175
229
|
"#!/bin/bash",
|
|
176
230
|
`cd "${escBash(projectRoot)}"`,
|
|
177
231
|
...envLines,
|
|
178
|
-
|
|
232
|
+
quotedCommand,
|
|
179
233
|
"atomic_exit_code=$?",
|
|
180
234
|
'exit "$atomic_exit_code"',
|
|
181
235
|
].join("\n");
|
|
@@ -206,8 +260,10 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
|
|
|
206
260
|
|
|
207
261
|
const config = AGENT_CONFIG[agentType];
|
|
208
262
|
|
|
263
|
+
const executable = resolveChatCommand(agentType);
|
|
264
|
+
|
|
209
265
|
// Check the agent CLI is installed
|
|
210
|
-
if (!
|
|
266
|
+
if (!executable) {
|
|
211
267
|
console.error(
|
|
212
268
|
`${COLORS.red}Error: '${config.cmd}' is not installed or not in PATH.${COLORS.reset}`
|
|
213
269
|
);
|
|
@@ -236,12 +292,14 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
|
|
|
236
292
|
|
|
237
293
|
// ── Build argv ──
|
|
238
294
|
const args = await buildAgentArgs(agentType, passthroughArgs, projectRoot);
|
|
239
|
-
const cmd = [
|
|
295
|
+
const cmd = [executable, ...args];
|
|
240
296
|
const overrides = await getProviderOverrides(agentType, projectRoot);
|
|
297
|
+
const claudeTempEnv = agentType === "claude" ? atomicTempEnv() : {};
|
|
241
298
|
// ATOMIC_AGENT must be baked into the launcher env so the agent CLI
|
|
242
299
|
// and anything it spawns can read it from process start.
|
|
243
300
|
const envVars: Record<string, string> = {
|
|
244
301
|
...config.env_vars,
|
|
302
|
+
...claudeTempEnv,
|
|
245
303
|
...overrides.envVars,
|
|
246
304
|
ATOMIC_AGENT: agentType,
|
|
247
305
|
};
|
|
@@ -267,9 +325,13 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
|
|
|
267
325
|
}
|
|
268
326
|
}
|
|
269
327
|
|
|
328
|
+
const spawnEnv = buildSpawnEnv(envVars);
|
|
329
|
+
const launcherEnv = buildLauncherEnv(envVars);
|
|
330
|
+
const tmuxEnv = buildTmuxEnv(envVars);
|
|
331
|
+
|
|
270
332
|
// ── No TTY: tmux attach requires a real terminal ──
|
|
271
333
|
if (!process.stdin.isTTY) {
|
|
272
|
-
return spawnDirect(cmd, projectRoot,
|
|
334
|
+
return spawnDirect(cmd, projectRoot, spawnEnv);
|
|
273
335
|
}
|
|
274
336
|
|
|
275
337
|
// ── Ensure tmux is available ──
|
|
@@ -283,7 +345,7 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
|
|
|
283
345
|
}
|
|
284
346
|
if (!isTmuxInstalled()) {
|
|
285
347
|
// No tmux available — fall back to direct spawn
|
|
286
|
-
return spawnDirect(cmd, projectRoot,
|
|
348
|
+
return spawnDirect(cmd, projectRoot, spawnEnv);
|
|
287
349
|
}
|
|
288
350
|
}
|
|
289
351
|
|
|
@@ -294,10 +356,10 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
|
|
|
294
356
|
const sessionsDir = join(homedir(), ".atomic", "sessions", "chat");
|
|
295
357
|
await mkdir(sessionsDir, { recursive: true });
|
|
296
358
|
const { script, ext } = buildLauncherScript(
|
|
297
|
-
|
|
359
|
+
executable,
|
|
298
360
|
args,
|
|
299
361
|
projectRoot,
|
|
300
|
-
|
|
362
|
+
launcherEnv,
|
|
301
363
|
);
|
|
302
364
|
const launcherPath = join(sessionsDir, `${windowName}.${ext}`);
|
|
303
365
|
await writeFile(launcherPath, script, { mode: 0o755 });
|
|
@@ -308,14 +370,14 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
|
|
|
308
370
|
|
|
309
371
|
// ── Create session on the atomic socket and attach ──
|
|
310
372
|
try {
|
|
311
|
-
const paneId = createSession(windowName, shellCmd, undefined, projectRoot);
|
|
373
|
+
const paneId = createSession(windowName, shellCmd, undefined, projectRoot, tmuxEnv);
|
|
312
374
|
spawnAttachedFooter(windowName, paneId, agentType);
|
|
313
375
|
killSessionOnPaneExit(windowName, paneId);
|
|
314
376
|
|
|
315
377
|
if (isInsideAtomicSocket()) {
|
|
316
378
|
// Already on the atomic server — just switch to the new session.
|
|
317
379
|
switchClient(windowName);
|
|
318
|
-
|
|
380
|
+
await removeLauncher(launcherPath);
|
|
319
381
|
return 0;
|
|
320
382
|
}
|
|
321
383
|
|
|
@@ -323,30 +385,29 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
|
|
|
323
385
|
// Inside a different tmux server — detach and replace the client
|
|
324
386
|
// with an attach to the atomic socket (no nesting).
|
|
325
387
|
detachAndAttachAtomic(windowName);
|
|
326
|
-
|
|
388
|
+
await removeLauncher(launcherPath);
|
|
327
389
|
return 0;
|
|
328
390
|
}
|
|
329
391
|
|
|
330
392
|
const attachProc = spawnMuxAttach(windowName);
|
|
331
393
|
const exitCode = await attachProc.exited;
|
|
332
394
|
|
|
333
|
-
|
|
334
|
-
try { await rm(launcherPath, { force: true }); } catch {}
|
|
395
|
+
await removeLauncher(launcherPath);
|
|
335
396
|
|
|
336
397
|
// If tmux attach itself failed (e.g. lost TTY), clean up and fall back
|
|
337
398
|
if (exitCode !== 0) {
|
|
338
399
|
try { killSession(windowName); } catch {}
|
|
339
|
-
return spawnDirect(cmd, projectRoot,
|
|
400
|
+
return spawnDirect(cmd, projectRoot, spawnEnv);
|
|
340
401
|
}
|
|
341
402
|
|
|
342
403
|
return exitCode;
|
|
343
404
|
} catch (error) {
|
|
344
|
-
|
|
405
|
+
await removeLauncher(launcherPath);
|
|
345
406
|
const message = error instanceof Error ? error.message : String(error);
|
|
346
407
|
console.error(
|
|
347
408
|
`${COLORS.yellow}Warning: Failed to create tmux session (${message}). Falling back to direct spawn.${COLORS.reset}`
|
|
348
409
|
);
|
|
349
|
-
return spawnDirect(cmd, projectRoot,
|
|
410
|
+
return spawnDirect(cmd, projectRoot, spawnEnv);
|
|
350
411
|
}
|
|
351
412
|
}
|
|
352
413
|
|
|
@@ -357,12 +418,12 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
|
|
|
357
418
|
async function spawnDirect(
|
|
358
419
|
cmd: string[],
|
|
359
420
|
projectRoot: string,
|
|
360
|
-
|
|
421
|
+
env: Record<string, string> = {},
|
|
361
422
|
): Promise<number> {
|
|
362
423
|
const proc = Bun.spawn(cmd, {
|
|
363
424
|
stdio: ["inherit", "inherit", "inherit"],
|
|
364
425
|
cwd: projectRoot,
|
|
365
|
-
env
|
|
426
|
+
env,
|
|
366
427
|
});
|
|
367
428
|
|
|
368
429
|
return await proc.exited;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync, statSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
atomicContentTempPath,
|
|
7
|
+
atomicTempDir,
|
|
8
|
+
atomicTempEnv,
|
|
9
|
+
atomicTempPath,
|
|
10
|
+
ensureAtomicTempDir,
|
|
11
|
+
withAtomicTempEnv,
|
|
12
|
+
} from "./atomic-temp.ts";
|
|
13
|
+
|
|
14
|
+
const createdDirs: string[] = [];
|
|
15
|
+
|
|
16
|
+
function makeTempRoot(): string {
|
|
17
|
+
const dir = mkdtempSync(join(tmpdir(), "atomic-temp-test-"));
|
|
18
|
+
createdDirs.push(dir);
|
|
19
|
+
return dir;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
for (const dir of createdDirs.splice(0)) {
|
|
24
|
+
rmSync(dir, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("atomic temp helpers", () => {
|
|
29
|
+
test("uses a per-user directory under ~/.atomic/tmp", () => {
|
|
30
|
+
expect(atomicTempDir("/home/alice")).toBe("/home/alice/.atomic/tmp");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("creates the temp directory with owner-only permissions", () => {
|
|
34
|
+
const dir = join(makeTempRoot(), "owned-tmp");
|
|
35
|
+
|
|
36
|
+
expect(ensureAtomicTempDir(dir)).toBe(dir);
|
|
37
|
+
expect(statSync(dir).isDirectory()).toBe(true);
|
|
38
|
+
if (process.platform !== "win32") {
|
|
39
|
+
expect(statSync(dir).mode & 0o777).toBe(0o700);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("builds all Node temp env aliases from the same directory", () => {
|
|
44
|
+
const dir = join(makeTempRoot(), "env-tmp");
|
|
45
|
+
|
|
46
|
+
expect(atomicTempEnv(dir)).toEqual({
|
|
47
|
+
TMPDIR: dir,
|
|
48
|
+
TMP: dir,
|
|
49
|
+
TEMP: dir,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("builds random and content-addressed paths inside the Atomic temp dir", () => {
|
|
54
|
+
const dir = join(makeTempRoot(), "paths");
|
|
55
|
+
|
|
56
|
+
expect(atomicTempPath("prompt", ".txt", "abc", dir)).toBe(
|
|
57
|
+
join(dir, "prompt-abc.txt"),
|
|
58
|
+
);
|
|
59
|
+
expect(atomicContentTempPath("settings", ".json", "same", dir)).toBe(
|
|
60
|
+
atomicContentTempPath("settings", ".json", "same", dir),
|
|
61
|
+
);
|
|
62
|
+
expect(atomicContentTempPath("settings", ".json", "same", dir)).not.toBe(
|
|
63
|
+
atomicContentTempPath("settings", ".json", "different", dir),
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("scopes process temp env while an async operation runs", async () => {
|
|
68
|
+
const dir = join(makeTempRoot(), "scoped");
|
|
69
|
+
const before = {
|
|
70
|
+
TMPDIR: process.env.TMPDIR,
|
|
71
|
+
TMP: process.env.TMP,
|
|
72
|
+
TEMP: process.env.TEMP,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const seen = await withAtomicTempEnv(async () => ({
|
|
76
|
+
TMPDIR: process.env.TMPDIR,
|
|
77
|
+
TMP: process.env.TMP,
|
|
78
|
+
TEMP: process.env.TEMP,
|
|
79
|
+
}), dir);
|
|
80
|
+
|
|
81
|
+
expect(seen).toEqual({ TMPDIR: dir, TMP: dir, TEMP: dir });
|
|
82
|
+
expect(process.env.TMPDIR).toBe(before.TMPDIR);
|
|
83
|
+
expect(process.env.TMP).toBe(before.TMP);
|
|
84
|
+
expect(process.env.TEMP).toBe(before.TEMP);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
export const ATOMIC_TEMP_ENV_KEYS = ["TMPDIR", "TMP", "TEMP"] as const;
|
|
7
|
+
|
|
8
|
+
export function atomicTempDir(homeDir: string = homedir()): string {
|
|
9
|
+
return join(homeDir, ".atomic", "tmp");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ensureAtomicTempDir(dir: string = atomicTempDir()): string {
|
|
13
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
14
|
+
if (process.platform !== "win32") {
|
|
15
|
+
chmodSync(dir, 0o700);
|
|
16
|
+
}
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function atomicTempEnv(dir: string = ensureAtomicTempDir()): Record<string, string> {
|
|
21
|
+
return Object.fromEntries(ATOMIC_TEMP_ENV_KEYS.map((key) => [key, dir]));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function atomicTempPath(
|
|
25
|
+
prefix: string,
|
|
26
|
+
extension: string,
|
|
27
|
+
id: string = randomUUID(),
|
|
28
|
+
dir: string = ensureAtomicTempDir(),
|
|
29
|
+
): string {
|
|
30
|
+
return join(dir, `${prefix}-${id}${extension}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function atomicContentTempPath(
|
|
34
|
+
prefix: string,
|
|
35
|
+
extension: string,
|
|
36
|
+
content: string,
|
|
37
|
+
dir: string = ensureAtomicTempDir(),
|
|
38
|
+
): string {
|
|
39
|
+
const id = createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
40
|
+
return atomicTempPath(prefix, extension, id, dir);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function withAtomicTempEnv<T>(
|
|
44
|
+
fn: () => Promise<T>,
|
|
45
|
+
dir: string = ensureAtomicTempDir(),
|
|
46
|
+
): Promise<T> {
|
|
47
|
+
const previous = Object.fromEntries(
|
|
48
|
+
ATOMIC_TEMP_ENV_KEYS.map((key) => [key, process.env[key]]),
|
|
49
|
+
);
|
|
50
|
+
for (const key of ATOMIC_TEMP_ENV_KEYS) {
|
|
51
|
+
process.env[key] = dir;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
return await fn();
|
|
55
|
+
} finally {
|
|
56
|
+
for (const key of ATOMIC_TEMP_ENV_KEYS) {
|
|
57
|
+
const value = previous[key];
|
|
58
|
+
if (value === undefined) delete process.env[key];
|
|
59
|
+
else process.env[key] = value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|