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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buddy-workbench",
3
- "version": "0.1.39",
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": {
@@ -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
- --bg: #0f172a;
10
- --card-bg: #1e293b;
11
- --text: #f8fafc;
12
- --text-muted: #94a3b8;
13
- --accent: #38bdf8;
14
- --border: #334155;
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: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
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: rgba(15, 23, 42, 0.8);
89
+ background: var(--code-bg);
55
90
  padding: 4px 8px;
56
91
  border-radius: 6px;
57
- color: #f43f5e;
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
- await git(folder, ['checkout', '-B', task.sourceBranch, `origin/${task.targetBranch}`]);
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
- await exec('npm', ['install', '--legacy-peer-deps', '--package-lock-only'], { cwd: folder, timeout: 20 * 60 * 1000, maxBuffer: 4 * 1024 * 1024 });
50
- await git(folder, ['add', 'package.json', 'package-lock.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); });
@@ -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
- async function run(command, args) {
12
- const { stdout } = await execFileAsync(command, args, { timeout: 5000, maxBuffer: 1024 * 1024 });
13
- return stdout.trim();
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: 2 * 1024 * 1024 });
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 { stdout } = await execFileAsync('npm', ['ls', '--global', '--depth=0', '--json'], { timeout: 10000, maxBuffer: 4 * 1024 * 1024 });
65
- const packageJson = JSON.parse(stdout);
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 = JSON.parse(error.stdout || '');
126
+ const packageJson = parseJsonOutput(error.stdout || '');
70
127
  return Object.entries(packageJson.dependencies || {}).map(([name, info]) => ({ name, version: info.version || '' }));
71
- } catch { return []; }
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 execFileAsync('npm', args, { timeout: 10 * 60 * 1000, maxBuffer: 4 * 1024 * 1024 });
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 { stdout } = await execFileAsync('npm', ['search', term, '--json', '--long'], { timeout: 30 * 1000, maxBuffer: 8 * 1024 * 1024 });
97
- const results = JSON.parse(stdout);
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 [{ 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 })
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 = JSON.parse(versionsOutput);
112
- const publishTimes = JSON.parse(timeOutput) || {};
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
+