@bastani/atomic 0.6.5 → 0.6.6-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/.agents/skills/ado-commit/SKILL.md +2 -0
- package/.agents/skills/ado-create-pr/SKILL.md +2 -0
- package/.agents/skills/advanced-evaluation/SKILL.md +2 -0
- package/.agents/skills/ast-grep/SKILL.md +2 -0
- package/.agents/skills/bdi-mental-states/SKILL.md +2 -0
- package/.agents/skills/bun/SKILL.md +156 -122
- package/.agents/skills/context-compression/SKILL.md +2 -0
- package/.agents/skills/context-degradation/SKILL.md +2 -0
- package/.agents/skills/context-fundamentals/SKILL.md +2 -0
- package/.agents/skills/context-optimization/SKILL.md +2 -0
- package/.agents/skills/create-spec/SKILL.md +2 -0
- package/.agents/skills/docx/SKILL.md +2 -0
- package/.agents/skills/evaluation/SKILL.md +2 -0
- package/.agents/skills/explain-code/SKILL.md +2 -0
- package/.agents/skills/filesystem-context/SKILL.md +2 -0
- package/.agents/skills/find-skills/SKILL.md +2 -0
- package/.agents/skills/gh-commit/SKILL.md +2 -0
- package/.agents/skills/gh-create-pr/SKILL.md +2 -0
- package/.agents/skills/hosted-agents/SKILL.md +2 -0
- package/.agents/skills/impeccable/SKILL.md +117 -304
- package/.agents/skills/impeccable/agents/openai.yaml +4 -0
- package/.agents/skills/{adapt/SKILL.md → impeccable/reference/adapt.md} +2 -11
- package/.agents/skills/{animate/SKILL.md → impeccable/reference/animate.md} +15 -15
- package/.agents/skills/{audit/SKILL.md → impeccable/reference/audit.md} +8 -22
- package/.agents/skills/{bolder/SKILL.md → impeccable/reference/bolder.md} +9 -13
- package/.agents/skills/impeccable/reference/brand.md +114 -0
- package/.agents/skills/{clarify/SKILL.md → impeccable/reference/clarify.md} +2 -11
- package/.agents/skills/{colorize/SKILL.md → impeccable/reference/colorize.md} +23 -12
- package/.agents/skills/impeccable/reference/craft.md +152 -29
- package/.agents/skills/{critique/SKILL.md → impeccable/reference/critique.md} +25 -37
- package/.agents/skills/{delight/SKILL.md → impeccable/reference/delight.md} +9 -11
- package/.agents/skills/{distill/SKILL.md → impeccable/reference/distill.md} +2 -13
- package/.agents/skills/impeccable/reference/document.md +427 -0
- package/.agents/skills/impeccable/reference/extract.md +1 -1
- package/.agents/skills/{harden/SKILL.md → impeccable/reference/harden.md} +1 -43
- package/.agents/skills/{layout/SKILL.md → impeccable/reference/layout.md} +27 -11
- package/.agents/skills/impeccable/reference/live.md +594 -0
- package/.agents/skills/impeccable/reference/motion-design.md +12 -2
- package/.agents/skills/impeccable/reference/onboard.md +234 -0
- package/.agents/skills/{optimize/SKILL.md → impeccable/reference/optimize.md} +4 -12
- package/.agents/skills/{overdrive/SKILL.md → impeccable/reference/overdrive.md} +9 -21
- package/.agents/skills/{critique → impeccable}/reference/personas.md +1 -1
- package/.agents/skills/{polish/SKILL.md → impeccable/reference/polish.md} +31 -23
- package/.agents/skills/impeccable/reference/product.md +62 -0
- package/.agents/skills/{quieter/SKILL.md → impeccable/reference/quieter.md} +7 -11
- package/.agents/skills/impeccable/reference/shape.md +151 -0
- package/.agents/skills/impeccable/reference/teach.md +156 -0
- package/.agents/skills/{typeset/SKILL.md → impeccable/reference/typeset.md} +19 -11
- package/.agents/skills/impeccable/reference/typography.md +31 -14
- package/.agents/skills/impeccable/scripts/cleanup-deprecated.mjs +87 -17
- package/.agents/skills/impeccable/scripts/command-metadata.json +94 -0
- package/.agents/skills/impeccable/scripts/design-parser.mjs +820 -0
- package/.agents/skills/impeccable/scripts/detect-csp.mjs +198 -0
- package/.agents/skills/impeccable/scripts/is-generated.mjs +69 -0
- package/.agents/skills/impeccable/scripts/live-accept.mjs +595 -0
- package/.agents/skills/impeccable/scripts/live-browser.js +4781 -0
- package/.agents/skills/impeccable/scripts/live-inject.mjs +445 -0
- package/.agents/skills/impeccable/scripts/live-poll.mjs +186 -0
- package/.agents/skills/impeccable/scripts/live-server.mjs +694 -0
- package/.agents/skills/impeccable/scripts/live-wrap.mjs +571 -0
- package/.agents/skills/impeccable/scripts/live.mjs +247 -0
- package/.agents/skills/impeccable/scripts/load-context.mjs +141 -0
- package/.agents/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
- package/.agents/skills/impeccable/scripts/pin.mjs +214 -0
- package/.agents/skills/init/SKILL.md +2 -0
- package/.agents/skills/liteparse/SKILL.md +1 -0
- package/.agents/skills/memory-systems/SKILL.md +2 -0
- package/.agents/skills/multi-agent-patterns/SKILL.md +2 -0
- package/.agents/skills/opentui/SKILL.md +1 -0
- package/.agents/skills/pdf/SKILL.md +2 -0
- package/.agents/skills/playwright-cli/SKILL.md +51 -5
- package/.agents/skills/playwright-cli/references/playwright-tests.md +1 -1
- package/.agents/skills/playwright-cli/references/running-code.md +10 -0
- package/.agents/skills/playwright-cli/references/session-management.md +56 -0
- package/.agents/skills/playwright-cli/references/spec-driven-testing.md +305 -0
- package/.agents/skills/playwright-cli/references/test-generation.md +49 -3
- package/.agents/skills/pptx/SKILL.md +2 -0
- package/.agents/skills/project-development/SKILL.md +2 -0
- package/.agents/skills/prompt-engineer/SKILL.md +2 -0
- package/.agents/skills/research-codebase/SKILL.md +2 -0
- package/.agents/skills/ripgrep/SKILL.md +2 -0
- package/.agents/skills/skill-creator/LICENSE.txt +1 -1
- package/.agents/skills/skill-creator/SKILL.md +2 -0
- package/.agents/skills/sl-commit/SKILL.md +2 -0
- package/.agents/skills/sl-submit-diff/SKILL.md +2 -0
- package/.agents/skills/tdd/SKILL.md +4 -0
- package/.agents/skills/tool-design/SKILL.md +2 -0
- package/.agents/skills/typescript-advanced-types/SKILL.md +2 -1
- package/.agents/skills/typescript-expert/SKILL.md +7 -1
- package/.agents/skills/typescript-react-reviewer/SKILL.md +2 -1
- package/.agents/skills/workflow-creator/SKILL.md +75 -72
- package/.agents/skills/workflow-creator/references/session-config.md +48 -1
- package/.agents/skills/xlsx/SKILL.md +2 -0
- package/.opencode/opencode.json +6 -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 +24 -14
- package/dist/sdk/providers/copilot.d.ts.map +1 -1
- package/dist/sdk/runtime/executor.d.ts +8 -0
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/runtime/port-discovery.d.ts +71 -0
- package/dist/sdk/runtime/port-discovery.d.ts.map +1 -0
- package/dist/sdk/runtime/tmux.d.ts +10 -0
- package/dist/sdk/runtime/tmux.d.ts.map +1 -1
- package/dist/sdk/types.d.ts +1 -0
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/open-claude-design/opencode/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts +15 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts.map +1 -1
- package/dist/sdk/workflows/builtin/ralph/opencode/index.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 +83 -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 +117 -15
- package/src/sdk/runtime/cc-debounce.ts +2 -2
- package/src/sdk/runtime/executor.test.ts +322 -1
- package/src/sdk/runtime/executor.ts +159 -96
- package/src/sdk/runtime/port-discovery.test.ts +573 -0
- package/src/sdk/runtime/port-discovery.ts +496 -0
- package/src/sdk/runtime/tmux.ts +22 -2
- package/src/sdk/types.ts +1 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +24 -6
- package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +52 -13
- package/src/sdk/workflows/builtin/ralph/claude/index.ts +31 -3
- package/src/sdk/workflows/builtin/ralph/copilot/index.ts +16 -0
- package/src/sdk/workflows/builtin/ralph/helpers/prompts.ts +70 -3
- package/src/sdk/workflows/builtin/ralph/opencode/index.ts +50 -6
- 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
- package/.agents/skills/shape/SKILL.md +0 -96
- /package/.agents/skills/{critique → impeccable}/reference/cognitive-load.md +0 -0
- /package/.agents/skills/{critique → impeccable}/reference/heuristics-scoring.md +0 -0
|
@@ -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,21 @@ 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 { resolveCopilotCliPath } from "../../../sdk/providers/copilot.ts";
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
buildLauncherEnv,
|
|
56
|
+
buildSpawnEnv,
|
|
57
|
+
buildTmuxEnv,
|
|
58
|
+
TERMINAL_ENV_KEYS,
|
|
59
|
+
type TerminalEnvKey,
|
|
60
|
+
} from "../../../lib/terminal-env.ts";
|
|
45
61
|
|
|
46
62
|
// ============================================================================
|
|
47
63
|
// Types
|
|
@@ -118,6 +134,15 @@ export function getAdditionalInstructionsDir(
|
|
|
118
134
|
return path ? dirname(path) : undefined;
|
|
119
135
|
}
|
|
120
136
|
|
|
137
|
+
export function resolveChatCommand(agentType: AgentType): string | undefined {
|
|
138
|
+
if (agentType === "copilot") {
|
|
139
|
+
return resolveCopilotCliPath();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const config = AGENT_CONFIG[agentType];
|
|
143
|
+
return getCommandPath(config.cmd) ?? undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
121
146
|
function generateChatId(): string {
|
|
122
147
|
return crypto.randomUUID().slice(0, 8);
|
|
123
148
|
}
|
|
@@ -132,6 +157,28 @@ function escPwsh(s: string): string {
|
|
|
132
157
|
return s.replace(/[`"$]/g, "`$&");
|
|
133
158
|
}
|
|
134
159
|
|
|
160
|
+
const POSIX_ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
161
|
+
|
|
162
|
+
function assertBashEnvKey(key: string): void {
|
|
163
|
+
if (!POSIX_ENV_KEY_RE.test(key)) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Invalid Bash env key "${key}": must match /^[A-Za-z_][A-Za-z0-9_]*$/`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function escPwshEnvKey(key: string): string {
|
|
171
|
+
return key.replace(/}/g, "`}");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function removeLauncher(path: string): Promise<void> {
|
|
175
|
+
try {
|
|
176
|
+
await rm(path, { force: true });
|
|
177
|
+
} catch {
|
|
178
|
+
// Cleanup best effort; attach/fallback result should remain authoritative.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
135
182
|
/**
|
|
136
183
|
* Build a launcher script that preserves cwd and properly quotes args.
|
|
137
184
|
* This avoids shell-injection risks from passthrough args.
|
|
@@ -149,7 +196,7 @@ export function buildLauncherScript(
|
|
|
149
196
|
// PowerShell: use array splatting for safe arg passing
|
|
150
197
|
const argList = args.map((a) => `"${escPwsh(a)}"`).join(", ");
|
|
151
198
|
const envLines = envEntries.map(
|
|
152
|
-
([key, value]) =>
|
|
199
|
+
([key, value]) => `\${env:${escPwshEnvKey(key)}} = "${escPwsh(value)}"`,
|
|
153
200
|
);
|
|
154
201
|
const script = [
|
|
155
202
|
`Set-Location "${escPwsh(projectRoot)}"`,
|
|
@@ -164,18 +211,19 @@ export function buildLauncherScript(
|
|
|
164
211
|
return { script, ext: "ps1" };
|
|
165
212
|
}
|
|
166
213
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
.map((
|
|
170
|
-
|
|
171
|
-
const envLines = envEntries.map(
|
|
172
|
-
(
|
|
173
|
-
|
|
214
|
+
const quotedCommand = [
|
|
215
|
+
`"${escBash(cmd)}"`,
|
|
216
|
+
...args.map((arg) => `"${escBash(arg)}"`),
|
|
217
|
+
].join(" ");
|
|
218
|
+
const envLines = envEntries.map(([key, value]) => {
|
|
219
|
+
assertBashEnvKey(key);
|
|
220
|
+
return `export ${key}="${escBash(value)}"`;
|
|
221
|
+
});
|
|
174
222
|
const script = [
|
|
175
223
|
"#!/bin/bash",
|
|
176
224
|
`cd "${escBash(projectRoot)}"`,
|
|
177
225
|
...envLines,
|
|
178
|
-
|
|
226
|
+
quotedCommand,
|
|
179
227
|
"atomic_exit_code=$?",
|
|
180
228
|
'exit "$atomic_exit_code"',
|
|
181
229
|
].join("\n");
|
|
@@ -206,8 +254,10 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
|
|
|
206
254
|
|
|
207
255
|
const config = AGENT_CONFIG[agentType];
|
|
208
256
|
|
|
257
|
+
const executable = resolveChatCommand(agentType);
|
|
258
|
+
|
|
209
259
|
// Check the agent CLI is installed
|
|
210
|
-
if (!
|
|
260
|
+
if (!executable) {
|
|
211
261
|
console.error(
|
|
212
262
|
`${COLORS.red}Error: '${config.cmd}' is not installed or not in PATH.${COLORS.reset}`
|
|
213
263
|
);
|
|
@@ -236,12 +286,14 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
|
|
|
236
286
|
|
|
237
287
|
// ── Build argv ──
|
|
238
288
|
const args = await buildAgentArgs(agentType, passthroughArgs, projectRoot);
|
|
239
|
-
const cmd = [
|
|
289
|
+
const cmd = [executable, ...args];
|
|
240
290
|
const overrides = await getProviderOverrides(agentType, projectRoot);
|
|
291
|
+
const claudeTempEnv = agentType === "claude" ? atomicTempEnv() : {};
|
|
241
292
|
// ATOMIC_AGENT must be baked into the launcher env so the agent CLI
|
|
242
293
|
// and anything it spawns can read it from process start.
|
|
243
294
|
const envVars: Record<string, string> = {
|
|
244
295
|
...config.env_vars,
|
|
296
|
+
...claudeTempEnv,
|
|
245
297
|
...overrides.envVars,
|
|
246
298
|
ATOMIC_AGENT: agentType,
|
|
247
299
|
};
|
|
@@ -267,9 +319,13 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
|
|
|
267
319
|
}
|
|
268
320
|
}
|
|
269
321
|
|
|
322
|
+
const spawnEnv = buildSpawnEnv(envVars);
|
|
323
|
+
const launcherEnv = buildLauncherEnv(envVars);
|
|
324
|
+
const tmuxEnv = buildTmuxEnv(envVars);
|
|
325
|
+
|
|
270
326
|
// ── No TTY: tmux attach requires a real terminal ──
|
|
271
327
|
if (!process.stdin.isTTY) {
|
|
272
|
-
return spawnDirect(cmd, projectRoot,
|
|
328
|
+
return spawnDirect(cmd, projectRoot, spawnEnv);
|
|
273
329
|
}
|
|
274
330
|
|
|
275
331
|
// ── Ensure tmux is available ──
|
|
@@ -283,7 +339,7 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
|
|
|
283
339
|
}
|
|
284
340
|
if (!isTmuxInstalled()) {
|
|
285
341
|
// No tmux available — fall back to direct spawn
|
|
286
|
-
return spawnDirect(cmd, projectRoot,
|
|
342
|
+
return spawnDirect(cmd, projectRoot, spawnEnv);
|
|
287
343
|
}
|
|
288
344
|
}
|
|
289
345
|
|
|
@@ -294,10 +350,10 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
|
|
|
294
350
|
const sessionsDir = join(homedir(), ".atomic", "sessions", "chat");
|
|
295
351
|
await mkdir(sessionsDir, { recursive: true });
|
|
296
352
|
const { script, ext } = buildLauncherScript(
|
|
297
|
-
|
|
353
|
+
executable,
|
|
298
354
|
args,
|
|
299
355
|
projectRoot,
|
|
300
|
-
|
|
356
|
+
launcherEnv,
|
|
301
357
|
);
|
|
302
358
|
const launcherPath = join(sessionsDir, `${windowName}.${ext}`);
|
|
303
359
|
await writeFile(launcherPath, script, { mode: 0o755 });
|
|
@@ -308,14 +364,14 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
|
|
|
308
364
|
|
|
309
365
|
// ── Create session on the atomic socket and attach ──
|
|
310
366
|
try {
|
|
311
|
-
const paneId = createSession(windowName, shellCmd, undefined, projectRoot);
|
|
367
|
+
const paneId = createSession(windowName, shellCmd, undefined, projectRoot, tmuxEnv);
|
|
312
368
|
spawnAttachedFooter(windowName, paneId, agentType);
|
|
313
369
|
killSessionOnPaneExit(windowName, paneId);
|
|
314
370
|
|
|
315
371
|
if (isInsideAtomicSocket()) {
|
|
316
372
|
// Already on the atomic server — just switch to the new session.
|
|
317
373
|
switchClient(windowName);
|
|
318
|
-
|
|
374
|
+
await removeLauncher(launcherPath);
|
|
319
375
|
return 0;
|
|
320
376
|
}
|
|
321
377
|
|
|
@@ -323,30 +379,29 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
|
|
|
323
379
|
// Inside a different tmux server — detach and replace the client
|
|
324
380
|
// with an attach to the atomic socket (no nesting).
|
|
325
381
|
detachAndAttachAtomic(windowName);
|
|
326
|
-
|
|
382
|
+
await removeLauncher(launcherPath);
|
|
327
383
|
return 0;
|
|
328
384
|
}
|
|
329
385
|
|
|
330
386
|
const attachProc = spawnMuxAttach(windowName);
|
|
331
387
|
const exitCode = await attachProc.exited;
|
|
332
388
|
|
|
333
|
-
|
|
334
|
-
try { await rm(launcherPath, { force: true }); } catch {}
|
|
389
|
+
await removeLauncher(launcherPath);
|
|
335
390
|
|
|
336
391
|
// If tmux attach itself failed (e.g. lost TTY), clean up and fall back
|
|
337
392
|
if (exitCode !== 0) {
|
|
338
393
|
try { killSession(windowName); } catch {}
|
|
339
|
-
return spawnDirect(cmd, projectRoot,
|
|
394
|
+
return spawnDirect(cmd, projectRoot, spawnEnv);
|
|
340
395
|
}
|
|
341
396
|
|
|
342
397
|
return exitCode;
|
|
343
398
|
} catch (error) {
|
|
344
|
-
|
|
399
|
+
await removeLauncher(launcherPath);
|
|
345
400
|
const message = error instanceof Error ? error.message : String(error);
|
|
346
401
|
console.error(
|
|
347
402
|
`${COLORS.yellow}Warning: Failed to create tmux session (${message}). Falling back to direct spawn.${COLORS.reset}`
|
|
348
403
|
);
|
|
349
|
-
return spawnDirect(cmd, projectRoot,
|
|
404
|
+
return spawnDirect(cmd, projectRoot, spawnEnv);
|
|
350
405
|
}
|
|
351
406
|
}
|
|
352
407
|
|
|
@@ -357,12 +412,12 @@ export async function chatCommand(options: ChatCommandOptions = {}): Promise<num
|
|
|
357
412
|
async function spawnDirect(
|
|
358
413
|
cmd: string[],
|
|
359
414
|
projectRoot: string,
|
|
360
|
-
|
|
415
|
+
env: Record<string, string> = {},
|
|
361
416
|
): Promise<number> {
|
|
362
417
|
const proc = Bun.spawn(cmd, {
|
|
363
418
|
stdio: ["inherit", "inherit", "inherit"],
|
|
364
419
|
cwd: projectRoot,
|
|
365
|
-
env
|
|
420
|
+
env,
|
|
366
421
|
});
|
|
367
422
|
|
|
368
423
|
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
|
+
}
|