@ekkos/cli 0.2.10 → 0.2.11
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/dist/agent/daemon.d.ts +86 -0
- package/dist/agent/daemon.js +297 -0
- package/dist/agent/pty-runner.d.ts +51 -0
- package/dist/agent/pty-runner.js +184 -0
- package/dist/commands/agent.d.ts +44 -0
- package/dist/commands/agent.js +300 -0
- package/dist/commands/doctor.js +27 -14
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +289 -65
- package/dist/commands/setup-remote.d.ts +20 -0
- package/dist/commands/setup-remote.js +467 -0
- package/dist/index.js +75 -1
- package/dist/utils/state.d.ts +2 -0
- package/package.json +1 -1
- package/templates/ekkos-manifest.json +1 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ekkOS Agent Daemon
|
|
3
|
+
*
|
|
4
|
+
* Background daemon that:
|
|
5
|
+
* 1. Maintains WebSocket connection to cloud relay
|
|
6
|
+
* 2. Receives session start requests
|
|
7
|
+
* 3. Spawns `ekkos run -d` in PTY
|
|
8
|
+
* 4. Relays PTY I/O to cloud
|
|
9
|
+
*/
|
|
10
|
+
interface DaemonConfig {
|
|
11
|
+
deviceToken: string;
|
|
12
|
+
deviceId: string;
|
|
13
|
+
deviceName: string;
|
|
14
|
+
verbose?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare class AgentDaemon {
|
|
17
|
+
private config;
|
|
18
|
+
private ws;
|
|
19
|
+
private reconnectAttempt;
|
|
20
|
+
private heartbeatTimer;
|
|
21
|
+
private ptyRunner;
|
|
22
|
+
private currentSessionId;
|
|
23
|
+
private running;
|
|
24
|
+
constructor(config: DaemonConfig);
|
|
25
|
+
/**
|
|
26
|
+
* Start the daemon
|
|
27
|
+
*/
|
|
28
|
+
start(): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Stop the daemon
|
|
31
|
+
*/
|
|
32
|
+
stop(): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Connect to relay server
|
|
35
|
+
*/
|
|
36
|
+
private connect;
|
|
37
|
+
/**
|
|
38
|
+
* Handle WebSocket open
|
|
39
|
+
*/
|
|
40
|
+
private handleOpen;
|
|
41
|
+
/**
|
|
42
|
+
* Handle incoming message
|
|
43
|
+
*/
|
|
44
|
+
private handleMessage;
|
|
45
|
+
/**
|
|
46
|
+
* Handle session start request
|
|
47
|
+
*/
|
|
48
|
+
private handleSessionStart;
|
|
49
|
+
/**
|
|
50
|
+
* Handle session end request
|
|
51
|
+
*/
|
|
52
|
+
private handleSessionEnd;
|
|
53
|
+
/**
|
|
54
|
+
* Handle input from browser
|
|
55
|
+
*/
|
|
56
|
+
private handleInput;
|
|
57
|
+
/**
|
|
58
|
+
* Handle resize from browser
|
|
59
|
+
*/
|
|
60
|
+
private handleResize;
|
|
61
|
+
/**
|
|
62
|
+
* Handle PTY exit
|
|
63
|
+
*/
|
|
64
|
+
private handlePTYExit;
|
|
65
|
+
/**
|
|
66
|
+
* Send PTY output to server
|
|
67
|
+
*/
|
|
68
|
+
private sendOutput;
|
|
69
|
+
/**
|
|
70
|
+
* Handle WebSocket close
|
|
71
|
+
*/
|
|
72
|
+
private handleClose;
|
|
73
|
+
/**
|
|
74
|
+
* Handle WebSocket error
|
|
75
|
+
*/
|
|
76
|
+
private handleError;
|
|
77
|
+
/**
|
|
78
|
+
* Send message to server
|
|
79
|
+
*/
|
|
80
|
+
private sendMessage;
|
|
81
|
+
/**
|
|
82
|
+
* Log message
|
|
83
|
+
*/
|
|
84
|
+
private log;
|
|
85
|
+
}
|
|
86
|
+
export {};
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ekkOS Agent Daemon
|
|
4
|
+
*
|
|
5
|
+
* Background daemon that:
|
|
6
|
+
* 1. Maintains WebSocket connection to cloud relay
|
|
7
|
+
* 2. Receives session start requests
|
|
8
|
+
* 3. Spawns `ekkos run -d` in PTY
|
|
9
|
+
* 4. Relays PTY I/O to cloud
|
|
10
|
+
*/
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.AgentDaemon = void 0;
|
|
49
|
+
const ws_1 = __importDefault(require("ws"));
|
|
50
|
+
const os = __importStar(require("os"));
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const path = __importStar(require("path"));
|
|
53
|
+
const pty_runner_1 = require("./pty-runner");
|
|
54
|
+
const RELAY_URL = process.env.RELAY_WS_URL || 'wss://mcp.ekkos.dev';
|
|
55
|
+
const HEARTBEAT_INTERVAL = 30000; // 30 seconds
|
|
56
|
+
const RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 16000, 32000, 60000]; // Exponential backoff
|
|
57
|
+
class AgentDaemon {
|
|
58
|
+
constructor(config) {
|
|
59
|
+
this.ws = null;
|
|
60
|
+
this.reconnectAttempt = 0;
|
|
61
|
+
this.heartbeatTimer = null;
|
|
62
|
+
this.ptyRunner = null;
|
|
63
|
+
this.currentSessionId = null;
|
|
64
|
+
this.running = false;
|
|
65
|
+
this.config = config;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Start the daemon
|
|
69
|
+
*/
|
|
70
|
+
async start() {
|
|
71
|
+
this.running = true;
|
|
72
|
+
this.log('Starting ekkOS agent daemon...');
|
|
73
|
+
this.connect();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Stop the daemon
|
|
77
|
+
*/
|
|
78
|
+
async stop() {
|
|
79
|
+
this.running = false;
|
|
80
|
+
// Stop heartbeat
|
|
81
|
+
if (this.heartbeatTimer) {
|
|
82
|
+
clearInterval(this.heartbeatTimer);
|
|
83
|
+
this.heartbeatTimer = null;
|
|
84
|
+
}
|
|
85
|
+
// Close PTY
|
|
86
|
+
if (this.ptyRunner) {
|
|
87
|
+
this.ptyRunner.kill();
|
|
88
|
+
this.ptyRunner = null;
|
|
89
|
+
}
|
|
90
|
+
// Close WebSocket
|
|
91
|
+
if (this.ws) {
|
|
92
|
+
this.ws.close();
|
|
93
|
+
this.ws = null;
|
|
94
|
+
}
|
|
95
|
+
this.log('Daemon stopped');
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Connect to relay server
|
|
99
|
+
*/
|
|
100
|
+
connect() {
|
|
101
|
+
if (!this.running)
|
|
102
|
+
return;
|
|
103
|
+
const url = `${RELAY_URL}/api/v1/relay/device`;
|
|
104
|
+
this.log(`Connecting to ${url}...`);
|
|
105
|
+
this.ws = new ws_1.default(url, {
|
|
106
|
+
headers: {
|
|
107
|
+
'X-Device-Token': this.config.deviceToken,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
this.ws.on('open', () => this.handleOpen());
|
|
111
|
+
this.ws.on('message', (data) => this.handleMessage(data));
|
|
112
|
+
this.ws.on('close', (code, reason) => this.handleClose(code, reason.toString()));
|
|
113
|
+
this.ws.on('error', (err) => this.handleError(err));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Handle WebSocket open
|
|
117
|
+
*/
|
|
118
|
+
handleOpen() {
|
|
119
|
+
this.log('Connected to relay server');
|
|
120
|
+
this.reconnectAttempt = 0;
|
|
121
|
+
// Start heartbeat
|
|
122
|
+
this.heartbeatTimer = setInterval(() => {
|
|
123
|
+
this.sendMessage({ type: 'heartbeat' });
|
|
124
|
+
}, HEARTBEAT_INTERVAL);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Handle incoming message
|
|
128
|
+
*/
|
|
129
|
+
handleMessage(rawData) {
|
|
130
|
+
let message;
|
|
131
|
+
try {
|
|
132
|
+
message = JSON.parse(rawData.toString());
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
this.log('Invalid message from server:', err);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
switch (message.type) {
|
|
139
|
+
case 'connected':
|
|
140
|
+
this.log('Registered with relay');
|
|
141
|
+
break;
|
|
142
|
+
case 'session_start':
|
|
143
|
+
this.handleSessionStart(message.sessionId);
|
|
144
|
+
break;
|
|
145
|
+
case 'session_end':
|
|
146
|
+
this.handleSessionEnd(message.sessionId);
|
|
147
|
+
break;
|
|
148
|
+
case 'input':
|
|
149
|
+
this.handleInput(message.data || '');
|
|
150
|
+
break;
|
|
151
|
+
case 'resize':
|
|
152
|
+
this.handleResize(message.cols || 80, message.rows || 24);
|
|
153
|
+
break;
|
|
154
|
+
case 'error':
|
|
155
|
+
this.log('Server error:', message.error);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Handle session start request
|
|
161
|
+
*/
|
|
162
|
+
handleSessionStart(sessionId) {
|
|
163
|
+
this.log(`Session start request: ${sessionId}`);
|
|
164
|
+
// Kill existing session if any
|
|
165
|
+
if (this.ptyRunner) {
|
|
166
|
+
this.ptyRunner.kill();
|
|
167
|
+
this.ptyRunner = null;
|
|
168
|
+
}
|
|
169
|
+
this.currentSessionId = sessionId;
|
|
170
|
+
// Start PTY with ekkos run -d
|
|
171
|
+
this.ptyRunner = new pty_runner_1.PTYRunner({
|
|
172
|
+
command: 'ekkos',
|
|
173
|
+
args: ['run', '-d', '-b'], // -d for doctor check, -b for bypass permissions
|
|
174
|
+
onData: (data) => this.sendOutput(data),
|
|
175
|
+
onExit: (code) => this.handlePTYExit(code),
|
|
176
|
+
verbose: this.config.verbose,
|
|
177
|
+
});
|
|
178
|
+
this.ptyRunner.start();
|
|
179
|
+
// Notify server that PTY is ready
|
|
180
|
+
this.sendMessage({
|
|
181
|
+
type: 'ready',
|
|
182
|
+
sessionId,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Handle session end request
|
|
187
|
+
*/
|
|
188
|
+
handleSessionEnd(sessionId) {
|
|
189
|
+
if (sessionId && sessionId !== this.currentSessionId) {
|
|
190
|
+
return; // Not our session
|
|
191
|
+
}
|
|
192
|
+
this.log('Session ended');
|
|
193
|
+
if (this.ptyRunner) {
|
|
194
|
+
this.ptyRunner.kill();
|
|
195
|
+
this.ptyRunner = null;
|
|
196
|
+
}
|
|
197
|
+
this.currentSessionId = null;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Handle input from browser
|
|
201
|
+
*/
|
|
202
|
+
handleInput(data) {
|
|
203
|
+
if (this.ptyRunner) {
|
|
204
|
+
this.ptyRunner.write(data);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Handle resize from browser
|
|
209
|
+
*/
|
|
210
|
+
handleResize(cols, rows) {
|
|
211
|
+
if (this.ptyRunner) {
|
|
212
|
+
this.ptyRunner.resize(cols, rows);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Handle PTY exit
|
|
217
|
+
*/
|
|
218
|
+
handlePTYExit(code) {
|
|
219
|
+
this.log(`PTY exited with code ${code}`);
|
|
220
|
+
this.sendMessage({
|
|
221
|
+
type: 'session_end',
|
|
222
|
+
sessionId: this.currentSessionId || undefined,
|
|
223
|
+
});
|
|
224
|
+
this.ptyRunner = null;
|
|
225
|
+
this.currentSessionId = null;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Send PTY output to server
|
|
229
|
+
*/
|
|
230
|
+
sendOutput(data) {
|
|
231
|
+
this.sendMessage({
|
|
232
|
+
type: 'output',
|
|
233
|
+
sessionId: this.currentSessionId || undefined,
|
|
234
|
+
data,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Handle WebSocket close
|
|
239
|
+
*/
|
|
240
|
+
handleClose(code, reason) {
|
|
241
|
+
this.log(`Disconnected: ${code} ${reason}`);
|
|
242
|
+
// Stop heartbeat
|
|
243
|
+
if (this.heartbeatTimer) {
|
|
244
|
+
clearInterval(this.heartbeatTimer);
|
|
245
|
+
this.heartbeatTimer = null;
|
|
246
|
+
}
|
|
247
|
+
// Kill PTY if running
|
|
248
|
+
if (this.ptyRunner) {
|
|
249
|
+
this.ptyRunner.kill();
|
|
250
|
+
this.ptyRunner = null;
|
|
251
|
+
}
|
|
252
|
+
this.currentSessionId = null;
|
|
253
|
+
// Reconnect if still running
|
|
254
|
+
if (this.running) {
|
|
255
|
+
const delay = RECONNECT_DELAYS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS.length - 1)];
|
|
256
|
+
this.reconnectAttempt++;
|
|
257
|
+
this.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt})...`);
|
|
258
|
+
setTimeout(() => this.connect(), delay);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Handle WebSocket error
|
|
263
|
+
*/
|
|
264
|
+
handleError(err) {
|
|
265
|
+
this.log('WebSocket error:', err.message);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Send message to server
|
|
269
|
+
*/
|
|
270
|
+
sendMessage(message) {
|
|
271
|
+
if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
|
|
272
|
+
this.ws.send(JSON.stringify(message));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Log message
|
|
277
|
+
*/
|
|
278
|
+
log(...args) {
|
|
279
|
+
const timestamp = new Date().toISOString();
|
|
280
|
+
const message = `[${timestamp}] ${args.map(String).join(' ')}`;
|
|
281
|
+
if (this.config.verbose) {
|
|
282
|
+
console.log(message);
|
|
283
|
+
}
|
|
284
|
+
// Also log to file
|
|
285
|
+
try {
|
|
286
|
+
const logDir = path.join(os.homedir(), '.ekkos');
|
|
287
|
+
if (!fs.existsSync(logDir)) {
|
|
288
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
289
|
+
}
|
|
290
|
+
fs.appendFileSync(path.join(logDir, 'agent.log'), message + '\n');
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
// Ignore log errors
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
exports.AgentDaemon = AgentDaemon;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PTY Runner for ekkOS Agent
|
|
3
|
+
*
|
|
4
|
+
* Spawns `ekkos run -d` in a pseudoterminal and handles I/O.
|
|
5
|
+
* Based on the PTY patterns from packages/ekkos-cli/src/commands/run.ts
|
|
6
|
+
*/
|
|
7
|
+
interface PTYRunnerConfig {
|
|
8
|
+
command: string;
|
|
9
|
+
args: string[];
|
|
10
|
+
onData: (data: string) => void;
|
|
11
|
+
onExit: (code: number) => void;
|
|
12
|
+
cols?: number;
|
|
13
|
+
rows?: number;
|
|
14
|
+
verbose?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare class PTYRunner {
|
|
17
|
+
private config;
|
|
18
|
+
private ptyProcess;
|
|
19
|
+
private spawnProcess;
|
|
20
|
+
private usePty;
|
|
21
|
+
constructor(config: PTYRunnerConfig);
|
|
22
|
+
/**
|
|
23
|
+
* Start the PTY process
|
|
24
|
+
*/
|
|
25
|
+
start(): void;
|
|
26
|
+
/**
|
|
27
|
+
* Start using node-pty (preferred)
|
|
28
|
+
*/
|
|
29
|
+
private startWithPty;
|
|
30
|
+
/**
|
|
31
|
+
* Start using spawn (fallback for Windows without PTY)
|
|
32
|
+
*/
|
|
33
|
+
private startWithSpawn;
|
|
34
|
+
/**
|
|
35
|
+
* Write data to PTY stdin
|
|
36
|
+
*/
|
|
37
|
+
write(data: string): void;
|
|
38
|
+
/**
|
|
39
|
+
* Resize PTY
|
|
40
|
+
*/
|
|
41
|
+
resize(cols: number, rows: number): void;
|
|
42
|
+
/**
|
|
43
|
+
* Kill the PTY process
|
|
44
|
+
*/
|
|
45
|
+
kill(): void;
|
|
46
|
+
/**
|
|
47
|
+
* Check if PTY is running
|
|
48
|
+
*/
|
|
49
|
+
isRunning(): boolean;
|
|
50
|
+
}
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* PTY Runner for ekkOS Agent
|
|
4
|
+
*
|
|
5
|
+
* Spawns `ekkos run -d` in a pseudoterminal and handles I/O.
|
|
6
|
+
* Based on the PTY patterns from packages/ekkos-cli/src/commands/run.ts
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.PTYRunner = void 0;
|
|
43
|
+
const os = __importStar(require("os"));
|
|
44
|
+
const child_process_1 = require("child_process");
|
|
45
|
+
// Try to load node-pty
|
|
46
|
+
let pty = null;
|
|
47
|
+
try {
|
|
48
|
+
pty = require('node-pty');
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// node-pty not available, will use spawn fallback
|
|
52
|
+
}
|
|
53
|
+
class PTYRunner {
|
|
54
|
+
constructor(config) {
|
|
55
|
+
this.ptyProcess = null;
|
|
56
|
+
this.spawnProcess = null;
|
|
57
|
+
this.config = config;
|
|
58
|
+
this.usePty = pty !== null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Start the PTY process
|
|
62
|
+
*/
|
|
63
|
+
start() {
|
|
64
|
+
if (this.usePty && pty) {
|
|
65
|
+
this.startWithPty();
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
this.startWithSpawn();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Start using node-pty (preferred)
|
|
73
|
+
*/
|
|
74
|
+
startWithPty() {
|
|
75
|
+
if (!pty)
|
|
76
|
+
return;
|
|
77
|
+
const shell = os.platform() === 'win32' ? 'cmd.exe' : 'bash';
|
|
78
|
+
const shellArgs = os.platform() === 'win32'
|
|
79
|
+
? ['/c', this.config.command, ...this.config.args]
|
|
80
|
+
: ['-c', `${this.config.command} ${this.config.args.join(' ')}`];
|
|
81
|
+
this.ptyProcess = pty.spawn(shell, shellArgs, {
|
|
82
|
+
name: 'xterm-256color',
|
|
83
|
+
cols: this.config.cols || 80,
|
|
84
|
+
rows: this.config.rows || 24,
|
|
85
|
+
cwd: process.cwd(),
|
|
86
|
+
env: {
|
|
87
|
+
...process.env,
|
|
88
|
+
TERM: 'xterm-256color',
|
|
89
|
+
COLORTERM: 'truecolor',
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
this.ptyProcess.onData((data) => {
|
|
93
|
+
this.config.onData(data);
|
|
94
|
+
});
|
|
95
|
+
this.ptyProcess.onExit(({ exitCode }) => {
|
|
96
|
+
this.config.onExit(exitCode);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Start using spawn (fallback for Windows without PTY)
|
|
101
|
+
*/
|
|
102
|
+
startWithSpawn() {
|
|
103
|
+
const isWindows = os.platform() === 'win32';
|
|
104
|
+
if (isWindows) {
|
|
105
|
+
// Windows: Use cmd.exe
|
|
106
|
+
this.spawnProcess = (0, child_process_1.spawn)('cmd.exe', ['/c', this.config.command, ...this.config.args], {
|
|
107
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
108
|
+
cwd: process.cwd(),
|
|
109
|
+
env: process.env,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// Unix: Use script for PTY emulation
|
|
114
|
+
const scriptArgs = os.platform() === 'darwin'
|
|
115
|
+
? ['-q', '/dev/null', this.config.command, ...this.config.args]
|
|
116
|
+
: ['-q', '-c', `${this.config.command} ${this.config.args.join(' ')}`, '/dev/null'];
|
|
117
|
+
this.spawnProcess = (0, child_process_1.spawn)('script', scriptArgs, {
|
|
118
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
119
|
+
cwd: process.cwd(),
|
|
120
|
+
env: {
|
|
121
|
+
...process.env,
|
|
122
|
+
TERM: 'xterm-256color',
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (this.spawnProcess.stdout) {
|
|
127
|
+
this.spawnProcess.stdout.on('data', (data) => {
|
|
128
|
+
this.config.onData(data.toString());
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (this.spawnProcess.stderr) {
|
|
132
|
+
this.spawnProcess.stderr.on('data', (data) => {
|
|
133
|
+
this.config.onData(data.toString());
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
this.spawnProcess.on('exit', (code) => {
|
|
137
|
+
this.config.onExit(code || 0);
|
|
138
|
+
});
|
|
139
|
+
this.spawnProcess.on('error', (err) => {
|
|
140
|
+
this.config.onData(`Error: ${err.message}\r\n`);
|
|
141
|
+
this.config.onExit(1);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Write data to PTY stdin
|
|
146
|
+
*/
|
|
147
|
+
write(data) {
|
|
148
|
+
if (this.ptyProcess) {
|
|
149
|
+
this.ptyProcess.write(data);
|
|
150
|
+
}
|
|
151
|
+
else if (this.spawnProcess && this.spawnProcess.stdin) {
|
|
152
|
+
this.spawnProcess.stdin.write(data);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Resize PTY
|
|
157
|
+
*/
|
|
158
|
+
resize(cols, rows) {
|
|
159
|
+
if (this.ptyProcess && this.ptyProcess.resize) {
|
|
160
|
+
this.ptyProcess.resize(cols, rows);
|
|
161
|
+
}
|
|
162
|
+
// Spawn fallback doesn't support resize
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Kill the PTY process
|
|
166
|
+
*/
|
|
167
|
+
kill() {
|
|
168
|
+
if (this.ptyProcess) {
|
|
169
|
+
this.ptyProcess.kill();
|
|
170
|
+
this.ptyProcess = null;
|
|
171
|
+
}
|
|
172
|
+
if (this.spawnProcess) {
|
|
173
|
+
this.spawnProcess.kill('SIGTERM');
|
|
174
|
+
this.spawnProcess = null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Check if PTY is running
|
|
179
|
+
*/
|
|
180
|
+
isRunning() {
|
|
181
|
+
return this.ptyProcess !== null || this.spawnProcess !== null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
exports.PTYRunner = PTYRunner;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ekkos agent - Agent management commands for remote terminal
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* - daemon: Run the agent daemon (used by service)
|
|
6
|
+
* - start: Start the agent service
|
|
7
|
+
* - stop: Stop the agent service
|
|
8
|
+
* - restart: Restart the agent service
|
|
9
|
+
* - status: Check agent status
|
|
10
|
+
* - uninstall: Remove agent service
|
|
11
|
+
*/
|
|
12
|
+
export interface AgentOptions {
|
|
13
|
+
verbose?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Run the agent daemon (foreground)
|
|
17
|
+
*/
|
|
18
|
+
export declare function agentDaemon(options?: AgentOptions): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Start the agent service
|
|
21
|
+
*/
|
|
22
|
+
export declare function agentStart(options?: AgentOptions): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Stop the agent service
|
|
25
|
+
*/
|
|
26
|
+
export declare function agentStop(options?: AgentOptions): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Restart the agent service
|
|
29
|
+
*/
|
|
30
|
+
export declare function agentRestart(options?: AgentOptions): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Check agent status
|
|
33
|
+
*/
|
|
34
|
+
export declare function agentStatus(options?: AgentOptions): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Uninstall the agent service
|
|
37
|
+
*/
|
|
38
|
+
export declare function agentUninstall(options?: AgentOptions): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Show agent logs
|
|
41
|
+
*/
|
|
42
|
+
export declare function agentLogs(options?: {
|
|
43
|
+
follow?: boolean;
|
|
44
|
+
}): Promise<void>;
|