diff --git a/src/tools/editFileByLines.js b/src/tools/editFileByLines.js new file mode 100644 index 0000000..7e65af8 --- /dev/null +++ b/src/tools/editFileByLines.js @@ -0,0 +1,57 @@ +// src/tools/editFileByLines.js +import fs from 'fs/promises'; +import { existsSync } from 'fs'; +import path from 'path'; + +editFileByLines.spec = { + name: editFileByLines.name, + description: + 'Replaces a specific range of lines within a file. Will replace inclusively everything from the start line to end line.', + parameters: { + type: 'object', + properties: { + filepath: { + type: 'string', + description: 'The relative path to the file where the replacement should occur.', + }, + range: { + type: 'string', + pattern: '^d+-d+$', + description: 'The range of line numbers to replace, formatted as "start-end".', + }, + replacement: { + type: 'string', + description: 'The text content (discluding line numbers) that should replace the target lines.', + }, + }, + required: ['filepath', 'range', 'replacement'], + }, +}; + +export default async function editFileByLines({ filepath, range, replacement }) { + console.log('Editing by lines', filepath); + const fullPath = path.resolve(filepath); + + console.log('Replacing with: ', replacement); + + const exists = existsSync(fullPath); + if (!exists) { + throw new Error(`File not found at ${fullPath}`); + } + + const fileContents = await fs.readFile(fullPath, 'utf8'); + + const [start, end] = range.split('-').map(Number); + const lines = fileContents.split('\n'); + + const toInsert = replacement.split('\n'); + + let updatedFileContents = [...lines.slice(0, start - 1), ...toInsert, ...lines.slice(end)].join('\n'); + + await fs.writeFile(fullPath, updatedFileContents, 'utf8'); + return { + success: true, + previousContent: fileContents, + updatedContent: updatedFileContents, + }; +} diff --git a/src/tools/editFile.js b/src/tools/editFileBySubstring.js similarity index 85% rename from src/tools/editFile.js rename to src/tools/editFileBySubstring.js index 3d90a73..d19c431 100644 --- a/src/tools/editFile.js +++ b/src/tools/editFileBySubstring.js @@ -1,9 +1,9 @@ -// src/tools/editFile.js +// src/tools/editFileBySubstring.js import fs from 'fs/promises'; import path from 'path'; -editFile.spec = { - name: editFile.name, +editFileBySubstring.spec = { + name: editFileBySubstring.name, description: 'Replaces a specific substring within a file. Will replace the first instance after the start of the specified unique search context.', parameters: { @@ -23,7 +23,7 @@ editFile.spec = { }, replacement: { type: 'string', - description: 'The text that should replace the target substring.', + description: 'The text content (discluding line numbers) that should replace the target substring.', }, }, required: ['filepath', 'uniqueContext', 'exactTarget', 'replacements'], @@ -37,12 +37,13 @@ editFile.spec = { * * This thing tries to make this ergonomic for UC */ -export default async function editFile({ filepath, uniqueContext, exactTarget, replacement }) { - console.log('Editing', filepath); +export default async function editFileBySubstring({ filepath, uniqueContext, exactTarget, replacement }) { + console.log('Editing by substring', filepath); // console.log('CBTEST ctx', uniqueContext) // console.log('CBTEST sbstr', exactTarget) // console.log('CBTEST repl', replacement) const fullPath = path.resolve(filepath); + const fileContents = await fs.readFile(fullPath, 'utf8'); const chunks = fileContents.split(uniqueContext); diff --git a/src/tools/index.js b/src/tools/index.js index 70a2368..f1ffd03 100644 --- a/src/tools/index.js +++ b/src/tools/index.js @@ -1,5 +1,6 @@ // src/tools/index.js -export { default as editFile } from './editFile.js'; +export { default as editFileBySubstring } from './editFileBySubstring.js'; +export { default as editFileByLines } from './editFileByLines.js'; export { default as execShell } from './execShell.js'; export { default as getSummary } from './getSummary.js'; export { default as regexReplace } from './regexReplace.js'; @@ -25,10 +26,10 @@ export default async function importAllTools(directory = dirname(fileURLToPath(i const module = await import(filePath); let toolsFound = 0; for (let k of Object.keys(module)) { - if (typeof module[k] === 'function' && module[k].spec) { + if (typeof module[k] === 'function' && module[k].spec && !module[k].spec.exclude) { const name = module[k].name; if (toolsByName[name]) { - throw new Error("Duplicate tool name: " + name); + throw new Error('Duplicate tool name: ' + name); } toolsByName[name] = module[k]; toolsFound++; diff --git a/src/tools/readFile.js b/src/tools/readFile.js index c75a351..bd53940 100644 --- a/src/tools/readFile.js +++ b/src/tools/readFile.js @@ -1,11 +1,10 @@ // src/tools/readFile.js import fs from 'fs/promises'; import path from 'path'; -import { execMulti } from './execShell.js'; readFile.spec = { name: readFile.name, - description: 'Retrieves the full content of the file and some relevant info.', + description: 'Retrieves the full content of the file and some relevant info. Line numbers are artifially added to the content.', parameters: { type: 'object', properties: { @@ -23,18 +22,37 @@ readFile.spec = { }, }; -export default async function readFile({ filepath, range }) { +export default async function readFile({ filepath, range, omitLineNumbers = false }) { console.log(`Reading ${filepath}${range ? ` Lines: ${range}` : ''}`); const fullPath = path.resolve(filepath); try { let content = await fs.readFile(fullPath, 'utf8'); + if (omitLineNumbers) { + if (range) { + const [start, end] = range.split('-').map(Number); + const lines = content.split('\n'); + content = lines.slice(start - 1, end).join('\n'); + } + return { content }; + } + content = addLineNumbers(content); if (range) { const [start, end] = range.split('-').map(Number); const lines = content.split('\n'); content = lines.slice(start - 1, end).join('\n'); } - return { content }; + return { + content, + }; } catch (error) { throw error; // Rethrow the error to be handled by the caller } } + +function addLineNumbers(content) { + const withNumbers = content.split('\n').map((l, i) => { + return `${i + 1} ${l}`; + }); + + return withNumbers.join('\n'); +} diff --git a/test/tools/editFileByLines.test.js b/test/tools/editFileByLines.test.js new file mode 100644 index 0000000..6915911 --- /dev/null +++ b/test/tools/editFileByLines.test.js @@ -0,0 +1,36 @@ +import test from 'ava'; +import editFileByLines from '../../src/tools/editFileByLines.js'; +import fs from 'fs/promises'; +import path from 'path'; +import os from 'os'; + +const testDir = path.join(os.tmpdir(), 'editFileTests'); +let i = 1; + +function setup() { + return { + testFile: path.join(testDir, `testReplaceInFile.tmp${i++}.txt`), + }; +} + +// Creating a temporary test file before the tests +test.before(async () => { + await fs.mkdir(testDir, { recursive: true }); +}); + +// Cleanup: remove the temporary file and directory after the tests +test.after.always(async () => { + await fs.rm(testDir, { force: true, recursive: true }); +}); + +test('editFileByLines replaces lines within a file', async (t) => { + const { testFile } = setup(); + await fs.writeFile(testFile, 'First line\nSecond line\nThird line\nFourth line', 'utf8'); + await editFileByLines({ + filepath: testFile, + range: '2-3', + replacement: 'Line number two\nLine number three', + }); + const content = await fs.readFile(testFile, 'utf8'); + t.is(content, 'First line\nLine number two\nLine number three\nFourth line', 'Lines should be replaced correctly'); +}); diff --git a/test/tools/editFileTest.js b/test/tools/editFileBySubstring.test.js similarity index 75% rename from test/tools/editFileTest.js rename to test/tools/editFileBySubstring.test.js index f5193bf..0930c57 100644 --- a/test/tools/editFileTest.js +++ b/test/tools/editFileBySubstring.test.js @@ -1,5 +1,5 @@ import test from 'ava'; -import editFile from '../../src/tools/editFile.js'; +import editFileBySubstring from '../../src/tools/editFileBySubstring.js'; import fs from 'fs/promises'; import path from 'path'; import os from 'os'; @@ -23,10 +23,10 @@ test.after.always(async () => { await fs.rm(testDir, { force: true, recursive: true }); }); -test('editFile replaces a string within a file', async (t) => { +test('editFileBySubstring replaces a string within a file', async (t) => { const { testFile } = setup(); await fs.writeFile(testFile, 'Hello World, Hello Universe', 'utf8'); - await editFile({ + await editFileBySubstring({ filepath: testFile, uniqueContext: 'Hello World, Hello Universe', exactTarget: 'Universe', @@ -36,27 +36,27 @@ test('editFile replaces a string within a file', async (t) => { t.is(content, 'Hello World, Hello AVA', 'Content should be replaced correctly'); }); -test('editFile fails with multiple instances of the search context', async (t) => { +test('editFileBySubstring fails with multiple instances of the search context', async (t) => { const { testFile } = setup(); await fs.writeFile(testFile, 'Hello World. Hello World.', 'utf8'); try { - await editFile({ + await editFileBySubstring({ filepath: testFile, uniqueContext: 'Hello World', exactTarget: 'World', replacement: 'AVA', }); - t.fail('editFile should throw an error if the search context appears more than once.'); + t.fail('editFileBySubstring should throw an error if the search context appears more than once.'); } catch (error) { - t.pass('editFile should throw an error if the search context appears more than once.'); + t.pass('editFileBySubstring should throw an error if the search context appears more than once.'); } }); -test('editFile works on a file with many lines', async (t) => { +test('editFileBySubstring works on a file with many lines', async (t) => { const { testFile } = setup(); const multilineContent = `First line\nSecond line target\nThird line`; await fs.writeFile(testFile, multilineContent); - await editFile({ + await editFileBySubstring({ filepath: testFile, uniqueContext: '\nSecond line target\n', exactTarget: 'target', @@ -70,11 +70,11 @@ test('editFile works on a file with many lines', async (t) => { ); }); -test('editFile uniqueContext lets you specify a given replacement among many', async (t) => { +test('editFileBySubstring uniqueContext lets you specify a given replacement among many', async (t) => { const { testFile } = setup(); const multiTargetContent = `Target line one\nUseless line\nTarget line two\nTarget line one`; await fs.writeFile(testFile, multiTargetContent); - await editFile({ + await editFileBySubstring({ filepath: testFile, uniqueContext: 'Target line two', exactTarget: 'Target', diff --git a/test/tools/readFile.test.js b/test/tools/readFile.test.js index e936f99..dc04ff6 100644 --- a/test/tools/readFile.test.js +++ b/test/tools/readFile.test.js @@ -19,14 +19,29 @@ test.after.always(async () => { await fs.rm(testDir, { recursive: true, force: true }); }); -test('readFile reads and returns content of a file', async (t) => { - const result = await readFile({ filepath: testFilePath }); - t.is(result.content, testContent, 'Content should match the test content'); +test('readFile reads and returns content of a file without line numbers', async (t) => { + const result = await readFile({ filepath: testFilePath, omitLineNumbers: true }); + t.is(result.content, testContent, 'Content should match the test content exactly'); }); -test('readFile reads and returns a correct range of lines from a file', async (t) => { +test('readFile reads and returns the content of a file with line numbers', async (t) => { + const expectedResult = '1 Line 1\n2 Line 2\n3 Line 3'; + const result = await readFile({ + filepath: testFilePath, + }); + t.is(result.content, expectedResult, 'Content should match the test content plus line numbers'); +}); + +test('readFile reads and returns a correct range of lines from a file without line numbers', async (t) => { const range = '2-3'; const expectedResult = 'Line 2\nLine 3'; + const result = await readFile({ filepath: testFilePath, range, omitLineNumbers: true }); + t.is(result.content, expectedResult, 'Content should match lines 2 to 3 inclusive without line numbers'); +}); + +test('readFile reads and returns a correct range of lines from a file with line numbers', async (t) => { + const range = '2-3'; + const expectedResult = '2 Line 2\n3 Line 3'; const result = await readFile({ filepath: testFilePath, range }); - t.is(result.content, expectedResult, 'Content should match lines 2 to 3 inclusive'); + t.is(result.content, expectedResult, 'Content should match lines 2 to 3 inclusive with line numbers'); });