git0 0.3.65 → 0.3.67

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/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  <!-- template-git-repo:badges:start -->
6
6
  <p align="center">
7
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>
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
+ <a href="https://1computer.online/docs/packages/git0-repo-downloader"><img src="https://img.shields.io/badge/Docs-blue?logo=ReadTheDocs&logoColor=white" alt="Documentation" /></a>
9
9
  <br />
10
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>
11
11
  <a href="https://www.npmjs.com/package/git0"><img src="https://img.shields.io/npm/dm/git0.svg" alt="NPM Monthly Downloads" /></a>
@@ -61,6 +61,7 @@ bun install -g git0
61
61
  - **Partial paths** - paste a `/tree/` or `/blob/` link and download just that folder or file
62
62
  - **Optional `.git` history** - `--history` attaches the full history *after* the source is already on disk
63
63
  - **Faster than git** - skips `.git` history and uncompresses while downloading
64
+ - **One-click install service** - `--service=install` lets website download buttons install the right release for your system and show progress in the button
64
65
 
65
66
  ## 🎯 Usage
66
67
 
@@ -218,6 +219,53 @@ git0 automatically detects and opens projects in your preferred IDE:
218
219
  - **Neovim**
219
220
  - **Webstorm**
220
221
 
222
+ ## ⚡ Background Install Service
223
+
224
+ git0 can run as a small background service that turns any website's download button into a one-click installer. The button hands the service a repository; the service picks the release asset built for your OS and CPU, downloads it, installs it, and opens the app, while the button fills up as a progress bar.
225
+
226
+ ```bash
227
+ git0 --service=install # start at login and register git0:// links
228
+ git0 --service=status # is it running, and what is it installing
229
+ git0 --service=uninstall # remove it
230
+ git0 --service # or run it in the foreground, on 127.0.0.1:47100
231
+ ```
232
+
233
+ It installs as a **per-user** service, because launching a desktop app has to happen inside your session: a `systemd --user` unit on Linux, a LaunchAgent on macOS, and a logon task on Windows. No admin rights are needed to install it.
234
+
235
+ ### Add the button to a site
236
+
237
+ ```html
238
+ <a href="https://github.com/owner/repo/releases/latest" data-git0-repo="owner/repo">Download</a>
239
+ <script src="https://unpkg.com/git0/demo/git0-button.js"></script>
240
+ ```
241
+
242
+ When the service is running, clicking the link does not download a file. The button morphs into a progress bar showing the download, then the install, then the launch. When the service is not running, the link behaves as a normal link. `demo/index.html` has a live and a simulated example.
243
+
244
+ Links also work without the script: `git0://install/owner/repo` is registered with the OS on Linux and Windows, and `http://127.0.0.1:47100/install?repo=owner/repo` works everywhere.
245
+
246
+ ### What it installs
247
+
248
+ | OS | Preferred asset, in order | Result |
249
+ | --- | --- | --- |
250
+ | Linux | `.AppImage`, `.deb`, `.rpm`, `.tar.gz`, bare binary | AppImage and binaries go to `~/.git0/apps`; `.deb`/`.rpm` install through the desktop's password prompt (`pkexec`) |
251
+ | macOS | `.dmg`, `.pkg`, `.zip`/`.tar.gz` | The `.app` is copied to `~/Applications`; a `.pkg` opens Installer |
252
+ | Windows | setup `.exe`, `.msi`, `.zip` | The installer runs with its own UI; a portable `.zip` is extracted to `~/.git0/apps` |
253
+
254
+ Asset names are matched on whole words, so `darwin` is never mistaken for `win`. An exact CPU match beats an unlabelled build, and an unlabelled build beats Rosetta or 32-bit fallbacks. Checksums, signatures and source archives are never picked. A command-line tool is installed but not launched, because it would start with no terminal attached.
255
+
256
+ ### Security
257
+
258
+ Any website can ask, but only you can say yes. The first install of a repository opens a git0 page from `127.0.0.1` asking you to allow it, and the website cannot answer that prompt for you: the page carries a one-time token, is served without CORS headers so other sites can't read it, and refuses to be framed. Tick "Always allow" to skip the prompt for that repository next time (stored in `~/.git0/service.json`). The service listens on loopback only, rejects requests whose `Host` isn't loopback (DNS rebinding), and caps pending prompts at five.
259
+
260
+ ### HTTP API
261
+
262
+ | Route | Returns |
263
+ | --- | --- |
264
+ | `GET /health` | `{ ok, name: "git0", version, os, arch }` |
265
+ | `POST /install` `{ "repo": "owner/repo" }` | the job, plus `statusUrl` and `approveUrl` |
266
+ | `GET /status/:id` | `status` (`awaiting-approval`, `resolving`, `downloading`, `installing`, `launching`, `done`, `error`, `denied`), `progress` 0–1, `receivedBytes`, `totalBytes`, `asset`, `tag`, `installedPath`, `launched`, `error` |
267
+ | `GET /jobs` | every job |
268
+
221
269
  ## 🔧 Configuration
222
270
 
223
271
  ### What Happens After Download
@@ -0,0 +1,237 @@
1
+ /**
2
+ * git0 install button.
3
+ *
4
+ * Turns any link or button carrying `data-git0-repo="owner/repo"` into a
5
+ * one-click installer. When the git0 background service is running on the
6
+ * visitor's machine, a click does not download a file: the button asks the
7
+ * service to install the right release for that machine, then morphs into a
8
+ * progress bar that fills as the service downloads, installs and launches it.
9
+ * When the service is not running, the link behaves exactly as before — it
10
+ * still points at your normal download page.
11
+ *
12
+ * <a href="https://github.com/owner/repo/releases/latest"
13
+ * data-git0-repo="owner/repo" class="git0-button">Download</a>
14
+ * <script src="git0-button.js"></script>
15
+ *
16
+ * Options, as attributes on the element:
17
+ * data-git0-port="47100" the service port, if changed with --port
18
+ * data-git0-simulate run the animation without the service (demos)
19
+ *
20
+ * The first install of a repository opens a small git0 page from 127.0.0.1
21
+ * asking the visitor to allow it. That prompt is the service's, not the
22
+ * website's, and a website cannot answer it.
23
+ */
24
+ (function () {
25
+ const DEFAULT_PORT = 47100;
26
+ const TERMINAL = ['done', 'error', 'denied'];
27
+
28
+ const css = `
29
+ .git0-button{--g0-accent:#6d28d9;--g0-track:rgba(109,40,217,.18);--g0-fg:#fff;
30
+ position:relative;display:inline-flex;align-items:center;justify-content:center;
31
+ min-width:220px;height:48px;padding:0 24px;border:0;border-radius:24px;
32
+ background:var(--g0-accent);color:var(--g0-fg);font:600 15px/1 system-ui,sans-serif;
33
+ text-decoration:none;cursor:pointer;overflow:hidden;isolation:isolate;
34
+ transition:min-width .45s cubic-bezier(.2,.8,.2,1),border-radius .45s,background-color .3s,box-shadow .3s}
35
+ .git0-button:hover{box-shadow:0 6px 20px rgba(109,40,217,.35)}
36
+ .git0-button:focus-visible{outline:3px solid var(--g0-track);outline-offset:3px}
37
+ .git0-button .g0-label{position:relative;z-index:2;white-space:nowrap;transition:opacity .2s}
38
+ .git0-button .g0-fill{position:absolute;inset:0;width:0;z-index:1;background:var(--g0-accent);
39
+ transition:width .35s ease}
40
+ .git0-button.g0-busy{min-width:320px;border-radius:12px;background:var(--g0-track);color:inherit;cursor:default;box-shadow:none}
41
+ .git0-button .g0-label-on{position:absolute;inset:0;z-index:3;display:none;align-items:center;justify-content:center;
42
+ color:#fff;white-space:nowrap;clip-path:inset(0 calc(100% - var(--g0-p,0%)) 0 0);transition:clip-path .35s ease}
43
+ .git0-button.g0-busy .g0-label{color:#3b0764}
44
+ .git0-button.g0-busy:not(.g0-indeterminate) .g0-label-on{display:flex}
45
+ .git0-button.g0-indeterminate .g0-fill{width:35%!important;animation:g0-slide 1.1s ease-in-out infinite}
46
+ .git0-button.g0-done{background:#15803d;min-width:260px;border-radius:24px}
47
+ .git0-button.g0-done .g0-fill{opacity:0}
48
+ .git0-button.g0-done .g0-label{color:#fff}
49
+ .git0-button.g0-error{background:#b91c1c;border-radius:24px}
50
+ .git0-button.g0-error .g0-fill{opacity:0}
51
+ .git0-button.g0-error .g0-label{color:#fff}
52
+ @keyframes g0-slide{0%{transform:translateX(-100%)}100%{transform:translateX(300%)}}
53
+ @media (prefers-reduced-motion:reduce){.git0-button,.git0-button .g0-fill{transition:none}
54
+ .git0-button.g0-indeterminate .g0-fill{animation:none;width:100%!important;opacity:.4}}`;
55
+
56
+ function injectCss() {
57
+ if (document.getElementById('git0-button-css')) return;
58
+ const style = document.createElement('style');
59
+ style.id = 'git0-button-css';
60
+ style.textContent = css;
61
+ document.head.appendChild(style);
62
+ }
63
+
64
+ const mb = n => (n / 1048576).toFixed(1);
65
+
66
+ function labelFor(job) {
67
+ switch (job.status) {
68
+ case 'awaiting-approval': return 'Allow the install in the git0 tab…';
69
+ case 'resolving': return 'Finding your build…';
70
+ case 'downloading': return job.totalBytes
71
+ ? `Downloading ${Math.round((job.receivedBytes / job.totalBytes) * 100)}% · ${mb(job.receivedBytes)} / ${mb(job.totalBytes)} MB`
72
+ : 'Downloading…';
73
+ case 'installing': return 'Installing…';
74
+ case 'launching': return 'Launching…';
75
+ case 'done': return job.launched ? 'Installed · opened ✓' : 'Installed ✓';
76
+ case 'denied': return 'Install cancelled';
77
+ default: return `Failed — ${job.error || 'try again'}`;
78
+ }
79
+ }
80
+
81
+ /** Paints a job onto the button. */
82
+ function render(el, job) {
83
+ const fill = el.querySelector('.g0-fill');
84
+ const label = el.querySelector('.g0-label');
85
+ const busy = !TERMINAL.includes(job.status);
86
+ const indeterminate = job.status === 'awaiting-approval' || job.status === 'resolving' ||
87
+ (job.status === 'downloading' && !job.totalBytes);
88
+
89
+ el.classList.toggle('g0-busy', busy);
90
+ el.classList.toggle('g0-indeterminate', busy && indeterminate);
91
+ el.classList.toggle('g0-done', job.status === 'done');
92
+ el.classList.toggle('g0-error', job.status === 'error' || job.status === 'denied');
93
+ el.setAttribute('aria-busy', String(busy));
94
+ const pct = `${Math.round(job.progress * 100)}%`;
95
+ if (!indeterminate) fill.style.width = pct;
96
+ // A white copy of the label, clipped to the filled part, keeps the text
97
+ // readable on both sides of the bar's edge.
98
+ el.style.setProperty('--g0-p', pct);
99
+ label.textContent = labelFor(job);
100
+ el.querySelector('.g0-label-on').textContent = label.textContent;
101
+ fill.setAttribute('aria-valuenow', String(Math.round(job.progress * 100)));
102
+ }
103
+
104
+ function prepare(el) {
105
+ if (el.querySelector('.g0-label')) return;
106
+ const text = el.textContent.trim();
107
+ el.textContent = '';
108
+ const fill = document.createElement('span');
109
+ fill.className = 'g0-fill';
110
+ fill.setAttribute('role', 'progressbar');
111
+ fill.setAttribute('aria-valuemin', '0');
112
+ fill.setAttribute('aria-valuemax', '100');
113
+ const label = document.createElement('span');
114
+ label.className = 'g0-label';
115
+ label.setAttribute('aria-live', 'polite');
116
+ label.textContent = text;
117
+ const labelOn = document.createElement('span');
118
+ labelOn.className = 'g0-label-on';
119
+ labelOn.setAttribute('aria-hidden', 'true');
120
+ el.append(fill, label, labelOn);
121
+ el.dataset.git0Label = text;
122
+ }
123
+
124
+ function reset(el) {
125
+ el.className = el.className.replace(/\bg0-\S+/g, '').trim();
126
+ el.querySelector('.g0-fill').style.width = '0';
127
+ el.style.removeProperty('--g0-p');
128
+ el.querySelector('.g0-label').textContent = el.dataset.git0Label;
129
+ el.removeAttribute('aria-busy');
130
+ delete el.dataset.git0Running;
131
+ }
132
+
133
+ async function serviceUp(base) {
134
+ try {
135
+ const ctl = new AbortController();
136
+ const t = setTimeout(() => ctl.abort(), 700);
137
+ const res = await fetch(`${base}/health`, { signal: ctl.signal });
138
+ clearTimeout(t);
139
+ return res.ok && (await res.json()).name === 'git0';
140
+ } catch {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ /** Plays the whole sequence with fake numbers, for demo pages. */
146
+ async function simulate(el) {
147
+ const total = 48 * 1048576;
148
+ const job = { status: 'resolving', progress: 0, receivedBytes: 0, totalBytes: 0, launched: false };
149
+ render(el, job);
150
+ await new Promise(r => setTimeout(r, 900));
151
+ job.status = 'downloading';
152
+ job.totalBytes = total;
153
+ while (job.receivedBytes < total) {
154
+ job.receivedBytes = Math.min(total, job.receivedBytes + total * (0.015 + Math.random() * 0.035));
155
+ job.progress = (job.receivedBytes / total) * 0.9;
156
+ render(el, job);
157
+ await new Promise(r => setTimeout(r, 90));
158
+ }
159
+ for (const [status, progress, wait] of [['installing', 0.94, 900], ['launching', 0.98, 600]]) {
160
+ Object.assign(job, { status, progress });
161
+ render(el, job);
162
+ await new Promise(r => setTimeout(r, wait));
163
+ }
164
+ Object.assign(job, { status: 'done', progress: 1, launched: true });
165
+ render(el, job);
166
+ }
167
+
168
+ async function onClick(event) {
169
+ const el = event.currentTarget;
170
+ if (el.dataset.git0Running) { event.preventDefault(); return; }
171
+
172
+ if (el.hasAttribute('data-git0-simulate')) {
173
+ event.preventDefault();
174
+ el.dataset.git0Running = '1';
175
+ await simulate(el);
176
+ setTimeout(() => reset(el), 4000);
177
+ return;
178
+ }
179
+
180
+ const base = `http://127.0.0.1:${el.dataset.git0Port || DEFAULT_PORT}`;
181
+ // Decide synchronously that we own the click, so the browser does not
182
+ // start the plain download while we check for the service. If it turns
183
+ // out not to be running, follow the link ourselves.
184
+ event.preventDefault();
185
+ el.dataset.git0Running = '1';
186
+ // Opened now, inside the click, so pop-up blockers allow it; pointed at
187
+ // the approval page once we know there is one, closed if there is not.
188
+ let approval = null;
189
+
190
+ if (!(await serviceUp(base))) {
191
+ reset(el);
192
+ const href = el.getAttribute('href');
193
+ if (href) window.location.href = href;
194
+ return;
195
+ }
196
+
197
+ try {
198
+ approval = window.open('', 'git0-approval', 'popup,width=480,height=420');
199
+ const res = await fetch(`${base}/install`, {
200
+ method: 'POST',
201
+ headers: { 'Content-Type': 'application/json' },
202
+ body: JSON.stringify({ repo: el.dataset.git0Repo }),
203
+ });
204
+ let job = await res.json();
205
+ if (!res.ok) throw new Error(job.error || `HTTP ${res.status}`);
206
+
207
+ if (job.approveUrl && approval) approval.location.href = job.approveUrl;
208
+ else if (approval) approval.close();
209
+
210
+ while (!TERMINAL.includes(job.status)) {
211
+ render(el, job);
212
+ await new Promise(r => setTimeout(r, 400));
213
+ job = await (await fetch(`${base}/status/${job.id}`)).json();
214
+ }
215
+ render(el, job);
216
+ } catch (err) {
217
+ if (approval) approval.close();
218
+ render(el, { status: 'error', progress: 0, error: err.message });
219
+ }
220
+ setTimeout(() => reset(el), 6000);
221
+ }
222
+
223
+ function init(root) {
224
+ injectCss();
225
+ for (const el of (root || document).querySelectorAll('[data-git0-repo]')) {
226
+ if (el.dataset.git0Bound) continue;
227
+ el.dataset.git0Bound = '1';
228
+ el.classList.add('git0-button');
229
+ prepare(el);
230
+ el.addEventListener('click', onClick);
231
+ }
232
+ }
233
+
234
+ window.git0Button = { init, render };
235
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => init());
236
+ else init();
237
+ })();
@@ -0,0 +1,71 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>git0 install button</title>
7
+ <style>
8
+ :root { --bg:#fafafa; --fg:#18181b; --muted:#52525b; --card:#fff; --line:#e4e4e7; }
9
+ @media (prefers-color-scheme: dark) {
10
+ :root:not([data-theme="light"]) { --bg:#0b0b0f; --fg:#f4f4f5; --muted:#a1a1aa; --card:#18181b; --line:#27272a; }
11
+ }
12
+ :root[data-theme="dark"] { --bg:#0b0b0f; --fg:#f4f4f5; --muted:#a1a1aa; --card:#18181b; --line:#27272a; }
13
+ body { margin:0; background:var(--bg); color:var(--fg); font:16px/1.6 system-ui,sans-serif; }
14
+ main { max-width:720px; margin:0 auto; padding:48px 16px; }
15
+ h1 { font-size:28px; margin:0 0 8px; }
16
+ p { color:var(--muted); margin:0 0 16px; }
17
+ .card { background:var(--card); border:1px solid var(--line); border-radius:16px; padding:28px; margin:24px 0; }
18
+ .card h2 { font-size:17px; margin:0 0 6px; }
19
+ .stage { display:flex; flex-wrap:wrap; gap:16px; align-items:center; margin-top:18px; }
20
+ code, pre { font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace; }
21
+ pre { background:var(--bg); border:1px solid var(--line); border-radius:10px; padding:14px; overflow-x:auto; }
22
+ .status { font-size:13px; }
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <main>
27
+ <h1>One-click install from any website</h1>
28
+ <p>
29
+ With the git0 background service running, a download button installs the right
30
+ release for the visitor's OS and CPU, shows the progress inside the button,
31
+ and opens the app when it is done.
32
+ </p>
33
+
34
+ <section class="card">
35
+ <h2>Simulated</h2>
36
+ <p>Plays the full sequence with fake numbers, no service needed.</p>
37
+ <div class="stage">
38
+ <a href="#" data-git0-repo="junegunn/fzf" data-git0-simulate>Download for your system</a>
39
+ </div>
40
+ </section>
41
+
42
+ <section class="card">
43
+ <h2>Live, through your local service</h2>
44
+ <p class="status" id="svc">Checking for the git0 service on 127.0.0.1:47100…</p>
45
+ <div class="stage">
46
+ <a href="https://github.com/obsidianmd/obsidian-releases/releases/latest"
47
+ data-git0-repo="obsidianmd/obsidian-releases">Install Obsidian</a>
48
+ <a href="https://github.com/junegunn/fzf/releases/latest"
49
+ data-git0-repo="junegunn/fzf">Install fzf</a>
50
+ </div>
51
+ <p style="margin-top:16px">Without the service these links fall back to the normal releases page. Start it with:</p>
52
+ <pre>npx git0 --service=install # start at login, register git0:// links
53
+ npx git0 --service # or just run it in this terminal</pre>
54
+ </section>
55
+
56
+ <section class="card">
57
+ <h2>Add it to your site</h2>
58
+ <pre>&lt;a href="https://github.com/owner/repo/releases/latest"
59
+ data-git0-repo="owner/repo"&gt;Download&lt;/a&gt;
60
+ &lt;script src="https://unpkg.com/git0/demo/git0-button.js"&gt;&lt;/script&gt;</pre>
61
+ </section>
62
+ </main>
63
+ <script src="git0-button.js"></script>
64
+ <script>
65
+ fetch('http://127.0.0.1:47100/health')
66
+ .then(r => r.json())
67
+ .then(h => { document.getElementById('svc').textContent = `git0 service ${h.version ?? ''} is running (${h.os} ${h.arch}).`; })
68
+ .catch(() => { document.getElementById('svc').textContent = 'The git0 service is not running, so these buttons fall back to the releases page.'; });
69
+ </script>
70
+ </body>
71
+ </html>