github-filesystem
A TypeScript library that allows you to interact with GitHub repositories like a file system. Supports both instant mode (direct writes to GitHub) and commit mode (stage changes in Upstash KV before committing).
Features
- 🚀 Instant Mode: Write directly to GitHub on each operation
- 📦 Commit Mode: Stage multiple changes and commit them together
- 💾 Resume Work: Resume interrupted work sessions from Upstash KV
- 🔄 Full File System API:
writeFile, readFile, readdir, mkdir, deleteFile
- 📁 Node.js fs Compatible: Drop-in replacement for
fs.promises API
- 🔁 Callback Support: Works with both promises and callbacks
- ⏰ Auto-expiry: Work sessions automatically expire after 30 days
Installation
npm install github-filesystem
Prerequisites
You'll need:
- A GitHub personal access token with repo permissions
- An Upstash Redis database (for commit mode)
Important: Credentials are read from environment variables:
GITHUB_TOKEN - GitHub personal access token
UPSTASH_REDIS_REST_URL - Upstash Redis REST URL
UPSTASH_REDIS_REST_TOKEN - Upstash Redis REST token
Usage
Basic Setup
import { GitHubFS } from "github-filesystem";
const fs = new GitHubFS({
repo: "owner/repo-name",
namespace: "my-project",
branch: "main",
});
Instant Mode
In instant mode, every operation immediately writes to GitHub:
await fs.writeFile("docs/README.md", "# Hello World");
const content = await fs.readFile("docs/README.md");
console.log(content.toString());
const entries = await fs.readdir("docs");
entries.forEach(entry => {
console.log(`${entry.name} (${entry.type})`);
});
await fs.mkdir("src/components", { recursive: true });
await fs.deleteFile("old-file.txt");
Commit Mode
In commit mode, changes are staged in Upstash KV and committed together:
const sessionId = await fs.startWork();
console.log(`Started work session: ${sessionId}`);
await fs.writeFile("file1.txt", "Content 1");
await fs.writeFile("file2.txt", "Content 2");
await fs.mkdir("new-folder", { recursive: true });
await fs.deleteFile("old-file.txt");
const content = await fs.readFile("file1.txt");
await fs.commitWork("Add multiple files and reorganize structure");
Resume Work
Resume an interrupted work session:
const sessionId = await fs.resumeWork();
if (sessionId) {
console.log(`Resumed work session: ${sessionId}`);
await fs.writeFile("another-file.txt", "More content");
await fs.commitWork("Complete the interrupted work");
} else {
console.log("No previous work session found");
}
Cancel Work
Cancel a work session without committing:
await fs.startWork();
await fs.writeFile("temp.txt", "Temporary content");
await fs.cancelWork();
Node.js fs Compatible API
GitHubFS implements a Node.js fs-compatible API, making it a drop-in replacement for most file system operations (except for startWork/commitWork/cancelWork which are GitHubFS-specific).
fs.promises API (Recommended)
All methods return Promises and work exactly like Node.js fs.promises:
import { GitHubFS } from "github-filesystem";
const fs = new GitHubFS({ repo: "owner/repo" });
await fs.readFile("file.txt");
await fs.writeFile("file.txt", "content");
await fs.appendFile("file.txt", "more content");
await fs.copyFile("src.txt", "dest.txt");
await fs.rename("old.txt", "new.txt");
await fs.unlink("file.txt");
await fs.readdir("path/to/dir");
await fs.mkdir("new-dir", { recursive: true });
await fs.rmdir("dir", { recursive: true });
await fs.rm("file-or-dir", { recursive: true, force: true });
const stats = await fs.stat("file.txt");
console.log(stats.isFile(), stats.isDirectory(), stats.size);
await fs.access("file.txt");
const exists = await fs.exists("file.txt");
Callback-based API
For compatibility with older code, callback-based methods are also available:
fs.readFileCallback("file.txt", (err, data) => {
if (err) throw err;
console.log(data.toString());
});
fs.writeFileCallback("file.txt", "content", (err) => {
if (err) throw err;
console.log("File written!");
});
fs.readdirCallback("path", (err, files) => {
if (err) throw err;
console.log(files);
});
fs.statCallback("file.txt", (err, stats) => {
if (err) throw err;
console.log(stats.isFile());
});
Synchronous API (Not Supported)
Synchronous operations are not supported for remote filesystems and will throw an error:
try {
fs.readFileSync("file.txt");
} catch (error) {
console.error("Synchronous operations are not supported");
}
Using as a Drop-in Replacement
You can use GitHubFS as a drop-in replacement for fs.promises:
import { promises as fs } from "fs";
import { GitHubFS } from "github-filesystem";
const fs = new GitHubFS({ repo: "owner/repo" });
const content = await fs.readFile("file.txt");
await fs.writeFile("output.txt", content);
const files = await fs.readdir(".");
Note: The only difference is you need to use startWork()/commitWork() if you want to batch changes, otherwise each operation creates an immediate commit.
API Reference
Constructor
new GitHubFS(config: GitHubFSConfig)
Config Options:
repo: GitHub repository in format "owner/repo"
namespace: Namespace for organizing work sessions
branch: Git branch to work with (default: "main")
Environment Variables (Required):
GITHUB_TOKEN: GitHub personal access token
UPSTASH_REDIS_REST_URL: Upstash Redis REST URL
UPSTASH_REDIS_REST_TOKEN: Upstash Redis REST token
Mode Management
startWork(): Promise<string>
Start a work session (commit mode). Returns the session ID.
resumeWork(): Promise<string | null>
Resume the last work session for this namespace. Returns the session ID if found, null otherwise.
commitWork(message: string): Promise<void>
Commit all staged changes to GitHub with the given commit message.
cancelWork(): Promise<void>
Cancel the current work session without committing changes.
getMode(): FSMode
Get the current mode ("instant" or "commit").
getCurrentSessionId(): string | null
Get the current work session ID (if in commit mode).
File Operations (fs.promises compatible)
readFile(path: string, options?: ReadFileOptions): Promise<Buffer>
Read a file. In commit mode, checks KV first, then falls back to GitHub.
writeFile(path: string, content: Buffer | string, options?: WriteFileOptions): Promise<void>
Write a file. In instant mode, creates a commit immediately. In commit mode, stages the change in KV.
appendFile(path: string, data: string | Buffer, options?: WriteFileOptions): Promise<void>
Append to a file. Creates the file if it doesn't exist.
copyFile(src: string, dest: string): Promise<void>
Copy a file from source to destination.
rename(oldPath: string, newPath: string): Promise<void>
Rename or move a file.
unlink(path: string): Promise<void>
Delete a file. Alias for deleteFile().
deleteFile(path: string): Promise<void>
Delete a file. In instant mode, creates a commit immediately. In commit mode, stages the deletion.
readdir(path: string, options?: ReaddirOptions): Promise<DirEntry[] | string[]>
List directory contents. Returns an array of entries with name, type, path, and optional sha/size.
Options:
withFileTypes: Return Dirent objects instead of strings (default: false)
mkdir(path: string, options?: MkdirOptions): Promise<void>
Create a directory. Since GitHub doesn't support empty directories, this creates a .gitkeep file.
Options:
recursive: Create parent directories if they don't exist (default: false)
rmdir(path: string, options?: { recursive?: boolean }): Promise<void>
Remove a directory. Alias for rm().
rm(path: string, options?: RmOptions): Promise<void>
Remove a file or directory.
Options:
recursive: Remove directories and their contents recursively (default: false)
force: Ignore errors if file doesn't exist (default: false)
stat(path: string): Promise<Stats>
Get file or directory statistics. Returns a Stats object with isFile(), isDirectory(), size, etc.
lstat(path: string): Promise<Stats>
Get file or directory statistics (alias for stat(), no symlink support).
access(path: string, mode?: number): Promise<void>
Check if a file exists and is accessible. Throws if not accessible.
exists(path: string): Promise<boolean>
Check if a file or directory exists. Returns true or false.
Callback-based Methods
All promise-based methods have callback equivalents:
readFileCallback(path, [options], callback)
writeFileCallback(path, data, [options], callback)
readdirCallback(path, [options], callback)
statCallback(path, callback)
mkdirCallback(path, [options], callback)
unlinkCallback(path, callback)
rmdirCallback(path, [options], callback)
existsCallback(path, callback)
Synchronous Methods (Not Supported)
These methods throw an error:
readFileSync(), writeFileSync(), readdirSync(), statSync(), mkdirSync(), unlinkSync(), rmdirSync(), existsSync()
How It Works
Instant Mode
- Each operation directly interacts with the GitHub API
- Every write/delete creates a new commit
- Simple but creates many commits
Commit Mode
- Changes are stored in Upstash KV with a 30-day expiry
- KV keys are prefixed with
{namespace}/{sessionId}/{filepath}
- Session metadata tracks modified and deleted files
commitWork() creates a single commit with all changes
- Read operations check KV first (for staged changes), then GitHub
Resume Functionality
- Session metadata is stored at
{namespace}/session
resumeWork() retrieves the last session for the namespace
- Allows continuing work after interruptions or across different environments
Examples
Example 1: Batch Updates
const fs = new GitHubFS({ });
await fs.startWork();
const files = ["README.md", "CONTRIBUTING.md", "LICENSE"];
for (const file of files) {
const content = await fs.readFile(file);
const updated = content.toString().replace("2023", "2024");
await fs.writeFile(file, updated);
}
await fs.commitWork("Update copyright year to 2024");
Example 2: Safe Experimentation
const fs = new GitHubFS({ });
await fs.startWork();
try {
await fs.writeFile("config.json", JSON.stringify(newConfig));
await fs.deleteFile("old-config.json");
const valid = await validateConfig();
if (valid) {
await fs.commitWork("Update configuration");
} else {
await fs.cancelWork();
}
} catch (error) {
await fs.cancelWork();
throw error;
}
Example 3: Long-Running Tasks
const fs = new GitHubFS({ });
let sessionId = await fs.resumeWork();
if (!sessionId) {
sessionId = await fs.startWork();
}
for (const file of largeFileList) {
await processAndWrite(fs, file);
}
await fs.commitWork("Process all files");
Environment Variables
Credentials are automatically loaded from environment variables. Set them in your .env file or environment:
GITHUB_TOKEN=ghp_your_github_token
UPSTASH_REDIS_REST_URL=https://your-redis.upstash.io
UPSTASH_REDIS_REST_TOKEN=your_upstash_token
Then use the class:
const fs = new GitHubFS({
repo: process.env.GITHUB_REPO || "owner/repo",
namespace: process.env.NAMESPACE || "default",
});
Error Handling
try {
await fs.readFile("non-existent.txt");
} catch (error) {
console.error("File not found:", error);
}
try {
await fs.startWork();
await fs.startWork();
} catch (error) {
console.error("Cannot start work:", error);
}
Limitations
- GitHub API rate limits apply (5000 requests/hour for authenticated requests)
- Maximum file size: 100 MB (GitHub limit)
- Upstash KV expiry: 30 days maximum
- Empty directories are not supported (GitHub limitation) - use
.gitkeep files
License
MIT
Contributing
Contributions are welcome! Please open an issue or submit a pull request.