buddy-workbench 0.1.8 → 0.1.10
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 +2 -1
- package/server/config.js +3 -0
- package/server/repositories/jira-filters.js +16 -0
- package/server/repositories/settings.js +30 -0
- package/server/repositories/static-pages.js +29 -0
- package/server/repositories/todos.js +16 -0
- package/server/routes/clipboard.js +11 -2
- package/server/routes/jira-filters.js +160 -0
- package/server/routes/launchers.js +2 -2
- package/server/routes/pr-review.js +32 -28
- package/server/routes/settings.js +23 -1
- package/server/routes/static-pages.js +20 -0
- package/server/routes/todos.js +177 -0
- package/server/services/browser.js +28 -0
- package/server/services/clipboard-history.js +71 -5
- package/server/services/process-manager.js +68 -4
- package/server.js +6 -0
- package/ui/dist/assets/index-BbLFuEqP.js +519 -0
- package/ui/dist/assets/index-BxgOdOl7.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-BHs_wPig.css +0 -1
- package/ui/dist/assets/index-CkXRrM63.js +0 -500
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { listTodos, saveTodos } from '../repositories/todos.js';
|
|
3
|
+
|
|
4
|
+
const router = Router();
|
|
5
|
+
|
|
6
|
+
function validTodo(body) {
|
|
7
|
+
const title = String(body?.title || '').trim();
|
|
8
|
+
if (!title) return null;
|
|
9
|
+
|
|
10
|
+
const priority = ['high', 'medium', 'low'].includes(body?.priority) ? body.priority : 'medium';
|
|
11
|
+
const category = String(body?.category || 'work').trim() || 'work';
|
|
12
|
+
const description = String(body?.description || '').trim();
|
|
13
|
+
const dueDate = body?.dueDate ? String(body.dueDate).trim() : null;
|
|
14
|
+
const completed = Boolean(body?.completed);
|
|
15
|
+
const archived = Boolean(body?.archived);
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
title,
|
|
19
|
+
description,
|
|
20
|
+
priority,
|
|
21
|
+
category,
|
|
22
|
+
dueDate,
|
|
23
|
+
completed,
|
|
24
|
+
archived
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
router.get('/', (_req, res) => {
|
|
29
|
+
res.json(listTodos());
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
router.post('/', (req, res) => {
|
|
33
|
+
const data = validTodo(req.body);
|
|
34
|
+
if (!data) {
|
|
35
|
+
return res.status(400).json({ error: 'Title is required.' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const now = new Date().toISOString();
|
|
39
|
+
const newTodo = {
|
|
40
|
+
id: crypto.randomUUID(),
|
|
41
|
+
...data,
|
|
42
|
+
completedAt: data.completed ? now : null,
|
|
43
|
+
archivedAt: data.archived ? now : null,
|
|
44
|
+
createdAt: now,
|
|
45
|
+
updatedAt: now
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const todos = listTodos();
|
|
49
|
+
todos.unshift(newTodo);
|
|
50
|
+
saveTodos(todos);
|
|
51
|
+
|
|
52
|
+
res.status(201).json(newTodo);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
router.put('/:id', (req, res) => {
|
|
56
|
+
const { id } = req.params;
|
|
57
|
+
const todos = listTodos();
|
|
58
|
+
const index = todos.findIndex((item) => item.id === id);
|
|
59
|
+
|
|
60
|
+
if (index < 0) {
|
|
61
|
+
return res.status(404).json({ error: 'Todo item not found.' });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const data = validTodo(req.body);
|
|
65
|
+
if (!data) {
|
|
66
|
+
return res.status(400).json({ error: 'Title is required.' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const existing = todos[index];
|
|
70
|
+
const now = new Date().toISOString();
|
|
71
|
+
const completedAt = data.completed
|
|
72
|
+
? (existing.completed ? existing.completedAt : now)
|
|
73
|
+
: null;
|
|
74
|
+
const archivedAt = data.archived
|
|
75
|
+
? (existing.archived ? existing.archivedAt : now)
|
|
76
|
+
: null;
|
|
77
|
+
|
|
78
|
+
const updated = {
|
|
79
|
+
...existing,
|
|
80
|
+
...data,
|
|
81
|
+
completedAt,
|
|
82
|
+
archivedAt,
|
|
83
|
+
updatedAt: now
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
todos[index] = updated;
|
|
87
|
+
saveTodos(todos);
|
|
88
|
+
|
|
89
|
+
res.json(updated);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
router.post('/:id/archive', (req, res) => {
|
|
93
|
+
const { id } = req.params;
|
|
94
|
+
const todos = listTodos();
|
|
95
|
+
const index = todos.findIndex((item) => item.id === id);
|
|
96
|
+
|
|
97
|
+
if (index < 0) {
|
|
98
|
+
return res.status(404).json({ error: 'Todo item not found.' });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const now = new Date().toISOString();
|
|
102
|
+
const updated = {
|
|
103
|
+
...todos[index],
|
|
104
|
+
archived: true,
|
|
105
|
+
archivedAt: now,
|
|
106
|
+
updatedAt: now
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
todos[index] = updated;
|
|
110
|
+
saveTodos(todos);
|
|
111
|
+
res.json(updated);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
router.post('/:id/unarchive', (req, res) => {
|
|
115
|
+
const { id } = req.params;
|
|
116
|
+
const todos = listTodos();
|
|
117
|
+
const index = todos.findIndex((item) => item.id === id);
|
|
118
|
+
|
|
119
|
+
if (index < 0) {
|
|
120
|
+
return res.status(404).json({ error: 'Todo item not found.' });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const now = new Date().toISOString();
|
|
124
|
+
const updated = {
|
|
125
|
+
...todos[index],
|
|
126
|
+
archived: false,
|
|
127
|
+
archivedAt: null,
|
|
128
|
+
updatedAt: now
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
todos[index] = updated;
|
|
132
|
+
saveTodos(todos);
|
|
133
|
+
res.json(updated);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
router.delete('/:id', (req, res) => {
|
|
137
|
+
const { id } = req.params;
|
|
138
|
+
const todos = listTodos();
|
|
139
|
+
const next = todos.filter((item) => item.id !== id);
|
|
140
|
+
|
|
141
|
+
if (next.length === todos.length) {
|
|
142
|
+
return res.status(404).json({ error: 'Todo item not found.' });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
saveTodos(next);
|
|
146
|
+
res.status(204).end();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
router.post('/batch', (req, res) => {
|
|
150
|
+
const { action, ids } = req.body || {};
|
|
151
|
+
let todos = listTodos();
|
|
152
|
+
const now = new Date().toISOString();
|
|
153
|
+
const idSet = new Set(Array.isArray(ids) ? ids : []);
|
|
154
|
+
|
|
155
|
+
if (action === 'markCompleted') {
|
|
156
|
+
todos = todos.map((item) => (idSet.has(item.id) ? { ...item, completed: true, completedAt: item.completedAt || now, updatedAt: now } : item));
|
|
157
|
+
} else if (action === 'markPending') {
|
|
158
|
+
todos = todos.map((item) => (idSet.has(item.id) ? { ...item, completed: false, completedAt: null, updatedAt: now } : item));
|
|
159
|
+
} else if (action === 'archive') {
|
|
160
|
+
todos = todos.map((item) => (idSet.has(item.id) ? { ...item, archived: true, archivedAt: now, updatedAt: now } : item));
|
|
161
|
+
} else if (action === 'unarchive') {
|
|
162
|
+
todos = todos.map((item) => (idSet.has(item.id) ? { ...item, archived: false, archivedAt: null, updatedAt: now } : item));
|
|
163
|
+
} else if (action === 'delete') {
|
|
164
|
+
todos = todos.filter((item) => !idSet.has(item.id));
|
|
165
|
+
} else if (action === 'clearCompleted') {
|
|
166
|
+
todos = todos.filter((item) => !item.completed || item.archived);
|
|
167
|
+
} else if (action === 'clearArchived') {
|
|
168
|
+
todos = todos.filter((item) => !item.archived);
|
|
169
|
+
} else {
|
|
170
|
+
return res.status(400).json({ error: 'Invalid batch action.' });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
saveTodos(todos);
|
|
174
|
+
res.json(todos);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
export default router;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readSettings } from '../repositories/settings.js';
|
|
3
|
+
|
|
4
|
+
export function openInSystemBrowser(url) {
|
|
5
|
+
if (!url || typeof url !== 'string') return;
|
|
6
|
+
const settings = readSettings();
|
|
7
|
+
const browser = settings.defaultBrowser || 'chrome';
|
|
8
|
+
|
|
9
|
+
if (process.platform === 'darwin') {
|
|
10
|
+
const macAppMap = {
|
|
11
|
+
chrome: 'Google Chrome',
|
|
12
|
+
edge: 'Microsoft Edge',
|
|
13
|
+
safari: 'Safari'
|
|
14
|
+
};
|
|
15
|
+
const appName = macAppMap[browser] || 'Google Chrome';
|
|
16
|
+
execFile('open', ['-a', appName, url]);
|
|
17
|
+
} else if (process.platform === 'win32') {
|
|
18
|
+
const winCmdMap = {
|
|
19
|
+
chrome: 'chrome',
|
|
20
|
+
edge: 'msedge',
|
|
21
|
+
safari: 'msedge'
|
|
22
|
+
};
|
|
23
|
+
const cmd = winCmdMap[browser] || 'chrome';
|
|
24
|
+
execFile('cmd', ['/c', 'start', '', cmd, url]);
|
|
25
|
+
} else {
|
|
26
|
+
execFile('xdg-open', [url]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -19,8 +19,74 @@ export function clipboardDates() {
|
|
|
19
19
|
return readdirSync(paths.clipboardDir, { withFileTypes: true }).filter((year) => year.isDirectory() && /^\d{4}$/.test(year.name)).flatMap((year) => readdirSync(join(paths.clipboardDir, year.name), { withFileTypes: true }).filter((month) => month.isDirectory() && /^\d{2}$/.test(month.name)).flatMap((month) => readdirSync(join(paths.clipboardDir, year.name, month.name), { withFileTypes: true }).filter((day) => day.isFile() && /^\d{2}\.json$/.test(day.name)).map((day) => `${year.name}-${month.name}-${day.name.slice(0, 2)}`))).sort().reverse();
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function getEditorUrl(editor = 'vscode', path = '') {
|
|
23
|
+
if (editor === 'idea') return `idea://open?file=${encodeURIComponent(path)}`;
|
|
24
|
+
if (editor === 'devin') return `devin://file${encodeURI(path)}`;
|
|
25
|
+
return `vscode://file${encodeURI(path)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
22
28
|
function dayItems(date) { return readJson(dayFile(date), []); }
|
|
23
|
-
export function clipboardItems(date = today()) {
|
|
29
|
+
export function clipboardItems(date = today()) {
|
|
30
|
+
const settings = readSettings();
|
|
31
|
+
const editor = settings.defaultEditor || 'vscode';
|
|
32
|
+
const editorName = editor === 'idea' ? 'IntelliJ IDEA' : editor === 'devin' ? 'Devin' : 'VS Code';
|
|
33
|
+
return dayItems(date).map((item) => item.contentFile ? { ...item, editorUrl: getEditorUrl(editor, join(paths.clipboardDir, 'content', item.contentFile)), editorName } : item);
|
|
34
|
+
}
|
|
35
|
+
export function allTaggedClipboardItems() {
|
|
36
|
+
const settings = readSettings();
|
|
37
|
+
const editor = settings.defaultEditor || 'vscode';
|
|
38
|
+
const editorName = editor === 'idea' ? 'IntelliJ IDEA' : editor === 'devin' ? 'Devin' : 'VS Code';
|
|
39
|
+
const dates = clipboardDates();
|
|
40
|
+
const result = [];
|
|
41
|
+
|
|
42
|
+
for (const date of dates) {
|
|
43
|
+
const items = dayItems(date);
|
|
44
|
+
for (const item of items) {
|
|
45
|
+
if (Array.isArray(item.tags) && item.tags.length > 0) {
|
|
46
|
+
result.push(
|
|
47
|
+
item.contentFile
|
|
48
|
+
? {
|
|
49
|
+
...item,
|
|
50
|
+
date,
|
|
51
|
+
editorUrl: getEditorUrl(editor, join(paths.clipboardDir, 'content', item.contentFile)),
|
|
52
|
+
editorName
|
|
53
|
+
}
|
|
54
|
+
: { ...item, date }
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function updateClipboardItemTags(date, id, tags) {
|
|
63
|
+
let targetDate = date;
|
|
64
|
+
let items = dayItems(targetDate);
|
|
65
|
+
let item = items.find((entry) => entry.id === id);
|
|
66
|
+
|
|
67
|
+
if (!item) {
|
|
68
|
+
const dates = clipboardDates();
|
|
69
|
+
for (const d of dates) {
|
|
70
|
+
const dayList = dayItems(d);
|
|
71
|
+
const found = dayList.find((entry) => entry.id === id);
|
|
72
|
+
if (found) {
|
|
73
|
+
targetDate = d;
|
|
74
|
+
items = dayList;
|
|
75
|
+
item = found;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!item) return null;
|
|
82
|
+
|
|
83
|
+
const validTags = Array.isArray(tags) ? tags.map((t) => String(t).trim()).filter(Boolean) : [];
|
|
84
|
+
const updatedItem = { ...item, tags: validTags };
|
|
85
|
+
const nextItems = items.map((entry) => (entry.id === id ? updatedItem : entry));
|
|
86
|
+
writeJson(dayFile(targetDate), nextItems);
|
|
87
|
+
return updatedItem;
|
|
88
|
+
}
|
|
89
|
+
|
|
24
90
|
export function clipboardOriginal(date, id) { const item = dayItems(date).find((entry) => entry.id === id); if (!item?.contentFile) return item?.text || null; try { return readFileSync(join(paths.clipboardDir, 'content', item.contentFile), 'utf8'); } catch { return null; } }
|
|
25
91
|
export function deleteClipboardItem(date, id) {
|
|
26
92
|
const items = dayItems(date); const item = items.find((entry) => entry.id === id);
|
|
@@ -82,18 +148,18 @@ export async function captureClipboard() {
|
|
|
82
148
|
if (alreadyExists) return;
|
|
83
149
|
|
|
84
150
|
const id = crypto.randomUUID(); const isLong = text.length > previewLimit;
|
|
85
|
-
if (isLong) { mkdirSync(join(paths.clipboardDir, 'content'), { recursive: true }); writeFileSync(join(paths.clipboardDir, 'content',
|
|
86
|
-
const item = isLong ? { id, preview: `${text.slice(0, previewLimit)}…`, contentFile:
|
|
151
|
+
if (isLong) { mkdirSync(join(paths.clipboardDir, 'content'), { recursive: true }); writeFileSync(join(paths.clipboardDir, 'content', id), text, 'utf8'); }
|
|
152
|
+
const item = isLong ? { id, preview: `${text.slice(0, previewLimit)}…`, contentFile: id, createdAt: new Date().toISOString() } : { id, text, createdAt: new Date().toISOString() };
|
|
87
153
|
writeJson(dayFile(date), [item, ...items].slice(0, 200));
|
|
88
154
|
} catch {}
|
|
89
155
|
}
|
|
90
156
|
|
|
91
157
|
export function startClipboardCapture() { captureClipboard(); const timer = setInterval(captureClipboard, 2000); timer.unref(); }
|
|
92
158
|
|
|
93
|
-
export function saveClipboardImage(buffer) {
|
|
159
|
+
export function saveClipboardImage(buffer, ext = 'png') {
|
|
94
160
|
const date = today();
|
|
95
161
|
const id = crypto.randomUUID();
|
|
96
|
-
const filename = `${id}
|
|
162
|
+
const filename = `${id}.${ext}`;
|
|
97
163
|
|
|
98
164
|
mkdirSync(join(paths.clipboardDir, 'content'), { recursive: true });
|
|
99
165
|
writeFileSync(join(paths.clipboardDir, 'content', filename), buffer);
|
|
@@ -14,13 +14,69 @@ const savedPortHistory = new Map(listPortHistory().map((record) => [record.port,
|
|
|
14
14
|
const supervisorPath = fileURLToPath(new URL('./script-supervisor.js', import.meta.url));
|
|
15
15
|
const execFileAsync = promisify(execFile);
|
|
16
16
|
const keyFor = (launcherId, scriptId) => `${launcherId}:${scriptId}`;
|
|
17
|
+
function countErrorBlocks(text, isStderr = false) {
|
|
18
|
+
if (!text) return 0;
|
|
19
|
+
const lines = text.split(/\r?\n/);
|
|
20
|
+
let count = 0;
|
|
21
|
+
let inErrorBlock = false;
|
|
22
|
+
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
const trimmed = line.trim();
|
|
25
|
+
if (!trimmed) {
|
|
26
|
+
inErrorBlock = false;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const isNewHeader =
|
|
31
|
+
/^(?:\[?(?:error|fatal|fail|failed)\]?|error:|uncaught|fatal:)/i.test(trimmed) ||
|
|
32
|
+
/^(?:TypeError|ReferenceError|SyntaxError|RangeError|EvalError|URIError|Error|UnhandledPromiseRejectionWarning|UnhandledPromiseRejection):/i.test(trimmed) ||
|
|
33
|
+
/^npm ERR! (?:code|syscall|path|errno)/i.test(trimmed) ||
|
|
34
|
+
/^\[vite\] Internal server error/i.test(trimmed) ||
|
|
35
|
+
/\bTS\d{4,5}\b/i.test(trimmed) ||
|
|
36
|
+
/\berror TS\d+/i.test(trimmed) ||
|
|
37
|
+
/^(?:\[TypeScript\]|TS ERROR)/i.test(trimmed);
|
|
38
|
+
|
|
39
|
+
if (isNewHeader) {
|
|
40
|
+
inErrorBlock = false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const isErrorLine =
|
|
44
|
+
isStderr ||
|
|
45
|
+
isNewHeader ||
|
|
46
|
+
/\b(error|failed|fatal|exception)\b/i.test(trimmed) ||
|
|
47
|
+
/^npm ERR!/i.test(trimmed) ||
|
|
48
|
+
/\bTS\d{4,5}\b/i.test(trimmed) ||
|
|
49
|
+
/^\s*at\s+[\w\d_$.<>]+\s+\(/i.test(line);
|
|
50
|
+
|
|
51
|
+
if (isErrorLine) {
|
|
52
|
+
if (!inErrorBlock) {
|
|
53
|
+
count++;
|
|
54
|
+
inErrorBlock = true;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
inErrorBlock = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return count;
|
|
62
|
+
}
|
|
63
|
+
|
|
17
64
|
const appendLog = (key, type, chunk) => {
|
|
18
|
-
const current = logs.get(key) || { output: '', error: '' };
|
|
19
|
-
|
|
65
|
+
const current = logs.get(key) || { output: '', error: '', errorCount: 0 };
|
|
66
|
+
const str = String(chunk || '');
|
|
67
|
+
current[type] = `${current[type]}${str}`.split(/\r?\n/).slice(-1000).join('\n');
|
|
68
|
+
current.errorCount = countErrorBlocks(current.error, true) + countErrorBlocks(current.output, false);
|
|
20
69
|
logs.set(key, current);
|
|
21
70
|
};
|
|
22
71
|
|
|
23
72
|
export function runningScripts() { return [...running.keys()]; }
|
|
73
|
+
export function runningErrorCounts() {
|
|
74
|
+
const counts = {};
|
|
75
|
+
for (const [key, data] of logs.entries()) {
|
|
76
|
+
if (running.has(key) && data?.errorCount) counts[key] = data.errorCount;
|
|
77
|
+
}
|
|
78
|
+
return counts;
|
|
79
|
+
}
|
|
24
80
|
export function scriptLogs(launcherId, scriptId) { return logs.get(keyFor(launcherId, scriptId))?.output || ''; }
|
|
25
81
|
export function scriptErrorLogs(launcherId, scriptId) { return logs.get(keyFor(launcherId, scriptId))?.error || ''; }
|
|
26
82
|
|
|
@@ -149,7 +205,7 @@ export function runScript(launcher, script) {
|
|
|
149
205
|
const key = keyFor(launcher.id, script.id);
|
|
150
206
|
if (running.has(key)) throw new Error('This script is already running.');
|
|
151
207
|
if (!existsSync(launcher.folder)) throw new Error('The configured project folder does not exist.');
|
|
152
|
-
logs.set(key, { output: `$ ${script.command}\n`, error: '' });
|
|
208
|
+
logs.set(key, { output: `$ ${script.command}\n`, error: '', errorCount: 0 });
|
|
153
209
|
const child = spawn(process.execPath, [supervisorPath], { cwd: launcher.folder, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, ...(launcher.executor === 'pnpm' || /^\s*pnpm\b/.test(script.command) ? { CI: 'true' } : {}), BUDDY_PARENT_PID: String(process.pid), BUDDY_SCRIPT_COMMAND: script.command } });
|
|
154
210
|
child.stdout.on('data', (chunk) => appendLog(key, 'output', chunk.toString()));
|
|
155
211
|
child.stderr.on('data', (chunk) => appendLog(key, 'error', chunk.toString()));
|
|
@@ -158,7 +214,13 @@ export function runScript(launcher, script) {
|
|
|
158
214
|
launchHistory.push({ key, groupPid: child.pid, launcher: launcher.alias, script: script.name, startedAt: new Date().toISOString(), ports: new Map() });
|
|
159
215
|
if (launchHistory.length > 200) launchHistory.splice(0, launchHistory.length - 200);
|
|
160
216
|
setTimeout(() => { void observeLauncherPorts().catch(() => {}); }, 1000).unref();
|
|
161
|
-
child.on('exit', (code) => {
|
|
217
|
+
child.on('exit', (code) => {
|
|
218
|
+
appendLog(key, code === 0 ? 'output' : 'error', `\nProcess exited with code ${code}.\n`);
|
|
219
|
+
const item = logs.get(key);
|
|
220
|
+
if (item) item.errorCount = 0;
|
|
221
|
+
running.delete(key);
|
|
222
|
+
retireGroup(child.pid);
|
|
223
|
+
});
|
|
162
224
|
}
|
|
163
225
|
|
|
164
226
|
export function stopScript(launcherId, scriptId) {
|
|
@@ -166,6 +228,8 @@ export function stopScript(launcherId, scriptId) {
|
|
|
166
228
|
const child = running.get(key);
|
|
167
229
|
if (!child) throw new Error('This script is not running.');
|
|
168
230
|
appendLog(key, 'output', '\nStop requested.\n');
|
|
231
|
+
const item = logs.get(key);
|
|
232
|
+
if (item) item.errorCount = 0;
|
|
169
233
|
stoppingGroups.add(child.pid);
|
|
170
234
|
terminateGroup(child.pid, 'SIGTERM');
|
|
171
235
|
setTimeout(() => { terminateGroup(child.pid, 'SIGKILL'); stoppingGroups.delete(child.pid); processGroups.delete(child.pid); }, 1500).unref();
|
package/server.js
CHANGED
|
@@ -13,6 +13,9 @@ import groupTaskRoutes from './server/routes/group-tasks.js';
|
|
|
13
13
|
import portDiagnosticsRoutes from './server/routes/port-diagnostics.js';
|
|
14
14
|
import settingsRoutes from './server/routes/settings.js';
|
|
15
15
|
import prReviewRoutes from './server/routes/pr-review.js';
|
|
16
|
+
import jiraFiltersRoutes from './server/routes/jira-filters.js';
|
|
17
|
+
import todoRoutes from './server/routes/todos.js';
|
|
18
|
+
import staticPagesRoutes from './server/routes/static-pages.js';
|
|
16
19
|
import { startClipboardCapture } from './server/services/clipboard-history.js';
|
|
17
20
|
|
|
18
21
|
const port = Number(process.env.PORT || 3100);
|
|
@@ -27,6 +30,9 @@ app.use('/api/group-tasks', groupTaskRoutes);
|
|
|
27
30
|
app.use('/api/port-diagnostics', portDiagnosticsRoutes);
|
|
28
31
|
app.use('/api/settings', settingsRoutes);
|
|
29
32
|
app.use('/api/pr-review', prReviewRoutes);
|
|
33
|
+
app.use('/api/jira-filters', jiraFiltersRoutes);
|
|
34
|
+
app.use('/api/todos', todoRoutes);
|
|
35
|
+
app.use('/api/static-pages', staticPagesRoutes);
|
|
30
36
|
|
|
31
37
|
await freePort(port);
|
|
32
38
|
const server = app.listen(port, () => { console.log(`Buddy Workbench: http://localhost:${port} (pid ${process.pid})`); shutdownTrace(`server started (pid=${process.pid})`); });
|