├── apps └── cf-demo │ ├── bun.lockb │ ├── src │ ├── App.css │ ├── vite-env.d.ts │ ├── lib │ │ ├── types.ts │ │ └── utils.ts │ ├── main.tsx │ ├── components │ │ ├── progress.tsx │ │ ├── ui │ │ │ ├── label.tsx │ │ │ ├── textarea.tsx │ │ │ ├── progress.tsx │ │ │ ├── input.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── tabs.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ └── select.tsx │ │ ├── file-viewer.tsx │ │ ├── repo-header.tsx │ │ └── file-explorer.tsx │ ├── App.tsx │ ├── app │ │ └── page.tsx │ ├── pages │ │ ├── home.tsx │ │ └── repo.tsx │ ├── assets │ │ ├── Cloudflare_Logo.svg │ │ └── react.svg │ └── index.css │ ├── README.md │ ├── tsconfig.worker.json │ ├── tsconfig.json │ ├── .gitignore │ ├── index.html │ ├── components.json │ ├── vite.config.ts │ ├── wrangler.jsonc │ ├── tsconfig.node.json │ ├── tsconfig.app.json │ ├── eslint.config.js │ ├── public │ └── vite.svg │ ├── package.json │ └── worker │ ├── index.ts │ ├── lib │ └── walk-tree.ts │ └── do │ └── readonly-repo-object.ts ├── index.ts ├── packages ├── git-sqlite-cli │ ├── .gitignore │ ├── src │ │ ├── index.ts │ │ ├── types │ │ │ └── minimist.d.ts │ │ ├── cli.ts │ │ └── commands │ │ │ ├── ls-tree.ts │ │ │ └── clone.ts │ ├── tsconfig.json │ └── package.json └── sqlite-fs │ ├── README.md │ ├── src │ ├── index.ts │ ├── path-utils.ts │ ├── error-utils.ts │ ├── types.ts │ ├── errors.ts │ ├── schema.ts │ ├── interfaces.ts │ ├── stats-utils.ts │ ├── bun-sqlite-adapter.ts │ ├── do-sqlite-adapter.ts │ └── sqlite-fs-adapter.ts │ ├── .gitignore │ ├── package.json │ ├── tsconfig.json │ └── tests │ ├── path-utils.test.ts │ ├── error-utils.test.ts │ ├── stats-utils.test.ts │ ├── bun-sqlite-adapter.test.ts │ └── sqlite-fs-adapter.test.ts ├── package.json ├── .gitignore ├── README.md ├── tsconfig.json ├── OpenCode.md ├── pnpm-lock.yaml └── docs └── steps ├── step_8.md ├── step_7.md ├── step_3.md ├── step_5.md ├── step_4.md ├── step_6.md └── step_2.md /apps/cf-demo/bun.lockb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/cf-demo/src/App.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | console.log("Hello via Bun!"); -------------------------------------------------------------------------------- /packages/git-sqlite-cli/.gitignore: -------------------------------------------------------------------------------- 1 | data 2 | -------------------------------------------------------------------------------- /apps/cf-demo/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/sqlite-fs/README.md: -------------------------------------------------------------------------------- 1 | # sqlite-fs 2 | 3 | To install dependencies: 4 | 5 | ```bash 6 | bun install 7 | ``` 8 | -------------------------------------------------------------------------------- /packages/sqlite-fs/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./interfaces"; 2 | export * from "./types"; 3 | export { SQLiteFSAdapter } from "./sqlite-fs-adapter"; 4 | -------------------------------------------------------------------------------- /apps/cf-demo/README.md: -------------------------------------------------------------------------------- 1 | # Git Durable Object Demo 2 | 3 | This is a demo of using sqlite-fs together with isomorphic-git to create Git repositories backed by Cloudflare Durable Objects. 4 | -------------------------------------------------------------------------------- /apps/cf-demo/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export type FileEntry = { 2 | path: string; 3 | name: string; 4 | type: "file" | "directory"; 5 | children?: FileEntry[]; 6 | oid: string; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/git-sqlite-cli/src/index.ts: -------------------------------------------------------------------------------- 1 | // src/index.ts 2 | // This file serves as the main entry point for the package 3 | // It simply re-exports the CLI functionality 4 | 5 | import './cli.js'; -------------------------------------------------------------------------------- /apps/cf-demo/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /packages/git-sqlite-cli/src/types/minimist.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'minimist' { 2 | export interface ParsedArgs { 3 | _: string[]; 4 | [key: string]: any; 5 | } 6 | 7 | export default function parseArgs(args: string[], opts?: any): ParsedArgs; 8 | } -------------------------------------------------------------------------------- /apps/cf-demo/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.tsx' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /apps/cf-demo/tsconfig.worker.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.node.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.worker.tsbuildinfo", 5 | "types": ["@cloudflare/workers-types/2023-07-01", "vite/client", "node"] 6 | }, 7 | "include": ["./worker-configuration.d.ts", "./worker"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/cf-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" }, 6 | { "path": "./tsconfig.worker.json" } 7 | ], 8 | "compilerOptions": { 9 | "baseUrl": ".", 10 | "paths": { 11 | "@/*": ["./src/*"] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /apps/cf-demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .wrangler 26 | dist 27 | -------------------------------------------------------------------------------- /apps/cf-demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Git Durable Object Demo 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/git-sqlite-cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "outDir": "dist", 11 | "declaration": true, 12 | "types": ["bun-types"] 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules", "dist"] 16 | } -------------------------------------------------------------------------------- /packages/sqlite-fs/src/path-utils.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | export const dirname = path.dirname; 4 | export const basename = path.basename; 5 | export const join = path.join; 6 | export const normalize = path.normalize; // Useful for handling '.' and '..' 7 | 8 | // Custom helper for handling root path case 9 | export function getParentPath(p: string): string { 10 | const parent = path.dirname(p); 11 | return parent === p ? "" : parent; // Handle root case 12 | } 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cf-git-durable-object", 3 | "module": "index.ts", 4 | "type": "module", 5 | "private": true, 6 | "workspaces": [ 7 | "packages/*", 8 | "apps/*" 9 | ], 10 | "devDependencies": { 11 | "@types/bun": "latest" 12 | }, 13 | "peerDependencies": { 14 | "typescript": "^5" 15 | }, 16 | "packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0" 17 | } 18 | -------------------------------------------------------------------------------- /apps/cf-demo/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/index.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /apps/cf-demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import { cloudflare } from "@cloudflare/vite-plugin"; 4 | import path from "node:path"; 5 | 6 | import tailwindcss from "@tailwindcss/vite"; 7 | 8 | // https://vite.dev/config/ 9 | export default defineConfig({ 10 | // @ts-expect-error some dependency incompatibility 11 | plugins: [tailwindcss(), react(), cloudflare()], 12 | resolve: { 13 | alias: { 14 | "@": path.resolve(__dirname, "./src"), 15 | }, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies (bun install) 2 | node_modules 3 | 4 | # output 5 | out 6 | dist 7 | *.tgz 8 | 9 | # code coverage 10 | coverage 11 | *.lcov 12 | 13 | # logs 14 | logs 15 | _.log 16 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 | 18 | # dotenv environment variable files 19 | .env 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | .env.local 24 | 25 | # caches 26 | .eslintcache 27 | .cache 28 | *.tsbuildinfo 29 | 30 | # IntelliJ based IDEs 31 | .idea 32 | 33 | # Finder (MacOS) folder config 34 | .DS_Store 35 | .opencode 36 | -------------------------------------------------------------------------------- /packages/sqlite-fs/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies (bun install) 2 | node_modules 3 | 4 | # output 5 | out 6 | dist 7 | *.tgz 8 | 9 | # code coverage 10 | coverage 11 | *.lcov 12 | 13 | # logs 14 | logs 15 | _.log 16 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 | 18 | # dotenv environment variable files 19 | .env 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | .env.local 24 | 25 | # caches 26 | .eslintcache 27 | .cache 28 | *.tsbuildinfo 29 | 30 | # IntelliJ based IDEs 31 | .idea 32 | 33 | # Finder (MacOS) folder config 34 | .DS_Store 35 | -------------------------------------------------------------------------------- /apps/cf-demo/src/components/progress.tsx: -------------------------------------------------------------------------------- 1 | import { Progress as ProgressBar } from "./ui/progress"; 2 | 3 | export function Progress({ 4 | phase, 5 | loaded, 6 | total, 7 | }: { 8 | phase: string; 9 | loaded: number; 10 | total: number; 11 | }) { 12 | return ( 13 |
14 |

{phase}

15 |
16 | 17 |
18 |

{`${loaded} / ${total}`}

19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /apps/cf-demo/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Route, Switch } from "wouter"; 2 | import "./App.css"; 3 | import { HomePage } from "./pages/home"; 4 | import { RepoPage } from "./pages/repo"; 5 | 6 | function App() { 7 | return ( 8 | 9 | 10 | 11 | 12 | {(params) => } 13 | 14 | 15 | {/* Default route in a switch */} 16 | 404: No such page! 17 | 18 | ); 19 | } 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Git Durable Object Demo 2 | 3 | This is a proof of concept for using isomorphic-git with Cloudflare Durable Objects to create a Git repository that is backed by a SQLite database. 4 | 5 | ## Packages 6 | 7 | - [sqlite-fs](./packages/sqlite-fs): A node fs implementation that uses SQLite as a backend, provides a bun sqlite adapater and a durable object sqlite adapater. 8 | - [git-sqlite-clit](./packages/git-sqlite-cli): Proof of concept cli app using the bun sqlite backend. 9 | - [cf-demo](./apps/cf-demo): A cloudflare workers/durable object demo app showcasing `clone` and `fetch` 10 | -------------------------------------------------------------------------------- /packages/sqlite-fs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sqlite-fs", 3 | "module": "src/index.ts", 4 | "type": "module", 5 | "exports": { 6 | ".": "./src/index.ts", 7 | "./bun": "./src/bun-sqlite-adapter.ts", 8 | "./do": "./src/do-sqlite-adapter.ts" 9 | }, 10 | "scripts": { 11 | "build": "bun build src/index.ts --outdir=dist --format=esm", 12 | "typecheck": "tsc --noEmit", 13 | "test": "vitest run" 14 | }, 15 | "devDependencies": { 16 | "@types/bun": "latest", 17 | "vitest": "^3.1.2" 18 | }, 19 | "peerDependencies": { 20 | "typescript": "^5" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/cf-demo/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-durable-object-demo", 3 | "main": "worker/index.ts", 4 | "compatibility_date": "2025-05-01", 5 | "compatibility_flags": [ 6 | "nodejs_compat" 7 | ], 8 | "assets": { "not_found_handling": "single-page-application" }, 9 | "durable_objects": { 10 | "bindings": [ 11 | { 12 | "name": "READONLY_REPO", 13 | "class_name": "ReadonlyRepoObject", 14 | } 15 | ] 16 | }, 17 | "migrations": [ 18 | { 19 | "tag": "v1", 20 | "new_sqlite_classes": [ 21 | "ReadonlyRepoObject" 22 | ] 23 | } 24 | ], 25 | "observability": { 26 | "enabled": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/cf-demo/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | function Label({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | ) 20 | } 21 | 22 | export { Label } 23 | -------------------------------------------------------------------------------- /apps/cf-demo/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/git-sqlite-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-sqlite-cli", 3 | "module": "src/index.ts", 4 | "type": "module", 5 | "bin": { 6 | "git-sqlite": "./dist/index.js" 7 | }, 8 | "scripts": { 9 | "build": "bun build src/index.ts --outdir=dist --format=esm", 10 | "typecheck": "tsc --noEmit", 11 | "test": "bun test", 12 | "start": "bun run src/cli.ts" 13 | }, 14 | "devDependencies": { 15 | "@types/bun": "latest", 16 | "@types/minimist": "^1.2.5", 17 | "@types/node": "^20.11.30", 18 | "typescript": "^5" 19 | }, 20 | "dependencies": { 21 | "chalk": "^5.4.1", 22 | "isomorphic-git": "^1.30.1", 23 | "minimist": "^1.2.8", 24 | "sqlite-fs": "workspace:*" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Environment setup & latest features 4 | "lib": ["ESNext"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedIndexedAccess": true, 22 | 23 | // Some stricter flags (disabled by default) 24 | "noUnusedLocals": false, 25 | "noUnusedParameters": false, 26 | "noPropertyAccessFromIndexSignature": false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/cf-demo/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import { FileExplorer } from "@/components/file-explorer" 3 | import { FileViewer } from "@/components/file-viewer" 4 | import { RepoHeader } from "@/components/repo-header" 5 | 6 | export default function Home() { 7 | const [selectedFile, setSelectedFile] = useState(null) 8 | const [currentPath, setCurrentPath] = useState([]) 9 | 10 | return ( 11 |
12 | 13 |
14 | 15 | 16 |
17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /packages/sqlite-fs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Environment setup & latest features 4 | "lib": ["ESNext"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedIndexedAccess": true, 22 | 23 | // Some stricter flags (disabled by default) 24 | "noUnusedLocals": false, 25 | "noUnusedParameters": false, 26 | "noPropertyAccessFromIndexSignature": false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/sqlite-fs/src/error-utils.ts: -------------------------------------------------------------------------------- 1 | // src/error-utils.ts 2 | import type { FSError } from './types'; 3 | 4 | /** 5 | * Creates an error object mimicking Node.js filesystem errors. 6 | */ 7 | export function createError( 8 | code: string, 9 | path?: string, 10 | syscall?: string, 11 | message?: string 12 | ): FSError { 13 | const displayPath = path ? `'${path}'` : ''; 14 | const displaySyscall = syscall ? ` ${syscall}` : ''; 15 | const baseMessage = message || `${code}:${displaySyscall}${displayPath}`; 16 | 17 | const error = new Error(baseMessage) as FSError; 18 | error.code = code; 19 | if (path) error.path = path; 20 | if (syscall) error.syscall = syscall; 21 | 22 | // Could potentially add errno mapping here if needed, but code is primary identifier 23 | return error; 24 | } -------------------------------------------------------------------------------- /packages/sqlite-fs/src/types.ts: -------------------------------------------------------------------------------- 1 | // src/types.ts 2 | 3 | // Basic structure for file system errors 4 | export interface FSError extends Error { 5 | code?: string; 6 | path?: string; 7 | syscall?: string; 8 | } 9 | 10 | // Basic structure mimicking Node.js Stats object (key properties) 11 | // We'll refine this later based on SQLiteFSAdapter needs 12 | export interface Stats { 13 | isFile(): boolean; 14 | isDirectory(): boolean; 15 | isSymbolicLink(): boolean; 16 | size: number; 17 | mtimeMs: number; 18 | mode: number; 19 | // Add other common fields with placeholder types if needed now 20 | atimeMs?: number; 21 | ctimeMs?: number; 22 | birthtimeMs?: number; 23 | dev?: number; ino?: number; nlink?: number; uid?: number; gid?: number; 24 | rdev?: number; 25 | blksize?: number; 26 | blocks?: number; 27 | } -------------------------------------------------------------------------------- /apps/cf-demo/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 |