@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
|
@@ -62,11 +62,13 @@ import {
|
|
|
62
62
|
HeadlessClaudeSessionWrapper,
|
|
63
63
|
} from "../providers/claude.ts";
|
|
64
64
|
import { withHeadlessOpencodeEnv } from "../providers/opencode.ts";
|
|
65
|
+
import { resolveCopilotCliPath } from "../providers/copilot.ts";
|
|
65
66
|
import { OrchestratorPanel } from "./panel.tsx";
|
|
66
67
|
import { GraphFrontierTracker } from "./graph-inference.ts";
|
|
67
68
|
import { buildSnapshot, writeSnapshot } from "./status-writer.ts";
|
|
68
69
|
import { errorMessage } from "../errors.ts";
|
|
69
70
|
import { createPainter } from "../../theme/colors.ts";
|
|
71
|
+
import { atomicTempEnv } from "../../lib/atomic-temp.ts";
|
|
70
72
|
|
|
71
73
|
/** Maximum time (ms) for the SDK probe to succeed after port is discovered. */
|
|
72
74
|
export const SERVER_PROBE_TIMEOUT_MS = 60_000;
|
|
@@ -287,29 +289,37 @@ export function buildPaneCommand(
|
|
|
287
289
|
envVars: defaultEnvVars,
|
|
288
290
|
} = AGENT_CLI[agent];
|
|
289
291
|
const chatFlags = overrides.chatFlags ?? defaultFlags;
|
|
292
|
+
const claudeTempEnv = agent === "claude" ? atomicTempEnv() : {};
|
|
290
293
|
const envVars = overrides.envVars
|
|
291
294
|
? { ...defaultEnvVars, ...overrides.envVars }
|
|
292
295
|
: defaultEnvVars;
|
|
296
|
+
const mergedEnvVars = { ...envVars, ...claudeTempEnv, ...overrides.envVars };
|
|
293
297
|
|
|
294
298
|
const resolvedCmd = quotePathIfNeeded(resolveCliBinary(cmd));
|
|
295
299
|
|
|
296
300
|
switch (agent) {
|
|
297
|
-
case "copilot":
|
|
301
|
+
case "copilot": {
|
|
302
|
+
// Prefer the copilot binary resolved via resolveCopilotCliPath so that
|
|
303
|
+
// COPILOT_CLI_PATH (set by applyContainerEnvDefaults in Bun-without-node
|
|
304
|
+
// environments) is honoured in the tmux pane command, keeping the pane
|
|
305
|
+
// binary consistent with the SDK subprocess binary.
|
|
306
|
+
const copilotBin = resolveCopilotCliPath() ?? resolveCliBinary(cmd);
|
|
298
307
|
return {
|
|
299
308
|
command: [
|
|
300
|
-
|
|
309
|
+
quotePathIfNeeded(copilotBin),
|
|
301
310
|
"--ui-server",
|
|
302
311
|
"--port",
|
|
303
312
|
"0",
|
|
304
313
|
...chatFlags,
|
|
305
314
|
...extraChatFlags,
|
|
306
315
|
].join(" "),
|
|
307
|
-
envVars,
|
|
316
|
+
envVars: mergedEnvVars,
|
|
308
317
|
};
|
|
318
|
+
}
|
|
309
319
|
case "opencode":
|
|
310
320
|
return {
|
|
311
321
|
command: [resolvedCmd, "--port", "0", ...chatFlags].join(" "),
|
|
312
|
-
envVars,
|
|
322
|
+
envVars: mergedEnvVars,
|
|
313
323
|
};
|
|
314
324
|
case "claude": {
|
|
315
325
|
// Claude is started via createClaudeSession() in the workflow's run().
|
|
@@ -323,7 +333,7 @@ export function buildPaneCommand(
|
|
|
323
333
|
: resolveCliBinary(shellCandidate);
|
|
324
334
|
return {
|
|
325
335
|
command: quotePathIfNeeded(resolvedShell),
|
|
326
|
-
envVars,
|
|
336
|
+
envVars: mergedEnvVars,
|
|
327
337
|
};
|
|
328
338
|
}
|
|
329
339
|
default:
|
|
@@ -494,6 +504,7 @@ export async function executeWorkflow(
|
|
|
494
504
|
const launcherExt = isWin ? "ps1" : "sh";
|
|
495
505
|
const launcherPath = join(sessionsBaseDir, `orchestrator.${launcherExt}`);
|
|
496
506
|
const logPath = join(sessionsBaseDir, "orchestrator.log");
|
|
507
|
+
const launcherEnvVars = agent === "claude" ? atomicTempEnv() : {};
|
|
497
508
|
|
|
498
509
|
// Inputs are passed through as base64-encoded JSON so long multiline
|
|
499
510
|
// text values survive shell quoting without any further escaping.
|
|
@@ -515,6 +526,9 @@ export async function executeWorkflow(
|
|
|
515
526
|
const launcherScript = isWin
|
|
516
527
|
? [
|
|
517
528
|
`Set-Location "${escPwsh(projectRoot)}"`,
|
|
529
|
+
...Object.entries(launcherEnvVars).map(
|
|
530
|
+
([key, value]) => `$env:${key} = "${escPwsh(value)}"`,
|
|
531
|
+
),
|
|
518
532
|
`$env:ATOMIC_WF_ID = "${escPwsh(workflowRunId)}"`,
|
|
519
533
|
`$env:ATOMIC_WF_TMUX = "${escPwsh(tmuxSessionName)}"`,
|
|
520
534
|
`$env:ATOMIC_WF_AGENT = "${escPwsh(agent)}"`,
|
|
@@ -524,6 +538,9 @@ export async function executeWorkflow(
|
|
|
524
538
|
: [
|
|
525
539
|
"#!/bin/bash",
|
|
526
540
|
`cd "${escBash(projectRoot)}"`,
|
|
541
|
+
...Object.entries(launcherEnvVars).map(
|
|
542
|
+
([key, value]) => `export ${key}="${escBash(value)}"`,
|
|
543
|
+
),
|
|
527
544
|
`export ATOMIC_WF_ID="${escBash(workflowRunId)}"`,
|
|
528
545
|
`export ATOMIC_WF_TMUX="${escBash(tmuxSessionName)}"`,
|
|
529
546
|
`export ATOMIC_WF_AGENT="${escBash(agent)}"`,
|
|
@@ -536,7 +553,7 @@ export async function executeWorkflow(
|
|
|
536
553
|
const shellCmd = isWin
|
|
537
554
|
? `pwsh -NoProfile -File "${escPwsh(launcherPath)}"`
|
|
538
555
|
: `bash "${escBash(launcherPath)}"`;
|
|
539
|
-
tmux.createSession(tmuxSessionName, shellCmd, "orchestrator");
|
|
556
|
+
tmux.createSession(tmuxSessionName, shellCmd, "orchestrator", undefined, launcherEnvVars);
|
|
540
557
|
tmux.setSessionEnv(tmuxSessionName, "ATOMIC_AGENT", agent);
|
|
541
558
|
|
|
542
559
|
if (detach) {
|
|
@@ -1322,7 +1339,7 @@ async function initProviderClientAndSession<A extends AgentType>(
|
|
|
1322
1339
|
switch (agent) {
|
|
1323
1340
|
case "copilot": {
|
|
1324
1341
|
const { CopilotClient, approveAll } = await import("@github/copilot-sdk");
|
|
1325
|
-
const {
|
|
1342
|
+
const { copilotSdkLaunchOptions, mergeCopilotSystemMessage } =
|
|
1326
1343
|
await import("../providers/copilot.ts");
|
|
1327
1344
|
const { resolveAdditionalInstructionsContent } =
|
|
1328
1345
|
await import("../../services/config/additional-instructions.ts");
|
|
@@ -1331,7 +1348,7 @@ async function initProviderClientAndSession<A extends AgentType>(
|
|
|
1331
1348
|
// Headless: let the SDK spawn its own CLI process (no cliUrl).
|
|
1332
1349
|
// Non-headless: connect to the CLI server running in a tmux pane.
|
|
1333
1350
|
// `env` is only meaningful in the headless path — the SDK ignores
|
|
1334
|
-
// it when `cliUrl` is set — but layering in `
|
|
1351
|
+
// it when `cliUrl` is set — but layering in `copilotSdkLaunchOptions`
|
|
1335
1352
|
// when the caller didn't supply their own env keeps the
|
|
1336
1353
|
// SQLite `ExperimentalWarning` from leaking through the SDK's
|
|
1337
1354
|
// `[CLI subprocess]` stderr forwarder.
|
|
@@ -1339,7 +1356,7 @@ async function initProviderClientAndSession<A extends AgentType>(
|
|
|
1339
1356
|
let client: InstanceType<typeof CopilotClient>;
|
|
1340
1357
|
if (headless) {
|
|
1341
1358
|
client = new CopilotClient({
|
|
1342
|
-
|
|
1359
|
+
...copilotSdkLaunchOptions(),
|
|
1343
1360
|
...copilotClientOpts,
|
|
1344
1361
|
});
|
|
1345
1362
|
} else {
|
package/src/sdk/runtime/tmux.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { requiredMuxBinaryCandidatesForPlatform } from "../../lib/spawn.ts";
|
|
11
11
|
import { writeFileSync, unlinkSync } from "node:fs";
|
|
12
|
-
import { tmpdir } from "node:os";
|
|
13
12
|
import type { Subprocess } from "bun";
|
|
13
|
+
import { atomicTempPath } from "../../lib/atomic-temp.ts";
|
|
14
14
|
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
// Constants
|
|
@@ -353,7 +353,11 @@ export function sendLiteralText(paneId: string, text: string): void {
|
|
|
353
353
|
*/
|
|
354
354
|
export function sendViaPasteBuffer(paneId: string, text: string): void {
|
|
355
355
|
const normalized = text.replace(/[\r\n]+/g, " ");
|
|
356
|
-
const tmp =
|
|
356
|
+
const tmp = atomicTempPath(
|
|
357
|
+
"atomic-paste",
|
|
358
|
+
".txt",
|
|
359
|
+
`${process.pid}-${Date.now()}`,
|
|
360
|
+
);
|
|
357
361
|
|
|
358
362
|
writeFileSync(tmp, normalized, "utf-8");
|
|
359
363
|
try {
|
|
@@ -38,7 +38,13 @@ let copilotGetAuthStatus = mock<() => Promise<CopilotAuthStatus>>(async () => ({
|
|
|
38
38
|
login: "octocat",
|
|
39
39
|
}));
|
|
40
40
|
|
|
41
|
+
// Captures the options passed to `new CopilotClient(...)` on each call.
|
|
42
|
+
let lastCopilotConstructorOptions: unknown = undefined;
|
|
43
|
+
|
|
41
44
|
class FakeCopilotClient {
|
|
45
|
+
constructor(options: unknown) {
|
|
46
|
+
lastCopilotConstructorOptions = options;
|
|
47
|
+
}
|
|
42
48
|
async start(): Promise<void> {
|
|
43
49
|
await copilotStart();
|
|
44
50
|
}
|
|
@@ -114,6 +120,7 @@ afterAll(() => {
|
|
|
114
120
|
const { checkAgentAuth, printAuthError } = await import("./auth.ts");
|
|
115
121
|
|
|
116
122
|
beforeEach(() => {
|
|
123
|
+
lastCopilotConstructorOptions = undefined;
|
|
117
124
|
copilotStart.mockClear();
|
|
118
125
|
copilotStart.mockImplementation(async () => {});
|
|
119
126
|
copilotStop.mockClear();
|
|
@@ -173,6 +180,52 @@ describe("checkAgentAuth(copilot)", () => {
|
|
|
173
180
|
// The stop failure must not shadow the probe result.
|
|
174
181
|
expect(result.detail).not.toContain("stop crashed");
|
|
175
182
|
});
|
|
183
|
+
|
|
184
|
+
test("constructs CopilotClient with COPILOT_CLI_PATH as cliPath and NODE_NO_WARNINGS=1 in env", async () => {
|
|
185
|
+
const origCliPath = process.env["COPILOT_CLI_PATH"];
|
|
186
|
+
process.env["COPILOT_CLI_PATH"] = "/explicit/bin/copilot";
|
|
187
|
+
try {
|
|
188
|
+
await checkAgentAuth("copilot");
|
|
189
|
+
const opts = lastCopilotConstructorOptions as {
|
|
190
|
+
cliPath?: string;
|
|
191
|
+
env?: Record<string, string | undefined>;
|
|
192
|
+
};
|
|
193
|
+
expect(opts).toBeDefined();
|
|
194
|
+
expect(opts.cliPath).toBe("/explicit/bin/copilot");
|
|
195
|
+
expect(opts.env?.["NODE_NO_WARNINGS"]).toBe("1");
|
|
196
|
+
} finally {
|
|
197
|
+
if (origCliPath === undefined) delete process.env["COPILOT_CLI_PATH"];
|
|
198
|
+
else process.env["COPILOT_CLI_PATH"] = origCliPath;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("constructs CopilotClient via centralized launch options — env.NODE_NO_WARNINGS=1 present even when COPILOT_CLI_PATH is unset", async () => {
|
|
203
|
+
// When COPILOT_CLI_PATH is absent the auth probe delegates entirely to
|
|
204
|
+
// copilotSdkLaunchOptions(), which always injects NODE_NO_WARNINGS=1
|
|
205
|
+
// via copilotSubprocessEnv(). No cliPath should appear in the options
|
|
206
|
+
// because resolveCopilotCliPath() returns undefined when the env var is
|
|
207
|
+
// not set and no standalone binary is found on PATH.
|
|
208
|
+
const origCliPath = process.env["COPILOT_CLI_PATH"];
|
|
209
|
+
delete process.env["COPILOT_CLI_PATH"];
|
|
210
|
+
try {
|
|
211
|
+
await checkAgentAuth("copilot");
|
|
212
|
+
const opts = lastCopilotConstructorOptions as {
|
|
213
|
+
cliPath?: string;
|
|
214
|
+
env?: Record<string, string | undefined>;
|
|
215
|
+
};
|
|
216
|
+
expect(opts).toBeDefined();
|
|
217
|
+
// Centralized env must always include NODE_NO_WARNINGS regardless of cliPath.
|
|
218
|
+
expect(opts.env?.["NODE_NO_WARNINGS"]).toBe("1");
|
|
219
|
+
// cliPath must not be injected from a stale or undefined resolution.
|
|
220
|
+
// (It may be defined if a copilot binary happens to be on PATH in the
|
|
221
|
+
// test environment, but it must never be "/explicit/bin/copilot" which
|
|
222
|
+
// is only set by the sibling test above.)
|
|
223
|
+
expect(opts.cliPath).not.toBe("/explicit/bin/copilot");
|
|
224
|
+
} finally {
|
|
225
|
+
if (origCliPath === undefined) delete process.env["COPILOT_CLI_PATH"];
|
|
226
|
+
else process.env["COPILOT_CLI_PATH"] = origCliPath;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
176
229
|
});
|
|
177
230
|
|
|
178
231
|
describe("checkAgentAuth(claude)", () => {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
import type { AgentKey } from "../config/index.ts";
|
|
19
19
|
import { COLORS } from "../../theme/colors.ts";
|
|
20
|
-
import {
|
|
20
|
+
import { copilotSdkLaunchOptions } from "../../sdk/providers/copilot.ts";
|
|
21
|
+
import { withAtomicTempEnv } from "../../lib/atomic-temp.ts";
|
|
21
22
|
|
|
22
23
|
export interface AuthCheckResult {
|
|
23
24
|
/** True when the SDK reports the user is authenticated. */
|
|
@@ -72,7 +73,7 @@ const AUTH_PROMPTS: Record<AgentKey, { name: string; loginHint: string }> = {
|
|
|
72
73
|
|
|
73
74
|
async function checkCopilotAuth(): Promise<AuthCheckResult> {
|
|
74
75
|
const { CopilotClient } = await import("@github/copilot-sdk");
|
|
75
|
-
const client = new CopilotClient(
|
|
76
|
+
const client = new CopilotClient(copilotSdkLaunchOptions());
|
|
76
77
|
try {
|
|
77
78
|
await client.start();
|
|
78
79
|
const status = await client.getAuthStatus();
|
|
@@ -105,33 +106,35 @@ async function checkClaudeAuth(): Promise<AuthCheckResult> {
|
|
|
105
106
|
// actually delivered.
|
|
106
107
|
async function* emptyStream(): AsyncGenerator<never, void, void> {}
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
return await withAtomicTempEnv(async () => {
|
|
110
|
+
const q = query({
|
|
111
|
+
prompt: emptyStream(),
|
|
112
|
+
options: {
|
|
113
|
+
pathToClaudeCodeExecutable: resolveHeadlessClaudeBin(),
|
|
114
|
+
},
|
|
115
|
+
});
|
|
114
116
|
|
|
115
|
-
try {
|
|
116
|
-
const init = await q.initializationResult();
|
|
117
|
-
const account = init.account ?? {};
|
|
118
|
-
const loggedIn = Boolean(
|
|
119
|
-
account.email || account.tokenSource || account.apiKeySource,
|
|
120
|
-
);
|
|
121
|
-
return {
|
|
122
|
-
loggedIn,
|
|
123
|
-
identity: account.email,
|
|
124
|
-
};
|
|
125
|
-
} catch (err) {
|
|
126
|
-
return {
|
|
127
|
-
loggedIn: false,
|
|
128
|
-
detail: err instanceof Error ? err.message : String(err),
|
|
129
|
-
};
|
|
130
|
-
} finally {
|
|
131
117
|
try {
|
|
132
|
-
q.
|
|
133
|
-
|
|
134
|
-
|
|
118
|
+
const init = await q.initializationResult();
|
|
119
|
+
const account = init.account ?? {};
|
|
120
|
+
const loggedIn = Boolean(
|
|
121
|
+
account.email || account.tokenSource || account.apiKeySource,
|
|
122
|
+
);
|
|
123
|
+
return {
|
|
124
|
+
loggedIn,
|
|
125
|
+
identity: account.email,
|
|
126
|
+
};
|
|
127
|
+
} catch (err) {
|
|
128
|
+
return {
|
|
129
|
+
loggedIn: false,
|
|
130
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
131
|
+
};
|
|
132
|
+
} finally {
|
|
133
|
+
try {
|
|
134
|
+
q.close();
|
|
135
|
+
} catch {
|
|
136
|
+
// Best effort — the subprocess is torn down on process exit anyway.
|
|
137
|
+
}
|
|
135
138
|
}
|
|
136
|
-
}
|
|
139
|
+
});
|
|
137
140
|
}
|
|
@@ -24,7 +24,7 @@ export function isCommandInstalled(cmd: string): boolean {
|
|
|
24
24
|
* @returns The absolute path to the command, or null if not found
|
|
25
25
|
*/
|
|
26
26
|
export function getCommandPath(cmd: string): string | null {
|
|
27
|
-
return Bun.which(cmd);
|
|
27
|
+
return Bun.which(cmd, { PATH: process.env["PATH"] ?? "" });
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|