Compare commits

..

7 Commits

Author SHA1 Message Date
github-actions[bot]
54e13e4330 release: v0.1.22 2025-12-05 13:13:29 +00:00
YeonGyu-Kim
1780e2971d refactor(ast-grep): simplify binary resolution, rely on auto-download
- Remove hardcoded homebrew paths
- Remove npm package path resolution (prone to placeholder issues)
- Only check cached binary (~/.cache/oh-my-opencode/bin/sg)
- If not found, cli.ts will auto-download from GitHub releases

The download logic in cli.ts handles all cases properly.
2025-12-05 22:12:12 +09:00
github-actions[bot]
ded97701b8 release: v0.1.21 2025-12-05 13:04:11 +00:00
YeonGyu-Kim
316cdc1a62 fix(ast-grep): validate binary before using, prioritize homebrew path
- Add isValidBinary() check: file must be >10KB (placeholder files are ~100 bytes)
- Check homebrew paths first on macOS (most reliable)
- Check cached binary second
- npm package paths last (prone to placeholder issues)

Fixes ENOEXEC error when @ast-grep/cli has placeholder instead of real binary
2025-12-05 22:03:05 +09:00
YeonGyu-Kim
f19cd8fc71 improve(ast-grep): better Python pattern hints
- Show exact pattern without colon when pattern ends with ':'
- More actionable hint message
2025-12-05 21:57:58 +09:00
github-actions[bot]
181194ae3c release: v0.1.19 2025-12-05 12:00:31 +00:00
YeonGyu-Kim
b8f5599e61 feat(ast-grep): add helpful hints for incomplete Python patterns
- Show hints when Python class/function patterns return empty results
- Detect patterns ending with ':' that need body (class :, def ():)
- Removed validation that could cause false positives
- Hints only appear on empty results, not on successful matches
2025-12-05 20:59:05 +09:00
3 changed files with 36 additions and 100 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode",
"version": "0.1.18",
"version": "0.1.22",
"description": "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -1,71 +1,17 @@
import { createRequire } from "module"
import { dirname, join } from "path"
import { existsSync } from "fs"
import { existsSync, statSync } from "fs"
import { getCachedBinaryPath } from "./downloader"
type Platform = "darwin" | "linux" | "win32" | "unsupported"
function getPlatformPackageName(): string | null {
const platform = process.platform as Platform
const arch = process.arch
const platformMap: Record<string, string> = {
"darwin-arm64": "@ast-grep/cli-darwin-arm64",
"darwin-x64": "@ast-grep/cli-darwin-x64",
"linux-arm64": "@ast-grep/cli-linux-arm64-gnu",
"linux-x64": "@ast-grep/cli-linux-x64-gnu",
"win32-x64": "@ast-grep/cli-win32-x64-msvc",
"win32-arm64": "@ast-grep/cli-win32-arm64-msvc",
"win32-ia32": "@ast-grep/cli-win32-ia32-msvc",
function isValidBinary(filePath: string): boolean {
try {
return statSync(filePath).size > 10000
} catch {
return false
}
return platformMap[`${platform}-${arch}`] ?? null
}
export function findSgCliPathSync(): string | null {
const binaryName = process.platform === "win32" ? "sg.exe" : "sg"
try {
const require = createRequire(import.meta.url)
const cliPkgPath = require.resolve("@ast-grep/cli/package.json")
const cliDir = dirname(cliPkgPath)
const sgPath = join(cliDir, binaryName)
if (existsSync(sgPath)) {
return sgPath
}
} catch {
// @ast-grep/cli not installed
}
const platformPkg = getPlatformPackageName()
if (platformPkg) {
try {
const require = createRequire(import.meta.url)
const pkgPath = require.resolve(`${platformPkg}/package.json`)
const pkgDir = dirname(pkgPath)
const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep"
const binaryPath = join(pkgDir, astGrepName)
if (existsSync(binaryPath)) {
return binaryPath
}
} catch {
// Platform-specific package not installed
}
}
if (process.platform === "darwin") {
const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"]
for (const path of homebrewPaths) {
if (existsSync(path)) {
return path
}
}
}
const cachedPath = getCachedBinaryPath()
if (cachedPath) {
if (cachedPath && isValidBinary(cachedPath)) {
return cachedPath
}

View File

@@ -10,44 +10,27 @@ function showOutputToUser(context: unknown, output: string): void {
ctx.metadata?.({ metadata: { output } })
}
/**
* JS/TS languages that require complete function declaration patterns
*/
const JS_TS_LANGUAGES = ["javascript", "typescript", "tsx"] as const
/**
* Validates AST pattern for common incomplete patterns that will fail silently.
* Only validates JS/TS languages where function declarations require body.
*
* @throws Error with helpful message if pattern is incomplete
*/
function validatePatternForCli(pattern: string, lang: CliLanguage): void {
if (!JS_TS_LANGUAGES.includes(lang as (typeof JS_TS_LANGUAGES)[number])) {
return
}
function getEmptyResultHint(pattern: string, lang: CliLanguage): string | null {
const src = pattern.trim()
// Detect incomplete function declarations:
// - "function $NAME" (no params/body)
// - "export function $NAME" (no params/body)
// - "export async function $NAME" (no params/body)
// - "export default function $NAME" (no params/body)
// Pattern: ends with $METAVAR (uppercase, underscore, digits) without ( or {
const incompleteFunctionDecl =
/^(export\s+)?(default\s+)?(async\s+)?function\s+\$[A-Z_][A-Z0-9_]*\s*$/i.test(src)
if (incompleteFunctionDecl) {
throw new Error(
`Incomplete AST pattern for ${lang}: "${pattern}"\n\n` +
`ast-grep requires complete AST nodes. Function declarations must include parameters and body.\n\n` +
`Examples of correct patterns:\n` +
` - "export async function $NAME($$$) { $$$ }" (matches export async functions)\n` +
` - "function $NAME($$$) { $$$ }" (matches all function declarations)\n` +
` - "async function $NAME($$$) { $$$ }" (matches async functions)\n\n` +
`Your pattern "${pattern}" is missing the parameter list and body.`
)
if (lang === "python") {
if (src.startsWith("class ") && src.endsWith(":")) {
const withoutColon = src.slice(0, -1)
return `💡 Hint: Remove trailing colon. Try: "${withoutColon}"`
}
if ((src.startsWith("def ") || src.startsWith("async def ")) && src.endsWith(":")) {
const withoutColon = src.slice(0, -1)
return `💡 Hint: Remove trailing colon. Try: "${withoutColon}"`
}
}
if (["javascript", "typescript", "tsx"].includes(lang)) {
if (/^(export\s+)?(async\s+)?function\s+\$[A-Z_]+\s*$/i.test(src)) {
return `💡 Hint: Function patterns need params and body. Try "function $NAME($$$) { $$$ }"`
}
}
return null
}
export const ast_grep_search = tool({
@@ -66,8 +49,6 @@ export const ast_grep_search = tool({
},
execute: async (args, context) => {
try {
validatePatternForCli(args.pattern, args.lang as CliLanguage)
const matches = await runSg({
pattern: args.pattern,
lang: args.lang as CliLanguage,
@@ -75,7 +56,16 @@ export const ast_grep_search = tool({
globs: args.globs,
context: args.context,
})
const output = formatSearchResult(matches)
let output = formatSearchResult(matches)
if (matches.length === 0) {
const hint = getEmptyResultHint(args.pattern, args.lang as CliLanguage)
if (hint) {
output += `\n\n${hint}`
}
}
showOutputToUser(context, output)
return output
} catch (e) {