@oussema_mili/test-pkg-123 1.1.22

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.

Potentially problematic release.


This version of @oussema_mili/test-pkg-123 might be problematic. Click here for more details.

Files changed (49) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +220 -0
  3. package/auth-callback.html +97 -0
  4. package/auth.js +276 -0
  5. package/cli-commands.js +1923 -0
  6. package/containerManager.js +304 -0
  7. package/daemon/agentRunner.js +429 -0
  8. package/daemon/daemonEntry.js +64 -0
  9. package/daemon/daemonManager.js +271 -0
  10. package/daemon/logManager.js +227 -0
  11. package/dist/styles.css +504 -0
  12. package/docker-actions/apps.js +3938 -0
  13. package/docker-actions/config-transformer.js +380 -0
  14. package/docker-actions/containers.js +355 -0
  15. package/docker-actions/general.js +171 -0
  16. package/docker-actions/images.js +1128 -0
  17. package/docker-actions/logs.js +224 -0
  18. package/docker-actions/metrics.js +270 -0
  19. package/docker-actions/registry.js +1100 -0
  20. package/docker-actions/setup-tasks.js +859 -0
  21. package/docker-actions/terminal.js +247 -0
  22. package/docker-actions/volumes.js +696 -0
  23. package/helper-functions.js +193 -0
  24. package/index.html +83 -0
  25. package/index.js +341 -0
  26. package/package.json +82 -0
  27. package/postcss.config.mjs +5 -0
  28. package/scripts/release.sh +212 -0
  29. package/setup/setupWizard.js +403 -0
  30. package/store/agentSessionStore.js +51 -0
  31. package/store/agentStore.js +113 -0
  32. package/store/configStore.js +171 -0
  33. package/store/daemonStore.js +217 -0
  34. package/store/deviceCredentialStore.js +107 -0
  35. package/store/npmTokenStore.js +65 -0
  36. package/store/registryStore.js +329 -0
  37. package/store/setupState.js +147 -0
  38. package/styles.css +1 -0
  39. package/utils/appLogger.js +223 -0
  40. package/utils/deviceInfo.js +98 -0
  41. package/utils/ecrAuth.js +225 -0
  42. package/utils/encryption.js +112 -0
  43. package/utils/envSetup.js +44 -0
  44. package/utils/errorHandler.js +327 -0
  45. package/utils/portUtils.js +59 -0
  46. package/utils/prerequisites.js +323 -0
  47. package/utils/prompts.js +318 -0
  48. package/utils/ssl-certificates.js +256 -0
  49. 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
+ };