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 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:${process.env.PORT || 3100}`);
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.31",
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
+ }