buddy-workbench 0.1.21 → 0.1.23
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 +1 -1
- package/server/config.js +1 -0
- package/server/repositories/settings.js +2 -1
- package/server/routes/pr-review.js +269 -112
- package/server/routes/settings.js +20 -1
- package/server/services/browser.js +12 -0
- package/ui/dist/assets/index-BEv9wWjc.js +530 -0
- package/ui/dist/index.html +1 -1
- package/ui/dist/assets/index-kLUg9ehS.js +0 -530
package/package.json
CHANGED
package/server/config.js
CHANGED
|
@@ -3,6 +3,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
3
3
|
|
|
4
4
|
export const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
5
5
|
export const paths = {
|
|
6
|
+
dataDir: join(root, 'data'),
|
|
6
7
|
launchers: join(root, 'data', 'launchers.json'),
|
|
7
8
|
portHistory: join(root, 'data', 'port-history.json'),
|
|
8
9
|
groupTasks: join(root, 'data', 'group-tasks.json'),
|
|
@@ -20,7 +20,8 @@ export function settingsStatus() {
|
|
|
20
20
|
isMac: process.platform === 'darwin',
|
|
21
21
|
clipboardEnabled: settings.clipboardEnabled !== false,
|
|
22
22
|
clipboardImageEnabled: settings.clipboardImageEnabled !== false,
|
|
23
|
-
clipboardDeduplicateMinutes: typeof settings.clipboardDeduplicateMinutes === 'number' && !isNaN(settings.clipboardDeduplicateMinutes) ? settings.clipboardDeduplicateMinutes : 60
|
|
23
|
+
clipboardDeduplicateMinutes: typeof settings.clipboardDeduplicateMinutes === 'number' && !isNaN(settings.clipboardDeduplicateMinutes) ? settings.clipboardDeduplicateMinutes : 60,
|
|
24
|
+
dataDir: paths.dataDir
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -23,6 +23,14 @@ function getBitbucketHost() {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function parseBitbucketPrUrl(url) {
|
|
26
|
+
if (url && (url.toLowerCase().includes('demo') || url.toLowerCase().includes('mock'))) {
|
|
27
|
+
return {
|
|
28
|
+
host: 'bitbucket.example.com',
|
|
29
|
+
projectKey: 'DEMO',
|
|
30
|
+
repositorySlug: 'demo-repo',
|
|
31
|
+
pullRequestId: '999'
|
|
32
|
+
};
|
|
33
|
+
}
|
|
26
34
|
const match = url.match(/https?:\/\/([^/]+)\/projects\/([^/]+)\/repos\/([^/]+)\/pull-requests\/(\d+)/i);
|
|
27
35
|
if (match) {
|
|
28
36
|
return {
|
|
@@ -53,142 +61,184 @@ const isFiltered = (filePath) => {
|
|
|
53
61
|
|
|
54
62
|
const analyzeDiff = (filePath, hunks) => {
|
|
55
63
|
const issues = [];
|
|
64
|
+
|
|
56
65
|
for (const hunk of hunks || []) {
|
|
57
66
|
for (const segment of hunk.segments || []) {
|
|
58
67
|
if (segment.type !== 'ADDED') continue;
|
|
59
|
-
|
|
68
|
+
const lines = segment.lines || [];
|
|
69
|
+
if (lines.length === 0) continue;
|
|
70
|
+
|
|
71
|
+
// Build combined segment text & line index mapping to handle multiline formatting/line breaks
|
|
72
|
+
let segmentText = '';
|
|
73
|
+
const charToLineMap = [];
|
|
74
|
+
|
|
75
|
+
for (const line of lines) {
|
|
60
76
|
const text = line.text || '';
|
|
61
77
|
const lineNum = line.destinationLine;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Rule 2: Conflict Markers
|
|
75
|
-
if (/^<<<<<<<|^=======|^>>>>>>>/.test(text)) {
|
|
76
|
-
issues.push({
|
|
77
|
-
severity: 'critical',
|
|
78
|
-
rule: 'Conflict Markers',
|
|
79
|
-
message: 'Unresolved git conflict markers found.',
|
|
80
|
-
line: lineNum,
|
|
81
|
-
code: text
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Rule 3: Focused tests
|
|
86
|
-
if (/\.(only|skip)\(/.test(text) && /\.(test|spec)\./i.test(filePath)) {
|
|
87
|
-
issues.push({
|
|
88
|
-
severity: 'critical',
|
|
89
|
-
rule: 'Focused/Skipped Test',
|
|
90
|
-
message: 'Do not commit focused (`.only`) or skipped (`.skip`) tests.',
|
|
91
|
-
line: lineNum,
|
|
92
|
-
code: text
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Rule 4: Secrets
|
|
97
|
-
if (/(password|passwd|secret|token|api_key|apikey|private_key|auth_token)\s*[:=]\s*['"`][a-zA-Z0-9_\-+=/]{16,}['"`]/i.test(text)) {
|
|
98
|
-
if (!text.trim().startsWith('//') && !text.includes('placeholder') && !text.includes('dummy')) {
|
|
99
|
-
issues.push({
|
|
100
|
-
severity: 'critical',
|
|
101
|
-
rule: 'Hardcoded Secret',
|
|
102
|
-
message: 'Potential hardcoded token, password, or key detected.',
|
|
103
|
-
line: lineNum,
|
|
104
|
-
code: text
|
|
105
|
-
});
|
|
78
|
+
const startIdx = segmentText.length;
|
|
79
|
+
segmentText += text + '\n';
|
|
80
|
+
const endIdx = segmentText.length;
|
|
81
|
+
charToLineMap.push({ startIdx, endIdx, lineNum, text });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const getLineInfo = (charIndex) => {
|
|
85
|
+
for (const item of charToLineMap) {
|
|
86
|
+
if (charIndex >= item.startIdx && charIndex < item.endIdx) {
|
|
87
|
+
return { lineNum: item.lineNum, text: item.text };
|
|
106
88
|
}
|
|
107
89
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
90
|
+
return { lineNum: lines[0]?.destinationLine || 1, text: lines[0]?.text || '' };
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const addIssue = (severity, rule, message, charIndex, customCode) => {
|
|
94
|
+
const { lineNum, text } = getLineInfo(charIndex);
|
|
95
|
+
if (!issues.some(i => i.rule === rule && i.line === lineNum)) {
|
|
111
96
|
issues.push({
|
|
112
|
-
severity
|
|
113
|
-
rule
|
|
114
|
-
message
|
|
97
|
+
severity,
|
|
98
|
+
rule,
|
|
99
|
+
message,
|
|
115
100
|
line: lineNum,
|
|
116
|
-
code: text
|
|
101
|
+
code: (customCode || text).trim()
|
|
117
102
|
});
|
|
118
103
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Rule 1: Object property extraction requires lodash.get
|
|
107
|
+
const propAccessRegex = /\b([a-zA-Z_$][\w$]*)\s*\.\s*([a-zA-Z_$][\w$]*)\b(?!\s*\()/g;
|
|
108
|
+
for (const match of segmentText.matchAll(propAccessRegex)) {
|
|
109
|
+
const objName = match[1];
|
|
110
|
+
const propName = match[2];
|
|
111
|
+
const matchIdx = match.index;
|
|
112
|
+
|
|
113
|
+
if (objName === 'row' && propName === 'original') continue;
|
|
114
|
+
if (objName === 'original' && matchIdx >= 4 && segmentText.slice(matchIdx - 4, matchIdx).includes('row.')) continue;
|
|
115
|
+
|
|
116
|
+
const globalNamespaces = [
|
|
117
|
+
'Math', 'Object', 'Array', 'JSON', 'Promise', 'Reflect', 'Symbol', 'Date',
|
|
118
|
+
'Number', 'String', 'Boolean', 'React', 'process', 'e', 'evt', 'event',
|
|
119
|
+
'console', 'window', 'document', 'history', 'location', 'styles', 'style'
|
|
120
|
+
];
|
|
121
|
+
if (globalNamespaces.includes(objName)) continue;
|
|
122
|
+
|
|
123
|
+
const lineText = getLineInfo(matchIdx).text.trim();
|
|
124
|
+
if (/^\s*(import|export|type|interface)\b/.test(lineText)) continue;
|
|
125
|
+
if (/\b(lodash\.)?get\s*\(/.test(lineText)) continue;
|
|
126
|
+
|
|
127
|
+
addIssue('warning', 'Lodash Get Required', 'Object property better use `lodash/get`: `get(obj, ...)`', matchIdx);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Rule 2: TS cannot use 'any' type
|
|
131
|
+
const anyRegex = /:\s*any\b|:\s*any\[\]|as\s+any\b|<[^>]*?\bany\b[^>]*?>|\bany\[\]/g;
|
|
132
|
+
for (const match of segmentText.matchAll(anyRegex)) {
|
|
133
|
+
addIssue('critical', 'No Any Type', 'TypeScript strictly prohibits using the `any` type.', match.index);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Rule 3: No hardcoded text / strings
|
|
137
|
+
const hardcodedVarRegex = /(const|let|var)\s+[a-zA-Z_$][\w$]*\s*=\s*['"`][^'"`]+['"`]/g;
|
|
138
|
+
for (const match of segmentText.matchAll(hardcodedVarRegex)) {
|
|
139
|
+
const lineText = getLineInfo(match.index).text.trim();
|
|
140
|
+
if (!/^\s*(import|export)\b/.test(lineText) && !/\brequire\s*\(/.test(lineText)) {
|
|
141
|
+
addIssue('warning', 'No Hardcoded Strings', 'Do not use hardcoded string literals (e.g., `const a = "aa"` or `attr="111"`). Use constants or i18n.', match.index);
|
|
129
142
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
rule: 'Hardcoded IP Address',
|
|
137
|
-
message: `Avoid hardcoding internal/private IP addresses (${ipMatch[0]}).`,
|
|
138
|
-
line: lineNum,
|
|
139
|
-
code: text
|
|
140
|
-
});
|
|
143
|
+
}
|
|
144
|
+
const hardcodedAttrRegex = /\b(?!className\b|type\b|key\b|name\b|rel\b|target\b|method\b)[a-zA-Z_$][\w$-]*\s*=\s*['"`][^'"`]+['"`]/g;
|
|
145
|
+
for (const match of segmentText.matchAll(hardcodedAttrRegex)) {
|
|
146
|
+
const lineText = getLineInfo(match.index).text.trim();
|
|
147
|
+
if (!/^\s*(import|export)\b/.test(lineText) && !/\brequire\s*\(/.test(lineText)) {
|
|
148
|
+
addIssue('warning', 'No Hardcoded Strings', 'Do not use hardcoded string literals (e.g., `const a = "aa"` or `attr="111"`). Use constants or i18n.', match.index);
|
|
141
149
|
}
|
|
150
|
+
}
|
|
142
151
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
message: 'Avoid hardcoding user-facing text strings directly; consider using internationalization (i18n).',
|
|
150
|
-
line: lineNum,
|
|
151
|
-
code: text
|
|
152
|
-
});
|
|
153
|
-
}
|
|
152
|
+
// Rule 4: useMemo and useCallback must specify dependencies (multiline regex)
|
|
153
|
+
const hookRegex = /\buse(Memo|Callback)\s*\(\s*(?:(?!,\s*\[).)*?\)/gs;
|
|
154
|
+
for (const match of segmentText.matchAll(hookRegex)) {
|
|
155
|
+
const matchedSnippet = match[0];
|
|
156
|
+
if (matchedSnippet.includes(', []') || !matchedSnippet.includes(', [')) {
|
|
157
|
+
addIssue('warning', 'Missing Hook Dependency', '`useMemo` and `useCallback` must specify dynamic dependencies (missing array or empty `[]` is prohibited).', match.index, matchedSnippet);
|
|
154
158
|
}
|
|
159
|
+
}
|
|
155
160
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
161
|
+
// Rule 5: No inline styles (multiline)
|
|
162
|
+
const styleRegex = /\bstyle\s*=\s*\{\s*\{[\s\S]*?\}\s*\}|\bstyle\s*=\s*['"][\s\S]*?['"]/g;
|
|
163
|
+
for (const match of segmentText.matchAll(styleRegex)) {
|
|
164
|
+
addIssue('warning', 'No Inline Styles', 'Inline styles are strictly prohibited. Use CSS classes or styled components.', match.index);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Rule 6: Clickable elements must be button or a (multiline JSX opening tag)
|
|
168
|
+
const nonClickableTagRegex = /<(div|span|p|li|tr|td|img|i|svg|section|article|header|footer|h[1-6]|label)\b[^>]*?\b(onClick|@click)\b[^>]*?>/gis;
|
|
169
|
+
for (const match of segmentText.matchAll(nonClickableTagRegex)) {
|
|
170
|
+
addIssue('warning', 'Clickable Element Tag', 'Clickable elements must use `<button>` or `<a>` tags (or Button/Link components). Do not bind onClick to non-clickable tags like div/span.', match.index, match[0]);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Rule 7: No relative paths (multiline import)
|
|
174
|
+
const relativeImportRegex = /(import\s*(?:\{[^}]*\}|[^{'"\n]+)\s*from\s*['"]\.\.?\/*['"]|require\s*\(\s*['"]\.\.?\/*['"]\))/gs;
|
|
175
|
+
for (const match of segmentText.matchAll(relativeImportRegex)) {
|
|
176
|
+
addIssue('warning', 'No Relative Paths', 'Relative module imports (`./` or `../`) are prohibited. Use absolute paths or path aliases.', match.index, match[0]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Rule 8: No debugger
|
|
180
|
+
const debuggerRegex = /\bdebugger\b/g;
|
|
181
|
+
for (const match of segmentText.matchAll(debuggerRegex)) {
|
|
182
|
+
addIssue('critical', 'No Debugger', 'Do not leave `debugger;` statements in code.', match.index);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Rule 9: No console.log
|
|
186
|
+
const consoleRegex = /\bconsole\.log\b/g;
|
|
187
|
+
for (const match of segmentText.matchAll(consoleRegex)) {
|
|
188
|
+
addIssue('critical', 'No Console Log', 'Do not leave `console.log` statements in code.', match.index);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Rule 10: No array index key
|
|
192
|
+
const keyIndexRegex = /\bkey\s*=\s*\{\s*(index|idx|i)\s*\}/g;
|
|
193
|
+
for (const match of segmentText.matchAll(keyIndexRegex)) {
|
|
194
|
+
addIssue('warning', 'No Array Index Key', 'Avoid using array index (e.g., `key={index}`) as a React key in list rendering.', match.index);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Rule 11: No nested components
|
|
198
|
+
const nestedCompRegex = /^\s*(const|function)\s+[A-Z][a-zA-Z0-9_$]*\s*=\s*\(|\bfunction\s+[A-Z][a-zA-Z0-9_$]*\s*\(/gm;
|
|
199
|
+
for (const match of segmentText.matchAll(nestedCompRegex)) {
|
|
200
|
+
const lineText = getLineInfo(match.index).text.trim();
|
|
201
|
+
if (!/^\s*(export|import)\b/.test(lineText)) {
|
|
202
|
+
addIssue('warning', 'No Nested Components', 'Do not define React components inside another component body. Move component definitions outside.', match.index);
|
|
166
203
|
}
|
|
204
|
+
}
|
|
167
205
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
206
|
+
// Rule 12: No async client component
|
|
207
|
+
const asyncCompRegex = /\b(const|function)\s+[A-Z][a-zA-Z0-9_$]*\s*=\s*async\b|\basync\s+function\s+[A-Z][a-zA-Z0-9_$]*/g;
|
|
208
|
+
for (const match of segmentText.matchAll(asyncCompRegex)) {
|
|
209
|
+
addIssue('critical', 'No Async Component', 'React client components must not be async functions.', match.index);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Rule 13: No non-null assertions (!.)
|
|
213
|
+
const nonNullRegex = /[a-zA-Z0-9_$]!\s*(\.|\[)/g;
|
|
214
|
+
for (const match of segmentText.matchAll(nonNullRegex)) {
|
|
215
|
+
addIssue('warning', 'No Non-null Assertion', 'Avoid using TypeScript non-null assertions (`!.`). Use optional chaining (`?.`) or defensive null checks instead.', match.index);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Rule 14: No direct state mutation
|
|
219
|
+
const stateMutationRegex = /\b(state|list|items|data)\.(push|pop|shift|unshift|splice|sort|reverse)\s*\(|\bstate\.[a-zA-Z0-9_$]+\s*=\s*/g;
|
|
220
|
+
for (const match of segmentText.matchAll(stateMutationRegex)) {
|
|
221
|
+
addIssue('warning', 'No Direct State Mutation', 'Do not mutate React state directly. Use immutable data patterns.', match.index);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Rule 15: Unhandled async event
|
|
225
|
+
const unhandledAsyncRegex = /\bon[A-Z][a-zA-Z0-9_$]*\s*=\s*\{\s*async\b[\s\S]*?\}/g;
|
|
226
|
+
for (const match of segmentText.matchAll(unhandledAsyncRegex)) {
|
|
227
|
+
if (!match[0].includes('try')) {
|
|
228
|
+
addIssue('warning', 'Unhandled Async Event', 'Async event handlers must wrap asynchronous calls in a try...catch block to handle errors.', match.index, match[0]);
|
|
177
229
|
}
|
|
230
|
+
}
|
|
178
231
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
message: '`variant="primary"` is the default variant.',
|
|
185
|
-
line: lineNum,
|
|
186
|
-
code: text
|
|
187
|
-
});
|
|
232
|
+
// Rule 16: No magic numbers
|
|
233
|
+
const magicNumberRegex = /(===|==|!==|!=)\s*([2-9]|\d{2,})\b|\bsetTimeout\s*\([^,]+,\s*([2-9]|\d{2,})\)/g;
|
|
234
|
+
for (const match of segmentText.matchAll(magicNumberRegex)) {
|
|
235
|
+
if (!/\b(200|201|204|400|401|403|404|500)\b/.test(match[0])) {
|
|
236
|
+
addIssue('info', 'No Magic Numbers', 'Avoid magic numbers in business logic. Define named constants or enums instead.', match.index);
|
|
188
237
|
}
|
|
189
238
|
}
|
|
190
239
|
}
|
|
191
240
|
}
|
|
241
|
+
|
|
192
242
|
return issues;
|
|
193
243
|
};
|
|
194
244
|
|
|
@@ -201,6 +251,113 @@ router.post('/check', async (req, res) => {
|
|
|
201
251
|
|
|
202
252
|
const { host, projectKey, repositorySlug, pullRequestId } = parsed;
|
|
203
253
|
lastUsedHost = host;
|
|
254
|
+
|
|
255
|
+
if (projectKey === 'DEMO' && repositorySlug === 'demo-repo') {
|
|
256
|
+
const demoHunksMap = {
|
|
257
|
+
'src/components/UserProfile.tsx': [
|
|
258
|
+
{
|
|
259
|
+
segments: [
|
|
260
|
+
{
|
|
261
|
+
type: 'ADDED',
|
|
262
|
+
lines: [
|
|
263
|
+
{ destinationLine: 14, text: 'const userName = user.profile.name;' },
|
|
264
|
+
{ destinationLine: 22, text: 'const handleData = (payload: any): void => {' },
|
|
265
|
+
{ destinationLine: 30, text: 'const title = "User Profile Settings";' },
|
|
266
|
+
{ destinationLine: 38, text: 'const SubCard = () => <div>Sub Card</div>;' },
|
|
267
|
+
{ destinationLine: 45, text: 'const UserCard = async () => {' },
|
|
268
|
+
{ destinationLine: 52, text: 'const userId = response!.data!.id;' }
|
|
269
|
+
]
|
|
270
|
+
}
|
|
271
|
+
]
|
|
272
|
+
}
|
|
273
|
+
],
|
|
274
|
+
'src/hooks/useUserStats.ts': [
|
|
275
|
+
{
|
|
276
|
+
segments: [
|
|
277
|
+
{
|
|
278
|
+
type: 'ADDED',
|
|
279
|
+
lines: [
|
|
280
|
+
{ destinationLine: 3, text: 'import { formatUser } from "../utils/format";' },
|
|
281
|
+
{ destinationLine: 18, text: 'const memoizedValue = useMemo(() => calculateStats(data), []);' },
|
|
282
|
+
{ destinationLine: 26, text: 'state.list.push(newItem);' }
|
|
283
|
+
]
|
|
284
|
+
}
|
|
285
|
+
]
|
|
286
|
+
}
|
|
287
|
+
],
|
|
288
|
+
'src/pages/Dashboard.tsx': [
|
|
289
|
+
{
|
|
290
|
+
segments: [
|
|
291
|
+
{
|
|
292
|
+
type: 'ADDED',
|
|
293
|
+
lines: [
|
|
294
|
+
{ destinationLine: 45, text: '<div style={{ padding: "20px", backgroundColor: "#fff" }}>' },
|
|
295
|
+
{ destinationLine: 52, text: '<div className="btn-wrapper" onClick={handleSave}>Save User</div>' },
|
|
296
|
+
{ destinationLine: 60, text: '{items.map((item, index) => <Item key={index} data={item} />)}' },
|
|
297
|
+
{ destinationLine: 68, text: '<button onClick={async () => fetchMore()}>Load More</button>' },
|
|
298
|
+
{ destinationLine: 75, text: 'if (userStatus === 5) {' }
|
|
299
|
+
]
|
|
300
|
+
}
|
|
301
|
+
]
|
|
302
|
+
}
|
|
303
|
+
],
|
|
304
|
+
'src/services/api.ts': [
|
|
305
|
+
{
|
|
306
|
+
segments: [
|
|
307
|
+
{
|
|
308
|
+
type: 'ADDED',
|
|
309
|
+
lines: [
|
|
310
|
+
{ destinationLine: 88, text: 'debugger;' },
|
|
311
|
+
{ destinationLine: 95, text: 'console.log("Fetch user response:", response.data);' }
|
|
312
|
+
]
|
|
313
|
+
}
|
|
314
|
+
]
|
|
315
|
+
}
|
|
316
|
+
]
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const reviewFiles = [];
|
|
320
|
+
let totalIssues = 0;
|
|
321
|
+
let criticalCount = 0;
|
|
322
|
+
let warningCount = 0;
|
|
323
|
+
let infoCount = 0;
|
|
324
|
+
|
|
325
|
+
for (const [filePath, hunks] of Object.entries(demoHunksMap)) {
|
|
326
|
+
const fileIssues = analyzeDiff(filePath, hunks);
|
|
327
|
+
if (fileIssues.length > 0) {
|
|
328
|
+
reviewFiles.push({ filePath, type: 'MODIFY', issues: fileIssues });
|
|
329
|
+
totalIssues += fileIssues.length;
|
|
330
|
+
criticalCount += fileIssues.filter(i => i.severity === 'critical').length;
|
|
331
|
+
warningCount += fileIssues.filter(i => i.severity === 'warning').length;
|
|
332
|
+
infoCount += fileIssues.filter(i => i.severity === 'info').length;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return res.json({
|
|
337
|
+
pr: {
|
|
338
|
+
title: 'Demo: Feature User Management & Code Standard Check',
|
|
339
|
+
description: 'Demo Pull Request showcasing all 9 PR Review Rules.',
|
|
340
|
+
author: 'Developer Alex',
|
|
341
|
+
sourceBranch: 'feature/user-management',
|
|
342
|
+
targetBranch: 'main',
|
|
343
|
+
projectKey: 'DEMO',
|
|
344
|
+
repositorySlug: 'demo-repo',
|
|
345
|
+
pullRequestId: '999'
|
|
346
|
+
},
|
|
347
|
+
stats: {
|
|
348
|
+
totalFilesChanged: 5,
|
|
349
|
+
filteredCount: 1,
|
|
350
|
+
reviewedCount: 4,
|
|
351
|
+
totalIssues,
|
|
352
|
+
criticalCount,
|
|
353
|
+
warningCount,
|
|
354
|
+
infoCount
|
|
355
|
+
},
|
|
356
|
+
filteredOut: ['package-lock.json'],
|
|
357
|
+
files: reviewFiles
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
204
361
|
const settings = readSettings();
|
|
205
362
|
const token = settings.bitbucketAccessToken;
|
|
206
363
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
1
2
|
import { Router } from 'express';
|
|
3
|
+
import { paths } from '../config.js';
|
|
2
4
|
import { saveAccessToken, saveClipboardDeduplicateMinutes, saveClipboardEnabled, saveClipboardImageEnabled, saveDefaultBrowser, saveDefaultEditor, saveDomain, saveJiraIssuePrefix, settingsStatus } from '../repositories/settings.js';
|
|
3
|
-
import { openInSystemBrowser, openInSystemEditor } from '../services/browser.js';
|
|
5
|
+
import { openInSystemBrowser, openInSystemEditor, openInSystemFolder } from '../services/browser.js';
|
|
4
6
|
import { selectDirectory } from '../services/dialog.js';
|
|
5
7
|
|
|
6
8
|
const router = Router();
|
|
@@ -38,6 +40,23 @@ router.post('/open-editor', (req, res) => {
|
|
|
38
40
|
openInSystemEditor(path.trim(), location ? String(location).trim() : '');
|
|
39
41
|
res.json({ success: true });
|
|
40
42
|
});
|
|
43
|
+
router.post('/open-folder', (req, res) => {
|
|
44
|
+
const { path } = req.body || {};
|
|
45
|
+
const targetPath = (typeof path === 'string' && path.trim()) ? path.trim() : paths.dataDir;
|
|
46
|
+
if (!existsSync(targetPath)) {
|
|
47
|
+
mkdirSync(targetPath, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
openInSystemFolder(targetPath);
|
|
50
|
+
res.json({ success: true });
|
|
51
|
+
});
|
|
52
|
+
router.post('/open-data-dir', (req, res) => {
|
|
53
|
+
const targetPath = paths.dataDir;
|
|
54
|
+
if (!existsSync(targetPath)) {
|
|
55
|
+
mkdirSync(targetPath, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
openInSystemFolder(targetPath);
|
|
58
|
+
res.json({ success: true });
|
|
59
|
+
});
|
|
41
60
|
router.post('/select-directory', async (_req, res) => {
|
|
42
61
|
try {
|
|
43
62
|
const result = await selectDirectory();
|
|
@@ -96,3 +96,15 @@ export function openInSystemEditor(path, location = '') {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
+
|
|
100
|
+
export function openInSystemFolder(targetPath) {
|
|
101
|
+
if (!targetPath || typeof targetPath !== 'string') return;
|
|
102
|
+
if (process.platform === 'darwin') {
|
|
103
|
+
execFile('open', [targetPath]);
|
|
104
|
+
} else if (process.platform === 'win32') {
|
|
105
|
+
execFile('cmd', ['/c', 'start', '', targetPath]);
|
|
106
|
+
} else {
|
|
107
|
+
execFile('xdg-open', [targetPath]);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|