[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_style = tab\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.yml]\nindent_style = space\nindent_size = 2\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\nnode_modules\ndist\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 farouqaldori\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "package.json",
    "content": "{\n\t\"name\": \"aiformat\",\n\t\"version\": \"0.0.5\",\n\t\"license\": \"MIT\",\n\t\"bin\": \"dist/cli.js\",\n\t\"type\": \"module\",\n\t\"engines\": {\n\t\t\"node\": \">=16\"\n\t},\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"https://github.com/farouqaldori/aiformat.git\"\n\t},\n\t\"scripts\": {\n\t\t\"build\": \"tsc\",\n\t\t\"dev\": \"tsc --watch\",\n\t\t\"test\": \"prettier --check . && xo && ava\"\n\t},\n\t\"files\": [\n\t\t\"dist\"\n\t],\n\t\"dependencies\": {\n\t\t\"clipboardy\": \"^4.0.0\",\n\t\t\"figures\": \"^6.1.0\",\n\t\t\"ink\": \"^4.1.0\",\n\t\t\"meow\": \"^11.0.0\",\n\t\t\"react\": \"^18.2.0\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@sindresorhus/tsconfig\": \"^3.0.1\",\n\t\t\"@types/react\": \"^18.0.32\",\n\t\t\"@vdemedes/prettier-config\": \"^2.0.1\",\n\t\t\"ava\": \"^5.2.0\",\n\t\t\"chalk\": \"^5.2.0\",\n\t\t\"eslint-config-xo-react\": \"^0.27.0\",\n\t\t\"eslint-plugin-react\": \"^7.32.2\",\n\t\t\"eslint-plugin-react-hooks\": \"^4.6.0\",\n\t\t\"ink-testing-library\": \"^3.0.0\",\n\t\t\"prettier\": \"^2.8.7\",\n\t\t\"ts-node\": \"^10.9.1\",\n\t\t\"typescript\": \"^5.0.3\",\n\t\t\"xo\": \"^0.53.1\"\n\t},\n\t\"ava\": {\n\t\t\"extensions\": {\n\t\t\t\"ts\": \"module\",\n\t\t\t\"tsx\": \"module\"\n\t\t},\n\t\t\"nodeArguments\": [\n\t\t\t\"--loader=ts-node/esm\"\n\t\t]\n\t},\n\t\"xo\": {\n\t\t\"extends\": \"xo-react\",\n\t\t\"prettier\": true,\n\t\t\"rules\": {\n\t\t\t\"react/prop-types\": \"off\"\n\t\t}\n\t},\n\t\"prettier\": \"@vdemedes/prettier-config\"\n}\n"
  },
  {
    "path": "readme.md",
    "content": "# aiformat\n\nhttps://github.com/farouqaldori/aiformat/assets/16778033/2dd13fc7-5859-4169-893a-4bfe99bd8f0a\n\naiformat is a simple tool you can use from the command line. It helps you select files and folders and change them into a format that AI assistants like Claude can understand. \n\nThis way, you can share code snippets and project structures faster and easier directly from the console, without having to copy and paste them manually.\n\nThis cli tool is built using [Ink](https://github.com/vadimdemedes/ink).\n\n## Updates\n### **Mar 18 2024:**  Folder navigation support\n* Added searching inside deeply nested files.\n* Added the ability to expand/collapse folders with the `Tab` key.\n* Added emojis to differentiate between folders (🗂️) and files (📄).\n* Full code re-write, including ID based navigation.\n\n## Features\n\n- Interactively select files and folders from the current directory\n- Filter files and folders using a search query\n- Navigate through the list using arrow keys\n- Select/deselect items using left/right arrow keys\n- Convert selected files and folders into a format compatible with Claude\n- Automatically copy the formatted output to the clipboard\n\n\n## Install\n\nTo install aiformat, make sure you have Node.js installed on your system. Then, run the following command:\n\n\n```bash\n$ npm install --global aiformat\n```\n\n## Usage\n\nTo use aiformat, navigate to the directory containing the files and folders you want to share with Claude. Then, run the following command:\n\n```bash\naiformat\n```\n\nThe CLI will display a list of files and folders in the current directory. You can navigate through the list using the up and down arrow keys. To select or deselect an item, use the left or right arrow keys.\n\nYou can also filter the list by typing a search query. The list will update in real-time as you type.\n\nOnce you have selected the desired files and folders, press Enter. The CLI will format the selected items into a structure that Claude can understand and automatically copy the output to your clipboard.\n\n## Example\n\n```bash\n$ cd /path/to/your/project\n$ aiformat\n```\n\n![aiformat example](https://i.imgur.com/Vx1EYLn.png)\n\nNavigate through the list, select the desired files and folders, and press Enter. The formatted output will be copied to your clipboard, ready to be pasted into your conversation with your AI assistant.\n\nThe output is optimized for usage with Claude, by wrapping files with XML tags. \n\nExample prompt:\n\n```\n<file name=\"package.json\">\n{\n\t\"name\": \"aiformat\",\n\t\"version\": \"0.0.1\",\n\t\"license\": \"MIT\",\n  ...\n}\n</file>\n\n<directory name=\"source\">\n<file name=\"source/app.tsx\">\nimport React, { FC, useState, useEffect } from 'react';\nconst App: FC = () => {\n\treturn (\n    ...\n\t);\n};\n\nexport default App;\n</file>\n\n<file name=\"source/cli.tsx\">\n#!/usr/bin/env node\nimport React from 'react';\nimport App from './app.js';\n\nrender(<App />);\n</file>\n</directory>\n\n// Add this part manually\n<task>\nModify the files above and update the version from 0.0.1 to 0.0.2\n</task>\n```\n\n## Local Development\n\nTo start developing aiformat locally, follow these steps:\n\n1. Clone the repository:\n   ```bash\n   git clone https://github.com/farouqaldori/aiformat.git\n   ```\n\n2. Change to the project directory:\n   ```bash\n   cd aiformat\n   ```\n\n3. Install the dependencies:\n   ```bash\n   npm install\n   ```\n\n4. Build the project:\n   ```bash\n   npm run build\n   ```\n\n5. Link the package globally:\n   ```bash\n   npm link\n   ```\n\n6. Now you can use the `aiformat` command globally to test your local changes.\n\n\n## Contributing\n\nIf you find any issues or have suggestions for improvements, please feel free to open an issue or submit a pull request on the [GitHub repository](https://github.com/farouqaldori/aiformat).\n\n## License\n\nThis project is licensed under the [MIT License](LICENSE).\n"
  },
  {
    "path": "source/app.tsx",
    "content": "import React, { FC, ReactNode, useEffect, useState } from 'react';\nimport { Box, Text, useInput } from 'ink';\nimport fs from 'fs';\nimport path from 'path';\nimport { outputXml } from './utils/generateOutput.js';\nimport clipboard from 'clipboardy';\n\ninterface Item {\n\tid: string;\n\tname: string;\n\tisDirectory: boolean;\n\tchildren: Item[];\n\tpath: string;\n\tisExpanded: boolean;\n\tlevel: number;\n}\n\nconst generateId = (itemPath: string): string => {\n\treturn itemPath;\n};\n\n// Clear console\nprocess.stdout.write('\\x1Bc');\n\nconst App: FC = () => {\n\tconst [excludedFolders] = useState<string[]>(['node_modules', '.git', 'dist', 'build', 'coverage', 'public']);\n\tconst [currentItemId, setCurrentItemId] = useState<string | null>(null);\n\tconst [items, setItems] = useState<Item[]>([]);\n\tconst [selectedItems, setSelectedItems] = useState<Item[]>([]);\n\tconst [searchQuery, setSearchQuery] = useState<string>('');\n\tconst [message, setMessage] = useState<ReactNode | null>(null)\n\n\n\tuseInput((input, key) => {\n\t\tif (key.return) {\n\t\t\tcopyContentsOfFilesAndFolders();\n\t\t\treturn;\n\t\t}\n\t\tif (input) {\n\t\t\tsetSearchQuery((prev) => prev + input);\n\t\t}\n\t\tif (key.backspace || key.delete) {\n\t\t\tsetSearchQuery((prev) => prev.slice(0, -1));\n\t\t}\n\t\tif (key.downArrow) {\n\t\t\tnavigateToNextItem();\n\t\t}\n\t\tif (key.upArrow) {\n\t\t\tnavigateToPreviousItem();\n\t\t}\n\t\tif (key.tab) {\n\t\t\ttoggleFolderExpansion();\n\t\t}\n\t\tif (key.leftArrow || key.rightArrow) {\n\t\t\ttoggleSelection();\n\t\t}\n\t});\n\n\tconst copyContentsOfFilesAndFolders = () => {\n\t\tconst files = outputXml(selectedItems);\n\t\tclipboard.writeSync(files.content);\n\t\tsetMessage(\n\t\t\t<Text color=\"white\">✨ Successfully copied <Text color=\"cyan\">{files.fileCount}</Text> file{files.fileCount > 1 && \"s\"} to clipboard</Text>\n\t\t);\n\t\tsetTimeout(() => {\n\t\t\tprocess.exit(0);\n\t\t}, 300);\n\t};\n\n\tconst toggleSelection = () => {\n\t\tif (!currentItemId) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst currentItem = findItemById(currentItemId, items);\n\t\tif (!currentItem) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (currentItem.isDirectory) {\n\t\t\tconst itemsInFolder = getItemsFromFolder(currentItem);\n\t\t\tconst allItemsInFolderAreSelected = itemsInFolder.every((item) => selectedItems.includes(item));\n\t\t\tif (allItemsInFolderAreSelected) {\n\t\t\t\tsetSelectedItems(selectedItems.filter((item) => !itemsInFolder.find((i) => i.id === item.id)));\n\t\t\t} else {\n\t\t\t\tconst newSelectedItems = selectedItems.filter((item) => !itemsInFolder.find((i) => i.id === item.id));\n\t\t\t\tsetSelectedItems([...newSelectedItems, ...itemsInFolder]);\n\t\t\t}\n\t\t} else {\n\t\t\tif (selectedItems.find((item) => item.id === currentItem.id)) {\n\t\t\t\tsetSelectedItems(selectedItems.filter((item) => item.id !== currentItem.id));\n\t\t\t} else {\n\t\t\t\tsetSelectedItems([...selectedItems, currentItem]);\n\t\t\t}\n\t\t}\n\t}\n\n\tconst getItemsFromFolder = (folder: Item): Item[] => {\n\t\tconst items: Item[] = [];\n\t\tconst traverseItems = (item: Item) => {\n\t\t\titems.push(item);\n\t\t\tif (item.isDirectory) {\n\t\t\t\titem.children.forEach(traverseItems);\n\t\t\t}\n\t\t};\n\t\ttraverseItems(folder);\n\t\treturn items;\n\t}\n\n\tconst loadFilesAndFolders = (dirPath: string, level: number = 0): Item[] => {\n\t\tconst items: Item[] = [];\n\t\tconst dirItems = fs.readdirSync(dirPath);\n\t\tconst sortedItems = dirItems.sort((a, b) => {\n\t\t\tconst aIsDir = fs.statSync(path.join(dirPath, a)).isDirectory();\n\t\t\tconst bIsDir = fs.statSync(path.join(dirPath, b)).isDirectory();\n\t\t\tif (aIsDir && !bIsDir) return -1;\n\t\t\tif (!aIsDir && bIsDir) return 1;\n\t\t\treturn a.localeCompare(b);\n\t\t});\n\n\t\tfor (const item of sortedItems) {\n\t\t\tconst itemPath = path.join(dirPath, item);\n\t\t\tconst isDirectory = fs.statSync(itemPath).isDirectory();\n\t\t\tconst id = generateId(itemPath);\n\n\t\t\tif (!excludedFolders.includes(item)) {\n\t\t\t\tconst newItem: Item = {\n\t\t\t\t\tid,\n\t\t\t\t\tname: item,\n\t\t\t\t\tisDirectory,\n\t\t\t\t\tchildren: [],\n\t\t\t\t\tpath: itemPath,\n\t\t\t\t\tisExpanded: false,\n\t\t\t\t\tlevel,\n\t\t\t\t};\n\n\t\t\t\tif (isDirectory) {\n\t\t\t\t\tnewItem.children = loadFilesAndFolders(itemPath, level + 1);\n\t\t\t\t}\n\n\t\t\t\titems.push(newItem);\n\t\t\t}\n\t\t}\n\n\t\treturn items;\n\t};\n\n\n\tuseEffect(() => {\n\t\tconst items = loadFilesAndFolders(process.cwd());\n\t\tsetItems(items);\n\t\tsetCurrentItemId(items[0]?.id || null);\n\t}, []);\n\n\n\tconst findItemByIdInFilteredItems = (itemId: string, items: Item[]): Item | undefined => {\n\t\tfor (const item of items) {\n\t\t\tif (item.id === itemId) {\n\t\t\t\treturn item;\n\t\t\t}\n\t\t\tif (item.isDirectory && item.isExpanded) {\n\t\t\t\tconst foundItem = findItemByIdInFilteredItems(itemId, item.children);\n\t\t\t\tif (foundItem) {\n\t\t\t\t\treturn foundItem;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t};\n\n\n\tconst findItemById = (itemId: string, items: Item[]): Item | undefined => {\n\t\tfor (const item of items) {\n\t\t\tif (item.id === itemId) {\n\t\t\t\treturn item;\n\t\t\t}\n\t\t\tif (item.isDirectory) {\n\t\t\t\tconst foundItem = findItemById(itemId, item.children);\n\t\t\t\tif (foundItem) {\n\t\t\t\t\treturn foundItem;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t};\n\n\tconst navigateToNextItem = () => {\n\t\tif (!currentItemId) {\n\t\t\tsetCurrentItemId(expandedItems[0]?.id || null);\n\t\t\treturn;\n\t\t}\n\t\tconst currentItem = findItemByIdInFilteredItems(currentItemId, expandedItems);\n\t\tif (!currentItem) {\n\t\t\treturn;\n\t\t}\n\t\tif (currentItem.isDirectory && currentItem.isExpanded && currentItem.children.length > 0) {\n\t\t\tsetCurrentItemId(currentItem.children[0]?.id || null);\n\t\t} else {\n\t\t\tconst flattenedItems = flattenItems(expandedItems);\n\t\t\tconst currentIndex = flattenedItems.findIndex((item) => item.id === currentItemId);\n\t\t\tconst nextIndex = (currentIndex + 1) % flattenedItems.length;\n\t\t\tsetCurrentItemId(flattenedItems[nextIndex]?.id || null);\n\t\t}\n\t};\n\tconst navigateToPreviousItem = () => {\n\t\tif (!currentItemId) {\n\t\t\treturn;\n\t\t}\n\t\tconst flattenedItems = flattenItems(expandedItems);\n\t\tconst currentIndex = flattenedItems.findIndex((item) => item.id === currentItemId);\n\t\tconst previousIndex = (currentIndex - 1 + flattenedItems.length) % flattenedItems.length;\n\t\tsetCurrentItemId(flattenedItems[previousIndex]?.id || null);\n\t};\n\n\tconst flattenItems = (items: Item[]): Item[] => {\n\t\tconst flattenedItems: Item[] = [];\n\n\t\tconst traverseItems = (items: Item[]) => {\n\t\t\tfor (const item of items) {\n\t\t\t\tflattenedItems.push(item);\n\t\t\t\tif (item.isDirectory && item.isExpanded) {\n\t\t\t\t\ttraverseItems(item.children);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\ttraverseItems(items);\n\t\treturn flattenedItems;\n\t};\n\n\tconst toggleFolderExpansion = () => {\n\t\tif (!currentItemId) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst currentItem = findItemById(currentItemId, items);\n\t\tif (currentItem && currentItem.isDirectory) {\n\t\t\tcurrentItem.isExpanded = !currentItem.isExpanded;\n\t\t}\n\t\tsetItems(items.map((item) => {\n\t\t\tif (item.id === currentItem?.id) {\n\t\t\t\treturn currentItem;\n\t\t\t}\n\t\t\treturn item;\n\t\t}));\n\t};\n\n\tconst expandParentFolders = (item: Item, items: Item[]): Item[] => {\n\t\treturn items.map((i) => {\n\t\t\tif (i.id === item.id) {\n\t\t\t\treturn { ...i, isExpanded: true };\n\t\t\t}\n\t\t\tif (i.isDirectory && item.path.startsWith(i.path)) {\n\t\t\t\treturn { ...i, isExpanded: true, children: expandParentFolders(item, i.children) };\n\t\t\t}\n\t\t\treturn i;\n\t\t});\n\t};\n\n\tconst searchItems = (items: Item[], query: string): Item[] => {\n\t\treturn items.reduce((result, item) => {\n\t\t\tif (item.isDirectory) {\n\t\t\t\tconst matchingChildren = searchItems(item.children, query);\n\t\t\t\tif (matchingChildren.length > 0) {\n\t\t\t\t\tconst expandedItem = { ...item, isExpanded: true, children: matchingChildren };\n\t\t\t\t\tresult.push(expandedItem);\n\t\t\t\t}\n\t\t\t} else if (item.name.toLowerCase().includes(query.toLowerCase())) {\n\t\t\t\tresult.push(item);\n\t\t\t}\n\t\t\treturn result;\n\t\t}, [] as Item[]);\n\t};\n\n\tconst renderItems = (items: Item[], indentationLevel = 0): ReactNode[] => {\n\t\treturn items.map((item) => (\n\t\t\t<Box key={item.path} flexDirection=\"column\">\n\t\t\t\t<Box marginLeft={indentationLevel} key={item.id}>\n\t\t\t\t\t<Text color={item.id === currentItemId ? 'green' : selectedItems.find((selectedItem) => selectedItem.id === item.id) ? 'cyan' : 'white'}>\n\t\t\t\t\t\t{selectedItems.find((selectedItem) => selectedItem.id === item.id) ? '[X]' : '[ ]'}{' '}\n\t\t\t\t\t\t{item.isDirectory ? '🗂️ ' : '📄 '} {item.name}{item.isDirectory && \"/\"}\n\t\t\t\t\t</Text>\n\t\t\t\t</Box>\n\t\t\t\t{item.isDirectory && item.isExpanded && item.children.length > 0 && renderItems(item.children, indentationLevel + 1)}\n\t\t\t</Box>\n\t\t));\n\t};\n\n\tconst filteredItems = searchQuery ? searchItems(items, searchQuery) : items;\n\tconst expandedItems = filteredItems.reduce((result, item) => {\n\t\tif (item.isDirectory && item.isExpanded) {\n\t\t\treturn expandParentFolders(item, result);\n\t\t}\n\t\treturn result;\n\t}, filteredItems);\n\n\tuseEffect(() => {\n\n\t\t// Get the first file that is not a directory\n\t\tconst firstFile = expandedItems[0];\n\t\tif (firstFile && firstFile.isDirectory) {\n\t\t\tconst itemsInFolder = getItemsFromFolder(firstFile);\n\t\t\tconst firstItem = itemsInFolder.find((item) => !item.isDirectory);\n\t\t\tif (searchQuery === \"\") {\n\t\t\t\tsetCurrentItemId(firstFile.id);\n\t\t\t} else {\n\t\t\t\tsetCurrentItemId(firstItem?.id || null);\n\t\t\t}\n\t\t} else {\n\t\t\tif (firstFile) {\n\t\t\t\tsetCurrentItemId(firstFile.id);\n\t\t\t}\n\t\t}\n\n\t}, [searchQuery]);\n\n\treturn (\n\t\t<Box flexDirection=\"column\" marginTop={2} marginBottom={2}>\n\t\t\t<Box flexDirection=\"column\" marginBottom={1}>\n\t\t\t\t<Text>Select files and folders to include.</Text>\n\t\t\t\t<Text>\n\t\t\t\t\tSelected files: <Text color=\"cyan\">{selectedItems.length}</Text>\n\t\t\t\t</Text>\n\t\t\t\t<Text>\n\t\t\t\t\tSearch query: {searchQuery ? searchQuery : <Text color=\"gray\" italic>None</Text>}\n\t\t\t\t</Text>\n\t\t\t</Box>\n\t\t\t<Box marginBottom={1} flexDirection=\"column\">\n\t\t\t\t{renderItems(expandedItems)}\n\t\t\t\t{expandedItems.length === 0 && <Text color=\"gray\" italic>No items found</Text>}\n\t\t\t</Box>\n\t\t\t<Box flexDirection=\"column\">\n\t\t\t\t<Text>\n\t\t\t\t\tUse <Text color=\"green\">Up</Text> / <Text color=\"green\">Down</Text> to\n\t\t\t\t\tnavigate, and <Text color=\"green\">Left</Text> /{' '}\n\t\t\t\t\t<Text color=\"green\">Right</Text> to select\n\t\t\t\t</Text>\n\t\t\t\t<Text>\n\t\t\t\t\tUse <Text color=\"green\">Tab</Text> to expand/collapse, and{' '}\n\t\t\t\t\t<Text color=\"green\">Enter</Text> to copy selected files.\n\t\t\t\t</Text>\n\t\t\t</Box>\n\t\t\t<Box marginTop={1}>\n\t\t\t\t{message && message}\n\t\t\t</Box>\n\t\t</Box>\n\t);\n};\n\nexport default App;"
  },
  {
    "path": "source/cli.tsx",
    "content": "#!/usr/bin/env node\nimport React from 'react';\nimport { render } from 'ink';\nimport App from './app.js';\n// import meow from 'meow';\n\n// This code is commented out in case we want to use it later to pass arguments to the CLI\n// const cli = meow(\n//     `\n// \tUsage\n// \t  $ aiformat\n\n// \tOptions\n// \t\t--name  Your name\n\n// \tExamples\n// \t  $ aiformat --name=Jane\n// \t  Hello, Jane\n// `,\n//     {\n//         importMeta: import.meta,\n//         flags: {\n//             name: {\n//                 type: 'string',\n//             },\n//         },\n//     },\n// );\n\nrender(<App />);\n"
  },
  {
    "path": "source/utils/generateOutput.ts",
    "content": "import fs from 'fs';\n\ninterface FileOrFolder {\n    id: string;\n    name: string;\n    isDirectory: boolean;\n    children: FileOrFolder[];\n    path: string;\n    isExpanded: boolean;\n    level: number;\n}\n\nconst cleanupFileTree = (fileTree: FileOrFolder[]): FileOrFolder[] => {\n    const idSet = new Set<string>();\n\n    function traverse(node: FileOrFolder) {\n        if (idSet.has(node.id)) {\n            return null;\n        }\n        idSet.add(node.id);\n\n        if (node.isDirectory) {\n            node.children = node.children.map(traverse).filter(Boolean) as FileOrFolder[];\n        }\n\n        return node;\n    }\n\n    return fileTree.map(traverse).filter(Boolean) as FileOrFolder[];\n};\n\nexport const outputXml = (fileTree: FileOrFolder[]): {\n    content: string;\n    fileCount: number;\n} => {\n    const cleanedFileTree = cleanupFileTree(fileTree);\n\n    function generateXml(node: FileOrFolder, parentPath: string = ''): string {\n        const currentPath = parentPath ? `${parentPath}/${node.name}` : node.name;\n\n        if (node.isDirectory) {\n            const childXml = node.children.map(child => generateXml(child, currentPath)).join('\\n');\n            return `<folder name=\"${currentPath}\">\\n${childXml}\\n</folder>`;\n        } else {\n            const fileContent = fs.readFileSync(node.path, 'utf8');\n            return `<file name=\"${currentPath}\">\\n${fileContent}\\n</file>`;\n        }\n    }\n\n    function countFiles(node: FileOrFolder): number {\n        if (node.isDirectory) {\n            return node.children.reduce((acc, child) => acc + countFiles(child), 0);\n        } else {\n            return 1;\n        }\n    }\n\n    return {\n        content: cleanedFileTree.map(node => generateXml(node)).join('\\n\\n'),\n        fileCount: cleanedFileTree.reduce((acc, node) => acc + countFiles(node), 0),\n    };\n};"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n\t\"extends\": \"@sindresorhus/tsconfig\",\n\t\"compilerOptions\": {\n\t\t\"outDir\": \"dist\"\n\t},\n\t\"include\": [\"source\"]\n}\n"
  }
]