automerge daemon to keep local files in sync

feat: pull from Automerge URL and save to filesystem

Changed files
+59 -48
+59 -48
index.ts
··· 6 6 import { Command, createArgument } from 'commander'; 7 7 import ora from 'ora'; 8 8 import path from 'path'; 9 - import { mkdir, readdir, stat, copyFile, readFile } from 'fs/promises'; 9 + import { mkdir, readdir, stat, copyFile, readFile, writeFile } from 'fs/promises'; 10 10 import { cosmiconfig } from 'cosmiconfig'; 11 11 import ignore, { Ignore } from 'ignore'; 12 12 import type { Stats } from 'fs'; 13 + 13 14 14 15 const repo = new Repo({ 15 16 network: [ ··· 131 132 return folderHandle 132 133 } 133 134 135 + async function downloadAutomergeDocuments( 136 + rootUrl: AutomergeUrl, 137 + outputPath: string 138 + ) { 139 + console.log(rootUrl) 140 + const rootHandle = repo.find<Folder | ImportedFile>(rootUrl) 141 + const rootDoc = await rootHandle.doc() 142 + console.log(rootHandle.state) 143 + console.log(rootDoc) 144 + 145 + async function downloadItem(doc: Folder | ImportedFile, currentPath: string) { 146 + // TODO: 147 + // We need to check mimetypes 148 + if ('contents' in doc && Array.isArray(doc.contents)) { 149 + // This is a folder 150 + const folderPath = path.join(currentPath, doc.name) 151 + console.log(folderPath) 152 + await mkdir(folderPath, { recursive: true }) 153 + 154 + // Recursively process all items in the folder 155 + for (const item of doc.contents) { 156 + const itemHandle = repo.find(item.automergeUrl) 157 + const itemDoc = await itemHandle.doc() 158 + await downloadItem(itemDoc, folderPath) 159 + } 160 + } else { 161 + // This is a file 162 + const filePath = path.join(currentPath, doc.name) 163 + 164 + if (typeof doc.contents === 'string') { 165 + await writeFile(filePath, doc.contents, 'utf-8') 166 + } else if (doc.contents instanceof Uint8Array) { 167 + await writeFile(filePath, doc.contents) 168 + } 169 + 170 + //if (doc.executable) { 171 + // await chmod(filePath, 0o755) 172 + //} 173 + } 174 + } 175 + 176 + await downloadItem(rootDoc, outputPath) 177 + } 178 + 179 + 134 180 async function* walk(dir: string, root = dir): AsyncGenerator<FileInfo> { 135 181 const files = await readdir(dir); 136 182 ··· 165 211 return destPath; 166 212 }; 167 213 168 - const push = async (source: string, options: CommandOptions): Promise<void> => { 169 - const spinner = ora('Pushing files...').start(); 170 - const config = await loadConfig(); 171 - const destination = options.dest ?? config.defaultDestination ?? './dest'; 172 - let fileCount = 0; 173 214 174 - try { 175 - await mkdir(destination, { recursive: true }); 176 - 177 - for await (const fileInfo of walk(source)) { 178 - spinner.text = `Processing: ${fileInfo.relativePath}`; 179 - await processFile(fileInfo, destination); 180 - fileCount++; 181 - } 182 - 183 - spinner.succeed(`Successfully pushed ${fileCount} files to ${destination}`); 184 - } catch (err) { 185 - spinner.fail(`Push failed: ${err instanceof Error ? err.message : String(err)}`); 186 - process.exit(1); 187 - } 188 - }; 189 - 190 - const pull = async (source: string, options: CommandOptions): Promise<void> => { 191 - const spinner = ora('Pulling files...').start(); 192 - const config = await loadConfig(); 193 - const destination = options.dest ?? config.defaultSource ?? './src'; 194 - let fileCount = 0; 215 + const pull = async (source: string, path: string): Promise<void> => { 216 + console.log(`Listing all files in ${source}:`); 217 + const s = <AutomergeUrl>(source) 195 218 196 219 try { 197 - await mkdir(destination, { recursive: true }); 198 - 199 - for await (const fileInfo of walk(source)) { 200 - spinner.text = `Processing: ${fileInfo.relativePath}`; 201 - await processFile(fileInfo, destination); 202 - fileCount++; 203 - } 204 - 205 - spinner.succeed(`Successfully pulled ${fileCount} files to ${destination}`); 220 + const folderHandle = await downloadAutomergeDocuments(s, path.dest) 221 + repo.shutdown() 206 222 } catch (err) { 207 - spinner.fail(`Pull failed: ${err instanceof Error ? err.message : String(err)}`); 223 + console.error('List failed:', err instanceof Error ? err.message : err); 208 224 process.exit(1); 209 225 } 210 226 }; 211 227 212 - const list = async (source: string): Promise<void> => { 228 + const push = async (source: string): Promise<void> => { 213 229 console.log(`Listing all files in ${source}:`); 214 230 215 231 try { ··· 221 237 process.exit(1); 222 238 } 223 239 }; 240 + 224 241 225 242 const program = new Command(); 226 243 ··· 236 253 await initIgnorePatterns(program.opts().ignore); 237 254 }); 238 255 239 - program.command('push') 240 - .description('Push files to destination') 241 - .argument('<source>', 'Source directory') 242 - .option('-d, --dest <path>', 'Destination directory') 243 - .action(push); 244 - 245 256 program.command('pull') 246 257 .description('Pull files from source') 247 - .argument('<source>', 'Source directory') 258 + .argument('<source>', 'Source Automerge URL') 248 259 .option('-d, --dest <path>', 'Destination directory') 249 260 .action(pull); 250 261 251 - program.command('list') 252 - .description('List all files in directory') 262 + program.command('push') 263 + .description('Push all files in directory into Automerge') 253 264 .argument('<source>', 'Source directory') 254 - .action(list); 265 + .action(push); 255 266 256 267 program.parse();