chaiterm 0.0.1 → 0.0.3

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 CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  Command-line tool for accessing your terminal from anywhere.
4
4
 
5
+ > **⚠️ Under Development**
6
+ >
7
+ > This package is currently under active development and is **not ready for production use**. Features may be incomplete, unstable, or subject to breaking changes. Use at your own risk.
8
+
5
9
  ## Installation
6
10
 
7
11
  ```bash
@@ -26,7 +30,7 @@ Start terminal access - connects your local PTY to the gateway.
26
30
  chaiterm start
27
31
  ```
28
32
 
29
- Once connected, access your terminal at: `https://terminal.chaiterm.com`
33
+ Once connected, access your terminal at: `https://chaiterm.com/terminals`
30
34
 
31
35
  ### `chaiterm expose <port>`
32
36
 
@@ -54,17 +58,55 @@ Clear stored credentials.
54
58
  chaiterm logout
55
59
  ```
56
60
 
61
+ ### `chaiterm kill`
62
+
63
+ Kill all running chaiterm processes (useful when PTY instances get stuck).
64
+
65
+ ```bash
66
+ chaiterm kill
67
+ ```
68
+
57
69
  ## Architecture
58
70
 
59
71
  The CLI wraps two components:
60
72
 
61
- 1. **pty-client** - Connects to `terminal.chaiterm.com` via WebSocket, spawns local PTY
73
+ 1. **pty-client** - Connects to `chaiterm.com/ws/pty` via WebSocket, spawns local PTY
62
74
  2. **frp-wrapper** - Wraps frpc binary for port exposure tunnels
63
75
 
64
76
  ## Configuration
65
77
 
66
78
  Config is stored in `~/.config/chaiterm/config.json` (or platform equivalent).
67
79
 
80
+ ### Using Custom Config Files
81
+
82
+ Use the `--config` (or `-c`) flag to specify a different config file:
83
+
84
+ ```bash
85
+ # Use a local development config
86
+ chaiterm --config ./local-config.json start
87
+
88
+ # Use a specific config file
89
+ chaiterm -c ~/.config/chaiterm/dev.json status
90
+ ```
91
+
92
+ ### Config File Format
93
+
94
+ ```json
95
+ {
96
+ "email": "your-email@example.com",
97
+ "gatewayUrl": "wss://chaiterm.com/ws/pty",
98
+ "frpServerAddr": "frp.chaiterm.com",
99
+ "frpServerPort": 7000
100
+ }
101
+ ```
102
+
103
+ | Field | Description | Default |
104
+ |-------|-------------|---------|
105
+ | `email` | Your email for terminal access | (required) |
106
+ | `gatewayUrl` | Terminal gateway WebSocket URL | `wss://chaiterm.com/ws/pty` |
107
+ | `frpServerAddr` | frp server address | `frp.chaiterm.com` |
108
+ | `frpServerPort` | frp server port | `7000` |
109
+
68
110
  ## Requirements
69
111
 
70
112
  - Node.js 18+
@@ -83,3 +125,29 @@ bun run build
83
125
  bun run dev
84
126
  ```
85
127
 
128
+ ### Local Development
129
+
130
+ To test CLI against a local server:
131
+
132
+ ```bash
133
+ cd packages/cli
134
+
135
+ # 1. Create a local config file
136
+ echo '{
137
+ "email": "your-email@example.com",
138
+ "gatewayUrl": "ws://localhost:3000/ws/pty"
139
+ }' > local-config.json
140
+
141
+ # 2. Build and run with the local config
142
+ bun run build
143
+ node dist/bin/chaiterm.mjs --config ./local-config.json start
144
+ ```
145
+
146
+ ### Testing Remote Server
147
+
148
+ To test against the production server, use the default config (no --config flag):
149
+
150
+ ```bash
151
+ node dist/bin/chaiterm.mjs start
152
+ ```
153
+
@@ -1,34 +1,70 @@
1
1
  #!/usr/bin/env node
2
- import { n as FrpWrapper, r as PtyClient, t as Config } from "../config-CGPKYUde.mjs";
2
+ import { i as PtyClient, n as setConfigPath, r as FrpWrapper, t as Config } from "../config-2bJzN_Fc.mjs";
3
+ import { execSync } from "child_process";
3
4
  import { Command } from "commander";
4
5
  import chalk from "chalk";
5
- import "open";
6
+ import { createRequire } from "module";
7
+ import readline from "readline";
6
8
 
7
9
  //#region src/commands/init.ts
8
10
  /**
9
11
  * Init command
10
12
  *
11
- * Authenticates user and stores credentials.
13
+ * Sets up chaiterm with user's email.
14
+ * Email is used to identify which terminal belongs to which user.
15
+ *
16
+ * Usage:
17
+ * chaiterm init # prompts for email
18
+ * chaiterm init user@example.com # sets email directly
19
+ */
20
+ /**
21
+ * Prompt user for input
22
+ */
23
+ function prompt(question) {
24
+ const rl = readline.createInterface({
25
+ input: process.stdin,
26
+ output: process.stdout
27
+ });
28
+ return new Promise((resolve) => {
29
+ rl.question(question, (answer) => {
30
+ rl.close();
31
+ resolve(answer.trim());
32
+ });
33
+ });
34
+ }
35
+ /**
36
+ * Basic email validation
12
37
  */
13
- async function initCommand() {
38
+ function isValidEmail(email) {
39
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
40
+ }
41
+ async function initCommand(emailArg) {
14
42
  const config = new Config();
15
43
  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()}`);
44
+ const existingEmail = config.getEmail();
45
+ if (existingEmail) {
46
+ console.log(chalk.yellow("⚠️ Already configured."));
47
+ console.log(` Email: ${existingEmail}`);
19
48
  console.log(` Config: ${config.getPath()}`);
20
- console.log(chalk.gray("\n Run 'chaiterm logout' to clear credentials.\n"));
49
+ console.log(chalk.gray("\n Run 'chaiterm logout' to reset configuration.\n"));
21
50
  return;
22
51
  }
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");
52
+ let email = emailArg;
53
+ if (!email) {
54
+ console.log("Enter the email you use to sign in at chaiterm.com\n");
55
+ email = await prompt(chalk.white(" Email: "));
56
+ }
57
+ if (!email || !isValidEmail(email)) {
58
+ console.log(chalk.red("\n❌ Invalid email address.\n"));
59
+ process.exit(1);
60
+ }
61
+ config.setEmail(email);
62
+ console.log(chalk.green("\n✓ Configuration saved!\n"));
63
+ console.log(` Email: ${email}`);
64
+ console.log(` Config: ${config.getPath()}`);
65
+ console.log(chalk.cyan("\n📋 Next steps:"));
66
+ console.log(" 1. Sign up at https://chaiterm.com (if you haven't)");
67
+ console.log(" 2. Run 'chaiterm start' to connect your terminal\n");
32
68
  }
33
69
 
34
70
  //#endregion
@@ -37,28 +73,32 @@ async function initCommand() {
37
73
  * Start command
38
74
  *
39
75
  * Starts the terminal connection (pty-client).
76
+ *
77
+ * Note: Token is NOT needed for terminal access.
78
+ * Security is enforced at browser connection via session cookie.
40
79
  */
41
80
  async function startCommand() {
42
81
  const config = new Config();
43
82
  console.log(chalk.cyan("\n🖥️ Starting terminal connection...\n"));
44
- if (!config.isAuthenticated()) {
45
- console.log(chalk.red("❌ Not authenticated."));
83
+ const email = config.getEmail();
84
+ if (!email) {
85
+ console.log(chalk.red("❌ Not configured."));
46
86
  console.log(chalk.gray(" Run 'chaiterm init' first.\n"));
47
87
  process.exit(1);
48
88
  }
49
- const userId = config.getUserId();
50
- const token = config.getToken();
51
89
  const gatewayUrl = config.getGatewayUrl();
52
- console.log(` User: ${chalk.green(userId)}`);
90
+ console.log(chalk.gray(" Config: ") + config.getPath());
91
+ if (config.isCustomConfig()) console.log(chalk.gray(" (custom config via --config flag)"));
92
+ console.log("");
93
+ console.log(` Email: ${chalk.green(email)}`);
53
94
  console.log(` Gateway: ${chalk.gray(gatewayUrl)}`);
54
95
  console.log("");
55
96
  const client = new PtyClient({
56
97
  gatewayUrl,
57
- userId,
58
- token,
98
+ email,
59
99
  onConnect: () => {
60
100
  console.log(chalk.green("✓ Connected to terminal gateway"));
61
- console.log(chalk.cyan(`\n📱 Access your terminal at: https://terminal.chaiterm.com\n`));
101
+ console.log(chalk.cyan(`\n📱 Access your terminal at: https://chaiterm.com/terminals\n`));
62
102
  console.log(chalk.gray("Press Ctrl+C to disconnect\n"));
63
103
  },
64
104
  onDisconnect: (code, reason) => {
@@ -85,8 +125,7 @@ async function startCommand() {
85
125
  console.log(chalk.red(`\n❌ Failed to connect: ${err.message}`));
86
126
  console.log(chalk.gray("\nTroubleshooting:"));
87
127
  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");
128
+ console.log(" 2. Try 'chaiterm init' to re-configure\n");
90
129
  process.exit(1);
91
130
  }
92
131
  }
@@ -193,35 +232,22 @@ function generateSubdomainId() {
193
232
  async function statusCommand() {
194
233
  const config = new Config();
195
234
  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()}`);
235
+ const email = config.getEmail();
236
+ if (email) {
237
+ console.log(chalk.green("✓ Configured"));
238
+ console.log(` Email: ${email}`);
199
239
  } else {
200
- console.log(chalk.yellow("○ Not authenticated"));
201
- console.log(chalk.gray(" Run 'chaiterm init' to authenticate"));
240
+ console.log(chalk.yellow("○ Not configured"));
241
+ console.log(chalk.gray(" Run 'chaiterm init' to set up"));
202
242
  }
203
243
  console.log("");
204
244
  console.log(chalk.gray("Configuration:"));
205
245
  console.log(` Gateway: ${config.getGatewayUrl()}`);
206
- console.log(` frp Server: ${config.getFrpServerAddr()}:${config.getFrpServerPort()}`);
207
246
  console.log(` Config file: ${config.getPath()}`);
208
247
  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
248
  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");
249
+ console.log(" chaiterm start - Start terminal connection");
250
+ console.log(" chaiterm logout - Clear configuration");
225
251
  console.log("");
226
252
  }
227
253
 
@@ -230,20 +256,64 @@ async function statusCommand() {
230
256
  /**
231
257
  * Logout command
232
258
  *
233
- * Clears stored credentials.
259
+ * Clears stored configuration.
234
260
  */
235
261
  async function logoutCommand() {
236
262
  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"));
263
+ console.log(chalk.cyan("\n🔒 Clearing configuration...\n"));
264
+ const email = config.getEmail();
265
+ if (!email) {
266
+ console.log(chalk.yellow("⚠️ Not configured.\n"));
240
267
  return;
241
268
  }
242
- const userId = config.getUserId();
243
269
  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"));
270
+ console.log(chalk.green("✓ Configuration cleared"));
271
+ console.log(` Previous email: ${email}`);
272
+ console.log(chalk.gray("\n Run 'chaiterm init' to configure again.\n"));
273
+ }
274
+
275
+ //#endregion
276
+ //#region src/commands/kill.ts
277
+ /**
278
+ * Kill command
279
+ *
280
+ * Terminates any running chaiterm processes (node-pty instances).
281
+ */
282
+ async function killCommand() {
283
+ console.log(chalk.cyan("\n🔍 Looking for running chaiterm processes...\n"));
284
+ try {
285
+ const patterns = [
286
+ "chaiterm.mjs",
287
+ "chaiterm.js",
288
+ "pty-client",
289
+ "node-pty"
290
+ ];
291
+ let foundAny = false;
292
+ for (const pattern of patterns) try {
293
+ const result = execSync(`pgrep -f "${pattern}" | grep -v "^${process.pid}$" || true`, { encoding: "utf-8" }).trim();
294
+ if (result) {
295
+ const pids = result.split("\n").filter(Boolean);
296
+ for (const pid of pids) try {
297
+ const processInfo = execSync(`ps -p ${pid} -o command= 2>/dev/null || true`, { encoding: "utf-8" }).trim();
298
+ if (processInfo && processInfo.includes("chaiterm")) {
299
+ process.kill(parseInt(pid, 10), "SIGTERM");
300
+ console.log(chalk.yellow(` Killed PID ${pid}: ${processInfo.substring(0, 60)}...`));
301
+ foundAny = true;
302
+ }
303
+ } catch {}
304
+ }
305
+ } catch {}
306
+ try {
307
+ execSync("pkill -f \"chaiterm.mjs\" 2>/dev/null || true");
308
+ execSync("pkill -f \"chaiterm start\" 2>/dev/null || true");
309
+ } catch {}
310
+ if (foundAny) console.log(chalk.green("\n✓ Killed chaiterm processes\n"));
311
+ else console.log(chalk.gray(" No running chaiterm processes found.\n"));
312
+ } catch (error) {
313
+ const err = error;
314
+ console.log(chalk.red(`\n❌ Error: ${err.message}\n`));
315
+ process.exit(1);
316
+ }
247
317
  }
248
318
 
249
319
  //#endregion
@@ -252,14 +322,23 @@ async function logoutCommand() {
252
322
  * Chaiterm CLI
253
323
  *
254
324
  * Main entry point for the chaiterm command line tool.
325
+ *
326
+ * Use --config to specify a custom config file:
327
+ * chaiterm --config ./local-config.json start
328
+ * chaiterm -c ~/.config/chaiterm/remote.json status
255
329
  */
330
+ const pkg = createRequire(import.meta.url)("../../package.json");
256
331
  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);
332
+ program.name("chaiterm").description("Access your terminal from anywhere").version(pkg.version).option("-c, --config <path>", "Path to config file (default: ~/.config/chaiterm/config.json)").hook("preAction", (thisCommand) => {
333
+ const opts = thisCommand.opts();
334
+ if (opts.config) setConfigPath(opts.config);
335
+ });
336
+ program.command("init").description("Initialize chaiterm with your email").argument("[email]", "Your email (will prompt if not provided)").action(initCommand);
259
337
  program.command("start").description("Start terminal access (connect pty-client to gateway)").action(startCommand);
260
338
  program.command("expose").description("Expose a local port to the internet").argument("<port>", "Local port to expose").action(exposeCommand);
261
339
  program.command("status").description("Show connection status").action(statusCommand);
262
340
  program.command("logout").description("Clear credentials and logout").action(logoutCommand);
341
+ program.command("kill").description("Kill all running chaiterm processes").action(killCommand);
263
342
  program.showHelpAfterError();
264
343
  program.parse();
265
344
  if (!process.argv.slice(2).length) {
@@ -1 +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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"}
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/commands/kill.ts","../../src/bin/chaiterm.ts"],"sourcesContent":["/**\n * Init command\n *\n * Sets up chaiterm with user's email.\n * Email is used to identify which terminal belongs to which user.\n *\n * Usage:\n * chaiterm init # prompts for email\n * chaiterm init user@example.com # sets email directly\n */\n\nimport chalk from \"chalk\";\nimport readline from \"readline\";\nimport { Config } from \"../lib/config.js\";\n\n/**\n * Prompt user for input\n */\nfunction prompt(question: string): Promise<string> {\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n\n return new Promise((resolve) => {\n rl.question(question, (answer) => {\n rl.close();\n resolve(answer.trim());\n });\n });\n}\n\n/**\n * Basic email validation\n */\nfunction isValidEmail(email: string): boolean {\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email);\n}\n\nexport async function initCommand(emailArg?: string): Promise<void> {\n const config = new Config();\n\n console.log(chalk.cyan(\"\\n🚀 Chaiterm Setup\\n\"));\n\n // Check if already configured\n const existingEmail = config.getEmail();\n if (existingEmail) {\n console.log(chalk.yellow(\"⚠️ Already configured.\"));\n console.log(` Email: ${existingEmail}`);\n console.log(` Config: ${config.getPath()}`);\n console.log(\n chalk.gray(\"\\n Run 'chaiterm logout' to reset configuration.\\n\")\n );\n return;\n }\n\n // Get email from argument or prompt\n let email = emailArg;\n\n if (!email) {\n console.log(\"Enter the email you use to sign in at chaiterm.com\\n\");\n email = await prompt(chalk.white(\" Email: \"));\n }\n\n // Validate email\n if (!email || !isValidEmail(email)) {\n console.log(chalk.red(\"\\n❌ Invalid email address.\\n\"));\n process.exit(1);\n }\n\n // Save email to config\n config.setEmail(email);\n\n console.log(chalk.green(\"\\n✓ Configuration saved!\\n\"));\n console.log(` Email: ${email}`);\n console.log(` Config: ${config.getPath()}`);\n console.log(chalk.cyan(\"\\n📋 Next steps:\"));\n console.log(\" 1. Sign up at https://chaiterm.com (if you haven't)\");\n console.log(\" 2. Run 'chaiterm start' to connect your terminal\\n\");\n}\n\n","/**\n * Start command\n *\n * Starts the terminal connection (pty-client).\n *\n * Note: Token is NOT needed for terminal access.\n * Security is enforced at browser connection via session cookie.\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 // Only email is required for terminal access (no token needed)\n const email = config.getEmail();\n if (!email) {\n console.log(chalk.red(\"❌ Not configured.\"));\n console.log(chalk.gray(\" Run 'chaiterm init' first.\\n\"));\n process.exit(1);\n }\n\n const gatewayUrl = config.getGatewayUrl();\n\n // Print configuration\n console.log(chalk.gray(\" Config: \") + config.getPath());\n if (config.isCustomConfig()) {\n console.log(chalk.gray(\" (custom config via --config flag)\"));\n }\n console.log(\"\");\n\n console.log(` Email: ${chalk.green(email)}`);\n console.log(` Gateway: ${chalk.gray(gatewayUrl)}`);\n console.log(\"\");\n\n const client = new PtyClient({\n gatewayUrl,\n email,\n onConnect: () => {\n console.log(chalk.green(\"✓ Connected to terminal gateway\"));\n console.log(\n chalk.cyan(`\\n📱 Access your terminal at: https://chaiterm.com/terminals\\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. Try 'chaiterm init' to re-configure\\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 // Configuration status\n const email = config.getEmail();\n if (email) {\n console.log(chalk.green(\"✓ Configured\"));\n console.log(` Email: ${email}`);\n } else {\n console.log(chalk.yellow(\"○ Not configured\"));\n console.log(chalk.gray(\" Run 'chaiterm init' to set up\"));\n }\n\n console.log(\"\");\n\n // Gateway configuration\n console.log(chalk.gray(\"Configuration:\"));\n console.log(` Gateway: ${config.getGatewayUrl()}`);\n console.log(` Config file: ${config.getPath()}`);\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 logout - Clear configuration\");\n console.log(\"\");\n}\n\n","/**\n * Logout command\n *\n * Clears stored configuration.\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🔒 Clearing configuration...\\n\"));\n\n const email = config.getEmail();\n if (!email) {\n console.log(chalk.yellow(\"⚠️ Not configured.\\n\"));\n return;\n }\n\n config.clear();\n\n console.log(chalk.green(\"✓ Configuration cleared\"));\n console.log(` Previous email: ${email}`);\n console.log(chalk.gray(\"\\n Run 'chaiterm init' to configure again.\\n\"));\n}\n\n","/**\n * Kill command\n *\n * Terminates any running chaiterm processes (node-pty instances).\n */\n\nimport chalk from \"chalk\";\nimport { execSync } from \"child_process\";\n\nexport async function killCommand(): Promise<void> {\n console.log(chalk.cyan(\"\\n🔍 Looking for running chaiterm processes...\\n\"));\n\n try {\n // Find chaiterm processes\n // Look for: node processes running chaiterm, or processes with node-pty\n const patterns = [\n \"chaiterm.mjs\",\n \"chaiterm.js\",\n \"pty-client\",\n \"node-pty\",\n ];\n\n let foundAny = false;\n\n for (const pattern of patterns) {\n try {\n // Use pgrep to find matching processes (excluding the current process)\n const result = execSync(\n `pgrep -f \"${pattern}\" | grep -v \"^${process.pid}$\" || true`,\n { encoding: \"utf-8\" }\n ).trim();\n\n if (result) {\n const pids = result.split(\"\\n\").filter(Boolean);\n for (const pid of pids) {\n try {\n // Get process info before killing\n const processInfo = execSync(`ps -p ${pid} -o command= 2>/dev/null || true`, {\n encoding: \"utf-8\",\n }).trim();\n\n if (processInfo && processInfo.includes(\"chaiterm\")) {\n process.kill(parseInt(pid, 10), \"SIGTERM\");\n console.log(chalk.yellow(` Killed PID ${pid}: ${processInfo.substring(0, 60)}...`));\n foundAny = true;\n }\n } catch {\n // Process might have already exited\n }\n }\n }\n } catch {\n // pgrep might not find anything, that's OK\n }\n }\n\n // Also try pkill as a fallback for any remaining chaiterm processes\n try {\n execSync('pkill -f \"chaiterm.mjs\" 2>/dev/null || true');\n execSync('pkill -f \"chaiterm start\" 2>/dev/null || true');\n } catch {\n // Ignore errors\n }\n\n if (foundAny) {\n console.log(chalk.green(\"\\n✓ Killed chaiterm processes\\n\"));\n } else {\n console.log(chalk.gray(\" No running chaiterm processes found.\\n\"));\n }\n } catch (error) {\n const err = error as Error;\n console.log(chalk.red(`\\n❌ Error: ${err.message}\\n`));\n process.exit(1);\n }\n}\n","#!/usr/bin/env node\n/**\n * Chaiterm CLI\n *\n * Main entry point for the chaiterm command line tool.\n *\n * Use --config to specify a custom config file:\n * chaiterm --config ./local-config.json start\n * chaiterm -c ~/.config/chaiterm/remote.json status\n */\n\nimport { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport { createRequire } from \"module\";\nimport { setConfigPath, isUsingCustomConfig, getCustomConfigPath } from \"../lib/config.js\";\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\";\nimport { killCommand } from \"../commands/kill.js\";\n\n// Read version from package.json\nconst require = createRequire(import.meta.url);\nconst pkg = require(\"../../package.json\");\n\nconst program = new Command();\n\nprogram\n .name(\"chaiterm\")\n .description(\"Access your terminal from anywhere\")\n .version(pkg.version)\n .option(\"-c, --config <path>\", \"Path to config file (default: ~/.config/chaiterm/config.json)\")\n .hook(\"preAction\", (thisCommand) => {\n const opts = thisCommand.opts();\n if (opts.config) {\n setConfigPath(opts.config);\n }\n });\n\nprogram\n .command(\"init\")\n .description(\"Initialize chaiterm with your email\")\n .argument(\"[email]\", \"Your email (will prompt if not provided)\")\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\nprogram\n .command(\"kill\")\n .description(\"Kill all running chaiterm processes\")\n .action(killCommand);\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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"}
@@ -1,7 +1,7 @@
1
1
  import WebSocket from "ws";
2
2
  import { spawn } from "child_process";
3
- import { existsSync, mkdirSync, writeFileSync } from "fs";
4
- import { join } from "path";
3
+ import fs, { existsSync, mkdirSync, writeFileSync } from "fs";
4
+ import path, { join } from "path";
5
5
  import { tmpdir } from "os";
6
6
  import Conf from "conf";
7
7
 
@@ -11,6 +11,11 @@ import Conf from "conf";
11
11
  *
12
12
  * Connects to the terminal gateway and spawns a local PTY.
13
13
  * This is the core of terminal access functionality.
14
+ *
15
+ * Security Model:
16
+ * - No token needed for terminal access (security is at browser via session cookie)
17
+ * - CLI just registers which user's machine it is by email
18
+ * - Token is only used for port exposure (frp tunnels)
14
19
  */
15
20
  var PtyClient = class {
16
21
  ws = null;
@@ -25,8 +30,8 @@ var PtyClient = class {
25
30
  * Connect to the terminal gateway
26
31
  */
27
32
  async connect() {
28
- const { gatewayUrl, userId, token } = this.options;
29
- const url = `${gatewayUrl}/register?user=${userId}&token=${token}`;
33
+ const { gatewayUrl, email } = this.options;
34
+ const url = `${gatewayUrl}/register?email=${encodeURIComponent(email)}`;
30
35
  console.log(`[pty-client] Connecting to gateway...`);
31
36
  return new Promise((resolve, reject) => {
32
37
  this.ws = new WebSocket(url);
@@ -274,7 +279,7 @@ subdomain = "${subdomain.id}"
274
279
  * Find frpc binary in various locations
275
280
  */
276
281
  async findFrpcBinary() {
277
- const { execSync } = await import("child_process");
282
+ const { execSync: execSync$1 } = await import("child_process");
278
283
  const possiblePaths = [
279
284
  "frpc",
280
285
  join(process.cwd(), "node_modules", ".bin", "frpc"),
@@ -282,9 +287,9 @@ subdomain = "${subdomain.id}"
282
287
  "/usr/local/bin/frpc",
283
288
  "/usr/bin/frpc"
284
289
  ];
285
- for (const path of possiblePaths) try {
286
- execSync(`${path} --version`, { stdio: "ignore" });
287
- return path;
290
+ for (const path$1 of possiblePaths) try {
291
+ execSync$1(`${path$1} --version`, { stdio: "ignore" });
292
+ return path$1;
288
293
  } catch {
289
294
  continue;
290
295
  }
@@ -297,18 +302,41 @@ subdomain = "${subdomain.id}"
297
302
  /**
298
303
  * Configuration management for chaiterm CLI
299
304
  *
300
- * Stores user credentials and settings in ~/.config/chaiterm/
305
+ * Uses config file at:
306
+ * - Custom path via --config flag
307
+ * - Default: ~/.config/chaiterm/config.json
308
+ *
309
+ * All configuration comes from the config file only.
310
+ * Use --config flag to switch between different config files (e.g., local vs remote).
311
+ */
312
+ let customConfigPath = null;
313
+ /**
314
+ * Set custom config path (called before Config instantiation)
301
315
  */
316
+ function setConfigPath(configPath) {
317
+ customConfigPath = path.resolve(configPath);
318
+ }
302
319
  const CONFIG_DEFAULTS = {
303
- gatewayUrl: "wss://terminal.chaiterm.com",
320
+ gatewayUrl: "wss://chaiterm.com/ws/pty",
304
321
  frpServerAddr: "frp.chaiterm.com",
305
322
  frpServerPort: 7e3,
306
323
  subdomains: []
307
324
  };
308
325
  var Config = class {
309
326
  store;
327
+ customPath;
310
328
  constructor() {
311
- this.store = new Conf({
329
+ this.customPath = customConfigPath;
330
+ if (this.customPath) {
331
+ const dir = path.dirname(this.customPath);
332
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
333
+ this.store = new Conf({
334
+ projectName: "chaiterm",
335
+ defaults: CONFIG_DEFAULTS,
336
+ cwd: dir,
337
+ configName: path.basename(this.customPath, ".json")
338
+ });
339
+ } else this.store = new Conf({
312
340
  projectName: "chaiterm",
313
341
  defaults: CONFIG_DEFAULTS
314
342
  });
@@ -326,17 +354,30 @@ var Config = class {
326
354
  return this.store.get("token");
327
355
  }
328
356
  /**
329
- * Get user ID
357
+ * Get user ID (used for frp/port exposure)
330
358
  */
331
359
  getUserId() {
332
360
  return this.store.get("userId");
333
361
  }
334
362
  /**
363
+ * Get user email (used for terminal access)
364
+ */
365
+ getEmail() {
366
+ return this.store.get("email");
367
+ }
368
+ /**
335
369
  * Set authentication credentials
336
370
  */
337
- setCredentials(token, userId) {
371
+ setCredentials(token, userId, email) {
338
372
  this.store.set("token", token);
339
373
  this.store.set("userId", userId);
374
+ if (email) this.store.set("email", email);
375
+ }
376
+ /**
377
+ * Set email for terminal access
378
+ */
379
+ setEmail(email) {
380
+ this.store.set("email", email);
340
381
  }
341
382
  /**
342
383
  * Get gateway URL for terminal access
@@ -389,8 +430,14 @@ var Config = class {
389
430
  getPath() {
390
431
  return this.store.path;
391
432
  }
433
+ /**
434
+ * Check if using a custom config path
435
+ */
436
+ isCustomConfig() {
437
+ return this.customPath !== null;
438
+ }
392
439
  };
393
440
 
394
441
  //#endregion
395
- export { FrpWrapper as n, PtyClient as r, Config as t };
396
- //# sourceMappingURL=config-CGPKYUde.mjs.map
442
+ export { PtyClient as i, setConfigPath as n, FrpWrapper as r, Config as t };
443
+ //# sourceMappingURL=config-2bJzN_Fc.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-2bJzN_Fc.mjs","names":["msg: TerminalMessage","path","customConfigPath: string | null","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 * Security Model:\n * - No token needed for terminal access (security is at browser via session cookie)\n * - CLI just registers which user's machine it is by email\n * - Token is only used for port exposure (frp tunnels)\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 email: string;\n // Note: token is NOT needed for terminal access, only for port exposure\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, email } = this.options;\n // No token in URL - security is enforced at browser connection via session cookie\n const url = `${gatewayUrl}/register?email=${encodeURIComponent(email)}`;\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 * Uses config file at:\n * - Custom path via --config flag\n * - Default: ~/.config/chaiterm/config.json\n *\n * All configuration comes from the config file only.\n * Use --config flag to switch between different config files (e.g., local vs remote).\n */\n\nimport Conf from \"conf\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport type { ChaitermConfig, SubdomainConfig } 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 // Resolve to absolute path\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/**\n * Check if using a custom config path\n */\nexport function isUsingCustomConfig(): boolean {\n return customConfigPath !== null;\n}\n\nconst CONFIG_DEFAULTS: Partial<ChaitermConfig> = {\n // Path-based WebSocket URL (same domain as web app)\n // CLI appends /register to this URL\n gatewayUrl: \"wss://chaiterm.com/ws/pty\",\n frpServerAddr: \"frp.chaiterm.com\",\n frpServerPort: 7000,\n subdomains: [],\n};\n\nexport class Config {\n private store: Conf<ChaitermConfig>;\n private customPath: string | null;\n\n constructor() {\n this.customPath = customConfigPath;\n\n if (this.customPath) {\n // Use custom config file path\n // Ensure directory exists\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<ChaitermConfig>({\n projectName: \"chaiterm\",\n defaults: CONFIG_DEFAULTS as ChaitermConfig,\n cwd: dir,\n configName: path.basename(this.customPath, \".json\"),\n });\n } else {\n // Use default config location (~/.config/chaiterm/config.json)\n this.store = new Conf<ChaitermConfig>({\n projectName: \"chaiterm\",\n defaults: CONFIG_DEFAULTS as ChaitermConfig,\n });\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 (used for frp/port exposure)\n */\n getUserId(): string | undefined {\n return this.store.get(\"userId\");\n }\n\n /**\n * Get user email (used for terminal access)\n */\n getEmail(): string | undefined {\n return this.store.get(\"email\");\n }\n\n /**\n * Set authentication credentials\n */\n setCredentials(token: string, userId: string, email?: string): void {\n this.store.set(\"token\", token);\n this.store.set(\"userId\", userId);\n if (email) {\n this.store.set(\"email\", email);\n }\n }\n\n /**\n * Set email for terminal access\n */\n setEmail(email: string): void {\n this.store.set(\"email\", email);\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 * Check if using a custom config path\n */\n isCustomConfig(): boolean {\n return this.customPath !== null;\n }\n}\n\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAyBA,IAAa,YAAb,MAAuB;CACrB,AAAQ,KAAuB;CAC/B,AAAQ,MAAmB;CAC3B,AAAQ;CACR,AAAQ,mBAAyD;CACjE,AAAQ,kBAAkB;CAE1B,YAAY,SAA2B;AACrC,OAAK,UAAU;;;;;CAMjB,MAAM,UAAyB;EAC7B,MAAM,EAAE,YAAY,UAAU,KAAK;EAEnC,MAAM,MAAM,GAAG,WAAW,kBAAkB,mBAAmB,MAAM;AAErE,UAAQ,IAAI,wCAAwC;AAEpD,SAAO,IAAI,SAAS,SAAS,WAAW;AACtC,QAAK,KAAK,IAAI,UAAU,IAAI;AAE5B,QAAK,GAAG,GAAG,cAAc;AACvB,YAAQ,IAAI,oCAAoC;AAChD,SAAK,QAAQ,aAAa;AAC1B,aAAS;KACT;AAEF,QAAK,GAAG,GAAG,YAAY,QAAQ;AAC7B,SAAK,cAAc,IAAI,UAAU,CAAC;KAClC;AAEF,QAAK,GAAG,GAAG,UAAU,MAAM,WAAW;AACpC,YAAQ,IACN,mCAAmC,KAAK,WAAW,SACpD;AACD,SAAK,SAAS;AACd,SAAK,QAAQ,eAAe,MAAM,OAAO,UAAU,CAAC;AACpD,SAAK,mBAAmB;KACxB;AAEF,QAAK,GAAG,GAAG,UAAU,QAAQ;AAC3B,YAAQ,MAAM,uBAAuB,IAAI,UAAU;AACnD,SAAK,QAAQ,UAAU,IAAI;AAC3B,WAAO,IAAI;KACX;IACF;;;;;CAMJ,aAAmB;AACjB,OAAK,kBAAkB;AACvB,MAAI,KAAK,kBAAkB;AACzB,gBAAa,KAAK,iBAAiB;AACnC,QAAK,mBAAmB;;AAE1B,OAAK,SAAS;AACd,MAAI,KAAK,IAAI;AACX,QAAK,GAAG,OAAO;AACf,QAAK,KAAK;;;CAId,AAAQ,cAAc,KAAmB;EACvC,IAAIA;AACJ,MAAI;AACF,SAAM,KAAK,MAAM,IAAI;UACf;AACN;;AAGF,UAAQ,IAAI,MAAZ;GACE,KAAK;AACH,SAAK,QAAQ,IAAI,MAAM,IAAI,KAAK;AAChC;GACF,KAAK;AACH,SAAK,KAAK,MAAM,IAAI,KAAK;AACzB;GACF,KAAK;AACH,QAAI,KAAK,OAAO,IAAI,QAAQ,IAAI,KAC9B,MAAK,IAAI,OAAO,IAAI,MAAM,IAAI,KAAK;AAErC;GACF,KAAK;AACH,SAAK,UAAU;AACf;;;CAIN,MAAc,QAAQ,MAAc,MAA6B;AAE/D,OAAK,UAAU;EAGf,MAAM,UAAU,MAAM,OAAO;EAE7B,MAAM,QAAQ,QAAQ,IAAI,SAAS;EACnC,MAAM,MAAM,QAAQ,IAAI,QAAQ,QAAQ,KAAK;AAE7C,UAAQ,IAAI,6BAA6B,KAAK,GAAG,OAAO;AACxD,UAAQ,IAAI,uBAAuB,MAAM,SAAS,MAAM;AAExD,MAAI;AACF,QAAK,MAAM,QAAQ,MAAM,OAAO,EAAE,EAAE;IAClC,MAAM;IACN,MAAM,QAAQ;IACd,MAAM,QAAQ;IACT;IACL,KAAK,QAAQ;IACd,CAAC;AAGF,QAAK,KAAK;IAAE,MAAM;IAAS,KAAK,KAAK,IAAI;IAAK,CAAC;AAG/C,QAAK,IAAI,QAAQ,SAAiB;AAChC,SAAK,KAAK;KAAE,MAAM;KAAQ;KAAM,CAAC;KACjC;AAGF,QAAK,IAAI,QAAQ,EAAE,UAAU,aAAa;AACxC,YAAQ,IACN,iCAAiC,SAAS,WAAW,SACtD;AACD,SAAK,KAAK;KAAE,MAAM;KAAQ,MAAM;KAAU;KAAQ,CAAC;AACnD,SAAK,MAAM;KACX;WACK,KAAK;GACZ,MAAM,QAAQ;AACd,WAAQ,MAAM,qCAAqC,MAAM,UAAU;AACnE,QAAK,KAAK;IACR,MAAM;IACN,MAAM,qCAAqC,MAAM,QAAQ;IAC1D,CAAC;;;CAIN,AAAQ,WAAiB;AACvB,MAAI,KAAK,KAAK;AACZ,WAAQ,IAAI,2BAA2B;AACvC,OAAI;AACF,SAAK,IAAI,MAAM;WACT;AAGR,QAAK,MAAM;;;CAIf,AAAQ,KAAK,KAA4B;AACvC,MAAI,KAAK,MAAM,KAAK,GAAG,eAAe,UAAU,KAC9C,MAAK,GAAG,KAAK,KAAK,UAAU,IAAI,CAAC;;CAIrC,AAAQ,UAAgB;AACtB,OAAK,UAAU;;CAGjB,AAAQ,oBAA0B;AAChC,MAAI,CAAC,KAAK,mBAAmB,KAAK,iBAAkB;AAEpD,UAAQ,IAAI,4CAA4C;AACxD,OAAK,mBAAmB,iBAAiB;AACvC,QAAK,mBAAmB;AACxB,QAAK,SAAS,CAAC,YAAY,GAEzB;KACD,IAAK;;;;;;;;;;;;AC5KZ,IAAa,aAAb,MAAwB;CACtB,AAAQ;CACR,AAAQ,UAA+B;CACvC,AAAQ;CAER,YAAY,SAA4B;AACtC,OAAK,UAAU;AACf,OAAK,aAAa,KAAK,QAAQ,EAAE,YAAY,QAAQ,QAAQ,OAAO,OAAO;;;;;CAM7E,MAAM,QAAuB;EAE3B,MAAM,YAAY,KAAK,QAAQ,EAAE,WAAW;AAC5C,MAAI,CAAC,WAAW,UAAU,CACxB,WAAU,WAAW,EAAE,WAAW,MAAM,CAAC;AAI3C,OAAK,gBAAgB;EAGrB,MAAM,WAAW,MAAM,KAAK,gBAAgB;AAC5C,MAAI,CAAC,SACH,OAAM,IAAI,MACR,qEACD;AAGH,UAAQ,IAAI,4CAA4C,KAAK,aAAa;AAE1E,SAAO,IAAI,SAAS,SAAS,WAAW;AACtC,QAAK,UAAU,MAAM,UAAU,CAAC,MAAM,KAAK,WAAW,EAAE,EACtD,OAAO;IAAC;IAAU;IAAQ;IAAO,EAClC,CAAC;GAEF,IAAI,YAAY;AAEhB,QAAK,QAAQ,QAAQ,GAAG,SAAS,SAAiB;IAChD,MAAM,UAAU,KAAK,UAAU;AAC/B,SAAK,QAAQ,QAAQ,QAAQ;AAG7B,QAAI,QAAQ,SAAS,sBAAsB,IAAI,CAAC,WAAW;AACzD,iBAAY;AACZ,UAAK,QAAQ,aAAa;AAC1B,cAAS;;KAEX;AAEF,QAAK,QAAQ,QAAQ,GAAG,SAAS,SAAiB;IAChD,MAAM,UAAU,KAAK,UAAU;AAC/B,SAAK,QAAQ,QAAQ,QAAQ;AAG7B,QAAI,QAAQ,SAAS,QAAQ,IAAI,QAAQ,SAAS,QAAQ,CACxD,MAAK,QAAQ,UAAU,IAAI,MAAM,QAAQ,CAAC;KAE5C;AAEF,QAAK,QAAQ,GAAG,UAAU,QAAQ;AAChC,SAAK,QAAQ,UAAU,IAAI;AAC3B,WAAO,IAAI;KACX;AAEF,QAAK,QAAQ,GAAG,UAAU,SAAS;AACjC,YAAQ,IAAI,uCAAuC,OAAO;AAC1D,SAAK,UAAU;AACf,SAAK,QAAQ,gBAAgB;KAC7B;AAGF,oBAAiB;AACf,QAAI,CAAC,UACH,wBAAO,IAAI,MAAM,qBAAqB,CAAC;MAExC,IAAM;IACT;;;;;CAMJ,OAAa;AACX,MAAI,KAAK,SAAS;AAChB,WAAQ,IAAI,8BAA8B;AAC1C,QAAK,QAAQ,KAAK,UAAU;AAC5B,QAAK,UAAU;;;;;;CAOnB,YAAqB;AACnB,SAAO,KAAK,YAAY;;;;;CAM1B,AAAQ,iBAAuB;EAC7B,MAAM,EAAE,YAAY,YAAY,QAAQ,OAAO,eAAe,KAAK;EAEnE,IAAI,SAAS;UACP,OAAO;;gBAED,WAAW;eACZ,WAAW;;;;;;;UAOhB,OAAO;qBACI,MAAM;;;;;;;;;;AAYvB,OAAK,MAAM,aAAa,WACtB,KAAI,UAAU,OACZ,WAAU;;UAER,OAAO,GAAG,UAAU,GAAG;;cAEnB,UAAU,UAAU;eACnB,UAAU,GAAG;;AAKxB,gBAAc,KAAK,YAAY,QAAQ,QAAQ;AAC/C,UAAQ,IAAI,mCAAmC,KAAK,aAAa;;;;;CAMnE,MAAc,iBAAyC;EACrD,MAAM,EAAE,yBAAa,MAAM,OAAO;EAGlC,MAAM,gBAAgB;GAEpB;GAEA,KAAK,QAAQ,KAAK,EAAE,gBAAgB,QAAQ,OAAO;GAEnD;GACA;GAEA;GACD;AAED,OAAK,MAAMC,UAAQ,cACjB,KAAI;AACF,cAAS,GAAGA,OAAK,aAAa,EAAE,OAAO,UAAU,CAAC;AAClD,UAAOA;UACD;AACN;;AAIJ,SAAO;;;;;;;;;;;;;;;;ACtLX,IAAIC,mBAAkC;;;;AAKtC,SAAgB,cAAc,YAA0B;AAEtD,oBAAmB,KAAK,QAAQ,WAAW;;AAiB7C,MAAMC,kBAA2C;CAG/C,YAAY;CACZ,eAAe;CACf,eAAe;CACf,YAAY,EAAE;CACf;AAED,IAAa,SAAb,MAAoB;CAClB,AAAQ;CACR,AAAQ;CAER,cAAc;AACZ,OAAK,aAAa;AAElB,MAAI,KAAK,YAAY;GAGnB,MAAM,MAAM,KAAK,QAAQ,KAAK,WAAW;AACzC,OAAI,CAAC,GAAG,WAAW,IAAI,CACrB,IAAG,UAAU,KAAK,EAAE,WAAW,MAAM,CAAC;AAGxC,QAAK,QAAQ,IAAI,KAAqB;IACpC,aAAa;IACb,UAAU;IACV,KAAK;IACL,YAAY,KAAK,SAAS,KAAK,YAAY,QAAQ;IACpD,CAAC;QAGF,MAAK,QAAQ,IAAI,KAAqB;GACpC,aAAa;GACb,UAAU;GACX,CAAC;;;;;CAON,kBAA2B;AACzB,SAAO,CAAC,CAAC,KAAK,MAAM,IAAI,QAAQ,IAAI,CAAC,CAAC,KAAK,MAAM,IAAI,SAAS;;;;;CAMhE,WAA+B;AAC7B,SAAO,KAAK,MAAM,IAAI,QAAQ;;;;;CAMhC,YAAgC;AAC9B,SAAO,KAAK,MAAM,IAAI,SAAS;;;;;CAMjC,WAA+B;AAC7B,SAAO,KAAK,MAAM,IAAI,QAAQ;;;;;CAMhC,eAAe,OAAe,QAAgB,OAAsB;AAClE,OAAK,MAAM,IAAI,SAAS,MAAM;AAC9B,OAAK,MAAM,IAAI,UAAU,OAAO;AAChC,MAAI,MACF,MAAK,MAAM,IAAI,SAAS,MAAM;;;;;CAOlC,SAAS,OAAqB;AAC5B,OAAK,MAAM,IAAI,SAAS,MAAM;;;;;CAMhC,gBAAwB;AACtB,SAAO,KAAK,MAAM,IAAI,aAAa,IAAI,gBAAgB;;;;;CAMzD,mBAA2B;AACzB,SAAO,KAAK,MAAM,IAAI,gBAAgB,IAAI,gBAAgB;;;;;CAM5D,mBAA2B;AACzB,SAAO,KAAK,MAAM,IAAI,gBAAgB,IAAI,gBAAgB;;;;;CAM5D,gBAAmC;AACjC,SAAO,KAAK,MAAM,IAAI,aAAa,IAAI,EAAE;;;;;CAM3C,aAAa,WAAkC;EAC7C,MAAM,aAAa,KAAK,eAAe;AACvC,aAAW,KAAK,UAAU;AAC1B,OAAK,MAAM,IAAI,cAAc,WAAW;;;;;CAM1C,gBAAgB,IAAkB;EAChC,MAAM,aAAa,KAAK,eAAe,CAAC,QAAQ,MAAM,EAAE,OAAO,GAAG;AAClE,OAAK,MAAM,IAAI,cAAc,WAAW;;;;;CAM1C,QAAc;AACZ,OAAK,MAAM,OAAO;;;;;CAMpB,UAAkB;AAChB,SAAO,KAAK,MAAM;;;;;CAMpB,iBAA0B;AACxB,SAAO,KAAK,eAAe"}
package/dist/index.d.mts CHANGED
@@ -4,11 +4,15 @@
4
4
  *
5
5
  * Connects to the terminal gateway and spawns a local PTY.
6
6
  * This is the core of terminal access functionality.
7
+ *
8
+ * Security Model:
9
+ * - No token needed for terminal access (security is at browser via session cookie)
10
+ * - CLI just registers which user's machine it is by email
11
+ * - Token is only used for port exposure (frp tunnels)
7
12
  */
8
13
  interface PtyClientOptions {
9
14
  gatewayUrl: string;
10
- userId: string;
11
- token: string;
15
+ email: string;
12
16
  onConnect?: () => void;
13
17
  onDisconnect?: (code: number, reason: string) => void;
14
18
  onError?: (error: Error) => void;
@@ -41,10 +45,12 @@ declare class PtyClient {
41
45
  * Shared types for chaiterm CLI
42
46
  */
43
47
  interface ChaitermConfig {
44
- /** User's authentication token */
48
+ /** User's authentication token (for port exposure) */
45
49
  token: string;
46
- /** User ID from server */
50
+ /** User ID from server (for port exposure) */
47
51
  userId: string;
52
+ /** User email (for terminal access) */
53
+ email: string;
48
54
  /** Gateway URL for terminal access */
49
55
  gatewayUrl: string;
50
56
  /** frp server address */
@@ -134,8 +140,10 @@ declare class FrpWrapper {
134
140
  }
135
141
  //#endregion
136
142
  //#region src/lib/config.d.ts
143
+
137
144
  declare class Config {
138
145
  private store;
146
+ private customPath;
139
147
  constructor();
140
148
  /**
141
149
  * Check if user is authenticated
@@ -146,13 +154,21 @@ declare class Config {
146
154
  */
147
155
  getToken(): string | undefined;
148
156
  /**
149
- * Get user ID
157
+ * Get user ID (used for frp/port exposure)
150
158
  */
151
159
  getUserId(): string | undefined;
160
+ /**
161
+ * Get user email (used for terminal access)
162
+ */
163
+ getEmail(): string | undefined;
152
164
  /**
153
165
  * Set authentication credentials
154
166
  */
155
- setCredentials(token: string, userId: string): void;
167
+ setCredentials(token: string, userId: string, email?: string): void;
168
+ /**
169
+ * Set email for terminal access
170
+ */
171
+ setEmail(email: string): void;
156
172
  /**
157
173
  * Get gateway URL for terminal access
158
174
  */
@@ -185,6 +201,10 @@ declare class Config {
185
201
  * Get config file path (for debugging)
186
202
  */
187
203
  getPath(): string;
204
+ /**
205
+ * Check if using a custom config path
206
+ */
207
+ isCustomConfig(): boolean;
188
208
  }
189
209
  //#endregion
190
210
  export { ChaitermConfig, Config, ConnectionStatus, FrpWrapper, PtyClient, SubdomainConfig, TerminalMessage, TunnelStatus };
@@ -1 +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"}
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":";;AAgBA;AASA;;;;ACrBA;AAiBA;AASA;AAKA;AAOA;UD1BiB,gBAAA;;;EEHA,SAAA,CAAA,EAAA,GAAA,GAAA,IAAiB;EAYrB,YAAA,CAAU,EAAA,CAAA,IAAA,EAAA,MAKA,EAAA,MAAA,EAAA,MAQN,EAAA,GAAA,IAAO;oBFhBJ;;cAGP,SAAA;EGyBA,QAAA,EAAM;;;;;uBHlBI;;;;aAOJ;;;;;;;;;;;;;;;AAvBnB;AASA;UCrBiB,cAAA;;;EAAA;EAiBA,MAAA,EAAA,MAAA;EASA;EAKA,KAAA,EAAA,MAAA;EAOL;;;;EC7BK;EAYJ,aAAU,EAAA,MAAA;;cDPT;;AEgCD,UF7BI,eAAA,CEiIE;;;;;;;;UFxHF,gBAAA;;WAEN;;UAGM,YAAA;;;;;;KAOL,eAAA;;;;;;;;;;;;;;;;;;;;;;;AArBK,UCRA,iBAAA,CDQe;EASf,UAAA,EAAA,MAAA;EAKA,UAAA,EAAA,MAAY;EAOjB,MAAA,EAAA,MAAA;;cCxBE;;EALG,YAAA,CAAA,EAAA,GAAA,GAAiB,IAAA;EAYrB,OAAA,CAAA,EAAA,CAAA,KAAU,EAJH,KAIG,EAKA,GAAA,IAAA;;;cALV,UAAA;ECyBA,QAAA,OAAM;;;uBDpBI;;;;WAQN;;;;;;;;;;;;;;;;;;;;;cCYJ,MAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAoGM;;;;0BAOO"}
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { n as FrpWrapper, r as PtyClient, t as Config } from "./config-CGPKYUde.mjs";
1
+ import { i as PtyClient, r as FrpWrapper, t as Config } from "./config-2bJzN_Fc.mjs";
2
2
 
3
3
  export { Config, FrpWrapper, PtyClient };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chaiterm",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Access your terminal from anywhere - CLI tool for chaiterm",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,9 @@
18
18
  "scripts": {
19
19
  "build": "tsdown",
20
20
  "dev": "tsdown --watch",
21
- "check-types": "tsc --noEmit"
21
+ "dev:local": "node --import tsx src/bin/pty-server.ts",
22
+ "check-types": "tsc --noEmit",
23
+ "bin:chaiterm": "node dist/bin/chaiterm.mjs"
22
24
  },
23
25
  "keywords": [
24
26
  "terminal",
@@ -49,4 +51,3 @@
49
51
  },
50
52
  "optionalDependencies": {}
51
53
  }
52
-
@@ -1 +0,0 @@
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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"}