chaitunnel 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/dist/bin/chaitunnel.d.mts +1 -0
- package/dist/bin/chaitunnel.mjs +239 -0
- package/dist/bin/chaitunnel.mjs.map +1 -0
- package/dist/frp-wrapper-DgAvulMB.mjs +355 -0
- package/dist/frp-wrapper-DgAvulMB.mjs.map +1 -0
- package/dist/index.d.mts +222 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +3 -0
- package/package.json +52 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { a as setConfigPath, n as ApiClient, r as Config, t as FrpWrapper } from "../frp-wrapper-DgAvulMB.mjs";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
|
|
6
|
+
//#region src/commands/expose.ts
|
|
7
|
+
/**
|
|
8
|
+
* Expose command
|
|
9
|
+
*
|
|
10
|
+
* Exposes a local port to the internet via frp tunnel.
|
|
11
|
+
*/
|
|
12
|
+
async function exposeCommand(port) {
|
|
13
|
+
const config = new Config();
|
|
14
|
+
const localPort = parseInt(port, 10);
|
|
15
|
+
if (isNaN(localPort) || localPort < 1 || localPort > 65535) {
|
|
16
|
+
console.log(chalk.red(`\nInvalid port: ${port}`));
|
|
17
|
+
console.log(chalk.gray(" Port must be a number between 1 and 65535\n"));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
if (!config.isAuthenticated()) {
|
|
21
|
+
console.log(chalk.red("\nNot authenticated."));
|
|
22
|
+
console.log(chalk.gray(" Run 'chaitunnel authtoken <token>' first."));
|
|
23
|
+
console.log(chalk.gray(" Get your token from: https://chaiterm.com/dashboard\n"));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
const authToken = config.getAuthToken();
|
|
27
|
+
const apiUrl = config.getApiUrl();
|
|
28
|
+
const serverAddr = config.getFrpServerAddr();
|
|
29
|
+
const serverPort = config.getFrpServerPort();
|
|
30
|
+
const tlsEnabled = config.getTlsEnabled();
|
|
31
|
+
const publicUrlTemplate = config.getPublicUrlTemplate();
|
|
32
|
+
console.log(chalk.cyan(`\nExposing port ${localPort}...\n`));
|
|
33
|
+
const api = new ApiClient(apiUrl, authToken);
|
|
34
|
+
let tunnel;
|
|
35
|
+
try {
|
|
36
|
+
tunnel = await api.createTunnel(localPort);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
const err = error;
|
|
39
|
+
console.log(chalk.red(`\nFailed to create tunnel: ${err.message}`));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
console.log(` Subdomain: ${chalk.green(tunnel.subdomain)}`);
|
|
43
|
+
console.log(` Local port: ${chalk.green(localPort)}`);
|
|
44
|
+
console.log(` Server: ${chalk.gray(`${serverAddr}:${serverPort}`)}`);
|
|
45
|
+
console.log("");
|
|
46
|
+
const frp = new FrpWrapper({
|
|
47
|
+
serverAddr,
|
|
48
|
+
serverPort,
|
|
49
|
+
authToken,
|
|
50
|
+
tunnelId: tunnel.id,
|
|
51
|
+
tlsEnabled,
|
|
52
|
+
proxy: {
|
|
53
|
+
tunnelId: tunnel.id,
|
|
54
|
+
subdomain: tunnel.subdomain,
|
|
55
|
+
localPort
|
|
56
|
+
},
|
|
57
|
+
onConnect: () => {
|
|
58
|
+
console.log(chalk.green("Tunnel established"));
|
|
59
|
+
const publicUrl = publicUrlTemplate.replace("{subdomain}", tunnel.subdomain);
|
|
60
|
+
console.log(chalk.cyan(`\nYour app is available at: ${publicUrl}\n`));
|
|
61
|
+
console.log(chalk.gray("Press Ctrl+C to stop\n"));
|
|
62
|
+
},
|
|
63
|
+
onDisconnect: () => {
|
|
64
|
+
console.log(chalk.yellow("\nTunnel disconnected"));
|
|
65
|
+
},
|
|
66
|
+
onError: (error) => {
|
|
67
|
+
console.log(chalk.red(`\nError: ${error.message}`));
|
|
68
|
+
},
|
|
69
|
+
onLog: (message) => {
|
|
70
|
+
if (process.env.DEBUG) console.log(chalk.gray(`[frp] ${message.trim()}`));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
const cleanup = async () => {
|
|
74
|
+
console.log(chalk.gray("\n\nShutting down tunnel..."));
|
|
75
|
+
frp.stop();
|
|
76
|
+
try {
|
|
77
|
+
await api.deleteTunnel(tunnel.id);
|
|
78
|
+
console.log(chalk.gray("Tunnel removed."));
|
|
79
|
+
} catch {}
|
|
80
|
+
process.exit(0);
|
|
81
|
+
};
|
|
82
|
+
process.on("SIGINT", cleanup);
|
|
83
|
+
process.on("SIGTERM", cleanup);
|
|
84
|
+
try {
|
|
85
|
+
await frp.start();
|
|
86
|
+
await new Promise(() => {});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
const err = error;
|
|
89
|
+
console.log(chalk.red(`\nFailed to establish tunnel: ${err.message}`));
|
|
90
|
+
console.log(chalk.gray("\nTroubleshooting:"));
|
|
91
|
+
console.log(" 1. Ensure frpc is installed (brew install frpc on macOS)");
|
|
92
|
+
console.log(" 2. Check that your local port is accessible");
|
|
93
|
+
console.log(" 3. Verify your network allows outbound connections\n");
|
|
94
|
+
try {
|
|
95
|
+
await api.deleteTunnel(tunnel.id);
|
|
96
|
+
} catch {}
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/commands/list.ts
|
|
103
|
+
/**
|
|
104
|
+
* List command
|
|
105
|
+
*
|
|
106
|
+
* Lists all tunnels for the authenticated user.
|
|
107
|
+
*/
|
|
108
|
+
async function listCommand() {
|
|
109
|
+
const config = new Config();
|
|
110
|
+
if (!config.isAuthenticated()) {
|
|
111
|
+
console.log(chalk.red("\nNot authenticated."));
|
|
112
|
+
console.log(chalk.gray(" Run 'chaitunnel authtoken <token>' first."));
|
|
113
|
+
console.log(chalk.gray(" Get your token from: https://chaiterm.com/dashboard\n"));
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
const authToken = config.getAuthToken();
|
|
117
|
+
const api = new ApiClient(config.getApiUrl(), authToken);
|
|
118
|
+
try {
|
|
119
|
+
const tunnels = await api.listTunnels();
|
|
120
|
+
if (tunnels.length === 0) {
|
|
121
|
+
console.log(chalk.yellow("\nNo active tunnels."));
|
|
122
|
+
console.log(chalk.gray(" Run 'chaitunnel <port>' to expose a port.\n"));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
console.log(chalk.cyan("\nYour tunnels:\n"));
|
|
126
|
+
console.log(chalk.gray(" " + "SUBDOMAIN".padEnd(15) + "PORT".padEnd(8) + "STATUS".padEnd(12) + "URL"));
|
|
127
|
+
console.log(chalk.gray(" " + "-".repeat(70)));
|
|
128
|
+
for (const tunnel of tunnels) {
|
|
129
|
+
const statusColor = tunnel.status === "active" ? chalk.green : chalk.yellow;
|
|
130
|
+
const url = `https://${tunnel.subdomain}.chaiterm.com`;
|
|
131
|
+
console.log(" " + chalk.white(tunnel.subdomain.padEnd(15)) + chalk.white(tunnel.localPort.toString().padEnd(8)) + statusColor(tunnel.status.padEnd(12)) + chalk.blue(url));
|
|
132
|
+
}
|
|
133
|
+
console.log("");
|
|
134
|
+
} catch (error) {
|
|
135
|
+
const err = error;
|
|
136
|
+
console.log(chalk.red(`\nFailed to list tunnels: ${err.message}\n`));
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/commands/authtoken.ts
|
|
143
|
+
/**
|
|
144
|
+
* Authtoken command
|
|
145
|
+
*
|
|
146
|
+
* Saves the authentication token to config.
|
|
147
|
+
* Usage: chaitunnel authtoken <token>
|
|
148
|
+
*/
|
|
149
|
+
async function authtokenCommand(token) {
|
|
150
|
+
if (!token || token.trim() === "") {
|
|
151
|
+
console.log(chalk.red("\nError: Token is required"));
|
|
152
|
+
console.log(chalk.gray("Usage: chaitunnel authtoken <token>\n"));
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
const config = new Config();
|
|
156
|
+
config.setAuthToken(token.trim());
|
|
157
|
+
console.log(chalk.green("\nAuthtoken saved to config"));
|
|
158
|
+
console.log(chalk.gray(`Config: ${config.getPath()}\n`));
|
|
159
|
+
console.log(chalk.cyan("You can now run:"));
|
|
160
|
+
console.log(chalk.white(" chaitunnel <port>\n"));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/commands/logout.ts
|
|
165
|
+
/**
|
|
166
|
+
* Logout command
|
|
167
|
+
*
|
|
168
|
+
* Clears authentication credentials.
|
|
169
|
+
*/
|
|
170
|
+
async function logoutCommand() {
|
|
171
|
+
const config = new Config();
|
|
172
|
+
if (!config.isAuthenticated()) {
|
|
173
|
+
console.log(chalk.yellow("\nNot currently authenticated.\n"));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
config.clear();
|
|
177
|
+
console.log(chalk.green(`\nAuthtoken cleared.`));
|
|
178
|
+
console.log(chalk.gray(` Config: ${config.getPath()}\n`));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/commands/status.ts
|
|
183
|
+
/**
|
|
184
|
+
* Status command
|
|
185
|
+
*
|
|
186
|
+
* Shows current authentication and configuration status.
|
|
187
|
+
*/
|
|
188
|
+
async function statusCommand() {
|
|
189
|
+
const config = new Config();
|
|
190
|
+
console.log(chalk.cyan("\nchaitunnel status\n"));
|
|
191
|
+
if (config.isAuthenticated()) {
|
|
192
|
+
const token = config.getAuthToken();
|
|
193
|
+
const maskedToken = token.slice(0, 6) + "..." + token.slice(-4);
|
|
194
|
+
console.log(` Auth: ${chalk.green("authenticated")}`);
|
|
195
|
+
console.log(` Token: ${chalk.gray(maskedToken)}`);
|
|
196
|
+
} else {
|
|
197
|
+
console.log(` Auth: ${chalk.yellow("not authenticated")}`);
|
|
198
|
+
console.log(chalk.gray(" Run 'chaitunnel authtoken <token>' to authenticate."));
|
|
199
|
+
console.log(chalk.gray(" Get your token from: https://chaiterm.com/dashboard"));
|
|
200
|
+
}
|
|
201
|
+
console.log("");
|
|
202
|
+
console.log(` API: ${chalk.gray(config.getApiUrl())}`);
|
|
203
|
+
console.log(` frp: ${chalk.gray(`${config.getFrpServerAddr()}:${config.getFrpServerPort()}`)}`);
|
|
204
|
+
console.log(` Config: ${chalk.gray(config.getPath())}`);
|
|
205
|
+
if (config.isCustomConfig()) console.log(chalk.yellow(" (using custom config path)"));
|
|
206
|
+
console.log("");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/bin/chaitunnel.ts
|
|
211
|
+
/**
|
|
212
|
+
* chaitunnel CLI
|
|
213
|
+
*
|
|
214
|
+
* Expose local ports to the internet via frp tunnels.
|
|
215
|
+
*
|
|
216
|
+
* Setup:
|
|
217
|
+
* 1. Get your authtoken from https://chaiterm.com/dashboard
|
|
218
|
+
* 2. Run: chaitunnel authtoken <token>
|
|
219
|
+
* 3. Run: chaitunnel <port>
|
|
220
|
+
*/
|
|
221
|
+
const program = new Command();
|
|
222
|
+
program.name("chaitunnel").description("Expose local ports to the internet").version("0.0.1").option("-c, --config <path>", "Use custom config file").enablePositionalOptions();
|
|
223
|
+
program.hook("preAction", (thisCommand) => {
|
|
224
|
+
const opts = thisCommand.opts();
|
|
225
|
+
if (opts.config) setConfigPath(opts.config);
|
|
226
|
+
});
|
|
227
|
+
program.argument("[port]", "Local port to expose").action(async (port) => {
|
|
228
|
+
if (port) await exposeCommand(port);
|
|
229
|
+
else program.help();
|
|
230
|
+
});
|
|
231
|
+
program.command("authtoken").description("Save your authtoken (get it from chaiterm.com/dashboard)").argument("<token>", "Your authtoken from the dashboard").allowExcessArguments(false).passThroughOptions().action(authtokenCommand);
|
|
232
|
+
program.command("list").description("List your tunnels").action(listCommand);
|
|
233
|
+
program.command("logout").description("Clear authentication").action(logoutCommand);
|
|
234
|
+
program.command("status").description("Show authentication and config status").action(statusCommand);
|
|
235
|
+
program.parse();
|
|
236
|
+
|
|
237
|
+
//#endregion
|
|
238
|
+
export { };
|
|
239
|
+
//# sourceMappingURL=chaitunnel.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chaitunnel.mjs","names":[],"sources":["../../src/commands/expose.ts","../../src/commands/list.ts","../../src/commands/authtoken.ts","../../src/commands/logout.ts","../../src/commands/status.ts","../../src/bin/chaitunnel.ts"],"sourcesContent":["/**\n * Expose command\n *\n * Exposes a local port to the internet via frp tunnel.\n */\n\nimport chalk from \"chalk\";\nimport { Config } from \"../lib/config.js\";\nimport { ApiClient } from \"../lib/api-client.js\";\nimport { FrpWrapper } from \"../lib/frp-wrapper.js\";\n\nexport async function exposeCommand(port: string): Promise<void> {\n const config = new Config();\n const localPort = parseInt(port, 10);\n\n if (isNaN(localPort) || localPort < 1 || localPort > 65535) {\n console.log(chalk.red(`\\nInvalid port: ${port}`));\n console.log(chalk.gray(\" Port must be a number between 1 and 65535\\n\"));\n process.exit(1);\n }\n\n if (!config.isAuthenticated()) {\n console.log(chalk.red(\"\\nNot authenticated.\"));\n console.log(chalk.gray(\" Run 'chaitunnel authtoken <token>' first.\"));\n console.log(chalk.gray(\" Get your token from: https://chaiterm.com/dashboard\\n\"));\n process.exit(1);\n }\n\n const authToken = config.getAuthToken()!;\n const apiUrl = config.getApiUrl();\n const serverAddr = config.getFrpServerAddr();\n const serverPort = config.getFrpServerPort();\n const tlsEnabled = config.getTlsEnabled();\n const publicUrlTemplate = config.getPublicUrlTemplate();\n\n console.log(chalk.cyan(`\\nExposing port ${localPort}...\\n`));\n\n // Create tunnel via API\n const api = new ApiClient(apiUrl, authToken);\n let tunnel;\n\n try {\n tunnel = await api.createTunnel(localPort);\n } catch (error) {\n const err = error as Error;\n console.log(chalk.red(`\\nFailed to create tunnel: ${err.message}`));\n process.exit(1);\n }\n\n console.log(` Subdomain: ${chalk.green(tunnel.subdomain)}`);\n console.log(` Local port: ${chalk.green(localPort)}`);\n console.log(` Server: ${chalk.gray(`${serverAddr}:${serverPort}`)}`);\n console.log(\"\");\n\n const frp = new FrpWrapper({\n serverAddr,\n serverPort,\n authToken,\n tunnelId: tunnel.id,\n tlsEnabled,\n proxy: {\n tunnelId: tunnel.id,\n subdomain: tunnel.subdomain,\n localPort,\n },\n onConnect: () => {\n console.log(chalk.green(\"Tunnel established\"));\n const publicUrl = publicUrlTemplate.replace(\"{subdomain}\", tunnel.subdomain);\n console.log(\n chalk.cyan(`\\nYour app is available at: ${publicUrl}\\n`)\n );\n console.log(chalk.gray(\"Press Ctrl+C to stop\\n\"));\n },\n onDisconnect: () => {\n console.log(chalk.yellow(\"\\nTunnel disconnected\"));\n },\n onError: (error) => {\n console.log(chalk.red(`\\nError: ${error.message}`));\n },\n onLog: (message) => {\n // Only show important logs in verbose mode\n if (process.env.DEBUG) {\n console.log(chalk.gray(`[frp] ${message.trim()}`));\n }\n },\n });\n\n // Cleanup function\n const cleanup = async () => {\n console.log(chalk.gray(\"\\n\\nShutting down tunnel...\"));\n frp.stop();\n try {\n await api.deleteTunnel(tunnel.id);\n console.log(chalk.gray(\"Tunnel removed.\"));\n } catch {\n // Ignore cleanup errors\n }\n process.exit(0);\n };\n\n // Handle Ctrl+C\n process.on(\"SIGINT\", cleanup);\n process.on(\"SIGTERM\", cleanup);\n\n try {\n await frp.start();\n\n // Keep process running\n await new Promise(() => {\n // Never resolves - keeps the process alive\n });\n } catch (error) {\n const err = error as Error;\n console.log(chalk.red(`\\nFailed to establish tunnel: ${err.message}`));\n console.log(chalk.gray(\"\\nTroubleshooting:\"));\n console.log(\" 1. Ensure frpc is installed (brew install frpc on macOS)\");\n console.log(\" 2. Check that your local port is accessible\");\n console.log(\" 3. Verify your network allows outbound connections\\n\");\n\n // Cleanup on failure\n try {\n await api.deleteTunnel(tunnel.id);\n } catch {\n // Ignore cleanup errors\n }\n process.exit(1);\n }\n}\n","/**\n * List command\n *\n * Lists all tunnels for the authenticated user.\n */\n\nimport chalk from \"chalk\";\nimport { Config } from \"../lib/config.js\";\nimport { ApiClient } from \"../lib/api-client.js\";\n\nexport async function listCommand(): Promise<void> {\n const config = new Config();\n\n if (!config.isAuthenticated()) {\n console.log(chalk.red(\"\\nNot authenticated.\"));\n console.log(chalk.gray(\" Run 'chaitunnel authtoken <token>' first.\"));\n console.log(chalk.gray(\" Get your token from: https://chaiterm.com/dashboard\\n\"));\n process.exit(1);\n }\n\n const authToken = config.getAuthToken()!;\n const apiUrl = config.getApiUrl();\n\n const api = new ApiClient(apiUrl, authToken);\n\n try {\n const tunnels = await api.listTunnels();\n\n if (tunnels.length === 0) {\n console.log(chalk.yellow(\"\\nNo active tunnels.\"));\n console.log(chalk.gray(\" Run 'chaitunnel <port>' to expose a port.\\n\"));\n return;\n }\n\n console.log(chalk.cyan(\"\\nYour tunnels:\\n\"));\n console.log(\n chalk.gray(\n \" \" +\n \"SUBDOMAIN\".padEnd(15) +\n \"PORT\".padEnd(8) +\n \"STATUS\".padEnd(12) +\n \"URL\"\n )\n );\n console.log(chalk.gray(\" \" + \"-\".repeat(70)));\n\n for (const tunnel of tunnels) {\n const statusColor =\n tunnel.status === \"active\" ? chalk.green : chalk.yellow;\n const url = `https://${tunnel.subdomain}.chaiterm.com`;\n\n console.log(\n \" \" +\n chalk.white(tunnel.subdomain.padEnd(15)) +\n chalk.white(tunnel.localPort.toString().padEnd(8)) +\n statusColor(tunnel.status.padEnd(12)) +\n chalk.blue(url)\n );\n }\n\n console.log(\"\");\n } catch (error) {\n const err = error as Error;\n console.log(chalk.red(`\\nFailed to list tunnels: ${err.message}\\n`));\n process.exit(1);\n }\n}\n","/**\n * Authtoken command\n *\n * Saves the authentication token to config.\n * Usage: chaitunnel authtoken <token>\n */\n\nimport chalk from \"chalk\";\nimport { Config } from \"../lib/config.js\";\n\nexport async function authtokenCommand(token: string): Promise<void> {\n if (!token || token.trim() === \"\") {\n console.log(chalk.red(\"\\nError: Token is required\"));\n console.log(chalk.gray(\"Usage: chaitunnel authtoken <token>\\n\"));\n process.exit(1);\n }\n\n const config = new Config();\n config.setAuthToken(token.trim());\n\n console.log(chalk.green(\"\\nAuthtoken saved to config\"));\n console.log(chalk.gray(`Config: ${config.getPath()}\\n`));\n console.log(chalk.cyan(\"You can now run:\"));\n console.log(chalk.white(\" chaitunnel <port>\\n\"));\n}\n","/**\n * Logout command\n *\n * Clears authentication credentials.\n */\n\nimport chalk from \"chalk\";\nimport { Config } from \"../lib/config.js\";\n\nexport async function logoutCommand(): Promise<void> {\n const config = new Config();\n\n if (!config.isAuthenticated()) {\n console.log(chalk.yellow(\"\\nNot currently authenticated.\\n\"));\n return;\n }\n\n config.clear();\n\n console.log(chalk.green(`\\nAuthtoken cleared.`));\n console.log(chalk.gray(` Config: ${config.getPath()}\\n`));\n}\n","/**\n * Status command\n *\n * Shows current authentication and configuration status.\n */\n\nimport chalk from \"chalk\";\nimport { Config } from \"../lib/config.js\";\n\nexport async function statusCommand(): Promise<void> {\n const config = new Config();\n\n console.log(chalk.cyan(\"\\nchaitunnel status\\n\"));\n\n // Authentication\n if (config.isAuthenticated()) {\n const token = config.getAuthToken()!;\n const maskedToken = token.slice(0, 6) + \"...\" + token.slice(-4);\n console.log(` Auth: ${chalk.green(\"authenticated\")}`);\n console.log(` Token: ${chalk.gray(maskedToken)}`);\n } else {\n console.log(` Auth: ${chalk.yellow(\"not authenticated\")}`);\n console.log(chalk.gray(\" Run 'chaitunnel authtoken <token>' to authenticate.\"));\n console.log(chalk.gray(\" Get your token from: https://chaiterm.com/dashboard\"));\n }\n\n console.log(\"\");\n\n // Configuration\n console.log(` API: ${chalk.gray(config.getApiUrl())}`);\n console.log(\n ` frp: ${chalk.gray(`${config.getFrpServerAddr()}:${config.getFrpServerPort()}`)}`\n );\n console.log(` Config: ${chalk.gray(config.getPath())}`);\n\n if (config.isCustomConfig()) {\n console.log(chalk.yellow(\" (using custom config path)\"));\n }\n\n console.log(\"\");\n}\n","#!/usr/bin/env node\n/**\n * chaitunnel CLI\n *\n * Expose local ports to the internet via frp tunnels.\n *\n * Setup:\n * 1. Get your authtoken from https://chaiterm.com/dashboard\n * 2. Run: chaitunnel authtoken <token>\n * 3. Run: chaitunnel <port>\n */\n\nimport { Command } from \"commander\";\nimport { exposeCommand } from \"../commands/expose.js\";\nimport { listCommand } from \"../commands/list.js\";\nimport { authtokenCommand } from \"../commands/authtoken.js\";\nimport { logoutCommand } from \"../commands/logout.js\";\nimport { statusCommand } from \"../commands/status.js\";\nimport { setConfigPath } from \"../lib/config.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"chaitunnel\")\n .description(\"Expose local ports to the internet\")\n .version(\"0.0.1\")\n .option(\"-c, --config <path>\", \"Use custom config file\")\n .enablePositionalOptions();\n\n// Parse global options first\nprogram.hook(\"preAction\", (thisCommand) => {\n const opts = thisCommand.opts();\n if (opts.config) {\n setConfigPath(opts.config);\n }\n});\n\n// Main command: chaitunnel <port>\nprogram\n .argument(\"[port]\", \"Local port to expose\")\n .action(async (port: string | undefined) => {\n if (port) {\n await exposeCommand(port);\n } else {\n program.help();\n }\n });\n\n// Subcommands\nprogram\n .command(\"authtoken\")\n .description(\"Save your authtoken (get it from chaiterm.com/dashboard)\")\n .argument(\"<token>\", \"Your authtoken from the dashboard\")\n .allowExcessArguments(false)\n .passThroughOptions()\n .action(authtokenCommand);\n\nprogram\n .command(\"list\")\n .description(\"List your tunnels\")\n .action(listCommand);\n\nprogram\n .command(\"logout\")\n .description(\"Clear authentication\")\n .action(logoutCommand);\n\nprogram\n .command(\"status\")\n .description(\"Show authentication and config status\")\n .action(statusCommand);\n\nprogram.parse();\n"],"mappings":";;;;;;;;;;;AAWA,eAAsB,cAAc,MAA6B;CAC/D,MAAM,SAAS,IAAI,QAAQ;CAC3B,MAAM,YAAY,SAAS,MAAM,GAAG;AAEpC,KAAI,MAAM,UAAU,IAAI,YAAY,KAAK,YAAY,OAAO;AAC1D,UAAQ,IAAI,MAAM,IAAI,mBAAmB,OAAO,CAAC;AACjD,UAAQ,IAAI,MAAM,KAAK,iDAAiD,CAAC;AACzE,UAAQ,KAAK,EAAE;;AAGjB,KAAI,CAAC,OAAO,iBAAiB,EAAE;AAC7B,UAAQ,IAAI,MAAM,IAAI,uBAAuB,CAAC;AAC9C,UAAQ,IAAI,MAAM,KAAK,+CAA+C,CAAC;AACvE,UAAQ,IAAI,MAAM,KAAK,2DAA2D,CAAC;AACnF,UAAQ,KAAK,EAAE;;CAGjB,MAAM,YAAY,OAAO,cAAc;CACvC,MAAM,SAAS,OAAO,WAAW;CACjC,MAAM,aAAa,OAAO,kBAAkB;CAC5C,MAAM,aAAa,OAAO,kBAAkB;CAC5C,MAAM,aAAa,OAAO,eAAe;CACzC,MAAM,oBAAoB,OAAO,sBAAsB;AAEvD,SAAQ,IAAI,MAAM,KAAK,mBAAmB,UAAU,OAAO,CAAC;CAG5D,MAAM,MAAM,IAAI,UAAU,QAAQ,UAAU;CAC5C,IAAI;AAEJ,KAAI;AACF,WAAS,MAAM,IAAI,aAAa,UAAU;UACnC,OAAO;EACd,MAAM,MAAM;AACZ,UAAQ,IAAI,MAAM,IAAI,8BAA8B,IAAI,UAAU,CAAC;AACnE,UAAQ,KAAK,EAAE;;AAGjB,SAAQ,IAAI,iBAAiB,MAAM,MAAM,OAAO,UAAU,GAAG;AAC7D,SAAQ,IAAI,kBAAkB,MAAM,MAAM,UAAU,GAAG;AACvD,SAAQ,IAAI,cAAc,MAAM,KAAK,GAAG,WAAW,GAAG,aAAa,GAAG;AACtE,SAAQ,IAAI,GAAG;CAEf,MAAM,MAAM,IAAI,WAAW;EACzB;EACA;EACA;EACA,UAAU,OAAO;EACjB;EACA,OAAO;GACL,UAAU,OAAO;GACjB,WAAW,OAAO;GAClB;GACD;EACD,iBAAiB;AACf,WAAQ,IAAI,MAAM,MAAM,qBAAqB,CAAC;GAC9C,MAAM,YAAY,kBAAkB,QAAQ,eAAe,OAAO,UAAU;AAC5E,WAAQ,IACN,MAAM,KAAK,+BAA+B,UAAU,IAAI,CACzD;AACD,WAAQ,IAAI,MAAM,KAAK,yBAAyB,CAAC;;EAEnD,oBAAoB;AAClB,WAAQ,IAAI,MAAM,OAAO,wBAAwB,CAAC;;EAEpD,UAAU,UAAU;AAClB,WAAQ,IAAI,MAAM,IAAI,YAAY,MAAM,UAAU,CAAC;;EAErD,QAAQ,YAAY;AAElB,OAAI,QAAQ,IAAI,MACd,SAAQ,IAAI,MAAM,KAAK,SAAS,QAAQ,MAAM,GAAG,CAAC;;EAGvD,CAAC;CAGF,MAAM,UAAU,YAAY;AAC1B,UAAQ,IAAI,MAAM,KAAK,8BAA8B,CAAC;AACtD,MAAI,MAAM;AACV,MAAI;AACF,SAAM,IAAI,aAAa,OAAO,GAAG;AACjC,WAAQ,IAAI,MAAM,KAAK,kBAAkB,CAAC;UACpC;AAGR,UAAQ,KAAK,EAAE;;AAIjB,SAAQ,GAAG,UAAU,QAAQ;AAC7B,SAAQ,GAAG,WAAW,QAAQ;AAE9B,KAAI;AACF,QAAM,IAAI,OAAO;AAGjB,QAAM,IAAI,cAAc,GAEtB;UACK,OAAO;EACd,MAAM,MAAM;AACZ,UAAQ,IAAI,MAAM,IAAI,iCAAiC,IAAI,UAAU,CAAC;AACtE,UAAQ,IAAI,MAAM,KAAK,qBAAqB,CAAC;AAC7C,UAAQ,IAAI,8DAA8D;AAC1E,UAAQ,IAAI,iDAAiD;AAC7D,UAAQ,IAAI,0DAA0D;AAGtE,MAAI;AACF,SAAM,IAAI,aAAa,OAAO,GAAG;UAC3B;AAGR,UAAQ,KAAK,EAAE;;;;;;;;;;;ACnHnB,eAAsB,cAA6B;CACjD,MAAM,SAAS,IAAI,QAAQ;AAE3B,KAAI,CAAC,OAAO,iBAAiB,EAAE;AAC7B,UAAQ,IAAI,MAAM,IAAI,uBAAuB,CAAC;AAC9C,UAAQ,IAAI,MAAM,KAAK,+CAA+C,CAAC;AACvE,UAAQ,IAAI,MAAM,KAAK,2DAA2D,CAAC;AACnF,UAAQ,KAAK,EAAE;;CAGjB,MAAM,YAAY,OAAO,cAAc;CAGvC,MAAM,MAAM,IAAI,UAFD,OAAO,WAAW,EAEC,UAAU;AAE5C,KAAI;EACF,MAAM,UAAU,MAAM,IAAI,aAAa;AAEvC,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAQ,IAAI,MAAM,OAAO,uBAAuB,CAAC;AACjD,WAAQ,IAAI,MAAM,KAAK,iDAAiD,CAAC;AACzE;;AAGF,UAAQ,IAAI,MAAM,KAAK,oBAAoB,CAAC;AAC5C,UAAQ,IACN,MAAM,KACJ,OACE,YAAY,OAAO,GAAG,GACtB,OAAO,OAAO,EAAE,GAChB,SAAS,OAAO,GAAG,GACnB,MACH,CACF;AACD,UAAQ,IAAI,MAAM,KAAK,OAAO,IAAI,OAAO,GAAG,CAAC,CAAC;AAE9C,OAAK,MAAM,UAAU,SAAS;GAC5B,MAAM,cACJ,OAAO,WAAW,WAAW,MAAM,QAAQ,MAAM;GACnD,MAAM,MAAM,WAAW,OAAO,UAAU;AAExC,WAAQ,IACN,OACE,MAAM,MAAM,OAAO,UAAU,OAAO,GAAG,CAAC,GACxC,MAAM,MAAM,OAAO,UAAU,UAAU,CAAC,OAAO,EAAE,CAAC,GAClD,YAAY,OAAO,OAAO,OAAO,GAAG,CAAC,GACrC,MAAM,KAAK,IAAI,CAClB;;AAGH,UAAQ,IAAI,GAAG;UACR,OAAO;EACd,MAAM,MAAM;AACZ,UAAQ,IAAI,MAAM,IAAI,6BAA6B,IAAI,QAAQ,IAAI,CAAC;AACpE,UAAQ,KAAK,EAAE;;;;;;;;;;;;ACtDnB,eAAsB,iBAAiB,OAA8B;AACnE,KAAI,CAAC,SAAS,MAAM,MAAM,KAAK,IAAI;AACjC,UAAQ,IAAI,MAAM,IAAI,6BAA6B,CAAC;AACpD,UAAQ,IAAI,MAAM,KAAK,wCAAwC,CAAC;AAChE,UAAQ,KAAK,EAAE;;CAGjB,MAAM,SAAS,IAAI,QAAQ;AAC3B,QAAO,aAAa,MAAM,MAAM,CAAC;AAEjC,SAAQ,IAAI,MAAM,MAAM,8BAA8B,CAAC;AACvD,SAAQ,IAAI,MAAM,KAAK,WAAW,OAAO,SAAS,CAAC,IAAI,CAAC;AACxD,SAAQ,IAAI,MAAM,KAAK,mBAAmB,CAAC;AAC3C,SAAQ,IAAI,MAAM,MAAM,wBAAwB,CAAC;;;;;;;;;;ACdnD,eAAsB,gBAA+B;CACnD,MAAM,SAAS,IAAI,QAAQ;AAE3B,KAAI,CAAC,OAAO,iBAAiB,EAAE;AAC7B,UAAQ,IAAI,MAAM,OAAO,mCAAmC,CAAC;AAC7D;;AAGF,QAAO,OAAO;AAEd,SAAQ,IAAI,MAAM,MAAM,uBAAuB,CAAC;AAChD,SAAQ,IAAI,MAAM,KAAK,cAAc,OAAO,SAAS,CAAC,IAAI,CAAC;;;;;;;;;;ACX7D,eAAsB,gBAA+B;CACnD,MAAM,SAAS,IAAI,QAAQ;AAE3B,SAAQ,IAAI,MAAM,KAAK,wBAAwB,CAAC;AAGhD,KAAI,OAAO,iBAAiB,EAAE;EAC5B,MAAM,QAAQ,OAAO,cAAc;EACnC,MAAM,cAAc,MAAM,MAAM,GAAG,EAAE,GAAG,QAAQ,MAAM,MAAM,GAAG;AAC/D,UAAQ,IAAI,cAAc,MAAM,MAAM,gBAAgB,GAAG;AACzD,UAAQ,IAAI,cAAc,MAAM,KAAK,YAAY,GAAG;QAC/C;AACL,UAAQ,IAAI,cAAc,MAAM,OAAO,oBAAoB,GAAG;AAC9D,UAAQ,IAAI,MAAM,KAAK,iEAAiE,CAAC;AACzF,UAAQ,IAAI,MAAM,KAAK,iEAAiE,CAAC;;AAG3F,SAAQ,IAAI,GAAG;AAGf,SAAQ,IAAI,cAAc,MAAM,KAAK,OAAO,WAAW,CAAC,GAAG;AAC3D,SAAQ,IACN,cAAc,MAAM,KAAK,GAAG,OAAO,kBAAkB,CAAC,GAAG,OAAO,kBAAkB,GAAG,GACtF;AACD,SAAQ,IAAI,cAAc,MAAM,KAAK,OAAO,SAAS,CAAC,GAAG;AAEzD,KAAI,OAAO,gBAAgB,CACzB,SAAQ,IAAI,MAAM,OAAO,wCAAwC,CAAC;AAGpE,SAAQ,IAAI,GAAG;;;;;;;;;;;;;;;ACnBjB,MAAM,UAAU,IAAI,SAAS;AAE7B,QACG,KAAK,aAAa,CAClB,YAAY,qCAAqC,CACjD,QAAQ,QAAQ,CAChB,OAAO,uBAAuB,yBAAyB,CACvD,yBAAyB;AAG5B,QAAQ,KAAK,cAAc,gBAAgB;CACzC,MAAM,OAAO,YAAY,MAAM;AAC/B,KAAI,KAAK,OACP,eAAc,KAAK,OAAO;EAE5B;AAGF,QACG,SAAS,UAAU,uBAAuB,CAC1C,OAAO,OAAO,SAA6B;AAC1C,KAAI,KACF,OAAM,cAAc,KAAK;KAEzB,SAAQ,MAAM;EAEhB;AAGJ,QACG,QAAQ,YAAY,CACpB,YAAY,2DAA2D,CACvE,SAAS,WAAW,oCAAoC,CACxD,qBAAqB,MAAM,CAC3B,oBAAoB,CACpB,OAAO,iBAAiB;AAE3B,QACG,QAAQ,OAAO,CACf,YAAY,oBAAoB,CAChC,OAAO,YAAY;AAEtB,QACG,QAAQ,SAAS,CACjB,YAAY,uBAAuB,CACnC,OAAO,cAAc;AAExB,QACG,QAAQ,SAAS,CACjB,YAAY,wCAAwC,CACpD,OAAO,cAAc;AAExB,QAAQ,OAAO"}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import Conf from "conf";
|
|
2
|
+
import fs, { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
3
|
+
import path, { join } from "path";
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import { tmpdir } from "os";
|
|
6
|
+
|
|
7
|
+
//#region src/lib/config.ts
|
|
8
|
+
/**
|
|
9
|
+
* Configuration management for chaitunnel CLI
|
|
10
|
+
*
|
|
11
|
+
* Uses shared config file with chaiterm at:
|
|
12
|
+
* - Custom path via --config flag
|
|
13
|
+
* - Default: ~/.config/chaiterm/config.json
|
|
14
|
+
*
|
|
15
|
+
* Auth flow (ngrok-style):
|
|
16
|
+
* 1. User gets authtoken from web dashboard
|
|
17
|
+
* 2. Runs: chaitunnel authtoken <token>
|
|
18
|
+
* 3. Token saved here, used for all API calls
|
|
19
|
+
*/
|
|
20
|
+
let customConfigPath = null;
|
|
21
|
+
/**
|
|
22
|
+
* Set custom config path (called before Config instantiation)
|
|
23
|
+
*/
|
|
24
|
+
function setConfigPath(configPath) {
|
|
25
|
+
customConfigPath = path.resolve(configPath);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Get the custom config path if set
|
|
29
|
+
*/
|
|
30
|
+
function getCustomConfigPath() {
|
|
31
|
+
return customConfigPath;
|
|
32
|
+
}
|
|
33
|
+
const CONFIG_DEFAULTS = {
|
|
34
|
+
apiUrl: "https://chaiterm.com",
|
|
35
|
+
frpServerAddr: "frp.chaiterm.com",
|
|
36
|
+
frpServerPort: 7e3,
|
|
37
|
+
tlsEnabled: true,
|
|
38
|
+
publicUrlTemplate: "https://{subdomain}.chaiterm.com"
|
|
39
|
+
};
|
|
40
|
+
var Config = class {
|
|
41
|
+
store;
|
|
42
|
+
customPath;
|
|
43
|
+
constructor() {
|
|
44
|
+
this.customPath = customConfigPath;
|
|
45
|
+
if (this.customPath) {
|
|
46
|
+
const dir = path.dirname(this.customPath);
|
|
47
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
48
|
+
this.store = new Conf({
|
|
49
|
+
projectName: "chaiterm",
|
|
50
|
+
defaults: CONFIG_DEFAULTS,
|
|
51
|
+
cwd: dir,
|
|
52
|
+
configName: path.basename(this.customPath, ".json")
|
|
53
|
+
});
|
|
54
|
+
} else this.store = new Conf({
|
|
55
|
+
projectName: "chaiterm",
|
|
56
|
+
defaults: CONFIG_DEFAULTS
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Check if user is authenticated (has authtoken)
|
|
61
|
+
*/
|
|
62
|
+
isAuthenticated() {
|
|
63
|
+
return !!this.store.get("authToken");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get auth token (ngrok-style token from dashboard)
|
|
67
|
+
*/
|
|
68
|
+
getAuthToken() {
|
|
69
|
+
return this.store.get("authToken");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Set auth token (from chaitunnel authtoken <token>)
|
|
73
|
+
*/
|
|
74
|
+
setAuthToken(token) {
|
|
75
|
+
this.store.set("authToken", token);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get user ID (extracted from token validation response)
|
|
79
|
+
*/
|
|
80
|
+
getUserId() {
|
|
81
|
+
return this.store.get("userId");
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Set user ID
|
|
85
|
+
*/
|
|
86
|
+
setUserId(userId) {
|
|
87
|
+
this.store.set("userId", userId);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Get user email
|
|
91
|
+
*/
|
|
92
|
+
getEmail() {
|
|
93
|
+
return this.store.get("email");
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Set user email
|
|
97
|
+
*/
|
|
98
|
+
setEmail(email) {
|
|
99
|
+
this.store.set("email", email);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Get API base URL
|
|
103
|
+
*/
|
|
104
|
+
getApiUrl() {
|
|
105
|
+
return this.store.get("apiUrl") || "https://chaiterm.com";
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get frp server address
|
|
109
|
+
*/
|
|
110
|
+
getFrpServerAddr() {
|
|
111
|
+
return this.store.get("frpServerAddr") || "frp.chaiterm.com";
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get frp server port
|
|
115
|
+
*/
|
|
116
|
+
getFrpServerPort() {
|
|
117
|
+
return this.store.get("frpServerPort") || 7e3;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get TLS enabled setting
|
|
121
|
+
*/
|
|
122
|
+
getTlsEnabled() {
|
|
123
|
+
const value = this.store.get("tlsEnabled");
|
|
124
|
+
return value !== void 0 ? value : true;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Get public URL template
|
|
128
|
+
*/
|
|
129
|
+
getPublicUrlTemplate() {
|
|
130
|
+
return this.store.get("publicUrlTemplate") || "https://{subdomain}.chaiterm.com";
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Clear all configuration (logout)
|
|
134
|
+
*/
|
|
135
|
+
clear() {
|
|
136
|
+
this.store.clear();
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get config file path (for debugging)
|
|
140
|
+
*/
|
|
141
|
+
getPath() {
|
|
142
|
+
return this.store.path;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Check if using a custom config path
|
|
146
|
+
*/
|
|
147
|
+
isCustomConfig() {
|
|
148
|
+
return this.customPath !== null;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/lib/api-client.ts
|
|
154
|
+
var ApiClient = class {
|
|
155
|
+
baseUrl;
|
|
156
|
+
token;
|
|
157
|
+
constructor(baseUrl, token) {
|
|
158
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
159
|
+
this.token = token;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Create a new tunnel
|
|
163
|
+
*/
|
|
164
|
+
async createTunnel(localPort) {
|
|
165
|
+
return (await this.trpcMutation("tunnel.create", { localPort })).result.data.json;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* List user's tunnels
|
|
169
|
+
*/
|
|
170
|
+
async listTunnels() {
|
|
171
|
+
return (await this.trpcQuery("tunnel.list")).result.data.json;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Delete a tunnel
|
|
175
|
+
*/
|
|
176
|
+
async deleteTunnel(id) {
|
|
177
|
+
await this.trpcMutation("tunnel.delete", { id });
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get a single tunnel
|
|
181
|
+
*/
|
|
182
|
+
async getTunnel(id) {
|
|
183
|
+
return (await this.trpcQuery("tunnel.get", { id })).result.data.json;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Make a tRPC query (GET)
|
|
187
|
+
*/
|
|
188
|
+
async trpcQuery(procedure, input) {
|
|
189
|
+
const url = new URL(`${this.baseUrl}/trpc/${procedure}`);
|
|
190
|
+
if (input) url.searchParams.set("input", JSON.stringify(input));
|
|
191
|
+
const response = await fetch(url.toString(), {
|
|
192
|
+
method: "GET",
|
|
193
|
+
headers: {
|
|
194
|
+
Authorization: `Bearer ${this.token}`,
|
|
195
|
+
"Content-Type": "application/json"
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
if (!response.ok) {
|
|
199
|
+
const error = await response.text();
|
|
200
|
+
throw new Error(`API error: ${response.status} - ${error}`);
|
|
201
|
+
}
|
|
202
|
+
return response.json();
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Make a tRPC mutation (POST)
|
|
206
|
+
*/
|
|
207
|
+
async trpcMutation(procedure, input) {
|
|
208
|
+
const response = await fetch(`${this.baseUrl}/trpc/${procedure}`, {
|
|
209
|
+
method: "POST",
|
|
210
|
+
headers: {
|
|
211
|
+
Authorization: `Bearer ${this.token}`,
|
|
212
|
+
"Content-Type": "application/json"
|
|
213
|
+
},
|
|
214
|
+
body: JSON.stringify({ json: input })
|
|
215
|
+
});
|
|
216
|
+
if (!response.ok) {
|
|
217
|
+
const error = await response.text();
|
|
218
|
+
throw new Error(`API error: ${response.status} - ${error}`);
|
|
219
|
+
}
|
|
220
|
+
return response.json();
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/lib/frp-wrapper.ts
|
|
226
|
+
/**
|
|
227
|
+
* frp Wrapper
|
|
228
|
+
*
|
|
229
|
+
* Wraps the frpc binary for port exposure functionality.
|
|
230
|
+
* Generates config and spawns frpc process.
|
|
231
|
+
*/
|
|
232
|
+
var FrpWrapper = class {
|
|
233
|
+
options;
|
|
234
|
+
process = null;
|
|
235
|
+
configPath;
|
|
236
|
+
constructor(options) {
|
|
237
|
+
this.options = options;
|
|
238
|
+
this.configPath = join(tmpdir(), "chaitunnel", `frpc-${options.tunnelId}.toml`);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Start frpc with current configuration
|
|
242
|
+
*/
|
|
243
|
+
async start() {
|
|
244
|
+
const configDir = join(tmpdir(), "chaitunnel");
|
|
245
|
+
if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
|
|
246
|
+
this.generateConfig();
|
|
247
|
+
const frpcPath = await this.findFrpcBinary();
|
|
248
|
+
if (!frpcPath) throw new Error("frpc binary not found. Please install frpc (brew install frpc on macOS)");
|
|
249
|
+
return new Promise((resolve, reject) => {
|
|
250
|
+
this.process = spawn(frpcPath, ["-c", this.configPath], { stdio: [
|
|
251
|
+
"ignore",
|
|
252
|
+
"pipe",
|
|
253
|
+
"pipe"
|
|
254
|
+
] });
|
|
255
|
+
let connected = false;
|
|
256
|
+
this.process.stdout?.on("data", (data) => {
|
|
257
|
+
const message = data.toString();
|
|
258
|
+
this.options.onLog?.(message);
|
|
259
|
+
if (message.includes("start proxy success") && !connected) {
|
|
260
|
+
connected = true;
|
|
261
|
+
this.options.onConnect?.();
|
|
262
|
+
resolve();
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
this.process.stderr?.on("data", (data) => {
|
|
266
|
+
const message = data.toString();
|
|
267
|
+
this.options.onLog?.(message);
|
|
268
|
+
if (message.includes("error") || message.includes("Error")) this.options.onError?.(new Error(message));
|
|
269
|
+
});
|
|
270
|
+
this.process.on("error", (err) => {
|
|
271
|
+
this.options.onError?.(err);
|
|
272
|
+
reject(err);
|
|
273
|
+
});
|
|
274
|
+
this.process.on("close", () => {
|
|
275
|
+
this.process = null;
|
|
276
|
+
this.options.onDisconnect?.();
|
|
277
|
+
});
|
|
278
|
+
setTimeout(() => {
|
|
279
|
+
if (!connected) reject(/* @__PURE__ */ new Error("Connection timeout - could not connect to frp server"));
|
|
280
|
+
}, 3e4);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Stop frpc process
|
|
285
|
+
*/
|
|
286
|
+
stop() {
|
|
287
|
+
if (this.process) {
|
|
288
|
+
this.process.kill("SIGTERM");
|
|
289
|
+
this.process = null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Check if frpc is running
|
|
294
|
+
*/
|
|
295
|
+
isRunning() {
|
|
296
|
+
return this.process !== null;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Generate frpc config file
|
|
300
|
+
*/
|
|
301
|
+
generateConfig() {
|
|
302
|
+
const { serverAddr, serverPort, authToken, tunnelId, tlsEnabled, proxy } = this.options;
|
|
303
|
+
const config = `# Auto-generated by chaitunnel CLI
|
|
304
|
+
# Tunnel: ${tunnelId}
|
|
305
|
+
|
|
306
|
+
serverAddr = "${serverAddr}"
|
|
307
|
+
serverPort = ${serverPort}
|
|
308
|
+
|
|
309
|
+
# Authentication
|
|
310
|
+
auth.method = "token"
|
|
311
|
+
auth.token = "base-token"
|
|
312
|
+
|
|
313
|
+
# CLI auth token (sent to auth plugin for validation)
|
|
314
|
+
metadatas.token = "${authToken}"
|
|
315
|
+
metadatas.tunnelId = "${tunnelId}"
|
|
316
|
+
|
|
317
|
+
# TLS
|
|
318
|
+
transport.tls.enable = ${tlsEnabled}
|
|
319
|
+
|
|
320
|
+
# Logging
|
|
321
|
+
log.to = "console"
|
|
322
|
+
log.level = "info"
|
|
323
|
+
|
|
324
|
+
# Proxy configuration
|
|
325
|
+
[[proxies]]
|
|
326
|
+
name = "${tunnelId}"
|
|
327
|
+
type = "http"
|
|
328
|
+
localPort = ${proxy.localPort}
|
|
329
|
+
subdomain = "${proxy.subdomain}"
|
|
330
|
+
`;
|
|
331
|
+
writeFileSync(this.configPath, config, "utf-8");
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Find frpc binary in various locations
|
|
335
|
+
*/
|
|
336
|
+
async findFrpcBinary() {
|
|
337
|
+
const { execSync } = await import("child_process");
|
|
338
|
+
for (const path$1 of [
|
|
339
|
+
"frpc",
|
|
340
|
+
"/opt/homebrew/bin/frpc",
|
|
341
|
+
"/usr/local/bin/frpc",
|
|
342
|
+
"/usr/bin/frpc"
|
|
343
|
+
]) try {
|
|
344
|
+
execSync(`${path$1} --version`, { stdio: "ignore" });
|
|
345
|
+
return path$1;
|
|
346
|
+
} catch {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
//#endregion
|
|
354
|
+
export { setConfigPath as a, getCustomConfigPath as i, ApiClient as n, Config as r, FrpWrapper as t };
|
|
355
|
+
//# sourceMappingURL=frp-wrapper-DgAvulMB.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frp-wrapper-DgAvulMB.mjs","names":["customConfigPath: string | null","CONFIG_DEFAULTS: Partial<ChaitunnelConfig>","path"],"sources":["../src/lib/config.ts","../src/lib/api-client.ts","../src/lib/frp-wrapper.ts"],"sourcesContent":["/**\n * Configuration management for chaitunnel CLI\n *\n * Uses shared config file with chaiterm at:\n * - Custom path via --config flag\n * - Default: ~/.config/chaiterm/config.json\n *\n * Auth flow (ngrok-style):\n * 1. User gets authtoken from web dashboard\n * 2. Runs: chaitunnel authtoken <token>\n * 3. Token saved here, used for all API calls\n */\n\nimport Conf from \"conf\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport type { ChaitunnelConfig } from \"../types.js\";\n\n// Track custom config path if provided via --config flag\nlet customConfigPath: string | null = null;\n\n/**\n * Set custom config path (called before Config instantiation)\n */\nexport function setConfigPath(configPath: string): void {\n customConfigPath = path.resolve(configPath);\n}\n\n/**\n * Get the custom config path if set\n */\nexport function getCustomConfigPath(): string | null {\n return customConfigPath;\n}\n\n// Production defaults\nconst CONFIG_DEFAULTS: Partial<ChaitunnelConfig> = {\n apiUrl: \"https://chaiterm.com\",\n frpServerAddr: \"frp.chaiterm.com\",\n frpServerPort: 7000,\n tlsEnabled: true,\n publicUrlTemplate: \"https://{subdomain}.chaiterm.com\",\n};\n\nexport class Config {\n private store: Conf<ChaitunnelConfig>;\n private customPath: string | null;\n\n constructor() {\n this.customPath = customConfigPath;\n\n if (this.customPath) {\n const dir = path.dirname(this.customPath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n this.store = new Conf<ChaitunnelConfig>({\n projectName: \"chaiterm\", // Same project name to share config\n defaults: CONFIG_DEFAULTS as ChaitunnelConfig,\n cwd: dir,\n configName: path.basename(this.customPath, \".json\"),\n });\n } else {\n this.store = new Conf<ChaitunnelConfig>({\n projectName: \"chaiterm\", // Same project name to share config\n defaults: CONFIG_DEFAULTS as ChaitunnelConfig,\n });\n }\n }\n\n /**\n * Check if user is authenticated (has authtoken)\n */\n isAuthenticated(): boolean {\n return !!this.store.get(\"authToken\");\n }\n\n /**\n * Get auth token (ngrok-style token from dashboard)\n */\n getAuthToken(): string | undefined {\n return this.store.get(\"authToken\");\n }\n\n /**\n * Set auth token (from chaitunnel authtoken <token>)\n */\n setAuthToken(token: string): void {\n this.store.set(\"authToken\", token);\n }\n\n /**\n * Get user ID (extracted from token validation response)\n */\n getUserId(): string | undefined {\n return this.store.get(\"userId\");\n }\n\n /**\n * Set user ID\n */\n setUserId(userId: string): void {\n this.store.set(\"userId\", userId);\n }\n\n /**\n * Get user email\n */\n getEmail(): string | undefined {\n return this.store.get(\"email\");\n }\n\n /**\n * Set user email\n */\n setEmail(email: string): void {\n this.store.set(\"email\", email);\n }\n\n /**\n * Get API base URL\n */\n getApiUrl(): string {\n return this.store.get(\"apiUrl\") || \"https://chaiterm.com\";\n }\n\n /**\n * Get frp server address\n */\n getFrpServerAddr(): string {\n return this.store.get(\"frpServerAddr\") || \"frp.chaiterm.com\";\n }\n\n /**\n * Get frp server port\n */\n getFrpServerPort(): number {\n return this.store.get(\"frpServerPort\") || 7000;\n }\n\n /**\n * Get TLS enabled setting\n */\n getTlsEnabled(): boolean {\n const value = this.store.get(\"tlsEnabled\");\n return value !== undefined ? value : true;\n }\n\n /**\n * Get public URL template\n */\n getPublicUrlTemplate(): string {\n return this.store.get(\"publicUrlTemplate\") || \"https://{subdomain}.chaiterm.com\";\n }\n\n /**\n * Clear all configuration (logout)\n */\n clear(): void {\n this.store.clear();\n }\n\n /**\n * Get config file path (for debugging)\n */\n getPath(): string {\n return this.store.path;\n }\n\n /**\n * Check if using a custom config path\n */\n isCustomConfig(): boolean {\n return this.customPath !== null;\n }\n}\n","/**\n * API Client for chaitunnel CLI\n *\n * Makes HTTP calls to the tRPC API for tunnel management.\n */\n\nimport type { CreateTunnelResponse, TunnelInfo } from \"../types.js\";\n\nexport class ApiClient {\n private baseUrl: string;\n private token: string;\n\n constructor(baseUrl: string, token: string) {\n this.baseUrl = baseUrl.replace(/\\/$/, \"\"); // Remove trailing slash\n this.token = token;\n }\n\n /**\n * Create a new tunnel\n */\n async createTunnel(localPort: number): Promise<CreateTunnelResponse> {\n const response = await this.trpcMutation(\"tunnel.create\", { localPort });\n return response.result.data.json as CreateTunnelResponse;\n }\n\n /**\n * List user's tunnels\n */\n async listTunnels(): Promise<TunnelInfo[]> {\n const response = await this.trpcQuery(\"tunnel.list\");\n return response.result.data.json as TunnelInfo[];\n }\n\n /**\n * Delete a tunnel\n */\n async deleteTunnel(id: string): Promise<void> {\n await this.trpcMutation(\"tunnel.delete\", { id });\n }\n\n /**\n * Get a single tunnel\n */\n async getTunnel(id: string): Promise<TunnelInfo> {\n const response = await this.trpcQuery(\"tunnel.get\", { id });\n return response.result.data.json as TunnelInfo;\n }\n\n /**\n * Make a tRPC query (GET)\n */\n private async trpcQuery(\n procedure: string,\n input?: Record<string, unknown>\n ): Promise<{ result: { data: { json: unknown } } }> {\n const url = new URL(`${this.baseUrl}/trpc/${procedure}`);\n if (input) {\n url.searchParams.set(\"input\", JSON.stringify(input));\n }\n\n const response = await fetch(url.toString(), {\n method: \"GET\",\n headers: {\n Authorization: `Bearer ${this.token}`,\n \"Content-Type\": \"application/json\",\n },\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`API error: ${response.status} - ${error}`);\n }\n\n return response.json() as Promise<{ result: { data: { json: unknown } } }>;\n }\n\n /**\n * Make a tRPC mutation (POST)\n */\n private async trpcMutation(\n procedure: string,\n input: Record<string, unknown>\n ): Promise<{ result: { data: { json: unknown } } }> {\n const response = await fetch(`${this.baseUrl}/trpc/${procedure}`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.token}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({ json: input }),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`API error: ${response.status} - ${error}`);\n }\n\n return response.json() as Promise<{ result: { data: { json: unknown } } }>;\n }\n}\n","/**\n * frp Wrapper\n *\n * Wraps the frpc binary for port exposure functionality.\n * Generates config and spawns frpc process.\n */\n\nimport { spawn, type ChildProcess } from \"child_process\";\nimport { writeFileSync, mkdirSync, existsSync } from \"fs\";\nimport { join } from \"path\";\nimport { tmpdir } from \"os\";\n\nexport interface FrpProxyConfig {\n /** Tunnel ID (used as proxy name) */\n tunnelId: string;\n /** Subdomain allocated by server */\n subdomain: string;\n /** Local port to expose */\n localPort: number;\n}\n\nexport interface FrpWrapperOptions {\n serverAddr: string;\n serverPort: number;\n /** CLI auth token for frp authentication */\n authToken: string;\n tunnelId: string;\n /** Enable TLS for frp connection */\n tlsEnabled: boolean;\n proxy: FrpProxyConfig;\n onConnect?: () => void;\n onDisconnect?: () => void;\n onError?: (error: Error) => void;\n onLog?: (message: string) => void;\n}\n\nexport class FrpWrapper {\n private options: FrpWrapperOptions;\n private process: ChildProcess | null = null;\n private configPath: string;\n\n constructor(options: FrpWrapperOptions) {\n this.options = options;\n this.configPath = join(\n tmpdir(),\n \"chaitunnel\",\n `frpc-${options.tunnelId}.toml`\n );\n }\n\n /**\n * Start frpc with current configuration\n */\n async start(): Promise<void> {\n // Ensure config directory exists\n const configDir = join(tmpdir(), \"chaitunnel\");\n if (!existsSync(configDir)) {\n mkdirSync(configDir, { recursive: true });\n }\n\n // Generate config file\n this.generateConfig();\n\n // Find frpc binary\n const frpcPath = await this.findFrpcBinary();\n if (!frpcPath) {\n throw new Error(\n \"frpc binary not found. Please install frpc (brew install frpc on macOS)\"\n );\n }\n\n return new Promise((resolve, reject) => {\n this.process = spawn(frpcPath, [\"-c\", this.configPath], {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let connected = false;\n\n this.process.stdout?.on(\"data\", (data: Buffer) => {\n const message = data.toString();\n this.options.onLog?.(message);\n\n // Detect connection success\n if (message.includes(\"start proxy success\") && !connected) {\n connected = true;\n this.options.onConnect?.();\n resolve();\n }\n });\n\n this.process.stderr?.on(\"data\", (data: Buffer) => {\n const message = data.toString();\n this.options.onLog?.(message);\n\n // Check for errors\n if (message.includes(\"error\") || message.includes(\"Error\")) {\n this.options.onError?.(new Error(message));\n }\n });\n\n this.process.on(\"error\", (err) => {\n this.options.onError?.(err);\n reject(err);\n });\n\n this.process.on(\"close\", () => {\n this.process = null;\n this.options.onDisconnect?.();\n });\n\n // Timeout if no connection after 30 seconds\n setTimeout(() => {\n if (!connected) {\n reject(new Error(\"Connection timeout - could not connect to frp server\"));\n }\n }, 30000);\n });\n }\n\n /**\n * Stop frpc process\n */\n stop(): void {\n if (this.process) {\n this.process.kill(\"SIGTERM\");\n this.process = null;\n }\n }\n\n /**\n * Check if frpc is running\n */\n isRunning(): boolean {\n return this.process !== null;\n }\n\n /**\n * Generate frpc config file\n */\n private generateConfig(): void {\n const { serverAddr, serverPort, authToken, tunnelId, tlsEnabled, proxy } = this.options;\n\n const config = `# Auto-generated by chaitunnel CLI\n# Tunnel: ${tunnelId}\n\nserverAddr = \"${serverAddr}\"\nserverPort = ${serverPort}\n\n# Authentication\nauth.method = \"token\"\nauth.token = \"base-token\"\n\n# CLI auth token (sent to auth plugin for validation)\nmetadatas.token = \"${authToken}\"\nmetadatas.tunnelId = \"${tunnelId}\"\n\n# TLS\ntransport.tls.enable = ${tlsEnabled}\n\n# Logging\nlog.to = \"console\"\nlog.level = \"info\"\n\n# Proxy configuration\n[[proxies]]\nname = \"${tunnelId}\"\ntype = \"http\"\nlocalPort = ${proxy.localPort}\nsubdomain = \"${proxy.subdomain}\"\n`;\n\n writeFileSync(this.configPath, config, \"utf-8\");\n }\n\n /**\n * Find frpc binary in various locations\n */\n private async findFrpcBinary(): Promise<string | null> {\n const { execSync } = await import(\"child_process\");\n\n const possiblePaths = [\n // In PATH\n \"frpc\",\n // Homebrew (macOS)\n \"/opt/homebrew/bin/frpc\",\n \"/usr/local/bin/frpc\",\n // Linux\n \"/usr/bin/frpc\",\n ];\n\n for (const path of possiblePaths) {\n try {\n execSync(`${path} --version`, { stdio: \"ignore\" });\n return path;\n } catch {\n continue;\n }\n }\n\n return null;\n }\n}\n"],"mappings":"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"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
//#region src/lib/config.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Configuration management for chaitunnel CLI
|
|
4
|
+
*
|
|
5
|
+
* Uses shared config file with chaiterm at:
|
|
6
|
+
* - Custom path via --config flag
|
|
7
|
+
* - Default: ~/.config/chaiterm/config.json
|
|
8
|
+
*
|
|
9
|
+
* Auth flow (ngrok-style):
|
|
10
|
+
* 1. User gets authtoken from web dashboard
|
|
11
|
+
* 2. Runs: chaitunnel authtoken <token>
|
|
12
|
+
* 3. Token saved here, used for all API calls
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Set custom config path (called before Config instantiation)
|
|
16
|
+
*/
|
|
17
|
+
declare function setConfigPath(configPath: string): void;
|
|
18
|
+
/**
|
|
19
|
+
* Get the custom config path if set
|
|
20
|
+
*/
|
|
21
|
+
declare function getCustomConfigPath(): string | null;
|
|
22
|
+
declare class Config {
|
|
23
|
+
private store;
|
|
24
|
+
private customPath;
|
|
25
|
+
constructor();
|
|
26
|
+
/**
|
|
27
|
+
* Check if user is authenticated (has authtoken)
|
|
28
|
+
*/
|
|
29
|
+
isAuthenticated(): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Get auth token (ngrok-style token from dashboard)
|
|
32
|
+
*/
|
|
33
|
+
getAuthToken(): string | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Set auth token (from chaitunnel authtoken <token>)
|
|
36
|
+
*/
|
|
37
|
+
setAuthToken(token: string): void;
|
|
38
|
+
/**
|
|
39
|
+
* Get user ID (extracted from token validation response)
|
|
40
|
+
*/
|
|
41
|
+
getUserId(): string | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Set user ID
|
|
44
|
+
*/
|
|
45
|
+
setUserId(userId: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* Get user email
|
|
48
|
+
*/
|
|
49
|
+
getEmail(): string | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Set user email
|
|
52
|
+
*/
|
|
53
|
+
setEmail(email: string): void;
|
|
54
|
+
/**
|
|
55
|
+
* Get API base URL
|
|
56
|
+
*/
|
|
57
|
+
getApiUrl(): string;
|
|
58
|
+
/**
|
|
59
|
+
* Get frp server address
|
|
60
|
+
*/
|
|
61
|
+
getFrpServerAddr(): string;
|
|
62
|
+
/**
|
|
63
|
+
* Get frp server port
|
|
64
|
+
*/
|
|
65
|
+
getFrpServerPort(): number;
|
|
66
|
+
/**
|
|
67
|
+
* Get TLS enabled setting
|
|
68
|
+
*/
|
|
69
|
+
getTlsEnabled(): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Get public URL template
|
|
72
|
+
*/
|
|
73
|
+
getPublicUrlTemplate(): string;
|
|
74
|
+
/**
|
|
75
|
+
* Clear all configuration (logout)
|
|
76
|
+
*/
|
|
77
|
+
clear(): void;
|
|
78
|
+
/**
|
|
79
|
+
* Get config file path (for debugging)
|
|
80
|
+
*/
|
|
81
|
+
getPath(): string;
|
|
82
|
+
/**
|
|
83
|
+
* Check if using a custom config path
|
|
84
|
+
*/
|
|
85
|
+
isCustomConfig(): boolean;
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/types.d.ts
|
|
89
|
+
/**
|
|
90
|
+
* Shared types for chaitunnel CLI
|
|
91
|
+
*/
|
|
92
|
+
interface ChaitunnelConfig {
|
|
93
|
+
/** Auth token from dashboard (ngrok-style) */
|
|
94
|
+
authToken: string;
|
|
95
|
+
/** User ID (from token validation) */
|
|
96
|
+
userId: string;
|
|
97
|
+
/** User email */
|
|
98
|
+
email: string;
|
|
99
|
+
/** API base URL */
|
|
100
|
+
apiUrl: string;
|
|
101
|
+
/** frp server address */
|
|
102
|
+
frpServerAddr: string;
|
|
103
|
+
/** frp server port */
|
|
104
|
+
frpServerPort: number;
|
|
105
|
+
/** Enable TLS for frp connection (default: true) */
|
|
106
|
+
tlsEnabled: boolean;
|
|
107
|
+
/** Public URL template with {subdomain} placeholder */
|
|
108
|
+
publicUrlTemplate: string;
|
|
109
|
+
}
|
|
110
|
+
interface TunnelInfo {
|
|
111
|
+
/** Tunnel ID from server */
|
|
112
|
+
id: string;
|
|
113
|
+
/** Subdomain (e.g., "k7x9m2") */
|
|
114
|
+
subdomain: string;
|
|
115
|
+
/** Local port being exposed */
|
|
116
|
+
localPort: number;
|
|
117
|
+
/** Tunnel status */
|
|
118
|
+
status: "active" | "inactive" | "deleted";
|
|
119
|
+
/** Creation timestamp */
|
|
120
|
+
createdAt: string;
|
|
121
|
+
}
|
|
122
|
+
interface CreateTunnelResponse {
|
|
123
|
+
id: string;
|
|
124
|
+
subdomain: string;
|
|
125
|
+
localPort: number;
|
|
126
|
+
status: string;
|
|
127
|
+
}
|
|
128
|
+
interface TunnelStatus {
|
|
129
|
+
subdomain: string;
|
|
130
|
+
localPort: number;
|
|
131
|
+
status: "connected" | "disconnected" | "connecting";
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/lib/api-client.d.ts
|
|
135
|
+
declare class ApiClient {
|
|
136
|
+
private baseUrl;
|
|
137
|
+
private token;
|
|
138
|
+
constructor(baseUrl: string, token: string);
|
|
139
|
+
/**
|
|
140
|
+
* Create a new tunnel
|
|
141
|
+
*/
|
|
142
|
+
createTunnel(localPort: number): Promise<CreateTunnelResponse>;
|
|
143
|
+
/**
|
|
144
|
+
* List user's tunnels
|
|
145
|
+
*/
|
|
146
|
+
listTunnels(): Promise<TunnelInfo[]>;
|
|
147
|
+
/**
|
|
148
|
+
* Delete a tunnel
|
|
149
|
+
*/
|
|
150
|
+
deleteTunnel(id: string): Promise<void>;
|
|
151
|
+
/**
|
|
152
|
+
* Get a single tunnel
|
|
153
|
+
*/
|
|
154
|
+
getTunnel(id: string): Promise<TunnelInfo>;
|
|
155
|
+
/**
|
|
156
|
+
* Make a tRPC query (GET)
|
|
157
|
+
*/
|
|
158
|
+
private trpcQuery;
|
|
159
|
+
/**
|
|
160
|
+
* Make a tRPC mutation (POST)
|
|
161
|
+
*/
|
|
162
|
+
private trpcMutation;
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/lib/frp-wrapper.d.ts
|
|
166
|
+
/**
|
|
167
|
+
* frp Wrapper
|
|
168
|
+
*
|
|
169
|
+
* Wraps the frpc binary for port exposure functionality.
|
|
170
|
+
* Generates config and spawns frpc process.
|
|
171
|
+
*/
|
|
172
|
+
interface FrpProxyConfig {
|
|
173
|
+
/** Tunnel ID (used as proxy name) */
|
|
174
|
+
tunnelId: string;
|
|
175
|
+
/** Subdomain allocated by server */
|
|
176
|
+
subdomain: string;
|
|
177
|
+
/** Local port to expose */
|
|
178
|
+
localPort: number;
|
|
179
|
+
}
|
|
180
|
+
interface FrpWrapperOptions {
|
|
181
|
+
serverAddr: string;
|
|
182
|
+
serverPort: number;
|
|
183
|
+
/** CLI auth token for frp authentication */
|
|
184
|
+
authToken: string;
|
|
185
|
+
tunnelId: string;
|
|
186
|
+
/** Enable TLS for frp connection */
|
|
187
|
+
tlsEnabled: boolean;
|
|
188
|
+
proxy: FrpProxyConfig;
|
|
189
|
+
onConnect?: () => void;
|
|
190
|
+
onDisconnect?: () => void;
|
|
191
|
+
onError?: (error: Error) => void;
|
|
192
|
+
onLog?: (message: string) => void;
|
|
193
|
+
}
|
|
194
|
+
declare class FrpWrapper {
|
|
195
|
+
private options;
|
|
196
|
+
private process;
|
|
197
|
+
private configPath;
|
|
198
|
+
constructor(options: FrpWrapperOptions);
|
|
199
|
+
/**
|
|
200
|
+
* Start frpc with current configuration
|
|
201
|
+
*/
|
|
202
|
+
start(): Promise<void>;
|
|
203
|
+
/**
|
|
204
|
+
* Stop frpc process
|
|
205
|
+
*/
|
|
206
|
+
stop(): void;
|
|
207
|
+
/**
|
|
208
|
+
* Check if frpc is running
|
|
209
|
+
*/
|
|
210
|
+
isRunning(): boolean;
|
|
211
|
+
/**
|
|
212
|
+
* Generate frpc config file
|
|
213
|
+
*/
|
|
214
|
+
private generateConfig;
|
|
215
|
+
/**
|
|
216
|
+
* Find frpc binary in various locations
|
|
217
|
+
*/
|
|
218
|
+
private findFrpcBinary;
|
|
219
|
+
}
|
|
220
|
+
//#endregion
|
|
221
|
+
export { ApiClient, type ChaitunnelConfig, Config, type CreateTunnelResponse, type FrpProxyConfig, FrpWrapper, type FrpWrapperOptions, type TunnelInfo, type TunnelStatus, getCustomConfigPath, setConfigPath };
|
|
222
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/lib/config.ts","../src/types.ts","../src/lib/api-client.ts","../src/lib/frp-wrapper.ts"],"sourcesContent":[],"mappings":";;AAwBA;AAOA;AAaA;;;;ACxCA;AAmBA;AAaA;AAOA;;;;ACnCA;AAYiD,iBFIjC,aAAA,CEJiC,UAAA,EAAA,MAAA,CAAA,EAAA,IAAA;;;;AAgBf,iBFLlB,mBAAA,CAAA,CEKkB,EAAA,MAAA,GAAA,IAAA;AAOK,cFC1B,MAAA,CED0B;EAAR,QAAA,KAAA;EAAO,QAAA,UAAA;;;;AC/BtC;EASiB,eAAA,CAAA,CAAA,EAAA,OAAiB;EAerB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AHZb;AAOA;AAaa,UCxCI,gBAAA,CDwCE;;;;ECxCF,MAAA,EAAA,MAAA;EAmBA;EAaA,KAAA,EAAA,MAAA;EAOA;;;;ECnCJ;EAYoC,aAAA,EAAA,MAAA;EAAR;EAQV,UAAA,EAAA,OAAA;EAAR;EAQW,iBAAA,EAAA,MAAA;;AAOH,UDpBd,UAAA,CCoBc;EAAO;;;;EC/BrB;EASA,SAAA,EAAA,MAAA;EAeJ;;;;;UFAI,oBAAA;;;;;;UAOA,YAAA;;;;;;;cCnCJ,SAAA;EDJI,QAAA,OAAA;EAmBA,QAAA,KAAU;EAaV,WAAA,CAAA,OAAA,EAAA,MAAoB,EAAA,KAAA,EAAA,MAAA;EAOpB;;;mCCvBwB,QAAQ;EAZpC;;;EAoBkB,WAAA,CAAA,CAAA,EAAR,OAAQ,CAAA,UAAA,EAAA,CAAA;EAAR;;;EAeQ,YAAA,CAAA,EAAA,EAAA,MAAA,CAAA,EAPG,OAOH,CAAA,IAAA,CAAA;EAAO;;;yBAAP,QAAQ;EC/BtB;AASjB;AAeA;;;;;;;;;;AHZA;AAOA;AAaA;;;UGhCiB,cAAA;EFRA;EAmBA,QAAA,EAAA,MAAU;EAaV;EAOA,SAAA,EAAA,MAAY;;;;ACnChB,UCaI,iBAAA,CDbK;EAY2B,UAAA,EAAA,MAAA;EAAR,UAAA,EAAA,MAAA;EAQV;EAAR,SAAA,EAAA,MAAA;EAQW,QAAA,EAAA,MAAA;EAOK;EAAR,UAAA,EAAA,OAAA;EAAO,KAAA,ECd7B,cDc6B;;;oBCXlB;EApBH,KAAA,CAAA,EAAA,CAAA,OAAA,EAAc,MAAA,EAAA,GAAA,IAAA;AAS/B;AAea,cAAA,UAAA,CAKU;;;;uBAAA;;;;WAYN"}
|
package/dist/index.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chaitunnel",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Expose local ports to the internet - tunnel CLI for chaiterm",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"chaitunnel": "dist/bin/chaitunnel.mjs"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"default": "./dist/index.mjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsdown",
|
|
19
|
+
"dev": "tsdown --watch",
|
|
20
|
+
"check-types": "tsc --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"tunnel",
|
|
24
|
+
"port-forwarding",
|
|
25
|
+
"frp",
|
|
26
|
+
"cli",
|
|
27
|
+
"expose",
|
|
28
|
+
"chaiterm",
|
|
29
|
+
"localhost"
|
|
30
|
+
],
|
|
31
|
+
"author": "Kerem Atam",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"homepage": "https://chaiterm.com",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/anthropics/chaiterm.git"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"typescript": "^5.8.3",
|
|
40
|
+
"@types/node": "^22.15.29",
|
|
41
|
+
"tsdown": "^0.18.3"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"commander": "^13.1.0",
|
|
45
|
+
"chalk": "^5.4.1",
|
|
46
|
+
"conf": "^13.1.0",
|
|
47
|
+
"open": "^10.2.0"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18"
|
|
51
|
+
}
|
|
52
|
+
}
|