easy-devops 0.2.0 → 0.2.2

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/README.md CHANGED
@@ -21,6 +21,7 @@ A unified DevOps management tool with interactive CLI and web dashboard for mana
21
21
 
22
22
  - **Node.js 18+** (with npm)
23
23
  - **Linux** (Debian/Ubuntu) or **Windows**
24
+ - ⚠️ **Windows users: PowerShell must be run as Administrator** (required for installing packages via winget, managing services, and SSL certificates)
24
25
  - Optional: Nginx, Certbot, nvm (installed separately or via the tool)
25
26
 
26
27
  ## Installation
@@ -49,6 +50,8 @@ wget -qO- https://raw.githubusercontent.com/omar00050/Easy-DevOps/main/install.s
49
50
 
50
51
  #### Windows (PowerShell)
51
52
 
53
+ > ⚠️ **Important:** Run PowerShell **as Administrator**. Right-click PowerShell → "Run as Administrator".
54
+
52
55
  ```powershell
53
56
  Invoke-WebRequest -Uri "https://raw.githubusercontent.com/omar00050/Easy-DevOps/main/install.ps1" -OutFile "install.ps1"; ./install.ps1
54
57
  ```
@@ -175,6 +178,20 @@ Manage Let's Encrypt SSL certificates using Certbot.
175
178
 
176
179
  > **Note:** Renewing a certificate temporarily stops Nginx to free port 80, then restarts it automatically.
177
180
 
181
+ #### Windows Package Manager (winget)
182
+
183
+ Easy DevOps uses **winget** (Windows Package Manager) to install certbot and other packages on Windows. If winget is not installed, Easy DevOps will automatically prompt to install it using the [asheroto/winget-install](https://github.com/asheroto/winget-install) script.
184
+
185
+ **Installing winget manually:**
186
+
187
+ If you prefer to install winget separately, you can:
188
+
189
+ 1. **Install from Microsoft Store:** Search for "App Installer" in the Microsoft Store
190
+ 2. **Use the winget-install script:** [https://github.com/asheroto/winget-install](https://github.com/asheroto/winget-install)
191
+ 3. **Official Microsoft documentation:** [https://learn.microsoft.com/en-us/windows/package-manager/winget/](https://learn.microsoft.com/en-us/windows/package-manager/winget/)
192
+
193
+ > **Note:** The embedded `winget-install.ps1` script in this project is sourced from [asheroto/winget-install](https://github.com/asheroto/winget-install) — a community-maintained, reliable installer for winget on Windows Server and systems without the Microsoft Store.
194
+
178
195
  ---
179
196
 
180
197
  ### Web Dashboard
@@ -187,7 +204,15 @@ Start the web dashboard:
187
204
  npm run dashboard
188
205
  ```
189
206
 
190
- Access at `http://localhost:3000` (or configured port).
207
+ Access at `http://localhost:6443` (or configured port).
208
+
209
+ #### First-Time Login
210
+
211
+ Default credentials:
212
+ - **Username:** `admin`
213
+ - **Password:** Set in Settings menu or check your configuration
214
+
215
+ > **Tip:** From the Dashboard menu, select "How to use" for a quick guide on getting started.
191
216
 
192
217
  #### Dashboard Pages
193
218
 
@@ -245,7 +270,8 @@ easy-devops/
245
270
  │ ├── config.js # Configuration loader
246
271
  │ ├── db.js # SQLite database (good.db)
247
272
  │ ├── detector.js # System environment detection
248
- └── shell.js # Cross-platform shell executor
273
+ ├── shell.js # Cross-platform shell executor
274
+ │ └── nginx-conf-generator.js # Nginx config file generator
249
275
  ├── dashboard/
250
276
  │ ├── server.js # Express + Socket.io server
251
277
  │ ├── routes/ # API endpoints
@@ -255,7 +281,10 @@ easy-devops/
255
281
  ├── data/
256
282
  │ └── easy-devops.sqlite
257
283
  └── lib/
258
- └── installer/ # Bootstrap scripts
284
+ └── installer/
285
+ ├── install.ps1 # Windows bootstrap installer
286
+ ├── install.sh # Linux/macOS bootstrap installer
287
+ └── winget-install.ps1 # Windows Package Manager installer
259
288
  ```
260
289
 
261
290
  ---
@@ -15,6 +15,7 @@ import inquirer from 'inquirer';
15
15
  import ora from 'ora';
16
16
  import fs from 'fs/promises';
17
17
  import path from 'path';
18
+ import { fileURLToPath } from 'url';
18
19
  import { run, runLive } from '../../core/shell.js';
19
20
  import { loadConfig } from '../../core/config.js';
20
21
 
@@ -340,23 +341,7 @@ async function installCertbot() {
340
341
  if (r.success) return true;
341
342
  if (showErrors && r.stderr) lastDownloadError = `Invoke-WebRequest: ${r.stderr}`;
342
343
 
343
- // Method 2: WebClient with TLS
344
- r = await safeRun(
345
- `${tlsSetup}; (New-Object System.Net.WebClient).DownloadFile('${safeUrl}','${safeDest}')`,
346
- { timeout: 130000 },
347
- );
348
- if (r.success) return true;
349
- if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `WebClient: ${r.stderr}`;
350
-
351
- // Method 3: BITS Transfer
352
- r = await safeRun(
353
- `Import-Module BitsTransfer -ErrorAction SilentlyContinue; Start-BitsTransfer -Source '${safeUrl}' -Destination '${safeDest}' -ErrorAction Stop`,
354
- { timeout: 130000 },
355
- );
356
- if (r.success) return true;
357
- if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `BITS: ${r.stderr}`;
358
-
359
- // Method 4: curl.exe (works on Windows 10+)
344
+ // Method 2: curl.exe (works on Windows 10+, often bypasses AV detection)
360
345
  if (hasCurl) {
361
346
  r = await safeRun(
362
347
  `curl.exe -L --ssl-no-revoke --max-time 120 -o '${safeDest}' '${safeUrl}'`,
@@ -366,61 +351,33 @@ async function installCertbot() {
366
351
  if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `curl: ${r.stderr}`;
367
352
  }
368
353
 
369
- // Method 5: HttpClient with custom handler
354
+ // Method 3: WebClient with TLS
370
355
  r = await safeRun(
371
- `${tlsSetup}; $handler=[System.Net.Http.HttpClientHandler]::new(); $handler.ServerCertificateCustomValidationCallback={$true}; $handler.AllowAutoRedirect=$true; $hc=[System.Net.Http.HttpClient]::new($handler); $hc.DefaultRequestHeaders.Add('User-Agent','Mozilla/5.0 (Windows NT 10.0; Win64; x64)'); $hc.Timeout=[TimeSpan]::FromSeconds(120); $bytes=$hc.GetByteArrayAsync('${safeUrl}').GetAwaiter().GetResult(); [System.IO.File]::WriteAllBytes('${safeDest}',$bytes)`,
356
+ `${tlsSetup}; (New-Object System.Net.WebClient).DownloadFile('${safeUrl}','${safeDest}')`,
372
357
  { timeout: 130000 },
373
358
  );
374
359
  if (r.success) return true;
375
- if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `HttpClient: ${r.stderr}`;
360
+ if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `WebClient: ${r.stderr}`;
376
361
 
377
- // Method 6: Certutil (built-in Windows tool)
362
+ // Method 4: HttpClient with custom handler
378
363
  r = await safeRun(
379
- `certutil.exe -urlcache -split -f "${safeUrl}" "${safeDest}"`,
364
+ `${tlsSetup}; $handler=[System.Net.Http.HttpClientHandler]::new(); $handler.ServerCertificateCustomValidationCallback={$true}; $handler.AllowAutoRedirect=$true; $hc=[System.Net.Http.HttpClient]::new($handler); $hc.DefaultRequestHeaders.Add('User-Agent','Mozilla/5.0 (Windows NT 10.0; Win64; x64)'); $hc.Timeout=[TimeSpan]::FromSeconds(120); $bytes=$hc.GetByteArrayAsync('${safeUrl}').GetAwaiter().GetResult(); [System.IO.File]::WriteAllBytes('${safeDest}',$bytes)`,
380
365
  { timeout: 130000 },
381
366
  );
382
367
  if (r.success) return true;
383
- if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `certutil: ${r.stderr}`;
368
+ if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `HttpClient: ${r.stderr}`;
384
369
 
385
- // Method 7: PowerShell with DisableKeepAlive
370
+ // Method 5: BITS Transfer (background transfers)
386
371
  r = await safeRun(
387
- `${tlsSetup}; $req=[System.Net.WebRequest]::Create('${safeUrl}'); $req.Method='GET'; $req.KeepAlive=$false; $req.UserAgent='Mozilla/5.0'; $resp=$req.GetResponse(); $stream=$resp.GetResponseStream(); $reader=[System.IO.BinaryReader]::new($stream); $bytes=$reader.ReadBytes($resp.ContentLength); $reader.Close(); [System.IO.File]::WriteAllBytes('${safeDest}',$bytes)`,
372
+ `Import-Module BitsTransfer -ErrorAction SilentlyContinue; Start-BitsTransfer -Source '${safeUrl}' -Destination '${safeDest}' -ErrorAction Stop`,
388
373
  { timeout: 130000 },
389
374
  );
390
375
  if (r.success) return true;
391
- if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `WebRequest: ${r.stderr}`;
392
-
393
- return false;
394
- }
395
-
396
- // Test network connectivity to common endpoints
397
- async function testNetworkConnectivity() {
398
- console.log(chalk.cyan('\n Testing network connectivity...'));
399
- const tests = [
400
- { name: 'GitHub', url: 'https://github.com' },
401
- { name: 'Microsoft', url: 'https://microsoft.com' },
402
- { name: 'EFF', url: 'https://eff.org' },
403
- ];
404
- const results = [];
376
+ if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `BITS: ${r.stderr}`;
405
377
 
406
- for (const test of tests) {
407
- const r = await run(`curl.exe -I --ssl-no-revoke --max-time 10 "${test.url}"`, { timeout: 15000 });
408
- const success = r.success || r.stdout.includes('HTTP') || r.stdout.includes('200');
409
- results.push({ name: test.name, success });
410
- console.log(chalk.gray(` ${test.name}: ${success ? chalk.green('✓ Connected') : chalk.red('✗ Failed')}`));
411
- }
378
+ // Note: certutil.exe removed - it triggers Trojan:Win32/Ceprolad.A detection
412
379
 
413
- const allFailed = results.every(r => !r.success);
414
- if (allFailed) {
415
- console.log(chalk.yellow('\n ⚠ All external connections failed.'));
416
- console.log(chalk.gray(' This could indicate:'));
417
- console.log(chalk.gray(' • Firewall blocking outbound connections'));
418
- console.log(chalk.gray(' • Proxy server required'));
419
- console.log(chalk.gray(' • DNS resolution issues'));
420
- console.log(chalk.gray(' • Antivirus blocking downloads'));
421
- }
422
- console.log();
423
- return results;
380
+ return false;
424
381
  }
425
382
 
426
383
  async function runNsisInstaller(exePath) {
@@ -445,70 +402,62 @@ async function installCertbot() {
445
402
 
446
403
  if (!wingetAvailable) {
447
404
  console.log(chalk.yellow('\n ⚠ winget is not installed on this system.'));
448
- console.log(chalk.gray(' winget (Windows Package Manager) provides the easiest installation method.'));
405
+ console.log(chalk.gray(' winget (Windows Package Manager) provides the easiest installation method.'));
449
406
 
450
407
  let installWinget;
451
408
  try {
452
409
  ({ installWinget } = await inquirer.prompt([{
453
410
  type: 'confirm',
454
411
  name: 'installWinget',
455
- message: 'Would you like to install winget (App Installer) from Microsoft Store?',
412
+ message: 'Would you like to install winget automatically?',
456
413
  default: true,
457
414
  }]));
458
415
  } catch { /* user cancelled */ }
459
416
 
460
417
  if (installWinget) {
461
- console.log(chalk.cyan('\n Opening Microsoft Store to install App Installer...'));
462
- console.log(chalk.gray(' Please complete the installation, then run this command again.\n'));
463
-
464
- // Try to open Microsoft Store
465
- const storeOpened = await run(
466
- 'Start-Process "ms-windows-store://pdp/?ProductId=9NBLGGH4NNS1"',
467
- { timeout: 10000 }
468
- ).catch(() => ({ success: false }));
469
-
470
- if (storeOpened.success) {
471
- console.log(chalk.green(' Microsoft Store opened successfully.'));
472
- console.log(chalk.gray(' After installing App Installer, winget will be available.\n'));
418
+ console.log(chalk.cyan('\n Opening winget installer in a new window...'));
419
+ console.log(chalk.gray(' The installer will run in a separate PowerShell window.'));
420
+ console.log(chalk.gray(' Please complete the installation in that window.\n'));
421
+
422
+ // Use the embedded winget-install.ps1 script
423
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
424
+ const wingetScriptPath = path.join(scriptDir, '..', '..', 'lib', 'installer', 'winget-install.ps1');
425
+
426
+ // Check if the embedded script exists
427
+ const scriptExists = await run(`Test-Path "${wingetScriptPath}"`);
428
+ if (scriptExists.stdout.trim().toLowerCase() !== 'true') {
429
+ console.log(chalk.red('\n Embedded winget installer script not found.'));
430
+ console.log(chalk.gray(' Expected location: ' + wingetScriptPath + '\n'));
473
431
  } else {
474
- console.log(chalk.yellow(' Could not open Microsoft Store directly.'));
475
- console.log(chalk.gray(' Please manually install "App Installer" from:'));
476
- console.log(chalk.cyan(' https://apps.microsoft.com/store/detail/app-installer/9NBLGGH4NNS1\n'));
477
- }
432
+ // Run the installer in a new PowerShell window with -NoExit so user can see output
433
+ // The script has a built-in -NoExit parameter we can use
434
+ await run(`Start-Process powershell.exe -ArgumentList '-ExecutionPolicy Bypass -NoProfile -File "${wingetScriptPath}" -NoExit' -Verb RunAs -Wait`);
478
435
 
479
- // Optional: try direct download of App Installer
480
- let tryDownload;
481
- try {
482
- ({ tryDownload } = await inquirer.prompt([{
483
- type: 'confirm',
484
- name: 'tryDownload',
485
- message: 'Would you like to try downloading App Installer directly?',
486
- default: false,
487
- }]));
488
- } catch { tryDownload = false; }
489
-
490
- if (tryDownload) {
491
- const appInstallerUrl = 'https://aka.ms/getwinget';
492
- const appInstallerDest = '$env:TEMP\\AppInstaller.msixbundle';
493
-
494
- console.log(chalk.gray('\n Downloading App Installer...'));
495
- let downloaded = false; try { downloaded = await downloadFile(appInstallerUrl, appInstallerDest); } catch (err) { if (err.code === 'EPERM' || err.message?.includes('EPERM')) { console.log(chalk.red('\n ? Windows Defender blocked the download.')); console.log(chalk.gray(' Add an exclusion in Windows Defender or download manually.')); console.log(chalk.gray(' Download URL: https://aka.ms/getwinget\n')); } else { console.log(chalk.yellow('\n Download failed: ' + err.message + '\n')); } }
496
-
497
- if (downloaded) {
498
- console.log(chalk.gray(' Running App Installer...'));
499
- await run(`Start-Process -FilePath '${appInstallerDest}' -Wait`, { timeout: 300000 });
500
-
501
- // Re-check for winget
502
- const recheck = await run('where.exe winget 2>$null');
503
- if (recheck.success && recheck.stdout.trim()) {
436
+ console.log(chalk.gray('\n Winget installer window has closed.'));
437
+ console.log(chalk.cyan(' Checking if winget is now available...\n'));
438
+
439
+ // Give a moment for PATH to update
440
+ await new Promise(res => setTimeout(res, 2000));
441
+
442
+ // Re-check for winget
443
+ const recheck = await run('where.exe winget 2>$null');
444
+ if (recheck.success && recheck.stdout.trim()) {
445
+ console.log(chalk.green(' winget installed successfully!\n'));
446
+ wingetAvailable = true;
447
+ } else {
448
+ // Try refreshing PATH from registry and check again
449
+ console.log(chalk.yellow(' winget not immediately detected. Refreshing PATH...'));
450
+ await run(`$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('PATH', 'User')`);
451
+ await new Promise(res => setTimeout(res, 1000));
452
+
453
+ const recheck2 = await run('where.exe winget 2>$null');
454
+ if (recheck2.success && recheck2.stdout.trim()) {
504
455
  console.log(chalk.green('\n ✓ winget installed successfully!\n'));
505
456
  wingetAvailable = true;
506
457
  } else {
507
- console.log(chalk.yellow('\n App Installer ran but winget is not yet available.'));
508
- console.log(chalk.gray(' You may need to restart your terminal or sign out/in.\n'));
458
+ console.log(chalk.yellow('\n winget may have been installed but is not in PATH yet.'));
459
+ console.log(chalk.gray(' You may need to restart your terminal or computer.\n'));
509
460
  }
510
- } else {
511
- console.log(chalk.yellow('\n Could not download App Installer automatically.\n'));
512
461
  }
513
462
  }
514
463
 
@@ -521,28 +470,28 @@ async function installCertbot() {
521
470
  }
522
471
  }
523
472
 
524
- // ── Method 1: winget (win-acme - has better success rate) ─────────────────────
473
+ // ── Method 1: winget (certbot EFF - most reliable) ────────────────────────────
525
474
  if (wingetAvailable) {
526
- step('Trying winget (win-acme) ...');
527
- console.log(chalk.gray(' Running: winget install win-acme.win-acme\n'));
475
+ step('Trying winget (EFF.Certbot) ...');
476
+ console.log(chalk.gray(' Running: winget install EFF.Certbot\n'));
528
477
  const exitCode = await runLive(
529
- 'winget install -e --id win-acme.win-acme --accept-package-agreements --accept-source-agreements',
478
+ 'winget install -e --id EFF.Certbot --accept-package-agreements --accept-source-agreements',
530
479
  { timeout: 180000 },
531
480
  );
532
- if (exitCode === 0 || await verifyWinAcme()) return { success: true, client: 'winacme' };
533
- console.log(chalk.yellow(' winget win-acme failed, trying next...\n'));
481
+ if (exitCode === 0 || await verifyCertbot()) return { success: true };
482
+ console.log(chalk.yellow(' winget certbot failed, trying next...\n'));
534
483
  }
535
484
 
536
- // ── Method 2: winget (certbot EFF) ────────────────────────────────────────────
485
+ // ── Method 2: winget (win-acme) ───────────────────────────────────────────────
537
486
  if (wingetAvailable) {
538
- step('Trying winget (EFF.Certbot) ...');
539
- console.log(chalk.gray(' Running: winget install EFF.Certbot\n'));
487
+ step('Trying winget (win-acme) ...');
488
+ console.log(chalk.gray(' Running: winget install win-acme.win-acme\n'));
540
489
  const exitCode = await runLive(
541
- 'winget install -e --id EFF.Certbot --accept-package-agreements --accept-source-agreements',
490
+ 'winget install -e --id win-acme.win-acme --accept-package-agreements --accept-source-agreements',
542
491
  { timeout: 180000 },
543
492
  );
544
- if (exitCode === 0 || await verifyCertbot()) return { success: true };
545
- console.log(chalk.yellow(' winget certbot failed, trying next...\n'));
493
+ if (exitCode === 0 || await verifyWinAcme()) return { success: true, client: 'winacme' };
494
+ console.log(chalk.yellow(' winget win-acme failed, trying next...\n'));
546
495
  }
547
496
 
548
497
  // ── Method 3: Chocolatey (win-acme) ───────────────────────────────────────────
@@ -585,9 +534,6 @@ async function installCertbot() {
585
534
  // ── Method 7: Direct download win-acme (ZIP - smaller, no installer) ──────────
586
535
  const WINACME_DEST = 'C:\\Program Files\\win-acme';
587
536
 
588
- // Test network before attempting downloads
589
- try { await testNetworkConnectivity(); } catch (err) { console.log(chalk.yellow('Network test failed: ' + err.message)); }
590
-
591
537
  step('Downloading win-acme from GitHub ...');
592
538
  const winAcmeUrls = [
593
539
  'https://github.com/win-acme/win-acme/releases/latest/download/win-acme.zip',
@@ -600,7 +546,14 @@ async function installCertbot() {
600
546
 
601
547
  const zipDest = `$env:TEMP\\win-acme.zip`;
602
548
  lastDownloadError = '';
603
- let dlOk = false; try { dlOk = await downloadFile(url, zipDest, true); } catch (err) { if (err.code === 'EPERM' || err.message?.includes('EPERM')) { console.log(chalk.red('Windows Defender blocked this download.')); console.log(chalk.gray('Use the manual installer option or add a Windows Defender exclusion.')); } } if (dlOk) {
549
+ let dlOk = false;
550
+ try { dlOk = await downloadFile(url, zipDest, true); } catch (err) {
551
+ if (err.code === 'EPERM' || err.message?.includes('EPERM')) {
552
+ console.log(chalk.red('Windows Defender blocked this download.'));
553
+ console.log(chalk.gray('Use the manual installer option or add a Windows Defender exclusion.'));
554
+ }
555
+ }
556
+ if (dlOk) {
604
557
  console.log(chalk.gray(' Extracting win-acme ...\n'));
605
558
  await run(`New-Item -ItemType Directory -Force -Path '${WINACME_DEST}'`);
606
559
  await run(`Expand-Archive -Path '${zipDest}' -DestinationPath '${WINACME_DEST}' -Force`);
@@ -632,7 +585,14 @@ async function installCertbot() {
632
585
  step(`Downloading certbot installer from ${hostname} ...`);
633
586
 
634
587
  lastDownloadError = '';
635
- let dlOk = false; try { dlOk = await downloadFile(url, INSTALLER_DEST, true); } catch (err) { if (err.code === 'EPERM' || err.message?.includes('EPERM')) { console.log(chalk.red('Windows Defender blocked this download.')); console.log(chalk.gray('Use the manual installer option or add a Windows Defender exclusion.')); } } if (dlOk) {
588
+ let dlOk = false;
589
+ try { dlOk = await downloadFile(url, INSTALLER_DEST, true); } catch (err) {
590
+ if (err.code === 'EPERM' || err.message?.includes('EPERM')) {
591
+ console.log(chalk.red('Windows Defender blocked this download.'));
592
+ console.log(chalk.gray('Use the manual installer option or add a Windows Defender exclusion.'));
593
+ }
594
+ }
595
+ if (dlOk) {
636
596
  console.log(chalk.gray(' Running installer silently ...\n'));
637
597
  const ok = await runNsisInstaller(INSTALLER_DEST);
638
598
  await run(`Remove-Item -Force '${INSTALLER_DEST}' -ErrorAction SilentlyContinue`);
@@ -677,7 +637,7 @@ async function installCertbot() {
677
637
  await run('Start-Process "https://certbot.eff.org/instructions?ws=other&os=windows"');
678
638
 
679
639
  console.log(chalk.green('✓ Browser opened. Download the installer, then run:'));
680
- console.log(chalk.cyan(' easy-devops → SSL Manager → Install certbot → Specify local installer path\n'));
640
+ console.log(chalk.cyan(' easy-devops → SSL Manager → Install certbot → Specify local installer path\n'));
681
641
 
682
642
  return { success: false };
683
643
  }
@@ -139,6 +139,26 @@ function openBrowser(url) {
139
139
  run(cmd).catch(() => {});
140
140
  }
141
141
 
142
+ // ─── Usage Info ──────────────────────────────────────────────────────────────
143
+
144
+ function showUsageInfo() {
145
+ console.log(chalk.cyan('\n How to use the Dashboard:'));
146
+ console.log(chalk.gray(' ─'.repeat(40)));
147
+ console.log(' 1. Start the dashboard from this menu');
148
+ console.log(' 2. Open it in your browser (or visit the URL shown)');
149
+ console.log(' 3. Log in with the admin password from config');
150
+ console.log(' 4. Use the dashboard to:');
151
+ console.log(chalk.gray(' • Manage domains → create nginx configs'));
152
+ console.log(chalk.gray(' • View SSL certificates'));
153
+ console.log(chalk.gray(' • Control nginx (start/stop/reload)'));
154
+ console.log(chalk.gray(' • Edit nginx configuration files'));
155
+ console.log();
156
+ console.log(chalk.yellow(' Default login:'));
157
+ console.log(chalk.gray(' Username: admin'));
158
+ console.log(chalk.gray(' Password: (check your config or set one)'));
159
+ console.log();
160
+ }
161
+
142
162
  // ─── Menu ─────────────────────────────────────────────────────────────────────
143
163
 
144
164
  export default async function dashboardMenu() {
@@ -160,8 +180,8 @@ export default async function dashboardMenu() {
160
180
  console.log();
161
181
 
162
182
  const choices = status.running
163
- ? ['Open in browser', 'Stop dashboard', new inquirer.Separator(), '← Back']
164
- : ['Start dashboard', new inquirer.Separator(), '← Back'];
183
+ ? ['Open in browser', 'Stop dashboard', 'How to use', new inquirer.Separator(), '← Back']
184
+ : ['Start dashboard', 'How to use', new inquirer.Separator(), '← Back'];
165
185
 
166
186
  let choice;
167
187
  try {
@@ -257,7 +257,8 @@ export function buildConf(domain, nginxDir, certbotDir) {
257
257
 
258
258
  // Logging
259
259
  if (advanced?.accessLog) {
260
- mainBlock.push(` access_log /var/log/nginx/${name}.access.log;`);
260
+ const logDir = isWindows ? `${nginxDir.replace(/\\/g, '/')}/logs` : '/var/log/nginx';
261
+ mainBlock.push(` access_log ${logDir}/${name}.access.log;`);
261
262
  }
262
263
 
263
264
  // ─── Location Block ─────────────────────────────────────────────────────────
@@ -301,10 +302,10 @@ export function buildConf(domain, nginxDir, certbotDir) {
301
302
  mainBlock.push(` error_page 500 502 503 504 /50x.html;`);
302
303
  }
303
304
  if (security.custom404) {
304
- mainBlock.push(` location = /404.html { root /usr/share/nginx/html; internal; }`);
305
+ mainBlock.push(` location = /404.html { root ${isWindows ? nginxDir.replace(/\\/g, '/') + '/html' : '/usr/share/nginx/html'}; internal; }`);
305
306
  }
306
307
  if (security.custom50x) {
307
- mainBlock.push(` location = /50x.html { root /usr/share/nginx/html; internal; }`);
308
+ mainBlock.push(` location = /50x.html { root ${isWindows ? nginxDir.replace(/\\/g, '/') + '/html' : '/usr/share/nginx/html'}; internal; }`);
308
309
  }
309
310
  }
310
311
 
@@ -141,17 +141,26 @@ export async function start() {
141
141
  throw new NginxNotFoundError('nginx binary not found');
142
142
  }
143
143
 
144
- // Test config before starting
144
+ // Ensure required directories exist on Windows
145
+ if (process.platform === 'win32') {
146
+ await fs.mkdir(path.join(nginxDir, 'logs'), { recursive: true });
147
+ await fs.mkdir(path.join(nginxDir, 'temp'), { recursive: true });
148
+ }
149
+
150
+ // Test config before starting with explicit config path on Windows
145
151
  await ensureNginxInclude(nginxDir);
146
- const testResult = await run(`${nginxExe} -t`);
152
+ const testCmd = process.platform === 'win32'
153
+ ? `${nginxExe} -c "${path.join(nginxDir, 'conf', 'nginx.conf')}" -t`
154
+ : `${nginxExe} -t`;
155
+ const testResult = await run(testCmd);
147
156
  if (!testResult.success) {
148
157
  return { success: false, output: combineOutput(testResult) };
149
158
  }
150
159
 
151
- // Start nginx
160
+ // Start nginx with explicit config path on Windows
152
161
  if (process.platform === 'win32') {
153
- // On Windows, nginx runs in foreground so we need Start-Process
154
- const startCmd = `Start-Process -FilePath "${path.join(nginxDir, 'nginx.exe')}" -WorkingDirectory "${nginxDir}" -WindowStyle Hidden`;
162
+ const confPath = path.join(nginxDir, 'conf', 'nginx.conf');
163
+ const startCmd = `Start-Process -FilePath "${path.join(nginxDir, 'nginx.exe')}" -ArgumentList '-c','"${confPath}"' -WorkingDirectory "${nginxDir}" -WindowStyle Hidden`;
155
164
  await run(startCmd, { timeout: 10000 });
156
165
  } else {
157
166
  const result = await run('nginx', { cwd: nginxDir, timeout: 15000 });
@@ -216,7 +225,11 @@ export async function test() {
216
225
  }
217
226
 
218
227
  await ensureNginxInclude(nginxDir);
219
- const result = await run(`${nginxExe} -t`);
228
+ // Use explicit -c flag on Windows to avoid path issues
229
+ const testCmd = process.platform === 'win32'
230
+ ? `${nginxExe} -c "${path.join(nginxDir, 'conf', 'nginx.conf')}" -t`
231
+ : `${nginxExe} -t`;
232
+ const result = await run(testCmd);
220
233
  return { success: result.success, output: combineOutput(result) };
221
234
  }
222
235
 
@@ -18,7 +18,12 @@ function getNginxExe(nginxDir) {
18
18
 
19
19
  function nginxTestCmd(nginxDir) {
20
20
  const exe = getNginxExe(nginxDir);
21
- return isWindows ? `& "${exe}" -t` : 'nginx -t';
21
+ // Use explicit -c flag on Windows to avoid path issues
22
+ if (isWindows) {
23
+ const confPath = `${nginxDir}\\conf\\nginx.conf`;
24
+ return `& "${exe}" -c "${confPath}" -t`;
25
+ }
26
+ return 'nginx -t';
22
27
  }
23
28
 
24
29
  function nginxReloadCmd(nginxDir) {