@freestyle-sh/with-ttyd 0.0.5 → 0.0.7
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/dist/index.d.ts +12 -3
- package/dist/index.js +41 -14
- package/examples/basic.ts +19 -30
- package/package.json +3 -2
- package/src/index.ts +59 -23
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import * as freestyle_sandboxes from 'freestyle-sandboxes';
|
|
2
|
-
import { VmWith, VmWithInstance,
|
|
2
|
+
import { VmWith, VmWithInstance, VmSpec } from 'freestyle-sandboxes';
|
|
3
|
+
import { VmPtySessionLike } from '@freestyle-sh/with-pty';
|
|
3
4
|
|
|
4
5
|
type TtydConfig = {
|
|
5
6
|
/** Port to run ttyd on (default: auto-assigned starting at 7682) */
|
|
6
7
|
port?: number;
|
|
7
8
|
/** Shell or command to run (default: /bin/bash) */
|
|
8
9
|
command?: string;
|
|
10
|
+
/** Attach ttyd to a PTY session */
|
|
11
|
+
pty?: VmPtySessionLike;
|
|
9
12
|
/** User to run terminal as (default: current user) */
|
|
10
13
|
user?: string;
|
|
11
14
|
/** Working directory (default: user home) */
|
|
@@ -19,6 +22,8 @@ type TtydConfig = {
|
|
|
19
22
|
title?: string;
|
|
20
23
|
/** Read-only terminal (no input allowed) */
|
|
21
24
|
readOnly?: boolean;
|
|
25
|
+
/** Xterm.js theme settings passed to ttyd client options */
|
|
26
|
+
theme?: Record<string, string | number | boolean>;
|
|
22
27
|
};
|
|
23
28
|
type ResolvedTerminalConfig = {
|
|
24
29
|
port: number;
|
|
@@ -31,11 +36,15 @@ type ResolvedTerminalConfig = {
|
|
|
31
36
|
};
|
|
32
37
|
title: string;
|
|
33
38
|
readOnly: boolean;
|
|
39
|
+
theme?: Record<string, string | number | boolean>;
|
|
34
40
|
};
|
|
35
41
|
declare class VmWebTerminal<T extends TtydConfig[] = TtydConfig[]> extends VmWith<VmWebTerminalInstance<T>> {
|
|
36
42
|
private resolvedTerminals;
|
|
43
|
+
private ptySessions;
|
|
37
44
|
constructor(terminals: T | TtydConfig);
|
|
38
|
-
|
|
45
|
+
private composeTtydSpec;
|
|
46
|
+
configureSnapshotSpec(spec: VmSpec): VmSpec;
|
|
47
|
+
configureSpec(spec: VmSpec): VmSpec;
|
|
39
48
|
createInstance(): VmWebTerminalInstance<T>;
|
|
40
49
|
installServiceName(): string;
|
|
41
50
|
}
|
|
@@ -46,7 +55,7 @@ declare class WebTerminal {
|
|
|
46
55
|
constructor({ port, command, instance, }: {
|
|
47
56
|
port: number;
|
|
48
57
|
command: string;
|
|
49
|
-
instance: VmWebTerminalInstance<
|
|
58
|
+
instance: VmWebTerminalInstance<TtydConfig[]>;
|
|
50
59
|
});
|
|
51
60
|
/** Expose this terminal publicly via Freestyle routing */
|
|
52
61
|
route({ domain }: {
|
package/dist/index.js
CHANGED
|
@@ -1,33 +1,50 @@
|
|
|
1
|
-
import { VmWith,
|
|
1
|
+
import { VmWith, VmSpec, VmWithInstance } from 'freestyle-sandboxes';
|
|
2
2
|
|
|
3
3
|
class VmWebTerminal extends VmWith {
|
|
4
4
|
resolvedTerminals;
|
|
5
|
+
ptySessions;
|
|
5
6
|
constructor(terminals) {
|
|
6
7
|
super();
|
|
7
8
|
const terminalList = Array.isArray(terminals) ? terminals : [terminals];
|
|
9
|
+
this.ptySessions = terminalList.map((config) => config.pty).filter((value) => value !== void 0);
|
|
8
10
|
let nextPort = 7682;
|
|
9
11
|
this.resolvedTerminals = terminalList.map((config) => {
|
|
12
|
+
if (config.pty && config.command) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
"VmWebTerminal config cannot include both pty and command. Use one or the other."
|
|
15
|
+
);
|
|
16
|
+
}
|
|
10
17
|
const port = config.port ?? nextPort++;
|
|
18
|
+
const resolvedCommand = config.pty ? config.pty.attachCommand(config.readOnly ?? false) : config.command ?? "bash -l";
|
|
11
19
|
return {
|
|
12
20
|
port,
|
|
13
|
-
command:
|
|
21
|
+
command: resolvedCommand,
|
|
14
22
|
user: config.user ?? "root",
|
|
15
23
|
cwd: config.cwd ?? "/root",
|
|
16
24
|
credential: config.credential,
|
|
17
25
|
title: config.title ?? `terminal-${port}`,
|
|
18
|
-
readOnly: config.readOnly ?? false
|
|
26
|
+
readOnly: config.readOnly ?? false,
|
|
27
|
+
theme: config.theme
|
|
19
28
|
};
|
|
20
29
|
});
|
|
21
30
|
}
|
|
22
|
-
|
|
31
|
+
composeTtydSpec(spec) {
|
|
32
|
+
const uniquePtySessions = Array.from(new Set(this.ptySessions));
|
|
33
|
+
let composed = spec;
|
|
34
|
+
for (const pty of uniquePtySessions) {
|
|
35
|
+
composed = pty.applyToSpec(composed);
|
|
36
|
+
}
|
|
23
37
|
const installScript = `#!/bin/bash
|
|
24
|
-
set -e
|
|
38
|
+
set -e
|
|
25
39
|
|
|
26
|
-
TTYD_VERSION="1.7.7"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
/
|
|
30
|
-
|
|
40
|
+
TTYD_VERSION="1.7.7"
|
|
41
|
+
mkdir -p /usr/local/bin
|
|
42
|
+
tmpfile="$(mktemp)"
|
|
43
|
+
curl -fsSL -o "$tmpfile" "https://github.com/tsl0922/ttyd/releases/download/\${TTYD_VERSION}/ttyd.x86_64"
|
|
44
|
+
mv "$tmpfile" /usr/local/bin/ttyd
|
|
45
|
+
chmod +x /usr/local/bin/ttyd
|
|
46
|
+
/usr/local/bin/ttyd --version
|
|
47
|
+
`;
|
|
31
48
|
const services = this.resolvedTerminals.map((t) => {
|
|
32
49
|
const args = [`/usr/local/bin/ttyd`, `-p ${t.port}`];
|
|
33
50
|
if (t.credential) {
|
|
@@ -50,6 +67,10 @@ chmod +x /usr/local/bin/ttyd
|
|
|
50
67
|
} else {
|
|
51
68
|
args.push(`--writable`);
|
|
52
69
|
}
|
|
70
|
+
if (t.theme) {
|
|
71
|
+
const themeJson = JSON.stringify(t.theme);
|
|
72
|
+
args.push(`-t 'theme=${themeJson}'`);
|
|
73
|
+
}
|
|
53
74
|
args.push(t.command);
|
|
54
75
|
return {
|
|
55
76
|
name: `web-terminal-${t.port}`,
|
|
@@ -63,8 +84,9 @@ chmod +x /usr/local/bin/ttyd
|
|
|
63
84
|
requires: ["systemd-sysusers.service"]
|
|
64
85
|
};
|
|
65
86
|
});
|
|
66
|
-
|
|
67
|
-
|
|
87
|
+
return this.composeSpecs(
|
|
88
|
+
composed,
|
|
89
|
+
new VmSpec({
|
|
68
90
|
additionalFiles: {
|
|
69
91
|
"/opt/install-ttyd.sh": { content: installScript }
|
|
70
92
|
},
|
|
@@ -81,8 +103,13 @@ chmod +x /usr/local/bin/ttyd
|
|
|
81
103
|
]
|
|
82
104
|
}
|
|
83
105
|
})
|
|
84
|
-
|
|
85
|
-
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
configureSnapshotSpec(spec) {
|
|
109
|
+
return this.composeTtydSpec(spec);
|
|
110
|
+
}
|
|
111
|
+
configureSpec(spec) {
|
|
112
|
+
return this.composeTtydSpec(spec);
|
|
86
113
|
}
|
|
87
114
|
createInstance() {
|
|
88
115
|
return new VmWebTerminalInstance(this, this.resolvedTerminals);
|
package/examples/basic.ts
CHANGED
|
@@ -2,10 +2,15 @@ import "dotenv/config";
|
|
|
2
2
|
import { freestyle, VmSpec } from "freestyle-sandboxes";
|
|
3
3
|
import { VmWebTerminal as VmTtyd } from "../src/index.ts";
|
|
4
4
|
import { VmDevServer } from "../../with-dev-server/src/index.ts";
|
|
5
|
+
import { VmPtySession } from "../../with-pty/src/index.ts";
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
+
const devPty = new VmPtySession({
|
|
8
|
+
sessionId: "npm-dev",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const devCommandTtyd = new VmTtyd({
|
|
7
12
|
port: 3010,
|
|
8
|
-
|
|
13
|
+
pty: devPty,
|
|
9
14
|
readOnly: true,
|
|
10
15
|
cwd: "/root/repo",
|
|
11
16
|
});
|
|
@@ -17,44 +22,26 @@ const otherTerminals = new VmTtyd({
|
|
|
17
22
|
|
|
18
23
|
const domain = `${crypto.randomUUID()}.style.dev`;
|
|
19
24
|
|
|
20
|
-
const innerDeps = new VmSpec({
|
|
21
|
-
discriminator: "inner-deps",
|
|
22
|
-
with: {
|
|
23
|
-
devLogs: devLogs,
|
|
24
|
-
otherTerminals: otherTerminals,
|
|
25
|
-
},
|
|
26
|
-
aptDeps: ["tmux"],
|
|
27
|
-
// systemd: {
|
|
28
|
-
// services: [
|
|
29
|
-
// {
|
|
30
|
-
// name: "install-tmux",
|
|
31
|
-
// mode: "oneshot",
|
|
32
|
-
// bash: "apt-get update && apt-get install -y tmux",
|
|
33
|
-
// timeoutSec: 120,
|
|
34
|
-
// },
|
|
35
|
-
// ],
|
|
36
|
-
// },
|
|
37
|
-
additionalFiles: {
|
|
38
|
-
"/root/.tmux.conf": {
|
|
39
|
-
content: `set -g mouse on`,
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
|
|
44
25
|
const snapshot = new VmSpec({
|
|
45
|
-
snapshot: innerDeps,
|
|
46
26
|
with: {
|
|
27
|
+
devCommandTtyd: devCommandTtyd,
|
|
28
|
+
otherTerminals: otherTerminals,
|
|
47
29
|
devServer: new VmDevServer({
|
|
48
30
|
templateRepo: "https://github.com/freestyle-sh/freestyle-next",
|
|
49
31
|
workdir: "/root/repo",
|
|
50
|
-
devCommand: "
|
|
32
|
+
devCommand: "npm run dev",
|
|
33
|
+
devCommandPty: devPty,
|
|
51
34
|
}),
|
|
52
35
|
},
|
|
53
36
|
});
|
|
54
37
|
|
|
55
|
-
const {
|
|
38
|
+
const { vmId, vm } = await freestyle.vms.create({
|
|
56
39
|
snapshot: snapshot,
|
|
57
40
|
domains: [
|
|
41
|
+
{
|
|
42
|
+
domain: domain,
|
|
43
|
+
vmPort: 3000,
|
|
44
|
+
},
|
|
58
45
|
{
|
|
59
46
|
domain: "dev-logs-" + domain,
|
|
60
47
|
vmPort: 3010,
|
|
@@ -66,7 +53,9 @@ const { vm, vmId } = await freestyle.vms.create({
|
|
|
66
53
|
],
|
|
67
54
|
});
|
|
68
55
|
|
|
69
|
-
console.log
|
|
56
|
+
await vm.devServer.getLogs().then(console.log);
|
|
70
57
|
|
|
58
|
+
console.log("npx freestyle-sandboxes vm ssh " + vmId);
|
|
59
|
+
console.log(`Dev server available at: https://${domain}`);
|
|
71
60
|
console.log(`Terminal available at: https://dev-logs-${domain}`);
|
|
72
61
|
console.log(`Other terminals available at: https://other-terminals-${domain}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@freestyle-sh/with-ttyd",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Web terminal for freestyle sandboxes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"typescript": "^5.8.3"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"freestyle-sandboxes": "^0.1.
|
|
27
|
+
"freestyle-sandboxes": "^0.1.28",
|
|
28
|
+
"@freestyle-sh/with-pty": "^0.0.3"
|
|
28
29
|
},
|
|
29
30
|
"scripts": {
|
|
30
31
|
"build": "pkgroll"
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
VmWith,
|
|
5
|
-
VmWithInstance,
|
|
6
|
-
Freestyle,
|
|
7
|
-
} from "freestyle-sandboxes";
|
|
1
|
+
import { VmSpec, VmWith, VmWithInstance } from "freestyle-sandboxes";
|
|
2
|
+
import type { VmPtySessionLike } from "@freestyle-sh/with-pty";
|
|
3
|
+
import type { Freestyle } from "freestyle-sandboxes";
|
|
8
4
|
|
|
9
5
|
// ============================================================================
|
|
10
6
|
// Configuration Types
|
|
@@ -15,6 +11,8 @@ export type TtydConfig = {
|
|
|
15
11
|
port?: number;
|
|
16
12
|
/** Shell or command to run (default: /bin/bash) */
|
|
17
13
|
command?: string;
|
|
14
|
+
/** Attach ttyd to a PTY session */
|
|
15
|
+
pty?: VmPtySessionLike;
|
|
18
16
|
/** User to run terminal as (default: current user) */
|
|
19
17
|
user?: string;
|
|
20
18
|
/** Working directory (default: user home) */
|
|
@@ -25,6 +23,8 @@ export type TtydConfig = {
|
|
|
25
23
|
title?: string;
|
|
26
24
|
/** Read-only terminal (no input allowed) */
|
|
27
25
|
readOnly?: boolean;
|
|
26
|
+
/** Xterm.js theme settings passed to ttyd client options */
|
|
27
|
+
theme?: Record<string, string | number | boolean>;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
export type ResolvedTerminalConfig = {
|
|
@@ -35,6 +35,7 @@ export type ResolvedTerminalConfig = {
|
|
|
35
35
|
credential?: { username: string; password: string };
|
|
36
36
|
title: string;
|
|
37
37
|
readOnly: boolean;
|
|
38
|
+
theme?: Record<string, string | number | boolean>;
|
|
38
39
|
};
|
|
39
40
|
|
|
40
41
|
// ============================================================================
|
|
@@ -45,38 +46,61 @@ export class VmWebTerminal<
|
|
|
45
46
|
T extends TtydConfig[] = TtydConfig[],
|
|
46
47
|
> extends VmWith<VmWebTerminalInstance<T>> {
|
|
47
48
|
private resolvedTerminals: ResolvedTerminalConfig[];
|
|
49
|
+
private ptySessions: VmPtySessionLike[];
|
|
48
50
|
|
|
49
51
|
constructor(terminals: T | TtydConfig) {
|
|
50
52
|
super();
|
|
51
53
|
const terminalList = Array.isArray(terminals) ? terminals : [terminals];
|
|
54
|
+
this.ptySessions = terminalList
|
|
55
|
+
.map((config) => config.pty)
|
|
56
|
+
.filter((value): value is VmPtySessionLike => value !== undefined);
|
|
52
57
|
// Resolve config once with defaults
|
|
53
58
|
let nextPort = 7682;
|
|
54
59
|
this.resolvedTerminals = terminalList.map((config) => {
|
|
60
|
+
if (config.pty && config.command) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
"VmWebTerminal config cannot include both pty and command. Use one or the other.",
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
55
66
|
const port = config.port ?? nextPort++;
|
|
67
|
+
const resolvedCommand = config.pty
|
|
68
|
+
? config.pty.attachCommand(config.readOnly ?? false)
|
|
69
|
+
: (config.command ?? "bash -l");
|
|
70
|
+
|
|
56
71
|
return {
|
|
57
72
|
port,
|
|
58
|
-
command:
|
|
73
|
+
command: resolvedCommand,
|
|
59
74
|
user: config.user ?? "root",
|
|
60
75
|
cwd: config.cwd ?? "/root",
|
|
61
76
|
credential: config.credential,
|
|
62
77
|
title: config.title ?? `terminal-${port}`,
|
|
63
78
|
readOnly: config.readOnly ?? false,
|
|
79
|
+
theme: config.theme,
|
|
64
80
|
};
|
|
65
81
|
});
|
|
66
82
|
}
|
|
67
83
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
private composeTtydSpec(spec: VmSpec): VmSpec {
|
|
85
|
+
const uniquePtySessions = Array.from(new Set(this.ptySessions));
|
|
86
|
+
let composed = spec;
|
|
87
|
+
|
|
88
|
+
for (const pty of uniquePtySessions) {
|
|
89
|
+
composed = pty.applyToSpec(composed) as VmSpec;
|
|
90
|
+
}
|
|
91
|
+
|
|
71
92
|
// Generate install script
|
|
72
93
|
const installScript = `#!/bin/bash
|
|
73
|
-
set -e
|
|
94
|
+
set -e
|
|
74
95
|
|
|
75
|
-
TTYD_VERSION="1.7.7"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
/
|
|
79
|
-
|
|
96
|
+
TTYD_VERSION="1.7.7"
|
|
97
|
+
mkdir -p /usr/local/bin
|
|
98
|
+
tmpfile="$(mktemp)"
|
|
99
|
+
curl -fsSL -o "$tmpfile" "https://github.com/tsl0922/ttyd/releases/download/\${TTYD_VERSION}/ttyd.x86_64"
|
|
100
|
+
mv "$tmpfile" /usr/local/bin/ttyd
|
|
101
|
+
chmod +x /usr/local/bin/ttyd
|
|
102
|
+
/usr/local/bin/ttyd --version
|
|
103
|
+
`;
|
|
80
104
|
|
|
81
105
|
// Generate systemd service for each terminal
|
|
82
106
|
const services = this.resolvedTerminals.map((t) => {
|
|
@@ -109,6 +133,11 @@ chmod +x /usr/local/bin/ttyd
|
|
|
109
133
|
args.push(`--writable`);
|
|
110
134
|
}
|
|
111
135
|
|
|
136
|
+
if (t.theme) {
|
|
137
|
+
const themeJson = JSON.stringify(t.theme);
|
|
138
|
+
args.push(`-t 'theme=${themeJson}'`);
|
|
139
|
+
}
|
|
140
|
+
|
|
112
141
|
// Shell command at the end
|
|
113
142
|
args.push(t.command);
|
|
114
143
|
|
|
@@ -125,8 +154,9 @@ chmod +x /usr/local/bin/ttyd
|
|
|
125
154
|
};
|
|
126
155
|
});
|
|
127
156
|
|
|
128
|
-
|
|
129
|
-
|
|
157
|
+
return this.composeSpecs(
|
|
158
|
+
composed,
|
|
159
|
+
new VmSpec({
|
|
130
160
|
additionalFiles: {
|
|
131
161
|
"/opt/install-ttyd.sh": { content: installScript },
|
|
132
162
|
},
|
|
@@ -143,9 +173,15 @@ chmod +x /usr/local/bin/ttyd
|
|
|
143
173
|
],
|
|
144
174
|
},
|
|
145
175
|
}),
|
|
146
|
-
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
override configureSnapshotSpec(spec: VmSpec): VmSpec {
|
|
180
|
+
return this.composeTtydSpec(spec);
|
|
181
|
+
}
|
|
147
182
|
|
|
148
|
-
|
|
183
|
+
override configureSpec(spec: VmSpec): VmSpec {
|
|
184
|
+
return this.composeTtydSpec(spec);
|
|
149
185
|
}
|
|
150
186
|
|
|
151
187
|
createInstance(): VmWebTerminalInstance<T> {
|
|
@@ -164,7 +200,7 @@ chmod +x /usr/local/bin/ttyd
|
|
|
164
200
|
export class WebTerminal {
|
|
165
201
|
readonly port: number;
|
|
166
202
|
readonly command: string;
|
|
167
|
-
private instance: VmWebTerminalInstance<
|
|
203
|
+
private instance: VmWebTerminalInstance<TtydConfig[]>;
|
|
168
204
|
|
|
169
205
|
constructor({
|
|
170
206
|
port,
|
|
@@ -173,7 +209,7 @@ export class WebTerminal {
|
|
|
173
209
|
}: {
|
|
174
210
|
port: number;
|
|
175
211
|
command: string;
|
|
176
|
-
instance: VmWebTerminalInstance<
|
|
212
|
+
instance: VmWebTerminalInstance<TtydConfig[]>;
|
|
177
213
|
}) {
|
|
178
214
|
this.port = port;
|
|
179
215
|
this.command = command;
|