@fnnm/pi-ast-grep 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/index.ts +115 -0
  2. package/package.json +25 -0
  3. package/tsconfig.json +18 -0
package/index.ts ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * AST-Grep Tools
3
+ *
4
+ * Search and replace code patterns using AST-aware matching.
5
+ */
6
+
7
+ import { Type } from "@sinclair/typebox";
8
+ import { spawn } from "child_process";
9
+
10
+ const LANGUAGES = [
11
+ "c", "cpp", "csharp", "css", "dart", "elixir", "go", "haskell", "html",
12
+ "java", "javascript", "json", "kotlin", "lua", "php", "python", "ruby",
13
+ "rust", "scala", "sql", "swift", "tsx", "typescript", "yaml",
14
+ ] as const;
15
+
16
+ interface Match {
17
+ file: string;
18
+ range: { start: { line: number; column: number }; end: { line: number; column: number } };
19
+ text: string;
20
+ replacement?: string;
21
+ }
22
+
23
+ async function runSg(args: string[]): Promise<{ matches: Match[]; error?: string }> {
24
+ return new Promise((resolve) => {
25
+ const proc = spawn("sg", args, { stdio: ["ignore", "pipe", "pipe"] });
26
+ let stdout = "";
27
+ let stderr = "";
28
+
29
+ proc.stdout.on("data", (data) => (stdout += data.toString()));
30
+ proc.stderr.on("data", (data) => (stderr += data.toString()));
31
+
32
+ proc.on("error", (err) => {
33
+ if (err.message.includes("ENOENT")) {
34
+ resolve({ matches: [], error: "ast-grep CLI not found. Install: npm i -g @ast-grep/cli" });
35
+ } else {
36
+ resolve({ matches: [], error: err.message });
37
+ }
38
+ });
39
+
40
+ proc.on("close", (code) => {
41
+ if (code !== 0 && !stdout.trim()) {
42
+ resolve({ matches: [], error: stderr.includes("No files found") ? undefined : stderr.trim() || `Exit code ${code}` });
43
+ return;
44
+ }
45
+ if (!stdout.trim()) { resolve({ matches: [] }); return; }
46
+ try {
47
+ resolve({ matches: JSON.parse(stdout) });
48
+ } catch {
49
+ resolve({ matches: [], error: "Failed to parse output" });
50
+ }
51
+ });
52
+ });
53
+ }
54
+
55
+ function formatMatches(matches: Match[], isDryRun = false): string {
56
+ if (matches.length === 0) return "No matches found";
57
+ const MAX = 100;
58
+ const shown = matches.slice(0, MAX);
59
+ const lines = shown.map((m) => {
60
+ const loc = `${m.file}:${m.range.start.line}:${m.range.start.column}`;
61
+ const text = m.text.length > 100 ? m.text.slice(0, 100) + "..." : m.text;
62
+ return isDryRun && m.replacement ? `${loc}\n - ${text}\n + ${m.replacement}` : `${loc}: ${text}`;
63
+ });
64
+ if (matches.length > MAX) lines.unshift(`Found ${matches.length} matches (showing first ${MAX}):`);
65
+ return lines.join("\n");
66
+ }
67
+
68
+ const factory: CustomToolFactory = (_pi) => [
69
+ {
70
+ name: "ast_grep_search",
71
+ label: "AST Search",
72
+ description: "Search code patterns using AST-aware matching. Use meta-variables: $VAR (single node), $$$ (multiple). Examples: 'console.log($MSG)', 'def $FUNC($$$):'",
73
+ parameters: Type.Object({
74
+ pattern: Type.String({ description: "AST pattern with meta-variables" }),
75
+ lang: Type.Union(LANGUAGES.map((l) => Type.Literal(l)), { description: "Target language" }),
76
+ paths: Type.Optional(Type.Array(Type.String(), { description: "Paths to search" })),
77
+ }),
78
+ async execute(_toolCallId, params, _signal, _onUpdate) {
79
+ const { pattern, lang, paths } = params as { pattern: string; lang: string; paths?: string[] };
80
+ const args = ["run", "-p", pattern, "--lang", lang, "--json=compact", ...(paths?.length ? paths : ["."])];
81
+ const result = await runSg(args);
82
+ if (result.error) return { content: [{ type: "text", text: `Error: ${result.error}` }], details: {}, isError: true };
83
+ return { content: [{ type: "text", text: formatMatches(result.matches) }], details: { matchCount: result.matches.length } };
84
+ },
85
+ },
86
+ {
87
+ name: "ast_grep_replace",
88
+ label: "AST Replace",
89
+ description: "Replace code patterns with AST-aware rewriting. Dry-run by default. Example: pattern='console.log($MSG)' rewrite='logger.info($MSG)'",
90
+ parameters: Type.Object({
91
+ pattern: Type.String({ description: "AST pattern to match" }),
92
+ rewrite: Type.String({ description: "Replacement pattern" }),
93
+ lang: Type.Union(LANGUAGES.map((l) => Type.Literal(l)), { description: "Target language" }),
94
+ paths: Type.Optional(Type.Array(Type.String(), { description: "Paths to search" })),
95
+ apply: Type.Optional(Type.Boolean({ description: "Apply changes (default: false)" })),
96
+ }),
97
+ async execute(_toolCallId, params, _signal, _onUpdate) {
98
+ const { pattern, rewrite, lang, paths, apply } = params as { pattern: string; rewrite: string; lang: string; paths?: string[]; apply?: boolean };
99
+ const args = ["run", "-p", pattern, "-r", rewrite, "--lang", lang, "--json=compact"];
100
+ if (apply) args.push("--update-all");
101
+ args.push(...(paths?.length ? paths : ["."]));
102
+
103
+ const result = await runSg(args);
104
+ if (result.error) return { content: [{ type: "text", text: `Error: ${result.error}` }], details: {}, isError: true };
105
+
106
+ let output = formatMatches(result.matches, !apply);
107
+ if (!apply && result.matches.length > 0) output += "\n\n(Dry run - use apply=true to apply)";
108
+ if (apply && result.matches.length > 0) output = `Applied ${result.matches.length} replacements:\n${output}`;
109
+
110
+ return { content: [{ type: "text", text: output }], details: { matchCount: result.matches.length, applied: apply } };
111
+ },
112
+ },
113
+ ];
114
+
115
+ export default factory;
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@fnnm/pi-ast-grep",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "version": "0.1.0",
7
+ "description": "ast-grep extension for Pi.",
8
+ "license": "MIT",
9
+ "author": "fnnm",
10
+ "keywords": [
11
+ "pi-package"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/fxcl/pi-ast_grep.git",
16
+ "directory": "pi-ast_grep"
17
+ },
18
+ "bugs": "https://github.com/fxcl/pi-ast_grep/issues",
19
+ "homepage": "https://github.com/fxcl/pi-ast_grep/tree/main/pi-ast_grep",
20
+ "pi": {
21
+ "extensions": [
22
+ "index.ts"
23
+ ]
24
+ }
25
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": [
4
+ "ESNext",
5
+ "DOM"
6
+ ],
7
+ "target": "ESNext",
8
+ "module": "ESNext",
9
+ "moduleResolution": "bundler",
10
+ "strict": true,
11
+ "noEmit": true,
12
+ "skipLibCheck": true,
13
+ "allowImportingTsExtensions": true,
14
+ "types": [
15
+ "bun-types"
16
+ ]
17
+ }
18
+ }