main
ts 200 lines 5.45 KB
Raw
1 import * as vscode from "vscode";
2
3 import {
4 buildCommand,
5 defaultRelayRegistryURL,
6 validateRelayUrl,
7 } from "./command";
8
9 let tunnelTerminal: vscode.Terminal | undefined;
10 const defaultTunnelHost = "localhost:3000";
11
12 interface RelaySelection {
13 relayUrls: string[];
14 }
15
16 export function activate(context: vscode.ExtensionContext) {
17 context.subscriptions.push(
18 vscode.commands.registerCommand("portal.startTunnel", startTunnel),
19 vscode.commands.registerCommand("portal.startTunnelAdvanced", startTunnelAdvanced),
20 vscode.commands.registerCommand("portal.stopTunnel", stopTunnel),
21 vscode.window.onDidCloseTerminal((t) => {
22 if (t === tunnelTerminal) {
23 tunnelTerminal = undefined;
24 }
25 })
26 );
27 }
28
29 export function deactivate() {
30 tunnelTerminal?.dispose();
31 }
32
33 async function startTunnel() {
34 const host = await promptHost();
35 if (!host) { return; }
36
37 const relaySelection = await resolveRelaySelection(false);
38 if (!relaySelection) { return; }
39
40 runTunnelCommand({
41 host,
42 name: "",
43 relaySelection,
44 thumbnail: "",
45 });
46 }
47
48 async function startTunnelAdvanced() {
49 const host = await promptHost();
50 if (!host) { return; }
51
52 const name = await promptName();
53 if (name === undefined) { return; }
54
55 const relaySelection = await resolveRelaySelection(true);
56 if (!relaySelection) { return; }
57
58 const thumbnail = await promptThumbnail();
59 if (thumbnail === undefined) { return; }
60
61 runTunnelCommand({
62 host,
63 name,
64 relaySelection,
65 thumbnail,
66 });
67 }
68
69 function runTunnelCommand(args: {
70 host: string;
71 name: string;
72 relaySelection: RelaySelection;
73 thumbnail: string;
74 }) {
75 let command: string;
76 try {
77 command = buildCommand({
78 host: args.host,
79 name: args.name,
80 relayList: args.relaySelection.relayUrls.join(","),
81 thumbnail: args.thumbnail,
82 });
83 } catch (error) {
84 const message = error instanceof Error ? error.message : "Failed to build the Portal tunnel command.";
85 vscode.window.showErrorMessage(message);
86 return;
87 }
88
89 if (tunnelTerminal) {
90 tunnelTerminal.dispose();
91 }
92 tunnelTerminal = createTunnelTerminal();
93 tunnelTerminal.show();
94 tunnelTerminal.sendText(command);
95 }
96
97 function stopTunnel() {
98 if (tunnelTerminal) {
99 tunnelTerminal.dispose();
100 tunnelTerminal = undefined;
101 vscode.window.showInformationMessage("Portal tunnel stopped.");
102 } else {
103 vscode.window.showWarningMessage("No active Portal tunnel.");
104 }
105 }
106
107 async function promptHost(): Promise<string | undefined> {
108 const config = vscode.workspace.getConfiguration("portal");
109 const defaultHost = config.get<string>("defaultHost") ?? defaultTunnelHost;
110 return vscode.window.showInputBox({
111 title: "Portal: Local Host",
112 prompt: "Hostname or IP:Port where your service is running",
113 value: defaultHost,
114 validateInput: (v) => (v.trim() ? undefined : "Required"),
115 });
116 }
117
118 async function promptName(): Promise<string | undefined> {
119 const config = vscode.workspace.getConfiguration("portal");
120 const defaultName = config.get<string>("defaultName") ?? "";
121 return vscode.window.showInputBox({
122 title: "Portal: Service Name",
123 prompt: "Optional public hostname prefix. Leave empty to omit --name.",
124 value: defaultName,
125 });
126 }
127
128 async function resolveRelaySelection(interactive: boolean): Promise<RelaySelection | undefined> {
129 const config = vscode.workspace.getConfiguration("portal");
130 const saved = config.get<string[]>("relayUrls") ?? [];
131 if (saved.length > 0) {
132 const invalid = saved.find((url) => validateRelayUrl(url) !== undefined);
133 if (invalid) {
134 vscode.window.showErrorMessage("portal.relayUrls must contain only valid https:// relay URLs.");
135 return undefined;
136 }
137 const relayUrls = saved.map((url) => url.trim());
138 return { relayUrls };
139 }
140
141 if (!interactive) {
142 return { relayUrls: [] };
143 }
144
145 const choice = await vscode.window.showQuickPick([
146 {
147 label: "Use default public registry",
148 description: defaultRelayRegistryURL,
149 },
150 {
151 label: "Enter relay URL",
152 description: "Connect the tunnel to a specific https:// relay",
153 },
154 ], {
155 title: "Portal: Relay Source",
156 placeHolder: "Choose a public registry or a specific relay URL",
157 });
158 if (!choice) {
159 return undefined;
160 }
161 if (choice.label === "Use default public registry") {
162 vscode.window.showInformationMessage(`Portal will use the default public registry: ${defaultRelayRegistryURL}`);
163 return { relayUrls: [] };
164 }
165
166 const input = await vscode.window.showInputBox({
167 title: "Portal: Relay URL",
168 prompt: "Relay server URL (e.g. https://my-relay.example.com)",
169 validateInput: validateRelayUrl,
170 });
171 if (!input) {
172 return undefined;
173 }
174 const relayUrl = input.trim();
175 return { relayUrls: [relayUrl] };
176 }
177
178 async function promptThumbnail(): Promise<string | undefined> {
179 const result = await vscode.window.showInputBox({
180 title: "Portal: Thumbnail URL (optional)",
181 prompt: "Image URL to display as thumbnail. Leave empty to skip.",
182 placeHolder: "https://example.com/image.png",
183 validateInput: (v) => {
184 if (!v.trim()) { return undefined; }
185 try { new URL(v.trim()); return undefined; } catch { return "Enter a valid URL or leave empty"; }
186 },
187 });
188 return result;
189 }
190
191 function createTunnelTerminal(): vscode.Terminal {
192 if (process.platform !== "win32") {
193 return vscode.window.createTerminal("Portal Tunnel");
194 }
195
196 return vscode.window.createTerminal({
197 name: "Portal Tunnel",
198 shellPath: "powershell.exe",
199 });
200 }