@justin06lee/subaru 0.1.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/dist/card-BFhpWbA5.d.ts +32 -0
- package/dist/chunk-CPOOJ2JD.js +213 -0
- package/dist/chunk-CPOOJ2JD.js.map +1 -0
- package/dist/chunk-K7IY5EW6.js +138 -0
- package/dist/chunk-K7IY5EW6.js.map +1 -0
- package/dist/chunk-MI26ZEPP.js +92 -0
- package/dist/chunk-MI26ZEPP.js.map +1 -0
- package/dist/chunk-POOH3Z7G.js +178 -0
- package/dist/chunk-POOH3Z7G.js.map +1 -0
- package/dist/chunk-SRQ3JWJK.js +261 -0
- package/dist/chunk-SRQ3JWJK.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +59 -0
- package/dist/cli.js.map +1 -0
- package/dist/core-DmEjLKLp.d.ts +104 -0
- package/dist/electron-renderer.d.ts +34 -0
- package/dist/electron-renderer.js +81 -0
- package/dist/electron-renderer.js.map +1 -0
- package/dist/electron.d.ts +239 -0
- package/dist/electron.js +428 -0
- package/dist/electron.js.map +1 -0
- package/dist/github-Ds4DWDq9.d.ts +37 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/react.d.ts +42 -0
- package/dist/react.js +109 -0
- package/dist/react.js.map +1 -0
- package/dist/source.d.ts +57 -0
- package/dist/source.js +11 -0
- package/dist/source.js.map +1 -0
- package/dist/styles-zfa0Pc0l.d.ts +47 -0
- package/dist/tauri.d.ts +71 -0
- package/dist/tauri.js +109 -0
- package/dist/tauri.js.map +1 -0
- package/dist/web.d.ts +59 -0
- package/dist/web.js +79 -0
- package/dist/web.js.map +1 -0
- package/package.json +120 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { U as UpdaterState, a as UpdaterLike } from './core-DmEjLKLp.js';
|
|
2
|
+
import { d as CardStyleOptions } from './styles-zfa0Pc0l.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The update card without React: the same markup, classes and behaviour as
|
|
6
|
+
* <UpdatePrompt>, driven straight from the DOM. subaru() in the web and Tauri
|
|
7
|
+
* entries mounts it so a single call is the whole integration.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
interface CardLabels {
|
|
11
|
+
available: (version: string, name: string) => string;
|
|
12
|
+
installing: (progress: number | null) => string;
|
|
13
|
+
ready: (kind: UpdaterState['kind']) => string;
|
|
14
|
+
error: (message: string) => string;
|
|
15
|
+
update: string;
|
|
16
|
+
restart: string;
|
|
17
|
+
skip: string;
|
|
18
|
+
later: string;
|
|
19
|
+
retry: string;
|
|
20
|
+
notes: string;
|
|
21
|
+
}
|
|
22
|
+
declare const defaultCardLabels: CardLabels;
|
|
23
|
+
interface MountCardOptions extends CardStyleOptions {
|
|
24
|
+
name?: string;
|
|
25
|
+
labels?: Partial<CardLabels>;
|
|
26
|
+
/** Where to append the card. Default document.body. */
|
|
27
|
+
container?: HTMLElement;
|
|
28
|
+
}
|
|
29
|
+
/** Mount the card; returns a function that removes it and unsubscribes. */
|
|
30
|
+
declare function mountCard(updater: UpdaterLike, options?: MountCardOptions): () => void;
|
|
31
|
+
|
|
32
|
+
export { type CardLabels as C, type MountCardOptions as M, defaultCardLabels as d, mountCard as m };
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// src/storage.ts
|
|
2
|
+
function fileStorage(path) {
|
|
3
|
+
return {
|
|
4
|
+
async get(key) {
|
|
5
|
+
const fs = await import("fs/promises");
|
|
6
|
+
try {
|
|
7
|
+
const data = JSON.parse(await fs.readFile(path, "utf8"));
|
|
8
|
+
return data[key] ?? null;
|
|
9
|
+
} catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
async set(key, value) {
|
|
14
|
+
const fs = await import("fs/promises");
|
|
15
|
+
const nodePath = await import("path");
|
|
16
|
+
let data = {};
|
|
17
|
+
try {
|
|
18
|
+
data = JSON.parse(await fs.readFile(path, "utf8"));
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
data[key] = value;
|
|
22
|
+
await fs.mkdir(nodePath.dirname(path), { recursive: true });
|
|
23
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
24
|
+
await fs.writeFile(tmp, JSON.stringify(data, null, 2));
|
|
25
|
+
await fs.rename(tmp, path);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/github.ts
|
|
31
|
+
var API = "https://api.github.com";
|
|
32
|
+
function splitRepo(repo) {
|
|
33
|
+
const s = repo.trim().replace(/^https?:\/\//, "").replace(/^github\.com\//, "").replace(/\.git$/, "").replace(/^\/+|\/+$/g, "");
|
|
34
|
+
const parts = s.split("/");
|
|
35
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) throw new Error(`subaru: repository "${repo}" is not owner/name`);
|
|
36
|
+
return [parts[0], parts[1]];
|
|
37
|
+
}
|
|
38
|
+
async function fetchLatestRelease(repo, options = {}, signal) {
|
|
39
|
+
const [owner, name] = splitRepo(repo);
|
|
40
|
+
const doFetch = options.fetch ?? ((input, init) => fetch(input, init));
|
|
41
|
+
const headers = {
|
|
42
|
+
accept: "application/vnd.github+json",
|
|
43
|
+
"x-github-api-version": "2022-11-28",
|
|
44
|
+
"user-agent": "subaru (+https://github.com/justin06lee/subaru)"
|
|
45
|
+
};
|
|
46
|
+
const token = options.token ?? envToken();
|
|
47
|
+
if (token) headers.authorization = `Bearer ${token}`;
|
|
48
|
+
const res = await doFetch(`${options.api ?? API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/releases/latest`, { headers, signal });
|
|
49
|
+
if (res.status === 404) return null;
|
|
50
|
+
if (res.status === 403 || res.status === 429) throw new Error("subaru: GitHub API rate limited (set GITHUB_TOKEN to raise the limit)");
|
|
51
|
+
if (!res.ok) throw new Error(`subaru: GitHub API returned ${res.status}`);
|
|
52
|
+
const json = await res.json();
|
|
53
|
+
if (!json.tag_name) return null;
|
|
54
|
+
return {
|
|
55
|
+
tag: json.tag_name,
|
|
56
|
+
version: json.tag_name,
|
|
57
|
+
notes: json.body ?? void 0,
|
|
58
|
+
url: json.html_url ?? `https://github.com/${owner}/${name}/releases/tag/${json.tag_name}`,
|
|
59
|
+
date: json.published_at ?? void 0,
|
|
60
|
+
assets: (json.assets ?? []).map((a) => ({ name: a.name, url: a.browser_download_url, size: a.size }))
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function envToken() {
|
|
64
|
+
const env = globalThis.process?.env;
|
|
65
|
+
return env?.GITHUB_TOKEN?.trim() || env?.GH_TOKEN?.trim() || void 0;
|
|
66
|
+
}
|
|
67
|
+
function allowedUrl(raw) {
|
|
68
|
+
let u;
|
|
69
|
+
try {
|
|
70
|
+
u = new URL(raw);
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
if (u.protocol !== "https:") return false;
|
|
75
|
+
const host = u.hostname.toLowerCase();
|
|
76
|
+
return host === "github.com" || host === "api.github.com" || host.endsWith(".githubusercontent.com");
|
|
77
|
+
}
|
|
78
|
+
var osAliases = {
|
|
79
|
+
darwin: ["darwin", "macos", "osx", "mac"],
|
|
80
|
+
linux: ["linux"],
|
|
81
|
+
windows: ["windows", "win"],
|
|
82
|
+
win32: ["windows", "win"]
|
|
83
|
+
};
|
|
84
|
+
var archAliases = {
|
|
85
|
+
amd64: ["amd64", "x86_64", "x64"],
|
|
86
|
+
x64: ["amd64", "x86_64", "x64"],
|
|
87
|
+
arm64: ["arm64", "aarch64"],
|
|
88
|
+
ia32: ["386", "i386", "x86"],
|
|
89
|
+
"386": ["386", "i386", "x86"]
|
|
90
|
+
};
|
|
91
|
+
var skipExt = /* @__PURE__ */ new Set([".txt", ".sha256", ".sha512", ".sig", ".asc", ".pem", ".json", ".sbom", ".md", ".deb", ".rpm", ".apk", ".msi", ".pkg", ".appimage", ".snap", ".yml", ".yaml", ".blockmap"]);
|
|
92
|
+
function pickAsset(assets, binName, os, arch) {
|
|
93
|
+
const nameTokens = tokens(binName.toLowerCase());
|
|
94
|
+
const oses = osAliases[os] ?? [os];
|
|
95
|
+
let arches = archAliases[arch] ?? [arch];
|
|
96
|
+
if (os === "darwin") arches = [...arches, "universal", "all"];
|
|
97
|
+
let best = null;
|
|
98
|
+
let bestScore = Number.NEGATIVE_INFINITY;
|
|
99
|
+
for (const a of assets) {
|
|
100
|
+
let lower = a.name.toLowerCase();
|
|
101
|
+
const ext = lower.slice(lower.lastIndexOf("."));
|
|
102
|
+
if (skipExt.has(ext) || lower.includes("checksum") || lower.includes("sha256sum")) continue;
|
|
103
|
+
lower = lower.replace(/x86_64|x86-64/g, "amd64");
|
|
104
|
+
const t = tokens(lower);
|
|
105
|
+
if (!t.some((x) => oses.includes(x)) || !t.some((x) => arches.includes(x))) continue;
|
|
106
|
+
let score = 0;
|
|
107
|
+
let rest = t;
|
|
108
|
+
if (nameTokens.every((n, i) => t[i] === n)) {
|
|
109
|
+
score += 100;
|
|
110
|
+
rest = t.slice(nameTokens.length);
|
|
111
|
+
} else if (lower.startsWith(binName.toLowerCase())) {
|
|
112
|
+
score += 50;
|
|
113
|
+
}
|
|
114
|
+
for (const tok of rest) {
|
|
115
|
+
if (oses.includes(tok) || arches.includes(tok) || explained(tok)) continue;
|
|
116
|
+
score -= 10;
|
|
117
|
+
}
|
|
118
|
+
if (score > bestScore) {
|
|
119
|
+
best = a;
|
|
120
|
+
bestScore = score;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return best;
|
|
124
|
+
}
|
|
125
|
+
function tokens(s) {
|
|
126
|
+
return s.split(/[-_. ]+/);
|
|
127
|
+
}
|
|
128
|
+
function explained(tok) {
|
|
129
|
+
if (["", "tar", "gz", "tgz", "zip", "exe", "app", "dmg", "bin", "static", "musl", "gnu"].includes(tok)) return true;
|
|
130
|
+
return /^v?\d+$/.test(tok);
|
|
131
|
+
}
|
|
132
|
+
function findChecksumAsset(assets, assetName) {
|
|
133
|
+
const lower = assetName.toLowerCase();
|
|
134
|
+
for (const a of assets) {
|
|
135
|
+
const l = a.name.toLowerCase();
|
|
136
|
+
if (l === `${lower}.sha256` || l === `${lower}.sha256sum` || l === `${lower}.sha256.txt`) return a;
|
|
137
|
+
}
|
|
138
|
+
for (const a of assets) {
|
|
139
|
+
const l = a.name.toLowerCase();
|
|
140
|
+
if (l.includes("checksum") || l.startsWith("sha256sums")) return a;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
function parseChecksums(text, assetName) {
|
|
145
|
+
const lines = text.split("\n");
|
|
146
|
+
for (const line of lines) {
|
|
147
|
+
const fields = line.trim().split(/\s+/);
|
|
148
|
+
const hash = fields[0] ?? "";
|
|
149
|
+
if (!/^[0-9a-fA-F]{64}$/.test(hash)) continue;
|
|
150
|
+
if (fields.length === 1 && lines.length <= 2) return hash.toLowerCase();
|
|
151
|
+
const file = (fields[1] ?? "").replace(/^\*/, "");
|
|
152
|
+
if (file.split("/").pop() === assetName) return hash.toLowerCase();
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
function parseSemver(v) {
|
|
157
|
+
let s = v.trim();
|
|
158
|
+
if (s.startsWith("v") || s.startsWith("V")) s = s.slice(1);
|
|
159
|
+
s = s.replace(/-dirty$/, "");
|
|
160
|
+
const plus = s.indexOf("+");
|
|
161
|
+
if (plus >= 0) s = s.slice(0, plus);
|
|
162
|
+
let pre = null;
|
|
163
|
+
const dash = s.indexOf("-");
|
|
164
|
+
if (dash >= 0) {
|
|
165
|
+
pre = s.slice(dash + 1).split(".");
|
|
166
|
+
s = s.slice(0, dash);
|
|
167
|
+
if (pre.some((p) => !/^[0-9A-Za-z-]+$/.test(p))) return null;
|
|
168
|
+
if (pre.length === 1 && /^\d+-g[0-9a-fA-F]{4,}$/.test(pre[0])) pre = null;
|
|
169
|
+
}
|
|
170
|
+
const nums = s.split(".");
|
|
171
|
+
if (nums.length < 1 || nums.length > 3 || nums.some((n) => !/^(0|[1-9]\d*)$/.test(n))) return null;
|
|
172
|
+
const core = [Number(nums[0]), Number(nums[1] ?? 0), Number(nums[2] ?? 0)];
|
|
173
|
+
return { core, pre };
|
|
174
|
+
}
|
|
175
|
+
function compareSemver(a, b) {
|
|
176
|
+
for (let i = 0; i < 3; i++) {
|
|
177
|
+
if (a.core[i] !== b.core[i]) return a.core[i] < b.core[i] ? -1 : 1;
|
|
178
|
+
}
|
|
179
|
+
if (!a.pre && !b.pre) return 0;
|
|
180
|
+
if (!a.pre) return 1;
|
|
181
|
+
if (!b.pre) return -1;
|
|
182
|
+
for (let i = 0; i < Math.min(a.pre.length, b.pre.length); i++) {
|
|
183
|
+
const x = a.pre[i];
|
|
184
|
+
const y = b.pre[i];
|
|
185
|
+
const xn = /^\d+$/.test(x);
|
|
186
|
+
const yn = /^\d+$/.test(y);
|
|
187
|
+
if (xn && yn) {
|
|
188
|
+
if (Number(x) !== Number(y)) return Number(x) < Number(y) ? -1 : 1;
|
|
189
|
+
} else if (xn) return -1;
|
|
190
|
+
else if (yn) return 1;
|
|
191
|
+
else if (x !== y) return x < y ? -1 : 1;
|
|
192
|
+
}
|
|
193
|
+
return a.pre.length === b.pre.length ? 0 : a.pre.length < b.pre.length ? -1 : 1;
|
|
194
|
+
}
|
|
195
|
+
function isNewer(latest, current) {
|
|
196
|
+
const l = parseSemver(latest);
|
|
197
|
+
if (!l) return false;
|
|
198
|
+
const c = current ? parseSemver(current) : null;
|
|
199
|
+
if (!c) return true;
|
|
200
|
+
return compareSemver(l, c) > 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export {
|
|
204
|
+
fileStorage,
|
|
205
|
+
splitRepo,
|
|
206
|
+
fetchLatestRelease,
|
|
207
|
+
allowedUrl,
|
|
208
|
+
pickAsset,
|
|
209
|
+
findChecksumAsset,
|
|
210
|
+
parseChecksums,
|
|
211
|
+
isNewer
|
|
212
|
+
};
|
|
213
|
+
//# sourceMappingURL=chunk-CPOOJ2JD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/storage.ts","../src/github.ts"],"sourcesContent":["import type { StateStorage } from './core';\n\n/**\n * A JSON file, for Electron's main process (point it at app.getPath('userData'))\n * or any Node program. Writes are atomic: temp file, then rename.\n */\nexport function fileStorage(path: string): StateStorage {\n return {\n async get(key) {\n const fs = await import('node:fs/promises');\n try {\n const data = JSON.parse(await fs.readFile(path, 'utf8')) as Record<string, string>;\n return data[key] ?? null;\n } catch {\n return null;\n }\n },\n async set(key, value) {\n const fs = await import('node:fs/promises');\n const nodePath = await import('node:path');\n let data: Record<string, string> = {};\n try {\n data = JSON.parse(await fs.readFile(path, 'utf8')) as Record<string, string>;\n } catch {\n /* first write */\n }\n data[key] = value;\n await fs.mkdir(nodePath.dirname(path), { recursive: true });\n const tmp = `${path}.${process.pid}.tmp`;\n await fs.writeFile(tmp, JSON.stringify(data, null, 2));\n await fs.rename(tmp, path);\n },\n };\n}\n","/**\n * GitHub Releases, the same way the Go module reads them: the latest\n * non-draft, non-prerelease release, its assets matched to a platform by\n * name tokens, and its checksum file when it publishes one.\n */\nimport type { Release } from './core';\n\nexport interface GitHubAsset {\n name: string;\n url: string;\n size: number;\n}\n\nexport interface GitHubRelease extends Release {\n assets: GitHubAsset[];\n}\n\nexport type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;\n\nexport interface GitHubOptions {\n fetch?: FetchLike;\n token?: string;\n api?: string;\n}\n\nconst API = 'https://api.github.com';\n\nexport function splitRepo(repo: string): [string, string] {\n const s = repo\n .trim()\n .replace(/^https?:\\/\\//, '')\n .replace(/^github\\.com\\//, '')\n .replace(/\\.git$/, '')\n .replace(/^\\/+|\\/+$/g, '');\n const parts = s.split('/');\n if (parts.length !== 2 || !parts[0] || !parts[1]) throw new Error(`subaru: repository \"${repo}\" is not owner/name`);\n return [parts[0], parts[1]];\n}\n\nexport async function fetchLatestRelease(repo: string, options: GitHubOptions = {}, signal?: AbortSignal): Promise<GitHubRelease | null> {\n const [owner, name] = splitRepo(repo);\n const doFetch = options.fetch ?? ((input, init) => fetch(input, init));\n const headers: Record<string, string> = {\n accept: 'application/vnd.github+json',\n 'x-github-api-version': '2022-11-28',\n 'user-agent': 'subaru (+https://github.com/justin06lee/subaru)',\n };\n const token = options.token ?? envToken();\n if (token) headers.authorization = `Bearer ${token}`;\n const res = await doFetch(`${options.api ?? API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/releases/latest`, { headers, signal });\n if (res.status === 404) return null;\n if (res.status === 403 || res.status === 429) throw new Error('subaru: GitHub API rate limited (set GITHUB_TOKEN to raise the limit)');\n if (!res.ok) throw new Error(`subaru: GitHub API returned ${res.status}`);\n const json = (await res.json()) as {\n tag_name?: string;\n body?: string;\n html_url?: string;\n published_at?: string;\n assets?: Array<{ name: string; browser_download_url: string; size: number }>;\n };\n if (!json.tag_name) return null;\n return {\n tag: json.tag_name,\n version: json.tag_name,\n notes: json.body ?? undefined,\n url: json.html_url ?? `https://github.com/${owner}/${name}/releases/tag/${json.tag_name}`,\n date: json.published_at ?? undefined,\n assets: (json.assets ?? []).map((a) => ({ name: a.name, url: a.browser_download_url, size: a.size })),\n };\n}\n\nfunction envToken(): string | undefined {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env;\n return env?.GITHUB_TOKEN?.trim() || env?.GH_TOKEN?.trim() || undefined;\n}\n\n/** Only GitHub itself may serve something that gets executed. */\nexport function allowedUrl(raw: string): boolean {\n let u: URL;\n try {\n u = new URL(raw);\n } catch {\n return false;\n }\n if (u.protocol !== 'https:') return false;\n const host = u.hostname.toLowerCase();\n return host === 'github.com' || host === 'api.github.com' || host.endsWith('.githubusercontent.com');\n}\n\n// Asset selection ---------------------------------------------------------------\n\nconst osAliases: Record<string, string[]> = {\n darwin: ['darwin', 'macos', 'osx', 'mac'],\n linux: ['linux'],\n windows: ['windows', 'win'],\n win32: ['windows', 'win'],\n};\nconst archAliases: Record<string, string[]> = {\n amd64: ['amd64', 'x86_64', 'x64'],\n x64: ['amd64', 'x86_64', 'x64'],\n arm64: ['arm64', 'aarch64'],\n ia32: ['386', 'i386', 'x86'],\n '386': ['386', 'i386', 'x86'],\n};\nconst skipExt = new Set(['.txt', '.sha256', '.sha512', '.sig', '.asc', '.pem', '.json', '.sbom', '.md', '.deb', '.rpm', '.apk', '.msi', '.pkg', '.appimage', '.snap', '.yml', '.yaml', '.blockmap']);\n\n/**\n * Pick the asset built for this program on this platform by tokenising the\n * name on \"-\", \"_\", \".\" and \" \" and requiring one OS token and one\n * architecture token (aliases understood). The asset that starts with the\n * program name and has the fewest unexplained words wins, so\n * \"app-helper-darwin-arm64\" loses to \"app-darwin-arm64\". Darwin also accepts\n * \"universal\".\n */\nexport function pickAsset(assets: GitHubAsset[], binName: string, os: string, arch: string): GitHubAsset | null {\n const nameTokens = tokens(binName.toLowerCase());\n const oses = osAliases[os] ?? [os];\n let arches = archAliases[arch] ?? [arch];\n if (os === 'darwin') arches = [...arches, 'universal', 'all'];\n let best: GitHubAsset | null = null;\n let bestScore = Number.NEGATIVE_INFINITY;\n for (const a of assets) {\n let lower = a.name.toLowerCase();\n const ext = lower.slice(lower.lastIndexOf('.'));\n if (skipExt.has(ext) || lower.includes('checksum') || lower.includes('sha256sum')) continue;\n lower = lower.replace(/x86_64|x86-64/g, 'amd64');\n const t = tokens(lower);\n if (!t.some((x) => oses.includes(x)) || !t.some((x) => arches.includes(x))) continue;\n let score = 0;\n let rest = t;\n if (nameTokens.every((n, i) => t[i] === n)) {\n score += 100;\n rest = t.slice(nameTokens.length);\n } else if (lower.startsWith(binName.toLowerCase())) {\n score += 50;\n }\n for (const tok of rest) {\n if (oses.includes(tok) || arches.includes(tok) || explained(tok)) continue;\n score -= 10;\n }\n if (score > bestScore) {\n best = a;\n bestScore = score;\n }\n }\n return best;\n}\n\nfunction tokens(s: string): string[] {\n return s.split(/[-_. ]+/);\n}\n\nfunction explained(tok: string): boolean {\n if (['', 'tar', 'gz', 'tgz', 'zip', 'exe', 'app', 'dmg', 'bin', 'static', 'musl', 'gnu'].includes(tok)) return true;\n return /^v?\\d+$/.test(tok);\n}\n\n// Checksums -----------------------------------------------------------------------\n\nexport function findChecksumAsset(assets: GitHubAsset[], assetName: string): GitHubAsset | null {\n const lower = assetName.toLowerCase();\n for (const a of assets) {\n const l = a.name.toLowerCase();\n if (l === `${lower}.sha256` || l === `${lower}.sha256sum` || l === `${lower}.sha256.txt`) return a;\n }\n for (const a of assets) {\n const l = a.name.toLowerCase();\n if (l.includes('checksum') || l.startsWith('sha256sums')) return a;\n }\n return null;\n}\n\nexport function parseChecksums(text: string, assetName: string): string | null {\n const lines = text.split('\\n');\n for (const line of lines) {\n const fields = line.trim().split(/\\s+/);\n const hash = fields[0] ?? '';\n if (!/^[0-9a-fA-F]{64}$/.test(hash)) continue;\n if (fields.length === 1 && lines.length <= 2) return hash.toLowerCase();\n const file = (fields[1] ?? '').replace(/^\\*/, '');\n if (file.split('/').pop() === assetName) return hash.toLowerCase();\n }\n return null;\n}\n\n// Versions ------------------------------------------------------------------------\n\ninterface Semver {\n core: [number, number, number];\n pre: string[] | null;\n}\n\nexport function parseSemver(v: string): Semver | null {\n let s = v.trim();\n if (s.startsWith('v') || s.startsWith('V')) s = s.slice(1);\n s = s.replace(/-dirty$/, '');\n const plus = s.indexOf('+');\n if (plus >= 0) s = s.slice(0, plus);\n let pre: string[] | null = null;\n const dash = s.indexOf('-');\n if (dash >= 0) {\n pre = s.slice(dash + 1).split('.');\n s = s.slice(0, dash);\n if (pre.some((p) => !/^[0-9A-Za-z-]+$/.test(p))) return null;\n // git describe: N commits past the tag is at least the tag\n if (pre.length === 1 && /^\\d+-g[0-9a-fA-F]{4,}$/.test(pre[0]!)) pre = null;\n }\n const nums = s.split('.');\n if (nums.length < 1 || nums.length > 3 || nums.some((n) => !/^(0|[1-9]\\d*)$/.test(n))) return null;\n const core: [number, number, number] = [Number(nums[0]), Number(nums[1] ?? 0), Number(nums[2] ?? 0)];\n return { core, pre };\n}\n\nexport function compareSemver(a: Semver, b: Semver): number {\n for (let i = 0; i < 3; i++) {\n if (a.core[i] !== b.core[i]) return a.core[i]! < b.core[i]! ? -1 : 1;\n }\n if (!a.pre && !b.pre) return 0;\n if (!a.pre) return 1;\n if (!b.pre) return -1;\n for (let i = 0; i < Math.min(a.pre.length, b.pre.length); i++) {\n const x = a.pre[i]!;\n const y = b.pre[i]!;\n const xn = /^\\d+$/.test(x);\n const yn = /^\\d+$/.test(y);\n if (xn && yn) {\n if (Number(x) !== Number(y)) return Number(x) < Number(y) ? -1 : 1;\n } else if (xn) return -1;\n else if (yn) return 1;\n else if (x !== y) return x < y ? -1 : 1;\n }\n return a.pre.length === b.pre.length ? 0 : a.pre.length < b.pre.length ? -1 : 1;\n}\n\n/** Is `latest` strictly newer than `current`? Unknown current means yes; unknown latest means no. */\nexport function isNewer(latest: string, current: string | undefined): boolean {\n const l = parseSemver(latest);\n if (!l) return false;\n const c = current ? parseSemver(current) : null;\n if (!c) return true;\n return compareSemver(l, c) > 0;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cardVars,
|
|
3
|
+
injectStyles
|
|
4
|
+
} from "./chunk-POOH3Z7G.js";
|
|
5
|
+
|
|
6
|
+
// src/card.ts
|
|
7
|
+
var defaultCardLabels = {
|
|
8
|
+
available: (version, name) => `${name ? `${name} ` : ""}${version} is available.`,
|
|
9
|
+
installing: (progress) => progress === null ? "Updating\u2026" : `Updating\u2026 ${Math.round(progress * 100)}%`,
|
|
10
|
+
ready: (kind) => kind === "web" ? "New version ready. It loads the next time you come back." : "Update ready. Restart to finish.",
|
|
11
|
+
error: (message) => `Update failed: ${message}`,
|
|
12
|
+
update: "Update now",
|
|
13
|
+
restart: "Restart now",
|
|
14
|
+
skip: "Skip this version",
|
|
15
|
+
later: "Later",
|
|
16
|
+
retry: "Try again",
|
|
17
|
+
notes: "What changed"
|
|
18
|
+
};
|
|
19
|
+
function mountCard(updater, options = {}) {
|
|
20
|
+
const labels = { ...defaultCardLabels, ...options.labels };
|
|
21
|
+
const name = options.name ?? "";
|
|
22
|
+
if (!options.unstyled) injectStyles();
|
|
23
|
+
const host = document.createElement("div");
|
|
24
|
+
host.setAttribute("data-subaru-card", "");
|
|
25
|
+
let showNotes = false;
|
|
26
|
+
const card = el("div", "subaru");
|
|
27
|
+
card.setAttribute("data-subaru-theme", options.theme ?? "auto");
|
|
28
|
+
card.setAttribute("data-subaru-position", options.position ?? "bottom-right");
|
|
29
|
+
for (const [key, value] of Object.entries(cardVars(options))) card.style.setProperty(key, value);
|
|
30
|
+
card.setAttribute("role", "status");
|
|
31
|
+
card.setAttribute("aria-live", "polite");
|
|
32
|
+
const render = (u) => {
|
|
33
|
+
if (u.dismissed || !["available", "installing", "ready", "error"].includes(u.status)) {
|
|
34
|
+
card.remove();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const version = u.release?.version ?? u.release?.tag ?? "";
|
|
38
|
+
card.className = ["subaru", `subaru-${u.status}`, options.className ?? ""].filter(Boolean).join(" ");
|
|
39
|
+
if (u.status === "installing") card.setAttribute("aria-busy", "true");
|
|
40
|
+
else card.removeAttribute("aria-busy");
|
|
41
|
+
card.replaceChildren();
|
|
42
|
+
const body = el("div", "subaru-body");
|
|
43
|
+
const text = el("span", "subaru-text");
|
|
44
|
+
const actions = el("div", "subaru-actions");
|
|
45
|
+
card.append(body);
|
|
46
|
+
const button = (label, cls, action) => {
|
|
47
|
+
const b = el("button", `subaru-button ${cls}`);
|
|
48
|
+
b.type = "button";
|
|
49
|
+
b.textContent = label;
|
|
50
|
+
b.addEventListener("click", action);
|
|
51
|
+
actions.append(b);
|
|
52
|
+
};
|
|
53
|
+
switch (u.status) {
|
|
54
|
+
case "available": {
|
|
55
|
+
text.textContent = labels.available(version, name);
|
|
56
|
+
body.append(text);
|
|
57
|
+
if (u.release?.notes) {
|
|
58
|
+
const toggle = el("button", "subaru-notes-toggle");
|
|
59
|
+
toggle.type = "button";
|
|
60
|
+
toggle.textContent = labels.notes;
|
|
61
|
+
toggle.setAttribute("aria-expanded", String(showNotes));
|
|
62
|
+
toggle.addEventListener("click", () => {
|
|
63
|
+
showNotes = !showNotes;
|
|
64
|
+
render(updater.getState());
|
|
65
|
+
});
|
|
66
|
+
body.append(toggle);
|
|
67
|
+
if (showNotes) {
|
|
68
|
+
const pre = el("pre", "subaru-notes");
|
|
69
|
+
pre.textContent = u.release.notes;
|
|
70
|
+
body.append(pre);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const passive = u.policy === "notify" || u.skipped;
|
|
74
|
+
if (passive) {
|
|
75
|
+
if (u.release?.url) {
|
|
76
|
+
const a = el("a", "subaru-link");
|
|
77
|
+
a.href = u.release.url;
|
|
78
|
+
a.target = "_blank";
|
|
79
|
+
a.rel = "noreferrer";
|
|
80
|
+
a.textContent = labels.notes;
|
|
81
|
+
actions.append(a);
|
|
82
|
+
}
|
|
83
|
+
button(labels.later, "subaru-secondary", () => updater.dismiss());
|
|
84
|
+
} else {
|
|
85
|
+
button(labels.update, "subaru-primary", () => void updater.install());
|
|
86
|
+
button(labels.skip, "subaru-secondary", () => void updater.skip());
|
|
87
|
+
button(labels.later, "subaru-secondary", () => updater.dismiss());
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
case "installing": {
|
|
92
|
+
text.textContent = labels.installing(u.progress);
|
|
93
|
+
body.append(text);
|
|
94
|
+
const bar = el("div", u.progress === null ? "subaru-bar subaru-bar-indeterminate" : "subaru-bar");
|
|
95
|
+
bar.setAttribute("aria-hidden", "true");
|
|
96
|
+
const fill = el("div", "subaru-bar-fill");
|
|
97
|
+
if (u.progress !== null) fill.style.width = `${Math.round(u.progress * 100)}%`;
|
|
98
|
+
bar.append(fill);
|
|
99
|
+
body.append(bar);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case "ready":
|
|
103
|
+
text.textContent = labels.ready(u.kind);
|
|
104
|
+
body.append(text);
|
|
105
|
+
button(labels.restart, "subaru-primary", () => void updater.restart());
|
|
106
|
+
button(labels.later, "subaru-secondary", () => updater.dismiss());
|
|
107
|
+
break;
|
|
108
|
+
case "error":
|
|
109
|
+
text.textContent = labels.error(u.error ?? "");
|
|
110
|
+
body.append(text);
|
|
111
|
+
button(labels.retry, "subaru-primary", () => void updater.install());
|
|
112
|
+
button(labels.later, "subaru-secondary", () => updater.dismiss());
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
if (actions.childNodes.length) card.append(actions);
|
|
116
|
+
if (!card.isConnected) host.append(card);
|
|
117
|
+
};
|
|
118
|
+
const unsubscribe = updater.subscribe(render);
|
|
119
|
+
const attach = () => (options.container ?? document.body).append(host);
|
|
120
|
+
if (options.container || document.body) attach();
|
|
121
|
+
else document.addEventListener("DOMContentLoaded", attach, { once: true });
|
|
122
|
+
render(updater.getState());
|
|
123
|
+
return () => {
|
|
124
|
+
unsubscribe();
|
|
125
|
+
host.remove();
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function el(tag, className) {
|
|
129
|
+
const node = document.createElement(tag);
|
|
130
|
+
if (className) node.className = className;
|
|
131
|
+
return node;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export {
|
|
135
|
+
defaultCardLabels,
|
|
136
|
+
mountCard
|
|
137
|
+
};
|
|
138
|
+
//# sourceMappingURL=chunk-K7IY5EW6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/card.ts"],"sourcesContent":["/**\n * The update card without React: the same markup, classes and behaviour as\n * <UpdatePrompt>, driven straight from the DOM. subaru() in the web and Tauri\n * entries mounts it so a single call is the whole integration.\n */\nimport type { UpdaterLike, UpdaterState } from './core';\nimport { cardVars, injectStyles, type CardStyleOptions } from './styles';\n\nexport interface CardLabels {\n available: (version: string, name: string) => string;\n installing: (progress: number | null) => string;\n ready: (kind: UpdaterState['kind']) => string;\n error: (message: string) => string;\n update: string;\n restart: string;\n skip: string;\n later: string;\n retry: string;\n notes: string;\n}\n\nexport const defaultCardLabels: CardLabels = {\n available: (version, name) => `${name ? `${name} ` : ''}${version} is available.`,\n installing: (progress) => (progress === null ? 'Updating…' : `Updating… ${Math.round(progress * 100)}%`),\n ready: (kind) => (kind === 'web' ? 'New version ready. It loads the next time you come back.' : 'Update ready. Restart to finish.'),\n error: (message) => `Update failed: ${message}`,\n update: 'Update now',\n restart: 'Restart now',\n skip: 'Skip this version',\n later: 'Later',\n retry: 'Try again',\n notes: 'What changed',\n};\n\nexport interface MountCardOptions extends CardStyleOptions {\n name?: string;\n labels?: Partial<CardLabels>;\n /** Where to append the card. Default document.body. */\n container?: HTMLElement;\n}\n\n/** Mount the card; returns a function that removes it and unsubscribes. */\nexport function mountCard(updater: UpdaterLike, options: MountCardOptions = {}): () => void {\n const labels = { ...defaultCardLabels, ...options.labels };\n const name = options.name ?? '';\n if (!options.unstyled) injectStyles();\n const host = document.createElement('div');\n host.setAttribute('data-subaru-card', '');\n let showNotes = false;\n\n // One element for the life of the card, as React keeps one too: a state\n // change rewrites its contents instead of replacing it, so the entry\n // animation plays once, when the card joins the document.\n const card = el('div', 'subaru');\n card.setAttribute('data-subaru-theme', options.theme ?? 'auto');\n card.setAttribute('data-subaru-position', options.position ?? 'bottom-right');\n for (const [key, value] of Object.entries(cardVars(options))) card.style.setProperty(key, value);\n card.setAttribute('role', 'status');\n card.setAttribute('aria-live', 'polite');\n\n const render = (u: UpdaterState) => {\n if (u.dismissed || !['available', 'installing', 'ready', 'error'].includes(u.status)) {\n card.remove();\n return;\n }\n const version = u.release?.version ?? u.release?.tag ?? '';\n card.className = ['subaru', `subaru-${u.status}`, options.className ?? ''].filter(Boolean).join(' ');\n if (u.status === 'installing') card.setAttribute('aria-busy', 'true');\n else card.removeAttribute('aria-busy');\n card.replaceChildren();\n const body = el('div', 'subaru-body');\n const text = el('span', 'subaru-text');\n const actions = el('div', 'subaru-actions');\n card.append(body);\n\n const button = (label: string, cls: string, action: () => void) => {\n const b = el('button', `subaru-button ${cls}`) as HTMLButtonElement;\n b.type = 'button';\n b.textContent = label;\n b.addEventListener('click', action);\n actions.append(b);\n };\n\n switch (u.status) {\n case 'available': {\n text.textContent = labels.available(version, name);\n body.append(text);\n if (u.release?.notes) {\n const toggle = el('button', 'subaru-notes-toggle') as HTMLButtonElement;\n toggle.type = 'button';\n toggle.textContent = labels.notes;\n toggle.setAttribute('aria-expanded', String(showNotes));\n toggle.addEventListener('click', () => {\n showNotes = !showNotes;\n render(updater.getState());\n });\n body.append(toggle);\n if (showNotes) {\n const pre = el('pre', 'subaru-notes');\n pre.textContent = u.release.notes;\n body.append(pre);\n }\n }\n const passive = u.policy === 'notify' || u.skipped;\n if (passive) {\n if (u.release?.url) {\n const a = el('a', 'subaru-link') as HTMLAnchorElement;\n a.href = u.release.url;\n a.target = '_blank';\n a.rel = 'noreferrer';\n a.textContent = labels.notes;\n actions.append(a);\n }\n button(labels.later, 'subaru-secondary', () => updater.dismiss());\n } else {\n button(labels.update, 'subaru-primary', () => void updater.install());\n button(labels.skip, 'subaru-secondary', () => void updater.skip());\n button(labels.later, 'subaru-secondary', () => updater.dismiss());\n }\n break;\n }\n case 'installing': {\n text.textContent = labels.installing(u.progress);\n body.append(text);\n const bar = el('div', u.progress === null ? 'subaru-bar subaru-bar-indeterminate' : 'subaru-bar');\n bar.setAttribute('aria-hidden', 'true');\n const fill = el('div', 'subaru-bar-fill');\n if (u.progress !== null) fill.style.width = `${Math.round(u.progress * 100)}%`;\n bar.append(fill);\n body.append(bar);\n break;\n }\n case 'ready':\n text.textContent = labels.ready(u.kind);\n body.append(text);\n button(labels.restart, 'subaru-primary', () => void updater.restart());\n button(labels.later, 'subaru-secondary', () => updater.dismiss());\n break;\n case 'error':\n text.textContent = labels.error(u.error ?? '');\n body.append(text);\n button(labels.retry, 'subaru-primary', () => void updater.install());\n button(labels.later, 'subaru-secondary', () => updater.dismiss());\n break;\n }\n if (actions.childNodes.length) card.append(actions);\n if (!card.isConnected) host.append(card);\n };\n\n const unsubscribe = updater.subscribe(render);\n const attach = () => (options.container ?? document.body).append(host);\n if (options.container || document.body) attach();\n else document.addEventListener('DOMContentLoaded', attach, { once: true });\n render(updater.getState());\n return () => {\n unsubscribe();\n host.remove();\n };\n}\n\nfunction el(tag: string, className: string): HTMLElement {\n const node = document.createElement(tag);\n if (className) node.className = className;\n return node;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// src/source.ts
|
|
2
|
+
function sourceAdapter(options) {
|
|
3
|
+
const checkout = options.checkout;
|
|
4
|
+
const run = options.run;
|
|
5
|
+
const track = options.track ?? "branch";
|
|
6
|
+
const buildCmd = options.build ?? ["make", "build"];
|
|
7
|
+
const updateCmd = options.update ?? ["make", "update"];
|
|
8
|
+
let target = "";
|
|
9
|
+
async function git(args, cwd = checkout) {
|
|
10
|
+
const result = await run({ argv: ["git", ...args], cwd });
|
|
11
|
+
if (result.code !== 0) {
|
|
12
|
+
throw new Error(`git ${args[0]} failed: ${result.output.trim() || `exit ${result.code}`}`);
|
|
13
|
+
}
|
|
14
|
+
return result.output;
|
|
15
|
+
}
|
|
16
|
+
async function ensureCheckout() {
|
|
17
|
+
const probe = await run({ argv: ["git", "rev-parse", "--show-toplevel"], cwd: checkout });
|
|
18
|
+
if (probe.code === 0) return;
|
|
19
|
+
if (!options.repo) {
|
|
20
|
+
throw new Error(`${checkout} is not a git checkout (pass repo to clone it)`);
|
|
21
|
+
}
|
|
22
|
+
const url = /^[\w.-]+\/[\w.-]+$/.test(options.repo) ? `https://github.com/${options.repo}.git` : options.repo;
|
|
23
|
+
const parent = checkout.replace(/\/[^/]+\/?$/, "") || "/";
|
|
24
|
+
const cloned = await run({ argv: ["git", "clone", "--quiet", url, checkout], cwd: parent });
|
|
25
|
+
if (cloned.code !== 0) {
|
|
26
|
+
throw new Error(`git clone failed: ${cloned.output.trim() || `exit ${cloned.code}`}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function upstream() {
|
|
30
|
+
const ref = (await git(["rev-parse", "--abbrev-ref", "@{u}"])).trim();
|
|
31
|
+
if (!ref) throw new Error("the checkout tracks no upstream branch");
|
|
32
|
+
return ref;
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
kind: "source",
|
|
36
|
+
name: options.name ?? `source:${checkout}`,
|
|
37
|
+
async check() {
|
|
38
|
+
await ensureCheckout();
|
|
39
|
+
await git(["fetch", "--quiet", "--tags", "origin"]);
|
|
40
|
+
const up = await upstream();
|
|
41
|
+
if (track === "branch") {
|
|
42
|
+
const ahead2 = Number((await git(["rev-list", "--count", `HEAD..${up}`])).trim());
|
|
43
|
+
if (!ahead2) return null;
|
|
44
|
+
const sha = (await git(["rev-parse", up])).trim();
|
|
45
|
+
const notes2 = (await git(["log", "--format=%h %s", `HEAD..${up}`])).trim();
|
|
46
|
+
target = up;
|
|
47
|
+
const release2 = { tag: sha, version: sha.slice(0, 7) };
|
|
48
|
+
if (notes2) release2.notes = notes2;
|
|
49
|
+
return release2;
|
|
50
|
+
}
|
|
51
|
+
const latest = (await git(["describe", "--tags", "--abbrev=0", up])).trim();
|
|
52
|
+
if (!latest) return null;
|
|
53
|
+
const ahead = Number((await git(["rev-list", "--count", `HEAD..${latest}`])).trim());
|
|
54
|
+
if (!ahead) return null;
|
|
55
|
+
const notes = (await git(["log", "--format=%h %s", `HEAD..${latest}`])).trim();
|
|
56
|
+
target = latest;
|
|
57
|
+
const release = { tag: latest, version: latest };
|
|
58
|
+
if (notes) release.notes = notes;
|
|
59
|
+
return release;
|
|
60
|
+
},
|
|
61
|
+
async install(release, { relaunch }) {
|
|
62
|
+
await ensureCheckout();
|
|
63
|
+
const ref = target || (track === "releases" ? release.tag : await upstream());
|
|
64
|
+
await git(["merge", "--ff-only", ref]);
|
|
65
|
+
if (relaunch) {
|
|
66
|
+
const started = await run({ argv: updateCmd, cwd: checkout, detach: true });
|
|
67
|
+
if (started.code !== 0) throw new Error(`${updateCmd.join(" ")}: ${started.output.trim() || `exit ${started.code}`}`);
|
|
68
|
+
return "restarting";
|
|
69
|
+
}
|
|
70
|
+
const built = await run({ argv: buildCmd, cwd: checkout });
|
|
71
|
+
if (built.code !== 0) throw new Error(`${buildCmd.join(" ")} failed: ${built.output.trim().split("\n").slice(-5).join("\n")}`);
|
|
72
|
+
return "ready";
|
|
73
|
+
},
|
|
74
|
+
async restart() {
|
|
75
|
+
const started = await run({ argv: updateCmd, cwd: checkout, detach: true });
|
|
76
|
+
if (started.code !== 0) throw new Error(`${updateCmd.join(" ")}: ${started.output.trim() || `exit ${started.code}`}`);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
var TOOL_DIRS = ["/opt/homebrew/bin", "/usr/local/bin", "~/.bun/bin", "~/go/bin", "~/.cargo/bin", "/usr/local/go/bin"];
|
|
81
|
+
function pathWithTools(current, home) {
|
|
82
|
+
const dirs = TOOL_DIRS.map((d) => home ? d.replace(/^~/, home) : d).filter((d) => !d.startsWith("~"));
|
|
83
|
+
const seen = /* @__PURE__ */ new Set();
|
|
84
|
+
return [...dirs, ...(current ?? "").split(":")].filter((d) => d && !seen.has(d) && seen.add(d)).join(":");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export {
|
|
88
|
+
sourceAdapter,
|
|
89
|
+
TOOL_DIRS,
|
|
90
|
+
pathWithTools
|
|
91
|
+
};
|
|
92
|
+
//# sourceMappingURL=chunk-MI26ZEPP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/source.ts"],"sourcesContent":["/**\n * Source mode: the program updates from its own git checkout instead of from\n * published bundles. For unsigned personal apps that a Makefile builds and\n * copies into /Applications, this is the update path that actually works:\n * fetch, fast-forward, `make update`, and the Makefile does what it already\n * does (stop, delete, build, install, reset permissions, relaunch).\n *\n * The adapter runs no commands itself; it hands argv to a Runner. Electron's\n * main process gets one from \"@justin06lee/subaru/electron\" (nodeRunner) and\n * a Tauri window from \"@justin06lee/subaru/tauri\" (shellRunner).\n */\nimport type { Adapter, Release } from './core';\n\nexport interface RunRequest {\n argv: string[];\n cwd: string;\n /** Start it and return immediately: the command is going to kill this program. */\n detach?: boolean;\n}\n\nexport interface RunResult {\n code: number;\n output: string;\n}\n\nexport type Runner = (request: RunRequest) => Promise<RunResult>;\n\nexport interface SourceAdapterOptions {\n /** Absolute path of the git checkout the installed program was built from. */\n checkout: string;\n run: Runner;\n /**\n * \"owner/name\" or a full git URL. Only used to clone when `checkout` does\n * not exist yet (a fresh machine). The checkout's own origin is what gets\n * fetched otherwise.\n */\n repo?: string;\n /**\n * What counts as an update. \"branch\": any new commit on the tracked\n * upstream branch (every push is an update). \"releases\": only a newer tag.\n * Default \"branch\".\n */\n track?: 'branch' | 'releases';\n /** Builds without installing; run in the background under the auto policy. Default [\"make\", \"build\"]. */\n build?: string[];\n /** Stops, rebuilds, reinstalls and relaunches the program. Default [\"make\", \"update\"]. */\n update?: string[];\n /** State key. Default: the checkout path. */\n name?: string;\n}\n\nexport function sourceAdapter(options: SourceAdapterOptions): Adapter {\n const checkout = options.checkout;\n const run = options.run;\n const track = options.track ?? 'branch';\n const buildCmd = options.build ?? ['make', 'build'];\n const updateCmd = options.update ?? ['make', 'update'];\n let target = '';\n\n async function git(args: string[], cwd = checkout): Promise<string> {\n const result = await run({ argv: ['git', ...args], cwd });\n if (result.code !== 0) {\n throw new Error(`git ${args[0]} failed: ${result.output.trim() || `exit ${result.code}`}`);\n }\n return result.output;\n }\n\n async function ensureCheckout(): Promise<void> {\n const probe = await run({ argv: ['git', 'rev-parse', '--show-toplevel'], cwd: checkout });\n if (probe.code === 0) return;\n if (!options.repo) {\n throw new Error(`${checkout} is not a git checkout (pass repo to clone it)`);\n }\n const url = /^[\\w.-]+\\/[\\w.-]+$/.test(options.repo) ? `https://github.com/${options.repo}.git` : options.repo;\n const parent = checkout.replace(/\\/[^/]+\\/?$/, '') || '/';\n const cloned = await run({ argv: ['git', 'clone', '--quiet', url, checkout], cwd: parent });\n if (cloned.code !== 0) {\n throw new Error(`git clone failed: ${cloned.output.trim() || `exit ${cloned.code}`}`);\n }\n }\n\n async function upstream(): Promise<string> {\n const ref = (await git(['rev-parse', '--abbrev-ref', '@{u}'])).trim();\n if (!ref) throw new Error('the checkout tracks no upstream branch');\n return ref;\n }\n\n return {\n kind: 'source',\n name: options.name ?? `source:${checkout}`,\n async check() {\n await ensureCheckout();\n await git(['fetch', '--quiet', '--tags', 'origin']);\n const up = await upstream();\n if (track === 'branch') {\n const ahead = Number((await git(['rev-list', '--count', `HEAD..${up}`])).trim());\n if (!ahead) return null;\n const sha = (await git(['rev-parse', up])).trim();\n const notes = (await git(['log', '--format=%h %s', `HEAD..${up}`])).trim();\n target = up;\n const release: Release = { tag: sha, version: sha.slice(0, 7) };\n if (notes) release.notes = notes;\n return release;\n }\n const latest = (await git(['describe', '--tags', '--abbrev=0', up])).trim();\n if (!latest) return null;\n const ahead = Number((await git(['rev-list', '--count', `HEAD..${latest}`])).trim());\n if (!ahead) return null;\n const notes = (await git(['log', '--format=%h %s', `HEAD..${latest}`])).trim();\n target = latest;\n const release: Release = { tag: latest, version: latest };\n if (notes) release.notes = notes;\n return release;\n },\n async install(release, { relaunch }) {\n await ensureCheckout();\n const ref = target || (track === 'releases' ? release.tag : await upstream());\n // Fast-forward only: local commits or conflicts stop here, visibly,\n // rather than in a log after the program is gone.\n await git(['merge', '--ff-only', ref]);\n if (relaunch) {\n const started = await run({ argv: updateCmd, cwd: checkout, detach: true });\n if (started.code !== 0) throw new Error(`${updateCmd.join(' ')}: ${started.output.trim() || `exit ${started.code}`}`);\n return 'restarting';\n }\n const built = await run({ argv: buildCmd, cwd: checkout });\n if (built.code !== 0) throw new Error(`${buildCmd.join(' ')} failed: ${built.output.trim().split('\\n').slice(-5).join('\\n')}`);\n return 'ready';\n },\n async restart() {\n const started = await run({ argv: updateCmd, cwd: checkout, detach: true });\n if (started.code !== 0) throw new Error(`${updateCmd.join(' ')}: ${started.output.trim() || `exit ${started.code}`}`);\n },\n };\n}\n\n/**\n * Directories a GUI app launched from Finder does not have on PATH but a\n * Makefile almost always needs.\n */\nexport const TOOL_DIRS = ['/opt/homebrew/bin', '/usr/local/bin', '~/.bun/bin', '~/go/bin', '~/.cargo/bin', '/usr/local/go/bin'];\n\nexport function pathWithTools(current: string | undefined, home: string | undefined): string {\n const dirs = TOOL_DIRS.map((d) => (home ? d.replace(/^~/, home) : d)).filter((d) => !d.startsWith('~'));\n const seen = new Set<string>();\n return [...dirs, ...(current ?? '').split(':')].filter((d) => d && !seen.has(d) && seen.add(d)).join(':');\n}\n"],"mappings":"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","names":["ahead","notes","release"]}
|