diff --git a/packages/git/src/filesystem.js b/packages/git/src/filesystem.js
new file mode 100644
index 000000000..88bf74db6
--- /dev/null
+++ b/packages/git/src/filesystem.js
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Puter's Git client.
+ *
+ * Puter's Git client is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { PosixError } from '@heyputer/puter-js-common/src/PosixError.js';
+import path_ from 'path-browserify';
+
+let debug = false;
+
+// Takes a Puter stat() result and converts it to the format expected by isomorphic-git
+const convert_stat = (stat, options) => {
+ // Puter returns the times as timestamps in seconds
+ const timestamp_date = (timestamp) => new Date(timestamp * 1000);
+ const timestamp_ms = (timestamp) => options.bigint ? BigInt(timestamp) * 1000n : timestamp * 1000;
+ const timestamp_ns = (timestamp) => options.bigint ? BigInt(timestamp) * 1000000n : undefined;
+
+ // We don't record ctime, but the most recent of atime and mtime is a reasonable approximation
+ const ctime = Math.max(stat.accessed, stat.modified);
+
+ const mode = (() => {
+ // Puter doesn't expose this, but we can approximate it based on the stats we have.
+ let user = stat.immutable ? 4 : 6;
+ let group = stat.immutable ? 4 : 6;
+ let other = stat.is_public ? 4 : 0;
+ // Octal number
+ return user << 6 | group << 3 | other;
+ })();
+
+ return {
+ dev: 1, // Puter doesn't expose this
+ ino: stat.id,
+ mode: mode,
+ nlink: 1, // Definition of hard-link number is platform-defined. Linux includes subdir count, Mac includes child count.
+ uid: stat.uid,
+ gid: stat.uid, // Puter doesn't have gids
+ rdev: 0,
+ size: stat.size,
+ blksize: 4096, // Abitrary!
+ blocks: Math.ceil(stat.size / 4096),
+ atime: timestamp_date(stat.accessed),
+ mtime: timestamp_date(stat.modified),
+ ctime: timestamp_date(ctime),
+ birthtime: timestamp_date(stat.created),
+ atimeMs: timestamp_ms(stat.accessed),
+ mtimeMs: timestamp_ms(stat.modified),
+ ctimeMs: timestamp_ms(ctime),
+ birthtimeMs: timestamp_ms(stat.created),
+ atimeNs: timestamp_ns(stat.accessed),
+ mtimeNs: timestamp_ns(stat.modified),
+ ctimeNs: timestamp_ns(ctime),
+ birthtimeNs: timestamp_ns(stat.created),
+
+ isBlockDevice: () => false,
+ isCharacterDevice: () => false,
+ isDirectory: () => stat.is_dir,
+ isFIFO: () => false,
+ isFile: () => !stat.is_dir,
+ isSocket: () => false,
+ isSymbolicLink: () => stat.is_symlink,
+ };
+};
+
+const adapt_path = (input_path) => {
+ if (input_path[0] === '/') return input_path;
+ return path_.relative(window.process.cwd(), input_path);
+};
+
+// Implements the API expected by isomorphic-git
+// See: https://isomorphic-git.org/docs/en/fs#using-the-promise-api-preferred
+export default {
+ enable_debugging: () => { debug = true; },
+ promises: {
+ readFile: async (path, options = {}) => {
+ if (debug) console.trace('readFile', path, options);
+ // TODO: Obey options
+ try {
+ const blob = await puter.fs.read(adapt_path(path));
+ if (options.encoding === 'utf8')
+ return await blob.text();
+ return blob.arrayBuffer();
+ } catch (e) {
+ throw PosixError.fromPuterAPIError(e);
+ }
+ },
+ writeFile: async (path, data, options = {}) => {
+ if (debug) console.trace('writeFile', path, data, options);
+ // TODO: Obey options
+
+ // Convert data into a type puter.fs.write() understands.
+ // Can be: | | |
+ // Puter supports: | |
+ if (
+ data instanceof window.Buffer // Buffer
+ || ArrayBuffer.isView(data) // TypedArray
+ || data instanceof DataView // DataView
+ ) {
+ data = new File([data], path_.basename(path));
+ }
+
+ try {
+ return await puter.fs.write(adapt_path(path), data);
+ } catch (e) {
+ throw PosixError.fromPuterAPIError(e);
+ }
+ },
+ unlink: async (path) => {
+ if (debug) console.trace('unlink', path);
+ // TODO: If `path` is a symlink, only remove the link
+ try {
+ return await puter.fs.delete(adapt_path(path), { recursive: false });
+ } catch (e) {
+ throw PosixError.fromPuterAPIError(e);
+ }
+ },
+ readdir: async (path, options = {}) => {
+ if (debug) console.trace('readdir', path, options);
+ // TODO: Obey options
+ try {
+ const results = await puter.fs.readdir(adapt_path(path));
+ // Puter returns an array of stat entries, but we only want the file names
+ return results.map(it => path_.basename(it.path));
+ } catch (e) {
+ throw PosixError.fromPuterAPIError(e);
+ }
+ },
+ mkdir: async (path, mode) => {
+ if (debug) console.trace('mkdir', path, mode);
+ // NOTE: Puter filesystem doesn't have file permissions
+ try {
+ return await puter.fs.mkdir(adapt_path(path));
+ } catch (e) {
+ throw PosixError.fromPuterAPIError(e);
+ }
+ },
+ rmdir: async (path) => {
+ if (debug) console.trace('rmdir', path);
+ // TODO: Only delete dir if it's empty
+ try {
+ return await puter.fs.delete(adapt_path(path), { recursive: true });
+ } catch (e) {
+ throw PosixError.fromPuterAPIError(e);
+ }
+ },
+ stat: async (path, options = {}) => {
+ if (debug) console.trace('stat', path, options);
+ // TODO: Obey options
+ try {
+ return convert_stat(await puter.fs.stat(adapt_path(path)), options);
+ } catch (e) {
+ throw PosixError.fromPuterAPIError(e);
+ }
+ },
+ lstat: async (path, options = {}) => {
+ if (debug) console.trace('lstat', path, options);
+ // TODO: Obey options
+ // TODO: Stat the link itself.
+ try {
+ return convert_stat(await puter.fs.stat(adapt_path(path)), options);
+ } catch (e) {
+ throw PosixError.fromPuterAPIError(e);
+ }
+ },
+ readlink: async (path, options = {}) => {
+ if (debug) console.trace('readlink', path, options);
+ try {
+ const stat = await puter.fs.stat(adapt_path(path));
+ return stat.symlink_path;
+ } catch (e) {
+ throw PosixError.fromPuterAPIError(e);
+ }
+ },
+ symlink: async (target, path, type) => {
+ if (debug) console.trace('symlink', target, path, type);
+ // TODO: Add symlink creation to puter.fs API
+ throw PosixError.OperationNotPermitted({ message: 'Puter.fs API does not support creating symlinks' });
+ },
+ chmod: async (path, mode) => {
+ if (debug) console.trace('chmod', path, mode);
+ // NOTE: No-op, Puter doesn't have file permissions
+ },
+ },
+};
\ No newline at end of file