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.
Files changed (71) hide show
  1. package/dist/cli.js +27780 -0
  2. package/dist/cli.js.map +331 -0
  3. package/docs/404.html +2 -2
  4. package/docs/Footer/index.html +2 -2
  5. package/docs/assets/js/1df93b7f.1385eba0.js +2 -0
  6. package/docs/assets/js/22dd74f7.e13ac7a4.js +1 -0
  7. package/docs/assets/js/28ab763d.192aa3bb.js +1 -0
  8. package/docs/assets/js/299d276b.c9496717.js +1 -0
  9. package/docs/assets/js/{68cef36b.c312447e.js → 68cef36b.e5b1975b.js} +1 -1
  10. package/docs/assets/js/a9cef9d5.a7253e41.js +1 -0
  11. package/docs/assets/js/c3a618e1.1c56fb03.js +590 -0
  12. package/docs/assets/js/{main.cf858a7d.js → main.1f646b09.js} +2 -2
  13. package/docs/assets/js/runtime~main.f69f8f44.js +1 -0
  14. package/docs/functions/fm/index.html +26 -0
  15. package/docs/functions/git0/index.html +6 -6
  16. package/docs/functions/github-api/index.html +20 -20
  17. package/docs/functions/index.html +889 -71
  18. package/docs/functions/modules/index.html +4 -3
  19. package/docs/index.html +2 -2
  20. package/docs/lunr-index-1749760982052.json +1 -0
  21. package/docs/lunr-index.json +1 -1
  22. package/docs/search-doc-1749760982052.json +1 -0
  23. package/docs/search-doc.json +1 -1
  24. package/docs/sitemap.xml +1 -1
  25. package/docs-config/.docusaurus/client-manifest.json +36 -24
  26. package/docs-config/.docusaurus/docusaurus-plugin-content-docs/default/p/index-466.json +1 -1
  27. package/docs-config/.docusaurus/docusaurus-plugin-content-docs/default/site-src-functions-fm-md-a9c.json +19 -0
  28. package/docs-config/.docusaurus/docusaurus-plugin-content-docs/default/site-src-functions-git-0-md-299.json +4 -0
  29. package/docs-config/.docusaurus/docusaurus-plugin-content-docs/default/site-src-functions-index-md-c3a.json +1 -1
  30. package/docs-config/.docusaurus/globalData.json +10 -5
  31. package/docs-config/.docusaurus/registry.js +1 -0
  32. package/docs-config/.docusaurus/routes.js +9 -3
  33. package/docs-config/.docusaurus/routesChunkNames.json +7 -3
  34. package/docs-config/src/functions/fm.md +1 -0
  35. package/docs-config/src/functions/git0.md +2 -2
  36. package/docs-config/src/functions/github-api.md +34 -17
  37. package/docs-config/src/functions/index.md +19 -30
  38. package/docs-config/src/functions/modules.md +1 -0
  39. package/docs-config/src/functions/typedoc-sidebar.cjs +5 -0
  40. package/docs-config/src/pages/index.tsx +2 -3
  41. package/package.json +20 -10
  42. package/readme.md +19 -21
  43. package/src/cli.ts +150 -0
  44. package/src/download.ts +240 -0
  45. package/src/fm.js +5 -5
  46. package/src/github-api.ts +237 -0
  47. package/src/ide.ts +141 -0
  48. package/src/install.ts +147 -0
  49. package/src/package-menu.ts +183 -0
  50. package/src/platform.ts +53 -0
  51. package/src/releases.ts +159 -0
  52. package/src/setup.ts +9 -0
  53. package/src/types.ts +97 -0
  54. package/src/utils.ts +49 -0
  55. package/test/download.test.ts +48 -0
  56. package/test/github-api.test.ts +79 -0
  57. package/test/package-menu.test.ts +134 -0
  58. package/test/platform.test.ts +64 -0
  59. package/test/releases.test.ts +169 -0
  60. package/docs/assets/js/1df93b7f.d8c05d2c.js +0 -2
  61. package/docs/assets/js/22dd74f7.237398b4.js +0 -1
  62. package/docs/assets/js/28ab763d.5714aa16.js +0 -1
  63. package/docs/assets/js/299d276b.1a1baa1c.js +0 -1
  64. package/docs/assets/js/c3a618e1.965a31da.js +0 -1
  65. package/docs/assets/js/runtime~main.7520dc36.js +0 -1
  66. package/docs/lunr-index-1749613752315.json +0 -1
  67. package/docs/search-doc-1749613752315.json +0 -1
  68. package/src/git0.js +0 -380
  69. package/src/github-api.js +0 -472
  70. /package/docs/assets/js/{1df93b7f.d8c05d2c.js.LICENSE.txt → 1df93b7f.1385eba0.js.LICENSE.txt} +0 -0
  71. /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/poOtI3N.png" />
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
- <img src="https://img.shields.io/discord/1110227955554209923.svg?label=Chat&logo=Discord&colorB=7289da&style=flat"
7
- alt="Join Discord" />
8
- </a>
9
- <a href="https://github.com/vtempest/git0/discussions">
10
- <img alt="GitHub Stars" src="https://img.shields.io/github/stars/vtempest/git0" /></a>
11
- <a href="https://github.com/vtempest/git0/discussions">
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 detection** and installation for multiple project types
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
- ## gg and git0 both work
62
- gg https://github.com/facebook/react
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 starter
53
+ git0 facebook/react
66
54
 
67
- ## Use git0 without installing, (only node needed)
68
- # (copy into your project's readme for quick setup)
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
 
@@ -1,4 +1,5 @@
1
1
  ## Modules
2
2
 
3
+ - [fm](fm.md)
3
4
  - [git0](git0.md)
4
5
  - [github-api](github-api.md)
@@ -2,6 +2,11 @@
2
2
  /** @type {import("@docusaurus/plugin-content-docs").SidebarsConfig} */
3
3
  const typedocSidebar = {
4
4
  items: [
5
+ {
6
+ type: "doc",
7
+ id: "functions/fm",
8
+ label: "fm"
9
+ },
5
10
  {
6
11
  type: "doc",
7
12
  id: "functions/git0",
@@ -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
- setTimeout(() =>
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.12",
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
- ".": "./src/github-api.js"
22
+ ".": "./dist/cli.js"
16
23
  },
17
24
  "bin": {
18
- "g": "./src/git0.js",
19
- "gg": "./src/git0.js",
25
+ "g": "./dist/cli.js",
26
+ "gg": "./dist/cli.js",
20
27
  "fm": "./src/fm.js",
21
- "git0": "./src/git0.js"
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
- "scripts": {
30
- "docs": "cd docs-config && bun run build:docs",
31
- "demo": "bun src/git0.js react template",
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
- "type": "module"
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/poOtI3N.png" />
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
  ![livepreview](https://i.imgur.com/Io3ukRC.gif)
30
30
  ![preview](https://i.imgur.com/K22NiBq.png)
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 | Detection | Installation |
69
- |-------------|-----------|-------------|
70
- | **Node.js** | `package.json` | `bun install` (fallback to `npm install`) |
71
- | **Docker** | `Dockerfile`, `docker-compose.yml` | `docker-compose up -d` or `docker build` |
72
- | **Python** | `requirements.txt`, `setup.py` | Virtual environment + pip install |
73
- | **Rust** | `Cargo.toml` | `cargo build` |
74
- | **Go** | `go.mod` | `go mod tidy` |
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
- - **Cursor** (`cursor`)
81
- - **Windsurf** (`windsurf`)
82
- - **VS Code** (`code`)
83
- - **Code Server** (`code-server`)
84
- - **Neovim** (`nvim`)
85
- - **Webstorm** (`webstorm`)
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();
@@ -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() {