@zenbujs/core 0.0.5 → 0.0.8
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/{advice-config-QYB2qEd_.mjs → advice-config-DXSIo0sg.mjs} +40 -39
- package/dist/advice.d.mts +8 -8
- package/dist/advice.mjs +2 -2
- package/dist/{base-window-BbFRRhKP.mjs → base-window-BxBZ2md_.mjs} +51 -7
- package/dist/{transforms-CuTODvDx.d.mts → build-config-Dzg2frpk.d.mts} +98 -28
- package/dist/build-config-pWdmLnrk.mjs +53 -0
- package/dist/{build-electron-CNJ0dLND.mjs → build-electron-Dsbb1EMl.mjs} +308 -120
- package/dist/{build-source-C2puqEVr.mjs → build-source-d1J3shV8.mjs} +62 -27
- package/dist/cli/bin.mjs +7 -7
- package/dist/cli/build.d.mts +2 -2
- package/dist/cli/build.mjs +2 -3
- package/dist/cli/resolve-config.mjs +1 -1
- package/dist/{cli-C3R1LBMY.mjs → cli-kL6mPgBE.mjs} +2 -2
- package/dist/config.d.mts +3 -3
- package/dist/config.mjs +2 -3
- package/dist/{db-xjvahRFJ.mjs → db-Bc292RYo.mjs} +2 -2
- package/dist/db.d.mts +1 -1
- package/dist/dev-B2emj0HZ.mjs +301 -0
- package/dist/env-bootstrap.d.mts +1 -1
- package/dist/events.d.mts +19 -0
- package/dist/events.mjs +1 -0
- package/dist/host-version-BIrF8tX7.mjs +65 -0
- package/dist/index-w5QyDjuf.d.mts +780 -0
- package/dist/index.d.mts +5 -6
- package/dist/index.mjs +2 -2
- package/dist/installing-preload.cjs +60 -0
- package/dist/launcher.mjs +2615 -122
- package/dist/{link-c0_aLWQ3.mjs → link-glX89NV5.mjs} +215 -89
- package/dist/{load-config-xMf2wxH8.mjs → load-config-C4Oe2qZO.mjs} +5 -1
- package/dist/loaders/zenbu.mjs +102 -0
- package/dist/node-loader.mjs +1 -1
- package/dist/{publish-source-Dill72NS.mjs → publish-source-Dq2c0iOw.mjs} +2 -2
- package/dist/react.d.mts +55 -6
- package/dist/react.mjs +116 -5
- package/dist/registry-CMp8FYgS.d.mts +47 -0
- package/dist/registry-generated.d.mts +26 -0
- package/dist/registry-generated.mjs +1 -0
- package/dist/registry.d.mts +2 -2
- package/dist/{reloader-DzEO8kJr.mjs → reloader-B22UiNA2.mjs} +2 -4
- package/dist/{renderer-host-Cau9JK0v.mjs → renderer-host-DD16MXhI.mjs} +152 -43
- package/dist/{rpc-JfGv-Wuw.mjs → rpc-C4_NQmpT.mjs} +5 -4
- package/dist/{runtime-pCeVzj--.d.mts → runtime-BQWntcOb.d.mts} +85 -48
- package/dist/runtime.d.mts +2 -2
- package/dist/runtime.mjs +139 -83
- package/dist/{schema-Dl85YjXW.d.mts → schema-CjrMVk36.d.mts} +3 -3
- package/dist/schema.d.mts +1 -1
- package/dist/schema.mjs +1 -1
- package/dist/{server-y3PPbh3l.mjs → server-CZLMF8Dj.mjs} +1 -3
- package/dist/services/default.d.mts +3 -3
- package/dist/services/default.mjs +14 -13
- package/dist/services/index.d.mts +2 -280
- package/dist/services/index.mjs +8 -7
- package/dist/setup-gate.d.mts +1 -1
- package/dist/setup-gate.mjs +117 -24
- package/dist/{transform-CmFYPmt8.mjs → transform-BzrwkEdf.mjs} +22 -916
- package/dist/updater-DCkz9M1c.mjs +1008 -0
- package/dist/{vite-plugins-Do7liKi_.mjs → vite-plugins-tt6KAtyE.mjs} +26 -25
- package/dist/vite.d.mts +3 -3
- package/dist/vite.mjs +1 -1
- package/dist/{window-o2NGUsIb.mjs → window-YFKvAM0l.mjs} +30 -16
- package/package.json +15 -2
- package/dist/build-config-C3a-o3_B.mjs +0 -23
- package/dist/dev-Dazhu66l.mjs +0 -85
- package/dist/registry-eX6e2oql.d.mts +0 -61
- package/dist/transforms-htxfTwsY.mjs +0 -47
- /package/dist/{config-DXRCDUxG.mjs → config-BK78JDRI.mjs} +0 -0
- /package/dist/{env-bootstrap-DW2hVhSO.d.mts → env-bootstrap-rTs8KR3-.d.mts} +0 -0
- /package/dist/{index-M_lSNBrq.d.mts → index-DeDxePAa.d.mts} +0 -0
- /package/dist/{mirror-sync-PDzxhf1w.mjs → mirror-sync-pYU6f3-c.mjs} +0 -0
- /package/dist/{monorepo-3avKJwzJ.mjs → monorepo-Dct-kkbQ.mjs} +0 -0
- /package/dist/{node-_8xShqxr.mjs → node-BhfLKYCi.mjs} +0 -0
- /package/dist/{setup-gate-Dcy8gGPJ.d.mts → setup-gate-BQq0QgZH.d.mts} +0 -0
|
@@ -0,0 +1,1008 @@
|
|
|
1
|
+
import { n as __exportAll } from "./chunk-DsiFFCwN.mjs";
|
|
2
|
+
import { Service, runtime } from "./runtime.mjs";
|
|
3
|
+
import { t as createLogger } from "./log-6rzaCV0I.mjs";
|
|
4
|
+
import { n as tryReadHostVersion } from "./host-version-BIrF8tX7.mjs";
|
|
5
|
+
import fs, { existsSync } from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import semver from "semver";
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import crypto from "node:crypto";
|
|
11
|
+
import fsp from "node:fs/promises";
|
|
12
|
+
import { app } from "electron";
|
|
13
|
+
import * as git from "isomorphic-git";
|
|
14
|
+
import http from "isomorphic-git/http/node";
|
|
15
|
+
import { pauseWatcherPath } from "@zenbujs/hmr/pause";
|
|
16
|
+
//#region src/shared/pm-install.ts
|
|
17
|
+
/**
|
|
18
|
+
* Bundled-toolchain `<pm> install` helpers.
|
|
19
|
+
*
|
|
20
|
+
* Imported by both:
|
|
21
|
+
* - `packages/core/src/launcher.ts` (tsdown inlines this into
|
|
22
|
+
* `dist/launcher.mjs`; the launcher cannot `import "@zenbujs/core"`)
|
|
23
|
+
* - `packages/core/src/services/updater.ts` (resolved through normal
|
|
24
|
+
* `@zenbujs/core/...` resolution at runtime)
|
|
25
|
+
*
|
|
26
|
+
* Both call sites operate on the apps-dir (`~/.zenbu/apps/<name>/`) and
|
|
27
|
+
* the .app's `Resources/` (where `provisionToolchain` staged
|
|
28
|
+
* `bun`, `pnpm`, etc.). The launcher kicks off the FIRST install at
|
|
29
|
+
* launch; the updater service runs the SAME logic when an `update()`
|
|
30
|
+
* lockfile-diff says deps drifted.
|
|
31
|
+
*/
|
|
32
|
+
function lockfileFor(type) {
|
|
33
|
+
switch (type) {
|
|
34
|
+
case "pnpm": return "pnpm-lock.yaml";
|
|
35
|
+
case "npm": return "package-lock.json";
|
|
36
|
+
case "yarn": return "yarn.lock";
|
|
37
|
+
case "bun": return "bun.lock";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function isYarnBerry(version) {
|
|
41
|
+
const major = parseInt(version.split(".")[0] ?? "", 10);
|
|
42
|
+
return Number.isFinite(major) && major >= 2;
|
|
43
|
+
}
|
|
44
|
+
function bundledToolPath(name, resourcesPath) {
|
|
45
|
+
const candidates = [path.join(resourcesPath, "toolchain", "bin", name), path.join(resourcesPath, "toolchain", name)];
|
|
46
|
+
for (const candidate of candidates) if (existsSync(candidate)) return candidate;
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the entrypoint we'll exec for a given PM, mirroring the layout
|
|
51
|
+
* `provisionToolchain` writes into the bundle's `Resources/toolchain/`.
|
|
52
|
+
*/
|
|
53
|
+
function bundledPmEntry(pm, resourcesPath) {
|
|
54
|
+
switch (pm.type) {
|
|
55
|
+
case "pnpm": {
|
|
56
|
+
const p = path.join(resourcesPath, "toolchain", "pnpm", "bin", "pnpm.cjs");
|
|
57
|
+
if (!existsSync(p)) throw new Error(`bundled pnpm entry not found at ${p}. The .app's toolchain is incomplete.`);
|
|
58
|
+
return {
|
|
59
|
+
kind: "js",
|
|
60
|
+
path: p
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
case "npm": {
|
|
64
|
+
const p = path.join(resourcesPath, "toolchain", "npm", "bin", "npm-cli.js");
|
|
65
|
+
if (!existsSync(p)) throw new Error(`bundled npm entry not found at ${p}. The .app's toolchain is incomplete.`);
|
|
66
|
+
return {
|
|
67
|
+
kind: "js",
|
|
68
|
+
path: p
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
case "yarn": {
|
|
72
|
+
if (isYarnBerry(pm.version)) {
|
|
73
|
+
const p = path.join(resourcesPath, "toolchain", "yarn.cjs");
|
|
74
|
+
if (!existsSync(p)) throw new Error(`bundled yarn (berry) entry not found at ${p}. The .app's toolchain is incomplete.`);
|
|
75
|
+
return {
|
|
76
|
+
kind: "js",
|
|
77
|
+
path: p
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const p = path.join(resourcesPath, "toolchain", "yarn", "bin", "yarn.js");
|
|
81
|
+
if (!existsSync(p)) throw new Error(`bundled yarn (classic) entry not found at ${p}. The .app's toolchain is incomplete.`);
|
|
82
|
+
return {
|
|
83
|
+
kind: "js",
|
|
84
|
+
path: p
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
case "bun": return { kind: "bun" };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function electronTargetVersion(appsDir) {
|
|
91
|
+
if (process.versions.electron) return process.versions.electron;
|
|
92
|
+
try {
|
|
93
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(appsDir, "package.json"), "utf8"));
|
|
94
|
+
return (pkg.devDependencies?.electron ?? pkg.dependencies?.electron ?? "").replace(/^[^\d]*/, "") || "42.0.0";
|
|
95
|
+
} catch {
|
|
96
|
+
return "42.0.0";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function buildInstallEnv(appsDir) {
|
|
100
|
+
const target = electronTargetVersion(appsDir);
|
|
101
|
+
return {
|
|
102
|
+
...process.env,
|
|
103
|
+
CI: "true",
|
|
104
|
+
HOME: path.join(appsDir, ".zenbu", ".node-gyp"),
|
|
105
|
+
npm_config_runtime: "electron",
|
|
106
|
+
npm_config_target: target,
|
|
107
|
+
npm_config_disturl: "https://electronjs.org/headers",
|
|
108
|
+
npm_config_arch: process.arch
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Best-effort progress regex per package manager. We pass each stdout line
|
|
113
|
+
* through these and return a `progress` payload when one matches; otherwise
|
|
114
|
+
* `null`. Failing to match is fine — the UI just won't show fine-grained
|
|
115
|
+
* progress for that PM.
|
|
116
|
+
*/
|
|
117
|
+
const PNPM_RESOLVED_RE = /Progress:\s+resolved\s+(\d+),\s+reused\s+(\d+),\s+downloaded\s+(\d+)/i;
|
|
118
|
+
const PNPM_PROGRESS_RE = /(\d+)\s*\/\s*(\d+)/;
|
|
119
|
+
function parseInstallProgress(pm, line) {
|
|
120
|
+
if (pm === "pnpm") {
|
|
121
|
+
const m = line.match(PNPM_RESOLVED_RE);
|
|
122
|
+
if (m) {
|
|
123
|
+
const resolved = parseInt(m[1], 10);
|
|
124
|
+
const reused = parseInt(m[2], 10);
|
|
125
|
+
const downloaded = parseInt(m[3], 10);
|
|
126
|
+
return {
|
|
127
|
+
phase: "resolve",
|
|
128
|
+
loaded: reused + downloaded,
|
|
129
|
+
total: resolved,
|
|
130
|
+
ratio: resolved > 0 ? (reused + downloaded) / resolved : void 0
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const m = line.match(PNPM_PROGRESS_RE);
|
|
135
|
+
if (m) {
|
|
136
|
+
const loaded = parseInt(m[1], 10);
|
|
137
|
+
const total = parseInt(m[2], 10);
|
|
138
|
+
if (total > 0) return {
|
|
139
|
+
loaded,
|
|
140
|
+
total,
|
|
141
|
+
ratio: loaded / total
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
function spawnInstall(args) {
|
|
147
|
+
const { bin, cliArgs, cwd, env, label, pmType, reporter, signal } = args;
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
if (signal?.aborted) {
|
|
150
|
+
reject(/* @__PURE__ */ new Error(`${label} aborted before start`));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const usePiped = reporter != null;
|
|
154
|
+
const child = spawn(bin, cliArgs, {
|
|
155
|
+
cwd,
|
|
156
|
+
stdio: usePiped ? [
|
|
157
|
+
"inherit",
|
|
158
|
+
"pipe",
|
|
159
|
+
"pipe"
|
|
160
|
+
] : "inherit",
|
|
161
|
+
env
|
|
162
|
+
});
|
|
163
|
+
const onAbort = () => {
|
|
164
|
+
try {
|
|
165
|
+
child.kill("SIGTERM");
|
|
166
|
+
} catch {}
|
|
167
|
+
};
|
|
168
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
169
|
+
if (usePiped) {
|
|
170
|
+
const wireStream = (stream, which) => {
|
|
171
|
+
if (!stream) return;
|
|
172
|
+
let buf = "";
|
|
173
|
+
stream.setEncoding("utf8");
|
|
174
|
+
stream.on("data", (chunk) => {
|
|
175
|
+
buf += chunk;
|
|
176
|
+
let nl;
|
|
177
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
178
|
+
const rawLine = buf.slice(0, nl);
|
|
179
|
+
buf = buf.slice(nl + 1);
|
|
180
|
+
reporter?.rawLine?.(which, rawLine);
|
|
181
|
+
const line = rawLine.replace(/\r/g, "").trimEnd();
|
|
182
|
+
if (!line) continue;
|
|
183
|
+
reporter?.message?.(line);
|
|
184
|
+
const progress = parseInstallProgress(pmType, line);
|
|
185
|
+
if (progress) reporter?.progress?.(progress);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
stream.on("end", () => {
|
|
189
|
+
if (buf.length > 0) {
|
|
190
|
+
reporter?.rawLine?.(which, buf);
|
|
191
|
+
const line = buf.replace(/\r/g, "").trimEnd();
|
|
192
|
+
if (line) reporter?.message?.(line);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
wireStream(child.stdout, "stdout");
|
|
197
|
+
wireStream(child.stderr, "stderr");
|
|
198
|
+
}
|
|
199
|
+
child.on("error", (err) => {
|
|
200
|
+
signal?.removeEventListener("abort", onAbort);
|
|
201
|
+
reject(err);
|
|
202
|
+
});
|
|
203
|
+
child.on("close", (code, sig) => {
|
|
204
|
+
signal?.removeEventListener("abort", onAbort);
|
|
205
|
+
if (code === 0) resolve();
|
|
206
|
+
else if (signal?.aborted) reject(/* @__PURE__ */ new Error(`${label} aborted (${sig ?? code})`));
|
|
207
|
+
else reject(/* @__PURE__ */ new Error(`${label} exited with code ${code}`));
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
async function runInstall(opts) {
|
|
212
|
+
const { appsDir, resourcesPath, pm, reporter = null, signal } = opts;
|
|
213
|
+
const env = buildInstallEnv(appsDir);
|
|
214
|
+
const entry = bundledPmEntry(pm, resourcesPath);
|
|
215
|
+
switch (pm.type) {
|
|
216
|
+
case "pnpm": {
|
|
217
|
+
if (entry.kind !== "js") throw new Error("internal: pnpm entry shape");
|
|
218
|
+
const bun = bundledToolPath("bun", resourcesPath);
|
|
219
|
+
if (!bun) throw new Error(`bundled bun not found in ${resourcesPath}/toolchain (required to host the pnpm.cjs entry)`);
|
|
220
|
+
await spawnInstall({
|
|
221
|
+
bin: bun,
|
|
222
|
+
cliArgs: [
|
|
223
|
+
entry.path,
|
|
224
|
+
"install",
|
|
225
|
+
"--reporter=append-only"
|
|
226
|
+
],
|
|
227
|
+
cwd: appsDir,
|
|
228
|
+
env,
|
|
229
|
+
label: "pnpm install",
|
|
230
|
+
pmType: pm.type,
|
|
231
|
+
reporter,
|
|
232
|
+
signal
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
case "npm": {
|
|
237
|
+
if (entry.kind !== "js") throw new Error("internal: npm entry shape");
|
|
238
|
+
const bun = bundledToolPath("bun", resourcesPath);
|
|
239
|
+
if (!bun) throw new Error(`bundled bun not found in ${resourcesPath}/toolchain (required to host the npm-cli.js entry)`);
|
|
240
|
+
await spawnInstall({
|
|
241
|
+
bin: bun,
|
|
242
|
+
cliArgs: [
|
|
243
|
+
entry.path,
|
|
244
|
+
"install",
|
|
245
|
+
"--no-audit",
|
|
246
|
+
"--no-fund",
|
|
247
|
+
"--no-progress"
|
|
248
|
+
],
|
|
249
|
+
cwd: appsDir,
|
|
250
|
+
env,
|
|
251
|
+
label: "npm install",
|
|
252
|
+
pmType: pm.type,
|
|
253
|
+
reporter,
|
|
254
|
+
signal
|
|
255
|
+
});
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
case "yarn": {
|
|
259
|
+
if (entry.kind !== "js") throw new Error("internal: yarn entry shape");
|
|
260
|
+
const bun = bundledToolPath("bun", resourcesPath);
|
|
261
|
+
if (!bun) throw new Error(`bundled bun not found in ${resourcesPath}/toolchain (required to host the yarn.js entry)`);
|
|
262
|
+
if (isYarnBerry(pm.version)) await spawnInstall({
|
|
263
|
+
bin: bun,
|
|
264
|
+
cliArgs: [entry.path, "install"],
|
|
265
|
+
cwd: appsDir,
|
|
266
|
+
env: {
|
|
267
|
+
...env,
|
|
268
|
+
YARN_ENABLE_IMMUTABLE_INSTALLS: "false"
|
|
269
|
+
},
|
|
270
|
+
label: `yarn install (${pm.version})`,
|
|
271
|
+
pmType: pm.type,
|
|
272
|
+
reporter,
|
|
273
|
+
signal
|
|
274
|
+
});
|
|
275
|
+
else {
|
|
276
|
+
const rcPath = path.join(appsDir, ".zenbu", "yarn-classic-bun.yarnrc");
|
|
277
|
+
await fsp.mkdir(path.dirname(rcPath), { recursive: true });
|
|
278
|
+
await fsp.writeFile(rcPath, "strict-ssl false\n");
|
|
279
|
+
await spawnInstall({
|
|
280
|
+
bin: bun,
|
|
281
|
+
cliArgs: [
|
|
282
|
+
entry.path,
|
|
283
|
+
"install",
|
|
284
|
+
"--non-interactive",
|
|
285
|
+
"--no-progress",
|
|
286
|
+
"--network-timeout",
|
|
287
|
+
"600000",
|
|
288
|
+
"--use-yarnrc",
|
|
289
|
+
rcPath,
|
|
290
|
+
"--registry",
|
|
291
|
+
"https://registry.npmjs.org/"
|
|
292
|
+
],
|
|
293
|
+
cwd: appsDir,
|
|
294
|
+
env,
|
|
295
|
+
label: `yarn install (${pm.version})`,
|
|
296
|
+
pmType: pm.type,
|
|
297
|
+
reporter,
|
|
298
|
+
signal
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
case "bun": {
|
|
304
|
+
const bun = bundledToolPath("bun", resourcesPath);
|
|
305
|
+
if (!bun) throw new Error(`bundled bun not found in ${resourcesPath}/toolchain. The .app is missing required toolchain binaries.`);
|
|
306
|
+
await spawnInstall({
|
|
307
|
+
bin: bun,
|
|
308
|
+
cliArgs: ["install", "--no-progress"],
|
|
309
|
+
cwd: appsDir,
|
|
310
|
+
env,
|
|
311
|
+
label: "bun install",
|
|
312
|
+
pmType: pm.type,
|
|
313
|
+
reporter,
|
|
314
|
+
signal
|
|
315
|
+
});
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function fileHash(hash, filePath) {
|
|
321
|
+
hash.update(filePath);
|
|
322
|
+
hash.update("\0");
|
|
323
|
+
try {
|
|
324
|
+
hash.update(await fsp.readFile(filePath));
|
|
325
|
+
} catch {}
|
|
326
|
+
hash.update("\0");
|
|
327
|
+
}
|
|
328
|
+
async function depsSignature(appsDir, pm) {
|
|
329
|
+
const hash = crypto.createHash("sha256");
|
|
330
|
+
await fileHash(hash, path.join(appsDir, "package.json"));
|
|
331
|
+
await fileHash(hash, path.join(appsDir, lockfileFor(pm.type)));
|
|
332
|
+
hash.update(`${pm.type}@${pm.version}`);
|
|
333
|
+
hash.update("\0");
|
|
334
|
+
hash.update(process.versions.electron ?? "no-electron");
|
|
335
|
+
hash.update("\0");
|
|
336
|
+
hash.update(process.platform);
|
|
337
|
+
hash.update("\0");
|
|
338
|
+
hash.update(process.arch);
|
|
339
|
+
return hash.digest("hex");
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Read `<appsDir>/.zenbu/deps-sig` (the signature recorded after the
|
|
343
|
+
* last successful install). Returns `null` when missing.
|
|
344
|
+
*/
|
|
345
|
+
async function readDepsSig(appsDir) {
|
|
346
|
+
const sigPath = path.join(appsDir, ".zenbu", "deps-sig");
|
|
347
|
+
try {
|
|
348
|
+
return await fsp.readFile(sigPath, "utf8");
|
|
349
|
+
} catch {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async function writeDepsSig(appsDir, sig) {
|
|
354
|
+
const sigPath = path.join(appsDir, ".zenbu", "deps-sig");
|
|
355
|
+
await fsp.mkdir(path.dirname(sigPath), { recursive: true });
|
|
356
|
+
await fsp.writeFile(sigPath, sig);
|
|
357
|
+
}
|
|
358
|
+
//#endregion
|
|
359
|
+
//#region src/shared/range-resolver.ts
|
|
360
|
+
/**
|
|
361
|
+
* Resolves the latest commit on a source-mirror branch whose
|
|
362
|
+
* `package.json#zenbu.host` semver range still satisfies the running
|
|
363
|
+
* .app's `host.json#version`. Pure functions over `isomorphic-git` plus
|
|
364
|
+
* `semver` — no Electron dependency.
|
|
365
|
+
*
|
|
366
|
+
* Imported by both:
|
|
367
|
+
* - `packages/core/src/launcher.ts` (tsdown inlines this into
|
|
368
|
+
* `dist/launcher.mjs`; the launcher cannot `import "@zenbujs/core"`)
|
|
369
|
+
* - `packages/core/src/services/updater.ts` (resolved through normal
|
|
370
|
+
* `@zenbujs/core/...` resolution at runtime)
|
|
371
|
+
*
|
|
372
|
+
* Algorithm:
|
|
373
|
+
* 1. Hot path — resolve `refs/remotes/origin/<branch>` (caller is
|
|
374
|
+
* responsible for an upstream-fresh ref; the launcher and updater
|
|
375
|
+
* both `git.fetch` before invoking us). If the tip's
|
|
376
|
+
* `package.json#zenbu.host` already satisfies `hostVersion`,
|
|
377
|
+
* return tip.
|
|
378
|
+
* 2. Cold path — deepen the local shallow history geometrically
|
|
379
|
+
* (`git.fetch({depth: nextDepth, relative: false})`), running
|
|
380
|
+
* `git.log({depth: nextDepth})` to enumerate the now-known history.
|
|
381
|
+
* Lower-bound binary search inside that window for the smallest
|
|
382
|
+
* index whose commit is compatible — i.e. the LATEST compatible
|
|
383
|
+
* commit. The search assumes monotonic ranges (newer commits'
|
|
384
|
+
* `zenbu.host` is equal-or-stricter than older commits'); if a
|
|
385
|
+
* project violates this, the resolver may pick a slightly older
|
|
386
|
+
* compatible commit than the absolute latest.
|
|
387
|
+
* 3. If deepen exhausts the branch's history without finding a
|
|
388
|
+
* compatible commit, return `targetSha: null`. The caller decides
|
|
389
|
+
* what to do (the launcher errors out; the updater service surfaces
|
|
390
|
+
* `phase: "incompatible"`).
|
|
391
|
+
*/
|
|
392
|
+
const FIRST_DEEPEN_DEPTH = 16;
|
|
393
|
+
const MAX_DEEPEN_DEPTH = 4096;
|
|
394
|
+
async function readZenbuHostAt(args) {
|
|
395
|
+
try {
|
|
396
|
+
const { blob } = await git.readBlob({
|
|
397
|
+
fs: args.fs,
|
|
398
|
+
dir: args.dir,
|
|
399
|
+
oid: args.commitOid,
|
|
400
|
+
filepath: "package.json"
|
|
401
|
+
});
|
|
402
|
+
const text = Buffer.from(blob).toString("utf8");
|
|
403
|
+
const raw = JSON.parse(text).zenbu?.host;
|
|
404
|
+
if (typeof raw !== "string" || raw.trim().length === 0) return {
|
|
405
|
+
range: null,
|
|
406
|
+
missing: true
|
|
407
|
+
};
|
|
408
|
+
return {
|
|
409
|
+
range: raw.trim(),
|
|
410
|
+
missing: false
|
|
411
|
+
};
|
|
412
|
+
} catch {
|
|
413
|
+
return {
|
|
414
|
+
range: null,
|
|
415
|
+
missing: true
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function isCompatible(hostVersion, range) {
|
|
420
|
+
if (!range) return true;
|
|
421
|
+
try {
|
|
422
|
+
return semver.satisfies(hostVersion, range, { includePrerelease: true });
|
|
423
|
+
} catch {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async function resolveTargetSha(input) {
|
|
428
|
+
const { fs, http, dir, mirror, hostVersion, reporter = null, signal } = input;
|
|
429
|
+
const remoteRef = `refs/remotes/origin/${mirror.branch}`;
|
|
430
|
+
reporter?.step?.("resolve", `Resolving compatible commit (host=${hostVersion})`);
|
|
431
|
+
const tipSha = await git.resolveRef({
|
|
432
|
+
fs,
|
|
433
|
+
dir,
|
|
434
|
+
ref: remoteRef
|
|
435
|
+
});
|
|
436
|
+
let consideredCommits = 1;
|
|
437
|
+
const tipInfo = await readZenbuHostAt({
|
|
438
|
+
fs,
|
|
439
|
+
dir,
|
|
440
|
+
commitOid: tipSha
|
|
441
|
+
});
|
|
442
|
+
if (isCompatible(hostVersion, tipInfo.range)) {
|
|
443
|
+
reporter?.message?.(`[resolve] tip ${tipSha.slice(0, 7)} ` + (tipInfo.range ? `range="${tipInfo.range}" ok for host=${hostVersion}` : `no zenbu.host declared; assuming compatible`));
|
|
444
|
+
reporter?.done?.("resolve");
|
|
445
|
+
return {
|
|
446
|
+
targetSha: tipSha,
|
|
447
|
+
tipSha,
|
|
448
|
+
consideredCommits,
|
|
449
|
+
exhaustedHistory: false
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
reporter?.message?.(`[resolve] tip ${tipSha.slice(0, 7)} range="${tipInfo.range}" does not satisfy host=${hostVersion}; deepening history...`);
|
|
453
|
+
let depth = FIRST_DEEPEN_DEPTH;
|
|
454
|
+
let lastSeen = 1;
|
|
455
|
+
let exhausted = false;
|
|
456
|
+
while (depth <= MAX_DEEPEN_DEPTH) {
|
|
457
|
+
if (signal?.aborted) throw new Error("resolveTargetSha aborted");
|
|
458
|
+
reporter?.progress?.({
|
|
459
|
+
phase: "resolve",
|
|
460
|
+
loaded: depth,
|
|
461
|
+
total: MAX_DEEPEN_DEPTH
|
|
462
|
+
});
|
|
463
|
+
try {
|
|
464
|
+
await git.fetch({
|
|
465
|
+
fs,
|
|
466
|
+
http,
|
|
467
|
+
dir,
|
|
468
|
+
url: mirror.url,
|
|
469
|
+
ref: mirror.branch,
|
|
470
|
+
singleBranch: true,
|
|
471
|
+
depth,
|
|
472
|
+
relative: false,
|
|
473
|
+
tags: false
|
|
474
|
+
});
|
|
475
|
+
} catch (err) {
|
|
476
|
+
reporter?.message?.(`[resolve] deepen depth=${depth} failed: ${err.message}`);
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
const log = await git.log({
|
|
480
|
+
fs,
|
|
481
|
+
dir,
|
|
482
|
+
ref: remoteRef,
|
|
483
|
+
depth
|
|
484
|
+
});
|
|
485
|
+
if (log.length <= lastSeen) {
|
|
486
|
+
exhausted = true;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
lastSeen = log.length;
|
|
490
|
+
consideredCommits = log.length;
|
|
491
|
+
const oids = log.map((c) => c.oid);
|
|
492
|
+
let lo = 0;
|
|
493
|
+
let hi = oids.length;
|
|
494
|
+
while (lo < hi) {
|
|
495
|
+
if (signal?.aborted) throw new Error("resolveTargetSha aborted");
|
|
496
|
+
const mid = lo + hi >>> 1;
|
|
497
|
+
if (isCompatible(hostVersion, (await readZenbuHostAt({
|
|
498
|
+
fs,
|
|
499
|
+
dir,
|
|
500
|
+
commitOid: oids[mid]
|
|
501
|
+
})).range)) hi = mid;
|
|
502
|
+
else lo = mid + 1;
|
|
503
|
+
}
|
|
504
|
+
if (lo < oids.length) {
|
|
505
|
+
const targetSha = oids[lo];
|
|
506
|
+
reporter?.message?.(`[resolve] picked ${targetSha.slice(0, 7)} (${oids.length - lo} commits behind tip; host=${hostVersion} ok)`);
|
|
507
|
+
reporter?.done?.("resolve");
|
|
508
|
+
return {
|
|
509
|
+
targetSha,
|
|
510
|
+
tipSha,
|
|
511
|
+
consideredCommits,
|
|
512
|
+
exhaustedHistory: false
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
depth *= 2;
|
|
516
|
+
}
|
|
517
|
+
reporter?.message?.(`[resolve] no compatible commit found in ${consideredCommits} commits on origin/${mirror.branch} (host=${hostVersion})`);
|
|
518
|
+
reporter?.error?.({
|
|
519
|
+
id: "resolve",
|
|
520
|
+
message: `no commit on origin/${mirror.branch} declares zenbu.host that satisfies host=${hostVersion}`
|
|
521
|
+
});
|
|
522
|
+
return {
|
|
523
|
+
targetSha: null,
|
|
524
|
+
tipSha,
|
|
525
|
+
consideredCommits,
|
|
526
|
+
exhaustedHistory: exhausted
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
//#endregion
|
|
530
|
+
//#region src/services/updater.ts
|
|
531
|
+
/**
|
|
532
|
+
* In-process source updater. Pulls the configured mirror via
|
|
533
|
+
* `isomorphic-git`, finds the newest commit whose
|
|
534
|
+
* `package.json#zenbu.host` semver range still satisfies the running
|
|
535
|
+
* .app's `host.json#version`, applies it with HMR paused, and re-runs
|
|
536
|
+
* the bundled package manager when the lockfile changes.
|
|
537
|
+
*
|
|
538
|
+
* Boundary contract:
|
|
539
|
+
* - In dev (`ZENBU_LAUNCHED_FROM_BUNDLE !== "1"` or no `app-config.json`),
|
|
540
|
+
* every action returns `{ status: "dev-mode" }` immediately. We
|
|
541
|
+
* never touch git or run an install in `pnpm dev`.
|
|
542
|
+
* - In production, all heavy work happens inside `update()`; `check()`
|
|
543
|
+
* does fetch + resolve but never modifies the working tree.
|
|
544
|
+
*
|
|
545
|
+
* Companion: `packages/core/src/launcher.ts` runs the SAME range
|
|
546
|
+
* resolver (`shared/range-resolver.ts`) at every launch, so first
|
|
547
|
+
* launches and updater-driven updates always agree on the target sha.
|
|
548
|
+
*/
|
|
549
|
+
var updater_exports = /* @__PURE__ */ __exportAll({ UpdaterService: () => UpdaterService });
|
|
550
|
+
const log = createLogger("updater");
|
|
551
|
+
const LEGACY_PACKAGE_MANAGER = {
|
|
552
|
+
type: "pnpm",
|
|
553
|
+
version: "10.33.0"
|
|
554
|
+
};
|
|
555
|
+
function isBundleMode() {
|
|
556
|
+
return process.env.ZENBU_LAUNCHED_FROM_BUNDLE === "1";
|
|
557
|
+
}
|
|
558
|
+
function readAppConfig(appPath) {
|
|
559
|
+
const configPath = path.join(appPath, "app-config.json");
|
|
560
|
+
try {
|
|
561
|
+
return JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
562
|
+
} catch {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function appsDirFor(name) {
|
|
567
|
+
if (process.env.ZENBU_APPS_DIR) return path.resolve(process.env.ZENBU_APPS_DIR);
|
|
568
|
+
return path.join(os.homedir(), ".zenbu", "apps", name);
|
|
569
|
+
}
|
|
570
|
+
async function hasLocalModifications(dir) {
|
|
571
|
+
try {
|
|
572
|
+
return (await git.statusMatrix({
|
|
573
|
+
fs,
|
|
574
|
+
dir
|
|
575
|
+
})).some(([, head, workdir, stage]) => head !== 1 || workdir !== 1 || stage !== 1);
|
|
576
|
+
} catch {
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async function tryResolveHead(dir) {
|
|
581
|
+
try {
|
|
582
|
+
return await git.resolveRef({
|
|
583
|
+
fs,
|
|
584
|
+
dir,
|
|
585
|
+
ref: "HEAD"
|
|
586
|
+
});
|
|
587
|
+
} catch {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
var UpdaterService = class extends Service.create({ key: "updater" }) {
|
|
592
|
+
status = { phase: "idle" };
|
|
593
|
+
listeners = /* @__PURE__ */ new Set();
|
|
594
|
+
inflight = null;
|
|
595
|
+
pollDisposers = /* @__PURE__ */ new Set();
|
|
596
|
+
evaluate() {
|
|
597
|
+
this.status = { phase: "idle" };
|
|
598
|
+
this.setup("updater-cancel", () => {
|
|
599
|
+
return () => {
|
|
600
|
+
this.inflight?.abort();
|
|
601
|
+
this.inflight = null;
|
|
602
|
+
for (const dispose of this.pollDisposers) try {
|
|
603
|
+
dispose();
|
|
604
|
+
} catch {}
|
|
605
|
+
this.pollDisposers.clear();
|
|
606
|
+
};
|
|
607
|
+
});
|
|
608
|
+
log.verbose(`updater service ready (mode=${isBundleMode() ? "bundle" : "dev"})`);
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Returns the current `UpdaterPhase`. Cheap; safe to poll from the
|
|
612
|
+
* renderer at ~250ms cadence to drive a progress UI while
|
|
613
|
+
* `update()` runs.
|
|
614
|
+
*/
|
|
615
|
+
async getStatus() {
|
|
616
|
+
return this.status;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Synchronous-ish snapshot of the updater's environment. Doesn't
|
|
620
|
+
* touch the network. `currentSha` is read off-disk so very recent
|
|
621
|
+
* checkouts may lag by a few ms; callers that care about exactness
|
|
622
|
+
* should call `check()` first.
|
|
623
|
+
*/
|
|
624
|
+
async getInfo() {
|
|
625
|
+
if (!isBundleMode()) return {
|
|
626
|
+
mode: "dev",
|
|
627
|
+
appsDir: null,
|
|
628
|
+
hostVersion: null,
|
|
629
|
+
currentSha: null,
|
|
630
|
+
mirror: null,
|
|
631
|
+
packageManager: null
|
|
632
|
+
};
|
|
633
|
+
const ctx = this.loadContext();
|
|
634
|
+
if (!ctx) return {
|
|
635
|
+
mode: "bundle",
|
|
636
|
+
appsDir: null,
|
|
637
|
+
hostVersion: null,
|
|
638
|
+
currentSha: null,
|
|
639
|
+
mirror: null,
|
|
640
|
+
packageManager: null
|
|
641
|
+
};
|
|
642
|
+
const currentSha = await tryResolveHead(ctx.appsDir);
|
|
643
|
+
return {
|
|
644
|
+
mode: "bundle",
|
|
645
|
+
appsDir: ctx.appsDir,
|
|
646
|
+
hostVersion: ctx.hostVersion,
|
|
647
|
+
currentSha,
|
|
648
|
+
mirror: ctx.mirror,
|
|
649
|
+
packageManager: ctx.packageManager
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Fetch + resolve target sha without touching the working tree. Use
|
|
654
|
+
* this for "is there an update?" UI. Cheap enough to call on a timer.
|
|
655
|
+
*/
|
|
656
|
+
async check() {
|
|
657
|
+
if (!isBundleMode()) return {
|
|
658
|
+
status: "dev-mode",
|
|
659
|
+
currentSha: null,
|
|
660
|
+
targetSha: null,
|
|
661
|
+
tipSha: null,
|
|
662
|
+
hasUpdate: false,
|
|
663
|
+
incompatibleHead: false
|
|
664
|
+
};
|
|
665
|
+
const ctx = this.loadContext();
|
|
666
|
+
if (!ctx) return {
|
|
667
|
+
status: ctx === null ? "no-mirror" : "no-host-version",
|
|
668
|
+
currentSha: null,
|
|
669
|
+
targetSha: null,
|
|
670
|
+
tipSha: null,
|
|
671
|
+
hasUpdate: false,
|
|
672
|
+
incompatibleHead: false,
|
|
673
|
+
error: "missing app-config.json or host.json (rebuild .app)"
|
|
674
|
+
};
|
|
675
|
+
this.setStatus({ phase: "checking" });
|
|
676
|
+
const reporter = this.makeReporter("checking");
|
|
677
|
+
const ac = new AbortController();
|
|
678
|
+
try {
|
|
679
|
+
await git.fetch({
|
|
680
|
+
fs,
|
|
681
|
+
http,
|
|
682
|
+
dir: ctx.appsDir,
|
|
683
|
+
url: ctx.mirror.url,
|
|
684
|
+
ref: ctx.mirror.branch,
|
|
685
|
+
singleBranch: true,
|
|
686
|
+
depth: 1,
|
|
687
|
+
tags: false,
|
|
688
|
+
onProgress: (e) => this.setStatus({
|
|
689
|
+
phase: "fetching",
|
|
690
|
+
loaded: e.loaded,
|
|
691
|
+
total: e.total,
|
|
692
|
+
ratio: e.total ? e.loaded / e.total : void 0
|
|
693
|
+
})
|
|
694
|
+
});
|
|
695
|
+
const result = await resolveTargetSha({
|
|
696
|
+
fs,
|
|
697
|
+
http,
|
|
698
|
+
dir: ctx.appsDir,
|
|
699
|
+
mirror: ctx.mirror,
|
|
700
|
+
hostVersion: ctx.hostVersion,
|
|
701
|
+
reporter,
|
|
702
|
+
signal: ac.signal
|
|
703
|
+
});
|
|
704
|
+
const currentSha = await tryResolveHead(ctx.appsDir);
|
|
705
|
+
const incompatibleHead = result.targetSha !== result.tipSha;
|
|
706
|
+
this.setStatus({ phase: "idle" });
|
|
707
|
+
return {
|
|
708
|
+
status: "ok",
|
|
709
|
+
currentSha,
|
|
710
|
+
targetSha: result.targetSha,
|
|
711
|
+
tipSha: result.tipSha,
|
|
712
|
+
hasUpdate: result.targetSha !== null && result.targetSha !== currentSha,
|
|
713
|
+
incompatibleHead
|
|
714
|
+
};
|
|
715
|
+
} catch (err) {
|
|
716
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
717
|
+
this.setStatus({
|
|
718
|
+
phase: "error",
|
|
719
|
+
message
|
|
720
|
+
});
|
|
721
|
+
return {
|
|
722
|
+
status: "error",
|
|
723
|
+
currentSha: null,
|
|
724
|
+
targetSha: null,
|
|
725
|
+
tipSha: null,
|
|
726
|
+
hasUpdate: false,
|
|
727
|
+
incompatibleHead: false,
|
|
728
|
+
error: message
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Apply the latest compatible commit. Pauses HMR while git operations
|
|
734
|
+
* and `<pm> install` run, so the renderer never sees a half-applied
|
|
735
|
+
* tree. Refuses when the apps-dir has uncommitted edits.
|
|
736
|
+
*/
|
|
737
|
+
async update(opts) {
|
|
738
|
+
if (this.inflight) return { status: "in-progress" };
|
|
739
|
+
if (!isBundleMode()) return { status: "dev-mode" };
|
|
740
|
+
const ctx = this.loadContext();
|
|
741
|
+
if (!ctx) return {
|
|
742
|
+
status: "no-mirror",
|
|
743
|
+
error: "missing app-config.json or host.json (rebuild .app)"
|
|
744
|
+
};
|
|
745
|
+
const ac = this.inflight = new AbortController();
|
|
746
|
+
const release = pauseWatcherPath(ctx.appsDir);
|
|
747
|
+
let releaseCalled = false;
|
|
748
|
+
const safeRelease = () => {
|
|
749
|
+
if (releaseCalled) return;
|
|
750
|
+
releaseCalled = true;
|
|
751
|
+
try {
|
|
752
|
+
release();
|
|
753
|
+
} catch {}
|
|
754
|
+
};
|
|
755
|
+
try {
|
|
756
|
+
if (await hasLocalModifications(ctx.appsDir)) {
|
|
757
|
+
this.setStatus({ phase: "idle" });
|
|
758
|
+
return {
|
|
759
|
+
status: "dirty-tree",
|
|
760
|
+
error: `uncommitted changes in ${ctx.appsDir}; refusing to update`
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
const reporter = this.makeReporter("update");
|
|
764
|
+
this.setStatus({ phase: "fetching" });
|
|
765
|
+
await git.fetch({
|
|
766
|
+
fs,
|
|
767
|
+
http,
|
|
768
|
+
dir: ctx.appsDir,
|
|
769
|
+
url: ctx.mirror.url,
|
|
770
|
+
ref: ctx.mirror.branch,
|
|
771
|
+
singleBranch: true,
|
|
772
|
+
depth: 1,
|
|
773
|
+
tags: false,
|
|
774
|
+
onProgress: (e) => this.setStatus({
|
|
775
|
+
phase: "fetching",
|
|
776
|
+
loaded: e.loaded,
|
|
777
|
+
total: e.total,
|
|
778
|
+
ratio: e.total ? e.loaded / e.total : void 0
|
|
779
|
+
})
|
|
780
|
+
});
|
|
781
|
+
this.setStatus({
|
|
782
|
+
phase: "resolving",
|
|
783
|
+
depthConsidered: 1
|
|
784
|
+
});
|
|
785
|
+
const resolution = await resolveTargetSha({
|
|
786
|
+
fs,
|
|
787
|
+
http,
|
|
788
|
+
dir: ctx.appsDir,
|
|
789
|
+
mirror: ctx.mirror,
|
|
790
|
+
hostVersion: ctx.hostVersion,
|
|
791
|
+
reporter,
|
|
792
|
+
signal: ac.signal
|
|
793
|
+
});
|
|
794
|
+
if (resolution.targetSha === null) {
|
|
795
|
+
const message = `no commit on origin/${ctx.mirror.branch} declares zenbu.host that satisfies host=${ctx.hostVersion}`;
|
|
796
|
+
this.setStatus({
|
|
797
|
+
phase: "incompatible",
|
|
798
|
+
remoteHead: resolution.tipSha,
|
|
799
|
+
reason: message
|
|
800
|
+
});
|
|
801
|
+
return {
|
|
802
|
+
status: "incompatible",
|
|
803
|
+
error: message
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
const currentSha = await tryResolveHead(ctx.appsDir);
|
|
807
|
+
if (currentSha === resolution.targetSha) {
|
|
808
|
+
this.setStatus({
|
|
809
|
+
phase: "up-to-date",
|
|
810
|
+
sha: resolution.targetSha
|
|
811
|
+
});
|
|
812
|
+
return {
|
|
813
|
+
status: "up-to-date",
|
|
814
|
+
appliedSha: resolution.targetSha
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
if (opts?.dryRun) {
|
|
818
|
+
this.setStatus({ phase: "idle" });
|
|
819
|
+
return {
|
|
820
|
+
status: "applied",
|
|
821
|
+
appliedSha: resolution.targetSha,
|
|
822
|
+
reranInstall: false
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
log.verbose(`checking out ${resolution.targetSha.slice(0, 7)} (was ${currentSha?.slice(0, 7) ?? "?"})`);
|
|
826
|
+
await git.checkout({
|
|
827
|
+
fs,
|
|
828
|
+
dir: ctx.appsDir,
|
|
829
|
+
ref: resolution.targetSha,
|
|
830
|
+
force: true
|
|
831
|
+
});
|
|
832
|
+
const nextSig = await depsSignature(ctx.appsDir, ctx.packageManager);
|
|
833
|
+
const prevSig = await readDepsSig(ctx.appsDir);
|
|
834
|
+
let reranInstall = false;
|
|
835
|
+
if (prevSig !== nextSig) {
|
|
836
|
+
log.verbose(`lockfile changed; running ${ctx.packageManager.type}@${ctx.packageManager.version}`);
|
|
837
|
+
this.setStatus({
|
|
838
|
+
phase: "installing",
|
|
839
|
+
pm: `${ctx.packageManager.type}@${ctx.packageManager.version}`
|
|
840
|
+
});
|
|
841
|
+
await runInstall({
|
|
842
|
+
appsDir: ctx.appsDir,
|
|
843
|
+
resourcesPath: ctx.resourcesPath,
|
|
844
|
+
pm: ctx.packageManager,
|
|
845
|
+
reporter: this.makeInstallReporter(ctx.packageManager),
|
|
846
|
+
signal: ac.signal
|
|
847
|
+
});
|
|
848
|
+
await writeDepsSig(ctx.appsDir, nextSig);
|
|
849
|
+
reranInstall = true;
|
|
850
|
+
} else log.verbose("lockfile unchanged; skipping install");
|
|
851
|
+
this.setStatus({
|
|
852
|
+
phase: "applied",
|
|
853
|
+
sha: resolution.targetSha,
|
|
854
|
+
reranInstall
|
|
855
|
+
});
|
|
856
|
+
return {
|
|
857
|
+
status: "applied",
|
|
858
|
+
appliedSha: resolution.targetSha,
|
|
859
|
+
reranInstall
|
|
860
|
+
};
|
|
861
|
+
} catch (err) {
|
|
862
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
863
|
+
log.error(`update failed: ${message}`);
|
|
864
|
+
this.setStatus({
|
|
865
|
+
phase: "error",
|
|
866
|
+
message
|
|
867
|
+
});
|
|
868
|
+
return {
|
|
869
|
+
status: "error",
|
|
870
|
+
error: message
|
|
871
|
+
};
|
|
872
|
+
} finally {
|
|
873
|
+
safeRelease();
|
|
874
|
+
if (this.inflight === ac) this.inflight = null;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Subscribe to phase transitions. Fires synchronously with the
|
|
879
|
+
* current status on attach so callers can render initial state
|
|
880
|
+
* without a separate read.
|
|
881
|
+
*/
|
|
882
|
+
onChange(cb) {
|
|
883
|
+
this.listeners.add(cb);
|
|
884
|
+
try {
|
|
885
|
+
cb(this.status);
|
|
886
|
+
} catch (err) {
|
|
887
|
+
log.warn(`updater listener threw: ${err.message}`);
|
|
888
|
+
}
|
|
889
|
+
return () => {
|
|
890
|
+
this.listeners.delete(cb);
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Convenience polling loop. Calls `check()` every `intervalMs` and,
|
|
895
|
+
* when `autoApply` is set, invokes `update()` whenever a new
|
|
896
|
+
* compatible target is found. Returns a disposer.
|
|
897
|
+
*/
|
|
898
|
+
startPolling(intervalMs, opts) {
|
|
899
|
+
if (!Number.isFinite(intervalMs) || intervalMs < 1e3) throw new Error(`[updater] startPolling requires intervalMs >= 1000 (got ${intervalMs})`);
|
|
900
|
+
let cancelled = false;
|
|
901
|
+
let timer = null;
|
|
902
|
+
const tick = async () => {
|
|
903
|
+
if (cancelled) return;
|
|
904
|
+
try {
|
|
905
|
+
const result = await this.check();
|
|
906
|
+
if (opts?.autoApply && result.status === "ok" && result.hasUpdate && result.targetSha) await this.update();
|
|
907
|
+
} catch (err) {
|
|
908
|
+
log.warn(`updater poll failed: ${err.message}`);
|
|
909
|
+
} finally {
|
|
910
|
+
if (!cancelled) {
|
|
911
|
+
timer = setTimeout(tick, intervalMs);
|
|
912
|
+
timer.unref?.();
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
timer = setTimeout(tick, intervalMs);
|
|
917
|
+
timer.unref?.();
|
|
918
|
+
const dispose = () => {
|
|
919
|
+
cancelled = true;
|
|
920
|
+
if (timer) {
|
|
921
|
+
clearTimeout(timer);
|
|
922
|
+
timer = null;
|
|
923
|
+
}
|
|
924
|
+
this.pollDisposers.delete(dispose);
|
|
925
|
+
};
|
|
926
|
+
this.pollDisposers.add(dispose);
|
|
927
|
+
return dispose;
|
|
928
|
+
}
|
|
929
|
+
loadContext() {
|
|
930
|
+
const appPath = app.getAppPath();
|
|
931
|
+
const cfg = readAppConfig(appPath);
|
|
932
|
+
if (!cfg || !cfg.mirrorUrl) return null;
|
|
933
|
+
const host = tryReadHostVersion(appPath);
|
|
934
|
+
if (!host) return null;
|
|
935
|
+
return {
|
|
936
|
+
appsDir: appsDirFor(cfg.name),
|
|
937
|
+
resourcesPath: path.dirname(appPath),
|
|
938
|
+
mirror: {
|
|
939
|
+
url: cfg.mirrorUrl,
|
|
940
|
+
branch: cfg.branch ?? "main"
|
|
941
|
+
},
|
|
942
|
+
packageManager: cfg.packageManager ?? LEGACY_PACKAGE_MANAGER,
|
|
943
|
+
hostVersion: host.version,
|
|
944
|
+
appName: cfg.name
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
setStatus(next) {
|
|
948
|
+
this.status = next;
|
|
949
|
+
for (const cb of this.listeners) try {
|
|
950
|
+
cb(next);
|
|
951
|
+
} catch (err) {
|
|
952
|
+
log.warn(`updater listener threw: ${err.message}`);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
makeReporter(_label) {
|
|
956
|
+
return {
|
|
957
|
+
step: (id, _label2) => {
|
|
958
|
+
if (id === "resolve") this.setStatus({
|
|
959
|
+
phase: "resolving",
|
|
960
|
+
depthConsidered: 1
|
|
961
|
+
});
|
|
962
|
+
},
|
|
963
|
+
progress: (p) => {
|
|
964
|
+
const current = this.status;
|
|
965
|
+
if (current.phase === "resolving") this.setStatus({
|
|
966
|
+
phase: "resolving",
|
|
967
|
+
depthConsidered: p.loaded ?? current.depthConsidered
|
|
968
|
+
});
|
|
969
|
+
else if (current.phase === "fetching") this.setStatus({
|
|
970
|
+
phase: "fetching",
|
|
971
|
+
loaded: p.loaded,
|
|
972
|
+
total: p.total,
|
|
973
|
+
ratio: p.ratio
|
|
974
|
+
});
|
|
975
|
+
},
|
|
976
|
+
message: (text) => log.verbose(text),
|
|
977
|
+
error: ({ message }) => log.warn(`reporter error: ${message}`)
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
makeInstallReporter(pm) {
|
|
981
|
+
const pmLabel = `${pm.type}@${pm.version}`;
|
|
982
|
+
return {
|
|
983
|
+
message: (line) => {
|
|
984
|
+
const cur = this.status;
|
|
985
|
+
if (cur.phase === "installing") this.setStatus({
|
|
986
|
+
...cur,
|
|
987
|
+
line
|
|
988
|
+
});
|
|
989
|
+
},
|
|
990
|
+
progress: (p) => {
|
|
991
|
+
const cur = this.status;
|
|
992
|
+
if (cur.phase === "installing") this.setStatus({
|
|
993
|
+
...cur,
|
|
994
|
+
pm: pmLabel,
|
|
995
|
+
loaded: p.loaded,
|
|
996
|
+
total: p.total,
|
|
997
|
+
ratio: p.ratio
|
|
998
|
+
});
|
|
999
|
+
},
|
|
1000
|
+
step: () => {},
|
|
1001
|
+
done: () => {},
|
|
1002
|
+
error: ({ message }) => log.warn(`install reporter error: ${message}`)
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
runtime.register(UpdaterService, import.meta);
|
|
1007
|
+
//#endregion
|
|
1008
|
+
export { updater_exports as n, UpdaterService as t };
|