Skip to content

Commit 548b3c5

Browse files
committed
interactive shell
1 parent b4a88aa commit 548b3c5

File tree

4 files changed

+144
-6
lines changed

4 files changed

+144
-6
lines changed

package-lock.json

Lines changed: 15 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "disco",
33
"description": "deploy and manage your web projects",
4-
"version": "0.5.50",
4+
"version": "0.5.51",
55
"author": "The Disco Team",
66
"bin": {
77
"disco": "./bin/run.js"
@@ -26,7 +26,8 @@
2626
"node-ssh": "^13.2.0",
2727
"open": "^10.1.0",
2828
"tunnel-ssh": "^5.1.2",
29-
"undici": "^7.7.0"
29+
"undici": "^7.7.0",
30+
"ws": "^8.18.3"
3031
},
3132
"devDependencies": {
3233
"@oclif/prettier-config": "^0.2.1",
@@ -35,6 +36,7 @@
3536
"@types/mocha": "^10",
3637
"@types/node": "^18",
3738
"@types/ssh2": "^1.15.0",
39+
"@types/ws": "^8.18.1",
3840
"chai": "^4",
3941
"eslint": "^8",
4042
"eslint-config-oclif": "^5",

src/commands/shell.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { Command, Flags } from '@oclif/core'
2+
import { compare } from 'compare-versions'
3+
import WebSocket from 'ws'
4+
5+
import { request } from '../auth-request.js'
6+
import { getDisco } from '../config.js'
7+
8+
export default class Shell extends Command {
9+
10+
static override description = 'open an interactive shell session in a project container'
11+
12+
static override examples = [
13+
'<%= config.bin %> <%= command.id %> --project mysite',
14+
'<%= config.bin %> <%= command.id %> --project mysite --service worker',
15+
]
16+
17+
static hidden = true
18+
19+
static override flags = {
20+
project: Flags.string({ required: true, description: 'project name' }),
21+
service: Flags.string({ required: false, description: 'service name (defaults to web or first non-static service)' }),
22+
disco: Flags.string({ required: false, description: 'disco to use' }),
23+
}
24+
25+
public async run(): Promise<void> {
26+
const { flags } = await this.parse(Shell)
27+
28+
const discoConfig = getDisco(flags.disco || null)
29+
30+
// Check daemon version supports shell
31+
const metaUrl = `https://${discoConfig.host}/api/disco/meta`
32+
const res = await request({ method: 'GET', url: metaUrl, discoConfig })
33+
const meta = (await res.json()) as { version: string }
34+
35+
if (compare(meta.version, '0.28.0', '<')) {
36+
this.error(
37+
'Interactive shell is not available for this version of Disco. Please upgrade using `disco meta:upgrade`',
38+
)
39+
}
40+
41+
// Connect directly to WebSocket endpoint
42+
const wsUrl = `wss://${discoConfig.host}/api/projects/${flags.project}/shell`
43+
44+
const ws = new WebSocket(wsUrl)
45+
46+
const restoreTerminal = () => {
47+
if (process.stdin.isTTY) {
48+
process.stdin.setRawMode(false)
49+
}
50+
process.stdin.pause()
51+
}
52+
53+
ws.on('open', () => {
54+
// Send API key and optional service as first message for authentication
55+
const authMessage: { token: string; service?: string } = { token: discoConfig.apiKey }
56+
if (flags.service) {
57+
authMessage.service = flags.service
58+
}
59+
ws.send(JSON.stringify(authMessage))
60+
})
61+
62+
ws.on('message', (data: WebSocket.RawData, isBinary: boolean) => {
63+
if (isBinary) {
64+
// Binary data = terminal output
65+
process.stdout.write(data as Buffer)
66+
} else {
67+
// Text data = JSON control message
68+
try {
69+
const message = JSON.parse(data.toString())
70+
if (message.type === 'connected') {
71+
// Successfully authenticated, set up terminal
72+
if (process.stdin.isTTY) {
73+
process.stdin.setRawMode(true)
74+
}
75+
process.stdin.resume()
76+
77+
// Send initial terminal size
78+
if (process.stdout.isTTY) {
79+
ws.send(JSON.stringify({
80+
type: 'resize',
81+
cols: process.stdout.columns,
82+
rows: process.stdout.rows,
83+
}))
84+
}
85+
86+
// Handle terminal resize
87+
process.stdout.on('resize', () => {
88+
if (process.stdout.isTTY && ws.readyState === WebSocket.OPEN) {
89+
ws.send(JSON.stringify({
90+
type: 'resize',
91+
cols: process.stdout.columns,
92+
rows: process.stdout.rows,
93+
}))
94+
}
95+
})
96+
97+
// Forward stdin to WebSocket as binary
98+
process.stdin.on('data', (chunk: Buffer) => {
99+
if (ws.readyState === WebSocket.OPEN) {
100+
ws.send(chunk)
101+
}
102+
})
103+
}
104+
} catch {
105+
// Not JSON, treat as text output
106+
process.stdout.write(data.toString())
107+
}
108+
}
109+
})
110+
111+
ws.on('close', (code, reason) => {
112+
restoreTerminal()
113+
if (code !== 1000) {
114+
this.error(`Connection closed: ${code} ${reason.toString()}`)
115+
}
116+
process.exit(0)
117+
})
118+
119+
ws.on('error', (err) => {
120+
restoreTerminal()
121+
this.error(`WebSocket error: ${err.message}`)
122+
})
123+
}
124+
}

tsconfig.tsbuildinfo

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"root":["./src/auth-request.ts","./src/config.ts","./src/index.ts","./src/commands/deploy.ts","./src/commands/init.ts","./src/commands/logs.ts","./src/commands/run.ts","./src/commands/apikeys/list.ts","./src/commands/apikeys/remove.ts","./src/commands/deploy/cancel.ts","./src/commands/deploy/list.ts","./src/commands/deploy/output.ts","./src/commands/discos/list.ts","./src/commands/domains/add.ts","./src/commands/domains/list.ts","./src/commands/domains/remove.ts","./src/commands/env/get.ts","./src/commands/env/list.ts","./src/commands/env/remove.ts","./src/commands/env/set.ts","./src/commands/github/apps/add.ts","./src/commands/github/apps/list.ts","./src/commands/github/apps/manage.ts","./src/commands/github/apps/prune.ts","./src/commands/github/repos/list.ts","./src/commands/invite/accept.ts","./src/commands/invite/create.ts","./src/commands/meta/host.ts","./src/commands/meta/info.ts","./src/commands/meta/stats.ts","./src/commands/meta/upgrade.ts","./src/commands/nodes/add.ts","./src/commands/nodes/list.ts","./src/commands/nodes/remove.ts","./src/commands/postgres/create.ts","./src/commands/postgres/tunnel.ts","./src/commands/postgres/addon/install.ts","./src/commands/postgres/addon/remove.ts","./src/commands/postgres/addon/update.ts","./src/commands/postgres/databases/add.ts","./src/commands/postgres/databases/attach.ts","./src/commands/postgres/databases/detach.ts","./src/commands/postgres/databases/list.ts","./src/commands/postgres/databases/remove.ts","./src/commands/postgres/instances/add.ts","./src/commands/postgres/instances/list.ts","./src/commands/postgres/instances/remove.ts","./src/commands/projects/add.ts","./src/commands/projects/list.ts","./src/commands/projects/move.ts","./src/commands/projects/remove.ts","./src/commands/registry/addon/install.ts","./src/commands/registry/addon/remove.ts","./src/commands/registry/addon/update.ts","./src/commands/scale/get.ts","./src/commands/scale/set.ts","./src/commands/syslog/add.ts","./src/commands/syslog/list.ts","./src/commands/syslog/remove.ts","./src/commands/volumes/export.ts","./src/commands/volumes/import.ts","./src/commands/volumes/list.ts"],"version":"5.8.3"}
1+
{"root":["./src/auth-request.ts","./src/config.ts","./src/index.ts","./src/commands/deploy.ts","./src/commands/init.ts","./src/commands/logs.ts","./src/commands/run.ts","./src/commands/shell.ts","./src/commands/apikeys/list.ts","./src/commands/apikeys/remove.ts","./src/commands/deploy/cancel.ts","./src/commands/deploy/list.ts","./src/commands/deploy/output.ts","./src/commands/discos/list.ts","./src/commands/domains/add.ts","./src/commands/domains/list.ts","./src/commands/domains/remove.ts","./src/commands/env/get.ts","./src/commands/env/list.ts","./src/commands/env/remove.ts","./src/commands/env/set.ts","./src/commands/github/apps/add.ts","./src/commands/github/apps/list.ts","./src/commands/github/apps/manage.ts","./src/commands/github/apps/prune.ts","./src/commands/github/repos/list.ts","./src/commands/invite/accept.ts","./src/commands/invite/create.ts","./src/commands/meta/host.ts","./src/commands/meta/info.ts","./src/commands/meta/stats.ts","./src/commands/meta/upgrade.ts","./src/commands/nodes/add.ts","./src/commands/nodes/list.ts","./src/commands/nodes/remove.ts","./src/commands/postgres/create.ts","./src/commands/postgres/tunnel.ts","./src/commands/postgres/addon/install.ts","./src/commands/postgres/addon/remove.ts","./src/commands/postgres/addon/update.ts","./src/commands/postgres/databases/add.ts","./src/commands/postgres/databases/attach.ts","./src/commands/postgres/databases/detach.ts","./src/commands/postgres/databases/list.ts","./src/commands/postgres/databases/remove.ts","./src/commands/postgres/instances/add.ts","./src/commands/postgres/instances/list.ts","./src/commands/postgres/instances/remove.ts","./src/commands/projects/add.ts","./src/commands/projects/list.ts","./src/commands/projects/move.ts","./src/commands/projects/remove.ts","./src/commands/registry/addon/install.ts","./src/commands/registry/addon/remove.ts","./src/commands/registry/addon/update.ts","./src/commands/scale/get.ts","./src/commands/scale/set.ts","./src/commands/syslog/add.ts","./src/commands/syslog/list.ts","./src/commands/syslog/remove.ts","./src/commands/volumes/export.ts","./src/commands/volumes/import.ts","./src/commands/volumes/list.ts"],"version":"5.8.3"}

0 commit comments

Comments
 (0)