easy-devops 0.1.0 → 0.1.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/cli/managers/nginx-manager.js +86 -12
- package/cli/managers/ssl-manager.js +55 -12
- package/core/nginx-conf-generator.js +63 -2
- package/dashboard/lib/nginx-service.js +15 -4
- package/install.ps1 +239 -130
- package/install.sh +85 -38
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@ import inquirer from 'inquirer';
|
|
|
15
15
|
import ora from 'ora';
|
|
16
16
|
import { run, runLive } from '../../core/shell.js';
|
|
17
17
|
import { loadConfig } from '../../core/config.js';
|
|
18
|
+
import { ensureNginxInclude } from '../../core/nginx-conf-generator.js';
|
|
18
19
|
|
|
19
20
|
const isWindows = process.platform === 'win32';
|
|
20
21
|
|
|
@@ -51,6 +52,7 @@ async function getNginxStatus(nginxDir) {
|
|
|
51
52
|
// ─── testConfig ───────────────────────────────────────────────────────────────
|
|
52
53
|
|
|
53
54
|
async function testConfig(nginxExe, nginxDir) {
|
|
55
|
+
await ensureNginxInclude(nginxDir);
|
|
54
56
|
const cmd = isWindows ? `& "${nginxExe}" -t` : 'nginx -t';
|
|
55
57
|
const result = await run(cmd, { cwd: nginxDir });
|
|
56
58
|
return {
|
|
@@ -222,23 +224,95 @@ async function stopNginx(nginxDir) {
|
|
|
222
224
|
// ─── installNginx ─────────────────────────────────────────────────────────────
|
|
223
225
|
|
|
224
226
|
async function installNginx() {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (result.success) {
|
|
234
|
-
spinner.succeed('nginx installed successfully');
|
|
235
|
-
return { success: true, message: 'nginx installed successfully', output: result.stdout };
|
|
236
|
-
} else {
|
|
227
|
+
if (!isWindows) {
|
|
228
|
+
const spinner = ora('Installing nginx…').start();
|
|
229
|
+
const result = await run('sudo apt-get install -y nginx', { timeout: 120000 });
|
|
230
|
+
if (result.success) {
|
|
231
|
+
spinner.succeed('nginx installed successfully');
|
|
232
|
+
return { success: true, message: 'nginx installed successfully', output: result.stdout };
|
|
233
|
+
}
|
|
237
234
|
spinner.fail('Installation failed');
|
|
238
235
|
console.log(chalk.red(result.stderr || result.stdout));
|
|
239
236
|
console.log(chalk.gray('\n Manual instructions: https://nginx.org/en/docs/install.html\n'));
|
|
240
237
|
return { success: false, message: 'Installation failed', output: result.stderr || result.stdout };
|
|
241
238
|
}
|
|
239
|
+
|
|
240
|
+
// ── Windows ──────────────────────────────────────────────────────────────────
|
|
241
|
+
const spinner = ora('Checking for winget…').start();
|
|
242
|
+
|
|
243
|
+
// Try winget first (available on Windows 10/11 desktop; not on Windows Server by default)
|
|
244
|
+
const wingetCheck = await run('where.exe winget 2>$null');
|
|
245
|
+
const hasWinget = wingetCheck.success && wingetCheck.stdout.trim().length > 0;
|
|
246
|
+
|
|
247
|
+
if (hasWinget) {
|
|
248
|
+
spinner.text = 'Installing nginx via winget…';
|
|
249
|
+
const result = await run(
|
|
250
|
+
'winget install -e --id Nginx.Nginx --accept-package-agreements --accept-source-agreements',
|
|
251
|
+
{ timeout: 120000 },
|
|
252
|
+
);
|
|
253
|
+
if (result.success) {
|
|
254
|
+
spinner.succeed('nginx installed successfully');
|
|
255
|
+
return { success: true, message: 'nginx installed successfully', output: result.stdout };
|
|
256
|
+
}
|
|
257
|
+
spinner.fail('winget install failed');
|
|
258
|
+
console.log(chalk.red(result.stderr || result.stdout));
|
|
259
|
+
console.log(chalk.gray('\n Manual instructions: https://nginx.org/en/docs/install.html\n'));
|
|
260
|
+
return { success: false, message: 'Installation failed', output: result.stderr || result.stdout };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── Fallback: direct download from nginx.org ─────────────────────────────────
|
|
264
|
+
spinner.text = 'Fetching latest nginx version…';
|
|
265
|
+
|
|
266
|
+
// Try to resolve the current stable version; fall back to a known-good release
|
|
267
|
+
const FALLBACK_VERSION = '1.26.3';
|
|
268
|
+
let nginxVersion = FALLBACK_VERSION;
|
|
269
|
+
|
|
270
|
+
const fetchVersionResult = await run(
|
|
271
|
+
`try { $p=(Invoke-WebRequest -Uri 'https://nginx.org/en/download.html' -UseBasicParsing -TimeoutSec 15).Content; if($p -match 'nginx-(\\d+\\.\\d+\\.\\d+)\\.zip'){$Matches[1]}else{''} } catch { '' }`,
|
|
272
|
+
{ timeout: 20000 },
|
|
273
|
+
);
|
|
274
|
+
const fetched = (fetchVersionResult.stdout || '').trim();
|
|
275
|
+
if (/^\d+\.\d+\.\d+$/.test(fetched)) nginxVersion = fetched;
|
|
276
|
+
|
|
277
|
+
const { nginxDir } = loadConfig();
|
|
278
|
+
const zipUrl = `https://nginx.org/download/nginx-${nginxVersion}.zip`;
|
|
279
|
+
|
|
280
|
+
spinner.text = `Downloading nginx ${nginxVersion}…`;
|
|
281
|
+
|
|
282
|
+
const downloadResult = await run(
|
|
283
|
+
`$ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '${zipUrl}' -OutFile "$env:TEMP\\nginx-${nginxVersion}.zip" -UseBasicParsing -TimeoutSec 120`,
|
|
284
|
+
{ timeout: 130000 },
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
if (!downloadResult.success) {
|
|
288
|
+
spinner.fail('Download failed');
|
|
289
|
+
console.log(chalk.red(downloadResult.stderr || downloadResult.stdout));
|
|
290
|
+
console.log(chalk.gray(`\n Download nginx manually from: https://nginx.org/en/download.html\n`));
|
|
291
|
+
return { success: false, message: 'Download failed', output: downloadResult.stderr || downloadResult.stdout };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
spinner.text = 'Extracting nginx…';
|
|
295
|
+
|
|
296
|
+
const extractResult = await run(
|
|
297
|
+
`$ProgressPreference='SilentlyContinue'; $tmp="$env:TEMP\\nginx-extract-${nginxVersion}"; if(Test-Path $tmp){Remove-Item -Recurse -Force $tmp}; Expand-Archive -Path "$env:TEMP\\nginx-${nginxVersion}.zip" -DestinationPath $tmp -Force; $src=Join-Path $tmp 'nginx-${nginxVersion}'; if(-not(Test-Path '${nginxDir}')){New-Item -ItemType Directory -Force '${nginxDir}'|Out-Null}; Copy-Item -Path "$src\\*" -Destination '${nginxDir}' -Recurse -Force; Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue; Remove-Item -Force "$env:TEMP\\nginx-${nginxVersion}.zip" -ErrorAction SilentlyContinue`,
|
|
298
|
+
{ timeout: 60000 },
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
if (!extractResult.success) {
|
|
302
|
+
spinner.fail('Extraction failed');
|
|
303
|
+
console.log(chalk.red(extractResult.stderr || extractResult.stdout));
|
|
304
|
+
return { success: false, message: 'Extraction failed', output: extractResult.stderr || extractResult.stdout };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Verify nginx.exe is present
|
|
308
|
+
const verifyResult = await run(`Test-Path '${nginxDir}\\nginx.exe'`);
|
|
309
|
+
if (!verifyResult.success || !verifyResult.stdout.trim().toLowerCase().includes('true')) {
|
|
310
|
+
spinner.fail(`nginx.exe not found in ${nginxDir} after extraction`);
|
|
311
|
+
return { success: false, message: 'nginx.exe not found after extraction' };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
spinner.succeed(`nginx ${nginxVersion} installed to ${nginxDir}`);
|
|
315
|
+
return { success: true, message: `nginx ${nginxVersion} installed successfully`, output: '' };
|
|
242
316
|
}
|
|
243
317
|
|
|
244
318
|
// ─── showNginxManager ─────────────────────────────────────────────────────────
|
|
@@ -242,25 +242,68 @@ async function renewExpiring(certs) {
|
|
|
242
242
|
// ─── installCertbot ───────────────────────────────────────────────────────────
|
|
243
243
|
|
|
244
244
|
async function installCertbot() {
|
|
245
|
-
if (isWindows) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
245
|
+
if (!isWindows) {
|
|
246
|
+
const exitCode = await runLive('sudo apt-get install -y certbot', { timeout: 180000 });
|
|
247
|
+
return { success: exitCode === 0 };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── Windows: winget → Chocolatey → direct EFF installer ──────────────────────
|
|
251
|
+
|
|
252
|
+
// 1. Try winget (Windows 10/11 desktop; not on Windows Server by default)
|
|
253
|
+
const wingetCheck = await run('where.exe winget 2>$null');
|
|
254
|
+
if (wingetCheck.success && wingetCheck.stdout.trim().length > 0) {
|
|
250
255
|
console.log(chalk.gray('\n Installing via winget (EFF.Certbot) ...\n'));
|
|
251
256
|
const exitCode = await runLive(
|
|
252
257
|
'winget install -e --id EFF.Certbot --accept-package-agreements --accept-source-agreements',
|
|
253
258
|
{ timeout: 180000 },
|
|
254
259
|
);
|
|
255
|
-
if (exitCode
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
+
if (exitCode === 0) {
|
|
261
|
+
const check = await run(`Test-Path "${CERTBOT_WIN_EXE}"`);
|
|
262
|
+
return { success: check.stdout.trim().toLowerCase() === 'true' };
|
|
263
|
+
}
|
|
264
|
+
console.log(chalk.yellow(' winget failed, trying Chocolatey...\n'));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 2. Try Chocolatey (common on Windows Server)
|
|
268
|
+
const chocoCheck = await run('where.exe choco 2>$null');
|
|
269
|
+
if (chocoCheck.success && chocoCheck.stdout.trim().length > 0) {
|
|
270
|
+
console.log(chalk.gray('\n Installing via Chocolatey ...\n'));
|
|
271
|
+
const exitCode = await runLive('choco install certbot -y', { timeout: 180000 });
|
|
272
|
+
if (exitCode === 0) {
|
|
273
|
+
const check = await run(`Test-Path "${CERTBOT_WIN_EXE}"`);
|
|
274
|
+
return { success: check.stdout.trim().toLowerCase() === 'true' };
|
|
275
|
+
}
|
|
276
|
+
console.log(chalk.yellow(' Chocolatey failed, trying direct download...\n'));
|
|
260
277
|
}
|
|
261
278
|
|
|
262
|
-
|
|
263
|
-
|
|
279
|
+
// 3. Direct download — official EFF installer (NSIS, supports /S silent flag)
|
|
280
|
+
const installerUrl = 'https://dl.eff.org/certbot-beta-installer-win_amd64_signed.exe';
|
|
281
|
+
|
|
282
|
+
console.log(chalk.gray('\n Downloading certbot installer from dl.eff.org ...\n'));
|
|
283
|
+
|
|
284
|
+
const downloadResult = await run(
|
|
285
|
+
`$ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '${installerUrl}' -OutFile "$env:TEMP\\certbot-installer.exe" -UseBasicParsing -TimeoutSec 120`,
|
|
286
|
+
{ timeout: 130000 },
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
if (!downloadResult.success) {
|
|
290
|
+
console.log(chalk.red(' Download failed: ' + (downloadResult.stderr || downloadResult.stdout)));
|
|
291
|
+
return { success: false };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
console.log(chalk.gray(' Running installer silently ...\n'));
|
|
295
|
+
|
|
296
|
+
await run(
|
|
297
|
+
`Start-Process -FilePath "$env:TEMP\\certbot-installer.exe" -ArgumentList '/S' -Wait -NoNewWindow`,
|
|
298
|
+
{ timeout: 120000 },
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// Cleanup installer
|
|
302
|
+
await run(`Remove-Item -Force "$env:TEMP\\certbot-installer.exe" -ErrorAction SilentlyContinue`);
|
|
303
|
+
|
|
304
|
+
// Verify
|
|
305
|
+
const check = await run(`Test-Path "${CERTBOT_WIN_EXE}"`);
|
|
306
|
+
return { success: check.stdout.trim().toLowerCase() === 'true' };
|
|
264
307
|
}
|
|
265
308
|
|
|
266
309
|
// ─── showSslManager ───────────────────────────────────────────────────────────
|
|
@@ -11,6 +11,63 @@ import fs from 'fs/promises';
|
|
|
11
11
|
import path from 'path';
|
|
12
12
|
import { loadConfig } from './config.js';
|
|
13
13
|
|
|
14
|
+
const isWindows = process.platform === 'win32';
|
|
15
|
+
|
|
16
|
+
/** Returns the conf.d directory for domain config files. */
|
|
17
|
+
function getConfDDir(nginxDir) {
|
|
18
|
+
return isWindows
|
|
19
|
+
? path.join(nginxDir, 'conf', 'conf.d')
|
|
20
|
+
: path.join(nginxDir, 'conf.d');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Returns the path to nginx.conf. */
|
|
24
|
+
function getNginxConfPath(nginxDir) {
|
|
25
|
+
return isWindows
|
|
26
|
+
? path.join(nginxDir, 'conf', 'nginx.conf')
|
|
27
|
+
: path.join(nginxDir, 'nginx.conf');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Returns the include directive line for this platform. */
|
|
31
|
+
function buildIncludeLine(nginxDir) {
|
|
32
|
+
if (isWindows) {
|
|
33
|
+
const fwd = nginxDir.replace(/\\/g, '/');
|
|
34
|
+
return ` include "${fwd}/conf/conf.d/*.conf";`;
|
|
35
|
+
}
|
|
36
|
+
return ` include ${nginxDir}/conf.d/*.conf;`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Ensures that nginx.conf contains an include directive for conf.d/*.conf.
|
|
41
|
+
* If the line is missing it is inserted just before the closing } of the http block.
|
|
42
|
+
* @param {string} nginxDir
|
|
43
|
+
*/
|
|
44
|
+
export async function ensureNginxInclude(nginxDir) {
|
|
45
|
+
const confPath = getNginxConfPath(nginxDir);
|
|
46
|
+
|
|
47
|
+
let content;
|
|
48
|
+
try {
|
|
49
|
+
content = await fs.readFile(confPath, 'utf8');
|
|
50
|
+
} catch {
|
|
51
|
+
return; // nginx.conf not present yet — skip silently
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Already has a conf.d include
|
|
55
|
+
if (/include\s+[^\n]*conf\.d[^\n]*\*\.conf/.test(content)) return;
|
|
56
|
+
|
|
57
|
+
const includeLine = buildIncludeLine(nginxDir);
|
|
58
|
+
|
|
59
|
+
// Insert before the last } in the file (closes the http block)
|
|
60
|
+
const lastBrace = content.lastIndexOf('}');
|
|
61
|
+
if (lastBrace === -1) return;
|
|
62
|
+
|
|
63
|
+
const newContent =
|
|
64
|
+
content.slice(0, lastBrace) +
|
|
65
|
+
`${includeLine}\n` +
|
|
66
|
+
content.slice(lastBrace);
|
|
67
|
+
|
|
68
|
+
await fs.writeFile(confPath, newContent, 'utf8');
|
|
69
|
+
}
|
|
70
|
+
|
|
14
71
|
// ─── DOMAIN DEFAULTS (v2 schema) ─────────────────────────────────────────────
|
|
15
72
|
|
|
16
73
|
export const DOMAIN_DEFAULTS = {
|
|
@@ -274,13 +331,17 @@ export function buildConf(domain, nginxDir, certbotDir) {
|
|
|
274
331
|
*/
|
|
275
332
|
export async function generateConf(domain) {
|
|
276
333
|
const { nginxDir, certbotDir } = loadConfig();
|
|
277
|
-
const
|
|
334
|
+
const confDir = getConfDDir(nginxDir);
|
|
335
|
+
const confPath = path.join(confDir, `${domain.name}.conf`);
|
|
278
336
|
const confContent = buildConf(domain, nginxDir, certbotDir);
|
|
279
337
|
|
|
280
338
|
// Ensure conf.d directory exists
|
|
281
|
-
await fs.mkdir(
|
|
339
|
+
await fs.mkdir(confDir, { recursive: true });
|
|
282
340
|
await fs.writeFile(confPath, confContent, 'utf8');
|
|
283
341
|
|
|
342
|
+
// Ensure nginx.conf includes conf.d
|
|
343
|
+
await ensureNginxInclude(nginxDir);
|
|
344
|
+
|
|
284
345
|
// Update domain with config file path
|
|
285
346
|
domain.configFile = confPath;
|
|
286
347
|
domain.updatedAt = new Date().toISOString();
|
|
@@ -2,6 +2,7 @@ import fs from 'fs/promises';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { run } from '../../core/shell.js';
|
|
4
4
|
import { loadConfig } from '../../core/config.js';
|
|
5
|
+
import { ensureNginxInclude } from '../../core/nginx-conf-generator.js';
|
|
5
6
|
|
|
6
7
|
// ─── Error Types ──────────────────────────────────────────────────────────────
|
|
7
8
|
|
|
@@ -23,6 +24,12 @@ function getNginxDir() {
|
|
|
23
24
|
return nginxDir;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
function getConfDDir(nginxDir) {
|
|
28
|
+
return process.platform === 'win32'
|
|
29
|
+
? path.join(nginxDir, 'conf', 'conf.d')
|
|
30
|
+
: path.join(nginxDir, 'conf.d');
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
/**
|
|
27
34
|
* Returns the PS-safe invocation string for the nginx binary.
|
|
28
35
|
* On Windows: checks PATH first, then falls back to configured nginxDir.
|
|
@@ -50,7 +57,7 @@ export function validateFilename(filename) {
|
|
|
50
57
|
throw new InvalidFilenameError('Invalid filename');
|
|
51
58
|
}
|
|
52
59
|
const nginxDir = getNginxDir();
|
|
53
|
-
const confDir =
|
|
60
|
+
const confDir = getConfDDir(nginxDir);
|
|
54
61
|
const resolved = path.resolve(path.join(confDir, filename));
|
|
55
62
|
if (!resolved.startsWith(path.resolve(confDir))) {
|
|
56
63
|
throw new InvalidFilenameError('Invalid filename');
|
|
@@ -135,6 +142,7 @@ export async function start() {
|
|
|
135
142
|
}
|
|
136
143
|
|
|
137
144
|
// Test config before starting
|
|
145
|
+
await ensureNginxInclude(nginxDir);
|
|
138
146
|
const testResult = await run(`${nginxExe} -t`);
|
|
139
147
|
if (!testResult.success) {
|
|
140
148
|
return { success: false, output: combineOutput(testResult) };
|
|
@@ -201,11 +209,13 @@ export async function stop() {
|
|
|
201
209
|
|
|
202
210
|
export async function test() {
|
|
203
211
|
const nginxExe = getNginxExe();
|
|
212
|
+
const nginxDir = getNginxDir();
|
|
204
213
|
const versionResult = await run(`${nginxExe} -v`);
|
|
205
214
|
if (!versionResult.success && !versionResult.stderr.includes('nginx/')) {
|
|
206
215
|
throw new NginxNotFoundError('nginx binary not found');
|
|
207
216
|
}
|
|
208
217
|
|
|
218
|
+
await ensureNginxInclude(nginxDir);
|
|
209
219
|
const result = await run(`${nginxExe} -t`);
|
|
210
220
|
return { success: result.success, output: combineOutput(result) };
|
|
211
221
|
}
|
|
@@ -214,7 +224,7 @@ export async function test() {
|
|
|
214
224
|
|
|
215
225
|
export async function listConfigs() {
|
|
216
226
|
const nginxDir = getNginxDir();
|
|
217
|
-
const confDir =
|
|
227
|
+
const confDir = getConfDDir(nginxDir);
|
|
218
228
|
let entries;
|
|
219
229
|
try {
|
|
220
230
|
entries = await fs.readdir(confDir);
|
|
@@ -228,7 +238,7 @@ export async function listConfigs() {
|
|
|
228
238
|
export async function getConfig(filename) {
|
|
229
239
|
validateFilename(filename);
|
|
230
240
|
const nginxDir = getNginxDir();
|
|
231
|
-
const confPath = path.join(nginxDir,
|
|
241
|
+
const confPath = path.join(getConfDDir(nginxDir), filename);
|
|
232
242
|
const content = await fs.readFile(confPath, 'utf8');
|
|
233
243
|
return { content };
|
|
234
244
|
}
|
|
@@ -236,7 +246,7 @@ export async function getConfig(filename) {
|
|
|
236
246
|
export async function saveConfig(filename, content) {
|
|
237
247
|
validateFilename(filename);
|
|
238
248
|
const nginxDir = getNginxDir();
|
|
239
|
-
const confPath = path.join(nginxDir,
|
|
249
|
+
const confPath = path.join(getConfDDir(nginxDir), filename);
|
|
240
250
|
const backupPath = confPath + '.bak';
|
|
241
251
|
|
|
242
252
|
// Backup only if the file already exists (it may be a new file)
|
|
@@ -252,6 +262,7 @@ export async function saveConfig(filename, content) {
|
|
|
252
262
|
await fs.writeFile(confPath, content, 'utf8');
|
|
253
263
|
|
|
254
264
|
const nginxExe = getNginxExe();
|
|
265
|
+
await ensureNginxInclude(nginxDir);
|
|
255
266
|
const result = await run(`${nginxExe} -t`);
|
|
256
267
|
if (!result.success) {
|
|
257
268
|
if (hasBackup) {
|
package/install.ps1
CHANGED
|
@@ -63,8 +63,54 @@ function Write-Err { param([string]$msg) Write-Host " ERROR $msg" -Foreg
|
|
|
63
63
|
function Write-Info { param([string]$msg) Write-Host " $msg" -ForegroundColor Gray }
|
|
64
64
|
|
|
65
65
|
function Refresh-Path {
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
# Read NVM vars from User scope, fall back to Machine scope
|
|
67
|
+
# (nvm-windows writes to Machine on system-wide installs)
|
|
68
|
+
$nvmHome = [System.Environment]::GetEnvironmentVariable('NVM_HOME', 'User')
|
|
69
|
+
if (-not $nvmHome) { $nvmHome = [System.Environment]::GetEnvironmentVariable('NVM_HOME', 'Machine') }
|
|
70
|
+
$nvmSymlink = [System.Environment]::GetEnvironmentVariable('NVM_SYMLINK', 'User')
|
|
71
|
+
if (-not $nvmSymlink) { $nvmSymlink = [System.Environment]::GetEnvironmentVariable('NVM_SYMLINK', 'Machine') }
|
|
72
|
+
|
|
73
|
+
if ($nvmHome) { $env:NVM_HOME = $nvmHome }
|
|
74
|
+
if ($nvmSymlink) { $env:NVM_SYMLINK = $nvmSymlink }
|
|
75
|
+
|
|
76
|
+
$raw = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' +
|
|
77
|
+
[System.Environment]::GetEnvironmentVariable('Path', 'User')
|
|
78
|
+
|
|
79
|
+
# Expand literal %NVM_HOME% / %NVM_SYMLINK% tokens nvm-windows may have written
|
|
80
|
+
if ($nvmHome) { $raw = $raw -ireplace [regex]::Escape('%NVM_HOME%'), $nvmHome }
|
|
81
|
+
if ($nvmSymlink) { $raw = $raw -ireplace [regex]::Escape('%NVM_SYMLINK%'), $nvmSymlink }
|
|
82
|
+
|
|
83
|
+
$env:Path = $raw
|
|
84
|
+
|
|
85
|
+
# Ensure NVM_SYMLINK is in PATH even if the registry entry was already absolute
|
|
86
|
+
if ($nvmSymlink -and (Test-Path $nvmSymlink -ErrorAction SilentlyContinue)) {
|
|
87
|
+
if ($env:Path -notlike "*$([regex]::Escape($nvmSymlink))*") {
|
|
88
|
+
$env:Path = "$nvmSymlink;$env:Path"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Find nvm.exe: checks PATH first, then known install locations
|
|
94
|
+
function Find-NvmExe {
|
|
95
|
+
try {
|
|
96
|
+
$f = (& where.exe nvm 2>$null)
|
|
97
|
+
if ($LASTEXITCODE -eq 0 -and $f) { return 'nvm' }
|
|
98
|
+
} catch {}
|
|
99
|
+
|
|
100
|
+
$candidates = @()
|
|
101
|
+
if ($env:NVM_HOME) { $candidates += Join-Path $env:NVM_HOME 'nvm.exe' }
|
|
102
|
+
if ($env:APPDATA) { $candidates += Join-Path $env:APPDATA 'nvm\nvm.exe' }
|
|
103
|
+
if ($env:ProgramData) { $candidates += Join-Path $env:ProgramData 'nvm\nvm.exe' }
|
|
104
|
+
$candidates += 'C:\ProgramData\nvm\nvm.exe'
|
|
105
|
+
|
|
106
|
+
foreach ($c in $candidates) {
|
|
107
|
+
if (Test-Path $c -ErrorAction SilentlyContinue) {
|
|
108
|
+
$dir = Split-Path $c -Parent
|
|
109
|
+
if ($env:Path -notlike "*$dir*") { $env:Path = "$dir;$env:Path" }
|
|
110
|
+
return $c
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return $null
|
|
68
114
|
}
|
|
69
115
|
|
|
70
116
|
function Get-NodeMajor {
|
|
@@ -204,18 +250,29 @@ function Select-NodeVersion {
|
|
|
204
250
|
}
|
|
205
251
|
|
|
206
252
|
# ─── Detect install mode ──────────────────────────────────────────────────────
|
|
207
|
-
#
|
|
208
|
-
#
|
|
253
|
+
# source = running from a git-cloned project directory -> npm install + npm link
|
|
254
|
+
# update = easy-devops already on PATH -> skip install steps
|
|
255
|
+
# npm = downloaded installer standalone -> npm install -g easy-devops
|
|
256
|
+
|
|
257
|
+
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
258
|
+
$packageJson = Join-Path $scriptDir 'package.json'
|
|
209
259
|
|
|
210
|
-
$
|
|
211
|
-
$
|
|
212
|
-
$existingCmd
|
|
260
|
+
$isSourceMode = $false
|
|
261
|
+
$isAlreadyInstalled = $false
|
|
262
|
+
$existingCmd = $null
|
|
213
263
|
|
|
264
|
+
# Source mode: script dir has this project's package.json
|
|
265
|
+
if (Test-Path $packageJson) {
|
|
266
|
+
try {
|
|
267
|
+
$pkg = Get-Content $packageJson -Raw | ConvertFrom-Json
|
|
268
|
+
if ($pkg.name -eq 'easy-devops') { $isSourceMode = $true }
|
|
269
|
+
} catch {}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
# Already installed: easy-devops on PATH
|
|
214
273
|
try {
|
|
215
274
|
$existingCmd = (& where.exe easy-devops 2>$null)
|
|
216
|
-
if ($LASTEXITCODE -eq 0 -and $existingCmd) {
|
|
217
|
-
$isSourceMode = $false
|
|
218
|
-
}
|
|
275
|
+
if ($LASTEXITCODE -eq 0 -and $existingCmd) { $isAlreadyInstalled = $true }
|
|
219
276
|
} catch {}
|
|
220
277
|
|
|
221
278
|
# ─── Banner ───────────────────────────────────────────────────────────────────
|
|
@@ -225,13 +282,14 @@ Write-Host " ==========================================" -ForegroundColor Cyan
|
|
|
225
282
|
Write-Host " Easy DevOps -- Windows Installer" -ForegroundColor Cyan
|
|
226
283
|
Write-Host " ==========================================" -ForegroundColor Cyan
|
|
227
284
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
Write-Host " Mode:
|
|
231
|
-
Write-Host "
|
|
285
|
+
Write-Host ""
|
|
286
|
+
if ($isAlreadyInstalled) {
|
|
287
|
+
Write-Host " Mode: update (easy-devops already installed at $existingCmd)" -ForegroundColor DarkGray
|
|
288
|
+
Write-Host " Node.js will still be managed; npm steps skipped." -ForegroundColor DarkGray
|
|
289
|
+
} elseif ($isSourceMode) {
|
|
290
|
+
Write-Host " Mode: source (project directory -- npm install + npm link)" -ForegroundColor DarkGray
|
|
232
291
|
} else {
|
|
233
|
-
Write-Host ""
|
|
234
|
-
Write-Host " Mode: source (installing from project directory)" -ForegroundColor DarkGray
|
|
292
|
+
Write-Host " Mode: npm (will run: npm install -g easy-devops)" -ForegroundColor DarkGray
|
|
235
293
|
}
|
|
236
294
|
|
|
237
295
|
# ─── Step 1: Detect system ───────────────────────────────────────────────────
|
|
@@ -307,7 +365,7 @@ if ($KeepNode) {
|
|
|
307
365
|
Write-Warn "Using fallback version $NODE_FALLBACK."
|
|
308
366
|
$script:ltsVersions = @([PSCustomObject]@{ version = "v$NODE_FALLBACK.0.0"; lts = "LTS" })
|
|
309
367
|
}
|
|
310
|
-
Add-Result "Node.js release list" $true "@($script:ltsVersions).Count LTS versions"
|
|
368
|
+
Add-Result "Node.js release list" $true "$(@($script:ltsVersions).Count) LTS versions fetched"
|
|
311
369
|
}
|
|
312
370
|
|
|
313
371
|
# ─── Step 3: Node.js version selection ───────────────────────────────────────
|
|
@@ -404,15 +462,35 @@ if ($NODE_ACTION -eq "keep") {
|
|
|
404
462
|
} else {
|
|
405
463
|
Write-Step "Installing nvm-windows"
|
|
406
464
|
|
|
407
|
-
# Check if nvm-windows is already present
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
$
|
|
465
|
+
# Check if nvm-windows is already present (check PATH + known locations)
|
|
466
|
+
Refresh-Path
|
|
467
|
+
$nvmExeCheck = Find-NvmExe
|
|
468
|
+
if ($nvmExeCheck) {
|
|
469
|
+
try {
|
|
470
|
+
$nvmVersion = (& $nvmExeCheck version 2>$null).Trim()
|
|
471
|
+
if ($nvmVersion) {
|
|
472
|
+
Write-OK "nvm-windows $nvmVersion already installed"
|
|
473
|
+
Add-Result "nvm-windows" $true $nvmVersion
|
|
474
|
+
$nvmReady = $true
|
|
475
|
+
}
|
|
476
|
+
} catch {}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (-not $nvmReady) {
|
|
480
|
+
# After Refresh-Path, try locating nvm at known paths
|
|
481
|
+
Refresh-Path
|
|
482
|
+
$nvmExePath = Find-NvmExe
|
|
483
|
+
if ($nvmExePath) {
|
|
484
|
+
try {
|
|
485
|
+
$nvmVersion = (& $nvmExePath version 2>$null).Trim()
|
|
486
|
+
if ($nvmVersion) {
|
|
487
|
+
Write-OK "nvm-windows $nvmVersion already installed (found after PATH refresh)"
|
|
488
|
+
Add-Result "nvm-windows" $true $nvmVersion
|
|
489
|
+
$nvmReady = $true
|
|
490
|
+
}
|
|
491
|
+
} catch {}
|
|
414
492
|
}
|
|
415
|
-
}
|
|
493
|
+
}
|
|
416
494
|
|
|
417
495
|
if (-not $nvmReady) {
|
|
418
496
|
$skipNvm = $false
|
|
@@ -480,53 +558,60 @@ if ($NODE_ACTION -eq "keep") {
|
|
|
480
558
|
} else {
|
|
481
559
|
Write-Step "Installing Node.js via nvm"
|
|
482
560
|
|
|
483
|
-
if ($nvmReady -and -not $nodeOK) {
|
|
484
|
-
# No compatible Node.js: install chosen version
|
|
561
|
+
if ($nvmReady -and (-not $nodeOK -or $NODE_ACTION -eq "upgrade" -or $NODE_ACTION -eq "switch")) {
|
|
485
562
|
Write-Info "Installing Node.js $NODE_TARGET via nvm..."
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
if
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
$
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
Write-Warn "
|
|
500
|
-
Add-Result "Node.js install" $false "
|
|
563
|
+
$nvmExe = Find-NvmExe
|
|
564
|
+
if (-not $nvmExe) {
|
|
565
|
+
Write-Warn "nvm not found -- open a new terminal and run: nvm install $NODE_TARGET"
|
|
566
|
+
Add-Result "Node.js install" $false "nvm not found"
|
|
567
|
+
} else {
|
|
568
|
+
# IMPORTANT: do NOT pipe nvm output (2>&1 | ...).
|
|
569
|
+
# nvm.exe checks if its stdout is a real console; piping makes it think
|
|
570
|
+
# it is not in a terminal and it shows a GUI dialog instead of running.
|
|
571
|
+
$nvmRunOK = $true
|
|
572
|
+
try {
|
|
573
|
+
& $nvmExe install $NODE_TARGET
|
|
574
|
+
& $nvmExe use $NODE_TARGET
|
|
575
|
+
} catch {
|
|
576
|
+
Write-Warn "nvm error: $_"
|
|
577
|
+
Add-Result "Node.js install" $false "nvm error -- see above"
|
|
578
|
+
$nvmRunOK = $false
|
|
501
579
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
580
|
+
|
|
581
|
+
if ($nvmRunOK) {
|
|
582
|
+
# PATH in this session may still be stale after nvm use.
|
|
583
|
+
# Read NVM_SYMLINK directly from registry and call node.exe by full path.
|
|
584
|
+
Refresh-Path
|
|
585
|
+
$raw = ""
|
|
586
|
+
try { $raw = (& node --version 2>&1).Trim() } catch {}
|
|
587
|
+
|
|
588
|
+
if (-not ($raw -match '^v')) {
|
|
589
|
+
$nvmSym = [System.Environment]::GetEnvironmentVariable('NVM_SYMLINK', 'Machine')
|
|
590
|
+
if (-not $nvmSym) { $nvmSym = [System.Environment]::GetEnvironmentVariable('NVM_SYMLINK', 'User') }
|
|
591
|
+
if ($nvmSym) {
|
|
592
|
+
$nodeExe = Join-Path $nvmSym 'node.exe'
|
|
593
|
+
if (Test-Path $nodeExe -ErrorAction SilentlyContinue) {
|
|
594
|
+
# Add to PATH so npm also works in the rest of this script
|
|
595
|
+
if ($env:Path -notlike "*$([regex]::Escape($nvmSym))*") {
|
|
596
|
+
$env:Path = "$nvmSym;$env:Path"
|
|
597
|
+
}
|
|
598
|
+
try { $raw = (& $nodeExe --version 2>&1).Trim() } catch {}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if ($raw -match '^v') {
|
|
604
|
+
$nodeVersion = $raw
|
|
605
|
+
Write-OK "Node.js $nodeVersion installed and active"
|
|
606
|
+
Add-Result "Node.js install" $true $nodeVersion
|
|
607
|
+
$nodeOK = $true
|
|
608
|
+
} else {
|
|
609
|
+
Write-Warn "Node.js installed but PATH not yet updated in this session"
|
|
610
|
+
Write-Warn "Open a new terminal and run: nvm use $NODE_TARGET"
|
|
611
|
+
Add-Result "Node.js install" $false "PATH refresh needed -- open a new terminal"
|
|
612
|
+
$nodeOK = $false
|
|
613
|
+
}
|
|
525
614
|
}
|
|
526
|
-
} catch {
|
|
527
|
-
Write-Warn "nvm error: $_ -- open a new terminal and run: nvm use $NODE_TARGET"
|
|
528
|
-
Add-Result "Node.js install" $false "Manual step needed"
|
|
529
|
-
$nodeOK = $false
|
|
530
615
|
}
|
|
531
616
|
} elseif (-not $nvmReady -and $nodeOK) {
|
|
532
617
|
# nvm not available but Node >= 18 already present: skip
|
|
@@ -539,82 +624,106 @@ if ($NODE_ACTION -eq "keep") {
|
|
|
539
624
|
}
|
|
540
625
|
}
|
|
541
626
|
|
|
542
|
-
# ───
|
|
627
|
+
# ─── Steps 6 + 7: Install Easy DevOps & register CLI ─────────────────────────
|
|
543
628
|
|
|
544
|
-
if (
|
|
545
|
-
#
|
|
546
|
-
Write-Step "Installing Easy DevOps
|
|
547
|
-
Write-OK "Skipped (
|
|
548
|
-
Add-Result "npm install" $true "Skipped (
|
|
629
|
+
if ($isAlreadyInstalled) {
|
|
630
|
+
# ── Already installed: skip both steps ──────────────────────────────────────
|
|
631
|
+
Write-Step "Installing Easy DevOps"
|
|
632
|
+
Write-OK "Skipped (easy-devops already installed at $existingCmd)"
|
|
633
|
+
Add-Result "npm install" $true "Skipped (already installed)"
|
|
549
634
|
|
|
550
635
|
Write-Step "Registering global command"
|
|
551
|
-
Write-OK "Skipped (
|
|
552
|
-
Add-Result "CLI registered" $true "Skipped (
|
|
553
|
-
} else {
|
|
554
|
-
|
|
555
|
-
# ─── Step 6: npm install ─────────────────────────────────────────────────────
|
|
556
|
-
|
|
557
|
-
Write-Step "Installing Easy DevOps dependencies"
|
|
636
|
+
Write-OK "Skipped (already registered)"
|
|
637
|
+
Add-Result "CLI registered" $true "Skipped (already installed)"
|
|
558
638
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
Write-
|
|
562
|
-
Write-Err "Run this installer from the Easy DevOps project root."
|
|
563
|
-
exit 1
|
|
564
|
-
}
|
|
639
|
+
} elseif ($isSourceMode) {
|
|
640
|
+
# ── Source mode: npm install in project dir + npm link ───────────────────────
|
|
641
|
+
Write-Step "Installing Easy DevOps dependencies"
|
|
565
642
|
|
|
566
|
-
if (-not $nodeOK) {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
} else {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
643
|
+
if (-not $nodeOK) {
|
|
644
|
+
Write-Warn "Skipping -- Node.js is not ready. Open a new terminal and re-run install.ps1."
|
|
645
|
+
Add-Result "npm install" $false "Skipped -- Node.js not ready"
|
|
646
|
+
} else {
|
|
647
|
+
try {
|
|
648
|
+
Push-Location $scriptDir
|
|
649
|
+
Write-Info "Running npm install..."
|
|
650
|
+
& npm install
|
|
651
|
+
if ($LASTEXITCODE -ne 0) {
|
|
652
|
+
Write-Err "npm install failed (exit code $LASTEXITCODE)"
|
|
653
|
+
Add-Result "npm install" $false "Exit code $LASTEXITCODE"
|
|
654
|
+
exit 1
|
|
655
|
+
}
|
|
656
|
+
Write-OK "All dependencies installed"
|
|
657
|
+
Add-Result "npm install" $true ""
|
|
658
|
+
} catch {
|
|
659
|
+
Write-Err "npm install error: $_"
|
|
660
|
+
Add-Result "npm install" $false "$_"
|
|
577
661
|
exit 1
|
|
662
|
+
} finally {
|
|
663
|
+
Pop-Location
|
|
578
664
|
}
|
|
579
|
-
Write-OK "All dependencies installed"
|
|
580
|
-
Add-Result "npm install" $true ""
|
|
581
|
-
} catch {
|
|
582
|
-
Write-Err "npm install error: $_"
|
|
583
|
-
Add-Result "npm install" $false "$_"
|
|
584
|
-
exit 1
|
|
585
|
-
} finally {
|
|
586
|
-
Pop-Location
|
|
587
665
|
}
|
|
588
|
-
}
|
|
589
666
|
|
|
590
|
-
|
|
667
|
+
Write-Step "Registering global command"
|
|
591
668
|
|
|
592
|
-
|
|
669
|
+
if (-not $nodeOK) {
|
|
670
|
+
Write-Warn "Skipping -- Node.js is not ready"
|
|
671
|
+
Add-Result "CLI registered" $false "Skipped -- Node.js not ready"
|
|
672
|
+
} else {
|
|
673
|
+
try {
|
|
674
|
+
Push-Location $scriptDir
|
|
675
|
+
& npm link
|
|
676
|
+
if ($LASTEXITCODE -ne 0) {
|
|
677
|
+
Write-Warn "npm link failed -- CLI won't be globally available"
|
|
678
|
+
Write-Warn "You can still run: node cli/index.js"
|
|
679
|
+
Add-Result "CLI registered" $false "Exit code $LASTEXITCODE"
|
|
680
|
+
} else {
|
|
681
|
+
Write-OK "easy-devops command linked globally"
|
|
682
|
+
Add-Result "CLI registered" $true ""
|
|
683
|
+
}
|
|
684
|
+
} catch {
|
|
685
|
+
Write-Warn "npm link failed: $_ -- run: node cli/index.js"
|
|
686
|
+
Add-Result "CLI registered" $false "$_"
|
|
687
|
+
} finally {
|
|
688
|
+
Pop-Location
|
|
689
|
+
}
|
|
690
|
+
}
|
|
593
691
|
|
|
594
|
-
if (-not $nodeOK) {
|
|
595
|
-
Write-Warn "Skipping -- Node.js is not ready"
|
|
596
|
-
Add-Result "CLI registered" $false "Skipped -- Node.js not ready"
|
|
597
692
|
} else {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
693
|
+
# ── npm global mode: npm install -g easy-devops ──────────────────────────────
|
|
694
|
+
Write-Step "Installing Easy DevOps"
|
|
695
|
+
|
|
696
|
+
if (-not $nodeOK) {
|
|
697
|
+
Write-Warn "Skipping -- Node.js is not ready."
|
|
698
|
+
Write-Warn "Open a new terminal and run: npm install -g easy-devops"
|
|
699
|
+
Add-Result "npm install" $false "Skipped -- Node.js not ready"
|
|
700
|
+
} else {
|
|
701
|
+
try {
|
|
702
|
+
Write-Info "Running npm install -g easy-devops..."
|
|
703
|
+
& npm install -g easy-devops
|
|
704
|
+
if ($LASTEXITCODE -ne 0) {
|
|
705
|
+
Write-Err "npm install -g easy-devops failed (exit code $LASTEXITCODE)"
|
|
706
|
+
Add-Result "npm install" $false "Exit code $LASTEXITCODE"
|
|
707
|
+
exit 1
|
|
708
|
+
}
|
|
709
|
+
Write-OK "easy-devops installed globally"
|
|
710
|
+
Add-Result "npm install" $true "npm install -g easy-devops"
|
|
711
|
+
} catch {
|
|
712
|
+
Write-Err "npm install -g error: $_"
|
|
713
|
+
Add-Result "npm install" $false "$_"
|
|
714
|
+
exit 1
|
|
608
715
|
}
|
|
609
|
-
} catch {
|
|
610
|
-
Write-Warn "npm link failed: $_ -- run: node cli/index.js"
|
|
611
|
-
Add-Result "CLI registered" $false "$_"
|
|
612
|
-
} finally {
|
|
613
|
-
Pop-Location
|
|
614
716
|
}
|
|
615
|
-
}
|
|
616
717
|
|
|
617
|
-
|
|
718
|
+
Write-Step "Registering global command"
|
|
719
|
+
if ($nodeOK) {
|
|
720
|
+
Write-OK "Registered via npm install -g"
|
|
721
|
+
Add-Result "CLI registered" $true "npm install -g"
|
|
722
|
+
} else {
|
|
723
|
+
Write-Warn "Skipped -- Node.js not ready"
|
|
724
|
+
Add-Result "CLI registered" $false "Skipped -- Node.js not ready"
|
|
725
|
+
}
|
|
726
|
+
}
|
|
618
727
|
|
|
619
728
|
# ─── Summary (mirrors install.sh summary block) ───────────────────────────────
|
|
620
729
|
|
package/install.sh
CHANGED
|
@@ -23,11 +23,35 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
23
23
|
EASYDEVOPS_DIR="${EASYDEVOPS_DIR:-$SCRIPT_DIR}"
|
|
24
24
|
|
|
25
25
|
# ---------------------------------------------------------------------------
|
|
26
|
-
# Source lib modules
|
|
26
|
+
# Source lib modules (auto-download if running standalone)
|
|
27
27
|
# ---------------------------------------------------------------------------
|
|
28
28
|
LIB_DIR="$SCRIPT_DIR/lib/installer"
|
|
29
|
+
_LIB_BASE="https://raw.githubusercontent.com/omar00050/Easy-DevOps/main/lib/installer"
|
|
30
|
+
_LIB_MODULES="progress.sh detect.sh node-versions.sh picker.sh nvm-bootstrap.sh"
|
|
31
|
+
|
|
32
|
+
if [ ! -f "$LIB_DIR/progress.sh" ]; then
|
|
33
|
+
printf 'Lib modules not found locally — downloading from GitHub...\n'
|
|
34
|
+
mkdir -p "$LIB_DIR"
|
|
35
|
+
_dl_ok=true
|
|
36
|
+
for _module in $_LIB_MODULES; do
|
|
37
|
+
if command -v curl >/dev/null 2>&1; then
|
|
38
|
+
curl -fsSL "$_LIB_BASE/$_module" -o "$LIB_DIR/$_module" 2>/dev/null || _dl_ok=false
|
|
39
|
+
elif command -v wget >/dev/null 2>&1; then
|
|
40
|
+
wget -q "$_LIB_BASE/$_module" -O "$LIB_DIR/$_module" 2>/dev/null || _dl_ok=false
|
|
41
|
+
else
|
|
42
|
+
printf 'Error: curl or wget is required to download installer modules.\n' >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
done
|
|
46
|
+
if [ "$_dl_ok" = "false" ]; then
|
|
47
|
+
printf 'Error: Failed to download one or more installer modules from GitHub.\n' >&2
|
|
48
|
+
printf 'Check your internet connection and try again.\n' >&2
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
printf 'Modules downloaded.\n\n'
|
|
52
|
+
fi
|
|
29
53
|
|
|
30
|
-
for _module in
|
|
54
|
+
for _module in $_LIB_MODULES; do
|
|
31
55
|
if [ ! -f "$LIB_DIR/$_module" ]; then
|
|
32
56
|
printf 'Error: Required module not found: %s/%s\n' "$LIB_DIR" "$_module" >&2
|
|
33
57
|
exit 1
|
|
@@ -147,19 +171,35 @@ while [ "$#" -gt 0 ]; do
|
|
|
147
171
|
done
|
|
148
172
|
|
|
149
173
|
# ---------------------------------------------------------------------------
|
|
150
|
-
#
|
|
174
|
+
# Install mode detection (mirrors install.ps1 3-mode logic)
|
|
151
175
|
#
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
#
|
|
176
|
+
# source : package.json with name=easy-devops found in script dir
|
|
177
|
+
# -> npm install + npm link
|
|
178
|
+
# update : easy-devops already on PATH
|
|
179
|
+
# -> skip install steps
|
|
180
|
+
# npm : standalone download, not in project dir
|
|
181
|
+
# -> npm install -g easy-devops
|
|
156
182
|
# ---------------------------------------------------------------------------
|
|
157
|
-
|
|
183
|
+
SOURCE_MODE=false
|
|
184
|
+
ALREADY_INSTALLED=false
|
|
158
185
|
EXISTING_CMD=""
|
|
159
186
|
|
|
187
|
+
# Source mode: script is running from the project directory
|
|
188
|
+
if [ -f "$SCRIPT_DIR/package.json" ]; then
|
|
189
|
+
if command -v node >/dev/null 2>&1; then
|
|
190
|
+
_pkg_name="$(node -e "try{process.stdout.write(require('$SCRIPT_DIR/package.json').name)}catch(e){}" 2>/dev/null || true)"
|
|
191
|
+
else
|
|
192
|
+
_pkg_name="$(grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' "$SCRIPT_DIR/package.json" 2>/dev/null | grep -o '"[^"]*"$' | tr -d '"' || true)"
|
|
193
|
+
fi
|
|
194
|
+
if [ "$_pkg_name" = "easy-devops" ]; then
|
|
195
|
+
SOURCE_MODE=true
|
|
196
|
+
fi
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
# Already installed: easy-devops command is on PATH
|
|
160
200
|
if command -v easy-devops >/dev/null 2>&1; then
|
|
161
201
|
EXISTING_CMD="$(command -v easy-devops)"
|
|
162
|
-
|
|
202
|
+
ALREADY_INSTALLED=true
|
|
163
203
|
fi
|
|
164
204
|
|
|
165
205
|
# ---------------------------------------------------------------------------
|
|
@@ -171,12 +211,15 @@ printf '║ Easy DevOps -- Bootstrap Installer ║\n'
|
|
|
171
211
|
printf '╚══════════════════════════════════════╝\n'
|
|
172
212
|
printf '\n'
|
|
173
213
|
|
|
174
|
-
if [ "$
|
|
175
|
-
printf ' Mode:
|
|
176
|
-
printf '
|
|
214
|
+
if [ "$ALREADY_INSTALLED" = "true" ]; then
|
|
215
|
+
printf ' Mode: update (easy-devops already installed at %s)\n' "$EXISTING_CMD"
|
|
216
|
+
printf ' Node.js will still be managed; npm steps skipped.\n'
|
|
217
|
+
printf '\n'
|
|
218
|
+
elif [ "$SOURCE_MODE" = "true" ]; then
|
|
219
|
+
printf ' Mode: source (project directory -- npm install + npm link)\n'
|
|
177
220
|
printf '\n'
|
|
178
221
|
else
|
|
179
|
-
printf ' Mode:
|
|
222
|
+
printf ' Mode: npm (will run: npm install -g easy-devops)\n'
|
|
180
223
|
printf '\n'
|
|
181
224
|
fi
|
|
182
225
|
|
|
@@ -369,40 +412,31 @@ else
|
|
|
369
412
|
fi
|
|
370
413
|
|
|
371
414
|
# ---------------------------------------------------------------------------
|
|
372
|
-
#
|
|
415
|
+
# Steps 6 + 7: Install Easy DevOps & register CLI
|
|
373
416
|
# ---------------------------------------------------------------------------
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
417
|
+
|
|
418
|
+
if [ "$ALREADY_INSTALLED" = "true" ]; then
|
|
419
|
+
# ── Already installed: skip both steps ──────────────────────────────────────
|
|
420
|
+
step_done "${STEPS[5]} (skipped -- easy-devops already installed)"
|
|
421
|
+
add_result "npm install" "ok" "Skipped (already installed)"
|
|
422
|
+
step_done "${STEPS[6]} (skipped -- already registered)"
|
|
423
|
+
add_result "CLI registered" "ok" "Skipped (already installed)"
|
|
424
|
+
|
|
425
|
+
elif [ "$SOURCE_MODE" = "true" ]; then
|
|
426
|
+
# ── Source mode: npm install in project dir + npm link ───────────────────────
|
|
378
427
|
step_running "${STEPS[5]}"
|
|
379
428
|
printf 'Running npm install in %s...\n' "$EASYDEVOPS_DIR"
|
|
380
|
-
if !
|
|
429
|
+
if ! (cd "$EASYDEVOPS_DIR" && npm install 2>&1); then
|
|
381
430
|
die "${STEPS[5]}" "npm install failed" \
|
|
382
431
|
"cd $EASYDEVOPS_DIR && npm install" \
|
|
383
432
|
"npm link"
|
|
384
433
|
fi
|
|
385
434
|
step_done "${STEPS[5]}"
|
|
386
435
|
add_result "npm install" "ok" ""
|
|
387
|
-
fi
|
|
388
436
|
|
|
389
|
-
# ---------------------------------------------------------------------------
|
|
390
|
-
# Step 7: npm link -- register global command
|
|
391
|
-
# ---------------------------------------------------------------------------
|
|
392
|
-
if [ "$PACKAGE_MODE" = "true" ]; then
|
|
393
|
-
step_done "${STEPS[6]} (skipped -- package mode)"
|
|
394
|
-
add_result "CLI registered" "ok" "Skipped (package mode)"
|
|
395
|
-
else
|
|
396
437
|
step_running "${STEPS[6]}"
|
|
397
438
|
printf 'Registering global command via npm link...\n'
|
|
398
|
-
|
|
399
|
-
if npm link --prefix "$EASYDEVOPS_DIR" 2>&1; then
|
|
400
|
-
_link_ok=true
|
|
401
|
-
elif (cd "$EASYDEVOPS_DIR" && npm link 2>&1); then
|
|
402
|
-
_link_ok=true
|
|
403
|
-
fi
|
|
404
|
-
|
|
405
|
-
if [ "$_link_ok" = "false" ]; then
|
|
439
|
+
if ! (cd "$EASYDEVOPS_DIR" && npm link 2>&1); then
|
|
406
440
|
die "${STEPS[6]}" "npm link failed -- could not register global command" \
|
|
407
441
|
"cd $EASYDEVOPS_DIR && npm link" \
|
|
408
442
|
"If permission denied, try: sudo npm link"
|
|
@@ -410,13 +444,26 @@ else
|
|
|
410
444
|
|
|
411
445
|
# Verify the command is on PATH
|
|
412
446
|
if ! command -v easy-devops >/dev/null 2>&1; then
|
|
413
|
-
printf 'Warning: easy-devops
|
|
414
|
-
printf 'You may need to open a new terminal or run:\n' >&2
|
|
447
|
+
printf 'Warning: easy-devops not yet on PATH. Open a new terminal or run:\n' >&2
|
|
415
448
|
printf ' export PATH="$(npm bin -g):$PATH"\n' >&2
|
|
416
449
|
fi
|
|
417
|
-
|
|
418
450
|
step_done "${STEPS[6]}"
|
|
419
451
|
add_result "CLI registered" "ok" ""
|
|
452
|
+
|
|
453
|
+
else
|
|
454
|
+
# ── npm global mode: npm install -g easy-devops ──────────────────────────────
|
|
455
|
+
step_running "${STEPS[5]}"
|
|
456
|
+
printf 'Running npm install -g easy-devops...\n'
|
|
457
|
+
if ! npm install -g easy-devops 2>&1; then
|
|
458
|
+
die "${STEPS[5]}" "npm install -g easy-devops failed" \
|
|
459
|
+
"npm install -g easy-devops" \
|
|
460
|
+
"If permission denied, try: sudo npm install -g easy-devops"
|
|
461
|
+
fi
|
|
462
|
+
step_done "${STEPS[5]}"
|
|
463
|
+
add_result "npm install" "ok" "npm install -g easy-devops"
|
|
464
|
+
|
|
465
|
+
step_done "${STEPS[6]} (registered via npm install -g)"
|
|
466
|
+
add_result "CLI registered" "ok" "npm install -g"
|
|
420
467
|
fi
|
|
421
468
|
|
|
422
469
|
# ---------------------------------------------------------------------------
|
package/package.json
CHANGED