@zenbujs/core 0.0.8 → 0.0.12

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.
Files changed (82) hide show
  1. package/dist/advice-config-BiYhyeTz.d.mts +41 -0
  2. package/dist/advice.d.mts +2 -36
  3. package/dist/advice.mjs +2 -2
  4. package/dist/{base-window-BxBZ2md_.mjs → base-window-4P-fVvC_.mjs} +37 -26
  5. package/dist/{build-config-Dzg2frpk.d.mts → build-config-GF0XzR_Y.d.mts} +42 -18
  6. package/dist/{build-config-pWdmLnrk.mjs → build-config-HMMqpXI1.mjs} +0 -8
  7. package/dist/{build-electron-Dsbb1EMl.mjs → build-electron-Di_FE62r.mjs} +10 -6
  8. package/dist/{build-source-d1J3shV8.mjs → build-source-BIaWpaxE.mjs} +2 -2
  9. package/dist/cli/bin.mjs +7 -7
  10. package/dist/cli/build.d.mts +1 -1
  11. package/dist/cli/build.mjs +1 -1
  12. package/dist/cli/resolve-config.mjs +6 -1
  13. package/dist/{cli-kL6mPgBE.mjs → cli-5jFDJWM4.mjs} +4 -4
  14. package/dist/config.d.mts +3 -3
  15. package/dist/config.mjs +2 -2
  16. package/dist/{db-Bc292RYo.mjs → db-MkOccvBS.mjs} +2 -2
  17. package/dist/db.d.mts +3 -2
  18. package/dist/db.mjs +2 -10
  19. package/dist/{dev-B2emj0HZ.mjs → dev-BSDyzO4j.mjs} +3 -9
  20. package/dist/env-bootstrap.d.mts +1 -1
  21. package/dist/events.d.mts +0 -9
  22. package/dist/{host-version-BIrF8tX7.mjs → host-version-Cog_odmD.mjs} +4 -3
  23. package/dist/{index-w5QyDjuf.d.mts → index-C0mXKol5.d.mts} +189 -141
  24. package/dist/{index-DeDxePAa.d.mts → index-FaexRVl_.d.mts} +13 -1
  25. package/dist/index.d.mts +4 -4
  26. package/dist/index.mjs +2 -2
  27. package/dist/launcher.mjs +64 -6
  28. package/dist/link-Bt3LB_NW.mjs +586 -0
  29. package/dist/{load-config-C4Oe2qZO.mjs → load-config-C2XloBaQ.mjs} +68 -5
  30. package/dist/node-loader.mjs +1 -1
  31. package/dist/{publish-source-Dq2c0iOw.mjs → publish-source-v93eB9kA.mjs} +6 -2
  32. package/dist/react.d.mts +6 -6
  33. package/dist/react.mjs +4 -4
  34. package/dist/registry-generated.d.mts +19 -14
  35. package/dist/registry-saQDMUhT.d.mts +13 -0
  36. package/dist/registry.d.mts +1 -1
  37. package/dist/{reloader-B22UiNA2.mjs → reloader-CFzxYa67.mjs} +3 -3
  38. package/dist/{renderer-host-DD16MXhI.mjs → renderer-host-Cw38dSDe.mjs} +35 -24
  39. package/dist/{rpc-C4_NQmpT.mjs → rpc-Dg9zwZ33.mjs} +4 -4
  40. package/dist/rpc.d.mts +1 -1
  41. package/dist/rpc.mjs +1 -1
  42. package/dist/runtime-DYUONc3S.mjs +861 -0
  43. package/dist/{runtime-BQWntcOb.d.mts → runtime-fnPDZFYM.d.mts} +100 -3
  44. package/dist/runtime.d.mts +2 -2
  45. package/dist/runtime.mjs +2 -578
  46. package/dist/{schema-CjrMVk36.d.mts → schema-brYpUjYO.d.mts} +13 -25
  47. package/dist/schema.d.mts +2 -2
  48. package/dist/schema.mjs +9 -2
  49. package/dist/{server-CZLMF8Dj.mjs → server-BJ2ZC2z2.mjs} +2 -2
  50. package/dist/services/default.d.mts +1 -5
  51. package/dist/services/default.mjs +12 -16
  52. package/dist/services/index.d.mts +1 -1
  53. package/dist/services/index.mjs +7 -7
  54. package/dist/setup-gate.d.mts +1 -1
  55. package/dist/setup-gate.mjs +25 -11
  56. package/dist/{transport-F2hv_OEm.mjs → transport-Bqlv9pmJ.mjs} +1 -1
  57. package/dist/updater-Bs1Jtem6.mjs +480 -0
  58. package/dist/{vite-plugins-tt6KAtyE.mjs → vite-plugins-Df-cfldF.mjs} +2 -49
  59. package/dist/vite.d.mts +0 -5
  60. package/dist/vite.mjs +1 -1
  61. package/dist/{window-YFKvAM0l.mjs → window-DgB70qeZ.mjs} +113 -22
  62. package/dist/{write-DgIRjo23.mjs → write-7IfKa_nq.mjs} +1 -1
  63. package/dist/zenbu-bg-parse-CIyPkJOY.mjs +46 -0
  64. package/package.json +19 -18
  65. package/LICENSE +0 -11
  66. package/dist/advice-config-DXSIo0sg.mjs +0 -154
  67. package/dist/link-glX89NV5.mjs +0 -673
  68. package/dist/registry-CMp8FYgS.d.mts +0 -47
  69. package/dist/updater-DCkz9M1c.mjs +0 -1008
  70. /package/dist/{config-BK78JDRI.mjs → config-DfciRzDu.mjs} +0 -0
  71. /package/dist/{env-bootstrap-rTs8KR3-.d.mts → env-bootstrap-UBug-4Kw.d.mts} +0 -0
  72. /package/dist/{index-C-ALz_SH.d.mts → index-CSMHYi3u.d.mts} +0 -0
  73. /package/dist/{index-ClXLQ1fw.d.mts → index-DJOHDG5e.d.mts} +0 -0
  74. /package/dist/{log-6rzaCV0I.mjs → log-BkRqDwwB.mjs} +0 -0
  75. /package/dist/{mirror-sync-pYU6f3-c.mjs → mirror-sync-snqh9kEp.mjs} +0 -0
  76. /package/dist/{monorepo-Dct-kkbQ.mjs → monorepo-CBzK3l2i.mjs} +0 -0
  77. /package/dist/{node-BhfLKYCi.mjs → node-BuHlEsE4.mjs} +0 -0
  78. /package/dist/{schema-Ca7SxXgS.mjs → schema-C6k0SroY.mjs} +0 -0
  79. /package/dist/{setup-gate-BQq0QgZH.d.mts → setup-gate-DkysEZQO.d.mts} +0 -0
  80. /package/dist/{src-Cven45mq.mjs → src-BpZAt9zL.mjs} +0 -0
  81. /package/dist/{trace-BaVg0rnY.mjs → trace-BVcQSD59.mjs} +0 -0
  82. /package/dist/{transform-BzrwkEdf.mjs → transform-czrcGnVV.mjs} +0 -0
@@ -1,1008 +0,0 @@
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 };