buddy-workbench 0.1.39 → 0.1.41
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/package.json +12 -27
- package/pages/welcome/index.html +44 -9
- package/server/routes/package-upgrade.js +48 -3
- package/server/routes/updates.js +29 -0
- package/server/services/dev-configurations.js +81 -17
- package/ui/dist/assets/index-CW_VbnGx.js +547 -0
- package/ui/dist/assets/index-Cszvjr07.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/sw.js +2 -2
- package/ui/dist/assets/index-BWhse591.css +0 -1
- package/ui/dist/assets/index-D_0x1vPi.js +0 -547
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "buddy-workbench",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.41",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,6 +24,17 @@
|
|
|
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.41",
|
|
29
|
+
"name": "Workflow improvements and theme-aware demo pages",
|
|
30
|
+
"notes": [
|
|
31
|
+
"Added folder selection and system-folder opening for NPM Prefix and Cache settings, with loading feedback while system configuration loads.",
|
|
32
|
+
"Improved Package Upgrade tasks with repository-based dependency suggestions, npm version lookup with cascading release selection, multi-package-manager lockfile support, and clearer branch and task history workflows.",
|
|
33
|
+
"Added a Check all action to Branch Sync Status without automatically checking on page entry, and improved task detail viewing.",
|
|
34
|
+
"Added a one-click Static Pages test demo and made the Welcome demo page adapt to DevBuddy light and dark themes.",
|
|
35
|
+
"Improved Clipboard History keyboard focus behavior so date navigation remains available after interacting with category tabs."
|
|
36
|
+
]
|
|
37
|
+
},
|
|
27
38
|
{
|
|
28
39
|
"version": "0.1.33",
|
|
29
40
|
"name": "Developer configuration management",
|
|
@@ -32,32 +43,6 @@
|
|
|
32
43
|
"Added NVM detection, Node.js version installation, download mirror configuration, and current runtime details.",
|
|
33
44
|
"Added global NPM package search, version selection with publish dates, install confirmation, upgrade, and uninstall actions."
|
|
34
45
|
]
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
"version": "0.1.28",
|
|
38
|
-
"name": "Presentation planning and presenter tools",
|
|
39
|
-
"notes": [
|
|
40
|
-
"Added a Presentations library and Presentation Studio for planning, timing, and delivering presentations.",
|
|
41
|
-
"Added a configurable audience view with Markdown takeaways, themes, time boxes, branding, and full-screen support.",
|
|
42
|
-
"Added step-linked quick captures and Markdown export with clipboard copy and .md downloads."
|
|
43
|
-
]
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
"version": "0.1.27",
|
|
47
|
-
"name": "Data backups and settings refinements",
|
|
48
|
-
"notes": [
|
|
49
|
-
"Added data directory size statistics and secure .tar.gz backup import and export.",
|
|
50
|
-
"Refined the Static Pages action column header alignment."
|
|
51
|
-
]
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
"version": "0.1.26",
|
|
55
|
-
"name": "Light theme and update center",
|
|
56
|
-
"notes": [
|
|
57
|
-
"Added a switchable light theme with persistent appearance settings.",
|
|
58
|
-
"Refined the light theme window color, primary actions, and navigation styling.",
|
|
59
|
-
"Added an npm-based update indicator and in-app Change Log."
|
|
60
|
-
]
|
|
61
46
|
}
|
|
62
47
|
],
|
|
63
48
|
"dependencies": {
|
package/pages/welcome/index.html
CHANGED
|
@@ -4,14 +4,47 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Welcome - DevBuddy Static Pages</title>
|
|
7
|
+
<script>
|
|
8
|
+
(() => {
|
|
9
|
+
const applyTheme = (theme) => {
|
|
10
|
+
document.documentElement.dataset.theme = theme;
|
|
11
|
+
};
|
|
12
|
+
let savedTheme = '';
|
|
13
|
+
try { savedTheme = localStorage.getItem('devbuddy-theme') || ''; } catch {}
|
|
14
|
+
applyTheme(savedTheme === 'dark' || savedTheme === 'light'
|
|
15
|
+
? savedTheme
|
|
16
|
+
: (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'));
|
|
17
|
+
window.addEventListener('storage', (event) => {
|
|
18
|
+
if (event.key === 'devbuddy-theme' && (event.newValue === 'dark' || event.newValue === 'light')) {
|
|
19
|
+
applyTheme(event.newValue);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
})();
|
|
23
|
+
</script>
|
|
7
24
|
<style>
|
|
8
25
|
:root {
|
|
9
|
-
|
|
10
|
-
--
|
|
11
|
-
--
|
|
12
|
-
--text
|
|
13
|
-
--
|
|
14
|
-
--
|
|
26
|
+
color-scheme: light;
|
|
27
|
+
--bg: #f7f8fc;
|
|
28
|
+
--card-bg: #ffffff;
|
|
29
|
+
--text: #172033;
|
|
30
|
+
--text-muted: #667085;
|
|
31
|
+
--accent: #5b50e0;
|
|
32
|
+
--border: #e5e8f0;
|
|
33
|
+
--code-bg: #f0f1f8;
|
|
34
|
+
--code-text: #cf3038;
|
|
35
|
+
--shadow: 0 18px 44px rgba(31, 42, 68, .085);
|
|
36
|
+
}
|
|
37
|
+
:root[data-theme="dark"] {
|
|
38
|
+
color-scheme: dark;
|
|
39
|
+
--bg: #0f1117;
|
|
40
|
+
--card-bg: #171a22;
|
|
41
|
+
--text: #e7eaf0;
|
|
42
|
+
--text-muted: #929aaa;
|
|
43
|
+
--accent: #7669ee;
|
|
44
|
+
--border: #2a3040;
|
|
45
|
+
--code-bg: #101522;
|
|
46
|
+
--code-text: #ff8f9a;
|
|
47
|
+
--shadow: 0 18px 44px rgba(0, 0, 0, .28);
|
|
15
48
|
}
|
|
16
49
|
body {
|
|
17
50
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
@@ -24,16 +57,18 @@
|
|
|
24
57
|
margin: 0;
|
|
25
58
|
padding: 24px;
|
|
26
59
|
box-sizing: border-box;
|
|
60
|
+
transition: background-color .2s ease, color .2s ease;
|
|
27
61
|
}
|
|
28
62
|
.card {
|
|
29
63
|
background: var(--card-bg);
|
|
30
64
|
padding: 32px;
|
|
31
65
|
border-radius: 16px;
|
|
32
|
-
box-shadow:
|
|
66
|
+
box-shadow: var(--shadow);
|
|
33
67
|
border: 1px solid var(--border);
|
|
34
68
|
text-align: center;
|
|
35
69
|
max-width: 520px;
|
|
36
70
|
width: 100%;
|
|
71
|
+
transition: background-color .2s ease, border-color .2s ease, box-shadow .2s ease;
|
|
37
72
|
}
|
|
38
73
|
.icon {
|
|
39
74
|
font-size: 48px;
|
|
@@ -51,10 +86,10 @@
|
|
|
51
86
|
margin-bottom: 20px;
|
|
52
87
|
}
|
|
53
88
|
code {
|
|
54
|
-
background:
|
|
89
|
+
background: var(--code-bg);
|
|
55
90
|
padding: 4px 8px;
|
|
56
91
|
border-radius: 6px;
|
|
57
|
-
color:
|
|
92
|
+
color: var(--code-text);
|
|
58
93
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
59
94
|
font-size: 0.9em;
|
|
60
95
|
border: 1px solid var(--border);
|
|
@@ -29,6 +29,25 @@ function updateTask(taskId, updater) {
|
|
|
29
29
|
const next = updater(task); data.tasks = data.tasks.map((item) => item.id === taskId ? next : item); savePackageUpgradeData(data);
|
|
30
30
|
}
|
|
31
31
|
async function git(folder, args) { return (await exec('git', args, { cwd: folder, timeout: 10 * 60 * 1000, maxBuffer: 2 * 1024 * 1024 })).stdout.trim(); }
|
|
32
|
+
async function gitRefExists(folder, ref) {
|
|
33
|
+
try {
|
|
34
|
+
await exec('git', ['show-ref', '--verify', '--quiet', ref], { cwd: folder, timeout: 30 * 1000 });
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function updateDependencyLockfile(folder) {
|
|
41
|
+
const lockfile = existsSync(join(folder, 'pnpm-lock.yaml')) ? 'pnpm-lock.yaml' : existsSync(join(folder, 'yarn.lock')) ? 'yarn.lock' : 'package-lock.json';
|
|
42
|
+
const packageManager = lockfile === 'pnpm-lock.yaml' ? 'pnpm' : lockfile === 'yarn.lock' ? 'yarn' : 'npm';
|
|
43
|
+
const commands = packageManager === 'pnpm'
|
|
44
|
+
? ['pnpm', ['install', '--lockfile-only', '--ignore-scripts']]
|
|
45
|
+
: packageManager === 'yarn'
|
|
46
|
+
? ['yarn', ['install', '--ignore-scripts']]
|
|
47
|
+
: ['npm', ['install', '--legacy-peer-deps', '--package-lock-only', '--ignore-scripts']];
|
|
48
|
+
await exec(commands[0], commands[1], { cwd: folder, timeout: 20 * 60 * 1000, maxBuffer: 4 * 1024 * 1024 });
|
|
49
|
+
return lockfile;
|
|
50
|
+
}
|
|
32
51
|
async function runRepo(task, repo) {
|
|
33
52
|
const data = readPackageUpgradeData(); const folder = repoPath(data, repo); const name = repo.name || repoName(repo.url);
|
|
34
53
|
const result = { repoId: repo.id, name, status: 'running', message: '', prUrl: null };
|
|
@@ -37,7 +56,15 @@ async function runRepo(task, repo) {
|
|
|
37
56
|
mkdirSync(data.workspaceDir, { recursive: true });
|
|
38
57
|
if (!existsSync(join(folder, '.git'))) await exec('git', ['clone', repo.url, folder], { cwd: data.workspaceDir, timeout: 20 * 60 * 1000, maxBuffer: 2 * 1024 * 1024 });
|
|
39
58
|
await git(folder, ['fetch', 'origin', '--prune']);
|
|
40
|
-
|
|
59
|
+
const remoteTargetRef = `refs/remotes/origin/${task.targetBranch}`;
|
|
60
|
+
const localTargetRef = `refs/heads/${task.targetBranch}`;
|
|
61
|
+
const targetRef = await gitRefExists(folder, remoteTargetRef)
|
|
62
|
+
? `origin/${task.targetBranch}`
|
|
63
|
+
: await gitRefExists(folder, localTargetRef)
|
|
64
|
+
? task.targetBranch
|
|
65
|
+
: null;
|
|
66
|
+
if (!targetRef) throw new Error(`Target branch "${task.targetBranch}" was not found in ${name}. Check the target branch name and confirm it exists on origin.`);
|
|
67
|
+
await git(folder, ['checkout', '-B', task.sourceBranch, targetRef]);
|
|
41
68
|
const packageFile = join(folder, 'package.json');
|
|
42
69
|
if (!existsSync(packageFile)) throw new Error('package.json not found');
|
|
43
70
|
const pkg = JSON.parse(readFileSync(packageFile, 'utf8'));
|
|
@@ -46,8 +73,8 @@ async function runRepo(task, repo) {
|
|
|
46
73
|
if (!section) throw new Error(`Package ${task.packageName} is not in package.json`);
|
|
47
74
|
pkg[section][task.packageName] = task.version;
|
|
48
75
|
writeFileSync(packageFile, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
49
|
-
|
|
50
|
-
await git(folder, ['add', 'package.json',
|
|
76
|
+
const lockfile = await updateDependencyLockfile(folder);
|
|
77
|
+
await git(folder, ['add', 'package.json', lockfile]);
|
|
51
78
|
await git(folder, ['commit', '-m', task.commitMessage]);
|
|
52
79
|
await git(folder, ['push', 'origin', task.sourceBranch]);
|
|
53
80
|
const parsed = parseBitbucket(repo.url); const settings = readSettings();
|
|
@@ -70,6 +97,24 @@ async function execute(task) {
|
|
|
70
97
|
router.get('/config', (_req, res) => { const data = readPackageUpgradeData(); res.json({ workspaceDir: data.workspaceDir, repos: data.repos.map((repo) => ({ ...repo, cloned: existsSync(join(repoPath(data, repo), '.git')) })), tasks: data.tasks }); });
|
|
71
98
|
router.put('/config', (req, res) => { const data = readPackageUpgradeData(); data.workspaceDir = String(req.body?.workspaceDir || '').trim(); if (!data.workspaceDir) return res.status(400).json({ error: 'Workspace directory is required.' }); savePackageUpgradeData(data); res.json({ workspaceDir: data.workspaceDir, repos: data.repos }); });
|
|
72
99
|
router.post('/repos', (req, res) => { const url = String(req.body?.url || '').trim(); if (!url) return res.status(400).json({ error: 'Repository URL is required.' }); const data = readPackageUpgradeData(); const repo = { id: `repo-${Date.now()}`, url, name: String(req.body?.name || repoName(url)), directory: String(req.body?.directory || repoName(url)) }; data.repos.push(repo); savePackageUpgradeData(data); res.status(201).json(repo); });
|
|
100
|
+
router.post('/repos/packages', (req, res) => {
|
|
101
|
+
const data = readPackageUpgradeData();
|
|
102
|
+
const repoIds = Array.isArray(req.body?.repoIds) ? req.body.repoIds : [];
|
|
103
|
+
const packageNames = new Set();
|
|
104
|
+
repoIds.forEach((repoId) => {
|
|
105
|
+
const repo = data.repos.find((item) => item.id === repoId);
|
|
106
|
+
if (!repo) return;
|
|
107
|
+
const packageFile = join(repoPath(data, repo), 'package.json');
|
|
108
|
+
if (!existsSync(packageFile)) return;
|
|
109
|
+
try {
|
|
110
|
+
const packageJson = JSON.parse(readFileSync(packageFile, 'utf8'));
|
|
111
|
+
['dependencies', 'devDependencies'].forEach((section) => {
|
|
112
|
+
Object.keys(packageJson[section] || {}).forEach((name) => packageNames.add(name));
|
|
113
|
+
});
|
|
114
|
+
} catch {}
|
|
115
|
+
});
|
|
116
|
+
res.json([...packageNames].sort((a, b) => a.localeCompare(b)));
|
|
117
|
+
});
|
|
73
118
|
router.post('/repos/:id/clone', async (req, res) => { const data = readPackageUpgradeData(); const repo = data.repos.find((item) => item.id === req.params.id); if (!repo) return res.status(404).json({ error: 'Repository not found.' }); if (!data.workspaceDir) return res.status(400).json({ error: 'Configure a workspace directory first.' }); const folder = repoPath(data, repo); try { mkdirSync(data.workspaceDir, { recursive: true }); if (!existsSync(join(folder, '.git'))) await exec('git', ['clone', repo.url, folder], { cwd: data.workspaceDir, timeout: 20 * 60 * 1000, maxBuffer: 2 * 1024 * 1024 }); res.json({ ...repo, cloned: true }); } catch (error) { res.status(400).json({ error: error.stderr || error.message }); } });
|
|
74
119
|
router.delete('/repos/:id', (req, res) => { const data = readPackageUpgradeData(); data.repos = data.repos.filter((r) => r.id !== req.params.id); savePackageUpgradeData(data); res.status(204).end(); });
|
|
75
120
|
router.post('/tasks', (req, res) => { const body = req.body || {}; const required = ['sourceBranch', 'targetBranch', 'packageName', 'version', 'commitMessage']; if (required.some((key) => !String(body[key] || '').trim()) || !Array.isArray(body.repoIds) || !body.repoIds.length) return res.status(400).json({ error: 'All fields and at least one repository are required.' }); const data = readPackageUpgradeData(); if (!data.workspaceDir) return res.status(400).json({ error: 'Configure a workspace directory first.' }); const repoIds = body.repoIds.filter((id) => data.repos.some((repo) => repo.id === id)); if (!repoIds.length) return res.status(400).json({ error: 'Select at least one configured repository.' }); const task = { id: `task-${Date.now()}`, ...Object.fromEntries(required.map((key) => [key, String(body[key]).trim()])), repoIds, repos: repoIds.map((repoId) => ({ repoId, status: 'queued' })), status: 'running', createdAt: new Date().toISOString() }; data.tasks.unshift(task); savePackageUpgradeData(data); void execute(task); res.status(201).json(task); });
|
package/server/routes/updates.js
CHANGED
|
@@ -14,6 +14,7 @@ const CACHE_TTL_MS = 15 * 60 * 1000;
|
|
|
14
14
|
|
|
15
15
|
let cachedResult = null;
|
|
16
16
|
let cachedAt = 0;
|
|
17
|
+
let selfUpdateRunning = false;
|
|
17
18
|
|
|
18
19
|
function versionParts(version) {
|
|
19
20
|
return String(version || '')
|
|
@@ -137,4 +138,32 @@ router.get('/', async (req, res) => {
|
|
|
137
138
|
return res.json(cachedResult);
|
|
138
139
|
});
|
|
139
140
|
|
|
141
|
+
router.post('/self-update', async (_req, res) => {
|
|
142
|
+
if (selfUpdateRunning) {
|
|
143
|
+
return res.status(409).json({ error: 'An update is already in progress.' });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
selfUpdateRunning = true;
|
|
147
|
+
try {
|
|
148
|
+
const { stdout } = await execFileAsync(
|
|
149
|
+
process.execPath,
|
|
150
|
+
[join(root, 'bin', 'devbuddy.js'), 'self-update'],
|
|
151
|
+
{
|
|
152
|
+
cwd: root,
|
|
153
|
+
timeout: 120000,
|
|
154
|
+
maxBuffer: 1024 * 1024,
|
|
155
|
+
windowsHide: true
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
cachedResult = null;
|
|
159
|
+
cachedAt = 0;
|
|
160
|
+
return res.json({ success: true, message: stdout.trim() || 'DevBuddy was updated successfully.' });
|
|
161
|
+
} catch (error) {
|
|
162
|
+
const details = error?.stderr?.trim() || error?.stdout?.trim() || error?.message;
|
|
163
|
+
return res.status(500).json({ error: details || 'DevBuddy could not be updated.' });
|
|
164
|
+
} finally {
|
|
165
|
+
selfUpdateRunning = false;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
140
169
|
export default router;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { basename, join } from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
@@ -8,15 +8,72 @@ const execFileAsync = promisify(execFile);
|
|
|
8
8
|
const NPM_KEYS = ['prefix', 'registry', 'strict-ssl', 'cache', 'proxy', 'https-proxy'];
|
|
9
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
10
|
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
11
|
+
function getEnrichedEnv() {
|
|
12
|
+
const home = homedir();
|
|
13
|
+
const extraPaths = [
|
|
14
|
+
join(home, '.nvm', 'current', 'bin'),
|
|
15
|
+
join(home, '.npm-global', 'bin'),
|
|
16
|
+
'/opt/homebrew/bin',
|
|
17
|
+
'/usr/local/bin',
|
|
18
|
+
'/usr/bin',
|
|
19
|
+
'/bin',
|
|
20
|
+
'/usr/sbin',
|
|
21
|
+
'/sbin'
|
|
22
|
+
];
|
|
23
|
+
const nvmDir = process.env.NVM_DIR || join(home, '.nvm');
|
|
24
|
+
if (existsSync(nvmDir)) {
|
|
25
|
+
try {
|
|
26
|
+
const versionsDir = join(nvmDir, 'versions', 'node');
|
|
27
|
+
if (existsSync(versionsDir)) {
|
|
28
|
+
for (const v of readdirSync(versionsDir)) {
|
|
29
|
+
extraPaths.unshift(join(versionsDir, v, 'bin'));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
const currentPath = process.env.PATH || '';
|
|
35
|
+
const mergedPath = [...new Set([...extraPaths, ...currentPath.split(':')])].filter(Boolean).join(':');
|
|
36
|
+
return { ...process.env, PATH: mergedPath };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseJsonOutput(text) {
|
|
40
|
+
const str = String(text || '').trim();
|
|
41
|
+
const objStart = str.indexOf('{');
|
|
42
|
+
const arrStart = str.indexOf('[');
|
|
43
|
+
let start = -1;
|
|
44
|
+
let end = -1;
|
|
45
|
+
if (objStart !== -1 && (arrStart === -1 || objStart < arrStart)) {
|
|
46
|
+
start = objStart;
|
|
47
|
+
end = str.lastIndexOf('}');
|
|
48
|
+
} else if (arrStart !== -1) {
|
|
49
|
+
start = arrStart;
|
|
50
|
+
end = str.lastIndexOf(']');
|
|
51
|
+
}
|
|
52
|
+
if (start !== -1 && end !== -1 && end > start) {
|
|
53
|
+
return JSON.parse(str.slice(start, end + 1));
|
|
54
|
+
}
|
|
55
|
+
return JSON.parse(str);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function run(command, args, timeout = 5000) {
|
|
59
|
+
try {
|
|
60
|
+
const { stdout } = await execFileAsync(command, args, { timeout, maxBuffer: 8 * 1024 * 1024, env: getEnrichedEnv() });
|
|
61
|
+
return stdout.trim();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (['node', 'npm'].includes(command)) {
|
|
64
|
+
try {
|
|
65
|
+
const cmdStr = `${command} ${args.map(shellQuote).join(' ')}`;
|
|
66
|
+
return await runNvm(cmdStr, timeout);
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
14
71
|
}
|
|
15
72
|
|
|
16
73
|
async function runNvm(script, timeout = 5000) {
|
|
17
74
|
for (const shell of ['zsh', 'bash']) {
|
|
18
75
|
try {
|
|
19
|
-
const { stdout } = await execFileAsync(shell, ['-ilc', `${NVM_LOAD}${script}`], { timeout, maxBuffer:
|
|
76
|
+
const { stdout } = await execFileAsync(shell, ['-ilc', `${NVM_LOAD}${script}`], { timeout, maxBuffer: 4 * 1024 * 1024, env: getEnrichedEnv() });
|
|
20
77
|
return stdout.trim();
|
|
21
78
|
} catch {}
|
|
22
79
|
}
|
|
@@ -61,14 +118,20 @@ async function readGitConfig(key) {
|
|
|
61
118
|
|
|
62
119
|
async function readGlobalNpmPackages() {
|
|
63
120
|
try {
|
|
64
|
-
const
|
|
65
|
-
const packageJson =
|
|
121
|
+
const stdout = await run('npm', ['ls', '--global', '--depth=0', '--json'], 10000);
|
|
122
|
+
const packageJson = parseJsonOutput(stdout);
|
|
66
123
|
return Object.entries(packageJson.dependencies || {}).map(([name, info]) => ({ name, version: info.version || '' }));
|
|
67
124
|
} catch (error) {
|
|
68
125
|
try {
|
|
69
|
-
const packageJson =
|
|
126
|
+
const packageJson = parseJsonOutput(error.stdout || '');
|
|
70
127
|
return Object.entries(packageJson.dependencies || {}).map(([name, info]) => ({ name, version: info.version || '' }));
|
|
71
|
-
} catch {
|
|
128
|
+
} catch {
|
|
129
|
+
try {
|
|
130
|
+
const output = await runNvm('npm ls --global --depth=0 --json', 10000);
|
|
131
|
+
const packageJson = parseJsonOutput(output);
|
|
132
|
+
return Object.entries(packageJson.dependencies || {}).map(([name, info]) => ({ name, version: info.version || '' }));
|
|
133
|
+
} catch { return []; }
|
|
134
|
+
}
|
|
72
135
|
}
|
|
73
136
|
}
|
|
74
137
|
|
|
@@ -86,15 +149,15 @@ export async function manageGlobalNpmPackage(action, packageName) {
|
|
|
86
149
|
? ['update', '--global', name]
|
|
87
150
|
: ['uninstall', '--global', name];
|
|
88
151
|
if (!['install', 'upgrade', 'uninstall'].includes(action)) throw new Error('Unsupported npm package action.');
|
|
89
|
-
await
|
|
152
|
+
await run('npm', args, 10 * 60 * 1000);
|
|
90
153
|
return readGlobalNpmPackages();
|
|
91
154
|
}
|
|
92
155
|
|
|
93
156
|
export async function searchNpmPackages(query) {
|
|
94
157
|
const term = String(query || '').trim();
|
|
95
158
|
if (!term || term.length > 100) throw new Error('Enter a package search term.');
|
|
96
|
-
const
|
|
97
|
-
const results =
|
|
159
|
+
const stdout = await run('npm', ['search', term, '--json', '--long'], 30 * 1000);
|
|
160
|
+
const results = parseJsonOutput(stdout);
|
|
98
161
|
return (Array.isArray(results) ? results : []).slice(0, 20).map((item) => ({
|
|
99
162
|
name: item.name,
|
|
100
163
|
version: item.version || '',
|
|
@@ -104,12 +167,12 @@ export async function searchNpmPackages(query) {
|
|
|
104
167
|
|
|
105
168
|
export async function getNpmPackageVersions(packageName) {
|
|
106
169
|
const name = validateNpmPackageName(packageName).replace(/@[^@]+$/, '');
|
|
107
|
-
const [
|
|
108
|
-
|
|
109
|
-
|
|
170
|
+
const [versionsOutput, timeOutput] = await Promise.all([
|
|
171
|
+
run('npm', ['view', name, 'versions', '--json'], 30 * 1000),
|
|
172
|
+
run('npm', ['view', name, 'time', '--json'], 30 * 1000)
|
|
110
173
|
]);
|
|
111
|
-
const versions =
|
|
112
|
-
const publishTimes =
|
|
174
|
+
const versions = parseJsonOutput(versionsOutput);
|
|
175
|
+
const publishTimes = parseJsonOutput(timeOutput) || {};
|
|
113
176
|
return (Array.isArray(versions) ? versions : versions ? [versions] : []).slice(-100).reverse().map((version) => ({ version, publishedAt: publishTimes[version] || '' }));
|
|
114
177
|
}
|
|
115
178
|
|
|
@@ -204,3 +267,4 @@ export async function installNvmVersion(version) {
|
|
|
204
267
|
await runNvm(`nvm install ${shellQuote(target)}`, 10 * 60 * 1000);
|
|
205
268
|
return readNvmConfiguration();
|
|
206
269
|
}
|
|
270
|
+
|