@dimina-kit/devtools 0.4.0-dev.20260718085557 → 0.4.0-dev.20260718143821

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 (30) hide show
  1. package/dist/main/app/app.d.ts +10 -1
  2. package/dist/main/app/app.js +11 -1
  3. package/dist/main/app/lifecycle.d.ts +12 -1
  4. package/dist/main/app/lifecycle.js +18 -1
  5. package/dist/main/index.bundle.js +326 -35
  6. package/dist/main/index.js +49 -1
  7. package/dist/main/runtime/devtools-backend-before-quit.harness.d.ts +61 -0
  8. package/dist/main/runtime/devtools-backend-before-quit.harness.js +356 -0
  9. package/dist/main/runtime/devtools-backend.js +49 -4
  10. package/dist/main/services/mcp/server.js +24 -2
  11. package/dist/main/services/projects/create-project-service.js +10 -3
  12. package/dist/main/services/update/github-release-checker.js +22 -3
  13. package/dist/main/services/update/update-manager.d.ts +4 -1
  14. package/dist/main/services/update/update-manager.js +30 -5
  15. package/dist/native-host/common/common.js +154 -58
  16. package/dist/native-host/container/pageFrame.css +1 -1
  17. package/dist/native-host/render/render.js +6578 -3291
  18. package/dist/native-host/service/service.js +2 -2
  19. package/dist/renderer/assets/{index-U9F8MrtV.js → index-DfDIvgvK.js} +2 -2
  20. package/dist/renderer/entries/main/index.html +1 -1
  21. package/dist/service-host/sync-impls/menu-button-geometry.js +15 -5
  22. package/dist/service-host/sync-impls/menu-button.js +5 -3
  23. package/dist/simulator/assets/device-shell-Cs-rmP1f.js +2 -0
  24. package/dist/simulator/assets/device-shell-DOy53Y0s.css +1 -0
  25. package/dist/simulator/assets/{simulator-Cy8c-eA2.js → simulator-BG-5CADK.js} +3 -3
  26. package/dist/simulator/device-shell/menu-button-geometry.js +15 -5
  27. package/dist/simulator/simulator.html +1 -1
  28. package/package.json +6 -6
  29. package/dist/simulator/assets/device-shell-C-HZY3bL.css +0 -1
  30. package/dist/simulator/assets/device-shell-Cu1ZPLfe.js +0 -2
@@ -32,5 +32,14 @@ export declare function runDevtoolsBootstrap(config?: WorkbenchAppConfig): void;
32
32
  * v2 `RuntimeBackend.assemble` reuses the exact same body (parity by shared
33
33
  * implementation, not behavioural re-creation).
34
34
  */
35
- export declare function createDevtoolsRuntime(config?: WorkbenchAppConfig): Promise<WorkbenchAppInstance>;
35
+ export declare function createDevtoolsRuntime(config?: WorkbenchAppConfig,
36
+ /**
37
+ * Fired the instant the `WorkbenchAppInstance` exists — BEFORE `config.onSetup`
38
+ * is awaited below, which may run arbitrarily long host code (including
39
+ * loading the host-toolbar, which opens a live MessagePort). A caller that
40
+ * only learns `instance` from this function's return value would not see it
41
+ * until `onSetup` resolves, leaving a window where an app-quit teardown hook
42
+ * has nothing to dispose yet a live toolbar port already exists.
43
+ */
44
+ onInstanceCreated?: (instance: WorkbenchAppInstance) => void): Promise<WorkbenchAppInstance>;
36
45
  //# sourceMappingURL=app.d.ts.map
@@ -357,7 +357,16 @@ export function runDevtoolsBootstrap(config = {}) {
357
357
  * v2 `RuntimeBackend.assemble` reuses the exact same body (parity by shared
358
358
  * implementation, not behavioural re-creation).
359
359
  */
360
- export async function createDevtoolsRuntime(config = {}) {
360
+ export async function createDevtoolsRuntime(config = {},
361
+ /**
362
+ * Fired the instant the `WorkbenchAppInstance` exists — BEFORE `config.onSetup`
363
+ * is awaited below, which may run arbitrarily long host code (including
364
+ * loading the host-toolbar, which opens a live MessagePort). A caller that
365
+ * only learns `instance` from this function's return value would not see it
366
+ * until `onSetup` resolves, leaving a window where an app-quit teardown hook
367
+ * has nothing to dispose yet a live toolbar port already exists.
368
+ */
369
+ onInstanceCreated) {
361
370
  // Self-gate on Electron readiness: this builds a BrowserWindow immediately, so
362
371
  // it must run after `app.whenReady()`. The framework backend path already
363
372
  // awaited it (idempotent no-op here); this guards any direct caller against
@@ -414,6 +423,7 @@ export async function createDevtoolsRuntime(config = {}) {
414
423
  registerSimulatorApi: (name, handler) => context.registry.add(toDisposable(context.simulatorApis.register(name, handler))),
415
424
  dispose: () => disposeContext(context),
416
425
  };
426
+ onInstanceCreated?.(instance);
417
427
  // Built-in simulator APIs: devtools-supplied wx.* implementations that run
418
428
  // in the main process. Hosts can override any of these in their onSetup by
419
429
  // re-registering the same name (last-write-wins on the Map).
@@ -1,3 +1,14 @@
1
1
  export declare function isAppQuitting(): boolean;
2
- export declare function registerAppLifecycle(): void;
2
+ /**
3
+ * `onBeforeQuit`: invoked synchronously, once per `before-quit`, while
4
+ * Electron's main loop is still fully healthy — i.e. BEFORE `will-quit` and
5
+ * any window/WebContentsView destruction. Callers use this to tear down
6
+ * resources (child WebContentsViews, MessagePorts) that are unsafe to let
7
+ * survive into Chromium's native shutdown sequence, where a JS `'destroyed'`
8
+ * handler closing them can hit an already-torn-down native object. Isolated
9
+ * like `host-toolbar-port-channel.ts`'s `invokeReadyHandler`: a throwing
10
+ * callback must not stop `appIsQuitting` from flipping or escape as an
11
+ * uncaught exception out of Electron's event dispatch.
12
+ */
13
+ export declare function registerAppLifecycle(onBeforeQuit?: () => void): void;
3
14
  //# sourceMappingURL=lifecycle.d.ts.map
@@ -8,7 +8,18 @@ let appIsQuitting = false;
8
8
  export function isAppQuitting() {
9
9
  return appIsQuitting;
10
10
  }
11
- export function registerAppLifecycle() {
11
+ /**
12
+ * `onBeforeQuit`: invoked synchronously, once per `before-quit`, while
13
+ * Electron's main loop is still fully healthy — i.e. BEFORE `will-quit` and
14
+ * any window/WebContentsView destruction. Callers use this to tear down
15
+ * resources (child WebContentsViews, MessagePorts) that are unsafe to let
16
+ * survive into Chromium's native shutdown sequence, where a JS `'destroyed'`
17
+ * handler closing them can hit an already-torn-down native object. Isolated
18
+ * like `host-toolbar-port-channel.ts`'s `invokeReadyHandler`: a throwing
19
+ * callback must not stop `appIsQuitting` from flipping or escape as an
20
+ * uncaught exception out of Electron's event dispatch.
21
+ */
22
+ export function registerAppLifecycle(onBeforeQuit) {
12
23
  app.on('window-all-closed', () => {
13
24
  globalShortcut.unregisterAll();
14
25
  app.quit();
@@ -16,6 +27,12 @@ export function registerAppLifecycle() {
16
27
  app.on('before-quit', () => {
17
28
  appIsQuitting = true;
18
29
  globalShortcut.unregisterAll();
30
+ try {
31
+ onBeforeQuit?.();
32
+ }
33
+ catch (err) {
34
+ console.error('[lifecycle] onBeforeQuit handler threw:', err);
35
+ }
19
36
  });
20
37
  }
21
38
  //# sourceMappingURL=lifecycle.js.map
@@ -893,8 +893,8 @@ function registerProjectFsIpc(ctx) {
893
893
 
894
894
  // src/main/app/app.ts
895
895
  import { app as app15, BrowserWindow as BrowserWindow8, nativeImage, session as session4 } from "electron";
896
- import fs12 from "fs";
897
- import path24 from "path";
896
+ import fs13 from "fs";
897
+ import path25 from "path";
898
898
 
899
899
  // src/main/utils/paths.ts
900
900
  import fs3 from "fs";
@@ -1080,7 +1080,7 @@ var appIsQuitting = false;
1080
1080
  function isAppQuitting() {
1081
1081
  return appIsQuitting;
1082
1082
  }
1083
- function registerAppLifecycle() {
1083
+ function registerAppLifecycle(onBeforeQuit) {
1084
1084
  app4.on("window-all-closed", () => {
1085
1085
  globalShortcut2.unregisterAll();
1086
1086
  app4.quit();
@@ -1088,6 +1088,11 @@ function registerAppLifecycle() {
1088
1088
  app4.on("before-quit", () => {
1089
1089
  appIsQuitting = true;
1090
1090
  globalShortcut2.unregisterAll();
1091
+ try {
1092
+ onBeforeQuit?.();
1093
+ } catch (err2) {
1094
+ console.error("[lifecycle] onBeforeQuit handler threw:", err2);
1095
+ }
1091
1096
  });
1092
1097
  }
1093
1098
 
@@ -4068,8 +4073,8 @@ function createHostToolbarView(ctx, reconciler, deps) {
4068
4073
  hide() {
4069
4074
  hideHostToolbar();
4070
4075
  },
4071
- setPreloadPath(path25) {
4072
- hostToolbarPreloadOverride = path25;
4076
+ setPreloadPath(path26) {
4077
+ hostToolbarPreloadOverride = path26;
4073
4078
  },
4074
4079
  setHeightMode(mode) {
4075
4080
  if (mode !== "auto" && !(Number.isFinite(mode.fixed) && mode.fixed >= 0)) {
@@ -5867,7 +5872,7 @@ async function materializeTemplate(target, template, name) {
5867
5872
  `Template source missing on disk: ${template.source.path}`
5868
5873
  );
5869
5874
  }
5870
- fs7.cpSync(template.source.path, target, {
5875
+ await fs7.promises.cp(template.source.path, target, {
5871
5876
  recursive: true,
5872
5877
  force: true
5873
5878
  });
@@ -10250,7 +10255,6 @@ function startMcpServer(resolvedCdpPort, mcpPort) {
10250
10255
  });
10251
10256
  connectTarget("workbench").catch(() => {
10252
10257
  });
10253
- const server = buildServer();
10254
10258
  const transports = /* @__PURE__ */ new Map();
10255
10259
  const httpServer = createServer(async (req, res) => {
10256
10260
  const url = new URL(req.url ?? "/", `http://localhost`);
@@ -10258,7 +10262,14 @@ function startMcpServer(resolvedCdpPort, mcpPort) {
10258
10262
  const transport = new SSEServerTransport("/message", res);
10259
10263
  transports.set(transport.sessionId, transport);
10260
10264
  res.on("close", () => transports.delete(transport.sessionId));
10261
- await server.connect(transport);
10265
+ try {
10266
+ await buildServer().connect(transport);
10267
+ } catch (err2) {
10268
+ transports.delete(transport.sessionId);
10269
+ if (!res.headersSent) res.writeHead(500).end("MCP connect failed");
10270
+ else res.end();
10271
+ console.error("[MCP] SSE connect failed:", err2);
10272
+ }
10262
10273
  } else if (req.method === "POST" && url.pathname === "/message") {
10263
10274
  const sessionId = url.searchParams.get("sessionId") ?? "";
10264
10275
  const transport = transports.get(sessionId);
@@ -10607,13 +10618,13 @@ function createRenderInspector(options = {}) {
10607
10618
  import { toDisposable as toDisposable6 } from "@dimina-kit/electron-deck/main";
10608
10619
 
10609
10620
  // src/main/services/simulator-temp-files/store.ts
10610
- function registerTempFile(store, path25, mime, bytes) {
10621
+ function registerTempFile(store, path26, mime, bytes) {
10611
10622
  const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(new Uint8Array(bytes));
10612
- store.delete(path25);
10613
- store.set(path25, { bytes: buf, mime });
10623
+ store.delete(path26);
10624
+ store.set(path26, { bytes: buf, mime });
10614
10625
  }
10615
- function revokeTempFile(store, path25) {
10616
- store.delete(path25);
10626
+ function revokeTempFile(store, path26) {
10627
+ store.delete(path26);
10617
10628
  }
10618
10629
  function revokeAllTempFiles(store) {
10619
10630
  store.clear();
@@ -11041,10 +11052,10 @@ function setupSimulatorTempFiles(simSession) {
11041
11052
  const store = /* @__PURE__ */ new Map();
11042
11053
  const pendingWaiters = /* @__PURE__ */ new Map();
11043
11054
  let disposed = false;
11044
- function drainWaiters(path25) {
11045
- const list = pendingWaiters.get(path25);
11055
+ function drainWaiters(path26) {
11056
+ const list = pendingWaiters.get(path26);
11046
11057
  if (!list) return;
11047
- pendingWaiters.delete(path25);
11058
+ pendingWaiters.delete(path26);
11048
11059
  for (const fn of list) fn();
11049
11060
  }
11050
11061
  function drainAllWaiters() {
@@ -11057,15 +11068,15 @@ function setupSimulatorTempFiles(simSession) {
11057
11068
  const registry = new IpcRegistry(simulatorOnlyPolicy);
11058
11069
  registry.on("simulator:temp-file:write", (_event, payload) => {
11059
11070
  if (disposed) return;
11060
- const { path: path25, mime, bytes } = payload;
11061
- registerTempFile(store, path25, mime, bytes);
11071
+ const { path: path26, mime, bytes } = payload;
11072
+ registerTempFile(store, path26, mime, bytes);
11062
11073
  enforceStoreCap(store);
11063
- drainWaiters(path25);
11074
+ drainWaiters(path26);
11064
11075
  });
11065
11076
  registry.on("simulator:temp-file:revoke", (_event, payload) => {
11066
11077
  if (disposed) return;
11067
- const { path: path25 } = payload;
11068
- revokeTempFile(store, path25);
11078
+ const { path: path26 } = payload;
11079
+ revokeTempFile(store, path26);
11069
11080
  });
11070
11081
  registry.on("simulator:temp-file:revoke-all", () => {
11071
11082
  if (disposed) return;
@@ -11619,8 +11630,9 @@ var UpdateManager = class {
11619
11630
  const currentVersion = this.getCurrentVersion();
11620
11631
  const info = await this.checker.checkForUpdates(currentVersion);
11621
11632
  if (info) {
11633
+ const isSameUpdate = this.latestUpdate?.version === info.version && this.latestUpdate?.downloadUrl === info.downloadUrl;
11622
11634
  this.latestUpdate = info;
11623
- this.downloadedPath = null;
11635
+ if (!isSameUpdate) this.downloadedPath = null;
11624
11636
  return { hasUpdate: true, info };
11625
11637
  }
11626
11638
  return { hasUpdate: false };
@@ -11644,11 +11656,17 @@ var UpdateManager = class {
11644
11656
  return { success: false, error: String(err2) };
11645
11657
  }
11646
11658
  }
11647
- install() {
11648
- if (this.downloadedPath) {
11649
- shell3.openPath(this.downloadedPath);
11650
- app13.quit();
11659
+ async install() {
11660
+ if (!this.downloadedPath) {
11661
+ return { success: false, error: "No update downloaded" };
11651
11662
  }
11663
+ const failure = await shell3.openPath(this.downloadedPath);
11664
+ if (failure) {
11665
+ console.warn("[UpdateManager] install failed:", failure);
11666
+ return { success: false, error: failure };
11667
+ }
11668
+ app13.quit();
11669
+ return { success: true };
11652
11670
  }
11653
11671
  /**
11654
11672
  * Tear down timers + IPC handlers. Returns a Promise so callers that need
@@ -11681,7 +11699,249 @@ var UpdateManager = class {
11681
11699
  };
11682
11700
 
11683
11701
  // src/main/services/update/github-release-checker.ts
11702
+ import fs12 from "fs";
11703
+ import path24 from "path";
11704
+ import https from "https";
11684
11705
  import { app as app14 } from "electron";
11706
+ function createGitHubReleaseChecker(opts) {
11707
+ const releaseUrl = opts.includePrereleases ? `https://api.github.com/repos/${opts.owner}/${opts.repo}/releases` : `https://api.github.com/repos/${opts.owner}/${opts.repo}/releases/latest`;
11708
+ const parseVersion = opts.parseVersion ?? schemeParser(opts.versionScheme ?? "semver");
11709
+ return {
11710
+ async checkForUpdates(currentVersion) {
11711
+ const release = await fetchRelease(releaseUrl, opts);
11712
+ if (!release) return null;
11713
+ const latestVersion = parseVersion(release);
11714
+ if (!latestVersion) return null;
11715
+ if (compareSemver(latestVersion, stripV(currentVersion)) <= 0) return null;
11716
+ const asset = (opts.pickAsset ?? defaultPickAsset)(release.assets, {
11717
+ platform: process.platform,
11718
+ arch: process.arch
11719
+ });
11720
+ if (!asset) return null;
11721
+ return {
11722
+ version: latestVersion,
11723
+ downloadUrl: asset.browser_download_url,
11724
+ releaseNotes: release.body,
11725
+ mandatory: opts.mandatory
11726
+ };
11727
+ },
11728
+ async downloadUpdate(info, onProgress) {
11729
+ const tmpDir = path24.join(app14.getPath("temp"), `${opts.repo}-update`);
11730
+ if (!fs12.existsSync(tmpDir)) fs12.mkdirSync(tmpDir, { recursive: true });
11731
+ const fileName = path24.basename(new URL(info.downloadUrl).pathname);
11732
+ const filePath = path24.join(tmpDir, fileName);
11733
+ await downloadFile(info.downloadUrl, filePath, opts.token, onProgress);
11734
+ return filePath;
11735
+ }
11736
+ };
11737
+ }
11738
+ function defaultPickAsset(assets, ctx) {
11739
+ const candidates = platformMatchers(ctx.platform, ctx.arch);
11740
+ for (const match of candidates) {
11741
+ const hit = assets.find((a) => match(a.name));
11742
+ if (hit) return hit;
11743
+ }
11744
+ return void 0;
11745
+ }
11746
+ function platformMatchers(platform, arch) {
11747
+ const lc = (s) => s.toLowerCase();
11748
+ const archTags = archAliases(arch);
11749
+ const hasArch = (name) => archTags.some((tag) => lc(name).includes(tag));
11750
+ const endsWith = (name, ext) => lc(name).endsWith(ext);
11751
+ switch (platform) {
11752
+ case "darwin":
11753
+ return [
11754
+ (n) => endsWith(n, ".dmg") && hasArch(n),
11755
+ (n) => endsWith(n, ".dmg"),
11756
+ // mac builds are often universal / unarch-tagged
11757
+ (n) => endsWith(n, ".zip") && (lc(n).includes("mac") || lc(n).includes("darwin"))
11758
+ ];
11759
+ case "win32":
11760
+ return [
11761
+ (n) => endsWith(n, ".exe") && hasArch(n),
11762
+ (n) => endsWith(n, ".zip") && (lc(n).includes("win") || lc(n).includes("windows")) && hasArch(n),
11763
+ (n) => endsWith(n, ".exe"),
11764
+ (n) => endsWith(n, ".zip") && (lc(n).includes("win") || lc(n).includes("windows"))
11765
+ ];
11766
+ case "linux":
11767
+ return [
11768
+ (n) => endsWith(n, ".appimage") && hasArch(n),
11769
+ (n) => endsWith(n, ".tar.gz") && lc(n).includes("linux") && hasArch(n),
11770
+ (n) => endsWith(n, ".appimage"),
11771
+ (n) => endsWith(n, ".tar.gz") && lc(n).includes("linux"),
11772
+ (n) => endsWith(n, ".deb") && hasArch(n)
11773
+ ];
11774
+ default:
11775
+ return [];
11776
+ }
11777
+ }
11778
+ function archAliases(arch) {
11779
+ switch (arch) {
11780
+ case "x64":
11781
+ return ["x64", "x86_64", "amd64"];
11782
+ case "arm64":
11783
+ return ["arm64", "aarch64"];
11784
+ case "ia32":
11785
+ return ["ia32", "x86", "i386"];
11786
+ default:
11787
+ return [arch];
11788
+ }
11789
+ }
11790
+ var SEMVER_RE = /(\d+)\.(\d+)\.(\d+)(?:[-+]([0-9A-Za-z.-]+))?/;
11791
+ var TRAILING_NUMBER_RE = /-(\d+)$/;
11792
+ function schemeParser(scheme) {
11793
+ if (scheme === "trailing-number") {
11794
+ return (release) => {
11795
+ const m = release.tag_name.match(TRAILING_NUMBER_RE);
11796
+ return m ? m[1] : null;
11797
+ };
11798
+ }
11799
+ return defaultParseVersion;
11800
+ }
11801
+ function defaultParseVersion(release) {
11802
+ const fromTag = extractSemver(release.tag_name);
11803
+ if (fromTag) return fromTag;
11804
+ if (release.name) {
11805
+ const fromName = extractSemver(release.name);
11806
+ if (fromName) return fromName;
11807
+ }
11808
+ for (const asset of release.assets) {
11809
+ const hit = extractSemver(asset.name);
11810
+ if (hit) return hit;
11811
+ }
11812
+ return null;
11813
+ }
11814
+ function extractSemver(input) {
11815
+ const m = input.match(SEMVER_RE);
11816
+ if (!m) return null;
11817
+ const [, major, minor, patch, pre] = m;
11818
+ return pre ? `${major}.${minor}.${patch}-${pre}` : `${major}.${minor}.${patch}`;
11819
+ }
11820
+ function stripV(version) {
11821
+ return version.replace(/^v/i, "");
11822
+ }
11823
+ function compareSemver(a, b) {
11824
+ const [aCore, aPre] = splitPre(a);
11825
+ const [bCore, bPre] = splitPre(b);
11826
+ const [a1, a2, a3] = parseCore(aCore);
11827
+ const [b1, b2, b3] = parseCore(bCore);
11828
+ if (a1 !== b1) return a1 - b1;
11829
+ if (a2 !== b2) return a2 - b2;
11830
+ if (a3 !== b3) return a3 - b3;
11831
+ if (!aPre && bPre) return 1;
11832
+ if (aPre && !bPre) return -1;
11833
+ if (!aPre && !bPre) return 0;
11834
+ return aPre.localeCompare(bPre);
11835
+ }
11836
+ function splitPre(v) {
11837
+ const i = v.indexOf("-");
11838
+ return i < 0 ? [v, null] : [v.slice(0, i), v.slice(i + 1)];
11839
+ }
11840
+ function parseCore(core) {
11841
+ const parts = core.split(".").map((s) => parseInt(s, 10) || 0);
11842
+ return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
11843
+ }
11844
+ function ghHeaders(token) {
11845
+ const headers = {
11846
+ "User-Agent": "dimina-kit-updater",
11847
+ "Accept": "application/vnd.github+json",
11848
+ "X-GitHub-Api-Version": "2022-11-28"
11849
+ };
11850
+ if (token) headers.Authorization = `Bearer ${token}`;
11851
+ return headers;
11852
+ }
11853
+ async function fetchRelease(url, opts) {
11854
+ const body = await httpGetText(url, { headers: ghHeaders(opts.token) });
11855
+ if (!body) return null;
11856
+ if (opts.includePrereleases) {
11857
+ const list = JSON.parse(body);
11858
+ const first = list.find((r) => !r.draft);
11859
+ return first ?? null;
11860
+ }
11861
+ const release = JSON.parse(body);
11862
+ return release.draft ? null : release;
11863
+ }
11864
+ function httpGetText(url, init) {
11865
+ return new Promise((resolve, reject) => {
11866
+ const req = https.get(url, { headers: init.headers }, (res) => {
11867
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
11868
+ res.resume();
11869
+ resolve(httpGetText(res.headers.location, init));
11870
+ return;
11871
+ }
11872
+ if (res.statusCode !== 200) {
11873
+ res.resume();
11874
+ resolve(null);
11875
+ return;
11876
+ }
11877
+ let body = "";
11878
+ res.setEncoding("utf8");
11879
+ res.on("data", (chunk) => {
11880
+ body += chunk;
11881
+ });
11882
+ res.on("end", () => resolve(body));
11883
+ });
11884
+ req.on("error", reject);
11885
+ });
11886
+ }
11887
+ function downloadFile(url, dest, token, onProgress) {
11888
+ return new Promise((resolve, reject) => {
11889
+ const headers = ghHeaders(token);
11890
+ const cleanup = () => fs12.promises.unlink(dest).catch(() => {
11891
+ });
11892
+ const attempt = (target, redirectsLeft) => {
11893
+ const req = https.get(target, { headers }, (res) => {
11894
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
11895
+ res.resume();
11896
+ if (redirectsLeft <= 0) {
11897
+ reject(new Error("Too many redirects"));
11898
+ return;
11899
+ }
11900
+ attempt(res.headers.location, redirectsLeft - 1);
11901
+ return;
11902
+ }
11903
+ if (res.statusCode !== 200) {
11904
+ res.resume();
11905
+ reject(new Error(`HTTP ${res.statusCode} while downloading update`));
11906
+ return;
11907
+ }
11908
+ const total = parseInt(res.headers["content-length"] || "0", 10);
11909
+ let received = 0;
11910
+ const out = fs12.createWriteStream(dest);
11911
+ res.on("data", (chunk) => {
11912
+ received += chunk.length;
11913
+ if (total > 0 && onProgress) {
11914
+ onProgress(received / total * 100);
11915
+ }
11916
+ });
11917
+ res.on("error", async (err2) => {
11918
+ out.destroy();
11919
+ await cleanup();
11920
+ reject(err2);
11921
+ });
11922
+ res.pipe(out);
11923
+ out.on("finish", async () => {
11924
+ out.close();
11925
+ if (total > 0 && received !== total) {
11926
+ await cleanup();
11927
+ reject(new Error(`Downloaded ${received} bytes, expected ${total} \u2014 update download was truncated`));
11928
+ return;
11929
+ }
11930
+ resolve();
11931
+ });
11932
+ out.on("error", async (err2) => {
11933
+ await cleanup();
11934
+ reject(err2);
11935
+ });
11936
+ });
11937
+ req.on("error", async (err2) => {
11938
+ await cleanup();
11939
+ reject(err2);
11940
+ });
11941
+ };
11942
+ attempt(url, 5);
11943
+ });
11944
+ }
11685
11945
 
11686
11946
  // src/main/app/app.ts
11687
11947
  import { toDisposable as toDisposable8 } from "@dimina-kit/electron-deck/main";
@@ -11752,7 +12012,7 @@ async function disposeContext(ctx) {
11752
12012
  function createConfiguredMainWindow(config, rendererDir2) {
11753
12013
  const mainWindow = createMainWindow({
11754
12014
  title: config.appName ?? "Dimina DevTools",
11755
- indexHtml: path24.join(rendererDir2, "entries/main/index.html"),
12015
+ indexHtml: path25.join(rendererDir2, "entries/main/index.html"),
11756
12016
  width: config.window?.width,
11757
12017
  height: config.window?.height,
11758
12018
  minWidth: config.window?.minWidth,
@@ -11865,7 +12125,7 @@ function enableDevRendererAutoReload(rendererDir2) {
11865
12125
  });
11866
12126
  }
11867
12127
  let reloadTimer = null;
11868
- const watcher = fs12.watch(rendererDir2, { recursive: true }, () => {
12128
+ const watcher = fs13.watch(rendererDir2, { recursive: true }, () => {
11869
12129
  if (reloadTimer) clearTimeout(reloadTimer);
11870
12130
  reloadTimer = setTimeout(() => {
11871
12131
  for (const win of BrowserWindow8.getAllWindows()) {
@@ -11895,7 +12155,7 @@ function runDevtoolsBootstrap(config = {}) {
11895
12155
  } catch {
11896
12156
  }
11897
12157
  }
11898
- async function createDevtoolsRuntime(config = {}) {
12158
+ async function createDevtoolsRuntime(config = {}, onInstanceCreated) {
11899
12159
  await app15.whenReady();
11900
12160
  applyTheme(loadWorkbenchSettings().theme);
11901
12161
  const rendererDir2 = config.rendererDir ?? rendererDir;
@@ -11924,6 +12184,7 @@ async function createDevtoolsRuntime(config = {}) {
11924
12184
  registerSimulatorApi: (name, handler) => context.registry.add(toDisposable8(context.simulatorApis.register(name, handler))),
11925
12185
  dispose: () => disposeContext(context)
11926
12186
  };
12187
+ onInstanceCreated?.(instance);
11927
12188
  instance.registerSimulatorApi("login", async () => {
11928
12189
  return "hello";
11929
12190
  });
@@ -12036,8 +12297,8 @@ async function createDevtoolsRuntime(config = {}) {
12036
12297
  bridge: context.bridge
12037
12298
  }));
12038
12299
  }
12039
- const bundleDir = config.editorViewConfig?.bundleDir ?? path24.join(devtoolsPackageRoot, "dist/vscode-workbench");
12040
- if (!fs12.existsSync(path24.join(bundleDir, "index.html"))) {
12300
+ const bundleDir = config.editorViewConfig?.bundleDir ?? path25.join(devtoolsPackageRoot, "dist/vscode-workbench");
12301
+ if (!fs13.existsSync(path25.join(bundleDir, "index.html"))) {
12041
12302
  console.warn(
12042
12303
  `[workbench] editor bundle not found at ${bundleDir} \u2014 skipping embedded editor assembly`
12043
12304
  );
@@ -12074,6 +12335,7 @@ async function createDevtoolsRuntime(config = {}) {
12074
12335
  // src/main/runtime/devtools-backend.ts
12075
12336
  function createDevtoolsBackend(config = {}) {
12076
12337
  let instance = null;
12338
+ let assembling = null;
12077
12339
  return {
12078
12340
  // The backend builds the devtools main window itself (framework skips its own).
12079
12341
  // Its only construction-time needs are the host preload + `sandbox:false`
@@ -12095,15 +12357,33 @@ function createDevtoolsBackend(config = {}) {
12095
12357
  // Setup (post-whenReady): app lifecycle (window-all-closed → quit) + the
12096
12358
  // full devtools assembly.
12097
12359
  assemble: async () => {
12098
- registerAppLifecycle();
12099
- instance = await createDevtoolsRuntime(config);
12360
+ registerAppLifecycle(() => instance?.context.views.disposeAll());
12361
+ assembling = createDevtoolsRuntime(config, (created) => {
12362
+ instance = created;
12363
+ if (isAppQuitting()) instance.context.views.disposeAll();
12364
+ }).then(() => {
12365
+ });
12366
+ await assembling;
12100
12367
  },
12101
12368
  // Dispose the devtools context during the framework's deterministic shutdown
12102
12369
  // (app.on('will-quit') → shutdown() → runShutdownCleanup(), awaited once).
12103
12370
  // Without this, the compile session (a child process, torn down by
12104
12371
  // closeProject) and the IPC registry would leak on exit. The framework awaits
12105
12372
  // this hook, so teardown is no longer a best-effort before-quit reach-around.
12106
- onShutdown: () => instance?.dispose()
12373
+ //
12374
+ // Waits for `assembling` first: `instance` can be non-null (published
12375
+ // early, above) while `createDevtoolsRuntime`'s own async body — the
12376
+ // host's `onSetup` and everything scheduled after it — is still running
12377
+ // and still adding entries to `instance.context.registry`. Disposing
12378
+ // that registry out from under an in-flight assembly throws instead of
12379
+ // tearing down cleanly (`DisposableRegistry.add` rejects post-dispose).
12380
+ // `.catch()` here: assembly failing is `assemble`'s problem to surface,
12381
+ // not a reason to skip disposing whatever DID get constructed.
12382
+ onShutdown: async () => {
12383
+ await assembling?.catch(() => {
12384
+ });
12385
+ await instance?.dispose();
12386
+ }
12107
12387
  };
12108
12388
  }
12109
12389
 
@@ -12113,7 +12393,18 @@ function launch(config = {}) {
12113
12393
  }
12114
12394
 
12115
12395
  // src/main/index.ts
12116
- launch().catch((err2) => {
12396
+ var updateChecker = app16.isPackaged && !app16.getVersion().includes("-dev.") ? createGitHubReleaseChecker({
12397
+ owner: "EchoTechFE",
12398
+ repo: "dimina-kit",
12399
+ parseVersion: (release) => {
12400
+ for (const asset of release.assets) {
12401
+ const match = /^dimina-devtools-(\d+\.\d+\.\d+)-/.exec(asset.name);
12402
+ if (match) return match[1];
12403
+ }
12404
+ return null;
12405
+ }
12406
+ }) : void 0;
12407
+ launch({ updateChecker }).catch((err2) => {
12117
12408
  console.error("[devtools] fatal: launch() failed during boot", err2);
12118
12409
  app16.exit(1);
12119
12410
  });
@@ -1,10 +1,58 @@
1
1
  import { app } from 'electron';
2
2
  import { launch } from './app/launch.js';
3
+ import { createGitHubReleaseChecker } from './services/update/index.js';
4
+ // Only a packaged RELEASE-channel build self-updates against this repo's
5
+ // GitHub Releases:
6
+ // - `pnpm dev` runs stay quiet (no API calls, no update dialog) — same
7
+ // app.isPackaged gate used elsewhere for dev-only behavior.
8
+ // - A packaged DEV-channel build (release.yml's `channel: dev` — distributed
9
+ // only as a 30-day Actions artifact for QA/branch testing, never a GitHub
10
+ // Release) must NOT get this either: release.yml's dev channel never
11
+ // creates a GitHub Release, so /releases/latest always reflects the
12
+ // RELEASE channel — a dev build would be prompted to "update" to an
13
+ // unrelated stable build, and clicking it would silently swap out the
14
+ // very branch/PR build the person downloaded it to test. dev-channel
15
+ // versions always carry bump-dev-version.js's `-dev.<timestamp>` suffix;
16
+ // release-channel versions never do — use that as the channel signal.
17
+ // - Hosts that embed devtools via `launch(config)` supply their own
18
+ // `updateChecker` (or none) and are unaffected — this default only
19
+ // applies to this entry point.
20
+ //
21
+ // Custom parseVersion, NOT the default asset-name fallback and NOT
22
+ // 'trailing-number':
23
+ // - release.yml's GitHub Release tag (`release-YYYYMMDD-N`) is an unrelated
24
+ // release-sequence label, not this app's version — 'trailing-number' would
25
+ // compare its bare counter against app.getVersion() (e.g. "0.4.0")
26
+ // numerically, which looks "newer" once the counter passes the app's major
27
+ // version, reporting an update on every single check forever.
28
+ // - The built-in asset-name fallback (defaultParseVersion's SEMVER_RE) finds
29
+ // the version inside `dimina-devtools-0.4.0-mac-arm64.dmg` (see release.yml's
30
+ // "Rename macOS dmg" / Archive steps) but its optional prerelease-suffix
31
+ // capture greedily swallows the trailing `-mac-arm64.dmg` as a "prerelease"
32
+ // tag, so the update dialog would show a garbled "0.5.0-mac-arm64.dmg".
33
+ // Extract just the clean `x.y.z` from our own known asset naming instead.
34
+ // Short-circuited: app.getVersion() must only be read once isPackaged is
35
+ // already known true, both because it's meaningless before then and because
36
+ // some minimal test/dev electron mocks don't stub getVersion() at all.
37
+ const updateChecker = app.isPackaged && !app.getVersion().includes('-dev.')
38
+ ? createGitHubReleaseChecker({
39
+ owner: 'EchoTechFE',
40
+ repo: 'dimina-kit',
41
+ parseVersion: (release) => {
42
+ for (const asset of release.assets) {
43
+ const match = /^dimina-devtools-(\d+\.\d+\.\d+)-/.exec(asset.name);
44
+ if (match)
45
+ return match[1];
46
+ }
47
+ return null;
48
+ },
49
+ })
50
+ : undefined;
3
51
  // Fire-and-forget boot: launch() returns the electron-deck whenReady gate
4
52
  // promise. A bare call would let a boot failure escape as an unhandledRejection
5
53
  // (no diagnostics, can be swallowed). Surface it via a structured failure exit:
6
54
  // log the cause and exit non-zero (per workbench-model.md fire-and-forget rule).
7
- launch().catch((err) => {
55
+ launch({ updateChecker }).catch((err) => {
8
56
  console.error('[devtools] fatal: launch() failed during boot', err);
9
57
  app.exit(1);
10
58
  });