easy-devops 0.1.0

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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +325 -0
  3. package/cli/index.js +91 -0
  4. package/cli/managers/domain-manager.js +451 -0
  5. package/cli/managers/nginx-manager.js +329 -0
  6. package/cli/managers/node-manager.js +275 -0
  7. package/cli/managers/ssl-manager.js +397 -0
  8. package/cli/menus/.gitkeep +0 -0
  9. package/cli/menus/dashboard.js +223 -0
  10. package/cli/menus/domains.js +5 -0
  11. package/cli/menus/nginx.js +5 -0
  12. package/cli/menus/nodejs.js +5 -0
  13. package/cli/menus/settings.js +83 -0
  14. package/cli/menus/ssl.js +5 -0
  15. package/core/config.js +37 -0
  16. package/core/db.js +30 -0
  17. package/core/detector.js +257 -0
  18. package/core/nginx-conf-generator.js +309 -0
  19. package/core/shell.js +151 -0
  20. package/dashboard/lib/.gitkeep +0 -0
  21. package/dashboard/lib/cert-reader.js +59 -0
  22. package/dashboard/lib/domains-db.js +51 -0
  23. package/dashboard/lib/nginx-conf-generator.js +16 -0
  24. package/dashboard/lib/nginx-service.js +282 -0
  25. package/dashboard/public/js/app.js +486 -0
  26. package/dashboard/routes/.gitkeep +0 -0
  27. package/dashboard/routes/auth.js +30 -0
  28. package/dashboard/routes/domains.js +300 -0
  29. package/dashboard/routes/nginx.js +151 -0
  30. package/dashboard/routes/settings.js +78 -0
  31. package/dashboard/routes/ssl.js +105 -0
  32. package/dashboard/server.js +79 -0
  33. package/dashboard/views/index.ejs +327 -0
  34. package/dashboard/views/partials/domain-form.ejs +229 -0
  35. package/dashboard/views/partials/domains-panel.ejs +66 -0
  36. package/dashboard/views/partials/login.ejs +50 -0
  37. package/dashboard/views/partials/nginx-panel.ejs +90 -0
  38. package/dashboard/views/partials/overview.ejs +67 -0
  39. package/dashboard/views/partials/settings-panel.ejs +37 -0
  40. package/dashboard/views/partials/sidebar.ejs +45 -0
  41. package/dashboard/views/partials/ssl-panel.ejs +53 -0
  42. package/data/.gitkeep +0 -0
  43. package/install.bat +41 -0
  44. package/install.ps1 +653 -0
  45. package/install.sh +452 -0
  46. package/lib/installer/.gitkeep +0 -0
  47. package/lib/installer/detect.sh +88 -0
  48. package/lib/installer/node-versions.sh +109 -0
  49. package/lib/installer/nvm-bootstrap.sh +77 -0
  50. package/lib/installer/picker.sh +163 -0
  51. package/lib/installer/progress.sh +25 -0
  52. package/package.json +67 -0
@@ -0,0 +1,66 @@
1
+ <!-- Domains List Page -->
2
+ <div v-show="page === 'domains'" class="max-w-6xl mx-auto">
3
+ <div class="flex items-center justify-between mb-2">
4
+ <h2 class="text-2xl font-bold">Domains</h2>
5
+ <button @click="toggleDomainForm()" class="btn btn-primary">
6
+ <svg v-if="!domains.showForm" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
7
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
8
+ </svg>
9
+ {{ domains.showForm ? 'Cancel' : 'Add Domain' }}
10
+ </button>
11
+ </div>
12
+ <p class="text-slate-400 mb-8">Nginx reverse proxy configurations</p>
13
+
14
+ <!-- Domain Form (included from domain-form.ejs) -->
15
+ <%- include('domain-form.ejs') %>
16
+
17
+ <!-- Domain List -->
18
+ <div v-if="domains.loading" class="text-slate-500">Loading...</div>
19
+ <div v-else-if="domains.list.length === 0" class="card p-12 text-center">
20
+ <p class="text-slate-500">No domains configured. Click "Add Domain" to get started.</p>
21
+ </div>
22
+ <div v-else class="card overflow-hidden">
23
+ <table class="data-table">
24
+ <thead>
25
+ <tr>
26
+ <th>Domain</th>
27
+ <th>Port</th>
28
+ <th>Type</th>
29
+ <th>SSL</th>
30
+ <th>Cert</th>
31
+ <th class="text-right">Actions</th>
32
+ </tr>
33
+ </thead>
34
+ <tbody>
35
+ <tr v-for="d in domains.list" :key="d.name">
36
+ <td class="font-mono font-medium">{{ d.name }}</td>
37
+ <td class="font-mono text-slate-400">:{{ d.port }}</td>
38
+ <td><span class="badge badge-neutral">{{ (d.upstreamType || 'http').toUpperCase() }}</span></td>
39
+ <td>
40
+ <span :class="d.ssl?.enabled ? 'badge badge-success' : 'badge badge-neutral'">
41
+ {{ d.ssl?.enabled ? 'HTTPS' : 'HTTP' }}
42
+ </span>
43
+ </td>
44
+ <td class="font-mono text-slate-400">
45
+ {{ d.daysLeft != null ? d.daysLeft + 'd' : '—' }}
46
+ </td>
47
+ <td class="text-right">
48
+ <div class="flex justify-end gap-2">
49
+ <button @click="editDomain(d.name)" class="btn btn-secondary btn-sm">Edit</button>
50
+ <button @click="reloadDomain(d.name)" class="btn btn-secondary btn-sm btn-icon" title="Reload">
51
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
52
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
53
+ </svg>
54
+ </button>
55
+ <button @click="deleteDomain(d.name)" class="btn btn-danger btn-sm btn-icon" title="Delete">
56
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
57
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
58
+ </svg>
59
+ </button>
60
+ </div>
61
+ </td>
62
+ </tr>
63
+ </tbody>
64
+ </table>
65
+ </div>
66
+ </div>
@@ -0,0 +1,50 @@
1
+ <!-- Login Page -->
2
+ <div v-if="!authenticated" class="min-h-screen flex items-center justify-center p-4">
3
+ <div class="w-full max-w-md">
4
+ <div class="text-center mb-10">
5
+ <div class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-br from-brand-400 to-brand-600 mb-4">
6
+ <svg class="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
7
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
8
+ </svg>
9
+ </div>
10
+ <h1 class="text-2xl font-bold text-white">Easy DevOps</h1>
11
+ <p class="text-slate-400 mt-2">Sign in to access your dashboard</p>
12
+ </div>
13
+
14
+ <div class="card p-8">
15
+ <div class="space-y-5">
16
+ <div class="input-group">
17
+ <input v-model="login.password" type="password" placeholder=" " @keyup.enter="doLogin" />
18
+ <label>Password</label>
19
+ </div>
20
+
21
+ <p v-if="login.error" class="text-red-400 text-sm flex items-center gap-2">
22
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
23
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
24
+ </svg>
25
+ {{ login.error }}
26
+ </p>
27
+
28
+ <button @click="doLogin" :disabled="login.loading" class="btn btn-primary w-full">
29
+ <svg v-if="login.loading" class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
30
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
31
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
32
+ </svg>
33
+ {{ login.loading ? 'Signing in...' : 'Sign In' }}
34
+ </button>
35
+ </div>
36
+
37
+ <p class="text-center text-xs text-slate-500 mt-6">No password set? Leave blank and press Sign In.</p>
38
+ </div>
39
+
40
+ <button @click="toggleTheme" class="mx-auto mt-6 flex items-center gap-2 text-sm text-slate-500 hover:text-slate-300 transition">
41
+ <svg v-if="isDark" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
42
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
43
+ </svg>
44
+ <svg v-else class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
45
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
46
+ </svg>
47
+ {{ isDark ? 'Light mode' : 'Dark mode' }}
48
+ </button>
49
+ </div>
50
+ </div>
@@ -0,0 +1,90 @@
1
+ <!-- Nginx Page -->
2
+ <div v-show="page === 'nginx'" class="max-w-6xl mx-auto">
3
+ <div class="flex items-center justify-between mb-2">
4
+ <h2 class="text-2xl font-bold">Nginx</h2>
5
+ <button @click="loadNginxStatus" class="btn btn-secondary btn-sm">
6
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
7
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
8
+ </svg>
9
+ Refresh
10
+ </button>
11
+ </div>
12
+ <p class="text-slate-400 mb-8">Control, configure and monitor nginx</p>
13
+
14
+ <!-- Status Bar -->
15
+ <div class="card p-6 mb-6">
16
+ <div class="flex items-center justify-between flex-wrap gap-4">
17
+ <div class="flex items-center gap-4">
18
+ <span :class="nginx.status?.running ? 'status-dot online' : 'status-dot offline'" class="w-4 h-4"></span>
19
+ <div>
20
+ <span class="text-xl font-semibold">{{ nginx.status?.running ? 'Running' : 'Stopped' }}</span>
21
+ <span v-if="nginx.status?.version" class="text-slate-400 ml-2 font-mono text-sm">{{ nginx.status.version }}</span>
22
+ </div>
23
+ </div>
24
+ <div class="flex gap-2">
25
+ <button v-if="!nginx.status?.running" @click="nginxAction('start')" class="btn btn-primary">
26
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
27
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
28
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
29
+ </svg>
30
+ Start
31
+ </button>
32
+ <template v-else>
33
+ <button @click="nginxAction('reload')" class="btn btn-primary btn-sm">Reload</button>
34
+ <button @click="nginxAction('restart')" class="btn btn-secondary btn-sm">Restart</button>
35
+ <button @click="nginxAction('stop')" class="btn btn-danger btn-sm">Stop</button>
36
+ </template>
37
+ <button @click="nginxAction('test')" class="btn btn-secondary btn-sm">Test Config</button>
38
+ </div>
39
+ </div>
40
+
41
+ <div v-if="nginx.actionMsg" class="mt-4 p-4 rounded-xl bg-brand-500/10 border border-brand-500/20 text-brand-400 text-sm font-mono">
42
+ {{ nginx.actionMsg }}
43
+ </div>
44
+ <div v-if="nginx.actionError" class="mt-4 p-4 rounded-xl bg-red-500/10 border border-red-500/20 text-red-400 text-sm font-mono">
45
+ {{ nginx.actionError }}
46
+ </div>
47
+ </div>
48
+
49
+ <!-- Config Editor -->
50
+ <div class="card mb-6">
51
+ <div class="flex items-center justify-between p-4 border-b border-slate-800">
52
+ <h3 class="font-semibold">Config Editor</h3>
53
+ <div class="flex gap-2">
54
+ <select v-model="nginx.selectedConfig" @change="selectConfig(nginx.selectedConfig)" class="input-group bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-brand-500">
55
+ <option value="" disabled>Select file...</option>
56
+ <option v-for="c in nginx.configs" :key="c" :value="c">{{ c }}</option>
57
+ </select>
58
+ <button @click="saveNginxConfig" :disabled="!nginx.selectedConfig || nginx.configSaving" class="btn btn-primary btn-sm">
59
+ {{ nginx.configSaving ? 'Saving...' : 'Save' }}
60
+ </button>
61
+ </div>
62
+ </div>
63
+ <div class="p-4">
64
+ <textarea v-model="nginx.configContent" :disabled="!nginx.selectedConfig" rows="16"
65
+ class="w-full bg-slate-900 border border-slate-800 rounded-xl p-4 font-mono text-sm text-slate-300 focus:outline-none focus:ring-2 focus:ring-brand-500 disabled:opacity-40 resize-y"
66
+ placeholder="Select a config file to start editing..."></textarea>
67
+ <p v-if="nginx.configMsg" class="mt-2 text-sm font-mono" :class="nginx.configMsg.includes('pass') ? 'text-brand-400' : 'text-red-400'">
68
+ {{ nginx.configMsg }}
69
+ </p>
70
+ </div>
71
+ </div>
72
+
73
+ <!-- Error Log -->
74
+ <div class="card">
75
+ <div class="flex items-center justify-between p-4 border-b border-slate-800">
76
+ <h3 class="font-semibold">Error Log</h3>
77
+ <button @click="loadNginxLogs" class="btn btn-secondary btn-sm">
78
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
79
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
80
+ </svg>
81
+ Refresh
82
+ </button>
83
+ </div>
84
+ <div class="p-4 max-h-64 overflow-y-auto">
85
+ <div v-if="nginx.logsLoading" class="text-slate-500 text-sm">Loading...</div>
86
+ <div v-else-if="nginx.logs.length === 0" class="text-slate-500 text-sm">No errors logged.</div>
87
+ <pre v-else class="text-xs text-slate-400 font-mono leading-relaxed">{{ nginx.logs.join('\n') }}</pre>
88
+ </div>
89
+ </div>
90
+ </div>
@@ -0,0 +1,67 @@
1
+ <!-- Overview Page -->
2
+ <div v-show="page === 'overview'" class="max-w-6xl mx-auto">
3
+ <h2 class="text-2xl font-bold mb-2">Dashboard</h2>
4
+ <p class="text-slate-400 mb-8">System status at a glance</p>
5
+
6
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
7
+ <!-- Nginx Card -->
8
+ <div class="card p-6">
9
+ <div class="flex items-start justify-between mb-4">
10
+ <div class="w-12 h-12 rounded-xl bg-slate-800 flex items-center justify-center">
11
+ <svg class="w-6 h-6 text-brand-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
12
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
13
+ </svg>
14
+ </div>
15
+ <span v-if="nginx.status" :class="nginx.status.running ? 'status-dot online' : 'status-dot offline'"></span>
16
+ </div>
17
+ <h3 class="font-semibold text-lg">Nginx</h3>
18
+ <p v-if="nginx.status" class="text-slate-400 text-sm mt-1">{{ nginx.status.running ? 'Running' : 'Stopped' }}</p>
19
+ <p v-else class="text-slate-500 text-sm mt-1">Checking...</p>
20
+ <button @click="loadPage('nginx')" class="mt-4 text-sm text-brand-400 hover:text-brand-300 font-medium flex items-center gap-1">
21
+ Manage
22
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
23
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
24
+ </svg>
25
+ </button>
26
+ </div>
27
+
28
+ <!-- SSL Card -->
29
+ <div class="card p-6">
30
+ <div class="flex items-start justify-between mb-4">
31
+ <div class="w-12 h-12 rounded-xl bg-slate-800 flex items-center justify-center">
32
+ <svg class="w-6 h-6 text-brand-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
33
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.044-.133-2.05-.382-3.016z" />
34
+ </svg>
35
+ </div>
36
+ <span v-if="expiringCount > 0" class="badge badge-warning">{{ expiringCount }} expiring</span>
37
+ </div>
38
+ <h3 class="font-semibold text-lg">SSL Certificates</h3>
39
+ <p class="text-slate-400 text-sm mt-1">{{ ssl.certs.length }} certificate{{ ssl.certs.length !== 1 ? 's' : '' }}</p>
40
+ <button @click="loadPage('ssl')" class="mt-4 text-sm text-brand-400 hover:text-brand-300 font-medium flex items-center gap-1">
41
+ Manage
42
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
43
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
44
+ </svg>
45
+ </button>
46
+ </div>
47
+
48
+ <!-- Domains Card -->
49
+ <div class="card p-6">
50
+ <div class="flex items-start justify-between mb-4">
51
+ <div class="w-12 h-12 rounded-xl bg-slate-800 flex items-center justify-center">
52
+ <svg class="w-6 h-6 text-brand-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
53
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
54
+ </svg>
55
+ </div>
56
+ </div>
57
+ <h3 class="font-semibold text-lg">Domains</h3>
58
+ <p class="text-slate-400 text-sm mt-1">{{ domains.list.length }} domain{{ domains.list.length !== 1 ? 's' : '' }}</p>
59
+ <button @click="loadPage('domains')" class="mt-4 text-sm text-brand-400 hover:text-brand-300 font-medium flex items-center gap-1">
60
+ Manage
61
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
62
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
63
+ </svg>
64
+ </button>
65
+ </div>
66
+ </div>
67
+ </div>
@@ -0,0 +1,37 @@
1
+ <!-- Settings Page -->
2
+ <div v-show="page === 'settings'" class="max-w-2xl mx-auto">
3
+ <h2 class="text-2xl font-bold mb-2">Settings</h2>
4
+ <p class="text-slate-400 mb-8">Dashboard configuration and service paths</p>
5
+
6
+ <div class="card p-6 space-y-6">
7
+ <div class="input-group">
8
+ <input v-model.number="settings.dashboardPort" type="number" placeholder=" " />
9
+ <label>Dashboard Port</label>
10
+ <p class="text-xs text-slate-500 mt-1 ml-1">Restart dashboard after changing</p>
11
+ </div>
12
+
13
+ <div class="input-group">
14
+ <input v-model="settings.nginxDir" placeholder=" " />
15
+ <label>Nginx Directory</label>
16
+ </div>
17
+
18
+ <div class="input-group">
19
+ <input v-model="settings.certbotDir" placeholder=" " />
20
+ <label>Certbot Directory</label>
21
+ </div>
22
+
23
+ <div class="input-group">
24
+ <input v-model="settings.password" type="password" placeholder=" " />
25
+ <label>New Password</label>
26
+ <p class="text-xs text-slate-500 mt-1 ml-1">Leave blank to keep current</p>
27
+ </div>
28
+
29
+ <div class="flex items-center gap-4 pt-2">
30
+ <button @click="saveSettings" :disabled="settings.loading" class="btn btn-primary">
31
+ {{ settings.loading ? 'Saving...' : 'Save Settings' }}
32
+ </button>
33
+ <span v-if="settings.msg" class="text-brand-400 text-sm">{{ settings.msg }}</span>
34
+ <span v-if="settings.error" class="text-red-400 text-sm">{{ settings.error }}</span>
35
+ </div>
36
+ </div>
37
+ </div>
@@ -0,0 +1,45 @@
1
+ <!-- Sidebar Navigation -->
2
+ <aside class="w-60 flex-shrink-0 bg-slate-900 border-r border-slate-800 flex flex-col">
3
+ <div class="p-5 border-b border-slate-800">
4
+ <div class="flex items-center gap-3">
5
+ <div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-400 to-brand-600 flex items-center justify-center">
6
+ <svg class="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
7
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
8
+ </svg>
9
+ </div>
10
+ <div>
11
+ <h1 class="font-bold text-white">Easy DevOps</h1>
12
+ <p class="text-xs text-slate-500">v0.1.0</p>
13
+ </div>
14
+ </div>
15
+ </div>
16
+
17
+ <nav class="flex-1 p-3 space-y-1 overflow-y-auto">
18
+ <button v-for="item in navItems" :key="item.id" @click="navigateTo(item.id)"
19
+ :class="['w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all',
20
+ page === item.id
21
+ ? 'bg-brand-500/10 text-brand-400'
22
+ : 'text-slate-400 hover:text-white hover:bg-slate-800']">
23
+ <span v-html="item.icon"></span>
24
+ {{ item.label }}
25
+ </button>
26
+ </nav>
27
+
28
+ <div class="p-3 border-t border-slate-800 space-y-1">
29
+ <button @click="toggleTheme" class="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm text-slate-400 hover:text-white hover:bg-slate-800 transition">
30
+ <svg v-if="isDark" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
31
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
32
+ </svg>
33
+ <svg v-else class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
34
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
35
+ </svg>
36
+ {{ isDark ? 'Light mode' : 'Dark mode' }}
37
+ </button>
38
+ <button @click="doLogout" class="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm text-slate-400 hover:text-red-400 hover:bg-red-500/10 transition">
39
+ <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
40
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
41
+ </svg>
42
+ Sign out
43
+ </button>
44
+ </div>
45
+ </aside>
@@ -0,0 +1,53 @@
1
+ <!-- SSL Page -->
2
+ <div v-show="page === 'ssl'" class="max-w-6xl mx-auto">
3
+ <div class="flex items-center justify-between mb-2">
4
+ <h2 class="text-2xl font-bold">SSL Certificates</h2>
5
+ <button @click="renewAll" :disabled="ssl.renewingDomain !== null" class="btn btn-primary btn-sm">
6
+ {{ ssl.renewingDomain === 'all' ? 'Renewing...' : 'Renew Expiring' }}
7
+ </button>
8
+ </div>
9
+ <p class="text-slate-400 mb-8">Let's Encrypt certificates managed by certbot</p>
10
+
11
+ <div v-if="ssl.error" class="card p-4 mb-6 bg-red-500/10 border-red-500/20 text-red-400">
12
+ {{ ssl.error }}
13
+ </div>
14
+
15
+ <div v-if="ssl.loading" class="text-slate-500">Loading certificates...</div>
16
+
17
+ <div v-else-if="ssl.certs.length === 0" class="card p-12 text-center">
18
+ <p class="text-slate-500">No certificates found. Use the CLI to issue certificates.</p>
19
+ </div>
20
+
21
+ <div v-else class="card overflow-hidden">
22
+ <table class="data-table">
23
+ <thead>
24
+ <tr>
25
+ <th>Domain</th>
26
+ <th>Expires</th>
27
+ <th>Days Left</th>
28
+ <th>Status</th>
29
+ <th class="text-right">Actions</th>
30
+ </tr>
31
+ </thead>
32
+ <tbody>
33
+ <tr v-for="cert in ssl.certs" :key="cert.domain">
34
+ <td class="font-mono text-sm">{{ cert.domain }}</td>
35
+ <td class="text-slate-400">{{ cert.expiry ? new Date(cert.expiry).toLocaleDateString() : '—' }}</td>
36
+ <td>
37
+ <span :class="certDaysColor(cert)" class="font-mono font-semibold">
38
+ {{ cert.daysLeft !== null ? cert.daysLeft + 'd' : '—' }}
39
+ </span>
40
+ </td>
41
+ <td>
42
+ <span :class="'badge badge-' + certBadgeType(cert)">{{ certStatus(cert) }}</span>
43
+ </td>
44
+ <td class="text-right">
45
+ <button @click="renewCert(cert.domain)" :disabled="ssl.renewingDomain !== null" class="btn btn-secondary btn-sm" :class="{ 'opacity-50': ssl.renewingDomain !== null }">
46
+ {{ ssl.renewingDomain === cert.domain ? 'Renewing...' : 'Renew' }}
47
+ </button>
48
+ </td>
49
+ </tr>
50
+ </tbody>
51
+ </table>
52
+ </div>
53
+ </div>
package/data/.gitkeep ADDED
File without changes
package/install.bat ADDED
@@ -0,0 +1,41 @@
1
+ @echo off
2
+ :: install.bat — Easy DevOps Bootstrap Installer (Windows)
3
+ :: Thin launcher: checks for PowerShell 5.1+ and delegates to install.ps1
4
+ ::
5
+ :: Usage:
6
+ :: install.bat [OPTIONS]
7
+ ::
8
+ :: Options are passed through to install.ps1:
9
+ :: --help Print usage and exit
10
+ :: --version VERSION Skip picker; use the specified Node.js version
11
+ :: --keep-node Skip Node.js management
12
+
13
+ setlocal EnableDelayedExpansion
14
+
15
+ :: Check that PowerShell is available
16
+ where powershell.exe >nul 2>&1
17
+ if errorlevel 1 (
18
+ echo Error: PowerShell is required but was not found on PATH. >&2
19
+ echo Please install PowerShell 5.1 or later: >&2
20
+ echo https://github.com/PowerShell/PowerShell/releases >&2
21
+ exit /b 1
22
+ )
23
+
24
+ :: Verify minimum PowerShell version (5.1)
25
+ for /f "usebackq tokens=*" %%v in (
26
+ `powershell.exe -NoProfile -Command "$PSVersionTable.PSVersion.Major" 2^>nul`
27
+ ) do set PS_MAJOR=%%v
28
+
29
+ if not defined PS_MAJOR (
30
+ echo Error: Could not determine PowerShell version. >&2
31
+ exit /b 1
32
+ )
33
+ if %PS_MAJOR% LSS 5 (
34
+ echo Error: PowerShell 5.1 or later is required. Found version %PS_MAJOR%. >&2
35
+ echo Please upgrade PowerShell: https://github.com/PowerShell/PowerShell/releases >&2
36
+ exit /b 1
37
+ )
38
+
39
+ :: Delegate to install.ps1 in the same directory
40
+ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0install.ps1" %*
41
+ exit /b %ERRORLEVEL%