bosun 0.41.2 → 0.41.4
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/.env.example +1 -1
- package/agent/agent-pool.mjs +9 -2
- package/agent/agent-prompt-catalog.mjs +971 -0
- package/agent/agent-prompts.mjs +2 -970
- package/agent/agent-supervisor.mjs +119 -6
- package/agent/autofix-git.mjs +33 -0
- package/agent/autofix-prompts.mjs +151 -0
- package/agent/autofix.mjs +11 -175
- package/agent/bosun-skills.mjs +3 -2
- package/bosun.config.example.json +17 -0
- package/bosun.schema.json +87 -188
- package/cli.mjs +34 -1
- package/config/config-doctor.mjs +5 -250
- package/config/config-file-names.mjs +5 -0
- package/config/config.mjs +89 -493
- package/config/executor-config.mjs +493 -0
- package/config/repo-root.mjs +1 -2
- package/config/workspace-health.mjs +242 -0
- package/git/git-safety.mjs +15 -0
- package/github/github-oauth-portal.mjs +46 -0
- package/infra/library-manager-utils.mjs +22 -0
- package/infra/library-manager-well-known-sources.mjs +578 -0
- package/infra/library-manager.mjs +512 -1030
- package/infra/monitor.mjs +35 -9
- package/infra/session-tracker.mjs +10 -7
- package/kanban/kanban-adapter.mjs +17 -1
- package/lib/codebase-audit-manifests.mjs +117 -0
- package/lib/codebase-audit.mjs +18 -115
- package/package.json +18 -3
- package/server/setup-web-server.mjs +58 -5
- package/server/ui-server.mjs +1394 -79
- package/shell/codex-config-file.mjs +178 -0
- package/shell/codex-config.mjs +538 -575
- package/task/task-cli.mjs +54 -3
- package/task/task-executor.mjs +143 -13
- package/task/task-store.mjs +409 -1
- package/telegram/telegram-bot.mjs +127 -0
- package/tools/apply-pr-suggestions.mjs +401 -0
- package/tools/syntax-check.mjs +28 -9
- package/ui/app.js +3 -14
- package/ui/components/kanban-board.js +227 -4
- package/ui/components/session-list.js +85 -5
- package/ui/demo-defaults.js +338 -84
- package/ui/demo.html +155 -0
- package/ui/modules/session-api.js +96 -0
- package/ui/modules/settings-schema.js +1 -2
- package/ui/modules/state.js +43 -3
- package/ui/setup.html +4 -5
- package/ui/styles/components.css +58 -4
- package/ui/tabs/agents.js +12 -15
- package/ui/tabs/control.js +1 -0
- package/ui/tabs/library.js +484 -22
- package/ui/tabs/manual-flows.js +105 -29
- package/ui/tabs/tasks.js +848 -141
- package/ui/tabs/telemetry.js +129 -11
- package/ui/tabs/workflow-canvas-utils.mjs +130 -0
- package/ui/tabs/workflows.js +293 -23
- package/voice/voice-tool-definitions.mjs +757 -0
- package/voice/voice-tools.mjs +34 -778
- package/workflow/manual-flow-audit.mjs +165 -0
- package/workflow/manual-flows.mjs +164 -259
- package/workflow/workflow-engine.mjs +147 -58
- package/workflow/workflow-nodes/definitions.mjs +1207 -0
- package/workflow/workflow-nodes/transforms.mjs +612 -0
- package/workflow/workflow-nodes.mjs +358 -63
- package/workflow/workflow-templates.mjs +313 -191
- package/workflow-templates/_helpers.mjs +154 -0
- package/workflow-templates/agents.mjs +61 -4
- package/workflow-templates/code-quality.mjs +7 -7
- package/workflow-templates/github.mjs +20 -10
- package/workflow-templates/task-batch.mjs +44 -11
- package/workflow-templates/task-lifecycle.mjs +31 -6
- package/workspace/worktree-manager.mjs +277 -3
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* apply-pr-suggestions.mjs — Batch-apply pending code-review suggestions on a PR.
|
|
3
|
+
*
|
|
4
|
+
* Fetches all review comments containing ```suggestion blocks, applies them to
|
|
5
|
+
* the affected files, and creates a single commit on the PR branch via the
|
|
6
|
+
* GitHub Git Data API (blobs → tree → commit → ref update).
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node tools/apply-pr-suggestions.mjs [--owner virtengine] [--repo bosun] <pr-number>
|
|
10
|
+
* bosun apply-suggestions <pr-number>
|
|
11
|
+
*
|
|
12
|
+
* Options:
|
|
13
|
+
* --owner Repository owner (default: auto-detect from git remote)
|
|
14
|
+
* --repo Repository name (default: auto-detect from git remote)
|
|
15
|
+
* --dry-run Show what would be applied without committing
|
|
16
|
+
* --author Only apply suggestions from this author (e.g. "copilot[bot]")
|
|
17
|
+
* --json Output result as JSON
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { execSync } from "node:child_process";
|
|
21
|
+
|
|
22
|
+
// ── GitHub API helper ─────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
function getToken() {
|
|
25
|
+
// Try gh CLI first, then env vars
|
|
26
|
+
try {
|
|
27
|
+
return execSync("gh auth token", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
28
|
+
} catch {
|
|
29
|
+
return process.env.GITHUB_TOKEN || process.env.GH_TOKEN || "";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function ghApiFetch(path, options = {}) {
|
|
34
|
+
const token = getToken();
|
|
35
|
+
if (!token) throw new Error("No GitHub token found. Run `gh auth login` or set GITHUB_TOKEN.");
|
|
36
|
+
const url = path.startsWith("http") ? path : `https://api.github.com/${path}`;
|
|
37
|
+
const resp = await fetch(url, {
|
|
38
|
+
method: options.method || "GET",
|
|
39
|
+
headers: {
|
|
40
|
+
Authorization: `Bearer ${token}`,
|
|
41
|
+
Accept: "application/vnd.github+json",
|
|
42
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
43
|
+
...(options.body ? { "Content-Type": "application/json" } : {}),
|
|
44
|
+
},
|
|
45
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
46
|
+
});
|
|
47
|
+
if (!resp.ok) {
|
|
48
|
+
const text = await resp.text();
|
|
49
|
+
throw new Error(`GitHub API ${resp.status} on ${path}: ${text}`);
|
|
50
|
+
}
|
|
51
|
+
return resp.json();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Paginate a list endpoint, collecting all pages. */
|
|
55
|
+
async function ghApiPaginate(path) {
|
|
56
|
+
const results = [];
|
|
57
|
+
let page = 1;
|
|
58
|
+
while (true) {
|
|
59
|
+
const sep = path.includes("?") ? "&" : "?";
|
|
60
|
+
const data = await ghApiFetch(`${path}${sep}per_page=100&page=${page}`);
|
|
61
|
+
if (!Array.isArray(data) || data.length === 0) break;
|
|
62
|
+
results.push(...data);
|
|
63
|
+
if (data.length < 100) break;
|
|
64
|
+
page++;
|
|
65
|
+
}
|
|
66
|
+
return results;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Suggestion parser ─────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
const SUGGESTION_RE = /```suggestion\r?\n([\s\S]*?)```/g;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse review comments into structured suggestion objects.
|
|
75
|
+
* @param {Array} comments - PR review comments from GitHub API
|
|
76
|
+
* @param {string} [authorFilter] - Only include suggestions from this author
|
|
77
|
+
* @returns {Array<{commentId, path, startLine, endLine, suggestedCode, author, url}>}
|
|
78
|
+
*/
|
|
79
|
+
function parseSuggestions(comments, authorFilter) {
|
|
80
|
+
const suggestions = [];
|
|
81
|
+
for (const comment of comments) {
|
|
82
|
+
if (authorFilter && comment.user?.login !== authorFilter) continue;
|
|
83
|
+
|
|
84
|
+
const body = comment.body || "";
|
|
85
|
+
const matches = [...body.matchAll(SUGGESTION_RE)];
|
|
86
|
+
if (matches.length === 0) continue;
|
|
87
|
+
|
|
88
|
+
for (const match of matches) {
|
|
89
|
+
const suggestedCode = match[1];
|
|
90
|
+
// Single-line suggestions have line only; multi-line have start_line + line
|
|
91
|
+
const endLine = comment.line ?? comment.original_line;
|
|
92
|
+
const startLine = comment.start_line ?? comment.original_start_line ?? endLine;
|
|
93
|
+
if (!endLine || !comment.path) continue;
|
|
94
|
+
|
|
95
|
+
suggestions.push({
|
|
96
|
+
commentId: comment.id,
|
|
97
|
+
path: comment.path,
|
|
98
|
+
startLine,
|
|
99
|
+
endLine,
|
|
100
|
+
suggestedCode,
|
|
101
|
+
author: comment.user?.login || "unknown",
|
|
102
|
+
url: comment.html_url,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return suggestions;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Group suggestions by file path and sort within each file by endLine descending
|
|
111
|
+
* so we can apply from bottom-to-top without shifting line numbers.
|
|
112
|
+
*/
|
|
113
|
+
function groupByFile(suggestions) {
|
|
114
|
+
const groups = new Map();
|
|
115
|
+
for (const s of suggestions) {
|
|
116
|
+
if (!groups.has(s.path)) groups.set(s.path, []);
|
|
117
|
+
groups.get(s.path).push(s);
|
|
118
|
+
}
|
|
119
|
+
// Sort each group by endLine DESC so bottom-up application preserves indices
|
|
120
|
+
for (const [, arr] of groups) {
|
|
121
|
+
arr.sort((a, b) => b.endLine - a.endLine);
|
|
122
|
+
}
|
|
123
|
+
return groups;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check for overlapping suggestions in a sorted-descending list.
|
|
128
|
+
* Returns only non-overlapping suggestions (keeps first = highest line number).
|
|
129
|
+
*/
|
|
130
|
+
function removeOverlaps(sortedDesc) {
|
|
131
|
+
const kept = [];
|
|
132
|
+
let minLine = Infinity;
|
|
133
|
+
for (const s of sortedDesc) {
|
|
134
|
+
if (s.endLine < minLine) {
|
|
135
|
+
kept.push(s);
|
|
136
|
+
minLine = s.startLine;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return kept;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Apply suggestion replacements to file content.
|
|
144
|
+
* Suggestions must be sorted by endLine descending.
|
|
145
|
+
*/
|
|
146
|
+
function applyToContent(content, sortedSuggestions) {
|
|
147
|
+
const lines = content.split("\n");
|
|
148
|
+
for (const s of sortedSuggestions) {
|
|
149
|
+
// Lines are 1-indexed; convert to 0-indexed for splice
|
|
150
|
+
const startIdx = s.startLine - 1;
|
|
151
|
+
const count = s.endLine - s.startLine + 1;
|
|
152
|
+
// Suggestion code may end with trailing newline from the code fence; strip it
|
|
153
|
+
let code = s.suggestedCode;
|
|
154
|
+
if (code.endsWith("\n")) code = code.slice(0, -1);
|
|
155
|
+
const newLines = code.split("\n");
|
|
156
|
+
lines.splice(startIdx, count, ...newLines);
|
|
157
|
+
}
|
|
158
|
+
return lines.join("\n");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Git Data API commit helper ────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Create a single commit with all file changes on a branch using the Git Data API.
|
|
165
|
+
* @returns {string} New commit SHA
|
|
166
|
+
*/
|
|
167
|
+
async function createBatchCommit(owner, repo, branch, fileChanges, message) {
|
|
168
|
+
// 1. Get current branch head
|
|
169
|
+
const ref = await ghApiFetch(`repos/${owner}/${repo}/git/refs/heads/${branch}`);
|
|
170
|
+
const headSha = ref.object.sha;
|
|
171
|
+
|
|
172
|
+
// 2. Get the tree SHA of the head commit
|
|
173
|
+
const headCommit = await ghApiFetch(`repos/${owner}/${repo}/git/commits/${headSha}`);
|
|
174
|
+
const baseTreeSha = headCommit.tree.sha;
|
|
175
|
+
|
|
176
|
+
// 3. Create blobs for each changed file
|
|
177
|
+
const treeItems = [];
|
|
178
|
+
for (const [path, content] of fileChanges) {
|
|
179
|
+
const blob = await ghApiFetch(`repos/${owner}/${repo}/git/blobs`, {
|
|
180
|
+
method: "POST",
|
|
181
|
+
body: { content, encoding: "utf-8" },
|
|
182
|
+
});
|
|
183
|
+
treeItems.push({ path, mode: "100644", type: "blob", sha: blob.sha });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 4. Create new tree
|
|
187
|
+
const newTree = await ghApiFetch(`repos/${owner}/${repo}/git/trees`, {
|
|
188
|
+
method: "POST",
|
|
189
|
+
body: { base_tree: baseTreeSha, tree: treeItems },
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// 5. Create commit
|
|
193
|
+
const newCommit = await ghApiFetch(`repos/${owner}/${repo}/git/commits`, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
body: { message, tree: newTree.sha, parents: [headSha] },
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// 6. Update the branch ref
|
|
199
|
+
await ghApiFetch(`repos/${owner}/${repo}/git/refs/heads/${branch}`, {
|
|
200
|
+
method: "PATCH",
|
|
201
|
+
body: { sha: newCommit.sha, force: false },
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return newCommit.sha;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── Repo detection ────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
function detectOwnerRepo() {
|
|
210
|
+
try {
|
|
211
|
+
const url = execSync("git config --get remote.origin.url", {
|
|
212
|
+
encoding: "utf8",
|
|
213
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
214
|
+
}).trim();
|
|
215
|
+
// https://github.com/owner/repo.git or git@github.com:owner/repo.git
|
|
216
|
+
const m = url.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
|
|
217
|
+
if (m) return { owner: m[1], repo: m[2] };
|
|
218
|
+
} catch { /* ignore */ }
|
|
219
|
+
return { owner: "", repo: "" };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Apply all pending review suggestions on a PR.
|
|
226
|
+
* @param {Object} opts
|
|
227
|
+
* @param {string} opts.owner - Repo owner
|
|
228
|
+
* @param {string} opts.repo - Repo name
|
|
229
|
+
* @param {number} opts.prNumber - PR number
|
|
230
|
+
* @param {boolean} [opts.dryRun=false] - Don't commit, just show what would be applied
|
|
231
|
+
* @param {string} [opts.author] - Only apply suggestions from this author
|
|
232
|
+
* @returns {Object} { applied, skipped, commitSha }
|
|
233
|
+
*/
|
|
234
|
+
export async function applyPrSuggestions({ owner, repo, prNumber, dryRun = false, author }) {
|
|
235
|
+
// 1. Fetch PR info to get the branch name
|
|
236
|
+
const pr = await ghApiFetch(`repos/${owner}/${repo}/pulls/${prNumber}`);
|
|
237
|
+
const branch = pr.head.ref;
|
|
238
|
+
const headSha = pr.head.sha;
|
|
239
|
+
|
|
240
|
+
// 2. Fetch all review comments
|
|
241
|
+
const comments = await ghApiPaginate(`repos/${owner}/${repo}/pulls/${prNumber}/comments`);
|
|
242
|
+
|
|
243
|
+
// 3. Parse suggestions
|
|
244
|
+
const suggestions = parseSuggestions(comments, author);
|
|
245
|
+
if (suggestions.length === 0) {
|
|
246
|
+
return { applied: 0, skipped: 0, commitSha: null, message: "No pending suggestions found." };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 4. Group by file and remove overlaps
|
|
250
|
+
const groups = groupByFile(suggestions);
|
|
251
|
+
const fileChanges = new Map(); // path → new content
|
|
252
|
+
let applied = 0;
|
|
253
|
+
let skipped = 0;
|
|
254
|
+
const appliedDetails = [];
|
|
255
|
+
|
|
256
|
+
for (const [path, fileSuggestions] of groups) {
|
|
257
|
+
// Fetch current file content from the PR branch head
|
|
258
|
+
let content;
|
|
259
|
+
try {
|
|
260
|
+
const fileData = await ghApiFetch(
|
|
261
|
+
`repos/${owner}/${repo}/contents/${encodeURIComponent(path)}?ref=${headSha}`
|
|
262
|
+
);
|
|
263
|
+
content = Buffer.from(fileData.content, "base64").toString("utf8");
|
|
264
|
+
} catch (err) {
|
|
265
|
+
console.error(` ⚠ Could not fetch ${path}: ${err.message}`);
|
|
266
|
+
skipped += fileSuggestions.length;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Remove overlapping suggestions
|
|
271
|
+
const valid = removeOverlaps(fileSuggestions);
|
|
272
|
+
skipped += fileSuggestions.length - valid.length;
|
|
273
|
+
|
|
274
|
+
// Check if suggestions are still applicable (lines haven't changed)
|
|
275
|
+
const contentLines = content.split("\n");
|
|
276
|
+
const applicable = valid.filter((s) => {
|
|
277
|
+
if (s.endLine > contentLines.length) return false;
|
|
278
|
+
return true;
|
|
279
|
+
});
|
|
280
|
+
skipped += valid.length - applicable.length;
|
|
281
|
+
|
|
282
|
+
if (applicable.length === 0) continue;
|
|
283
|
+
|
|
284
|
+
// Apply changes
|
|
285
|
+
const newContent = applyToContent(content, applicable);
|
|
286
|
+
if (newContent === content) {
|
|
287
|
+
skipped += applicable.length;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
fileChanges.set(path, newContent);
|
|
292
|
+
applied += applicable.length;
|
|
293
|
+
for (const s of applicable) {
|
|
294
|
+
appliedDetails.push({
|
|
295
|
+
path: s.path,
|
|
296
|
+
lines: `${s.startLine}-${s.endLine}`,
|
|
297
|
+
author: s.author,
|
|
298
|
+
url: s.url,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (applied === 0) {
|
|
304
|
+
return { applied: 0, skipped, commitSha: null, message: "All suggestions were already applied or inapplicable." };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (dryRun) {
|
|
308
|
+
return { applied, skipped, commitSha: null, dryRun: true, details: appliedDetails };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 5. Create batch commit
|
|
312
|
+
const paths = [...fileChanges.keys()];
|
|
313
|
+
const commitMsg = applied === 1
|
|
314
|
+
? `Apply code review suggestion\n\nApply suggestion in ${paths[0]}`
|
|
315
|
+
: `Apply ${applied} code review suggestions\n\nFiles: ${paths.join(", ")}`;
|
|
316
|
+
|
|
317
|
+
const commitSha = await createBatchCommit(owner, repo, branch, fileChanges, commitMsg);
|
|
318
|
+
|
|
319
|
+
return { applied, skipped, commitSha, details: appliedDetails };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ── CLI entry point ───────────────────────────────────────────────────────────
|
|
323
|
+
|
|
324
|
+
async function main() {
|
|
325
|
+
const args = process.argv.slice(2);
|
|
326
|
+
|
|
327
|
+
let owner = "";
|
|
328
|
+
let repo = "";
|
|
329
|
+
let prNumber = 0;
|
|
330
|
+
let dryRun = false;
|
|
331
|
+
let author = "";
|
|
332
|
+
let jsonOut = false;
|
|
333
|
+
|
|
334
|
+
for (let i = 0; i < args.length; i++) {
|
|
335
|
+
const arg = args[i];
|
|
336
|
+
if (arg === "--owner" && args[i + 1]) { owner = args[++i]; continue; }
|
|
337
|
+
if (arg === "--repo" && args[i + 1]) { repo = args[++i]; continue; }
|
|
338
|
+
if (arg === "--author" && args[i + 1]) { author = args[++i]; continue; }
|
|
339
|
+
if (arg === "--dry-run") { dryRun = true; continue; }
|
|
340
|
+
if (arg === "--json") { jsonOut = true; continue; }
|
|
341
|
+
if (arg === "--help" || arg === "-h") {
|
|
342
|
+
console.log("Usage: node tools/apply-pr-suggestions.mjs [options] <pr-number>");
|
|
343
|
+
console.log("");
|
|
344
|
+
console.log("Options:");
|
|
345
|
+
console.log(" --owner <owner> Repo owner (default: auto-detect)");
|
|
346
|
+
console.log(" --repo <repo> Repo name (default: auto-detect)");
|
|
347
|
+
console.log(" --author <login> Only apply from this author (e.g. copilot[bot])");
|
|
348
|
+
console.log(" --dry-run Show what would be applied without committing");
|
|
349
|
+
console.log(" --json Output result as JSON");
|
|
350
|
+
process.exit(0);
|
|
351
|
+
}
|
|
352
|
+
if (/^\d+$/.test(arg)) { prNumber = parseInt(arg, 10); continue; }
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!prNumber) {
|
|
356
|
+
console.error("Error: PR number is required.");
|
|
357
|
+
console.error("Usage: node tools/apply-pr-suggestions.mjs <pr-number>");
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!owner || !repo) {
|
|
362
|
+
const detected = detectOwnerRepo();
|
|
363
|
+
owner = owner || detected.owner;
|
|
364
|
+
repo = repo || detected.repo;
|
|
365
|
+
}
|
|
366
|
+
if (!owner || !repo) {
|
|
367
|
+
console.error("Error: Could not detect owner/repo. Use --owner and --repo flags.");
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
const result = await applyPrSuggestions({ owner, repo, prNumber, dryRun, author: author || undefined });
|
|
373
|
+
|
|
374
|
+
if (jsonOut) {
|
|
375
|
+
console.log(JSON.stringify(result, null, 2));
|
|
376
|
+
} else {
|
|
377
|
+
if (result.dryRun) {
|
|
378
|
+
console.log(`\n 🔍 Dry run — ${result.applied} suggestion(s) would be applied:`);
|
|
379
|
+
for (const d of result.details || []) {
|
|
380
|
+
console.log(` ${d.path}:${d.lines} (by ${d.author})`);
|
|
381
|
+
}
|
|
382
|
+
} else if (result.commitSha) {
|
|
383
|
+
console.log(`\n ✅ Applied ${result.applied} suggestion(s) in commit ${result.commitSha.slice(0, 8)}`);
|
|
384
|
+
if (result.skipped) console.log(` (${result.skipped} skipped — overlapping or inapplicable)`);
|
|
385
|
+
for (const d of result.details || []) {
|
|
386
|
+
console.log(` ${d.path}:${d.lines}`);
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
console.log(`\n ℹ ${result.message}`);
|
|
390
|
+
}
|
|
391
|
+
console.log("");
|
|
392
|
+
}
|
|
393
|
+
} catch (err) {
|
|
394
|
+
console.error(`Error: ${err.message}`);
|
|
395
|
+
process.exit(1);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Run as CLI script
|
|
400
|
+
const isDirectRun = process.argv[1]?.replace(/\\/g, "/").endsWith("tools/apply-pr-suggestions.mjs");
|
|
401
|
+
if (isDirectRun) main();
|
package/tools/syntax-check.mjs
CHANGED
|
@@ -51,14 +51,30 @@ function validateModuleSyntax(filePath) {
|
|
|
51
51
|
new vm.SourceTextModule(source, { identifier: filePath });
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
function validateBrowserModuleSyntax(filePath) {
|
|
55
|
+
const source = readFileSync(filePath, "utf8");
|
|
56
|
+
const mod = new vm.SourceTextModule(source, { identifier: filePath });
|
|
57
|
+
let hasTLA = false;
|
|
58
|
+
const tlaProp = mod.hasTopLevelAwait;
|
|
59
|
+
if (typeof tlaProp === "function") {
|
|
60
|
+
hasTLA = !!tlaProp.call(mod);
|
|
61
|
+
} else if (typeof tlaProp === "boolean") {
|
|
62
|
+
hasTLA = tlaProp;
|
|
63
|
+
}
|
|
64
|
+
if (hasTLA) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"Top-level await is not allowed in browser-served modules because embedded WebViews can fail with 'Unexpected reserved word'.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
/**
|
|
55
72
|
* Parse a JS file using the Module compiler.
|
|
56
73
|
* Catches syntax errors such as unterminated statements or bad tokens.
|
|
57
74
|
* UI files use ES module syntax (import/export) via browser importmaps.
|
|
58
75
|
*/
|
|
59
76
|
function validateScriptSyntax(filePath) {
|
|
60
|
-
|
|
61
|
-
new vm.SourceTextModule(source, { identifier: filePath });
|
|
77
|
+
validateBrowserModuleSyntax(filePath);
|
|
62
78
|
}
|
|
63
79
|
|
|
64
80
|
async function main() {
|
|
@@ -87,14 +103,17 @@ async function main() {
|
|
|
87
103
|
process.exit(1);
|
|
88
104
|
}
|
|
89
105
|
|
|
90
|
-
// ── Phase 2: Parse-check
|
|
91
|
-
// These are
|
|
92
|
-
//
|
|
93
|
-
const
|
|
94
|
-
|
|
106
|
+
// ── Phase 2: Parse-check browser JavaScript files ─────────────────────
|
|
107
|
+
// These files are loaded directly in the browser via import maps. Keep
|
|
108
|
+
// them free of syntax that older embedded WebViews reject at parse time.
|
|
109
|
+
const browserRoots = [
|
|
110
|
+
resolve(process.cwd(), "ui"),
|
|
111
|
+
resolve(process.cwd(), "site", "ui"),
|
|
112
|
+
];
|
|
113
|
+
const browserFiles = [...new Set(browserRoots.flatMap((dir) => listJsFilesRecursive(dir)))];
|
|
95
114
|
let uiFailed = false;
|
|
96
115
|
|
|
97
|
-
for (const filePath of
|
|
116
|
+
for (const filePath of browserFiles) {
|
|
98
117
|
try {
|
|
99
118
|
validateScriptSyntax(filePath);
|
|
100
119
|
} catch (error) {
|
|
@@ -110,7 +129,7 @@ async function main() {
|
|
|
110
129
|
process.exit(1);
|
|
111
130
|
}
|
|
112
131
|
|
|
113
|
-
console.log(`Syntax OK: ${files.length} modules + ${
|
|
132
|
+
console.log(`Syntax OK: ${files.length} modules + ${browserFiles.length} browser files checked`);
|
|
114
133
|
}
|
|
115
134
|
|
|
116
135
|
main().catch((error) => {
|
package/ui/app.js
CHANGED
|
@@ -249,6 +249,8 @@ import {
|
|
|
249
249
|
apiFetch,
|
|
250
250
|
connectWebSocket,
|
|
251
251
|
disconnectWebSocket,
|
|
252
|
+
wsLatency,
|
|
253
|
+
wsReconnectIn,
|
|
252
254
|
wsConnected,
|
|
253
255
|
loadingCount,
|
|
254
256
|
} from "./modules/api.js";
|
|
@@ -262,6 +264,7 @@ import {
|
|
|
262
264
|
initWsInvalidationListener,
|
|
263
265
|
loadNotificationPrefs,
|
|
264
266
|
applyStoredDefaults,
|
|
267
|
+
dataFreshness,
|
|
265
268
|
hasPendingChanges,
|
|
266
269
|
} from "./modules/state.js";
|
|
267
270
|
import {
|
|
@@ -314,20 +317,6 @@ import { WorkflowsTab } from "./tabs/workflows.js";
|
|
|
314
317
|
import { LibraryTab } from "./tabs/library.js";
|
|
315
318
|
import { ManualFlowsTab } from "./tabs/manual-flows.js";
|
|
316
319
|
|
|
317
|
-
/* ── Placeholder signals for connection quality (may be provided by api.js) ── */
|
|
318
|
-
let wsLatency = signal(null);
|
|
319
|
-
let wsReconnectIn = signal(null);
|
|
320
|
-
let dataFreshness = signal(null);
|
|
321
|
-
try {
|
|
322
|
-
const apiMod = await import("./modules/api.js");
|
|
323
|
-
if (apiMod.wsLatency) wsLatency = apiMod.wsLatency;
|
|
324
|
-
if (apiMod.wsReconnectIn) wsReconnectIn = apiMod.wsReconnectIn;
|
|
325
|
-
} catch { /* use placeholder signals */ }
|
|
326
|
-
try {
|
|
327
|
-
const stateMod = await import("./modules/state.js");
|
|
328
|
-
if (stateMod.dataFreshness) dataFreshness = stateMod.dataFreshness;
|
|
329
|
-
} catch { /* use placeholder signals */ }
|
|
330
|
-
|
|
331
320
|
/* ── Shared components ── */
|
|
332
321
|
|
|
333
322
|
/**
|