@exreve/exk 1.0.45 → 1.0.46
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/agentSession.js +108 -8
- package/dist/app-child.js +144 -1673
- package/dist/appHandlers.js +142 -0
- package/dist/appManager.js +5 -5
- package/dist/appRunner.js +2 -2
- package/dist/cloudflaredHandlers.js +279 -0
- package/dist/containerHandlers.js +193 -0
- package/dist/fsHandlers.js +86 -0
- package/dist/githubHandlers.js +521 -0
- package/dist/index.js +142 -1741
- package/dist/projectManager.js +1 -1
- package/dist/runnerGenerator.js +2 -3
- package/dist/sessionHandlers.js +271 -0
- package/dist/ttc-cli.tar.gz +0 -0
- package/dist/updateHandlers.js +82 -0
- package/dist/updater.js +2 -5
- package/package.json +1 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Control Handlers Module
|
|
3
|
+
*
|
|
4
|
+
* Handles app:start, app:stop, app:restart, app:status, app:logs.
|
|
5
|
+
*/
|
|
6
|
+
import { getProjectConfig } from './projectAnalyzer.js';
|
|
7
|
+
import { startApp, stopApp, restartApp, getAppStatuses, getAppLogs } from './appManager.js';
|
|
8
|
+
export function registerAppHandlers(socket, foreground) {
|
|
9
|
+
socket.on('app:start', async (data, callback) => {
|
|
10
|
+
try {
|
|
11
|
+
const { projectId, projectPath, appName } = data;
|
|
12
|
+
const config = await getProjectConfig(projectPath);
|
|
13
|
+
if (!config) {
|
|
14
|
+
callback?.({ success: false, error: 'Project config not found' });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const app = config.apps.find(a => a.name === appName);
|
|
18
|
+
if (!app) {
|
|
19
|
+
callback?.({ success: false, error: `App "${appName}" not found in project config` });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const result = await startApp(projectPath, projectId, app);
|
|
23
|
+
if (result.success) {
|
|
24
|
+
if (foreground)
|
|
25
|
+
console.log(`✓ Started app: ${appName} (PID: ${result.pid})`);
|
|
26
|
+
socket.emit('app:started', {
|
|
27
|
+
projectId,
|
|
28
|
+
appName,
|
|
29
|
+
processId: result.processId,
|
|
30
|
+
pid: result.pid,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
callback?.(result);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (foreground)
|
|
37
|
+
console.error(`✗ Error starting app: ${error.message}`);
|
|
38
|
+
callback?.({ success: false, error: error.message });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
socket.on('app:stop', async (data, callback) => {
|
|
42
|
+
try {
|
|
43
|
+
const { projectId, appName } = data;
|
|
44
|
+
const config = await getProjectConfig(data.projectPath);
|
|
45
|
+
const app = config?.apps.find(a => a.name === appName);
|
|
46
|
+
const result = await stopApp(projectId, appName, app);
|
|
47
|
+
if (result.success) {
|
|
48
|
+
if (foreground)
|
|
49
|
+
console.log(`✓ Stopped app: ${appName}`);
|
|
50
|
+
socket.emit('app:stopped', {
|
|
51
|
+
projectId,
|
|
52
|
+
appName,
|
|
53
|
+
appControlId: data.appControlId,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
callback?.(result);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (foreground)
|
|
60
|
+
console.error(`✗ Error stopping app: ${error.message}`);
|
|
61
|
+
callback?.({ success: false, error: error.message });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
socket.on('app:restart', async (data, callback) => {
|
|
65
|
+
try {
|
|
66
|
+
const { projectId, projectPath, appName } = data;
|
|
67
|
+
const config = await getProjectConfig(projectPath);
|
|
68
|
+
if (!config) {
|
|
69
|
+
callback?.({ success: false, error: 'Project config not found' });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const app = config.apps.find(a => a.name === appName);
|
|
73
|
+
if (!app) {
|
|
74
|
+
callback?.({ success: false, error: `App "${appName}" not found in project config` });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const result = await restartApp(projectPath, projectId, app);
|
|
78
|
+
if (result.success) {
|
|
79
|
+
if (foreground)
|
|
80
|
+
console.log(`✓ Restarted app: ${appName} (PID: ${result.pid})`);
|
|
81
|
+
socket.emit('app:restarted', {
|
|
82
|
+
projectId,
|
|
83
|
+
appName,
|
|
84
|
+
processId: result.processId,
|
|
85
|
+
pid: result.pid,
|
|
86
|
+
appControlId: data.appControlId,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
callback?.(result);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (foreground)
|
|
93
|
+
console.error(`✗ Error restarting app: ${error.message}`);
|
|
94
|
+
callback?.({ success: false, error: error.message });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
socket.on('app:status', async (data, callback) => {
|
|
98
|
+
try {
|
|
99
|
+
const { projectId, projectPath, appName } = data;
|
|
100
|
+
const config = await getProjectConfig(projectPath);
|
|
101
|
+
if (!config) {
|
|
102
|
+
callback?.({ success: false, error: 'Project config not found' });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const appsToCheck = appName
|
|
106
|
+
? config.apps.filter(a => a.name === appName)
|
|
107
|
+
: config.apps;
|
|
108
|
+
const statuses = getAppStatuses(projectId, appsToCheck);
|
|
109
|
+
if (data.appControlId) {
|
|
110
|
+
socket.emit('app:control:response', {
|
|
111
|
+
appControlId: data.appControlId,
|
|
112
|
+
success: true,
|
|
113
|
+
apps: statuses,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
callback?.({ success: true, apps: statuses });
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (foreground)
|
|
120
|
+
console.error(`✗ Error getting app status: ${error.message}`);
|
|
121
|
+
callback?.({ success: false, error: error.message });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
socket.on('app:logs', async (data, callback) => {
|
|
125
|
+
try {
|
|
126
|
+
const { projectId, appName } = data;
|
|
127
|
+
const result = await getAppLogs(projectId, appName, 100);
|
|
128
|
+
if (data.appControlId) {
|
|
129
|
+
socket.emit('app:control:response', {
|
|
130
|
+
appControlId: data.appControlId,
|
|
131
|
+
...result,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
callback?.(result);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (foreground)
|
|
138
|
+
console.error(`✗ Error getting app logs: ${error.message}`);
|
|
139
|
+
callback?.({ success: false, error: error.message });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
package/dist/appManager.js
CHANGED
|
@@ -46,16 +46,16 @@ export async function startApp(projectPath, projectId, app) {
|
|
|
46
46
|
const logFile = getLogFilePath(projectId, app.name);
|
|
47
47
|
// Create runner based on app type
|
|
48
48
|
const runner = createRunner(app, projectPath, projectId, {
|
|
49
|
-
onOutput: async (
|
|
49
|
+
onOutput: async (_output) => {
|
|
50
50
|
// Logs are handled by the runner itself
|
|
51
51
|
},
|
|
52
|
-
onError: (
|
|
52
|
+
onError: (_error) => {
|
|
53
53
|
// Errors are logged by runner
|
|
54
54
|
},
|
|
55
|
-
onExit: (
|
|
55
|
+
onExit: (_code) => {
|
|
56
56
|
removeAppFromRunning(projectId, app.name);
|
|
57
57
|
},
|
|
58
|
-
onStats: (
|
|
58
|
+
onStats: (_stats) => {
|
|
59
59
|
// Stats updates can be used for monitoring
|
|
60
60
|
},
|
|
61
61
|
});
|
|
@@ -90,7 +90,7 @@ export async function startApp(projectPath, projectId, app) {
|
|
|
90
90
|
/**
|
|
91
91
|
* Stop an app using wrapper runner
|
|
92
92
|
*/
|
|
93
|
-
export async function stopApp(projectId, appName,
|
|
93
|
+
export async function stopApp(projectId, appName, _app) {
|
|
94
94
|
try {
|
|
95
95
|
const running = getRunningApp(projectId, appName);
|
|
96
96
|
if (!running) {
|
package/dist/appRunner.js
CHANGED
|
@@ -140,7 +140,7 @@ export class StaticFrontendRunner extends BaseRunner {
|
|
|
140
140
|
// Create a mock process for compatibility
|
|
141
141
|
this.process = {
|
|
142
142
|
pid: process.pid,
|
|
143
|
-
kill: (
|
|
143
|
+
kill: (_signal) => {
|
|
144
144
|
this.stop();
|
|
145
145
|
},
|
|
146
146
|
};
|
|
@@ -291,7 +291,7 @@ export class BackendRunner extends BaseRunner {
|
|
|
291
291
|
? path.join(this.projectPath, this.app.directory)
|
|
292
292
|
: this.projectPath;
|
|
293
293
|
const { shell: stopShell, args: stopArgs } = shellSpawnOpts(this.app.stopCommand);
|
|
294
|
-
|
|
294
|
+
spawn(stopShell, stopArgs, {
|
|
295
295
|
cwd: workingDir,
|
|
296
296
|
stdio: 'ignore',
|
|
297
297
|
});
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflared Handlers Module
|
|
3
|
+
*
|
|
4
|
+
* Handles cloudflared:check, cloudflared:sync, cloudflared:login,
|
|
5
|
+
* cloudflared:regenerate operations.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'fs/promises';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
import { spawn, execSync } from 'child_process';
|
|
11
|
+
export function registerCloudflaredHandlers(socket, foreground) {
|
|
12
|
+
socket.on('cloudflared:check:request', async () => {
|
|
13
|
+
try {
|
|
14
|
+
let installed = false;
|
|
15
|
+
let hasCert = false;
|
|
16
|
+
try {
|
|
17
|
+
execSync('which cloudflared', { stdio: 'ignore' });
|
|
18
|
+
installed = true;
|
|
19
|
+
const certPath = path.join(os.homedir(), '.cloudflared', 'cert.pem');
|
|
20
|
+
try {
|
|
21
|
+
const stats = await fs.stat(certPath);
|
|
22
|
+
hasCert = stats.isFile();
|
|
23
|
+
if (foreground && hasCert) {
|
|
24
|
+
console.log(`✓ Found cert.pem at ${certPath}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
hasCert = false;
|
|
29
|
+
if (foreground) {
|
|
30
|
+
console.log(`✗ cert.pem not found at ${certPath}: ${err.message}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
installed = false;
|
|
36
|
+
}
|
|
37
|
+
socket.emit('cloudflared:check:response', { installed, hasCert });
|
|
38
|
+
if (foreground) {
|
|
39
|
+
console.log(`Cloudflared check: installed=${installed}, hasCert=${hasCert}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
socket.emit('cloudflared:check:response', { installed: false, hasCert: false });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
socket.on('cloudflared:sync:request', async () => {
|
|
47
|
+
try {
|
|
48
|
+
const certPath = path.join(os.homedir(), '.cloudflared', 'cert.pem');
|
|
49
|
+
if (foreground) {
|
|
50
|
+
console.log(`Syncing credentials from ${certPath}`);
|
|
51
|
+
}
|
|
52
|
+
const certContent = await fs.readFile(certPath, 'utf-8');
|
|
53
|
+
const tokenMatch = certContent.match(/-----BEGIN ARGO TUNNEL TOKEN-----\s*([\s\S]*?)\s*-----END ARGO TUNNEL TOKEN-----/);
|
|
54
|
+
if (tokenMatch && tokenMatch[1]) {
|
|
55
|
+
const tokenBase64 = tokenMatch[1].replace(/\s/g, '');
|
|
56
|
+
const tokenJson = Buffer.from(tokenBase64, 'base64').toString('utf-8');
|
|
57
|
+
const tokenData = JSON.parse(tokenJson);
|
|
58
|
+
const apiToken = tokenData.apiToken;
|
|
59
|
+
const accountId = tokenData.accountID;
|
|
60
|
+
const zoneId = tokenData.zoneID;
|
|
61
|
+
if (foreground) {
|
|
62
|
+
console.log(`✓ Extracted credentials: accountId=${accountId}, zoneId=${zoneId}`);
|
|
63
|
+
}
|
|
64
|
+
socket.emit('cloudflared:sync:complete', {
|
|
65
|
+
accountId,
|
|
66
|
+
accountName: undefined,
|
|
67
|
+
apiToken,
|
|
68
|
+
zoneId
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const error = 'Failed to extract token from cert.pem';
|
|
73
|
+
if (foreground) {
|
|
74
|
+
console.error(`✗ ${error}`);
|
|
75
|
+
}
|
|
76
|
+
socket.emit('cloudflared:sync:error', { error });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
const errorMsg = `Failed to read cert.pem: ${error.message}`;
|
|
81
|
+
if (foreground) {
|
|
82
|
+
console.error(`✗ ${errorMsg}`);
|
|
83
|
+
}
|
|
84
|
+
socket.emit('cloudflared:sync:error', { error: errorMsg });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
socket.on('cloudflared:login:request', async () => {
|
|
88
|
+
try {
|
|
89
|
+
// Check if cloudflared is installed
|
|
90
|
+
try {
|
|
91
|
+
execSync('which cloudflared', { stdio: 'ignore' });
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
socket.emit('cloudflared:login:error', { error: 'cloudflared is not installed' });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const loginProcess = spawn('cloudflared', ['tunnel', 'login'], {
|
|
98
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
99
|
+
});
|
|
100
|
+
let stdout = '';
|
|
101
|
+
let stderr = '';
|
|
102
|
+
let urlEmitted = false;
|
|
103
|
+
let alreadyLoggedIn = false;
|
|
104
|
+
let certPath = null;
|
|
105
|
+
const extractCertPath = (text) => {
|
|
106
|
+
const pathMatch = text.match(/existing certificate at\s+([^\s]+)/i) ||
|
|
107
|
+
text.match(/certificate at\s+([^\s]+)/i) ||
|
|
108
|
+
text.match(/cert\.pem.*?at\s+([^\s]+)/i);
|
|
109
|
+
return pathMatch ? pathMatch[1] : null;
|
|
110
|
+
};
|
|
111
|
+
const extractLoginUrl = (text) => {
|
|
112
|
+
const urlPatterns = [
|
|
113
|
+
/https:\/\/dash\.cloudflare\.com\/argotunnel[^\s\)]+/g,
|
|
114
|
+
/https:\/\/[^\s\)]+cloudflareaccess\.org[^\s\)]+/g,
|
|
115
|
+
/https:\/\/[^\s\)]+cloudflare\.com[^\s\)]+/g
|
|
116
|
+
];
|
|
117
|
+
for (const pattern of urlPatterns) {
|
|
118
|
+
const matches = text.match(pattern);
|
|
119
|
+
if (matches && matches.length > 0) {
|
|
120
|
+
return matches[0];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
};
|
|
125
|
+
loginProcess.stdout.on('data', (data) => {
|
|
126
|
+
const text = data.toString();
|
|
127
|
+
stdout += text;
|
|
128
|
+
if (text.includes('existing certificate') || text.includes('cert.pem which login would overwrite')) {
|
|
129
|
+
alreadyLoggedIn = true;
|
|
130
|
+
const extractedPath = extractCertPath(text);
|
|
131
|
+
if (extractedPath) {
|
|
132
|
+
certPath = extractedPath;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!alreadyLoggedIn && !urlEmitted) {
|
|
136
|
+
const url = extractLoginUrl(text);
|
|
137
|
+
if (url) {
|
|
138
|
+
urlEmitted = true;
|
|
139
|
+
socket.emit('cloudflared:login:url', { loginUrl: url });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
loginProcess.stderr.on('data', (data) => {
|
|
144
|
+
const text = data.toString();
|
|
145
|
+
stderr += text;
|
|
146
|
+
if (text.includes('existing certificate') || text.includes('cert.pem which login would overwrite')) {
|
|
147
|
+
alreadyLoggedIn = true;
|
|
148
|
+
const extractedPath = extractCertPath(text);
|
|
149
|
+
if (extractedPath) {
|
|
150
|
+
certPath = extractedPath;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!alreadyLoggedIn && !urlEmitted) {
|
|
154
|
+
const url = extractLoginUrl(text);
|
|
155
|
+
if (url) {
|
|
156
|
+
urlEmitted = true;
|
|
157
|
+
socket.emit('cloudflared:login:url', { loginUrl: url });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
loginProcess.on('close', async (code) => {
|
|
162
|
+
if (alreadyLoggedIn && certPath) {
|
|
163
|
+
// Already logged in - extract credentials from existing cert
|
|
164
|
+
try {
|
|
165
|
+
if (foreground) {
|
|
166
|
+
console.log(`Already logged in, extracting credentials from ${certPath}`);
|
|
167
|
+
}
|
|
168
|
+
const certContent = await fs.readFile(certPath, 'utf-8');
|
|
169
|
+
const tokenMatch = certContent.match(/-----BEGIN ARGO TUNNEL TOKEN-----\s*([\s\S]*?)\s*-----END ARGO TUNNEL TOKEN-----/);
|
|
170
|
+
if (tokenMatch && tokenMatch[1]) {
|
|
171
|
+
const tokenBase64 = tokenMatch[1].replace(/\s/g, '');
|
|
172
|
+
const tokenJson = Buffer.from(tokenBase64, 'base64').toString('utf-8');
|
|
173
|
+
const tokenData = JSON.parse(tokenJson);
|
|
174
|
+
if (foreground) {
|
|
175
|
+
console.log(`✓ Extracted credentials from existing cert`);
|
|
176
|
+
}
|
|
177
|
+
socket.emit('cloudflared:login:complete', {
|
|
178
|
+
accountId: tokenData.accountID,
|
|
179
|
+
accountName: undefined,
|
|
180
|
+
apiToken: tokenData.apiToken,
|
|
181
|
+
zoneId: tokenData.zoneID
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const error = 'Failed to extract token from existing cert.pem';
|
|
186
|
+
if (foreground) {
|
|
187
|
+
console.error(`✗ ${error}`);
|
|
188
|
+
}
|
|
189
|
+
socket.emit('cloudflared:login:error', { error });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
const errorMsg = `Failed to read cert.pem: ${error.message}`;
|
|
194
|
+
if (foreground) {
|
|
195
|
+
console.error(`✗ ${errorMsg}`);
|
|
196
|
+
}
|
|
197
|
+
socket.emit('cloudflared:login:error', { error: errorMsg });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else if (code === 0 && !alreadyLoggedIn) {
|
|
201
|
+
// Login completed successfully - wait a moment then extract credentials
|
|
202
|
+
if (foreground) {
|
|
203
|
+
console.log('Login completed, extracting credentials...');
|
|
204
|
+
}
|
|
205
|
+
setTimeout(async () => {
|
|
206
|
+
try {
|
|
207
|
+
const certFilePath = path.join(os.homedir(), '.cloudflared', 'cert.pem');
|
|
208
|
+
const certContent = await fs.readFile(certFilePath, 'utf-8');
|
|
209
|
+
const tokenMatch = certContent.match(/-----BEGIN ARGO TUNNEL TOKEN-----\s*([\s\S]*?)\s*-----END ARGO TUNNEL TOKEN-----/);
|
|
210
|
+
if (tokenMatch && tokenMatch[1]) {
|
|
211
|
+
const tokenBase64 = tokenMatch[1].replace(/\s/g, '');
|
|
212
|
+
const tokenJson = Buffer.from(tokenBase64, 'base64').toString('utf-8');
|
|
213
|
+
const tokenData = JSON.parse(tokenJson);
|
|
214
|
+
if (foreground) {
|
|
215
|
+
console.log(`✓ Extracted credentials after login`);
|
|
216
|
+
}
|
|
217
|
+
socket.emit('cloudflared:login:complete', {
|
|
218
|
+
accountId: tokenData.accountID,
|
|
219
|
+
accountName: undefined,
|
|
220
|
+
apiToken: tokenData.apiToken,
|
|
221
|
+
zoneId: tokenData.zoneID
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
const error = 'Failed to extract token from cert.pem after login';
|
|
226
|
+
if (foreground) {
|
|
227
|
+
console.error(`✗ ${error}`);
|
|
228
|
+
}
|
|
229
|
+
socket.emit('cloudflared:login:error', { error });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
const errorMsg = `Failed to read cert.pem after login: ${error.message}`;
|
|
234
|
+
if (foreground) {
|
|
235
|
+
console.error(`✗ ${errorMsg}`);
|
|
236
|
+
}
|
|
237
|
+
socket.emit('cloudflared:login:error', { error: errorMsg });
|
|
238
|
+
}
|
|
239
|
+
}, 1000);
|
|
240
|
+
}
|
|
241
|
+
else if (!alreadyLoggedIn) {
|
|
242
|
+
const error = `Login failed with code ${code}: ${stderr || stdout}`;
|
|
243
|
+
if (foreground) {
|
|
244
|
+
console.error(`✗ ${error}`);
|
|
245
|
+
}
|
|
246
|
+
socket.emit('cloudflared:login:error', { error });
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
loginProcess.on('error', (error) => {
|
|
250
|
+
socket.emit('cloudflared:login:error', { error: error.message });
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
socket.emit('cloudflared:login:error', { error: error.message });
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
socket.on('cloudflared:regenerate:request', async () => {
|
|
258
|
+
try {
|
|
259
|
+
const certPath = path.join(os.homedir(), '.cloudflared', 'cert.pem');
|
|
260
|
+
// Delete existing cert.pem
|
|
261
|
+
try {
|
|
262
|
+
await fs.unlink(certPath);
|
|
263
|
+
if (foreground) {
|
|
264
|
+
console.log(`✓ Deleted existing cert.pem`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
if (foreground && error.code !== 'ENOENT') {
|
|
269
|
+
console.log(`Note: Could not delete cert.pem: ${error.message}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// Emit success - frontend will then trigger login
|
|
273
|
+
socket.emit('cloudflared:regenerate:complete', {});
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
socket.emit('cloudflared:regenerate:error', { error: error.message });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Container Handlers Module
|
|
3
|
+
*
|
|
4
|
+
* Handles container:check, container:list, container:start,
|
|
5
|
+
* container:stop, container:remove, container:logs operations.
|
|
6
|
+
*/
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
function getContainerRuntime() {
|
|
10
|
+
try {
|
|
11
|
+
execSync('which docker', { stdio: 'ignore' });
|
|
12
|
+
return 'docker';
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
try {
|
|
16
|
+
execSync('which podman', { stdio: 'ignore' });
|
|
17
|
+
return 'podman';
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function registerContainerHandlers(socket, foreground, cliDir) {
|
|
25
|
+
socket.on('container:check:request', async () => {
|
|
26
|
+
try {
|
|
27
|
+
let enabled = false;
|
|
28
|
+
let runtime;
|
|
29
|
+
let version = '';
|
|
30
|
+
try {
|
|
31
|
+
execSync('which docker', { stdio: 'ignore' });
|
|
32
|
+
runtime = 'docker';
|
|
33
|
+
version = execSync('docker --version', { encoding: 'utf-8' }).trim();
|
|
34
|
+
enabled = true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
try {
|
|
38
|
+
execSync('which podman', { stdio: 'ignore' });
|
|
39
|
+
runtime = 'podman';
|
|
40
|
+
version = execSync('podman --version', { encoding: 'utf-8' }).trim();
|
|
41
|
+
enabled = true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
enabled = false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
socket.emit('container:check:response', { enabled, runtime, version });
|
|
48
|
+
if (foreground) {
|
|
49
|
+
console.log(`Container runtime check: ${enabled ? `${runtime} (${version})` : 'Not found'}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
socket.emit('container:check:response', { enabled: false, runtime: undefined, version: '' });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
socket.on('container:list:request', async () => {
|
|
57
|
+
try {
|
|
58
|
+
const runtime = getContainerRuntime();
|
|
59
|
+
if (!runtime) {
|
|
60
|
+
socket.emit('container:list:response', { success: false, error: 'No container runtime found' });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const output = execSync(`${runtime} ps -a --format "{{json .}}"`, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
|
|
64
|
+
const containers = output.trim().split('\n').filter(Boolean).map(line => {
|
|
65
|
+
try {
|
|
66
|
+
const c = JSON.parse(line);
|
|
67
|
+
const ports = [];
|
|
68
|
+
if (c.Ports) {
|
|
69
|
+
const portMatches = c.Ports.match(/(\d+)->(\d+)/g);
|
|
70
|
+
if (portMatches) {
|
|
71
|
+
portMatches.forEach((p) => {
|
|
72
|
+
const [host, container] = p.split('->').map(Number);
|
|
73
|
+
ports.push({ host, container });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
containerId: c.ID,
|
|
79
|
+
name: c.Names.replace(/^\//, ''),
|
|
80
|
+
image: c.Image,
|
|
81
|
+
status: c.State === 'running' ? 'running' : c.State === 'paused' ? 'paused' : c.Status === 'exited' ? 'exited' : 'stopped',
|
|
82
|
+
ports,
|
|
83
|
+
createdAt: new Date(c.CreatedAt).toISOString()
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}).filter(Boolean);
|
|
90
|
+
socket.emit('container:list:response', { success: true, containers });
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
socket.emit('container:list:response', { success: false, error: error.message });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
socket.on('container:start:request', async (data) => {
|
|
97
|
+
try {
|
|
98
|
+
const runtime = getContainerRuntime();
|
|
99
|
+
if (!runtime) {
|
|
100
|
+
socket.emit('container:start:response', { success: false, error: 'No container runtime found' });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const { name, image, ports = [], env = {}, runAsRoot = false } = data;
|
|
104
|
+
let cmd = `${runtime} run -d --name ${name}`;
|
|
105
|
+
if (!runAsRoot) {
|
|
106
|
+
cmd += ' --user 1000:1000';
|
|
107
|
+
}
|
|
108
|
+
cmd += ' --memory=8g';
|
|
109
|
+
cmd += ` -v "${cliDir}:/opt/ttc:ro"`;
|
|
110
|
+
const entrypointScript = path.join(cliDir, 'container-entrypoint.sh');
|
|
111
|
+
cmd += ` -v "${entrypointScript}:/entrypoint.sh:ro"`;
|
|
112
|
+
ports.forEach(p => {
|
|
113
|
+
cmd += ` -p ${p.host}:${p.container}`;
|
|
114
|
+
});
|
|
115
|
+
Object.entries(env).forEach(([k, v]) => {
|
|
116
|
+
const escapedValue = String(v).replace(/"/g, '\\"');
|
|
117
|
+
cmd += ` -e ${k}="${escapedValue}"`;
|
|
118
|
+
});
|
|
119
|
+
cmd += ` -e CONTAINER_NAME="${name}"`;
|
|
120
|
+
cmd += ` -e HOSTNAME="${name}"`;
|
|
121
|
+
cmd += ` --entrypoint /bin/sh ${image} /entrypoint.sh`;
|
|
122
|
+
if (foreground) {
|
|
123
|
+
console.log(`Starting container: ${cmd}`);
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
execSync(`${runtime} pull ${image}`, { stdio: 'ignore' });
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Pull failed, but might already exist locally
|
|
130
|
+
}
|
|
131
|
+
const output = execSync(cmd, { encoding: 'utf-8', stdio: 'pipe' });
|
|
132
|
+
const containerId = output.trim();
|
|
133
|
+
socket.emit('container:start:response', { success: true, containerId });
|
|
134
|
+
if (foreground) {
|
|
135
|
+
console.log(`✓ Container started: ${containerId}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
socket.emit('container:start:response', { success: false, error: error.message });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
socket.on('container:stop:request', async (data) => {
|
|
143
|
+
try {
|
|
144
|
+
const runtime = getContainerRuntime();
|
|
145
|
+
if (!runtime) {
|
|
146
|
+
socket.emit('container:stop:response', { success: false, error: 'No container runtime found' });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const { containerId } = data;
|
|
150
|
+
execSync(`${runtime} stop ${containerId}`, { stdio: 'ignore' });
|
|
151
|
+
socket.emit('container:stop:response', { success: true });
|
|
152
|
+
if (foreground) {
|
|
153
|
+
console.log(`✓ Container stopped: ${containerId}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
socket.emit('container:stop:response', { success: false, error: error.message });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
socket.on('container:remove:request', async (data) => {
|
|
161
|
+
try {
|
|
162
|
+
const runtime = getContainerRuntime();
|
|
163
|
+
if (!runtime) {
|
|
164
|
+
socket.emit('container:remove:response', { success: false, error: 'No container runtime found' });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const { containerId } = data;
|
|
168
|
+
execSync(`${runtime} rm -f ${containerId}`, { stdio: 'ignore' });
|
|
169
|
+
socket.emit('container:remove:response', { success: true });
|
|
170
|
+
if (foreground) {
|
|
171
|
+
console.log(`✓ Container removed: ${containerId}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
socket.emit('container:remove:response', { success: false, error: error.message });
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
socket.on('container:logs:request', async (data) => {
|
|
179
|
+
try {
|
|
180
|
+
const runtime = getContainerRuntime();
|
|
181
|
+
if (!runtime) {
|
|
182
|
+
socket.emit('container:logs:response', { success: false, error: 'No container runtime found' });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const { containerId, lines = 100 } = data;
|
|
186
|
+
const logs = execSync(`${runtime} logs --tail ${lines} ${containerId}`, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
|
|
187
|
+
socket.emit('container:logs:response', { success: true, logs });
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
socket.emit('container:logs:response', { success: false, error: error.message });
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|