buddy-workbench 0.1.31 → 0.1.33
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 +14 -0
- package/bin/devbuddy.js +102 -11
- package/package.json +10 -1
- package/server/routes/settings.js +23 -0
- package/server/services/dev-configurations.js +206 -0
- package/ui/dist/assets/{index-CZxzAp4g.js → index-C0wrNU_v.js} +134 -134
- package/ui/dist/assets/index-Dmw-IAaC.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-jOfBtkeJ.css +0 -1
package/README.md
CHANGED
|
@@ -12,6 +12,20 @@ npm start
|
|
|
12
12
|
|
|
13
13
|
Open `http://localhost:3100`. To use another port, run `PORT=4000 npm start`. Startup attempts to stop any process currently using the selected port.
|
|
14
14
|
|
|
15
|
+
When using the `devbuddy` CLI, pass the port directly with `--port`:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
devbuddy start --port 4000
|
|
19
|
+
devbuddy restart --port 4000
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Update the globally installed CLI from npm:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
devbuddy self-update
|
|
26
|
+
devbuddy self-update 0.1.32
|
|
27
|
+
```
|
|
28
|
+
|
|
15
29
|
## Running on macOS
|
|
16
30
|
|
|
17
31
|
### Double-click Launcher (Terminal)
|
package/bin/devbuddy.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { execSync, spawn } from 'node:child_process';
|
|
3
|
+
import { execFileSync, execSync, spawn } from 'node:child_process';
|
|
4
4
|
import { existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
@@ -13,6 +13,85 @@ const dataDir = join(root, 'data');
|
|
|
13
13
|
const pidFile = join(dataDir, 'devbuddy.pid');
|
|
14
14
|
const logFile = join(dataDir, 'devbuddy.log');
|
|
15
15
|
const serverJs = join(root, 'server.js');
|
|
16
|
+
const packageJsonFile = join(root, 'package.json');
|
|
17
|
+
const defaultPort = 3100;
|
|
18
|
+
|
|
19
|
+
function parsePort(args) {
|
|
20
|
+
let port;
|
|
21
|
+
|
|
22
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
23
|
+
const arg = args[index];
|
|
24
|
+
if (arg === '--port') {
|
|
25
|
+
if (port !== undefined || args[index + 1] === undefined) {
|
|
26
|
+
throw new Error('Usage: --port <number>');
|
|
27
|
+
}
|
|
28
|
+
port = args[++index];
|
|
29
|
+
} else if (arg.startsWith('--port=')) {
|
|
30
|
+
if (port !== undefined) throw new Error('Port was specified more than once.');
|
|
31
|
+
port = arg.slice('--port='.length);
|
|
32
|
+
} else {
|
|
33
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const value = port ?? process.env.PORT ?? defaultPort;
|
|
38
|
+
if (!/^\d+$/.test(String(value)) || Number(value) < 1 || Number(value) > 65535) {
|
|
39
|
+
throw new Error('Port must be a number between 1 and 65535.');
|
|
40
|
+
}
|
|
41
|
+
return Number(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function packageDetails() {
|
|
45
|
+
return JSON.parse(readFileSync(packageJsonFile, 'utf8'));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function npmCommand() {
|
|
49
|
+
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseUpdateVersion(args) {
|
|
53
|
+
if (args.length > 1) throw new Error('Usage: devbuddy self-update [version]');
|
|
54
|
+
if (!args[0]) return 'latest';
|
|
55
|
+
if (args[0].startsWith('-') || /\s/.test(args[0])) {
|
|
56
|
+
throw new Error('Version must be a package version, tag, or dist-tag.');
|
|
57
|
+
}
|
|
58
|
+
return args[0];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function latestPublishedVersion(packageName) {
|
|
62
|
+
try {
|
|
63
|
+
return execFileSync(npmCommand(), ['view', packageName, 'version'], {
|
|
64
|
+
cwd: root,
|
|
65
|
+
encoding: 'utf8',
|
|
66
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
67
|
+
}).trim();
|
|
68
|
+
} catch {
|
|
69
|
+
throw new Error(`Unable to query the latest ${packageName} version from npm.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function selfUpdate(args) {
|
|
74
|
+
const details = packageDetails();
|
|
75
|
+
const version = parseUpdateVersion(args);
|
|
76
|
+
const currentVersion = details.version;
|
|
77
|
+
const targetVersion = version === 'latest' ? latestPublishedVersion(details.name) : version;
|
|
78
|
+
|
|
79
|
+
if (version === 'latest' && targetVersion === currentVersion) {
|
|
80
|
+
console.log(`${details.name} is already up to date (${currentVersion}).`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(`Updating ${details.name} from ${currentVersion} to ${version}…`);
|
|
85
|
+
try {
|
|
86
|
+
execFileSync(npmCommand(), ['install', '--global', `${details.name}@${version}`], {
|
|
87
|
+
cwd: root,
|
|
88
|
+
stdio: 'inherit'
|
|
89
|
+
});
|
|
90
|
+
} catch {
|
|
91
|
+
throw new Error(`Unable to install ${details.name}@${version}. Check npm permissions and network access.`);
|
|
92
|
+
}
|
|
93
|
+
console.log(`Updated ${details.name} to ${targetVersion}.`);
|
|
94
|
+
}
|
|
16
95
|
|
|
17
96
|
function isProcessRunning(pid) {
|
|
18
97
|
try {
|
|
@@ -58,11 +137,11 @@ function sendSignal(pid, signal) {
|
|
|
58
137
|
}
|
|
59
138
|
}
|
|
60
139
|
|
|
61
|
-
async function start() {
|
|
140
|
+
async function start(port) {
|
|
62
141
|
const existingPid = getRunningPid();
|
|
63
142
|
if (existingPid) {
|
|
64
143
|
console.log(`DevBuddy is already running (PID ${existingPid}).`);
|
|
65
|
-
console.log(`URL: http://localhost:${
|
|
144
|
+
console.log(`URL: http://localhost:${port}`);
|
|
66
145
|
return;
|
|
67
146
|
}
|
|
68
147
|
|
|
@@ -75,7 +154,7 @@ async function start() {
|
|
|
75
154
|
detached: true,
|
|
76
155
|
stdio: ['ignore', out, err],
|
|
77
156
|
cwd: root,
|
|
78
|
-
env: { ...process.env }
|
|
157
|
+
env: { ...process.env, PORT: String(port) }
|
|
79
158
|
});
|
|
80
159
|
|
|
81
160
|
if (!child.pid) {
|
|
@@ -90,7 +169,6 @@ async function start() {
|
|
|
90
169
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
91
170
|
|
|
92
171
|
if (isProcessRunning(child.pid)) {
|
|
93
|
-
const port = process.env.PORT || 3100;
|
|
94
172
|
console.log(`DevBuddy started successfully (PID ${child.pid}).`);
|
|
95
173
|
console.log(`URL: http://localhost:${port}`);
|
|
96
174
|
console.log(`Logs: ${logFile}`);
|
|
@@ -133,10 +211,9 @@ async function stop() {
|
|
|
133
211
|
console.log('DevBuddy stopped.');
|
|
134
212
|
}
|
|
135
213
|
|
|
136
|
-
function status() {
|
|
214
|
+
function status(port) {
|
|
137
215
|
const pid = getRunningPid();
|
|
138
216
|
if (pid) {
|
|
139
|
-
const port = process.env.PORT || 3100;
|
|
140
217
|
console.log(`DevBuddy is running (PID ${pid}).`);
|
|
141
218
|
console.log(`URL: http://localhost:${port}`);
|
|
142
219
|
} else {
|
|
@@ -149,33 +226,47 @@ function help() {
|
|
|
149
226
|
DevBuddy CLI
|
|
150
227
|
|
|
151
228
|
Usage:
|
|
152
|
-
devbuddy <command>
|
|
229
|
+
devbuddy <command> [--port <number>]
|
|
153
230
|
|
|
154
231
|
Commands:
|
|
155
232
|
start Start DevBuddy in the background
|
|
156
233
|
stop Stop the running DevBuddy instance
|
|
157
234
|
status Check the status of DevBuddy
|
|
158
235
|
restart Restart DevBuddy
|
|
236
|
+
self-update [version]
|
|
237
|
+
Update the globally installed DevBuddy CLI
|
|
159
238
|
help Display this help message
|
|
239
|
+
|
|
240
|
+
Options:
|
|
241
|
+
--port <number> Port to listen on (default: 3100)
|
|
242
|
+
|
|
243
|
+
Examples:
|
|
244
|
+
devbuddy start --port 4000
|
|
160
245
|
`);
|
|
161
246
|
}
|
|
162
247
|
|
|
163
248
|
async function main() {
|
|
164
249
|
const command = process.argv[2] || 'help';
|
|
250
|
+
const args = process.argv.slice(3);
|
|
165
251
|
|
|
166
252
|
switch (command.toLowerCase()) {
|
|
167
253
|
case 'start':
|
|
168
|
-
await start();
|
|
254
|
+
await start(parsePort(args));
|
|
169
255
|
break;
|
|
170
256
|
case 'stop':
|
|
171
257
|
await stop();
|
|
172
258
|
break;
|
|
173
259
|
case 'status':
|
|
174
|
-
status();
|
|
260
|
+
status(parsePort(args));
|
|
175
261
|
break;
|
|
176
262
|
case 'restart':
|
|
177
263
|
await stop();
|
|
178
|
-
await start();
|
|
264
|
+
await start(parsePort(args));
|
|
265
|
+
break;
|
|
266
|
+
case 'self-update':
|
|
267
|
+
case 'selfupdate':
|
|
268
|
+
case 'selfupgrade':
|
|
269
|
+
selfUpdate(args);
|
|
179
270
|
break;
|
|
180
271
|
case 'help':
|
|
181
272
|
case '-h':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "buddy-workbench",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
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",
|
|
@@ -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
|
+
}
|