chaiterm 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/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # chaiterm CLI
2
+
3
+ Command-line tool for accessing your terminal from anywhere.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g chaiterm
9
+ ```
10
+
11
+ ## Commands
12
+
13
+ ### `chaiterm init`
14
+
15
+ Authenticate and initialize configuration.
16
+
17
+ ```bash
18
+ chaiterm init
19
+ ```
20
+
21
+ ### `chaiterm start`
22
+
23
+ Start terminal access - connects your local PTY to the gateway.
24
+
25
+ ```bash
26
+ chaiterm start
27
+ ```
28
+
29
+ Once connected, access your terminal at: `https://terminal.chaiterm.com`
30
+
31
+ ### `chaiterm expose <port>`
32
+
33
+ Expose a local port to the internet.
34
+
35
+ ```bash
36
+ chaiterm expose 3000
37
+ ```
38
+
39
+ Your app will be available at a random subdomain like: `https://k7x9m2.chaiterm.com`
40
+
41
+ ### `chaiterm status`
42
+
43
+ Show current connection status and configuration.
44
+
45
+ ```bash
46
+ chaiterm status
47
+ ```
48
+
49
+ ### `chaiterm logout`
50
+
51
+ Clear stored credentials.
52
+
53
+ ```bash
54
+ chaiterm logout
55
+ ```
56
+
57
+ ## Architecture
58
+
59
+ The CLI wraps two components:
60
+
61
+ 1. **pty-client** - Connects to `terminal.chaiterm.com` via WebSocket, spawns local PTY
62
+ 2. **frp-wrapper** - Wraps frpc binary for port exposure tunnels
63
+
64
+ ## Configuration
65
+
66
+ Config is stored in `~/.config/chaiterm/config.json` (or platform equivalent).
67
+
68
+ ## Requirements
69
+
70
+ - Node.js 18+
71
+ - For port exposure: frpc binary (install via `brew install frpc` on macOS)
72
+
73
+ ## Development
74
+
75
+ ```bash
76
+ # Install dependencies
77
+ bun install
78
+
79
+ # Build
80
+ bun run build
81
+
82
+ # Watch mode
83
+ bun run dev
84
+ ```
85
+
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env node
2
+ import { n as FrpWrapper, r as PtyClient, t as Config } from "../config-CGPKYUde.mjs";
3
+ import { Command } from "commander";
4
+ import chalk from "chalk";
5
+ import "open";
6
+
7
+ //#region src/commands/init.ts
8
+ /**
9
+ * Init command
10
+ *
11
+ * Authenticates user and stores credentials.
12
+ */
13
+ async function initCommand() {
14
+ const config = new Config();
15
+ console.log(chalk.cyan("\n🚀 Chaiterm Setup\n"));
16
+ if (config.isAuthenticated()) {
17
+ console.log(chalk.yellow("⚠️ You are already authenticated."));
18
+ console.log(` User ID: ${config.getUserId()}`);
19
+ console.log(` Config: ${config.getPath()}`);
20
+ console.log(chalk.gray("\n Run 'chaiterm logout' to clear credentials.\n"));
21
+ return;
22
+ }
23
+ console.log("To use chaiterm, you need to authenticate with your account.\n");
24
+ console.log(chalk.gray("Opening browser for authentication...\n"));
25
+ console.log(chalk.yellow("⚠️ Authentication not yet implemented.\n"));
26
+ console.log("For development, you can manually set credentials:");
27
+ console.log(chalk.gray(` Config file: ${config.getPath()}\n`));
28
+ console.log(chalk.cyan("📋 Next steps:"));
29
+ console.log(" 1. Sign up at https://chaiterm.com");
30
+ console.log(" 2. Get your CLI token from settings");
31
+ console.log(" 3. Run 'chaiterm start' to connect your terminal\n");
32
+ }
33
+
34
+ //#endregion
35
+ //#region src/commands/start.ts
36
+ /**
37
+ * Start command
38
+ *
39
+ * Starts the terminal connection (pty-client).
40
+ */
41
+ async function startCommand() {
42
+ const config = new Config();
43
+ console.log(chalk.cyan("\n🖥️ Starting terminal connection...\n"));
44
+ if (!config.isAuthenticated()) {
45
+ console.log(chalk.red("❌ Not authenticated."));
46
+ console.log(chalk.gray(" Run 'chaiterm init' first.\n"));
47
+ process.exit(1);
48
+ }
49
+ const userId = config.getUserId();
50
+ const token = config.getToken();
51
+ const gatewayUrl = config.getGatewayUrl();
52
+ console.log(` User: ${chalk.green(userId)}`);
53
+ console.log(` Gateway: ${chalk.gray(gatewayUrl)}`);
54
+ console.log("");
55
+ const client = new PtyClient({
56
+ gatewayUrl,
57
+ userId,
58
+ token,
59
+ onConnect: () => {
60
+ console.log(chalk.green("✓ Connected to terminal gateway"));
61
+ console.log(chalk.cyan(`\n📱 Access your terminal at: https://terminal.chaiterm.com\n`));
62
+ console.log(chalk.gray("Press Ctrl+C to disconnect\n"));
63
+ },
64
+ onDisconnect: (code, reason) => {
65
+ console.log(chalk.yellow(`\n⚠️ Disconnected: ${reason} (code: ${code})`));
66
+ },
67
+ onError: (error) => {
68
+ console.log(chalk.red(`\n❌ Error: ${error.message}`));
69
+ }
70
+ });
71
+ process.on("SIGINT", () => {
72
+ console.log(chalk.gray("\n\nShutting down..."));
73
+ client.disconnect();
74
+ process.exit(0);
75
+ });
76
+ process.on("SIGTERM", () => {
77
+ client.disconnect();
78
+ process.exit(0);
79
+ });
80
+ try {
81
+ await client.connect();
82
+ await new Promise(() => {});
83
+ } catch (error) {
84
+ const err = error;
85
+ console.log(chalk.red(`\n❌ Failed to connect: ${err.message}`));
86
+ console.log(chalk.gray("\nTroubleshooting:"));
87
+ console.log(" 1. Check your internet connection");
88
+ console.log(" 2. Verify your token is valid");
89
+ console.log(" 3. Try 'chaiterm init' to re-authenticate\n");
90
+ process.exit(1);
91
+ }
92
+ }
93
+
94
+ //#endregion
95
+ //#region src/commands/expose.ts
96
+ /**
97
+ * Expose command
98
+ *
99
+ * Exposes a local port to the internet via frp tunnel.
100
+ */
101
+ async function exposeCommand(port) {
102
+ const config = new Config();
103
+ const localPort = parseInt(port, 10);
104
+ if (isNaN(localPort) || localPort < 1 || localPort > 65535) {
105
+ console.log(chalk.red(`\n❌ Invalid port: ${port}`));
106
+ console.log(chalk.gray(" Port must be a number between 1 and 65535\n"));
107
+ process.exit(1);
108
+ }
109
+ console.log(chalk.cyan(`\n🌐 Exposing port ${localPort}...\n`));
110
+ if (!config.isAuthenticated()) {
111
+ console.log(chalk.red("❌ Not authenticated."));
112
+ console.log(chalk.gray(" Run 'chaiterm init' first.\n"));
113
+ process.exit(1);
114
+ }
115
+ const userId = config.getUserId();
116
+ const token = config.getToken();
117
+ const serverAddr = config.getFrpServerAddr();
118
+ const serverPort = config.getFrpServerPort();
119
+ const subdomainId = generateSubdomainId();
120
+ console.log(` User: ${chalk.green(userId)}`);
121
+ console.log(` Local port: ${chalk.green(localPort)}`);
122
+ console.log(` Server: ${chalk.gray(`${serverAddr}:${serverPort}`)}`);
123
+ console.log("");
124
+ config.addSubdomain({
125
+ id: subdomainId,
126
+ localPort,
127
+ active: true
128
+ });
129
+ const frp = new FrpWrapper({
130
+ serverAddr,
131
+ serverPort,
132
+ userId,
133
+ token,
134
+ subdomains: config.getSubdomains(),
135
+ onConnect: () => {
136
+ console.log(chalk.green("✓ Tunnel established"));
137
+ console.log(chalk.cyan(`\n🔗 Your app is available at: https://${subdomainId}.chaiterm.com\n`));
138
+ console.log(chalk.gray("Press Ctrl+C to stop\n"));
139
+ },
140
+ onDisconnect: () => {
141
+ console.log(chalk.yellow("\n⚠️ Tunnel disconnected"));
142
+ },
143
+ onError: (error) => {
144
+ console.log(chalk.red(`\n❌ Error: ${error.message}`));
145
+ },
146
+ onLog: (message) => {
147
+ if (message.includes("error") || message.includes("success")) console.log(chalk.gray(`[frp] ${message.trim()}`));
148
+ }
149
+ });
150
+ process.on("SIGINT", () => {
151
+ console.log(chalk.gray("\n\nShutting down tunnel..."));
152
+ frp.stop();
153
+ config.removeSubdomain(subdomainId);
154
+ process.exit(0);
155
+ });
156
+ process.on("SIGTERM", () => {
157
+ frp.stop();
158
+ config.removeSubdomain(subdomainId);
159
+ process.exit(0);
160
+ });
161
+ try {
162
+ await frp.start();
163
+ await new Promise(() => {});
164
+ } catch (error) {
165
+ const err = error;
166
+ console.log(chalk.red(`\n❌ Failed to establish tunnel: ${err.message}`));
167
+ console.log(chalk.gray("\nTroubleshooting:"));
168
+ console.log(" 1. Ensure frpc is installed (brew install frpc)");
169
+ console.log(" 2. Check that the local port is accessible");
170
+ console.log(" 3. Verify your network allows outbound connections\n");
171
+ config.removeSubdomain(subdomainId);
172
+ process.exit(1);
173
+ }
174
+ }
175
+ /**
176
+ * Generate a random subdomain ID
177
+ * In production, this would come from the server
178
+ */
179
+ function generateSubdomainId() {
180
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
181
+ let result = "";
182
+ for (let i = 0; i < 6; i++) result += chars.charAt(Math.floor(Math.random() * 36));
183
+ return result;
184
+ }
185
+
186
+ //#endregion
187
+ //#region src/commands/status.ts
188
+ /**
189
+ * Status command
190
+ *
191
+ * Shows current connection status and configuration.
192
+ */
193
+ async function statusCommand() {
194
+ const config = new Config();
195
+ console.log(chalk.cyan("\n📊 Chaiterm Status\n"));
196
+ if (config.isAuthenticated()) {
197
+ console.log(chalk.green("✓ Authenticated"));
198
+ console.log(` User ID: ${config.getUserId()}`);
199
+ } else {
200
+ console.log(chalk.yellow("○ Not authenticated"));
201
+ console.log(chalk.gray(" Run 'chaiterm init' to authenticate"));
202
+ }
203
+ console.log("");
204
+ console.log(chalk.gray("Configuration:"));
205
+ console.log(` Gateway: ${config.getGatewayUrl()}`);
206
+ console.log(` frp Server: ${config.getFrpServerAddr()}:${config.getFrpServerPort()}`);
207
+ console.log(` Config file: ${config.getPath()}`);
208
+ console.log("");
209
+ const subdomains = config.getSubdomains();
210
+ if (subdomains.length > 0) {
211
+ console.log(chalk.gray("Configured subdomains:"));
212
+ for (const subdomain of subdomains) {
213
+ const status = subdomain.active ? chalk.green("●") : chalk.gray("○");
214
+ console.log(` ${status} ${subdomain.id}.chaiterm.com → localhost:${subdomain.localPort}`);
215
+ }
216
+ } else {
217
+ console.log(chalk.gray("No subdomains configured"));
218
+ console.log(chalk.gray(" Run 'chaiterm expose <port>' to expose a port"));
219
+ }
220
+ console.log("");
221
+ console.log(chalk.gray("Commands:"));
222
+ console.log(" chaiterm start - Start terminal connection");
223
+ console.log(" chaiterm expose <port> - Expose a local port");
224
+ console.log(" chaiterm logout - Clear credentials");
225
+ console.log("");
226
+ }
227
+
228
+ //#endregion
229
+ //#region src/commands/logout.ts
230
+ /**
231
+ * Logout command
232
+ *
233
+ * Clears stored credentials.
234
+ */
235
+ async function logoutCommand() {
236
+ const config = new Config();
237
+ console.log(chalk.cyan("\n🔒 Logging out...\n"));
238
+ if (!config.isAuthenticated()) {
239
+ console.log(chalk.yellow("⚠️ Not currently authenticated.\n"));
240
+ return;
241
+ }
242
+ const userId = config.getUserId();
243
+ config.clear();
244
+ console.log(chalk.green("✓ Credentials cleared"));
245
+ console.log(` Previous user: ${userId}`);
246
+ console.log(chalk.gray("\n Run 'chaiterm init' to authenticate again.\n"));
247
+ }
248
+
249
+ //#endregion
250
+ //#region src/bin/chaiterm.ts
251
+ /**
252
+ * Chaiterm CLI
253
+ *
254
+ * Main entry point for the chaiterm command line tool.
255
+ */
256
+ const program = new Command();
257
+ program.name("chaiterm").description("Access your terminal from anywhere").version("0.0.1");
258
+ program.command("init").description("Initialize chaiterm and authenticate").action(initCommand);
259
+ program.command("start").description("Start terminal access (connect pty-client to gateway)").action(startCommand);
260
+ program.command("expose").description("Expose a local port to the internet").argument("<port>", "Local port to expose").action(exposeCommand);
261
+ program.command("status").description("Show connection status").action(statusCommand);
262
+ program.command("logout").description("Clear credentials and logout").action(logoutCommand);
263
+ program.showHelpAfterError();
264
+ program.parse();
265
+ if (!process.argv.slice(2).length) {
266
+ console.log(chalk.cyan("\n chaiterm - Access your terminal from anywhere\n"));
267
+ program.outputHelp();
268
+ }
269
+
270
+ //#endregion
271
+ export { };
272
+ //# sourceMappingURL=chaiterm.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chaiterm.mjs","names":[],"sources":["../../src/commands/init.ts","../../src/commands/start.ts","../../src/commands/expose.ts","../../src/commands/status.ts","../../src/commands/logout.ts","../../src/bin/chaiterm.ts"],"sourcesContent":["/**\n * Init command\n *\n * Authenticates user and stores credentials.\n */\n\nimport chalk from \"chalk\";\nimport open from \"open\";\nimport { Config } from \"../lib/config.js\";\n\nexport async function initCommand(): Promise<void> {\n const config = new Config();\n\n console.log(chalk.cyan(\"\\n🚀 Chaiterm Setup\\n\"));\n\n if (config.isAuthenticated()) {\n console.log(chalk.yellow(\"⚠️ You are already authenticated.\"));\n console.log(` User ID: ${config.getUserId()}`);\n console.log(` Config: ${config.getPath()}`);\n console.log(\n chalk.gray(\"\\n Run 'chaiterm logout' to clear credentials.\\n\")\n );\n return;\n }\n\n console.log(\"To use chaiterm, you need to authenticate with your account.\\n\");\n\n // TODO: Implement actual OAuth flow\n // For now, show placeholder\n console.log(chalk.gray(\"Opening browser for authentication...\\n\"));\n\n // This would open the auth URL\n // await open('https://chaiterm.com/cli-auth');\n\n console.log(chalk.yellow(\"⚠️ Authentication not yet implemented.\\n\"));\n console.log(\"For development, you can manually set credentials:\");\n console.log(chalk.gray(` Config file: ${config.getPath()}\\n`));\n\n // Placeholder: In production, this would:\n // 1. Open browser to chaiterm.com/cli-auth\n // 2. User logs in with Google SSO\n // 3. Server generates CLI token\n // 4. Callback URL captures token\n // 5. Store token in config\n\n console.log(chalk.cyan(\"📋 Next steps:\"));\n console.log(\" 1. Sign up at https://chaiterm.com\");\n console.log(\" 2. Get your CLI token from settings\");\n console.log(\" 3. Run 'chaiterm start' to connect your terminal\\n\");\n}\n\n","/**\n * Start command\n *\n * Starts the terminal connection (pty-client).\n */\n\nimport chalk from \"chalk\";\nimport { Config } from \"../lib/config.js\";\nimport { PtyClient } from \"../lib/pty-client.js\";\n\nexport async function startCommand(): Promise<void> {\n const config = new Config();\n\n console.log(chalk.cyan(\"\\n🖥️ Starting terminal connection...\\n\"));\n\n if (!config.isAuthenticated()) {\n console.log(chalk.red(\"❌ Not authenticated.\"));\n console.log(chalk.gray(\" Run 'chaiterm init' first.\\n\"));\n process.exit(1);\n }\n\n const userId = config.getUserId()!;\n const token = config.getToken()!;\n const gatewayUrl = config.getGatewayUrl();\n\n console.log(` User: ${chalk.green(userId)}`);\n console.log(` Gateway: ${chalk.gray(gatewayUrl)}`);\n console.log(\"\");\n\n const client = new PtyClient({\n gatewayUrl,\n userId,\n token,\n onConnect: () => {\n console.log(chalk.green(\"✓ Connected to terminal gateway\"));\n console.log(\n chalk.cyan(`\\n📱 Access your terminal at: https://terminal.chaiterm.com\\n`)\n );\n console.log(chalk.gray(\"Press Ctrl+C to disconnect\\n\"));\n },\n onDisconnect: (code, reason) => {\n console.log(chalk.yellow(`\\n⚠️ Disconnected: ${reason} (code: ${code})`));\n },\n onError: (error) => {\n console.log(chalk.red(`\\n❌ Error: ${error.message}`));\n },\n });\n\n // Handle Ctrl+C\n process.on(\"SIGINT\", () => {\n console.log(chalk.gray(\"\\n\\nShutting down...\"));\n client.disconnect();\n process.exit(0);\n });\n\n process.on(\"SIGTERM\", () => {\n client.disconnect();\n process.exit(0);\n });\n\n try {\n await client.connect();\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(`\\n❌ Failed to connect: ${err.message}`));\n console.log(chalk.gray(\"\\nTroubleshooting:\"));\n console.log(\" 1. Check your internet connection\");\n console.log(\" 2. Verify your token is valid\");\n console.log(\" 3. Try 'chaiterm init' to re-authenticate\\n\");\n process.exit(1);\n }\n}\n\n","/**\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 { 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(`\\n❌ Invalid port: ${port}`));\n console.log(chalk.gray(\" Port must be a number between 1 and 65535\\n\"));\n process.exit(1);\n }\n\n console.log(chalk.cyan(`\\n🌐 Exposing port ${localPort}...\\n`));\n\n if (!config.isAuthenticated()) {\n console.log(chalk.red(\"❌ Not authenticated.\"));\n console.log(chalk.gray(\" Run 'chaiterm init' first.\\n\"));\n process.exit(1);\n }\n\n const userId = config.getUserId()!;\n const token = config.getToken()!;\n const serverAddr = config.getFrpServerAddr();\n const serverPort = config.getFrpServerPort();\n\n // TODO: Request subdomain from server API\n // For now, generate a placeholder random ID\n const subdomainId = generateSubdomainId();\n\n console.log(` User: ${chalk.green(userId)}`);\n console.log(` Local port: ${chalk.green(localPort)}`);\n console.log(` Server: ${chalk.gray(`${serverAddr}:${serverPort}`)}`);\n console.log(\"\");\n\n // Add subdomain to config\n config.addSubdomain({\n id: subdomainId,\n localPort,\n active: true,\n });\n\n const frp = new FrpWrapper({\n serverAddr,\n serverPort,\n userId,\n token,\n subdomains: config.getSubdomains(),\n onConnect: () => {\n console.log(chalk.green(\"✓ Tunnel established\"));\n console.log(\n chalk.cyan(`\\n🔗 Your app is available at: https://${subdomainId}.chaiterm.com\\n`)\n );\n console.log(chalk.gray(\"Press Ctrl+C to stop\\n\"));\n },\n onDisconnect: () => {\n console.log(chalk.yellow(\"\\n⚠️ Tunnel disconnected\"));\n },\n onError: (error) => {\n console.log(chalk.red(`\\n❌ Error: ${error.message}`));\n },\n onLog: (message) => {\n // Only show important logs\n if (message.includes(\"error\") || message.includes(\"success\")) {\n console.log(chalk.gray(`[frp] ${message.trim()}`));\n }\n },\n });\n\n // Handle Ctrl+C\n process.on(\"SIGINT\", () => {\n console.log(chalk.gray(\"\\n\\nShutting down tunnel...\"));\n frp.stop();\n // Remove subdomain from config\n config.removeSubdomain(subdomainId);\n process.exit(0);\n });\n\n process.on(\"SIGTERM\", () => {\n frp.stop();\n config.removeSubdomain(subdomainId);\n process.exit(0);\n });\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(`\\n❌ Failed to establish tunnel: ${err.message}`));\n console.log(chalk.gray(\"\\nTroubleshooting:\"));\n console.log(\" 1. Ensure frpc is installed (brew install frpc)\");\n console.log(\" 2. Check that the local port is accessible\");\n console.log(\" 3. Verify your network allows outbound connections\\n\");\n // Remove subdomain from config on failure\n config.removeSubdomain(subdomainId);\n process.exit(1);\n }\n}\n\n/**\n * Generate a random subdomain ID\n * In production, this would come from the server\n */\nfunction generateSubdomainId(): string {\n const chars = \"abcdefghijklmnopqrstuvwxyz0123456789\";\n let result = \"\";\n for (let i = 0; i < 6; i++) {\n result += chars.charAt(Math.floor(Math.random() * chars.length));\n }\n return result;\n}\n\n","/**\n * Status command\n *\n * Shows current connection status and configuration.\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(\"\\n📊 Chaiterm Status\\n\"));\n\n // Authentication status\n if (config.isAuthenticated()) {\n console.log(chalk.green(\"✓ Authenticated\"));\n console.log(` User ID: ${config.getUserId()}`);\n } else {\n console.log(chalk.yellow(\"○ Not authenticated\"));\n console.log(chalk.gray(\" Run 'chaiterm init' to authenticate\"));\n }\n\n console.log(\"\");\n\n // Gateway configuration\n console.log(chalk.gray(\"Configuration:\"));\n console.log(` Gateway: ${config.getGatewayUrl()}`);\n console.log(` frp Server: ${config.getFrpServerAddr()}:${config.getFrpServerPort()}`);\n console.log(` Config file: ${config.getPath()}`);\n\n console.log(\"\");\n\n // Subdomains\n const subdomains = config.getSubdomains();\n if (subdomains.length > 0) {\n console.log(chalk.gray(\"Configured subdomains:\"));\n for (const subdomain of subdomains) {\n const status = subdomain.active ? chalk.green(\"●\") : chalk.gray(\"○\");\n console.log(\n ` ${status} ${subdomain.id}.chaiterm.com → localhost:${subdomain.localPort}`\n );\n }\n } else {\n console.log(chalk.gray(\"No subdomains configured\"));\n console.log(chalk.gray(\" Run 'chaiterm expose <port>' to expose a port\"));\n }\n\n console.log(\"\");\n\n // Help\n console.log(chalk.gray(\"Commands:\"));\n console.log(\" chaiterm start - Start terminal connection\");\n console.log(\" chaiterm expose <port> - Expose a local port\");\n console.log(\" chaiterm logout - Clear credentials\");\n console.log(\"\");\n}\n\n","/**\n * Logout command\n *\n * Clears stored 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 console.log(chalk.cyan(\"\\n🔒 Logging out...\\n\"));\n\n if (!config.isAuthenticated()) {\n console.log(chalk.yellow(\"⚠️ Not currently authenticated.\\n\"));\n return;\n }\n\n const userId = config.getUserId();\n config.clear();\n\n console.log(chalk.green(\"✓ Credentials cleared\"));\n console.log(` Previous user: ${userId}`);\n console.log(chalk.gray(\"\\n Run 'chaiterm init' to authenticate again.\\n\"));\n}\n\n","#!/usr/bin/env node\n/**\n * Chaiterm CLI\n *\n * Main entry point for the chaiterm command line tool.\n */\n\nimport { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport { initCommand } from \"../commands/init.js\";\nimport { startCommand } from \"../commands/start.js\";\nimport { exposeCommand } from \"../commands/expose.js\";\nimport { statusCommand } from \"../commands/status.js\";\nimport { logoutCommand } from \"../commands/logout.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"chaiterm\")\n .description(\"Access your terminal from anywhere\")\n .version(\"0.0.1\");\n\nprogram\n .command(\"init\")\n .description(\"Initialize chaiterm and authenticate\")\n .action(initCommand);\n\nprogram\n .command(\"start\")\n .description(\"Start terminal access (connect pty-client to gateway)\")\n .action(startCommand);\n\nprogram\n .command(\"expose\")\n .description(\"Expose a local port to the internet\")\n .argument(\"<port>\", \"Local port to expose\")\n .action(exposeCommand);\n\nprogram\n .command(\"status\")\n .description(\"Show connection status\")\n .action(statusCommand);\n\nprogram\n .command(\"logout\")\n .description(\"Clear credentials and logout\")\n .action(logoutCommand);\n\n// Add some helpful output on errors\nprogram.showHelpAfterError();\n\n// Parse arguments\nprogram.parse();\n\n// If no command provided, show help\nif (!process.argv.slice(2).length) {\n console.log(chalk.cyan(\"\\n chaiterm - Access your terminal from anywhere\\n\"));\n program.outputHelp();\n}\n\n"],"mappings":"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"}
@@ -0,0 +1,396 @@
1
+ import WebSocket from "ws";
2
+ import { spawn } from "child_process";
3
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
4
+ import { join } from "path";
5
+ import { tmpdir } from "os";
6
+ import Conf from "conf";
7
+
8
+ //#region src/lib/pty-client.ts
9
+ /**
10
+ * PTY Client
11
+ *
12
+ * Connects to the terminal gateway and spawns a local PTY.
13
+ * This is the core of terminal access functionality.
14
+ */
15
+ var PtyClient = class {
16
+ ws = null;
17
+ pty = null;
18
+ options;
19
+ reconnectTimeout = null;
20
+ shouldReconnect = true;
21
+ constructor(options) {
22
+ this.options = options;
23
+ }
24
+ /**
25
+ * Connect to the terminal gateway
26
+ */
27
+ async connect() {
28
+ const { gatewayUrl, userId, token } = this.options;
29
+ const url = `${gatewayUrl}/register?user=${userId}&token=${token}`;
30
+ console.log(`[pty-client] Connecting to gateway...`);
31
+ return new Promise((resolve, reject) => {
32
+ this.ws = new WebSocket(url);
33
+ this.ws.on("open", () => {
34
+ console.log("[pty-client] Connected to gateway");
35
+ this.options.onConnect?.();
36
+ resolve();
37
+ });
38
+ this.ws.on("message", (raw) => {
39
+ this.handleMessage(raw.toString());
40
+ });
41
+ this.ws.on("close", (code, reason) => {
42
+ console.log(`[pty-client] Disconnected: code=${code}, reason=${reason}`);
43
+ this.cleanup();
44
+ this.options.onDisconnect?.(code, reason.toString());
45
+ this.scheduleReconnect();
46
+ });
47
+ this.ws.on("error", (err) => {
48
+ console.error(`[pty-client] Error: ${err.message}`);
49
+ this.options.onError?.(err);
50
+ reject(err);
51
+ });
52
+ });
53
+ }
54
+ /**
55
+ * Disconnect from gateway
56
+ */
57
+ disconnect() {
58
+ this.shouldReconnect = false;
59
+ if (this.reconnectTimeout) {
60
+ clearTimeout(this.reconnectTimeout);
61
+ this.reconnectTimeout = null;
62
+ }
63
+ this.cleanup();
64
+ if (this.ws) {
65
+ this.ws.close();
66
+ this.ws = null;
67
+ }
68
+ }
69
+ handleMessage(raw) {
70
+ let msg;
71
+ try {
72
+ msg = JSON.parse(raw);
73
+ } catch {
74
+ return;
75
+ }
76
+ switch (msg.kind) {
77
+ case "open":
78
+ this.openPty(msg.cols, msg.rows);
79
+ break;
80
+ case "data":
81
+ this.pty?.write(msg.data);
82
+ break;
83
+ case "resize":
84
+ if (this.pty && msg.cols && msg.rows) this.pty.resize(msg.cols, msg.rows);
85
+ break;
86
+ case "close":
87
+ this.closePty();
88
+ break;
89
+ }
90
+ }
91
+ async openPty(cols, rows) {
92
+ this.closePty();
93
+ const nodePty = await import("node-pty");
94
+ const shell = process.env.SHELL || "/bin/bash";
95
+ const cwd = process.env.HOME || process.cwd();
96
+ console.log(`[pty-client] Opening PTY: ${cols}x${rows}`);
97
+ console.log(`[pty-client] Shell: ${shell}, CWD: ${cwd}`);
98
+ try {
99
+ this.pty = nodePty.spawn(shell, [], {
100
+ name: "xterm-256color",
101
+ cols: cols || 80,
102
+ rows: rows || 24,
103
+ cwd,
104
+ env: process.env
105
+ });
106
+ this.send({
107
+ kind: "ready",
108
+ pid: this.pty.pid
109
+ });
110
+ this.pty.onData((data) => {
111
+ this.send({
112
+ kind: "data",
113
+ data
114
+ });
115
+ });
116
+ this.pty.onExit(({ exitCode, signal }) => {
117
+ console.log(`[pty-client] PTY exited: code=${exitCode}, signal=${signal}`);
118
+ this.send({
119
+ kind: "exit",
120
+ code: exitCode,
121
+ signal
122
+ });
123
+ this.pty = null;
124
+ });
125
+ } catch (err) {
126
+ const error = err;
127
+ console.error(`[pty-client] Failed to spawn PTY: ${error.message}`);
128
+ this.send({
129
+ kind: "data",
130
+ data: `\r\nError: Failed to spawn shell: ${error.message}\r\n`
131
+ });
132
+ }
133
+ }
134
+ closePty() {
135
+ if (this.pty) {
136
+ console.log("[pty-client] Closing PTY");
137
+ try {
138
+ this.pty.kill();
139
+ } catch {}
140
+ this.pty = null;
141
+ }
142
+ }
143
+ send(msg) {
144
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(msg));
145
+ }
146
+ cleanup() {
147
+ this.closePty();
148
+ }
149
+ scheduleReconnect() {
150
+ if (!this.shouldReconnect || this.reconnectTimeout) return;
151
+ console.log("[pty-client] Reconnecting in 3 seconds...");
152
+ this.reconnectTimeout = setTimeout(() => {
153
+ this.reconnectTimeout = null;
154
+ this.connect().catch(() => {});
155
+ }, 3e3);
156
+ }
157
+ };
158
+
159
+ //#endregion
160
+ //#region src/lib/frp-wrapper.ts
161
+ /**
162
+ * frp Wrapper
163
+ *
164
+ * Wraps the frpc binary for port exposure functionality.
165
+ * Generates config and spawns frpc process.
166
+ */
167
+ var FrpWrapper = class {
168
+ options;
169
+ process = null;
170
+ configPath;
171
+ constructor(options) {
172
+ this.options = options;
173
+ this.configPath = join(tmpdir(), "chaiterm", `frpc-${options.userId}.toml`);
174
+ }
175
+ /**
176
+ * Start frpc with current configuration
177
+ */
178
+ async start() {
179
+ const configDir = join(tmpdir(), "chaiterm");
180
+ if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
181
+ this.generateConfig();
182
+ const frpcPath = await this.findFrpcBinary();
183
+ if (!frpcPath) throw new Error("frpc binary not found. Please ensure frpc is installed or bundled.");
184
+ console.log(`[frp-wrapper] Starting frpc with config: ${this.configPath}`);
185
+ return new Promise((resolve, reject) => {
186
+ this.process = spawn(frpcPath, ["-c", this.configPath], { stdio: [
187
+ "ignore",
188
+ "pipe",
189
+ "pipe"
190
+ ] });
191
+ let connected = false;
192
+ this.process.stdout?.on("data", (data) => {
193
+ const message = data.toString();
194
+ this.options.onLog?.(message);
195
+ if (message.includes("start proxy success") && !connected) {
196
+ connected = true;
197
+ this.options.onConnect?.();
198
+ resolve();
199
+ }
200
+ });
201
+ this.process.stderr?.on("data", (data) => {
202
+ const message = data.toString();
203
+ this.options.onLog?.(message);
204
+ if (message.includes("error") || message.includes("Error")) this.options.onError?.(new Error(message));
205
+ });
206
+ this.process.on("error", (err) => {
207
+ this.options.onError?.(err);
208
+ reject(err);
209
+ });
210
+ this.process.on("close", (code) => {
211
+ console.log(`[frp-wrapper] frpc exited with code ${code}`);
212
+ this.process = null;
213
+ this.options.onDisconnect?.();
214
+ });
215
+ setTimeout(() => {
216
+ if (!connected) reject(/* @__PURE__ */ new Error("Connection timeout"));
217
+ }, 3e4);
218
+ });
219
+ }
220
+ /**
221
+ * Stop frpc process
222
+ */
223
+ stop() {
224
+ if (this.process) {
225
+ console.log("[frp-wrapper] Stopping frpc");
226
+ this.process.kill("SIGTERM");
227
+ this.process = null;
228
+ }
229
+ }
230
+ /**
231
+ * Check if frpc is running
232
+ */
233
+ isRunning() {
234
+ return this.process !== null;
235
+ }
236
+ /**
237
+ * Generate frpc config file
238
+ */
239
+ generateConfig() {
240
+ const { serverAddr, serverPort, userId, token, subdomains } = this.options;
241
+ let config = `# Auto-generated by chaiterm CLI
242
+ # User: ${userId}
243
+
244
+ serverAddr = "${serverAddr}"
245
+ serverPort = ${serverPort}
246
+
247
+ # Authentication
248
+ auth.method = "token"
249
+ auth.token = "base-token"
250
+
251
+ # User identification (sent to auth plugin)
252
+ user = "${userId}"
253
+ metadatas.token = "${token}"
254
+
255
+ # Encrypt tunnel
256
+ transport.tls.enable = true
257
+
258
+ # Logging
259
+ log.to = "console"
260
+ log.level = "info"
261
+
262
+ `;
263
+ for (const subdomain of subdomains) if (subdomain.active) config += `
264
+ [[proxies]]
265
+ name = "${userId}-${subdomain.id}"
266
+ type = "http"
267
+ localPort = ${subdomain.localPort}
268
+ subdomain = "${subdomain.id}"
269
+ `;
270
+ writeFileSync(this.configPath, config, "utf-8");
271
+ console.log(`[frp-wrapper] Config written to ${this.configPath}`);
272
+ }
273
+ /**
274
+ * Find frpc binary in various locations
275
+ */
276
+ async findFrpcBinary() {
277
+ const { execSync } = await import("child_process");
278
+ const possiblePaths = [
279
+ "frpc",
280
+ join(process.cwd(), "node_modules", ".bin", "frpc"),
281
+ "/opt/homebrew/bin/frpc",
282
+ "/usr/local/bin/frpc",
283
+ "/usr/bin/frpc"
284
+ ];
285
+ for (const path of possiblePaths) try {
286
+ execSync(`${path} --version`, { stdio: "ignore" });
287
+ return path;
288
+ } catch {
289
+ continue;
290
+ }
291
+ return null;
292
+ }
293
+ };
294
+
295
+ //#endregion
296
+ //#region src/lib/config.ts
297
+ /**
298
+ * Configuration management for chaiterm CLI
299
+ *
300
+ * Stores user credentials and settings in ~/.config/chaiterm/
301
+ */
302
+ const CONFIG_DEFAULTS = {
303
+ gatewayUrl: "wss://terminal.chaiterm.com",
304
+ frpServerAddr: "frp.chaiterm.com",
305
+ frpServerPort: 7e3,
306
+ subdomains: []
307
+ };
308
+ var Config = class {
309
+ store;
310
+ constructor() {
311
+ this.store = new Conf({
312
+ projectName: "chaiterm",
313
+ defaults: CONFIG_DEFAULTS
314
+ });
315
+ }
316
+ /**
317
+ * Check if user is authenticated
318
+ */
319
+ isAuthenticated() {
320
+ return !!this.store.get("token") && !!this.store.get("userId");
321
+ }
322
+ /**
323
+ * Get authentication token
324
+ */
325
+ getToken() {
326
+ return this.store.get("token");
327
+ }
328
+ /**
329
+ * Get user ID
330
+ */
331
+ getUserId() {
332
+ return this.store.get("userId");
333
+ }
334
+ /**
335
+ * Set authentication credentials
336
+ */
337
+ setCredentials(token, userId) {
338
+ this.store.set("token", token);
339
+ this.store.set("userId", userId);
340
+ }
341
+ /**
342
+ * Get gateway URL for terminal access
343
+ */
344
+ getGatewayUrl() {
345
+ return this.store.get("gatewayUrl") || CONFIG_DEFAULTS.gatewayUrl;
346
+ }
347
+ /**
348
+ * Get frp server address
349
+ */
350
+ getFrpServerAddr() {
351
+ return this.store.get("frpServerAddr") || CONFIG_DEFAULTS.frpServerAddr;
352
+ }
353
+ /**
354
+ * Get frp server port
355
+ */
356
+ getFrpServerPort() {
357
+ return this.store.get("frpServerPort") || CONFIG_DEFAULTS.frpServerPort;
358
+ }
359
+ /**
360
+ * Get all configured subdomains
361
+ */
362
+ getSubdomains() {
363
+ return this.store.get("subdomains") || [];
364
+ }
365
+ /**
366
+ * Add a subdomain configuration
367
+ */
368
+ addSubdomain(subdomain) {
369
+ const subdomains = this.getSubdomains();
370
+ subdomains.push(subdomain);
371
+ this.store.set("subdomains", subdomains);
372
+ }
373
+ /**
374
+ * Remove a subdomain configuration
375
+ */
376
+ removeSubdomain(id) {
377
+ const subdomains = this.getSubdomains().filter((s) => s.id !== id);
378
+ this.store.set("subdomains", subdomains);
379
+ }
380
+ /**
381
+ * Clear all configuration (logout)
382
+ */
383
+ clear() {
384
+ this.store.clear();
385
+ }
386
+ /**
387
+ * Get config file path (for debugging)
388
+ */
389
+ getPath() {
390
+ return this.store.path;
391
+ }
392
+ };
393
+
394
+ //#endregion
395
+ export { FrpWrapper as n, PtyClient as r, Config as t };
396
+ //# sourceMappingURL=config-CGPKYUde.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-CGPKYUde.mjs","names":["msg: TerminalMessage","CONFIG_DEFAULTS: Partial<ChaitermConfig>"],"sources":["../src/lib/pty-client.ts","../src/lib/frp-wrapper.ts","../src/lib/config.ts"],"sourcesContent":["/**\n * PTY Client\n *\n * Connects to the terminal gateway and spawns a local PTY.\n * This is the core of terminal access functionality.\n */\n\nimport WebSocket from \"ws\";\nimport type { IPty } from \"node-pty\";\nimport type { TerminalMessage } from \"../types.js\";\n\nexport interface PtyClientOptions {\n gatewayUrl: string;\n userId: string;\n token: string;\n onConnect?: () => void;\n onDisconnect?: (code: number, reason: string) => void;\n onError?: (error: Error) => void;\n}\n\nexport class PtyClient {\n private ws: WebSocket | null = null;\n private pty: IPty | null = null;\n private options: PtyClientOptions;\n private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;\n private shouldReconnect = true;\n\n constructor(options: PtyClientOptions) {\n this.options = options;\n }\n\n /**\n * Connect to the terminal gateway\n */\n async connect(): Promise<void> {\n const { gatewayUrl, userId, token } = this.options;\n const url = `${gatewayUrl}/register?user=${userId}&token=${token}`;\n\n console.log(`[pty-client] Connecting to gateway...`);\n\n return new Promise((resolve, reject) => {\n this.ws = new WebSocket(url);\n\n this.ws.on(\"open\", () => {\n console.log(\"[pty-client] Connected to gateway\");\n this.options.onConnect?.();\n resolve();\n });\n\n this.ws.on(\"message\", (raw) => {\n this.handleMessage(raw.toString());\n });\n\n this.ws.on(\"close\", (code, reason) => {\n console.log(\n `[pty-client] Disconnected: code=${code}, reason=${reason}`\n );\n this.cleanup();\n this.options.onDisconnect?.(code, reason.toString());\n this.scheduleReconnect();\n });\n\n this.ws.on(\"error\", (err) => {\n console.error(`[pty-client] Error: ${err.message}`);\n this.options.onError?.(err);\n reject(err);\n });\n });\n }\n\n /**\n * Disconnect from gateway\n */\n disconnect(): void {\n this.shouldReconnect = false;\n if (this.reconnectTimeout) {\n clearTimeout(this.reconnectTimeout);\n this.reconnectTimeout = null;\n }\n this.cleanup();\n if (this.ws) {\n this.ws.close();\n this.ws = null;\n }\n }\n\n private handleMessage(raw: string): void {\n let msg: TerminalMessage;\n try {\n msg = JSON.parse(raw);\n } catch {\n return;\n }\n\n switch (msg.kind) {\n case \"open\":\n this.openPty(msg.cols, msg.rows);\n break;\n case \"data\":\n this.pty?.write(msg.data);\n break;\n case \"resize\":\n if (this.pty && msg.cols && msg.rows) {\n this.pty.resize(msg.cols, msg.rows);\n }\n break;\n case \"close\":\n this.closePty();\n break;\n }\n }\n\n private async openPty(cols: number, rows: number): Promise<void> {\n // Close existing PTY if any\n this.closePty();\n\n // Dynamic import for node-pty (native module)\n const nodePty = await import(\"node-pty\");\n\n const shell = process.env.SHELL || \"/bin/bash\";\n const cwd = process.env.HOME || process.cwd();\n\n console.log(`[pty-client] Opening PTY: ${cols}x${rows}`);\n console.log(`[pty-client] Shell: ${shell}, CWD: ${cwd}`);\n\n try {\n this.pty = nodePty.spawn(shell, [], {\n name: \"xterm-256color\",\n cols: cols || 80,\n rows: rows || 24,\n cwd: cwd,\n env: process.env as Record<string, string>,\n });\n\n // Send ready message\n this.send({ kind: \"ready\", pid: this.pty.pid });\n\n // Forward PTY output to gateway\n this.pty.onData((data: string) => {\n this.send({ kind: \"data\", data });\n });\n\n // Handle PTY exit\n this.pty.onExit(({ exitCode, signal }) => {\n console.log(\n `[pty-client] PTY exited: code=${exitCode}, signal=${signal}`\n );\n this.send({ kind: \"exit\", code: exitCode, signal });\n this.pty = null;\n });\n } catch (err) {\n const error = err as Error;\n console.error(`[pty-client] Failed to spawn PTY: ${error.message}`);\n this.send({\n kind: \"data\",\n data: `\\r\\nError: Failed to spawn shell: ${error.message}\\r\\n`,\n });\n }\n }\n\n private closePty(): void {\n if (this.pty) {\n console.log(\"[pty-client] Closing PTY\");\n try {\n this.pty.kill();\n } catch {\n // Ignore errors\n }\n this.pty = null;\n }\n }\n\n private send(msg: TerminalMessage): void {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(msg));\n }\n }\n\n private cleanup(): void {\n this.closePty();\n }\n\n private scheduleReconnect(): void {\n if (!this.shouldReconnect || this.reconnectTimeout) return;\n\n console.log(\"[pty-client] Reconnecting in 3 seconds...\");\n this.reconnectTimeout = setTimeout(() => {\n this.reconnectTimeout = null;\n this.connect().catch(() => {\n // Will retry on next disconnect\n });\n }, 3000);\n }\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\";\nimport type { SubdomainConfig } from \"../types.js\";\n\nexport interface FrpWrapperOptions {\n serverAddr: string;\n serverPort: number;\n userId: string;\n token: string;\n subdomains: SubdomainConfig[];\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(tmpdir(), \"chaiterm\", `frpc-${options.userId}.toml`);\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(), \"chaiterm\");\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 ensure frpc is installed or bundled.\"\n );\n }\n\n console.log(`[frp-wrapper] Starting frpc with config: ${this.configPath}`);\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\", (code) => {\n console.log(`[frp-wrapper] frpc exited with code ${code}`);\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\"));\n }\n }, 30000);\n });\n }\n\n /**\n * Stop frpc process\n */\n stop(): void {\n if (this.process) {\n console.log(\"[frp-wrapper] Stopping frpc\");\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, userId, token, subdomains } = this.options;\n\n let config = `# Auto-generated by chaiterm CLI\n# User: ${userId}\n\nserverAddr = \"${serverAddr}\"\nserverPort = ${serverPort}\n\n# Authentication\nauth.method = \"token\"\nauth.token = \"base-token\"\n\n# User identification (sent to auth plugin)\nuser = \"${userId}\"\nmetadatas.token = \"${token}\"\n\n# Encrypt tunnel\ntransport.tls.enable = true\n\n# Logging\nlog.to = \"console\"\nlog.level = \"info\"\n\n`;\n\n // Add proxy configurations for each subdomain\n for (const subdomain of subdomains) {\n if (subdomain.active) {\n config += `\n[[proxies]]\nname = \"${userId}-${subdomain.id}\"\ntype = \"http\"\nlocalPort = ${subdomain.localPort}\nsubdomain = \"${subdomain.id}\"\n`;\n }\n }\n\n writeFileSync(this.configPath, config, \"utf-8\");\n console.log(`[frp-wrapper] Config written to ${this.configPath}`);\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 // Check common locations\n const possiblePaths = [\n // In PATH\n \"frpc\",\n // Local node_modules bin\n join(process.cwd(), \"node_modules\", \".bin\", \"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\n","/**\n * Configuration management for chaiterm CLI\n *\n * Stores user credentials and settings in ~/.config/chaiterm/\n */\n\nimport Conf from \"conf\";\nimport type { ChaitermConfig, SubdomainConfig } from \"../types.js\";\n\nconst CONFIG_DEFAULTS: Partial<ChaitermConfig> = {\n gatewayUrl: \"wss://terminal.chaiterm.com\",\n frpServerAddr: \"frp.chaiterm.com\",\n frpServerPort: 7000,\n subdomains: [],\n};\n\nexport class Config {\n private store: Conf<ChaitermConfig>;\n\n constructor() {\n this.store = new Conf<ChaitermConfig>({\n projectName: \"chaiterm\",\n defaults: CONFIG_DEFAULTS as ChaitermConfig,\n });\n }\n\n /**\n * Check if user is authenticated\n */\n isAuthenticated(): boolean {\n return !!this.store.get(\"token\") && !!this.store.get(\"userId\");\n }\n\n /**\n * Get authentication token\n */\n getToken(): string | undefined {\n return this.store.get(\"token\");\n }\n\n /**\n * Get user ID\n */\n getUserId(): string | undefined {\n return this.store.get(\"userId\");\n }\n\n /**\n * Set authentication credentials\n */\n setCredentials(token: string, userId: string): void {\n this.store.set(\"token\", token);\n this.store.set(\"userId\", userId);\n }\n\n /**\n * Get gateway URL for terminal access\n */\n getGatewayUrl(): string {\n return this.store.get(\"gatewayUrl\") || CONFIG_DEFAULTS.gatewayUrl!;\n }\n\n /**\n * Get frp server address\n */\n getFrpServerAddr(): string {\n return this.store.get(\"frpServerAddr\") || CONFIG_DEFAULTS.frpServerAddr!;\n }\n\n /**\n * Get frp server port\n */\n getFrpServerPort(): number {\n return this.store.get(\"frpServerPort\") || CONFIG_DEFAULTS.frpServerPort!;\n }\n\n /**\n * Get all configured subdomains\n */\n getSubdomains(): SubdomainConfig[] {\n return this.store.get(\"subdomains\") || [];\n }\n\n /**\n * Add a subdomain configuration\n */\n addSubdomain(subdomain: SubdomainConfig): void {\n const subdomains = this.getSubdomains();\n subdomains.push(subdomain);\n this.store.set(\"subdomains\", subdomains);\n }\n\n /**\n * Remove a subdomain configuration\n */\n removeSubdomain(id: string): void {\n const subdomains = this.getSubdomains().filter((s) => s.id !== id);\n this.store.set(\"subdomains\", subdomains);\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"],"mappings":"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"}
@@ -0,0 +1,191 @@
1
+ //#region src/lib/pty-client.d.ts
2
+ /**
3
+ * PTY Client
4
+ *
5
+ * Connects to the terminal gateway and spawns a local PTY.
6
+ * This is the core of terminal access functionality.
7
+ */
8
+ interface PtyClientOptions {
9
+ gatewayUrl: string;
10
+ userId: string;
11
+ token: string;
12
+ onConnect?: () => void;
13
+ onDisconnect?: (code: number, reason: string) => void;
14
+ onError?: (error: Error) => void;
15
+ }
16
+ declare class PtyClient {
17
+ private ws;
18
+ private pty;
19
+ private options;
20
+ private reconnectTimeout;
21
+ private shouldReconnect;
22
+ constructor(options: PtyClientOptions);
23
+ /**
24
+ * Connect to the terminal gateway
25
+ */
26
+ connect(): Promise<void>;
27
+ /**
28
+ * Disconnect from gateway
29
+ */
30
+ disconnect(): void;
31
+ private handleMessage;
32
+ private openPty;
33
+ private closePty;
34
+ private send;
35
+ private cleanup;
36
+ private scheduleReconnect;
37
+ }
38
+ //#endregion
39
+ //#region src/types.d.ts
40
+ /**
41
+ * Shared types for chaiterm CLI
42
+ */
43
+ interface ChaitermConfig {
44
+ /** User's authentication token */
45
+ token: string;
46
+ /** User ID from server */
47
+ userId: string;
48
+ /** Gateway URL for terminal access */
49
+ gatewayUrl: string;
50
+ /** frp server address */
51
+ frpServerAddr: string;
52
+ /** frp server port */
53
+ frpServerPort: number;
54
+ /** Assigned subdomains */
55
+ subdomains: SubdomainConfig[];
56
+ }
57
+ interface SubdomainConfig {
58
+ /** Random subdomain ID (e.g., "k7x9m2") */
59
+ id: string;
60
+ /** Local port to expose */
61
+ localPort: number;
62
+ /** Whether this subdomain is active */
63
+ active: boolean;
64
+ }
65
+ interface ConnectionStatus {
66
+ terminal: "connected" | "disconnected" | "connecting";
67
+ tunnels: TunnelStatus[];
68
+ }
69
+ interface TunnelStatus {
70
+ subdomain: string;
71
+ localPort: number;
72
+ status: "connected" | "disconnected" | "connecting";
73
+ }
74
+ /** Message types for terminal communication */
75
+ type TerminalMessage = {
76
+ kind: "open";
77
+ cols: number;
78
+ rows: number;
79
+ } | {
80
+ kind: "data";
81
+ data: string;
82
+ } | {
83
+ kind: "resize";
84
+ cols: number;
85
+ rows: number;
86
+ } | {
87
+ kind: "close";
88
+ } | {
89
+ kind: "ready";
90
+ pid: number;
91
+ } | {
92
+ kind: "exit";
93
+ code: number;
94
+ signal?: number;
95
+ };
96
+ //#endregion
97
+ //#region src/lib/frp-wrapper.d.ts
98
+ interface FrpWrapperOptions {
99
+ serverAddr: string;
100
+ serverPort: number;
101
+ userId: string;
102
+ token: string;
103
+ subdomains: SubdomainConfig[];
104
+ onConnect?: () => void;
105
+ onDisconnect?: () => void;
106
+ onError?: (error: Error) => void;
107
+ onLog?: (message: string) => void;
108
+ }
109
+ declare class FrpWrapper {
110
+ private options;
111
+ private process;
112
+ private configPath;
113
+ constructor(options: FrpWrapperOptions);
114
+ /**
115
+ * Start frpc with current configuration
116
+ */
117
+ start(): Promise<void>;
118
+ /**
119
+ * Stop frpc process
120
+ */
121
+ stop(): void;
122
+ /**
123
+ * Check if frpc is running
124
+ */
125
+ isRunning(): boolean;
126
+ /**
127
+ * Generate frpc config file
128
+ */
129
+ private generateConfig;
130
+ /**
131
+ * Find frpc binary in various locations
132
+ */
133
+ private findFrpcBinary;
134
+ }
135
+ //#endregion
136
+ //#region src/lib/config.d.ts
137
+ declare class Config {
138
+ private store;
139
+ constructor();
140
+ /**
141
+ * Check if user is authenticated
142
+ */
143
+ isAuthenticated(): boolean;
144
+ /**
145
+ * Get authentication token
146
+ */
147
+ getToken(): string | undefined;
148
+ /**
149
+ * Get user ID
150
+ */
151
+ getUserId(): string | undefined;
152
+ /**
153
+ * Set authentication credentials
154
+ */
155
+ setCredentials(token: string, userId: string): void;
156
+ /**
157
+ * Get gateway URL for terminal access
158
+ */
159
+ getGatewayUrl(): string;
160
+ /**
161
+ * Get frp server address
162
+ */
163
+ getFrpServerAddr(): string;
164
+ /**
165
+ * Get frp server port
166
+ */
167
+ getFrpServerPort(): number;
168
+ /**
169
+ * Get all configured subdomains
170
+ */
171
+ getSubdomains(): SubdomainConfig[];
172
+ /**
173
+ * Add a subdomain configuration
174
+ */
175
+ addSubdomain(subdomain: SubdomainConfig): void;
176
+ /**
177
+ * Remove a subdomain configuration
178
+ */
179
+ removeSubdomain(id: string): void;
180
+ /**
181
+ * Clear all configuration (logout)
182
+ */
183
+ clear(): void;
184
+ /**
185
+ * Get config file path (for debugging)
186
+ */
187
+ getPath(): string;
188
+ }
189
+ //#endregion
190
+ export { ChaitermConfig, Config, ConnectionStatus, FrpWrapper, PtyClient, SubdomainConfig, TerminalMessage, TunnelStatus };
191
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/lib/pty-client.ts","../src/types.ts","../src/lib/frp-wrapper.ts","../src/lib/config.ts"],"sourcesContent":[],"mappings":";;AAWA;AASA;;;;AChBiB,UDOA,gBAAA,CCKH;EAGG,UAAA,EAAA,MAAe;EASf,MAAA,EAAA,MAAA;EAKA,KAAA,EAAA,MAAA;EAOL,SAAA,CAAA,EAAA,GAAA,GAAe,IAAA;;oBDvBP;;AEJH,cFOJ,SAAA,CEPqB;EAYrB,QAAA,EAAA;;;;ECTA,QAAA,eA+DM;uBHpDI;;;;aAOJ;;;;;;;;;;;;;;;AAvBnB;AASA;UChBiB,cAAA;;;EAAA;EAeA,MAAA,EAAA,MAAA;EASA;EAKA,UAAA,EAAA,MAAY;EAOjB;;;;EC3BK;EAYJ,UAAA,EDTC,eCcS,EAAA;;UDXN,eAAA;;EEHJ,EAAA,EAAA,MAAM;;;;;;UFYF,gBAAA;;WAEN;;UAGM,YAAA;;;;;;KAOL,eAAA;;;;;;;;;;;;;;;;;;;;;;;AArBK,UCNA,iBAAA,CDMe;EASf,UAAA,EAAA,MAAA;EAKA,UAAA,EAAA,MAAY;EAOjB,MAAA,EAAA,MAAA;;cCtBE;;EALG,YAAA,CAAA,EAAA,GAAA,GAAiB,IAAA;EAYrB,OAAA,CAAA,EAAA,CAAA,KAAU,EAJH,KAIG,EAKA,GAAA,IAAA;;;cALV,UAAA;ECTA,QAAA,OAAM;;;uBDcI;;;;WAQN;;;;;;;;;;;;;;;;;;;;ADlCA,cEYJ,MAAA,CFZkB;EAed,QAAA,KAAA;EASA,WAAA,CAAA;EAKA;AAOjB;;;;AC3BA;AAYA;;;;ACTA;;;;;;;;;;;;;;;;;;;;;mBA+DmB;;;;0BAOO"}
package/dist/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import { n as FrpWrapper, r as PtyClient, t as Config } from "./config-CGPKYUde.mjs";
2
+
3
+ export { Config, FrpWrapper, PtyClient };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "chaiterm",
3
+ "version": "0.0.1",
4
+ "description": "Access your terminal from anywhere - CLI tool for chaiterm",
5
+ "type": "module",
6
+ "bin": {
7
+ "chaiterm": "./dist/bin/chaiterm.mjs"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "default": "./dist/index.mjs"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "bin"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsdown",
20
+ "dev": "tsdown --watch",
21
+ "check-types": "tsc --noEmit"
22
+ },
23
+ "keywords": [
24
+ "terminal",
25
+ "remote",
26
+ "cli",
27
+ "pty",
28
+ "tunnel"
29
+ ],
30
+ "author": "",
31
+ "license": "MIT",
32
+ "devDependencies": {
33
+ "typescript": "catalog:",
34
+ "@app/config": "workspace:*",
35
+ "@types/node": "^22.15.29",
36
+ "@types/ws": "^8.18.1",
37
+ "tsdown": "^0.18.3"
38
+ },
39
+ "dependencies": {
40
+ "commander": "^13.1.0",
41
+ "node-pty": "^1.0.0",
42
+ "ws": "^8.18.2",
43
+ "chalk": "^5.4.1",
44
+ "conf": "^13.1.0",
45
+ "open": "^10.2.0"
46
+ },
47
+ "engines": {
48
+ "node": ">=18"
49
+ },
50
+ "optionalDependencies": {}
51
+ }
52
+