automerge daemon to keep local files in sync

feat: createAutomergeDocuments can read a path and upload all the files and folders in it recursively

Changed files
+94 -14
+87 -7
index.ts
··· 2 2 3 3 import { AutomergeUrl, Repo } from "@automerge/automerge-repo" 4 4 import { BrowserWebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket" 5 - import { Command } from 'commander'; 5 + import type { ImportedFile, Folder, FolderItem } from './types'; 6 + import { Command, createArgument } from 'commander'; 6 7 import ora from 'ora'; 7 8 import path from 'path'; 8 9 import { mkdir, readdir, stat, copyFile, readFile } from 'fs/promises'; ··· 64 65 } 65 66 }; 66 67 68 + async function populateDocs(dir: string, docs: Doc[]): Doc[] { 69 + const files = await readdir(dir); 70 + for (const file of files) { 71 + const filepath = path.join(dir, file); 72 + // Skip if file matches ignore patterns 73 + if (ig.ignores(filepath)) { 74 + continue; 75 + } 76 + const stats = await stat(filepath); 77 + if (stats.isDirectory()) { 78 + 79 + } 80 + } 81 + } 82 + 83 + async function createAutomergeDocuments(startPath: string) { 84 + // Create the root folder handle that will accumulate all documents 85 + const folderHandle = repo.create<Folder>({ 86 + name: path.basename(startPath), 87 + contents: [] 88 + }) 89 + 90 + async function dfs(currentPath: string, parentHandle = folderHandle) { 91 + const stats = await stat(currentPath) 92 + 93 + const relativePath = path.relative(startPath, currentPath) 94 + // Skip if path matches gitignore rules 95 + if (relativePath && ig.ignores(relativePath)) { 96 + console.log("ignoring: " + currentPath) 97 + return parentHandle 98 + } 99 + 100 + console.log("recursing: " + currentPath) 101 + 102 + if (stats.isFile()) { 103 + const fileHandle = repo.create<ImportedFile>() 104 + const contents = await readFile(currentPath, 'utf-8') 105 + 106 + fileHandle.change(d => { 107 + d.contents = contents 108 + d.name = path.basename(currentPath) 109 + d.executable = !!(stats.mode & 0o111) 110 + }) 111 + 112 + parentHandle.change(d => { 113 + d.contents.push({ 114 + name: path.basename(currentPath), 115 + automergeUrl: fileHandle.url 116 + }) 117 + }) 118 + 119 + return parentHandle 120 + } 121 + 122 + if (stats.isDirectory()) { 123 + const dirHandle = repo.create<Folder>({ 124 + name: path.basename(currentPath), 125 + contents: [] 126 + }) 127 + 128 + const children = await readdir(currentPath) 129 + 130 + for (const child of children) { 131 + await dfs(path.join(currentPath, child), dirHandle) 132 + } 133 + 134 + parentHandle.change(d => { 135 + d.contents.push({ 136 + name: path.basename(currentPath), 137 + automergeUrl: dirHandle.url 138 + }) 139 + }) 140 + 141 + return parentHandle 142 + } 143 + } 144 + 145 + await dfs(startPath) 146 + return folderHandle 147 + } 148 + 67 149 async function* walk(dir: string, root = dir): AsyncGenerator<FileInfo> { 68 150 const files = await readdir(dir); 69 151 ··· 92 174 93 175 const processFile = async (fileInfo: FileInfo, destDir: string): Promise<string> => { 94 176 const destPath = path.join(destDir, fileInfo.relativePath); 95 - console.log({destPath, fileInfo}) 177 + console.log({ destPath, fileInfo }) 96 178 // await mkdir(path.dirname(destPath), { recursive: true }); 97 179 // await copyFile(fileInfo.path, destPath); 98 180 return destPath; ··· 146 228 console.log(`Listing all files in ${source}:`); 147 229 148 230 try { 149 - for await (const fileInfo of walk(source)) { 150 - console.log(`- ${fileInfo.relativePath}`); 151 - console.log(` Size: ${fileInfo.stats.size} bytes`); 152 - console.log(` Modified: ${fileInfo.stats.mtime}`); 153 - } 231 + const folderHandle = await createAutomergeDocuments(source) 232 + console.log(folderHandle.url) 233 + repo.shutdown() 154 234 } catch (err) { 155 235 console.error('List failed:', err instanceof Error ? err.message : err); 156 236 process.exit(1);
+7 -7
types.ts
··· 1 1 import { AutomergeUrl } from "@automerge/automerge-repo" 2 2 3 - export interface File { 4 - name: string 5 - contentType: string 6 - executable: boolean 7 - contents: string | Uint8Array 3 + export interface ImportedFile { 4 + name: string 5 + contentType: string 6 + executable: boolean 7 + contents: string | Uint8Array 8 8 } 9 9 10 10 export interface Folder { 11 - name: string 12 - contents: FolderItem[] 11 + name: string 12 + contents: FolderItem[] 13 13 } 14 14 15 15 export interface FolderItem {