mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-05-03 07:11:31 +08:00
Compare commits
1 Commits
fix/usage-
...
fix-config
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcb37c0d91 |
1
bun.lock
1
bun.lock
@@ -451,7 +451,6 @@
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"drizzle-kit": "catalog:",
|
||||
"drizzle-orm": "catalog:",
|
||||
"prettier": "3.6.2",
|
||||
"typescript": "catalog:",
|
||||
"vscode-languageserver-types": "3.17.5",
|
||||
"why-is-node-running": "3.2.2",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-V1Rt2k7ujkqGw4pDkn++WALTy1fAugvoKLhKvwFKkss=",
|
||||
"aarch64-linux": "sha256-ho0AuGbJ1qw9Hvb3EbGC8f0lWqqgUslvda/wTe32MFo=",
|
||||
"aarch64-darwin": "sha256-hdUyNmp+snwtnBckHXsPMgNFUYS1sYDdngkk+AXVqzc=",
|
||||
"x86_64-darwin": "sha256-P57LpQNF8fplFKQBBIukhOKbIugbViyBUIUjClXohuk="
|
||||
"x86_64-linux": "sha256-+G3/s18NZO1Dpc5TsZyix2Npodzei25Svw3nTjfzXW8=",
|
||||
"aarch64-linux": "sha256-39HPencmRYRbyCk/cZIdPFk6ocY1AMlyuN9j25zAKzI=",
|
||||
"aarch64-darwin": "sha256-043korPEjSHKiZ3P+EfWyOfKpgOC7CBpviccviaDa0o=",
|
||||
"x86_64-darwin": "sha256-vsZ7e//rL9e7Cl5kl/Xplvi1fqayljxTLwRSbxvCxeM="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,16 +27,12 @@ Chart.register(BarController, BarElement, CategoryScale, LinearScale, Tooltip, L
|
||||
async function getCosts(workspaceID: string, year: number, month: number) {
|
||||
"use server"
|
||||
return withActor(async () => {
|
||||
// Fetch a UTC buffer so client-side local dates at month boundaries are not dropped.
|
||||
const startDate = new Date(Date.UTC(year, month, 1))
|
||||
startDate.setUTCDate(startDate.getUTCDate() - 1)
|
||||
const endDate = new Date(Date.UTC(year, month + 1, 1))
|
||||
endDate.setUTCDate(endDate.getUTCDate() + 1)
|
||||
const hourBucket = sql<number>`FLOOR(UNIX_TIMESTAMP(${UsageTable.timeCreated}) / 3600)`
|
||||
const startDate = new Date(year, month, 1)
|
||||
const endDate = new Date(year, month + 1, 1)
|
||||
const usageData = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
hourBucket: hourBucket.as("hour_bucket"),
|
||||
date: sql<string>`DATE(${UsageTable.timeCreated})`,
|
||||
model: UsageTable.model,
|
||||
totalCost: sum(UsageTable.cost),
|
||||
keyId: UsageTable.keyID,
|
||||
@@ -51,7 +47,7 @@ async function getCosts(workspaceID: string, year: number, month: number) {
|
||||
),
|
||||
)
|
||||
.groupBy(
|
||||
hourBucket,
|
||||
sql`DATE(${UsageTable.timeCreated})`,
|
||||
UsageTable.model,
|
||||
UsageTable.keyID,
|
||||
sql`JSON_EXTRACT(${UsageTable.enrichment}, '$.plan')`,
|
||||
@@ -59,7 +55,6 @@ async function getCosts(workspaceID: string, year: number, month: number) {
|
||||
.then((x) =>
|
||||
x.map((r) => ({
|
||||
...r,
|
||||
hourBucket: Number(r.hourBucket),
|
||||
totalCost: r.totalCost ? parseInt(r.totalCost) : 0,
|
||||
plan: r.plan as "sub" | "lite" | "byok" | null,
|
||||
})),
|
||||
@@ -130,13 +125,15 @@ function getModelColor(model: string): string {
|
||||
}
|
||||
|
||||
function formatDateLabel(dateStr: string): string {
|
||||
const [year, month, day] = dateStr.split("-").map(Number)
|
||||
const date = new Date(year, month - 1, day)
|
||||
return `${date.toLocaleDateString(undefined, { month: "short" })} ${day.toString().padStart(2, "0")}`
|
||||
}
|
||||
|
||||
function formatDateKey(date: Date) {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`
|
||||
const date = new Date()
|
||||
const [y, m, d] = dateStr.split("-").map(Number)
|
||||
date.setFullYear(y)
|
||||
date.setMonth(m - 1)
|
||||
date.setDate(d)
|
||||
date.setHours(0, 0, 0, 0)
|
||||
const month = date.toLocaleDateString(undefined, { month: "short" })
|
||||
const day = date.getUTCDate().toString().padStart(2, "0")
|
||||
return `${month} ${day}`
|
||||
}
|
||||
|
||||
function addOpacityToColor(color: string, opacity: number): string {
|
||||
@@ -182,24 +179,17 @@ export function GraphSection() {
|
||||
|
||||
const onSelectKey = (keyID: string | null) => setStore({ key: keyID, keyDropdownOpen: false })
|
||||
|
||||
const getModels = createMemo(() => {
|
||||
if (!store.data?.usage) return []
|
||||
return Array.from(new Set(store.data.usage.map((row) => row.model))).sort()
|
||||
})
|
||||
|
||||
const getDates = createMemo(() => {
|
||||
const daysInMonth = new Date(store.year, store.month + 1, 0).getDate()
|
||||
const dates = Array.from({ length: daysInMonth }, (_, i) => formatDateKey(new Date(store.year, store.month, i + 1)))
|
||||
const dateSet = new Set(dates)
|
||||
return {
|
||||
dates,
|
||||
dateSet,
|
||||
}
|
||||
})
|
||||
|
||||
const getUsageForMonth = createMemo(() => {
|
||||
if (!store.data?.usage) return []
|
||||
const dateSet = getDates().dateSet
|
||||
return store.data.usage.filter((row) => dateSet.has(formatDateKey(new Date(row.hourBucket * 3600 * 1000))))
|
||||
})
|
||||
|
||||
const getModels = createMemo(() => {
|
||||
return Array.from(new Set(getUsageForMonth().map((row) => row.model))).sort()
|
||||
return Array.from({ length: daysInMonth }, (_, i) => {
|
||||
const date = new Date(store.year, store.month, i + 1)
|
||||
return date.toISOString().split("T")[0]
|
||||
})
|
||||
})
|
||||
|
||||
const getKeyName = (keyID: string | null): string => {
|
||||
@@ -215,9 +205,9 @@ export function GraphSection() {
|
||||
const isCurrentMonth = () => store.year === now.getFullYear() && store.month === now.getMonth()
|
||||
|
||||
const chartConfig = createMemo((): ChartConfiguration | null => {
|
||||
const dates = getDates().dates
|
||||
const usage = getUsageForMonth()
|
||||
if (usage.length === 0) return null
|
||||
const data = store.data
|
||||
const dates = getDates()
|
||||
if (!data?.usage?.length) return null
|
||||
|
||||
store.colorScheme
|
||||
const styles = getComputedStyle(document.documentElement)
|
||||
@@ -239,12 +229,11 @@ export function GraphSection() {
|
||||
dailyDataLite.set(dateKey, new Map())
|
||||
}
|
||||
|
||||
usage
|
||||
data.usage
|
||||
.filter((row) => (store.key ? row.keyId === store.key : true))
|
||||
.forEach((row) => {
|
||||
const dateKey = formatDateKey(new Date(row.hourBucket * 3600 * 1000))
|
||||
const targetMap = row.plan === "sub" ? dailyDataSub : row.plan === "lite" ? dailyDataLite : dailyDataRegular
|
||||
const dayMap = targetMap.get(dateKey)
|
||||
const dayMap = targetMap.get(row.date)
|
||||
if (!dayMap) return
|
||||
dayMap.set(row.model, (dayMap.get(row.model) ?? 0) + row.totalCost)
|
||||
})
|
||||
@@ -297,8 +286,6 @@ export function GraphSection() {
|
||||
}),
|
||||
]
|
||||
|
||||
if (datasets.length === 0) return null
|
||||
|
||||
return {
|
||||
type: "bar",
|
||||
data: {
|
||||
|
||||
@@ -69,7 +69,6 @@
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"drizzle-kit": "catalog:",
|
||||
"drizzle-orm": "catalog:",
|
||||
"prettier": "3.6.2",
|
||||
"typescript": "catalog:",
|
||||
"vscode-languageserver-types": "3.17.5",
|
||||
"why-is-node-running": "3.2.2",
|
||||
|
||||
@@ -14,7 +14,6 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
|
||||
".cc": "cpp",
|
||||
".c++": "cpp",
|
||||
".cs": "csharp",
|
||||
".csx": "csharp",
|
||||
".css": "css",
|
||||
".d": "d",
|
||||
".pas": "pascal",
|
||||
|
||||
@@ -703,10 +703,31 @@ export const Zls: Info = {
|
||||
export const CSharp: Info = {
|
||||
id: "csharp",
|
||||
root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]),
|
||||
extensions: [".cs", ".csx"],
|
||||
extensions: [".cs"],
|
||||
async spawn(root) {
|
||||
const bin = await getRoslynLanguageServer()
|
||||
if (!bin) return
|
||||
let bin = which("roslyn-language-server")
|
||||
if (!bin) {
|
||||
if (!which("dotnet")) {
|
||||
log.error(".NET SDK is required to install roslyn-language-server")
|
||||
return
|
||||
}
|
||||
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
log.info("installing roslyn-language-server via dotnet tool")
|
||||
const proc = Process.spawn(["dotnet", "tool", "install", "--global", "roslyn-language-server", "--prerelease"], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
stdin: "pipe",
|
||||
})
|
||||
const exit = await proc.exited
|
||||
if (exit !== 0) {
|
||||
log.error("Failed to install roslyn-language-server")
|
||||
return
|
||||
}
|
||||
|
||||
bin = path.join(Global.Path.bin, "roslyn-language-server" + (process.platform === "win32" ? ".exe" : ""))
|
||||
log.info(`installed roslyn-language-server`, { bin })
|
||||
}
|
||||
|
||||
return {
|
||||
process: spawn(bin, ["--stdio", "--autoLoadProjects"], {
|
||||
@@ -716,135 +737,6 @@ export const CSharp: Info = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Razor: Info = {
|
||||
id: "razor",
|
||||
root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]),
|
||||
extensions: [".razor", ".cshtml"],
|
||||
async spawn(root) {
|
||||
const bin = await getRoslynLanguageServer()
|
||||
if (!bin) return
|
||||
|
||||
const razor = await findVscodeRazorExtension()
|
||||
if (!razor) {
|
||||
log.info("VS Code C# extension with Razor support not found, skipping Razor LSP")
|
||||
return
|
||||
}
|
||||
|
||||
log.info("using VS Code Razor extension for roslyn-language-server", { extension: razor.extension })
|
||||
return {
|
||||
process: spawn(
|
||||
bin,
|
||||
[
|
||||
"--stdio",
|
||||
"--autoLoadProjects",
|
||||
`--razorSourceGenerator=${razor.compiler}`,
|
||||
`--razorDesignTimePath=${razor.targets}`,
|
||||
"--extension",
|
||||
razor.extension,
|
||||
],
|
||||
{
|
||||
cwd: root,
|
||||
},
|
||||
),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
let roslynLanguageServerInstall: Promise<string | undefined> | undefined
|
||||
|
||||
async function getRoslynLanguageServer() {
|
||||
const existing = which("roslyn-language-server")
|
||||
if (existing) return existing
|
||||
|
||||
const global = await roslynLanguageServerGlobalPath()
|
||||
if (global) return global
|
||||
|
||||
roslynLanguageServerInstall ||= installRoslynLanguageServer().finally(() => {
|
||||
roslynLanguageServerInstall = undefined
|
||||
})
|
||||
return roslynLanguageServerInstall
|
||||
}
|
||||
|
||||
async function installRoslynLanguageServer() {
|
||||
if (!which("dotnet")) {
|
||||
log.error(".NET SDK is required to install roslyn-language-server")
|
||||
return
|
||||
}
|
||||
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
log.info("installing roslyn-language-server via dotnet tool")
|
||||
const proc = Process.spawn(["dotnet", "tool", "install", "--global", "roslyn-language-server", "--prerelease"], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
stdin: "pipe",
|
||||
})
|
||||
const exit = await proc.exited
|
||||
if (exit !== 0) {
|
||||
log.error("Failed to install roslyn-language-server")
|
||||
return
|
||||
}
|
||||
|
||||
const resolved = which("roslyn-language-server")
|
||||
if (resolved) {
|
||||
log.info(`installed roslyn-language-server`, { bin: resolved })
|
||||
return resolved
|
||||
}
|
||||
|
||||
const global = await roslynLanguageServerGlobalPath()
|
||||
if (global) {
|
||||
log.info(`installed roslyn-language-server`, { bin: global })
|
||||
return global
|
||||
}
|
||||
|
||||
log.error("Installed roslyn-language-server but could not resolve executable")
|
||||
}
|
||||
|
||||
async function roslynLanguageServerGlobalPath() {
|
||||
const bin = path.join(
|
||||
process.env.DOTNET_CLI_HOME ?? os.homedir(),
|
||||
".dotnet",
|
||||
"tools",
|
||||
"roslyn-language-server" + (process.platform === "win32" ? ".cmd" : ""),
|
||||
)
|
||||
return (await pathExists(bin)) ? bin : undefined
|
||||
}
|
||||
|
||||
async function findVscodeRazorExtension() {
|
||||
const roots = [
|
||||
process.env.VSCODE_EXTENSIONS,
|
||||
path.join(os.homedir(), ".vscode", "extensions"),
|
||||
path.join(os.homedir(), ".vscode-insiders", "extensions"),
|
||||
path.join(os.homedir(), ".vscode-server", "extensions"),
|
||||
path.join(os.homedir(), ".vscode-server-insiders", "extensions"),
|
||||
].filter((item) => item !== undefined)
|
||||
|
||||
for (const root of [...new Set(roots)]) {
|
||||
const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => [])
|
||||
const candidates = await Promise.all(
|
||||
entries
|
||||
.filter((entry) => entry.isDirectory() && entry.name.startsWith("ms-dotnettools.csharp-"))
|
||||
.map(async (entry) => ({
|
||||
path: path.join(root, entry.name, ".razorExtension"),
|
||||
modified: (await fs.stat(path.join(root, entry.name)).catch(() => undefined))?.mtimeMs ?? 0,
|
||||
})),
|
||||
)
|
||||
for (const entry of candidates.sort((a, b) => b.modified - a.modified).map((candidate) => candidate.path)) {
|
||||
const result = {
|
||||
compiler: path.join(entry, "Microsoft.CodeAnalysis.Razor.Compiler.dll"),
|
||||
targets: path.join(entry, "Targets", "Microsoft.NET.Sdk.Razor.DesignTime.targets"),
|
||||
extension: path.join(entry, "Microsoft.VisualStudioCode.RazorExtension.dll"),
|
||||
}
|
||||
if (
|
||||
(await pathExists(result.compiler)) &&
|
||||
(await pathExists(result.targets)) &&
|
||||
(await pathExists(result.extension))
|
||||
) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const FSharp: Info = {
|
||||
id: "fsharp",
|
||||
root: NearestRoot([".slnx", ".sln", ".fsproj", "global.json"]),
|
||||
|
||||
@@ -288,18 +288,8 @@ function expand(pattern: string): string {
|
||||
}
|
||||
|
||||
export function fromConfig(permission: ConfigPermission.Info) {
|
||||
// Sort top-level keys so wildcard permissions (`*`, `mcp_*`) come before
|
||||
// specific ones. Combined with `findLast` in evaluate(), this gives the
|
||||
// intuitive semantic "specific tool rules override the `*` fallback"
|
||||
// regardless of the user's JSON key order. Sub-pattern order inside a
|
||||
// single permission key is preserved — only top-level keys are sorted.
|
||||
const entries = Object.entries(permission).sort(([a], [b]) => {
|
||||
const aWild = a.includes("*")
|
||||
const bWild = b.includes("*")
|
||||
return aWild === bWild ? 0 : aWild ? -1 : 1
|
||||
})
|
||||
const ruleset: Ruleset = []
|
||||
for (const [key, value] of entries) {
|
||||
for (const [key, value] of Object.entries(permission)) {
|
||||
if (typeof value === "string") {
|
||||
ruleset.push({ permission: key, action: value, pattern: "*" })
|
||||
continue
|
||||
|
||||
@@ -787,7 +787,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
const shellName = (
|
||||
process.platform === "win32" ? path.win32.basename(sh, ".exe") : path.basename(sh)
|
||||
).toLowerCase()
|
||||
const cwd = ctx.directory
|
||||
const invocations: Record<string, { args: string[] }> = {
|
||||
nu: { args: ["-c", input.command] },
|
||||
fish: { args: ["-c", input.command] },
|
||||
@@ -796,13 +795,12 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
"-l",
|
||||
"-c",
|
||||
`
|
||||
__oc_cwd=$PWD
|
||||
[[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true
|
||||
[[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true
|
||||
cd -- "$1"
|
||||
cd "$__oc_cwd"
|
||||
eval ${JSON.stringify(input.command)}
|
||||
`,
|
||||
"opencode",
|
||||
cwd,
|
||||
],
|
||||
},
|
||||
bash: {
|
||||
@@ -810,13 +808,12 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
"-l",
|
||||
"-c",
|
||||
`
|
||||
__oc_cwd=$PWD
|
||||
shopt -s expand_aliases
|
||||
[[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true
|
||||
cd -- "$1"
|
||||
cd "$__oc_cwd"
|
||||
eval ${JSON.stringify(input.command)}
|
||||
`,
|
||||
"opencode",
|
||||
cwd,
|
||||
],
|
||||
},
|
||||
cmd: { args: ["/c", input.command] },
|
||||
@@ -826,6 +823,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
}
|
||||
|
||||
const args = (invocations[shellName] ?? invocations[""]).args
|
||||
const cwd = ctx.directory
|
||||
const shellEnv = yield* plugin.trigger(
|
||||
"shell.env",
|
||||
{ cwd, sessionID: input.sessionID, callID: part.callID },
|
||||
|
||||
@@ -1501,10 +1501,8 @@ test("permission config canonicalises known keys first, preserves rest-key inser
|
||||
// todowrite, external_directory are declared in `config/permission.ts`),
|
||||
// followed by rest keys in the user's insertion order.
|
||||
//
|
||||
// Rule precedence is NOT affected by this reordering: `Permission.fromConfig`
|
||||
// sorts wildcards before specifics before iterating. See the
|
||||
// "fromConfig - specific key beats wildcard regardless of JSON key order"
|
||||
// test in test/permission/next.test.ts for the behavioural guarantee.
|
||||
// Permission.fromConfig preserves this effective order, and permission
|
||||
// evaluation uses last-match-wins precedence.
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Filesystem.write(
|
||||
|
||||
@@ -130,49 +130,45 @@ test("fromConfig - does not expand tilde in middle of path", () => {
|
||||
|
||||
// Top-level wildcard-vs-specific precedence semantics.
|
||||
//
|
||||
// fromConfig sorts top-level keys so wildcard permissions (containing "*")
|
||||
// come before specific permissions. Combined with `findLast` in evaluate(),
|
||||
// this gives the intuitive semantic "specific tool rules override the `*`
|
||||
// fallback", regardless of the order the user wrote the keys in their JSON.
|
||||
// fromConfig preserves top-level key order. Combined with `findLast` in
|
||||
// evaluate(), later matching rules win.
|
||||
//
|
||||
// Sub-pattern order inside a single permission key (e.g. `bash: { "*": "allow", "rm": "deny" }`)
|
||||
// still depends on insertion order — only top-level keys are sorted.
|
||||
// also depends on insertion order.
|
||||
|
||||
test("fromConfig - specific key beats wildcard regardless of JSON key order", () => {
|
||||
test("fromConfig - top-level key order controls wildcard precedence", () => {
|
||||
const wildcardFirst = Permission.fromConfig({ "*": "deny", bash: "allow" })
|
||||
const specificFirst = Permission.fromConfig({ bash: "allow", "*": "deny" })
|
||||
|
||||
// Both orderings produce the same ruleset
|
||||
expect(wildcardFirst).toEqual(specificFirst)
|
||||
|
||||
// And both evaluate bash → allow (bash rule wins over * fallback)
|
||||
expect(Permission.evaluate("bash", "ls", wildcardFirst).action).toBe("allow")
|
||||
expect(Permission.evaluate("bash", "ls", specificFirst).action).toBe("allow")
|
||||
expect(Permission.evaluate("bash", "ls", specificFirst).action).toBe("deny")
|
||||
})
|
||||
|
||||
test("fromConfig - regression: trailing wildcard overrides earlier specific rule", () => {
|
||||
const ruleset = Permission.fromConfig({ bash: "ask", "*": "allow" })
|
||||
expect(Permission.evaluate("bash", "glab", ruleset).action).toBe("allow")
|
||||
})
|
||||
|
||||
test("fromConfig - wildcard acts as fallback for permissions with no specific rule", () => {
|
||||
const ruleset = Permission.fromConfig({ bash: "allow", "*": "ask" })
|
||||
const ruleset = Permission.fromConfig({ "*": "ask", bash: "allow" })
|
||||
expect(Permission.evaluate("edit", "foo.ts", ruleset).action).toBe("ask")
|
||||
expect(Permission.evaluate("bash", "ls", ruleset).action).toBe("allow")
|
||||
})
|
||||
|
||||
test("fromConfig - top-level ordering: wildcards first, specifics after", () => {
|
||||
test("fromConfig - preserves top-level ordering", () => {
|
||||
const ruleset = Permission.fromConfig({
|
||||
bash: "allow",
|
||||
"*": "ask",
|
||||
edit: "deny",
|
||||
"mcp_*": "allow",
|
||||
})
|
||||
// wildcards (* and mcp_*) come before specifics (bash, edit)
|
||||
const permissions = ruleset.map((r) => r.permission)
|
||||
expect(permissions.slice(0, 2).sort()).toEqual(["*", "mcp_*"])
|
||||
expect(permissions.slice(2)).toEqual(["bash", "edit"])
|
||||
expect(ruleset.map((r) => r.permission)).toEqual(["bash", "*", "edit", "mcp_*"])
|
||||
})
|
||||
|
||||
test("fromConfig - sub-pattern insertion order inside a tool key is preserved (only top-level sorts)", () => {
|
||||
test("fromConfig - sub-pattern insertion order inside a tool key is preserved", () => {
|
||||
// Sub-patterns within a single tool key use the documented "`*` first,
|
||||
// specific patterns after" convention (findLast picks specifics). The
|
||||
// top-level sort must not touch sub-pattern ordering.
|
||||
// top-level order must not affect sub-pattern ordering.
|
||||
const ruleset = Permission.fromConfig({ bash: { "*": "deny", "git *": "allow" } })
|
||||
expect(ruleset.map((r) => r.pattern)).toEqual(["*", "git *"])
|
||||
// * fallback for unknown commands
|
||||
|
||||
@@ -1078,30 +1078,6 @@ unix("shell completes a fast command on the preferred shell", () =>
|
||||
),
|
||||
)
|
||||
|
||||
unix("shell commands can change directory after startup", () =>
|
||||
provideTmpdirInstance(
|
||||
(dir) =>
|
||||
Effect.gen(function* () {
|
||||
const { prompt, run, chat } = yield* boot()
|
||||
const parent = path.dirname(dir)
|
||||
const result = yield* prompt.shell({
|
||||
sessionID: chat.id,
|
||||
agent: "build",
|
||||
command: "cd .. && pwd",
|
||||
})
|
||||
|
||||
expect(result.info.role).toBe("assistant")
|
||||
const tool = completedTool(result.parts)
|
||||
if (!tool) return
|
||||
|
||||
expect(tool.state.output).toContain(parent)
|
||||
expect(tool.state.metadata.output).toContain(parent)
|
||||
yield* run.assertNotBusy(chat.id)
|
||||
}),
|
||||
{ git: true, config: cfg },
|
||||
),
|
||||
)
|
||||
|
||||
unix("shell lists files from the project directory", () =>
|
||||
provideTmpdirInstance(
|
||||
(dir) =>
|
||||
|
||||
@@ -16,7 +16,7 @@ OpenCode comes with several built-in LSP servers for popular languages:
|
||||
| astro | .astro | Auto-installs for Astro projects |
|
||||
| bash | .sh, .bash, .zsh, .ksh | Auto-installs bash-language-server |
|
||||
| clangd | .c, .cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++ | Auto-installs for C/C++ projects |
|
||||
| csharp | .cs, .csx | `.NET SDK` installed |
|
||||
| csharp | .cs | `.NET SDK` installed |
|
||||
| clojure-lsp | .clj, .cljs, .cljc, .edn | `clojure-lsp` command available |
|
||||
| dart | .dart | `dart` command available |
|
||||
| deno | .ts, .tsx, .js, .jsx, .mjs | `deno` command available (auto-detects deno.json/deno.jsonc) |
|
||||
@@ -36,7 +36,6 @@ OpenCode comes with several built-in LSP servers for popular languages:
|
||||
| php intelephense | .php | Auto-installs for PHP projects |
|
||||
| prisma | .prisma | `prisma` command available |
|
||||
| pyright | .py, .pyi | `pyright` dependency installed |
|
||||
| razor | .razor, .cshtml | `.NET SDK` and VS Code C# extension installed |
|
||||
| ruby-lsp (rubocop) | .rb, .rake, .gemspec, .ru | `ruby` and `gem` commands available |
|
||||
| rust | .rs | `rust-analyzer` command available |
|
||||
| sourcekit-lsp | .swift, .objc, .objcpp | `swift` installed (`xcode` on macOS) |
|
||||
|
||||
120
script/beta.ts
120
script/beta.ts
@@ -57,19 +57,6 @@ function lines(prs: PR[]) {
|
||||
return prs.map((x) => `- #${x.number}: ${x.title}`).join("\n") || "(none)"
|
||||
}
|
||||
|
||||
function group(title: string) {
|
||||
if (process.env.GITHUB_ACTIONS !== "true") {
|
||||
console.log(title)
|
||||
return { [Symbol.dispose]() {} }
|
||||
}
|
||||
console.log(`::group::${title}`)
|
||||
return {
|
||||
[Symbol.dispose]() {
|
||||
console.log("::endgroup::")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function typecheck() {
|
||||
console.log(" Running typecheck...")
|
||||
|
||||
@@ -94,39 +81,6 @@ async function build() {
|
||||
}
|
||||
}
|
||||
|
||||
async function validate() {
|
||||
if (!(await typecheck())) return false
|
||||
if (!(await build())) return false
|
||||
return true
|
||||
}
|
||||
|
||||
async function commitSmokeChanges() {
|
||||
const out = await $`git status --porcelain`.text()
|
||||
if (!out.trim()) {
|
||||
console.log("Smoke check passed")
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
await $`git add -A`
|
||||
await $`git commit -m "Fix beta integration"`
|
||||
} catch (err) {
|
||||
console.log(`Failed to commit smoke fixes: ${err}`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (!(await validate())) return false
|
||||
|
||||
const left = await $`git status --porcelain`.text()
|
||||
if (!left.trim()) {
|
||||
console.log("Smoke check passed")
|
||||
return true
|
||||
}
|
||||
|
||||
console.log(`Smoke check left uncommitted changes:\n${left}`)
|
||||
return false
|
||||
}
|
||||
|
||||
async function install() {
|
||||
console.log(" Regenerating bun.lock...")
|
||||
|
||||
@@ -189,15 +143,11 @@ async function fix(pr: PR, files: string[], prs: PR[], applied: number[], idx: n
|
||||
}
|
||||
|
||||
async function smoke(prs: PR[], applied: number[]) {
|
||||
console.log("\nRunning final smoke check...")
|
||||
|
||||
if (await validate()) return commitSmokeChanges()
|
||||
|
||||
console.log("\nTrying to fix final smoke check with opencode...")
|
||||
console.log("\nRunning final smoke check with opencode...")
|
||||
|
||||
const done = lines(prs.filter((x) => applied.includes(x.number)))
|
||||
const prompt = [
|
||||
"The beta merge batch is complete, but the deterministic final smoke check failed.",
|
||||
"The beta merge batch is complete.",
|
||||
`Merged PRs on HEAD:\n${done}`,
|
||||
"Run `bun typecheck` at the repo root.",
|
||||
"Run `./script/build.ts --single` in `packages/opencode`.",
|
||||
@@ -212,8 +162,38 @@ async function smoke(prs: PR[], applied: number[]) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!(await validate())) return false
|
||||
return commitSmokeChanges()
|
||||
if (!(await typecheck())) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!(await build())) {
|
||||
return false
|
||||
}
|
||||
|
||||
const out = await $`git status --porcelain`.text()
|
||||
if (!out.trim()) {
|
||||
console.log("Smoke check passed")
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
await $`git add -A`
|
||||
await $`git commit -m "Fix beta integration"`
|
||||
} catch (err) {
|
||||
console.log(`Failed to commit smoke fixes: ${err}`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (!(await typecheck())) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!(await build())) {
|
||||
return false
|
||||
}
|
||||
|
||||
console.log("Smoke check passed")
|
||||
return true
|
||||
}
|
||||
|
||||
async function main() {
|
||||
@@ -240,8 +220,8 @@ async function main() {
|
||||
const failed: FailedPR[] = []
|
||||
|
||||
for (const [idx, pr] of prs.entries()) {
|
||||
console.log()
|
||||
using _ = group(`Processing PR ${idx + 1}/${prs.length} #${pr.number}: ${pr.title}`)
|
||||
console.log(`\nProcessing PR ${idx + 1}/${prs.length} #${pr.number}: ${pr.title}`)
|
||||
|
||||
console.log(" Fetching PR head...")
|
||||
try {
|
||||
await $`git fetch origin pull/${pr.number}/head:pr/${pr.number}`
|
||||
@@ -314,13 +294,17 @@ async function main() {
|
||||
throw new Error(`${failed.length} PR(s) failed to merge`)
|
||||
}
|
||||
|
||||
if (applied.length > 0) {
|
||||
console.log("\nSkipping final smoke check")
|
||||
}
|
||||
|
||||
console.log("\nChecking if beta branch has changes...")
|
||||
await $`git fetch origin beta`
|
||||
|
||||
const localTree = (await $`git rev-parse beta^{tree}`.text()).trim()
|
||||
const localTree = await $`git rev-parse beta^{tree}`.text()
|
||||
const remoteTrees = (await $`git log origin/dev..origin/beta --format=%T`.text()).split("\n")
|
||||
|
||||
const matchIdx = remoteTrees.indexOf(localTree)
|
||||
const matchIdx = remoteTrees.indexOf(localTree.trim())
|
||||
if (matchIdx !== -1) {
|
||||
if (matchIdx !== 0) {
|
||||
console.log(`Beta branch contains this sync, but additional commits exist after it. Leaving beta branch as is.`)
|
||||
@@ -330,25 +314,7 @@ async function main() {
|
||||
return
|
||||
}
|
||||
|
||||
if (!(await smoke(prs, applied))) throw new Error("Final smoke check failed")
|
||||
|
||||
await $`git fetch origin beta`
|
||||
|
||||
const validatedTree = (await $`git rev-parse beta^{tree}`.text()).trim()
|
||||
const remoteTreesAfterSmoke = (await $`git log origin/dev..origin/beta --format=%T`.text()).split("\n")
|
||||
const matchIdxAfterSmoke = remoteTreesAfterSmoke.indexOf(validatedTree)
|
||||
if (matchIdxAfterSmoke !== -1) {
|
||||
if (matchIdxAfterSmoke !== 0) {
|
||||
console.log(
|
||||
`Beta branch contains this validated sync, but additional commits exist after it. Leaving beta branch as is.`,
|
||||
)
|
||||
} else {
|
||||
console.log("Validated beta branch now matches remote contents, no push needed")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Force pushing validated beta branch...")
|
||||
console.log("Force pushing beta branch...")
|
||||
await $`git push origin beta --force --no-verify`
|
||||
|
||||
console.log("Successfully synced beta branch")
|
||||
|
||||
@@ -37,7 +37,7 @@ async function close(num: number) {
|
||||
const patch = await fetch(base, {
|
||||
method: "PATCH",
|
||||
headers,
|
||||
body: JSON.stringify({ state: "closed", state_reason: "not_planned" }),
|
||||
body: JSON.stringify({ state: "closed", state_reason: "completed" }),
|
||||
})
|
||||
if (!patch.ok) throw new Error(`Failed to close #${num}: ${patch.status} ${patch.statusText}`)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user