mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-03-31 06:12:26 +00:00
refactor(opencode): replace Bun shell in core flows (#16286)
This commit is contained in:
@@ -27,8 +27,9 @@ import { Provider } from "../../provider/provider"
|
||||
import { Bus } from "../../bus"
|
||||
import { MessageV2 } from "../../session/message-v2"
|
||||
import { SessionPrompt } from "@/session/prompt"
|
||||
import { $ } from "bun"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
import { Process } from "@/util/process"
|
||||
import { git } from "@/util/git"
|
||||
|
||||
type GitHubAuthor = {
|
||||
login: string
|
||||
@@ -255,7 +256,7 @@ export const GithubInstallCommand = cmd({
|
||||
}
|
||||
|
||||
// Get repo info
|
||||
const info = (await $`git remote get-url origin`.quiet().nothrow().text()).trim()
|
||||
const info = (await git(["remote", "get-url", "origin"], { cwd: Instance.worktree })).text().trim()
|
||||
const parsed = parseGitHubRemote(info)
|
||||
if (!parsed) {
|
||||
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
|
||||
@@ -493,6 +494,26 @@ export const GithubRunCommand = cmd({
|
||||
? "pr_review"
|
||||
: "issue"
|
||||
: undefined
|
||||
const gitText = async (args: string[]) => {
|
||||
const result = await git(args, { cwd: Instance.worktree })
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
|
||||
}
|
||||
return result.text().trim()
|
||||
}
|
||||
const gitRun = async (args: string[]) => {
|
||||
const result = await git(args, { cwd: Instance.worktree })
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
|
||||
}
|
||||
return result
|
||||
}
|
||||
const gitStatus = (args: string[]) => git(args, { cwd: Instance.worktree })
|
||||
const commitChanges = async (summary: string, actor?: string) => {
|
||||
const args = ["commit", "-m", summary]
|
||||
if (actor) args.push("-m", `Co-authored-by: ${actor} <${actor}@users.noreply.github.com>`)
|
||||
await gitRun(args)
|
||||
}
|
||||
|
||||
try {
|
||||
if (useGithubToken) {
|
||||
@@ -553,7 +574,7 @@ export const GithubRunCommand = cmd({
|
||||
}
|
||||
const branchPrefix = isWorkflowDispatchEvent ? "dispatch" : "schedule"
|
||||
const branch = await checkoutNewBranch(branchPrefix)
|
||||
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
|
||||
const head = await gitText(["rev-parse", "HEAD"])
|
||||
const response = await chat(userPrompt, promptFiles)
|
||||
const { dirty, uncommittedChanges, switched } = await branchIsDirty(head, branch)
|
||||
if (switched) {
|
||||
@@ -587,7 +608,7 @@ export const GithubRunCommand = cmd({
|
||||
// Local PR
|
||||
if (prData.headRepository.nameWithOwner === prData.baseRepository.nameWithOwner) {
|
||||
await checkoutLocalBranch(prData)
|
||||
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
|
||||
const head = await gitText(["rev-parse", "HEAD"])
|
||||
const dataPrompt = buildPromptDataForPR(prData)
|
||||
const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
|
||||
const { dirty, uncommittedChanges, switched } = await branchIsDirty(head, prData.headRefName)
|
||||
@@ -605,7 +626,7 @@ export const GithubRunCommand = cmd({
|
||||
// Fork PR
|
||||
else {
|
||||
const forkBranch = await checkoutForkBranch(prData)
|
||||
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
|
||||
const head = await gitText(["rev-parse", "HEAD"])
|
||||
const dataPrompt = buildPromptDataForPR(prData)
|
||||
const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
|
||||
const { dirty, uncommittedChanges, switched } = await branchIsDirty(head, forkBranch)
|
||||
@@ -624,7 +645,7 @@ export const GithubRunCommand = cmd({
|
||||
// Issue
|
||||
else {
|
||||
const branch = await checkoutNewBranch("issue")
|
||||
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
|
||||
const head = await gitText(["rev-parse", "HEAD"])
|
||||
const issueData = await fetchIssue()
|
||||
const dataPrompt = buildPromptDataForIssue(issueData)
|
||||
const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
|
||||
@@ -658,7 +679,7 @@ export const GithubRunCommand = cmd({
|
||||
exitCode = 1
|
||||
console.error(e instanceof Error ? e.message : String(e))
|
||||
let msg = e
|
||||
if (e instanceof $.ShellError) {
|
||||
if (e instanceof Process.RunFailedError) {
|
||||
msg = e.stderr.toString()
|
||||
} else if (e instanceof Error) {
|
||||
msg = e.message
|
||||
@@ -1049,29 +1070,29 @@ export const GithubRunCommand = cmd({
|
||||
const config = "http.https://github.com/.extraheader"
|
||||
// actions/checkout@v6 no longer stores credentials in .git/config,
|
||||
// so this may not exist - use nothrow() to handle gracefully
|
||||
const ret = await $`git config --local --get ${config}`.nothrow()
|
||||
const ret = await gitStatus(["config", "--local", "--get", config])
|
||||
if (ret.exitCode === 0) {
|
||||
gitConfig = ret.stdout.toString().trim()
|
||||
await $`git config --local --unset-all ${config}`
|
||||
await gitRun(["config", "--local", "--unset-all", config])
|
||||
}
|
||||
|
||||
const newCredentials = Buffer.from(`x-access-token:${appToken}`, "utf8").toString("base64")
|
||||
|
||||
await $`git config --local ${config} "AUTHORIZATION: basic ${newCredentials}"`
|
||||
await $`git config --global user.name "${AGENT_USERNAME}"`
|
||||
await $`git config --global user.email "${AGENT_USERNAME}@users.noreply.github.com"`
|
||||
await gitRun(["config", "--local", config, `AUTHORIZATION: basic ${newCredentials}`])
|
||||
await gitRun(["config", "--global", "user.name", AGENT_USERNAME])
|
||||
await gitRun(["config", "--global", "user.email", `${AGENT_USERNAME}@users.noreply.github.com`])
|
||||
}
|
||||
|
||||
async function restoreGitConfig() {
|
||||
if (gitConfig === undefined) return
|
||||
const config = "http.https://github.com/.extraheader"
|
||||
await $`git config --local ${config} "${gitConfig}"`
|
||||
await gitRun(["config", "--local", config, gitConfig])
|
||||
}
|
||||
|
||||
async function checkoutNewBranch(type: "issue" | "schedule" | "dispatch") {
|
||||
console.log("Checking out new branch...")
|
||||
const branch = generateBranchName(type)
|
||||
await $`git checkout -b ${branch}`
|
||||
await gitRun(["checkout", "-b", branch])
|
||||
return branch
|
||||
}
|
||||
|
||||
@@ -1081,8 +1102,8 @@ export const GithubRunCommand = cmd({
|
||||
const branch = pr.headRefName
|
||||
const depth = Math.max(pr.commits.totalCount, 20)
|
||||
|
||||
await $`git fetch origin --depth=${depth} ${branch}`
|
||||
await $`git checkout ${branch}`
|
||||
await gitRun(["fetch", "origin", `--depth=${depth}`, branch])
|
||||
await gitRun(["checkout", branch])
|
||||
}
|
||||
|
||||
async function checkoutForkBranch(pr: GitHubPullRequest) {
|
||||
@@ -1092,9 +1113,9 @@ export const GithubRunCommand = cmd({
|
||||
const localBranch = generateBranchName("pr")
|
||||
const depth = Math.max(pr.commits.totalCount, 20)
|
||||
|
||||
await $`git remote add fork https://github.com/${pr.headRepository.nameWithOwner}.git`
|
||||
await $`git fetch fork --depth=${depth} ${remoteBranch}`
|
||||
await $`git checkout -b ${localBranch} fork/${remoteBranch}`
|
||||
await gitRun(["remote", "add", "fork", `https://github.com/${pr.headRepository.nameWithOwner}.git`])
|
||||
await gitRun(["fetch", "fork", `--depth=${depth}`, remoteBranch])
|
||||
await gitRun(["checkout", "-b", localBranch, `fork/${remoteBranch}`])
|
||||
return localBranch
|
||||
}
|
||||
|
||||
@@ -1115,28 +1136,23 @@ export const GithubRunCommand = cmd({
|
||||
async function pushToNewBranch(summary: string, branch: string, commit: boolean, isSchedule: boolean) {
|
||||
console.log("Pushing to new branch...")
|
||||
if (commit) {
|
||||
await $`git add .`
|
||||
await gitRun(["add", "."])
|
||||
if (isSchedule) {
|
||||
// No co-author for scheduled events - the schedule is operating as the repo
|
||||
await $`git commit -m "${summary}"`
|
||||
await commitChanges(summary)
|
||||
} else {
|
||||
await $`git commit -m "${summary}
|
||||
|
||||
Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
|
||||
await commitChanges(summary, actor)
|
||||
}
|
||||
}
|
||||
await $`git push -u origin ${branch}`
|
||||
await gitRun(["push", "-u", "origin", branch])
|
||||
}
|
||||
|
||||
async function pushToLocalBranch(summary: string, commit: boolean) {
|
||||
console.log("Pushing to local branch...")
|
||||
if (commit) {
|
||||
await $`git add .`
|
||||
await $`git commit -m "${summary}
|
||||
|
||||
Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
|
||||
await gitRun(["add", "."])
|
||||
await commitChanges(summary, actor)
|
||||
}
|
||||
await $`git push`
|
||||
await gitRun(["push"])
|
||||
}
|
||||
|
||||
async function pushToForkBranch(summary: string, pr: GitHubPullRequest, commit: boolean) {
|
||||
@@ -1145,30 +1161,28 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
|
||||
const remoteBranch = pr.headRefName
|
||||
|
||||
if (commit) {
|
||||
await $`git add .`
|
||||
await $`git commit -m "${summary}
|
||||
|
||||
Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
|
||||
await gitRun(["add", "."])
|
||||
await commitChanges(summary, actor)
|
||||
}
|
||||
await $`git push fork HEAD:${remoteBranch}`
|
||||
await gitRun(["push", "fork", `HEAD:${remoteBranch}`])
|
||||
}
|
||||
|
||||
async function branchIsDirty(originalHead: string, expectedBranch: string) {
|
||||
console.log("Checking if branch is dirty...")
|
||||
// Detect if the agent switched branches during chat (e.g. created
|
||||
// its own branch, committed, and possibly pushed/created a PR).
|
||||
const current = (await $`git rev-parse --abbrev-ref HEAD`).stdout.toString().trim()
|
||||
const current = await gitText(["rev-parse", "--abbrev-ref", "HEAD"])
|
||||
if (current !== expectedBranch) {
|
||||
console.log(`Branch changed during chat: expected ${expectedBranch}, now on ${current}`)
|
||||
return { dirty: true, uncommittedChanges: false, switched: true }
|
||||
}
|
||||
|
||||
const ret = await $`git status --porcelain`
|
||||
const ret = await gitStatus(["status", "--porcelain"])
|
||||
const status = ret.stdout.toString().trim()
|
||||
if (status.length > 0) {
|
||||
return { dirty: true, uncommittedChanges: true, switched: false }
|
||||
}
|
||||
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
|
||||
const head = await gitText(["rev-parse", "HEAD"])
|
||||
return {
|
||||
dirty: head !== originalHead,
|
||||
uncommittedChanges: false,
|
||||
@@ -1180,11 +1194,11 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
|
||||
// Falls back to fetching from origin when local refs are missing
|
||||
// (common in shallow clones from actions/checkout).
|
||||
async function hasNewCommits(base: string, head: string) {
|
||||
const result = await $`git rev-list --count ${base}..${head}`.nothrow()
|
||||
const result = await gitStatus(["rev-list", "--count", `${base}..${head}`])
|
||||
if (result.exitCode !== 0) {
|
||||
console.log(`rev-list failed, fetching origin/${base}...`)
|
||||
await $`git fetch origin ${base} --depth=1`.nothrow()
|
||||
const retry = await $`git rev-list --count origin/${base}..${head}`.nothrow()
|
||||
await gitStatus(["fetch", "origin", base, "--depth=1"])
|
||||
const retry = await gitStatus(["rev-list", "--count", `origin/${base}..${head}`])
|
||||
if (retry.exitCode !== 0) return true // assume dirty if we can't tell
|
||||
return parseInt(retry.stdout.toString().trim()) > 0
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { UI } from "../ui"
|
||||
import { cmd } from "./cmd"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { $ } from "bun"
|
||||
import { Process } from "@/util/process"
|
||||
import { git } from "@/util/git"
|
||||
|
||||
export const PrCommand = cmd({
|
||||
command: "pr <number>",
|
||||
@@ -27,21 +28,35 @@ export const PrCommand = cmd({
|
||||
UI.println(`Fetching and checking out PR #${prNumber}...`)
|
||||
|
||||
// Use gh pr checkout with custom branch name
|
||||
const result = await $`gh pr checkout ${prNumber} --branch ${localBranchName} --force`.nothrow()
|
||||
const result = await Process.run(
|
||||
["gh", "pr", "checkout", `${prNumber}`, "--branch", localBranchName, "--force"],
|
||||
{
|
||||
nothrow: true,
|
||||
},
|
||||
)
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
if (result.code !== 0) {
|
||||
UI.error(`Failed to checkout PR #${prNumber}. Make sure you have gh CLI installed and authenticated.`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Fetch PR info for fork handling and session link detection
|
||||
const prInfoResult =
|
||||
await $`gh pr view ${prNumber} --json headRepository,headRepositoryOwner,isCrossRepository,headRefName,body`.nothrow()
|
||||
const prInfoResult = await Process.text(
|
||||
[
|
||||
"gh",
|
||||
"pr",
|
||||
"view",
|
||||
`${prNumber}`,
|
||||
"--json",
|
||||
"headRepository,headRepositoryOwner,isCrossRepository,headRefName,body",
|
||||
],
|
||||
{ nothrow: true },
|
||||
)
|
||||
|
||||
let sessionId: string | undefined
|
||||
|
||||
if (prInfoResult.exitCode === 0) {
|
||||
const prInfoText = prInfoResult.text()
|
||||
if (prInfoResult.code === 0) {
|
||||
const prInfoText = prInfoResult.text
|
||||
if (prInfoText.trim()) {
|
||||
const prInfo = JSON.parse(prInfoText)
|
||||
|
||||
@@ -52,15 +67,19 @@ export const PrCommand = cmd({
|
||||
const remoteName = forkOwner
|
||||
|
||||
// Check if remote already exists
|
||||
const remotes = (await $`git remote`.nothrow().text()).trim()
|
||||
const remotes = (await git(["remote"], { cwd: Instance.worktree })).text().trim()
|
||||
if (!remotes.split("\n").includes(remoteName)) {
|
||||
await $`git remote add ${remoteName} https://github.com/${forkOwner}/${forkName}.git`.nothrow()
|
||||
await git(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], {
|
||||
cwd: Instance.worktree,
|
||||
})
|
||||
UI.println(`Added fork remote: ${remoteName}`)
|
||||
}
|
||||
|
||||
// Set upstream to the fork so pushes go there
|
||||
const headRefName = prInfo.headRefName
|
||||
await $`git branch --set-upstream-to=${remoteName}/${headRefName} ${localBranchName}`.nothrow()
|
||||
await git(["branch", `--set-upstream-to=${remoteName}/${headRefName}`, localBranchName], {
|
||||
cwd: Instance.worktree,
|
||||
})
|
||||
}
|
||||
|
||||
// Check for opencode session link in PR body
|
||||
@@ -71,9 +90,11 @@ export const PrCommand = cmd({
|
||||
UI.println(`Found opencode session: ${sessionUrl}`)
|
||||
UI.println(`Importing session...`)
|
||||
|
||||
const importResult = await $`opencode import ${sessionUrl}`.nothrow()
|
||||
if (importResult.exitCode === 0) {
|
||||
const importOutput = importResult.text().trim()
|
||||
const importResult = await Process.text(["opencode", "import", sessionUrl], {
|
||||
nothrow: true,
|
||||
})
|
||||
if (importResult.code === 0) {
|
||||
const importOutput = importResult.text.trim()
|
||||
// Extract session ID from the output (format: "Imported session: <session-id>")
|
||||
const sessionIdMatch = importOutput.match(/Imported session: ([a-zA-Z0-9_-]+)/)
|
||||
if (sessionIdMatch) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { $ } from "bun"
|
||||
import { platform, release } from "os"
|
||||
import clipboardy from "clipboardy"
|
||||
import { lazy } from "../../../../util/lazy.js"
|
||||
import { tmpdir } from "os"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { Filesystem } from "../../../../util/filesystem"
|
||||
import { Process } from "../../../../util/process"
|
||||
import { which } from "../../../../util/which"
|
||||
@@ -34,23 +34,38 @@ export namespace Clipboard {
|
||||
if (os === "darwin") {
|
||||
const tmpfile = path.join(tmpdir(), "opencode-clipboard.png")
|
||||
try {
|
||||
await $`osascript -e 'set imageData to the clipboard as "PNGf"' -e 'set fileRef to open for access POSIX file "${tmpfile}" with write permission' -e 'set eof fileRef to 0' -e 'write imageData to fileRef' -e 'close access fileRef'`
|
||||
.nothrow()
|
||||
.quiet()
|
||||
await Process.run(
|
||||
[
|
||||
"osascript",
|
||||
"-e",
|
||||
'set imageData to the clipboard as "PNGf"',
|
||||
"-e",
|
||||
`set fileRef to open for access POSIX file "${tmpfile}" with write permission`,
|
||||
"-e",
|
||||
"set eof fileRef to 0",
|
||||
"-e",
|
||||
"write imageData to fileRef",
|
||||
"-e",
|
||||
"close access fileRef",
|
||||
],
|
||||
{ nothrow: true },
|
||||
)
|
||||
const buffer = await Filesystem.readBytes(tmpfile)
|
||||
return { data: buffer.toString("base64"), mime: "image/png" }
|
||||
} catch {
|
||||
} finally {
|
||||
await $`rm -f "${tmpfile}"`.nothrow().quiet()
|
||||
await fs.rm(tmpfile, { force: true }).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
if (os === "win32" || release().includes("WSL")) {
|
||||
const script =
|
||||
"Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $ms = New-Object System.IO.MemoryStream; $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); [System.Convert]::ToBase64String($ms.ToArray()) }"
|
||||
const base64 = await $`powershell.exe -NonInteractive -NoProfile -command "${script}"`.nothrow().text()
|
||||
if (base64) {
|
||||
const imageBuffer = Buffer.from(base64.trim(), "base64")
|
||||
const base64 = await Process.text(["powershell.exe", "-NonInteractive", "-NoProfile", "-command", script], {
|
||||
nothrow: true,
|
||||
})
|
||||
if (base64.text) {
|
||||
const imageBuffer = Buffer.from(base64.text.trim(), "base64")
|
||||
if (imageBuffer.length > 0) {
|
||||
return { data: imageBuffer.toString("base64"), mime: "image/png" }
|
||||
}
|
||||
@@ -58,13 +73,15 @@ export namespace Clipboard {
|
||||
}
|
||||
|
||||
if (os === "linux") {
|
||||
const wayland = await $`wl-paste -t image/png`.nothrow().arrayBuffer()
|
||||
if (wayland && wayland.byteLength > 0) {
|
||||
return { data: Buffer.from(wayland).toString("base64"), mime: "image/png" }
|
||||
const wayland = await Process.run(["wl-paste", "-t", "image/png"], { nothrow: true })
|
||||
if (wayland.stdout.byteLength > 0) {
|
||||
return { data: Buffer.from(wayland.stdout).toString("base64"), mime: "image/png" }
|
||||
}
|
||||
const x11 = await $`xclip -selection clipboard -t image/png -o`.nothrow().arrayBuffer()
|
||||
if (x11 && x11.byteLength > 0) {
|
||||
return { data: Buffer.from(x11).toString("base64"), mime: "image/png" }
|
||||
const x11 = await Process.run(["xclip", "-selection", "clipboard", "-t", "image/png", "-o"], {
|
||||
nothrow: true,
|
||||
})
|
||||
if (x11.stdout.byteLength > 0) {
|
||||
return { data: Buffer.from(x11.stdout).toString("base64"), mime: "image/png" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +98,7 @@ export namespace Clipboard {
|
||||
console.log("clipboard: using osascript")
|
||||
return async (text: string) => {
|
||||
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
||||
await $`osascript -e 'set the clipboard to "${escaped}"'`.nothrow().quiet()
|
||||
await Process.run(["osascript", "-e", `set the clipboard to "${escaped}"`], { nothrow: true })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ import { UI } from "../ui"
|
||||
import * as prompts from "@clack/prompts"
|
||||
import { Installation } from "../../installation"
|
||||
import { Global } from "../../global"
|
||||
import { $ } from "bun"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { Filesystem } from "../../util/filesystem"
|
||||
import { Process } from "../../util/process"
|
||||
|
||||
interface UninstallArgs {
|
||||
keepConfig: boolean
|
||||
@@ -192,16 +192,13 @@ async function executeUninstall(method: Installation.Method, targets: RemovalTar
|
||||
const cmd = cmds[method]
|
||||
if (cmd) {
|
||||
spinner.start(`Running ${cmd.join(" ")}...`)
|
||||
const result =
|
||||
method === "choco"
|
||||
? await $`echo Y | choco uninstall opencode -y -r`.quiet().nothrow()
|
||||
: await $`${cmd}`.quiet().nothrow()
|
||||
if (result.exitCode !== 0) {
|
||||
spinner.stop(`Package manager uninstall failed: exit code ${result.exitCode}`, 1)
|
||||
if (
|
||||
method === "choco" &&
|
||||
result.stdout.toString("utf8").includes("not running from an elevated command shell")
|
||||
) {
|
||||
const result = await Process.run(method === "choco" ? ["choco", "uninstall", "opencode", "-y", "-r"] : cmd, {
|
||||
nothrow: true,
|
||||
})
|
||||
if (result.code !== 0) {
|
||||
spinner.stop(`Package manager uninstall failed: exit code ${result.code}`, 1)
|
||||
const text = `${result.stdout.toString("utf8")}\n${result.stderr.toString("utf8")}`
|
||||
if (method === "choco" && text.includes("not running from an elevated command shell")) {
|
||||
prompts.log.warn(`You may need to run '${cmd.join(" ")}' from an elevated command shell`)
|
||||
} else {
|
||||
prompts.log.warn(`You may need to run manually: ${cmd.join(" ")}`)
|
||||
|
||||
Reference in New Issue
Block a user