Skip to content

Commit 97a547e

Browse files
committed
unify run and shell
1 parent 9071088 commit 97a547e

3 files changed

Lines changed: 16 additions & 90 deletions

File tree

src/commands/run.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {Args, Command, Flags} from '@oclif/core'
22

33
import {getDisco} from '../config.js'
44
import {request, readEventSource} from '../auth-request.js'
5-
import {checkShellSupport, runCommandViaShell} from '../shell-client.js'
5+
import {checkShellSupport, runShell} from '../shell-client.js'
66

77
interface RunResponse {
88
run: {
@@ -37,12 +37,13 @@ export default class Run extends Command {
3737
if (shellSupported && args.command) {
3838
// Use websocket shell for running commands
3939
try {
40-
const result = await runCommandViaShell({
40+
const result = await runShell({
4141
project: flags.project,
4242
discoConfig,
4343
service: flags.service,
4444
command: args.command,
4545
})
46+
4647
if (result.exitCode !== 0) {
4748
this.exit(result.exitCode)
4849
}

src/commands/shell.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Command, Flags } from '@oclif/core'
22

33
import { getDisco } from '../config.js'
4-
import { checkShellSupport, runInteractiveShell } from '../shell-client.js'
4+
import { checkShellSupport, runShell } from '../shell-client.js'
55

66
export default class Shell extends Command {
77

@@ -33,11 +33,10 @@ export default class Shell extends Command {
3333
}
3434

3535
try {
36-
await runInteractiveShell({
36+
await runShell({
3737
project: flags.project,
3838
discoConfig,
3939
service: flags.service,
40-
interactive: true,
4140
})
4241
} catch (error) {
4342
this.error((error as Error).message)

src/shell-client.ts

Lines changed: 11 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,10 @@ export interface ShellOptions {
99
discoConfig: DiscoConfig
1010
service?: string
1111
command?: string
12-
interactive?: boolean
1312
}
1413

1514
export interface ShellResult {
1615
exitCode: number
17-
output: string
1816
}
1917

2018
export async function checkShellSupport(discoConfig: DiscoConfig): Promise<{ supported: boolean; version: string }> {
@@ -27,85 +25,6 @@ export async function checkShellSupport(discoConfig: DiscoConfig): Promise<{ sup
2725
}
2826
}
2927

30-
export function runCommandViaShell(options: ShellOptions): Promise<ShellResult> {
31-
const { project, discoConfig, service, command } = options
32-
33-
return new Promise((resolve, reject) => {
34-
const wsUrl = `wss://${discoConfig.host}/api/projects/${project}/shell`
35-
const ws = new WS(wsUrl)
36-
let output = ''
37-
let exitCode = 0
38-
39-
// Handler for forwarding stdin - defined here so we can remove it on close
40-
const stdinHandler = (chunk: Buffer) => {
41-
if (ws.readyState === WS.OPEN) {
42-
ws.send(chunk)
43-
}
44-
}
45-
46-
const cleanup = () => {
47-
process.stdin.removeListener('data', stdinHandler)
48-
process.stdin.pause()
49-
}
50-
51-
ws.on('open', () => {
52-
const authMessage: { token: string; service?: string; command?: string } = { token: discoConfig.apiKey }
53-
54-
if (service) {
55-
authMessage.service = service
56-
}
57-
58-
if (command) {
59-
authMessage.command = command
60-
}
61-
62-
ws.send(JSON.stringify(authMessage))
63-
})
64-
65-
ws.on('message', (data: WS.RawData, isBinary: boolean) => {
66-
if (isBinary) {
67-
const chunk = (data as Buffer).toString()
68-
output += chunk
69-
process.stdout.write(chunk)
70-
} else {
71-
try {
72-
const message = JSON.parse(data.toString())
73-
if (message.type === 'connected') {
74-
// Forward stdin in case the command needs input (e.g. accidentally ran python REPL)
75-
// This allows user to type 'exit' or Ctrl+C to escape
76-
process.stdin.resume()
77-
process.stdin.on('data', stdinHandler)
78-
} else if (message.type === 'ping' && ws.readyState === WS.OPEN) {
79-
ws.send(JSON.stringify({ type: 'pong' }))
80-
} else if (message.type === 'exit') {
81-
exitCode = message.code ?? 0
82-
}
83-
} catch {
84-
// Not JSON, treat as text output
85-
const text = data.toString()
86-
output += text
87-
process.stdout.write(text)
88-
}
89-
}
90-
})
91-
92-
ws.on('close', (code, reason) => {
93-
cleanup()
94-
95-
if (code === 1000) {
96-
resolve({ exitCode, output })
97-
} else {
98-
reject(new Error(`Connection closed: ${code} ${reason.toString()}`))
99-
}
100-
})
101-
102-
ws.on('error', (err) => {
103-
cleanup()
104-
reject(new Error(`WebSocket error: ${err.message}`))
105-
})
106-
})
107-
}
108-
10928
function restoreTerminal(): void {
11029
if (process.stdin.isTTY) {
11130
process.stdin.setRawMode(false)
@@ -114,20 +33,25 @@ function restoreTerminal(): void {
11433
process.stdin.pause()
11534
}
11635

117-
export function runInteractiveShell(options: ShellOptions): Promise<void> {
118-
const { project, discoConfig, service } = options
36+
export function runShell(options: ShellOptions): Promise<ShellResult> {
37+
const { project, discoConfig, service, command } = options
11938

12039
return new Promise((resolve, reject) => {
12140
const wsUrl = `wss://${discoConfig.host}/api/projects/${project}/shell`
12241
const ws = new WS(wsUrl)
42+
let exitCode = 0
12343

12444
ws.on('open', () => {
125-
const authMessage: { token: string; service?: string } = { token: discoConfig.apiKey }
45+
const authMessage: { token: string; service?: string; command?: string } = { token: discoConfig.apiKey }
12646

12747
if (service) {
12848
authMessage.service = service
12949
}
13050

51+
if (command) {
52+
authMessage.command = command
53+
}
54+
13155
ws.send(JSON.stringify(authMessage))
13256
})
13357

@@ -169,6 +93,8 @@ export function runInteractiveShell(options: ShellOptions): Promise<void> {
16993
})
17094
} else if (message.type === 'ping' && ws.readyState === WS.OPEN) {
17195
ws.send(JSON.stringify({ type: 'pong' }))
96+
} else if (message.type === 'exit') {
97+
exitCode = message.code ?? 0
17298
}
17399
} catch {
174100
process.stdout.write(data.toString())
@@ -180,7 +106,7 @@ export function runInteractiveShell(options: ShellOptions): Promise<void> {
180106
restoreTerminal()
181107

182108
if (code === 1000) {
183-
resolve()
109+
resolve({ exitCode })
184110
} else {
185111
reject(new Error(`Connection closed: ${code} ${reason.toString()}`))
186112
}

0 commit comments

Comments
 (0)