mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
feat(python-sdk): support bundled preset runtime dependencies
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
* @module @deepseek-ai/dsh-tool-fs-search/search-core
|
||||
*/
|
||||
|
||||
import { existsSync } from 'node:fs'
|
||||
import { isAbsolute, relative, sep } from 'node:path'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
||||
@@ -158,18 +159,21 @@ let rgPathPromise: Promise<string> | undefined
|
||||
/**
|
||||
* The packaged ripgrep binary path, resolved lazily once per process.
|
||||
*
|
||||
* `@vscode/ripgrep` resolves its platform package (`@vscode/ripgrep-<platform>
|
||||
* -<arch>`) at module evaluation, so a static import would turn a missing or
|
||||
* corrupt platform package (`pnpm install --omit=optional`, partial install)
|
||||
* into a failure of the whole Loader composition. Resolving at the call
|
||||
* boundary keeps that failure at the first search call as `SEARCH_FAILED` —
|
||||
* the package's documented no-load-time-probe contract.
|
||||
* A single-file runtime uses the executable's `-rg` sidecar because a native
|
||||
* helper cannot be spawned from pkg's virtual filesystem. Node-mode builds
|
||||
* fall back to the platform package selected by `@vscode/ripgrep`. Resolving
|
||||
* at the call boundary keeps a missing or corrupt binary at the first search
|
||||
* call as `SEARCH_FAILED`, rather than failing the Loader composition.
|
||||
*
|
||||
* @returns the packaged binary's absolute path; the memoized promise rejects
|
||||
* when the platform package cannot be resolved.
|
||||
*/
|
||||
export function resolveRgPath(): Promise<string> {
|
||||
rgPathPromise ??= import('@vscode/ripgrep').then(module => module.rgPath)
|
||||
rgPathPromise ??= Promise.resolve().then(async () => {
|
||||
const executableSidecar = `${process.execPath}-rg`
|
||||
if (existsSync(executableSidecar)) return executableSidecar
|
||||
return (await import('@vscode/ripgrep')).rgPath
|
||||
})
|
||||
return rgPathPromise
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { randomUUID } from 'node:crypto'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import z from '@deepseek-ai/schemastery'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { CONTROLLED_PROMPT } from '@deepseek-ai/dsh-terminal'
|
||||
import type { TerminalReadResult, TerminalSendResult, TerminalSessionId } from '@deepseek-ai/dsh-terminal'
|
||||
import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
@@ -15,7 +16,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
const TRUNCATED_MESSAGE = '<response clipped><NOTE>To save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with `grep -n` in order to find the line numbers of what you are looking for.</NOTE>'
|
||||
const LOST_PREFIX_MESSAGE = '<response clipped><NOTE>The beginning of this command output was dropped by the terminal scrollback limit. The following text is the earliest retained output.</NOTE>\n'
|
||||
const SHELL_RESET_MESSAGE = 'The persistent bash shell was reset; the next bash call starts from the workspace with a fresh current directory and environment.'
|
||||
const SHELL_PROMPT = '__DSH_PERSISTENT_BASH_PROMPT__ '
|
||||
const SHELL_PROMPT = CONTROLLED_PROMPT
|
||||
const TIMEOUT_CODE = 'PERSISTENT_BASH_TIMEOUT'
|
||||
// One page is enough to find a just-emitted completion marker; the full
|
||||
// scrollback is assembled only when a command settles or needs partial output.
|
||||
|
||||
@@ -84,8 +84,8 @@ suite('persistent Bash through a real cordis.yml Loader composition', () => {
|
||||
' config:',
|
||||
' pollIntervalMs: 10',
|
||||
' exactProbeAfterMs: 20',
|
||||
' idleSilenceMs: 100',
|
||||
' handoffGraceMs: 100',
|
||||
' idleSilenceMs: 3000',
|
||||
' handoffGraceMs: 500',
|
||||
' scrollbackLines: 20000',
|
||||
' timeoutMs: 2000',
|
||||
' disposeGraceMs: 500',
|
||||
@@ -131,7 +131,9 @@ suite('persistent Bash through a real cordis.yml Loader composition', () => {
|
||||
})
|
||||
|
||||
expect(context.tools.schemas().map(schema => schema.name)).toEqual(['bash'])
|
||||
const startedAt = Date.now()
|
||||
await execute('state', 'export KEEP=loader; mkdir -p nested; cd nested')
|
||||
expect(Date.now() - startedAt).toBeLessThan(2_000)
|
||||
const observed = text(await execute('observe', 'printf "cwd=%s keep=%s\\n" "$PWD" "$KEEP"'))
|
||||
expect(observed).toContain(`cwd=${join(root, 'nested')} keep=loader`)
|
||||
expect(observed).not.toContain('DSH_PERSISTENT_BASH')
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import TerminalSessionService from '@deepseek-ai/dsh-terminal'
|
||||
import TerminalSessionService, { CONTROLLED_PROMPT } from '@deepseek-ai/dsh-terminal'
|
||||
import type {
|
||||
TerminalBackend,
|
||||
TerminalBackendSession,
|
||||
@@ -100,7 +100,7 @@ type StubMode =
|
||||
| 'paged-scrollback'
|
||||
|
||||
class StubPtySession implements TerminalBackendSession {
|
||||
readonly motd = '__DSH_PERSISTENT_BASH_PROMPT__ '
|
||||
readonly motd = CONTROLLED_PROMPT
|
||||
readonly pid = 123
|
||||
statusValue: TerminalSessionStatus = { kind: 'running' }
|
||||
scrollback = this.motd
|
||||
|
||||
@@ -13,9 +13,9 @@ export interface ProcessIdentity {
|
||||
/** Injectable OS process operations used by one local PTY session. */
|
||||
export interface ProcessInspector {
|
||||
foregroundPgid(shellPid: number): number | undefined
|
||||
isStdinWaiting(pgid: number): boolean
|
||||
isStdinWaiting(pgid: number, scanNamespace?: boolean): boolean
|
||||
/** Return the root and its current transitive descendants, children first. */
|
||||
processTree(rootPid: number): ProcessIdentity[]
|
||||
processTree(rootPid: number, scanNamespace?: boolean): ProcessIdentity[]
|
||||
/** Return current members of one POSIX process session when the platform exposes them. */
|
||||
processSession(sessionId: number): ProcessIdentity[]
|
||||
/** Return whether the exact identity remains a non-quiescent process. */
|
||||
@@ -226,12 +226,26 @@ function syscallWaitsOnStdin(
|
||||
return false
|
||||
}
|
||||
|
||||
function processWaitsOnStdin(
|
||||
internals: ProcessInspectorInternals,
|
||||
pid: number,
|
||||
processGroupId: number,
|
||||
table: SyscallTable,
|
||||
): boolean {
|
||||
if (readLinuxStat(internals, pid)?.pgrp !== processGroupId) return false
|
||||
for (const tid of numericEntries(internals, `/proc/${pid}/task`)) {
|
||||
const syscall = readSyscall(internals, pid, tid)
|
||||
if (syscall !== undefined && syscallWaitsOnStdin(internals, pid, syscall, table)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
abstract class PosixProcessInspector implements ProcessInspector {
|
||||
constructor(protected readonly internals: ProcessInspectorInternals) {}
|
||||
|
||||
abstract foregroundPgid(shellPid: number): number | undefined
|
||||
abstract isStdinWaiting(pgid: number): boolean
|
||||
abstract processTree(rootPid: number): ProcessIdentity[]
|
||||
abstract isStdinWaiting(pgid: number, scanNamespace?: boolean): boolean
|
||||
abstract processTree(rootPid: number, scanNamespace?: boolean): ProcessIdentity[]
|
||||
abstract processSession(sessionId: number): ProcessIdentity[]
|
||||
abstract isAlive(identity: ProcessIdentity): boolean
|
||||
|
||||
@@ -270,6 +284,34 @@ function processTree(entries: ProcessTreeEntry[], rootPid: number): ProcessIdent
|
||||
return result
|
||||
}
|
||||
|
||||
function linuxProcessTreeFromChildren(
|
||||
internals: ProcessInspectorInternals,
|
||||
rootPid: number,
|
||||
): ProcessIdentity[] | undefined {
|
||||
const root = readLinuxStat(internals, rootPid)
|
||||
if (root === undefined) return []
|
||||
const visited = new Set<number>()
|
||||
const result: ProcessIdentity[] = []
|
||||
const visit = (entry: ProcStat): boolean => {
|
||||
if (visited.has(entry.pid)) return true
|
||||
visited.add(entry.pid)
|
||||
let children: string
|
||||
try {
|
||||
children = internals.readFile(`/proc/${entry.pid}/task/${entry.pid}/children`)
|
||||
} catch (_unreadableChildren) {
|
||||
return false
|
||||
}
|
||||
for (const token of children.trim().split(/\s+/)) {
|
||||
if (token.length === 0 || !/^\d+$/.test(token)) continue
|
||||
const child = readLinuxStat(internals, Number(token))
|
||||
if (child !== undefined && !visit(child)) return false
|
||||
}
|
||||
result.push({ pid: entry.pid, started: entry.started })
|
||||
return true
|
||||
}
|
||||
return visit(root) ? result : undefined
|
||||
}
|
||||
|
||||
class LinuxProcessInspector extends PosixProcessInspector {
|
||||
constructor(
|
||||
private readonly arch: NodeJS.Architecture,
|
||||
@@ -283,20 +325,32 @@ class LinuxProcessInspector extends PosixProcessInspector {
|
||||
return tpgid !== undefined && tpgid > 0 ? tpgid : undefined
|
||||
}
|
||||
|
||||
isStdinWaiting(pgid: number): boolean {
|
||||
isStdinWaiting(pgid: number, scanNamespace = true): boolean {
|
||||
const table = SYSCALLS[this.arch]
|
||||
if (table === undefined) return false
|
||||
// A POSIX process group is normally led by PID == PGID. Interactive shells
|
||||
// wait on stdin in that leader, so inspect it before walking the whole PID
|
||||
// namespace. Large container PID namespaces otherwise make every PTY
|
||||
// readiness poll scan thousands of unrelated processes.
|
||||
if (processWaitsOnStdin(this.internals, pgid, pgid, table)) return true
|
||||
if (!scanNamespace) return false
|
||||
for (const pid of numericEntries(this.internals, '/proc')) {
|
||||
if (readLinuxStat(this.internals, pid)?.pgrp !== pgid) continue
|
||||
for (const tid of numericEntries(this.internals, `/proc/${pid}/task`)) {
|
||||
const syscall = readSyscall(this.internals, pid, tid)
|
||||
if (syscall !== undefined && syscallWaitsOnStdin(this.internals, pid, syscall, table)) return true
|
||||
}
|
||||
if (pid === pgid) continue
|
||||
if (processWaitsOnStdin(this.internals, pid, pgid, table)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
processTree(rootPid: number): ProcessIdentity[] {
|
||||
processTree(rootPid: number, scanNamespace = true): ProcessIdentity[] {
|
||||
// Linux exposes each process's direct children without requiring a scan of
|
||||
// the container's whole PID namespace. Fall back for kernels or procfs
|
||||
// mounts that do not provide the children file.
|
||||
const rooted = linuxProcessTreeFromChildren(this.internals, rootPid)
|
||||
if (rooted !== undefined) return rooted
|
||||
if (!scanNamespace) {
|
||||
const root = readLinuxStat(this.internals, rootPid)
|
||||
return root === undefined ? [] : [{ pid: root.pid, started: root.started }]
|
||||
}
|
||||
const entries = numericEntries(this.internals, '/proc').flatMap((pid) => {
|
||||
const stat = readLinuxStat(this.internals, pid)
|
||||
return stat === undefined ? [] : [{ pid, parentPid: stat.parentPid, started: stat.started }]
|
||||
@@ -338,11 +392,11 @@ class MacProcessInspector extends PosixProcessInspector {
|
||||
}
|
||||
}
|
||||
|
||||
isStdinWaiting(_pgid: number): boolean {
|
||||
isStdinWaiting(_pgid: number, _scanNamespace = true): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
processTree(rootPid: number): ProcessIdentity[] {
|
||||
processTree(rootPid: number, _scanNamespace = true): ProcessIdentity[] {
|
||||
return processTree(macProcessTable(this.internals), rootPid)
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle {
|
||||
private readonly graceMs: number,
|
||||
) {
|
||||
this.pid = terminal.pid
|
||||
this.rootIdentity = inspector.processTree(this.pid).find(member => member.pid === this.pid)
|
||||
this.rootIdentity = inspector.processTree(this.pid, false).find(member => member.pid === this.pid)
|
||||
this.done = this.outcome.promise
|
||||
this.dataDisposable = terminal.onData((data) => { this.output.write(Buffer.from(data, 'utf8')) })
|
||||
this.exitDisposable = terminal.onExit(({ exitCode, signal: exitSignal }) => {
|
||||
@@ -81,12 +81,14 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle {
|
||||
// Local inspection is synchronous; the seam returns a promise for remote transports.
|
||||
// oxlint-disable-next-line typescript/require-await -- Preserve promise rejection semantics at the async provider contract.
|
||||
async inspectForeground(): Promise<SubprocessTerminalForeground | undefined> {
|
||||
this.descendants()
|
||||
// Readiness polling may run every few milliseconds. Track the rooted tree
|
||||
// here, but reserve the full process-session sweep for teardown.
|
||||
this.descendants(false, false)
|
||||
const processGroupId = this.inspector.foregroundPgid(this.pid)
|
||||
if (processGroupId === undefined) return undefined
|
||||
return {
|
||||
processGroupId,
|
||||
inputWaiting: this.inspector.isStdinWaiting(processGroupId),
|
||||
inputWaiting: this.inspector.isStdinWaiting(processGroupId, false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,20 +143,22 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle {
|
||||
return members.filter(member => this.inspector.isAlive(member))
|
||||
}
|
||||
|
||||
private descendants(): ProcessIdentity[] {
|
||||
private descendants(includeSession = true, scanNamespace = true): ProcessIdentity[] {
|
||||
// Adopt newly scanned members only while the numeric root pid provably
|
||||
// still carries the spawned shell's start identity: after the shell dies,
|
||||
// a recycled pid's tree and session must not donate an unrelated
|
||||
// process's children to this session's signalling. Already-adopted
|
||||
// members keep their own start identities, which every signal rechecks.
|
||||
const tree = this.inspector.processTree(this.pid)
|
||||
const tree = this.inspector.processTree(this.pid, scanNamespace)
|
||||
const root = tree.find(member => member.pid === this.pid)
|
||||
const rootVerified = this.rootIdentity !== undefined
|
||||
&& root !== undefined
|
||||
&& root.started === this.rootIdentity.started
|
||||
this.trackedDescendants = this.survivors(this.unionMembers(
|
||||
this.trackedDescendants,
|
||||
...rootVerified ? [tree, this.inspector.processSession(this.pid)] : [],
|
||||
...rootVerified
|
||||
? [tree, ...includeSession ? [this.inspector.processSession(this.pid)] : []]
|
||||
: [],
|
||||
).filter(member => member.pid !== this.pid))
|
||||
return this.trackedDescendants
|
||||
}
|
||||
|
||||
@@ -95,6 +95,9 @@ describe('Linux process inspector', () => {
|
||||
fake.files.set('/proc/11/stat', stat(11, 21, 30, -1, '501'))
|
||||
fake.files.set('/proc/12/stat', stat(12, 22, 30, -1, '502', 10))
|
||||
fake.files.set('/proc/13/stat', stat(13, 23, 30, -1, '503', 12))
|
||||
fake.files.set('/proc/10/task/10/children', '12')
|
||||
fake.files.set('/proc/12/task/12/children', '13')
|
||||
fake.files.set('/proc/13/task/13/children', '')
|
||||
const inspector = createProcessInspector('linux', 'x64', fake.internals)
|
||||
expect(inspector.foregroundPgid(10)).toBe(40)
|
||||
expect(inspector.foregroundPgid(11)).toBeUndefined()
|
||||
@@ -124,6 +127,28 @@ describe('Linux process inspector', () => {
|
||||
expect(fake.kills).toEqual([[-40, 'SIGINT'], [10, 'SIGTERM']])
|
||||
})
|
||||
|
||||
it('walks a rooted process tree without enumerating the PID namespace', () => {
|
||||
const fake = fakeInternals()
|
||||
fake.files.set('/proc/10/stat', stat(10, 10, 10, 10, '500'))
|
||||
fake.files.set('/proc/10/task/10/children', '11')
|
||||
fake.files.set('/proc/11/stat', stat(11, 10, 10, 10, '501', 10))
|
||||
fake.files.set('/proc/11/task/11/children', '')
|
||||
|
||||
expect(createProcessInspector('linux', 'x64', fake.internals).processTree(10)).toEqual([
|
||||
{ pid: 11, started: '501' },
|
||||
{ pid: 10, started: '500' },
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps readiness inspection local when procfs has no children index', () => {
|
||||
const fake = fakeInternals()
|
||||
fake.files.set('/proc/10/stat', stat(10, 10, 10, 10, '500'))
|
||||
const inspector = createProcessInspector('linux', 'x64', fake.internals)
|
||||
|
||||
expect(inspector.processTree(10, false)).toEqual([{ pid: 10, started: '500' }])
|
||||
expect(inspector.isStdinWaiting(10, false)).toBe(false)
|
||||
})
|
||||
|
||||
it('detects read, select, poll, and epoll waits across non-leader threads', () => {
|
||||
const fake = fakeInternals()
|
||||
fake.dirs.set('/proc', ['100', '101'])
|
||||
@@ -156,6 +181,16 @@ describe('Linux process inspector', () => {
|
||||
expect(inspector.isStdinWaiting(77)).toBe(true)
|
||||
})
|
||||
|
||||
it('checks a waiting process-group leader without scanning the PID namespace', () => {
|
||||
const fake = fakeInternals()
|
||||
fake.files.set('/proc/77/stat', stat(77, 77, 77, 77, '1'))
|
||||
fake.dirs.set('/proc/77/task', ['77'])
|
||||
fake.files.set('/proc/77/task/77/syscall', syscall(0, 0))
|
||||
|
||||
const inspector = createProcessInspector('linux', 'x64', fake.internals)
|
||||
expect(inspector.isStdinWaiting(77)).toBe(true)
|
||||
})
|
||||
|
||||
it('fails closed on unsupported, malformed, unreadable, or non-stdin waits', () => {
|
||||
const fake = fakeInternals()
|
||||
fake.dirs.set('/proc', ['100'])
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
import { Buffer } from 'node:buffer'
|
||||
|
||||
export { CONTROLLED_PROMPT } from '@deepseek-ai/dsh-terminal'
|
||||
|
||||
/** OSC marker emitted by the controlled bash before each prompt. */
|
||||
export const PROMPT_MARKER_PREFIX = '133;D;'
|
||||
|
||||
/** Exact printable prompt emitted after the private marker. */
|
||||
export const CONTROLLED_PROMPT = 'dsh> '
|
||||
|
||||
/** One sanitized chunk plus whether it contained the owned prompt marker. */
|
||||
export interface SanitizedChunk {
|
||||
text: string
|
||||
|
||||
@@ -42,6 +42,9 @@ export type {
|
||||
} from './types.ts'
|
||||
export { TerminalBackendCleanupError } from './types.ts'
|
||||
|
||||
/** Printable prompt shared by terminal backends and persistent shell consumers. */
|
||||
export const CONTROLLED_PROMPT = 'dsh> '
|
||||
|
||||
/** Opaque identity minted by {@link TerminalSessionService} for one live PTY session. */
|
||||
export type TerminalSessionId = TerminalSessionIdValue
|
||||
|
||||
|
||||
Generated
+18
@@ -8340,6 +8340,9 @@ importers:
|
||||
'@deepseek-ai/dsh-agent-spine-demo':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/examples/agent-spine-demo
|
||||
'@deepseek-ai/dsh-agent-tool-presentation':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/core/agent-tool-presentation
|
||||
'@deepseek-ai/dsh-anonymous-user-id':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/identity/anonymous-user-id
|
||||
@@ -8361,6 +8364,9 @@ importers:
|
||||
'@deepseek-ai/dsh-code-runtime-worker-thread':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/code-runtime/code-runtime-worker-thread
|
||||
'@deepseek-ai/dsh-command-compact':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/compaction/command-compact
|
||||
'@deepseek-ai/dsh-command-goal':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/goal/command-goal
|
||||
@@ -8442,6 +8448,9 @@ importers:
|
||||
'@deepseek-ai/dsh-permission-presets':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/interaction/permission-presets
|
||||
'@deepseek-ai/dsh-persona':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/preset/persona
|
||||
'@deepseek-ai/dsh-plan-mode':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/plan/plan-mode
|
||||
@@ -8514,6 +8523,9 @@ importers:
|
||||
'@deepseek-ai/dsh-skill-filesystem':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/skill/skill-filesystem
|
||||
'@deepseek-ai/dsh-spill':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/spill/spill
|
||||
'@deepseek-ai/dsh-subagent':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/subagent/subagent
|
||||
@@ -8568,12 +8580,18 @@ importers:
|
||||
'@deepseek-ai/dsh-tool-fs':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/fs/tool-fs
|
||||
'@deepseek-ai/dsh-tool-fs-search':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/fs/tool-fs-search
|
||||
'@deepseek-ai/dsh-tool-goal':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/goal/tool-goal
|
||||
'@deepseek-ai/dsh-tool-jobs':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/jobs/tool-jobs
|
||||
'@deepseek-ai/dsh-tool-ralph':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/workflow/tool-ralph
|
||||
'@deepseek-ai/dsh-tool-skill':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/skill/tool-skill
|
||||
|
||||
@@ -67,7 +67,7 @@ class RuntimeBuildHook(BuildHookInterface):
|
||||
expected_executable = matches[0][1]
|
||||
runtime_dir = Path(self.root) / "src" / "deepseek_harness_runtime" / "runtime"
|
||||
runtime_files = sorted(runtime_dir.glob("dsh-jsonrpc-agent-pkg-*") if runtime_dir.is_dir() else [])
|
||||
expected_files = [expected_executable]
|
||||
expected_files = [expected_executable, f"{expected_executable}-rg"]
|
||||
if "-macos-" in expected_executable:
|
||||
expected_files.append(f"{expected_executable}-spawn-helper")
|
||||
found_files = [path.name for path in runtime_files]
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-loop": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-spine-demo": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-tool-presentation": "workspace:^",
|
||||
"@deepseek-ai/dsh-app-boot": "workspace:^",
|
||||
"@deepseek-ai/dsh-attachment": "workspace:^",
|
||||
"@deepseek-ai/dsh-shell": "workspace:^",
|
||||
@@ -22,6 +23,7 @@
|
||||
"@deepseek-ai/dsh-brand": "workspace:^",
|
||||
"@deepseek-ai/dsh-code-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-code-runtime-worker-thread": "workspace:^",
|
||||
"@deepseek-ai/dsh-command-compact": "workspace:^",
|
||||
"@deepseek-ai/dsh-command-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-commands": "workspace:^",
|
||||
"@deepseek-ai/dsh-compaction": "workspace:^",
|
||||
@@ -49,6 +51,7 @@
|
||||
"@deepseek-ai/dsh-home-paths": "workspace:^",
|
||||
"@deepseek-ai/dsh-permission-presets": "workspace:^",
|
||||
"@deepseek-ai/dsh-plan-mode": "workspace:^",
|
||||
"@deepseek-ai/dsh-persona": "workspace:^",
|
||||
"@deepseek-ai/dsh-terminal": "workspace:^",
|
||||
"@deepseek-ai/dsh-terminal-bash": "workspace:^",
|
||||
"@deepseek-ai/dsh-repeat-tool-reminder": "workspace:^",
|
||||
@@ -71,6 +74,7 @@
|
||||
"@deepseek-ai/dsh-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-skill": "workspace:^",
|
||||
"@deepseek-ai/dsh-skill-filesystem": "workspace:^",
|
||||
"@deepseek-ai/dsh-spill": "workspace:^",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:^",
|
||||
"@deepseek-ai/dsh-subagent-acp": "workspace:^",
|
||||
"@deepseek-ai/dsh-subagent-fork-in-process": "workspace:^",
|
||||
@@ -89,7 +93,9 @@
|
||||
"@deepseek-ai/dsh-tool-bash-persistent": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-cordis": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-fs": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-fs-search": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-ralph": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-skill": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-subagent": "workspace:^",
|
||||
|
||||
@@ -5,8 +5,8 @@ Two runtime carriers coexist under ``runtime/``, both injected by the repo's
|
||||
|
||||
- **exe (production)**: single-file Node executables named
|
||||
``dsh-jsonrpc-agent-pkg-<platform>-<arch>`` (platform in {linux, macos}, arch in
|
||||
{x64, arm64}); macOS also uses a sibling ``-spawn-helper``. The target machine
|
||||
needs no Node installation.
|
||||
{x64, arm64}) with a sibling ``-rg`` executable; macOS also uses a sibling
|
||||
``-spawn-helper``. The target machine needs no Node installation.
|
||||
- **node (dev-only)**: the full deploy closure under ``runtime/node/``
|
||||
(``package.json`` + ``node_modules/``), executed as ``node
|
||||
runtime/node/node_modules/@deepseek-ai/dsh-sdk-jsonrpc-demo/lib/packaged-bin.js`` on a
|
||||
@@ -83,6 +83,12 @@ def bundled_runtime_path() -> Path:
|
||||
f"deepseek-harness-runtime-bin is missing the runtime executable at {path}. "
|
||||
+ _EXE_ACQUISITION_HINT
|
||||
)
|
||||
ripgrep = Path(f"{path}-rg")
|
||||
if not ripgrep.is_file():
|
||||
raise FileNotFoundError(
|
||||
f"deepseek-harness-runtime-bin is missing the ripgrep sidecar at {ripgrep}. "
|
||||
+ _EXE_ACQUISITION_HINT
|
||||
)
|
||||
if tag.startswith("macos-"):
|
||||
helper = Path(f"{path}-spawn-helper")
|
||||
if not helper.is_file():
|
||||
|
||||
@@ -92,6 +92,10 @@ def test_stage_runtime_copies_platform_payload(
|
||||
executable.write_bytes(b"runtime")
|
||||
executable.chmod(0o755)
|
||||
expected = {executable.name: b"runtime"}
|
||||
ripgrep = Path(f"{executable}-rg")
|
||||
ripgrep.write_bytes(b"ripgrep")
|
||||
ripgrep.chmod(0o755)
|
||||
expected[ripgrep.name] = b"ripgrep"
|
||||
if with_helper:
|
||||
spawn_helper = Path(f"{executable}-spawn-helper")
|
||||
spawn_helper.write_bytes(b"helper")
|
||||
|
||||
@@ -396,7 +396,8 @@ class SingleExeBuild {
|
||||
if (!this.cli.dryRun && !existsSync(product)) {
|
||||
throw new Error(`build-exe-for-python-sdk: product ${product} is missing after the pkg run; inspect ${this.outDir}.`)
|
||||
}
|
||||
if (target.platform !== 'macos') return [product]
|
||||
const ripgrep = await this.copyRipgrepSidecar(target, product)
|
||||
if (target.platform !== 'macos') return [product, ripgrep]
|
||||
const spawnHelper = `${product}-spawn-helper`
|
||||
const source = join(this.staging, 'node_modules', 'node-pty', 'prebuilds', `darwin-${target.arch}`, 'spawn-helper')
|
||||
if (this.cli.dryRun) {
|
||||
@@ -405,7 +406,31 @@ class SingleExeBuild {
|
||||
await copyFile(source, spawnHelper)
|
||||
await chmod(spawnHelper, 0o755)
|
||||
}
|
||||
return [product, spawnHelper]
|
||||
return [product, ripgrep, spawnHelper]
|
||||
}
|
||||
|
||||
/** Copy the target ripgrep binary beside the executable so Node can spawn it outside pkg's virtual filesystem. */
|
||||
private async copyRipgrepSidecar(target: Target, product: string): Promise<string> {
|
||||
const platform = target.platform === 'macos' ? 'darwin' : target.platform
|
||||
const source = join(
|
||||
this.staging,
|
||||
'node_modules',
|
||||
'@vscode',
|
||||
`ripgrep-${platform}-${target.arch}`,
|
||||
'bin',
|
||||
'rg',
|
||||
)
|
||||
const destination = `${product}-rg`
|
||||
if (this.cli.dryRun) {
|
||||
console.log(`build-exe-for-python-sdk: [dry-run] cp ${source} ${destination}`)
|
||||
return destination
|
||||
}
|
||||
if (!existsSync(source)) {
|
||||
throw new Error(`build-exe-for-python-sdk: target ripgrep binary is missing at ${source}.`)
|
||||
}
|
||||
await copyFile(source, destination)
|
||||
await chmod(destination, 0o755)
|
||||
return destination
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -48,7 +48,8 @@ PLATFORMS = load_platforms()
|
||||
|
||||
|
||||
def runtime_suffixes(executable_name: str) -> tuple[str, ...]:
|
||||
return ("", "-spawn-helper") if "-macos-" in executable_name else ("",)
|
||||
suffixes = ("", "-rg")
|
||||
return (*suffixes, "-spawn-helper") if "-macos-" in executable_name else suffixes
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
@@ -29,6 +29,9 @@ MINIMAL_PROMPT = "Exercise the packaged minimal agent's persistent Bash and stri
|
||||
MINIMAL_TEXT = "minimal agent smoke ok"
|
||||
MINIMAL_EDITOR_PATH_PREFIX = "Editor path: "
|
||||
MINIMAL_SYSTEM_PROMPT = "You are a helpful software engineer assistant."
|
||||
FS_SEARCH_PROMPT = "Exercise the packaged filesystem search tools."
|
||||
FS_SEARCH_TEXT = "filesystem search smoke ok"
|
||||
FS_SEARCH_MARKER = "PACKAGED_FS_SEARCH_OK"
|
||||
MINIMAL_CORDIS = (
|
||||
Path(__file__).resolve().parent.parent / "examples" / "jsonrpc-agent" / "minimal.cordis.yml"
|
||||
)
|
||||
@@ -109,6 +112,29 @@ CUSTOM_CORDIS = """\
|
||||
- id: cordis-tool
|
||||
name: '@deepseek-ai/dsh-tool-cordis'
|
||||
"""
|
||||
FS_SEARCH_CORDIS = """\
|
||||
- id: sdk-jsonrpc-server
|
||||
name: '@deepseek-ai/dsh-sdk-jsonrpc-server'
|
||||
- id: agent-core
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
workspaceContext: false
|
||||
skills:
|
||||
enabled: false
|
||||
toolBash: false
|
||||
toolJobs: false
|
||||
- id: sessions
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: !!js process.env.DSH_SESSION_ROOT
|
||||
compression: 'none'
|
||||
- id: subprocess
|
||||
name: '@deepseek-ai/dsh-subprocess-local'
|
||||
- id: fs-search
|
||||
name: '@deepseek-ai/dsh-tool-fs-search'
|
||||
config:
|
||||
sampleOverCapGlobResults: false
|
||||
"""
|
||||
class MockModelHandler(BaseHTTPRequestHandler):
|
||||
"""Return deterministic text, worker, and orchestration completions."""
|
||||
|
||||
@@ -143,6 +169,9 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]:
|
||||
if latest.get("role") == "tool":
|
||||
call_id, tool_name = latest_tool_call(messages)
|
||||
tool_text = message_text(latest.get("content"))
|
||||
fs_search = fs_search_tool_followup(call_id, tool_name, tool_text)
|
||||
if fs_search is not None:
|
||||
return fs_search
|
||||
minimal = minimal_tool_followup(body, call_id, tool_name, tool_text)
|
||||
if minimal is not None:
|
||||
return minimal
|
||||
@@ -192,6 +221,7 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]:
|
||||
SNAPSHOT_PROMPT,
|
||||
CODE_PROMPT,
|
||||
WORKFLOW_PROMPT,
|
||||
FS_SEARCH_PROMPT,
|
||||
}
|
||||
prompt = next(
|
||||
(candidate for candidate in user_prompts if candidate in scenario_prompts),
|
||||
@@ -233,9 +263,40 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]:
|
||||
},
|
||||
},
|
||||
)
|
||||
if prompt == FS_SEARCH_PROMPT:
|
||||
assert_advertised_tool(body, "grep")
|
||||
assert_advertised_tool(body, "glob")
|
||||
return tool_call_chunks(
|
||||
"fs-search-grep",
|
||||
"grep",
|
||||
{"pattern": FS_SEARCH_MARKER, "path": "."},
|
||||
)
|
||||
return text_chunks(EXPECTED_TEXT)
|
||||
|
||||
|
||||
def fs_search_tool_followup(
|
||||
call_id: str,
|
||||
tool_name: str,
|
||||
tool_text: str,
|
||||
) -> list[dict[str, object]] | None:
|
||||
"""Exercise both ripgrep-backed tools through the packaged executable."""
|
||||
if not call_id.startswith("fs-search-"):
|
||||
return None
|
||||
if call_id == "fs-search-grep" and tool_name == "grep":
|
||||
if "needle.txt" not in tool_text or FS_SEARCH_MARKER not in tool_text:
|
||||
raise AssertionError(f"packaged grep returned no marker: {tool_text}")
|
||||
return tool_call_chunks(
|
||||
"fs-search-glob",
|
||||
"glob",
|
||||
{"pattern": "**/*.txt"},
|
||||
)
|
||||
if call_id == "fs-search-glob" and tool_name == "glob":
|
||||
if "needle.txt" not in tool_text:
|
||||
raise AssertionError(f"packaged glob returned no fixture path: {tool_text}")
|
||||
return text_chunks(FS_SEARCH_TEXT)
|
||||
raise AssertionError(f"unexpected filesystem-search follow-up: {call_id} {tool_name}: {tool_text}")
|
||||
|
||||
|
||||
def minimal_tool_followup(
|
||||
body: dict[str, object],
|
||||
call_id: str,
|
||||
@@ -477,13 +538,13 @@ def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--scenario",
|
||||
choices=("all", "sdk-default", "sdk-custom", "sdk-minimal", "sdk-snapshot", "direct"),
|
||||
choices=("all", "sdk-default", "sdk-custom", "sdk-minimal", "sdk-fs-search", "sdk-snapshot", "direct"),
|
||||
default="all",
|
||||
)
|
||||
parser.add_argument("--exe", type=Path)
|
||||
parser.add_argument("--update-snapshots", action="store_true")
|
||||
args = parser.parse_args()
|
||||
if args.scenario in {"all", "sdk-custom", "sdk-minimal", "sdk-snapshot", "direct"} and args.exe is None:
|
||||
if args.scenario in {"all", "sdk-custom", "sdk-minimal", "sdk-fs-search", "sdk-snapshot", "direct"} and args.exe is None:
|
||||
parser.error("--exe is required for custom, minimal, snapshot, and direct scenarios")
|
||||
if args.update_snapshots and args.scenario not in {"all", "sdk-snapshot"}:
|
||||
parser.error("--update-snapshots requires --scenario sdk-snapshot or all")
|
||||
@@ -499,6 +560,9 @@ def main() -> None:
|
||||
if args.scenario in {"all", "sdk-minimal"}:
|
||||
assert args.exe is not None
|
||||
smoke_sdk_minimal(model.url, args.exe.resolve())
|
||||
if args.scenario in {"all", "sdk-fs-search"}:
|
||||
assert args.exe is not None
|
||||
smoke_sdk_fs_search(model.url, args.exe.resolve())
|
||||
if args.scenario in {"all", "sdk-snapshot"}:
|
||||
assert args.exe is not None
|
||||
smoke_sdk_snapshot(model.url, args.exe.resolve(), args.update_snapshots)
|
||||
@@ -588,6 +652,33 @@ def smoke_sdk_minimal(base_url: str, executable: Path) -> None:
|
||||
assert_session_log(sessions, root, MINIMAL_TEXT, "COUNT=1", "COUNT=2 CWD=/tmp")
|
||||
|
||||
|
||||
def smoke_sdk_fs_search(base_url: str, executable: Path) -> None:
|
||||
"""Exercise real grep and glob spawns through the packaged executable."""
|
||||
from deepseek_harness import DeepSeekHarness
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="dsh-sdk-fs-search-") as temporary:
|
||||
root = Path(temporary).resolve()
|
||||
(root / "needle.txt").write_text(f"{FS_SEARCH_MARKER}\n")
|
||||
sessions = root / "sessions"
|
||||
cordis = root / "cordis.yml"
|
||||
cordis.write_text(FS_SEARCH_CORDIS)
|
||||
with DeepSeekHarness(
|
||||
provider="deepseek-official",
|
||||
model="smoke-model",
|
||||
cwd=str(root),
|
||||
session_root=str(sessions),
|
||||
cordis=str(cordis),
|
||||
runtime_bin=str(executable),
|
||||
api_key="sk-keyless-smoke",
|
||||
base_url=base_url,
|
||||
request_timeout_seconds=60,
|
||||
) as harness:
|
||||
result = harness.run(FS_SEARCH_PROMPT, session_id="fs-search-smoke")
|
||||
|
||||
assert result.final_response == FS_SEARCH_TEXT, result.final_response
|
||||
assert_session_log(sessions, root, FS_SEARCH_TEXT, FS_SEARCH_MARKER, "needle.txt")
|
||||
|
||||
|
||||
def smoke_sdk_snapshot(base_url: str, executable: Path, update_snapshots: bool) -> None:
|
||||
"""Drive and compare the advanced SDK/executable behavioral snapshot."""
|
||||
from deepseek_harness import DeepSeekHarness
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
/**
|
||||
* Verify that the executable deploy manifest supplies every required workspace
|
||||
* peer in its dependency graph. With auto peer installation disabled, a missing
|
||||
* root peer can otherwise fail only when Cordis loads the packaged plugin.
|
||||
* Verify that the executable deploy manifest supplies every plugin referenced
|
||||
* by a shipped agent preset and every required workspace peer in its dependency
|
||||
* graph. With auto peer installation disabled, either omission can otherwise
|
||||
* fail only when Cordis loads the packaged plugin.
|
||||
*/
|
||||
import { globSync } from 'node:fs'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
import { basename, dirname, resolve } from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import * as yaml from 'js-yaml'
|
||||
|
||||
interface JsExpr {
|
||||
__jsExpr: string
|
||||
}
|
||||
|
||||
interface PackageManifest {
|
||||
name?: string
|
||||
@@ -21,6 +27,23 @@ interface WorkspacePackage {
|
||||
manifest: PackageManifest
|
||||
}
|
||||
|
||||
interface RuntimePlatform {
|
||||
tag: string
|
||||
executable: string
|
||||
}
|
||||
|
||||
type RuntimePlatformManifest = Record<string, RuntimePlatform>
|
||||
|
||||
const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
|
||||
kind: 'scalar',
|
||||
resolve: data => typeof data === 'string',
|
||||
construct: (data: unknown): JsExpr => {
|
||||
if (typeof data !== 'string') throw new TypeError('!!js requires a scalar string')
|
||||
return { __jsExpr: data }
|
||||
},
|
||||
})
|
||||
const schema = yaml.JSON_SCHEMA.extend(jsExprType)
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const { values } = parseArgs({
|
||||
args: process.argv.slice(2),
|
||||
@@ -31,6 +54,7 @@ const runtimeManifest = await loadManifest(runtimeManifestPath)
|
||||
const runtimeName = runtimeManifest.name ?? 'python/sdk-runtime'
|
||||
const workspace = await loadWorkspacePackages()
|
||||
const runtimeDependencies = runtimeManifest.dependencies ?? {}
|
||||
const platforms = await loadJson<RuntimePlatformManifest>(resolve(root, 'python/sdk-runtime/platforms.json'))
|
||||
const parents = new Map<string, string | undefined>()
|
||||
const queue: string[] = []
|
||||
|
||||
@@ -40,7 +64,7 @@ for (const dependency of Object.keys(runtimeDependencies).sort()) {
|
||||
queue.push(dependency)
|
||||
}
|
||||
|
||||
const failures: string[] = []
|
||||
const failures = await missingPresetPlugins(runtimeDependencies, platforms)
|
||||
for (let index = 0; index < queue.length; index += 1) {
|
||||
const packageName = queue[index]
|
||||
if (packageName === undefined) continue
|
||||
@@ -65,12 +89,91 @@ for (let index = 0; index < queue.length; index += 1) {
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error('verify-runtime-closure: required workspace peers are missing from python/sdk-runtime dependencies:')
|
||||
console.error('verify-runtime-closure: preset plugins or required workspace peers are missing from python/sdk-runtime dependencies:')
|
||||
for (const failure of failures) console.error(` ${failure}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`verify-runtime-closure: ${queue.length} workspace packages form a closed runtime dependency graph.`)
|
||||
const presetCount = globSync('apps/cli/config/agent-presets/*/agent.cordis.yml', { cwd: root }).length
|
||||
console.log(
|
||||
`verify-runtime-closure: ${presetCount} agent presets and ${queue.length} workspace packages form a closed runtime dependency graph.`,
|
||||
)
|
||||
|
||||
async function missingPresetPlugins(
|
||||
runtimeDependencies: Readonly<Record<string, string>>,
|
||||
platforms: RuntimePlatformManifest,
|
||||
): Promise<string[]> {
|
||||
const missing = new Map<string, Set<string>>()
|
||||
const failures: string[] = []
|
||||
const presetPaths = globSync('apps/cli/config/agent-presets/*/agent.cordis.yml', { cwd: root }).sort()
|
||||
for (const presetPath of presetPaths) {
|
||||
const document: unknown = yaml.load(await readFile(resolve(root, presetPath), 'utf8'), { schema })
|
||||
if (!Array.isArray(document)) {
|
||||
failures.push(`${presetPath}: preset root must be a Loader entry array`)
|
||||
continue
|
||||
}
|
||||
for (const target of Object.keys(platforms).sort()) {
|
||||
const processPlatform = processPlatformForTarget(target)
|
||||
for (const plugin of activeBarePluginPackages(document, processPlatform)) {
|
||||
if (runtimeDependencies[plugin] !== undefined) continue
|
||||
const preset = basename(dirname(presetPath))
|
||||
const key = `${preset} preset -> ${plugin}`
|
||||
const targets = missing.get(key) ?? new Set<string>()
|
||||
targets.add(target)
|
||||
missing.set(key, targets)
|
||||
}
|
||||
}
|
||||
}
|
||||
failures.push(...[...missing.entries()].map(([chain, targets]) =>
|
||||
`${chain} (${[...targets].sort().join(', ')})`))
|
||||
return failures
|
||||
}
|
||||
|
||||
function activeBarePluginPackages(entries: unknown[], processPlatform: string): Set<string> {
|
||||
const packages = new Set<string>()
|
||||
const visit = (value: unknown, parentDisabled: boolean): void => {
|
||||
if (!isRecord(value)) return
|
||||
const disabled = parentDisabled || disabledOnPlatform(value.disabled, processPlatform)
|
||||
if (disabled) return
|
||||
if (typeof value.name === 'string') {
|
||||
const packageName = barePackageName(value.name)
|
||||
if (packageName !== undefined) packages.add(packageName)
|
||||
}
|
||||
if (Array.isArray(value.config)) {
|
||||
for (const child of value.config) visit(child, disabled)
|
||||
}
|
||||
}
|
||||
for (const entry of entries) visit(entry, false)
|
||||
return packages
|
||||
}
|
||||
|
||||
function disabledOnPlatform(value: unknown, processPlatform: string): boolean {
|
||||
if (typeof value === 'boolean') return value
|
||||
if (!isRecord(value) || typeof value.__jsExpr !== 'string') return false
|
||||
const match = /^process\.platform\s*(===|!==)\s*(['"])(win32|linux|darwin)\2$/.exec(value.__jsExpr.trim())
|
||||
if (match === null) return false
|
||||
const [, operator, , expected] = match
|
||||
return operator === '===' ? processPlatform === expected : processPlatform !== expected
|
||||
}
|
||||
|
||||
function processPlatformForTarget(target: string): string {
|
||||
if (target.startsWith('linux-')) return 'linux'
|
||||
if (target.startsWith('macos-')) return 'darwin'
|
||||
throw new Error(`verify-runtime-closure: unsupported runtime target ${JSON.stringify(target)}`)
|
||||
}
|
||||
|
||||
function barePackageName(specifier: string): string | undefined {
|
||||
if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.includes(':')) return undefined
|
||||
const parts = specifier.split('/')
|
||||
if (specifier.startsWith('@')) {
|
||||
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : undefined
|
||||
}
|
||||
return parts[0] || undefined
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
async function loadWorkspacePackages(): Promise<Map<string, WorkspacePackage>> {
|
||||
const paths = globSync(['packages/*/*/package.json', 'vendor/*/package.json'], { cwd: root })
|
||||
@@ -85,7 +188,11 @@ async function loadWorkspacePackages(): Promise<Map<string, WorkspacePackage>> {
|
||||
}
|
||||
|
||||
async function loadManifest(path: string): Promise<PackageManifest> {
|
||||
return JSON.parse(await readFile(path, 'utf8')) as PackageManifest
|
||||
return loadJson<PackageManifest>(path)
|
||||
}
|
||||
|
||||
async function loadJson<T>(path: string): Promise<T> {
|
||||
return JSON.parse(await readFile(path, 'utf8')) as T
|
||||
}
|
||||
|
||||
function formatChain(
|
||||
|
||||
Reference in New Issue
Block a user