diff --git a/package-lock.json b/package-lock.json
index 713fe8402..912ca559c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3357,6 +3357,14 @@
"node": ">=10.0.0"
}
},
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -4547,6 +4555,11 @@
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
},
+ "node_modules/async-lock": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz",
+ "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ=="
+ },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -5148,6 +5161,11 @@
"node": ">= 10.0"
}
},
+ "node_modules/clean-git-ref": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz",
+ "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw=="
+ },
"node_modules/clean-stack": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@@ -5499,6 +5517,17 @@
"node": ">=10.0.0"
}
},
+ "node_modules/crc-32": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+ "bin": {
+ "crc32": "bin/crc32.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/cross-fetch": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz",
@@ -5768,6 +5797,11 @@
"node": ">=0.3.1"
}
},
+ "node_modules/diff3": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz",
+ "integrity": "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g=="
+ },
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -7226,7 +7260,6 @@
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
"integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
- "dev": true,
"engines": {
"node": ">= 4"
}
@@ -7615,6 +7648,43 @@
"whatwg-fetch": "^3.4.1"
}
},
+ "node_modules/isomorphic-git": {
+ "version": "1.25.10",
+ "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.25.10.tgz",
+ "integrity": "sha512-IxGiaKBwAdcgBXwIcxJU6rHLk+NrzYaaPKXXQffcA0GW3IUrQXdUPDXDo+hkGVcYruuz/7JlGBiuaeTCgIgivQ==",
+ "dependencies": {
+ "async-lock": "^1.4.1",
+ "clean-git-ref": "^2.0.1",
+ "crc-32": "^1.2.0",
+ "diff3": "0.0.3",
+ "ignore": "^5.1.4",
+ "minimisted": "^2.0.0",
+ "pako": "^1.0.10",
+ "pify": "^4.0.1",
+ "readable-stream": "^3.4.0",
+ "sha.js": "^2.4.9",
+ "simple-get": "^4.0.1"
+ },
+ "bin": {
+ "isogit": "cli.cjs"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/isomorphic-git/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/istanbul-lib-coverage": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
@@ -8402,6 +8472,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/minimisted": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz",
+ "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==",
+ "dependencies": {
+ "minimist": "^1.2.5"
+ }
+ },
"node_modules/minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
@@ -9303,6 +9381,11 @@
"node": ">= 0.8"
}
},
+ "node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -9423,6 +9506,14 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pify": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/pixelmatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz",
@@ -10280,6 +10371,18 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
+ "node_modules/sha.js": {
+ "version": "2.4.11",
+ "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+ "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ },
+ "bin": {
+ "sha.js": "bin.js"
+ }
+ },
"node_modules/shallow-clone": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
@@ -11947,6 +12050,11 @@
"packages/git": {
"version": "1.0.0",
"license": "AGPL-3.0-only",
+ "dependencies": {
+ "@pkgjs/parseargs": "^0.11.0",
+ "buffer": "^6.0.3",
+ "isomorphic-git": "^1.25.10"
+ },
"devDependencies": {
"@rollup/plugin-commonjs": "^24.1.0",
"@rollup/plugin-node-resolve": "^15.0.2",
@@ -11956,6 +12064,48 @@
"rollup-plugin-copy": "^3.4.0"
}
},
+ "packages/git/node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
+ "packages/git/node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
"packages/phoenix": {
"name": "@heyputer/phoenix",
"version": "0.0.0",
@@ -11988,13 +12138,6 @@
"node-pty": "^1.0.0"
}
},
- "packages/phoenix/node_modules/@pkgjs/parseargs": {
- "version": "0.11.0",
- "license": "MIT",
- "engines": {
- "node": ">=14"
- }
- },
"packages/phoenix/node_modules/@sinonjs/fake-timers": {
"version": "11.2.2",
"license": "BSD-3-Clause",
@@ -12079,10 +12222,6 @@
"version": "3.0.9",
"license": "MIT"
},
- "packages/phoenix/node_modules/path-browserify": {
- "version": "1.0.1",
- "license": "MIT"
- },
"packages/phoenix/node_modules/randomstring": {
"version": "1.3.0",
"license": "MIT",
diff --git a/packages/git/package.json b/packages/git/package.json
index 2e0f90e45..30cac607d 100644
--- a/packages/git/package.json
+++ b/packages/git/package.json
@@ -16,5 +16,10 @@
"mocha": "^10.2.0",
"rollup": "^3.21.4",
"rollup-plugin-copy": "^3.4.0"
+ },
+ "dependencies": {
+ "@pkgjs/parseargs": "^0.11.0",
+ "buffer": "^6.0.3",
+ "isomorphic-git": "^1.25.10"
}
}
diff --git a/packages/git/src/git-command-definition.js b/packages/git/src/git-command-definition.js
new file mode 100644
index 000000000..6ff37f0ab
--- /dev/null
+++ b/packages/git/src/git-command-definition.js
@@ -0,0 +1,39 @@
+/*
+ * 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 .
+ */
+
+/**
+ * The command definition for `git` itself, in the same format as subcommands.
+ */
+export default {
+ name: 'git',
+ usage: 'git [--version] [--help] [command] [command-args...]',
+ description: 'Git version-control client for Puter.',
+ args: {
+ options: {
+ help: {
+ description: 'Display help information for git itself, or a subcommand.',
+ type: 'boolean',
+ },
+ version: {
+ description: 'Display version information about git.',
+ type: 'boolean',
+ },
+ },
+ },
+};
diff --git a/packages/git/src/help.js b/packages/git/src/help.js
new file mode 100644
index 000000000..bd0b9c02a
--- /dev/null
+++ b/packages/git/src/help.js
@@ -0,0 +1,143 @@
+/*
+ * 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 .
+ */
+
+/**
+ * Throw this from a subcommand's execute() in order to print its usage text to stderr.
+ * @type {symbol}
+ */
+export const SHOW_USAGE = Symbol('SHOW_USAGE');
+
+/**
+ * Full manual page for the command.
+ * @param command
+ * @returns {string}
+ */
+export const produce_help_string = (command) => {
+ const { name, usage, description, args } = command;
+ const options = args?.options;
+
+ let s = '';
+ const indent = ' ';
+
+ const heading = (text) => {
+ s += `\n\x1B[34;1m${text}:\x1B[0m\n`
+ };
+
+ heading('SYNOPSIS');
+ if (!usage) {
+ s += `${indent}git ${name}\n`;
+ } else if (typeof usage === 'string') {
+ s += `${indent}${usage}\n`;
+ } else {
+ let first = true;
+ for (const usage_line of usage) {
+ if (first) {
+ first = false;
+ s += `${indent}${usage_line}\n`;
+ } else {
+ s += `${indent}${usage_line}\n`;
+ }
+ }
+ }
+
+ if (description) {
+ heading('DESCRIPTION');
+ s += `${indent}${description}\n`;
+ }
+
+ if (typeof options === 'object' && Object.keys(options).length > 0) {
+ heading('OPTIONS');
+ // Figure out how long each invocation is, so we can align the descriptions
+ for (const [name, option] of Object.entries(options)) {
+ // Invocation
+ s += indent;
+ if (option.short)
+ s += `-${option.short}, `;
+ s += `--${name}`;
+ if (option.type !== 'boolean')
+ s += ` <${option.type}>`;
+ s += '\n';
+
+ // Description
+ s += `${indent}${indent}${option.description}\n\n`;
+ }
+ }
+
+ if (!s.endsWith('\n\n'))
+ s += '\n';
+
+ return s;
+}
+
+/**
+ * Usage for the command, which is a short summary.
+ * @param command
+ * @returns {string}
+ */
+export const produce_usage_string = (command) => {
+ const { name, usage, args } = command;
+ const options = args?.options;
+
+ let s = '';
+
+ // Usage
+ if (!usage) {
+ s += `usage: git ${name}\n`;
+ } else if (typeof usage === 'string') {
+ s += `usage: ${usage}\n`;
+ } else {
+ let first = true;
+ for (const usage_line of usage) {
+ if (first) {
+ first = false;
+ s += `usage: ${usage_line}\n`;
+ } else {
+ s += ` or: ${usage_line}\n`;
+ }
+ }
+ }
+
+ // List of options
+ if (typeof options === 'object' && Object.keys(options).length > 0) {
+ // Figure out how long each invocation is, so we can align the descriptions
+ const option_strings = Object.entries(options).map(([name, option]) => {
+ let invocation = '';
+ if (option.short)
+ invocation += `-${option.short}, `;
+ invocation += `--${name}`;
+ if (option.type !== 'boolean')
+ invocation += ` <${option.type}>`;
+
+ return [invocation, option.description];
+ });
+
+ const indent_size = 2 + option_strings.reduce(
+ (max_length, option) => Math.max(max_length, option[0].length), 0);
+
+ s += '\n';
+ for (const [invocation, description] of option_strings) {
+ s += ` ${invocation}`;
+ if (indent_size - invocation.length > 0)
+ s += ' '.repeat(indent_size - invocation.length);
+ s += `${description}\n`;
+ }
+ }
+
+ return s;
+}
diff --git a/packages/git/src/main.js b/packages/git/src/main.js
index e871d539d..adb1f744b 100644
--- a/packages/git/src/main.js
+++ b/packages/git/src/main.js
@@ -16,6 +16,147 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
-window.main = () => {
- console.log('Well hello friends!');
-}
\ No newline at end of file
+import { parseArgs } from '@pkgjs/parseargs';
+import subcommands from './subcommands/__exports__.js';
+import git_command from './git-command-definition.js';
+import fs from './filesystem.js';
+import git from 'isomorphic-git';
+import { Buffer } from 'buffer';
+import { produce_usage_string, SHOW_USAGE } from './help.js';
+
+const encoder = new TextEncoder();
+
+window.Buffer = Buffer;
+
+window.main = async () => {
+ const shell = puter.ui.parentApp();
+ if (!shell) {
+ await puter.ui.alert('Git must be run from a terminal. Try `git --help`');
+ puter.exit();
+ return;
+ }
+
+ shell.on('close', () => {
+ console.log('Shell closed; exiting git...');
+ puter.exit();
+ });
+
+ const stdout = (message) => {
+ shell.postMessage({
+ $: 'stdout',
+ data: encoder.encode(message + '\n'),
+ });
+ };
+ // TODO: Separate stderr message?
+ const stderr = stdout;
+
+ const url_params = new URL(document.location).searchParams;
+ const puter_args = JSON.parse(url_params.get('puter.args')) ?? {};
+ const { command_line, env } = puter_args;
+
+ // isomorphic-git assumes the Node.js process object exists,
+ // so fill-in the parts it uses.
+ window.process = {
+ cwd: () => env.PWD,
+ platform: 'puter',
+ }
+
+ // Git's command structure is a little unusual:
+ // > git [options-for-git] [subcommand [options-and-args-for-subcommand]]
+ // Also, a couple of options (--help and --version) are syntactic sugar for `help` and `version` subcommands.
+ // The approach here is to first try and parse these top-level options, and then based on that, run a subcommand.
+
+ // If no raw args, just print help and exit
+ const raw_args = command_line?.args ?? [];
+ if (raw_args.length === 0) {
+ stdout(produce_usage_string(git_command));
+ puter.exit();
+ return;
+ }
+
+ const { values: global_options, positionals: global_positionals } = parseArgs({
+ options: git_command.args.options,
+ allowPositionals: true,
+ args: raw_args,
+ strict: false,
+ });
+
+ let subcommand_name = null;
+ let first_positional_is_subcommand = false;
+ if (global_options.help) {
+ subcommand_name = 'help';
+ } else if (global_options.version) {
+ subcommand_name = 'version';
+ }
+
+ if (!subcommand_name) {
+ subcommand_name = global_positionals[0];
+ first_positional_is_subcommand = true;
+ }
+
+ // See if we're running a subcommand we recognize
+ let exit_code = 0;
+ const subcommand = subcommands[subcommand_name];
+ if (!subcommand) {
+ stderr(`git: '${subcommand_name}' is not a recognized git command. See 'git --help'`);
+ puter.exit(1);
+ return;
+ }
+
+ // Try and remove the subcommand positional arg, and any global options, from args.
+ const subcommand_args = raw_args;
+ const remove_arg = (arg) => {
+ const index = subcommand_args.indexOf(arg);
+ if (index >= 0)
+ subcommand_args.splice(index, 1);
+
+ }
+ remove_arg('--help');
+ remove_arg('--version');
+ if (first_positional_is_subcommand) {
+ // TODO: This is not a 100% reliable way to do this, as it may also match the value of `--option-with-value value`
+ // But that's not a problem until we add some global options that take a value.
+ remove_arg(subcommand_name);
+ }
+
+ // Parse the remaining args scoped to this subcommand, and run it.
+ let parsed_args;
+ try {
+ parsed_args = parseArgs({
+ ...subcommand.args,
+ args: subcommand_args,
+ });
+ } catch (e) {
+ stderr(produce_usage_string(subcommand));
+ puter.exit(1);
+ return;
+ }
+
+ const ctx = {
+ io: {
+ stdout,
+ stderr,
+ },
+ fs,
+ args: {
+ options: parsed_args.values,
+ positionals: parsed_args.positionals,
+ },
+ env,
+ };
+
+ try {
+ exit_code = await subcommand.execute(ctx) ?? 0;
+ } catch (e) {
+ if (e === SHOW_USAGE) {
+ stderr(produce_usage_string(subcommand));
+ } else {
+ stderr(`fatal: ${e.message}`);
+ console.error(e);
+ }
+ exit_code = 1;
+ }
+
+ // TODO: Support passing an exit code to puter.exit();
+ puter.exit(exit_code);
+}
diff --git a/packages/git/src/subcommands/__exports__.js b/packages/git/src/subcommands/__exports__.js
new file mode 100644
index 000000000..1633f161f
--- /dev/null
+++ b/packages/git/src/subcommands/__exports__.js
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell 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 .
+ */
+// Generated by /tools/gen.js
+import module_help from './help.js'
+import module_version from './version.js'
+
+export default {
+ "help": module_help,
+ "version": module_version,
+};
diff --git a/packages/git/src/subcommands/help.js b/packages/git/src/subcommands/help.js
new file mode 100644
index 000000000..1a27f8315
--- /dev/null
+++ b/packages/git/src/subcommands/help.js
@@ -0,0 +1,67 @@
+/*
+ * 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 git from 'isomorphic-git';
+import { ErrorCodes } from '@heyputer/puter-js-common/src/PosixError.js';
+import subcommands from './__exports__.js';
+import git_command from '../git-command-definition.js';
+import { produce_help_string } from '../help.js';
+
+export default {
+ name: 'help',
+ usage: ['git help [-a|--all]', 'git help '],
+ description: `Display help information for git itself, or a subcommand.`,
+ args: {
+ allowPositionals: true,
+ options: {
+ all: {
+ description: 'List all available subcommands.',
+ type: 'boolean',
+ }
+ },
+ },
+ execute: async (ctx) => {
+ const { io, fs, env, args } = ctx;
+ const { stdout, stderr } = io;
+ const { options, positionals } = args;
+
+ if (options.all) {
+ stdout(`See 'git help ' for more information.\n`);
+ const max_name_length = Object.keys(subcommands).reduce((max, name) => Math.max(max, name.length), 0);
+ for (const [name, command] of Object.entries(subcommands)) {
+ stdout(` ${name} ${' '.repeat(Math.max(max_name_length - name.length, 0))} ${command.description || ''}`);
+ }
+ return;
+ }
+
+ if (positionals.length > 0) {
+ // Try and display help page for the subcommand
+ const subcommand_name = positionals[0];
+ const subcommand = subcommands[subcommand_name];
+ if (!subcommand)
+ throw new Error(`No manual entry for ${subcommand_name}`);
+
+ stdout(produce_help_string(subcommand));
+
+ return;
+ }
+
+ // No subcommand name, so show general help
+ stdout(produce_help_string(git_command));
+ }
+}
diff --git a/packages/git/src/subcommands/version.js b/packages/git/src/subcommands/version.js
new file mode 100644
index 000000000..2e639f0d0
--- /dev/null
+++ b/packages/git/src/subcommands/version.js
@@ -0,0 +1,40 @@
+/*
+ * 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 git from 'isomorphic-git';
+import path from 'path-browserify';
+import { ErrorCodes } from '@heyputer/puter-js-common/src/PosixError.js';
+
+const VERSION = '1.0.0';
+
+export default {
+ name: 'version',
+ usage: 'git version',
+ description: `Display version information about git.`,
+ args: {
+ allowPositionals: false,
+ options: {},
+ },
+ execute: async (ctx) => {
+ const { io, fs, env, args } = ctx;
+ const { stdout, stderr } = io;
+ const { options, positionals } = args;
+
+ stdout(`Puter git version ${VERSION} (isomorphic git version ${git.version()})`);
+ }
+}