@oussema_mili/test-pkg-123 1.1.32
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/LICENSE +29 -0
- package/README.md +220 -0
- package/auth-callback.html +97 -0
- package/auth.js +276 -0
- package/cli-commands.js +1921 -0
- package/containerManager.js +304 -0
- package/daemon/agentRunner.js +491 -0
- package/daemon/daemonEntry.js +64 -0
- package/daemon/daemonManager.js +266 -0
- package/daemon/logManager.js +227 -0
- package/dist/styles.css +504 -0
- package/docker-actions/apps.js +3913 -0
- package/docker-actions/config-transformer.js +380 -0
- package/docker-actions/containers.js +355 -0
- package/docker-actions/general.js +171 -0
- package/docker-actions/images.js +1128 -0
- package/docker-actions/logs.js +224 -0
- package/docker-actions/metrics.js +270 -0
- package/docker-actions/registry.js +1100 -0
- package/docker-actions/setup-tasks.js +859 -0
- package/docker-actions/terminal.js +247 -0
- package/docker-actions/volumes.js +713 -0
- package/helper-functions.js +193 -0
- package/index.html +83 -0
- package/index.js +341 -0
- package/package.json +82 -0
- package/postcss.config.mjs +5 -0
- package/scripts/release.sh +212 -0
- package/setup/setupWizard.js +403 -0
- package/store/agentSessionStore.js +51 -0
- package/store/agentStore.js +113 -0
- package/store/configStore.js +171 -0
- package/store/daemonStore.js +217 -0
- package/store/deviceCredentialStore.js +107 -0
- package/store/npmTokenStore.js +65 -0
- package/store/registryStore.js +329 -0
- package/store/setupState.js +147 -0
- package/styles.css +1 -0
- package/utils/appLogger.js +223 -0
- package/utils/deviceInfo.js +98 -0
- package/utils/ecrAuth.js +225 -0
- package/utils/encryption.js +112 -0
- package/utils/envSetup.js +41 -0
- package/utils/errorHandler.js +327 -0
- package/utils/portUtils.js +59 -0
- package/utils/prerequisites.js +323 -0
- package/utils/prompts.js +318 -0
- package/utils/ssl-certificates.js +256 -0
- package/websocket-server.js +415 -0
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { exec } from "child_process";
|
|
5
|
+
import dns from "dns";
|
|
6
|
+
import crypto from "crypto";
|
|
7
|
+
import { promisify } from "util";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import { loadConfig } from "../store/configStore.js";
|
|
10
|
+
import sslCertificates from "../utils/ssl-certificates.js";
|
|
11
|
+
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
const dnsResolve = promisify(dns.resolve);
|
|
14
|
+
|
|
15
|
+
// Load configuration
|
|
16
|
+
const config = loadConfig();
|
|
17
|
+
const FENWAVE_CONFIG_DIR = path.join(os.homedir(), ".fenwave");
|
|
18
|
+
const SETUP_DIR = path.join(FENWAVE_CONFIG_DIR, "setup");
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Ensure setup directory exists
|
|
22
|
+
*/
|
|
23
|
+
function ensureSetupDir() {
|
|
24
|
+
if (!fs.existsSync(SETUP_DIR)) {
|
|
25
|
+
fs.mkdirSync(SETUP_DIR, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Generate a hash of the setup wizard configuration
|
|
31
|
+
* This hash is used to determine if the setup tasks have changed
|
|
32
|
+
* Only includes task properties that affect execution (not order, title, description)
|
|
33
|
+
*/
|
|
34
|
+
function generateSetupConfigHash(tasks) {
|
|
35
|
+
if (!tasks || tasks.length === 0) {
|
|
36
|
+
return "empty";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Extract only the properties that matter for execution
|
|
40
|
+
const taskSignatures = tasks.map(task => ({
|
|
41
|
+
id: task.id,
|
|
42
|
+
type: task.type,
|
|
43
|
+
config: task.config,
|
|
44
|
+
required: task.required,
|
|
45
|
+
outputVariable: task.outputVariable,
|
|
46
|
+
verification: task.verification ? {
|
|
47
|
+
type: task.verification.type,
|
|
48
|
+
target: task.verification.target,
|
|
49
|
+
command: task.verification.command,
|
|
50
|
+
expectedFiles: task.verification.expectedFiles,
|
|
51
|
+
} : undefined,
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
const configString = JSON.stringify(taskSignatures);
|
|
55
|
+
return crypto.createHash("sha256").update(configString).digest("hex").substring(0, 16);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get setup progress file path for an app (by hash)
|
|
60
|
+
*/
|
|
61
|
+
function getSetupFilePathByHash(appId, configHash) {
|
|
62
|
+
ensureSetupDir();
|
|
63
|
+
return path.join(SETUP_DIR, `${appId}-hash-${configHash}.json`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get setup progress file path for an app (legacy, by version)
|
|
68
|
+
*/
|
|
69
|
+
function getSetupFilePath(appId, version) {
|
|
70
|
+
ensureSetupDir();
|
|
71
|
+
const sanitizedVersion = version ? version.replace(/[^a-z0-9.-]/gi, "-") : "default";
|
|
72
|
+
return path.join(SETUP_DIR, `${appId}-${sanitizedVersion}.json`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Load setup progress for an app
|
|
77
|
+
* Tries to find by configHash first, then falls back to version-based lookup
|
|
78
|
+
*/
|
|
79
|
+
function loadSetupProgress(appId, version, configHash = null) {
|
|
80
|
+
// First, try to load by config hash if provided
|
|
81
|
+
if (configHash) {
|
|
82
|
+
const hashFilePath = getSetupFilePathByHash(appId, configHash);
|
|
83
|
+
if (fs.existsSync(hashFilePath)) {
|
|
84
|
+
try {
|
|
85
|
+
const data = fs.readFileSync(hashFilePath, "utf8");
|
|
86
|
+
const progress = JSON.parse(data);
|
|
87
|
+
console.log(`✅ Found setup progress by config hash: ${configHash}`);
|
|
88
|
+
return progress;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.warn(`⚠️ Failed to load setup progress by hash: ${error.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Fall back to version-based lookup (for backwards compatibility)
|
|
96
|
+
const filePath = getSetupFilePath(appId, version);
|
|
97
|
+
if (fs.existsSync(filePath)) {
|
|
98
|
+
try {
|
|
99
|
+
const data = fs.readFileSync(filePath, "utf8");
|
|
100
|
+
const progress = JSON.parse(data);
|
|
101
|
+
console.log(`✅ Found setup progress by version: ${version}`);
|
|
102
|
+
return progress;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.warn(`⚠️ Failed to load setup progress: ${error.message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Also scan for any matching hash-based progress for this app
|
|
109
|
+
if (!configHash) {
|
|
110
|
+
try {
|
|
111
|
+
const files = fs.readdirSync(SETUP_DIR);
|
|
112
|
+
const appHashFiles = files.filter(f => f.startsWith(`${appId}-hash-`) && f.endsWith('.json'));
|
|
113
|
+
if (appHashFiles.length > 0) {
|
|
114
|
+
// Return the most recently modified one
|
|
115
|
+
const sortedFiles = appHashFiles
|
|
116
|
+
.map(f => ({ name: f, mtime: fs.statSync(path.join(SETUP_DIR, f)).mtime }))
|
|
117
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
118
|
+
|
|
119
|
+
const latestFile = path.join(SETUP_DIR, sortedFiles[0].name);
|
|
120
|
+
const data = fs.readFileSync(latestFile, "utf8");
|
|
121
|
+
const progress = JSON.parse(data);
|
|
122
|
+
console.log(`✅ Found latest setup progress for app: ${sortedFiles[0].name}`);
|
|
123
|
+
return progress;
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// Ignore scan errors
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Save setup progress for an app
|
|
135
|
+
* Saves by both configHash (primary) and version (for backwards compatibility)
|
|
136
|
+
*/
|
|
137
|
+
function saveSetupProgress(progress) {
|
|
138
|
+
ensureSetupDir();
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// Save by config hash if provided
|
|
142
|
+
if (progress.configHash) {
|
|
143
|
+
const hashFilePath = getSetupFilePathByHash(progress.appId, progress.configHash);
|
|
144
|
+
fs.writeFileSync(hashFilePath, JSON.stringify(progress, null, 2), "utf8");
|
|
145
|
+
console.log(`📝 Saved setup progress by hash: ${progress.configHash}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Also save by version for backwards compatibility
|
|
149
|
+
const filePath = getSetupFilePath(progress.appId, progress.version);
|
|
150
|
+
fs.writeFileSync(filePath, JSON.stringify(progress, null, 2), "utf8");
|
|
151
|
+
console.log(`📝 Saved setup progress by version: ${progress.version}`);
|
|
152
|
+
|
|
153
|
+
return true;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error(`❌ Failed to save setup progress: ${error.message}`);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Clear setup progress for an app
|
|
162
|
+
*/
|
|
163
|
+
function clearSetupProgress(appId, version, configHash = null) {
|
|
164
|
+
let cleared = false;
|
|
165
|
+
|
|
166
|
+
// Clear by hash if provided
|
|
167
|
+
if (configHash) {
|
|
168
|
+
const hashFilePath = getSetupFilePathByHash(appId, configHash);
|
|
169
|
+
if (fs.existsSync(hashFilePath)) {
|
|
170
|
+
try {
|
|
171
|
+
fs.unlinkSync(hashFilePath);
|
|
172
|
+
cleared = true;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.warn(`⚠️ Failed to clear setup progress by hash: ${error.message}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Also clear by version
|
|
180
|
+
const filePath = getSetupFilePath(appId, version);
|
|
181
|
+
if (fs.existsSync(filePath)) {
|
|
182
|
+
try {
|
|
183
|
+
fs.unlinkSync(filePath);
|
|
184
|
+
cleared = true;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.warn(`⚠️ Failed to clear setup progress: ${error.message}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return cleared;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// =============================================================================
|
|
194
|
+
// Verification Functions
|
|
195
|
+
// =============================================================================
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Verify a directory exists
|
|
199
|
+
*/
|
|
200
|
+
async function verifyDirExists(dirPath) {
|
|
201
|
+
try {
|
|
202
|
+
const stats = fs.statSync(dirPath);
|
|
203
|
+
return {
|
|
204
|
+
success: stats.isDirectory(),
|
|
205
|
+
message: stats.isDirectory()
|
|
206
|
+
? `Directory exists: ${dirPath}`
|
|
207
|
+
: `Path is not a directory: ${dirPath}`,
|
|
208
|
+
};
|
|
209
|
+
} catch (error) {
|
|
210
|
+
return {
|
|
211
|
+
success: false,
|
|
212
|
+
message: `Directory not found: ${dirPath}`,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Verify a file exists
|
|
219
|
+
*/
|
|
220
|
+
async function verifyFileExists(filePath) {
|
|
221
|
+
try {
|
|
222
|
+
const stats = fs.statSync(filePath);
|
|
223
|
+
return {
|
|
224
|
+
success: stats.isFile(),
|
|
225
|
+
message: stats.isFile()
|
|
226
|
+
? `File exists: ${filePath}`
|
|
227
|
+
: `Path is not a file: ${filePath}`,
|
|
228
|
+
};
|
|
229
|
+
} catch (error) {
|
|
230
|
+
return {
|
|
231
|
+
success: false,
|
|
232
|
+
message: `File not found: ${filePath}`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Verify a directory contains specific files
|
|
239
|
+
*/
|
|
240
|
+
async function verifyContainsFiles(dirPath, expectedFiles) {
|
|
241
|
+
const dirResult = await verifyDirExists(dirPath);
|
|
242
|
+
if (!dirResult.success) {
|
|
243
|
+
return dirResult;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const missingFiles = [];
|
|
247
|
+
for (const file of expectedFiles) {
|
|
248
|
+
const filePath = path.join(dirPath, file);
|
|
249
|
+
if (!fs.existsSync(filePath)) {
|
|
250
|
+
missingFiles.push(file);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (missingFiles.length > 0) {
|
|
255
|
+
return {
|
|
256
|
+
success: false,
|
|
257
|
+
message: `Missing files: ${missingFiles.join(", ")}`,
|
|
258
|
+
details: `Directory: ${dirPath}`,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
success: true,
|
|
264
|
+
message: `All required files found in ${dirPath}`,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Verify DNS resolution for a hostname
|
|
270
|
+
*/
|
|
271
|
+
async function verifyDnsResolve(hostname) {
|
|
272
|
+
try {
|
|
273
|
+
await dnsResolve(hostname);
|
|
274
|
+
return {
|
|
275
|
+
success: true,
|
|
276
|
+
message: `Hostname resolves: ${hostname}`,
|
|
277
|
+
};
|
|
278
|
+
} catch (error) {
|
|
279
|
+
// Also check /etc/hosts directly
|
|
280
|
+
try {
|
|
281
|
+
const hostsContent = fs.readFileSync("/etc/hosts", "utf8");
|
|
282
|
+
if (hostsContent.includes(hostname)) {
|
|
283
|
+
return {
|
|
284
|
+
success: true,
|
|
285
|
+
message: `Hostname found in /etc/hosts: ${hostname}`,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
} catch (hostsError) {
|
|
289
|
+
// Ignore hosts file read errors
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
success: false,
|
|
294
|
+
message: `Hostname does not resolve: ${hostname}`,
|
|
295
|
+
details: error.message,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Verify by running a command and checking exit code
|
|
302
|
+
*/
|
|
303
|
+
async function verifyCommand(command, workingDir) {
|
|
304
|
+
try {
|
|
305
|
+
const options = workingDir ? { cwd: workingDir } : {};
|
|
306
|
+
await execAsync(command, options);
|
|
307
|
+
return {
|
|
308
|
+
success: true,
|
|
309
|
+
message: "Command executed successfully",
|
|
310
|
+
};
|
|
311
|
+
} catch (error) {
|
|
312
|
+
return {
|
|
313
|
+
success: false,
|
|
314
|
+
message: `Command failed: ${error.message}`,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Verify mkcert is installed
|
|
321
|
+
*/
|
|
322
|
+
async function verifyMkcertInstalled(verification) {
|
|
323
|
+
const result = await sslCertificates.checkMkcertInstalled();
|
|
324
|
+
|
|
325
|
+
if (result.installed) {
|
|
326
|
+
return {
|
|
327
|
+
success: true,
|
|
328
|
+
message: verification.successMessage || `mkcert is installed (${result.version})`,
|
|
329
|
+
version: result.version,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
success: false,
|
|
335
|
+
message: verification.failureMessage || result.error,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Verify a setup task based on its verification config
|
|
341
|
+
*/
|
|
342
|
+
async function verifyTask(task, value) {
|
|
343
|
+
const { verification, config: taskConfig } = task;
|
|
344
|
+
|
|
345
|
+
if (!verification) {
|
|
346
|
+
// No verification required, auto-pass
|
|
347
|
+
return { success: true, message: "No verification required" };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
switch (verification.type) {
|
|
351
|
+
case "dir_exists":
|
|
352
|
+
return await verifyDirExists(value || verification.target);
|
|
353
|
+
|
|
354
|
+
case "file_exists":
|
|
355
|
+
return await verifyFileExists(value || verification.target);
|
|
356
|
+
|
|
357
|
+
case "contains_file":
|
|
358
|
+
return await verifyContainsFiles(
|
|
359
|
+
value || verification.target,
|
|
360
|
+
verification.expectedFiles || []
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
case "dns_resolve":
|
|
364
|
+
return await verifyDnsResolve(taskConfig.hostname || verification.target);
|
|
365
|
+
|
|
366
|
+
case "command":
|
|
367
|
+
return await verifyCommand(
|
|
368
|
+
verification.command,
|
|
369
|
+
value || taskConfig.workingDir
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
case "mkcert_installed":
|
|
373
|
+
return await verifyMkcertInstalled(verification);
|
|
374
|
+
|
|
375
|
+
default:
|
|
376
|
+
return {
|
|
377
|
+
success: false,
|
|
378
|
+
message: `Unknown verification type: ${verification.type}`,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// =============================================================================
|
|
384
|
+
// Task Execution Functions
|
|
385
|
+
// =============================================================================
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Execute a file copy task
|
|
389
|
+
*/
|
|
390
|
+
async function executeFileCopy(task, workingDir) {
|
|
391
|
+
const { config: taskConfig } = task;
|
|
392
|
+
const destPath = taskConfig.destinationPath.startsWith("/")
|
|
393
|
+
? taskConfig.destinationPath
|
|
394
|
+
: path.join(workingDir || os.homedir(), taskConfig.destinationPath);
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
// Ensure destination directory exists
|
|
398
|
+
const destDir = path.dirname(destPath);
|
|
399
|
+
if (!fs.existsSync(destDir)) {
|
|
400
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (taskConfig.template) {
|
|
404
|
+
// Write template content
|
|
405
|
+
fs.writeFileSync(destPath, taskConfig.template, "utf8");
|
|
406
|
+
} else if (taskConfig.sourcePath) {
|
|
407
|
+
// Copy from source
|
|
408
|
+
const sourcePath = taskConfig.sourcePath.startsWith("/")
|
|
409
|
+
? taskConfig.sourcePath
|
|
410
|
+
: path.join(workingDir || os.homedir(), taskConfig.sourcePath);
|
|
411
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
success: true,
|
|
416
|
+
message: `File created: ${destPath}`,
|
|
417
|
+
value: destPath,
|
|
418
|
+
};
|
|
419
|
+
} catch (error) {
|
|
420
|
+
return {
|
|
421
|
+
success: false,
|
|
422
|
+
message: `Failed to create file: ${error.message}`,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Execute a command task
|
|
429
|
+
*/
|
|
430
|
+
async function executeCommand(task) {
|
|
431
|
+
const { config: taskConfig } = task;
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
const options = taskConfig.workingDir
|
|
435
|
+
? { cwd: taskConfig.workingDir, timeout: 60000 }
|
|
436
|
+
: { timeout: 60000 };
|
|
437
|
+
|
|
438
|
+
const { stdout, stderr } = await execAsync(taskConfig.command, options);
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
success: true,
|
|
442
|
+
message: stdout || stderr || "Command executed successfully",
|
|
443
|
+
};
|
|
444
|
+
} catch (error) {
|
|
445
|
+
return {
|
|
446
|
+
success: false,
|
|
447
|
+
message: error.stderr || error.message || "Command failed",
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Execute an SSL certificate generation task
|
|
454
|
+
*/
|
|
455
|
+
async function executeSSLCertificate(task) {
|
|
456
|
+
const { config: taskConfig, outputVariable } = task;
|
|
457
|
+
const { domains, certName, installCA } = taskConfig;
|
|
458
|
+
|
|
459
|
+
// Check mkcert is installed
|
|
460
|
+
const mkcertCheck = await sslCertificates.checkMkcertInstalled();
|
|
461
|
+
if (!mkcertCheck.installed) {
|
|
462
|
+
return {
|
|
463
|
+
success: false,
|
|
464
|
+
message: mkcertCheck.error,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Install CA if requested
|
|
469
|
+
if (installCA !== false) {
|
|
470
|
+
const caResult = await sslCertificates.installMkcertCA();
|
|
471
|
+
if (!caResult.success) {
|
|
472
|
+
console.warn(`⚠️ CA installation warning: ${caResult.message}`);
|
|
473
|
+
// Continue anyway, user might have already installed CA
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Generate certificates
|
|
478
|
+
const result = await sslCertificates.generateCertificates(domains, certName);
|
|
479
|
+
|
|
480
|
+
if (!result.success) {
|
|
481
|
+
return {
|
|
482
|
+
success: false,
|
|
483
|
+
message: result.message,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Get CA path for the output variables
|
|
488
|
+
const caResult = await sslCertificates.getMkcertCAPath();
|
|
489
|
+
|
|
490
|
+
// Build output variables using the task's outputVariable as prefix
|
|
491
|
+
const prefix = outputVariable || "SSL";
|
|
492
|
+
const outputVariables = {
|
|
493
|
+
[`${prefix}_CERT_PATH`]: result.certPath,
|
|
494
|
+
[`${prefix}_KEY_PATH`]: result.keyPath,
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// Only include CA path if available
|
|
498
|
+
if (caResult.success && caResult.caPath) {
|
|
499
|
+
outputVariables[`${prefix}_CA_PATH`] = caResult.caPath;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
success: true,
|
|
504
|
+
message: result.message,
|
|
505
|
+
value: result.certPath, // Primary value for backward compatibility
|
|
506
|
+
outputVariables, // Multiple variables for SSL task
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Execute a setup task
|
|
512
|
+
*/
|
|
513
|
+
async function executeTask(task, params) {
|
|
514
|
+
switch (task.type) {
|
|
515
|
+
case "file_copy":
|
|
516
|
+
case "file_edit":
|
|
517
|
+
return await executeFileCopy(task, params?.workingDir);
|
|
518
|
+
|
|
519
|
+
case "command":
|
|
520
|
+
return await executeCommand(task);
|
|
521
|
+
|
|
522
|
+
case "ssl_certificate":
|
|
523
|
+
return await executeSSLCertificate(task);
|
|
524
|
+
|
|
525
|
+
default:
|
|
526
|
+
return {
|
|
527
|
+
success: false,
|
|
528
|
+
message: `Task type "${task.type}" does not support execution`,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// =============================================================================
|
|
534
|
+
// WebSocket Handlers
|
|
535
|
+
// =============================================================================
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Handle setup task actions
|
|
539
|
+
*/
|
|
540
|
+
async function handleSetupAction(ws, action, payload) {
|
|
541
|
+
switch (action) {
|
|
542
|
+
case "getSetupTasks":
|
|
543
|
+
return await handleGetSetupTasks(ws, payload);
|
|
544
|
+
case "getSetupStatus":
|
|
545
|
+
return await handleGetSetupStatus(ws, payload);
|
|
546
|
+
case "verifySetupTask":
|
|
547
|
+
return await handleVerifySetupTask(ws, payload);
|
|
548
|
+
case "executeSetupTask":
|
|
549
|
+
return await handleExecuteSetupTask(ws, payload);
|
|
550
|
+
case "saveSetupProgress":
|
|
551
|
+
return await handleSaveSetupProgress(ws, payload);
|
|
552
|
+
case "clearSetupProgress":
|
|
553
|
+
return await handleClearSetupProgress(ws, payload);
|
|
554
|
+
case "browseDirectory":
|
|
555
|
+
return await handleBrowseDirectory(ws, payload);
|
|
556
|
+
default:
|
|
557
|
+
ws.send(
|
|
558
|
+
JSON.stringify({
|
|
559
|
+
type: "error",
|
|
560
|
+
error: `Unknown setup action: ${action}`,
|
|
561
|
+
requestId: payload.requestId,
|
|
562
|
+
})
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Get setup tasks from app's deployment config
|
|
569
|
+
* Note: This requires fetching the app config from Fenwave
|
|
570
|
+
*/
|
|
571
|
+
async function handleGetSetupTasks(ws, payload) {
|
|
572
|
+
const { appId, version, requestId } = payload;
|
|
573
|
+
|
|
574
|
+
try {
|
|
575
|
+
// For now, return a mock response
|
|
576
|
+
// In production, this would fetch from the app's deploymentConfig.localSetup
|
|
577
|
+
// This will be populated by the app-list.tsx which already has the app data
|
|
578
|
+
|
|
579
|
+
ws.send(
|
|
580
|
+
JSON.stringify({
|
|
581
|
+
type: "setupTasks",
|
|
582
|
+
localSetup: null, // Will be populated from app data on client side
|
|
583
|
+
requestId,
|
|
584
|
+
})
|
|
585
|
+
);
|
|
586
|
+
} catch (error) {
|
|
587
|
+
ws.send(
|
|
588
|
+
JSON.stringify({
|
|
589
|
+
type: "error",
|
|
590
|
+
error: `Failed to get setup tasks: ${error.message}`,
|
|
591
|
+
requestId,
|
|
592
|
+
})
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Get setup status/progress for an app
|
|
599
|
+
* If tasks are provided, generates a config hash for precise matching
|
|
600
|
+
*/
|
|
601
|
+
async function handleGetSetupStatus(ws, payload) {
|
|
602
|
+
const { appId, version, tasks, requestId } = payload;
|
|
603
|
+
|
|
604
|
+
try {
|
|
605
|
+
// Generate config hash if tasks are provided
|
|
606
|
+
const configHash = tasks ? generateSetupConfigHash(tasks) : null;
|
|
607
|
+
|
|
608
|
+
console.log(`🔍 Looking for setup progress: appId=${appId}, version=${version}, configHash=${configHash}`);
|
|
609
|
+
|
|
610
|
+
const progress = loadSetupProgress(appId, version, configHash);
|
|
611
|
+
|
|
612
|
+
// If we found progress, check if the config hash matches
|
|
613
|
+
let hashMatches = true;
|
|
614
|
+
if (progress && configHash && progress.configHash && progress.configHash !== configHash) {
|
|
615
|
+
console.log(`⚠️ Config hash mismatch: stored=${progress.configHash}, current=${configHash}`);
|
|
616
|
+
hashMatches = false;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
ws.send(
|
|
620
|
+
JSON.stringify({
|
|
621
|
+
type: "setupStatus",
|
|
622
|
+
progress: hashMatches ? progress : null, // Return null if hash doesn't match
|
|
623
|
+
configHash, // Send back the current config hash
|
|
624
|
+
hashMatches,
|
|
625
|
+
requestId,
|
|
626
|
+
})
|
|
627
|
+
);
|
|
628
|
+
} catch (error) {
|
|
629
|
+
ws.send(
|
|
630
|
+
JSON.stringify({
|
|
631
|
+
type: "error",
|
|
632
|
+
error: `Failed to get setup status: ${error.message}`,
|
|
633
|
+
requestId,
|
|
634
|
+
})
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Verify a setup task
|
|
641
|
+
*/
|
|
642
|
+
async function handleVerifySetupTask(ws, payload) {
|
|
643
|
+
const { appId, taskId, value, task, requestId } = payload;
|
|
644
|
+
|
|
645
|
+
try {
|
|
646
|
+
// Task should be passed from client since we don't store tasks on agent
|
|
647
|
+
if (!task) {
|
|
648
|
+
ws.send(
|
|
649
|
+
JSON.stringify({
|
|
650
|
+
type: "verifyResult",
|
|
651
|
+
success: false,
|
|
652
|
+
message: "Task definition required for verification",
|
|
653
|
+
requestId,
|
|
654
|
+
})
|
|
655
|
+
);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const result = await verifyTask(task, value);
|
|
660
|
+
|
|
661
|
+
ws.send(
|
|
662
|
+
JSON.stringify({
|
|
663
|
+
type: "verifyResult",
|
|
664
|
+
...result,
|
|
665
|
+
requestId,
|
|
666
|
+
})
|
|
667
|
+
);
|
|
668
|
+
} catch (error) {
|
|
669
|
+
ws.send(
|
|
670
|
+
JSON.stringify({
|
|
671
|
+
type: "verifyResult",
|
|
672
|
+
success: false,
|
|
673
|
+
message: `Verification error: ${error.message}`,
|
|
674
|
+
requestId,
|
|
675
|
+
})
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Execute a setup task
|
|
682
|
+
*/
|
|
683
|
+
async function handleExecuteSetupTask(ws, payload) {
|
|
684
|
+
const { appId, taskId, task, params, requestId } = payload;
|
|
685
|
+
|
|
686
|
+
try {
|
|
687
|
+
// Task should be passed from client
|
|
688
|
+
if (!task) {
|
|
689
|
+
ws.send(
|
|
690
|
+
JSON.stringify({
|
|
691
|
+
type: "executeResult",
|
|
692
|
+
success: false,
|
|
693
|
+
message: "Task definition required for execution",
|
|
694
|
+
requestId,
|
|
695
|
+
})
|
|
696
|
+
);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const result = await executeTask(task, params);
|
|
701
|
+
|
|
702
|
+
ws.send(
|
|
703
|
+
JSON.stringify({
|
|
704
|
+
type: "executeResult",
|
|
705
|
+
...result,
|
|
706
|
+
requestId,
|
|
707
|
+
})
|
|
708
|
+
);
|
|
709
|
+
} catch (error) {
|
|
710
|
+
ws.send(
|
|
711
|
+
JSON.stringify({
|
|
712
|
+
type: "executeResult",
|
|
713
|
+
success: false,
|
|
714
|
+
message: `Execution error: ${error.message}`,
|
|
715
|
+
requestId,
|
|
716
|
+
})
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Save setup progress
|
|
723
|
+
*/
|
|
724
|
+
async function handleSaveSetupProgress(ws, payload) {
|
|
725
|
+
const { requestId, tasks, ...progress } = payload;
|
|
726
|
+
|
|
727
|
+
try {
|
|
728
|
+
// Generate config hash from tasks if provided
|
|
729
|
+
const configHash = tasks ? generateSetupConfigHash(tasks) : progress.configHash;
|
|
730
|
+
|
|
731
|
+
const progressWithHash = {
|
|
732
|
+
...progress,
|
|
733
|
+
configHash,
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
console.log(`📝 Saving setup progress for appId=${progress.appId}, version=${progress.version}, configHash=${configHash}`);
|
|
737
|
+
console.log(` Variables: ${JSON.stringify(progress.variables || {})}`);
|
|
738
|
+
console.log(` Tasks: ${progress.tasks?.length || 0} task statuses`);
|
|
739
|
+
|
|
740
|
+
const success = saveSetupProgress(progressWithHash);
|
|
741
|
+
|
|
742
|
+
console.log(` Save result: ${success ? 'SUCCESS' : 'FAILED'}`);
|
|
743
|
+
|
|
744
|
+
ws.send(
|
|
745
|
+
JSON.stringify({
|
|
746
|
+
type: "saveProgressResult",
|
|
747
|
+
success,
|
|
748
|
+
configHash,
|
|
749
|
+
requestId,
|
|
750
|
+
})
|
|
751
|
+
);
|
|
752
|
+
} catch (error) {
|
|
753
|
+
console.error(`❌ Failed to save setup progress: ${error.message}`);
|
|
754
|
+
ws.send(
|
|
755
|
+
JSON.stringify({
|
|
756
|
+
type: "error",
|
|
757
|
+
error: `Failed to save progress: ${error.message}`,
|
|
758
|
+
requestId,
|
|
759
|
+
})
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Clear setup progress
|
|
766
|
+
*/
|
|
767
|
+
async function handleClearSetupProgress(ws, payload) {
|
|
768
|
+
const { appId, version, requestId } = payload;
|
|
769
|
+
|
|
770
|
+
try {
|
|
771
|
+
const success = clearSetupProgress(appId, version);
|
|
772
|
+
|
|
773
|
+
ws.send(
|
|
774
|
+
JSON.stringify({
|
|
775
|
+
type: "clearProgressResult",
|
|
776
|
+
success,
|
|
777
|
+
requestId,
|
|
778
|
+
})
|
|
779
|
+
);
|
|
780
|
+
} catch (error) {
|
|
781
|
+
ws.send(
|
|
782
|
+
JSON.stringify({
|
|
783
|
+
type: "error",
|
|
784
|
+
error: `Failed to clear progress: ${error.message}`,
|
|
785
|
+
requestId,
|
|
786
|
+
})
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Browse for a directory (opens native dialog via AppleScript on macOS)
|
|
793
|
+
*/
|
|
794
|
+
async function handleBrowseDirectory(ws, payload) {
|
|
795
|
+
const { startPath, requestId } = payload;
|
|
796
|
+
|
|
797
|
+
try {
|
|
798
|
+
let selectedPath = null;
|
|
799
|
+
|
|
800
|
+
if (process.platform === "darwin") {
|
|
801
|
+
// macOS: Use AppleScript to open native folder picker
|
|
802
|
+
// choose folder is a Standard Additions command, returns POSIX path directly
|
|
803
|
+
const script = startPath
|
|
804
|
+
? `osascript -e 'POSIX path of (choose folder with prompt "Select Directory" default location POSIX file "${startPath}")'`
|
|
805
|
+
: `osascript -e 'POSIX path of (choose folder with prompt "Select Directory")'`;
|
|
806
|
+
|
|
807
|
+
try {
|
|
808
|
+
const { stdout } = await execAsync(script);
|
|
809
|
+
selectedPath = stdout.trim();
|
|
810
|
+
} catch (error) {
|
|
811
|
+
// User cancelled or error
|
|
812
|
+
console.log("Directory browse cancelled or failed:", error.message);
|
|
813
|
+
}
|
|
814
|
+
} else if (process.platform === "linux") {
|
|
815
|
+
// Linux: Try zenity or kdialog
|
|
816
|
+
try {
|
|
817
|
+
const { stdout } = await execAsync(
|
|
818
|
+
`zenity --file-selection --directory --title="Select Directory"${startPath ? ` --filename="${startPath}"` : ""}`
|
|
819
|
+
);
|
|
820
|
+
selectedPath = stdout.trim();
|
|
821
|
+
} catch (error) {
|
|
822
|
+
// Zenity not available or cancelled
|
|
823
|
+
try {
|
|
824
|
+
const { stdout } = await execAsync(
|
|
825
|
+
`kdialog --getexistingdirectory ${startPath || "~"}`
|
|
826
|
+
);
|
|
827
|
+
selectedPath = stdout.trim();
|
|
828
|
+
} catch (kdialogError) {
|
|
829
|
+
console.log("Directory browse not available on this system");
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
ws.send(
|
|
835
|
+
JSON.stringify({
|
|
836
|
+
type: "browseResult",
|
|
837
|
+
path: selectedPath,
|
|
838
|
+
requestId,
|
|
839
|
+
})
|
|
840
|
+
);
|
|
841
|
+
} catch (error) {
|
|
842
|
+
ws.send(
|
|
843
|
+
JSON.stringify({
|
|
844
|
+
type: "browseResult",
|
|
845
|
+
path: null,
|
|
846
|
+
requestId,
|
|
847
|
+
})
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export default {
|
|
853
|
+
handleSetupAction,
|
|
854
|
+
loadSetupProgress,
|
|
855
|
+
saveSetupProgress,
|
|
856
|
+
clearSetupProgress,
|
|
857
|
+
verifyTask,
|
|
858
|
+
executeTask,
|
|
859
|
+
};
|