buddy-workbench 0.1.65 → 0.1.67

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.65",
3
+ "version": "0.1.67",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
package/server/config.js CHANGED
@@ -29,6 +29,7 @@ export const paths = {
29
29
  jiraTemplates: join(userDataDir, 'jira-templates.json'),
30
30
  jiraRecentlyCreated: join(userDataDir, 'jira-recently-created.json'),
31
31
  todos: join(userDataDir, 'todos.json'),
32
+ memos: join(userDataDir, 'memos.json'),
32
33
  staticPages: join(userDataDir, 'static-pages.json'),
33
34
  errors: join(userDataDir, 'errors.json'),
34
35
  postman: join(userDataDir, 'postman.json'),
@@ -0,0 +1,83 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { paths } from '../config.js';
4
+
5
+ export const starterMemos = [
6
+ {
7
+ id: 'starter-weekly-reset',
8
+ title: 'Weekly reset',
9
+ content: '# Weekly reset\n\nA small review to start the week with less noise.\n\n- Pick one meaningful outcome\n- Clear the two oldest loose ends\n- Leave room for unexpected work\n\n> Progress feels better when the next step is obvious.',
10
+ tags: ['planning', 'weekly'],
11
+ createdAt: '2026-08-10T08:00:00.000Z',
12
+ updatedAt: '2026-08-10T08:00:00.000Z'
13
+ },
14
+ {
15
+ id: 'starter-product-ideas',
16
+ title: 'Product ideas',
17
+ content: '## Product ideas\n\nA lightweight inbox for small observations could become a useful source of product direction.\n\nThe important part is making capture effortless and review intentional.',
18
+ tags: ['ideas', 'product'],
19
+ createdAt: '2026-08-09T10:30:00.000Z',
20
+ updatedAt: '2026-08-09T10:30:00.000Z'
21
+ },
22
+ {
23
+ id: 'starter-reading-notes',
24
+ title: 'Reading notes: The Creative Act',
25
+ content: '## The Creative Act\n\nThe best work often begins with attention rather than ambition.\n\nA useful prompt:\n\n> What is asking to be noticed here?',
26
+ tags: ['reading', 'reflection'],
27
+ createdAt: '2026-08-08T15:15:00.000Z',
28
+ updatedAt: '2026-08-08T15:15:00.000Z'
29
+ },
30
+ {
31
+ id: 'starter-code-pattern',
32
+ title: 'Small code pattern',
33
+ content: '## Keep async flows boring\n\nPrefer a short, visible sequence over clever abstractions when a request has only a few steps.\n\n```js\nconst data = await load();\nconst result = transform(data);\nawait save(result);\n```',
34
+ tags: ['code', 'notes'],
35
+ createdAt: '2026-08-07T09:45:00.000Z',
36
+ updatedAt: '2026-08-07T09:45:00.000Z'
37
+ },
38
+ {
39
+ id: 'starter-design-checklist',
40
+ title: 'Design checklist',
41
+ content: '## Before shipping a screen\n\n- Is the primary action obvious?\n- Does the empty state help someone begin?\n- What happens on a narrow window?\n- Can the interface explain itself without a tour?',
42
+ tags: ['design', 'checklist'],
43
+ createdAt: '2026-08-06T13:20:00.000Z',
44
+ updatedAt: '2026-08-06T13:20:00.000Z'
45
+ },
46
+ {
47
+ id: 'starter-quick-capture',
48
+ title: 'Quick capture',
49
+ content: 'A good note does not need to be complete. Leave enough context for your future self to pick it up again.',
50
+ tags: ['inbox'],
51
+ createdAt: '2026-08-05T17:10:00.000Z',
52
+ updatedAt: '2026-08-05T17:10:00.000Z'
53
+ },
54
+ {
55
+ id: 'starter-weekend-list',
56
+ title: 'Weekend list',
57
+ content: '- Walk somewhere unfamiliar\n- Cook one new dish\n- Read without taking notes\n- Leave Sunday evening unplanned',
58
+ tags: ['personal', 'weekend'],
59
+ createdAt: '2026-08-04T11:00:00.000Z',
60
+ updatedAt: '2026-08-04T11:00:00.000Z'
61
+ },
62
+ {
63
+ id: 'starter-rich-markdown',
64
+ title: 'Markdown field guide',
65
+ content: '## Useful Markdown\n\nLinks, images, and tables make notes easier to revisit.\n\n| Syntax | Use case |\n| --- | --- |\n| `[label](url)` | Link to a reference |\n| `![alt](url)` | Add an image |\n| `| cell | cell |` | Compare details |\n\nRead the [Markdown Guide](https://www.markdownguide.org/basic-syntax/) for more patterns.\n\n![A quiet desk with a notebook](https://images.unsplash.com/photo-1499750310107-5fef28a66643?auto=format&fit=crop&w=1200&q=80)',
66
+ tags: ['markdown', 'reference'],
67
+ createdAt: '2026-08-03T09:00:00.000Z',
68
+ updatedAt: '2026-08-03T09:00:00.000Z'
69
+ }
70
+ ];
71
+
72
+ export function listMemos() {
73
+ try {
74
+ return existsSync(paths.memos) ? JSON.parse(readFileSync(paths.memos, 'utf8')) : [];
75
+ } catch {
76
+ return [];
77
+ }
78
+ }
79
+
80
+ export function saveMemos(memos) {
81
+ mkdirSync(dirname(paths.memos), { recursive: true });
82
+ writeFileSync(paths.memos, JSON.stringify(memos, null, 2));
83
+ }
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
3
  import { paths } from '../config.js';
4
4
 
5
- const defaults = { workspaceDir: '', repos: [], tasks: [] };
5
+ const defaults = { workspaceDir: '', repos: [], tasks: [], dependencyRepos: [] };
6
6
  export function readPackageUpgradeData() {
7
7
  try { return existsSync(paths.packageUpgrade) ? { ...defaults, ...JSON.parse(readFileSync(paths.packageUpgrade, 'utf8')) } : { ...defaults }; }
8
8
  catch { return { ...defaults }; }
@@ -0,0 +1,55 @@
1
+ import { Router } from 'express';
2
+ import { listMemos, saveMemos, starterMemos } from '../repositories/memos.js';
3
+
4
+ const router = Router();
5
+
6
+ function validMemo(body) {
7
+ const title = String(body?.title || '').trim();
8
+ const content = String(body?.content || '');
9
+ const tags = Array.isArray(body?.tags)
10
+ ? [...new Set(body.tags.map((tag) => String(tag).trim().toLowerCase()).filter(Boolean))].slice(0, 12)
11
+ : String(body?.tags || '').split(',').map((tag) => tag.trim().toLowerCase()).filter(Boolean).slice(0, 12);
12
+ if (!title && !content.trim()) return null;
13
+ return { title: title || 'Untitled memo', content, tags };
14
+ }
15
+
16
+ router.get('/', (_req, res) => {
17
+ const starterIds = new Set(starterMemos.map((memo) => memo.id));
18
+ const memos = listMemos();
19
+ const cleanedMemos = memos.filter((memo) => !starterIds.has(memo.id));
20
+ if (cleanedMemos.length !== memos.length) saveMemos(cleanedMemos);
21
+ res.json(cleanedMemos);
22
+ });
23
+
24
+ router.post('/', (req, res) => {
25
+ const data = validMemo(req.body);
26
+ if (!data) return res.status(400).json({ error: 'Memo content is required.' });
27
+ const now = new Date().toISOString();
28
+ const memo = { id: crypto.randomUUID(), ...data, createdAt: now, updatedAt: now };
29
+ const memos = listMemos();
30
+ memos.unshift(memo);
31
+ saveMemos(memos);
32
+ res.status(201).json(memo);
33
+ });
34
+
35
+ router.put('/:id', (req, res) => {
36
+ const memos = listMemos();
37
+ const index = memos.findIndex((memo) => memo.id === req.params.id);
38
+ if (index < 0) return res.status(404).json({ error: 'Memo not found.' });
39
+ const data = validMemo(req.body);
40
+ if (!data) return res.status(400).json({ error: 'Memo content is required.' });
41
+ const updated = { ...memos[index], ...data, updatedAt: new Date().toISOString() };
42
+ memos[index] = updated;
43
+ saveMemos(memos);
44
+ res.json(updated);
45
+ });
46
+
47
+ router.delete('/:id', (req, res) => {
48
+ const memos = listMemos();
49
+ const next = memos.filter((memo) => memo.id !== req.params.id);
50
+ if (next.length === memos.length) return res.status(404).json({ error: 'Memo not found.' });
51
+ saveMemos(next);
52
+ res.status(204).end();
53
+ });
54
+
55
+ export default router;
@@ -1,8 +1,8 @@
1
1
  import https from 'node:https';
2
2
  import axios from 'axios';
3
3
  import { Router } from 'express';
4
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
5
- import { join, basename } from 'node:path';
4
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
5
+ import { join, basename, resolve } from 'node:path';
6
6
  import { execFile } from 'node:child_process';
7
7
  import { promisify } from 'node:util';
8
8
  import { readSettings } from '../repositories/settings.js';
@@ -20,6 +20,60 @@ const httpClient = axios.create({
20
20
 
21
21
  function repoName(url) { return basename(String(url || '').replace(/[\\/]$/, '').replace(/\.git$/i, '').replace(/[:/]+$/, '')) || 'repo'; }
22
22
  function repoPath(data, repo) { return join(data.workspaceDir, repo.directory || repoName(repo.url)); }
23
+ const dependencySections = ['dependencies', 'devDependencies'];
24
+ function workspaceRepositoryCandidates(workspaceDir) {
25
+ const root = resolve(workspaceDir);
26
+ if (!existsSync(root) || !statSync(root).isDirectory()) throw new Error(`Workspace directory does not exist: ${root}`);
27
+ const folders = [];
28
+ if (existsSync(join(root, '.git')) || existsSync(join(root, 'package.json'))) folders.push(root);
29
+ readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules').forEach((entry) => {
30
+ const folder = join(root, entry.name);
31
+ if (existsSync(join(folder, '.git')) || existsSync(join(folder, 'package.json'))) folders.push(folder);
32
+ });
33
+ return folders.map((folder) => {
34
+ const directory = basename(folder);
35
+ let name = directory;
36
+ try { name = JSON.parse(readFileSync(join(folder, 'package.json'), 'utf8')).name || directory; } catch {}
37
+ return { directory, name, path: folder };
38
+ });
39
+ }
40
+ function scanWorkspaceDependencies(workspaceDir, selectedDirectories = []) {
41
+ const root = resolve(workspaceDir);
42
+ if (!existsSync(root) || !statSync(root).isDirectory()) throw new Error(`Workspace directory does not exist: ${root}`);
43
+ const selected = new Set(selectedDirectories);
44
+ const folders = workspaceRepositoryCandidates(root)
45
+ .filter((item) => !selected.size || selected.has(item.directory))
46
+ .map((item) => item.path);
47
+ const repositories = [];
48
+ const packages = new Map();
49
+ const errors = [];
50
+ folders.forEach((folder) => {
51
+ const packageFile = join(folder, 'package.json');
52
+ if (!existsSync(packageFile)) return;
53
+ const directory = basename(folder);
54
+ try {
55
+ const packageJson = JSON.parse(readFileSync(packageFile, 'utf8'));
56
+ const repository = { name: packageJson.name || directory, directory, path: folder, packageCount: 0 };
57
+ dependencySections.forEach((section) => Object.entries(packageJson[section] || {}).forEach(([name, version]) => {
58
+ if (typeof version !== 'string') return;
59
+ repository.packageCount += 1;
60
+ if (!packages.has(name)) packages.set(name, new Map());
61
+ const versions = packages.get(name);
62
+ if (!versions.has(version)) versions.set(version, []);
63
+ const existing = versions.get(version).find((item) => item.directory === directory);
64
+ if (existing) existing.sections.push(section);
65
+ else versions.get(version).push({ repository: repository.name, directory, sections: [section] });
66
+ }));
67
+ repositories.push(repository);
68
+ } catch (error) { errors.push({ repository: directory, message: error.message || 'Invalid package.json' }); }
69
+ });
70
+ const packageRows = [...packages.entries()].map(([name, versions]) => ({
71
+ name,
72
+ repositoryCount: new Set([...versions.values()].flat().map((item) => item.directory)).size,
73
+ versions: [...versions.entries()].sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true })).map(([version, items]) => ({ version, repositories: items }))
74
+ })).sort((a, b) => b.repositoryCount - a.repositoryCount || a.name.localeCompare(b.name));
75
+ return { workspaceDir: root, scannedAt: new Date().toISOString(), repositories, packages: packageRows, errors };
76
+ }
23
77
  function parseBitbucket(url) {
24
78
  const raw = String(url || '').replace(/\.git$/, '');
25
79
  let u;
@@ -134,6 +188,31 @@ async function execute(task) {
134
188
  }
135
189
 
136
190
  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 }); });
191
+ router.get('/dependency-repositories', (_req, res) => {
192
+ const data = readPackageUpgradeData();
193
+ if (!data.workspaceDir) return res.status(400).json({ error: 'Configure a workspace directory in Package Upgrade first.' });
194
+ try {
195
+ const selected = new Set(data.dependencyRepos || []);
196
+ res.json({ workspaceDir: resolve(data.workspaceDir), repositories: workspaceRepositoryCandidates(data.workspaceDir).map((repo) => ({ ...repo, selected: !selected.size || selected.has(repo.directory) })) });
197
+ } catch (error) { res.status(400).json({ error: error.message }); }
198
+ });
199
+ router.put('/dependency-repositories', (req, res) => {
200
+ const data = readPackageUpgradeData();
201
+ if (!data.workspaceDir) return res.status(400).json({ error: 'Configure a workspace directory in Package Upgrade first.' });
202
+ try {
203
+ const available = new Set(workspaceRepositoryCandidates(data.workspaceDir).map((repo) => repo.directory));
204
+ data.dependencyRepos = [...new Set(Array.isArray(req.body?.directories) ? req.body.directories.map(String).filter((directory) => available.has(directory)) : [])];
205
+ savePackageUpgradeData(data);
206
+ res.json({ directories: data.dependencyRepos });
207
+ } catch (error) { res.status(400).json({ error: error.message }); }
208
+ });
209
+ router.get('/dependency-scan', (_req, res) => {
210
+ const data = readPackageUpgradeData();
211
+ if (!data.workspaceDir) return res.status(400).json({ error: 'Configure a workspace directory in Package Upgrade first.' });
212
+ try {
213
+ res.json(scanWorkspaceDependencies(data.workspaceDir, data.dependencyRepos));
214
+ } catch (error) { res.status(400).json({ error: error.message }); }
215
+ });
137
216
  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 }); });
138
217
  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); });
139
218
  router.post('/repos/packages', (req, res) => {
@@ -2,7 +2,7 @@ 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
+ import { getNpmPackageVersions, installNvmVersion, manageGlobalNpmPackage, readDevConfigurations, readNvmConfiguration, saveDevConfigurations, searchNpmPackages, useSuggestedNpmPrefix } from '../services/dev-configurations.js';
6
6
  import { openInSystemBrowser, openInSystemEditor, openInSystemFolder } from '../services/browser.js';
7
7
  import { selectDirectory, selectFile } from '../services/dialog.js';
8
8
 
@@ -25,6 +25,9 @@ router.put('/dev-configurations', async (req, res) => {
25
25
  router.post('/dev-configurations/nvm/install', async (req, res) => {
26
26
  try { res.json(await installNvmVersion(req.body?.version)); } catch (error) { res.status(400).json({ error: error.message }); }
27
27
  });
28
+ router.post('/dev-configurations/npm/use-prefix-suggestion', async (_req, res) => {
29
+ try { res.json(await useSuggestedNpmPrefix()); } catch (error) { res.status(400).json({ error: error.message }); }
30
+ });
28
31
  router.post('/dev-configurations/npm/search', async (req, res) => {
29
32
  try { res.json(await searchNpmPackages(req.body?.query)); } catch (error) { res.status(400).json({ error: error.message }); }
30
33
  });
@@ -1,5 +1,5 @@
1
1
  import { execFile } from 'node:child_process';
2
- import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { basename, dirname, join } from 'node:path';
5
5
  import { promisify } from 'node:util';
@@ -224,6 +224,13 @@ async function setNpmConfig(key, value) {
224
224
  }
225
225
  }
226
226
 
227
+ export async function useSuggestedNpmPrefix() {
228
+ const prefix = join(homedir(), '.npm-global');
229
+ mkdirSync(prefix, { recursive: true });
230
+ await setNpmConfig('prefix', prefix);
231
+ return { prefix };
232
+ }
233
+
227
234
  export async function saveDevConfigurations(configurations = {}, options = {}) {
228
235
  const section = options.section || '';
229
236
  const npm = configurations.npm || {};
package/server.js CHANGED
@@ -16,6 +16,7 @@ import prReviewRoutes from './server/routes/pr-review.js';
16
16
  import prConflictRoutes from './server/routes/pr-conflicts.js';
17
17
  import jiraFiltersRoutes from './server/routes/jira-filters.js';
18
18
  import todoRoutes from './server/routes/todos.js';
19
+ import memoRoutes from './server/routes/memos.js';
19
20
  import staticPagesRoutes from './server/routes/static-pages.js';
20
21
  import errorRoutes from './server/routes/errors.js';
21
22
  import postmanRoutes from './server/routes/postman.js';
@@ -92,6 +93,7 @@ app.use('/api/pr-review', prReviewRoutes);
92
93
  app.use('/api/pr-conflicts', prConflictRoutes);
93
94
  app.use('/api/jira-filters', jiraFiltersRoutes);
94
95
  app.use('/api/todos', todoRoutes);
96
+ app.use('/api/memos', memoRoutes);
95
97
  app.use('/api/static-pages', staticPagesRoutes);
96
98
  app.use('/api/errors', errorRoutes);
97
99
  app.use('/api/postman', postmanRoutes);