minivibe 0.1.0
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/agent/agent.js +1218 -0
- package/login.html +331 -0
- package/package.json +49 -0
- package/pty-wrapper-node.js +157 -0
- package/pty-wrapper.py +225 -0
- package/vibe.js +1621 -0
package/login.html
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>MiniVibe CLI Login</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
10
|
+
<style>
|
|
11
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
12
|
+
|
|
13
|
+
:root {
|
|
14
|
+
--primary: #667eea;
|
|
15
|
+
--primary-dark: #5a67d8;
|
|
16
|
+
--gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
17
|
+
--text: #1a202c;
|
|
18
|
+
--text-muted: #718096;
|
|
19
|
+
--bg: #f7fafc;
|
|
20
|
+
--border: #e2e8f0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
body {
|
|
24
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
25
|
+
background: var(--gradient);
|
|
26
|
+
min-height: 100vh;
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: center;
|
|
30
|
+
padding: 20px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.container {
|
|
34
|
+
background: white;
|
|
35
|
+
padding: 48px 40px;
|
|
36
|
+
border-radius: 20px;
|
|
37
|
+
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.3);
|
|
38
|
+
text-align: center;
|
|
39
|
+
max-width: 420px;
|
|
40
|
+
width: 100%;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.logo {
|
|
44
|
+
width: 64px;
|
|
45
|
+
height: 64px;
|
|
46
|
+
background: var(--gradient);
|
|
47
|
+
border-radius: 16px;
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
margin: 0 auto 24px;
|
|
52
|
+
font-size: 32px;
|
|
53
|
+
color: white;
|
|
54
|
+
font-weight: 700;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
h1 {
|
|
58
|
+
color: var(--text);
|
|
59
|
+
font-size: 28px;
|
|
60
|
+
font-weight: 700;
|
|
61
|
+
margin-bottom: 8px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.subtitle {
|
|
65
|
+
color: var(--text-muted);
|
|
66
|
+
font-size: 15px;
|
|
67
|
+
margin-bottom: 36px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.btn {
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
gap: 12px;
|
|
75
|
+
width: 100%;
|
|
76
|
+
padding: 14px 24px;
|
|
77
|
+
font-size: 15px;
|
|
78
|
+
font-weight: 500;
|
|
79
|
+
border: 1px solid var(--border);
|
|
80
|
+
border-radius: 10px;
|
|
81
|
+
background: white;
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
transition: all 0.2s;
|
|
84
|
+
margin-bottom: 12px;
|
|
85
|
+
color: var(--text);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.btn:hover {
|
|
89
|
+
background: var(--bg);
|
|
90
|
+
border-color: #cbd5e0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.btn:disabled {
|
|
94
|
+
opacity: 0.6;
|
|
95
|
+
cursor: not-allowed;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.btn img {
|
|
99
|
+
width: 20px;
|
|
100
|
+
height: 20px;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.status {
|
|
104
|
+
margin-top: 20px;
|
|
105
|
+
padding: 14px 16px;
|
|
106
|
+
border-radius: 10px;
|
|
107
|
+
font-size: 14px;
|
|
108
|
+
display: none;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.status.success {
|
|
112
|
+
display: block;
|
|
113
|
+
background: #c6f6d5;
|
|
114
|
+
color: #22543d;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.status.error {
|
|
118
|
+
display: block;
|
|
119
|
+
background: #fed7d7;
|
|
120
|
+
color: #c53030;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.status.loading {
|
|
124
|
+
display: block;
|
|
125
|
+
background: #e9d8fd;
|
|
126
|
+
color: #553c9a;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.close-msg {
|
|
130
|
+
margin-top: 20px;
|
|
131
|
+
color: var(--text-muted);
|
|
132
|
+
font-size: 14px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.features {
|
|
136
|
+
margin-top: 32px;
|
|
137
|
+
padding-top: 24px;
|
|
138
|
+
border-top: 1px solid var(--border);
|
|
139
|
+
text-align: left;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.features h3 {
|
|
143
|
+
font-size: 13px;
|
|
144
|
+
font-weight: 600;
|
|
145
|
+
color: var(--text-muted);
|
|
146
|
+
text-transform: uppercase;
|
|
147
|
+
letter-spacing: 0.5px;
|
|
148
|
+
margin-bottom: 16px;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.feature {
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: flex-start;
|
|
154
|
+
gap: 12px;
|
|
155
|
+
margin-bottom: 14px;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.feature-icon {
|
|
159
|
+
font-size: 18px;
|
|
160
|
+
line-height: 1;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.feature-text {
|
|
164
|
+
font-size: 14px;
|
|
165
|
+
color: var(--text);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.success-content {
|
|
169
|
+
display: none;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.success-content.visible {
|
|
173
|
+
display: block;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.success-icon {
|
|
177
|
+
width: 80px;
|
|
178
|
+
height: 80px;
|
|
179
|
+
background: #c6f6d5;
|
|
180
|
+
border-radius: 50%;
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
justify-content: center;
|
|
184
|
+
margin: 0 auto 24px;
|
|
185
|
+
font-size: 40px;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.next-steps {
|
|
189
|
+
text-align: left;
|
|
190
|
+
margin-top: 32px;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.next-step {
|
|
194
|
+
background: var(--bg);
|
|
195
|
+
padding: 16px;
|
|
196
|
+
border-radius: 10px;
|
|
197
|
+
margin-bottom: 12px;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.next-step-title {
|
|
201
|
+
font-weight: 600;
|
|
202
|
+
font-size: 14px;
|
|
203
|
+
margin-bottom: 8px;
|
|
204
|
+
color: var(--text);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.next-step-code {
|
|
208
|
+
background: #1a202c;
|
|
209
|
+
color: #e2e8f0;
|
|
210
|
+
padding: 10px 14px;
|
|
211
|
+
border-radius: 6px;
|
|
212
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
213
|
+
font-size: 13px;
|
|
214
|
+
}
|
|
215
|
+
</style>
|
|
216
|
+
</head>
|
|
217
|
+
<body>
|
|
218
|
+
<div class="container">
|
|
219
|
+
<!-- Login Content -->
|
|
220
|
+
<div id="loginContent">
|
|
221
|
+
<div class="logo">M</div>
|
|
222
|
+
<h1>MiniVibe</h1>
|
|
223
|
+
<p class="subtitle">Sign in to connect your CLI to your account</p>
|
|
224
|
+
|
|
225
|
+
<button id="signInBtn" class="btn" onclick="signIn()">
|
|
226
|
+
<img src="https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/google.svg" alt="Google">
|
|
227
|
+
Sign in with Google
|
|
228
|
+
</button>
|
|
229
|
+
|
|
230
|
+
<div id="status" class="status"></div>
|
|
231
|
+
|
|
232
|
+
<div class="features">
|
|
233
|
+
<h3>What you'll get</h3>
|
|
234
|
+
<div class="feature">
|
|
235
|
+
<span class="feature-icon">📱</span>
|
|
236
|
+
<span class="feature-text">Control Claude Code sessions from your iPhone</span>
|
|
237
|
+
</div>
|
|
238
|
+
<div class="feature">
|
|
239
|
+
<span class="feature-icon">⚡</span>
|
|
240
|
+
<span class="feature-text">Approve permissions remotely</span>
|
|
241
|
+
</div>
|
|
242
|
+
<div class="feature">
|
|
243
|
+
<span class="feature-icon">🔄</span>
|
|
244
|
+
<span class="feature-text">Manage multiple sessions in parallel</span>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<!-- Success Content -->
|
|
250
|
+
<div id="successContent" class="success-content">
|
|
251
|
+
<div class="success-icon">✓</div>
|
|
252
|
+
<h1>You're Connected!</h1>
|
|
253
|
+
<p class="subtitle" id="emailDisplay">Signed in as user@example.com</p>
|
|
254
|
+
|
|
255
|
+
<div class="next-steps">
|
|
256
|
+
<div class="next-step">
|
|
257
|
+
<div class="next-step-title">Start your first session</div>
|
|
258
|
+
<div class="next-step-code">vibe "Fix the login bug"</div>
|
|
259
|
+
</div>
|
|
260
|
+
<div class="next-step">
|
|
261
|
+
<div class="next-step-title">Or start with a name</div>
|
|
262
|
+
<div class="next-step-code">vibe --name "Auth Fix" "Fix bug"</div>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<p class="close-msg">You can close this window and return to your terminal.</p>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<script type="module">
|
|
271
|
+
import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-app.js';
|
|
272
|
+
import { getAuth, signInWithPopup, GoogleAuthProvider } from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-auth.js';
|
|
273
|
+
|
|
274
|
+
// Firebase config will be injected by the server
|
|
275
|
+
const firebaseConfig = window.FIREBASE_CONFIG;
|
|
276
|
+
|
|
277
|
+
if (!firebaseConfig) {
|
|
278
|
+
showStatus('error', 'Firebase configuration not found. Check your setup.');
|
|
279
|
+
} else {
|
|
280
|
+
const app = initializeApp(firebaseConfig);
|
|
281
|
+
const auth = getAuth(app);
|
|
282
|
+
const provider = new GoogleAuthProvider();
|
|
283
|
+
|
|
284
|
+
window.signIn = async function() {
|
|
285
|
+
const btn = document.getElementById('signInBtn');
|
|
286
|
+
btn.disabled = true;
|
|
287
|
+
showStatus('loading', 'Opening sign-in popup...');
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const result = await signInWithPopup(auth, provider);
|
|
291
|
+
const idToken = await result.user.getIdToken();
|
|
292
|
+
// Get refresh token for auto-refresh capability
|
|
293
|
+
const refreshToken = result.user.stsTokenManager?.refreshToken || null;
|
|
294
|
+
|
|
295
|
+
showStatus('loading', 'Saving credentials...');
|
|
296
|
+
|
|
297
|
+
// Send token to local server
|
|
298
|
+
const response = await fetch('/callback', {
|
|
299
|
+
method: 'POST',
|
|
300
|
+
headers: { 'Content-Type': 'application/json' },
|
|
301
|
+
body: JSON.stringify({
|
|
302
|
+
token: idToken,
|
|
303
|
+
refreshToken: refreshToken,
|
|
304
|
+
email: result.user.email
|
|
305
|
+
})
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (response.ok) {
|
|
309
|
+
// Show success screen
|
|
310
|
+
document.getElementById('loginContent').style.display = 'none';
|
|
311
|
+
document.getElementById('successContent').classList.add('visible');
|
|
312
|
+
document.getElementById('emailDisplay').textContent = `Signed in as ${result.user.email}`;
|
|
313
|
+
} else {
|
|
314
|
+
throw new Error('Failed to save token');
|
|
315
|
+
}
|
|
316
|
+
} catch (error) {
|
|
317
|
+
console.error(error);
|
|
318
|
+
showStatus('error', error.message || 'Sign in failed. Please try again.');
|
|
319
|
+
btn.disabled = false;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function showStatus(type, message) {
|
|
325
|
+
const status = document.getElementById('status');
|
|
326
|
+
status.className = 'status ' + type;
|
|
327
|
+
status.textContent = message;
|
|
328
|
+
}
|
|
329
|
+
</script>
|
|
330
|
+
</body>
|
|
331
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "minivibe",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI wrapper for Claude Code with mobile remote control",
|
|
5
|
+
"author": "neng.ai",
|
|
6
|
+
"homepage": "https://github.com/python3isfun/neng",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/python3isfun/neng.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/python3isfun/neng/issues"
|
|
13
|
+
},
|
|
14
|
+
"main": "vibe.js",
|
|
15
|
+
"bin": {
|
|
16
|
+
"vibe": "./vibe.js",
|
|
17
|
+
"vibe-agent": "./agent/agent.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"vibe.js",
|
|
21
|
+
"login.html",
|
|
22
|
+
"pty-wrapper.py",
|
|
23
|
+
"pty-wrapper-node.js",
|
|
24
|
+
"agent/"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18.0.0"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"start": "node vibe.js"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"uuid": "^9.0.0",
|
|
34
|
+
"ws": "^8.14.2"
|
|
35
|
+
},
|
|
36
|
+
"optionalDependencies": {
|
|
37
|
+
"node-pty": "^1.0.0"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"claude",
|
|
41
|
+
"claude-code",
|
|
42
|
+
"cli",
|
|
43
|
+
"remote",
|
|
44
|
+
"mobile",
|
|
45
|
+
"ai",
|
|
46
|
+
"terminal"
|
|
47
|
+
],
|
|
48
|
+
"license": "MIT"
|
|
49
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Node.js PTY wrapper for Claude Code (Windows compatible)
|
|
4
|
+
*
|
|
5
|
+
* Alternative to pty-wrapper.py for Windows users.
|
|
6
|
+
* Uses node-pty for cross-platform pseudo-terminal support.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node pty-wrapper-node.js <command> [args...]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
// Try to load node-pty (optional dependency)
|
|
15
|
+
let pty;
|
|
16
|
+
try {
|
|
17
|
+
pty = require('node-pty');
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.error('Error: node-pty is not installed.');
|
|
20
|
+
console.error('Install it with: npm install node-pty');
|
|
21
|
+
console.error('');
|
|
22
|
+
console.error('On Windows, you may also need:');
|
|
23
|
+
console.error(' - Python 3.x');
|
|
24
|
+
console.error(' - Visual Studio Build Tools');
|
|
25
|
+
console.error(' npm install --global windows-build-tools');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Get command to run
|
|
30
|
+
const args = process.argv.slice(2);
|
|
31
|
+
if (args.length === 0) {
|
|
32
|
+
console.error('Usage: pty-wrapper-node.js <command> [args...]');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const command = args[0];
|
|
37
|
+
const commandArgs = args.slice(1);
|
|
38
|
+
|
|
39
|
+
// Get terminal size
|
|
40
|
+
const cols = process.stdout.columns || 80;
|
|
41
|
+
const rows = process.stdout.rows || 24;
|
|
42
|
+
|
|
43
|
+
// Buffer for detecting permission prompts
|
|
44
|
+
let outputBuffer = '';
|
|
45
|
+
const MAX_BUFFER = 2048;
|
|
46
|
+
|
|
47
|
+
// ANSI escape code pattern
|
|
48
|
+
const ansiPattern = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?\x07|\x1b[PX^_].*?\x1b\\/g;
|
|
49
|
+
|
|
50
|
+
function stripAnsi(text) {
|
|
51
|
+
return text.replace(ansiPattern, '');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function detectPermissionPrompt(text) {
|
|
55
|
+
const clean = stripAnsi(text);
|
|
56
|
+
const lines = clean.split('\n');
|
|
57
|
+
|
|
58
|
+
const options = [];
|
|
59
|
+
let question = null;
|
|
60
|
+
|
|
61
|
+
for (const line of lines) {
|
|
62
|
+
const trimmed = line.trim();
|
|
63
|
+
|
|
64
|
+
// Detect question line
|
|
65
|
+
if (/want to|allow|proceed/i.test(trimmed) && trimmed.includes('?')) {
|
|
66
|
+
question = trimmed;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Detect numbered options (1. Yes, 2. Yes and don't ask, 3. Type here)
|
|
70
|
+
const match = trimmed.match(/^[›\s]*(\d+)\.\s+(.+)$/);
|
|
71
|
+
if (match) {
|
|
72
|
+
options.push({
|
|
73
|
+
id: parseInt(match[1]),
|
|
74
|
+
label: match[2].trim(),
|
|
75
|
+
requiresInput: /type|tell/i.test(match[2])
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Only return if we found valid options
|
|
81
|
+
if (options.length >= 2) {
|
|
82
|
+
return {
|
|
83
|
+
type: 'permission_prompt',
|
|
84
|
+
question: question || 'Permission required',
|
|
85
|
+
options
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Spawn the PTY process
|
|
93
|
+
const ptyProcess = pty.spawn(command, commandArgs, {
|
|
94
|
+
name: 'xterm-256color',
|
|
95
|
+
cols,
|
|
96
|
+
rows,
|
|
97
|
+
cwd: process.cwd(),
|
|
98
|
+
env: process.env
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Handle terminal resize
|
|
102
|
+
process.stdout.on('resize', () => {
|
|
103
|
+
ptyProcess.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Forward PTY output to stdout
|
|
107
|
+
ptyProcess.onData((data) => {
|
|
108
|
+
process.stdout.write(data);
|
|
109
|
+
|
|
110
|
+
// Buffer output for prompt detection
|
|
111
|
+
outputBuffer += data;
|
|
112
|
+
if (outputBuffer.length > MAX_BUFFER) {
|
|
113
|
+
outputBuffer = outputBuffer.slice(-MAX_BUFFER);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Try to detect permission prompt
|
|
117
|
+
const prompt = detectPermissionPrompt(outputBuffer);
|
|
118
|
+
if (prompt) {
|
|
119
|
+
// Write to fd 3 if available (for vibe-cli to read)
|
|
120
|
+
try {
|
|
121
|
+
const fs = require('fs');
|
|
122
|
+
const jsonLine = JSON.stringify(prompt) + '\n';
|
|
123
|
+
fs.writeSync(3, jsonLine);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
// FD 3 not available, skip
|
|
126
|
+
}
|
|
127
|
+
outputBuffer = '';
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Forward stdin to PTY
|
|
132
|
+
if (process.stdin.isTTY) {
|
|
133
|
+
process.stdin.setRawMode(true);
|
|
134
|
+
}
|
|
135
|
+
process.stdin.resume();
|
|
136
|
+
process.stdin.on('data', (data) => {
|
|
137
|
+
ptyProcess.write(data.toString());
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Handle PTY exit
|
|
141
|
+
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
142
|
+
if (process.stdin.isTTY) {
|
|
143
|
+
process.stdin.setRawMode(false);
|
|
144
|
+
}
|
|
145
|
+
process.exit(exitCode);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Handle signals (Unix only - Windows doesn't have these)
|
|
149
|
+
if (os.platform() !== 'win32') {
|
|
150
|
+
process.on('SIGINT', () => {
|
|
151
|
+
ptyProcess.kill('SIGINT');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
process.on('SIGTERM', () => {
|
|
155
|
+
ptyProcess.kill('SIGTERM');
|
|
156
|
+
});
|
|
157
|
+
}
|