buddy-workbench 0.1.40 → 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.40",
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;