git0 0.2.12 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +27780 -0
- package/dist/cli.js.map +331 -0
- package/docs/404.html +2 -2
- package/docs/Footer/index.html +2 -2
- package/docs/assets/js/1df93b7f.1385eba0.js +2 -0
- package/docs/assets/js/22dd74f7.e13ac7a4.js +1 -0
- package/docs/assets/js/28ab763d.192aa3bb.js +1 -0
- package/docs/assets/js/299d276b.c9496717.js +1 -0
- package/docs/assets/js/{68cef36b.c312447e.js → 68cef36b.e5b1975b.js} +1 -1
- package/docs/assets/js/a9cef9d5.a7253e41.js +1 -0
- package/docs/assets/js/c3a618e1.1c56fb03.js +590 -0
- package/docs/assets/js/{main.cf858a7d.js → main.1f646b09.js} +2 -2
- package/docs/assets/js/runtime~main.f69f8f44.js +1 -0
- package/docs/functions/fm/index.html +26 -0
- package/docs/functions/git0/index.html +6 -6
- package/docs/functions/github-api/index.html +20 -20
- package/docs/functions/index.html +889 -71
- package/docs/functions/modules/index.html +4 -3
- package/docs/index.html +2 -2
- package/docs/lunr-index-1749760982052.json +1 -0
- package/docs/lunr-index.json +1 -1
- package/docs/search-doc-1749760982052.json +1 -0
- package/docs/search-doc.json +1 -1
- package/docs/sitemap.xml +1 -1
- package/docs-config/.docusaurus/client-manifest.json +36 -24
- package/docs-config/.docusaurus/docusaurus-plugin-content-docs/default/p/index-466.json +1 -1
- package/docs-config/.docusaurus/docusaurus-plugin-content-docs/default/site-src-functions-fm-md-a9c.json +19 -0
- package/docs-config/.docusaurus/docusaurus-plugin-content-docs/default/site-src-functions-git-0-md-299.json +4 -0
- package/docs-config/.docusaurus/docusaurus-plugin-content-docs/default/site-src-functions-index-md-c3a.json +1 -1
- package/docs-config/.docusaurus/globalData.json +10 -5
- package/docs-config/.docusaurus/registry.js +1 -0
- package/docs-config/.docusaurus/routes.js +9 -3
- package/docs-config/.docusaurus/routesChunkNames.json +7 -3
- package/docs-config/src/functions/fm.md +1 -0
- package/docs-config/src/functions/git0.md +2 -2
- package/docs-config/src/functions/github-api.md +34 -17
- package/docs-config/src/functions/index.md +19 -30
- package/docs-config/src/functions/modules.md +1 -0
- package/docs-config/src/functions/typedoc-sidebar.cjs +5 -0
- package/docs-config/src/pages/index.tsx +2 -3
- package/package.json +20 -10
- package/readme.md +19 -21
- package/src/cli.ts +150 -0
- package/src/download.ts +240 -0
- package/src/fm.js +5 -5
- package/src/github-api.ts +237 -0
- package/src/ide.ts +141 -0
- package/src/install.ts +147 -0
- package/src/package-menu.ts +183 -0
- package/src/platform.ts +53 -0
- package/src/releases.ts +159 -0
- package/src/setup.ts +9 -0
- package/src/types.ts +97 -0
- package/src/utils.ts +49 -0
- package/test/download.test.ts +48 -0
- package/test/github-api.test.ts +79 -0
- package/test/package-menu.test.ts +134 -0
- package/test/platform.test.ts +64 -0
- package/test/releases.test.ts +169 -0
- package/docs/assets/js/1df93b7f.d8c05d2c.js +0 -2
- package/docs/assets/js/22dd74f7.237398b4.js +0 -1
- package/docs/assets/js/28ab763d.5714aa16.js +0 -1
- package/docs/assets/js/299d276b.1a1baa1c.js +0 -1
- package/docs/assets/js/c3a618e1.965a31da.js +0 -1
- package/docs/assets/js/runtime~main.7520dc36.js +0 -1
- package/docs/lunr-index-1749613752315.json +0 -1
- package/docs/search-doc-1749613752315.json +0 -1
- package/src/git0.js +0 -380
- package/src/github-api.js +0 -472
- /package/docs/assets/js/{1df93b7f.d8c05d2c.js.LICENSE.txt → 1df93b7f.1385eba0.js.LICENSE.txt} +0 -0
- /package/docs/assets/js/{main.cf858a7d.js.LICENSE.txt → main.1f646b09.js.LICENSE.txt} +0 -0
|
@@ -1,28 +1,14 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://i.imgur.com/
|
|
2
|
+
<img src="https://i.imgur.com/RLXLAzY.png" />
|
|
3
3
|
</p>
|
|
4
4
|
<p align="center">
|
|
5
|
-
<a href="https://discord.gg/SJdBqBz3tV">
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<a href="https://
|
|
12
|
-
<img alt="GitHub Discussions"
|
|
13
|
-
src="https://img.shields.io/github/discussions/vtempest/git0" />
|
|
14
|
-
</a>
|
|
15
|
-
<a href="https://github.com/vtempest/git0/pulse" alt="Activity">
|
|
16
|
-
<img src="https://img.shields.io/github/commit-activity/m/vtempest/git0" />
|
|
17
|
-
</a>
|
|
18
|
-
<img src="https://img.shields.io/github/last-commit/vtempest/git0.svg?style=flat-square" alt="GitHub last commit" />
|
|
19
|
-
</p>
|
|
20
|
-
<p align="center">
|
|
21
|
-
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg"
|
|
22
|
-
alt="PRs Welcome" />
|
|
23
|
-
<a href="https://codespaces.new/vtempest/git0">
|
|
24
|
-
<img src="https://github.com/codespaces/badge.svg" width="150" height="20" />
|
|
25
|
-
</a>
|
|
5
|
+
<a href="https://discord.gg/SJdBqBz3tV"><img src="https://img.shields.io/discord/1110227955554209923.svg?label=Chat&logo=Discord&colorB=7289da&style=flat"/></a>
|
|
6
|
+
<a href="https://github.com/vtempest/git0/discussions"><img alt="GitHub Stars" src="https://img.shields.io/github/stars/vtempest/git0" /></a>
|
|
7
|
+
<a href="https://github.com/vtempest/git0/discussions"><img alt="GitHub Discussions" src="https://img.shields.io/github/discussions/vtempest/git0" /></a>
|
|
8
|
+
<a href="https://github.com/vtempest/git0/pulse" alt="Activity"><img src="https://img.shields.io/github/commit-activity/m/vtempest/git0" /></a>
|
|
9
|
+
<img src="https://img.shields.io/github/last-commit/vtempest/git0.svg" alt="GitHub last commit" />
|
|
10
|
+
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" />
|
|
11
|
+
<a href="https://codespaces.new/vtempest/git0"><img src="https://github.com/codespaces/badge.svg" width="150" height="20" /></a>
|
|
26
12
|
</p>
|
|
27
13
|
|
|
28
14
|
# Git0: Download Git Repo on Step Zero
|
|
@@ -46,26 +32,28 @@ bun install -g git0
|
|
|
46
32
|
- **Search GitHub repositories** by name with fuzzy matching
|
|
47
33
|
- **Download repositories** directly from GitHub URLs or owner/repo shortcuts. Skip the manual git clone, cd, install dance
|
|
48
34
|
- **Get Releases** instantly download latest release for your system or all systems
|
|
49
|
-
- **Automatic dependency
|
|
35
|
+
- **Automatic dependency installation** and installation for multiple project types
|
|
50
36
|
- **Smart IDE integration** - automatically opens projects in your preferred editor
|
|
51
37
|
- **Cross-platform support** - works on Windows, macOS, and Linux
|
|
52
38
|
- **Conflict resolution** - handles directory naming conflicts automatically
|
|
39
|
+
- **Faster than git** - skips `.git` history and uncompresses while downloading
|
|
53
40
|
|
|
54
41
|
## 🎯 Usage
|
|
55
42
|
|
|
56
43
|
```bash
|
|
57
|
-
# Search for repositories by name
|
|
58
|
-
gg react starter
|
|
59
44
|
|
|
60
45
|
# Direct download from GitHub URL
|
|
61
|
-
##
|
|
62
|
-
|
|
46
|
+
## g and git0 both work
|
|
47
|
+
g https://github.com/facebook/react
|
|
48
|
+
|
|
49
|
+
# Search for repositories by name
|
|
50
|
+
g react starter
|
|
63
51
|
|
|
64
52
|
# Download using owner/repo shorthand
|
|
65
|
-
git0 react
|
|
53
|
+
git0 facebook/react
|
|
66
54
|
|
|
67
|
-
|
|
68
|
-
# (copy into your project's readme
|
|
55
|
+
# Use git0 without installing, (only node needed)
|
|
56
|
+
# (copy this line into your project's readme to help others setup)
|
|
69
57
|
npx git0 facebook/react
|
|
70
58
|
```
|
|
71
59
|
|
|
@@ -90,6 +78,7 @@ git0 automatically detects and opens projects in your preferred IDE:
|
|
|
90
78
|
- **VS Code** (`code`)
|
|
91
79
|
- **Code Server** (`code-server`)
|
|
92
80
|
- **Neovim** (`nvim`)
|
|
81
|
+
- **Webstorm** (`webstorm`)
|
|
93
82
|
|
|
94
83
|
## 🔧 Configuration
|
|
95
84
|
|
|
@@ -171,10 +171,9 @@ function App() {
|
|
|
171
171
|
script.id = 'tailwind-cdn';
|
|
172
172
|
script.src = "https://cdn.tailwindcss.com";
|
|
173
173
|
script.async = true;
|
|
174
|
+
script.onload = () => setIsLoaded(true); // Only called when Tailwind is actually loaded
|
|
174
175
|
document.head.appendChild(script);
|
|
175
|
-
|
|
176
|
-
setIsLoaded(true),
|
|
177
|
-
900)
|
|
176
|
+
|
|
178
177
|
}, []);
|
|
179
178
|
|
|
180
179
|
const copyToClipboard = (text: string, command: string) => {
|
package/package.json
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git0",
|
|
3
3
|
"description": "CLI tool to search GitHub repositories, download source & releases for your system, and instantly set up, then install dependencies and open code editor.",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.14",
|
|
5
5
|
"author": "vtempest",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "bun build src/cli.ts --outdir dist --target node --format esm --sourcemap",
|
|
8
|
+
"test": "bun test",
|
|
9
|
+
"docs": "cd docs-config && bun run build:docs",
|
|
10
|
+
"demo": "bun src/cli.ts react template",
|
|
11
|
+
"publish:npm": "npm version patch && npm publish --access public"
|
|
12
|
+
},
|
|
6
13
|
"dependencies": {
|
|
7
14
|
"chalk": "^5.4.1",
|
|
8
15
|
"git-url-parse": "^16.1.0",
|
|
@@ -12,24 +19,27 @@
|
|
|
12
19
|
"tar": "^7.4.3"
|
|
13
20
|
},
|
|
14
21
|
"exports": {
|
|
15
|
-
".": "./
|
|
22
|
+
".": "./dist/cli.js"
|
|
16
23
|
},
|
|
17
24
|
"bin": {
|
|
18
|
-
"g": "./
|
|
19
|
-
"gg": "./
|
|
25
|
+
"g": "./dist/cli.js",
|
|
26
|
+
"gg": "./dist/cli.js",
|
|
20
27
|
"fm": "./src/fm.js",
|
|
21
|
-
"git0": "./
|
|
28
|
+
"git0": "./dist/cli.js"
|
|
22
29
|
},
|
|
30
|
+
"type": "module",
|
|
23
31
|
"keywords": [
|
|
24
32
|
"github",
|
|
33
|
+
"git",
|
|
34
|
+
"package",
|
|
35
|
+
"manager",
|
|
25
36
|
"cli",
|
|
26
37
|
"download"
|
|
27
38
|
],
|
|
28
39
|
"license": "MIT",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"publish:npm": "npm version patch && npm publish --access public"
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/OpenSourceAGI/appdemo-dev-tools/tree/master/packages/git0-repo-downloader"
|
|
33
43
|
},
|
|
34
|
-
"
|
|
44
|
+
"homepage": "https://github.com/OpenSourceAGI/appdemo-dev-tools/tree/master/packages/git0-repo-downloader"
|
|
35
45
|
}
|
package/readme.md
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://i.imgur.com/
|
|
2
|
+
<img src="https://i.imgur.com/td0AVb7.png" />
|
|
3
3
|
</p>
|
|
4
4
|
<p align="center">
|
|
5
5
|
<a href="https://discord.gg/SJdBqBz3tV"><img src="https://img.shields.io/discord/1110227955554209923.svg?label=Chat&logo=Discord&colorB=7289da&style=flat"/></a>
|
|
6
6
|
<a href="https://github.com/vtempest/git0/discussions"><img alt="GitHub Stars" src="https://img.shields.io/github/stars/vtempest/git0" /></a>
|
|
7
7
|
<a href="https://github.com/vtempest/git0/discussions"><img alt="GitHub Discussions" src="https://img.shields.io/github/discussions/vtempest/git0" /></a>
|
|
8
8
|
<a href="https://github.com/vtempest/git0/pulse" alt="Activity"><img src="https://img.shields.io/github/commit-activity/m/vtempest/git0" /></a>
|
|
9
|
+
<br />
|
|
9
10
|
<img src="https://img.shields.io/github/last-commit/vtempest/git0.svg" alt="GitHub last commit" />
|
|
10
11
|
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" />
|
|
11
12
|
<a href="https://codespaces.new/vtempest/git0"><img src="https://github.com/codespaces/badge.svg" width="150" height="20" /></a>
|
|
12
|
-
</p>
|
|
13
|
+
</p>
|
|
13
14
|
|
|
15
|
+
# Step 0: Download Git Repo
|
|
14
16
|
|
|
15
|
-
# Git0: Download Git Repo on Step Zero
|
|
16
17
|
CLI tool to search GitHub repositories, download source & releases for your system, and instantly set up, then install dependencies and open code editor.
|
|
17
18
|
|
|
18
|
-
|
|
19
19
|
## 🚀 Installation
|
|
20
20
|
|
|
21
21
|
```bash
|
|
@@ -29,7 +29,6 @@ bun install -g git0
|
|
|
29
29
|

|
|
30
30
|

|
|
31
31
|
|
|
32
|
-
|
|
33
32
|
## ✨ Features
|
|
34
33
|
|
|
35
34
|
- **Search GitHub repositories** by name with fuzzy matching
|
|
@@ -41,7 +40,6 @@ bun install -g git0
|
|
|
41
40
|
- **Conflict resolution** - handles directory naming conflicts automatically
|
|
42
41
|
- **Faster than git** - skips `.git` history and uncompresses while downloading
|
|
43
42
|
|
|
44
|
-
|
|
45
43
|
## 🎯 Usage
|
|
46
44
|
|
|
47
45
|
```bash
|
|
@@ -65,28 +63,28 @@ npx git0 facebook/react
|
|
|
65
63
|
|
|
66
64
|
git0 automatically detects and sets up the following project types:
|
|
67
65
|
|
|
68
|
-
| Project Type
|
|
69
|
-
|
|
70
|
-
| **Node.js** | `package.json`
|
|
71
|
-
| **Docker**
|
|
72
|
-
| **Python**
|
|
73
|
-
| **Rust**
|
|
74
|
-
| **Go**
|
|
66
|
+
| Project Type | Detection | Installation |
|
|
67
|
+
| ----------------- | -------------------------------------- | --------------------------------------------- |
|
|
68
|
+
| **Node.js** | `package.json` | `bun install` (fallback to `npm install`) |
|
|
69
|
+
| **Docker** | `Dockerfile`, `docker-compose.yml` | `docker-compose up -d` or `docker build` |
|
|
70
|
+
| **Python** | `requirements.txt`, `setup.py` | Virtual environment + pip install |
|
|
71
|
+
| **Rust** | `Cargo.toml` | `cargo build` |
|
|
72
|
+
| **Go** | `go.mod` | `go mod tidy` |
|
|
75
73
|
|
|
76
74
|
### Supported IDEs
|
|
77
75
|
|
|
78
76
|
git0 automatically detects and opens projects in your preferred IDE:
|
|
79
77
|
|
|
80
|
-
- **
|
|
81
|
-
- **
|
|
82
|
-
- **
|
|
83
|
-
- **Code
|
|
84
|
-
- **
|
|
85
|
-
- **
|
|
78
|
+
- **Antigravity**
|
|
79
|
+
- **Cursor**
|
|
80
|
+
- **Windsurf**
|
|
81
|
+
- **VS Code**
|
|
82
|
+
- **VSCode Server WebUI**
|
|
83
|
+
- **Neovim**
|
|
84
|
+
- **Webstorm**
|
|
86
85
|
|
|
87
86
|
## 🔧 Configuration
|
|
88
87
|
|
|
89
|
-
|
|
90
88
|
### What Happens After Download
|
|
91
89
|
|
|
92
90
|
1. **Repository is downloaded** to your current directory
|
|
@@ -105,4 +103,4 @@ For higher API rate limits, set [your GitHub token](https://docs.github.com/en/a
|
|
|
105
103
|
export GITHUB_TOKEN=your_github_token_here
|
|
106
104
|
```
|
|
107
105
|
|
|
108
|
-
Without a token, you're limited to 60 requests per hour. With a token, you get 5,000 requests per hour.
|
|
106
|
+
Without a token, you're limited to 60 requests per hour. With a token, you get 5,000 requests per hour.
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import GithubAPI from './github-api.js';
|
|
5
|
+
import { openInIDE } from './ide.js';
|
|
6
|
+
import { installDependencies } from './install.js';
|
|
7
|
+
import { showPackageMenu } from './package-menu.js';
|
|
8
|
+
import { printLogo } from './utils.js';
|
|
9
|
+
import type { SearchResult } from './types.js';
|
|
10
|
+
|
|
11
|
+
const Github = new GithubAPI({ debug: false });
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Downloads a GitHub repository tarball, then immediately opens the project in
|
|
15
|
+
* an IDE and installs its dependencies.
|
|
16
|
+
*
|
|
17
|
+
* The IDE launch is deferred 500 ms so the extraction can finish writing files
|
|
18
|
+
* before the editor tries to index them.
|
|
19
|
+
*
|
|
20
|
+
* @param repo - GitHub URL (`https://…`) or `owner/repo` shorthand.
|
|
21
|
+
* @param folderPath - Optional name for the extraction directory; defaults to
|
|
22
|
+
* the repository name.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* await downloadRepoAndSetup('https://github.com/vitejs/vite');
|
|
26
|
+
* // Downloads vite/, opens it in the first available IDE, runs bun install
|
|
27
|
+
*/
|
|
28
|
+
async function downloadRepoAndSetup(repo: string, folderPath: string | null = null): Promise<void> {
|
|
29
|
+
printLogo();
|
|
30
|
+
const extractPath = await Github.downloadRepo(repo, folderPath);
|
|
31
|
+
setTimeout(() => openInIDE(extractPath), 500);
|
|
32
|
+
await installDependencies(extractPath);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Builds the display label shown in the repository selection list.
|
|
37
|
+
*
|
|
38
|
+
* Shows the full `owner/name` slug, description, star count, language, and a
|
|
39
|
+
* colored badge when release packages are available for the current platform.
|
|
40
|
+
*
|
|
41
|
+
* @param repo - Enriched search result from `searchRepositories`.
|
|
42
|
+
* @returns Multi-line string suitable for an `inquirer` list choice label.
|
|
43
|
+
*
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
function formatRepoChoice(repo: SearchResult): string {
|
|
47
|
+
const packageInfo = repo.hasCompatibleReleases
|
|
48
|
+
? chalk.green(' 📦 Packages available')
|
|
49
|
+
: repo.hasReleases
|
|
50
|
+
? chalk.yellow(' 📦 Packages (other platforms)')
|
|
51
|
+
: '';
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
`${chalk.bold(repo.full_name)} - ${chalk.gray(repo.description || 'No description')}\n` +
|
|
55
|
+
` ${chalk.yellow(`★ ${repo.stargazers_count}`)} | ${chalk.blue(repo.language || 'Unknown')}${packageInfo}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Prompts the user to choose between downloading the binary package, the
|
|
61
|
+
* source code, or both when a repository has release assets.
|
|
62
|
+
*
|
|
63
|
+
* @param selectedRepo - The repository the user chose in the search results.
|
|
64
|
+
*
|
|
65
|
+
* @internal
|
|
66
|
+
*/
|
|
67
|
+
async function handleRepoDownload(selectedRepo: SearchResult): Promise<void> {
|
|
68
|
+
if (!selectedRepo.hasReleases && !selectedRepo.hasCompatibleReleases) {
|
|
69
|
+
await downloadRepoAndSetup(selectedRepo.url);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { downloadChoice } = await inquirer.prompt({
|
|
74
|
+
type: 'list',
|
|
75
|
+
name: 'downloadChoice',
|
|
76
|
+
message: 'This repository has downloadable packages. What would you like to do?',
|
|
77
|
+
choices: [
|
|
78
|
+
{ name: '📦 Download package/binary', value: 'package' },
|
|
79
|
+
{ name: '📂 Download source code', value: 'source' },
|
|
80
|
+
{ name: '📦📂 Download both', value: 'both' },
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (downloadChoice === 'package' || downloadChoice === 'both') {
|
|
85
|
+
await showPackageMenu(
|
|
86
|
+
selectedRepo,
|
|
87
|
+
Github.downloadPackage.bind(Github),
|
|
88
|
+
Github.getCurrentPlatform()
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (downloadChoice === 'source' || downloadChoice === 'both') {
|
|
92
|
+
await downloadRepoAndSetup(selectedRepo.url);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* CLI entry point for the `git0` / `g` / `gg` commands.
|
|
98
|
+
*
|
|
99
|
+
* **Flow:**
|
|
100
|
+
* 1. Parse `process.argv` for a search query or direct repo URL.
|
|
101
|
+
* 2. If the argument looks like a GitHub URL or `owner/repo`, download it
|
|
102
|
+
* directly — optionally into a custom folder (second CLI argument).
|
|
103
|
+
* 3. Otherwise, search GitHub and present an interactive list.
|
|
104
|
+
* 4. After the user picks a repo, offer package vs. source download when
|
|
105
|
+
* releases are available.
|
|
106
|
+
*
|
|
107
|
+
* Exits with code `1` on missing arguments or an empty search result.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* // Direct download:
|
|
111
|
+
* // git0 facebook/react
|
|
112
|
+
* // git0 https://github.com/vitejs/vite my-vite-copy
|
|
113
|
+
*
|
|
114
|
+
* // Search:
|
|
115
|
+
* // git0 react template starter
|
|
116
|
+
*/
|
|
117
|
+
async function main(): Promise<void> {
|
|
118
|
+
printLogo();
|
|
119
|
+
|
|
120
|
+
const args = process.argv.slice(2);
|
|
121
|
+
if (!args.length) {
|
|
122
|
+
console.log(chalk.yellow('Usage: git0 <github-url | owner/repo | search-query>'));
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const query = args.join(' ');
|
|
127
|
+
const parsed = Github.parseURL(query);
|
|
128
|
+
|
|
129
|
+
if (parsed && parsed.owner && parsed.name) {
|
|
130
|
+
await downloadRepoAndSetup(parsed.href, args[1] ?? null);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const results = await Github.searchRepositories(query);
|
|
135
|
+
if (!results?.length) {
|
|
136
|
+
console.log(chalk.yellow('No repositories found'));
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const { selectedRepo } = await inquirer.prompt({
|
|
141
|
+
type: 'list',
|
|
142
|
+
name: 'selectedRepo',
|
|
143
|
+
message: 'Select a repository to download:',
|
|
144
|
+
choices: results.map(repo => ({ name: formatRepoChoice(repo), value: repo })),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await handleRepoDownload(selectedRepo);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
main();
|
package/src/download.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import gitUrlParse from 'git-url-parse';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import * as tar from 'tar';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { getCurrentPlatform } from './platform.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns `basePath` unchanged when the path does not yet exist on disk.
|
|
10
|
+
* When it does exist, appends `-2`, `-3`, … until an unused path is found.
|
|
11
|
+
*
|
|
12
|
+
* Used so that downloading the same repo twice never silently overwrites the
|
|
13
|
+
* first copy.
|
|
14
|
+
*
|
|
15
|
+
* @param basePath - Desired extraction directory path.
|
|
16
|
+
* @returns An available directory path guaranteed not to exist yet.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // ./react already exists, ./react-2 does not
|
|
20
|
+
* getAvailableDirectoryName('./react'); // → './react-2'
|
|
21
|
+
*/
|
|
22
|
+
export function getAvailableDirectoryName(basePath: string): string {
|
|
23
|
+
if (!fs.existsSync(basePath)) return basePath;
|
|
24
|
+
let counter = 2;
|
|
25
|
+
while (true) {
|
|
26
|
+
const candidate = `${basePath}-${counter}`;
|
|
27
|
+
if (!fs.existsSync(candidate)) return candidate;
|
|
28
|
+
counter++;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Streams a GitHub tarball response directly into a `tar` extractor.
|
|
34
|
+
*
|
|
35
|
+
* `strip: 1` removes the top-level directory that GitHub adds to every
|
|
36
|
+
* tarball (e.g. `owner-repo-abc1234/`) so files land directly in
|
|
37
|
+
* `extractPath`.
|
|
38
|
+
*
|
|
39
|
+
* @param res - The `ReadableStream` body from the GitHub API response.
|
|
40
|
+
* @param extractPath - Absolute directory to extract into (must already exist).
|
|
41
|
+
* @returns Promise that resolves when extraction is complete.
|
|
42
|
+
*
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
async function streamTarball(res: ReadableStream, extractPath: string): Promise<void> {
|
|
46
|
+
const { Readable } = await import('stream');
|
|
47
|
+
const nodeStream = Readable.fromWeb(res);
|
|
48
|
+
await new Promise<void>((resolve, reject) => {
|
|
49
|
+
nodeStream
|
|
50
|
+
.pipe(tar.x({ C: extractPath, strip: 1 }))
|
|
51
|
+
.on('finish', resolve)
|
|
52
|
+
.on('error', reject);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Streams a GitHub API response body directly to a file on disk.
|
|
58
|
+
*
|
|
59
|
+
* @param res - The `ReadableStream` body from the GitHub API response.
|
|
60
|
+
* @param dest - Absolute path of the file to create/overwrite.
|
|
61
|
+
* @returns Promise that resolves when the file is fully written.
|
|
62
|
+
*
|
|
63
|
+
* @internal
|
|
64
|
+
*/
|
|
65
|
+
async function streamToFile(res: ReadableStream, dest: string): Promise<void> {
|
|
66
|
+
const { Readable } = await import('stream');
|
|
67
|
+
const nodeStream = Readable.fromWeb(res);
|
|
68
|
+
await new Promise<void>((resolve, reject) => {
|
|
69
|
+
nodeStream
|
|
70
|
+
.pipe(fs.createWriteStream(dest))
|
|
71
|
+
.on('finish', resolve)
|
|
72
|
+
.on('error', reject);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Downloads a GitHub repository tarball and extracts it into a local directory.
|
|
78
|
+
*
|
|
79
|
+
* The function first attempts to download the `master` branch; if that request
|
|
80
|
+
* returns an error it retries with `main`. The extracted directory name is
|
|
81
|
+
* derived from the repository name, with a numeric suffix appended when the
|
|
82
|
+
* target already exists (see {@link getAvailableDirectoryName}).
|
|
83
|
+
*
|
|
84
|
+
* @param callGithub - Pre-configured `grab` instance bound to the GitHub API.
|
|
85
|
+
* @param repo - GitHub URL (`https://github.com/owner/repo`) or `owner/repo`.
|
|
86
|
+
* @param targetDir - Optional custom folder name; defaults to the repo name.
|
|
87
|
+
* @returns Absolute path of the directory the repo was extracted into.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* const dir = await downloadRepo(callGithub, 'https://github.com/facebook/react');
|
|
91
|
+
* // → '/current/working/dir/react'
|
|
92
|
+
*/
|
|
93
|
+
export async function downloadRepo(
|
|
94
|
+
callGithub: Function,
|
|
95
|
+
repo: string,
|
|
96
|
+
targetDir: string | null = null
|
|
97
|
+
): Promise<string> {
|
|
98
|
+
const parsed = gitUrlParse(repo);
|
|
99
|
+
|
|
100
|
+
// GitHub tarballs for forks include the fork chain in `owner`, e.g. "upstream/fork".
|
|
101
|
+
// We only want the last segment.
|
|
102
|
+
if (parsed.owner.includes('/')) {
|
|
103
|
+
parsed.owner = parsed.owner.split('/').slice(-1)[0];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const defaultDir = path.resolve(process.cwd(), targetDir?.length ? targetDir : parsed.name);
|
|
107
|
+
const extractPath = getAvailableDirectoryName(defaultDir);
|
|
108
|
+
|
|
109
|
+
fs.mkdirSync(extractPath, { recursive: true });
|
|
110
|
+
|
|
111
|
+
console.log(chalk.blue(`📦 Downloading ${parsed.name} into ${path.basename(extractPath)}...`));
|
|
112
|
+
|
|
113
|
+
const defaultBranch = (parsed as any).default_branch || 'master';
|
|
114
|
+
const tarballUrl = `/repos/${parsed.owner}/${parsed.name}/tarball/${defaultBranch}`;
|
|
115
|
+
const params = { onStream: (res: ReadableStream) => streamTarball(res, extractPath) };
|
|
116
|
+
|
|
117
|
+
let response = await callGithub(tarballUrl, params);
|
|
118
|
+
if (response.error) {
|
|
119
|
+
response = await callGithub(tarballUrl.replace('/master', '/main'), params);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return extractPath;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Downloads a single release asset binary from GitHub to a local file.
|
|
127
|
+
*
|
|
128
|
+
* After a successful download:
|
|
129
|
+
* - On non-Windows, extension-less files are made executable (`chmod 755`).
|
|
130
|
+
* - Platform-specific install instructions are printed via
|
|
131
|
+
* {@link printInstallInstructions}.
|
|
132
|
+
*
|
|
133
|
+
* @param callGithub - Pre-configured `grab` instance bound to the GitHub API.
|
|
134
|
+
* @param packageURL - Direct HTTPS download URL for the asset.
|
|
135
|
+
* @param downloadPath - Absolute local file path to write the asset to.
|
|
136
|
+
* @returns The `downloadPath` on success.
|
|
137
|
+
* @throws When the download request fails.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* await downloadPackage(callGithub,
|
|
141
|
+
* 'https://github.com/user/repo/releases/download/v1.0/app-linux-x64',
|
|
142
|
+
* '/tmp/app-linux-x64'
|
|
143
|
+
* );
|
|
144
|
+
*/
|
|
145
|
+
export async function downloadPackage(
|
|
146
|
+
callGithub: Function,
|
|
147
|
+
packageURL: string,
|
|
148
|
+
downloadPath: string
|
|
149
|
+
): Promise<string> {
|
|
150
|
+
const fileName = path.basename(downloadPath);
|
|
151
|
+
|
|
152
|
+
console.log(chalk.blue(`📦 Downloading ${fileName}...`));
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
await callGithub(packageURL, {
|
|
156
|
+
onStream: (res: ReadableStream) => streamToFile(res, downloadPath),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
console.log(chalk.green(`✅ Downloaded ${fileName} to ${downloadPath}`));
|
|
160
|
+
|
|
161
|
+
if (process.platform !== 'win32' && !fileName.includes('.')) {
|
|
162
|
+
try {
|
|
163
|
+
fs.chmodSync(downloadPath, '755');
|
|
164
|
+
console.log(chalk.green(`✅ Made ${fileName} executable`));
|
|
165
|
+
} catch {
|
|
166
|
+
console.log(chalk.yellow(`⚠️ Could not make ${fileName} executable`));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
printInstallInstructions(downloadPath, fileName);
|
|
171
|
+
return downloadPath;
|
|
172
|
+
} catch (error: any) {
|
|
173
|
+
console.error(chalk.red(`❌ Failed to download ${fileName}:`), error.message);
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Prints platform-specific shell commands for installing or running a
|
|
180
|
+
* downloaded asset.
|
|
181
|
+
*
|
|
182
|
+
* The output adapts to the current OS and the file's extension:
|
|
183
|
+
* - **Windows**: `.exe` run command, `.msi` installer command.
|
|
184
|
+
* - **macOS**: `.dmg` open command, `.pkg` installer command.
|
|
185
|
+
* - **Linux**: `.deb` dpkg, `.rpm` rpm, `.AppImage` run command,
|
|
186
|
+
* extension-less binaries get a `mv` to `/usr/local/bin` suggestion.
|
|
187
|
+
*
|
|
188
|
+
* @param filePath - Absolute path of the downloaded file.
|
|
189
|
+
* @param fileName - Basename of the downloaded file (used for extension checks).
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* printInstallInstructions('/tmp/myapp', 'myapp');
|
|
193
|
+
* // Prints:
|
|
194
|
+
* // Binary is ready to use:
|
|
195
|
+
* // "/tmp/myapp"
|
|
196
|
+
* // Consider moving to PATH:
|
|
197
|
+
* // sudo mv "/tmp/myapp" /usr/local/bin/
|
|
198
|
+
*/
|
|
199
|
+
export function printInstallInstructions(filePath: string, fileName: string): void {
|
|
200
|
+
const platform = getCurrentPlatform();
|
|
201
|
+
|
|
202
|
+
if (platform.platform === 'win32') {
|
|
203
|
+
if (fileName.endsWith('.exe')) {
|
|
204
|
+
console.log(chalk.white(' Run the executable:'));
|
|
205
|
+
console.log(chalk.gray(` ${filePath}`));
|
|
206
|
+
} else if (fileName.endsWith('.msi')) {
|
|
207
|
+
console.log(chalk.white(' Install the MSI package:'));
|
|
208
|
+
console.log(chalk.gray(` msiexec /i "${filePath}"`));
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (platform.platform === 'darwin') {
|
|
214
|
+
if (fileName.endsWith('.dmg')) {
|
|
215
|
+
console.log(chalk.white(' Mount and install the DMG:'));
|
|
216
|
+
console.log(chalk.gray(` open "${filePath}"`));
|
|
217
|
+
} else if (fileName.endsWith('.pkg')) {
|
|
218
|
+
console.log(chalk.white(' Install the package:'));
|
|
219
|
+
console.log(chalk.gray(` sudo installer -pkg "${filePath}" -target /`));
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Linux / other Unix
|
|
225
|
+
if (fileName.endsWith('.deb')) {
|
|
226
|
+
console.log(chalk.white(' Install the DEB package:'));
|
|
227
|
+
console.log(chalk.gray(` sudo dpkg -i "${filePath}"`));
|
|
228
|
+
} else if (fileName.endsWith('.rpm')) {
|
|
229
|
+
console.log(chalk.white(' Install the RPM package:'));
|
|
230
|
+
console.log(chalk.gray(` sudo rpm -i "${filePath}"`));
|
|
231
|
+
} else if (fileName.endsWith('.AppImage')) {
|
|
232
|
+
console.log(chalk.white(' Run the AppImage:'));
|
|
233
|
+
console.log(chalk.gray(` chmod +x "${filePath}" && "${filePath}"`));
|
|
234
|
+
} else if (!fileName.includes('.')) {
|
|
235
|
+
console.log(chalk.white(' Binary is ready to use:'));
|
|
236
|
+
console.log(chalk.gray(` "${filePath}"`));
|
|
237
|
+
console.log(chalk.white(' Consider moving to PATH:'));
|
|
238
|
+
console.log(chalk.gray(` sudo mv "${filePath}" /usr/local/bin/`));
|
|
239
|
+
}
|
|
240
|
+
}
|
package/src/fm.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import readline from 'readline';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import os from 'os';
|
|
2
7
|
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const readline = require('readline');
|
|
6
|
-
const { execSync } = require('child_process');
|
|
7
|
-
const os = require('os');
|
|
8
8
|
|
|
9
9
|
class FileManager {
|
|
10
10
|
constructor() {
|