core: dynamically resolve formatter executable paths at runtime
Formatters now determine their executable location when enabled rather than using hardcoded paths. This ensures formatters work correctly regardless of how the tool was installed or where executables are located on the system.
This commit is contained in:
parent
0e176d3ac3
commit
528daf5490
6 changed files with 137 additions and 117 deletions
2
.opencode/.gitignore
vendored
2
.opencode/.gitignore
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
node_modules
|
node_modules
|
||||||
|
plans
|
||||||
package.json
|
package.json
|
||||||
bun.lock
|
bun.lock
|
||||||
.gitignore
|
.gitignore
|
||||||
package-lock.json
|
package-lock.json
|
||||||
plans
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import semver from "semver"
|
|
||||||
import { text } from "node:stream/consumers"
|
import { text } from "node:stream/consumers"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { Process } from "../util/process"
|
import { Process } from "../util/process"
|
||||||
|
|
@ -34,17 +33,4 @@ export namespace PackageRegistry {
|
||||||
if (!value) return null
|
if (!value) return null
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function isOutdated(pkg: string, cachedVersion: string, cwd?: string): Promise<boolean> {
|
|
||||||
const latestVersion = await info(pkg, "version", cwd)
|
|
||||||
if (!latestVersion) {
|
|
||||||
log.warn("Failed to resolve latest version, using cached", { pkg, cachedVersion })
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const isRange = /[\s^~*xX<>|=]/.test(cachedVersion)
|
|
||||||
if (isRange) return !semver.satisfies(latestVersion, cachedVersion)
|
|
||||||
|
|
||||||
return semver.lt(cachedVersion, latestVersion)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ import { Bus } from "@/bus"
|
||||||
import { GlobalBus } from "@/bus/global"
|
import { GlobalBus } from "@/bus/global"
|
||||||
import { Event } from "../server/event"
|
import { Event } from "../server/event"
|
||||||
import { Glob } from "../util/glob"
|
import { Glob } from "../util/glob"
|
||||||
import { PackageRegistry } from "@/bun/registry"
|
|
||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
import { Account } from "@/account"
|
import { Account } from "@/account"
|
||||||
import { ConfigPaths } from "./paths"
|
import { ConfigPaths } from "./paths"
|
||||||
|
|
@ -325,7 +324,7 @@ export namespace Config {
|
||||||
|
|
||||||
const targetVersion = Installation.isLocal() ? "latest" : Installation.VERSION
|
const targetVersion = Installation.isLocal() ? "latest" : Installation.VERSION
|
||||||
if (targetVersion === "latest") {
|
if (targetVersion === "latest") {
|
||||||
const isOutdated = await PackageRegistry.isOutdated("@opencode-ai/plugin", depVersion, dir)
|
const isOutdated = await Npm.outdated("@opencode-ai/plugin", depVersion)
|
||||||
if (!isOutdated) return false
|
if (!isOutdated) return false
|
||||||
log.info("Cached version is outdated, proceeding with install", {
|
log.info("Cached version is outdated, proceeding with install", {
|
||||||
pkg: "@opencode-ai/plugin",
|
pkg: "@opencode-ai/plugin",
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,40 @@
|
||||||
import { text } from "node:stream/consumers"
|
import { text } from "node:stream/consumers"
|
||||||
import { BunProc } from "../bun"
|
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { Filesystem } from "../util/filesystem"
|
import { Filesystem } from "../util/filesystem"
|
||||||
import { Process } from "../util/process"
|
import { Process } from "../util/process"
|
||||||
import { which } from "../util/which"
|
import { which } from "../util/which"
|
||||||
import { Flag } from "@/flag/flag"
|
import { Flag } from "@/flag/flag"
|
||||||
|
import { Npm } from "@/npm"
|
||||||
|
|
||||||
export interface Info {
|
export interface Info {
|
||||||
name: string
|
name: string
|
||||||
command: string[]
|
|
||||||
environment?: Record<string, string>
|
environment?: Record<string, string>
|
||||||
extensions: string[]
|
extensions: string[]
|
||||||
enabled(): Promise<boolean>
|
enabled(): Promise<string[] | false>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const gofmt: Info = {
|
export const gofmt: Info = {
|
||||||
name: "gofmt",
|
name: "gofmt",
|
||||||
command: ["gofmt", "-w", "$FILE"],
|
|
||||||
extensions: [".go"],
|
extensions: [".go"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
return which("gofmt") !== null
|
const p = which("gofmt")
|
||||||
|
if (p === null) return false
|
||||||
|
return [p, "-w", "$FILE"]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mix: Info = {
|
export const mix: Info = {
|
||||||
name: "mix",
|
name: "mix",
|
||||||
command: ["mix", "format", "$FILE"],
|
|
||||||
extensions: [".ex", ".exs", ".eex", ".heex", ".leex", ".neex", ".sface"],
|
extensions: [".ex", ".exs", ".eex", ".heex", ".leex", ".neex", ".sface"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
return which("mix") !== null
|
const p = which("mix")
|
||||||
|
if (p === null) return false
|
||||||
|
return [p, "format", "$FILE"]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const prettier: Info = {
|
export const prettier: Info = {
|
||||||
name: "prettier",
|
name: "prettier",
|
||||||
command: [BunProc.which(), "x", "prettier", "--write", "$FILE"],
|
|
||||||
environment: {
|
environment: {
|
||||||
BUN_BE_BUN: "1",
|
BUN_BE_BUN: "1",
|
||||||
},
|
},
|
||||||
|
|
@ -73,8 +73,9 @@ export const prettier: Info = {
|
||||||
dependencies?: Record<string, string>
|
dependencies?: Record<string, string>
|
||||||
devDependencies?: Record<string, string>
|
devDependencies?: Record<string, string>
|
||||||
}>(item)
|
}>(item)
|
||||||
if (json.dependencies?.prettier) return true
|
if (json.dependencies?.prettier || json.devDependencies?.prettier) {
|
||||||
if (json.devDependencies?.prettier) return true
|
return [await Npm.which("prettier"), "--write", "$FILE"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
|
|
@ -82,7 +83,6 @@ export const prettier: Info = {
|
||||||
|
|
||||||
export const oxfmt: Info = {
|
export const oxfmt: Info = {
|
||||||
name: "oxfmt",
|
name: "oxfmt",
|
||||||
command: [BunProc.which(), "x", "oxfmt", "$FILE"],
|
|
||||||
environment: {
|
environment: {
|
||||||
BUN_BE_BUN: "1",
|
BUN_BE_BUN: "1",
|
||||||
},
|
},
|
||||||
|
|
@ -95,8 +95,9 @@ export const oxfmt: Info = {
|
||||||
dependencies?: Record<string, string>
|
dependencies?: Record<string, string>
|
||||||
devDependencies?: Record<string, string>
|
devDependencies?: Record<string, string>
|
||||||
}>(item)
|
}>(item)
|
||||||
if (json.dependencies?.oxfmt) return true
|
if (json.dependencies?.oxfmt || json.devDependencies?.oxfmt) {
|
||||||
if (json.devDependencies?.oxfmt) return true
|
return [await Npm.which("oxfmt"), "$FILE"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
|
|
@ -104,7 +105,6 @@ export const oxfmt: Info = {
|
||||||
|
|
||||||
export const biome: Info = {
|
export const biome: Info = {
|
||||||
name: "biome",
|
name: "biome",
|
||||||
command: [BunProc.which(), "x", "@biomejs/biome", "check", "--write", "$FILE"],
|
|
||||||
environment: {
|
environment: {
|
||||||
BUN_BE_BUN: "1",
|
BUN_BE_BUN: "1",
|
||||||
},
|
},
|
||||||
|
|
@ -141,7 +141,7 @@ export const biome: Info = {
|
||||||
for (const config of configs) {
|
for (const config of configs) {
|
||||||
const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree)
|
const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree)
|
||||||
if (found.length > 0) {
|
if (found.length > 0) {
|
||||||
return true
|
return [await Npm.which("@biomejs/biome"), "check", "--write", "$FILE"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
@ -150,47 +150,49 @@ export const biome: Info = {
|
||||||
|
|
||||||
export const zig: Info = {
|
export const zig: Info = {
|
||||||
name: "zig",
|
name: "zig",
|
||||||
command: ["zig", "fmt", "$FILE"],
|
|
||||||
extensions: [".zig", ".zon"],
|
extensions: [".zig", ".zon"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
return which("zig") !== null
|
const p = which("zig")
|
||||||
|
if (p === null) return false
|
||||||
|
return [p, "fmt", "$FILE"]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const clang: Info = {
|
export const clang: Info = {
|
||||||
name: "clang-format",
|
name: "clang-format",
|
||||||
command: ["clang-format", "-i", "$FILE"],
|
|
||||||
extensions: [".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hh", ".hpp", ".hxx", ".h++", ".ino", ".C", ".H"],
|
extensions: [".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hh", ".hpp", ".hxx", ".h++", ".ino", ".C", ".H"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
const items = await Filesystem.findUp(".clang-format", Instance.directory, Instance.worktree)
|
const items = await Filesystem.findUp(".clang-format", Instance.directory, Instance.worktree)
|
||||||
return items.length > 0
|
if (items.length === 0) return false
|
||||||
|
return ["clang-format", "-i", "$FILE"]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ktlint: Info = {
|
export const ktlint: Info = {
|
||||||
name: "ktlint",
|
name: "ktlint",
|
||||||
command: ["ktlint", "-F", "$FILE"],
|
|
||||||
extensions: [".kt", ".kts"],
|
extensions: [".kt", ".kts"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
return which("ktlint") !== null
|
const p = which("ktlint")
|
||||||
|
if (p === null) return false
|
||||||
|
return [p, "-F", "$FILE"]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ruff: Info = {
|
export const ruff: Info = {
|
||||||
name: "ruff",
|
name: "ruff",
|
||||||
command: ["ruff", "format", "$FILE"],
|
|
||||||
extensions: [".py", ".pyi"],
|
extensions: [".py", ".pyi"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
if (!which("ruff")) return false
|
const p = which("ruff")
|
||||||
|
if (p === null) return false
|
||||||
const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"]
|
const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"]
|
||||||
for (const config of configs) {
|
for (const config of configs) {
|
||||||
const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree)
|
const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree)
|
||||||
if (found.length > 0) {
|
if (found.length > 0) {
|
||||||
if (config === "pyproject.toml") {
|
if (config === "pyproject.toml") {
|
||||||
const content = await Filesystem.readText(found[0])
|
const content = await Filesystem.readText(found[0])
|
||||||
if (content.includes("[tool.ruff]")) return true
|
if (content.includes("[tool.ruff]")) return [p, "format", "$FILE"]
|
||||||
} else {
|
} else {
|
||||||
return true
|
return [p, "format", "$FILE"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -199,7 +201,7 @@ export const ruff: Info = {
|
||||||
const found = await Filesystem.findUp(dep, Instance.directory, Instance.worktree)
|
const found = await Filesystem.findUp(dep, Instance.directory, Instance.worktree)
|
||||||
if (found.length > 0) {
|
if (found.length > 0) {
|
||||||
const content = await Filesystem.readText(found[0])
|
const content = await Filesystem.readText(found[0])
|
||||||
if (content.includes("ruff")) return true
|
if (content.includes("ruff")) return [p, "format", "$FILE"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
@ -208,14 +210,13 @@ export const ruff: Info = {
|
||||||
|
|
||||||
export const rlang: Info = {
|
export const rlang: Info = {
|
||||||
name: "air",
|
name: "air",
|
||||||
command: ["air", "format", "$FILE"],
|
|
||||||
extensions: [".R"],
|
extensions: [".R"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
const airPath = which("air")
|
const airPath = which("air")
|
||||||
if (airPath == null) return false
|
if (airPath == null) return false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const proc = Process.spawn(["air", "--help"], {
|
const proc = Process.spawn([airPath, "--help"], {
|
||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
stderr: "pipe",
|
stderr: "pipe",
|
||||||
})
|
})
|
||||||
|
|
@ -227,7 +228,10 @@ export const rlang: Info = {
|
||||||
const firstLine = output.split("\n")[0]
|
const firstLine = output.split("\n")[0]
|
||||||
const hasR = firstLine.includes("R language")
|
const hasR = firstLine.includes("R language")
|
||||||
const hasFormatter = firstLine.includes("formatter")
|
const hasFormatter = firstLine.includes("formatter")
|
||||||
return hasR && hasFormatter
|
if (hasR && hasFormatter) {
|
||||||
|
return [airPath, "format", "$FILE"]
|
||||||
|
}
|
||||||
|
return false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -236,14 +240,14 @@ export const rlang: Info = {
|
||||||
|
|
||||||
export const uvformat: Info = {
|
export const uvformat: Info = {
|
||||||
name: "uv",
|
name: "uv",
|
||||||
command: ["uv", "format", "--", "$FILE"],
|
|
||||||
extensions: [".py", ".pyi"],
|
extensions: [".py", ".pyi"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
if (await ruff.enabled()) return false
|
if (await ruff.enabled()) return false
|
||||||
if (which("uv") !== null) {
|
const uvPath = which("uv")
|
||||||
const proc = Process.spawn(["uv", "format", "--help"], { stderr: "pipe", stdout: "pipe" })
|
if (uvPath !== null) {
|
||||||
|
const proc = Process.spawn([uvPath, "format", "--help"], { stderr: "pipe", stdout: "pipe" })
|
||||||
const code = await proc.exited
|
const code = await proc.exited
|
||||||
return code === 0
|
if (code === 0) return [uvPath, "format", "--", "$FILE"]
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
|
|
@ -251,108 +255,118 @@ export const uvformat: Info = {
|
||||||
|
|
||||||
export const rubocop: Info = {
|
export const rubocop: Info = {
|
||||||
name: "rubocop",
|
name: "rubocop",
|
||||||
command: ["rubocop", "--autocorrect", "$FILE"],
|
|
||||||
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
return which("rubocop") !== null
|
const path = which("rubocop")
|
||||||
|
if (path === null) return false
|
||||||
|
return [path, "--autocorrect", "$FILE"]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const standardrb: Info = {
|
export const standardrb: Info = {
|
||||||
name: "standardrb",
|
name: "standardrb",
|
||||||
command: ["standardrb", "--fix", "$FILE"],
|
|
||||||
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
return which("standardrb") !== null
|
const path = which("standardrb")
|
||||||
|
if (path === null) return false
|
||||||
|
return [path, "--fix", "$FILE"]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const htmlbeautifier: Info = {
|
export const htmlbeautifier: Info = {
|
||||||
name: "htmlbeautifier",
|
name: "htmlbeautifier",
|
||||||
command: ["htmlbeautifier", "$FILE"],
|
|
||||||
extensions: [".erb", ".html.erb"],
|
extensions: [".erb", ".html.erb"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
return which("htmlbeautifier") !== null
|
const path = which("htmlbeautifier")
|
||||||
|
if (path === null) return false
|
||||||
|
return [path, "$FILE"]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dart: Info = {
|
export const dart: Info = {
|
||||||
name: "dart",
|
name: "dart",
|
||||||
command: ["dart", "format", "$FILE"],
|
|
||||||
extensions: [".dart"],
|
extensions: [".dart"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
return which("dart") !== null
|
const path = which("dart")
|
||||||
|
if (path === null) return false
|
||||||
|
return [path, "format", "$FILE"]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ocamlformat: Info = {
|
export const ocamlformat: Info = {
|
||||||
name: "ocamlformat",
|
name: "ocamlformat",
|
||||||
command: ["ocamlformat", "-i", "$FILE"],
|
|
||||||
extensions: [".ml", ".mli"],
|
extensions: [".ml", ".mli"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
if (!which("ocamlformat")) return false
|
const path = which("ocamlformat")
|
||||||
|
if (!path) return false
|
||||||
const items = await Filesystem.findUp(".ocamlformat", Instance.directory, Instance.worktree)
|
const items = await Filesystem.findUp(".ocamlformat", Instance.directory, Instance.worktree)
|
||||||
return items.length > 0
|
if (items.length === 0) return false
|
||||||
|
return [path, "-i", "$FILE"]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const terraform: Info = {
|
export const terraform: Info = {
|
||||||
name: "terraform",
|
name: "terraform",
|
||||||
command: ["terraform", "fmt", "$FILE"],
|
|
||||||
extensions: [".tf", ".tfvars"],
|
extensions: [".tf", ".tfvars"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
return which("terraform") !== null
|
const path = which("terraform")
|
||||||
|
if (path === null) return false
|
||||||
|
return [path, "fmt", "$FILE"]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const latexindent: Info = {
|
export const latexindent: Info = {
|
||||||
name: "latexindent",
|
name: "latexindent",
|
||||||
command: ["latexindent", "-w", "-s", "$FILE"],
|
|
||||||
extensions: [".tex"],
|
extensions: [".tex"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
return which("latexindent") !== null
|
const path = which("latexindent")
|
||||||
|
if (path === null) return false
|
||||||
|
return [path, "-w", "-s", "$FILE"]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const gleam: Info = {
|
export const gleam: Info = {
|
||||||
name: "gleam",
|
name: "gleam",
|
||||||
command: ["gleam", "format", "$FILE"],
|
|
||||||
extensions: [".gleam"],
|
extensions: [".gleam"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
return which("gleam") !== null
|
const path = which("gleam")
|
||||||
|
if (path === null) return false
|
||||||
|
return [path, "format", "$FILE"]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const shfmt: Info = {
|
export const shfmt: Info = {
|
||||||
name: "shfmt",
|
name: "shfmt",
|
||||||
command: ["shfmt", "-w", "$FILE"],
|
|
||||||
extensions: [".sh", ".bash"],
|
extensions: [".sh", ".bash"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
return which("shfmt") !== null
|
const path = which("shfmt")
|
||||||
|
if (path === null) return false
|
||||||
|
return [path, "-w", "$FILE"]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const nixfmt: Info = {
|
export const nixfmt: Info = {
|
||||||
name: "nixfmt",
|
name: "nixfmt",
|
||||||
command: ["nixfmt", "$FILE"],
|
|
||||||
extensions: [".nix"],
|
extensions: [".nix"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
return which("nixfmt") !== null
|
const path = which("nixfmt")
|
||||||
|
if (path === null) return false
|
||||||
|
return [path, "$FILE"]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rustfmt: Info = {
|
export const rustfmt: Info = {
|
||||||
name: "rustfmt",
|
name: "rustfmt",
|
||||||
command: ["rustfmt", "$FILE"],
|
|
||||||
extensions: [".rs"],
|
extensions: [".rs"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
return which("rustfmt") !== null
|
const path = which("rustfmt")
|
||||||
|
if (path === null) return false
|
||||||
|
return [path, "$FILE"]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const pint: Info = {
|
export const pint: Info = {
|
||||||
name: "pint",
|
name: "pint",
|
||||||
command: ["./vendor/bin/pint", "$FILE"],
|
|
||||||
extensions: [".php"],
|
extensions: [".php"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
const items = await Filesystem.findUp("composer.json", Instance.directory, Instance.worktree)
|
const items = await Filesystem.findUp("composer.json", Instance.directory, Instance.worktree)
|
||||||
|
|
@ -361,8 +375,9 @@ export const pint: Info = {
|
||||||
require?: Record<string, string>
|
require?: Record<string, string>
|
||||||
"require-dev"?: Record<string, string>
|
"require-dev"?: Record<string, string>
|
||||||
}>(item)
|
}>(item)
|
||||||
if (json.require?.["laravel/pint"]) return true
|
if (json.require?.["laravel/pint"] || json["require-dev"]?.["laravel/pint"]) {
|
||||||
if (json["require-dev"]?.["laravel/pint"]) return true
|
return ["./vendor/bin/pint", "$FILE"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
|
|
@ -370,27 +385,30 @@ export const pint: Info = {
|
||||||
|
|
||||||
export const ormolu: Info = {
|
export const ormolu: Info = {
|
||||||
name: "ormolu",
|
name: "ormolu",
|
||||||
command: ["ormolu", "-i", "$FILE"],
|
|
||||||
extensions: [".hs"],
|
extensions: [".hs"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
return which("ormolu") !== null
|
const path = which("ormolu")
|
||||||
|
if (path === null) return false
|
||||||
|
return [path, "-i", "$FILE"]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const cljfmt: Info = {
|
export const cljfmt: Info = {
|
||||||
name: "cljfmt",
|
name: "cljfmt",
|
||||||
command: ["cljfmt", "fix", "--quiet", "$FILE"],
|
|
||||||
extensions: [".clj", ".cljs", ".cljc", ".edn"],
|
extensions: [".clj", ".cljs", ".cljc", ".edn"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
return which("cljfmt") !== null
|
const path = which("cljfmt")
|
||||||
|
if (path === null) return false
|
||||||
|
return [path, "fix", "--quiet", "$FILE"]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dfmt: Info = {
|
export const dfmt: Info = {
|
||||||
name: "dfmt",
|
name: "dfmt",
|
||||||
command: ["dfmt", "-i", "$FILE"],
|
|
||||||
extensions: [".d"],
|
extensions: [".d"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
return which("dfmt") !== null
|
const path = which("dfmt")
|
||||||
|
if (path === null) return false
|
||||||
|
return [path, "-i", "$FILE"]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,14 @@ export namespace Format {
|
||||||
export type Status = z.infer<typeof Status>
|
export type Status = z.infer<typeof Status>
|
||||||
|
|
||||||
const state = Instance.state(async () => {
|
const state = Instance.state(async () => {
|
||||||
const enabled: Record<string, boolean> = {}
|
const cache: Record<string, string[] | false> = {}
|
||||||
const cfg = await Config.get()
|
const cfg = await Config.get()
|
||||||
|
|
||||||
const formatters: Record<string, Formatter.Info> = {}
|
const formatters: Record<string, Formatter.Info> = {}
|
||||||
if (cfg.formatter === false) {
|
if (cfg.formatter === false) {
|
||||||
log.info("all formatters are disabled")
|
log.info("all formatters are disabled")
|
||||||
return {
|
return {
|
||||||
enabled,
|
cache,
|
||||||
formatters,
|
formatters,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -46,43 +46,41 @@ export namespace Format {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const result: Formatter.Info = mergeDeep(formatters[name] ?? {}, {
|
const result: Formatter.Info = mergeDeep(formatters[name] ?? {}, {
|
||||||
command: [],
|
|
||||||
extensions: [],
|
extensions: [],
|
||||||
...item,
|
...item,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result.command.length === 0) continue
|
result.enabled = async () => item.command ?? false
|
||||||
|
|
||||||
result.enabled = async () => true
|
|
||||||
result.name = name
|
result.name = name
|
||||||
formatters[name] = result
|
formatters[name] = result
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled,
|
cache,
|
||||||
formatters,
|
formatters,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
async function isEnabled(item: Formatter.Info) {
|
async function resolveCommand(item: Formatter.Info) {
|
||||||
const s = await state()
|
const s = await state()
|
||||||
let status = s.enabled[item.name]
|
let command = s.cache[item.name]
|
||||||
if (status === undefined) {
|
if (command === undefined) {
|
||||||
status = await item.enabled()
|
log.info("resolving command", { name: item.name })
|
||||||
s.enabled[item.name] = status
|
command = await item.enabled()
|
||||||
|
s.cache[item.name] = command
|
||||||
}
|
}
|
||||||
return status
|
return command
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFormatter(ext: string) {
|
async function getFormatter(ext: string) {
|
||||||
const formatters = await state().then((x) => x.formatters)
|
const formatters = await state().then((x) => x.formatters)
|
||||||
const result = []
|
const result: { info: Formatter.Info; command: string[] }[] = []
|
||||||
for (const item of Object.values(formatters)) {
|
for (const item of Object.values(formatters)) {
|
||||||
log.info("checking", { name: item.name, ext })
|
|
||||||
if (!item.extensions.includes(ext)) continue
|
if (!item.extensions.includes(ext)) continue
|
||||||
if (!(await isEnabled(item))) continue
|
const command = await resolveCommand(item)
|
||||||
|
if (!command) continue
|
||||||
log.info("enabled", { name: item.name, ext })
|
log.info("enabled", { name: item.name, ext })
|
||||||
result.push(item)
|
result.push({ info: item, command })
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
@ -91,11 +89,11 @@ export namespace Format {
|
||||||
const s = await state()
|
const s = await state()
|
||||||
const result: Status[] = []
|
const result: Status[] = []
|
||||||
for (const formatter of Object.values(s.formatters)) {
|
for (const formatter of Object.values(s.formatters)) {
|
||||||
const enabled = await isEnabled(formatter)
|
const command = await resolveCommand(formatter)
|
||||||
result.push({
|
result.push({
|
||||||
name: formatter.name,
|
name: formatter.name,
|
||||||
extensions: formatter.extensions,
|
extensions: formatter.extensions,
|
||||||
enabled,
|
enabled: !!command,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|
@ -108,29 +106,27 @@ export namespace Format {
|
||||||
log.info("formatting", { file })
|
log.info("formatting", { file })
|
||||||
const ext = path.extname(file)
|
const ext = path.extname(file)
|
||||||
|
|
||||||
for (const item of await getFormatter(ext)) {
|
for (const { info, command } of await getFormatter(ext)) {
|
||||||
log.info("running", { command: item.command })
|
const replaced = command.map((x) => x.replace("$FILE", file))
|
||||||
|
log.info("running", { replaced })
|
||||||
try {
|
try {
|
||||||
const proc = Process.spawn(
|
const proc = Process.spawn(replaced, {
|
||||||
item.command.map((x) => x.replace("$FILE", file)),
|
cwd: Instance.directory,
|
||||||
{
|
env: { ...process.env, ...info.environment },
|
||||||
cwd: Instance.directory,
|
stdout: "ignore",
|
||||||
env: { ...process.env, ...item.environment },
|
stderr: "ignore",
|
||||||
stdout: "ignore",
|
})
|
||||||
stderr: "ignore",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
const exit = await proc.exited
|
const exit = await proc.exited
|
||||||
if (exit !== 0)
|
if (exit !== 0)
|
||||||
log.error("failed", {
|
log.error("failed", {
|
||||||
command: item.command,
|
command,
|
||||||
...item.environment,
|
...info.environment,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("failed to format file", {
|
log.error("failed to format file", {
|
||||||
error,
|
error,
|
||||||
command: item.command,
|
command,
|
||||||
...item.environment,
|
...info.environment,
|
||||||
file,
|
file,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import semver from "semver"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { NamedError } from "@opencode-ai/util/error"
|
import { NamedError } from "@opencode-ai/util/error"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
|
|
@ -21,6 +22,26 @@ export namespace Npm {
|
||||||
return path.join(Global.Path.cache, "packages", pkg)
|
return path.join(Global.Path.cache, "packages", pkg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function outdated(pkg: string, cachedVersion: string): Promise<boolean> {
|
||||||
|
const response = await fetch(`https://registry.npmjs.org/${pkg}`)
|
||||||
|
if (!response.ok) {
|
||||||
|
log.warn("Failed to resolve latest version, using cached", { pkg, cachedVersion })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as { "dist-tags"?: { latest?: string } }
|
||||||
|
const latestVersion = data?.["dist-tags"]?.latest
|
||||||
|
if (!latestVersion) {
|
||||||
|
log.warn("No latest version found, using cached", { pkg, cachedVersion })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRange = /[\s^~*xX<>|=]/.test(cachedVersion)
|
||||||
|
if (isRange) return !semver.satisfies(latestVersion, cachedVersion)
|
||||||
|
|
||||||
|
return semver.lt(cachedVersion, latestVersion)
|
||||||
|
}
|
||||||
|
|
||||||
export async function add(pkg: string) {
|
export async function add(pkg: string) {
|
||||||
using _ = await Lock.write("npm-install")
|
using _ = await Lock.write("npm-install")
|
||||||
log.info("installing package using npm arborist", {
|
log.info("installing package using npm arborist", {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue