dsh-ssh-workbench 0.0.0-stage → 1.6.4
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/CHANGELOG.md +504 -0
- package/CONTRIBUTING.md +62 -0
- package/LICENSE +21 -0
- package/README.md +159 -2
- package/SECURITY.md +25 -0
- package/cordis.patch.yml +5 -0
- package/docs/architecture.md +124 -0
- package/docs/quickstart.md +94 -0
- package/lib/client.js +4977 -0
- package/lib/core/activity.js +83 -0
- package/lib/core/background.js +69 -0
- package/lib/core/broker.js +799 -0
- package/lib/core/dialect.js +28 -0
- package/lib/core/errors.js +138 -0
- package/lib/core/gate-pwsh.js +88 -0
- package/lib/core/gate.js +572 -0
- package/lib/core/guard.js +71 -0
- package/lib/core/known-hosts.js +74 -0
- package/lib/core/local-file-api.js +174 -0
- package/lib/core/localfs.js +381 -0
- package/lib/core/paths.js +104 -0
- package/lib/core/posix-quote.js +4 -0
- package/lib/core/prefs.js +38 -0
- package/lib/core/remote-bash-tool.js +321 -0
- package/lib/core/remote-bash.js +337 -0
- package/lib/core/secrets.js +96 -0
- package/lib/core/server-reference.js +299 -0
- package/lib/core/shell-adapters.js +48 -0
- package/lib/core/ssh.js +761 -0
- package/lib/core/store.js +83 -0
- package/lib/core/targets.js +250 -0
- package/lib/core/terminals.js +299 -0
- package/lib/core/win32-adapter.js +141 -0
- package/lib/index.js +450 -0
- package/lib/routes.js +733 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +88 -4
- package/vendor/NOTICE.md +50 -0
- package/vendor/XTERM-LICENSE +21 -0
- package/vendor/node_modules/asn1/LICENSE +19 -0
- package/vendor/node_modules/asn1/README.md +50 -0
- package/vendor/node_modules/asn1/lib/ber/errors.js +13 -0
- package/vendor/node_modules/asn1/lib/ber/index.js +27 -0
- package/vendor/node_modules/asn1/lib/ber/reader.js +262 -0
- package/vendor/node_modules/asn1/lib/ber/types.js +36 -0
- package/vendor/node_modules/asn1/lib/ber/writer.js +317 -0
- package/vendor/node_modules/asn1/lib/index.js +20 -0
- package/vendor/node_modules/asn1/package.json +31 -0
- package/vendor/node_modules/bcrypt-pbkdf/CONTRIBUTING.md +13 -0
- package/vendor/node_modules/bcrypt-pbkdf/LICENSE +66 -0
- package/vendor/node_modules/bcrypt-pbkdf/README.md +45 -0
- package/vendor/node_modules/bcrypt-pbkdf/index.js +556 -0
- package/vendor/node_modules/bcrypt-pbkdf/package.json +15 -0
- package/vendor/node_modules/safer-buffer/LICENSE +21 -0
- package/vendor/node_modules/safer-buffer/Readme.md +156 -0
- package/vendor/node_modules/safer-buffer/dangerous.js +58 -0
- package/vendor/node_modules/safer-buffer/package.json +34 -0
- package/vendor/node_modules/safer-buffer/safer.js +77 -0
- package/vendor/node_modules/tweetnacl/AUTHORS.md +28 -0
- package/vendor/node_modules/tweetnacl/LICENSE +24 -0
- package/vendor/node_modules/tweetnacl/README.md +459 -0
- package/vendor/node_modules/tweetnacl/nacl-fast.js +2388 -0
- package/vendor/node_modules/tweetnacl/nacl.js +1175 -0
- package/vendor/node_modules/tweetnacl/package.json +58 -0
- package/vendor/ssh2/LICENSE +19 -0
- package/vendor/ssh2/lib/Channel.js +295 -0
- package/vendor/ssh2/lib/agent.js +1123 -0
- package/vendor/ssh2/lib/client.js +2176 -0
- package/vendor/ssh2/lib/http-agents.js +84 -0
- package/vendor/ssh2/lib/index.js +44 -0
- package/vendor/ssh2/lib/keygen.js +582 -0
- package/vendor/ssh2/lib/protocol/Protocol.js +2136 -0
- package/vendor/ssh2/lib/protocol/SFTP.js +4052 -0
- package/vendor/ssh2/lib/protocol/constants.js +356 -0
- package/vendor/ssh2/lib/protocol/crypto/poly1305.js +43 -0
- package/vendor/ssh2/lib/protocol/crypto.js +1602 -0
- package/vendor/ssh2/lib/protocol/handlers.js +16 -0
- package/vendor/ssh2/lib/protocol/handlers.misc.js +1285 -0
- package/vendor/ssh2/lib/protocol/kex.js +1908 -0
- package/vendor/ssh2/lib/protocol/keyParser.js +1484 -0
- package/vendor/ssh2/lib/protocol/node-fs-compat.js +115 -0
- package/vendor/ssh2/lib/protocol/utils.js +356 -0
- package/vendor/ssh2/lib/protocol/zlib.js +255 -0
- package/vendor/ssh2/lib/server.js +1380 -0
- package/vendor/ssh2/lib/utils.js +336 -0
- package/vendor/ssh2/package.json +34 -0
- package/vendor/xterm.js +53 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Target context for @server mentions.
|
|
3
|
+
*
|
|
4
|
+
* The composer inserts an `@name` chip as plain text, so the model can read it
|
|
5
|
+
* as a teammate mention. This module turns matches against the managed
|
|
6
|
+
* inventory into (a) the session's active remote target and (b) one compact
|
|
7
|
+
* user-role context message that tells the model where `bash` runs and how to
|
|
8
|
+
* switch targets.
|
|
9
|
+
*
|
|
10
|
+
* The injected message enters the same step (`agent/pre-step` replaces the
|
|
11
|
+
* messages that enter a step), which `agent.inject()` cannot promise because it
|
|
12
|
+
* splices into the next step. The decision is spread so fields such as
|
|
13
|
+
* `startsRequestSeries` survive.
|
|
14
|
+
*
|
|
15
|
+
* Scope: every agent mode; no preset gating.
|
|
16
|
+
*/
|
|
17
|
+
import { randomUUID } from 'node:crypto'
|
|
18
|
+
|
|
19
|
+
import { effectiveDialect } from './dialect.js'
|
|
20
|
+
import { loadServers } from './store.js'
|
|
21
|
+
import {
|
|
22
|
+
LOCAL_TARGET,
|
|
23
|
+
TARGET_CONTEXT_KIND,
|
|
24
|
+
TARGET_TOOL_NAME,
|
|
25
|
+
createTargetStore,
|
|
26
|
+
extractServerMentions,
|
|
27
|
+
extractUserText,
|
|
28
|
+
lastHumanMessage,
|
|
29
|
+
} from './targets.js'
|
|
30
|
+
|
|
31
|
+
export { TARGET_CONTEXT_KIND, TARGET_TOOL_NAME }
|
|
32
|
+
|
|
33
|
+
function describeError(error) {
|
|
34
|
+
return error instanceof Error ? error.message : String(error)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function serverLine(server) {
|
|
38
|
+
const target = `${server.username ?? 'root'}@${server.host}:${server.port ?? 22}`
|
|
39
|
+
const state = server.enabled === false ? ' · disabled' : ''
|
|
40
|
+
const dialect = effectiveDialect(server)
|
|
41
|
+
return `- ${server.name} — ${target} · ${dialect}${state}${factsSummary(server)}`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Compact facts summary (hostname only: stable, not a stale capacity snapshot). */
|
|
45
|
+
function factsSummary(server) {
|
|
46
|
+
const facts = server?.facts
|
|
47
|
+
if (!facts || typeof facts !== 'object') return ''
|
|
48
|
+
const hostname = typeof facts.hostname === 'string' ? facts.hostname.trim() : ''
|
|
49
|
+
return hostname ? ` · ${hostname}` : ''
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the copy dialect for one Host.
|
|
54
|
+
*
|
|
55
|
+
* `remoteToolName` is the tool that actually executes on the target: the native
|
|
56
|
+
* `bash` tool on POSIX Hosts, the plugin-owned `remote_bash` tool on Windows
|
|
57
|
+
* Hosts (where DSH ships no bash). The context must describe the tool the model
|
|
58
|
+
* really has, never a tool name that cannot exist on this Host.
|
|
59
|
+
*/
|
|
60
|
+
function contextOptions(options = {}) {
|
|
61
|
+
const hostOs = options.hostPlatform === 'win32' ? 'win32' : 'posix'
|
|
62
|
+
const remoteToolName =
|
|
63
|
+
typeof options.remoteToolName === 'string' && options.remoteToolName.length > 0
|
|
64
|
+
? options.remoteToolName
|
|
65
|
+
: hostOs === 'win32'
|
|
66
|
+
? 'remote_bash'
|
|
67
|
+
: 'bash'
|
|
68
|
+
const windowsToolName =
|
|
69
|
+
typeof options.windowsToolName === 'string' && options.windowsToolName.length > 0 ? options.windowsToolName : 'remote_pwsh'
|
|
70
|
+
return { hostOs, remoteToolName, windowsToolName }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Render the model-facing target context.
|
|
75
|
+
* @param {{ active: string|null, candidates: string[] }} state
|
|
76
|
+
* @param {object[]} servers managed inventory
|
|
77
|
+
* @param {{ hostPlatform?: 'win32'|'posix', remoteToolName?: string, windowsToolName?: string }} [options]
|
|
78
|
+
*/
|
|
79
|
+
export function renderTargetContext(state, servers, options = {}) {
|
|
80
|
+
const { hostOs, remoteToolName, windowsToolName } = contextOptions(options)
|
|
81
|
+
const byName = new Map()
|
|
82
|
+
for (const server of servers ?? []) byName.set(server.name, server)
|
|
83
|
+
const lines = ['[ssh-workbench] Managed server context:']
|
|
84
|
+
if (typeof state?.active === 'string' && state.active.length > 0) {
|
|
85
|
+
const server = byName.get(state.active)
|
|
86
|
+
lines.push(server ? serverLine(server) : `- ${state.active}`)
|
|
87
|
+
const activeDialect = effectiveDialect(server)
|
|
88
|
+
if (activeDialect === 'windows') {
|
|
89
|
+
lines.push(
|
|
90
|
+
`The ${windowsToolName} tool runs PowerShell commands on ${state.active} over SSH; pass ${windowsToolName}.workdir to change its working directory.`,
|
|
91
|
+
)
|
|
92
|
+
lines.push(`The native ${hostOs === 'win32' ? 'pwsh' : 'bash'} tool always runs on the local DSH host: never use it for commands meant for ${state.active}.`)
|
|
93
|
+
lines.push(
|
|
94
|
+
`read/write/edit/glob/grep always run on the local DSH host: never use write/edit for remote files — change them through ${windowsToolName} or the GUI SFTP panel.`,
|
|
95
|
+
)
|
|
96
|
+
lines.push(`Do not use ssh/scp inside ${windowsToolName} to reach other servers; switch targets with ${TARGET_TOOL_NAME}.`)
|
|
97
|
+
lines.push('After a state change, verify the result before reporting; exit code 0 does not prove success.')
|
|
98
|
+
lines.push(`Switch target: ${TARGET_TOOL_NAME}({ server: "<name>" }); detach: server: "${LOCAL_TARGET}".`)
|
|
99
|
+
return lines.join('\n')
|
|
100
|
+
}
|
|
101
|
+
if (hostOs === 'win32') {
|
|
102
|
+
lines.push(
|
|
103
|
+
`The ${remoteToolName} tool runs POSIX shell commands on ${state.active} over SSH; pass ${remoteToolName}.workdir to change its working directory.`,
|
|
104
|
+
)
|
|
105
|
+
lines.push(`The native pwsh tool always runs on the local DSH host: never use it for commands meant for ${state.active}.`)
|
|
106
|
+
lines.push(
|
|
107
|
+
`read/write/edit/glob/grep always run on the local DSH host: never use write/edit for remote files — change them through ${remoteToolName} (heredoc, sed -i) or the GUI SFTP panel.`,
|
|
108
|
+
)
|
|
109
|
+
lines.push(`Do not use ssh/scp inside ${remoteToolName} to reach other servers; switch targets with ${TARGET_TOOL_NAME}.`)
|
|
110
|
+
} else {
|
|
111
|
+
lines.push(`The ${remoteToolName} tool runs on ${state.active}; pass ${remoteToolName}.workdir to change its working directory.`)
|
|
112
|
+
lines.push(
|
|
113
|
+
`read/write/edit/glob/grep always run on the local DSH host: never use write/edit for remote files — change them through ${remoteToolName} (heredoc, sed -i) or the GUI SFTP panel.`,
|
|
114
|
+
)
|
|
115
|
+
lines.push(`Do not use ssh/scp inside ${remoteToolName} to reach other servers; switch targets with ${TARGET_TOOL_NAME}.`)
|
|
116
|
+
}
|
|
117
|
+
lines.push('After a state change, verify the result before reporting; exit code 0 does not prove success.')
|
|
118
|
+
lines.push(`Switch target: ${TARGET_TOOL_NAME}({ server: "<name>" }); detach: server: "${LOCAL_TARGET}".`)
|
|
119
|
+
return lines.join('\n')
|
|
120
|
+
}
|
|
121
|
+
const candidates = Array.isArray(state?.candidates) ? state.candidates : []
|
|
122
|
+
const listed = candidates.map((name) => byName.get(name)).filter(Boolean)
|
|
123
|
+
for (const server of listed) lines.push(serverLine(server))
|
|
124
|
+
if (listed.length === 0) lines.push(`- (unresolved: ${candidates.join(', ')})`)
|
|
125
|
+
if (hostOs === 'win32') {
|
|
126
|
+
lines.push(
|
|
127
|
+
`The native pwsh tool runs on the local DSH host; ${remoteToolName} only runs after you choose a target with ${TARGET_TOOL_NAME}({ server: "<name>" }).`,
|
|
128
|
+
)
|
|
129
|
+
lines.push(
|
|
130
|
+
`read/write/edit/glob/grep always run on the local DSH host: remote files are only reachable through ${remoteToolName}.`,
|
|
131
|
+
)
|
|
132
|
+
} else {
|
|
133
|
+
lines.push(
|
|
134
|
+
`${remoteToolName} still runs on the local DSH host until you choose a target with ${TARGET_TOOL_NAME}({ server: "<name>" }).`,
|
|
135
|
+
)
|
|
136
|
+
lines.push(`read/write/edit/glob/grep always run on the local DSH host: remote files are only reachable through ${remoteToolName}.`)
|
|
137
|
+
}
|
|
138
|
+
return lines.join('\n')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Build one user-role context message, preferring the host's message factory.
|
|
143
|
+
* `@` symbols are avoided so this message can never be re-matched as a mention.
|
|
144
|
+
*/
|
|
145
|
+
export function buildTargetContextMessage(state, servers, createUserMessage, options = {}) {
|
|
146
|
+
const text = renderTargetContext(state, servers, options)
|
|
147
|
+
const source = {
|
|
148
|
+
kind: TARGET_CONTEXT_KIND,
|
|
149
|
+
form: 'notice',
|
|
150
|
+
summary: state?.active ? `remote target: ${state.active}` : 'remote target not selected',
|
|
151
|
+
}
|
|
152
|
+
const input = { content: [{ type: 'text', text }], source }
|
|
153
|
+
if (typeof createUserMessage === 'function') return createUserMessage(input)
|
|
154
|
+
return { role: 'user', id: `ssh-workbench-target-${randomUUID()}`, ...structuredClone(input) }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Install the target binding + context provider.
|
|
159
|
+
*
|
|
160
|
+
* @param {object} ctx plugin context
|
|
161
|
+
* @param {{
|
|
162
|
+
* enabled?: boolean,
|
|
163
|
+
* remoteAvailable?: boolean,
|
|
164
|
+
* hostPlatform?: 'win32'|'posix',
|
|
165
|
+
* remoteToolName?: string,
|
|
166
|
+
* windowsToolName?: string,
|
|
167
|
+
* ensureDialect?: (server: object) => Promise<string|undefined>,
|
|
168
|
+
* targets?: object,
|
|
169
|
+
* loadInventory?: () => object[],
|
|
170
|
+
* createUserMessage?: Function,
|
|
171
|
+
* }} [options]
|
|
172
|
+
*/
|
|
173
|
+
export function installTargetContext(ctx, options = {}) {
|
|
174
|
+
const enabled = options.enabled !== false
|
|
175
|
+
const remoteAvailable = options.remoteAvailable !== false
|
|
176
|
+
const contextOptions = {
|
|
177
|
+
hostPlatform: options.hostPlatform,
|
|
178
|
+
remoteToolName: options.remoteToolName,
|
|
179
|
+
windowsToolName: options.windowsToolName,
|
|
180
|
+
}
|
|
181
|
+
const loadInventory = options.loadInventory ?? loadServers
|
|
182
|
+
const targets = options.targets ?? createTargetStore({ loadInventory })
|
|
183
|
+
const warn = (message) => {
|
|
184
|
+
try {
|
|
185
|
+
ctx?.logger?.warn?.(`dsh-ssh-workbench: ${message}`)
|
|
186
|
+
} catch {
|
|
187
|
+
// Logging must never fail the plugin.
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (!ctx || typeof ctx.on !== 'function') return { targets }
|
|
191
|
+
|
|
192
|
+
let factory = options.createUserMessage
|
|
193
|
+
const messageFactory = async () => {
|
|
194
|
+
if (factory !== undefined) return factory
|
|
195
|
+
try {
|
|
196
|
+
const mod = await import('@deepseek-ai/dsh-llm')
|
|
197
|
+
factory = typeof mod.createUserMessage === 'function' ? mod.createUserMessage : null
|
|
198
|
+
} catch {
|
|
199
|
+
factory = null
|
|
200
|
+
}
|
|
201
|
+
return factory
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const handler = async (payload, next) => {
|
|
205
|
+
const decision = await next()
|
|
206
|
+
try {
|
|
207
|
+
if (!decision || decision.kind === 'reject' || payload?.signal?.aborted) return decision
|
|
208
|
+
if (enabled === false) return decision
|
|
209
|
+
// Never describe a remote world the Host cannot execute in: without a
|
|
210
|
+
// working remote tool the model must keep treating every tool as local.
|
|
211
|
+
if (!remoteAvailable) return decision
|
|
212
|
+
const agent = payload?.agent
|
|
213
|
+
const session = agent?.session
|
|
214
|
+
const key = session?.id ?? agent?.id
|
|
215
|
+
if (typeof key !== 'string' || key.length === 0) return decision
|
|
216
|
+
let servers = loadInventory()
|
|
217
|
+
// Restore a resumed Session's target from its durable human messages once;
|
|
218
|
+
// deriveMessages() is intentionally skipped on every later step.
|
|
219
|
+
if (targets.get(key)?.seeded !== true) {
|
|
220
|
+
try {
|
|
221
|
+
const history = typeof session?.deriveMessages === 'function' ? session.deriveMessages() : []
|
|
222
|
+
targets.ensureSeeded(key, history, servers)
|
|
223
|
+
} catch {
|
|
224
|
+
targets.ensureSeeded(key, [], servers)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const human = lastHumanMessage(decision.messages)
|
|
228
|
+
const mentions = human ? extractServerMentions(extractUserText(human), servers) : []
|
|
229
|
+
const state = targets.bindMentions(key, mentions)
|
|
230
|
+
if (!state) return decision
|
|
231
|
+
// Describe the target in its real dialect. A fresh record may not have a
|
|
232
|
+
// probe result yet; probing here (bounded) avoids telling the model to
|
|
233
|
+
// use a POSIX shell against a Windows host for the whole first turn.
|
|
234
|
+
const activeServer = state.active ? servers.find((entry) => entry.name === state.active) : undefined
|
|
235
|
+
if (activeServer && typeof options.ensureDialect === 'function' && !activeServer.facts?.dialect) {
|
|
236
|
+
try {
|
|
237
|
+
await Promise.race([
|
|
238
|
+
Promise.resolve().then(() => options.ensureDialect(activeServer)),
|
|
239
|
+
new Promise((resolve) => {
|
|
240
|
+
const timer = setTimeout(resolve, 2500)
|
|
241
|
+
timer.unref?.()
|
|
242
|
+
}),
|
|
243
|
+
])
|
|
244
|
+
servers = loadInventory()
|
|
245
|
+
} catch {
|
|
246
|
+
// Keep the record hint when probing fails.
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const needsChoice = state.candidates.length > 1 && !state.active
|
|
250
|
+
if (!state.active && !needsChoice) return decision
|
|
251
|
+
const changed = state.version !== state.injectedVersion
|
|
252
|
+
const firstStep = payload?.step === 1
|
|
253
|
+
if (!changed) {
|
|
254
|
+
if (!firstStep) return decision
|
|
255
|
+
// One context message per turn is enough while it is still present in
|
|
256
|
+
// the step messages (it is committed to the session like other runtime
|
|
257
|
+
// context). Re-inject after compaction or when the target changed.
|
|
258
|
+
const hasContext = decision.messages.some((message) => message?.source?.kind === TARGET_CONTEXT_KIND)
|
|
259
|
+
if (hasContext) return decision
|
|
260
|
+
}
|
|
261
|
+
const message = buildTargetContextMessage(state, servers, await messageFactory(), contextOptions)
|
|
262
|
+
targets.markInjected(key, state.version)
|
|
263
|
+
return { ...decision, messages: [...decision.messages, message] }
|
|
264
|
+
} catch (error) {
|
|
265
|
+
warn(`target context skipped: ${describeError(error)}`)
|
|
266
|
+
return decision
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const offPreStep = ctx.on('agent/pre-step', handler, { prepend: true })
|
|
271
|
+
const offDisposed = ctx.on('agent/disposed', (disposed) => {
|
|
272
|
+
try {
|
|
273
|
+
const session = disposed?.agent?.session
|
|
274
|
+
const key = session?.id ?? disposed?.agent?.id
|
|
275
|
+
if (typeof key === 'string') targets.clear(key)
|
|
276
|
+
} catch {
|
|
277
|
+
// Best effort cleanup.
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
if (typeof ctx.effect === 'function') {
|
|
281
|
+
ctx.effect(
|
|
282
|
+
() => () => {
|
|
283
|
+
try {
|
|
284
|
+
offPreStep?.()
|
|
285
|
+
} catch {
|
|
286
|
+
// Disposal is best effort.
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
offDisposed?.()
|
|
290
|
+
} catch {
|
|
291
|
+
// Disposal is best effort.
|
|
292
|
+
}
|
|
293
|
+
targets.clearAll?.()
|
|
294
|
+
},
|
|
295
|
+
'dsh-ssh-workbench: target context',
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
return { targets, offPreStep, offDisposed }
|
|
299
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell adapters: one managed target dialect → the commands the plugin builds.
|
|
3
|
+
*
|
|
4
|
+
* A Host does not always ship a native shell tool for every target dialect.
|
|
5
|
+
* Windows Desktop builds have `pwsh` but no `bash` (DSH disables `bash` by
|
|
6
|
+
* platform), so remote POSIX execution must come from a plugin-owned tool
|
|
7
|
+
* rather than from the native `bash` interception. Adapters keep everything
|
|
8
|
+
* dialect-specific — quoting, working-directory prefix, timeout wrapper, exit
|
|
9
|
+
* code propagation, user-facing labels — in one object so adding a dialect
|
|
10
|
+
* never grows another branch inside a tool body.
|
|
11
|
+
*/
|
|
12
|
+
import { buildPosixBackgroundCommand } from './background.js'
|
|
13
|
+
import { normalizeDialect } from './dialect.js'
|
|
14
|
+
import { applyWorkdir, resolveTimeoutMs, wrapRemoteTimeout } from './remote-bash.js'
|
|
15
|
+
import { win32Adapter } from './win32-adapter.js'
|
|
16
|
+
|
|
17
|
+
/** Normalize the Host process platform to the two worlds the plugin cares about. */
|
|
18
|
+
export function hostPlatform(platform = process.platform) {
|
|
19
|
+
return platform === 'win32' ? 'win32' : 'posix'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* POSIX adapter: Linux/Unix targets reached through the remote login shell.
|
|
24
|
+
*
|
|
25
|
+
* `remoteTimeout` means the target can run the coreutils `timeout` wrapper, so
|
|
26
|
+
* a client-side deadline can also stop the remote process group.
|
|
27
|
+
*/
|
|
28
|
+
export const posixAdapter = {
|
|
29
|
+
id: 'posix',
|
|
30
|
+
platform: 'posix',
|
|
31
|
+
toolName: 'remote_bash',
|
|
32
|
+
label: 'Linux/Unix (POSIX)',
|
|
33
|
+
remoteTimeout: true,
|
|
34
|
+
supportsDialect: (dialect) => normalizeDialect(dialect) !== 'windows',
|
|
35
|
+
unsupportedMessage: (server) =>
|
|
36
|
+
`target "${server?.name ?? 'unknown'}" is a Windows server; remote_bash runs POSIX commands only — use remote_pwsh for Windows targets`,
|
|
37
|
+
buildCommand: (command, workdir) => applyWorkdir(command, workdir),
|
|
38
|
+
buildBackgroundCommand(command, workdir) {
|
|
39
|
+
const built = applyWorkdir(command, workdir)
|
|
40
|
+
if (built.error !== undefined) return built
|
|
41
|
+
return { command: buildPosixBackgroundCommand(built.command), cwd: built.cwd }
|
|
42
|
+
},
|
|
43
|
+
wrapTimeout: wrapRemoteTimeout,
|
|
44
|
+
resolveTimeoutMs,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
export { win32Adapter }
|