@wrongstack/tools 0.1.1 → 0.1.3
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/dist/audit.d.ts +25 -0
- package/dist/audit.js +209 -0
- package/dist/audit.js.map +1 -0
- package/dist/bash.d.ts +16 -0
- package/dist/bash.js +180 -0
- package/dist/bash.js.map +1 -0
- package/dist/batch-tool-use.d.ts +26 -0
- package/dist/batch-tool-use.js +106 -0
- package/dist/batch-tool-use.js.map +1 -0
- package/dist/builtin.d.ts +5 -0
- package/dist/builtin.js +3735 -0
- package/dist/builtin.js.map +1 -0
- package/dist/diff.d.ts +20 -0
- package/dist/diff.js +142 -0
- package/dist/diff.js.map +1 -0
- package/dist/document.d.ts +27 -0
- package/dist/document.js +148 -0
- package/dist/document.js.map +1 -0
- package/dist/edit.d.ts +22 -0
- package/dist/edit.js +138 -0
- package/dist/edit.js.map +1 -0
- package/dist/exec.d.ts +21 -0
- package/dist/exec.js +159 -0
- package/dist/exec.js.map +1 -0
- package/dist/fetch.d.ts +15 -0
- package/dist/fetch.js +213 -0
- package/dist/fetch.js.map +1 -0
- package/dist/format.d.ts +18 -0
- package/dist/format.js +194 -0
- package/dist/format.js.map +1 -0
- package/dist/git.d.ts +27 -0
- package/dist/git.js +174 -0
- package/dist/git.js.map +1 -0
- package/dist/glob.d.ts +14 -0
- package/dist/glob.js +101 -0
- package/dist/glob.js.map +1 -0
- package/dist/grep.d.ts +20 -0
- package/dist/grep.js +264 -0
- package/dist/grep.js.map +1 -0
- package/dist/index.d.ts +34 -563
- package/dist/index.js +717 -442
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +19 -0
- package/dist/install.js +186 -0
- package/dist/install.js.map +1 -0
- package/dist/json.d.ts +20 -0
- package/dist/json.js +124 -0
- package/dist/json.js.map +1 -0
- package/dist/lint.d.ts +20 -0
- package/dist/lint.js +191 -0
- package/dist/lint.js.map +1 -0
- package/dist/logs.d.ts +27 -0
- package/dist/logs.js +180 -0
- package/dist/logs.js.map +1 -0
- package/dist/memory.d.ts +22 -0
- package/dist/memory.js +53 -0
- package/dist/memory.js.map +1 -0
- package/dist/mode.d.ts +20 -0
- package/dist/mode.js +81 -0
- package/dist/mode.js.map +1 -0
- package/dist/outdated.d.ts +26 -0
- package/dist/outdated.js +138 -0
- package/dist/outdated.js.map +1 -0
- package/dist/patch.d.ts +18 -0
- package/dist/patch.js +101 -0
- package/dist/patch.js.map +1 -0
- package/dist/read.d.ts +16 -0
- package/dist/read.js +81 -0
- package/dist/read.js.map +1 -0
- package/dist/replace.d.ts +23 -0
- package/dist/replace.js +196 -0
- package/dist/replace.js.map +1 -0
- package/dist/scaffold.d.ts +20 -0
- package/dist/scaffold.js +185 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/search.d.ts +20 -0
- package/dist/search.js +212 -0
- package/dist/search.js.map +1 -0
- package/dist/test.d.ts +24 -0
- package/dist/test.js +247 -0
- package/dist/test.js.map +1 -0
- package/dist/todo.d.ts +12 -0
- package/dist/todo.js +53 -0
- package/dist/todo.js.map +1 -0
- package/dist/tool-help.d.ts +23 -0
- package/dist/tool-help.js +122 -0
- package/dist/tool-help.js.map +1 -0
- package/dist/tool-search.d.ts +22 -0
- package/dist/tool-search.js +70 -0
- package/dist/tool-search.js.map +1 -0
- package/dist/tool-use.d.ts +16 -0
- package/dist/tool-use.js +79 -0
- package/dist/tool-use.js.map +1 -0
- package/dist/tree.d.ts +21 -0
- package/dist/tree.js +176 -0
- package/dist/tree.js.map +1 -0
- package/dist/typecheck.d.ts +19 -0
- package/dist/typecheck.js +181 -0
- package/dist/typecheck.js.map +1 -0
- package/dist/write.d.ts +15 -0
- package/dist/write.js +77 -0
- package/dist/write.js.map +1 -0
- package/package.json +137 -4
package/dist/patch.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
|
|
5
|
+
// src/patch.ts
|
|
6
|
+
function resolvePath(input, ctx) {
|
|
7
|
+
return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);
|
|
8
|
+
}
|
|
9
|
+
function ensureInsideRoot(absPath, ctx) {
|
|
10
|
+
const root = path.resolve(ctx.projectRoot);
|
|
11
|
+
const target = path.resolve(absPath);
|
|
12
|
+
const rel = path.relative(root, target);
|
|
13
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
14
|
+
throw new Error(`Path "${absPath}" is outside project root "${root}"`);
|
|
15
|
+
}
|
|
16
|
+
return target;
|
|
17
|
+
}
|
|
18
|
+
function safeResolve(input, ctx) {
|
|
19
|
+
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/patch.ts
|
|
23
|
+
var patchTool = {
|
|
24
|
+
name: "patch",
|
|
25
|
+
description: "Apply a unified diff patch to files. Writes .orig and .rej files on failure.",
|
|
26
|
+
usageHint: "Set `patch` (the diff text). `directory` defaults to cwd. `strip` removes leading path components. `dry_run` previews.",
|
|
27
|
+
permission: "confirm",
|
|
28
|
+
mutating: true,
|
|
29
|
+
timeoutMs: 3e4,
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
patch: { type: "string", description: "Unified diff patch content" },
|
|
34
|
+
directory: { type: "string", description: "Root directory for patch (default: cwd)" },
|
|
35
|
+
strip: { type: "integer", description: "Strip leading path components (default: 1)" },
|
|
36
|
+
dry_run: { type: "boolean", description: "Preview without applying" }
|
|
37
|
+
},
|
|
38
|
+
required: ["patch"]
|
|
39
|
+
},
|
|
40
|
+
async execute(input, ctx, opts) {
|
|
41
|
+
if (!input?.patch) throw new Error("patch: patch content is required");
|
|
42
|
+
const dir = input.directory ? safeResolve(input.directory, ctx) : ctx.cwd;
|
|
43
|
+
const strip = input.strip ?? 1;
|
|
44
|
+
const dryRun = input.dry_run ?? false;
|
|
45
|
+
const patchFile = path.join(dir, `.wstack_patch_${Date.now()}.diff`);
|
|
46
|
+
await fs.writeFile(patchFile, input.patch, "utf8");
|
|
47
|
+
const args = [
|
|
48
|
+
"-p" + strip,
|
|
49
|
+
"--merge",
|
|
50
|
+
...dryRun ? ["--dry-run"] : [],
|
|
51
|
+
"-i",
|
|
52
|
+
patchFile
|
|
53
|
+
];
|
|
54
|
+
const result = await runPatch(args, dir, opts.signal);
|
|
55
|
+
await fs.unlink(patchFile).catch(() => {
|
|
56
|
+
});
|
|
57
|
+
if (result.exitCode !== 0 && !dryRun) {
|
|
58
|
+
return {
|
|
59
|
+
applied: 0,
|
|
60
|
+
rejected: 1,
|
|
61
|
+
files: [],
|
|
62
|
+
dry_run: dryRun,
|
|
63
|
+
message: `patch failed: ${result.stderr || result.stdout}`
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
applied: result.stdout.includes("patching file") ? 1 : 0,
|
|
68
|
+
rejected: 0,
|
|
69
|
+
files: extractPatchedFiles(result.stdout),
|
|
70
|
+
dry_run: dryRun,
|
|
71
|
+
message: result.stdout || "patch applied"
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
function runPatch(args, cwd, signal) {
|
|
76
|
+
return new Promise((resolve2) => {
|
|
77
|
+
let stdout = "";
|
|
78
|
+
let stderr = "";
|
|
79
|
+
const child = spawn("patch", args, { cwd, signal, stdio: ["pipe", "pipe", "pipe"] });
|
|
80
|
+
child.stdout?.on("data", (c) => {
|
|
81
|
+
stdout += c.toString();
|
|
82
|
+
});
|
|
83
|
+
child.stderr?.on("data", (c) => {
|
|
84
|
+
stderr += c.toString();
|
|
85
|
+
});
|
|
86
|
+
child.on("close", (code) => resolve2({ exitCode: code ?? 1, stdout, stderr }));
|
|
87
|
+
child.on("error", (e) => resolve2({ exitCode: 1, stdout: "", stderr: e.message }));
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function extractPatchedFiles(output) {
|
|
91
|
+
const files = [];
|
|
92
|
+
const re = /patching file (.+)/gi;
|
|
93
|
+
for (const m of output.matchAll(re)) {
|
|
94
|
+
if (m[1]) files.push(m[1]);
|
|
95
|
+
}
|
|
96
|
+
return files;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { patchTool };
|
|
100
|
+
//# sourceMappingURL=patch.js.map
|
|
101
|
+
//# sourceMappingURL=patch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/_util.ts","../src/patch.ts"],"names":["path2","resolve","spawn"],"mappings":"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","file":"patch.js","sourcesContent":["import * as path from 'node:path';\r\nimport { spawn } from 'node:child_process';\r\nimport type { Context, ToolProgressEvent } from '@wrongstack/core';\r\n\r\nexport function resolvePath(input: string, ctx: Context): string {\r\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\r\n}\r\n\r\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\r\n const root = path.resolve(ctx.projectRoot);\r\n const target = path.resolve(absPath);\r\n const rel = path.relative(root, target);\r\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\r\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\r\n }\r\n return target;\r\n}\r\n\r\nexport function safeResolve(input: string, ctx: Context): string {\r\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\r\n}\r\n\r\nexport function truncateMiddle(s: string, max: number): string {\r\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\r\n const half = Math.floor(max / 2);\r\n return (\r\n s.slice(0, half) +\r\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\r\n s.slice(-half)\r\n );\r\n}\r\n\r\nexport function isBinaryBuffer(buf: Buffer): boolean {\r\n const len = Math.min(buf.length, 8192);\r\n for (let i = 0; i < len; i++) {\r\n if (buf[i] === 0) return true;\r\n }\r\n return false;\r\n}\r\n\r\nexport interface SpawnStreamResult {\r\n stdout: string;\r\n stderr: string;\r\n exitCode: number;\r\n truncated: boolean;\r\n error?: string;\r\n}\r\n\r\nexport interface SpawnStreamOptions {\r\n cmd: string;\r\n args: string[];\r\n cwd: string;\r\n signal: AbortSignal;\r\n maxBytes?: number;\r\n /** Bytes of new stdout/stderr to accumulate before yielding a `partial_output` event. */\r\n flushBytes?: number;\r\n}\r\n\r\n/**\r\n * Spawn a child process and yield `partial_output` progress events as\r\n * stdout/stderr arrive (batched by byte threshold), then return the full\r\n * buffered result. Shared between install/lint/format/typecheck/test/audit\r\n * so the TUI live tail sees consistent progress regardless of which tool\r\n * is running.\r\n */\r\nexport async function* spawnStream(\r\n opts: SpawnStreamOptions,\r\n): AsyncGenerator<ToolProgressEvent, SpawnStreamResult> {\r\n const max = opts.maxBytes ?? 200_000;\r\n const flushAt = opts.flushBytes ?? 4 * 1024;\r\n let stdout = '';\r\n let stderr = '';\r\n let pending = '';\r\n let error: string | undefined;\r\n\r\n const child = spawn(opts.cmd, opts.args, {\r\n cwd: opts.cwd,\r\n signal: opts.signal,\r\n stdio: ['ignore', 'pipe', 'pipe'],\r\n });\r\n\r\n type Chunk = { kind: 'out' | 'err' | 'close' | 'error'; data: string; code?: number };\r\n const queue: Chunk[] = [];\r\n let waiter: (() => void) | undefined;\r\n const wake = () => {\r\n if (waiter) {\r\n const w = waiter;\r\n waiter = undefined;\r\n w();\r\n }\r\n };\r\n\r\n child.stdout?.on('data', (c) => {\r\n const s = c.toString();\r\n if (stdout.length < max) stdout += s;\r\n queue.push({ kind: 'out', data: s });\r\n wake();\r\n });\r\n child.stderr?.on('data', (c) => {\r\n const s = c.toString();\r\n if (stderr.length < max) stderr += s;\r\n queue.push({ kind: 'err', data: s });\r\n wake();\r\n });\r\n child.on('error', (e) => {\r\n error = e.message;\r\n queue.push({ kind: 'error', data: e.message });\r\n wake();\r\n });\r\n child.on('close', (code) => {\r\n queue.push({ kind: 'close', data: '', code: code ?? 0 });\r\n wake();\r\n });\r\n\r\n let exitCode = 0;\r\n let spawnFailed = false;\r\n for (;;) {\r\n while (queue.length === 0) {\r\n await new Promise<void>((resolve) => {\r\n waiter = resolve;\r\n });\r\n }\r\n const chunk = queue.shift()!;\r\n if (chunk.kind === 'close') {\r\n // If we already saw a spawn error (ENOENT etc.), keep exitCode=1\r\n // rather than the negative platform code Node fabricates.\r\n if (!spawnFailed) exitCode = chunk.code ?? 0;\r\n break;\r\n }\r\n if (chunk.kind === 'error') {\r\n spawnFailed = true;\r\n exitCode = 1;\r\n // close usually follows\r\n continue;\r\n }\r\n pending += chunk.data;\r\n if (pending.length >= flushAt) {\r\n yield { type: 'partial_output', text: pending };\r\n pending = '';\r\n }\r\n }\r\n if (pending.length > 0) {\r\n yield { type: 'partial_output', text: pending };\r\n }\r\n\r\n return {\r\n stdout,\r\n stderr,\r\n exitCode,\r\n truncated: stdout.length >= max || stderr.length >= max,\r\n error,\r\n };\r\n}\r\n","import * as fs from 'node:fs/promises';\r\nimport * as path from 'node:path';\r\nimport { spawn } from 'node:child_process';\r\nimport type { Tool } from '@wrongstack/core';\r\nimport { safeResolve } from './_util.js';\r\n\r\ninterface PatchInput {\r\n patch: string;\r\n directory?: string;\r\n strip?: number;\r\n dry_run?: boolean;\r\n}\r\n\r\ninterface PatchOutput {\r\n applied: number;\r\n rejected: number;\r\n files: string[];\r\n dry_run: boolean;\r\n message: string;\r\n}\r\n\r\nexport const patchTool: Tool<PatchInput, PatchOutput> = {\r\n name: 'patch',\r\n description:\r\n 'Apply a unified diff patch to files. Writes .orig and .rej files on failure.',\r\n usageHint:\r\n 'Set `patch` (the diff text). `directory` defaults to cwd. `strip` removes leading path components. `dry_run` previews.',\r\n permission: 'confirm',\r\n mutating: true,\r\n timeoutMs: 30_000,\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n patch: { type: 'string', description: 'Unified diff patch content' },\r\n directory: { type: 'string', description: 'Root directory for patch (default: cwd)' },\r\n strip: { type: 'integer', description: 'Strip leading path components (default: 1)' },\r\n dry_run: { type: 'boolean', description: 'Preview without applying' },\r\n },\r\n required: ['patch'],\r\n },\r\n async execute(input, ctx, opts) {\r\n if (!input?.patch) throw new Error('patch: patch content is required');\r\n\r\n const dir = input.directory ? safeResolve(input.directory, ctx) : ctx.cwd;\r\n const strip = input.strip ?? 1;\r\n const dryRun = input.dry_run ?? false;\r\n\r\n const patchFile = path.join(dir, `.wstack_patch_${Date.now()}.diff`);\r\n await fs.writeFile(patchFile, input.patch, 'utf8');\r\n\r\n const args = [\r\n '-p' + strip,\r\n '--merge',\r\n ...(dryRun ? ['--dry-run'] : []),\r\n '-i', patchFile,\r\n ];\r\n\r\n const result = await runPatch(args, dir, opts.signal);\r\n await fs.unlink(patchFile).catch(() => {});\r\n\r\n if (result.exitCode !== 0 && !dryRun) {\r\n return {\r\n applied: 0,\r\n rejected: 1,\r\n files: [],\r\n dry_run: dryRun,\r\n message: `patch failed: ${result.stderr || result.stdout}`,\r\n };\r\n }\r\n\r\n return {\r\n applied: result.stdout.includes('patching file') ? 1 : 0,\r\n rejected: 0,\r\n files: extractPatchedFiles(result.stdout),\r\n dry_run: dryRun,\r\n message: result.stdout || 'patch applied',\r\n };\r\n },\r\n};\r\n\r\nfunction runPatch(args: string[], cwd: string, signal: AbortSignal): Promise<{ exitCode: number; stdout: string; stderr: string }> {\r\n return new Promise((resolve) => {\r\n let stdout = '';\r\n let stderr = '';\r\n\r\n const child = spawn('patch', args, { cwd, signal, stdio: ['pipe', 'pipe', 'pipe'] });\r\n child.stdout?.on('data', (c) => { stdout += c.toString(); });\r\n child.stderr?.on('data', (c) => { stderr += c.toString(); });\r\n child.on('close', (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));\r\n child.on('error', (e) => resolve({ exitCode: 1, stdout: '', stderr: e.message }));\r\n });\r\n}\r\n\r\nfunction extractPatchedFiles(output: string): string[] {\r\n const files: string[] = [];\r\n const re = /patching file (.+)/gi;\r\n for (const m of output.matchAll(re)) {\r\n if (m[1]) files.push(m[1]);\r\n }\r\n return files;\r\n}"]}
|
package/dist/read.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Tool } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
interface ReadInput {
|
|
4
|
+
path: string;
|
|
5
|
+
offset?: number;
|
|
6
|
+
limit?: number;
|
|
7
|
+
}
|
|
8
|
+
interface ReadOutput {
|
|
9
|
+
text: string;
|
|
10
|
+
total_lines: number;
|
|
11
|
+
encoding: string;
|
|
12
|
+
truncated: boolean;
|
|
13
|
+
}
|
|
14
|
+
declare const readTool: Tool<ReadInput, ReadOutput>;
|
|
15
|
+
|
|
16
|
+
export { readTool };
|
package/dist/read.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import 'child_process';
|
|
4
|
+
|
|
5
|
+
// src/read.ts
|
|
6
|
+
function resolvePath(input, ctx) {
|
|
7
|
+
return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);
|
|
8
|
+
}
|
|
9
|
+
function ensureInsideRoot(absPath, ctx) {
|
|
10
|
+
const root = path.resolve(ctx.projectRoot);
|
|
11
|
+
const target = path.resolve(absPath);
|
|
12
|
+
const rel = path.relative(root, target);
|
|
13
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
14
|
+
throw new Error(`Path "${absPath}" is outside project root "${root}"`);
|
|
15
|
+
}
|
|
16
|
+
return target;
|
|
17
|
+
}
|
|
18
|
+
function safeResolve(input, ctx) {
|
|
19
|
+
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
20
|
+
}
|
|
21
|
+
function isBinaryBuffer(buf) {
|
|
22
|
+
const len = Math.min(buf.length, 8192);
|
|
23
|
+
for (let i = 0; i < len; i++) {
|
|
24
|
+
if (buf[i] === 0) return true;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/read.ts
|
|
30
|
+
var MAX_BYTES = 5 * 1024 * 1024;
|
|
31
|
+
var readTool = {
|
|
32
|
+
name: "read",
|
|
33
|
+
description: "Read the contents of a file. Lines are 1-indexed and prefixed with line numbers.",
|
|
34
|
+
usageHint: "Read a file before editing it. Returns lines numbered like ` 1\u2192content`. Use `offset` and `limit` for large files (default reads up to 2000 lines).",
|
|
35
|
+
permission: "auto",
|
|
36
|
+
mutating: false,
|
|
37
|
+
maxOutputBytes: 262144,
|
|
38
|
+
timeoutMs: 5e3,
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
path: { type: "string", description: "File path (absolute or relative to cwd)" },
|
|
43
|
+
offset: { type: "integer", description: "1-based line number to start from" },
|
|
44
|
+
limit: { type: "integer", description: "Max lines to read (default 2000)" }
|
|
45
|
+
},
|
|
46
|
+
required: ["path"]
|
|
47
|
+
},
|
|
48
|
+
async execute(input, ctx) {
|
|
49
|
+
if (!input?.path) throw new Error("read: path is required");
|
|
50
|
+
const absPath = safeResolve(input.path, ctx);
|
|
51
|
+
const stat2 = await fs.stat(absPath);
|
|
52
|
+
if (!stat2.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
|
|
53
|
+
if (stat2.size > MAX_BYTES) {
|
|
54
|
+
throw new Error(`read: file too large (${stat2.size} bytes, limit ${MAX_BYTES})`);
|
|
55
|
+
}
|
|
56
|
+
const buf = await fs.readFile(absPath);
|
|
57
|
+
if (isBinaryBuffer(buf)) {
|
|
58
|
+
throw new Error(`read: "${input.path}" appears to be binary`);
|
|
59
|
+
}
|
|
60
|
+
const text = buf.toString("utf8");
|
|
61
|
+
const allLines = text.split(/\r\n|\r|\n/);
|
|
62
|
+
const total = allLines.length;
|
|
63
|
+
const offset = Math.max(1, input.offset ?? 1);
|
|
64
|
+
const limit = Math.max(1, Math.min(input.limit ?? 2e3, 5e3));
|
|
65
|
+
const slice = allLines.slice(offset - 1, offset - 1 + limit);
|
|
66
|
+
const truncated = offset - 1 + slice.length < total;
|
|
67
|
+
const width = String(offset + slice.length - 1).length;
|
|
68
|
+
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
69
|
+
ctx.recordRead(absPath, stat2.mtimeMs);
|
|
70
|
+
return {
|
|
71
|
+
text: numbered,
|
|
72
|
+
total_lines: total,
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
truncated
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export { readTool };
|
|
80
|
+
//# sourceMappingURL=read.js.map
|
|
81
|
+
//# sourceMappingURL=read.js.map
|
package/dist/read.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/_util.ts","../src/read.ts"],"names":["stat"],"mappings":"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","file":"read.js","sourcesContent":["import * as path from 'node:path';\r\nimport { spawn } from 'node:child_process';\r\nimport type { Context, ToolProgressEvent } from '@wrongstack/core';\r\n\r\nexport function resolvePath(input: string, ctx: Context): string {\r\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\r\n}\r\n\r\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\r\n const root = path.resolve(ctx.projectRoot);\r\n const target = path.resolve(absPath);\r\n const rel = path.relative(root, target);\r\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\r\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\r\n }\r\n return target;\r\n}\r\n\r\nexport function safeResolve(input: string, ctx: Context): string {\r\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\r\n}\r\n\r\nexport function truncateMiddle(s: string, max: number): string {\r\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\r\n const half = Math.floor(max / 2);\r\n return (\r\n s.slice(0, half) +\r\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\r\n s.slice(-half)\r\n );\r\n}\r\n\r\nexport function isBinaryBuffer(buf: Buffer): boolean {\r\n const len = Math.min(buf.length, 8192);\r\n for (let i = 0; i < len; i++) {\r\n if (buf[i] === 0) return true;\r\n }\r\n return false;\r\n}\r\n\r\nexport interface SpawnStreamResult {\r\n stdout: string;\r\n stderr: string;\r\n exitCode: number;\r\n truncated: boolean;\r\n error?: string;\r\n}\r\n\r\nexport interface SpawnStreamOptions {\r\n cmd: string;\r\n args: string[];\r\n cwd: string;\r\n signal: AbortSignal;\r\n maxBytes?: number;\r\n /** Bytes of new stdout/stderr to accumulate before yielding a `partial_output` event. */\r\n flushBytes?: number;\r\n}\r\n\r\n/**\r\n * Spawn a child process and yield `partial_output` progress events as\r\n * stdout/stderr arrive (batched by byte threshold), then return the full\r\n * buffered result. Shared between install/lint/format/typecheck/test/audit\r\n * so the TUI live tail sees consistent progress regardless of which tool\r\n * is running.\r\n */\r\nexport async function* spawnStream(\r\n opts: SpawnStreamOptions,\r\n): AsyncGenerator<ToolProgressEvent, SpawnStreamResult> {\r\n const max = opts.maxBytes ?? 200_000;\r\n const flushAt = opts.flushBytes ?? 4 * 1024;\r\n let stdout = '';\r\n let stderr = '';\r\n let pending = '';\r\n let error: string | undefined;\r\n\r\n const child = spawn(opts.cmd, opts.args, {\r\n cwd: opts.cwd,\r\n signal: opts.signal,\r\n stdio: ['ignore', 'pipe', 'pipe'],\r\n });\r\n\r\n type Chunk = { kind: 'out' | 'err' | 'close' | 'error'; data: string; code?: number };\r\n const queue: Chunk[] = [];\r\n let waiter: (() => void) | undefined;\r\n const wake = () => {\r\n if (waiter) {\r\n const w = waiter;\r\n waiter = undefined;\r\n w();\r\n }\r\n };\r\n\r\n child.stdout?.on('data', (c) => {\r\n const s = c.toString();\r\n if (stdout.length < max) stdout += s;\r\n queue.push({ kind: 'out', data: s });\r\n wake();\r\n });\r\n child.stderr?.on('data', (c) => {\r\n const s = c.toString();\r\n if (stderr.length < max) stderr += s;\r\n queue.push({ kind: 'err', data: s });\r\n wake();\r\n });\r\n child.on('error', (e) => {\r\n error = e.message;\r\n queue.push({ kind: 'error', data: e.message });\r\n wake();\r\n });\r\n child.on('close', (code) => {\r\n queue.push({ kind: 'close', data: '', code: code ?? 0 });\r\n wake();\r\n });\r\n\r\n let exitCode = 0;\r\n let spawnFailed = false;\r\n for (;;) {\r\n while (queue.length === 0) {\r\n await new Promise<void>((resolve) => {\r\n waiter = resolve;\r\n });\r\n }\r\n const chunk = queue.shift()!;\r\n if (chunk.kind === 'close') {\r\n // If we already saw a spawn error (ENOENT etc.), keep exitCode=1\r\n // rather than the negative platform code Node fabricates.\r\n if (!spawnFailed) exitCode = chunk.code ?? 0;\r\n break;\r\n }\r\n if (chunk.kind === 'error') {\r\n spawnFailed = true;\r\n exitCode = 1;\r\n // close usually follows\r\n continue;\r\n }\r\n pending += chunk.data;\r\n if (pending.length >= flushAt) {\r\n yield { type: 'partial_output', text: pending };\r\n pending = '';\r\n }\r\n }\r\n if (pending.length > 0) {\r\n yield { type: 'partial_output', text: pending };\r\n }\r\n\r\n return {\r\n stdout,\r\n stderr,\r\n exitCode,\r\n truncated: stdout.length >= max || stderr.length >= max,\r\n error,\r\n };\r\n}\r\n","import * as fs from 'node:fs/promises';\nimport type { Tool } from '@wrongstack/core';\nimport { safeResolve, isBinaryBuffer } from './_util.js';\n\ninterface ReadInput {\n path: string;\n offset?: number;\n limit?: number;\n}\n\ninterface ReadOutput {\n text: string;\n total_lines: number;\n encoding: string;\n truncated: boolean;\n}\n\nconst MAX_BYTES = 5 * 1024 * 1024;\n\nexport const readTool: Tool<ReadInput, ReadOutput> = {\n name: 'read',\n description: 'Read the contents of a file. Lines are 1-indexed and prefixed with line numbers.',\n usageHint:\n 'Read a file before editing it. Returns lines numbered like ` 1→content`. Use `offset` and `limit` for large files (default reads up to 2000 lines).',\n permission: 'auto',\n mutating: false,\n maxOutputBytes: 262_144,\n timeoutMs: 5_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: { type: 'string', description: 'File path (absolute or relative to cwd)' },\n offset: { type: 'integer', description: '1-based line number to start from' },\n limit: { type: 'integer', description: 'Max lines to read (default 2000)' },\n },\n required: ['path'],\n },\n async execute(input, ctx) {\n if (!input?.path) throw new Error('read: path is required');\n const absPath = safeResolve(input.path, ctx);\n\n const stat = await fs.stat(absPath);\n if (!stat.isFile()) throw new Error(`read: \"${input.path}\" is not a regular file`);\n if (stat.size > MAX_BYTES) {\n throw new Error(`read: file too large (${stat.size} bytes, limit ${MAX_BYTES})`);\n }\n\n const buf = await fs.readFile(absPath);\n if (isBinaryBuffer(buf)) {\n throw new Error(`read: \"${input.path}\" appears to be binary`);\n }\n\n const text = buf.toString('utf8');\n const allLines = text.split(/\\r\\n|\\r|\\n/);\n const total = allLines.length;\n const offset = Math.max(1, input.offset ?? 1);\n const limit = Math.max(1, Math.min(input.limit ?? 2000, 5000));\n const slice = allLines.slice(offset - 1, offset - 1 + limit);\n const truncated = offset - 1 + slice.length < total;\n\n const width = String(offset + slice.length - 1).length;\n const numbered = slice\n .map((line, i) => `${String(offset + i).padStart(width, ' ')}→${line}`)\n .join('\\n');\n\n ctx.recordRead(absPath, stat.mtimeMs);\n\n return {\n text: numbered,\n total_lines: total,\n encoding: 'utf8',\n truncated,\n };\n },\n};\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Tool } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
interface ReplaceInput {
|
|
4
|
+
pattern: string;
|
|
5
|
+
replacement: string;
|
|
6
|
+
files: string | string[];
|
|
7
|
+
glob?: string;
|
|
8
|
+
replace_all?: boolean;
|
|
9
|
+
dry_run?: boolean;
|
|
10
|
+
}
|
|
11
|
+
interface ReplaceOutput {
|
|
12
|
+
files_modified: number;
|
|
13
|
+
total_replacements: number;
|
|
14
|
+
results: {
|
|
15
|
+
path: string;
|
|
16
|
+
replacements: number;
|
|
17
|
+
diff?: string;
|
|
18
|
+
}[];
|
|
19
|
+
dry_run: boolean;
|
|
20
|
+
}
|
|
21
|
+
declare const replaceTool: Tool<ReplaceInput, ReplaceOutput>;
|
|
22
|
+
|
|
23
|
+
export { replaceTool };
|
package/dist/replace.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { compileGlob, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff } from '@wrongstack/core';
|
|
5
|
+
|
|
6
|
+
// src/replace.ts
|
|
7
|
+
function resolvePath(input, ctx) {
|
|
8
|
+
return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);
|
|
9
|
+
}
|
|
10
|
+
function ensureInsideRoot(absPath, ctx) {
|
|
11
|
+
const root = path.resolve(ctx.projectRoot);
|
|
12
|
+
const target = path.resolve(absPath);
|
|
13
|
+
const rel = path.relative(root, target);
|
|
14
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
15
|
+
throw new Error(`Path "${absPath}" is outside project root "${root}"`);
|
|
16
|
+
}
|
|
17
|
+
return target;
|
|
18
|
+
}
|
|
19
|
+
function safeResolve(input, ctx) {
|
|
20
|
+
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
21
|
+
}
|
|
22
|
+
function isBinaryBuffer(buf) {
|
|
23
|
+
const len = Math.min(buf.length, 8192);
|
|
24
|
+
for (let i = 0; i < len; i++) {
|
|
25
|
+
if (buf[i] === 0) return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/replace.ts
|
|
31
|
+
var DEFAULT_IGNORE = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
|
|
32
|
+
var replaceTool = {
|
|
33
|
+
name: "replace",
|
|
34
|
+
description: "Batch replace a pattern across multiple files matched by glob. Returns diff for each modified file.",
|
|
35
|
+
usageHint: 'Use `glob` for broad patterns (e.g. "**/*.ts"). Set `dry_run: true` to preview without modifying. `files` can be a single path, comma-separated list, or glob pattern.',
|
|
36
|
+
permission: "confirm",
|
|
37
|
+
mutating: true,
|
|
38
|
+
timeoutMs: 3e4,
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
pattern: { type: "string", description: "Regex pattern to match" },
|
|
43
|
+
replacement: { type: "string", description: "Replacement string" },
|
|
44
|
+
files: {
|
|
45
|
+
type: "string",
|
|
46
|
+
description: "File(s) to target: single path, comma-separated list, or glob pattern"
|
|
47
|
+
},
|
|
48
|
+
glob: { type: "string", description: 'Additional glob filter (e.g. "*.ts")' },
|
|
49
|
+
replace_all: { type: "boolean", description: "Replace all occurrences in each file (default: true)" },
|
|
50
|
+
dry_run: { type: "boolean", description: "Preview changes without writing" }
|
|
51
|
+
},
|
|
52
|
+
required: ["pattern", "replacement", "files"]
|
|
53
|
+
},
|
|
54
|
+
async execute(input, ctx) {
|
|
55
|
+
if (!input?.pattern) throw new Error("replace: pattern is required");
|
|
56
|
+
if (input.replacement === void 0) throw new Error("replace: replacement is required");
|
|
57
|
+
if (!input?.files) throw new Error("replace: files is required");
|
|
58
|
+
const re = new RegExp(input.pattern, "g");
|
|
59
|
+
const globRe = input.glob ? compileGlob(input.glob) : null;
|
|
60
|
+
const dryRun = input.dry_run ?? false;
|
|
61
|
+
const replaceAll = input.replace_all ?? true;
|
|
62
|
+
const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
|
|
63
|
+
const fileList = await resolveFiles(filesInput, ctx, globRe);
|
|
64
|
+
const results = [];
|
|
65
|
+
let totalReplacements = 0;
|
|
66
|
+
for (const absPath of fileList) {
|
|
67
|
+
const stat2 = await fs.stat(absPath).catch((err) => {
|
|
68
|
+
if (err.code === "ENOENT") return null;
|
|
69
|
+
throw err;
|
|
70
|
+
});
|
|
71
|
+
if (!stat2 || !stat2.isFile()) continue;
|
|
72
|
+
let content;
|
|
73
|
+
try {
|
|
74
|
+
const buf = await fs.readFile(absPath);
|
|
75
|
+
if (isBinaryBuffer(buf)) continue;
|
|
76
|
+
content = buf.toString("utf8");
|
|
77
|
+
} catch {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const style = detectNewlineStyle(content);
|
|
81
|
+
const contentLf = normalizeToLf(content);
|
|
82
|
+
re.lastIndex = 0;
|
|
83
|
+
const matches = [...contentLf.matchAll(re)];
|
|
84
|
+
if (matches.length === 0) continue;
|
|
85
|
+
const newContentLf = replaceAll ? contentLf.replace(re, input.replacement) : contentLf.replace(re, input.replacement);
|
|
86
|
+
re.lastIndex = 0;
|
|
87
|
+
const actualCount = replaceAll ? matches.length : 1;
|
|
88
|
+
totalReplacements += actualCount;
|
|
89
|
+
if (!dryRun) {
|
|
90
|
+
const newContent = toStyle(newContentLf, style);
|
|
91
|
+
await atomicWrite(absPath, newContent, { mode: stat2.mode & 511 });
|
|
92
|
+
}
|
|
93
|
+
const diff = dryRun || matches.length > 0 ? unifiedDiff(content, toStyle(newContentLf, style), { fromFile: absPath, toFile: absPath }) : void 0;
|
|
94
|
+
results.push({
|
|
95
|
+
path: absPath,
|
|
96
|
+
replacements: matches.length,
|
|
97
|
+
diff
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
files_modified: results.length,
|
|
102
|
+
total_replacements: totalReplacements,
|
|
103
|
+
results,
|
|
104
|
+
dry_run: dryRun
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
async function resolveFiles(filesInput, ctx, extraGlob) {
|
|
109
|
+
const base = ctx.cwd;
|
|
110
|
+
const normalized = filesInput.trim();
|
|
111
|
+
if (normalized.startsWith("**/") || normalized.startsWith("*") || normalized.includes("**")) {
|
|
112
|
+
return await globFiles(normalized, base, extraGlob);
|
|
113
|
+
}
|
|
114
|
+
const parts = normalized.split(",").map((s) => s.trim()).filter(Boolean);
|
|
115
|
+
const resolved = [];
|
|
116
|
+
for (const p of parts) {
|
|
117
|
+
const absPath = safeResolve(p, ctx);
|
|
118
|
+
const stat2 = await fs.stat(absPath).catch(() => null);
|
|
119
|
+
if (stat2?.isFile()) {
|
|
120
|
+
resolved.push(absPath);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return resolved;
|
|
124
|
+
}
|
|
125
|
+
async function globFiles(pattern, base, extraGlob) {
|
|
126
|
+
const { spawn: spawn3 } = await import('child_process');
|
|
127
|
+
const rgAvailable = await checkRg();
|
|
128
|
+
if (rgAvailable) {
|
|
129
|
+
try {
|
|
130
|
+
const { promise } = spawnRgFind(pattern, base);
|
|
131
|
+
return await promise;
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return await globNative(pattern, base, extraGlob);
|
|
136
|
+
}
|
|
137
|
+
function checkRg() {
|
|
138
|
+
return new Promise((resolve2) => {
|
|
139
|
+
try {
|
|
140
|
+
const p = spawn("rg", ["--version"], { stdio: "ignore" });
|
|
141
|
+
p.on("error", () => resolve2(false));
|
|
142
|
+
p.on("close", (code) => resolve2(code === 0));
|
|
143
|
+
} catch {
|
|
144
|
+
resolve2(false);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function spawnRgFind(pattern, base) {
|
|
149
|
+
const args = ["--files", "--glob", pattern, base];
|
|
150
|
+
const child = spawn("rg", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
151
|
+
let buf = "";
|
|
152
|
+
child.stdout?.on("data", (chunk) => {
|
|
153
|
+
buf += chunk.toString();
|
|
154
|
+
});
|
|
155
|
+
return {
|
|
156
|
+
promise: new Promise((resolve2, reject) => {
|
|
157
|
+
child.on("error", reject);
|
|
158
|
+
child.on("close", () => {
|
|
159
|
+
resolve2(buf.split("\n").filter(Boolean));
|
|
160
|
+
});
|
|
161
|
+
})
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
async function globNative(pattern, base, extraGlob) {
|
|
165
|
+
const results = [];
|
|
166
|
+
const globRe = compileGlob(pattern);
|
|
167
|
+
const walk = async (dir) => {
|
|
168
|
+
let entries;
|
|
169
|
+
try {
|
|
170
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
171
|
+
} catch {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
for (const e of entries) {
|
|
175
|
+
if (DEFAULT_IGNORE.includes(e.name)) continue;
|
|
176
|
+
const full = path.join(dir, e.name);
|
|
177
|
+
if (e.isDirectory()) {
|
|
178
|
+
await walk(full);
|
|
179
|
+
} else if (e.isFile()) {
|
|
180
|
+
const name = e.name;
|
|
181
|
+
if (globRe.test(name) || globRe.test(full)) {
|
|
182
|
+
if (extraGlob && !extraGlob.test(name) && !extraGlob.test(full)) continue;
|
|
183
|
+
results.push(full);
|
|
184
|
+
}
|
|
185
|
+
globRe.lastIndex = 0;
|
|
186
|
+
if (extraGlob) extraGlob.lastIndex = 0;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
await walk(base);
|
|
191
|
+
return results;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export { replaceTool };
|
|
195
|
+
//# sourceMappingURL=replace.js.map
|
|
196
|
+
//# sourceMappingURL=replace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/_util.ts","../src/replace.ts"],"names":["stat","spawn","resolve","path2"],"mappings":"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","file":"replace.js","sourcesContent":["import * as path from 'node:path';\r\nimport { spawn } from 'node:child_process';\r\nimport type { Context, ToolProgressEvent } from '@wrongstack/core';\r\n\r\nexport function resolvePath(input: string, ctx: Context): string {\r\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\r\n}\r\n\r\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\r\n const root = path.resolve(ctx.projectRoot);\r\n const target = path.resolve(absPath);\r\n const rel = path.relative(root, target);\r\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\r\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\r\n }\r\n return target;\r\n}\r\n\r\nexport function safeResolve(input: string, ctx: Context): string {\r\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\r\n}\r\n\r\nexport function truncateMiddle(s: string, max: number): string {\r\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\r\n const half = Math.floor(max / 2);\r\n return (\r\n s.slice(0, half) +\r\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\r\n s.slice(-half)\r\n );\r\n}\r\n\r\nexport function isBinaryBuffer(buf: Buffer): boolean {\r\n const len = Math.min(buf.length, 8192);\r\n for (let i = 0; i < len; i++) {\r\n if (buf[i] === 0) return true;\r\n }\r\n return false;\r\n}\r\n\r\nexport interface SpawnStreamResult {\r\n stdout: string;\r\n stderr: string;\r\n exitCode: number;\r\n truncated: boolean;\r\n error?: string;\r\n}\r\n\r\nexport interface SpawnStreamOptions {\r\n cmd: string;\r\n args: string[];\r\n cwd: string;\r\n signal: AbortSignal;\r\n maxBytes?: number;\r\n /** Bytes of new stdout/stderr to accumulate before yielding a `partial_output` event. */\r\n flushBytes?: number;\r\n}\r\n\r\n/**\r\n * Spawn a child process and yield `partial_output` progress events as\r\n * stdout/stderr arrive (batched by byte threshold), then return the full\r\n * buffered result. Shared between install/lint/format/typecheck/test/audit\r\n * so the TUI live tail sees consistent progress regardless of which tool\r\n * is running.\r\n */\r\nexport async function* spawnStream(\r\n opts: SpawnStreamOptions,\r\n): AsyncGenerator<ToolProgressEvent, SpawnStreamResult> {\r\n const max = opts.maxBytes ?? 200_000;\r\n const flushAt = opts.flushBytes ?? 4 * 1024;\r\n let stdout = '';\r\n let stderr = '';\r\n let pending = '';\r\n let error: string | undefined;\r\n\r\n const child = spawn(opts.cmd, opts.args, {\r\n cwd: opts.cwd,\r\n signal: opts.signal,\r\n stdio: ['ignore', 'pipe', 'pipe'],\r\n });\r\n\r\n type Chunk = { kind: 'out' | 'err' | 'close' | 'error'; data: string; code?: number };\r\n const queue: Chunk[] = [];\r\n let waiter: (() => void) | undefined;\r\n const wake = () => {\r\n if (waiter) {\r\n const w = waiter;\r\n waiter = undefined;\r\n w();\r\n }\r\n };\r\n\r\n child.stdout?.on('data', (c) => {\r\n const s = c.toString();\r\n if (stdout.length < max) stdout += s;\r\n queue.push({ kind: 'out', data: s });\r\n wake();\r\n });\r\n child.stderr?.on('data', (c) => {\r\n const s = c.toString();\r\n if (stderr.length < max) stderr += s;\r\n queue.push({ kind: 'err', data: s });\r\n wake();\r\n });\r\n child.on('error', (e) => {\r\n error = e.message;\r\n queue.push({ kind: 'error', data: e.message });\r\n wake();\r\n });\r\n child.on('close', (code) => {\r\n queue.push({ kind: 'close', data: '', code: code ?? 0 });\r\n wake();\r\n });\r\n\r\n let exitCode = 0;\r\n let spawnFailed = false;\r\n for (;;) {\r\n while (queue.length === 0) {\r\n await new Promise<void>((resolve) => {\r\n waiter = resolve;\r\n });\r\n }\r\n const chunk = queue.shift()!;\r\n if (chunk.kind === 'close') {\r\n // If we already saw a spawn error (ENOENT etc.), keep exitCode=1\r\n // rather than the negative platform code Node fabricates.\r\n if (!spawnFailed) exitCode = chunk.code ?? 0;\r\n break;\r\n }\r\n if (chunk.kind === 'error') {\r\n spawnFailed = true;\r\n exitCode = 1;\r\n // close usually follows\r\n continue;\r\n }\r\n pending += chunk.data;\r\n if (pending.length >= flushAt) {\r\n yield { type: 'partial_output', text: pending };\r\n pending = '';\r\n }\r\n }\r\n if (pending.length > 0) {\r\n yield { type: 'partial_output', text: pending };\r\n }\r\n\r\n return {\r\n stdout,\r\n stderr,\r\n exitCode,\r\n truncated: stdout.length >= max || stderr.length >= max,\r\n error,\r\n };\r\n}\r\n","import * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { spawn } from 'node:child_process';\nimport {\n atomicWrite,\n detectNewlineStyle,\n toStyle,\n normalizeToLf,\n compileGlob,\n unifiedDiff,\n} from '@wrongstack/core';\nimport type { Tool, Context } from '@wrongstack/core';\nimport { isBinaryBuffer, safeResolve } from './_util.js';\n\ninterface ReplaceInput {\n pattern: string;\n replacement: string;\n files: string | string[];\n glob?: string;\n replace_all?: boolean;\n dry_run?: boolean;\n}\n\ninterface ReplaceOutput {\n files_modified: number;\n total_replacements: number;\n results: { path: string; replacements: number; diff?: string }[];\n dry_run: boolean;\n}\n\nconst DEFAULT_IGNORE = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage'];\n\nexport const replaceTool: Tool<ReplaceInput, ReplaceOutput> = {\n name: 'replace',\n description:\n 'Batch replace a pattern across multiple files matched by glob. Returns diff for each modified file.',\n usageHint:\n 'Use `glob` for broad patterns (e.g. \"**/*.ts\"). Set `dry_run: true` to preview without modifying. `files` can be a single path, comma-separated list, or glob pattern.',\n permission: 'confirm',\n mutating: true,\n timeoutMs: 30_000,\n inputSchema: {\n type: 'object',\n properties: {\n pattern: { type: 'string', description: 'Regex pattern to match' },\n replacement: { type: 'string', description: 'Replacement string' },\n files: {\n type: 'string',\n description: 'File(s) to target: single path, comma-separated list, or glob pattern',\n },\n glob: { type: 'string', description: 'Additional glob filter (e.g. \"*.ts\")' },\n replace_all: { type: 'boolean', description: 'Replace all occurrences in each file (default: true)' },\n dry_run: { type: 'boolean', description: 'Preview changes without writing' },\n },\n required: ['pattern', 'replacement', 'files'],\n },\n async execute(input: ReplaceInput, ctx: Context) {\n if (!input?.pattern) throw new Error('replace: pattern is required');\n if (input.replacement === undefined) throw new Error('replace: replacement is required');\n if (!input?.files) throw new Error('replace: files is required');\n\n const re = new RegExp(input.pattern, 'g');\n const globRe = input.glob ? compileGlob(input.glob) : null;\n const dryRun = input.dry_run ?? false;\n const replaceAll = input.replace_all ?? true;\n\n const filesInput = Array.isArray(input.files) ? input.files.join(',') : input.files;\n const fileList = await resolveFiles(filesInput, ctx, globRe);\n\n const results: ReplaceOutput['results'] = [];\n let totalReplacements = 0;\n\n for (const absPath of fileList) {\n const stat = await fs.stat(absPath).catch((err) => {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;\n throw err;\n });\n if (!stat || !stat.isFile()) continue;\n\n let content: string;\n try {\n const buf = await fs.readFile(absPath);\n if (isBinaryBuffer(buf)) continue;\n content = buf.toString('utf8');\n } catch {\n continue;\n }\n\n const style = detectNewlineStyle(content);\n const contentLf = normalizeToLf(content);\n re.lastIndex = 0;\n const matches = [...contentLf.matchAll(re)];\n if (matches.length === 0) continue;\n\n const newContentLf = replaceAll\n ? contentLf.replace(re, input.replacement)\n : contentLf.replace(re, input.replacement);\n re.lastIndex = 0;\n\n // Only count replacements that were actually performed.\n const actualCount = replaceAll ? matches.length : 1;\n totalReplacements += actualCount;\n\n if (!dryRun) {\n const newContent = toStyle(newContentLf, style);\n await atomicWrite(absPath, newContent, { mode: stat.mode & 0o777 });\n }\n\n const diff = dryRun || matches.length > 0\n ? unifiedDiff(content, toStyle(newContentLf, style), { fromFile: absPath, toFile: absPath })\n : undefined;\n\n results.push({\n path: absPath,\n replacements: matches.length,\n diff,\n });\n }\n\n return {\n files_modified: results.length,\n total_replacements: totalReplacements,\n results,\n dry_run: dryRun,\n };\n },\n};\n\nasync function resolveFiles(\n filesInput: string,\n ctx: Context,\n extraGlob?: RegExp | null,\n): Promise<string[]> {\n const base = ctx.cwd;\n const normalized = filesInput.trim();\n\n if (normalized.startsWith('**/') || normalized.startsWith('*') || normalized.includes('**')) {\n return await globFiles(normalized, base, extraGlob);\n }\n\n const parts = normalized.split(',').map((s) => s.trim()).filter(Boolean);\n const resolved: string[] = [];\n\n for (const p of parts) {\n const absPath = safeResolve(p, ctx);\n const stat = await fs.stat(absPath).catch(() => null);\n if (stat?.isFile()) {\n resolved.push(absPath);\n }\n }\n\n return resolved;\n}\n\nasync function globFiles(pattern: string, base: string, extraGlob?: RegExp | null): Promise<string[]> {\n const { spawn } = await import('node:child_process');\n const results: string[] = [];\n\n const rgAvailable = await checkRg();\n if (rgAvailable) {\n try {\n const { promise } = spawnRgFind(pattern, base);\n return await promise;\n } catch {\n // fall through\n }\n }\n\n return await globNative(pattern, base, extraGlob);\n}\n\nfunction checkRg(): Promise<boolean> {\n return new Promise((resolve) => {\n try {\n const p = spawn('rg', ['--version'], { stdio: 'ignore' });\n p.on('error', () => resolve(false));\n p.on('close', (code) => resolve(code === 0));\n } catch {\n resolve(false);\n }\n });\n}\n\nfunction spawnRgFind(pattern: string, base: string): { promise: Promise<string[]> } {\n const args = ['--files', '--glob', pattern, base];\n const child = spawn('rg', args, { stdio: ['ignore', 'pipe', 'pipe'] });\n let buf = '';\n child.stdout?.on('data', (chunk: Buffer) => { buf += chunk.toString(); });\n return {\n promise: new Promise((resolve, reject) => {\n child.on('error', reject);\n child.on('close', () => {\n resolve(buf.split('\\n').filter(Boolean));\n });\n }),\n };\n}\n\nasync function globNative(pattern: string, base: string, extraGlob?: RegExp | null): Promise<string[]> {\n const results: string[] = [];\n const globRe = compileGlob(pattern);\n\n const walk = async (dir: string): Promise<void> => {\n let entries: import('node:fs').Dirent[];\n try {\n entries = await fs.readdir(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const e of entries) {\n if (DEFAULT_IGNORE.includes(e.name)) continue;\n const full = path.join(dir, e.name);\n if (e.isDirectory()) {\n await walk(full);\n } else if (e.isFile()) {\n const name = e.name;\n if (globRe.test(name) || globRe.test(full)) {\n if (extraGlob && !extraGlob.test(name) && !extraGlob.test(full)) continue;\n results.push(full);\n }\n globRe.lastIndex = 0;\n if (extraGlob) extraGlob.lastIndex = 0;\n }\n }\n };\n\n await walk(base);\n return results;\n}"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Tool } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
interface ScaffoldInput {
|
|
4
|
+
template: string;
|
|
5
|
+
name: string;
|
|
6
|
+
cwd?: string;
|
|
7
|
+
vars?: Record<string, string>;
|
|
8
|
+
dry_run?: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface ScaffoldOutput {
|
|
11
|
+
template: string;
|
|
12
|
+
name: string;
|
|
13
|
+
files_created: number;
|
|
14
|
+
files: string[];
|
|
15
|
+
dry_run: boolean;
|
|
16
|
+
output: string;
|
|
17
|
+
}
|
|
18
|
+
declare const scaffoldTool: Tool<ScaffoldInput, ScaffoldOutput>;
|
|
19
|
+
|
|
20
|
+
export { scaffoldTool };
|