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 +66 -2
- package/dist/bin/chaiterm.mjs +137 -57
- package/dist/bin/chaiterm.mjs.map +1 -1
- package/dist/{config-CGPKYUde.mjs → config-2bJzN_Fc.mjs} +62 -15
- package/dist/config-2bJzN_Fc.mjs.map +1 -0
- package/dist/index.d.mts +26 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +4 -3
- package/dist/config-CGPKYUde.mjs.map +0 -1
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://
|
|
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 `
|
|
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
|
+
|
package/dist/bin/chaiterm.mjs
CHANGED
|
@@ -1,33 +1,70 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
console.log(
|
|
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
|
|
49
|
+
console.log(chalk.gray("\n Run 'chaiterm logout' to reset configuration.\n"));
|
|
20
50
|
return;
|
|
21
51
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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(
|
|
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
|
-
|
|
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://
|
|
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.
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
console.log(
|
|
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
|
|
200
|
-
console.log(chalk.gray(" Run 'chaiterm init' to
|
|
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
|
|
222
|
-
console.log(" chaiterm
|
|
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
|
|
259
|
+
* Clears stored configuration.
|
|
233
260
|
*/
|
|
234
261
|
async function logoutCommand() {
|
|
235
262
|
const config = new Config();
|
|
236
|
-
console.log(chalk.cyan("\n🔒
|
|
237
|
-
|
|
238
|
-
|
|
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("✓
|
|
244
|
-
console.log(` Previous
|
|
245
|
-
console.log(chalk.gray("\n Run 'chaiterm init' to
|
|
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("
|
|
257
|
-
|
|
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,
|
|
29
|
-
const url = `${gatewayUrl}/register?
|
|
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
|
-
*
|
|
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://
|
|
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.
|
|
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 {
|
|
396
|
-
//# sourceMappingURL=config-
|
|
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
|
-
|
|
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 };
|
package/dist/index.d.mts.map
CHANGED
|
@@ -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":";;
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chaiterm",
|
|
3
|
-
"version": "0.0.
|
|
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
|
-
"
|
|
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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"}
|