git0 0.3.66 → 0.3.68
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 +49 -1
- package/demo/git0-button.js +237 -0
- package/demo/index.html +71 -0
- package/dist/cli.js +1015 -4
- package/dist/cli.js.map +11 -5
- package/docs/content/docs/meta.json +1 -0
- package/docs/content/docs/service.mdx +51 -0
- package/package.json +1 -1
- package/src/args.ts +10 -0
- package/src/cli.ts +9 -0
- package/src/service/assets.ts +260 -0
- package/src/service/client.ts +61 -0
- package/src/service/index.ts +77 -0
- package/src/service/installer.ts +246 -0
- package/src/service/server.ts +540 -0
- package/src/service/system-service.ts +190 -0
- package/test/args.test.ts +11 -0
- package/test/service-assets.test.ts +181 -0
- package/test/service-installer.test.ts +201 -0
- package/test/service-server.test.ts +262 -0
package/dist/cli.js
CHANGED
|
@@ -23602,7 +23602,7 @@ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
|
|
|
23602
23602
|
var source_default = chalk;
|
|
23603
23603
|
|
|
23604
23604
|
// src/cli.ts
|
|
23605
|
-
import
|
|
23605
|
+
import path10 from "path";
|
|
23606
23606
|
|
|
23607
23607
|
// node_modules/grab-api.js/dist/grab-api.es.js
|
|
23608
23608
|
function log(message = "", options = {}) {
|
|
@@ -27915,12 +27915,1020 @@ function parseArgs(argv) {
|
|
|
27915
27915
|
flags.historyOnly = true;
|
|
27916
27916
|
else if (name === "mirror")
|
|
27917
27917
|
flags.mirror = true;
|
|
27918
|
+
else if (name === "service" && ["", "run", "install", "uninstall", "status"].includes(value)) {
|
|
27919
|
+
flags.service = value || "run";
|
|
27920
|
+
} else if (name === "port" && /^\d+$/.test(value))
|
|
27921
|
+
flags.port = Number(value);
|
|
27918
27922
|
else
|
|
27919
27923
|
console.log(source_default.yellow(`Ignoring unknown flag: ${arg}`));
|
|
27920
27924
|
}
|
|
27921
27925
|
return { positional, flags };
|
|
27922
27926
|
}
|
|
27923
27927
|
|
|
27928
|
+
// src/service/index.ts
|
|
27929
|
+
import fs10 from "fs";
|
|
27930
|
+
import path9 from "path";
|
|
27931
|
+
|
|
27932
|
+
// src/service/assets.ts
|
|
27933
|
+
var NOT_INSTALLABLE = /(\.(sha\d*|sha256sum|md5|sig|asc|pem|crt|txt|json|ya?ml|blockmap|sbom|spdx|intoto\.jsonl|zsync|sym|pdb|dSYM\.zip)$)|checksum|sha256sums|(^|[-_.])src([-_.]|$)|source/i;
|
|
27934
|
+
function tokens(name) {
|
|
27935
|
+
return new Set(name.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean));
|
|
27936
|
+
}
|
|
27937
|
+
function assetOs(name) {
|
|
27938
|
+
const lower = name.toLowerCase();
|
|
27939
|
+
const t = tokens(name);
|
|
27940
|
+
const has = (...words) => words.some((w2) => t.has(w2));
|
|
27941
|
+
const hits = [];
|
|
27942
|
+
if (has("win", "windows", "win32", "win64", "msvc", "mingw") || /\.(exe|msi|msix|appx)$/.test(lower)) {
|
|
27943
|
+
hits.push("windows");
|
|
27944
|
+
}
|
|
27945
|
+
if (has("mac", "macos", "darwin", "osx", "apple", "macosx") || /\.(dmg|pkg)$/.test(lower)) {
|
|
27946
|
+
hits.push("macos");
|
|
27947
|
+
}
|
|
27948
|
+
if (has("linux", "ubuntu", "debian", "fedora", "musl", "gnu") || /\.(deb|rpm|appimage|snap|flatpak)$/.test(lower)) {
|
|
27949
|
+
hits.push("linux");
|
|
27950
|
+
}
|
|
27951
|
+
if (hits.length === 1)
|
|
27952
|
+
return hits[0];
|
|
27953
|
+
return hits.length === 0 ? "any" : "ambiguous";
|
|
27954
|
+
}
|
|
27955
|
+
function assetArch(name) {
|
|
27956
|
+
const n = name.toLowerCase();
|
|
27957
|
+
if (/universal/.test(n))
|
|
27958
|
+
return "universal";
|
|
27959
|
+
if (/aarch64|arm64/.test(n))
|
|
27960
|
+
return "arm64";
|
|
27961
|
+
if (/armv[67]|armhf|armel|(^|[^a-z])arm([^a-z0-9]|$)/.test(n))
|
|
27962
|
+
return "arm";
|
|
27963
|
+
if (/x86[_-]64|amd64|x64|win64/.test(n))
|
|
27964
|
+
return "x86_64";
|
|
27965
|
+
if (/i[36]86|x86[_-]32|ia32|win32|(^|[^a-z0-9])386([^0-9]|$)|(^|[^a-z0-9])x86([^_0-9-]|$)/.test(n))
|
|
27966
|
+
return "i386";
|
|
27967
|
+
return "unknown";
|
|
27968
|
+
}
|
|
27969
|
+
function installKind(name) {
|
|
27970
|
+
const n = name.toLowerCase();
|
|
27971
|
+
if (n.endsWith(".appimage"))
|
|
27972
|
+
return "appimage";
|
|
27973
|
+
if (n.endsWith(".deb"))
|
|
27974
|
+
return "deb";
|
|
27975
|
+
if (n.endsWith(".rpm"))
|
|
27976
|
+
return "rpm";
|
|
27977
|
+
if (n.endsWith(".dmg"))
|
|
27978
|
+
return "dmg";
|
|
27979
|
+
if (n.endsWith(".pkg"))
|
|
27980
|
+
return "pkg";
|
|
27981
|
+
if (n.endsWith(".exe"))
|
|
27982
|
+
return "exe";
|
|
27983
|
+
if (n.endsWith(".msi"))
|
|
27984
|
+
return "msi";
|
|
27985
|
+
if (/\.(zip|tar\.gz|tgz|tar\.xz|txz|tar\.bz2|tbz2?|tar\.zst|tar)$/.test(n))
|
|
27986
|
+
return "archive";
|
|
27987
|
+
if (!/\.[a-z][a-z0-9]{0,4}$/.test(n))
|
|
27988
|
+
return "binary";
|
|
27989
|
+
return null;
|
|
27990
|
+
}
|
|
27991
|
+
var KIND_SCORE = {
|
|
27992
|
+
linux: { appimage: 40, deb: 30, rpm: 25, archive: 20, binary: 15 },
|
|
27993
|
+
macos: { dmg: 40, pkg: 35, archive: 25, binary: 15 },
|
|
27994
|
+
windows: { exe: 40, msi: 35, archive: 20 }
|
|
27995
|
+
};
|
|
27996
|
+
function scoreAsset(name, platform) {
|
|
27997
|
+
if (NOT_INSTALLABLE.test(name))
|
|
27998
|
+
return null;
|
|
27999
|
+
const kind = installKind(name);
|
|
28000
|
+
if (!kind)
|
|
28001
|
+
return null;
|
|
28002
|
+
const kindScore = KIND_SCORE[platform.os]?.[kind];
|
|
28003
|
+
if (kindScore === undefined)
|
|
28004
|
+
return null;
|
|
28005
|
+
const os5 = assetOs(name);
|
|
28006
|
+
if (os5 === "ambiguous")
|
|
28007
|
+
return null;
|
|
28008
|
+
if (os5 !== "any" && os5 !== platform.os)
|
|
28009
|
+
return null;
|
|
28010
|
+
const arch = assetArch(name);
|
|
28011
|
+
let archScore;
|
|
28012
|
+
if (arch === platform.arch)
|
|
28013
|
+
archScore = 100;
|
|
28014
|
+
else if (arch === "universal")
|
|
28015
|
+
archScore = 90;
|
|
28016
|
+
else if (arch === "unknown")
|
|
28017
|
+
archScore = platform.arch === "x86_64" ? 95 : 50;
|
|
28018
|
+
else if (platform.os === "macos" && platform.arch === "arm64" && arch === "x86_64")
|
|
28019
|
+
archScore = 10;
|
|
28020
|
+
else if (platform.os === "windows" && platform.arch === "x86_64" && arch === "i386")
|
|
28021
|
+
archScore = 10;
|
|
28022
|
+
else
|
|
28023
|
+
return null;
|
|
28024
|
+
const osScore = os5 === platform.os ? 5 : 0;
|
|
28025
|
+
return archScore + kindScore + osScore;
|
|
28026
|
+
}
|
|
28027
|
+
function pickAsset(releases, platform) {
|
|
28028
|
+
const published = releases.filter((r) => !r.draft);
|
|
28029
|
+
const stable = published.filter((r) => !r.prerelease);
|
|
28030
|
+
for (const pool of [stable, published]) {
|
|
28031
|
+
for (const release of pool) {
|
|
28032
|
+
let best = null;
|
|
28033
|
+
let bestScore = -1;
|
|
28034
|
+
for (const asset of release.assets ?? []) {
|
|
28035
|
+
const score = scoreAsset(asset.name, platform);
|
|
28036
|
+
if (score === null || score <= bestScore)
|
|
28037
|
+
continue;
|
|
28038
|
+
bestScore = score;
|
|
28039
|
+
best = {
|
|
28040
|
+
name: asset.name,
|
|
28041
|
+
url: asset.browser_download_url,
|
|
28042
|
+
size: asset.size,
|
|
28043
|
+
tag: release.tag_name,
|
|
28044
|
+
kind: installKind(asset.name)
|
|
28045
|
+
};
|
|
28046
|
+
}
|
|
28047
|
+
if (best)
|
|
28048
|
+
return best;
|
|
28049
|
+
}
|
|
28050
|
+
}
|
|
28051
|
+
return null;
|
|
28052
|
+
}
|
|
28053
|
+
function normalizeRepoSlug(input) {
|
|
28054
|
+
if (!input)
|
|
28055
|
+
return null;
|
|
28056
|
+
let s3 = input.trim();
|
|
28057
|
+
s3 = s3.replace(/^(https?:\/\/)?(www\.)?github\.com\//i, "");
|
|
28058
|
+
s3 = s3.replace(/^git@github\.com:/i, "");
|
|
28059
|
+
const [owner, repo] = s3.split(/[/?#]/);
|
|
28060
|
+
if (!owner || !repo)
|
|
28061
|
+
return null;
|
|
28062
|
+
const name = repo.replace(/\.git$/i, "");
|
|
28063
|
+
if (!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(owner))
|
|
28064
|
+
return null;
|
|
28065
|
+
if (!/^[A-Za-z0-9._-]{1,100}$/.test(name) || name === "." || name === "..")
|
|
28066
|
+
return null;
|
|
28067
|
+
return `${owner}/${name}`;
|
|
28068
|
+
}
|
|
28069
|
+
function parseSchemeUrl(link) {
|
|
28070
|
+
const m2 = link.match(/^git0:\/{0,2}(.*)$/i);
|
|
28071
|
+
if (!m2)
|
|
28072
|
+
return null;
|
|
28073
|
+
let rest = m2[1];
|
|
28074
|
+
const query = rest.match(/[?&]repo=([^&#]+)/);
|
|
28075
|
+
if (query)
|
|
28076
|
+
return normalizeRepoSlug(decodeURIComponent(query[1]));
|
|
28077
|
+
rest = rest.replace(/^install\/?/i, "").replace(/[?#].*$/, "");
|
|
28078
|
+
return normalizeRepoSlug(decodeURIComponent(rest));
|
|
28079
|
+
}
|
|
28080
|
+
|
|
28081
|
+
// src/service/installer.ts
|
|
28082
|
+
import fs7 from "fs";
|
|
28083
|
+
import os5 from "os";
|
|
28084
|
+
import path6 from "path";
|
|
28085
|
+
function git0Home() {
|
|
28086
|
+
return process.env.GIT0_HOME || path6.join(os5.homedir(), ".git0");
|
|
28087
|
+
}
|
|
28088
|
+
async function downloadWithProgress(url, dest, onProgress, expectedSize = 0, fetchImpl = fetch) {
|
|
28089
|
+
const res = await fetchImpl(url, { headers: { "User-Agent": "git0-service" }, redirect: "follow" });
|
|
28090
|
+
if (!res.ok || !res.body)
|
|
28091
|
+
throw new Error(`Download failed: HTTP ${res.status}`);
|
|
28092
|
+
const total = Number(res.headers.get("content-length")) || expectedSize || 0;
|
|
28093
|
+
fs7.mkdirSync(path6.dirname(dest), { recursive: true });
|
|
28094
|
+
const partial = `${dest}.part`;
|
|
28095
|
+
const out = fs7.createWriteStream(partial);
|
|
28096
|
+
let received = 0;
|
|
28097
|
+
onProgress(0, total);
|
|
28098
|
+
try {
|
|
28099
|
+
const reader = res.body.getReader();
|
|
28100
|
+
for (;; ) {
|
|
28101
|
+
const { done, value } = await reader.read();
|
|
28102
|
+
if (done)
|
|
28103
|
+
break;
|
|
28104
|
+
received += value.byteLength;
|
|
28105
|
+
if (!out.write(value))
|
|
28106
|
+
await new Promise((r) => out.once("drain", r));
|
|
28107
|
+
onProgress(received, total);
|
|
28108
|
+
}
|
|
28109
|
+
await new Promise((resolve, reject) => out.end((err) => err ? reject(err) : resolve()));
|
|
28110
|
+
fs7.renameSync(partial, dest);
|
|
28111
|
+
} catch (err) {
|
|
28112
|
+
out.destroy();
|
|
28113
|
+
fs7.rmSync(partial, { force: true });
|
|
28114
|
+
throw err;
|
|
28115
|
+
}
|
|
28116
|
+
}
|
|
28117
|
+
function hasCommand(cmd) {
|
|
28118
|
+
try {
|
|
28119
|
+
shell.execSync(process.platform === "win32" ? `where ${cmd}` : `command -v ${cmd}`, { stdio: "ignore" });
|
|
28120
|
+
return true;
|
|
28121
|
+
} catch {
|
|
28122
|
+
return false;
|
|
28123
|
+
}
|
|
28124
|
+
}
|
|
28125
|
+
var q2 = (s3) => JSON.stringify(s3);
|
|
28126
|
+
function openCommand(target, platform = process.platform) {
|
|
28127
|
+
if (platform === "darwin")
|
|
28128
|
+
return { cmd: "open", args: [target] };
|
|
28129
|
+
if (platform === "win32")
|
|
28130
|
+
return { cmd: "explorer.exe", args: [target] };
|
|
28131
|
+
return { cmd: target, args: [] };
|
|
28132
|
+
}
|
|
28133
|
+
function findLaunchable(dir, repoName, platform = process.platform) {
|
|
28134
|
+
const hits = [];
|
|
28135
|
+
const want = repoName.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
28136
|
+
const walk = (d, depth) => {
|
|
28137
|
+
if (depth > 4)
|
|
28138
|
+
return;
|
|
28139
|
+
let entries;
|
|
28140
|
+
try {
|
|
28141
|
+
entries = fs7.readdirSync(d, { withFileTypes: true });
|
|
28142
|
+
} catch {
|
|
28143
|
+
return;
|
|
28144
|
+
}
|
|
28145
|
+
for (const e of entries) {
|
|
28146
|
+
const p2 = path6.join(d, e.name);
|
|
28147
|
+
const named = e.name.toLowerCase().replace(/[^a-z0-9]/g, "").includes(want);
|
|
28148
|
+
if (platform === "darwin" && e.isDirectory() && e.name.endsWith(".app")) {
|
|
28149
|
+
hits.push({ p: p2, depth, named });
|
|
28150
|
+
continue;
|
|
28151
|
+
}
|
|
28152
|
+
if (e.isDirectory()) {
|
|
28153
|
+
walk(p2, depth + 1);
|
|
28154
|
+
continue;
|
|
28155
|
+
}
|
|
28156
|
+
if (!e.isFile())
|
|
28157
|
+
continue;
|
|
28158
|
+
if (platform === "win32") {
|
|
28159
|
+
if (/\.exe$/i.test(e.name) && !/unins|uninstall|setup-helper/i.test(e.name))
|
|
28160
|
+
hits.push({ p: p2, depth, named });
|
|
28161
|
+
} else if (platform !== "darwin" || !e.name.includes(".")) {
|
|
28162
|
+
try {
|
|
28163
|
+
if (fs7.statSync(p2).mode & 73 && !/\.(so|dylib|sh)(\.|$)/.test(e.name))
|
|
28164
|
+
hits.push({ p: p2, depth, named });
|
|
28165
|
+
} catch {}
|
|
28166
|
+
}
|
|
28167
|
+
}
|
|
28168
|
+
};
|
|
28169
|
+
walk(dir, 0);
|
|
28170
|
+
hits.sort((a, b2) => Number(b2.named) - Number(a.named) || a.depth - b2.depth);
|
|
28171
|
+
return hits[0]?.p ?? null;
|
|
28172
|
+
}
|
|
28173
|
+
function extract(file, dest) {
|
|
28174
|
+
fs7.mkdirSync(dest, { recursive: true });
|
|
28175
|
+
const isZip = /\.zip$/i.test(file);
|
|
28176
|
+
if (isZip && process.platform === "linux") {
|
|
28177
|
+
shell.execSync(`unzip -q -o ${q2(file)} -d ${q2(dest)}`, { stdio: "ignore" });
|
|
28178
|
+
} else {
|
|
28179
|
+
shell.execSync(`tar -xf ${q2(file)} -C ${q2(dest)}`, { stdio: "ignore" });
|
|
28180
|
+
}
|
|
28181
|
+
}
|
|
28182
|
+
function installAsset(file, asset, repo) {
|
|
28183
|
+
const repoName = repo.split("/")[1];
|
|
28184
|
+
const appsDir = path6.join(git0Home(), "apps", repo.replace("/", "__"));
|
|
28185
|
+
fs7.mkdirSync(appsDir, { recursive: true });
|
|
28186
|
+
switch (asset.kind) {
|
|
28187
|
+
case "appimage":
|
|
28188
|
+
case "binary": {
|
|
28189
|
+
const target = path6.join(appsDir, asset.name);
|
|
28190
|
+
fs7.copyFileSync(file, target);
|
|
28191
|
+
fs7.chmodSync(target, 493);
|
|
28192
|
+
return asset.kind === "appimage" ? { installedPath: target, launch: { cmd: target, args: [] }, message: `Installed ${asset.name}` } : { installedPath: target, launch: null, message: `Installed command-line tool at ${target}` };
|
|
28193
|
+
}
|
|
28194
|
+
case "deb":
|
|
28195
|
+
case "rpm": {
|
|
28196
|
+
const tool = asset.kind === "deb" ? `dpkg -i ${q2(file)}` : `rpm -U --replacepkgs ${q2(file)}`;
|
|
28197
|
+
if (!hasCommand("pkexec")) {
|
|
28198
|
+
return { installedPath: file, launch: null, message: `Downloaded. Install with: sudo ${tool}` };
|
|
28199
|
+
}
|
|
28200
|
+
shell.execSync(`pkexec ${tool}`, { stdio: "ignore" });
|
|
28201
|
+
const launch = hasCommand(repoName) ? { cmd: repoName, args: [] } : null;
|
|
28202
|
+
return { installedPath: file, launch, message: `Installed ${asset.name} system-wide` };
|
|
28203
|
+
}
|
|
28204
|
+
case "dmg": {
|
|
28205
|
+
const mount = fs7.mkdtempSync(path6.join(os5.tmpdir(), "git0-dmg-"));
|
|
28206
|
+
shell.execSync(`hdiutil attach -nobrowse -readonly -mountpoint ${q2(mount)} ${q2(file)}`, { stdio: "ignore" });
|
|
28207
|
+
try {
|
|
28208
|
+
const app = fs7.readdirSync(mount).find((n) => n.endsWith(".app"));
|
|
28209
|
+
if (!app) {
|
|
28210
|
+
const pkg = fs7.readdirSync(mount).find((n) => n.endsWith(".pkg"));
|
|
28211
|
+
if (pkg) {
|
|
28212
|
+
const copy = path6.join(appsDir, pkg);
|
|
28213
|
+
fs7.copyFileSync(path6.join(mount, pkg), copy);
|
|
28214
|
+
return { installedPath: copy, launch: openCommand(copy), message: `Opening installer ${pkg}` };
|
|
28215
|
+
}
|
|
28216
|
+
throw new Error("No .app or .pkg found in the disk image");
|
|
28217
|
+
}
|
|
28218
|
+
const userApps = path6.join(os5.homedir(), "Applications");
|
|
28219
|
+
fs7.mkdirSync(userApps, { recursive: true });
|
|
28220
|
+
const target = path6.join(userApps, app);
|
|
28221
|
+
fs7.rmSync(target, { recursive: true, force: true });
|
|
28222
|
+
shell.execSync(`cp -R ${q2(path6.join(mount, app))} ${q2(target)}`, { stdio: "ignore" });
|
|
28223
|
+
return { installedPath: target, launch: openCommand(target), message: `Installed ${app} to ~/Applications` };
|
|
28224
|
+
} finally {
|
|
28225
|
+
try {
|
|
28226
|
+
shell.execSync(`hdiutil detach ${q2(mount)} -quiet`, { stdio: "ignore" });
|
|
28227
|
+
} catch {}
|
|
28228
|
+
fs7.rmSync(mount, { recursive: true, force: true });
|
|
28229
|
+
}
|
|
28230
|
+
}
|
|
28231
|
+
case "pkg":
|
|
28232
|
+
case "msi":
|
|
28233
|
+
case "exe": {
|
|
28234
|
+
const launch = asset.kind === "msi" ? { cmd: "msiexec", args: ["/i", file] } : asset.kind === "pkg" ? openCommand(file) : { cmd: file, args: [] };
|
|
28235
|
+
return { installedPath: file, launch, message: `Opening ${asset.name}` };
|
|
28236
|
+
}
|
|
28237
|
+
case "archive": {
|
|
28238
|
+
const dest = path6.join(appsDir, asset.tag || "latest");
|
|
28239
|
+
fs7.rmSync(dest, { recursive: true, force: true });
|
|
28240
|
+
extract(file, dest);
|
|
28241
|
+
const found = findLaunchable(dest, repoName);
|
|
28242
|
+
if (!found)
|
|
28243
|
+
return { installedPath: dest, launch: null, message: `Extracted to ${dest}` };
|
|
28244
|
+
if (found.endsWith(".app") || /\.exe$/i.test(found)) {
|
|
28245
|
+
return { installedPath: found, launch: openCommand(found), message: `Installed ${path6.basename(found)}` };
|
|
28246
|
+
}
|
|
28247
|
+
return { installedPath: found, launch: null, message: `Installed command-line tool at ${found}` };
|
|
28248
|
+
}
|
|
28249
|
+
}
|
|
28250
|
+
}
|
|
28251
|
+
function launchApp(launch) {
|
|
28252
|
+
const child = shell.spawn(launch.cmd, launch.args, { detached: true, stdio: "ignore" });
|
|
28253
|
+
child?.on?.("error", () => {});
|
|
28254
|
+
child?.unref?.();
|
|
28255
|
+
}
|
|
28256
|
+
|
|
28257
|
+
// src/service/server.ts
|
|
28258
|
+
import http from "http";
|
|
28259
|
+
import crypto from "crypto";
|
|
28260
|
+
import fs8 from "fs";
|
|
28261
|
+
import path7 from "path";
|
|
28262
|
+
var DEFAULT_PORT = 47100;
|
|
28263
|
+
var TERMINAL = ["done", "error", "denied"];
|
|
28264
|
+
async function fetchReleasesFromGithub(repo) {
|
|
28265
|
+
const headers = {
|
|
28266
|
+
"User-Agent": "git0-service",
|
|
28267
|
+
Accept: "application/vnd.github+json"
|
|
28268
|
+
};
|
|
28269
|
+
if (process.env.GITHUB_TOKEN)
|
|
28270
|
+
headers.Authorization = `token ${process.env.GITHUB_TOKEN}`;
|
|
28271
|
+
const url = `https://api.github.com/repos/${repo}/releases?per_page=15`;
|
|
28272
|
+
let res = await fetch(url, { headers });
|
|
28273
|
+
if (res.status === 401 && headers.Authorization) {
|
|
28274
|
+
delete headers.Authorization;
|
|
28275
|
+
res = await fetch(url, { headers });
|
|
28276
|
+
}
|
|
28277
|
+
if (res.status === 403 || res.status === 429) {
|
|
28278
|
+
throw new Error("GitHub rate limit reached — set GITHUB_TOKEN for the service");
|
|
28279
|
+
}
|
|
28280
|
+
if (res.status === 404)
|
|
28281
|
+
throw new Error(`Repository ${repo} not found`);
|
|
28282
|
+
if (!res.ok)
|
|
28283
|
+
throw new Error(`GitHub returned HTTP ${res.status}`);
|
|
28284
|
+
return await res.json();
|
|
28285
|
+
}
|
|
28286
|
+
function defaultDeps() {
|
|
28287
|
+
return {
|
|
28288
|
+
platform: getCurrentPlatform(),
|
|
28289
|
+
fetchReleases: fetchReleasesFromGithub,
|
|
28290
|
+
download: (url, dest, onProgress, size) => downloadWithProgress(url, dest, onProgress, size),
|
|
28291
|
+
install: installAsset,
|
|
28292
|
+
launch: launchApp
|
|
28293
|
+
};
|
|
28294
|
+
}
|
|
28295
|
+
|
|
28296
|
+
class TrustStore {
|
|
28297
|
+
file;
|
|
28298
|
+
constructor(file = path7.join(git0Home(), "service.json")) {
|
|
28299
|
+
this.file = file;
|
|
28300
|
+
}
|
|
28301
|
+
read() {
|
|
28302
|
+
try {
|
|
28303
|
+
const data = JSON.parse(fs8.readFileSync(this.file, "utf8"));
|
|
28304
|
+
return { trustedRepos: Array.isArray(data.trustedRepos) ? data.trustedRepos : [] };
|
|
28305
|
+
} catch {
|
|
28306
|
+
return { trustedRepos: [] };
|
|
28307
|
+
}
|
|
28308
|
+
}
|
|
28309
|
+
has(repo) {
|
|
28310
|
+
return this.read().trustedRepos.includes(repo.toLowerCase());
|
|
28311
|
+
}
|
|
28312
|
+
add(repo) {
|
|
28313
|
+
const data = this.read();
|
|
28314
|
+
if (!data.trustedRepos.includes(repo.toLowerCase()))
|
|
28315
|
+
data.trustedRepos.push(repo.toLowerCase());
|
|
28316
|
+
fs8.mkdirSync(path7.dirname(this.file), { recursive: true });
|
|
28317
|
+
fs8.writeFileSync(this.file, JSON.stringify(data, null, 2));
|
|
28318
|
+
}
|
|
28319
|
+
}
|
|
28320
|
+
var APPROVAL_TTL_MS = 10 * 60 * 1000;
|
|
28321
|
+
var MAX_PENDING = 5;
|
|
28322
|
+
var MAX_JOBS = 50;
|
|
28323
|
+
|
|
28324
|
+
class JobManager {
|
|
28325
|
+
deps;
|
|
28326
|
+
trust;
|
|
28327
|
+
jobs = new Map;
|
|
28328
|
+
constructor(deps, trust = new TrustStore) {
|
|
28329
|
+
this.deps = deps;
|
|
28330
|
+
this.trust = trust;
|
|
28331
|
+
}
|
|
28332
|
+
view(job) {
|
|
28333
|
+
const { approvalToken: _secret, ...rest } = job;
|
|
28334
|
+
return rest;
|
|
28335
|
+
}
|
|
28336
|
+
get(id) {
|
|
28337
|
+
this.expire();
|
|
28338
|
+
return this.jobs.get(id);
|
|
28339
|
+
}
|
|
28340
|
+
list() {
|
|
28341
|
+
this.expire();
|
|
28342
|
+
return [...this.jobs.values()].map((j2) => this.view(j2));
|
|
28343
|
+
}
|
|
28344
|
+
request(repoInput) {
|
|
28345
|
+
const repo = normalizeRepoSlug(repoInput);
|
|
28346
|
+
if (!repo)
|
|
28347
|
+
throw new Error("Expected a GitHub repository as owner/repo or a github.com URL");
|
|
28348
|
+
this.expire();
|
|
28349
|
+
const active = [...this.jobs.values()].find((j2) => j2.repo.toLowerCase() === repo.toLowerCase() && !TERMINAL.includes(j2.status));
|
|
28350
|
+
if (active)
|
|
28351
|
+
return active;
|
|
28352
|
+
const trusted = this.trust.has(repo);
|
|
28353
|
+
const pending = [...this.jobs.values()].filter((j2) => j2.status === "awaiting-approval").length;
|
|
28354
|
+
if (!trusted && pending >= MAX_PENDING)
|
|
28355
|
+
throw new Error("Too many install requests are waiting for approval");
|
|
28356
|
+
const now = Date.now();
|
|
28357
|
+
const job = {
|
|
28358
|
+
id: crypto.randomUUID(),
|
|
28359
|
+
repo,
|
|
28360
|
+
status: trusted ? "resolving" : "awaiting-approval",
|
|
28361
|
+
progress: 0,
|
|
28362
|
+
receivedBytes: 0,
|
|
28363
|
+
totalBytes: 0,
|
|
28364
|
+
asset: null,
|
|
28365
|
+
tag: null,
|
|
28366
|
+
installedPath: null,
|
|
28367
|
+
launched: false,
|
|
28368
|
+
message: trusted ? "Finding the right release" : "Waiting for you to allow this install",
|
|
28369
|
+
error: null,
|
|
28370
|
+
createdAt: now,
|
|
28371
|
+
updatedAt: now,
|
|
28372
|
+
approvalToken: crypto.randomBytes(24).toString("hex")
|
|
28373
|
+
};
|
|
28374
|
+
this.jobs.set(job.id, job);
|
|
28375
|
+
this.prune();
|
|
28376
|
+
if (trusted)
|
|
28377
|
+
this.run(job);
|
|
28378
|
+
return job;
|
|
28379
|
+
}
|
|
28380
|
+
decide(id, token, allow, alwaysTrust = false) {
|
|
28381
|
+
const job = this.get(id);
|
|
28382
|
+
if (!job || job.status !== "awaiting-approval")
|
|
28383
|
+
return false;
|
|
28384
|
+
const a = Buffer.from(token);
|
|
28385
|
+
const b2 = Buffer.from(job.approvalToken);
|
|
28386
|
+
if (a.length !== b2.length || !crypto.timingSafeEqual(a, b2))
|
|
28387
|
+
return false;
|
|
28388
|
+
if (!allow) {
|
|
28389
|
+
this.update(job, { status: "denied", message: "Install was declined" });
|
|
28390
|
+
return true;
|
|
28391
|
+
}
|
|
28392
|
+
if (alwaysTrust)
|
|
28393
|
+
this.trust.add(job.repo);
|
|
28394
|
+
this.update(job, { status: "resolving", message: "Finding the right release" });
|
|
28395
|
+
this.run(job);
|
|
28396
|
+
return true;
|
|
28397
|
+
}
|
|
28398
|
+
update(job, patch) {
|
|
28399
|
+
Object.assign(job, patch, { updatedAt: Date.now() });
|
|
28400
|
+
}
|
|
28401
|
+
expire() {
|
|
28402
|
+
const now = Date.now();
|
|
28403
|
+
for (const job of this.jobs.values()) {
|
|
28404
|
+
if (job.status === "awaiting-approval" && now - job.createdAt > APPROVAL_TTL_MS) {
|
|
28405
|
+
this.update(job, { status: "denied", message: "Approval request expired" });
|
|
28406
|
+
}
|
|
28407
|
+
}
|
|
28408
|
+
}
|
|
28409
|
+
prune() {
|
|
28410
|
+
const done = [...this.jobs.values()].filter((j2) => TERMINAL.includes(j2.status));
|
|
28411
|
+
while (this.jobs.size > MAX_JOBS && done.length)
|
|
28412
|
+
this.jobs.delete(done.shift().id);
|
|
28413
|
+
}
|
|
28414
|
+
async run(job) {
|
|
28415
|
+
const { platform } = this.deps;
|
|
28416
|
+
try {
|
|
28417
|
+
const releases = await this.deps.fetchReleases(job.repo);
|
|
28418
|
+
const asset = pickAsset(releases, platform);
|
|
28419
|
+
if (!asset) {
|
|
28420
|
+
throw new Error(`No release of ${job.repo} ships a build for ${platform.os} ${platform.arch}`);
|
|
28421
|
+
}
|
|
28422
|
+
this.update(job, {
|
|
28423
|
+
status: "downloading",
|
|
28424
|
+
asset: asset.name,
|
|
28425
|
+
tag: asset.tag,
|
|
28426
|
+
totalBytes: asset.size,
|
|
28427
|
+
message: `Downloading ${asset.name}`
|
|
28428
|
+
});
|
|
28429
|
+
const dest = path7.join(git0Home(), "downloads", job.repo.replace("/", "__"), asset.name);
|
|
28430
|
+
await this.deps.download(asset.url, dest, (received, total) => {
|
|
28431
|
+
const t = total || asset.size;
|
|
28432
|
+
this.update(job, {
|
|
28433
|
+
receivedBytes: received,
|
|
28434
|
+
totalBytes: t,
|
|
28435
|
+
progress: t ? Math.min(0.9, received / t * 0.9) : job.progress
|
|
28436
|
+
});
|
|
28437
|
+
}, asset.size);
|
|
28438
|
+
this.update(job, { status: "installing", progress: 0.92, message: `Installing ${asset.name}` });
|
|
28439
|
+
const result = this.deps.install(dest, asset, job.repo);
|
|
28440
|
+
this.update(job, { installedPath: result.installedPath, message: result.message });
|
|
28441
|
+
if (result.launch) {
|
|
28442
|
+
this.update(job, { status: "launching", progress: 0.97 });
|
|
28443
|
+
this.deps.launch(result.launch);
|
|
28444
|
+
this.update(job, { launched: true });
|
|
28445
|
+
}
|
|
28446
|
+
this.update(job, { status: "done", progress: 1 });
|
|
28447
|
+
} catch (err) {
|
|
28448
|
+
this.update(job, { status: "error", error: err?.message ?? String(err), message: "Install failed" });
|
|
28449
|
+
}
|
|
28450
|
+
}
|
|
28451
|
+
}
|
|
28452
|
+
var CORS = {
|
|
28453
|
+
"Access-Control-Allow-Origin": "*",
|
|
28454
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
28455
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
28456
|
+
"Access-Control-Allow-Private-Network": "true",
|
|
28457
|
+
"Access-Control-Max-Age": "600"
|
|
28458
|
+
};
|
|
28459
|
+
var NO_FRAME = {
|
|
28460
|
+
"X-Frame-Options": "DENY",
|
|
28461
|
+
"Content-Security-Policy": "frame-ancestors 'none'",
|
|
28462
|
+
"Cache-Control": "no-store"
|
|
28463
|
+
};
|
|
28464
|
+
function sendJson(res, status, body) {
|
|
28465
|
+
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store", ...CORS });
|
|
28466
|
+
res.end(JSON.stringify(body));
|
|
28467
|
+
}
|
|
28468
|
+
function sendHtml(res, status, html) {
|
|
28469
|
+
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", ...NO_FRAME });
|
|
28470
|
+
res.end(html);
|
|
28471
|
+
}
|
|
28472
|
+
function readBody(req, limit = 4096) {
|
|
28473
|
+
return new Promise((resolve, reject) => {
|
|
28474
|
+
let data = "";
|
|
28475
|
+
req.on("data", (chunk) => {
|
|
28476
|
+
data += chunk;
|
|
28477
|
+
if (data.length > limit) {
|
|
28478
|
+
reject(new Error("Body too large"));
|
|
28479
|
+
req.destroy();
|
|
28480
|
+
}
|
|
28481
|
+
});
|
|
28482
|
+
req.on("end", () => resolve(data));
|
|
28483
|
+
req.on("error", reject);
|
|
28484
|
+
});
|
|
28485
|
+
}
|
|
28486
|
+
var esc = (s3) => s3.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
28487
|
+
function jobPage(job, token) {
|
|
28488
|
+
const repo = esc(job.repo);
|
|
28489
|
+
const prompt2 = job.status === "awaiting-approval" && token ? `<p>A web page asked git0 to download and install the latest release of
|
|
28490
|
+
<a href="https://github.com/${repo}" target="_blank" rel="noopener"><b>${repo}</b></a>
|
|
28491
|
+
on this computer and open it.</p>
|
|
28492
|
+
<form method="post" action="/approve/${esc(job.id)}">
|
|
28493
|
+
<input type="hidden" name="token" value="${esc(token)}">
|
|
28494
|
+
<label><input type="checkbox" name="trust"> Always allow ${repo}</label>
|
|
28495
|
+
<div class="row">
|
|
28496
|
+
<button name="decision" value="allow" class="primary">Allow install</button>
|
|
28497
|
+
<button name="decision" value="deny">Deny</button>
|
|
28498
|
+
</div>
|
|
28499
|
+
</form>` : "";
|
|
28500
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
28501
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
28502
|
+
<title>git0 · ${repo}</title>
|
|
28503
|
+
<style>
|
|
28504
|
+
:root{color-scheme:light dark;--bg:#fafafa;--fg:#111;--muted:#666;--accent:#6d28d9;--track:#e5e5e5}
|
|
28505
|
+
@media (prefers-color-scheme:dark){:root{--bg:#111;--fg:#eee;--muted:#999;--accent:#a78bfa;--track:#333}}
|
|
28506
|
+
body{margin:0;font:15px/1.5 system-ui,sans-serif;background:var(--bg);color:var(--fg);display:grid;place-items:center;min-height:100vh}
|
|
28507
|
+
main{max-width:440px;padding:24px}
|
|
28508
|
+
h1{font-size:18px;margin:0 0 12px}
|
|
28509
|
+
a{color:var(--accent)}
|
|
28510
|
+
.row{display:flex;gap:8px;margin-top:16px}
|
|
28511
|
+
button{font:inherit;padding:8px 16px;border-radius:8px;border:1px solid var(--track);background:transparent;color:var(--fg);cursor:pointer}
|
|
28512
|
+
button.primary{background:var(--accent);border-color:var(--accent);color:#fff}
|
|
28513
|
+
.bar{height:10px;border-radius:5px;background:var(--track);overflow:hidden;margin:16px 0 8px}
|
|
28514
|
+
.bar>i{display:block;height:100%;width:0;background:var(--accent);transition:width .3s}
|
|
28515
|
+
#msg{color:var(--muted);font-size:13px}
|
|
28516
|
+
</style></head><body><main>
|
|
28517
|
+
<h1>git0 · ${repo}</h1>
|
|
28518
|
+
${prompt2}
|
|
28519
|
+
<div id="progress" ${prompt2 ? "hidden" : ""}>
|
|
28520
|
+
<div class="bar"><i id="fill"></i></div>
|
|
28521
|
+
<div id="msg">${esc(job.message)}</div>
|
|
28522
|
+
</div>
|
|
28523
|
+
<script>
|
|
28524
|
+
const id = ${JSON.stringify(job.id)};
|
|
28525
|
+
async function poll(){
|
|
28526
|
+
try{
|
|
28527
|
+
const j = await (await fetch('/status/' + id)).json();
|
|
28528
|
+
if (j.status === 'awaiting-approval') return setTimeout(poll, 1000);
|
|
28529
|
+
document.getElementById('progress').hidden = false;
|
|
28530
|
+
document.getElementById('fill').style.width = Math.round(j.progress * 100) + '%';
|
|
28531
|
+
const mb = n => (n / 1048576).toFixed(1);
|
|
28532
|
+
document.getElementById('msg').textContent =
|
|
28533
|
+
j.status === 'downloading' && j.totalBytes ? j.message + ' — ' + mb(j.receivedBytes) + ' / ' + mb(j.totalBytes) + ' MB'
|
|
28534
|
+
: j.status === 'error' ? 'Failed: ' + j.error : j.message;
|
|
28535
|
+
if (!['done','error','denied'].includes(j.status)) setTimeout(poll, 500);
|
|
28536
|
+
}catch{ setTimeout(poll, 2000) }
|
|
28537
|
+
}
|
|
28538
|
+
${prompt2 ? "" : "poll();"}
|
|
28539
|
+
</script>
|
|
28540
|
+
</main></body></html>`;
|
|
28541
|
+
}
|
|
28542
|
+
function createServiceServer(opts = {}) {
|
|
28543
|
+
const deps = opts.deps ?? defaultDeps();
|
|
28544
|
+
const jobs = new JobManager(deps, opts.trust ?? new TrustStore);
|
|
28545
|
+
const server = http.createServer(async (req, res) => {
|
|
28546
|
+
try {
|
|
28547
|
+
const port = server.address()?.port;
|
|
28548
|
+
const host = (req.headers.host ?? "").toLowerCase();
|
|
28549
|
+
if (host !== `127.0.0.1:${port}` && host !== `localhost:${port}`) {
|
|
28550
|
+
return sendJson(res, 403, { error: "Forbidden host" });
|
|
28551
|
+
}
|
|
28552
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
28553
|
+
const method = req.method ?? "GET";
|
|
28554
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
28555
|
+
if (method === "OPTIONS") {
|
|
28556
|
+
res.writeHead(204, CORS);
|
|
28557
|
+
return res.end();
|
|
28558
|
+
}
|
|
28559
|
+
if (parts[0] === "health" && method === "GET") {
|
|
28560
|
+
return sendJson(res, 200, {
|
|
28561
|
+
ok: true,
|
|
28562
|
+
name: "git0",
|
|
28563
|
+
version: opts.version ?? null,
|
|
28564
|
+
os: deps.platform.os,
|
|
28565
|
+
arch: deps.platform.arch
|
|
28566
|
+
});
|
|
28567
|
+
}
|
|
28568
|
+
if (parts[0] === "install" && (method === "GET" || method === "POST")) {
|
|
28569
|
+
let repo = url.searchParams.get("repo");
|
|
28570
|
+
if (!repo && method === "POST") {
|
|
28571
|
+
const raw = await readBody(req);
|
|
28572
|
+
try {
|
|
28573
|
+
repo = JSON.parse(raw).repo;
|
|
28574
|
+
} catch {
|
|
28575
|
+
repo = new URLSearchParams(raw).get("repo");
|
|
28576
|
+
}
|
|
28577
|
+
}
|
|
28578
|
+
let job;
|
|
28579
|
+
try {
|
|
28580
|
+
job = jobs.request(repo ?? "");
|
|
28581
|
+
} catch (err) {
|
|
28582
|
+
return sendJson(res, 400, { error: err.message });
|
|
28583
|
+
}
|
|
28584
|
+
const jobUrl = `http://127.0.0.1:${port}/job/${job.id}`;
|
|
28585
|
+
if (method === "GET" && (req.headers.accept ?? "").includes("text/html")) {
|
|
28586
|
+
res.writeHead(303, { Location: `/job/${job.id}`, ...CORS });
|
|
28587
|
+
return res.end();
|
|
28588
|
+
}
|
|
28589
|
+
return sendJson(res, 202, {
|
|
28590
|
+
...jobs.view(job),
|
|
28591
|
+
statusUrl: `http://127.0.0.1:${port}/status/${job.id}`,
|
|
28592
|
+
approveUrl: job.status === "awaiting-approval" ? jobUrl : null,
|
|
28593
|
+
jobUrl
|
|
28594
|
+
});
|
|
28595
|
+
}
|
|
28596
|
+
if (parts[0] === "status" && parts[1] && method === "GET") {
|
|
28597
|
+
const job = jobs.get(parts[1]);
|
|
28598
|
+
return job ? sendJson(res, 200, jobs.view(job)) : sendJson(res, 404, { error: "No such job" });
|
|
28599
|
+
}
|
|
28600
|
+
if (parts[0] === "jobs" && method === "GET") {
|
|
28601
|
+
return sendJson(res, 200, jobs.list());
|
|
28602
|
+
}
|
|
28603
|
+
if (parts[0] === "job" && parts[1] && method === "GET") {
|
|
28604
|
+
const job = jobs.get(parts[1]);
|
|
28605
|
+
if (!job)
|
|
28606
|
+
return sendHtml(res, 404, "<p>No such install request.</p>");
|
|
28607
|
+
return sendHtml(res, 200, jobPage(jobs.view(job), job.approvalToken));
|
|
28608
|
+
}
|
|
28609
|
+
if (parts[0] === "approve" && parts[1] && method === "POST") {
|
|
28610
|
+
const origin = req.headers.origin;
|
|
28611
|
+
if (origin && origin !== `http://127.0.0.1:${port}` && origin !== `http://localhost:${port}`) {
|
|
28612
|
+
return sendHtml(res, 403, "<p>Approval must come from the git0 page itself.</p>");
|
|
28613
|
+
}
|
|
28614
|
+
const form = new URLSearchParams(await readBody(req));
|
|
28615
|
+
const ok = jobs.decide(parts[1], form.get("token") ?? "", form.get("decision") === "allow", form.get("trust") === "on");
|
|
28616
|
+
if (!ok)
|
|
28617
|
+
return sendHtml(res, 400, "<p>This request is no longer waiting for approval.</p>");
|
|
28618
|
+
res.writeHead(303, { Location: `/job/${parts[1]}`, ...NO_FRAME });
|
|
28619
|
+
return res.end();
|
|
28620
|
+
}
|
|
28621
|
+
sendJson(res, 404, { error: "Not found" });
|
|
28622
|
+
} catch (err) {
|
|
28623
|
+
if (!res.headersSent)
|
|
28624
|
+
sendJson(res, 500, { error: err?.message ?? "Internal error" });
|
|
28625
|
+
}
|
|
28626
|
+
});
|
|
28627
|
+
return { server, jobs };
|
|
28628
|
+
}
|
|
28629
|
+
function startService(opts = {}) {
|
|
28630
|
+
const { server, jobs } = createServiceServer(opts);
|
|
28631
|
+
return new Promise((resolve, reject) => {
|
|
28632
|
+
server.once("error", reject);
|
|
28633
|
+
server.listen(opts.port ?? DEFAULT_PORT, "127.0.0.1", () => {
|
|
28634
|
+
resolve({ server, jobs, port: server.address().port });
|
|
28635
|
+
});
|
|
28636
|
+
});
|
|
28637
|
+
}
|
|
28638
|
+
|
|
28639
|
+
// src/service/client.ts
|
|
28640
|
+
var base = (port) => `http://127.0.0.1:${port}`;
|
|
28641
|
+
async function isServiceRunning(port = DEFAULT_PORT, fetchImpl = fetch) {
|
|
28642
|
+
try {
|
|
28643
|
+
const res = await fetchImpl(`${base(port)}/health`, { signal: AbortSignal.timeout(800) });
|
|
28644
|
+
return res.ok && (await res.json())?.name === "git0";
|
|
28645
|
+
} catch {
|
|
28646
|
+
return false;
|
|
28647
|
+
}
|
|
28648
|
+
}
|
|
28649
|
+
async function ensureService(runtime, script, port = DEFAULT_PORT) {
|
|
28650
|
+
if (await isServiceRunning(port))
|
|
28651
|
+
return true;
|
|
28652
|
+
const child = shell.spawn(runtime, [script, "--service", `--port=${port}`], { detached: true, stdio: "ignore" });
|
|
28653
|
+
child?.on?.("error", () => {});
|
|
28654
|
+
child?.unref?.();
|
|
28655
|
+
for (let i = 0;i < 20; i++) {
|
|
28656
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
28657
|
+
if (await isServiceRunning(port))
|
|
28658
|
+
return true;
|
|
28659
|
+
}
|
|
28660
|
+
return false;
|
|
28661
|
+
}
|
|
28662
|
+
async function requestInstall(repo, port = DEFAULT_PORT) {
|
|
28663
|
+
const res = await fetch(`${base(port)}/install`, {
|
|
28664
|
+
method: "POST",
|
|
28665
|
+
headers: { "Content-Type": "application/json" },
|
|
28666
|
+
body: JSON.stringify({ repo })
|
|
28667
|
+
});
|
|
28668
|
+
const body = await res.json();
|
|
28669
|
+
if (!res.ok)
|
|
28670
|
+
throw new Error(body.error ?? `Service returned HTTP ${res.status}`);
|
|
28671
|
+
const open = openCommand(body.jobUrl);
|
|
28672
|
+
const { cmd, args } = process.platform === "linux" ? { cmd: "xdg-open", args: [body.jobUrl] } : open;
|
|
28673
|
+
try {
|
|
28674
|
+
const child = shell.spawn(cmd, args, { detached: true, stdio: "ignore" });
|
|
28675
|
+
child?.on?.("error", () => {});
|
|
28676
|
+
child?.unref?.();
|
|
28677
|
+
} catch {}
|
|
28678
|
+
return { id: body.id, jobUrl: body.jobUrl };
|
|
28679
|
+
}
|
|
28680
|
+
async function listJobs(port = DEFAULT_PORT) {
|
|
28681
|
+
const res = await fetch(`${base(port)}/jobs`);
|
|
28682
|
+
return res.json();
|
|
28683
|
+
}
|
|
28684
|
+
|
|
28685
|
+
// src/service/system-service.ts
|
|
28686
|
+
import fs9 from "fs";
|
|
28687
|
+
import os6 from "os";
|
|
28688
|
+
import path8 from "path";
|
|
28689
|
+
var SERVICE_LABEL = "org.git0.service";
|
|
28690
|
+
var TASK_NAME = "git0-service";
|
|
28691
|
+
function systemdUnit(spec) {
|
|
28692
|
+
return `[Unit]
|
|
28693
|
+
Description=git0 background install service
|
|
28694
|
+
After=network-online.target
|
|
28695
|
+
|
|
28696
|
+
[Service]
|
|
28697
|
+
ExecStart="${spec.runtime}" "${spec.script}" --service --port=${spec.port}
|
|
28698
|
+
Restart=on-failure
|
|
28699
|
+
RestartSec=3
|
|
28700
|
+
|
|
28701
|
+
[Install]
|
|
28702
|
+
WantedBy=default.target
|
|
28703
|
+
`;
|
|
28704
|
+
}
|
|
28705
|
+
function desktopEntry(spec) {
|
|
28706
|
+
return `[Desktop Entry]
|
|
28707
|
+
Type=Application
|
|
28708
|
+
Name=git0
|
|
28709
|
+
Comment=Install GitHub releases from download links
|
|
28710
|
+
Exec="${spec.runtime}" "${spec.script}" %u
|
|
28711
|
+
NoDisplay=true
|
|
28712
|
+
Terminal=false
|
|
28713
|
+
MimeType=x-scheme-handler/git0;
|
|
28714
|
+
`;
|
|
28715
|
+
}
|
|
28716
|
+
var xml = (s3) => s3.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
28717
|
+
function launchAgentPlist(spec) {
|
|
28718
|
+
const logDir = path8.join(spec.home ?? os6.homedir(), "Library", "Logs");
|
|
28719
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
28720
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
28721
|
+
<plist version="1.0">
|
|
28722
|
+
<dict>
|
|
28723
|
+
<key>Label</key><string>${SERVICE_LABEL}</string>
|
|
28724
|
+
<key>ProgramArguments</key>
|
|
28725
|
+
<array>
|
|
28726
|
+
<string>${xml(spec.runtime)}</string>
|
|
28727
|
+
<string>${xml(spec.script)}</string>
|
|
28728
|
+
<string>--service</string>
|
|
28729
|
+
<string>--port=${spec.port}</string>
|
|
28730
|
+
</array>
|
|
28731
|
+
<key>RunAtLoad</key><true/>
|
|
28732
|
+
<key>KeepAlive</key><true/>
|
|
28733
|
+
<key>StandardOutPath</key><string>${xml(path8.join(logDir, "git0-service.log"))}</string>
|
|
28734
|
+
<key>StandardErrorPath</key><string>${xml(path8.join(logDir, "git0-service.log"))}</string>
|
|
28735
|
+
</dict>
|
|
28736
|
+
</plist>
|
|
28737
|
+
`;
|
|
28738
|
+
}
|
|
28739
|
+
function windowsCommands(spec) {
|
|
28740
|
+
const run2 = `\\"${spec.runtime}\\" \\"${spec.script}\\" --service --port=${spec.port}`;
|
|
28741
|
+
const open = `\\"${spec.runtime}\\" \\"${spec.script}\\" \\"%1\\"`;
|
|
28742
|
+
return [
|
|
28743
|
+
`schtasks /Create /F /SC ONLOGON /RL LIMITED /TN ${TASK_NAME} /TR "${run2}"`,
|
|
28744
|
+
`reg add "HKCU\\Software\\Classes\\git0" /ve /d "URL:git0 Protocol" /f`,
|
|
28745
|
+
`reg add "HKCU\\Software\\Classes\\git0" /v "URL Protocol" /d "" /f`,
|
|
28746
|
+
`reg add "HKCU\\Software\\Classes\\git0\\shell\\open\\command" /ve /d "${open}" /f`
|
|
28747
|
+
];
|
|
28748
|
+
}
|
|
28749
|
+
function serviceFiles(home = os6.homedir()) {
|
|
28750
|
+
return {
|
|
28751
|
+
systemdUnit: path8.join(home, ".config", "systemd", "user", "git0.service"),
|
|
28752
|
+
desktopEntry: path8.join(home, ".local", "share", "applications", "git0-handler.desktop"),
|
|
28753
|
+
launchAgent: path8.join(home, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`)
|
|
28754
|
+
};
|
|
28755
|
+
}
|
|
28756
|
+
function run2(cmd, quiet = false) {
|
|
28757
|
+
try {
|
|
28758
|
+
shell.execSync(cmd, { stdio: quiet ? "ignore" : "inherit" });
|
|
28759
|
+
return true;
|
|
28760
|
+
} catch {
|
|
28761
|
+
return false;
|
|
28762
|
+
}
|
|
28763
|
+
}
|
|
28764
|
+
function write(file, content) {
|
|
28765
|
+
fs9.mkdirSync(path8.dirname(file), { recursive: true });
|
|
28766
|
+
fs9.writeFileSync(file, content);
|
|
28767
|
+
}
|
|
28768
|
+
function installSystemService(spec, platform = process.platform) {
|
|
28769
|
+
const files = serviceFiles(spec.home);
|
|
28770
|
+
const out = [];
|
|
28771
|
+
if (platform === "linux") {
|
|
28772
|
+
write(files.systemdUnit, systemdUnit(spec));
|
|
28773
|
+
out.push(`Wrote ${files.systemdUnit}`);
|
|
28774
|
+
const started = run2("systemctl --user daemon-reload", true) && run2("systemctl --user enable --now git0.service", true);
|
|
28775
|
+
out.push(started ? "Started git0.service (systemctl --user status git0)" : "systemd --user is not available here — run `git0 --service` from your session startup instead");
|
|
28776
|
+
write(files.desktopEntry, desktopEntry(spec));
|
|
28777
|
+
run2(`xdg-mime default git0-handler.desktop x-scheme-handler/git0`, true);
|
|
28778
|
+
run2(`update-desktop-database ${JSON.stringify(path8.dirname(files.desktopEntry))}`, true);
|
|
28779
|
+
out.push("Registered git0:// links");
|
|
28780
|
+
return out;
|
|
28781
|
+
}
|
|
28782
|
+
if (platform === "darwin") {
|
|
28783
|
+
write(files.launchAgent, launchAgentPlist(spec));
|
|
28784
|
+
run2(`launchctl unload ${JSON.stringify(files.launchAgent)}`, true);
|
|
28785
|
+
out.push(run2(`launchctl load -w ${JSON.stringify(files.launchAgent)}`, true) ? `Loaded LaunchAgent ${SERVICE_LABEL}` : `Wrote ${files.launchAgent} — load it with launchctl load -w`);
|
|
28786
|
+
out.push("git0:// links are not registered on macOS; buttons reach the service over HTTP");
|
|
28787
|
+
return out;
|
|
28788
|
+
}
|
|
28789
|
+
if (platform === "win32") {
|
|
28790
|
+
for (const cmd of windowsCommands(spec))
|
|
28791
|
+
run2(cmd, true);
|
|
28792
|
+
out.push(`Created logon task ${TASK_NAME} and registered git0:// links`);
|
|
28793
|
+
out.push(run2(`schtasks /Run /TN ${TASK_NAME}`, true) ? "Started the service" : "Will start at next logon");
|
|
28794
|
+
return out;
|
|
28795
|
+
}
|
|
28796
|
+
throw new Error(`Installing as a service is not supported on ${platform}`);
|
|
28797
|
+
}
|
|
28798
|
+
function uninstallSystemService(platform = process.platform, home = os6.homedir()) {
|
|
28799
|
+
const files = serviceFiles(home);
|
|
28800
|
+
if (platform === "linux") {
|
|
28801
|
+
run2("systemctl --user disable --now git0.service", true);
|
|
28802
|
+
fs9.rmSync(files.systemdUnit, { force: true });
|
|
28803
|
+
fs9.rmSync(files.desktopEntry, { force: true });
|
|
28804
|
+
run2("systemctl --user daemon-reload", true);
|
|
28805
|
+
return ["Removed git0.service and the git0:// handler"];
|
|
28806
|
+
}
|
|
28807
|
+
if (platform === "darwin") {
|
|
28808
|
+
run2(`launchctl unload -w ${JSON.stringify(files.launchAgent)}`, true);
|
|
28809
|
+
fs9.rmSync(files.launchAgent, { force: true });
|
|
28810
|
+
return [`Removed LaunchAgent ${SERVICE_LABEL}`];
|
|
28811
|
+
}
|
|
28812
|
+
if (platform === "win32") {
|
|
28813
|
+
run2(`schtasks /End /TN ${TASK_NAME}`, true);
|
|
28814
|
+
run2(`schtasks /Delete /F /TN ${TASK_NAME}`, true);
|
|
28815
|
+
run2(`reg delete "HKCU\\Software\\Classes\\git0" /f`, true);
|
|
28816
|
+
return [`Removed task ${TASK_NAME} and the git0:// handler`];
|
|
28817
|
+
}
|
|
28818
|
+
throw new Error(`Not supported on ${platform}`);
|
|
28819
|
+
}
|
|
28820
|
+
|
|
28821
|
+
// src/service/index.ts
|
|
28822
|
+
function selfCommand() {
|
|
28823
|
+
const script = process.argv[1] ? fs10.realpathSync(path9.resolve(process.argv[1])) : "";
|
|
28824
|
+
return { runtime: process.execPath, script };
|
|
28825
|
+
}
|
|
28826
|
+
async function runServiceCommand(positional, flags, version) {
|
|
28827
|
+
const port = flags.port ?? DEFAULT_PORT;
|
|
28828
|
+
const link = positional.find((p2) => /^git0:/i.test(p2));
|
|
28829
|
+
if (link) {
|
|
28830
|
+
const repo = parseSchemeUrl(link);
|
|
28831
|
+
if (!repo)
|
|
28832
|
+
throw new Error(`Not a git0 install link: ${link}`);
|
|
28833
|
+
const self = selfCommand();
|
|
28834
|
+
if (!await ensureService(self.runtime, self.script, port)) {
|
|
28835
|
+
throw new Error("Could not start the git0 service");
|
|
28836
|
+
}
|
|
28837
|
+
const { jobUrl } = await requestInstall(repo, port);
|
|
28838
|
+
console.log(source_default.green(`Install of ${repo} requested — ${jobUrl}`));
|
|
28839
|
+
return true;
|
|
28840
|
+
}
|
|
28841
|
+
if (!flags.service)
|
|
28842
|
+
return false;
|
|
28843
|
+
switch (flags.service) {
|
|
28844
|
+
case "run": {
|
|
28845
|
+
const { port: bound } = await startService({ port, version });
|
|
28846
|
+
console.log(source_default.green(`git0 service listening on http://127.0.0.1:${bound}`));
|
|
28847
|
+
return true;
|
|
28848
|
+
}
|
|
28849
|
+
case "install": {
|
|
28850
|
+
for (const line of installSystemService({ ...selfCommand(), port }))
|
|
28851
|
+
console.log(source_default.green(`✅ ${line}`));
|
|
28852
|
+
return true;
|
|
28853
|
+
}
|
|
28854
|
+
case "uninstall": {
|
|
28855
|
+
for (const line of uninstallSystemService())
|
|
28856
|
+
console.log(source_default.green(`✅ ${line}`));
|
|
28857
|
+
return true;
|
|
28858
|
+
}
|
|
28859
|
+
case "status": {
|
|
28860
|
+
if (!await isServiceRunning(port)) {
|
|
28861
|
+
console.log(source_default.yellow(`git0 service is not running on port ${port}`));
|
|
28862
|
+
process.exitCode = 1;
|
|
28863
|
+
return true;
|
|
28864
|
+
}
|
|
28865
|
+
console.log(source_default.green(`git0 service is running on http://127.0.0.1:${port}`));
|
|
28866
|
+
for (const j2 of await listJobs(port)) {
|
|
28867
|
+
const pct = `${Math.round(j2.progress * 100)}%`.padStart(4);
|
|
28868
|
+
console.log(` ${pct} ${j2.status.padEnd(17)} ${j2.repo}${j2.error ? source_default.red(` ${j2.error}`) : ""}`);
|
|
28869
|
+
}
|
|
28870
|
+
return true;
|
|
28871
|
+
}
|
|
28872
|
+
}
|
|
28873
|
+
return false;
|
|
28874
|
+
}
|
|
28875
|
+
// package.json
|
|
28876
|
+
var package_default = {
|
|
28877
|
+
name: "git0",
|
|
28878
|
+
description: "CLI tool to search GitHub repositories, download source & releases for your system, and instantly set up, then install dependencies and open code editor.",
|
|
28879
|
+
version: "0.3.68",
|
|
28880
|
+
author: "vtempest",
|
|
28881
|
+
scripts: {
|
|
28882
|
+
build: "bun build src/cli.ts --outdir dist --target node --format esm --sourcemap",
|
|
28883
|
+
test: "bun test",
|
|
28884
|
+
"test:ci": "bun test --reporter=junit --reporter-outfile=./junit.xml --coverage --coverage-reporter=lcov --coverage-dir=./coverage",
|
|
28885
|
+
docs: "cd docs && bun install && bun run dev",
|
|
28886
|
+
"docs:build": "cd docs && bun install && bun run build",
|
|
28887
|
+
demo: "bun src/cli.ts react template",
|
|
28888
|
+
bench: "bun benchmark/tarball-vs-clone.bench.ts",
|
|
28889
|
+
"publish:npm": "npm version patch && npm publish --access public",
|
|
28890
|
+
"test:watch": "vitest",
|
|
28891
|
+
coverage: "vitest run --coverage"
|
|
28892
|
+
},
|
|
28893
|
+
dependencies: {
|
|
28894
|
+
chalk: "^5.4.1",
|
|
28895
|
+
"git-url-parse": "^16.1.0",
|
|
28896
|
+
"grab-api.js": "^0.9.127",
|
|
28897
|
+
inquirer: "^12.6.3",
|
|
28898
|
+
ora: "^8.2.0",
|
|
28899
|
+
tar: "^7.4.3"
|
|
28900
|
+
},
|
|
28901
|
+
exports: {
|
|
28902
|
+
".": "./dist/cli.js"
|
|
28903
|
+
},
|
|
28904
|
+
bin: {
|
|
28905
|
+
g: "dist/cli.js",
|
|
28906
|
+
gg: "dist/cli.js",
|
|
28907
|
+
fm: "src/fm.js",
|
|
28908
|
+
git0: "dist/cli.js"
|
|
28909
|
+
},
|
|
28910
|
+
type: "module",
|
|
28911
|
+
keywords: [
|
|
28912
|
+
"github",
|
|
28913
|
+
"git",
|
|
28914
|
+
"package",
|
|
28915
|
+
"manager",
|
|
28916
|
+
"cli",
|
|
28917
|
+
"download"
|
|
28918
|
+
],
|
|
28919
|
+
license: "MIT",
|
|
28920
|
+
repository: {
|
|
28921
|
+
type: "git",
|
|
28922
|
+
url: "git+https://github.com/OpenSourceAGI/dev-tools-starter-agent.git",
|
|
28923
|
+
directory: "packages/git0-repo-downloader"
|
|
28924
|
+
},
|
|
28925
|
+
homepage: "https://git0.js.org",
|
|
28926
|
+
devDependencies: {
|
|
28927
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
28928
|
+
vitest: "^4.1.0"
|
|
28929
|
+
}
|
|
28930
|
+
};
|
|
28931
|
+
|
|
27924
28932
|
// src/cli.ts
|
|
27925
28933
|
var Github = new github_api_default({ debug: false });
|
|
27926
28934
|
async function downloadRepoAndSetup(target, folderPath = null, flags = { history: false, historyOnly: false, mirror: false }) {
|
|
@@ -27938,8 +28946,8 @@ async function downloadRepoAndSetup(target, folderPath = null, flags = { history
|
|
|
27938
28946
|
}
|
|
27939
28947
|
async function downloadHistoryOnly(target, flags) {
|
|
27940
28948
|
printLogo();
|
|
27941
|
-
const targetDir = getAvailableDirectoryName(
|
|
27942
|
-
console.log(source_default.blue(`\uD83D\uDCE6 Cloning ${target.name} history into ${
|
|
28949
|
+
const targetDir = getAvailableDirectoryName(path10.resolve(process.cwd(), `${target.name}.git`));
|
|
28950
|
+
console.log(source_default.blue(`\uD83D\uDCE6 Cloning ${target.name} history into ${path10.basename(targetDir)}...`));
|
|
27943
28951
|
try {
|
|
27944
28952
|
await cloneHistoryOnly(target.href, targetDir, { bare: !flags.mirror });
|
|
27945
28953
|
console.log(source_default.green(`✅ History-only clone at ${targetDir}`));
|
|
@@ -27987,6 +28995,8 @@ async function handleRepoDownload(selectedRepo, flags) {
|
|
|
27987
28995
|
async function main() {
|
|
27988
28996
|
printLogo();
|
|
27989
28997
|
const { positional, flags } = parseArgs(process.argv.slice(2));
|
|
28998
|
+
if (await runServiceCommand(positional, flags, package_default.version))
|
|
28999
|
+
return;
|
|
27990
29000
|
if (!positional.length) {
|
|
27991
29001
|
console.log(source_default.yellow("Usage: git0 <github-url | owner/repo[/path] | search-query>"));
|
|
27992
29002
|
console.log(source_default.gray(" --path=<path> download only that folder or file"));
|
|
@@ -27994,6 +29004,7 @@ async function main() {
|
|
|
27994
29004
|
console.log(source_default.gray(" --history also fetch .git history, in the background"));
|
|
27995
29005
|
console.log(source_default.gray(" --history-only clone only the history, no working files"));
|
|
27996
29006
|
console.log(source_default.gray(" --mirror keep every ref when cloning history"));
|
|
29007
|
+
console.log(source_default.gray(" --service[=install|uninstall|status] background install service for download buttons"));
|
|
27997
29008
|
process.exit(1);
|
|
27998
29009
|
}
|
|
27999
29010
|
const query = positional.join(" ");
|
|
@@ -28024,5 +29035,5 @@ main().catch((error) => {
|
|
|
28024
29035
|
process.exit(1);
|
|
28025
29036
|
});
|
|
28026
29037
|
|
|
28027
|
-
//# debugId=
|
|
29038
|
+
//# debugId=4537FA246F91AB0B64756E2164756E21
|
|
28028
29039
|
//# sourceMappingURL=cli.js.map
|