diff --git a/multi-review/.gitignore b/multi-review/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/multi-review/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/multi-review/action.yml b/multi-review/action.yml new file mode 100644 index 0000000..c391af4 --- /dev/null +++ b/multi-review/action.yml @@ -0,0 +1,264 @@ +name: OpenCode Multi-Review +description: Multi-agent parallel code review using OpenCode SDK with coordinator synthesis. + +inputs: + install-url: + description: Installer URL used to bootstrap OpenCode. + required: false + default: https://opencode.ai/install + install-dir: + description: Directory where the opencode binary should be installed. + required: false + default: "" + xdg-cache-home: + description: Dedicated XDG cache directory for OpenCode. + required: false + default: "" + cache: + description: Cache the install and XDG cache directories with actions/cache. + required: false + default: "true" + cache-key: + description: Cache key suffix used to invalidate installer-based caches. + required: false + default: v1 + install-attempts: + description: Number of installer retry attempts. + required: false + default: "3" + allow-preinstalled: + description: Reuse an existing opencode found on PATH instead of forcing installer bootstrap. + required: false + default: "false" + version: + description: Minimum required OpenCode version (semver). Use "none" to disable version checking. + required: false + default: "" + working-directory: + description: Optional working directory before running review. + required: false + default: "" + timeout-seconds: + description: Global timeout for all reviewers in seconds. Set 0 to disable. + required: false + default: "900" + model: + description: Model to use for all reviewers and coordinator (format: provider/model). + required: false + default: "" + default-team: + description: Comma-separated team definition (e.g. "quality:1,security:1,performance:1"). + required: false + default: "" + coordinator-timeout-seconds: + description: Timeout in seconds for the coordinator synthesis step. + required: false + default: "300" + coordinator-prompt: + description: Custom coordinator prompt template. Use {{REVIEWS}} as placeholder for reviewer outputs. + required: false + default: "" + reasoning-effort: + description: Reasoning effort level for the model agent. Allowed values are low, medium, high, max. + required: false + default: "max" + enable-thinking: + description: Enable thinking mode for the model agent. + required: false + default: "true" + github-token: + description: GitHub token for posting PR comments. + required: false + default: "" + zhipu-api-key: + description: Zhipu AI API key. + required: false + default: "" + opencode-go-api-key: + description: OpenCode Go API key. + required: false + default: "" + deepseek-api-key: + description: DeepSeek API key. + required: false + default: "" + extra-env: + description: >- + Extra environment variables. Multi-line KEY=VALUE pairs, one per line. + Empty lines and lines starting with '#' are ignored. + required: false + default: "" + cleanup-error-comments: + description: Automatically delete error comments after a failed run. + required: false + default: "true" + +runs: + using: composite + steps: + - if: ${{ runner.os != 'Linux' }} + shell: bash + run: | + set -euo pipefail + printf 'multi-review currently supports Linux runners only\n' >&2 + exit 1 + + - id: version + shell: bash + run: | + set -euo pipefail + effective="${{ inputs.version }}" + if [[ "$effective" == "none" ]]; then + effective="" + elif [[ -z "$effective" ]]; then + default_file="${{ github.action_path }}/../setup-opencode/default-version" + if [[ ! -f "$default_file" ]]; then + printf 'error: default-version file not found at %s\n' "$default_file" >&2 + exit 1 + fi + effective="$(tr -d '[:space:]' < "$default_file")" + if [[ ! "$effective" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + printf 'error: invalid version in %s: %s\n' "$default_file" "$(cat "$default_file")" >&2 + exit 1 + fi + fi + printf 'version=%s\n' "$effective" >>"$GITHUB_OUTPUT" + + - id: paths + shell: bash + env: + INPUT_INSTALL_DIR: ${{ inputs.install-dir }} + INPUT_XDG_CACHE_HOME: ${{ inputs.xdg-cache-home }} + run: | + set -euo pipefail + install_dir="$INPUT_INSTALL_DIR" + xdg_cache_home="$INPUT_XDG_CACHE_HOME" + if [[ -z "$install_dir" ]]; then + install_dir="${RUNNER_TOOL_CACHE:-$HOME/.cache}/opencode/bin" + fi + if [[ -z "$xdg_cache_home" ]]; then + xdg_cache_home="${RUNNER_TOOL_CACHE:-$HOME/.cache}/opencode/cache" + fi + printf 'install_dir=%s\n' "$install_dir" >>"$GITHUB_OUTPUT" + printf 'xdg_cache_home=%s\n' "$xdg_cache_home" >>"$GITHUB_OUTPUT" + + - id: key + shell: bash + env: + INPUT_INSTALL_URL: ${{ inputs.install-url }} + run: | + set -euo pipefail + install_url_hash="$(printf '%s' "$INPUT_INSTALL_URL" | sha256sum | cut -d' ' -f1)" + printf 'install_url_hash=%s\n' "$install_url_hash" >>"$GITHUB_OUTPUT" + + - id: cache + if: ${{ inputs.cache == 'true' }} + uses: actions/cache@v5 + with: + path: | + ${{ steps.paths.outputs.install_dir }} + ${{ steps.paths.outputs.xdg_cache_home }} + key: multi-review-opencode-${{ runner.os }}-${{ runner.arch }}-${{ steps.key.outputs.install_url_hash }}-${{ steps.version.outputs.version }}-${{ inputs.cache-key }} + + - shell: bash + env: + OPENCODE_INSTALL_DIR: ${{ steps.paths.outputs.install_dir }} + XDG_CACHE_HOME: ${{ steps.paths.outputs.xdg_cache_home }} + OPENCODE_INSTALL_URL: ${{ inputs.install-url }} + OPENCODE_INSTALL_ATTEMPTS: ${{ inputs.install-attempts }} + OPENCODE_ALLOW_PREINSTALLED: ${{ inputs.allow-preinstalled }} + OPENCODE_MIN_VERSION: ${{ steps.version.outputs.version }} + run: ${{ github.action_path }}/../setup-opencode/install-opencode.sh + + - id: pr-context + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + run: | + set -euo pipefail + ref="${GITHUB_REF:-}" + if [[ "$ref" =~ refs/pull/([0-9]+)/merge ]]; then + pr="${BASH_REMATCH[1]}" + gh pr diff "$pr" > "${RUNNER_TEMP}/.pr-diff.txt" 2>/dev/null || echo "" > "${RUNNER_TEMP}/.pr-diff.txt" + gh pr view "$pr" --json title,body > "${RUNNER_TEMP}/.pr-meta.json" 2>/dev/null || echo "{}" > "${RUNNER_TEMP}/.pr-meta.json" + echo "pr_number=$pr" >> "$GITHUB_OUTPUT" + echo "Found PR #$pr" + else + echo "" > "${RUNNER_TEMP}/.pr-diff.txt" + echo "{}" > "${RUNNER_TEMP}/.pr-meta.json" + echo "pr_number=" >> "$GITHUB_OUTPUT" + fi + + - shell: bash + env: + OPENCODE_REASONING_EFFORT: ${{ inputs.reasoning-effort }} + OPENCODE_ENABLE_THINKING: ${{ inputs.enable-thinking }} + run: | + set -euo pipefail + effort="${OPENCODE_REASONING_EFFORT:-max}" + thinking="${OPENCODE_ENABLE_THINKING:-true}" + thinking_json="null" + if [[ "$thinking" == "true" ]]; then + thinking_json='{"type":"enabled"}' + fi + cat > opencode.json </dev/null 2>&1; then + printf 'node is required but not installed on this runner\n' >&2 + exit 1 + fi + if [[ -n "$MULTI_REVIEW_WORKING_DIRECTORY" ]]; then + cd "$MULTI_REVIEW_WORKING_DIRECTORY" + fi + node "${{ github.action_path }}/dist/index.js" diff --git a/multi-review/dist/index.js b/multi-review/dist/index.js new file mode 100644 index 0000000..10fa45a --- /dev/null +++ b/multi-review/dist/index.js @@ -0,0 +1,2645 @@ +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { + get: (a, b) => (typeof require !== "undefined" ? require : a)[b] +}) : x)(function(x) { + if (typeof require !== "undefined") return require.apply(this, arguments); + throw Error('Dynamic require of "' + x + '" is not supported'); +}); +var __commonJS = (cb, mod) => function __require2() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); + +// node_modules/isexe/windows.js +var require_windows = __commonJS({ + "node_modules/isexe/windows.js"(exports, module) { + "use strict"; + module.exports = isexe; + isexe.sync = sync; + var fs = __require("fs"); + function checkPathExt(path, options) { + var pathext = options.pathExt !== void 0 ? options.pathExt : process.env.PATHEXT; + if (!pathext) { + return true; + } + pathext = pathext.split(";"); + if (pathext.indexOf("") !== -1) { + return true; + } + for (var i = 0; i < pathext.length; i++) { + var p = pathext[i].toLowerCase(); + if (p && path.substr(-p.length).toLowerCase() === p) { + return true; + } + } + return false; + } + function checkStat(stat, path, options) { + if (!stat.isSymbolicLink() && !stat.isFile()) { + return false; + } + return checkPathExt(path, options); + } + function isexe(path, options, cb) { + fs.stat(path, function(er, stat) { + cb(er, er ? false : checkStat(stat, path, options)); + }); + } + function sync(path, options) { + return checkStat(fs.statSync(path), path, options); + } + } +}); + +// node_modules/isexe/mode.js +var require_mode = __commonJS({ + "node_modules/isexe/mode.js"(exports, module) { + "use strict"; + module.exports = isexe; + isexe.sync = sync; + var fs = __require("fs"); + function isexe(path, options, cb) { + fs.stat(path, function(er, stat) { + cb(er, er ? false : checkStat(stat, options)); + }); + } + function sync(path, options) { + return checkStat(fs.statSync(path), options); + } + function checkStat(stat, options) { + return stat.isFile() && checkMode(stat, options); + } + function checkMode(stat, options) { + var mod = stat.mode; + var uid = stat.uid; + var gid = stat.gid; + var myUid = options.uid !== void 0 ? options.uid : process.getuid && process.getuid(); + var myGid = options.gid !== void 0 ? options.gid : process.getgid && process.getgid(); + var u = parseInt("100", 8); + var g = parseInt("010", 8); + var o = parseInt("001", 8); + var ug = u | g; + var ret = mod & o || mod & g && gid === myGid || mod & u && uid === myUid || mod & ug && myUid === 0; + return ret; + } + } +}); + +// node_modules/isexe/index.js +var require_isexe = __commonJS({ + "node_modules/isexe/index.js"(exports, module) { + "use strict"; + var fs = __require("fs"); + var core; + if (process.platform === "win32" || global.TESTING_WINDOWS) { + core = require_windows(); + } else { + core = require_mode(); + } + module.exports = isexe; + isexe.sync = sync; + function isexe(path, options, cb) { + if (typeof options === "function") { + cb = options; + options = {}; + } + if (!cb) { + if (typeof Promise !== "function") { + throw new TypeError("callback not provided"); + } + return new Promise(function(resolve2, reject) { + isexe(path, options || {}, function(er, is) { + if (er) { + reject(er); + } else { + resolve2(is); + } + }); + }); + } + core(path, options || {}, function(er, is) { + if (er) { + if (er.code === "EACCES" || options && options.ignoreErrors) { + er = null; + is = false; + } + } + cb(er, is); + }); + } + function sync(path, options) { + try { + return core.sync(path, options || {}); + } catch (er) { + if (options && options.ignoreErrors || er.code === "EACCES") { + return false; + } else { + throw er; + } + } + } + } +}); + +// node_modules/which/which.js +var require_which = __commonJS({ + "node_modules/which/which.js"(exports, module) { + "use strict"; + var isWindows = process.platform === "win32" || process.env.OSTYPE === "cygwin" || process.env.OSTYPE === "msys"; + var path = __require("path"); + var COLON = isWindows ? ";" : ":"; + var isexe = require_isexe(); + var getNotFoundError = (cmd) => Object.assign(new Error(`not found: ${cmd}`), { code: "ENOENT" }); + var getPathInfo = (cmd, opt) => { + const colon = opt.colon || COLON; + const pathEnv = cmd.match(/\//) || isWindows && cmd.match(/\\/) ? [""] : [ + // windows always checks the cwd first + ...isWindows ? [process.cwd()] : [], + ...(opt.path || process.env.PATH || /* istanbul ignore next: very unusual */ + "").split(colon) + ]; + const pathExtExe = isWindows ? opt.pathExt || process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM" : ""; + const pathExt = isWindows ? pathExtExe.split(colon) : [""]; + if (isWindows) { + if (cmd.indexOf(".") !== -1 && pathExt[0] !== "") + pathExt.unshift(""); + } + return { + pathEnv, + pathExt, + pathExtExe + }; + }; + var which = (cmd, opt, cb) => { + if (typeof opt === "function") { + cb = opt; + opt = {}; + } + if (!opt) + opt = {}; + const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt); + const found = []; + const step = (i) => new Promise((resolve2, reject) => { + if (i === pathEnv.length) + return opt.all && found.length ? resolve2(found) : reject(getNotFoundError(cmd)); + const ppRaw = pathEnv[i]; + const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw; + const pCmd = path.join(pathPart, cmd); + const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd; + resolve2(subStep(p, i, 0)); + }); + const subStep = (p, i, ii) => new Promise((resolve2, reject) => { + if (ii === pathExt.length) + return resolve2(step(i + 1)); + const ext = pathExt[ii]; + isexe(p + ext, { pathExt: pathExtExe }, (er, is) => { + if (!er && is) { + if (opt.all) + found.push(p + ext); + else + return resolve2(p + ext); + } + return resolve2(subStep(p, i, ii + 1)); + }); + }); + return cb ? step(0).then((res) => cb(null, res), cb) : step(0); + }; + var whichSync = (cmd, opt) => { + opt = opt || {}; + const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt); + const found = []; + for (let i = 0; i < pathEnv.length; i++) { + const ppRaw = pathEnv[i]; + const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw; + const pCmd = path.join(pathPart, cmd); + const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd; + for (let j = 0; j < pathExt.length; j++) { + const cur = p + pathExt[j]; + try { + const is = isexe.sync(cur, { pathExt: pathExtExe }); + if (is) { + if (opt.all) + found.push(cur); + else + return cur; + } + } catch (ex) { + } + } + } + if (opt.all && found.length) + return found; + if (opt.nothrow) + return null; + throw getNotFoundError(cmd); + }; + module.exports = which; + which.sync = whichSync; + } +}); + +// node_modules/path-key/index.js +var require_path_key = __commonJS({ + "node_modules/path-key/index.js"(exports, module) { + "use strict"; + var pathKey = (options = {}) => { + const environment = options.env || process.env; + const platform = options.platform || process.platform; + if (platform !== "win32") { + return "PATH"; + } + return Object.keys(environment).reverse().find((key) => key.toUpperCase() === "PATH") || "Path"; + }; + module.exports = pathKey; + module.exports.default = pathKey; + } +}); + +// node_modules/cross-spawn/lib/util/resolveCommand.js +var require_resolveCommand = __commonJS({ + "node_modules/cross-spawn/lib/util/resolveCommand.js"(exports, module) { + "use strict"; + var path = __require("path"); + var which = require_which(); + var getPathKey = require_path_key(); + function resolveCommandAttempt(parsed, withoutPathExt) { + const env2 = parsed.options.env || process.env; + const cwd = process.cwd(); + const hasCustomCwd = parsed.options.cwd != null; + const shouldSwitchCwd = hasCustomCwd && process.chdir !== void 0 && !process.chdir.disabled; + if (shouldSwitchCwd) { + try { + process.chdir(parsed.options.cwd); + } catch (err) { + } + } + let resolved; + try { + resolved = which.sync(parsed.command, { + path: env2[getPathKey({ env: env2 })], + pathExt: withoutPathExt ? path.delimiter : void 0 + }); + } catch (e) { + } finally { + if (shouldSwitchCwd) { + process.chdir(cwd); + } + } + if (resolved) { + resolved = path.resolve(hasCustomCwd ? parsed.options.cwd : "", resolved); + } + return resolved; + } + function resolveCommand(parsed) { + return resolveCommandAttempt(parsed) || resolveCommandAttempt(parsed, true); + } + module.exports = resolveCommand; + } +}); + +// node_modules/cross-spawn/lib/util/escape.js +var require_escape = __commonJS({ + "node_modules/cross-spawn/lib/util/escape.js"(exports, module) { + "use strict"; + var metaCharsRegExp = /([()\][%!^"`<>&|;, *?])/g; + function escapeCommand(arg) { + arg = arg.replace(metaCharsRegExp, "^$1"); + return arg; + } + function escapeArgument(arg, doubleEscapeMetaChars) { + arg = `${arg}`; + arg = arg.replace(/(?=(\\+?)?)\1"/g, '$1$1\\"'); + arg = arg.replace(/(?=(\\+?)?)\1$/, "$1$1"); + arg = `"${arg}"`; + arg = arg.replace(metaCharsRegExp, "^$1"); + if (doubleEscapeMetaChars) { + arg = arg.replace(metaCharsRegExp, "^$1"); + } + return arg; + } + module.exports.command = escapeCommand; + module.exports.argument = escapeArgument; + } +}); + +// node_modules/shebang-regex/index.js +var require_shebang_regex = __commonJS({ + "node_modules/shebang-regex/index.js"(exports, module) { + "use strict"; + module.exports = /^#!(.*)/; + } +}); + +// node_modules/shebang-command/index.js +var require_shebang_command = __commonJS({ + "node_modules/shebang-command/index.js"(exports, module) { + "use strict"; + var shebangRegex = require_shebang_regex(); + module.exports = (string = "") => { + const match = string.match(shebangRegex); + if (!match) { + return null; + } + const [path, argument] = match[0].replace(/#! ?/, "").split(" "); + const binary = path.split("/").pop(); + if (binary === "env") { + return argument; + } + return argument ? `${binary} ${argument}` : binary; + }; + } +}); + +// node_modules/cross-spawn/lib/util/readShebang.js +var require_readShebang = __commonJS({ + "node_modules/cross-spawn/lib/util/readShebang.js"(exports, module) { + "use strict"; + var fs = __require("fs"); + var shebangCommand = require_shebang_command(); + function readShebang(command) { + const size = 150; + const buffer = Buffer.alloc(size); + let fd; + try { + fd = fs.openSync(command, "r"); + fs.readSync(fd, buffer, 0, size, 0); + fs.closeSync(fd); + } catch (e) { + } + return shebangCommand(buffer.toString()); + } + module.exports = readShebang; + } +}); + +// node_modules/cross-spawn/lib/parse.js +var require_parse = __commonJS({ + "node_modules/cross-spawn/lib/parse.js"(exports, module) { + "use strict"; + var path = __require("path"); + var resolveCommand = require_resolveCommand(); + var escape = require_escape(); + var readShebang = require_readShebang(); + var isWin = process.platform === "win32"; + var isExecutableRegExp = /\.(?:com|exe)$/i; + var isCmdShimRegExp = /node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i; + function detectShebang(parsed) { + parsed.file = resolveCommand(parsed); + const shebang = parsed.file && readShebang(parsed.file); + if (shebang) { + parsed.args.unshift(parsed.file); + parsed.command = shebang; + return resolveCommand(parsed); + } + return parsed.file; + } + function parseNonShell(parsed) { + if (!isWin) { + return parsed; + } + const commandFile = detectShebang(parsed); + const needsShell = !isExecutableRegExp.test(commandFile); + if (parsed.options.forceShell || needsShell) { + const needsDoubleEscapeMetaChars = isCmdShimRegExp.test(commandFile); + parsed.command = path.normalize(parsed.command); + parsed.command = escape.command(parsed.command); + parsed.args = parsed.args.map((arg) => escape.argument(arg, needsDoubleEscapeMetaChars)); + const shellCommand = [parsed.command].concat(parsed.args).join(" "); + parsed.args = ["/d", "/s", "/c", `"${shellCommand}"`]; + parsed.command = process.env.comspec || "cmd.exe"; + parsed.options.windowsVerbatimArguments = true; + } + return parsed; + } + function parse(command, args, options) { + if (args && !Array.isArray(args)) { + options = args; + args = null; + } + args = args ? args.slice(0) : []; + options = Object.assign({}, options); + const parsed = { + command, + args, + options, + file: void 0, + original: { + command, + args + } + }; + return options.shell ? parsed : parseNonShell(parsed); + } + module.exports = parse; + } +}); + +// node_modules/cross-spawn/lib/enoent.js +var require_enoent = __commonJS({ + "node_modules/cross-spawn/lib/enoent.js"(exports, module) { + "use strict"; + var isWin = process.platform === "win32"; + function notFoundError(original, syscall) { + return Object.assign(new Error(`${syscall} ${original.command} ENOENT`), { + code: "ENOENT", + errno: "ENOENT", + syscall: `${syscall} ${original.command}`, + path: original.command, + spawnargs: original.args + }); + } + function hookChildProcess(cp, parsed) { + if (!isWin) { + return; + } + const originalEmit = cp.emit; + cp.emit = function(name, arg1) { + if (name === "exit") { + const err = verifyENOENT(arg1, parsed); + if (err) { + return originalEmit.call(cp, "error", err); + } + } + return originalEmit.apply(cp, arguments); + }; + } + function verifyENOENT(status, parsed) { + if (isWin && status === 1 && !parsed.file) { + return notFoundError(parsed.original, "spawn"); + } + return null; + } + function verifyENOENTSync(status, parsed) { + if (isWin && status === 1 && !parsed.file) { + return notFoundError(parsed.original, "spawnSync"); + } + return null; + } + module.exports = { + hookChildProcess, + verifyENOENT, + verifyENOENTSync, + notFoundError + }; + } +}); + +// node_modules/cross-spawn/index.js +var require_cross_spawn = __commonJS({ + "node_modules/cross-spawn/index.js"(exports, module) { + "use strict"; + var cp = __require("child_process"); + var parse = require_parse(); + var enoent = require_enoent(); + function spawn(command, args, options) { + const parsed = parse(command, args, options); + const spawned = cp.spawn(parsed.command, parsed.args, parsed.options); + enoent.hookChildProcess(spawned, parsed); + return spawned; + } + function spawnSync2(command, args, options) { + const parsed = parse(command, args, options); + const result = cp.spawnSync(parsed.command, parsed.args, parsed.options); + result.error = result.error || enoent.verifyENOENTSync(result.status, parsed); + return result; + } + module.exports = spawn; + module.exports.spawn = spawn; + module.exports.sync = spawnSync2; + module.exports._parse = parse; + module.exports._enoent = enoent; + } +}); + +// node_modules/@opencode-ai/sdk/dist/gen/core/serverSentEvents.gen.js +var createSseClient = ({ onSseError, onSseEvent, responseTransformer, responseValidator, sseDefaultRetryDelay, sseMaxRetryAttempts, sseMaxRetryDelay, sseSleepFn, url, ...options }) => { + let lastEventId; + const sleep = sseSleepFn ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms))); + const createStream = async function* () { + let retryDelay = sseDefaultRetryDelay ?? 3e3; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + while (true) { + if (signal.aborted) + break; + attempt++; + const headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers); + if (lastEventId !== void 0) { + headers.set("Last-Event-ID", lastEventId); + } + try { + const response = await fetch(url, { ...options, headers, signal }); + if (!response.ok) + throw new Error(`SSE failed: ${response.status} ${response.statusText}`); + if (!response.body) + throw new Error("No body in SSE response"); + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); + let buffer = ""; + const abortHandler = () => { + try { + void reader.cancel(); + } catch { + } + }; + signal.addEventListener("abort", abortHandler); + try { + while (true) { + const { done, value } = await reader.read(); + if (done) + break; + buffer += value; + const chunks = buffer.split("\n\n"); + buffer = chunks.pop() ?? ""; + for (const chunk of chunks) { + const lines = chunk.split("\n"); + const dataLines = []; + let eventName; + for (const line of lines) { + if (line.startsWith("data:")) { + dataLines.push(line.replace(/^data:\s*/, "")); + } else if (line.startsWith("event:")) { + eventName = line.replace(/^event:\s*/, ""); + } else if (line.startsWith("id:")) { + lastEventId = line.replace(/^id:\s*/, ""); + } else if (line.startsWith("retry:")) { + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + let data; + let parsedJson = false; + if (dataLines.length) { + const rawData = dataLines.join("\n"); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + if (responseTransformer) { + data = await responseTransformer(data); + } + } + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay + }); + if (dataLines.length) { + yield data; + } + } + } + } finally { + signal.removeEventListener("abort", abortHandler); + reader.releaseLock(); + } + break; + } catch (error) { + onSseError?.(error); + if (sseMaxRetryAttempts !== void 0 && attempt >= sseMaxRetryAttempts) { + break; + } + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 3e4); + await sleep(backoff); + } + } + }; + const stream = createStream(); + return { stream }; +}; + +// node_modules/@opencode-ai/sdk/dist/gen/core/auth.gen.js +var getAuthToken = async (auth, callback) => { + const token = typeof callback === "function" ? await callback(auth) : callback; + if (!token) { + return; + } + if (auth.scheme === "bearer") { + return `Bearer ${token}`; + } + if (auth.scheme === "basic") { + return `Basic ${btoa(token)}`; + } + return token; +}; + +// node_modules/@opencode-ai/sdk/dist/gen/core/bodySerializer.gen.js +var jsonBodySerializer = { + bodySerializer: (body) => JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value) +}; + +// node_modules/@opencode-ai/sdk/dist/gen/core/pathSerializer.gen.js +var separatorArrayExplode = (style) => { + switch (style) { + case "label": + return "."; + case "matrix": + return ";"; + case "simple": + return ","; + default: + return "&"; + } +}; +var separatorArrayNoExplode = (style) => { + switch (style) { + case "form": + return ","; + case "pipeDelimited": + return "|"; + case "spaceDelimited": + return "%20"; + default: + return ","; + } +}; +var separatorObjectExplode = (style) => { + switch (style) { + case "label": + return "."; + case "matrix": + return ";"; + case "simple": + return ","; + default: + return "&"; + } +}; +var serializeArrayParam = ({ allowReserved, explode, name, style, value }) => { + if (!explode) { + const joinedValues2 = (allowReserved ? value : value.map((v) => encodeURIComponent(v))).join(separatorArrayNoExplode(style)); + switch (style) { + case "label": + return `.${joinedValues2}`; + case "matrix": + return `;${name}=${joinedValues2}`; + case "simple": + return joinedValues2; + default: + return `${name}=${joinedValues2}`; + } + } + const separator = separatorArrayExplode(style); + const joinedValues = value.map((v) => { + if (style === "label" || style === "simple") { + return allowReserved ? v : encodeURIComponent(v); + } + return serializePrimitiveParam({ + allowReserved, + name, + value: v + }); + }).join(separator); + return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues; +}; +var serializePrimitiveParam = ({ allowReserved, name, value }) => { + if (value === void 0 || value === null) { + return ""; + } + if (typeof value === "object") { + throw new Error("Deeply-nested arrays/objects aren\u2019t supported. Provide your own `querySerializer()` to handle these."); + } + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; +var serializeObjectParam = ({ allowReserved, explode, name, style, value, valueOnly }) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + if (style !== "deepObject" && !explode) { + let values = []; + Object.entries(value).forEach(([key, v]) => { + values = [...values, key, allowReserved ? v : encodeURIComponent(v)]; + }); + const joinedValues2 = values.join(","); + switch (style) { + case "form": + return `${name}=${joinedValues2}`; + case "label": + return `.${joinedValues2}`; + case "matrix": + return `;${name}=${joinedValues2}`; + default: + return joinedValues2; + } + } + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value).map(([key, v]) => serializePrimitiveParam({ + allowReserved, + name: style === "deepObject" ? `${name}[${key}]` : key, + value: v + })).join(separator); + return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues; +}; + +// node_modules/@opencode-ai/sdk/dist/gen/core/utils.gen.js +var PATH_PARAM_RE = /\{[^{}]+\}/g; +var defaultPathSerializer = ({ path, url: _url }) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style = "simple"; + if (name.endsWith("*")) { + explode = true; + name = name.substring(0, name.length - 1); + } + if (name.startsWith(".")) { + name = name.substring(1); + style = "label"; + } else if (name.startsWith(";")) { + name = name.substring(1); + style = "matrix"; + } + const value = path[name]; + if (value === void 0 || value === null) { + continue; + } + if (Array.isArray(value)) { + url = url.replace(match, serializeArrayParam({ explode, name, style, value })); + continue; + } + if (typeof value === "object") { + url = url.replace(match, serializeObjectParam({ + explode, + name, + style, + value, + valueOnly: true + })); + continue; + } + if (style === "matrix") { + url = url.replace(match, `;${serializePrimitiveParam({ + name, + value + })}`); + continue; + } + const replaceValue = encodeURIComponent(style === "label" ? `.${value}` : value); + url = url.replace(match, replaceValue); + } + } + return url; +}; +var getUrl = ({ baseUrl, path, query, querySerializer, url: _url }) => { + const pathUrl = _url.startsWith("/") ? _url : `/${_url}`; + let url = (baseUrl ?? "") + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ""; + if (search.startsWith("?")) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +// node_modules/@opencode-ai/sdk/dist/gen/client/utils.gen.js +var createQuerySerializer = ({ allowReserved, array, object } = {}) => { + const querySerializer = (queryParams) => { + const search = []; + if (queryParams && typeof queryParams === "object") { + for (const name in queryParams) { + const value = queryParams[name]; + if (value === void 0 || value === null) { + continue; + } + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved, + explode: true, + name, + style: "form", + value, + ...array + }); + if (serializedArray) + search.push(serializedArray); + } else if (typeof value === "object") { + const serializedObject = serializeObjectParam({ + allowReserved, + explode: true, + name, + style: "deepObject", + value, + ...object + }); + if (serializedObject) + search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved, + name, + value + }); + if (serializedPrimitive) + search.push(serializedPrimitive); + } + } + } + return search.join("&"); + }; + return querySerializer; +}; +var getParseAs = (contentType) => { + if (!contentType) { + return "stream"; + } + const cleanContent = contentType.split(";")[0]?.trim(); + if (!cleanContent) { + return; + } + if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) { + return "json"; + } + if (cleanContent === "multipart/form-data") { + return "formData"; + } + if (["application/", "audio/", "image/", "video/"].some((type) => cleanContent.startsWith(type))) { + return "blob"; + } + if (cleanContent.startsWith("text/")) { + return "text"; + } + return; +}; +var checkForExistence = (options, name) => { + if (!name) { + return false; + } + if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) { + return true; + } + return false; +}; +var setAuthParams = async ({ security, ...options }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + const token = await getAuthToken(auth, options.auth); + if (!token) { + continue; + } + const name = auth.name ?? "Authorization"; + switch (auth.in) { + case "query": + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case "cookie": + options.headers.append("Cookie", `${name}=${token}`); + break; + case "header": + default: + options.headers.set(name, token); + break; + } + } +}; +var buildUrl = (options) => getUrl({ + baseUrl: options.baseUrl, + path: options.path, + query: options.query, + querySerializer: typeof options.querySerializer === "function" ? options.querySerializer : createQuerySerializer(options.querySerializer), + url: options.url +}); +var mergeConfigs = (a, b) => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith("/")) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; +var mergeHeaders = (...headers) => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header || typeof header !== "object") { + continue; + } + const iterator = header instanceof Headers ? header.entries() : Object.entries(header); + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v); + } + } else if (value !== void 0) { + mergedHeaders.set(key, typeof value === "object" ? JSON.stringify(value) : value); + } + } + } + return mergedHeaders; +}; +var Interceptors = class { + _fns; + constructor() { + this._fns = []; + } + clear() { + this._fns = []; + } + getInterceptorIndex(id) { + if (typeof id === "number") { + return this._fns[id] ? id : -1; + } else { + return this._fns.indexOf(id); + } + } + exists(id) { + const index = this.getInterceptorIndex(id); + return !!this._fns[index]; + } + eject(id) { + const index = this.getInterceptorIndex(id); + if (this._fns[index]) { + this._fns[index] = null; + } + } + update(id, fn) { + const index = this.getInterceptorIndex(id); + if (this._fns[index]) { + this._fns[index] = fn; + return id; + } else { + return false; + } + } + use(fn) { + this._fns = [...this._fns, fn]; + return this._fns.length - 1; + } +}; +var createInterceptors = () => ({ + error: new Interceptors(), + request: new Interceptors(), + response: new Interceptors() +}); +var defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: "form" + }, + object: { + explode: true, + style: "deepObject" + } +}); +var defaultHeaders = { + "Content-Type": "application/json" +}; +var createConfig = (override = {}) => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: "auto", + querySerializer: defaultQuerySerializer, + ...override +}); + +// node_modules/@opencode-ai/sdk/dist/gen/client/client.gen.js +var createClient = (config = {}) => { + let _config = mergeConfigs(createConfig(), config); + const getConfig = () => ({ ..._config }); + const setConfig = (config2) => { + _config = mergeConfigs(_config, config2); + return getConfig(); + }; + const interceptors = createInterceptors(); + const beforeRequest = async (options) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: void 0 + }; + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security + }); + } + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + if (opts.body && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); + } + if (opts.serializedBody === void 0 || opts.serializedBody === "") { + opts.headers.delete("Content-Type"); + } + const url = buildUrl(opts); + return { opts, url }; + }; + const request = async (options) => { + const { opts, url } = await beforeRequest(options); + const requestInit = { + redirect: "follow", + ...opts, + body: opts.serializedBody + }; + let request2 = new Request(url, requestInit); + for (const fn of interceptors.request._fns) { + if (fn) { + request2 = await fn(request2, opts); + } + } + const _fetch = opts.fetch; + let response = await _fetch(request2); + for (const fn of interceptors.response._fns) { + if (fn) { + response = await fn(response, request2, opts); + } + } + const result = { + request: request2, + response + }; + if (response.ok) { + if (response.status === 204 || response.headers.get("Content-Length") === "0") { + return opts.responseStyle === "data" ? {} : { + data: {}, + ...result + }; + } + const parseAs = (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json"; + let data; + switch (parseAs) { + case "arrayBuffer": + case "blob": + case "formData": + case "json": + case "text": + data = await response[parseAs](); + break; + case "stream": + return opts.responseStyle === "data" ? response.body : { + data: response.body, + ...result + }; + } + if (parseAs === "json") { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + return opts.responseStyle === "data" ? data : { + data, + ...result + }; + } + const textError = await response.text(); + let jsonError; + try { + jsonError = JSON.parse(textError); + } catch { + } + const error = jsonError ?? textError; + let finalError = error; + for (const fn of interceptors.error._fns) { + if (fn) { + finalError = await fn(error, response, request2, opts); + } + } + finalError = finalError || {}; + if (opts.throwOnError) { + throw finalError; + } + return opts.responseStyle === "data" ? void 0 : { + error: finalError, + ...result + }; + }; + const makeMethod = (method) => { + const fn = (options) => request({ ...options, method }); + fn.sse = async (options) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body, + headers: opts.headers, + method, + url + }); + }; + return fn; + }; + return { + buildUrl, + connect: makeMethod("CONNECT"), + delete: makeMethod("DELETE"), + get: makeMethod("GET"), + getConfig, + head: makeMethod("HEAD"), + interceptors, + options: makeMethod("OPTIONS"), + patch: makeMethod("PATCH"), + post: makeMethod("POST"), + put: makeMethod("PUT"), + request, + setConfig, + trace: makeMethod("TRACE") + }; +}; + +// node_modules/@opencode-ai/sdk/dist/gen/core/params.gen.js +var extraPrefixesMap = { + $body_: "body", + $headers_: "headers", + $path_: "path", + $query_: "query" +}; +var extraPrefixes = Object.entries(extraPrefixesMap); + +// node_modules/@opencode-ai/sdk/dist/gen/client.gen.js +var client = createClient(createConfig({ + baseUrl: "http://localhost:4096" +})); + +// node_modules/@opencode-ai/sdk/dist/gen/sdk.gen.js +var _HeyApiClient = class { + _client = client; + constructor(args) { + if (args?.client) { + this._client = args.client; + } + } +}; +var Global = class extends _HeyApiClient { + /** + * Get events + */ + event(options) { + return (options?.client ?? this._client).get.sse({ + url: "/global/event", + ...options + }); + } +}; +var Project = class extends _HeyApiClient { + /** + * List all projects + */ + list(options) { + return (options?.client ?? this._client).get({ + url: "/project", + ...options + }); + } + /** + * Get the current project + */ + current(options) { + return (options?.client ?? this._client).get({ + url: "/project/current", + ...options + }); + } +}; +var Pty = class extends _HeyApiClient { + /** + * List all PTY sessions + */ + list(options) { + return (options?.client ?? this._client).get({ + url: "/pty", + ...options + }); + } + /** + * Create a new PTY session + */ + create(options) { + return (options?.client ?? this._client).post({ + url: "/pty", + ...options, + headers: { + "Content-Type": "application/json", + ...options?.headers + } + }); + } + /** + * Remove a PTY session + */ + remove(options) { + return (options.client ?? this._client).delete({ + url: "/pty/{id}", + ...options + }); + } + /** + * Get PTY session info + */ + get(options) { + return (options.client ?? this._client).get({ + url: "/pty/{id}", + ...options + }); + } + /** + * Update PTY session + */ + update(options) { + return (options.client ?? this._client).put({ + url: "/pty/{id}", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers + } + }); + } + /** + * Connect to a PTY session + */ + connect(options) { + return (options.client ?? this._client).get({ + url: "/pty/{id}/connect", + ...options + }); + } +}; +var Config = class extends _HeyApiClient { + /** + * Get config info + */ + get(options) { + return (options?.client ?? this._client).get({ + url: "/config", + ...options + }); + } + /** + * Update config + */ + update(options) { + return (options?.client ?? this._client).patch({ + url: "/config", + ...options, + headers: { + "Content-Type": "application/json", + ...options?.headers + } + }); + } + /** + * List all providers + */ + providers(options) { + return (options?.client ?? this._client).get({ + url: "/config/providers", + ...options + }); + } +}; +var Tool = class extends _HeyApiClient { + /** + * List all tool IDs (including built-in and dynamically registered) + */ + ids(options) { + return (options?.client ?? this._client).get({ + url: "/experimental/tool/ids", + ...options + }); + } + /** + * List tools with JSON schema parameters for a provider/model + */ + list(options) { + return (options.client ?? this._client).get({ + url: "/experimental/tool", + ...options + }); + } +}; +var Instance = class extends _HeyApiClient { + /** + * Dispose the current instance + */ + dispose(options) { + return (options?.client ?? this._client).post({ + url: "/instance/dispose", + ...options + }); + } +}; +var Path = class extends _HeyApiClient { + /** + * Get the current path + */ + get(options) { + return (options?.client ?? this._client).get({ + url: "/path", + ...options + }); + } +}; +var Vcs = class extends _HeyApiClient { + /** + * Get VCS info for the current instance + */ + get(options) { + return (options?.client ?? this._client).get({ + url: "/vcs", + ...options + }); + } +}; +var Session = class extends _HeyApiClient { + /** + * List all sessions + */ + list(options) { + return (options?.client ?? this._client).get({ + url: "/session", + ...options + }); + } + /** + * Create a new session + */ + create(options) { + return (options?.client ?? this._client).post({ + url: "/session", + ...options, + headers: { + "Content-Type": "application/json", + ...options?.headers + } + }); + } + /** + * Get session status + */ + status(options) { + return (options?.client ?? this._client).get({ + url: "/session/status", + ...options + }); + } + /** + * Delete a session and all its data + */ + delete(options) { + return (options.client ?? this._client).delete({ + url: "/session/{id}", + ...options + }); + } + /** + * Get session + */ + get(options) { + return (options.client ?? this._client).get({ + url: "/session/{id}", + ...options + }); + } + /** + * Update session properties + */ + update(options) { + return (options.client ?? this._client).patch({ + url: "/session/{id}", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers + } + }); + } + /** + * Get a session's children + */ + children(options) { + return (options.client ?? this._client).get({ + url: "/session/{id}/children", + ...options + }); + } + /** + * Get the todo list for a session + */ + todo(options) { + return (options.client ?? this._client).get({ + url: "/session/{id}/todo", + ...options + }); + } + /** + * Analyze the app and create an AGENTS.md file + */ + init(options) { + return (options.client ?? this._client).post({ + url: "/session/{id}/init", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers + } + }); + } + /** + * Fork an existing session at a specific message + */ + fork(options) { + return (options.client ?? this._client).post({ + url: "/session/{id}/fork", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers + } + }); + } + /** + * Abort a session + */ + abort(options) { + return (options.client ?? this._client).post({ + url: "/session/{id}/abort", + ...options + }); + } + /** + * Unshare the session + */ + unshare(options) { + return (options.client ?? this._client).delete({ + url: "/session/{id}/share", + ...options + }); + } + /** + * Share a session + */ + share(options) { + return (options.client ?? this._client).post({ + url: "/session/{id}/share", + ...options + }); + } + /** + * Get the diff for this session + */ + diff(options) { + return (options.client ?? this._client).get({ + url: "/session/{id}/diff", + ...options + }); + } + /** + * Summarize the session + */ + summarize(options) { + return (options.client ?? this._client).post({ + url: "/session/{id}/summarize", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers + } + }); + } + /** + * List messages for a session + */ + messages(options) { + return (options.client ?? this._client).get({ + url: "/session/{id}/message", + ...options + }); + } + /** + * Create and send a new message to a session + */ + prompt(options) { + return (options.client ?? this._client).post({ + url: "/session/{id}/message", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers + } + }); + } + /** + * Get a message from a session + */ + message(options) { + return (options.client ?? this._client).get({ + url: "/session/{id}/message/{messageID}", + ...options + }); + } + /** + * Create and send a new message to a session, start if needed and return immediately + */ + promptAsync(options) { + return (options.client ?? this._client).post({ + url: "/session/{id}/prompt_async", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers + } + }); + } + /** + * Send a new command to a session + */ + command(options) { + return (options.client ?? this._client).post({ + url: "/session/{id}/command", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers + } + }); + } + /** + * Run a shell command + */ + shell(options) { + return (options.client ?? this._client).post({ + url: "/session/{id}/shell", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers + } + }); + } + /** + * Revert a message + */ + revert(options) { + return (options.client ?? this._client).post({ + url: "/session/{id}/revert", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers + } + }); + } + /** + * Restore all reverted messages + */ + unrevert(options) { + return (options.client ?? this._client).post({ + url: "/session/{id}/unrevert", + ...options + }); + } +}; +var Command = class extends _HeyApiClient { + /** + * List all commands + */ + list(options) { + return (options?.client ?? this._client).get({ + url: "/command", + ...options + }); + } +}; +var Oauth = class extends _HeyApiClient { + /** + * Authorize a provider using OAuth + */ + authorize(options) { + return (options.client ?? this._client).post({ + url: "/provider/{id}/oauth/authorize", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers + } + }); + } + /** + * Handle OAuth callback for a provider + */ + callback(options) { + return (options.client ?? this._client).post({ + url: "/provider/{id}/oauth/callback", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers + } + }); + } +}; +var Provider = class extends _HeyApiClient { + /** + * List all providers + */ + list(options) { + return (options?.client ?? this._client).get({ + url: "/provider", + ...options + }); + } + /** + * Get provider authentication methods + */ + auth(options) { + return (options?.client ?? this._client).get({ + url: "/provider/auth", + ...options + }); + } + oauth = new Oauth({ client: this._client }); +}; +var Find = class extends _HeyApiClient { + /** + * Find text in files + */ + text(options) { + return (options.client ?? this._client).get({ + url: "/find", + ...options + }); + } + /** + * Find files + */ + files(options) { + return (options.client ?? this._client).get({ + url: "/find/file", + ...options + }); + } + /** + * Find workspace symbols + */ + symbols(options) { + return (options.client ?? this._client).get({ + url: "/find/symbol", + ...options + }); + } +}; +var File = class extends _HeyApiClient { + /** + * List files and directories + */ + list(options) { + return (options.client ?? this._client).get({ + url: "/file", + ...options + }); + } + /** + * Read a file + */ + read(options) { + return (options.client ?? this._client).get({ + url: "/file/content", + ...options + }); + } + /** + * Get file status + */ + status(options) { + return (options?.client ?? this._client).get({ + url: "/file/status", + ...options + }); + } +}; +var App = class extends _HeyApiClient { + /** + * Write a log entry to the server logs + */ + log(options) { + return (options?.client ?? this._client).post({ + url: "/log", + ...options, + headers: { + "Content-Type": "application/json", + ...options?.headers + } + }); + } + /** + * List all agents + */ + agents(options) { + return (options?.client ?? this._client).get({ + url: "/agent", + ...options + }); + } +}; +var Auth = class extends _HeyApiClient { + /** + * Remove OAuth credentials for an MCP server + */ + remove(options) { + return (options.client ?? this._client).delete({ + url: "/mcp/{name}/auth", + ...options + }); + } + /** + * Start OAuth authentication flow for an MCP server + */ + start(options) { + return (options.client ?? this._client).post({ + url: "/mcp/{name}/auth", + ...options + }); + } + /** + * Complete OAuth authentication with authorization code + */ + callback(options) { + return (options.client ?? this._client).post({ + url: "/mcp/{name}/auth/callback", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers + } + }); + } + /** + * Start OAuth flow and wait for callback (opens browser) + */ + authenticate(options) { + return (options.client ?? this._client).post({ + url: "/mcp/{name}/auth/authenticate", + ...options + }); + } + /** + * Set authentication credentials + */ + set(options) { + return (options.client ?? this._client).put({ + url: "/auth/{id}", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers + } + }); + } +}; +var Mcp = class extends _HeyApiClient { + /** + * Get MCP server status + */ + status(options) { + return (options?.client ?? this._client).get({ + url: "/mcp", + ...options + }); + } + /** + * Add MCP server dynamically + */ + add(options) { + return (options?.client ?? this._client).post({ + url: "/mcp", + ...options, + headers: { + "Content-Type": "application/json", + ...options?.headers + } + }); + } + /** + * Connect an MCP server + */ + connect(options) { + return (options.client ?? this._client).post({ + url: "/mcp/{name}/connect", + ...options + }); + } + /** + * Disconnect an MCP server + */ + disconnect(options) { + return (options.client ?? this._client).post({ + url: "/mcp/{name}/disconnect", + ...options + }); + } + auth = new Auth({ client: this._client }); +}; +var Lsp = class extends _HeyApiClient { + /** + * Get LSP server status + */ + status(options) { + return (options?.client ?? this._client).get({ + url: "/lsp", + ...options + }); + } +}; +var Formatter = class extends _HeyApiClient { + /** + * Get formatter status + */ + status(options) { + return (options?.client ?? this._client).get({ + url: "/formatter", + ...options + }); + } +}; +var Control = class extends _HeyApiClient { + /** + * Get the next TUI request from the queue + */ + next(options) { + return (options?.client ?? this._client).get({ + url: "/tui/control/next", + ...options + }); + } + /** + * Submit a response to the TUI request queue + */ + response(options) { + return (options?.client ?? this._client).post({ + url: "/tui/control/response", + ...options, + headers: { + "Content-Type": "application/json", + ...options?.headers + } + }); + } +}; +var Tui = class extends _HeyApiClient { + /** + * Append prompt to the TUI + */ + appendPrompt(options) { + return (options?.client ?? this._client).post({ + url: "/tui/append-prompt", + ...options, + headers: { + "Content-Type": "application/json", + ...options?.headers + } + }); + } + /** + * Open the help dialog + */ + openHelp(options) { + return (options?.client ?? this._client).post({ + url: "/tui/open-help", + ...options + }); + } + /** + * Open the session dialog + */ + openSessions(options) { + return (options?.client ?? this._client).post({ + url: "/tui/open-sessions", + ...options + }); + } + /** + * Open the theme dialog + */ + openThemes(options) { + return (options?.client ?? this._client).post({ + url: "/tui/open-themes", + ...options + }); + } + /** + * Open the model dialog + */ + openModels(options) { + return (options?.client ?? this._client).post({ + url: "/tui/open-models", + ...options + }); + } + /** + * Submit the prompt + */ + submitPrompt(options) { + return (options?.client ?? this._client).post({ + url: "/tui/submit-prompt", + ...options + }); + } + /** + * Clear the prompt + */ + clearPrompt(options) { + return (options?.client ?? this._client).post({ + url: "/tui/clear-prompt", + ...options + }); + } + /** + * Execute a TUI command (e.g. agent_cycle) + */ + executeCommand(options) { + return (options?.client ?? this._client).post({ + url: "/tui/execute-command", + ...options, + headers: { + "Content-Type": "application/json", + ...options?.headers + } + }); + } + /** + * Show a toast notification in the TUI + */ + showToast(options) { + return (options?.client ?? this._client).post({ + url: "/tui/show-toast", + ...options, + headers: { + "Content-Type": "application/json", + ...options?.headers + } + }); + } + /** + * Publish a TUI event + */ + publish(options) { + return (options?.client ?? this._client).post({ + url: "/tui/publish", + ...options, + headers: { + "Content-Type": "application/json", + ...options?.headers + } + }); + } + control = new Control({ client: this._client }); +}; +var Event = class extends _HeyApiClient { + /** + * Get events + */ + subscribe(options) { + return (options?.client ?? this._client).get.sse({ + url: "/event", + ...options + }); + } +}; +var OpencodeClient = class extends _HeyApiClient { + /** + * Respond to a permission request + */ + postSessionIdPermissionsPermissionId(options) { + return (options.client ?? this._client).post({ + url: "/session/{id}/permissions/{permissionID}", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers + } + }); + } + global = new Global({ client: this._client }); + project = new Project({ client: this._client }); + pty = new Pty({ client: this._client }); + config = new Config({ client: this._client }); + tool = new Tool({ client: this._client }); + instance = new Instance({ client: this._client }); + path = new Path({ client: this._client }); + vcs = new Vcs({ client: this._client }); + session = new Session({ client: this._client }); + command = new Command({ client: this._client }); + provider = new Provider({ client: this._client }); + find = new Find({ client: this._client }); + file = new File({ client: this._client }); + app = new App({ client: this._client }); + mcp = new Mcp({ client: this._client }); + lsp = new Lsp({ client: this._client }); + formatter = new Formatter({ client: this._client }); + tui = new Tui({ client: this._client }); + auth = new Auth({ client: this._client }); + event = new Event({ client: this._client }); +}; + +// node_modules/@opencode-ai/sdk/dist/error-interceptor.js +function wrapClientError(error, response, request, opts) { + if (!opts?.throwOnError) + return error; + if (error instanceof Error) + return error; + if (typeof error === "object" && error !== null && Object.keys(error).length > 0) { + const obj = error; + const message = typeof obj.data?.message === "string" && obj.data.message || typeof obj.message === "string" && obj.message || typeof obj.name === "string" && obj.name || describe(request, response); + return new Error(message, { cause: { body: error, status: response?.status } }); + } + if (typeof error === "string" && error.length > 0) { + return new Error(error, { cause: { body: error, status: response?.status } }); + } + const reason = response ? "(empty response body)" : "network error (no response)"; + return new Error(`opencode server ${describe(request, response)}: ${reason}`, { + cause: { body: error, status: response?.status } + }); +} +function describe(request, response) { + const method = request?.method ?? "?"; + const url = request?.url ?? "?"; + const status = response?.status; + const statusText = response?.statusText; + return `${method} ${url}${status ? " \u2192 " + status : ""}${statusText ? " " + statusText : ""}`; +} + +// node_modules/@opencode-ai/sdk/dist/client.js +function pick(value, fallback) { + if (!value) + return; + if (!fallback) + return value; + if (value === fallback) + return fallback; + if (value === encodeURIComponent(fallback)) + return fallback; + return value; +} +function rewrite(request, directory) { + if (request.method !== "GET" && request.method !== "HEAD") + return request; + const value = pick(request.headers.get("x-opencode-directory"), directory); + if (!value) + return request; + const url = new URL(request.url); + if (!url.searchParams.has("directory")) { + url.searchParams.set("directory", value); + } + const next = new Request(url, request); + next.headers.delete("x-opencode-directory"); + return next; +} +function createOpencodeClient(config) { + if (!config?.fetch) { + const customFetch = (req) => { + req.timeout = false; + return fetch(req); + }; + config = { + ...config, + fetch: customFetch + }; + } + if (config?.directory) { + config.headers = { + ...config.headers, + "x-opencode-directory": encodeURIComponent(config.directory) + }; + } + const client2 = createClient(config); + client2.interceptors.request.use((request) => rewrite(request, config?.directory)); + client2.interceptors.error.use(wrapClientError); + return new OpencodeClient({ client: client2 }); +} + +// node_modules/@opencode-ai/sdk/dist/server.js +var import_cross_spawn = __toESM(require_cross_spawn(), 1); + +// node_modules/@opencode-ai/sdk/dist/process.js +import { spawnSync } from "child_process"; +function stop(proc) { + if (proc.exitCode !== null || proc.signalCode !== null) + return; + if (process.platform === "win32" && proc.pid) { + const out = spawnSync("taskkill", ["/pid", String(proc.pid), "/T", "/F"], { windowsHide: true }); + if (!out.error && out.status === 0) + return; + } + proc.kill(); +} +function bindAbort(proc, signal, onAbort) { + if (!signal) + return () => { + }; + const abort = () => { + clear(); + stop(proc); + onAbort?.(); + }; + const clear = () => { + signal.removeEventListener("abort", abort); + proc.off("exit", clear); + proc.off("error", clear); + }; + signal.addEventListener("abort", abort, { once: true }); + proc.on("exit", clear); + proc.on("error", clear); + if (signal.aborted) + abort(); + return clear; +} + +// node_modules/@opencode-ai/sdk/dist/server.js +async function createOpencodeServer(options) { + options = Object.assign({ + hostname: "127.0.0.1", + port: 4096, + timeout: 5e3 + }, options ?? {}); + const args = [`serve`, `--hostname=${options.hostname}`, `--port=${options.port}`]; + if (options.config?.logLevel) + args.push(`--log-level=${options.config.logLevel}`); + const proc = (0, import_cross_spawn.default)(`opencode`, args, { + env: { + ...process.env, + OPENCODE_CONFIG_CONTENT: JSON.stringify(options.config ?? {}) + } + }); + let clear = () => { + }; + const url = await new Promise((resolve2, reject) => { + const id = setTimeout(() => { + clear(); + stop(proc); + reject(new Error(`Timeout waiting for server to start after ${options.timeout}ms`)); + }, options.timeout); + let output = ""; + let resolved = false; + proc.stdout?.on("data", (chunk) => { + if (resolved) + return; + output += chunk.toString(); + const lines = output.split("\n"); + for (const line of lines) { + if (line.startsWith("opencode server listening")) { + const match = line.match(/on\s+(https?:\/\/[^\s]+)/); + if (!match) { + clear(); + stop(proc); + clearTimeout(id); + reject(new Error(`Failed to parse server url from output: ${line}`)); + return; + } + clearTimeout(id); + resolved = true; + resolve2(match[1]); + return; + } + } + }); + proc.stderr?.on("data", (chunk) => { + output += chunk.toString(); + }); + proc.on("exit", (code) => { + clearTimeout(id); + let msg = `Server exited with code ${code}`; + if (output.trim()) { + msg += ` +Server output: ${output}`; + } + reject(new Error(msg)); + }); + proc.on("error", (error) => { + clearTimeout(id); + reject(error); + }); + clear = bindAbort(proc, options.signal, () => { + clearTimeout(id); + reject(options.signal?.reason); + }); + }); + return { + url, + close() { + clear(); + stop(proc); + } + }; +} + +// node_modules/@opencode-ai/sdk/dist/index.js +async function createOpencode(options) { + const server = await createOpencodeServer({ + ...options + }); + const client2 = createOpencodeClient({ + baseUrl: server.url + }); + return { + client: client2, + server + }; +} + +// src/index.ts +import { readFileSync as readFileSync2 } from "fs"; +import { join as join2 } from "path"; + +// src/reviewers.ts +import { readFileSync } from "fs"; +import { join } from "path"; +var DEFAULT_TEAM = "quality:1,security:1,performance:1,architecture:1"; +function parseTeam(teamStr) { + const result = /* @__PURE__ */ new Map(); + for (const entry of teamStr.split(",")) { + const [name, count] = entry.trim().split(":"); + if (name) result.set(name.trim(), Math.max(1, parseInt(count || "1", 10) || 1)); + } + return result; +} +function loadBuiltInReviewers(reviewersDir) { + const map = /* @__PURE__ */ new Map(); + for (const file of ["quality.yaml", "security.yaml", "performance.yaml", "architecture.yaml"]) { + try { + const raw = readFileSync(join(reviewersDir, file), "utf-8"); + const parsed = parseYAML(raw); + if (parsed.name && parsed.prompt) map.set(parsed.name, { name: parsed.name, prompt: parsed.prompt }); + } catch { + } + } + return map; +} +function parseYAML(raw) { + const result = {}; + let currentKey = ""; + let inPrompt = false; + for (const line of raw.split("\n")) { + if (!inPrompt && line.match(/^(\w+):\s*(.*)/)) { + const [, key, value] = line.match(/^(\w+):\s*(.*)/) || []; + if (key === "prompt" && value.trim().startsWith("|")) { + inPrompt = true; + currentKey = "prompt"; + result.prompt = ""; + } else if (key) { + result[key] = value?.trim() || ""; + } + } else if (inPrompt) { + if (line && !line.startsWith(" ") && !line.startsWith(" ")) { + inPrompt = false; + } else { + result[currentKey] = (result[currentKey] || "") + line.trimStart() + "\n"; + } + } + } + if (result.prompt) result.prompt = result.prompt.trim(); + return result; +} +function loadReviewers(opts) { + const builtInDir = join(opts.actionPath, "reviewers"); + const personas = loadBuiltInReviewers(builtInDir); + const teamStr = opts.team || env("MULTI_REVIEW_DEFAULT_TEAM") || DEFAULT_TEAM; + const team = parseTeam(teamStr); + const reviewers = []; + for (const [name, count] of team) { + const persona = personas.get(name); + if (!persona) { + console.warn(`Warning: unknown reviewer persona "${name}", skipping`); + continue; + } + for (let i = 0; i < count; i++) { + reviewers.push({ + name: count > 1 ? `${name}-${i + 1}` : name, + prompt: persona.prompt + }); + } + } + return reviewers; +} +function env(key) { + return process.env[key] || ""; +} +function intEnv(key, fallback) { + const v = parseInt(process.env[key] || "", 10); + return isNaN(v) ? fallback : v; +} +function resolveModel() { + const raw = env("MULTI_REVIEW_MODEL") || env("MODEL_NAME") || "zhipuai-coding-plan/glm-5.1"; + const idx = raw.indexOf("/"); + if (idx === -1) { + throw new Error(`Model "${raw}" missing provider (expected format: provider/model)`); + } + return { providerID: raw.slice(0, idx), modelID: raw.slice(idx + 1) }; +} + +// src/orchestrator.ts +var DEFAULT_COORDINATOR_PROMPT = `\u4F60\u662F\u4E00\u4E2A\u4EE3\u7801\u5BA1\u67E5\u534F\u8C03\u5458\u3002\u4EE5\u4E0B\u5BA1\u67E5\u7531\u72EC\u7ACB\u7684\u4E13\u5BB6 reviewer \u751F\u6210\u3002 +\u4F60\u7684\u4EFB\u52A1\u662F\u6574\u5408\u4E3A\u4E00\u4E2A\u53BB\u91CD\u540E\u7684\u7EFC\u5408\u62A5\u544A\u3002 + +\u89C4\u5219\uFF1A +1. \u8DE8 reviewer \u53BB\u91CD\uFF08\u540C\u4E00\u95EE\u9898\u53EA\u63D0\u4E00\u6B21\uFF09 +2. \u4EA4\u53C9\u9A8C\u8BC1\uFF1A\u81F3\u5C11 2 \u4E2A reviewer \u540C\u610F\u7684\u95EE\u9898\u6807\u8BB0\u4E3A"\u5DF2\u786E\u8BA4" +3. \u51B2\u7A81\u65F6\u53D6\u591A\u6570\u610F\u89C1 +4. \u4FDD\u7559\u9886\u57DF\u7279\u5B9A\u89C1\u89E3\uFF08\u5982\u5B89\u5168\u53D1\u73B0\u53EA\u6765\u81EA\u5B89\u5168 reviewer\uFF09 +5. \u4F7F\u7528\u6700\u4E25\u91CD\u53D1\u73B0\u7684\u51B3\u7B56\u4F5C\u4E3A\u6700\u7EC8\u51B3\u7B56 +6. \u53EA\u62A5\u544A\u5F53\u524D\u4EE3\u7801\u4E2D\u4ECD\u5B58\u5728\u7684\u95EE\u9898 + +\u4EE5\u4E0B\u662F\u5404 reviewer \u7684\u5BA1\u67E5\u7ED3\u679C\uFF1A + +{{REVIEWS}} + +\u8F93\u51FA\u683C\u5F0F\uFF08\u4E2D\u6587\uFF09\uFF1A +- \u7B2C\u4E00\u884C\uFF1A\u6700\u7EC8\u51B3\u7B56\uFF08\u53EF\u5408\u5E76 / \u6709\u6761\u4EF6\u5408\u5E76 / \u4E0D\u53EF\u5408\u5E76\uFF09 +- \u7136\u540E\u7B80\u8981\u603B\u7ED3 +- "\u963B\u585E\u9879"\u5217\u51FA\u5408\u5E76\u524D\u5FC5\u987B\u4FEE\u590D\u7684\u95EE\u9898\uFF1B\u5982\u65E0\uFF0C\u5199"\u963B\u585E\u9879\uFF1A\u65E0" +- "\u5EFA\u8BAE\u9879"\u5217\u51FA\u975E\u963B\u585E\u6539\u8FDB\u5EFA\u8BAE\uFF1B\u5982\u65E0\uFF0C\u5199"\u5EFA\u8BAE\u9879\uFF1A\u65E0"`; +function extractText(messages) { + return messages.filter((m) => m.info.role === "assistant").flatMap((m) => m.parts.filter((p) => p.type === "text")).map((p) => p.text).join("\n"); +} +function withTimeout(promise, ms, label) { + let timer; + return Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms); + }) + ]).finally(() => { + if (timer !== void 0) clearTimeout(timer); + }); +} +async function runParallelReviewers(client2, reviewers, prDiff, opts) { + const deadline = Date.now() + opts.globalTimeoutMs; + const promises = reviewers.map(async (reviewer) => { + try { + const remaining = () => Math.max(3e4, deadline - Date.now()); + console.log(`[${reviewer.name}] Starting review (timeout: ${remaining()}ms)...`); + const sessionResult = await withTimeout( + client2.session.create({ throwOnError: true }), + remaining(), + reviewer.name + ); + const sessionId = sessionResult.data.id; + const promptResult = await withTimeout( + client2.session.prompt({ + path: { id: sessionId }, + body: { + parts: [{ type: "text", text: reviewer.prompt + "\n\nPR Diff:\n```\n" + prDiff + "\n```" }] + }, + throwOnError: true + }), + remaining(), + reviewer.name + ); + const messagesResult = await withTimeout( + client2.session.messages({ path: { id: sessionId }, throwOnError: true }), + remaining(), + reviewer.name + ); + const content = extractText(messagesResult.data); + console.log(`[${reviewer.name}] Review complete (${content.length} chars)`); + try { + await client2.session.delete({ path: { id: sessionId } }); + } catch { + } + return { reviewer: reviewer.name, content, success: true }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + console.error(`[${reviewer.name}] Failed: ${msg}`); + return { reviewer: reviewer.name, content: "", success: false, error: msg }; + } + }); + return Promise.all(promises); +} +async function runCoordinator(client2, reviews, opts) { + const reviewsText = reviews.map((r) => `## ${r.reviewer} +${r.success ? r.content : `\uFF08\u5931\u8D25: ${r.error}\uFF09`}`).join("\n\n---\n\n"); + const promptTemplate = opts.coordinatorPrompt || DEFAULT_COORDINATOR_PROMPT; + const fullPrompt = promptTemplate.split("{{REVIEWS}}").join(reviewsText); + try { + const sessionResult = await withTimeout( + client2.session.create({ throwOnError: true }), + opts.coordinatorTimeoutMs, + "coordinator" + ); + const sessionId = sessionResult.data.id; + console.log("[coordinator] Starting synthesis..."); + await withTimeout( + client2.session.prompt({ + path: { id: sessionId }, + body: { parts: [{ type: "text", text: fullPrompt }] }, + throwOnError: true + }), + opts.coordinatorTimeoutMs, + "coordinator" + ); + const messagesResult = await withTimeout( + client2.session.messages({ path: { id: sessionId }, throwOnError: true }), + opts.coordinatorTimeoutMs, + "coordinator" + ); + const content = extractText(messagesResult.data); + console.log(`[coordinator] Synthesis complete (${content.length} chars)`); + try { + await client2.session.delete({ path: { id: sessionId } }); + } catch { + } + return content; + } catch (err) { + throw err; + } +} +function buildFallbackComment(reviews) { + const parts = reviews.map((r) => { + if (r.success) return `## ${r.reviewer} +${r.content}`; + return `## ${r.reviewer} +\uFF08\u5BA1\u67E5\u5931\u8D25: ${r.error}\uFF09`; + }); + return "**Multi-Review (fallback \u2014 coordinator failed)**\n\n" + parts.join("\n\n---\n\n"); +} + +// src/comment.ts +import { execFileSync } from "child_process"; +function resolvePRNumber() { + const ref = process.env.GITHUB_REF || ""; + const match = ref.match(/^refs\/pull\/(\d+)\/merge$/); + return match ? match[1] : null; +} +function postPRComment(body) { + const prNumber = resolvePRNumber(); + if (!prNumber) { + console.log("Not in PR context, printing review to stdout:"); + console.log("---"); + console.log(body); + return; + } + const repo = process.env.GITHUB_REPOSITORY || ""; + try { + execFileSync("gh", ["pr", "comment", prNumber, "--repo", repo, "--body", body], { + env: { ...process.env }, + timeout: 3e4, + stdio: "pipe" + }); + console.log(`Posted review comment on PR #${prNumber}`); + } catch (err) { + console.error(`Failed to post comment: ${err}`); + console.log("--- Review (fallback) ---"); + console.log(body); + } +} +function cleanupErrorComments() { + const enabled = process.env.MULTI_REVIEW_CLEANUP_ERROR_COMMENTS || "true"; + if (enabled.toLowerCase() !== "true") return; + const prNumber = resolvePRNumber(); + if (!prNumber) return; + const repo = process.env.GITHUB_REPOSITORY || ""; + const runId = process.env.GITHUB_RUN_ID || ""; + if (!repo || !runId) return; + const runLinkPattern = `/${repo}/actions/runs/${runId}`; + const errorRe = /(fatal:|remote:|error:\s*\d{3}|unable to access|Write access|permission denied)/i; + let comments; + try { + const raw = execFileSync("gh", ["api", "--paginate", "-H", "Accept: application/vnd.github+json", `/repos/${repo}/issues/${prNumber}/comments`], { + env: { ...process.env }, + timeout: 3e4, + stdio: "pipe", + maxBuffer: 5 * 1024 * 1024 + }); + comments = JSON.parse(raw.toString()); + } catch { + console.error("cleanup-error-comments: failed to list comments"); + return; + } + for (const comment of comments) { + if (!comment.body) continue; + if (!comment.body.includes(runLinkPattern) || !errorRe.test(comment.body)) continue; + try { + execFileSync("gh", ["api", "-X", "DELETE", `/repos/${repo}/issues/comments/${comment.id}`], { + env: { ...process.env }, + timeout: 1e4, + stdio: "pipe" + }); + console.log(`Deleted error comment ${comment.id}`); + } catch { + } + } +} +function parseExtraEnv() { + const raw = process.env.MULTI_REVIEW_EXTRA_ENV || ""; + if (!raw) return; + for (const line of raw.split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + const value = trimmed.slice(eqIdx + 1).trim(); + if (key) process.env[key] = value; + } +} + +// src/index.ts +async function main() { + parseExtraEnv(); + const actionPath = env("GITHUB_ACTION_PATH"); + const runnerTemp = env("RUNNER_TEMP") || "/tmp"; + const diffPath = join2(runnerTemp, ".pr-diff.txt"); + let prDiff = ""; + try { + prDiff = readFileSync2(diffPath, "utf-8"); + } catch { + console.error("No PR diff found at", diffPath); + return 1; + } + console.log(`PR diff loaded: ${prDiff.length} chars`); + const reviewers = loadReviewers({ actionPath }); + if (reviewers.length === 0) { + console.error("No reviewers configured"); + return 1; + } + console.log(`Reviewers: ${reviewers.map((r) => r.name).join(", ")}`); + const { providerID, modelID } = resolveModel(); + console.log(`Model: ${providerID}/${modelID}`); + console.log("Starting opencode server..."); + const { client: client2, server } = await createOpencode({ + config: { model: `${providerID}/${modelID}` } + }); + console.log("Server ready"); + try { + const globalTimeout = intEnv("MULTI_REVIEW_TIMEOUT_SECONDS", 900); + const coordinatorTimeout = intEnv("MULTI_REVIEW_COORDINATOR_TIMEOUT_SECONDS", 300); + const reviews = await runParallelReviewers(client2, reviewers, prDiff, { + globalTimeoutMs: globalTimeout * 1e3, + coordinatorTimeoutMs: coordinatorTimeout * 1e3, + coordinatorPrompt: env("MULTI_REVIEW_COORDINATOR_PROMPT") + }); + const successCount = reviews.filter((r) => r.success).length; + console.log(`Reviews: ${successCount}/${reviews.length} succeeded`); + if (successCount === 0) { + console.error("All reviewers failed"); + return 1; + } + let comment; + try { + comment = await runCoordinator(client2, reviews, { + globalTimeoutMs: globalTimeout * 1e3, + coordinatorTimeoutMs: coordinatorTimeout * 1e3, + coordinatorPrompt: env("MULTI_REVIEW_COORDINATOR_PROMPT") + }); + } catch (err) { + console.error(`Coordinator failed: ${err}`); + comment = buildFallbackComment(reviews); + } + postPRComment(comment); + cleanupErrorComments(); + return 0; + } finally { + server.close(); + } +} +main().then((code) => process.exit(code)).catch((err) => { + console.error("Fatal error:", err); + process.exit(1); +}); diff --git a/multi-review/package-lock.json b/multi-review/package-lock.json new file mode 100644 index 0000000..f949d60 --- /dev/null +++ b/multi-review/package-lock.json @@ -0,0 +1,1567 @@ +{ + "name": "opencode-multi-review", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "opencode-multi-review", + "version": "1.0.0", + "dependencies": { + "@opencode-ai/sdk": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsup": "^8.0.0", + "typescript": "^5.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@opencode-ai/sdk": { + "version": "1.15.10", + "resolved": "https://registry.npmmirror.com/@opencode-ai/sdk/-/sdk-1.15.10.tgz", + "integrity": "sha512-CUhpmMGGOqzvPnNNjjWmEIodAfP6Qnuki2ChIUKWYF7UImZ4zUcMZnzO5BtUxu/Ni1P8qzWxDioXs+7aIZQEhA==", + "license": "MIT", + "dependencies": { + "cross-spawn": "7.0.6" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmmirror.com/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmmirror.com/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmmirror.com/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + } + } +} diff --git a/multi-review/package.json b/multi-review/package.json new file mode 100644 index 0000000..5474caf --- /dev/null +++ b/multi-review/package.json @@ -0,0 +1,18 @@ +{ + "name": "opencode-multi-review", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsup", + "check": "tsc --noEmit" + }, + "dependencies": { + "@opencode-ai/sdk": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsup": "^8.0.0", + "typescript": "^5.7.0" + } +} diff --git a/multi-review/reviewers/architecture.yaml b/multi-review/reviewers/architecture.yaml new file mode 100644 index 0000000..fabe822 --- /dev/null +++ b/multi-review/reviewers/architecture.yaml @@ -0,0 +1,31 @@ +name: architecture +prompt: | + Review this pull request from an architecture perspective (read-only mode, DO NOT modify any code). + + Analyze the PR changes for: + - Coupling: unnecessary dependencies between modules + - Module placement: correct location and naming + - Layering: separation of concerns, mixing of domains + - Interface design: appropriate abstraction levels + - Shotgun surgery: scattered changes vs localized modifications + - Consistency: alignment with existing architectural patterns + + Please respond in Chinese. DO NOT modify any code, only provide review comments. + The first line of your response must be exactly one of: + - 架构合理 + - 架构有疑虑 + - 架构有问题 + + Decision rules: + - Use "架构合理" when the change follows existing architecture patterns. + - Use "架构有疑虑" when there are non-blocking architecture concerns. + - Use "架构有问题" when serious architectural violations should block merge. + + Review the latest PR HEAD only. + Do not repeat issues from earlier revisions unless they are still reproducible in the current files. + + Output format: + - First line: the decision only + - Then a short summary of architecture analysis + - Then "阻塞项" listing architecture issues that must block merge; if none, write "阻塞项:无" + - Then "建议项" listing non-blocking architecture improvements; if none, write "建议项:无" diff --git a/multi-review/reviewers/performance.yaml b/multi-review/reviewers/performance.yaml new file mode 100644 index 0000000..c105c14 --- /dev/null +++ b/multi-review/reviewers/performance.yaml @@ -0,0 +1,32 @@ +name: performance +prompt: | + Review this pull request for performance concerns (read-only mode, DO NOT modify any code). + + Please check: + - Algorithm complexity and efficiency + - Memory allocation patterns and potential leaks + - Database query efficiency (N+1 queries, missing indexes) + - Caching opportunities + - Unnecessary computations or redundant operations + - Concurrency and threading issues + - Resource utilization (CPU, memory, network, I/O) + + Please respond in Chinese. DO NOT modify any code, only provide review comments. + The first line of your response must be exactly one of: + - 性能良好 + - 性能有疑虑 + - 性能问题严重 + + Decision rules: + - Use "性能良好" when no performance concerns are found. + - Use "性能有疑虑" when non-critical performance issues are found. + - Use "性能问题严重" when critical performance regressions are found that should block merge. + + Review the latest PR HEAD only. + Do not repeat issues from earlier revisions unless they are still reproducible in the current files. + + Output format: + - First line: the decision only + - Then a short summary of performance analysis + - Then "阻塞项" listing performance issues that must block merge; if none, write "阻塞项:无" + - Then "建议项" listing non-blocking performance improvements; if none, write "建议项:无" diff --git a/multi-review/reviewers/quality.yaml b/multi-review/reviewers/quality.yaml new file mode 100644 index 0000000..1572082 --- /dev/null +++ b/multi-review/reviewers/quality.yaml @@ -0,0 +1,30 @@ +name: quality +prompt: | + Review this pull request for code quality (read-only mode, DO NOT modify any code). + + Please check: + - Code quality issues + - Potential bugs or logic errors + - Code style consistency + - Error handling completeness + + Please respond in Chinese. DO NOT modify any code, only provide review comments. + The first line of your response must be exactly one of: + - 可合并 + - 有条件合并 + - 不可合并 + + Decision rules: + - Use "可合并" only when there are no blocking issues. + - Use "有条件合并" when merge is acceptable only after specific issues are fixed. + - Use "不可合并" when there are blocking risks, correctness issues, or major concerns that should stop the merge. + + Review the latest PR HEAD only. + Do not repeat issues from earlier revisions unless they are still reproducible in the current files. + Verify any blocking claim against the current code and current CI status before listing it. + + Output format: + - First line: the decision only + - Then a short summary + - Then "阻塞项" listing required fixes for merge; if none, write "阻塞项:无" + - Then "建议项" listing non-blocking improvements; if none, write "建议项:无" diff --git a/multi-review/reviewers/security.yaml b/multi-review/reviewers/security.yaml new file mode 100644 index 0000000..620d6c6 --- /dev/null +++ b/multi-review/reviewers/security.yaml @@ -0,0 +1,31 @@ +name: security +prompt: | + Review this pull request for security concerns (read-only mode, DO NOT modify any code). + + Please check: + - Input validation and sanitization + - Authentication and authorization issues + - Injection vulnerabilities (SQL, XSS, command injection) + - Sensitive data exposure (secrets, tokens, PII) + - Insecure dependencies or APIs + - OWASP Top 10 compliance + + Please respond in Chinese. DO NOT modify any code, only provide review comments. + The first line of your response must be exactly one of: + - 安全无虞 + - 存在风险 + - 高危漏洞 + + Decision rules: + - Use "安全无虞" when no security concerns are found. + - Use "存在风险" when non-critical security issues are found that should be addressed. + - Use "高危漏洞" when critical security vulnerabilities are found that must block merge. + + Review the latest PR HEAD only. + Do not repeat issues from earlier revisions unless they are still reproducible in the current files. + + Output format: + - First line: the decision only + - Then a short summary of security analysis + - Then "阻塞项" listing security issues that must block merge; if none, write "阻塞项:无" + - Then "建议项" listing non-blocking security improvements; if none, write "建议项:无" diff --git a/multi-review/src/comment.ts b/multi-review/src/comment.ts new file mode 100644 index 0000000..43b7fb7 --- /dev/null +++ b/multi-review/src/comment.ts @@ -0,0 +1,90 @@ +import { execFileSync } from "node:child_process"; + +export function resolvePRNumber(): string | null { + const ref = process.env.GITHUB_REF || ""; + const match = ref.match(/^refs\/pull\/(\d+)\/merge$/); + return match ? match[1] : null; +} + +export function postPRComment(body: string): void { + const prNumber = resolvePRNumber(); + if (!prNumber) { + console.log("Not in PR context, printing review to stdout:"); + console.log("---"); + console.log(body); + return; + } + + const repo = process.env.GITHUB_REPOSITORY || ""; + + try { + execFileSync("gh", ["pr", "comment", prNumber, "--repo", repo, "--body", body], { + env: { ...process.env }, + timeout: 30_000, + stdio: "pipe", + }); + console.log(`Posted review comment on PR #${prNumber}`); + } catch (err) { + console.error(`Failed to post comment: ${err}`); + console.log("--- Review (fallback) ---"); + console.log(body); + } +} + +export function cleanupErrorComments(): void { + const enabled = process.env.MULTI_REVIEW_CLEANUP_ERROR_COMMENTS || "true"; + if (enabled.toLowerCase() !== "true") return; + + const prNumber = resolvePRNumber(); + if (!prNumber) return; + + const repo = process.env.GITHUB_REPOSITORY || ""; + const runId = process.env.GITHUB_RUN_ID || ""; + if (!repo || !runId) return; + + const runLinkPattern = `/${repo}/actions/runs/${runId}`; + const errorRe = /(fatal:|remote:|error:\s*\d{3}|unable to access|Write access|permission denied)/i; + + let comments: Array<{ id: number; body: string }>; + try { + const raw = execFileSync("gh", ["api", "--paginate", "-H", "Accept: application/vnd.github+json", `/repos/${repo}/issues/${prNumber}/comments`], { + env: { ...process.env }, + timeout: 30_000, + stdio: "pipe", + maxBuffer: 5 * 1024 * 1024, + }); + comments = JSON.parse(raw.toString()); + } catch { + console.error("cleanup-error-comments: failed to list comments"); + return; + } + + for (const comment of comments) { + if (!comment.body) continue; + if (!comment.body.includes(runLinkPattern) || !errorRe.test(comment.body)) continue; + try { + execFileSync("gh", ["api", "-X", "DELETE", `/repos/${repo}/issues/comments/${comment.id}`], { + env: { ...process.env }, + timeout: 10_000, + stdio: "pipe", + }); + console.log(`Deleted error comment ${comment.id}`); + } catch { + /* ignore */ + } + } +} + +export function parseExtraEnv(): void { + const raw = process.env.MULTI_REVIEW_EXTRA_ENV || ""; + if (!raw) return; + for (const line of raw.split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + const value = trimmed.slice(eqIdx + 1).trim(); + if (key) process.env[key] = value; + } +} diff --git a/multi-review/src/index.ts b/multi-review/src/index.ts new file mode 100644 index 0000000..24ccb93 --- /dev/null +++ b/multi-review/src/index.ts @@ -0,0 +1,94 @@ +import { createOpencode } from "@opencode-ai/sdk"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { loadReviewers, resolveModel, env, intEnv } from "./reviewers.js"; +import { runParallelReviewers, runCoordinator, buildFallbackComment } from "./orchestrator.js"; +import { postPRComment, cleanupErrorComments, parseExtraEnv } from "./comment.js"; + +async function main(): Promise { + // 0. Parse extra env vars into process.env + parseExtraEnv(); + + const actionPath = env("GITHUB_ACTION_PATH"); + const runnerTemp = env("RUNNER_TEMP") || "/tmp"; + + // 1. Read PR diff (pre-fetched by action.yml) + const diffPath = join(runnerTemp, ".pr-diff.txt"); + let prDiff = ""; + try { + prDiff = readFileSync(diffPath, "utf-8"); + } catch { + console.error("No PR diff found at", diffPath); + return 1; + } + console.log(`PR diff loaded: ${prDiff.length} chars`); + + // 2. Load reviewers + const reviewers = loadReviewers({ actionPath }); + if (reviewers.length === 0) { + console.error("No reviewers configured"); + return 1; + } + console.log(`Reviewers: ${reviewers.map((r) => r.name).join(", ")}`); + + // 3. Resolve model + const { providerID, modelID } = resolveModel(); + console.log(`Model: ${providerID}/${modelID}`); + + // 4. Start opencode server via SDK + console.log("Starting opencode server..."); + const { client, server } = await createOpencode({ + config: { model: `${providerID}/${modelID}` }, + }); + console.log("Server ready"); + + try { + // 5. Run reviewers in parallel + const globalTimeout = intEnv("MULTI_REVIEW_TIMEOUT_SECONDS", 900); + const coordinatorTimeout = intEnv("MULTI_REVIEW_COORDINATOR_TIMEOUT_SECONDS", 300); + + const reviews = await runParallelReviewers(client, reviewers, prDiff, { + globalTimeoutMs: globalTimeout * 1000, + coordinatorTimeoutMs: coordinatorTimeout * 1000, + coordinatorPrompt: env("MULTI_REVIEW_COORDINATOR_PROMPT"), + }); + + const successCount = reviews.filter((r) => r.success).length; + console.log(`Reviews: ${successCount}/${reviews.length} succeeded`); + + if (successCount === 0) { + console.error("All reviewers failed"); + return 1; + } + + // 6. Run coordinator + let comment: string; + try { + comment = await runCoordinator(client, reviews, { + globalTimeoutMs: globalTimeout * 1000, + coordinatorTimeoutMs: coordinatorTimeout * 1000, + coordinatorPrompt: env("MULTI_REVIEW_COORDINATOR_PROMPT"), + }); + } catch (err) { + console.error(`Coordinator failed: ${err}`); + comment = buildFallbackComment(reviews); + } + + // 7. Post comment + postPRComment(comment); + + // 8. Cleanup error comments from previous runs + cleanupErrorComments(); + + return 0; + } finally { + server.close(); + } +} + +main() + .then((code) => process.exit(code)) + .catch((err) => { + console.error("Fatal error:", err); + process.exit(1); + }); diff --git a/multi-review/src/orchestrator.ts b/multi-review/src/orchestrator.ts new file mode 100644 index 0000000..24f14ee --- /dev/null +++ b/multi-review/src/orchestrator.ts @@ -0,0 +1,150 @@ +import type { OpencodeClient } from "@opencode-ai/sdk"; +import type { Reviewer, ReviewResult, OrchestratorOptions } from "./types.js"; + +const DEFAULT_COORDINATOR_PROMPT = `你是一个代码审查协调员。以下审查由独立的专家 reviewer 生成。 +你的任务是整合为一个去重后的综合报告。 + +规则: +1. 跨 reviewer 去重(同一问题只提一次) +2. 交叉验证:至少 2 个 reviewer 同意的问题标记为"已确认" +3. 冲突时取多数意见 +4. 保留领域特定见解(如安全发现只来自安全 reviewer) +5. 使用最严重发现的决策作为最终决策 +6. 只报告当前代码中仍存在的问题 + +以下是各 reviewer 的审查结果: + +{{REVIEWS}} + +输出格式(中文): +- 第一行:最终决策(可合并 / 有条件合并 / 不可合并) +- 然后简要总结 +- "阻塞项"列出合并前必须修复的问题;如无,写"阻塞项:无" +- "建议项"列出非阻塞改进建议;如无,写"建议项:无"`; + +function extractText(messages: Array<{ info: { role: string }; parts: Array<{ type: string; text?: string }> }>): string { + return messages + .filter((m) => m.info.role === "assistant") + .flatMap((m) => m.parts.filter((p): p is { type: "text"; text: string } => p.type === "text")) + .map((p) => p.text) + .join("\n"); +} + +function withTimeout(promise: Promise, ms: number, label: string): Promise { + let timer: ReturnType | undefined; + return Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms); + }), + ]).finally(() => { if (timer !== undefined) clearTimeout(timer); }); +} + +export async function runParallelReviewers( + client: OpencodeClient, + reviewers: Reviewer[], + prDiff: string, + opts: OrchestratorOptions, +): Promise { + const deadline = Date.now() + opts.globalTimeoutMs; + + const promises = reviewers.map(async (reviewer) => { + try { + const remaining = () => Math.max(30_000, deadline - Date.now()); + console.log(`[${reviewer.name}] Starting review (timeout: ${remaining()}ms)...`); + + const sessionResult = await withTimeout( + client.session.create({ throwOnError: true }), + remaining(), + reviewer.name, + ); + const sessionId = sessionResult.data.id; + + const promptResult = await withTimeout( + client.session.prompt({ + path: { id: sessionId }, + body: { + parts: [{ type: "text", text: reviewer.prompt + "\n\nPR Diff:\n```\n" + prDiff + "\n```" }], + }, + throwOnError: true, + }), + remaining(), + reviewer.name, + ); + + const messagesResult = await withTimeout( + client.session.messages({ path: { id: sessionId }, throwOnError: true }), + remaining(), + reviewer.name, + ); + const content = extractText(messagesResult.data); + + console.log(`[${reviewer.name}] Review complete (${content.length} chars)`); + + try { await client.session.delete({ path: { id: sessionId } }); } catch { /* ignore */ } + return { reviewer: reviewer.name, content, success: true }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + console.error(`[${reviewer.name}] Failed: ${msg}`); + return { reviewer: reviewer.name, content: "", success: false, error: msg }; + } + }); + + return Promise.all(promises); +} + +export async function runCoordinator( + client: OpencodeClient, + reviews: ReviewResult[], + opts: OrchestratorOptions, +): Promise { + const reviewsText = reviews + .map((r) => `## ${r.reviewer}\n${r.success ? r.content : `(失败: ${r.error})`}`) + .join("\n\n---\n\n"); + + const promptTemplate = opts.coordinatorPrompt || DEFAULT_COORDINATOR_PROMPT; + const fullPrompt = promptTemplate.split("{{REVIEWS}}").join(reviewsText); + + try { + const sessionResult = await withTimeout( + client.session.create({ throwOnError: true }), + opts.coordinatorTimeoutMs, + "coordinator", + ); + const sessionId = sessionResult.data.id; + + console.log("[coordinator] Starting synthesis..."); + + await withTimeout( + client.session.prompt({ + path: { id: sessionId }, + body: { parts: [{ type: "text", text: fullPrompt }] }, + throwOnError: true, + }), + opts.coordinatorTimeoutMs, + "coordinator", + ); + + const messagesResult = await withTimeout( + client.session.messages({ path: { id: sessionId }, throwOnError: true }), + opts.coordinatorTimeoutMs, + "coordinator", + ); + const content = extractText(messagesResult.data); + + console.log(`[coordinator] Synthesis complete (${content.length} chars)`); + + try { await client.session.delete({ path: { id: sessionId } }); } catch { /* ignore */ } + return content; + } catch (err) { + throw err; + } +} + +export function buildFallbackComment(reviews: ReviewResult[]): string { + const parts = reviews.map((r) => { + if (r.success) return `## ${r.reviewer}\n${r.content}`; + return `## ${r.reviewer}\n(审查失败: ${r.error})`; + }); + return "**Multi-Review (fallback — coordinator failed)**\n\n" + parts.join("\n\n---\n\n"); +} diff --git a/multi-review/src/reviewers.ts b/multi-review/src/reviewers.ts new file mode 100644 index 0000000..d66d7e5 --- /dev/null +++ b/multi-review/src/reviewers.ts @@ -0,0 +1,104 @@ +import { readFileSync } from "node:fs"; +import { join, resolve } from "node:path"; +import type { Reviewer } from "./types.js"; + +interface PersonaYAML { + name: string; + prompt: string; +} + +const DEFAULT_TEAM = "quality:1,security:1,performance:1,architecture:1"; + +function parseTeam(teamStr: string): Map { + const result = new Map(); + for (const entry of teamStr.split(",")) { + const [name, count] = entry.trim().split(":"); + if (name) result.set(name.trim(), Math.max(1, parseInt(count || "1", 10) || 1)); + } + return result; +} + +function loadBuiltInReviewers(reviewersDir: string): Map { + const map = new Map(); + for (const file of ["quality.yaml", "security.yaml", "performance.yaml", "architecture.yaml"]) { + try { + const raw = readFileSync(join(reviewersDir, file), "utf-8"); + const parsed = parseYAML(raw); + if (parsed.name && parsed.prompt) map.set(parsed.name, { name: parsed.name, prompt: parsed.prompt }); + } catch { /* skip missing files */ } + } + return map; +} + +function parseYAML(raw: string): Record { + const result: Record = {}; + let currentKey = ""; + let inPrompt = false; + for (const line of raw.split("\n")) { + if (!inPrompt && line.match(/^(\w+):\s*(.*)/)) { + const [, key, value] = line.match(/^(\w+):\s*(.*)/) || []; + if (key === "prompt" && value.trim().startsWith("|")) { + inPrompt = true; + currentKey = "prompt"; + result.prompt = ""; + } else if (key) { + result[key] = value?.trim() || ""; + } + } else if (inPrompt) { + if (line && !line.startsWith(" ") && !line.startsWith("\t")) { + inPrompt = false; + } else { + result[currentKey] = (result[currentKey] || "") + line.trimStart() + "\n"; + } + } + } + if (result.prompt) result.prompt = result.prompt.trim(); + return result; +} + +export function loadReviewers(opts: { + actionPath: string; + team?: string; + configPath?: string; +}): Reviewer[] { + const builtInDir = join(opts.actionPath, "reviewers"); + const personas = loadBuiltInReviewers(builtInDir); + + const teamStr = opts.team || env("MULTI_REVIEW_DEFAULT_TEAM") || DEFAULT_TEAM; + const team = parseTeam(teamStr); + + const reviewers: Reviewer[] = []; + for (const [name, count] of team) { + const persona = personas.get(name); + if (!persona) { + console.warn(`Warning: unknown reviewer persona "${name}", skipping`); + continue; + } + for (let i = 0; i < count; i++) { + reviewers.push({ + name: count > 1 ? `${name}-${i + 1}` : name, + prompt: persona.prompt, + }); + } + } + + return reviewers; +} + +export function env(key: string): string { + return process.env[key] || ""; +} + +export function intEnv(key: string, fallback: number): number { + const v = parseInt(process.env[key] || "", 10); + return isNaN(v) ? fallback : v; +} + +export function resolveModel(): { providerID: string; modelID: string } { + const raw = env("MULTI_REVIEW_MODEL") || env("MODEL_NAME") || "zhipuai-coding-plan/glm-5.1"; + const idx = raw.indexOf("/"); + if (idx === -1) { + throw new Error(`Model "${raw}" missing provider (expected format: provider/model)`); + } + return { providerID: raw.slice(0, idx), modelID: raw.slice(idx + 1) }; +} diff --git a/multi-review/src/types.ts b/multi-review/src/types.ts new file mode 100644 index 0000000..8d752fb --- /dev/null +++ b/multi-review/src/types.ts @@ -0,0 +1,17 @@ +export interface Reviewer { + name: string; + prompt: string; +} + +export interface ReviewResult { + reviewer: string; + content: string; + success: boolean; + error?: string; +} + +export interface OrchestratorOptions { + globalTimeoutMs: number; + coordinatorTimeoutMs: number; + coordinatorPrompt: string; +} diff --git a/multi-review/tsconfig.json b/multi-review/tsconfig.json new file mode 100644 index 0000000..cd9fbd8 --- /dev/null +++ b/multi-review/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "src", + "resolveJsonModule": true, + "declaration": true + }, + "include": ["src/**/*.ts"] +} diff --git a/multi-review/tsup.config.ts b/multi-review/tsup.config.ts new file mode 100644 index 0000000..4a72c41 --- /dev/null +++ b/multi-review/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm"], + platform: "node", + target: "node20", + clean: true, + noExternal: ["@opencode-ai/sdk"], +});