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).
- 🚀 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.promisesAPI - 🔁 Callback Support: Works with both promises and callbacks
- ⏰ Auto-expiry: Work sessions automatically expire after 30 days
npm install github-filesystemYou'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 tokenUPSTASH_REDIS_REST_URL- Upstash Redis REST URLUPSTASH_REDIS_REST_TOKEN- Upstash Redis REST token
import { GitHubFS } from "github-filesystem";
// Credentials are automatically read from environment variables:
// - GITHUB_TOKEN
// - UPSTASH_REDIS_REST_URL
// - UPSTASH_REDIS_REST_TOKEN
const fs = new GitHubFS({
repo: "owner/repo-name",
namespace: "my-project", // Used to organize work sessions
branch: "main", // Optional, defaults to "main"
});In instant mode, every operation immediately writes to GitHub:
// Write a file (creates a commit)
await fs.writeFile("docs/README.md", "# Hello World");
// Read a file
const content = await fs.readFile("docs/README.md");
console.log(content.toString());
// List directory contents
const entries = await fs.readdir("docs");
entries.forEach(entry => {
console.log(`${entry.name} (${entry.type})`);
});
// Create a directory
await fs.mkdir("src/components", { recursive: true });
// Delete a file (creates a commit)
await fs.deleteFile("old-file.txt");In commit mode, changes are staged in Upstash KV and committed together:
// Start a work session
const sessionId = await fs.startWork();
console.log(`Started work session: ${sessionId}`);
// Make multiple changes (all staged in KV)
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");
// Read operations check KV first, then GitHub
const content = await fs.readFile("file1.txt");
// Commit all changes as a single commit
await fs.commitWork("Add multiple files and reorganize structure");Resume an interrupted work session:
// Try to resume the last work session for this namespace
const sessionId = await fs.resumeWork();
if (sessionId) {
console.log(`Resumed work session: ${sessionId}`);
// Continue making changes
await fs.writeFile("another-file.txt", "More content");
// Commit when done
await fs.commitWork("Complete the interrupted work");
} else {
console.log("No previous work session found");
}Cancel a work session without committing:
await fs.startWork();
// Make some changes...
await fs.writeFile("temp.txt", "Temporary content");
// Decide not to commit
await fs.cancelWork(); // All staged changes are discardedGitHubFS 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).
All methods return Promises and work exactly like Node.js fs.promises:
import { GitHubFS } from "github-filesystem";
const fs = new GitHubFS({ repo: "owner/repo" });
// Read/write files
await fs.readFile("file.txt");
await fs.writeFile("file.txt", "content");
await fs.appendFile("file.txt", "more content");
// File operations
await fs.copyFile("src.txt", "dest.txt");
await fs.rename("old.txt", "new.txt");
await fs.unlink("file.txt");
// Directory operations
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 });
// File info
const stats = await fs.stat("file.txt");
console.log(stats.isFile(), stats.isDirectory(), stats.size);
await fs.access("file.txt"); // Check if file exists
const exists = await fs.exists("file.txt"); // Returns booleanFor compatibility with older code, callback-based methods are also available:
// Read file with callback
fs.readFileCallback("file.txt", (err, data) => {
if (err) throw err;
console.log(data.toString());
});
// Write file with callback
fs.writeFileCallback("file.txt", "content", (err) => {
if (err) throw err;
console.log("File written!");
});
// Read directory with callback
fs.readdirCallback("path", (err, files) => {
if (err) throw err;
console.log(files);
});
// Stat with callback
fs.statCallback("file.txt", (err, stats) => {
if (err) throw err;
console.log(stats.isFile());
});
// Other callback methods:
// mkdirCallback, unlinkCallback, rmdirCallback, existsCallbackSynchronous operations are not supported for remote filesystems and will throw an error:
try {
fs.readFileSync("file.txt"); // Throws error
} catch (error) {
console.error("Synchronous operations are not supported");
}You can use GitHubFS as a drop-in replacement for fs.promises:
// Before (using Node.js fs)
import { promises as fs } from "fs";
// After (using GitHubFS)
import { GitHubFS } from "github-filesystem";
const fs = new GitHubFS({ repo: "owner/repo" });
// All your existing code works the same!
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.
new GitHubFS(config: GitHubFSConfig)Config Options:
repo: GitHub repository in format "owner/repo"namespace: Namespace for organizing work sessionsbranch: Git branch to work with (default: "main")
Environment Variables (Required):
GITHUB_TOKEN: GitHub personal access tokenUPSTASH_REDIS_REST_URL: Upstash Redis REST URLUPSTASH_REDIS_REST_TOKEN: Upstash Redis REST token
Start a work session (commit mode). Returns the session ID.
Resume the last work session for this namespace. Returns the session ID if found, null otherwise.
Commit all staged changes to GitHub with the given commit message.
Cancel the current work session without committing changes.
Get the current mode ("instant" or "commit").
Get the current work session ID (if in commit mode).
Read a file. In commit mode, checks KV first, then falls back to GitHub.
Write a file. In instant mode, creates a commit immediately. In commit mode, stages the change in KV.
Append to a file. Creates the file if it doesn't exist.
Copy a file from source to destination.
Rename or move a file.
Delete a file. Alias for deleteFile().
Delete a file. In instant mode, creates a commit immediately. In commit mode, stages the deletion.
List directory contents. Returns an array of entries with name, type, path, and optional sha/size.
Options:
withFileTypes: ReturnDirentobjects instead of strings (default: false)
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)
Remove a directory. Alias for rm().
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)
Get file or directory statistics. Returns a Stats object with isFile(), isDirectory(), size, etc.
Get file or directory statistics (alias for stat(), no symlink support).
Check if a file exists and is accessible. Throws if not accessible.
Check if a file or directory exists. Returns true or false.
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)
These methods throw an error:
readFileSync(),writeFileSync(),readdirSync(),statSync(),mkdirSync(),unlinkSync(),rmdirSync(),existsSync()
- Each operation directly interacts with the GitHub API
- Every write/delete creates a new commit
- Simple but creates many commits
- 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
- Session metadata is stored at
{namespace}/session resumeWork()retrieves the last session for the namespace- Allows continuing work after interruptions or across different environments
const fs = new GitHubFS({ /* config */ });
await fs.startWork();
// Update multiple documentation files
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");const fs = new GitHubFS({ /* config */ });
await fs.startWork();
try {
// Try some changes
await fs.writeFile("config.json", JSON.stringify(newConfig));
await fs.deleteFile("old-config.json");
// Test if everything works...
const valid = await validateConfig();
if (valid) {
await fs.commitWork("Update configuration");
} else {
await fs.cancelWork(); // Rollback
}
} catch (error) {
await fs.cancelWork(); // Rollback on error
throw error;
}const fs = new GitHubFS({ /* config */ });
// Start or resume work
let sessionId = await fs.resumeWork();
if (!sessionId) {
sessionId = await fs.startWork();
}
// Process files incrementally
for (const file of largeFileList) {
await processAndWrite(fs, file);
// Can stop and resume later...
}
await fs.commitWork("Process all files");Credentials are automatically loaded from environment variables. Set them in your .env file or environment:
# .env file
GITHUB_TOKEN=ghp_your_github_token
UPSTASH_REDIS_REST_URL=https://your-redis.upstash.io
UPSTASH_REDIS_REST_TOKEN=your_upstash_tokenThen use the class:
const fs = new GitHubFS({
repo: process.env.GITHUB_REPO || "owner/repo",
namespace: process.env.NAMESPACE || "default",
});try {
await fs.readFile("non-existent.txt");
} catch (error) {
console.error("File not found:", error);
}
try {
await fs.startWork();
await fs.startWork(); // Error: already in commit mode
} catch (error) {
console.error("Cannot start work:", error);
}- 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
.gitkeepfiles
MIT
Contributions are welcome! Please open an issue or submit a pull request.