@yofriadi/pi-ast 0.1.0 → 0.1.1
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.
- package/README.md +18 -0
- package/package.json +9 -3
- package/src/index.ts +13 -10
- package/src/tools/ast-rewrite.ts +22 -18
- package/src/tools/ast-search.ts +37 -20
- package/src/utils/exec.ts +51 -5
- package/test/fixtures/example.ts +2 -2
- package/test/integration.test.ts +57 -51
package/README.md
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
# AST Extension
|
|
2
|
+
## Install from git URL
|
|
3
|
+
|
|
4
|
+
```bash
|
|
5
|
+
pi install git:github.com/yofriadi/pi-extensions@ast-v<version>
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
To load only this extension from the monorepo package source, use package filtering in settings:
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"packages": [
|
|
13
|
+
{
|
|
14
|
+
"source": "git:github.com/yofriadi/pi-extensions@ast-v<version>",
|
|
15
|
+
"extensions": ["packages/ast/src/index.ts"]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
```
|
|
2
20
|
|
|
3
21
|
This extension provides integration with `ast-grep` (sg).
|
|
4
22
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yofriadi/pi-ast",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
"build": "echo 'nothing to build'"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
|
-
"@mariozechner/pi-coding-agent": "
|
|
20
|
-
}
|
|
19
|
+
"@mariozechner/pi-coding-agent": "^0.52.10"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/yofriadi/pi-extensions.git",
|
|
24
|
+
"directory": "packages/ast"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/yofriadi/pi-extensions/tree/main/packages/ast"
|
|
21
27
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { exec } from "./utils/exec";
|
|
3
|
+
import { registerAstRewrite } from "./tools/ast-rewrite.js";
|
|
4
|
+
import { registerAstSearch } from "./tools/ast-search.js";
|
|
5
|
+
import { exec } from "./utils/exec.js";
|
|
6
6
|
|
|
7
7
|
export default function astExtension(pi: ExtensionAPI): void {
|
|
8
8
|
// Register tools
|
|
@@ -17,21 +17,24 @@ export default function astExtension(pi: ExtensionAPI): void {
|
|
|
17
17
|
execute: async () => {
|
|
18
18
|
try {
|
|
19
19
|
const { exitCode, stdout, stderr } = await exec(["sg", "--version"]);
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
if (exitCode !== 0) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
return {
|
|
23
|
+
content: [{ type: "text", text: `ast-grep (sg) check failed: ${stderr}` }],
|
|
24
|
+
isError: true,
|
|
25
|
+
details: undefined,
|
|
26
|
+
};
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
return {
|
|
29
30
|
content: [{ type: "text", text: `ast-grep (sg) is available: ${stdout.trim()}` }],
|
|
31
|
+
details: undefined,
|
|
30
32
|
};
|
|
31
33
|
} catch (error) {
|
|
32
34
|
return {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
content: [{ type: "text", text: `ast-grep (sg) check failed: ${(error as Error).message}` }],
|
|
36
|
+
isError: true,
|
|
37
|
+
details: undefined,
|
|
35
38
|
};
|
|
36
39
|
}
|
|
37
40
|
},
|
package/src/tools/ast-rewrite.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Type } from "@sinclair/typebox";
|
|
2
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
3
|
-
import {
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import { exec } from "../utils/exec.js";
|
|
4
4
|
|
|
5
5
|
const MAX_OUTPUT_LENGTH = 10000;
|
|
6
6
|
|
|
@@ -8,7 +8,8 @@ export function registerAstRewrite(pi: ExtensionAPI): void {
|
|
|
8
8
|
pi.registerTool({
|
|
9
9
|
name: "ast_rewrite",
|
|
10
10
|
label: "AST Rewrite",
|
|
11
|
-
description:
|
|
11
|
+
description:
|
|
12
|
+
"Search and rewrite code using AST patterns with ast-grep (sg). Defaults to dry-run (preview). Use `apply: true` to execute changes.",
|
|
12
13
|
parameters: Type.Object({
|
|
13
14
|
pattern: Type.String({ description: "AST pattern to search for" }),
|
|
14
15
|
rewrite: Type.String({ description: "Replacement pattern" }),
|
|
@@ -21,49 +22,52 @@ export function registerAstRewrite(pi: ExtensionAPI): void {
|
|
|
21
22
|
|
|
22
23
|
try {
|
|
23
24
|
const args = ["sg", "run", "--pattern", pattern, "--rewrite", rewrite, "--color=never"];
|
|
24
|
-
|
|
25
|
+
|
|
25
26
|
if (lang) {
|
|
26
27
|
args.push("--lang", lang);
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
+
|
|
29
30
|
if (apply) {
|
|
30
31
|
args.push("-U"); // Update all (apply)
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
+
|
|
33
34
|
// Add path at the end
|
|
34
35
|
if (path) {
|
|
35
36
|
args.push(path);
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
const { exitCode, stdout, stderr } = await exec(args);
|
|
39
|
-
|
|
40
|
+
|
|
40
41
|
if (exitCode !== 0) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: "text", text: `ast-rewrite failed: ${stderr}` }],
|
|
44
|
+
isError: true,
|
|
45
|
+
details: undefined,
|
|
46
|
+
};
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
if (!stdout.trim()) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
return {
|
|
51
|
+
content: [{ type: "text", text: apply ? "No changes applied (no matches found)." : "No matches found." }],
|
|
52
|
+
details: undefined,
|
|
53
|
+
};
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
let output = stdout;
|
|
54
57
|
if (output.length > MAX_OUTPUT_LENGTH) {
|
|
55
|
-
output = output.substring(0, MAX_OUTPUT_LENGTH)
|
|
58
|
+
output = `${output.substring(0, MAX_OUTPUT_LENGTH)}\n... (truncated)`;
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
const mode = apply ? "APPLIED" : "DRY-RUN (preview)";
|
|
59
62
|
return {
|
|
60
63
|
content: [{ type: "text", text: `[${mode}]\n\n${output}` }],
|
|
64
|
+
details: undefined,
|
|
61
65
|
};
|
|
62
|
-
|
|
63
66
|
} catch (error) {
|
|
64
67
|
return {
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
content: [{ type: "text", text: `ast-rewrite execution error: ${(error as Error).message}` }],
|
|
69
|
+
isError: true,
|
|
70
|
+
details: undefined,
|
|
67
71
|
};
|
|
68
72
|
}
|
|
69
73
|
},
|
package/src/tools/ast-search.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import { Type } from "@sinclair/typebox";
|
|
2
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
3
|
-
import {
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import { exec } from "../utils/exec.js";
|
|
4
4
|
|
|
5
5
|
const MAX_OUTPUT_LENGTH = 10000;
|
|
6
6
|
|
|
7
|
+
interface SgMatch {
|
|
8
|
+
file: string;
|
|
9
|
+
range: {
|
|
10
|
+
start: { line: number };
|
|
11
|
+
end: { line: number };
|
|
12
|
+
};
|
|
13
|
+
text: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
export function registerAstSearch(pi: ExtensionAPI): void {
|
|
8
17
|
pi.registerTool({
|
|
9
18
|
name: "ast_search",
|
|
@@ -25,42 +34,49 @@ export function registerAstSearch(pi: ExtensionAPI): void {
|
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
const { exitCode, stdout, stderr } = await exec(args);
|
|
28
|
-
|
|
37
|
+
|
|
29
38
|
if (exitCode !== 0) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
// sg returns non-zero if no matches? No, usually 0 even if no matches.
|
|
40
|
+
// But if it fails to parse pattern, it returns non-zero.
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: `ast-search failed: ${stderr}` }],
|
|
43
|
+
isError: true,
|
|
44
|
+
details: undefined,
|
|
45
|
+
};
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
if (!stdout.trim()) {
|
|
39
49
|
return {
|
|
40
50
|
content: [{ type: "text", text: "No matches found." }],
|
|
51
|
+
details: undefined,
|
|
41
52
|
};
|
|
42
53
|
}
|
|
43
54
|
|
|
44
|
-
let results;
|
|
55
|
+
let results: SgMatch[];
|
|
45
56
|
try {
|
|
46
57
|
results = JSON.parse(stdout);
|
|
47
58
|
} catch (e) {
|
|
48
59
|
return {
|
|
49
|
-
content: [
|
|
50
|
-
|
|
60
|
+
content: [
|
|
61
|
+
{ type: "text", text: `Failed to parse ast-search output: ${(e as Error).message}\nOutput: ${stdout}` },
|
|
62
|
+
],
|
|
63
|
+
isError: true,
|
|
64
|
+
details: undefined,
|
|
51
65
|
};
|
|
52
66
|
}
|
|
53
67
|
|
|
54
68
|
if (!Array.isArray(results)) {
|
|
55
|
-
|
|
69
|
+
return {
|
|
56
70
|
content: [{ type: "text", text: `Unexpected output format from ast-search: ${stdout}` }],
|
|
57
|
-
isError: true
|
|
71
|
+
isError: true,
|
|
72
|
+
details: undefined,
|
|
58
73
|
};
|
|
59
74
|
}
|
|
60
75
|
|
|
61
76
|
if (results.length === 0) {
|
|
62
77
|
return {
|
|
63
78
|
content: [{ type: "text", text: "No matches found." }],
|
|
79
|
+
details: undefined,
|
|
64
80
|
};
|
|
65
81
|
}
|
|
66
82
|
|
|
@@ -70,22 +86,23 @@ export function registerAstSearch(pi: ExtensionAPI): void {
|
|
|
70
86
|
const startLine = match.range.start.line + 1;
|
|
71
87
|
const endLine = match.range.end.line + 1;
|
|
72
88
|
const text = match.text;
|
|
73
|
-
|
|
89
|
+
|
|
74
90
|
output += `${file}:${startLine}-${endLine}:\n${text}\n\n`;
|
|
75
91
|
}
|
|
76
|
-
|
|
92
|
+
|
|
77
93
|
if (output.length > MAX_OUTPUT_LENGTH) {
|
|
78
|
-
output = output.substring(0, MAX_OUTPUT_LENGTH)
|
|
94
|
+
output = `${output.substring(0, MAX_OUTPUT_LENGTH)}\n... (truncated)`;
|
|
79
95
|
}
|
|
80
96
|
|
|
81
97
|
return {
|
|
82
98
|
content: [{ type: "text", text: output.trim() }],
|
|
99
|
+
details: undefined,
|
|
83
100
|
};
|
|
84
|
-
|
|
85
101
|
} catch (error) {
|
|
86
102
|
return {
|
|
87
|
-
|
|
88
|
-
|
|
103
|
+
content: [{ type: "text", text: `ast-search execution error: ${(error as Error).message}` }],
|
|
104
|
+
isError: true,
|
|
105
|
+
details: undefined,
|
|
89
106
|
};
|
|
90
107
|
}
|
|
91
108
|
},
|
package/src/utils/exec.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
2
3
|
|
|
3
4
|
export interface ExecResult {
|
|
4
5
|
stdout: string;
|
|
@@ -6,20 +7,61 @@ export interface ExecResult {
|
|
|
6
7
|
exitCode: number;
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
interface BunProcess {
|
|
11
|
+
exited: Promise<number>;
|
|
12
|
+
stdout: ReadableStream<Uint8Array>;
|
|
13
|
+
stderr: ReadableStream<Uint8Array>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface BunInterface {
|
|
17
|
+
spawn: (
|
|
18
|
+
command: string[],
|
|
19
|
+
options: {
|
|
20
|
+
cwd?: string;
|
|
21
|
+
env?: Record<string, string | undefined>;
|
|
22
|
+
stdout?: "pipe";
|
|
23
|
+
stderr?: "pipe";
|
|
24
|
+
},
|
|
25
|
+
) => BunProcess;
|
|
26
|
+
}
|
|
27
|
+
|
|
9
28
|
export async function exec(command: string[], options: { cwd?: string } = {}): Promise<ExecResult> {
|
|
29
|
+
// Add node_modules/.bin to PATH
|
|
30
|
+
const env = { ...process.env };
|
|
31
|
+
const cwd = options.cwd ?? process.cwd();
|
|
32
|
+
|
|
33
|
+
// Try to find the closest node_modules/.bin
|
|
34
|
+
let currentDir = cwd;
|
|
35
|
+
const binPaths: string[] = [];
|
|
36
|
+
while (true) {
|
|
37
|
+
binPaths.push(join(currentDir, "node_modules", ".bin"));
|
|
38
|
+
const parentDir = resolve(currentDir, "..");
|
|
39
|
+
if (parentDir === currentDir) break;
|
|
40
|
+
currentDir = parentDir;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const pathSeparator = process.platform === "win32" ? ";" : ":";
|
|
44
|
+
const existingPath = env.PATH ?? "";
|
|
45
|
+
env.PATH = [existingPath, ...binPaths].filter((value) => value && value.length > 0).join(pathSeparator);
|
|
46
|
+
|
|
10
47
|
// Try Bun first
|
|
11
|
-
const bun = (globalThis as
|
|
48
|
+
const bun = (globalThis as Record<string, unknown>).Bun as BunInterface | undefined;
|
|
12
49
|
if (bun && typeof bun.spawn === "function") {
|
|
13
|
-
return execBun(bun, command, options);
|
|
50
|
+
return execBun(bun, command, { ...options, env });
|
|
14
51
|
}
|
|
15
52
|
|
|
16
53
|
// Fallback to Node
|
|
17
|
-
return execNode(command, options);
|
|
54
|
+
return execNode(command, { ...options, env });
|
|
18
55
|
}
|
|
19
56
|
|
|
20
|
-
async function execBun(
|
|
57
|
+
async function execBun(
|
|
58
|
+
bun: BunInterface,
|
|
59
|
+
command: string[],
|
|
60
|
+
options: { cwd?: string; env?: Record<string, string | undefined> },
|
|
61
|
+
): Promise<ExecResult> {
|
|
21
62
|
const proc = bun.spawn(command, {
|
|
22
63
|
cwd: options.cwd,
|
|
64
|
+
env: options.env,
|
|
23
65
|
stdout: "pipe",
|
|
24
66
|
stderr: "pipe",
|
|
25
67
|
});
|
|
@@ -31,11 +73,15 @@ async function execBun(bun: any, command: string[], options: { cwd?: string }):
|
|
|
31
73
|
return { stdout, stderr, exitCode };
|
|
32
74
|
}
|
|
33
75
|
|
|
34
|
-
function execNode(
|
|
76
|
+
function execNode(
|
|
77
|
+
command: string[],
|
|
78
|
+
options: { cwd?: string; env?: Record<string, string | undefined> },
|
|
79
|
+
): Promise<ExecResult> {
|
|
35
80
|
return new Promise((resolve, reject) => {
|
|
36
81
|
const [cmd, ...args] = command;
|
|
37
82
|
const child = spawn(cmd, args, {
|
|
38
83
|
cwd: options.cwd,
|
|
84
|
+
env: options.env,
|
|
39
85
|
stdio: ["ignore", "pipe", "pipe"],
|
|
40
86
|
});
|
|
41
87
|
|
package/test/fixtures/example.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
function
|
|
2
|
-
|
|
1
|
+
function _hello(name: string) {
|
|
2
|
+
console.log(`Hello, ${name}`);
|
|
3
3
|
}
|
package/test/integration.test.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { ExtensionAPI, ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { registerAstRewrite } from "../src/tools/ast-rewrite.js";
|
|
5
|
+
import { registerAstSearch } from "../src/tools/ast-search.js";
|
|
6
6
|
|
|
7
7
|
// Using Bun-compatible dirname
|
|
8
|
-
const fixturesDir = join(import.meta.dirname,
|
|
8
|
+
const fixturesDir = join(import.meta.dirname, "fixtures");
|
|
9
9
|
|
|
10
10
|
// Mock ExtensionAPI
|
|
11
11
|
const createMockPi = () => {
|
|
@@ -14,89 +14,95 @@ const createMockPi = () => {
|
|
|
14
14
|
registerTool: (tool: ToolDefinition) => {
|
|
15
15
|
tools[tool.name] = tool;
|
|
16
16
|
},
|
|
17
|
-
tools
|
|
17
|
+
tools,
|
|
18
18
|
};
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
describe(
|
|
21
|
+
describe("AST Extension Integration", () => {
|
|
22
22
|
let mockPi: ReturnType<typeof createMockPi>;
|
|
23
23
|
|
|
24
24
|
beforeEach(() => {
|
|
25
25
|
mockPi = createMockPi();
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
describe(
|
|
29
|
-
it(
|
|
28
|
+
describe("ast_search", () => {
|
|
29
|
+
it("should find patterns in TypeScript", async () => {
|
|
30
30
|
// Register tool
|
|
31
31
|
registerAstSearch(mockPi as unknown as ExtensionAPI);
|
|
32
|
-
const tool = mockPi.tools
|
|
32
|
+
const tool = mockPi.tools.ast_search;
|
|
33
33
|
expect(tool).toBeDefined();
|
|
34
34
|
|
|
35
35
|
// Execute tool
|
|
36
36
|
const params = {
|
|
37
|
-
pattern:
|
|
38
|
-
path: join(fixturesDir,
|
|
39
|
-
lang:
|
|
37
|
+
pattern: "console.log($A)",
|
|
38
|
+
path: join(fixturesDir, "example.ts"),
|
|
39
|
+
lang: "typescript",
|
|
40
40
|
};
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
// Mock context and other args
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking tool result
|
|
44
|
+
const result = (await tool.execute("test-id", params, undefined, undefined, {} as unknown as any)) as any;
|
|
45
|
+
|
|
46
|
+
expect(result.isError, result.content[0]?.type === "text" ? result.content[0].text : undefined).toBeFalsy();
|
|
47
|
+
|
|
48
|
+
const content = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
48
49
|
// Should verify output format
|
|
49
|
-
expect(content).toContain(
|
|
50
|
+
expect(content).toContain("example.ts");
|
|
50
51
|
// AST grep output usually includes line numbers
|
|
51
52
|
// e.g. "example.ts:2-2:\n console.log(\"Hello, \" + name);"
|
|
52
|
-
|
|
53
|
+
|
|
53
54
|
// Just verify matched text is present
|
|
54
|
-
|
|
55
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing for literal string in output
|
|
56
|
+
expect(content).toContain("console.log(`Hello, ${name}`)");
|
|
55
57
|
});
|
|
56
58
|
|
|
57
|
-
it(
|
|
59
|
+
it("should find patterns in Rust", async () => {
|
|
58
60
|
registerAstSearch(mockPi as unknown as ExtensionAPI);
|
|
59
|
-
const tool = mockPi.tools
|
|
60
|
-
|
|
61
|
+
const tool = mockPi.tools.ast_search;
|
|
62
|
+
|
|
61
63
|
const params = {
|
|
62
|
-
pattern:
|
|
63
|
-
path: join(fixturesDir,
|
|
64
|
-
lang:
|
|
64
|
+
pattern: "println!($A, $B)",
|
|
65
|
+
path: join(fixturesDir, "example.rs"),
|
|
66
|
+
lang: "rust",
|
|
65
67
|
};
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
|
|
69
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking tool result
|
|
70
|
+
const result = (await tool.execute("test-id", params, undefined, undefined, {} as unknown as any)) as any;
|
|
71
|
+
|
|
72
|
+
expect(result.isError, result.content[0]?.type === "text" ? result.content[0].text : undefined).toBeFalsy();
|
|
73
|
+
const content = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
71
74
|
expect(content).toContain('println!("Hello, {}", name)');
|
|
72
75
|
});
|
|
73
76
|
});
|
|
74
77
|
|
|
75
|
-
describe(
|
|
76
|
-
it(
|
|
78
|
+
describe("ast_rewrite", () => {
|
|
79
|
+
it("should dry-run rewrite in TypeScript", async () => {
|
|
77
80
|
registerAstRewrite(mockPi as unknown as ExtensionAPI);
|
|
78
|
-
const tool = mockPi.tools
|
|
81
|
+
const tool = mockPi.tools.ast_rewrite;
|
|
79
82
|
expect(tool).toBeDefined();
|
|
80
83
|
|
|
81
84
|
const params = {
|
|
82
|
-
pattern:
|
|
83
|
-
rewrite:
|
|
84
|
-
path: join(fixturesDir,
|
|
85
|
-
lang:
|
|
86
|
-
apply: false
|
|
85
|
+
pattern: "console.log($A)",
|
|
86
|
+
rewrite: "logger.info($A)",
|
|
87
|
+
path: join(fixturesDir, "example.ts"),
|
|
88
|
+
lang: "typescript",
|
|
89
|
+
apply: false,
|
|
87
90
|
};
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
|
|
92
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking tool result
|
|
93
|
+
const result = (await tool.execute("test-id", params, undefined, undefined, {} as unknown as any)) as any;
|
|
94
|
+
|
|
95
|
+
expect(result.isError, result.content[0]?.type === "text" ? result.content[0].text : undefined).toBeFalsy();
|
|
96
|
+
const content = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
97
|
+
|
|
98
|
+
expect(content).toContain("[DRY-RUN (preview)]");
|
|
95
99
|
// Verify diff output
|
|
96
100
|
// sg output usually has @@ ... @@
|
|
97
101
|
// and -old +new lines
|
|
98
|
-
|
|
99
|
-
expect(content).toContain(
|
|
102
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing for literal string in output
|
|
103
|
+
expect(content).toContain("console.log(`Hello, ${name}`);");
|
|
104
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing for literal string in output
|
|
105
|
+
expect(content).toContain("logger.info(`Hello, ${name}`);");
|
|
100
106
|
});
|
|
101
107
|
});
|
|
102
108
|
});
|