@freestyle-sh/with-web-terminal 0.0.1
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/.env +1 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +136 -0
- package/examples/basic.ts +16 -0
- package/examples/binary.ts +43 -0
- package/examples/cred.ts +19 -0
- package/examples/multi.ts +22 -0
- package/examples/readonly.ts +22 -0
- package/package.json +29 -0
- package/src/index.ts +216 -0
package/.env
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
FREESTYLE_API_KEY=NYDNxdzbycbhCF2M6DUhK5-7fPaRDNM7F9cerzZtEacSBgHVgjaZhDoJ9F44kbSWqJN
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as freestyle_sandboxes from 'freestyle-sandboxes';
|
|
2
|
+
import { VmWith, VmWithInstance, CreateVmOptions } from 'freestyle-sandboxes';
|
|
3
|
+
|
|
4
|
+
type TtydConfig = {
|
|
5
|
+
/** Port to run ttyd on (default: auto-assigned starting at 7682) */
|
|
6
|
+
port?: number;
|
|
7
|
+
/** Shell or command to run (default: /bin/bash) */
|
|
8
|
+
command?: string;
|
|
9
|
+
/** User to run terminal as (default: current user) */
|
|
10
|
+
user?: string;
|
|
11
|
+
/** Working directory (default: user home) */
|
|
12
|
+
cwd?: string;
|
|
13
|
+
/** Enable basic auth */
|
|
14
|
+
credential?: {
|
|
15
|
+
username: string;
|
|
16
|
+
password: string;
|
|
17
|
+
};
|
|
18
|
+
/** Terminal title shown in browser tab */
|
|
19
|
+
title?: string;
|
|
20
|
+
/** Read-only terminal (no input allowed) */
|
|
21
|
+
readOnly?: boolean;
|
|
22
|
+
};
|
|
23
|
+
type WebTerminalConfig = {
|
|
24
|
+
id: string;
|
|
25
|
+
} & TtydConfig;
|
|
26
|
+
type ResolvedTerminalConfig = {
|
|
27
|
+
id: string;
|
|
28
|
+
port: number;
|
|
29
|
+
command: string;
|
|
30
|
+
user: string;
|
|
31
|
+
cwd: string;
|
|
32
|
+
credential?: {
|
|
33
|
+
username: string;
|
|
34
|
+
password: string;
|
|
35
|
+
};
|
|
36
|
+
title: string;
|
|
37
|
+
readOnly: boolean;
|
|
38
|
+
};
|
|
39
|
+
declare class VmWebTerminal<T extends WebTerminalConfig[] = WebTerminalConfig[]> extends VmWith<VmWebTerminalInstance<T> & TerminalInstances<T>> {
|
|
40
|
+
private resolvedTerminals;
|
|
41
|
+
constructor(terminals: T);
|
|
42
|
+
configure(existingConfig: CreateVmOptions): CreateVmOptions | Promise<CreateVmOptions>;
|
|
43
|
+
createInstance(): VmWebTerminalInstance<T> & TerminalInstances<T>;
|
|
44
|
+
installServiceName(): string;
|
|
45
|
+
}
|
|
46
|
+
declare class WebTerminal {
|
|
47
|
+
readonly id: string;
|
|
48
|
+
readonly port: number;
|
|
49
|
+
readonly command: string;
|
|
50
|
+
private instance;
|
|
51
|
+
constructor({ id, port, command, instance }: {
|
|
52
|
+
id: string;
|
|
53
|
+
port: number;
|
|
54
|
+
command: string;
|
|
55
|
+
instance: VmWebTerminalInstance<any>;
|
|
56
|
+
});
|
|
57
|
+
/** Expose this terminal publicly via Freestyle routing */
|
|
58
|
+
route({ domain }: {
|
|
59
|
+
domain: string;
|
|
60
|
+
}): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
type TerminalInstances<T extends WebTerminalConfig[]> = {
|
|
63
|
+
[K in T[number]["id"]]: WebTerminal;
|
|
64
|
+
};
|
|
65
|
+
declare class VmWebTerminalInstance<T extends WebTerminalConfig[]> extends VmWithInstance {
|
|
66
|
+
builder: VmWebTerminal<T>;
|
|
67
|
+
constructor(builder: VmWebTerminal<T>, resolvedTerminals: ResolvedTerminalConfig[]);
|
|
68
|
+
/** @internal */
|
|
69
|
+
get _vm(): freestyle_sandboxes.Vm;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { VmWebTerminal, VmWebTerminalInstance, WebTerminal };
|
|
73
|
+
export type { ResolvedTerminalConfig, TerminalInstances, TtydConfig, WebTerminalConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { VmWith, VmTemplate, VmWithInstance } from 'freestyle-sandboxes';
|
|
2
|
+
|
|
3
|
+
class VmWebTerminal extends VmWith {
|
|
4
|
+
resolvedTerminals;
|
|
5
|
+
constructor(terminals) {
|
|
6
|
+
super();
|
|
7
|
+
let nextPort = 7682;
|
|
8
|
+
this.resolvedTerminals = terminals.map((config) => ({
|
|
9
|
+
id: config.id,
|
|
10
|
+
port: config.port ?? nextPort++,
|
|
11
|
+
command: config.command ?? "bash -l",
|
|
12
|
+
user: config.user ?? "root",
|
|
13
|
+
cwd: config.cwd ?? "/root",
|
|
14
|
+
credential: config.credential,
|
|
15
|
+
title: config.title ?? config.id,
|
|
16
|
+
readOnly: config.readOnly ?? false
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
configure(existingConfig) {
|
|
20
|
+
const installScript = `#!/bin/bash
|
|
21
|
+
set -e
|
|
22
|
+
|
|
23
|
+
TTYD_VERSION="1.7.7"
|
|
24
|
+
curl -fsSL -o /usr/local/bin/ttyd "https://github.com/tsl0922/ttyd/releases/download/\${TTYD_VERSION}/ttyd.x86_64"
|
|
25
|
+
chmod +x /usr/local/bin/ttyd
|
|
26
|
+
/usr/local/bin/ttyd --version
|
|
27
|
+
`;
|
|
28
|
+
const services = this.resolvedTerminals.map((t) => {
|
|
29
|
+
const args = [
|
|
30
|
+
`/usr/local/bin/ttyd`,
|
|
31
|
+
`-p ${t.port}`
|
|
32
|
+
];
|
|
33
|
+
if (t.credential) {
|
|
34
|
+
if (t.credential.username.length === 0 || t.credential.password.length === 0) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Invalid credential for terminal ${t.id}: username and password cannot be empty`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
if (t.credential.username.includes(":") || t.credential.password.includes(":")) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Invalid credential for terminal ${t.id}: username and password cannot contain colon (:) character`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
args.push(`--credential ${t.credential.username}:${t.credential.password}`);
|
|
45
|
+
}
|
|
46
|
+
if (t.readOnly) {
|
|
47
|
+
args.push(`--readonly`);
|
|
48
|
+
} else {
|
|
49
|
+
args.push(`--writable`);
|
|
50
|
+
}
|
|
51
|
+
args.push(t.command);
|
|
52
|
+
return {
|
|
53
|
+
name: `web-terminal-${t.id}`,
|
|
54
|
+
mode: "service",
|
|
55
|
+
exec: [args.join(" ")],
|
|
56
|
+
user: t.user,
|
|
57
|
+
cwd: t.cwd,
|
|
58
|
+
restart: "always",
|
|
59
|
+
restartSec: 2,
|
|
60
|
+
after: ["install-ttyd.service", "systemd-sysusers.service"],
|
|
61
|
+
requires: ["systemd-sysusers.service"]
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
const config = {
|
|
65
|
+
template: new VmTemplate({
|
|
66
|
+
additionalFiles: {
|
|
67
|
+
"/opt/install-ttyd.sh": { content: installScript }
|
|
68
|
+
},
|
|
69
|
+
systemd: {
|
|
70
|
+
services: [
|
|
71
|
+
{
|
|
72
|
+
name: "install-ttyd",
|
|
73
|
+
mode: "oneshot",
|
|
74
|
+
deleteAfterSuccess: true,
|
|
75
|
+
exec: ["bash /opt/install-ttyd.sh"],
|
|
76
|
+
timeoutSec: 120
|
|
77
|
+
},
|
|
78
|
+
...services
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
};
|
|
83
|
+
return this.compose(existingConfig, config);
|
|
84
|
+
}
|
|
85
|
+
createInstance() {
|
|
86
|
+
return new VmWebTerminalInstance(this, this.resolvedTerminals);
|
|
87
|
+
}
|
|
88
|
+
installServiceName() {
|
|
89
|
+
return "install-ttyd.service";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
class WebTerminal {
|
|
93
|
+
id;
|
|
94
|
+
port;
|
|
95
|
+
command;
|
|
96
|
+
instance;
|
|
97
|
+
constructor({ id, port, command, instance }) {
|
|
98
|
+
this.id = id;
|
|
99
|
+
this.port = port;
|
|
100
|
+
this.command = command;
|
|
101
|
+
this.instance = instance;
|
|
102
|
+
}
|
|
103
|
+
/** Expose this terminal publicly via Freestyle routing */
|
|
104
|
+
async route({ domain }) {
|
|
105
|
+
const vm = this.instance._vm;
|
|
106
|
+
const freestyle = vm._freestyle;
|
|
107
|
+
console.log(`Routing terminal ${this.id} on vm ${vm.vmId} at port ${this.port} to domain ${domain}`);
|
|
108
|
+
await freestyle.domains.mappings.create({
|
|
109
|
+
domain,
|
|
110
|
+
vmId: vm.vmId,
|
|
111
|
+
vmPort: this.port
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
class VmWebTerminalInstance extends VmWithInstance {
|
|
116
|
+
builder;
|
|
117
|
+
constructor(builder, resolvedTerminals) {
|
|
118
|
+
super();
|
|
119
|
+
this.builder = builder;
|
|
120
|
+
for (const config of resolvedTerminals) {
|
|
121
|
+
const terminal = new WebTerminal({
|
|
122
|
+
id: config.id,
|
|
123
|
+
port: config.port,
|
|
124
|
+
command: config.command,
|
|
125
|
+
instance: this
|
|
126
|
+
});
|
|
127
|
+
this[config.id] = terminal;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** @internal */
|
|
131
|
+
get _vm() {
|
|
132
|
+
return this.vm;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export { VmWebTerminal, VmWebTerminalInstance, WebTerminal };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { freestyle } from "freestyle-sandboxes";
|
|
3
|
+
import { VmWebTerminal } from "../src/index.ts";
|
|
4
|
+
|
|
5
|
+
const webTerminal = new VmWebTerminal([{ id: "main" }] as const);
|
|
6
|
+
|
|
7
|
+
const { vm } = await freestyle.vms.create({
|
|
8
|
+
with: {
|
|
9
|
+
terminal: webTerminal,
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const domain = `${crypto.randomUUID()}.style.dev`;
|
|
14
|
+
await vm.terminal.main.route({ domain });
|
|
15
|
+
|
|
16
|
+
console.log(`Terminal available at: https://${domain}`);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { freestyle, VmSpec } from "freestyle-sandboxes";
|
|
3
|
+
import { VmWebTerminal } from "../src/index.ts";
|
|
4
|
+
|
|
5
|
+
const id = crypto.randomUUID().slice(0, 8);
|
|
6
|
+
|
|
7
|
+
const webTerminal = new VmWebTerminal([{ id: "main", command: "bash -lc claude" }] as const);
|
|
8
|
+
|
|
9
|
+
const { vm } = await freestyle.vms.create({
|
|
10
|
+
spec: new VmSpec({
|
|
11
|
+
snapshot: new VmSpec({
|
|
12
|
+
additionalFiles: {
|
|
13
|
+
"/opt/install-claude.sh": {
|
|
14
|
+
content: `#!/bin/bash
|
|
15
|
+
|
|
16
|
+
curl -fsSL https://claude.ai/install.sh | bash
|
|
17
|
+
|
|
18
|
+
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
|
|
19
|
+
`,
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
systemd: {
|
|
23
|
+
|
|
24
|
+
services: [
|
|
25
|
+
{
|
|
26
|
+
name: "install-claude",
|
|
27
|
+
mode: "oneshot",
|
|
28
|
+
exec: [
|
|
29
|
+
`bash /opt/install-claude.sh`,
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
with: {
|
|
36
|
+
terminal: webTerminal,
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await vm.terminal.main.route({ domain: `${id}-claude.style.dev` });
|
|
42
|
+
|
|
43
|
+
console.log(`Main terminal: https://${id}-claude.style.dev`);
|
package/examples/cred.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { freestyle } from "freestyle-sandboxes";
|
|
3
|
+
import { VmWebTerminal } from "../src/index.ts";
|
|
4
|
+
|
|
5
|
+
const webTerminal = new VmWebTerminal([{ id: "main", credential: {
|
|
6
|
+
username: "admin",
|
|
7
|
+
password: "password123",
|
|
8
|
+
}}] as const);
|
|
9
|
+
|
|
10
|
+
const { vm } = await freestyle.vms.create({
|
|
11
|
+
with: {
|
|
12
|
+
terminal: webTerminal,
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const domain = `${crypto.randomUUID()}.style.dev`;
|
|
17
|
+
await vm.terminal.main.route({ domain });
|
|
18
|
+
|
|
19
|
+
console.log(`Terminal available at: https://${domain}`);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { freestyle } from "freestyle-sandboxes";
|
|
3
|
+
import { VmWebTerminal } from "../src/index.ts";
|
|
4
|
+
|
|
5
|
+
const id = crypto.randomUUID().slice(0, 8);
|
|
6
|
+
|
|
7
|
+
const webTerminal = new VmWebTerminal([
|
|
8
|
+
{ id: "public" },
|
|
9
|
+
{ id: "private", credential: { username: "admin", password: "password123" } },
|
|
10
|
+
] as const);
|
|
11
|
+
|
|
12
|
+
const { vm } = await freestyle.vms.create({
|
|
13
|
+
with: {
|
|
14
|
+
terminal: webTerminal,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
await vm.terminal.public.route({ domain: `${id}-noauth.style.dev` });
|
|
19
|
+
await vm.terminal.private.route({ domain: `${id}-auth.style.dev` });
|
|
20
|
+
|
|
21
|
+
console.log(`Public terminal: https://${id}-noauth.style.dev`);
|
|
22
|
+
console.log(`Private terminal: https://admin:password123@${id}-auth.style.dev`);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { freestyle } from "freestyle-sandboxes";
|
|
3
|
+
import { VmWebTerminal } from "../src/index.ts";
|
|
4
|
+
|
|
5
|
+
const webTerminal = new VmWebTerminal([
|
|
6
|
+
{
|
|
7
|
+
id: "counter",
|
|
8
|
+
readOnly: true,
|
|
9
|
+
shell: "watch -n1 date",
|
|
10
|
+
},
|
|
11
|
+
] as const);
|
|
12
|
+
|
|
13
|
+
const { vm } = await freestyle.vms.create({
|
|
14
|
+
with: {
|
|
15
|
+
terminal: webTerminal,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const domain = `${crypto.randomUUID().slice(0, 8)}-readonly.style.dev`;
|
|
20
|
+
await vm.terminal.counter.route({ domain });
|
|
21
|
+
|
|
22
|
+
console.log(`Read-only counter: https://${domain}`);
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@freestyle-sh/with-web-terminal",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Web terminal for freestyle sandboxes",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"source": "./src/index.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "pkgroll",
|
|
17
|
+
"prepublishOnly": "pnpm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": ["ttyd", "terminal", "web-terminal", "freestyle"],
|
|
20
|
+
"author": "",
|
|
21
|
+
"packageManager": "pnpm@10.11.0",
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"pkgroll": "^2.11.2",
|
|
24
|
+
"typescript": "^5.8.3"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"freestyle-sandboxes": "^0.1.12"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import {
|
|
2
|
+
VmTemplate,
|
|
3
|
+
type CreateVmOptions,
|
|
4
|
+
VmWith,
|
|
5
|
+
VmWithInstance,
|
|
6
|
+
Freestyle,
|
|
7
|
+
} from "freestyle-sandboxes";
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Configuration Types
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export type TtydConfig = {
|
|
14
|
+
/** Port to run ttyd on (default: auto-assigned starting at 7682) */
|
|
15
|
+
port?: number;
|
|
16
|
+
/** Shell or command to run (default: /bin/bash) */
|
|
17
|
+
command?: string;
|
|
18
|
+
/** User to run terminal as (default: current user) */
|
|
19
|
+
user?: string;
|
|
20
|
+
/** Working directory (default: user home) */
|
|
21
|
+
cwd?: string;
|
|
22
|
+
/** Enable basic auth */
|
|
23
|
+
credential?: { username: string; password: string };
|
|
24
|
+
/** Terminal title shown in browser tab */
|
|
25
|
+
title?: string;
|
|
26
|
+
/** Read-only terminal (no input allowed) */
|
|
27
|
+
readOnly?: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type WebTerminalConfig = { id: string } & TtydConfig;
|
|
31
|
+
|
|
32
|
+
export type ResolvedTerminalConfig = {
|
|
33
|
+
id: string;
|
|
34
|
+
port: number;
|
|
35
|
+
command: string;
|
|
36
|
+
user: string;
|
|
37
|
+
cwd: string;
|
|
38
|
+
credential?: { username: string; password: string };
|
|
39
|
+
title: string;
|
|
40
|
+
readOnly: boolean;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Builder Class
|
|
45
|
+
// ============================================================================
|
|
46
|
+
|
|
47
|
+
export class VmWebTerminal<
|
|
48
|
+
T extends WebTerminalConfig[] = WebTerminalConfig[]
|
|
49
|
+
> extends VmWith<VmWebTerminalInstance<T> & TerminalInstances<T>> {
|
|
50
|
+
private resolvedTerminals: ResolvedTerminalConfig[];
|
|
51
|
+
|
|
52
|
+
constructor(terminals: T) {
|
|
53
|
+
super();
|
|
54
|
+
// Resolve config once with defaults
|
|
55
|
+
let nextPort = 7682;
|
|
56
|
+
this.resolvedTerminals = terminals.map((config) => ({
|
|
57
|
+
id: config.id,
|
|
58
|
+
port: config.port ?? nextPort++,
|
|
59
|
+
command: config.command ?? "bash -l",
|
|
60
|
+
user: config.user ?? "root",
|
|
61
|
+
cwd: config.cwd ?? "/root",
|
|
62
|
+
credential: config.credential,
|
|
63
|
+
title: config.title ?? config.id,
|
|
64
|
+
readOnly: config.readOnly ?? false,
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
override configure(
|
|
69
|
+
existingConfig: CreateVmOptions
|
|
70
|
+
): CreateVmOptions | Promise<CreateVmOptions> {
|
|
71
|
+
|
|
72
|
+
// Generate install script
|
|
73
|
+
const installScript = `#!/bin/bash
|
|
74
|
+
set -e
|
|
75
|
+
|
|
76
|
+
TTYD_VERSION="1.7.7"
|
|
77
|
+
curl -fsSL -o /usr/local/bin/ttyd "https://github.com/tsl0922/ttyd/releases/download/\${TTYD_VERSION}/ttyd.x86_64"
|
|
78
|
+
chmod +x /usr/local/bin/ttyd
|
|
79
|
+
/usr/local/bin/ttyd --version
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
// Generate systemd service for each terminal
|
|
83
|
+
const services = this.resolvedTerminals.map((t) => {
|
|
84
|
+
const args: string[] = [
|
|
85
|
+
`/usr/local/bin/ttyd`,
|
|
86
|
+
`-p ${t.port}`,
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
if (t.credential) {
|
|
90
|
+
if (t.credential.username.length === 0 || t.credential.password.length === 0) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Invalid credential for terminal ${t.id}: username and password cannot be empty`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (t.credential.username.includes(":") || t.credential.password.includes(":")) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Invalid credential for terminal ${t.id}: username and password cannot contain colon (:) character`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
args.push(`--credential ${t.credential.username}:${t.credential.password}`);
|
|
101
|
+
}
|
|
102
|
+
if (t.readOnly) {
|
|
103
|
+
args.push(`--readonly`);
|
|
104
|
+
} else {
|
|
105
|
+
args.push(`--writable`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Shell command at the end
|
|
109
|
+
args.push(t.command);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
name: `web-terminal-${t.id}`,
|
|
113
|
+
mode: "service" as const,
|
|
114
|
+
exec: [args.join(" ")],
|
|
115
|
+
user: t.user,
|
|
116
|
+
cwd: t.cwd,
|
|
117
|
+
restart: "always" as const,
|
|
118
|
+
restartSec: 2,
|
|
119
|
+
after: ["install-ttyd.service", "systemd-sysusers.service"],
|
|
120
|
+
requires: ["systemd-sysusers.service"],
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const config: CreateVmOptions = {
|
|
125
|
+
template: new VmTemplate({
|
|
126
|
+
additionalFiles: {
|
|
127
|
+
"/opt/install-ttyd.sh": { content: installScript },
|
|
128
|
+
},
|
|
129
|
+
systemd: {
|
|
130
|
+
services: [
|
|
131
|
+
{
|
|
132
|
+
name: "install-ttyd",
|
|
133
|
+
mode: "oneshot",
|
|
134
|
+
deleteAfterSuccess: true,
|
|
135
|
+
exec: ["bash /opt/install-ttyd.sh"],
|
|
136
|
+
timeoutSec: 120,
|
|
137
|
+
},
|
|
138
|
+
...services,
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
return this.compose(existingConfig, config);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
createInstance(): VmWebTerminalInstance<T> & TerminalInstances<T> {
|
|
148
|
+
return new VmWebTerminalInstance(this, this.resolvedTerminals) as VmWebTerminalInstance<T> & TerminalInstances<T>;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
installServiceName(): string {
|
|
152
|
+
return "install-ttyd.service";
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ============================================================================
|
|
157
|
+
// Instance Class (runtime access)
|
|
158
|
+
// ============================================================================
|
|
159
|
+
|
|
160
|
+
export class WebTerminal {
|
|
161
|
+
readonly id: string;
|
|
162
|
+
readonly port: number;
|
|
163
|
+
readonly command: string;
|
|
164
|
+
private instance: VmWebTerminalInstance<any>;
|
|
165
|
+
|
|
166
|
+
constructor({ id, port, command, instance }: { id: string; port: number; command: string; instance: VmWebTerminalInstance<any> }) {
|
|
167
|
+
this.id = id;
|
|
168
|
+
this.port = port;
|
|
169
|
+
this.command = command;
|
|
170
|
+
this.instance = instance;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Expose this terminal publicly via Freestyle routing */
|
|
174
|
+
async route({ domain }: { domain: string }): Promise<void> {
|
|
175
|
+
const vm = this.instance._vm;
|
|
176
|
+
// @ts-expect-error using internal thing
|
|
177
|
+
const freestyle: Freestyle = vm._freestyle;
|
|
178
|
+
console.log(`Routing terminal ${this.id} on vm ${vm.vmId} at port ${this.port} to domain ${domain}`);
|
|
179
|
+
await freestyle.domains.mappings.create({
|
|
180
|
+
domain: domain,
|
|
181
|
+
vmId: vm.vmId,
|
|
182
|
+
vmPort: this.port,
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export type TerminalInstances<T extends WebTerminalConfig[]> = {
|
|
188
|
+
[K in T[number]["id"]]: WebTerminal;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export class VmWebTerminalInstance<
|
|
192
|
+
T extends WebTerminalConfig[]
|
|
193
|
+
> extends VmWithInstance {
|
|
194
|
+
builder: VmWebTerminal<T>;
|
|
195
|
+
|
|
196
|
+
constructor(builder: VmWebTerminal<T>, resolvedTerminals: ResolvedTerminalConfig[]) {
|
|
197
|
+
super();
|
|
198
|
+
this.builder = builder;
|
|
199
|
+
|
|
200
|
+
// Create terminals as properties using resolved config
|
|
201
|
+
for (const config of resolvedTerminals) {
|
|
202
|
+
const terminal = new WebTerminal({
|
|
203
|
+
id: config.id,
|
|
204
|
+
port: config.port,
|
|
205
|
+
command: config.command,
|
|
206
|
+
instance: this,
|
|
207
|
+
});
|
|
208
|
+
(this as any)[config.id] = terminal;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** @internal */
|
|
213
|
+
get _vm() {
|
|
214
|
+
return this.vm;
|
|
215
|
+
}
|
|
216
|
+
}
|