返回源码地图

packages/terminal/terminal-bash/src/config.ts

main snapshot · da00f7f5358f · 正文引用章节 10;完整原文可核对,不声称全文件人工逐行审计

完整原文供逐行核对;页面收录不代表每行都经过人工语义审核。MIT 许可见 许可证。

1/** Validated configuration for the local PTY backend. */
2
3import z from '@deepseek-ai/schemastery'
4import { resolvePwshPath } from '@deepseek-ai/dsh-pwsh-local'
5
6/** One supported interactive shell dialect. */
7export type ShellDialect = 'bash' | 'pwsh'
8
9/** Public plugin configuration. */
10export interface Config {
11 /** Backend registry type (default: `shell`). */
12 backendType?: string
13 /** Interactive shell dialect (default: `bash`); selects the argv/env/startup defaults. */
14 shellDialect?: ShellDialect
15 /** Interactive shell executable (default per dialect: `/bin/bash`, or the resolved pwsh). */
16 shellPath?: string
17 /** Shell arguments (default per dialect: bash `--noprofile --norc -i`, pwsh `-NoLogo -NoProfile`). */
18 shellArgs?: string[]
19 /** Terminal rows. */
20 rows?: number
21 /** Terminal columns. */
22 cols?: number
23 /** Maximum retained logical lines. */
24 scrollbackLines?: number
25 /** Maximum retained UTF-8 bytes. */
26 scrollbackMaxBytes?: number
27 /** Maximum bytes returned by one read or settled viewport. */
28 maxReadBytes?: number
29 /** Readiness polling interval. */
30 pollIntervalMs?: number
31 /** Delay before Linux exact syscall probes. */
32 exactProbeAfterMs?: number
33 /** Silence duration that yields `inferred_idle`. */
34 idleSilenceMs?: number
35 /**
36 * Extra wait beyond `idleSilenceMs`, once a prompt marker was seen, for the shell to
37 * regain the foreground before `inferred_idle` settles; at least one `pollIntervalMs`.
38 */
39 handoffGraceMs?: number
40 /**
41 * Extra wait beyond `idleSilenceMs` and `handoffGraceMs`, once a prompt marker was seen but
42 * its printable tail has not arrived, before `inferred_idle` settles. The marker is written
43 * by the shell's own prompt function and the tail by the same render, so a missing tail is a
44 * delivery delay on a contended host rather than an absent prompt. Zero keeps the bound at
45 * `idleSilenceMs + handoffGraceMs`; any other value covers at least one `pollIntervalMs`, so a
46 * nonzero tolerance always contains a readiness poll.
47 */
48 promptTailGraceMs?: number
49 /** Absolute bound for one send and the complete pwsh startup sequence. */
50 timeoutMs?: number
51 /** Grace before teardown escalates to `SIGKILL`. */
52 disposeGraceMs?: number
53}
54
55/** Configuration after Schemastery defaults and dialect resolution. */
56export type ResolvedConfig = Omit<Required<Config>, 'shellDialect' | 'shellPath' | 'shellArgs'> & {
57 shellDialect: ShellDialect
58 shellPath: string
59 shellArgs: string[]
60}
61
62/** Bash dialect default executable. */
63export const DEFAULT_BASH_SHELL = '/bin/bash'
64/** Bash dialect default arguments (interactive, profile-free). */
65export const DEFAULT_BASH_ARGS = ['--noprofile', '--norc', '-i']
66/** Pwsh dialect default arguments (interactive host, profile-free). */
67export const DEFAULT_PWSH_ARGS = ['-NoLogo', '-NoProfile']
68
69/**
70 * Resolve the effective per-dialect shell specification. Defaulting is this
71 * explicit step: an unset or empty `shellPath`/`shellArgs` selects the
72 * dialect's defaults, while a non-empty explicit value always wins.
73 * (Schemastery materializes an absent optional array as `[]`, so emptiness —
74 * not just `undefined` — means "dialect default".)
75 * @param config - Schemastery-resolved plugin configuration.
76 * @returns the fully resolved configuration.
77 */
78export function resolveConfig(config: Config): ResolvedConfig {
79 const shellDialect = config.shellDialect ?? 'bash'
80 return {
81 ...(config as Required<Config>),
82 shellDialect,
83 shellPath: config.shellPath !== undefined && config.shellPath.length > 0
84 ? config.shellPath
85 : (shellDialect === 'pwsh' ? resolvePwshPath() : DEFAULT_BASH_SHELL),
86 shellArgs: config.shellArgs !== undefined && config.shellArgs.length > 0
87 ? config.shellArgs
88 : (shellDialect === 'pwsh' ? DEFAULT_PWSH_ARGS : DEFAULT_BASH_ARGS),
89 }
90}
91
92/** Schemastery config exposed by the plugin. */
93export const Config: z<Config> = z.object({
94 backendType: z.string().default('shell'),
95 shellDialect: z.union(['bash', 'pwsh'] as const).default('bash'),
96 shellPath: z.string().required(false),
97 shellArgs: z.array(z.string()).required(false),
98 rows: z.number().default(40),
99 cols: z.number().default(160),
100 scrollbackLines: z.number().default(10_000),
101 scrollbackMaxBytes: z.number().default(4 * 1024 * 1024),
102 maxReadBytes: z.number().default(256 * 1024),
103 pollIntervalMs: z.number().default(50),
104 exactProbeAfterMs: z.number().default(150),
105 idleSilenceMs: z.number().default(3_000),
106 handoffGraceMs: z.number().default(500),
107 promptTailGraceMs: z.number().default(0),
108 timeoutMs: z.number().default(30_000),
109 disposeGraceMs: z.number().default(3_000),
110})
111
112/**
113 * Assert every effective numeric config field is a positive safe integer — except
114 * `promptTailGraceMs`, whose zero is the documented "no extension" value — and that bounds
115 * compose.
116 * @param config - Schemastery-resolved plugin configuration.
117 * @returns Narrows the input to the fully resolved configuration.
118 */
119export function validateConfig(config: Config): asserts config is ResolvedConfig {
120 const resolved = config as ResolvedConfig
121 if (resolved.backendType.length === 0) throw new Error('terminal-bash: backendType must be non-empty')
122 if (resolved.shellPath.length === 0) throw new Error('terminal-bash: shellPath must be non-empty')
123 for (const [name, value] of Object.entries(resolved)) {
124 if (name === 'promptTailGraceMs') continue
125 if (typeof value === 'number' && (!Number.isSafeInteger(value) || value <= 0)) {
126 throw new Error(`terminal-bash: ${name} must be a positive safe integer`)
127 }
128 }
129 if (typeof resolved.promptTailGraceMs === 'number'
130 && (!Number.isSafeInteger(resolved.promptTailGraceMs) || resolved.promptTailGraceMs < 0)) {
131 throw new Error('terminal-bash: promptTailGraceMs must be a non-negative safe integer')
132 }
133 if (resolved.maxReadBytes > resolved.scrollbackMaxBytes) {
134 throw new Error('terminal-bash: maxReadBytes must not exceed scrollbackMaxBytes')
135 }
136 if (resolved.handoffGraceMs < resolved.pollIntervalMs) {
137 throw new Error('terminal-bash: handoffGraceMs must be at least pollIntervalMs so one readiness poll runs inside the grace window')
138 }
139 if (resolved.promptTailGraceMs !== 0 && resolved.promptTailGraceMs < resolved.pollIntervalMs) {
140 throw new Error('terminal-bash: promptTailGraceMs must be zero or at least pollIntervalMs so a nonzero tolerance contains one readiness poll')
141 }
142}