onedeploy-cli 0.1.10 → 0.1.11
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/browser.js +1 -6
- package/dist/index.js +55 -5
- package/dist/nebula.js +15 -6
- package/dist/proxy.js +69 -0
- package/dist/tunnel-menu.js +169 -0
- package/dist/tunnel.js +76 -0
- package/package.json +4 -3
- package/src/index.ts +70 -5
package/dist/browser.js
CHANGED
|
@@ -9,12 +9,7 @@ export const openBrowser = async (url) => {
|
|
|
9
9
|
case "linux": {
|
|
10
10
|
if (isRoot && sudoUser) {
|
|
11
11
|
// Run as the original user to avoid browser sandbox issues
|
|
12
|
-
await promisify(execFile)("sudo", [
|
|
13
|
-
"-u",
|
|
14
|
-
sudoUser,
|
|
15
|
-
"xdg-open",
|
|
16
|
-
url,
|
|
17
|
-
]);
|
|
12
|
+
await promisify(execFile)("sudo", ["-u", sudoUser, "xdg-open", url]);
|
|
18
13
|
}
|
|
19
14
|
else {
|
|
20
15
|
await promisify(execFile)("xdg-open", [url]);
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import prompts from "prompts";
|
|
3
3
|
import { loadConfig, saveConfig } from "./config.js";
|
|
4
|
-
import { cleanupNebula,
|
|
4
|
+
import { cleanupNebula, getSession, installNebula, isNebulaRunning, startNebula, stopNebula, } from "./nebula.js";
|
|
5
|
+
import { deleteAllTunnels } from "./tunnel.js";
|
|
6
|
+
import { tunnelMenu } from "./tunnel-menu.js";
|
|
7
|
+
// Track active connection for cleanup on signals
|
|
8
|
+
let activeConnection = null;
|
|
9
|
+
const cleanup = async () => {
|
|
10
|
+
if (activeConnection) {
|
|
11
|
+
console.log("\nCleaning up...");
|
|
12
|
+
try {
|
|
13
|
+
await deleteAllTunnels(activeConnection.instance, activeConnection.session);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// ignore if already disconnected
|
|
17
|
+
}
|
|
18
|
+
activeConnection.stopRefresh();
|
|
19
|
+
stopNebula(activeConnection.nebulaProcess);
|
|
20
|
+
activeConnection = null;
|
|
21
|
+
}
|
|
22
|
+
await cleanupNebula();
|
|
23
|
+
};
|
|
24
|
+
const handleSignal = () => {
|
|
25
|
+
if (!isNebulaRunning) {
|
|
26
|
+
process.exit();
|
|
27
|
+
}
|
|
28
|
+
// Perform cleanup and exit
|
|
29
|
+
cleanup().finally(() => process.exit());
|
|
30
|
+
};
|
|
5
31
|
if (process.argv.includes("--version")) {
|
|
6
32
|
console.log("onedeploy-cli version 0.1.4");
|
|
7
33
|
process.exit();
|
|
@@ -109,9 +135,30 @@ const mainMenu = async () => {
|
|
|
109
135
|
else {
|
|
110
136
|
urlToConnect = selection;
|
|
111
137
|
}
|
|
112
|
-
// Connect first
|
|
113
138
|
const instance = { url: urlToConnect, apiKey: "" };
|
|
114
|
-
|
|
139
|
+
// 1. Authenticate
|
|
140
|
+
const session = await getSession(instance);
|
|
141
|
+
// 2. Start Nebula (non-blocking)
|
|
142
|
+
const { nebulaProcess, stopRefresh } = await startNebula(instance, session);
|
|
143
|
+
// Track connection for signal handlers
|
|
144
|
+
activeConnection = { instance, session, nebulaProcess, stopRefresh };
|
|
145
|
+
// 3. Enter tunnel management loop
|
|
146
|
+
try {
|
|
147
|
+
await tunnelMenu(instance, session);
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
// 4. Cleanup on exit
|
|
151
|
+
console.log("Cleaning up tunnels...");
|
|
152
|
+
try {
|
|
153
|
+
await deleteAllTunnels(instance, session);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// ignore if already disconnected
|
|
157
|
+
}
|
|
158
|
+
stopRefresh();
|
|
159
|
+
stopNebula(nebulaProcess);
|
|
160
|
+
activeConnection = null;
|
|
161
|
+
}
|
|
115
162
|
// Only save if connection was successful (didn't throw)
|
|
116
163
|
await addInstance(urlToConnect);
|
|
117
164
|
return true;
|
|
@@ -120,8 +167,11 @@ process.on("SIGINT", () => {
|
|
|
120
167
|
if (!isNebulaRunning) {
|
|
121
168
|
process.exit();
|
|
122
169
|
}
|
|
123
|
-
//
|
|
170
|
+
// When Nebula is running, let prompts handle SIGINT (returns undefined)
|
|
171
|
+
// which triggers the finally block in mainMenu
|
|
124
172
|
});
|
|
173
|
+
process.on("SIGTERM", handleSignal);
|
|
174
|
+
process.on("SIGHUP", handleSignal);
|
|
125
175
|
await installNebula();
|
|
126
176
|
try {
|
|
127
177
|
while (true) {
|
|
@@ -132,5 +182,5 @@ try {
|
|
|
132
182
|
}
|
|
133
183
|
}
|
|
134
184
|
finally {
|
|
135
|
-
await
|
|
185
|
+
await cleanup();
|
|
136
186
|
}
|
package/dist/nebula.js
CHANGED
|
@@ -107,6 +107,10 @@ export const getNebulaCert = async (instance, session) => {
|
|
|
107
107
|
`${tmpDir}/nebula-debug.crt`,
|
|
108
108
|
]);
|
|
109
109
|
const cert = JSON.parse(stdout);
|
|
110
|
+
// Extract Nebula IP from certificate (remove CIDR suffix)
|
|
111
|
+
if (cert.details.networks && cert.details.networks.length > 0) {
|
|
112
|
+
nebulaIp = cert.details.networks[0].split("/")[0];
|
|
113
|
+
}
|
|
110
114
|
// in case our local time isn't correct, wait for the certificate to become valid
|
|
111
115
|
const validFromDate = new Date(cert.details.notBefore);
|
|
112
116
|
if (validFromDate.toString() === "Invalid Date") {
|
|
@@ -119,7 +123,8 @@ export const getNebulaCert = async (instance, session) => {
|
|
|
119
123
|
}
|
|
120
124
|
};
|
|
121
125
|
export let isNebulaRunning = false;
|
|
122
|
-
|
|
126
|
+
export let nebulaIp = null;
|
|
127
|
+
export const getSession = (instance) => {
|
|
123
128
|
return new Promise((resolve) => {
|
|
124
129
|
const getServerAddr = () => {
|
|
125
130
|
const addr = server.address();
|
|
@@ -165,17 +170,21 @@ const getSession = (instance) => {
|
|
|
165
170
|
server.listen();
|
|
166
171
|
});
|
|
167
172
|
};
|
|
168
|
-
export const
|
|
169
|
-
const session = await getSession(instance);
|
|
173
|
+
export const startNebula = async (instance, session) => {
|
|
170
174
|
await getNebulaCert(instance, session);
|
|
171
175
|
isNebulaRunning = true;
|
|
172
|
-
const nebula = spawn(`${nebulaDir}/nebula`, ["-config", `${tmpDir}/nebula.yml`], { stdio: "
|
|
176
|
+
const nebula = spawn(`${nebulaDir}/nebula`, ["-config", `${tmpDir}/nebula.yml`], { stdio: "ignore" });
|
|
173
177
|
const interval = setInterval(async () => {
|
|
174
178
|
// refresh Nebula cert every 10 minutes (lifetime is 15 minutes)
|
|
175
179
|
await getNebulaCert(instance, session);
|
|
176
180
|
nebula.kill("SIGHUP");
|
|
177
181
|
}, 10 * 60 * 1000);
|
|
178
|
-
|
|
182
|
+
return {
|
|
183
|
+
nebulaProcess: nebula,
|
|
184
|
+
stopRefresh: () => clearInterval(interval),
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
export const stopNebula = (nebulaProcess) => {
|
|
188
|
+
nebulaProcess.kill("SIGTERM");
|
|
179
189
|
isNebulaRunning = false;
|
|
180
|
-
clearInterval(interval);
|
|
181
190
|
};
|
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createServer, Socket } from "node:net";
|
|
2
|
+
// Track active proxies by port
|
|
3
|
+
const activeProxies = new Map();
|
|
4
|
+
const connectToLocalhost = (port, clientSocket) => {
|
|
5
|
+
const targetSocket = new Socket();
|
|
6
|
+
const tryConnect = (host, fallback) => {
|
|
7
|
+
targetSocket.connect(port, host, () => {
|
|
8
|
+
clientSocket.pipe(targetSocket);
|
|
9
|
+
targetSocket.pipe(clientSocket);
|
|
10
|
+
});
|
|
11
|
+
targetSocket.once("error", (err) => {
|
|
12
|
+
if (err.code === "ECONNREFUSED" && fallback) {
|
|
13
|
+
// Try fallback address
|
|
14
|
+
targetSocket.removeAllListeners();
|
|
15
|
+
const fallbackSocket = new Socket();
|
|
16
|
+
fallbackSocket.connect(port, fallback, () => {
|
|
17
|
+
clientSocket.pipe(fallbackSocket);
|
|
18
|
+
fallbackSocket.pipe(clientSocket);
|
|
19
|
+
});
|
|
20
|
+
fallbackSocket.on("error", () => clientSocket.destroy());
|
|
21
|
+
clientSocket.on("error", () => fallbackSocket.destroy());
|
|
22
|
+
clientSocket.on("close", () => fallbackSocket.destroy());
|
|
23
|
+
fallbackSocket.on("close", () => clientSocket.destroy());
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
clientSocket.destroy();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
tryConnect("127.0.0.1", "::1");
|
|
31
|
+
clientSocket.on("error", () => targetSocket.destroy());
|
|
32
|
+
clientSocket.on("close", () => targetSocket.destroy());
|
|
33
|
+
targetSocket.on("close", () => clientSocket.destroy());
|
|
34
|
+
};
|
|
35
|
+
export const startProxy = (port, bindIp) => {
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
const server = createServer((clientSocket) => {
|
|
38
|
+
connectToLocalhost(port, clientSocket);
|
|
39
|
+
});
|
|
40
|
+
server.on("error", (err) => {
|
|
41
|
+
if (err.code === "EADDRINUSE") {
|
|
42
|
+
// App already binds to this IP, no proxy needed
|
|
43
|
+
resolve(false);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
// Other error, log and continue without proxy
|
|
47
|
+
console.error(`Proxy error on port ${port}: ${err.message}`);
|
|
48
|
+
resolve(false);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
server.listen(port, bindIp, () => {
|
|
52
|
+
activeProxies.set(port, server);
|
|
53
|
+
resolve(true);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
export const stopProxy = (port) => {
|
|
58
|
+
const server = activeProxies.get(port);
|
|
59
|
+
if (server) {
|
|
60
|
+
server.close();
|
|
61
|
+
activeProxies.delete(port);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
export const stopAllProxies = () => {
|
|
65
|
+
for (const [port, server] of activeProxies) {
|
|
66
|
+
server.close();
|
|
67
|
+
activeProxies.delete(port);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import prompts from "prompts";
|
|
2
|
+
import { createTunnel, deleteAllTunnels, deleteTunnel, getActiveTunnels, validateName, validatePort, } from "./tunnel.js";
|
|
3
|
+
const MENU_CREATE = "create";
|
|
4
|
+
const MENU_DELETE = "delete";
|
|
5
|
+
const MENU_DELETE_ALL = "delete_all";
|
|
6
|
+
const MENU_DISCONNECT = "disconnect";
|
|
7
|
+
const displayActiveTunnels = () => {
|
|
8
|
+
const tunnels = getActiveTunnels();
|
|
9
|
+
console.log("\n--- Active Tunnels ---");
|
|
10
|
+
if (tunnels.length === 0) {
|
|
11
|
+
console.log(" (no active tunnels)");
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
for (const tunnel of tunnels) {
|
|
15
|
+
const nameDisplay = tunnel.name ? ` (${tunnel.name})` : "";
|
|
16
|
+
console.log(` https://${tunnel.hostname}${nameDisplay} → localhost:${tunnel.port}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
console.log("");
|
|
20
|
+
};
|
|
21
|
+
const createTunnelPrompt = async (instance, session) => {
|
|
22
|
+
const { port } = await prompts({
|
|
23
|
+
type: "number",
|
|
24
|
+
name: "port",
|
|
25
|
+
message: "Enter port number",
|
|
26
|
+
validate: (value) => {
|
|
27
|
+
const error = validatePort(value);
|
|
28
|
+
return error ?? true;
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
if (port === undefined) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const { name } = await prompts({
|
|
35
|
+
type: "text",
|
|
36
|
+
name: "name",
|
|
37
|
+
message: "Enter tunnel name (optional, press Enter to skip)",
|
|
38
|
+
validate: (value) => {
|
|
39
|
+
if (!value)
|
|
40
|
+
return true;
|
|
41
|
+
const error = validateName(value);
|
|
42
|
+
return error ?? true;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
if (name === undefined) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
await createTunnel(instance, session, port, name || undefined);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error instanceof Error) {
|
|
53
|
+
console.error(`\nFailed to create tunnel: ${error.message}`);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
console.error("\nFailed to create tunnel");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const deleteTunnelPrompt = async (instance, session) => {
|
|
61
|
+
const tunnels = getActiveTunnels();
|
|
62
|
+
if (tunnels.length === 0) {
|
|
63
|
+
console.log("\nNo tunnels to delete.");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const { tunnelId } = await prompts({
|
|
67
|
+
type: "select",
|
|
68
|
+
name: "tunnelId",
|
|
69
|
+
message: "Select tunnel to delete",
|
|
70
|
+
choices: tunnels.map((tunnel) => {
|
|
71
|
+
const nameDisplay = tunnel.name ? ` (${tunnel.name})` : "";
|
|
72
|
+
return {
|
|
73
|
+
title: `https://${tunnel.hostname}${nameDisplay} → localhost:${tunnel.port}`,
|
|
74
|
+
value: tunnel.id,
|
|
75
|
+
};
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
if (tunnelId === undefined) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const { confirm } = await prompts({
|
|
82
|
+
type: "confirm",
|
|
83
|
+
name: "confirm",
|
|
84
|
+
message: "Are you sure you want to delete this tunnel?",
|
|
85
|
+
initial: false,
|
|
86
|
+
});
|
|
87
|
+
if (confirm !== true) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
await deleteTunnel(instance, session, tunnelId);
|
|
92
|
+
console.log("\nTunnel deleted.");
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error instanceof Error) {
|
|
96
|
+
console.error(`\nFailed to delete tunnel: ${error.message}`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
console.error("\nFailed to delete tunnel");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const deleteAllTunnelsPrompt = async (instance, session) => {
|
|
104
|
+
const tunnels = getActiveTunnels();
|
|
105
|
+
if (tunnels.length === 0) {
|
|
106
|
+
console.log("\nNo tunnels to delete.");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const { confirm } = await prompts({
|
|
110
|
+
type: "confirm",
|
|
111
|
+
name: "confirm",
|
|
112
|
+
message: `Are you sure you want to delete all ${tunnels.length} tunnel(s)?`,
|
|
113
|
+
initial: false,
|
|
114
|
+
});
|
|
115
|
+
if (confirm !== true) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
await deleteAllTunnels(instance, session);
|
|
120
|
+
console.log("\nAll tunnels deleted.");
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (error instanceof Error) {
|
|
124
|
+
console.error(`\nFailed to delete tunnels: ${error.message}`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
console.error("\nFailed to delete tunnels");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
export const tunnelMenu = async (instance, session) => {
|
|
132
|
+
while (true) {
|
|
133
|
+
displayActiveTunnels();
|
|
134
|
+
const tunnels = getActiveTunnels();
|
|
135
|
+
const { action } = await prompts({
|
|
136
|
+
type: "select",
|
|
137
|
+
name: "action",
|
|
138
|
+
message: "Tunnel Management",
|
|
139
|
+
choices: [
|
|
140
|
+
{ title: "Create new tunnel", value: MENU_CREATE },
|
|
141
|
+
{
|
|
142
|
+
title: "Delete a tunnel",
|
|
143
|
+
value: MENU_DELETE,
|
|
144
|
+
disabled: tunnels.length === 0,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
title: "Delete all tunnels",
|
|
148
|
+
value: MENU_DELETE_ALL,
|
|
149
|
+
disabled: tunnels.length === 0,
|
|
150
|
+
},
|
|
151
|
+
{ title: "Disconnect from Nebula", value: MENU_DISCONNECT },
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
if (action === undefined || action === MENU_DISCONNECT) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
switch (action) {
|
|
158
|
+
case MENU_CREATE:
|
|
159
|
+
await createTunnelPrompt(instance, session);
|
|
160
|
+
break;
|
|
161
|
+
case MENU_DELETE:
|
|
162
|
+
await deleteTunnelPrompt(instance, session);
|
|
163
|
+
break;
|
|
164
|
+
case MENU_DELETE_ALL:
|
|
165
|
+
await deleteAllTunnelsPrompt(instance, session);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
package/dist/tunnel.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { nebulaIp } from "./nebula.js";
|
|
2
|
+
import { startProxy, stopAllProxies, stopProxy } from "./proxy.js";
|
|
3
|
+
// In-memory list of active tunnels
|
|
4
|
+
let activeTunnels = [];
|
|
5
|
+
export const getActiveTunnels = () => {
|
|
6
|
+
return [...activeTunnels];
|
|
7
|
+
};
|
|
8
|
+
export const createTunnel = async (instance, session, port, name) => {
|
|
9
|
+
const body = { port };
|
|
10
|
+
if (name) {
|
|
11
|
+
body.name = name;
|
|
12
|
+
}
|
|
13
|
+
const response = await fetch(`${instance.url}/api/tunnels`, {
|
|
14
|
+
method: "POST",
|
|
15
|
+
headers: {
|
|
16
|
+
Authorization: `Bearer ${session}`,
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
},
|
|
19
|
+
body: JSON.stringify(body),
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
const errorText = await response.text();
|
|
23
|
+
throw new Error(`Failed to create tunnel: ${response.status} ${errorText}`);
|
|
24
|
+
}
|
|
25
|
+
const tunnel = (await response.json());
|
|
26
|
+
tunnel.port = port; // Ensure port is set even if API doesn't return it
|
|
27
|
+
activeTunnels.push(tunnel);
|
|
28
|
+
// Try to start IPv6→IPv4 proxy on the Nebula interface
|
|
29
|
+
if (nebulaIp) {
|
|
30
|
+
await startProxy(port, nebulaIp);
|
|
31
|
+
}
|
|
32
|
+
return tunnel;
|
|
33
|
+
};
|
|
34
|
+
export const deleteTunnel = async (instance, session, id) => {
|
|
35
|
+
const response = await fetch(`${instance.url}/api/tunnels/${id}`, {
|
|
36
|
+
method: "DELETE",
|
|
37
|
+
headers: {
|
|
38
|
+
Authorization: `Bearer ${session}`,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
if (!response.ok && response.status !== 404) {
|
|
42
|
+
const errorText = await response.text();
|
|
43
|
+
throw new Error(`Failed to delete tunnel: ${response.status} ${errorText}`);
|
|
44
|
+
}
|
|
45
|
+
const tunnel = activeTunnels.find((t) => t.id === id);
|
|
46
|
+
if (tunnel) {
|
|
47
|
+
stopProxy(tunnel.port);
|
|
48
|
+
}
|
|
49
|
+
activeTunnels = activeTunnels.filter((t) => t.id !== id);
|
|
50
|
+
};
|
|
51
|
+
export const deleteAllTunnels = async (instance, session) => {
|
|
52
|
+
const response = await fetch(`${instance.url}/api/tunnels`, {
|
|
53
|
+
method: "DELETE",
|
|
54
|
+
headers: {
|
|
55
|
+
Authorization: `Bearer ${session}`,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok && response.status !== 404) {
|
|
59
|
+
const errorText = await response.text();
|
|
60
|
+
throw new Error(`Failed to delete all tunnels: ${response.status} ${errorText}`);
|
|
61
|
+
}
|
|
62
|
+
stopAllProxies();
|
|
63
|
+
activeTunnels = [];
|
|
64
|
+
};
|
|
65
|
+
export const validatePort = (port) => {
|
|
66
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
67
|
+
return "Port must be an integer between 1 and 65535";
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
};
|
|
71
|
+
export const validateName = (name) => {
|
|
72
|
+
if (name && !/^[a-z0-9-]+$/.test(name)) {
|
|
73
|
+
return "Name must only contain lowercase letters, numbers, and hyphens";
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "onedeploy-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "CLI tool for connecting to OneDeploy via Nebula",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"onedeploy",
|
|
@@ -32,9 +32,10 @@
|
|
|
32
32
|
"fix": "biome check src/* --fix"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@biomejs/biome": "^2.3.
|
|
36
|
-
"@types/node": "^25.0
|
|
35
|
+
"@biomejs/biome": "^2.3.13",
|
|
36
|
+
"@types/node": "^25.2.0",
|
|
37
37
|
"@types/prompts": "^2.4.9",
|
|
38
|
+
"@typescript/native-preview": "^7.0.0-dev.20260203.1",
|
|
38
39
|
"typescript": "^5.9.3"
|
|
39
40
|
},
|
|
40
41
|
"dependencies": {
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,49 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
3
|
import prompts from "prompts";
|
|
4
|
+
import type { Instance } from "./config.js";
|
|
3
5
|
import { loadConfig, saveConfig } from "./config.js";
|
|
4
6
|
import {
|
|
5
7
|
cleanupNebula,
|
|
6
|
-
|
|
8
|
+
getSession,
|
|
7
9
|
installNebula,
|
|
8
10
|
isNebulaRunning,
|
|
11
|
+
startNebula,
|
|
12
|
+
stopNebula,
|
|
9
13
|
} from "./nebula.js";
|
|
14
|
+
import { deleteAllTunnels } from "./tunnel.js";
|
|
15
|
+
import { tunnelMenu } from "./tunnel-menu.js";
|
|
16
|
+
|
|
17
|
+
// Track active connection for cleanup on signals
|
|
18
|
+
let activeConnection: {
|
|
19
|
+
instance: Instance;
|
|
20
|
+
session: string;
|
|
21
|
+
nebulaProcess: ChildProcess;
|
|
22
|
+
stopRefresh: () => void;
|
|
23
|
+
} | null = null;
|
|
24
|
+
|
|
25
|
+
const cleanup = async () => {
|
|
26
|
+
if (activeConnection) {
|
|
27
|
+
console.log("\nCleaning up...");
|
|
28
|
+
try {
|
|
29
|
+
await deleteAllTunnels(activeConnection.instance, activeConnection.session);
|
|
30
|
+
} catch {
|
|
31
|
+
// ignore if already disconnected
|
|
32
|
+
}
|
|
33
|
+
activeConnection.stopRefresh();
|
|
34
|
+
stopNebula(activeConnection.nebulaProcess);
|
|
35
|
+
activeConnection = null;
|
|
36
|
+
}
|
|
37
|
+
await cleanupNebula();
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const handleSignal = () => {
|
|
41
|
+
if (!isNebulaRunning) {
|
|
42
|
+
process.exit();
|
|
43
|
+
}
|
|
44
|
+
// Perform cleanup and exit
|
|
45
|
+
cleanup().finally(() => process.exit());
|
|
46
|
+
};
|
|
10
47
|
|
|
11
48
|
if (process.argv.includes("--version")) {
|
|
12
49
|
console.log("onedeploy-cli version 0.1.4");
|
|
@@ -127,9 +164,33 @@ const mainMenu = async (): Promise<boolean> => {
|
|
|
127
164
|
urlToConnect = selection;
|
|
128
165
|
}
|
|
129
166
|
|
|
130
|
-
// Connect first
|
|
131
167
|
const instance = { url: urlToConnect, apiKey: "" };
|
|
132
|
-
|
|
168
|
+
|
|
169
|
+
// 1. Authenticate
|
|
170
|
+
const session = await getSession(instance);
|
|
171
|
+
|
|
172
|
+
// 2. Start Nebula (non-blocking)
|
|
173
|
+
const { nebulaProcess, stopRefresh } = await startNebula(instance, session);
|
|
174
|
+
|
|
175
|
+
// Track connection for signal handlers
|
|
176
|
+
activeConnection = { instance, session, nebulaProcess, stopRefresh };
|
|
177
|
+
|
|
178
|
+
// 3. Enter tunnel management loop
|
|
179
|
+
try {
|
|
180
|
+
await tunnelMenu(instance, session);
|
|
181
|
+
} finally {
|
|
182
|
+
// 4. Cleanup on exit
|
|
183
|
+
console.log("Cleaning up tunnels...");
|
|
184
|
+
try {
|
|
185
|
+
await deleteAllTunnels(instance, session);
|
|
186
|
+
} catch {
|
|
187
|
+
// ignore if already disconnected
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
stopRefresh();
|
|
191
|
+
stopNebula(nebulaProcess);
|
|
192
|
+
activeConnection = null;
|
|
193
|
+
}
|
|
133
194
|
|
|
134
195
|
// Only save if connection was successful (didn't throw)
|
|
135
196
|
await addInstance(urlToConnect);
|
|
@@ -141,8 +202,12 @@ process.on("SIGINT", () => {
|
|
|
141
202
|
if (!isNebulaRunning) {
|
|
142
203
|
process.exit();
|
|
143
204
|
}
|
|
144
|
-
//
|
|
205
|
+
// When Nebula is running, let prompts handle SIGINT (returns undefined)
|
|
206
|
+
// which triggers the finally block in mainMenu
|
|
145
207
|
});
|
|
208
|
+
process.on("SIGTERM", handleSignal);
|
|
209
|
+
process.on("SIGHUP", handleSignal);
|
|
210
|
+
|
|
146
211
|
await installNebula();
|
|
147
212
|
try {
|
|
148
213
|
while (true) {
|
|
@@ -152,5 +217,5 @@ try {
|
|
|
152
217
|
}
|
|
153
218
|
}
|
|
154
219
|
} finally {
|
|
155
|
-
await
|
|
220
|
+
await cleanup();
|
|
156
221
|
}
|