@tiphareth/dsh-hardssh 0.1.2
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/README.md +50 -0
- package/cordis.patch.yml +26 -0
- package/lib/client.js +17780 -0
- package/lib/environment-BL1jddfB.js +449 -0
- package/lib/fs.js +478 -0
- package/lib/index.js +6758 -0
- package/lib/subprocess.js +600 -0
- package/lib/switch-fs-CAJpFY9C.js +193 -0
- package/lib/switch-fs-RrZtG2gv.js +210 -0
- package/lib/types/backend.d.ts +108 -0
- package/lib/types/base/capability.d.ts +107 -0
- package/lib/types/base/index.d.ts +18 -0
- package/lib/types/base/ledger-router.d.ts +48 -0
- package/lib/types/base/ledger.d.ts +82 -0
- package/lib/types/base/model.d.ts +108 -0
- package/lib/types/base/namespace.d.ts +57 -0
- package/lib/types/base/plugin.d.ts +102 -0
- package/lib/types/base/registry.d.ts +60 -0
- package/lib/types/base/router.d.ts +40 -0
- package/lib/types/client/api.d.ts +85 -0
- package/lib/types/client/directory-flow.d.ts +82 -0
- package/lib/types/client/icons.d.ts +19 -0
- package/lib/types/client/index.d.ts +34 -0
- package/lib/types/client/locales.d.ts +104 -0
- package/lib/types/client/manager-button.d.ts +32 -0
- package/lib/types/client/migrate.d.ts +20 -0
- package/lib/types/client/ssh/api.d.ts +89 -0
- package/lib/types/client/ssh/apply.d.ts +23 -0
- package/lib/types/client/ssh/locales.d.ts +156 -0
- package/lib/types/client/ssh/mount.d.ts +13 -0
- package/lib/types/client/ssh/panel/ClusterTab.d.ts +8 -0
- package/lib/types/client/ssh/panel/HostFingerprintDialog.d.ts +16 -0
- package/lib/types/client/ssh/panel/HostFormDialog.d.ts +13 -0
- package/lib/types/client/ssh/panel/HostsTab.d.ts +10 -0
- package/lib/types/client/ssh/panel/SessionSecretDialog.d.ts +16 -0
- package/lib/types/client/ssh/panel/SshPanel.d.ts +14 -0
- package/lib/types/client/ssh/panel/TerminalTab.d.ts +12 -0
- package/lib/types/client/ssh/panel/TransferTab.d.ts +8 -0
- package/lib/types/client/ssh/panel/TunnelsTab.d.ts +8 -0
- package/lib/types/client/ssh/panel/controller.d.ts +23 -0
- package/lib/types/client/ssh/panel/helpers.d.ts +15 -0
- package/lib/types/client/ssh/panel/xterm.css.d.ts +3 -0
- package/lib/types/client/ssh/sidebar-entry.d.ts +25 -0
- package/lib/types/client/state.d.ts +32 -0
- package/lib/types/client/text.d.ts +11 -0
- package/lib/types/client/workspace-badges.d.ts +38 -0
- package/lib/types/client/workspace-gate.d.ts +15 -0
- package/lib/types/client-http.d.ts +15 -0
- package/lib/types/core.d.ts +43 -0
- package/lib/types/fs.d.ts +36 -0
- package/lib/types/host-http.d.ts +25 -0
- package/lib/types/index.d.ts +53 -0
- package/lib/types/ledger.d.ts +132 -0
- package/lib/types/protocol.d.ts +101 -0
- package/lib/types/providers/index.d.ts +19 -0
- package/lib/types/providers/local/provider.d.ts +58 -0
- package/lib/types/providers/ssh/provider.d.ts +112 -0
- package/lib/types/remote/environment.d.ts +29 -0
- package/lib/types/remote/output.d.ts +38 -0
- package/lib/types/remote/remote-fs.d.ts +69 -0
- package/lib/types/remote/remote-process.d.ts +41 -0
- package/lib/types/remote/remote-subprocess.d.ts +31 -0
- package/lib/types/remote/remote-terminal.d.ts +41 -0
- package/lib/types/remote-runner.d.ts +83 -0
- package/lib/types/remote-search.d.ts +50 -0
- package/lib/types/routes.d.ts +24 -0
- package/lib/types/runtime/workspace-core.d.ts +59 -0
- package/lib/types/seam-state.d.ts +69 -0
- package/lib/types/shell.d.ts +8 -0
- package/lib/types/ssh/connection/lease.d.ts +16 -0
- package/lib/types/ssh/connection/pool.d.ts +57 -0
- package/lib/types/ssh/engine.d.ts +434 -0
- package/lib/types/ssh/exec/output.d.ts +31 -0
- package/lib/types/ssh/known-hosts.d.ts +89 -0
- package/lib/types/ssh/plugin.d.ts +57 -0
- package/lib/types/ssh/protocol.d.ts +236 -0
- package/lib/types/ssh/routes.d.ts +44 -0
- package/lib/types/ssh/store.d.ts +107 -0
- package/lib/types/ssh/tools.d.ts +35 -0
- package/lib/types/ssh/transfer/progress.d.ts +14 -0
- package/lib/types/ssh/vault.d.ts +110 -0
- package/lib/types/subprocess.d.ts +32 -0
- package/lib/types/switch/switch-fs.d.ts +88 -0
- package/lib/types/switch/switch-subprocess.d.ts +34 -0
- package/lib/types/tools.d.ts +12 -0
- package/package.json +134 -0
- package/src/backend.ts +624 -0
- package/src/base/capability.ts +86 -0
- package/src/base/index.ts +18 -0
- package/src/base/ledger-router.ts +128 -0
- package/src/base/ledger.ts +299 -0
- package/src/base/model.ts +118 -0
- package/src/base/namespace.ts +102 -0
- package/src/base/plugin.ts +170 -0
- package/src/base/registry.ts +109 -0
- package/src/base/router.ts +43 -0
- package/src/client/api.ts +196 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/directory-flow.tsx +482 -0
- package/src/client/icons.tsx +50 -0
- package/src/client/index.ts +210 -0
- package/src/client/locales.ts +105 -0
- package/src/client/manager-button.tsx +269 -0
- package/src/client/migrate.ts +109 -0
- package/src/client/ssh/api.ts +411 -0
- package/src/client/ssh/apply.ts +50 -0
- package/src/client/ssh/locales.ts +322 -0
- package/src/client/ssh/mount.tsx +83 -0
- package/src/client/ssh/panel/ClusterTab.tsx +123 -0
- package/src/client/ssh/panel/HostFingerprintDialog.tsx +82 -0
- package/src/client/ssh/panel/HostFormDialog.tsx +228 -0
- package/src/client/ssh/panel/HostsTab.tsx +236 -0
- package/src/client/ssh/panel/SessionSecretDialog.tsx +80 -0
- package/src/client/ssh/panel/SshPanel.tsx +77 -0
- package/src/client/ssh/panel/TerminalTab.tsx +176 -0
- package/src/client/ssh/panel/TransferTab.tsx +232 -0
- package/src/client/ssh/panel/TunnelsTab.tsx +177 -0
- package/src/client/ssh/panel/controller.ts +48 -0
- package/src/client/ssh/panel/helpers.ts +26 -0
- package/src/client/ssh/panel/panel.module.css +1006 -0
- package/src/client/ssh/panel/xterm.css.ts +2 -0
- package/src/client/ssh/sidebar-entry.ts +123 -0
- package/src/client/state.ts +99 -0
- package/src/client/text.ts +22 -0
- package/src/client/workspace-badges.ts +96 -0
- package/src/client/workspace-gate.ts +225 -0
- package/src/client/workspace.module.css +283 -0
- package/src/client-http.ts +45 -0
- package/src/core.ts +47 -0
- package/src/fs.ts +85 -0
- package/src/host-http.ts +74 -0
- package/src/index.ts +244 -0
- package/src/ledger.ts +416 -0
- package/src/protocol.ts +114 -0
- package/src/providers/index.ts +34 -0
- package/src/providers/local/provider.ts +179 -0
- package/src/providers/ssh/provider.ts +274 -0
- package/src/remote/environment.ts +123 -0
- package/src/remote/output.ts +142 -0
- package/src/remote/remote-fs.ts +532 -0
- package/src/remote/remote-process.ts +203 -0
- package/src/remote/remote-subprocess.ts +159 -0
- package/src/remote/remote-terminal.ts +141 -0
- package/src/remote-runner.ts +201 -0
- package/src/remote-search.ts +163 -0
- package/src/routes.ts +395 -0
- package/src/runtime/workspace-core.ts +154 -0
- package/src/seam-state.ts +185 -0
- package/src/shell.ts +10 -0
- package/src/ssh/connection/lease.ts +17 -0
- package/src/ssh/connection/pool.ts +275 -0
- package/src/ssh/engine.ts +1761 -0
- package/src/ssh/exec/output.ts +70 -0
- package/src/ssh/known-hosts.ts +214 -0
- package/src/ssh/plugin.ts +184 -0
- package/src/ssh/protocol.ts +227 -0
- package/src/ssh/routes.ts +892 -0
- package/src/ssh/store.ts +544 -0
- package/src/ssh/tools.ts +402 -0
- package/src/ssh/transfer/progress.ts +75 -0
- package/src/ssh/vault.ts +477 -0
- package/src/subprocess.ts +71 -0
- package/src/switch/switch-fs.ts +253 -0
- package/src/switch/switch-subprocess.ts +59 -0
- package/src/tools.ts +221 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote-environment scrubbing and caching for the SSH process and terminal
|
|
3
|
+
* launchers. Ported from UynajGI/dsh-ssh (MIT) — adapted to the dsh-ssh
|
|
4
|
+
* engine. The read side is cached per (engine, alias) with a short TTL and
|
|
5
|
+
* merged concurrent requests, so repeated spawns/terminals on one host stop
|
|
6
|
+
* issuing an `env` exec each time (P1-12).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { SENSITIVE_ENV_PATTERN } from '@deepseek-ai/dsh-subprocess'
|
|
10
|
+
import type { SshEngine } from '../ssh/engine.ts'
|
|
11
|
+
import { shellQuote as quoteShellArg } from '../shell.ts'
|
|
12
|
+
|
|
13
|
+
/** Quote one argument for a POSIX login shell (canonical implementation in shell.ts). */
|
|
14
|
+
export { quoteShellArg }
|
|
15
|
+
/** Wrap a remote command so it runs from the given working directory. */
|
|
16
|
+
export function wrapCwd(cwd: string, command: string): string {
|
|
17
|
+
return `cd -- ${quoteShellArg(cwd)} && ${command}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** How long a scrubbed environment stays fresh per alias. */
|
|
21
|
+
const ENVIRONMENT_TTL_MS = 30_000
|
|
22
|
+
|
|
23
|
+
/** One cache entry: a settled value (until expiry) or an in-flight promise. */
|
|
24
|
+
interface EnvironmentCacheEntry {
|
|
25
|
+
expiresAt: number
|
|
26
|
+
value?: ReadonlyMap<string, string>
|
|
27
|
+
pending?: Promise<ReadonlyMap<string, string>>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Per-engine alias caches (WeakMap so engines are collected with the pool). */
|
|
31
|
+
const caches = new WeakMap<SshEngine, Map<string, EnvironmentCacheEntry>>()
|
|
32
|
+
|
|
33
|
+
function cacheFor(engine: SshEngine): Map<string, EnvironmentCacheEntry> {
|
|
34
|
+
let byAlias = caches.get(engine)
|
|
35
|
+
if (byAlias === undefined) {
|
|
36
|
+
byAlias = new Map()
|
|
37
|
+
caches.set(engine, byAlias)
|
|
38
|
+
}
|
|
39
|
+
return byAlias
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read + scrub the remote login environment, cached per (engine, alias).
|
|
44
|
+
* Concurrent requests share one fetch; failures are never cached.
|
|
45
|
+
*/
|
|
46
|
+
export function readScrubbedRemoteEnvironment(
|
|
47
|
+
engine: SshEngine,
|
|
48
|
+
alias: string,
|
|
49
|
+
): Promise<ReadonlyMap<string, string>> {
|
|
50
|
+
const byAlias = cacheFor(engine)
|
|
51
|
+
const now = Date.now()
|
|
52
|
+
const existing = byAlias.get(alias)
|
|
53
|
+
if (existing !== undefined && existing.expiresAt > now && existing.value !== undefined) {
|
|
54
|
+
return Promise.resolve(existing.value)
|
|
55
|
+
}
|
|
56
|
+
if (existing?.pending !== undefined) return existing.pending
|
|
57
|
+
const pending = fetchScrubbedEnvironment(engine, alias).then(
|
|
58
|
+
(value) => {
|
|
59
|
+
byAlias.set(alias, { expiresAt: Date.now() + ENVIRONMENT_TTL_MS, value })
|
|
60
|
+
return value
|
|
61
|
+
},
|
|
62
|
+
(error: unknown) => {
|
|
63
|
+
// A failed read must not poison the cache: drop it so the next call retries.
|
|
64
|
+
byAlias.delete(alias)
|
|
65
|
+
throw error
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
byAlias.set(alias, { expiresAt: 0, pending })
|
|
69
|
+
return pending
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Drop cached environments for one alias (call after a host config change). */
|
|
73
|
+
export function invalidateRemoteEnvironment(engine: SshEngine, alias: string): void {
|
|
74
|
+
cacheFor(engine).delete(alias)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** One `env -0` round-trip, scrubbed (NUL-delimited so values keep newlines/=). */
|
|
78
|
+
async function fetchScrubbedEnvironment(engine: SshEngine, alias: string): Promise<ReadonlyMap<string, string>> {
|
|
79
|
+
const result = await engine.exec(alias, 'env -0', 10_000)
|
|
80
|
+
if (!result.success) {
|
|
81
|
+
throw new Error(`subprocess-ssh: cannot read the remote environment: ${result.stderr.trim() || 'unknown error'}`)
|
|
82
|
+
}
|
|
83
|
+
const environment = new Map<string, string>()
|
|
84
|
+
for (const record of result.stdout.split('\0')) {
|
|
85
|
+
if (record === '') continue
|
|
86
|
+
const separator = record.indexOf('=')
|
|
87
|
+
if (separator <= 0) continue
|
|
88
|
+
const name = record.slice(0, separator)
|
|
89
|
+
if (name.includes('\0')) continue
|
|
90
|
+
environment.set(name, record.slice(separator + 1))
|
|
91
|
+
}
|
|
92
|
+
return scrubRemoteEnvironment(environment)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Remove harness-private and credential-shaped names from a remote environment.
|
|
97
|
+
*/
|
|
98
|
+
export function scrubRemoteEnvironment(environment: ReadonlyMap<string, string>): Map<string, string> {
|
|
99
|
+
const scrubbed = new Map<string, string>()
|
|
100
|
+
for (const [name, value] of environment) {
|
|
101
|
+
if (name.startsWith('DSH_') || SENSITIVE_ENV_PATTERN.test(name)) continue
|
|
102
|
+
scrubbed.set(name, value)
|
|
103
|
+
}
|
|
104
|
+
return scrubbed
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Overlay explicit entries and serialize one validated environment for `env -i`.
|
|
109
|
+
*/
|
|
110
|
+
export function serializeEnvironment(
|
|
111
|
+
scrubbed: ReadonlyMap<string, string>,
|
|
112
|
+
explicit: Readonly<NodeJS.ProcessEnv> | undefined,
|
|
113
|
+
): string {
|
|
114
|
+
const environment = new Map(scrubbed)
|
|
115
|
+
for (const [name, value] of Object.entries(explicit ?? {})) {
|
|
116
|
+
if (name.length === 0 || name.includes('=') || name.includes('\0') || value?.includes('\0') === true) {
|
|
117
|
+
throw new Error('subprocess-ssh: environment entries require non-empty NUL-free names without = and NUL-free values')
|
|
118
|
+
}
|
|
119
|
+
if (value === undefined) environment.delete(name)
|
|
120
|
+
else environment.set(name, value)
|
|
121
|
+
}
|
|
122
|
+
return [...environment].map(([name, value]) => quoteShellArg(`${name}=${value}`)).join(' ')
|
|
123
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded host-side projection of one remote output stream with a local
|
|
3
|
+
* spill file. Ported from UynajGI/dsh-ssh (MIT) — verbatim semantics.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { randomBytes } from 'node:crypto'
|
|
7
|
+
import { closeSync, mkdtempSync, openSync, unlinkSync, writeSync } from 'node:fs'
|
|
8
|
+
import { tmpdir } from 'node:os'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
import type { CollectedOutput, SubprocessOutputRead, SubprocessOutputReader } from '@deepseek-ai/dsh-subprocess'
|
|
11
|
+
|
|
12
|
+
let spillCounter = 0
|
|
13
|
+
let defaultSpillDir: string | undefined
|
|
14
|
+
|
|
15
|
+
/** Private (0700) per-process spill directory under the OS tmpdir, created lazily. */
|
|
16
|
+
function privateSpillDir(): string {
|
|
17
|
+
defaultSpillDir ??= mkdtempSync(join(tmpdir(), 'dsh-subprocess-ssh-'))
|
|
18
|
+
return defaultSpillDir
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Collects one remote stream with a bounded in-memory tail. On first overflow
|
|
23
|
+
* a spill file is opened (when a spill cap is configured) and every chunk —
|
|
24
|
+
* already-collected ones included — is appended there while the full stream
|
|
25
|
+
* stays within the cap; without one, only the in-memory tail is retained.
|
|
26
|
+
*/
|
|
27
|
+
export class SshOutputCollector implements SubprocessOutputReader {
|
|
28
|
+
private chunks: Buffer[] = []
|
|
29
|
+
private retained = 0
|
|
30
|
+
private total = 0
|
|
31
|
+
private dropped = false
|
|
32
|
+
private spillFd: number | undefined
|
|
33
|
+
private spillFile: string | undefined
|
|
34
|
+
private spillDisabled: boolean
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
private readonly maxBytes: number,
|
|
38
|
+
private readonly maxSpillBytes: number | undefined,
|
|
39
|
+
private readonly label: string,
|
|
40
|
+
private readonly spillDir: string = privateSpillDir(),
|
|
41
|
+
) {
|
|
42
|
+
this.spillDisabled = maxSpillBytes === undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Append one byte-faithful remote chunk, trimming the retained tail to the cap. */
|
|
46
|
+
push(chunk: Uint8Array): void {
|
|
47
|
+
if (chunk.length === 0) return
|
|
48
|
+
const buffer = Buffer.from(chunk)
|
|
49
|
+
this.total += buffer.length
|
|
50
|
+
const overflows = this.retained + buffer.length > this.maxBytes
|
|
51
|
+
if (!this.spillDisabled && (overflows || this.spillFd !== undefined)) this.spillAll(buffer)
|
|
52
|
+
this.chunks.push(buffer)
|
|
53
|
+
this.retained += buffer.length
|
|
54
|
+
while (this.retained > this.maxBytes) {
|
|
55
|
+
const head = this.chunks[0] as Buffer
|
|
56
|
+
const excess = this.retained - this.maxBytes
|
|
57
|
+
if (head.length <= excess) {
|
|
58
|
+
this.chunks.shift()
|
|
59
|
+
this.retained -= head.length
|
|
60
|
+
} else {
|
|
61
|
+
this.chunks[0] = head.subarray(excess)
|
|
62
|
+
this.retained -= excess
|
|
63
|
+
}
|
|
64
|
+
this.dropped = true
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @inheritdoc */
|
|
69
|
+
readFrom(fromByte: number): SubprocessOutputRead {
|
|
70
|
+
const retained = Buffer.concat(this.chunks, this.retained)
|
|
71
|
+
const windowStart = this.total - this.retained
|
|
72
|
+
const lossy = fromByte < windowStart
|
|
73
|
+
const start = lossy ? 0 : Math.min(retained.length, Math.max(0, fromByte - windowStart))
|
|
74
|
+
return {
|
|
75
|
+
text: retained.subarray(start).toString('utf8'),
|
|
76
|
+
nextOffset: this.total,
|
|
77
|
+
lossy,
|
|
78
|
+
...(this.spillFile !== undefined ? { spillPath: this.spillFile } : {}),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Open the spill file lazily and append one chunk (plus any prior chunks once). */
|
|
83
|
+
private spillAll(chunk: Buffer): void {
|
|
84
|
+
if (this.maxSpillBytes !== undefined && this.total > this.maxSpillBytes) {
|
|
85
|
+
this.discardSpill()
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
if (this.spillFd === undefined) {
|
|
89
|
+
this.spillFile = join(
|
|
90
|
+
this.spillDir,
|
|
91
|
+
`dsh-subprocess-ssh-${process.pid}-${++spillCounter}-${randomBytes(6).toString('hex')}-${this.label}.log`,
|
|
92
|
+
)
|
|
93
|
+
this.spillFd = openSync(this.spillFile, 'wx', 0o600)
|
|
94
|
+
for (const prior of this.chunks) writeSync(this.spillFd, prior)
|
|
95
|
+
}
|
|
96
|
+
writeSync(this.spillFd, chunk)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Stop spilling and remove the file once it can no longer hold the complete stream. */
|
|
100
|
+
private discardSpill(): void {
|
|
101
|
+
const fd = this.spillFd
|
|
102
|
+
const file = this.spillFile
|
|
103
|
+
this.spillFd = undefined
|
|
104
|
+
this.spillFile = undefined
|
|
105
|
+
this.spillDisabled = true
|
|
106
|
+
if (fd !== undefined) {
|
|
107
|
+
try {
|
|
108
|
+
closeSync(fd)
|
|
109
|
+
} catch {
|
|
110
|
+
this.spillFd = fd
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (file !== undefined) {
|
|
114
|
+
try {
|
|
115
|
+
unlinkSync(file)
|
|
116
|
+
} catch {
|
|
117
|
+
// A failed unlink leaves at most maxSpillBytes behind, never an unbounded file.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Close the spill file once the stream has ended; stop advertising it on a failed close. */
|
|
123
|
+
seal(): void {
|
|
124
|
+
if (this.spillFd === undefined) return
|
|
125
|
+
try {
|
|
126
|
+
closeSync(this.spillFd)
|
|
127
|
+
} catch {
|
|
128
|
+
this.spillFile = undefined
|
|
129
|
+
}
|
|
130
|
+
this.spillFd = undefined
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Seal the spill and return the final collected output. */
|
|
134
|
+
finalize(): CollectedOutput {
|
|
135
|
+
this.seal()
|
|
136
|
+
return {
|
|
137
|
+
text: Buffer.concat(this.chunks).toString('utf8'),
|
|
138
|
+
truncated: this.dropped,
|
|
139
|
+
...(this.spillFile !== undefined ? { spillPath: this.spillFile } : {}),
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|