uplink-cli 0.1.37 → 0.1.39
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/AGENTS.md +161 -0
- package/LICENSE +21 -0
- package/README.md +45 -44
- package/cli/src/index.ts +5 -3
- package/cli/src/registrars/cloudflare.ts +148 -0
- package/cli/src/registrars/godaddy.ts +99 -0
- package/cli/src/registrars/hostinger.ts +106 -0
- package/cli/src/registrars/http.ts +18 -0
- package/cli/src/registrars/index.ts +30 -0
- package/cli/src/registrars/namecheap.ts +163 -0
- package/cli/src/registrars/secret.ts +66 -0
- package/cli/src/registrars/store.ts +55 -0
- package/cli/src/registrars/types.ts +40 -0
- package/cli/src/subcommands/admin.ts +17 -30
- package/cli/src/subcommands/db.ts +63 -57
- package/cli/src/subcommands/dev.ts +23 -25
- package/cli/src/subcommands/domains.ts +268 -0
- package/cli/src/subcommands/host-domains.ts +148 -0
- package/cli/src/subcommands/host.ts +106 -32
- package/cli/src/subcommands/menu/colors.ts +1 -1
- package/cli/src/subcommands/menu/effects/tunnel-clients.ts +87 -14
- package/cli/src/subcommands/menu/inline-tree-select.ts +6 -5
- package/cli/src/subcommands/menu/io.ts +27 -5
- package/cli/src/subcommands/menu/menus/domains.ts +199 -0
- package/cli/src/subcommands/menu/menus/hosting.ts +14 -46
- package/cli/src/subcommands/menu/menus/index.ts +1 -0
- package/cli/src/subcommands/menu/menus/tunnels.ts +25 -67
- package/cli/src/subcommands/menu/render.ts +2 -2
- package/cli/src/subcommands/menu/tests.ts +1 -1
- package/cli/src/subcommands/menu/tunnels.ts +10 -99
- package/cli/src/subcommands/menu/types.ts +8 -0
- package/cli/src/subcommands/menu.ts +32 -524
- package/cli/src/subcommands/system.ts +58 -36
- package/cli/src/subcommands/tunnel.ts +124 -33
- package/cli/src/templates/index.ts +122 -0
- package/cli/src/tui/App.tsx +197 -0
- package/cli/src/tui/AppInspector.tsx +114 -0
- package/cli/src/tui/HomeStatus.tsx +59 -0
- package/cli/src/tui/brand.tsx +20 -0
- package/cli/src/tui/format.ts +22 -0
- package/cli/src/tui/index.mts +6 -0
- package/cli/src/tui/liveTree.ts +40 -0
- package/cli/src/tui/package.json +3 -0
- package/cli/src/tui/runMenu.tsx +57 -0
- package/cli/src/tui/session.mts +382 -0
- package/cli/src/tui/snapshot.ts +146 -0
- package/cli/src/utils/analyze.ts +27 -43
- package/cli/src/utils/framework-output.ts +171 -0
- package/cli/src/utils/launchDomainking.ts +64 -0
- package/docs/AGENTS.md +113 -146
- package/docs/MENU_STRUCTURE.md +56 -288
- package/docs/README.md +6 -6
- package/package.json +18 -35
- package/scripts/tunnel/client-improved.js +127 -38
- package/scripts/tunnel/client.js +118 -0
- package/assets/cli-screenshot.png +0 -0
|
@@ -14,9 +14,13 @@ type Deps = {
|
|
|
14
14
|
) => Promise<{ index: number; value: string | number | null } | null>;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
type HostedApp = { name: string; id: string; url?: string };
|
|
17
|
+
export type HostedApp = { name: string; id: string; url?: string };
|
|
18
18
|
|
|
19
|
-
function
|
|
19
|
+
function appOptionLabel(app: HostedApp): string {
|
|
20
|
+
return app.url ? `${app.name} ${app.url}` : app.name;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function parseHostedApps(output: string): HostedApp[] {
|
|
20
24
|
const lines = output.split("\n");
|
|
21
25
|
const apps: HostedApp[] = [];
|
|
22
26
|
for (let i = 0; i < lines.length; i += 1) {
|
|
@@ -47,7 +51,7 @@ async function resolvePath(promptLine: Deps["promptLine"]): Promise<string | nul
|
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
function runCli(args: string[]): void {
|
|
54
|
+
export function runCli(args: string[], extraEnv?: Record<string, string>): void {
|
|
51
55
|
try {
|
|
52
56
|
process.stdin.setRawMode(false);
|
|
53
57
|
} catch {
|
|
@@ -57,6 +61,7 @@ function runCli(args: string[]): void {
|
|
|
57
61
|
const cmd = cliBin ? [cliBin, ...args] : [process.argv[1], ...args];
|
|
58
62
|
const result = spawnSync(process.execPath, cmd, {
|
|
59
63
|
stdio: "inherit",
|
|
64
|
+
env: extraEnv ? { ...process.env, ...extraEnv } : process.env,
|
|
60
65
|
});
|
|
61
66
|
if (result.error) throw result.error;
|
|
62
67
|
if (result.status && result.status !== 0) {
|
|
@@ -64,7 +69,7 @@ function runCli(args: string[]): void {
|
|
|
64
69
|
}
|
|
65
70
|
}
|
|
66
71
|
|
|
67
|
-
function runCliCapture(args: string[]): string {
|
|
72
|
+
export function runCliCapture(args: string[], extraEnv?: Record<string, string>): string {
|
|
68
73
|
try {
|
|
69
74
|
process.stdin.setRawMode(false);
|
|
70
75
|
} catch {
|
|
@@ -75,6 +80,7 @@ function runCliCapture(args: string[]): string {
|
|
|
75
80
|
const result = spawnSync(process.execPath, cmd, {
|
|
76
81
|
stdio: ["ignore", "pipe", "pipe"],
|
|
77
82
|
encoding: "utf8",
|
|
83
|
+
env: extraEnv ? { ...process.env, ...extraEnv } : process.env,
|
|
78
84
|
});
|
|
79
85
|
if (result.error) throw result.error;
|
|
80
86
|
if (result.status && result.status !== 0) {
|
|
@@ -120,7 +126,7 @@ export function buildHostingMenu(deps: Deps): MenuChoice {
|
|
|
120
126
|
return "No apps found. Use Setup Wizard to create one first.";
|
|
121
127
|
}
|
|
122
128
|
const options: SelectOption[] = apps.map((app) => ({
|
|
123
|
-
label:
|
|
129
|
+
label: appOptionLabel(app),
|
|
124
130
|
value: app.id,
|
|
125
131
|
}));
|
|
126
132
|
const choice = await inlineSelect("Select app to deploy to", options, true);
|
|
@@ -160,45 +166,6 @@ export function buildHostingMenu(deps: Deps): MenuChoice {
|
|
|
160
166
|
return "Analysis complete.";
|
|
161
167
|
},
|
|
162
168
|
},
|
|
163
|
-
{
|
|
164
|
-
label: "List Hosted Apps",
|
|
165
|
-
action: async () => {
|
|
166
|
-
const output = runCliCapture(["host", "list"]);
|
|
167
|
-
if (!output || output.includes("No apps found")) {
|
|
168
|
-
restoreRawMode();
|
|
169
|
-
return "No apps found.";
|
|
170
|
-
}
|
|
171
|
-
const apps = parseHostedApps(output);
|
|
172
|
-
if (apps.length === 0) {
|
|
173
|
-
restoreRawMode();
|
|
174
|
-
return "No apps found.";
|
|
175
|
-
}
|
|
176
|
-
const options: SelectOption[] = apps.map((app) => ({
|
|
177
|
-
label: `${app.name} (${app.id})${app.url ? ` ${app.url}` : ""}`,
|
|
178
|
-
value: app.id,
|
|
179
|
-
}));
|
|
180
|
-
const choice = await inlineSelect("Hosted apps", options, true);
|
|
181
|
-
if (choice === null) {
|
|
182
|
-
restoreRawMode();
|
|
183
|
-
return "";
|
|
184
|
-
}
|
|
185
|
-
const selected = apps.find((app) => app.id === choice.value);
|
|
186
|
-
if (!selected) {
|
|
187
|
-
restoreRawMode();
|
|
188
|
-
return "Invalid selection.";
|
|
189
|
-
}
|
|
190
|
-
restoreRawMode();
|
|
191
|
-
return [
|
|
192
|
-
`App: ${selected.name}`,
|
|
193
|
-
`ID: ${selected.id}`,
|
|
194
|
-
selected.url ? `URL: ${selected.url}` : "",
|
|
195
|
-
"",
|
|
196
|
-
"Commands:",
|
|
197
|
-
` uplink host status --id ${selected.id}`,
|
|
198
|
-
` uplink host logs --id ${selected.id}`,
|
|
199
|
-
].join("\n");
|
|
200
|
-
},
|
|
201
|
-
},
|
|
202
169
|
{
|
|
203
170
|
label: "Delete Hosted App",
|
|
204
171
|
action: async () => {
|
|
@@ -213,7 +180,7 @@ export function buildHostingMenu(deps: Deps): MenuChoice {
|
|
|
213
180
|
return "No apps found.";
|
|
214
181
|
}
|
|
215
182
|
const options: SelectOption[] = apps.map((app) => ({
|
|
216
|
-
label:
|
|
183
|
+
label: appOptionLabel(app),
|
|
217
184
|
value: app.id,
|
|
218
185
|
}));
|
|
219
186
|
const choice = await inlineSelect("Select app to delete", options, true);
|
|
@@ -251,8 +218,9 @@ export function buildHostingMenu(deps: Deps): MenuChoice {
|
|
|
251
218
|
" Setup Wizard - First-time setup: creates Dockerfile, config, app, and deploys",
|
|
252
219
|
" Deploy - Redeploy to an existing app (faster, skips setup)",
|
|
253
220
|
" Analyze - Check project for deployment readiness",
|
|
254
|
-
"
|
|
221
|
+
" Apps - Arrow through apps to inspect url, status, and size",
|
|
255
222
|
" Delete App - Remove an app and optionally its data",
|
|
223
|
+
" Custom domains - also under the top-level Domains menu",
|
|
256
224
|
"",
|
|
257
225
|
"CLI commands:",
|
|
258
226
|
" uplink host setup --name <app> --path <path> # Full setup + deploy",
|
|
@@ -16,8 +16,10 @@ type Deps = {
|
|
|
16
16
|
scanCommonPorts: () => Promise<number[]>;
|
|
17
17
|
findTunnelClients: () => Array<{ pid: number; port: number; token: string }>;
|
|
18
18
|
createAndStartTunnel: (port: number) => Promise<string>;
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
stopTunnelClients: (
|
|
20
|
+
clients: Array<{ pid: number; port: number; token: string }>,
|
|
21
|
+
opts?: { connectedGhosts?: boolean }
|
|
22
|
+
) => Promise<{ killed: number; deleted: number }>;
|
|
21
23
|
colorDim: (text: string) => string;
|
|
22
24
|
colorRed: (text: string) => string;
|
|
23
25
|
};
|
|
@@ -33,8 +35,7 @@ export function buildManageTunnelsMenu(deps: Deps): MenuChoice {
|
|
|
33
35
|
scanCommonPorts,
|
|
34
36
|
findTunnelClients,
|
|
35
37
|
createAndStartTunnel,
|
|
36
|
-
|
|
37
|
-
killAllTunnelClients,
|
|
38
|
+
stopTunnelClients,
|
|
38
39
|
colorDim,
|
|
39
40
|
colorRed,
|
|
40
41
|
} = deps;
|
|
@@ -43,7 +44,7 @@ export function buildManageTunnelsMenu(deps: Deps): MenuChoice {
|
|
|
43
44
|
label: "Share",
|
|
44
45
|
subMenu: [
|
|
45
46
|
{
|
|
46
|
-
label: "Start
|
|
47
|
+
label: "Start tunnel",
|
|
47
48
|
action: async () => {
|
|
48
49
|
try {
|
|
49
50
|
// Scan for active ports
|
|
@@ -142,65 +143,25 @@ export function buildManageTunnelsMenu(deps: Deps): MenuChoice {
|
|
|
142
143
|
},
|
|
143
144
|
},
|
|
144
145
|
{
|
|
145
|
-
label: "
|
|
146
|
+
label: "Stop tunnel",
|
|
146
147
|
action: async () => {
|
|
147
|
-
const answer = await promptLine('Local port to expose (default 3000, or "back"): ');
|
|
148
|
-
if (isBackInput(answer)) {
|
|
149
|
-
restoreRawMode();
|
|
150
|
-
return "";
|
|
151
|
-
}
|
|
152
|
-
const port = Number(answer) || 3000;
|
|
153
148
|
try {
|
|
154
|
-
const result = await apiRequest("POST", "/v1/tunnels", { port });
|
|
155
|
-
try {
|
|
156
|
-
process.stdin.setRawMode(true);
|
|
157
|
-
process.stdin.resume();
|
|
158
|
-
} catch {
|
|
159
|
-
/* ignore */
|
|
160
|
-
}
|
|
161
|
-
const url = result.url || "(no url)";
|
|
162
|
-
const token = result.token || "(no token)";
|
|
163
|
-
const httpFallback = typeof url === "string" && url.startsWith("https://") ? url.replace(/^https:\/\//, "http://") : "";
|
|
164
|
-
return [
|
|
165
|
-
`Created tunnel: ${url}`,
|
|
166
|
-
httpFallback && url !== httpFallback ? `HTTP fallback: ${httpFallback}` : "",
|
|
167
|
-
`Token: ${token}`,
|
|
168
|
-
"",
|
|
169
|
-
"To start the tunnel client, run:",
|
|
170
|
-
` node scripts/tunnel/client-improved.js --token ${token} --port ${port} --ctrl ${process.env.TUNNEL_CTRL || "tunnel.uplink.spot:7071"}`,
|
|
171
|
-
]
|
|
172
|
-
.filter(Boolean)
|
|
173
|
-
.join("\n");
|
|
174
|
-
} catch (err: any) {
|
|
175
|
-
try {
|
|
176
|
-
process.stdin.setRawMode(true);
|
|
177
|
-
process.stdin.resume();
|
|
178
|
-
} catch {
|
|
179
|
-
/* ignore */
|
|
180
|
-
}
|
|
181
|
-
throw err;
|
|
182
|
-
}
|
|
183
|
-
},
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
label: "Stop Tunnel",
|
|
187
|
-
action: async () => {
|
|
188
|
-
try {
|
|
189
|
-
// Find running tunnel client processes
|
|
190
149
|
const processes = findTunnelClients();
|
|
191
150
|
|
|
192
151
|
if (processes.length === 0) {
|
|
152
|
+
const ghost = await stopTunnelClients([], { connectedGhosts: true });
|
|
193
153
|
restoreRawMode();
|
|
154
|
+
if (ghost.deleted > 0) {
|
|
155
|
+
return `✓ Removed ${ghost.deleted} relay-connected tunnel${ghost.deleted !== 1 ? "s" : ""} with no local client`;
|
|
156
|
+
}
|
|
194
157
|
return "No running tunnel clients found.";
|
|
195
158
|
}
|
|
196
159
|
|
|
197
|
-
// Build options from running tunnels
|
|
198
160
|
const options: SelectOption[] = processes.map((p) => ({
|
|
199
161
|
label: `Port ${p.port} ${colorDim(`(${truncate(p.token, 8)})`)}`,
|
|
200
162
|
value: p.pid,
|
|
201
163
|
}));
|
|
202
164
|
|
|
203
|
-
// Add "Stop all" option if more than one tunnel
|
|
204
165
|
if (processes.length > 1) {
|
|
205
166
|
options.push({ label: colorRed("Stop all tunnels"), value: "all" });
|
|
206
167
|
}
|
|
@@ -208,28 +169,25 @@ export function buildManageTunnelsMenu(deps: Deps): MenuChoice {
|
|
|
208
169
|
const result = await inlineSelect("Select tunnel to stop", options, true);
|
|
209
170
|
|
|
210
171
|
if (result === null) {
|
|
211
|
-
// User selected Back
|
|
212
172
|
restoreRawMode();
|
|
213
|
-
return "";
|
|
173
|
+
return "";
|
|
214
174
|
}
|
|
215
175
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (!ok) {
|
|
225
|
-
restoreRawMode();
|
|
226
|
-
throw new Error(`Failed to kill process ${pid}`);
|
|
227
|
-
}
|
|
228
|
-
killed = 1;
|
|
229
|
-
}
|
|
176
|
+
const selected =
|
|
177
|
+
result.value === "all"
|
|
178
|
+
? processes
|
|
179
|
+
: processes.filter((p) => {
|
|
180
|
+
const pid = result.value as number;
|
|
181
|
+
const chosen = processes.find((c) => c.pid === pid);
|
|
182
|
+
return p.pid === pid || (chosen && p.token === chosen.token);
|
|
183
|
+
});
|
|
230
184
|
|
|
185
|
+
const { killed, deleted } = await stopTunnelClients(selected);
|
|
231
186
|
restoreRawMode();
|
|
232
|
-
|
|
187
|
+
if (killed === 0 && deleted === 0) {
|
|
188
|
+
throw new Error("Failed to stop tunnel");
|
|
189
|
+
}
|
|
190
|
+
return `✓ Stopped ${killed} local client${killed !== 1 ? "s" : ""}, removed ${deleted} tunnel record${deleted !== 1 ? "s" : ""}`;
|
|
233
191
|
} catch (err: any) {
|
|
234
192
|
restoreRawMode();
|
|
235
193
|
throw err;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { clearScreen } from "./io";
|
|
2
|
-
import { colorBold,
|
|
2
|
+
import { colorBold, colorDim, colorGreen, colorRed } from "./colors";
|
|
3
3
|
import { DEFAULT_MENU_MESSAGE, type MenuChoice } from "./types";
|
|
4
4
|
|
|
5
5
|
export type RenderArgs = {
|
|
@@ -107,7 +107,7 @@ export function renderMenu(args: RenderArgs) {
|
|
|
107
107
|
const styledLine = line
|
|
108
108
|
.replace(/^✓/, colorGreen("✓"))
|
|
109
109
|
.replace(/^✗/, colorRed("✗"))
|
|
110
|
-
.replace(/^→/,
|
|
110
|
+
.replace(/^→/, colorDim("→"));
|
|
111
111
|
console.log(styledLine);
|
|
112
112
|
});
|
|
113
113
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fetch from "node-fetch";
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
|
-
import { resolveProjectRoot } from "
|
|
3
|
+
import { resolveProjectRoot } from "../../utils/project-root";
|
|
4
4
|
import { getResolvedApiBase, getResolvedApiToken } from "../../utils/api-base";
|
|
5
5
|
|
|
6
6
|
export function runSmoke(script: "smoke:tunnel" | "smoke:db" | "smoke:all" | "test:comprehensive") {
|
|
@@ -1,99 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
`⚠ Tunnel already running on port ${port}`,
|
|
12
|
-
``,
|
|
13
|
-
`→ PID: ${existing[0].pid}`,
|
|
14
|
-
`→ Token: ${existing[0].token.substring(0, 8)}...`,
|
|
15
|
-
``,
|
|
16
|
-
`Use "Stop Tunnel" first to disconnect the existing tunnel.`,
|
|
17
|
-
].join("\n");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const result = await apiRequest("POST", "/v1/tunnels", { port });
|
|
21
|
-
const url = result.url || "(no url)";
|
|
22
|
-
const token = result.token || "(no token)";
|
|
23
|
-
const alias = result.alias || null;
|
|
24
|
-
const ctrl = process.env.TUNNEL_CTRL || "tunnel.uplink.spot:7071";
|
|
25
|
-
|
|
26
|
-
const path = require("path");
|
|
27
|
-
const projectRoot = resolveProjectRoot(__dirname);
|
|
28
|
-
const clientPath = path.join(projectRoot, "scripts/tunnel/client-improved.js");
|
|
29
|
-
const clientProcess = spawn("node", [clientPath, "--token", token, "--port", String(port), "--ctrl", ctrl], {
|
|
30
|
-
stdio: "ignore",
|
|
31
|
-
detached: true,
|
|
32
|
-
cwd: projectRoot,
|
|
33
|
-
});
|
|
34
|
-
clientProcess.unref();
|
|
35
|
-
|
|
36
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
process.stdin.setRawMode(true);
|
|
40
|
-
process.stdin.resume();
|
|
41
|
-
} catch {
|
|
42
|
-
/* ignore */
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const lines = [
|
|
46
|
-
`✓ Tunnel created and client started`,
|
|
47
|
-
``,
|
|
48
|
-
`→ Public URL ${url}`,
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
if (alias) {
|
|
52
|
-
// Use aliasUrl from backend if available, otherwise construct it
|
|
53
|
-
const aliasUrl = result.aliasUrl || `https://${alias}.uplink.spot`;
|
|
54
|
-
lines.push(`→ Alias ${alias}`);
|
|
55
|
-
lines.push(`→ Alias URL ${aliasUrl}`);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
lines.push(
|
|
59
|
-
`→ Token ${token}`,
|
|
60
|
-
`→ Local port ${port}`,
|
|
61
|
-
``,
|
|
62
|
-
`Tunnel client running in background.`,
|
|
63
|
-
`Use "Stop Tunnel" to disconnect.`,
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
return lines.join("\n");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function findTunnelClients(): Array<{ pid: number; port: number; token: string }> {
|
|
70
|
-
try {
|
|
71
|
-
const user = process.env.USER || "";
|
|
72
|
-
const psCmd = user ? `ps -u ${user} -o pid=,command=` : "ps -eo pid=,command=";
|
|
73
|
-
const output = execSync(psCmd, { encoding: "utf-8" });
|
|
74
|
-
const lines = output
|
|
75
|
-
.trim()
|
|
76
|
-
.split("\n")
|
|
77
|
-
.filter((line) => line.includes("scripts/tunnel/client-improved.js"));
|
|
78
|
-
|
|
79
|
-
const clients: Array<{ pid: number; port: number; token: string }> = [];
|
|
80
|
-
|
|
81
|
-
for (const line of lines) {
|
|
82
|
-
const pidMatch = line.match(/^\s*(\d+)/);
|
|
83
|
-
const tokenMatch = line.match(/--token\s+(\S+)/);
|
|
84
|
-
const portMatch = line.match(/--port\s+(\d+)/);
|
|
85
|
-
|
|
86
|
-
if (pidMatch && tokenMatch && portMatch) {
|
|
87
|
-
clients.push({
|
|
88
|
-
pid: parseInt(pidMatch[1], 10),
|
|
89
|
-
port: parseInt(portMatch[1], 10),
|
|
90
|
-
token: tokenMatch[1],
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return clients;
|
|
96
|
-
} catch {
|
|
97
|
-
return [];
|
|
98
|
-
}
|
|
99
|
-
}
|
|
1
|
+
/** @deprecated Prefer `./effects/tunnel-clients` — kept as a thin re-export. */
|
|
2
|
+
export {
|
|
3
|
+
createAndStartTunnel,
|
|
4
|
+
findTunnelClients,
|
|
5
|
+
killAllTunnelClients,
|
|
6
|
+
killTunnelClient,
|
|
7
|
+
startTunnelClient,
|
|
8
|
+
stopTunnelClients,
|
|
9
|
+
resolveTunnelClientPath,
|
|
10
|
+
} from "./effects/tunnel-clients";
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
export type MenuInspect = {
|
|
2
|
+
kind: "app";
|
|
3
|
+
id: string;
|
|
4
|
+
url?: string;
|
|
5
|
+
createdAt?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
1
8
|
export type MenuChoice = {
|
|
2
9
|
label: string;
|
|
3
10
|
action?: () => Promise<string>;
|
|
4
11
|
subMenu?: MenuChoice[];
|
|
12
|
+
inspect?: MenuInspect;
|
|
5
13
|
};
|
|
6
14
|
|
|
7
15
|
export const DEFAULT_MENU_MESSAGE = "Use ↑/↓ and Enter. ← to go back. Ctrl+C to quit.";
|