diff --git a/.agents/notes/implemented/architecture/2026-08-19-shared-win32-process-primitives.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-19-shared-win32-process-primitives.i18n.yaml index 02bb27c70f..0159e844a6 100644 --- a/.agents/notes/implemented/architecture/2026-08-19-shared-win32-process-primitives.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-19-shared-win32-process-primitives.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-19-shared-win32-process-primitives.md -2026-08-19-shared-win32-process-primitives.md: 8e5878ff23a49d9f0fbc4e62b9e3e6bccf5fe4ed -2026-08-19-shared-win32-process-primitives.zh.md: 90ca5b21227136febafa946d20acbe59630436fd +2026-08-19-shared-win32-process-primitives.md: 60e9b5b76154a4833979b014dffb4015cb0c5c36 +2026-08-19-shared-win32-process-primitives.zh.md: 83bb71ddbe182084097c54325172a3f923b33138 diff --git a/.agents/notes/implemented/architecture/2026-08-19-shared-win32-process-primitives.md b/.agents/notes/implemented/architecture/2026-08-19-shared-win32-process-primitives.md index 8e5878ff23..60e9b5b761 100644 --- a/.agents/notes/implemented/architecture/2026-08-19-shared-win32-process-primitives.md +++ b/.agents/notes/implemented/architecture/2026-08-19-shared-win32-process-primitives.md @@ -14,7 +14,7 @@ The Windows ACL sandbox owns restricted-token, SID, DACL, grant, and workspace p The Windows ACL sandbox remains the only owner of restricted-token creation, SID and DACL policy, grants, writable-path decisions, temporary-directory policy, and the public sandbox child result. It extends the shared binding context with policy-specific APIs, supplies the primary token, combines pipe drains and waits, and closes the caller-owned Job at its lifecycle boundary. -Every native allocation and HANDLE has one owner. A process operation frees its Koffi out-parameters and closes every pipe, thread, process, or Job handle acquired before a failure. Successful pipe creation returns the process plus stdout/stderr read handles to the sandbox. Inherited-stdio creation puts the kill-on-close Job in `STARTUPINFOEXW`, so the child is already Job-owned before any user code can run; attribute or creation failure therefore has one deterministic cleanup owner. The sandbox owns returned process, pipe, and Job handles until wait or disposal. +Every native allocation and HANDLE has one owner. A process operation frees its Koffi out-parameters and closes every pipe, thread, process, or Job handle acquired before a failure. Successful pipe creation returns the process plus stdout/stderr read handles to the sandbox; if either drain fails, sandbox settlement terminates the child before its synchronous wait, or closes the process handle and reports the termination failure without blocking. Inherited-stdio creation puts the kill-on-close Job in `STARTUPINFOEXW`, so the child is already Job-owned before any user code can run; attribute or creation failure therefore has one deterministic cleanup owner. The sandbox owns returned process, pipe, and Job handles until wait or disposal. The package exports only operations used by the sandbox production path. Ordinary `CreateProcessW`, exact `applicationName`, parent-stdio release, and whole-Job settlement remain absent until an ordinary process consumer needs them. The package is a library, not a Cordis service or a public Windows SDK. diff --git a/.agents/notes/implemented/architecture/2026-08-19-shared-win32-process-primitives.zh.md b/.agents/notes/implemented/architecture/2026-08-19-shared-win32-process-primitives.zh.md index 90ca5b2122..83bb71ddbe 100644 --- a/.agents/notes/implemented/architecture/2026-08-19-shared-win32-process-primitives.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-19-shared-win32-process-primitives.zh.md @@ -14,7 +14,7 @@ Windows ACL sandbox 拥有 restricted token、SID、DACL、grant 与 workspace p Windows ACL sandbox 继续唯一拥有 restricted-token 创建、SID 与 DACL policy、grants、可写路径裁定、临时目录 policy 和公共 sandbox child result。它通过共享 binding context 扩展 policy-specific API,提供 primary token,组合 pipe drain 与 wait,并在自己的生命周期边界关闭调用方拥有的 Job。 -每项 native allocation 与 HANDLE 都只有一个 owner。process operation 会释放 Koffi out-parameter,并在失败前关闭已经取得的每个 pipe、thread、process 或 Job handle。pipe 创建成功时,把 process 与 stdout/stderr read handles 返回给 sandbox。inherited-stdio 创建会把 kill-on-close Job 放进 `STARTUPINFOEXW`,因此 child 在任何用户代码运行前已经归属 Job;attribute 或创建失败都有唯一且确定的 cleanup owner。sandbox 在 wait 或 disposal 前拥有返回的 process、pipe 与 Job handles。 +每项 native allocation 与 HANDLE 都只有一个 owner。process operation 会释放 Koffi out-parameter,并在失败前关闭已经取得的每个 pipe、thread、process 或 Job handle。pipe 创建成功时,把 process 与 stdout/stderr read handles 返回给 sandbox;任一 drain 失败时,sandbox settlement 会在同步 wait 前终止 child,若终止本身失败则关闭 process handle 并报告该失败,不阻塞事件循环。inherited-stdio 创建会把 kill-on-close Job 放进 `STARTUPINFOEXW`,因此 child 在任何用户代码运行前已经归属 Job;attribute 或创建失败都有唯一且确定的 cleanup owner。sandbox 在 wait 或 disposal 前拥有返回的 process、pipe 与 Job handles。 该包只导出 sandbox 生产路径已使用的操作。ordinary `CreateProcessW`、精确 `applicationName`、parent-stdio release 与 whole-Job settlement 在 ordinary process consumer 出现前保持缺席。该包是 library,不是 Cordis service 或公共 Windows SDK。 diff --git a/AGENTS.md b/AGENTS.md index 99b31077db..f50a5b24d3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ packages/ @deepseek-ai/dsh- workspaces at packages/// llm/ LLM capability: Service Definition/Consumer + DeepSeek providers e2b/ E2B POC: sandbox + FS/subprocess adapters shell/ bash capability: Service Definition + local/pwsh providers + shell Consumers - subprocess/ subprocess capability + local process-tree provider + subprocess/ subprocess capability + local process-tree provider + shared Win32 library terminal/ persistent sessions fs/ filesystem capability + policy lsp/ language-server capability diff --git a/packages/README.i18n.yaml b/packages/README.i18n.yaml index ee425f39d7..ba5767f75b 100644 --- a/packages/README.i18n.yaml +++ b/packages/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/README.md -README.md: a410d7148d14503a61edb9c4848b521552050ca4 -README.zh.md: 780d1356f2095c7dcc526e5cdac8994267e4a460 +README.md: defbd29942f7733919a9c65a1f8174148919b034 +README.zh.md: f47584209d4d54eedec48f5f5c4e90479cf5c7d9 diff --git a/packages/README.md b/packages/README.md index a410d7148d..defbd29942 100644 --- a/packages/README.md +++ b/packages/README.md @@ -19,7 +19,7 @@ Groups hold `packages///`; names stay `@deepseek-ai/dsh-`. **Gr | [`identity/`](identity/README.md) | Shared anonymous identity | Product — stable API | | [`llm/`](llm/README.md) | LLM capability family: the abstract service + provider adapters | Product — stable API | | [`e2b/`](e2b/README.md) | E2B providers | POC | -| [`subprocess/`](subprocess/README.md) | Subprocess capability family: Service Definition + local process-tree provider | Product — stable API | +| [`subprocess/`](subprocess/README.md) | Subprocess capability family: Service Definition, local process-tree provider, and shared Win32 process library | Product — stable API | | [`shell/`](shell/README.md) | Bash capability family: executor seam, local impl, model-facing tool | Product — stable API | | [`terminal/`](terminal/README.md) | Persistent PTY capability family: owner-scoped sessions, local implementation, and model-facing tools | Product — stable API | | [`code-runtime/`](code-runtime/README.md) | Code-execution capability family: Service Definition + worker-thread provider + Code Mode Consumer | Product — stable API | diff --git a/packages/README.zh.md b/packages/README.zh.md index 780d1356f2..f47584209d 100644 --- a/packages/README.zh.md +++ b/packages/README.zh.md @@ -19,7 +19,7 @@ npm scope 为 `@deepseek-ai/dsh-*`;Cordis `Service` 子类和函数插件通 | [`identity/`](identity/README.md) | 共享匿名身份 | 产品:稳定 API | | [`llm/`](llm/README.md) | LLM(大语言模型)能力系列:抽象服务 + 提供方适配器 | 产品:稳定 API | | [`e2b/`](e2b/README.md) | E2B 提供方 | POC | -| [`subprocess/`](subprocess/README.md) | 子进程能力系列:Service Definition + 本地进程树提供方 | 产品:稳定 API | +| [`subprocess/`](subprocess/README.md) | 子进程能力系列:Service Definition、本地进程树提供方与共享 Win32 进程库 | 产品:稳定 API | | [`shell/`](shell/README.md) | Bash 能力系列:执行器 seam、本地实现、面向模型的工具 | 产品:稳定 API | | [`terminal/`](terminal/README.md) | 持久 PTY 能力系列:限定所有者范围的会话、本地实现和面向模型的工具 | 产品:稳定 API | | [`code-runtime/`](code-runtime/README.md) | 代码执行能力系列:Service Definition + worker 线程提供方 + Code Mode Consumer | 产品:稳定 API | diff --git a/packages/sandbox/sandbox-windows-acl/src/index.ts b/packages/sandbox/sandbox-windows-acl/src/index.ts index 5a46eb1423..231d1cb0c3 100644 --- a/packages/sandbox/sandbox-windows-acl/src/index.ts +++ b/packages/sandbox/sandbox-windows-acl/src/index.ts @@ -55,7 +55,7 @@ import * as abi from './win32-abi.ts' export { AclWriteGrant } from './grant.ts' export { assertTempRootOutsideWorkspace } from './path-boundary.ts' export { tempWriteSid, workspaceWriteSid } from './workspace-sid.ts' -export { quoteArg, Win32Error } from '@deepseek-ai/dsh-win32-process' +export { Win32Error } from '@deepseek-ai/dsh-win32-process' /** Construction options: the workspace/temp allowlists and their distinct SID identities. */ export interface AclSandboxOptions { @@ -382,10 +382,11 @@ export class AclSandbox { const native = spawnSandboxed(api, token, { command: options.command, args, cwd }) const stdout = drainPipe(api, native.stdoutRead) const stderr = drainPipe(api, native.stderrRead) - // waitForExit is deliberately NOT started here: WaitForSingleObject blocks - // the thread and would starve the drains while the child is still running - // (pipe-buffer deadlock). The drains resolve only after the child closed - // its pipe ends — by then the wait returns immediately. + // WaitForSingleObject blocks the thread, so settlement starts it only after + // both drains settle. Successful drains mean the child closed its pipe ends + // and the wait returns immediately. A failed drain terminates the child + // before waiting, so a native pipe failure cannot pin the event loop on a + // still-running command. let settlement: Promise | undefined return { pid: native.pid, @@ -394,10 +395,20 @@ export class AclSandbox { const failures = drains.flatMap(outcome => outcome.status === 'rejected' ? [outcome.reason as unknown] : []) let exitCode = 0 - try { - exitCode = waitForExit(api, native.process) - } catch (error) { - failures.push(error) + if (failures.length > 0 && api.terminateProcess(native.process, 1) === 0) { + const terminationCode = api.getLastError() + try { + closeHandleChecked(api, native.process, 'piped child after drain failure') + } catch (error) { + failures.push(error) + } + failures.push(new Win32Error('TerminateProcess', terminationCode, `pid ${native.pid} after drain failure`)) + } else { + try { + exitCode = waitForExit(api, native.process) + } catch (error) { + failures.push(error) + } } if (failures.length === 1) throw failures[0] if (failures.length > 1) throw new AggregateError(failures, 'piped child settlement failed') diff --git a/packages/sandbox/sandbox-windows-acl/src/token.ts b/packages/sandbox/sandbox-windows-acl/src/token.ts index abd0c73617..96aa53b277 100644 --- a/packages/sandbox/sandbox-windows-acl/src/token.ts +++ b/packages/sandbox/sandbox-windows-acl/src/token.ts @@ -180,8 +180,9 @@ export interface RestrictingSidSet { * `AU:(AD)` + `AU:(OI)(CI)(IO)(M)` ACEs) is closed in both — documented in * README. INTERACTIVE/LOCAL are absent from BOTH lists too — the host's * Public tree grants write to INTERACTIVE, so removing it closes that - * escape. S-1-2-1 (console logon) is intentionally absent: see win32-abi.ts - * for the verified failure modes. FAILS CLOSED: any failure throws — never + * escape. S-1-2-1 (console logon) is intentionally absent: the package + * README's "Console isolation is unavailable" entry records the verified + * failure modes. FAILS CLOSED: any failure throws — never * spawn unrestricted. * @param api - the binding table. * @param currentToken - the process token to restrict. diff --git a/packages/sandbox/sandbox-windows-acl/src/win32-abi.ts b/packages/sandbox/sandbox-windows-acl/src/win32-abi.ts index 019f894a6c..478fc71e1d 100644 --- a/packages/sandbox/sandbox-windows-acl/src/win32-abi.ts +++ b/packages/sandbox/sandbox-windows-acl/src/win32-abi.ts @@ -20,7 +20,11 @@ export const FILE_GENERIC_WRITE = 0x00120116 export const DELETE = 0x00010000 /** Delete or rename a directory child. */ export const FILE_DELETE_CHILD = 0x0040 -/** Capability-SID access mask granting write, delete, and child deletion. */ +/** + * Capability-SID access mask granting write, delete, and child deletion. + * WRITE_DAC and WRITE_OWNER stay excluded so a confined child cannot rewrite + * DACLs or take ownership to escape the allowlist. + */ export const GRANT_MASK = (FILE_GENERIC_WRITE | DELETE | FILE_DELETE_CHILD) & ~STANDARD_RIGHTS_WRITE /** Full access used in the restricted token default DACL. */ export const FILE_ALL_ACCESS = 0x1F01FF diff --git a/packages/sandbox/sandbox-windows-acl/tests/ffi.spec.ts b/packages/sandbox/sandbox-windows-acl/tests/ffi.spec.ts index ebd3ba1b35..761598a073 100644 --- a/packages/sandbox/sandbox-windows-acl/tests/ffi.spec.ts +++ b/packages/sandbox/sandbox-windows-acl/tests/ffi.spec.ts @@ -9,7 +9,7 @@ import { describe, expect, it, vi } from 'vitest' import koffi from 'koffi' -import { Win32Error, quoteArg } from '../src/index.ts' +import { Win32Error } from '../src/index.ts' import { allocBytes, decodePtrAt, getTempPath, isInvalidHandle, sameSidAt, @@ -17,7 +17,7 @@ import { import type { NativePtr, Win32Bindings } from '../src/ffi.ts' import * as abi from '../src/win32-abi.ts' -/** A stub whose formatMessageW writes real UTF-16 text (the errorText round-trip). */ +/** A stub whose formatMessageW supplies text to the GetTempPath failure path. */ function formatApi(): { api: Win32Bindings; formatMessageW: ReturnType } { const formatMessageW = vi.fn((_flags: number, _source: null, _id: number, _lang: number, buffer: Buffer, _size: number, _args: null) => { const text = 'access denied' @@ -75,10 +75,9 @@ describe('getTempPath', () => { }) }) -describe('public compatibility exports', () => { - it('keeps the sandbox Win32 error and quoting API', () => { +describe('public error export', () => { + it('keeps the sandbox Win32 error type', () => { expect(new Win32Error('Probe', 5)).toBeInstanceOf(Error) - expect(quoteArg('a b')).toBe('"a b"') }) }) diff --git a/packages/sandbox/sandbox-windows-acl/tests/index-failure-paths.spec.ts b/packages/sandbox/sandbox-windows-acl/tests/index-failure-paths.spec.ts index a8834a8c61..a75f3546d3 100644 --- a/packages/sandbox/sandbox-windows-acl/tests/index-failure-paths.spec.ts +++ b/packages/sandbox/sandbox-windows-acl/tests/index-failure-paths.spec.ts @@ -448,6 +448,8 @@ describe('AclSandbox spawn', () => { it('pipe spawn still closes the process after a drain failure', async () => { const { api } = state.stubs as HappyStubs api.getLastError = vi.fn(() => 5) + const terminateProcess = vi.fn(() => 1) + api.terminateProcess = terminateProcess const waitForSingleObject = vi.fn(() => 0) api.waitForSingleObject = waitForSingleObject const workspace = scratch() @@ -460,8 +462,24 @@ describe('AclSandbox spawn', () => { expect.objectContaining({ api: 'PeekNamedPipe' }), ], }) + expect(terminateProcess).toHaveBeenCalledOnce() expect(waitForSingleObject).toHaveBeenCalledOnce() }) + + it('pipe spawn closes the process without waiting when termination after a drain failure fails', async () => { + const { api, closeHandle } = state.stubs as HappyStubs + api.getLastError = vi.fn(() => 5) + api.terminateProcess = vi.fn(() => 0) + const waitForSingleObject = vi.fn(() => { throw new Error('must not wait') }) + api.waitForSingleObject = waitForSingleObject + const workspace = scratch() + const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: null, writeSid: 'S-1-4-9000-14-3', mode: 'workspace-write' }) + await sandbox.init() + const child = sandbox.spawn({ command: 'probe.exe' }) + await expect(child.wait()).rejects.toBeInstanceOf(AggregateError) + expect(waitForSingleObject).not.toHaveBeenCalled() + expect(closeHandle).toHaveBeenCalled() + }) }) describe('AclSandbox dispose', () => { diff --git a/packages/subprocess/win32-process/README.i18n.yaml b/packages/subprocess/win32-process/README.i18n.yaml index 4fdca725f6..1f15b27c96 100644 --- a/packages/subprocess/win32-process/README.i18n.yaml +++ b/packages/subprocess/win32-process/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subprocess/win32-process/README.md -README.md: 601c095ea48fbf46f151ee69ad2e485881dcb4af -README.zh.md: 53a55ef58a404c52ded1d417ba4fa516a3092783 +README.md: c3bc0d74c3c5a375d289e9a3e4037648341f4906 +README.zh.md: 6763f2b24633d7dd250eecdafe5e1724ffa29fa6 diff --git a/packages/subprocess/win32-process/README.md b/packages/subprocess/win32-process/README.md index 601c095ea4..c3bc0d74c3 100644 --- a/packages/subprocess/win32-process/README.md +++ b/packages/subprocess/win32-process/README.md @@ -34,6 +34,6 @@ The package contributes no stable request prefix, so it does not invalidate mode - **Windows-only native loading** — importing the generic types is portable, but resolving the binding table loads Windows DLLs and fails on other hosts. Cross-platform tests inject a binding table instead of loading native APIs. - **No public process service** — the package intentionally does not wrap its primitives in Cordis or Node streams. A consumer must own its policy, async scheduling, output limits, cancellation, and final handle closure. -- **Inherited environment only** — process creation passes a null environment block. Callers that need environment changes must establish them before invoking the primitive or use their own runner process. +- **Inherited environment only** — process creation passes a null environment block. The sandbox establishes changes through `SetEnvironmentVariableW` first because passing an explicit block through Koffi makes `CreateProcessAsUserW` fail with `ERROR_INVALID_PARAMETER`. Other callers that need environment changes must establish them before invoking the primitive or use their own runner process. - **Restricted-token consumer only** — ordinary `CreateProcessW`, exact `applicationName`, parent-stdio release, and whole-Job settlement are absent until an ordinary process consumer requires them. - **Header evidence is architecture-specific** — the committed ABI probe and layout constants cover the repository's current 64-bit Windows targets. A new pointer width or incompatible Windows ABI requires updating the probe before support is claimed. diff --git a/packages/subprocess/win32-process/README.zh.md b/packages/subprocess/win32-process/README.zh.md index 53a55ef58a..6763f2b246 100644 --- a/packages/subprocess/win32-process/README.zh.md +++ b/packages/subprocess/win32-process/README.zh.md @@ -26,7 +26,7 @@ Windows ACL 沙箱在这些原语上增加 SID、DACL、grant、workspace 与公 没有直接影响。消费方决定进程输出是否进入工具结果或后续模型请求。 -#### KV Cache effect +#### KV Cache 影响 本包不贡献稳定请求前缀,因此不会使模型 KV Cache 失效。 @@ -34,6 +34,6 @@ Windows ACL 沙箱在这些原语上增加 SID、DACL、grant、workspace 与公 - **仅在 Windows 原生加载** — 导入通用类型可跨平台进行,但解析绑定表会加载 Windows DLL,并在其他宿主失败。跨平台测试注入绑定表,不加载原生 API。 - **没有公共进程服务** — 本包刻意不把原语包装成 Cordis 或 Node streams。消费方必须拥有自己的策略、异步调度、输出上限、取消与最终句柄关闭。 -- **只继承环境** — 进程创建传入空环境块。需要改写环境的调用方必须在调用原语前建立环境,或使用自己的 runner 进程。 +- **只继承环境** — 进程创建传入空环境块。sandbox 会先通过 `SetEnvironmentVariableW` 建立改动,因为经 Koffi 传入显式环境块会使 `CreateProcessAsUserW` 以 `ERROR_INVALID_PARAMETER` 失败。其他需要改写环境的调用方必须在调用原语前建立环境,或使用自己的 runner 进程。 - **只有 restricted-token 消费方** — ordinary `CreateProcessW`、精确 `applicationName`、parent-stdio release 与 whole-Job settlement 在 ordinary process 消费方出现前均不提供。 - **header 证据限定架构** — 已提交的 ABI probe 与布局常量覆盖仓库当前 64 位 Windows 目标。支持新的指针宽度或不兼容 Windows ABI 前,必须先更新 probe。 diff --git a/packages/subprocess/win32-process/src/index.ts b/packages/subprocess/win32-process/src/index.ts index e7693db495..fa7f2dd992 100644 --- a/packages/subprocess/win32-process/src/index.ts +++ b/packages/subprocess/win32-process/src/index.ts @@ -19,7 +19,6 @@ export type { export { closeHandleChecked, drainPipe, - quoteArg, spawnInheritedJobProcess, spawnPipedProcess, waitForProcessExit, diff --git a/packages/subprocess/win32-process/src/process.ts b/packages/subprocess/win32-process/src/process.ts index 7404ccb4ba..c9fb888515 100644 --- a/packages/subprocess/win32-process/src/process.ts +++ b/packages/subprocess/win32-process/src/process.ts @@ -142,6 +142,9 @@ function createRestrictedProcess( startupInfo: NativePtr, processInfo: NativePtr, ): number { + // The sandbox mutates its process environment before this call. Passing an + // explicit block through Koffi makes CreateProcessAsUserW reject the request + // with ERROR_INVALID_PARAMETER, so lpEnvironment remains NULL. return api.createProcessAsUserW( options.token, null, @@ -315,6 +318,10 @@ function createKillOnCloseJob(api: Win32ProcessBindings): NativePtr { * @param api - active binding table. * @param options - command, cwd, args, and restricted primary token. * @returns caller-owned process and Job handles after successful creation. + * @remarks Node clears stdio handle inheritability at startup through + * uv_disable_stdio_inheritance. This operation temporarily restores the bits + * required by STARTF_USESTDHANDLES. Restoring them afterward is best-effort: + * failure must not replace the already-created child's outcome. */ export function spawnInheritedJobProcess( api: Win32ProcessBindings, @@ -371,7 +378,10 @@ export function spawnInheritedJobProcess( api.closeHandle(job) throw error } finally { - for (const handle of enabled) api.setHandleInformation(handle, abi.HANDLE_FLAG_INHERIT, 0) + for (const handle of enabled) { + // The runner spawns nothing else; cleanup failure must not mask the child. + api.setHandleInformation(handle, abi.HANDLE_FLAG_INHERIT, 0) + } } if (created === 0) { freeNative(processInfo) diff --git a/packages/subprocess/win32-process/tests/quote.spec.ts b/packages/subprocess/win32-process/tests/quote.spec.ts index f93d721cc3..63dcc7bc77 100644 --- a/packages/subprocess/win32-process/tests/quote.spec.ts +++ b/packages/subprocess/win32-process/tests/quote.spec.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest' -import { quoteArg } from '../src/index.ts' -import { buildCommandLine } from '../src/process.ts' +import { buildCommandLine, quoteArg } from '../src/process.ts' const isWin32 = process.platform === 'win32'