buddy-workbench 0.1.32 → 0.1.34
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/bin/devbuddy.js +2 -1
- package/package.json +10 -1
- package/server/config.js +32 -15
- package/server/routes/settings.js +23 -0
- package/server/services/dev-configurations.js +206 -0
- package/ui/dist/assets/index-B0npoG1F.css +1 -0
- package/ui/dist/assets/{index-DfO9LrPe.js → index-MvbbPuFE.js} +134 -134
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-jOfBtkeJ.css +0 -1
package/bin/devbuddy.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { execFileSync, execSync, spawn } from 'node:child_process';
|
|
4
4
|
import { existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
5
6
|
import { dirname, join } from 'node:path';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
7
8
|
|
|
@@ -9,7 +10,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
9
10
|
const __dirname = dirname(__filename);
|
|
10
11
|
const root = dirname(__dirname);
|
|
11
12
|
|
|
12
|
-
const dataDir = join(
|
|
13
|
+
const dataDir = join(homedir(), '.devbuddy', 'data');
|
|
13
14
|
const pidFile = join(dataDir, 'devbuddy.pid');
|
|
14
15
|
const logFile = join(dataDir, 'devbuddy.log');
|
|
15
16
|
const serverJs = join(root, 'server.js');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "buddy-workbench",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.34",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,6 +24,15 @@
|
|
|
24
24
|
"version": "node -e \"const v=process.env.npm_package_version; const fs=require('fs'); ['ui/package.json', 'ui/package-lock.json'].forEach(p=>{if(fs.existsSync(p)){const j=JSON.parse(fs.readFileSync(p)); j.version=v; if(j.packages&&j.packages['']){j.packages[''].version=v;} fs.writeFileSync(p, JSON.stringify(j,null,2)+'\\n');}});\" && git add ui/package.json ui/package-lock.json"
|
|
25
25
|
},
|
|
26
26
|
"devbuddyChangelog": [
|
|
27
|
+
{
|
|
28
|
+
"version": "0.1.33",
|
|
29
|
+
"name": "Developer configuration management",
|
|
30
|
+
"notes": [
|
|
31
|
+
"Added system-level NPM, Git, and NVM configuration management.",
|
|
32
|
+
"Added NVM detection, Node.js version installation, download mirror configuration, and current runtime details.",
|
|
33
|
+
"Added global NPM package search, version selection with publish dates, install confirmation, upgrade, and uninstall actions."
|
|
34
|
+
]
|
|
35
|
+
},
|
|
27
36
|
{
|
|
28
37
|
"version": "0.1.28",
|
|
29
38
|
"name": "Presentation planning and presenter tools",
|
package/server/config.js
CHANGED
|
@@ -1,24 +1,41 @@
|
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
1
3
|
import { dirname, join } from 'node:path';
|
|
2
4
|
import { fileURLToPath } from 'node:url';
|
|
3
5
|
|
|
4
6
|
export const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
7
|
+
const legacyDataDir = join(root, 'data');
|
|
8
|
+
const userDataDir = join(homedir(), '.devbuddy', 'data');
|
|
9
|
+
|
|
10
|
+
function migrateLegacyData() {
|
|
11
|
+
mkdirSync(userDataDir, { recursive: true, mode: 0o700 });
|
|
12
|
+
if (!existsSync(legacyDataDir) || legacyDataDir === userDataDir) return;
|
|
13
|
+
for (const entry of readdirSync(legacyDataDir)) {
|
|
14
|
+
const source = join(legacyDataDir, entry);
|
|
15
|
+
const target = join(userDataDir, entry);
|
|
16
|
+
if (!existsSync(target)) cpSync(source, target, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
migrateLegacyData();
|
|
21
|
+
|
|
5
22
|
export const paths = {
|
|
6
|
-
dataDir:
|
|
7
|
-
launchers: join(
|
|
8
|
-
portHistory: join(
|
|
9
|
-
groupTasks: join(
|
|
10
|
-
settings: join(
|
|
11
|
-
jiraFilters: join(
|
|
12
|
-
todos: join(
|
|
13
|
-
staticPages: join(
|
|
14
|
-
errors: join(
|
|
15
|
-
postman: join(
|
|
16
|
-
branchSync: join(
|
|
17
|
-
presentations: join(
|
|
18
|
-
shutdownLog: join(
|
|
19
|
-
clipboardDir: join(
|
|
23
|
+
dataDir: userDataDir,
|
|
24
|
+
launchers: join(userDataDir, 'launchers.json'),
|
|
25
|
+
portHistory: join(userDataDir, 'port-history.json'),
|
|
26
|
+
groupTasks: join(userDataDir, 'group-tasks.json'),
|
|
27
|
+
settings: join(userDataDir, 'settings.json'),
|
|
28
|
+
jiraFilters: join(userDataDir, 'jira-filters.json'),
|
|
29
|
+
todos: join(userDataDir, 'todos.json'),
|
|
30
|
+
staticPages: join(userDataDir, 'static-pages.json'),
|
|
31
|
+
errors: join(userDataDir, 'errors.json'),
|
|
32
|
+
postman: join(userDataDir, 'postman.json'),
|
|
33
|
+
branchSync: join(userDataDir, 'branch-sync.json'),
|
|
34
|
+
presentations: join(userDataDir, 'presentations.json'),
|
|
35
|
+
shutdownLog: join(userDataDir, 'shutdown.log'),
|
|
36
|
+
clipboardDir: join(userDataDir, 'clipboard'),
|
|
20
37
|
plugins: join(root, 'plugins'),
|
|
21
38
|
pages: join(root, 'pages'),
|
|
22
|
-
dataPages: join(
|
|
39
|
+
dataPages: join(userDataDir, 'pages'),
|
|
23
40
|
ui: join(root, 'ui', 'dist')
|
|
24
41
|
};
|
|
@@ -2,12 +2,35 @@ import { existsSync, mkdirSync } from 'node:fs';
|
|
|
2
2
|
import { Router } from 'express';
|
|
3
3
|
import { paths } from '../config.js';
|
|
4
4
|
import { saveAccessToken, saveClipboardDeduplicateMinutes, saveClipboardEnabled, saveClipboardImageEnabled, saveDefaultBrowser, saveDefaultEditor, saveDomain, saveJiraIssuePrefix, saveTheme, settingsStatus } from '../repositories/settings.js';
|
|
5
|
+
import { getNpmPackageVersions, installNvmVersion, manageGlobalNpmPackage, readDevConfigurations, readNvmConfiguration, saveDevConfigurations, searchNpmPackages } from '../services/dev-configurations.js';
|
|
5
6
|
import { openInSystemBrowser, openInSystemEditor, openInSystemFolder } from '../services/browser.js';
|
|
6
7
|
import { selectDirectory } from '../services/dialog.js';
|
|
7
8
|
|
|
8
9
|
const router = Router();
|
|
9
10
|
|
|
10
11
|
router.get('/', (_req, res) => res.json(settingsStatus()));
|
|
12
|
+
router.get('/dev-configurations', async (_req, res) => {
|
|
13
|
+
try { res.json({ ...(await readDevConfigurations()), nvm: await readNvmConfiguration() }); } catch (error) { res.status(500).json({ error: error.message }); }
|
|
14
|
+
});
|
|
15
|
+
router.put('/dev-configurations', async (req, res) => {
|
|
16
|
+
const { npm, git } = req.body || {};
|
|
17
|
+
if (!npm || typeof npm !== 'object' || !git || typeof git !== 'object') {
|
|
18
|
+
return res.status(400).json({ error: 'NPM and GIT configurations are required.' });
|
|
19
|
+
}
|
|
20
|
+
try { res.json({ ...(await saveDevConfigurations({ npm, git, nvm: req.body.nvm })), nvm: await readNvmConfiguration() }); } catch (error) { res.status(500).json({ error: error.message }); }
|
|
21
|
+
});
|
|
22
|
+
router.post('/dev-configurations/nvm/install', async (req, res) => {
|
|
23
|
+
try { res.json(await installNvmVersion(req.body?.version)); } catch (error) { res.status(400).json({ error: error.message }); }
|
|
24
|
+
});
|
|
25
|
+
router.post('/dev-configurations/npm/search', async (req, res) => {
|
|
26
|
+
try { res.json(await searchNpmPackages(req.body?.query)); } catch (error) { res.status(400).json({ error: error.message }); }
|
|
27
|
+
});
|
|
28
|
+
router.post('/dev-configurations/npm/versions', async (req, res) => {
|
|
29
|
+
try { res.json(await getNpmPackageVersions(req.body?.packageName)); } catch (error) { res.status(400).json({ error: error.message }); }
|
|
30
|
+
});
|
|
31
|
+
router.post('/dev-configurations/npm/:action', async (req, res) => {
|
|
32
|
+
try { res.json(await manageGlobalNpmPackage(req.params.action, req.body?.packageName)); } catch (error) { res.status(400).json({ error: error.message }); }
|
|
33
|
+
});
|
|
11
34
|
router.put('/domain', (req, res) => {
|
|
12
35
|
const { domain } = req.body || {};
|
|
13
36
|
if (typeof domain !== 'string') return res.status(400).json({ error: 'Domain must be a string.' });
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { basename, join } from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const NPM_KEYS = ['prefix', 'registry', 'strict-ssl', 'cache', 'proxy', 'https-proxy'];
|
|
9
|
+
const NVM_LOAD = 'if [ -s "$NVM_DIR/nvm.sh" ]; then . "$NVM_DIR/nvm.sh"; elif [ -s "$HOME/.nvm/nvm.sh" ]; then . "$HOME/.nvm/nvm.sh"; fi; ';
|
|
10
|
+
|
|
11
|
+
async function run(command, args) {
|
|
12
|
+
const { stdout } = await execFileAsync(command, args, { timeout: 5000, maxBuffer: 1024 * 1024 });
|
|
13
|
+
return stdout.trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function runNvm(script, timeout = 5000) {
|
|
17
|
+
for (const shell of ['zsh', 'bash']) {
|
|
18
|
+
try {
|
|
19
|
+
const { stdout } = await execFileAsync(shell, ['-ilc', `${NVM_LOAD}${script}`], { timeout, maxBuffer: 2 * 1024 * 1024 });
|
|
20
|
+
return stdout.trim();
|
|
21
|
+
} catch {}
|
|
22
|
+
}
|
|
23
|
+
throw new Error('NVM is not installed or could not be loaded from the shell.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseNvmVersions(output) {
|
|
27
|
+
return [...new Set(String(output).split('\n').map((line) => line.match(/v\d+(?:\.\d+){0,2}/)?.[0]).filter(Boolean))];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function readNvmConfiguration() {
|
|
31
|
+
try {
|
|
32
|
+
const output = await runNvm('if ! command -v nvm >/dev/null 2>&1; then exit 1; fi; printf "__VERSION__%s\\n" "$(nvm --version)"; printf "__CURRENT__%s\\n" "$(nvm current)"; printf "__DEFAULT__%s\\n" "$(nvm alias default 2>/dev/null)"; printf "__DIR__%s\\n" "${NVM_DIR:-$HOME/.nvm}"; printf "__MIRROR__%s\\n" "${NVM_NODEJS_ORG_MIRROR:-}"; printf "__LIST__\\n"; nvm ls --no-colors');
|
|
33
|
+
const value = (key) => output.match(new RegExp(`__${key}__(.*)`))?.[1]?.trim() || '';
|
|
34
|
+
const defaultLine = value('DEFAULT');
|
|
35
|
+
return {
|
|
36
|
+
installed: true,
|
|
37
|
+
version: value('VERSION'),
|
|
38
|
+
currentVersion: value('CURRENT'),
|
|
39
|
+
defaultVersion: defaultLine.match(/->\s+(?:v)?(\d+(?:\.\d+){0,2}|node|lts\/[\w.-]+)/)?.[1] || '',
|
|
40
|
+
nvmDir: value('DIR'),
|
|
41
|
+
nodeJsMirror: value('MIRROR'),
|
|
42
|
+
installedVersions: parseNvmVersions(output.split('__LIST__')[1] || '')
|
|
43
|
+
};
|
|
44
|
+
} catch {
|
|
45
|
+
return { installed: false, version: '', currentVersion: '', defaultVersion: '', nvmDir: '', nodeJsMirror: '', installedVersions: [] };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function readNpmConfig(key) {
|
|
50
|
+
try {
|
|
51
|
+
const value = await run('npm', ['config', 'get', key]);
|
|
52
|
+
return value === 'undefined' || value === 'null' ? '' : value;
|
|
53
|
+
} catch {
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function readGitConfig(key) {
|
|
59
|
+
try { return await run('git', ['config', '--global', '--get', key]); } catch { return ''; }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function readGlobalNpmPackages() {
|
|
63
|
+
try {
|
|
64
|
+
const { stdout } = await execFileAsync('npm', ['ls', '--global', '--depth=0', '--json'], { timeout: 10000, maxBuffer: 4 * 1024 * 1024 });
|
|
65
|
+
const packageJson = JSON.parse(stdout);
|
|
66
|
+
return Object.entries(packageJson.dependencies || {}).map(([name, info]) => ({ name, version: info.version || '' }));
|
|
67
|
+
} catch (error) {
|
|
68
|
+
try {
|
|
69
|
+
const packageJson = JSON.parse(error.stdout || '');
|
|
70
|
+
return Object.entries(packageJson.dependencies || {}).map(([name, info]) => ({ name, version: info.version || '' }));
|
|
71
|
+
} catch { return []; }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function validateNpmPackageName(packageName) {
|
|
76
|
+
const value = String(packageName || '').trim();
|
|
77
|
+
if (!/^(@[a-z0-9._~-]+\/)?[a-z0-9._~-]+(?:@[a-z0-9.*^~<>=| -]+)?$/i.test(value)) throw new Error('Invalid npm package name.');
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function manageGlobalNpmPackage(action, packageName) {
|
|
82
|
+
const name = validateNpmPackageName(packageName);
|
|
83
|
+
const args = action === 'install'
|
|
84
|
+
? ['install', '--global', name]
|
|
85
|
+
: action === 'upgrade'
|
|
86
|
+
? ['update', '--global', name]
|
|
87
|
+
: ['uninstall', '--global', name];
|
|
88
|
+
if (!['install', 'upgrade', 'uninstall'].includes(action)) throw new Error('Unsupported npm package action.');
|
|
89
|
+
await execFileAsync('npm', args, { timeout: 10 * 60 * 1000, maxBuffer: 4 * 1024 * 1024 });
|
|
90
|
+
return readGlobalNpmPackages();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function searchNpmPackages(query) {
|
|
94
|
+
const term = String(query || '').trim();
|
|
95
|
+
if (!term || term.length > 100) throw new Error('Enter a package search term.');
|
|
96
|
+
const { stdout } = await execFileAsync('npm', ['search', term, '--json', '--long'], { timeout: 30 * 1000, maxBuffer: 8 * 1024 * 1024 });
|
|
97
|
+
const results = JSON.parse(stdout);
|
|
98
|
+
return (Array.isArray(results) ? results : []).slice(0, 20).map((item) => ({
|
|
99
|
+
name: item.name,
|
|
100
|
+
version: item.version || '',
|
|
101
|
+
description: item.description || ''
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function getNpmPackageVersions(packageName) {
|
|
106
|
+
const name = validateNpmPackageName(packageName).replace(/@[^@]+$/, '');
|
|
107
|
+
const [{ stdout: versionsOutput }, { stdout: timeOutput }] = await Promise.all([
|
|
108
|
+
execFileAsync('npm', ['view', name, 'versions', '--json'], { timeout: 30 * 1000, maxBuffer: 4 * 1024 * 1024 }),
|
|
109
|
+
execFileAsync('npm', ['view', name, 'time', '--json'], { timeout: 30 * 1000, maxBuffer: 4 * 1024 * 1024 })
|
|
110
|
+
]);
|
|
111
|
+
const versions = JSON.parse(versionsOutput);
|
|
112
|
+
const publishTimes = JSON.parse(timeOutput) || {};
|
|
113
|
+
return (Array.isArray(versions) ? versions : versions ? [versions] : []).slice(-100).reverse().map((version) => ({ version, publishedAt: publishTimes[version] || '' }));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function sshKeyFromCommand(command) {
|
|
117
|
+
const match = String(command || '').match(/(?:^|\s)-i\s+(?:"([^"]+)"|'([^']+)'|(\S+))/);
|
|
118
|
+
return match ? (match[1] || match[2] || match[3]) : '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function readDevConfigurations() {
|
|
122
|
+
const values = await Promise.all(NPM_KEYS.map(readNpmConfig));
|
|
123
|
+
const [name, email, sshCommand, globalPackages, nodeVersion, npmVersion] = await Promise.all([readGitConfig('user.name'), readGitConfig('user.email'), readGitConfig('core.sshCommand'), readGlobalNpmPackages(), run('node', ['--version']).catch(() => ''), run('npm', ['--version']).catch(() => '')]);
|
|
124
|
+
const npm = Object.fromEntries(NPM_KEYS.map((key, index) => [key, values[index]]));
|
|
125
|
+
return {
|
|
126
|
+
npm: {
|
|
127
|
+
prefix: npm.prefix,
|
|
128
|
+
registry: npm.registry || 'https://registry.npmjs.org/',
|
|
129
|
+
strictSsl: npm['strict-ssl'] !== 'false',
|
|
130
|
+
cache: npm.cache,
|
|
131
|
+
proxy: npm.proxy,
|
|
132
|
+
httpsProxy: npm['https-proxy'],
|
|
133
|
+
globalPackages,
|
|
134
|
+
nodeVersion,
|
|
135
|
+
npmVersion
|
|
136
|
+
},
|
|
137
|
+
git: { name, email, sshKey: sshKeyFromCommand(sshCommand) }
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function setNpmConfig(key, value) {
|
|
142
|
+
if (value === '') {
|
|
143
|
+
await run('npm', ['config', 'delete', key]);
|
|
144
|
+
} else {
|
|
145
|
+
await run('npm', ['config', 'set', key, value]);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function saveDevConfigurations(configurations = {}) {
|
|
150
|
+
const npm = configurations.npm || {};
|
|
151
|
+
const git = configurations.git || {};
|
|
152
|
+
const npmValues = {
|
|
153
|
+
prefix: String(npm.prefix || '').trim(),
|
|
154
|
+
registry: String(npm.registry || '').trim() || 'https://registry.npmjs.org/',
|
|
155
|
+
'strict-ssl': npm.strictSsl === false ? 'false' : 'true',
|
|
156
|
+
cache: String(npm.cache || '').trim(),
|
|
157
|
+
proxy: String(npm.proxy || '').trim(),
|
|
158
|
+
'https-proxy': String(npm.httpsProxy || '').trim()
|
|
159
|
+
};
|
|
160
|
+
for (const key of NPM_KEYS) await setNpmConfig(key, npmValues[key]);
|
|
161
|
+
const gitValues = { name: String(git.name || '').trim(), email: String(git.email || '').trim() };
|
|
162
|
+
for (const [key, value] of Object.entries(gitValues)) {
|
|
163
|
+
if (value) await run('git', ['config', '--global', `user.${key}`, value]);
|
|
164
|
+
else {
|
|
165
|
+
try { await run('git', ['config', '--global', '--unset', `user.${key}`]); } catch {}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const sshKey = String(git.sshKey || '').trim();
|
|
169
|
+
if (sshKey) {
|
|
170
|
+
const resolvedKey = sshKey.startsWith('~/') ? `${homedir()}/${sshKey.slice(2)}` : sshKey;
|
|
171
|
+
const quotedKey = `"${resolvedKey.replaceAll('"', '\\"')}"`;
|
|
172
|
+
await run('git', ['config', '--global', 'core.sshCommand', `ssh -i ${quotedKey} -o IdentitiesOnly=yes`]);
|
|
173
|
+
} else {
|
|
174
|
+
try { await run('git', ['config', '--global', '--unset', 'core.sshCommand']); } catch {}
|
|
175
|
+
}
|
|
176
|
+
const nvm = configurations.nvm || {};
|
|
177
|
+
const mirror = String(nvm.nodeJsMirror || '').trim().replace(/\/$/, '');
|
|
178
|
+
if (mirror && !/^https?:\/\/[^\s]+$/i.test(mirror)) throw new Error('Invalid NVM Node.js download mirror URL.');
|
|
179
|
+
updateNvmMirror(mirror);
|
|
180
|
+
if (String(nvm.defaultVersion || '').trim()) {
|
|
181
|
+
const version = String(nvm.defaultVersion).trim();
|
|
182
|
+
if (!/^(?:v?\d+(?:\.\d+){0,2}|node|lts\/[\w.-]+)$/.test(version)) throw new Error('Invalid NVM default version.');
|
|
183
|
+
await runNvm(`nvm alias default ${shellQuote(version)}`);
|
|
184
|
+
}
|
|
185
|
+
return readDevConfigurations();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function updateNvmMirror(mirror) {
|
|
189
|
+
const shellFile = basename(process.env.SHELL || '') === 'bash' ? '.bashrc' : '.zshrc';
|
|
190
|
+
const file = join(homedir(), shellFile);
|
|
191
|
+
const marker = '# DevBuddy NVM Node.js mirror';
|
|
192
|
+
let content = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
193
|
+
const block = new RegExp(`\\n?${marker}\\nexport NVM_NODEJS_ORG_MIRROR="[^"]*"\\n?`, 'g');
|
|
194
|
+
content = content.replace(block, '\n');
|
|
195
|
+
if (mirror) content = `${content.trimEnd()}\n\n${marker}\nexport NVM_NODEJS_ORG_MIRROR="${mirror.replaceAll('"', '\\\"')}"\n`;
|
|
196
|
+
writeFileSync(file, content, { mode: 0o600 });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function shellQuote(value) { return `'${String(value).replaceAll("'", "'\\''")}'`; }
|
|
200
|
+
|
|
201
|
+
export async function installNvmVersion(version) {
|
|
202
|
+
const target = String(version || '').trim();
|
|
203
|
+
if (!/^(?:v?\d+(?:\.\d+){0,2}|node|lts\/[\w.-]+|--lts)$/.test(target)) throw new Error('Invalid NVM version.');
|
|
204
|
+
await runNvm(`nvm install ${shellQuote(target)}`, 10 * 60 * 1000);
|
|
205
|
+
return readNvmConfiguration();
|
|
206
|
+
}
|