cartethyia 2.4.2 → 2.4.3

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 (22) hide show
  1. package/dist/dashboard/assets/{BannedPage-KprmzKo8.js → BannedPage-CFWM4WEi.js} +1 -1
  2. package/dist/dashboard/assets/{CliToolDetailPage-BL_oNmmg.js → CliToolDetailPage-DSGFhzfL.js} +1 -1
  3. package/dist/dashboard/assets/{CliToolsPage-XjDkuNCh.js → CliToolsPage-tsIxtVpX.js} +1 -1
  4. package/dist/dashboard/assets/{ConsoleLogPage-COtZ6SqU.js → ConsoleLogPage-BY7Y_0z8.js} +1 -1
  5. package/dist/dashboard/assets/{CustomizationPage-0K0ySe8K.js → CustomizationPage-DlyXzyIf.js} +1 -1
  6. package/dist/dashboard/assets/{HealthEventsModal-O05igcJY.js → HealthEventsModal-KG3K2C5M.js} +1 -1
  7. package/dist/dashboard/assets/{LoginPage-a_voqncN.js → LoginPage-B4T7XQcx.js} +1 -1
  8. package/dist/dashboard/assets/{OverviewPage-hl2ua1qC.js → OverviewPage-BeVW5HiF.js} +1 -1
  9. package/dist/dashboard/assets/{ProviderDetailPage-CjpQIFMI.js → ProviderDetailPage-D8CTrTMh.js} +1 -1
  10. package/dist/dashboard/assets/{ProvidersPage-xsrxYHsk.js → ProvidersPage-DSSnMPdU.js} +1 -1
  11. package/dist/dashboard/assets/{ProxyPage-Bm8Eu21x.js → ProxyPage-DjXFNVqh.js} +1 -1
  12. package/dist/dashboard/assets/{QuotaPage-BKU9VtlD.js → QuotaPage-ZRiPzRxI.js} +1 -1
  13. package/dist/dashboard/assets/{SetupPage-CdjhQfnC.js → SetupPage-4v0f70wB.js} +1 -1
  14. package/dist/dashboard/assets/{StudioPage-DMGEaI3G.js → StudioPage-DQ3V_uhU.js} +1 -1
  15. package/dist/dashboard/assets/{UsagePage-B6D-eJDG.js → UsagePage-DA2RUnXI.js} +1 -1
  16. package/dist/dashboard/assets/{entry-B92UGcyr.js → entry-8jVcj7OO.js} +3 -3
  17. package/dist/dashboard/assets/{index-BGSJSZeV.js → index-gJaMvav9.js} +2 -2
  18. package/dist/dashboard/index.html +1 -1
  19. package/package.json +1 -1
  20. package/scripts/cli/cli.ts +82 -270
  21. package/scripts/cli/tray.ps1 +98 -0
  22. package/scripts/cli/tray.ts +213 -0
@@ -1,15 +1,14 @@
1
- import { randomBytes } from "node:crypto";
2
- import { existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
3
- import { spawn, type ChildProcess } from "node:child_process";
1
+ import { existsSync, mkdirSync, openSync, writeFileSync } from "node:fs";
2
+ import { spawn } from "node:child_process";
4
3
  import { tmpdir } from "node:os";
5
4
  import { join } from "node:path";
6
- import SysTray from "systray2";
7
5
  import { isNewerRelease } from "../../src/console/updates/version-compare";
8
6
  import { PACKAGE_ROOT, resolvePackagePath, resolveUserConfigDir } from "../../src/config/package-paths";
9
7
  import { resolvePort } from "../../src/config/runtime";
10
8
  import { MINIMUM_BUN_VERSION, bunVersionAtLeast } from "../shared/env";
11
9
  import { chooseFromMenu, confirmByTyping, confirmStep } from "./menu";
12
10
  import { ensureDrainToken, readPersistedDrainToken } from "./runtime-state";
11
+ import { createTray } from "./tray";
13
12
  import { npmUpdate, readInstalledVersion, readLatestPublishedVersion } from "./version";
14
13
  import { renderWordmark } from "./wordmark";
15
14
 
@@ -69,36 +68,6 @@ function reportTrayStartup(signal: TrayStartupSignal): void {
69
68
  }
70
69
  }
71
70
 
72
- async function waitForTrayStartupSignal(
73
- signalPath: string,
74
- child: ChildProcess,
75
- timeoutMs: number,
76
- ): Promise<string> {
77
- let spawnError: Error | undefined;
78
- child.once("error", (error) => {
79
- spawnError = error;
80
- });
81
- const deadline = Date.now() + timeoutMs;
82
- while (Date.now() < deadline) {
83
- if (existsSync(signalPath)) {
84
- try {
85
- const parsed: unknown = JSON.parse(readFileSync(signalPath, "utf8"));
86
- if (parsed !== null && typeof parsed === "object" && "status" in parsed) {
87
- if (parsed.status === "ready" && "url" in parsed && typeof parsed.url === "string") return parsed.url;
88
- if (parsed.status === "failed" && "message" in parsed && typeof parsed.message === "string") {
89
- throw new Error(parsed.message);
90
- }
91
- }
92
- } catch (error) {
93
- if (!(error instanceof SyntaxError)) throw error;
94
- }
95
- }
96
- if (spawnError) throw spawnError;
97
- if (child.exitCode !== null) throw new Error(`updated Cartethyia exited during startup (exit code ${child.exitCode})`);
98
- await Bun.sleep(150);
99
- }
100
- throw new Error("Cartethyia did not finish tray and gateway startup within 45 seconds");
101
- }
102
71
 
103
72
 
104
73
  function openUrl(url: string): Promise<void> {
@@ -116,16 +85,6 @@ function openUrl(url: string): Promise<void> {
116
85
  }
117
86
 
118
87
 
119
- function launchUpdatedTray(signalPath: string): ChildProcess {
120
- const env = { ...process.env, CARTETHYIA_TRAY_STARTUP_SIGNAL: signalPath };
121
- const child = spawn(
122
- process.execPath,
123
- ["run", resolvePackagePath("scripts", "cli", "cli.ts"), "tray"],
124
- { cwd: tmpdir(), detached: true, stdio: "ignore", windowsHide: true, env },
125
- );
126
- child.unref();
127
- return child;
128
- }
129
88
 
130
89
  async function startGateway(
131
90
  port: number,
@@ -259,42 +218,7 @@ async function stopGateway(
259
218
  }
260
219
  }
261
220
 
262
- async function waitForTray(tray: SysTray): Promise<void> {
263
- let timer: ReturnType<typeof setTimeout> | undefined;
264
- try {
265
- await Promise.race([
266
- tray.ready(),
267
- new Promise<never>((_, reject) => {
268
- timer = setTimeout(() => reject(new Error("System tray startup timed out")), 15_000);
269
- }),
270
- ]);
271
- } finally {
272
- if (timer !== undefined) clearTimeout(timer);
273
- }
274
- }
275
-
276
- async function stopTray(tray: SysTray, ready: boolean, exited: boolean): Promise<void> {
277
- if (exited) return;
278
- if (!ready) {
279
- tray.process.kill();
280
- return;
281
- }
282
221
 
283
- let timer: ReturnType<typeof setTimeout> | undefined;
284
- try {
285
- await Promise.race([
286
- tray.kill(false),
287
- new Promise<void>((resolveExit) => {
288
- timer = setTimeout(() => resolveExit(), 5_000);
289
- }),
290
- ]);
291
- } catch {
292
- // Fall through to a direct child termination below.
293
- } finally {
294
- if (timer !== undefined) clearTimeout(timer);
295
- }
296
- if (!tray.killed) tray.process.kill();
297
- }
298
222
 
299
223
  /**
300
224
  * The default `carte` experience: start the gateway, then offer an arrow-key
@@ -620,243 +544,130 @@ async function stopRunningGateway(): Promise<number> {
620
544
  }
621
545
 
622
546
 
547
+ /**
548
+ * Runs the gateway with a tray icon. The tray is platform-specific (`tray.ts`):
549
+ * on Windows it is a PowerShell NotifyIcon, because a Node tray library leaves
550
+ * an empty console tab open for the tray's whole lifetime, and that window is
551
+ * owned by Windows Terminal — not by this process, so it cannot be found and
552
+ * hidden from inside.
553
+ */
623
554
  async function startTray(): Promise<void> {
624
555
  const port = resolvePort();
625
556
  const drainToken = ensureDrainToken();
626
- const currentVersion = await readInstalledVersion();
627
557
  const dashboardUrl = `http://127.0.0.1:${port}/console`;
628
- const icon = resolvePackagePath(
629
- "tray-assets",
630
- process.platform === "win32" ? "cartethyia.ico" : "cartethyia.png",
631
- );
632
- const repository = {
633
- title: "Repository: risunCode/cartethyia",
634
- tooltip: "Open the Cartethyia GitHub repository",
635
- enabled: true,
636
- };
637
- const update = {
638
- title: "Checking for updates…",
639
- tooltip: "Update Cartethyia and restart the tray process",
640
- enabled: false,
641
- };
642
- const openConsole = {
643
- title: `Open Console · ${dashboardUrl}`,
644
- tooltip: `Open the local Console at ${dashboardUrl}`,
645
- enabled: true,
646
- };
647
- const resetPassword = {
648
- title: "Reset console password",
649
- tooltip: "Delete the console account so /console asks for a new one",
650
- enabled: true,
651
- };
652
- const backupRestore = {
653
- title: "Backup",
654
- tooltip: "Write a gzipped config backup to your download directory",
655
- enabled: true,
656
- };
657
- const exit = {
658
- title: "Exit",
659
- tooltip: "Stop Cartethyia gracefully and close the tray",
660
- enabled: true,
661
- };
662
- const tray = new SysTray({
663
- menu: {
664
- icon,
665
- title: "Cartethyia",
666
- tooltip: "Cartethyia AI gateway",
667
- items: [
668
- repository,
669
- update,
670
- SysTray.separator,
671
- openConsole,
672
- resetPassword,
673
- backupRestore,
674
- SysTray.separator,
675
- exit,
676
- ],
677
- },
678
- copyDir: true,
679
- });
558
+ const repositoryUrl = "https://github.com/risunCode/cartethyia";
559
+ const currentVersion = await readInstalledVersion();
680
560
 
681
- let gateway: ReturnType<typeof Bun.spawn> | null = null;
682
- let trayReady = false;
683
- let trayExited = false;
561
+ let gateway: Bun.Subprocess | null = null;
684
562
  let closing = false;
685
563
  let busy = false;
686
564
  let resetArmed = false;
687
- let latestVersion: string | null = null;
688
565
  let closePromise: Promise<void> | undefined;
689
566
 
567
+ const tray = createTray({
568
+ iconPath: resolvePackagePath(
569
+ "tray-assets",
570
+ process.platform === "win32" ? "cartethyia.ico" : "cartethyia.png",
571
+ ),
572
+ tooltip: `Cartethyia ${currentVersion}`,
573
+ items: [
574
+ { kind: "action", id: "repository", title: "Visit repository" },
575
+ { kind: "action", id: "update", title: `Cartethyia ${currentVersion} \u00b7 checking\u2026`, enabled: false },
576
+ { kind: "separator" },
577
+ { kind: "action", id: "console", title: `Open Console \u00b7 ${dashboardUrl}` },
578
+ { kind: "action", id: "reset", title: "Reset console password" },
579
+ { kind: "action", id: "backup", title: "Backup" },
580
+ { kind: "separator" },
581
+ { kind: "action", id: "exit", title: "Exit" },
582
+ ],
583
+ onSelect: (id: string) => void handleSelect(id),
584
+ onExit: () => void close(1),
585
+ });
586
+
690
587
  const close = (exitCode = 0): Promise<void> => {
691
588
  if (closePromise) return closePromise;
692
589
  closing = true;
693
590
  closePromise = (async () => {
694
- const current: ReturnType<typeof Bun.spawn> | null = gateway;
591
+ const current: Bun.Subprocess | null = gateway;
695
592
  gateway = null;
696
593
  if (current) await stopGateway(current, port, drainToken);
697
- await stopTray(tray, trayReady, trayExited);
594
+ await tray.close();
698
595
  process.exitCode = exitCode;
699
596
  })();
700
597
  return closePromise;
701
598
  };
702
599
 
703
600
  const refreshVersion = async (): Promise<void> => {
704
- const fetchedVersion = await readLatestPublishedVersion();
601
+ const fetched = await readLatestPublishedVersion();
705
602
  if (closing) return;
706
- latestVersion = fetchedVersion;
707
- if (fetchedVersion === null) {
708
- update.title = `Cartethyia ${currentVersion} · check for updates`;
709
- update.enabled = true;
710
- } else {
711
- if (isNewerRelease(currentVersion, fetchedVersion)) {
712
- update.title = `Cartethyia ${currentVersion} · update to ${fetchedVersion}`;
713
- update.enabled = true;
714
- } else {
715
- update.title = `Cartethyia ${currentVersion} · up to date`;
716
- update.enabled = false;
717
- }
718
- }
719
- try {
720
- await tray.sendAction({ type: "update-item", item: update });
721
- } catch {
722
- // The tray may be closing while the background version check completes.
603
+ if (fetched === null) {
604
+ tray.update("update", `Cartethyia ${currentVersion} \u00b7 check for updates`, true);
605
+ return;
723
606
  }
607
+ const newer = isNewerRelease(currentVersion, fetched);
608
+ tray.update(
609
+ "update",
610
+ newer
611
+ ? `Cartethyia ${currentVersion} \u00b7 update to ${fetched}`
612
+ : `Cartethyia ${currentVersion} \u00b7 up to date`,
613
+ newer,
614
+ );
615
+ };
616
+
617
+ const runUpdate = async (): Promise<void> => {
618
+ if (busy) return;
619
+ busy = true;
620
+ tray.update("update", "Updating Cartethyia\u2026", false);
621
+ const code = await npmUpdate();
622
+ busy = false;
623
+ tray.update(
624
+ "update",
625
+ code === 0 ? "Updated \u2014 restart Cartethyia" : "Update failed \u2014 retry",
626
+ code !== 0,
627
+ );
724
628
  };
725
629
 
726
- const handleClick = async (title: string): Promise<void> => {
630
+ async function handleSelect(id: string): Promise<void> {
727
631
  if (closing) return;
728
- if (title === "Exit") {
729
- await close();
730
- return;
731
- }
732
- if (title === openConsole.title) {
733
- await openUrl(dashboardUrl);
734
- return;
735
- }
736
- if (title === resetPassword.title) {
737
- // Same recovery the menu offers: /change-password needs the current
738
- // password, so deleting the account is the only way back in. A tray click
739
- // cannot ask for a typed confirmation, so the first click arms the action
740
- // and the second one runs it.
741
- if (!resetArmed) {
742
- resetArmed = true;
743
- console.log(
744
- "Click \"Reset console password\" again to delete the console account. This removes every operator login; providers, API keys, and routing are kept.",
745
- );
746
- return;
747
- }
748
- resetArmed = false;
749
- const removed = await resetConsoleAccount(port, drainToken);
750
- if (removed === 0) {
751
- console.log(`Console account removed. Open ${dashboardUrl} to create a new one.`);
752
- await openUrl(dashboardUrl);
753
- }
754
- return;
755
- }
756
- if (title === backupRestore.title) {
757
- // Export straight to disk, like the menu does. Restore stays in the
758
- // console, where an operator picks the file and confirms the overwrite.
632
+ if (id === "exit") return void (await close());
633
+ if (id === "console") return void (await openUrl(dashboardUrl));
634
+ if (id === "repository") return void (await openUrl(repositoryUrl));
635
+ if (id === "backup") {
759
636
  const result = await exportBackup(port, drainToken);
760
- if (result !== null)
637
+ if (result !== null) {
761
638
  console.log(
762
639
  `Backup written to ${result.path} (${(result.bytes / 1024).toFixed(1)} KiB). Telemetry is not included.`,
763
640
  );
641
+ }
764
642
  return;
765
643
  }
766
- if (title === repository.title) {
767
- await openUrl("https://github.com/risunCode/cartethyia");
768
- return;
769
- }
770
- if (title === "Check for updates" && !busy) {
771
- await refreshVersion();
772
- return;
773
- }
774
- if (title !== update.title || !update.enabled || busy) return;
775
- if (latestVersion === null || !isNewerRelease(currentVersion, latestVersion)) {
776
- await refreshVersion();
777
- return;
778
- }
779
-
780
- busy = true;
781
- update.title = "Updating Cartethyia…";
782
- update.enabled = false;
783
- try {
784
- await tray.sendAction({ type: "update-item", item: update });
785
- const current = gateway;
786
- if (!current) throw new Error("Cartethyia gateway is not running");
787
- gateway = null;
788
- await stopGateway(current, port, drainToken);
789
- if (closing) return;
790
-
791
- console.log("Updating Cartethyia through npm...");
792
- const code = await npmUpdate();
793
- if (closing) return;
794
- if (code !== 0) {
795
- console.error(`npm update failed with exit code ${code}; restarting the current installation.`);
796
- gateway = await startGateway(port, drainToken);
797
- await refreshVersion();
644
+ if (id === "reset") {
645
+ // A tray click cannot ask for the typed confirmation the menu uses, so
646
+ // the first click arms the action and the second one runs it.
647
+ if (!resetArmed) {
648
+ resetArmed = true;
649
+ tray.update("reset", "Reset console password \u00b7 click again to delete", true);
798
650
  return;
799
651
  }
800
-
801
- const signalPath = join(tmpdir(), `cartethyia-tray-update-${randomBytes(16).toString("hex")}.json`);
802
- try {
803
- const updatedProcess = launchUpdatedTray(signalPath);
804
- const updatedUrl = await waitForTrayStartupSignal(signalPath, updatedProcess, 60_000);
805
- console.log(`Cartethyia updated to ${await readInstalledVersion()} and restarted at ${updatedUrl}`);
806
- await close();
807
- } finally {
808
- if (existsSync(signalPath)) unlinkSync(signalPath);
809
- }
810
- } catch (error) {
811
- const reason = error instanceof Error ? error.message : "unknown error";
812
- console.error(`Cartethyia update or restart failed: ${reason}`);
813
- if (!closing && gateway === null) {
814
- try {
815
- gateway = await startGateway(port, drainToken);
816
- } catch {
817
- await close(1);
818
- }
819
- }
820
- if (!closing) {
821
- update.title = "Update failed — retry";
822
- update.enabled = true;
823
- try {
824
- await tray.sendAction({ type: "update-item", item: update });
825
- } catch {
826
- // The tray may have failed while the update was being recovered.
827
- }
828
- }
829
- } finally {
830
- busy = false;
652
+ resetArmed = false;
653
+ tray.update("reset", "Reset console password", true);
654
+ await resetConsoleAccount(port, drainToken);
655
+ return;
831
656
  }
832
- };
657
+ if (id === "update") await runUpdate();
658
+ }
833
659
 
834
- await tray.onClick((event) => {
835
- void handleClick(event.item.title).catch(() => {
836
- console.error("The selected system-tray action failed.");
837
- });
838
- });
839
- tray.onExit(() => {
840
- trayExited = true;
841
- if (!closing) void close(1);
842
- });
843
- tray.onError(() => {
844
- console.error("The system-tray process failed; stopping Cartethyia.");
845
- if (!closing) void close(1);
846
- });
847
660
  process.once("SIGINT", () => void close());
848
661
  process.once("SIGTERM", () => void close());
849
662
 
850
663
  try {
851
- await waitForTray(tray);
852
- trayReady = true;
664
+ await tray.ready();
853
665
  gateway = await startGateway(port, drainToken);
854
- reportTrayStartup({ status: "ready", url: dashboardUrl });
855
666
  console.log(`Cartethyia is running at ${dashboardUrl}`);
856
667
  void refreshVersion();
857
668
 
858
669
  while (!closing) {
859
- const current: ReturnType<typeof Bun.spawn> | null = gateway;
670
+ const current: Bun.Subprocess | null = gateway;
860
671
  if (!current) {
861
672
  await Bun.sleep(100);
862
673
  continue;
@@ -873,6 +684,7 @@ async function startTray(): Promise<void> {
873
684
  }
874
685
  }
875
686
 
687
+
876
688
  async function main(): Promise<void> {
877
689
  assertBunVersion();
878
690
  const [command] = process.argv.slice(2);
@@ -0,0 +1,98 @@
1
+ # Cartethyia tray icon for Windows using NotifyIcon.
2
+ #
3
+ # Why PowerShell instead of a Node tray library: `systray2` and friends bundle a
4
+ # helper binary and still leave the spawning process attached to a console, so
5
+ # Windows Terminal shows an empty tab for the tray's whole lifetime. A
6
+ # PowerShell NotifyIcon owns no console of its own when started with
7
+ # -WindowStyle Hidden, and the protocol here is plain JSON lines on
8
+ # stdin/stdout, so the parent stays in charge.
9
+ #
10
+ # Protocol: the parent writes one JSON object per line.
11
+ # {"action":"add-item","index":0,"title":"...","enabled":true}
12
+ # {"action":"update-item","index":0,"title":"...","enabled":true}
13
+ # {"action":"set-tooltip","text":"..."}
14
+ # {"action":"kill"}
15
+ # This script writes one JSON object per line:
16
+ # {"type":"started"} {"type":"ready"} {"type":"click","index":0}
17
+ param([string]$IconPath, [string]$Tooltip)
18
+
19
+ $ErrorActionPreference = "Stop"
20
+
21
+ Add-Type -AssemblyName System.Windows.Forms
22
+ Add-Type -AssemblyName System.Drawing
23
+
24
+ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
25
+ [Console]::InputEncoding = [System.Text.Encoding]::UTF8
26
+ $OutputEncoding = [System.Text.Encoding]::UTF8
27
+
28
+ [System.Windows.Forms.Application]::EnableVisualStyles()
29
+ [System.Windows.Forms.Application]::SetCompatibleTextRenderingDefault($false)
30
+
31
+ $script:notifyIcon = New-Object System.Windows.Forms.NotifyIcon
32
+ $script:notifyIcon.Icon = New-Object System.Drawing.Icon($IconPath)
33
+ $script:notifyIcon.Text = $Tooltip
34
+ $script:notifyIcon.Visible = $true
35
+
36
+ $script:menu = New-Object System.Windows.Forms.ContextMenuStrip
37
+ $script:notifyIcon.ContextMenuStrip = $script:menu
38
+ $script:items = @()
39
+
40
+ function Write-Event($obj) {
41
+ $json = $obj | ConvertTo-Json -Compress
42
+ [Console]::Out.WriteLine($json)
43
+ [Console]::Out.Flush()
44
+ }
45
+
46
+ function Add-MenuItem($index, $title, $enabled) {
47
+ $item = New-Object System.Windows.Forms.ToolStripMenuItem
48
+ $item.Text = $title
49
+ $item.Enabled = $enabled
50
+ $idx = $index
51
+ $item.Add_Click({ Write-Event @{ type = "click"; index = $idx } }.GetNewClosure())
52
+ $script:menu.Items.Add($item) | Out-Null
53
+ $script:items += $item
54
+ }
55
+
56
+ function Update-MenuItem($index, $title, $enabled) {
57
+ if ($index -lt $script:items.Count) {
58
+ $script:items[$index].Text = $title
59
+ $script:items[$index].Enabled = $enabled
60
+ }
61
+ }
62
+
63
+ function Set-Tooltip($text) {
64
+ # NotifyIcon.Text is capped at 63 characters.
65
+ if ($text.Length -gt 63) { $text = $text.Substring(0, 63) }
66
+ $script:notifyIcon.Text = $text
67
+ }
68
+
69
+ # Stdin is polled from the UI thread: a background thread reading Console.In
70
+ # races the message loop and deadlocks when the menu is open.
71
+ $script:timer = New-Object System.Windows.Forms.Timer
72
+ $script:timer.Interval = 100
73
+ $script:timer.Add_Tick({
74
+ try {
75
+ while ([Console]::In.Peek() -ne -1) {
76
+ $line = [Console]::In.ReadLine()
77
+ if ([string]::IsNullOrWhiteSpace($line)) { continue }
78
+ $cmd = $line | ConvertFrom-Json
79
+ switch ($cmd.action) {
80
+ "add-item" { Add-MenuItem $cmd.index $cmd.title $cmd.enabled }
81
+ "update-item" { Update-MenuItem $cmd.index $cmd.title $cmd.enabled }
82
+ "set-tooltip" { Set-Tooltip $cmd.text }
83
+ "ready" { Write-Event @{ type = "ready" } }
84
+ "kill" {
85
+ $script:notifyIcon.Visible = $false
86
+ $script:notifyIcon.Dispose()
87
+ [System.Windows.Forms.Application]::Exit()
88
+ }
89
+ }
90
+ }
91
+ } catch {
92
+ Write-Event @{ type = "error"; message = $_.Exception.Message }
93
+ }
94
+ })
95
+ $script:timer.Start()
96
+
97
+ Write-Event @{ type = "started" }
98
+ [System.Windows.Forms.Application]::Run()