chaiterm 0.0.2 → 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
@@ -30,7 +30,7 @@ Start terminal access - connects your local PTY to the gateway.
30
30
  chaiterm start
31
31
  ```
32
32
 
33
- Once connected, access your terminal at: `https://terminal.chaiterm.com`
33
+ Once connected, access your terminal at: `https://chaiterm.com/terminals`
34
34
 
35
35
  ### `chaiterm expose <port>`
36
36
 
@@ -58,17 +58,55 @@ Clear stored credentials.
58
58
  chaiterm logout
59
59
  ```
60
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
+
61
69
  ## Architecture
62
70
 
63
71
  The CLI wraps two components:
64
72
 
65
- 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
66
74
  2. **frp-wrapper** - Wraps frpc binary for port exposure tunnels
67
75
 
68
76
  ## Configuration
69
77
 
70
78
  Config is stored in `~/.config/chaiterm/config.json` (or platform equivalent).
71
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
+
72
110
  ## Requirements
73
111
 
74
112
  - Node.js 18+
@@ -87,3 +125,29 @@ bun run build
87
125
  bun run dev
88
126
  ```
89
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,33 +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";
6
+ import { createRequire } from "module";
7
+ import readline from "readline";
5
8
 
6
9
  //#region src/commands/init.ts
7
10
  /**
8
11
  * Init command
9
12
  *
10
- * 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
11
37
  */
12
- async function initCommand() {
38
+ function isValidEmail(email) {
39
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
40
+ }
41
+ async function initCommand(emailArg) {
13
42
  const config = new Config();
14
43
  console.log(chalk.cyan("\n🚀 Chaiterm Setup\n"));
15
- if (config.isAuthenticated()) {
16
- console.log(chalk.yellow("⚠️ You are already authenticated."));
17
- 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}`);
18
48
  console.log(` Config: ${config.getPath()}`);
19
- 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"));
20
50
  return;
21
51
  }
22
- console.log("To use chaiterm, you need to authenticate with your account.\n");
23
- console.log(chalk.gray("Opening browser for authentication...\n"));
24
- console.log(chalk.yellow("⚠️ Authentication not yet implemented.\n"));
25
- console.log("For development, you can manually set credentials:");
26
- console.log(chalk.gray(` Config file: ${config.getPath()}\n`));
27
- console.log(chalk.cyan("📋 Next steps:"));
28
- console.log(" 1. Sign up at https://chaiterm.com");
29
- console.log(" 2. Get your CLI token from settings");
30
- 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");
31
68
  }
32
69
 
33
70
  //#endregion
@@ -36,28 +73,32 @@ async function initCommand() {
36
73
  * Start command
37
74
  *
38
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.
39
79
  */
40
80
  async function startCommand() {
41
81
  const config = new Config();
42
82
  console.log(chalk.cyan("\n🖥️ Starting terminal connection...\n"));
43
- if (!config.isAuthenticated()) {
44
- console.log(chalk.red("❌ Not authenticated."));
83
+ const email = config.getEmail();
84
+ if (!email) {
85
+ console.log(chalk.red("❌ Not configured."));
45
86
  console.log(chalk.gray(" Run 'chaiterm init' first.\n"));
46
87
  process.exit(1);
47
88
  }
48
- const userId = config.getUserId();
49
- const token = config.getToken();
50
89
  const gatewayUrl = config.getGatewayUrl();
51
- 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)}`);
52
94
  console.log(` Gateway: ${chalk.gray(gatewayUrl)}`);
53
95
  console.log("");
54
96
  const client = new PtyClient({
55
97
  gatewayUrl,
56
- userId,
57
- token,
98
+ email,
58
99
  onConnect: () => {
59
100
  console.log(chalk.green("✓ Connected to terminal gateway"));
60
- 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`));
61
102
  console.log(chalk.gray("Press Ctrl+C to disconnect\n"));
62
103
  },
63
104
  onDisconnect: (code, reason) => {
@@ -84,8 +125,7 @@ async function startCommand() {
84
125
  console.log(chalk.red(`\n❌ Failed to connect: ${err.message}`));
85
126
  console.log(chalk.gray("\nTroubleshooting:"));
86
127
  console.log(" 1. Check your internet connection");
87
- console.log(" 2. Verify your token is valid");
88
- console.log(" 3. Try 'chaiterm init' to re-authenticate\n");
128
+ console.log(" 2. Try 'chaiterm init' to re-configure\n");
89
129
  process.exit(1);
90
130
  }
91
131
  }
@@ -192,35 +232,22 @@ function generateSubdomainId() {
192
232
  async function statusCommand() {
193
233
  const config = new Config();
194
234
  console.log(chalk.cyan("\n📊 Chaiterm Status\n"));
195
- if (config.isAuthenticated()) {
196
- console.log(chalk.green("✓ Authenticated"));
197
- 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}`);
198
239
  } else {
199
- console.log(chalk.yellow("○ Not authenticated"));
200
- 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"));
201
242
  }
202
243
  console.log("");
203
244
  console.log(chalk.gray("Configuration:"));
204
245
  console.log(` Gateway: ${config.getGatewayUrl()}`);
205
- console.log(` frp Server: ${config.getFrpServerAddr()}:${config.getFrpServerPort()}`);
206
246
  console.log(` Config file: ${config.getPath()}`);
207
247
  console.log("");
208
- const subdomains = config.getSubdomains();
209
- if (subdomains.length > 0) {
210
- console.log(chalk.gray("Configured subdomains:"));
211
- for (const subdomain of subdomains) {
212
- const status = subdomain.active ? chalk.green("●") : chalk.gray("○");
213
- console.log(` ${status} ${subdomain.id}.chaiterm.com → localhost:${subdomain.localPort}`);
214
- }
215
- } else {
216
- console.log(chalk.gray("No subdomains configured"));
217
- console.log(chalk.gray(" Run 'chaiterm expose <port>' to expose a port"));
218
- }
219
- console.log("");
220
248
  console.log(chalk.gray("Commands:"));
221
- console.log(" chaiterm start - Start terminal connection");
222
- console.log(" chaiterm expose <port> - Expose a local port");
223
- console.log(" chaiterm logout - Clear credentials");
249
+ console.log(" chaiterm start - Start terminal connection");
250
+ console.log(" chaiterm logout - Clear configuration");
224
251
  console.log("");
225
252
  }
226
253
 
@@ -229,20 +256,64 @@ async function statusCommand() {
229
256
  /**
230
257
  * Logout command
231
258
  *
232
- * Clears stored credentials.
259
+ * Clears stored configuration.
233
260
  */
234
261
  async function logoutCommand() {
235
262
  const config = new Config();
236
- console.log(chalk.cyan("\n🔒 Logging out...\n"));
237
- if (!config.isAuthenticated()) {
238
- 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"));
239
267
  return;
240
268
  }
241
- const userId = config.getUserId();
242
269
  config.clear();
243
- console.log(chalk.green("✓ Credentials cleared"));
244
- console.log(` Previous user: ${userId}`);
245
- 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
+ }
246
317
  }
247
318
 
248
319
  //#endregion
@@ -251,14 +322,23 @@ async function logoutCommand() {
251
322
  * Chaiterm CLI
252
323
  *
253
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
254
329
  */
330
+ const pkg = createRequire(import.meta.url)("../../package.json");
255
331
  const program = new Command();
256
- program.name("chaiterm").description("Access your terminal from anywhere").version("0.0.1");
257
- 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);
258
337
  program.command("start").description("Start terminal access (connect pty-client to gateway)").action(startCommand);
259
338
  program.command("expose").description("Expose a local port to the internet").argument("<port>", "Local port to expose").action(exposeCommand);
260
339
  program.command("status").description("Show connection status").action(statusCommand);
261
340
  program.command("logout").description("Clear credentials and logout").action(logoutCommand);
341
+ program.command("kill").description("Kill all running chaiterm processes").action(killCommand);
262
342
  program.showHelpAfterError();
263
343
  program.parse();
264
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\";\n// import open from \"open\"; // TODO: Uncomment when OAuth is implemented\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":"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"}
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.2",
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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"}