-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.ts
More file actions
executable file
·288 lines (234 loc) · 7.64 KB
/
index.ts
File metadata and controls
executable file
·288 lines (234 loc) · 7.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
#!/usr/bin/env bun
import { AutomergeUrl, Repo } from "@automerge/automerge-repo"
import { BrowserWebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket"
import type { ImportedFile, Folder, FolderItem } from './types';
import { Command, createArgument } from 'commander';
import ora from 'ora';
import path from 'path';
import { mkdir, readdir, stat, copyFile, readFile, writeFile } from 'fs/promises';
import { cosmiconfig } from 'cosmiconfig';
import ignore, { Ignore } from 'ignore';
import type { Stats } from 'fs';
import isBinaryPath from 'is-binary-path';
import mime from "mime-types";
const repo = new Repo({
network: [
new BrowserWebSocketClientAdapter("wss://sync.automerge.org")
]
});
interface FileInfo {
path: string;
relativePath: string;
stats: Stats;
}
interface Config {
defaultDestination?: string;
defaultSource?: string;
}
interface CommandOptions {
dest?: string;
}
// Global ignore patterns
let ig: Ignore;
const initIgnorePatterns = async (ignoreFile = '.gitignore'): Promise<void> => {
ig = ignore();
// Default patterns
ig.add(['node_modules', '.git', '.DS_Store']);
try {
const patterns = await readFile(ignoreFile, 'utf8');
ig.add(patterns);
} catch (err) {
// If ignore file doesn't exist, just use defaults
if (err.code !== 'ENOENT') {
console.warn(`Warning: Error reading ignore file: ${err.message}`);
}
}
};
// Configuration explorer
const explorer = cosmiconfig('filesync');
const loadConfig = async (): Promise<Config> => {
try {
const result = await explorer.search();
return result?.config ?? {};
} catch (err) {
console.error('Error loading config:', err instanceof Error ? err.message : err);
return {};
}
};
async function createAutomergeDocuments(startPath: string) {
// Create the root folder handle that will accumulate all documents
const folderHandle = repo.create<Folder>({
name: path.basename(startPath),
contentType: "application/vnd.automerge.folder",
contents: []
})
async function dfs(currentPath: string, parentHandle = folderHandle) {
const stats = await stat(currentPath)
const relativePath = path.relative(startPath, currentPath)
// Skip if path matches gitignore rules
if (relativePath && ig.ignores(relativePath)) {
console.log("ignoring: " + currentPath)
return parentHandle
}
console.log("recursing: " + currentPath)
if (stats.isFile()) {
const fileHandle = repo.create<ImportedFile>()
const isBinary = isBinaryPath(currentPath);
const buffer = await readFile(currentPath);
const mimeType = mime.lookup(currentPath);
console.log({ currentPath, mimeType, isBinary })
if (isBinary) {
fileHandle.change(d => {
d.contents = Uint8Array.from(buffer)
d.contentType = mimeType || "application/octet-stream"
d.name = path.basename(currentPath)
d.executable = !!(stats.mode & 0o111)
})
} else {
const contents = await readFile(currentPath, 'utf-8')
fileHandle.change(d => {
d.contents = contents
d.contentType = mimeType || "text/plain"
d.name = path.basename(currentPath)
d.executable = !!(stats.mode & 0o111)
})
}
parentHandle.change(d => {
d.contents.push({
name: path.basename(currentPath),
automergeUrl: fileHandle.url
})
})
return parentHandle
}
if (stats.isDirectory()) {
const dirHandle = repo.create<Folder>({
name: path.basename(currentPath),
contentType: "application/vnd.automerge.folder",
contents: []
})
const children = await readdir(currentPath)
for (const child of children) {
await dfs(path.join(currentPath, child), dirHandle)
}
parentHandle.change(d => {
d.contents.push({
name: path.basename(currentPath),
automergeUrl: dirHandle.url
})
})
return parentHandle
}
}
await dfs(startPath)
return folderHandle
}
async function downloadAutomergeDocuments(
rootUrl: AutomergeUrl,
outputPath: string
) {
console.log(rootUrl)
const rootHandle = repo.find<Folder | ImportedFile>(rootUrl)
const rootDoc = await rootHandle.doc()
console.log(rootHandle.state)
console.log(rootDoc)
async function downloadItem(doc: Folder | ImportedFile, currentPath: string) {
// TODO:
// We need to check mimetypes
if ('contents' in doc && Array.isArray(doc.contents)) {
// This is a folder
const folderPath = path.join(currentPath, doc.name)
console.log(folderPath)
await mkdir(folderPath, { recursive: true })
// Recursively process all items in the folder
for (const item of doc.contents) {
const itemHandle = repo.find(item.automergeUrl)
const itemDoc = await itemHandle.doc()
await downloadItem(itemDoc, folderPath)
}
} else {
// This is a file
const filePath = path.join(currentPath, doc.name)
if (typeof doc.contents === 'string') {
await writeFile(filePath, doc.contents, 'utf-8')
} else if (doc.contents instanceof Uint8Array) {
await writeFile(filePath, doc.contents)
}
//if (doc.executable) {
// await chmod(filePath, 0o755)
//}
}
}
await downloadItem(rootDoc, outputPath)
}
async function* walk(dir: string, root = dir): AsyncGenerator<FileInfo> {
const files = await readdir(dir);
for (const file of files) {
const filepath = path.join(dir, file);
const relativePath = path.relative(root, filepath);
// Skip if file matches ignore patterns
if (ig.ignores(relativePath)) {
continue;
}
const stats = await stat(filepath);
if (stats.isDirectory()) {
yield* walk(filepath, root);
} else {
yield {
path: filepath,
relativePath,
stats
};
}
}
}
const processFile = async (fileInfo: FileInfo, destDir: string): Promise<string> => {
const destPath = path.join(destDir, fileInfo.relativePath);
console.log({ destPath, fileInfo })
// await mkdir(path.dirname(destPath), { recursive: true });
// await copyFile(fileInfo.path, destPath);
return destPath;
};
const pull = async (source: string, path: string): Promise<void> => {
console.log(`Listing all files in ${source}:`);
const s = <AutomergeUrl>(source)
try {
const folderHandle = await downloadAutomergeDocuments(s, path.dest)
repo.shutdown()
} catch (err) {
console.error('List failed:', err instanceof Error ? err.message : err);
process.exit(1);
}
};
const push = async (source: string): Promise<void> => {
console.log(`Listing all files in ${source}:`);
try {
const folderHandle = await createAutomergeDocuments(source)
console.log(folderHandle.url)
repo.shutdown()
} catch (err) {
console.error('List failed:', err instanceof Error ? err.message : err);
process.exit(1);
}
};
const program = new Command();
// Global ignore file option
program
.name('filesync')
.description('CLI to sync files between directories')
.version('0.1.0')
.option('-i, --ignore <path>', 'Path to ignore file (defaults to .gitignore)');
// Initialize ignore patterns before running commands
program.hook('preAction', async () => {
await initIgnorePatterns(program.opts().ignore);
});
program.command('pull')
.description('Pull files from source')
.argument('<source>', 'Source Automerge URL')
.option('-d, --dest <path>', 'Destination directory')
.action(pull);
program.command('push')
.description('Push all files in directory into Automerge')
.argument('<source>', 'Source directory')
.action(push);
program.parse();