git0 0.2.98 → 0.3.0

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/package.json CHANGED
@@ -1,7 +1,7 @@
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.98",
4
+ "version": "0.3.0",
5
5
  "author": "vtempest",
6
6
  "scripts": {
7
7
  "build": "bun build src/cli.ts --outdir dist --target node --format esm --sourcemap",
@@ -9,6 +9,7 @@
9
9
  "test:ci": "bun test --reporter=junit --reporter-outfile=./junit.xml --coverage --coverage-reporter=lcov --coverage-dir=./coverage",
10
10
  "docs": "cd docs-config && bun run build:docs",
11
11
  "demo": "bun src/cli.ts react template",
12
+ "bench": "bun benchmark/tarball-vs-clone.bench.ts",
12
13
  "publish:npm": "npm version patch && npm publish --access public",
13
14
  "test:watch": "vitest",
14
15
  "coverage": "vitest run --coverage"
@@ -45,7 +46,7 @@
45
46
  "url": "git+https://github.com/OpenSourceAGI/dev-tools-starter-agent.git",
46
47
  "directory": "packages/git0-repo-downloader"
47
48
  },
48
- "homepage": "https://github.com/OpenSourceAGI/dev-tools-starter-agent/tree/master/packages/git0-repo-downloader",
49
+ "homepage": "https://git0.js.org",
49
50
  "devDependencies": {
50
51
  "@vitest/coverage-v8": "^4.1.0",
51
52
  "vitest": "^4.1.0"
package/readme.md CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  <!-- template-git-repo:badges:start -->
6
6
  <p align="center">
7
+ <a href="https://git0.js.org"><img height="20px" src="https://img.shields.io/badge/App-blueviolet?style=for-the-badge&logo=googlechrome&logoColor=white" alt="Website" /></a>
7
8
  <a href="https://starterdocs.vtempest.workers.dev/docs/packages/git0-repo-downloader"><img src="https://img.shields.io/badge/Docs-blue?logo=ReadTheDocs&logoColor=white" alt="Documentation" /></a>
8
9
  <br />
9
10
  <a href="https://github.com/OpenSourceAGI/dev-tools-starter-agent/stargazers"><img src="https://img.shields.io/github/stars/OpenSourceAGI/dev-tools-starter-agent" alt="GitHub Stars" /></a>
@@ -33,6 +34,8 @@
33
34
 
34
35
  CLI tool to search GitHub repositories, download source & releases for your system, and instantly set up, then install dependencies and open code editor.
35
36
 
37
+ **🌐 [git0.js.org](https://git0.js.org)**
38
+
36
39
  ## 🚀 Installation
37
40
 
38
41
  ```bash
@@ -55,6 +58,8 @@ bun install -g git0
55
58
  - **Smart IDE integration** - automatically opens projects in your preferred editor
56
59
  - **Cross-platform support** - works on Windows, macOS, and Linux
57
60
  - **Conflict resolution** - handles directory naming conflicts automatically
61
+ - **Partial paths** - paste a `/tree/` or `/blob/` link and download just that folder or file
62
+ - **Optional `.git` history** - `--history` attaches the full history *after* the source is already on disk
58
63
  - **Faster than git** - skips `.git` history and uncompresses while downloading
59
64
 
60
65
  ## 🎯 Usage
@@ -74,6 +79,119 @@ git0 facebook/react
74
79
  # Use git0 without installing, (only node needed)
75
80
  # (copy this line into your project's readme to help others setup)
76
81
  npx git0 facebook/react
82
+
83
+ # Download only one folder — paste the GitHub link as you copied it
84
+ g https://github.com/facebook/react/tree/main/packages/react-dom
85
+
86
+ # ...or one file
87
+ g https://github.com/debate/debate-ai.com/blob/master/.continue/agents/new-config.yaml
88
+
89
+ # Same thing, without a URL
90
+ git0 facebook/react/packages/react-dom
91
+ git0 facebook/react --path=packages/react-dom
92
+
93
+ # A specific branch, tag or commit
94
+ git0 facebook/react --branch=v18.2.0
95
+
96
+ # With the full .git history attached after the download
97
+ git0 facebook/react --history
98
+
99
+ # History only — no working files at all
100
+ git0 facebook/react --history-only
101
+ ```
102
+
103
+ ### Options
104
+
105
+ | Flag | What it does |
106
+ | --- | --- |
107
+ | `--path=<path>` | Download only that folder or file of the repository |
108
+ | `--branch=<ref>` | Download a branch, tag or commit instead of the default branch |
109
+ | `--history` | Also fetch the full `.git` history, in the background |
110
+ | `--history-only` | Clone only the history — no working files |
111
+ | `--mirror` | Keep every ref (notes, remote-tracking) when cloning history |
112
+
113
+ ## 📁 Partial Paths
114
+
115
+ Copy a link out of the GitHub UI and git0 downloads exactly what the link points
116
+ at, not the repository around it:
117
+
118
+ ```bash
119
+ g https://github.com/facebook/react/tree/main/packages/react-dom
120
+ # → ./react-dom/ — the contents of that folder, at the root
121
+ ```
122
+
123
+ The branch and the path are both read off the URL. Entries outside the path are
124
+ discarded as the tarball streams, so they are never written to disk, and what is
125
+ kept is lifted to the root of the target folder — you get `react-dom/`, not
126
+ `react/packages/react-dom/`.
127
+
128
+ The folder is named after the path rather than the repository, because that is
129
+ what you asked for. A `/blob/` link to a single file downloads that one file into
130
+ its enclosing folder's name. Pass a second argument, or `--path` with any repo
131
+ reference, if you'd rather name it yourself:
132
+
133
+ ```bash
134
+ git0 facebook/react --path=packages/react-dom my-copy
135
+ ```
136
+
137
+ ## 🕐 Downloading With `.git` History
138
+
139
+ By default git0 downloads a tarball — the source at one commit, no history. That
140
+ is what makes it quick. When you *do* want the history, `--history` gets you both
141
+ without giving up the speed:
142
+
143
+ ```bash
144
+ git0 facebook/react --history
145
+ ```
146
+
147
+ The source tarball lands first. The history is fetched **after** the archive has
148
+ finished unzipping, in the background, while the IDE opens and dependencies
149
+ install. You end up in exactly the state `git clone` would have left you in — a
150
+ working tree with a full `.git` beside it, `git log` and `git blame` working,
151
+ `git status` clean — except you started working several seconds earlier.
152
+
153
+ **Why the lazy order is faster.** `git clone` is one serial operation: the remote
154
+ counts and packs every object in the project's history, sends the pack, and only
155
+ then writes files you can run. Nothing can start until all of it finishes. A
156
+ GitHub tarball is a pre-made, cached, already-compressed snapshot of one commit,
157
+ so git0 pipes it straight into a `tar` extractor — files are being written while
158
+ the rest is still downloading. And history is the one part of a clone that
159
+ nothing on the critical path is waiting for: `bun install` does not need `git
160
+ log`. So it is fetched last, overlapping the install rather than blocking it.
161
+
162
+ There is a benchmark for this in [`benchmark/`](./benchmark) — it measures
163
+ *time until the project can run*, separately from *time until history is
164
+ available*, for `git clone`, `git clone --depth=1`, git0, and `git0 --history`:
165
+
166
+ ```bash
167
+ bun run bench facebook/react
168
+ ```
169
+
170
+ ### History Only
171
+
172
+ To take the history and none of the working files — for a backup, a mirror, or
173
+ to move a repository between hosts:
174
+
175
+ ```bash
176
+ git0 facebook/react --history-only
177
+ # → ./react.git/ — HEAD, config, objects/, refs/, packed-refs
178
+ ```
179
+
180
+ That directory holds what normally lives inside `.git`, placed at its root: all
181
+ the commits, branches and tags, no editable files. Read it without checking
182
+ anything out, or restore a normal working copy from it:
183
+
184
+ ```bash
185
+ git --git-dir=react.git log --all --oneline
186
+ git clone react.git react
187
+ ```
188
+
189
+ `--history-only` uses a bare clone (branches and tags). Add `--mirror` for every
190
+ ref Git can fetch — notes, remote-tracking refs and the rest — which is what you
191
+ want when the point is a complete backup:
192
+
193
+ ```bash
194
+ git0 facebook/react --history-only --mirror
77
195
  ```
78
196
 
79
197
  ### Supported Project Types
@@ -104,11 +222,12 @@ git0 automatically detects and opens projects in your preferred IDE:
104
222
 
105
223
  ### What Happens After Download
106
224
 
107
- 1. **Repository is downloaded** to your current directory
225
+ 1. **Repository is downloaded** to your current directory — only the requested path, if you named one
108
226
  2. **Project type is detected** automatically
109
227
  3. **Dependencies are installed** based on project type
110
228
  4. **IDE is launched** automatically (if available)
111
229
  5. **Development server starts** (for Node.js projects)
230
+ 6. **`.git` history is attached** in the background, if you passed `--history`
112
231
 
113
232
  If a directory with the same name exists, git0 automatically appends a number (e.g., `react-2`, `react-3`).
114
233
 
package/src/args.ts ADDED
@@ -0,0 +1,58 @@
1
+ import chalk from 'chalk';
2
+
3
+ /**
4
+ * The flags git0 accepts, and what they mean.
5
+ *
6
+ * Kept to a handful on purpose — the common case is one argument and no flags.
7
+ */
8
+ export interface Flags {
9
+ /** `--path=<p>` — limit the download to one folder or file of the repo. */
10
+ path?: string;
11
+ /** `--branch=<ref>` — download a branch, tag or commit other than the default. */
12
+ branch?: string;
13
+ /** `--history` — also attach the full `.git` history, in the background. */
14
+ history: boolean;
15
+ /** `--history-only` — clone only the history, no working files. */
16
+ historyOnly: boolean;
17
+ /** `--mirror` — use `--mirror` rather than `--bare` when cloning history. */
18
+ mirror: boolean;
19
+ }
20
+
21
+ /**
22
+ * Splits `process.argv` into positional arguments and flags.
23
+ *
24
+ * Deliberately hand-rolled rather than pulled in: git0's surface is one
25
+ * positional argument and five flags, and the argument itself is frequently a
26
+ * URL, which most parsers are keen to mangle.
27
+ *
28
+ * @param argv - Arguments after the node binary and script name.
29
+ * @returns The positional arguments and the parsed flags.
30
+ *
31
+ * @example
32
+ * parseArgs(['facebook/react', '--history', '--path=packages/react-dom']);
33
+ * // → { positional: ['facebook/react'],
34
+ * // flags: { history: true, path: 'packages/react-dom', … } }
35
+ */
36
+ export function parseArgs(argv: string[]): { positional: string[]; flags: Flags } {
37
+ const positional: string[] = [];
38
+ const flags: Flags = { history: false, historyOnly: false, mirror: false };
39
+
40
+ for (const arg of argv) {
41
+ if (!arg.startsWith('--')) {
42
+ positional.push(arg);
43
+ continue;
44
+ }
45
+
46
+ const [name, ...rest] = arg.slice(2).split('=');
47
+ const value = rest.join('=');
48
+
49
+ if (name === 'path') flags.path = value;
50
+ else if (name === 'branch' || name === 'ref') flags.branch = value;
51
+ else if (name === 'history' || name === 'git') flags.history = true;
52
+ else if (name === 'history-only' || name === 'git-only') flags.historyOnly = true;
53
+ else if (name === 'mirror') flags.mirror = true;
54
+ else console.log(chalk.yellow(`Ignoring unknown flag: ${arg}`));
55
+ }
56
+
57
+ return { positional, flags };
58
+ }
package/src/cli.ts CHANGED
@@ -1,10 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import inquirer from 'inquirer';
3
3
  import chalk from 'chalk';
4
- import GithubAPI from './github-api.js';
4
+ import path from 'path';
5
+ import GithubAPI, { type RepoTarget } from './github-api.js';
5
6
  import { openInIDE } from './ide.js';
6
7
  import { installDependencies } from './install.js';
7
8
  import { showPackageMenu } from './package-menu.js';
9
+ import { attachGitHistoryLazily, cloneHistoryOnly } from './history.js';
10
+ import { parseArgs, type Flags } from './args.js';
11
+ import { getAvailableDirectoryName } from './download.js';
8
12
  import { printLogo } from './utils.js';
9
13
  import type { SearchResult } from './types.js';
10
14
 
@@ -17,19 +21,71 @@ const Github = new GithubAPI({ debug: false });
17
21
  * The IDE launch is deferred 500 ms so the extraction can finish writing files
18
22
  * before the editor tries to index them.
19
23
  *
20
- * @param repo - GitHub URL (`https://…`) or `owner/repo` shorthand.
24
+ * With `--history`, the `.git` directory is fetched *after* extraction and runs
25
+ * alongside the IDE launch and the dependency install rather than in front of
26
+ * them — see {@link attachGitHistoryLazily} for why that ordering is the whole
27
+ * point.
28
+ *
29
+ * @param target - Parsed repository reference, including any sub-path.
21
30
  * @param folderPath - Optional name for the extraction directory; defaults to
22
- * the repository name.
31
+ * the repository name, or the sub-path's name when one is given.
32
+ * @param flags - Parsed CLI flags.
23
33
  *
24
34
  * @example
25
- * await downloadRepoAndSetup('https://github.com/vitejs/vite');
35
+ * await downloadRepoAndSetup(parseTarget('vitejs/vite') as RepoTarget);
26
36
  * // Downloads vite/, opens it in the first available IDE, runs bun install
27
37
  */
28
- async function downloadRepoAndSetup(repo: string, folderPath: string | null = null): Promise<void> {
38
+ async function downloadRepoAndSetup(
39
+ target: RepoTarget,
40
+ folderPath: string | null = null,
41
+ flags: Flags = { history: false, historyOnly: false, mirror: false }
42
+ ): Promise<void> {
29
43
  printLogo();
30
- const extractPath = await Github.downloadRepo(repo, folderPath);
44
+
45
+ const ref = flags.branch || target.ref;
46
+ const extractPath = await Github.downloadRepo(target.href, folderPath, {
47
+ subPath: flags.path ?? target.subPath,
48
+ subPathType: flags.path ? '' : target.subPathType,
49
+ ref,
50
+ });
51
+
52
+ // Started here, not awaited here: the files are on disk, so everything below
53
+ // can proceed while the history downloads.
54
+ const history = flags.history
55
+ ? attachGitHistoryLazily(extractPath, target.href, { mirror: flags.mirror, ref })
56
+ : Promise.resolve();
57
+
31
58
  setTimeout(() => openInIDE(extractPath), 500);
32
59
  await installDependencies(extractPath);
60
+ await history;
61
+ }
62
+
63
+ /**
64
+ * Handles `--history-only`: clones the repository's history into `<name>.git`
65
+ * and downloads no working files at all.
66
+ *
67
+ * @param target - Parsed repository reference.
68
+ * @param flags - Parsed CLI flags.
69
+ *
70
+ * @internal
71
+ */
72
+ async function downloadHistoryOnly(target: RepoTarget, flags: Flags): Promise<void> {
73
+ printLogo();
74
+
75
+ const targetDir = getAvailableDirectoryName(path.resolve(process.cwd(), `${target.name}.git`));
76
+ console.log(chalk.blue(`📦 Cloning ${target.name} history into ${path.basename(targetDir)}...`));
77
+
78
+ try {
79
+ await cloneHistoryOnly(target.href, targetDir, { bare: !flags.mirror });
80
+ console.log(chalk.green(`✅ History-only clone at ${targetDir}`));
81
+ console.log(chalk.white(' Read the log without checking anything out:'));
82
+ console.log(chalk.gray(` git --git-dir="${targetDir}" log --all --oneline`));
83
+ console.log(chalk.white(' Or restore a working copy from it:'));
84
+ console.log(chalk.gray(` git clone "${targetDir}" ${target.name}`));
85
+ } catch (error: any) {
86
+ console.error(chalk.red('❌ History clone failed:'), error.message);
87
+ process.exit(1);
88
+ }
33
89
  }
34
90
 
35
91
  /**
@@ -61,12 +117,19 @@ function formatRepoChoice(repo: SearchResult): string {
61
117
  * source code, or both when a repository has release assets.
62
118
  *
63
119
  * @param selectedRepo - The repository the user chose in the search results.
120
+ * @param flags - Parsed CLI flags, forwarded to the source download.
64
121
  *
65
122
  * @internal
66
123
  */
67
- async function handleRepoDownload(selectedRepo: SearchResult): Promise<void> {
124
+ async function handleRepoDownload(selectedRepo: SearchResult, flags: Flags): Promise<void> {
125
+ const target = Github.parseTarget(selectedRepo.url);
126
+ if (!target) {
127
+ console.log(chalk.yellow(`Could not parse repository URL: ${selectedRepo.url}`));
128
+ return;
129
+ }
130
+
68
131
  if (!selectedRepo.hasReleases && !selectedRepo.hasCompatibleReleases) {
69
- await downloadRepoAndSetup(selectedRepo.url);
132
+ await downloadRepoAndSetup(target, null, flags);
70
133
  return;
71
134
  }
72
135
 
@@ -89,7 +152,7 @@ async function handleRepoDownload(selectedRepo: SearchResult): Promise<void> {
89
152
  );
90
153
  }
91
154
  if (downloadChoice === 'source' || downloadChoice === 'both') {
92
- await downloadRepoAndSetup(selectedRepo.url);
155
+ await downloadRepoAndSetup(target, null, flags);
93
156
  }
94
157
  }
95
158
 
@@ -97,9 +160,10 @@ async function handleRepoDownload(selectedRepo: SearchResult): Promise<void> {
97
160
  * CLI entry point for the `git0` / `g` / `gg` commands.
98
161
  *
99
162
  * **Flow:**
100
- * 1. Parse `process.argv` for a search query or direct repo URL.
163
+ * 1. Parse `process.argv` into flags and a search query or direct repo URL.
101
164
  * 2. If the argument looks like a GitHub URL or `owner/repo`, download it
102
- * directly — optionally into a custom folder (second CLI argument).
165
+ * directly — optionally into a custom folder (second positional argument),
166
+ * limited to a sub-path, and optionally with `.git` history.
103
167
  * 3. Otherwise, search GitHub and present an interactive list.
104
168
  * 4. After the user picks a repo, offer package vs. source download when
105
169
  * releases are available.
@@ -111,23 +175,40 @@ async function handleRepoDownload(selectedRepo: SearchResult): Promise<void> {
111
175
  * // git0 facebook/react
112
176
  * // git0 https://github.com/vitejs/vite my-vite-copy
113
177
  *
178
+ * // Just one folder of it:
179
+ * // git0 https://github.com/facebook/react/tree/main/packages/react-dom
180
+ * // git0 facebook/react --path=packages/react-dom
181
+ *
182
+ * // With git history, fetched after the source is already on disk:
183
+ * // git0 facebook/react --history
184
+ * // git0 facebook/react --history-only
185
+ *
114
186
  * // Search:
115
187
  * // git0 react template starter
116
188
  */
117
189
  async function main(): Promise<void> {
118
190
  printLogo();
119
191
 
120
- const args = process.argv.slice(2);
121
- if (!args.length) {
122
- console.log(chalk.yellow('Usage: git0 <github-url | owner/repo | search-query>'));
192
+ const { positional, flags } = parseArgs(process.argv.slice(2));
193
+ if (!positional.length) {
194
+ console.log(chalk.yellow('Usage: git0 <github-url | owner/repo[/path] | search-query>'));
195
+ console.log(chalk.gray(' --path=<path> download only that folder or file'));
196
+ console.log(chalk.gray(' --branch=<ref> download a branch, tag or commit'));
197
+ console.log(chalk.gray(' --history also fetch .git history, in the background'));
198
+ console.log(chalk.gray(' --history-only clone only the history, no working files'));
199
+ console.log(chalk.gray(' --mirror keep every ref when cloning history'));
123
200
  process.exit(1);
124
201
  }
125
202
 
126
- const query = args.join(' ');
127
- const parsed = Github.parseURL(query);
203
+ const query = positional.join(' ');
204
+ const target = Github.parseTarget(query);
128
205
 
129
- if (parsed && parsed.owner && parsed.name) {
130
- await downloadRepoAndSetup(parsed.href, args[1] ?? null);
206
+ if (target) {
207
+ if (flags.historyOnly) {
208
+ await downloadHistoryOnly(target, flags);
209
+ return;
210
+ }
211
+ await downloadRepoAndSetup(target, positional[1] ?? null, flags);
131
212
  return;
132
213
  }
133
214
 
@@ -144,7 +225,10 @@ async function main(): Promise<void> {
144
225
  choices: results.map(repo => ({ name: formatRepoChoice(repo), value: repo })),
145
226
  });
146
227
 
147
- await handleRepoDownload(selectedRepo);
228
+ await handleRepoDownload(selectedRepo, flags);
148
229
  }
149
230
 
150
- main();
231
+ main().catch((error: any) => {
232
+ console.error(chalk.red(`❌ ${error.message}`));
233
+ process.exit(1);
234
+ });