diff --git a/script/publish.ts b/script/publish.ts index 9ace01d..ab22634 100644 --- a/script/publish.ts +++ b/script/publish.ts @@ -78,13 +78,35 @@ async function gitTagAndRelease(newVersion: string, changelog: string): Promise< await $`git config user.email "github-actions[bot]@users.noreply.github.com"` await $`git config user.name "github-actions[bot]"` await $`git add package.json` - await $`git commit -m "release: v${newVersion}"` - await $`git tag v${newVersion}` + + // Commit only if there are staged changes (idempotent) + const hasStagedChanges = await $`git diff --cached --quiet`.nothrow() + if (hasStagedChanges.exitCode !== 0) { + await $`git commit -m "release: v${newVersion}"` + } else { + console.log("No changes to commit (version already updated)") + } + + // Tag only if it doesn't exist (idempotent) + const tagExists = await $`git rev-parse v${newVersion}`.nothrow() + if (tagExists.exitCode !== 0) { + await $`git tag v${newVersion}` + } else { + console.log(`Tag v${newVersion} already exists`) + } + + // Push (idempotent - git push is already idempotent) await $`git push origin HEAD --tags` + // Create release only if it doesn't exist (idempotent) console.log("\nCreating GitHub release...") const releaseNotes = changelog || "No notable changes" - await $`gh release create v${newVersion} --title "v${newVersion}" --notes ${releaseNotes}` + const releaseExists = await $`gh release view v${newVersion}`.nothrow() + if (releaseExists.exitCode !== 0) { + await $`gh release create v${newVersion} --title "v${newVersion}" --notes ${releaseNotes}` + } else { + console.log(`Release v${newVersion} already exists`) + } } async function checkVersionExists(version: string): Promise { diff --git a/src/tools/ast-grep/tools.ts b/src/tools/ast-grep/tools.ts index fcf1d98..49ff8c7 100644 --- a/src/tools/ast-grep/tools.ts +++ b/src/tools/ast-grep/tools.ts @@ -10,13 +10,55 @@ 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 + } + + 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.` + ) + } +} + export const ast_grep_search = tool({ description: "Search code patterns across filesystem using AST-aware matching. Supports 25 languages. " + "Use meta-variables: $VAR (single node), $$$ (multiple nodes). " + + "IMPORTANT: Patterns must be complete AST nodes (valid code). " + + "For functions, include params and body: 'export async function $NAME($$$) { $$$ }' not 'export async function $NAME'. " + "Examples: 'console.log($MSG)', 'def $FUNC($$$):', 'async function $NAME($$$)'", args: { - pattern: tool.schema.string().describe("AST pattern with meta-variables ($VAR, $$$)"), + pattern: tool.schema.string().describe("AST pattern with meta-variables ($VAR, $$$). Must be complete AST node."), lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"), paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search (default: ['.'])"), globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs (prefix ! to exclude)"), @@ -24,6 +66,8 @@ 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,