@wrongstack/tools 0.5.7 → 0.6.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/dist/replace.js CHANGED
@@ -6,12 +6,17 @@ import { compileGlob, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, u
6
6
  // src/replace.ts
7
7
 
8
8
  // src/_regex.ts
9
- var MAX_PATTERN_LEN = 512;
9
+ var MAX_PATTERN_LEN = 256;
10
10
  var DANGEROUS_PATTERNS = [
11
- /(\([^)]*[+*][^)]*\))[+*]/,
12
11
  // (a+)+, (.*)+, etc — nested quantifier on a group with internal quantifier
13
- /(\(\?:[^)]*[+*][^)]*\))[+*]/
14
- // same, with non-capturing group
12
+ /(\([^)]*[+*][^)]*\))[+*]/,
13
+ /(\(\?:[^)]*[+*][^)]*\))[+*]/,
14
+ // Adjacent quantifiers: a++ a*+
15
+ /[+*]{2,}/,
16
+ // Quantifier on alternation with length 2+
17
+ /\([^|)]+\|[^)]+\)[+*][+*]/,
18
+ // Greedy quantifier inside lookahead/lookbehind — (?!.*a+)
19
+ /[\(\[][^)\]]*[+*][^)\]]*[\)\]][^)]*\?\??/
15
20
  ];
16
21
  function compileUserRegex(pattern, flags) {
17
22
  if (typeof pattern !== "string") {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/_regex.ts","../src/_util.ts","../src/replace.ts"],"names":["lstat","path2","stat","spawn","resolve"],"mappings":"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","file":"replace.js","sourcesContent":["/**\n * Compile a user-supplied regex with conservative bounds against ReDoS.\n *\n * Node's regex engine (V8) is backtracking-based and cannot interrupt a\n * synchronous match — a pattern like `(a+)+$` against a sufficiently long\n * line will pin a worker for seconds. The executor's outer `timeoutMs` only\n * fires between async boundaries, so a long regex eval inside a sync loop\n * is uninterruptible.\n *\n * We can't fully prevent ReDoS without an alternative engine (re2-wasm), but\n * we can sharply limit the blast radius:\n *\n * 1. Cap pattern length — practically all legitimate user patterns are\n * under 256 characters. A 4 KB pattern is almost certainly malicious\n * or a copy-paste accident.\n * 2. Reject patterns containing the most obvious super-linear structures.\n * This is a coarse filter (false-positives are likely; we accept that\n * for hostile-input contexts).\n *\n * Callers should additionally bound the *subject* length (e.g. by capping\n * line size before matching).\n */\n\nconst MAX_PATTERN_LEN = 512;\n\n// Heuristics for catastrophic-backtracking constructs. Not exhaustive; bias\n// toward false-positives in tools that accept LLM-generated input.\nconst DANGEROUS_PATTERNS: ReadonlyArray<RegExp> = [\n /(\\([^)]*[+*][^)]*\\))[+*]/, // (a+)+, (.*)+, etc — nested quantifier on a group with internal quantifier\n /(\\(\\?:[^)]*[+*][^)]*\\))[+*]/, // same, with non-capturing group\n];\n\nexport interface CompileResult {\n ok: true;\n regex: RegExp;\n}\n\nexport interface CompileFail {\n ok: false;\n reason: string;\n}\n\nexport function compileUserRegex(pattern: string, flags: string): CompileResult | CompileFail {\n if (typeof pattern !== 'string') {\n return { ok: false, reason: 'pattern must be a string' };\n }\n if (pattern.length === 0) {\n return { ok: false, reason: 'pattern is empty' };\n }\n if (pattern.length > MAX_PATTERN_LEN) {\n return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };\n }\n for (const rx of DANGEROUS_PATTERNS) {\n if (rx.test(pattern)) {\n return {\n ok: false,\n reason:\n 'pattern looks vulnerable to catastrophic backtracking — rewrite without nested quantifiers',\n };\n }\n }\n try {\n return { ok: true, regex: new RegExp(pattern, flags) };\n } catch (err) {\n return {\n ok: false,\n reason: err instanceof Error ? err.message : 'invalid regex',\n };\n }\n}\n\n/**\n * Truncate a subject line to a safe length for synchronous regex eval.\n * The cap is conservative; tools that need exact-line matching against very\n * long lines should use ripgrep externally rather than the native walker.\n */\nexport const MAX_SUBJECT_LEN = 64 * 1024;\n\nexport function capSubject(line: string): string {\n return line.length > MAX_SUBJECT_LEN ? line.slice(0, MAX_SUBJECT_LEN) : line;\n}\n","import * as path from 'node:path';\nimport type { Context } from '@wrongstack/core';\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n","import { spawn } from 'node:child_process';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport {\n atomicWrite,\n buildChildEnv,\n compileGlob,\n detectNewlineStyle,\n normalizeToLf,\n toStyle,\n unifiedDiff,\n} from '@wrongstack/core';\nimport type { Context, Tool } from '@wrongstack/core';\nimport { compileUserRegex } from './_regex.js';\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 category: 'Transform',\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: {\n type: 'boolean',\n description: 'Replace all occurrences in each file (default: true)',\n },\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 replaceAll = input.replace_all ?? true;\n // Always compile with 'g' so matchAll() works — matchAll throws\n // TypeError on non-global regexes. The replaceAll flag controls\n // how many matches we act on, not whether the regex is global.\n const compiled = compileUserRegex(input.pattern, 'g');\n if (!compiled.ok) {\n throw new Error(`replace: ${compiled.reason}`);\n }\n const re = compiled.regex;\n const globRe = input.glob ? compileGlob(input.glob) : null;\n const dryRun = input.dry_run ?? false;\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 // Use lstat to detect symlinks. resolveFiles already applies\n // safeResolve, but a symlink with a target outside the project\n // root would still pass that string check — explicitly skip it\n // so we never read or write through a link.\n const lstat = await fs.lstat(absPath).catch((err) => {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;\n throw err;\n });\n if (!lstat || !lstat.isFile()) continue;\n if (lstat.isSymbolicLink()) continue;\n\n // Cross-check via realpath: if the resolved target lives outside the\n // project root (e.g. a bind mount or a parent-dir traversal we missed),\n // skip rather than rewrite through it.\n let realPath: string;\n try {\n realPath = await fs.realpath(absPath);\n } catch {\n continue;\n }\n const rel = path.relative(ctx.projectRoot, realPath);\n if (rel.startsWith('..') || path.isAbsolute(rel)) continue;\n\n // Now stat the real target so we use its mode for atomicWrite.\n const stat = await fs.stat(realPath).catch(() => null);\n if (!stat || !stat.isFile()) continue;\n\n let content: string;\n try {\n const buf = await fs.readFile(realPath);\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 allMatches = [...contentLf.matchAll(re)];\n if (allMatches.length === 0) continue;\n\n // When replace_all is false, only act on the first match.\n const matches = replaceAll ? allMatches : allMatches.slice(0, 1);\n const count = matches.length;\n\n // Rebuild: splice the replacement into each match position from\n // right to left so earlier indices stay valid.\n let newContentLf = contentLf;\n for (let i = matches.length - 1; i >= 0; i--) {\n const m = matches[i]!;\n newContentLf =\n newContentLf.slice(0, m.index) +\n input.replacement +\n newContentLf.slice(m.index! + m[0].length);\n }\n re.lastIndex = 0;\n totalReplacements += count;\n\n if (!dryRun) {\n const newContent = toStyle(newContentLf, style);\n // Write to the real path (already validated inside project root)\n // so atomicWrite's temp-and-rename can't be redirected through a\n // freshly-planted symlink at absPath.\n await atomicWrite(realPath, newContent, { mode: stat.mode & 0o777 });\n }\n\n const diff =\n dryRun || matches.length > 0\n ? unifiedDiff(content, toStyle(newContentLf, style), {\n fromFile: absPath,\n toFile: absPath,\n })\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\n .split(',')\n .map((s) => s.trim())\n .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(\n pattern: string,\n base: string,\n extraGlob?: RegExp | null,\n): 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'], { env: buildChildEnv(), 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, { env: buildChildEnv(), stdio: ['ignore', 'pipe', 'pipe'] });\n let buf = '';\n child.stdout?.on('data', (chunk: Buffer) => {\n buf += chunk.toString();\n });\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(\n pattern: string,\n base: string,\n extraGlob?: RegExp | null,\n): 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 // Dirent.isSymbolicLink() uses readdir's d_type, which may not detect\n // directory symlinks on Windows (d_type = DT_UNKNOWN). Defensive stat\n // call: skip any entry whose lstat shows a symlink — file or directory.\n try {\n const stat = await fs.lstat(full);\n if (stat.isSymbolicLink()) continue;\n } catch {\n // lstat fails for very unusual entries (e.g. broken symlinks to deleted\n // files on NFS); skip safely rather than surfacing an error.\n continue;\n }\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}\n"]}
1
+ {"version":3,"sources":["../src/_regex.ts","../src/_util.ts","../src/replace.ts"],"names":["lstat","path2","stat","spawn","resolve"],"mappings":"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","file":"replace.js","sourcesContent":["/**\n * Compile a user-supplied regex with conservative bounds against ReDoS.\n *\n * Node's regex engine (V8) is backtracking-based and cannot interrupt a\n * synchronous match — a pattern like `(a+)+$` against a sufficiently long\n * line will pin a worker for seconds. The executor's outer `timeoutMs` only\n * fires between async boundaries, so a long regex eval inside a sync loop\n * is uninterruptible.\n *\n * We can't fully prevent ReDoS without an alternative engine (re2-wasm), but\n * we can sharply limit the blast radius:\n *\n * 1. Cap pattern length — practically all legitimate user patterns are\n * under 256 characters. A 4 KB pattern is almost certainly malicious\n * or a copy-paste accident.\n * 2. Reject patterns containing the most obvious super-linear structures.\n * This is a coarse filter (false-positives are likely; we accept that\n * for hostile-input contexts).\n *\n * Callers should additionally bound the *subject* length (e.g. by capping\n * line size before matching).\n */\n\nconst MAX_PATTERN_LEN = 256;\n\n// Heuristics for catastrophic-backtracking constructs. Not exhaustive; bias\n// toward false-positives in tools that accept LLM-generated input.\nconst DANGEROUS_PATTERNS: ReadonlyArray<RegExp> = [\n // (a+)+, (.*)+, etc — nested quantifier on a group with internal quantifier\n /(\\([^)]*[+*][^)]*\\))[+*]/,\n /(\\(\\?:[^)]*[+*][^)]*\\))[+*]/,\n // Adjacent quantifiers: a++ a*+\n /[+*]{2,}/,\n // Quantifier on alternation with length 2+\n /\\([^|)]+\\|[^)]+\\)[+*][+*]/,\n // Greedy quantifier inside lookahead/lookbehind — (?!.*a+)\n /[\\(\\[][^)\\]]*[+*][^)\\]]*[\\)\\]][^)]*\\?\\??/,\n];\n\nexport interface CompileResult {\n ok: true;\n regex: RegExp;\n}\n\nexport interface CompileFail {\n ok: false;\n reason: string;\n}\n\nexport function compileUserRegex(pattern: string, flags: string): CompileResult | CompileFail {\n if (typeof pattern !== 'string') {\n return { ok: false, reason: 'pattern must be a string' };\n }\n if (pattern.length === 0) {\n return { ok: false, reason: 'pattern is empty' };\n }\n if (pattern.length > MAX_PATTERN_LEN) {\n return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };\n }\n for (const rx of DANGEROUS_PATTERNS) {\n if (rx.test(pattern)) {\n return {\n ok: false,\n reason:\n 'pattern looks vulnerable to catastrophic backtracking — rewrite without nested quantifiers',\n };\n }\n }\n try {\n return { ok: true, regex: new RegExp(pattern, flags) };\n } catch (err) {\n return {\n ok: false,\n reason: err instanceof Error ? err.message : 'invalid regex',\n };\n }\n}\n\n/**\n * Truncate a subject line to a safe length for synchronous regex eval.\n * The cap is conservative; tools that need exact-line matching against very\n * long lines should use ripgrep externally rather than the native walker.\n */\nexport const MAX_SUBJECT_LEN = 64 * 1024;\n\nexport function capSubject(line: string): string {\n return line.length > MAX_SUBJECT_LEN ? line.slice(0, MAX_SUBJECT_LEN) : line;\n}\n","import * as path from 'node:path';\nimport type { Context } from '@wrongstack/core';\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n","import { spawn } from 'node:child_process';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport {\n atomicWrite,\n buildChildEnv,\n compileGlob,\n detectNewlineStyle,\n normalizeToLf,\n toStyle,\n unifiedDiff,\n} from '@wrongstack/core';\nimport type { Context, Tool } from '@wrongstack/core';\nimport { compileUserRegex } from './_regex.js';\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 category: 'Transform',\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: {\n type: 'boolean',\n description: 'Replace all occurrences in each file (default: true)',\n },\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 replaceAll = input.replace_all ?? true;\n // Always compile with 'g' so matchAll() works — matchAll throws\n // TypeError on non-global regexes. The replaceAll flag controls\n // how many matches we act on, not whether the regex is global.\n const compiled = compileUserRegex(input.pattern, 'g');\n if (!compiled.ok) {\n throw new Error(`replace: ${compiled.reason}`);\n }\n const re = compiled.regex;\n const globRe = input.glob ? compileGlob(input.glob) : null;\n const dryRun = input.dry_run ?? false;\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 // Use lstat to detect symlinks. resolveFiles already applies\n // safeResolve, but a symlink with a target outside the project\n // root would still pass that string check — explicitly skip it\n // so we never read or write through a link.\n const lstat = await fs.lstat(absPath).catch((err) => {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;\n throw err;\n });\n if (!lstat || !lstat.isFile()) continue;\n if (lstat.isSymbolicLink()) continue;\n\n // Cross-check via realpath: if the resolved target lives outside the\n // project root (e.g. a bind mount or a parent-dir traversal we missed),\n // skip rather than rewrite through it.\n let realPath: string;\n try {\n realPath = await fs.realpath(absPath);\n } catch {\n continue;\n }\n const rel = path.relative(ctx.projectRoot, realPath);\n if (rel.startsWith('..') || path.isAbsolute(rel)) continue;\n\n // Now stat the real target so we use its mode for atomicWrite.\n const stat = await fs.stat(realPath).catch(() => null);\n if (!stat || !stat.isFile()) continue;\n\n let content: string;\n try {\n const buf = await fs.readFile(realPath);\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 allMatches = [...contentLf.matchAll(re)];\n if (allMatches.length === 0) continue;\n\n // When replace_all is false, only act on the first match.\n const matches = replaceAll ? allMatches : allMatches.slice(0, 1);\n const count = matches.length;\n\n // Rebuild: splice the replacement into each match position from\n // right to left so earlier indices stay valid.\n let newContentLf = contentLf;\n for (let i = matches.length - 1; i >= 0; i--) {\n const m = matches[i]!;\n newContentLf =\n newContentLf.slice(0, m.index) +\n input.replacement +\n newContentLf.slice(m.index! + m[0].length);\n }\n re.lastIndex = 0;\n totalReplacements += count;\n\n if (!dryRun) {\n const newContent = toStyle(newContentLf, style);\n // Write to the real path (already validated inside project root)\n // so atomicWrite's temp-and-rename can't be redirected through a\n // freshly-planted symlink at absPath.\n await atomicWrite(realPath, newContent, { mode: stat.mode & 0o777 });\n }\n\n const diff =\n dryRun || matches.length > 0\n ? unifiedDiff(content, toStyle(newContentLf, style), {\n fromFile: absPath,\n toFile: absPath,\n })\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\n .split(',')\n .map((s) => s.trim())\n .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(\n pattern: string,\n base: string,\n extraGlob?: RegExp | null,\n): 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'], { env: buildChildEnv(), 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, { env: buildChildEnv(), stdio: ['ignore', 'pipe', 'pipe'] });\n let buf = '';\n child.stdout?.on('data', (chunk: Buffer) => {\n buf += chunk.toString();\n });\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(\n pattern: string,\n base: string,\n extraGlob?: RegExp | null,\n): 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 // Dirent.isSymbolicLink() uses readdir's d_type, which may not detect\n // directory symlinks on Windows (d_type = DT_UNKNOWN). Defensive stat\n // call: skip any entry whose lstat shows a symlink — file or directory.\n try {\n const stat = await fs.lstat(full);\n if (stat.isSymbolicLink()) continue;\n } catch {\n // lstat fails for very unusual entries (e.g. broken symlinks to deleted\n // files on NFS); skip safely rather than surfacing an error.\n continue;\n }\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}\n"]}
package/dist/scaffold.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as fs from 'fs/promises';
2
2
  import * as path from 'path';
3
+ import { atomicWrite } from '@wrongstack/core';
3
4
 
4
5
  // src/scaffold.ts
5
6
  function resolvePath(input, ctx) {
@@ -175,7 +176,7 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
175
176
  const fullPath = target;
176
177
  if (!dryRun) {
177
178
  await fs.mkdir(path.dirname(fullPath), { recursive: true });
178
- await fs.writeFile(fullPath, substituteVars(content, name, vars), "utf8");
179
+ await atomicWrite(fullPath, substituteVars(content, name, vars));
179
180
  }
180
181
  files.push(resolvedPath);
181
182
  filesCreated++;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/_util.ts","../src/scaffold.ts"],"names":["path2"],"mappings":"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","file":"scaffold.js","sourcesContent":["import * as path from 'node:path';\nimport type { Context } from '@wrongstack/core';\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n","import * as fs from 'node:fs/promises';\r\nimport * as path from 'node:path';\r\nimport type { Tool } from '@wrongstack/core';\r\nimport { safeResolve } from './_util.js';\r\n\r\ninterface ScaffoldInput {\r\n template: string;\r\n name: string;\r\n cwd?: string;\r\n vars?: Record<string, string>;\r\n dry_run?: boolean;\r\n}\r\n\r\ninterface ScaffoldOutput {\r\n template: string;\r\n name: string;\r\n files_created: number;\r\n files: string[];\r\n dry_run: boolean;\r\n output: string;\r\n}\r\n\r\nconst BUILT_IN_TEMPLATES: Record<string, { description: string; files: Record<string, string> }> = {\r\n 'npm-package': {\r\n description: 'Basic npm package with ESM',\r\n files: {\r\n 'package.json': JSON.stringify(\r\n {\r\n name: '{{name}}',\r\n version: '0.1.1',\r\n type: 'module',\r\n main: './dist/index.js',\r\n scripts: { build: 'tsc', test: 'vitest run' },\r\n devDependencies: { typescript: '^5.0.0' },\r\n },\r\n null,\r\n 2,\r\n ),\r\n 'tsconfig.json': JSON.stringify(\r\n {\r\n compilerOptions: { target: 'ES2022', module: 'ESNext', strict: true },\r\n include: ['src'],\r\n },\r\n null,\r\n 2,\r\n ),\r\n 'src/index.ts': `export function hello() {\\n return 'Hello from {{name}}';\\n}\\n`,\r\n 'src/index.test.ts': `import { hello } from './index';\\nimport { describe, it, expect } from 'vitest';\\n\\ndescribe('hello', () => {\\n it('returns greeting', () => {\\n expect(hello()).toBe('Hello from {{name}}');\\n });\\n});\\n`,\r\n },\r\n },\r\n 'cli-tool': {\r\n description: 'CLI tool with argparse',\r\n files: {\r\n 'package.json': JSON.stringify(\r\n {\r\n name: '{{name}}',\r\n version: '0.1.1',\r\n type: 'module',\r\n bin: { '{{name}}': './src/index.js' },\r\n scripts: { build: 'tsc', start: 'node dist/index.js' },\r\n },\r\n null,\r\n 2,\r\n ),\r\n 'src/index.ts': `#!/usr/bin/env node\\n\\nasync function main() {\\n console.log('Hello from {{name}}');\\n}\\n\\nmain();\\n`,\r\n },\r\n },\r\n 'react-component': {\r\n description: 'React component with TypeScript',\r\n files: {\r\n '{{name}}.tsx': `interface {{Name}}Props {\\n className?: string;\\n}\\n\\nexport function {{Name}}({ className }: {{Name}}Props) {\\n return (\\n <div className={className}>\\n {{Name}} Component\\n </div>\\n );\\n}\\n`,\r\n '{{name}}.test.tsx': `import { render, screen } from '@testing-library/react';\\nimport { {{Name}} } from './{{Name}}';\\n\\ndescribe('{{Name}}', () => {\\n it('renders', () => {\\n render(<{{Name}} />);\\n expect(screen.getByText('{{Name}} Component')).toBeInTheDocument();\\n });\\n});\\n`,\r\n },\r\n },\r\n};\r\n\r\nexport const scaffoldTool: Tool<ScaffoldInput, ScaffoldOutput> = {\r\n name: 'scaffold',\r\n category: 'Project',\r\n description:\r\n 'Generate boilerplate code from built-in templates or paths. Creates package.json, source files, tests.',\r\n usageHint:\r\n 'Set `template` (npm-package, cli-tool, react-component) and `name`. `vars` for template variables. `dry_run` preview.',\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 template: {\r\n type: 'string',\r\n description:\r\n 'Template name (npm-package, cli-tool, react-component) or path to template directory',\r\n },\r\n name: {\r\n type: 'string',\r\n description: 'Project/component name (used in generated files)',\r\n },\r\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\r\n vars: {\r\n type: 'object',\r\n additionalProperties: { type: 'string' },\r\n description: 'Template variables for custom templates',\r\n },\r\n dry_run: {\r\n type: 'boolean',\r\n description: 'Preview generated files without creating (default: false)',\r\n },\r\n },\r\n required: ['template', 'name'],\r\n },\r\n async execute(input, ctx) {\r\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\r\n const name = input.name;\r\n const vars = { name, ...input.vars };\r\n\r\n const builtIn = BUILT_IN_TEMPLATES[input.template];\r\n if (builtIn) {\r\n return await handleBuiltIn(name, builtIn.files, cwd, ctx, input.dry_run ?? false, vars);\r\n }\r\n\r\n return {\r\n template: input.template,\r\n name,\r\n files_created: 0,\r\n files: [],\r\n dry_run: input.dry_run ?? false,\r\n output: `Template \"${input.template}\" not found. Available: ${Object.keys(BUILT_IN_TEMPLATES).join(', ')}`,\r\n };\r\n },\r\n};\r\n\r\nasync function handleBuiltIn(\r\n name: string,\r\n templateFiles: Record<string, string>,\r\n cwd: string,\r\n ctx: Parameters<Tool['execute']>[1],\r\n dryRun: boolean,\r\n vars: Record<string, string>,\r\n): Promise<ScaffoldOutput> {\r\n const files: string[] = [];\r\n let filesCreated = 0;\r\n\r\n for (const [filePath, content] of Object.entries(templateFiles)) {\r\n const resolvedPath = substituteVars(filePath, name, vars);\r\n const joinedPath = path.join(cwd, resolvedPath);\r\n // Ensure generated files cannot escape the project root via template variable injection (e.g. name containing \"../\")\r\n const root = path.resolve(ctx.projectRoot);\r\n const target = path.resolve(joinedPath);\r\n const rel = path.relative(root, target);\r\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\r\n throw new Error(`scaffold: generated path \"${resolvedPath}\" would escape project root`);\r\n }\r\n const fullPath = target;\r\n\r\n if (!dryRun) {\r\n await fs.mkdir(path.dirname(fullPath), { recursive: true });\r\n await fs.writeFile(fullPath, substituteVars(content, name, vars), 'utf8');\r\n }\r\n files.push(resolvedPath);\r\n filesCreated++;\r\n }\r\n\r\n return {\r\n template: 'built-in',\r\n name,\r\n files_created: filesCreated,\r\n files,\r\n dry_run: dryRun,\r\n output: dryRun\r\n ? `Would create ${filesCreated} files: ${files.join(', ')}`\r\n : `Created ${filesCreated} files: ${files.join(', ')}`,\r\n };\r\n}\r\n\r\nfunction substituteVars(content: string, name: string, vars: Record<string, string>): string {\r\n let result = content;\r\n result = result.replace(/\\{\\{name\\}\\}/g, name.toLowerCase().replace(/\\s+/g, '-'));\r\n result = result.replace(\r\n /\\{\\{Name\\}\\}/g,\r\n name.replace(/(?:^|[-_\\s]+)([a-z])/g, (_, c) => c.toUpperCase()),\r\n );\r\n for (const [k, v] of Object.entries(vars)) {\r\n result = result.replace(new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}`, 'g'), v);\r\n }\r\n return result;\r\n}\r\n"]}
1
+ {"version":3,"sources":["../src/_util.ts","../src/scaffold.ts"],"names":["path2"],"mappings":"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","file":"scaffold.js","sourcesContent":["import * as path from 'node:path';\nimport type { Context } from '@wrongstack/core';\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const root = path.resolve(ctx.projectRoot);\n const target = path.resolve(absPath);\n const rel = path.relative(root, target);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\n }\n return target;\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n","import * as fs from 'node:fs/promises';\r\nimport * as path from 'node:path';\r\nimport type { Tool } from '@wrongstack/core';\r\nimport { atomicWrite } from '@wrongstack/core';\r\nimport { safeResolve } from './_util.js';\r\n\r\ninterface ScaffoldInput {\r\n template: string;\r\n name: string;\r\n cwd?: string;\r\n vars?: Record<string, string>;\r\n dry_run?: boolean;\r\n}\r\n\r\ninterface ScaffoldOutput {\r\n template: string;\r\n name: string;\r\n files_created: number;\r\n files: string[];\r\n dry_run: boolean;\r\n output: string;\r\n}\r\n\r\nconst BUILT_IN_TEMPLATES: Record<string, { description: string; files: Record<string, string> }> = {\r\n 'npm-package': {\r\n description: 'Basic npm package with ESM',\r\n files: {\r\n 'package.json': JSON.stringify(\r\n {\r\n name: '{{name}}',\r\n version: '0.1.1',\r\n type: 'module',\r\n main: './dist/index.js',\r\n scripts: { build: 'tsc', test: 'vitest run' },\r\n devDependencies: { typescript: '^5.0.0' },\r\n },\r\n null,\r\n 2,\r\n ),\r\n 'tsconfig.json': JSON.stringify(\r\n {\r\n compilerOptions: { target: 'ES2022', module: 'ESNext', strict: true },\r\n include: ['src'],\r\n },\r\n null,\r\n 2,\r\n ),\r\n 'src/index.ts': `export function hello() {\\n return 'Hello from {{name}}';\\n}\\n`,\r\n 'src/index.test.ts': `import { hello } from './index';\\nimport { describe, it, expect } from 'vitest';\\n\\ndescribe('hello', () => {\\n it('returns greeting', () => {\\n expect(hello()).toBe('Hello from {{name}}');\\n });\\n});\\n`,\r\n },\r\n },\r\n 'cli-tool': {\r\n description: 'CLI tool with argparse',\r\n files: {\r\n 'package.json': JSON.stringify(\r\n {\r\n name: '{{name}}',\r\n version: '0.1.1',\r\n type: 'module',\r\n bin: { '{{name}}': './src/index.js' },\r\n scripts: { build: 'tsc', start: 'node dist/index.js' },\r\n },\r\n null,\r\n 2,\r\n ),\r\n 'src/index.ts': `#!/usr/bin/env node\\n\\nasync function main() {\\n console.log('Hello from {{name}}');\\n}\\n\\nmain();\\n`,\r\n },\r\n },\r\n 'react-component': {\r\n description: 'React component with TypeScript',\r\n files: {\r\n '{{name}}.tsx': `interface {{Name}}Props {\\n className?: string;\\n}\\n\\nexport function {{Name}}({ className }: {{Name}}Props) {\\n return (\\n <div className={className}>\\n {{Name}} Component\\n </div>\\n );\\n}\\n`,\r\n '{{name}}.test.tsx': `import { render, screen } from '@testing-library/react';\\nimport { {{Name}} } from './{{Name}}';\\n\\ndescribe('{{Name}}', () => {\\n it('renders', () => {\\n render(<{{Name}} />);\\n expect(screen.getByText('{{Name}} Component')).toBeInTheDocument();\\n });\\n});\\n`,\r\n },\r\n },\r\n};\r\n\r\nexport const scaffoldTool: Tool<ScaffoldInput, ScaffoldOutput> = {\r\n name: 'scaffold',\r\n category: 'Project',\r\n description:\r\n 'Generate boilerplate code from built-in templates or paths. Creates package.json, source files, tests.',\r\n usageHint:\r\n 'Set `template` (npm-package, cli-tool, react-component) and `name`. `vars` for template variables. `dry_run` preview.',\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 template: {\r\n type: 'string',\r\n description:\r\n 'Template name (npm-package, cli-tool, react-component) or path to template directory',\r\n },\r\n name: {\r\n type: 'string',\r\n description: 'Project/component name (used in generated files)',\r\n },\r\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\r\n vars: {\r\n type: 'object',\r\n additionalProperties: { type: 'string' },\r\n description: 'Template variables for custom templates',\r\n },\r\n dry_run: {\r\n type: 'boolean',\r\n description: 'Preview generated files without creating (default: false)',\r\n },\r\n },\r\n required: ['template', 'name'],\r\n },\r\n async execute(input, ctx) {\r\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\r\n const name = input.name;\r\n const vars = { name, ...input.vars };\r\n\r\n const builtIn = BUILT_IN_TEMPLATES[input.template];\r\n if (builtIn) {\r\n return await handleBuiltIn(name, builtIn.files, cwd, ctx, input.dry_run ?? false, vars);\r\n }\r\n\r\n return {\r\n template: input.template,\r\n name,\r\n files_created: 0,\r\n files: [],\r\n dry_run: input.dry_run ?? false,\r\n output: `Template \"${input.template}\" not found. Available: ${Object.keys(BUILT_IN_TEMPLATES).join(', ')}`,\r\n };\r\n },\r\n};\r\n\r\nasync function handleBuiltIn(\r\n name: string,\r\n templateFiles: Record<string, string>,\r\n cwd: string,\r\n ctx: Parameters<Tool['execute']>[1],\r\n dryRun: boolean,\r\n vars: Record<string, string>,\r\n): Promise<ScaffoldOutput> {\r\n const files: string[] = [];\r\n let filesCreated = 0;\r\n\r\n for (const [filePath, content] of Object.entries(templateFiles)) {\r\n const resolvedPath = substituteVars(filePath, name, vars);\r\n const joinedPath = path.join(cwd, resolvedPath);\r\n // Ensure generated files cannot escape the project root via template variable injection (e.g. name containing \"../\")\r\n const root = path.resolve(ctx.projectRoot);\r\n const target = path.resolve(joinedPath);\r\n const rel = path.relative(root, target);\r\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\r\n throw new Error(`scaffold: generated path \"${resolvedPath}\" would escape project root`);\r\n }\r\n const fullPath = target;\r\n\r\n if (!dryRun) {\r\n await fs.mkdir(path.dirname(fullPath), { recursive: true });\r\n // atomicWrite: scaffolded files land in the user's tracked tree.\r\n // A torn write here would commit a corrupt file to their repo.\r\n await atomicWrite(fullPath, substituteVars(content, name, vars));\r\n }\r\n files.push(resolvedPath);\r\n filesCreated++;\r\n }\r\n\r\n return {\r\n template: 'built-in',\r\n name,\r\n files_created: filesCreated,\r\n files,\r\n dry_run: dryRun,\r\n output: dryRun\r\n ? `Would create ${filesCreated} files: ${files.join(', ')}`\r\n : `Created ${filesCreated} files: ${files.join(', ')}`,\r\n };\r\n}\r\n\r\nfunction substituteVars(content: string, name: string, vars: Record<string, string>): string {\r\n let result = content;\r\n result = result.replace(/\\{\\{name\\}\\}/g, name.toLowerCase().replace(/\\s+/g, '-'));\r\n result = result.replace(\r\n /\\{\\{Name\\}\\}/g,\r\n name.replace(/(?:^|[-_\\s]+)([a-z])/g, (_, c) => c.toUpperCase()),\r\n );\r\n for (const [k, v] of Object.entries(vars)) {\r\n result = result.replace(new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}`, 'g'), v);\r\n }\r\n return result;\r\n}\r\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/tools",
3
- "version": "0.5.7",
3
+ "version": "0.6.1",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack built-in tools: read/write/edit, bash/exec, grep/glob, git, fetch, test, lint, and more.",
6
6
  "repository": {
@@ -169,7 +169,7 @@
169
169
  "dist"
170
170
  ],
171
171
  "dependencies": {
172
- "@wrongstack/core": "0.5.7"
172
+ "@wrongstack/core": "0.6.1"
173
173
  },
174
174
  "devDependencies": {
175
175
  "@types/node": "^22.19.19",