bosun 0.27.3 → 0.27.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -0
- package/autofix.mjs +47 -2
- package/cli.mjs +46 -0
- package/config.mjs +60 -4
- package/desktop/AGENTS.md +62 -0
- package/desktop/README.md +41 -0
- package/desktop/main.mjs +162 -15
- package/desktop/package-lock.json +4193 -0
- package/desktop/package.json +61 -3
- package/desktop-shortcut.mjs +312 -0
- package/kanban-adapter.mjs +35 -0
- package/monitor.mjs +3 -3
- package/package.json +5 -1
- package/postinstall.mjs +39 -1
- package/setup.mjs +65 -3
- package/task-executor.mjs +124 -19
- package/telegram-bot.mjs +492 -56
- package/ui/app.js +4 -5
- package/ui/app.monolith.js +4 -1
- package/ui/components/forms.js +3 -1
- package/ui/components/kanban-board.js +33 -17
- package/ui/components/workspace-switcher.js +424 -10
- package/ui/demo.html +50 -11
- package/ui/index.html +89 -3
- package/ui/styles/animations.css +2 -2
- package/ui/styles/components.css +74 -67
- package/ui/styles/kanban.css +50 -2
- package/ui/styles/layout.css +13 -7
- package/ui/styles/sessions.css +7 -7
- package/ui/styles/variables.css +54 -54
- package/ui/styles/workspace-switcher.css +540 -6
- package/ui/styles.css +8 -8
- package/ui/styles.monolith.css +15 -0
- package/ui/tabs/agents.js +5 -1
- package/ui/tabs/control.js +3 -1
- package/ui/tabs/infra.js +213 -0
- package/ui/tabs/logs.js +1 -1
- package/ui/tabs/settings.js +4 -4
- package/ui/tabs/tasks.js +147 -1
- package/ui-server.mjs +72 -38
- package/workspace-manager.mjs +91 -3
- package/worktree-manager.mjs +29 -28
package/.env.example
CHANGED
|
@@ -105,6 +105,14 @@ TELEGRAM_MINIAPP_ENABLED=false
|
|
|
105
105
|
# Tunnel mode control: auto | cloudflared | disabled
|
|
106
106
|
# TELEGRAM_UI_TUNNEL=auto
|
|
107
107
|
|
|
108
|
+
# ─── Desktop Portal ────────────────────────────────────────────────────────
|
|
109
|
+
# Auto-start bosun daemon when the desktop portal launches (default: true)
|
|
110
|
+
# BOSUN_DESKTOP_AUTO_START_DAEMON=true
|
|
111
|
+
# Enable auto-updates for packaged desktop builds (default: false)
|
|
112
|
+
# BOSUN_DESKTOP_AUTO_UPDATE=false
|
|
113
|
+
# Optional auto-update feed URL override
|
|
114
|
+
# BOSUN_DESKTOP_UPDATE_URL=https://updates.example.com/bosun
|
|
115
|
+
|
|
108
116
|
# ─── Telegram Sentinel (independent watchdog) ──────────────────────────────
|
|
109
117
|
# Keep Telegram command availability even when bosun is down.
|
|
110
118
|
# Sentinel can auto-restart monitor, detect crash loops, and run repair-agent.
|
package/autofix.mjs
CHANGED
|
@@ -328,6 +328,39 @@ async function readSourceContext(filePath, errorLine, contextLines = 30) {
|
|
|
328
328
|
*/
|
|
329
329
|
let _devModeCache = null;
|
|
330
330
|
|
|
331
|
+
function detectBosunRepo(startDir, maxDepth = 6) {
|
|
332
|
+
let cursor = resolve(startDir);
|
|
333
|
+
for (let i = 0; i < maxDepth; i += 1) {
|
|
334
|
+
const pkgPath = resolve(cursor, "package.json");
|
|
335
|
+
if (existsSync(pkgPath)) {
|
|
336
|
+
try {
|
|
337
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
338
|
+
if (pkg?.name === "bosun" && existsSync(resolve(cursor, "monitor.mjs"))) {
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
} catch {
|
|
342
|
+
/* ignore */
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const monoPkg = resolve(cursor, "scripts", "bosun", "package.json");
|
|
346
|
+
if (existsSync(monoPkg)) {
|
|
347
|
+
try {
|
|
348
|
+
const pkg = JSON.parse(readFileSync(monoPkg, "utf8"));
|
|
349
|
+
if (
|
|
350
|
+
pkg?.name === "bosun" &&
|
|
351
|
+
existsSync(resolve(cursor, "scripts", "bosun", "monitor.mjs"))
|
|
352
|
+
) {
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
} catch {
|
|
356
|
+
/* ignore */
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
cursor = resolve(cursor, "..");
|
|
360
|
+
}
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
|
|
331
364
|
export function isDevMode() {
|
|
332
365
|
if (_devModeCache !== null) return _devModeCache;
|
|
333
366
|
|
|
@@ -341,9 +374,21 @@ export function isDevMode() {
|
|
|
341
374
|
return false;
|
|
342
375
|
}
|
|
343
376
|
|
|
344
|
-
// Check if we're inside node_modules (npm install)
|
|
377
|
+
// Check if we're inside node_modules (npm install). Ignore Vite/Vitest
|
|
378
|
+
// cache paths so local tests that bundle via node_modules/.vite still
|
|
379
|
+
// fall back to repo detection.
|
|
345
380
|
const normalized = __dirname.replace(/\\/g, "/").toLowerCase();
|
|
346
|
-
|
|
381
|
+
const inNodeModules = normalized.includes("/node_modules/");
|
|
382
|
+
const inViteCache =
|
|
383
|
+
normalized.includes("/node_modules/.vite/") ||
|
|
384
|
+
normalized.includes("/node_modules/.vitest/") ||
|
|
385
|
+
normalized.includes("/node_modules/.cache/");
|
|
386
|
+
if (inNodeModules) {
|
|
387
|
+
if (inViteCache) {
|
|
388
|
+
const fromCwd = detectBosunRepo(process.cwd());
|
|
389
|
+
_devModeCache = fromCwd;
|
|
390
|
+
return fromCwd;
|
|
391
|
+
}
|
|
347
392
|
_devModeCache = false;
|
|
348
393
|
return false;
|
|
349
394
|
}
|
package/cli.mjs
CHANGED
|
@@ -70,6 +70,9 @@ function showHelp() {
|
|
|
70
70
|
--help Show this help
|
|
71
71
|
--version Show version
|
|
72
72
|
--portal, --desktop Launch the Bosun desktop portal (Electron)
|
|
73
|
+
--desktop-shortcut Create a desktop shortcut for the portal
|
|
74
|
+
--desktop-shortcut-remove Remove the desktop shortcut
|
|
75
|
+
--desktop-shortcut-status Show desktop shortcut status
|
|
73
76
|
--update Check for and install latest version
|
|
74
77
|
--no-update-check Skip automatic update check on startup
|
|
75
78
|
--no-auto-update Disable background auto-update polling
|
|
@@ -423,9 +426,11 @@ function startDaemon() {
|
|
|
423
426
|
/* ok */
|
|
424
427
|
}
|
|
425
428
|
|
|
429
|
+
const runAsNode = process.versions?.electron ? ["--run-as-node"] : [];
|
|
426
430
|
const child = spawn(
|
|
427
431
|
process.execPath,
|
|
428
432
|
[
|
|
433
|
+
...runAsNode,
|
|
429
434
|
"--max-old-space-size=4096",
|
|
430
435
|
fileURLToPath(new URL("./cli.mjs", import.meta.url)),
|
|
431
436
|
...process.argv.slice(2).filter((a) => a !== "--daemon" && a !== "-d"),
|
|
@@ -536,6 +541,47 @@ async function main() {
|
|
|
536
541
|
process.exit(0);
|
|
537
542
|
}
|
|
538
543
|
|
|
544
|
+
// Handle desktop shortcut controls
|
|
545
|
+
if (args.includes("--desktop-shortcut")) {
|
|
546
|
+
const { installDesktopShortcut, getDesktopShortcutMethodName } =
|
|
547
|
+
await import("./desktop-shortcut.mjs");
|
|
548
|
+
const result = installDesktopShortcut();
|
|
549
|
+
if (result.success) {
|
|
550
|
+
console.log(` ✅ Desktop shortcut installed (${result.method})`);
|
|
551
|
+
if (result.path) console.log(` Path: ${result.path}`);
|
|
552
|
+
if (result.name) console.log(` Name: ${result.name}`);
|
|
553
|
+
} else {
|
|
554
|
+
const method = getDesktopShortcutMethodName();
|
|
555
|
+
console.error(
|
|
556
|
+
` ❌ Failed to install desktop shortcut (${method}): ${result.error}`,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
process.exit(result.success ? 0 : 1);
|
|
560
|
+
}
|
|
561
|
+
if (args.includes("--desktop-shortcut-remove")) {
|
|
562
|
+
const { removeDesktopShortcut } = await import("./desktop-shortcut.mjs");
|
|
563
|
+
const result = removeDesktopShortcut();
|
|
564
|
+
if (result.success) {
|
|
565
|
+
console.log(` ✅ Desktop shortcut removed`);
|
|
566
|
+
} else {
|
|
567
|
+
console.error(
|
|
568
|
+
` ❌ Failed to remove desktop shortcut: ${result.error}`,
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
process.exit(result.success ? 0 : 1);
|
|
572
|
+
}
|
|
573
|
+
if (args.includes("--desktop-shortcut-status")) {
|
|
574
|
+
const { getDesktopShortcutStatus } = await import("./desktop-shortcut.mjs");
|
|
575
|
+
const status = getDesktopShortcutStatus();
|
|
576
|
+
if (status.installed) {
|
|
577
|
+
console.log(` Desktop shortcut: installed (${status.method})`);
|
|
578
|
+
if (status.path) console.log(` Path: ${status.path}`);
|
|
579
|
+
} else {
|
|
580
|
+
console.log(` Desktop shortcut: not installed`);
|
|
581
|
+
}
|
|
582
|
+
process.exit(0);
|
|
583
|
+
}
|
|
584
|
+
|
|
539
585
|
// Handle --portal / --desktop
|
|
540
586
|
if (args.includes("--portal") || args.includes("--desktop")) {
|
|
541
587
|
const launcher = resolve(__dirname, "desktop", "launch.mjs");
|
package/config.mjs
CHANGED
|
@@ -703,6 +703,48 @@ function loadRepoConfig(configDir, configData = {}, options = {}) {
|
|
|
703
703
|
];
|
|
704
704
|
}
|
|
705
705
|
|
|
706
|
+
function loadWorkspaceRepoConfig(configDir, configData = {}, activeWorkspace = "") {
|
|
707
|
+
const workspaces = Array.isArray(configData.workspaces)
|
|
708
|
+
? configData.workspaces
|
|
709
|
+
: [];
|
|
710
|
+
if (workspaces.length === 0) return [];
|
|
711
|
+
|
|
712
|
+
const targetWorkspaceId = normalizeKey(activeWorkspace || configData.activeWorkspace || "");
|
|
713
|
+
const targetWorkspace =
|
|
714
|
+
(targetWorkspaceId
|
|
715
|
+
? workspaces.find((workspace) => normalizeKey(workspace?.id) === targetWorkspaceId)
|
|
716
|
+
: null) ||
|
|
717
|
+
workspaces[0];
|
|
718
|
+
|
|
719
|
+
if (!targetWorkspace || !Array.isArray(targetWorkspace.repos)) {
|
|
720
|
+
return [];
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const workspacePath = resolve(configDir, "workspaces", targetWorkspace.id);
|
|
724
|
+
const activeRepoName = normalizeKey(targetWorkspace.activeRepo || "");
|
|
725
|
+
|
|
726
|
+
return targetWorkspace.repos
|
|
727
|
+
.map((repo, index) => {
|
|
728
|
+
if (!repo || typeof repo !== "object") return null;
|
|
729
|
+
const name = String(repo.name || repo.id || "").trim();
|
|
730
|
+
if (!name) return null;
|
|
731
|
+
const repoPath = resolve(workspacePath, name);
|
|
732
|
+
return {
|
|
733
|
+
name,
|
|
734
|
+
id: normalizeKey(name),
|
|
735
|
+
path: repoPath,
|
|
736
|
+
slug: String(repo.slug || "").trim(),
|
|
737
|
+
url: String(repo.url || "").trim(),
|
|
738
|
+
workspace: String(targetWorkspace.id || "").trim(),
|
|
739
|
+
primary:
|
|
740
|
+
repo.primary === true ||
|
|
741
|
+
(activeRepoName && normalizeKey(name) === activeRepoName) ||
|
|
742
|
+
(!activeRepoName && index === 0),
|
|
743
|
+
};
|
|
744
|
+
})
|
|
745
|
+
.filter(Boolean);
|
|
746
|
+
}
|
|
747
|
+
|
|
706
748
|
function loadAgentPrompts(configDir, repoRoot, configData) {
|
|
707
749
|
const resolved = resolveAgentPrompts(configDir, repoRoot, configData);
|
|
708
750
|
return { ...resolved.prompts, _sources: resolved.sources };
|
|
@@ -741,9 +783,16 @@ export function loadConfig(argv = process.argv, options = {}) {
|
|
|
741
783
|
configData.defaultWorkspace ||
|
|
742
784
|
"";
|
|
743
785
|
|
|
744
|
-
let repositories =
|
|
745
|
-
|
|
746
|
-
|
|
786
|
+
let repositories = loadWorkspaceRepoConfig(
|
|
787
|
+
configDir,
|
|
788
|
+
configData,
|
|
789
|
+
activeWorkspace,
|
|
790
|
+
);
|
|
791
|
+
if (!repositories.length) {
|
|
792
|
+
repositories = loadRepoConfig(configDir, configData, {
|
|
793
|
+
repoRootOverride,
|
|
794
|
+
});
|
|
795
|
+
}
|
|
747
796
|
|
|
748
797
|
const repoSelection =
|
|
749
798
|
cli["repo-name"] ||
|
|
@@ -798,7 +847,14 @@ export function loadConfig(argv = process.argv, options = {}) {
|
|
|
798
847
|
|
|
799
848
|
// Apply profile overrides (executors, repos, etc.)
|
|
800
849
|
configData = applyProfileOverrides(configData, profile);
|
|
801
|
-
repositories =
|
|
850
|
+
repositories = loadWorkspaceRepoConfig(
|
|
851
|
+
configDir,
|
|
852
|
+
configData,
|
|
853
|
+
activeWorkspace,
|
|
854
|
+
);
|
|
855
|
+
if (!repositories.length) {
|
|
856
|
+
repositories = loadRepoConfig(configDir, configData, { repoRootOverride });
|
|
857
|
+
}
|
|
802
858
|
selectedRepository =
|
|
803
859
|
resolveRepoSelection(
|
|
804
860
|
repositories,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Bosun Desktop — AGENTS Guide
|
|
2
|
+
|
|
3
|
+
## Module Overview
|
|
4
|
+
- Purpose: Native desktop shell for the Bosun control center, bundling the UI server and opening it in a desktop window.
|
|
5
|
+
- Use when: Updating desktop packaging, launch flow, auto-update, or native window behavior.
|
|
6
|
+
- Key entry points: `scripts/bosun/desktop/main.mjs:1`, `scripts/bosun/desktop/launch.mjs:1`, `scripts/bosun/ui-server.mjs:1`.
|
|
7
|
+
|
|
8
|
+
## Architecture
|
|
9
|
+
- The desktop app dynamically imports Bosun’s UI server and runs it locally, then loads the UI with a session token.
|
|
10
|
+
- Packaged builds copy the `scripts/bosun/` runtime into app resources and start from there.
|
|
11
|
+
- Entry points:
|
|
12
|
+
- `main.mjs` starts the UI server and creates the BrowserWindow.
|
|
13
|
+
- `launch.mjs` installs Electron (if needed) and runs the desktop app in dev.
|
|
14
|
+
|
|
15
|
+
```mermaid
|
|
16
|
+
flowchart TD
|
|
17
|
+
Desktop[desktop/main.mjs] --> UiServer[ui-server.mjs]
|
|
18
|
+
UiServer --> UI[ui/index.html]
|
|
19
|
+
Desktop --> Window[BrowserWindow]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Core Concepts
|
|
23
|
+
- Local UI server: desktop app embeds the same UI server used by the Telegram Mini App.
|
|
24
|
+
- Session token: desktop loads `/?token=...` so the UI server sets a session cookie.
|
|
25
|
+
- Packaged runtime: `extraResources` copies the Bosun runtime into app resources.
|
|
26
|
+
|
|
27
|
+
## Usage Examples
|
|
28
|
+
|
|
29
|
+
### Launch in dev
|
|
30
|
+
```bash
|
|
31
|
+
node scripts/bosun/desktop/launch.mjs
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Build installers
|
|
35
|
+
```bash
|
|
36
|
+
cd scripts/bosun/desktop
|
|
37
|
+
npm install
|
|
38
|
+
npm run dist
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Implementation Patterns
|
|
42
|
+
- Always resolve the Bosun runtime root with `resolveBosunRoot()` when packaged.
|
|
43
|
+
- Use dynamic import to load `ui-server.mjs` so packaged resources are used.
|
|
44
|
+
- Keep auto-update behind `BOSUN_DESKTOP_AUTO_UPDATE=1` to avoid noisy failures in dev.
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
- Desktop packaging config lives in `scripts/bosun/desktop/package.json:20`.
|
|
48
|
+
- Auto-update is opt-in via `BOSUN_DESKTOP_AUTO_UPDATE=1`.
|
|
49
|
+
- Optional update feed override: `BOSUN_DESKTOP_UPDATE_URL`.
|
|
50
|
+
- To skip Electron auto-install in dev, set `BOSUN_DESKTOP_SKIP_INSTALL=1`.
|
|
51
|
+
|
|
52
|
+
## Testing
|
|
53
|
+
- Bosun tests: `cd scripts/bosun && npm test`
|
|
54
|
+
- Desktop smoke: `node scripts/bosun/desktop/launch.mjs`
|
|
55
|
+
|
|
56
|
+
## Troubleshooting
|
|
57
|
+
- Electron missing
|
|
58
|
+
- Cause: Desktop dependencies not installed.
|
|
59
|
+
- Fix: `npm -C scripts/bosun/desktop install` or set `BOSUN_DESKTOP_SKIP_INSTALL=1` to avoid auto-install.
|
|
60
|
+
- UI server fails to start
|
|
61
|
+
- Cause: Port conflict or missing runtime files.
|
|
62
|
+
- Fix: Ensure the packaged `bosun/` runtime exists and retry with a clean start.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Bosun Desktop
|
|
2
|
+
|
|
3
|
+
Bosun Desktop is an Electron shell that launches the Bosun UI server locally
|
|
4
|
+
and opens the portal in a native window.
|
|
5
|
+
|
|
6
|
+
## Development
|
|
7
|
+
```bash
|
|
8
|
+
cd scripts/bosun/desktop
|
|
9
|
+
npm install
|
|
10
|
+
npm run start
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Launch via Bosun CLI
|
|
14
|
+
```bash
|
|
15
|
+
cd scripts/bosun
|
|
16
|
+
node cli.mjs --desktop
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Desktop shortcut
|
|
20
|
+
```bash
|
|
21
|
+
cd scripts/bosun
|
|
22
|
+
node cli.mjs --desktop-shortcut
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The shortcut launches the desktop portal and will auto-start the bosun daemon
|
|
26
|
+
if it is not already running.
|
|
27
|
+
|
|
28
|
+
## Build installers
|
|
29
|
+
```bash
|
|
30
|
+
cd scripts/bosun/desktop
|
|
31
|
+
npm run dist
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Auto-update
|
|
35
|
+
- Enable with `BOSUN_DESKTOP_AUTO_UPDATE=1`.
|
|
36
|
+
- Optional feed URL override: `BOSUN_DESKTOP_UPDATE_URL=https://.../`.
|
|
37
|
+
|
|
38
|
+
## Notes
|
|
39
|
+
- Packaged apps bundle the Bosun runtime under `resources/bosun/`.
|
|
40
|
+
- The UI server runs locally; the desktop app loads `/?token=...` to set the
|
|
41
|
+
session cookie.
|
package/desktop/main.mjs
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { app, BrowserWindow } from "electron";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
startTelegramUiServer,
|
|
8
|
-
stopTelegramUiServer,
|
|
9
|
-
} from "../ui-server.mjs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
6
|
+
import { homedir } from "node:os";
|
|
10
7
|
|
|
11
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
9
|
|
|
@@ -14,24 +11,151 @@ let mainWindow = null;
|
|
|
14
11
|
let shuttingDown = false;
|
|
15
12
|
let uiServerStarted = false;
|
|
16
13
|
let uiOrigin = null;
|
|
14
|
+
let uiApi = null;
|
|
15
|
+
|
|
16
|
+
const DAEMON_PID_FILE = resolve(homedir(), ".cache", "bosun", "daemon.pid");
|
|
17
|
+
|
|
18
|
+
function parseBoolEnv(value, fallback) {
|
|
19
|
+
if (value === undefined || value === null) return fallback;
|
|
20
|
+
const normalized = String(value).trim().toLowerCase();
|
|
21
|
+
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
22
|
+
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isProcessAlive(pid) {
|
|
27
|
+
try {
|
|
28
|
+
process.kill(pid, 0);
|
|
29
|
+
return true;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getDaemonPid() {
|
|
36
|
+
try {
|
|
37
|
+
if (!existsSync(DAEMON_PID_FILE)) return null;
|
|
38
|
+
const pid = parseInt(readFileSync(DAEMON_PID_FILE, "utf8").trim(), 10);
|
|
39
|
+
if (!Number.isFinite(pid)) return null;
|
|
40
|
+
return isProcessAlive(pid) ? pid : null;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function findGhostDaemonPids() {
|
|
47
|
+
if (process.platform === "win32") return [];
|
|
48
|
+
try {
|
|
49
|
+
const out = execFileSync(
|
|
50
|
+
"pgrep",
|
|
51
|
+
["-f", "bosun.*--daemon-child|cli\\.mjs.*--daemon-child"],
|
|
52
|
+
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 3000 },
|
|
53
|
+
).trim();
|
|
54
|
+
return out
|
|
55
|
+
.split("\n")
|
|
56
|
+
.map((s) => parseInt(s.trim(), 10))
|
|
57
|
+
.filter((n) => Number.isFinite(n) && n > 0 && n !== process.pid);
|
|
58
|
+
} catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function waitForDaemon(timeoutMs = 5000) {
|
|
64
|
+
const deadline = Date.now() + timeoutMs;
|
|
65
|
+
while (Date.now() < deadline) {
|
|
66
|
+
const pid = getDaemonPid();
|
|
67
|
+
if (pid) return pid;
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolveBosunRoot() {
|
|
74
|
+
if (app.isPackaged) {
|
|
75
|
+
return resolve(process.resourcesPath, "bosun");
|
|
76
|
+
}
|
|
77
|
+
return resolve(__dirname, "..");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveBosunRuntimePath(file) {
|
|
81
|
+
return resolve(resolveBosunRoot(), file);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function loadBosunModule(file) {
|
|
85
|
+
const modulePath = resolveBosunRuntimePath(file);
|
|
86
|
+
return import(pathToFileURL(modulePath).href);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function loadUiServerModule() {
|
|
90
|
+
if (uiApi) return uiApi;
|
|
91
|
+
uiApi = await loadBosunModule("ui-server.mjs");
|
|
92
|
+
return uiApi;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function ensureDaemonRunning() {
|
|
96
|
+
const autoStart = parseBoolEnv(
|
|
97
|
+
process.env.BOSUN_DESKTOP_AUTO_START_DAEMON,
|
|
98
|
+
true,
|
|
99
|
+
);
|
|
100
|
+
if (!autoStart) return;
|
|
101
|
+
|
|
102
|
+
const existing = getDaemonPid();
|
|
103
|
+
if (existing) return;
|
|
104
|
+
|
|
105
|
+
const ghosts = findGhostDaemonPids();
|
|
106
|
+
if (ghosts.length > 0) return;
|
|
107
|
+
|
|
108
|
+
const cliPath = resolveBosunRuntimePath("cli.mjs");
|
|
109
|
+
if (!existsSync(cliPath)) {
|
|
110
|
+
console.warn("[desktop] bosun CLI not found; daemon auto-start skipped");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const setupModule = await loadBosunModule("setup.mjs");
|
|
116
|
+
if (setupModule?.shouldRunSetup?.()) {
|
|
117
|
+
console.warn(
|
|
118
|
+
"[desktop] setup required before daemon start; run: bosun --setup",
|
|
119
|
+
);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.warn(
|
|
124
|
+
"[desktop] unable to verify setup state; starting daemon anyway",
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const child = spawn(process.execPath, ["--run-as-node", cliPath, "--daemon"], {
|
|
129
|
+
detached: true,
|
|
130
|
+
stdio: "ignore",
|
|
131
|
+
env: { ...process.env, BOSUN_DESKTOP: "1" },
|
|
132
|
+
cwd: resolveBosunRoot(),
|
|
133
|
+
windowsHide: true,
|
|
134
|
+
});
|
|
135
|
+
child.unref();
|
|
136
|
+
|
|
137
|
+
await waitForDaemon(4000);
|
|
138
|
+
}
|
|
17
139
|
|
|
18
140
|
async function startUiServer() {
|
|
19
141
|
if (uiServerStarted) return;
|
|
20
|
-
const
|
|
142
|
+
const api = await loadUiServerModule();
|
|
143
|
+
const server = await api.startTelegramUiServer({});
|
|
21
144
|
if (!server) {
|
|
22
145
|
throw new Error("Failed to start Telegram UI server.");
|
|
23
146
|
}
|
|
24
147
|
uiServerStarted = true;
|
|
25
148
|
}
|
|
26
149
|
|
|
27
|
-
function buildUiUrl() {
|
|
28
|
-
const
|
|
150
|
+
async function buildUiUrl() {
|
|
151
|
+
const api = await loadUiServerModule();
|
|
152
|
+
const uiServerUrl = api.getTelegramUiUrl();
|
|
29
153
|
if (!uiServerUrl) {
|
|
30
154
|
throw new Error("Telegram UI server URL is unavailable.");
|
|
31
155
|
}
|
|
32
156
|
const targetUrl = new URL(uiServerUrl);
|
|
33
157
|
uiOrigin = targetUrl.origin;
|
|
34
|
-
const sessionToken = getSessionToken();
|
|
158
|
+
const sessionToken = api.getSessionToken();
|
|
35
159
|
if (sessionToken) {
|
|
36
160
|
targetUrl.searchParams.set("token", sessionToken);
|
|
37
161
|
}
|
|
@@ -64,20 +188,40 @@ async function createMainWindow() {
|
|
|
64
188
|
mainWindow?.show();
|
|
65
189
|
});
|
|
66
190
|
|
|
67
|
-
const uiUrl = buildUiUrl();
|
|
191
|
+
const uiUrl = await buildUiUrl();
|
|
68
192
|
await mainWindow.loadURL(uiUrl);
|
|
69
193
|
}
|
|
70
194
|
|
|
71
195
|
async function bootstrap() {
|
|
72
196
|
try {
|
|
197
|
+
app.setAppUserModelId("com.virtengine.bosun");
|
|
198
|
+
process.chdir(resolveBosunRoot());
|
|
199
|
+
await ensureDaemonRunning();
|
|
73
200
|
await startUiServer();
|
|
74
201
|
await createMainWindow();
|
|
202
|
+
await maybeAutoUpdate();
|
|
75
203
|
} catch (error) {
|
|
76
204
|
console.error("[desktop] startup failed", error);
|
|
77
205
|
await shutdown("startup_failed");
|
|
78
206
|
}
|
|
79
207
|
}
|
|
80
208
|
|
|
209
|
+
async function maybeAutoUpdate() {
|
|
210
|
+
if (!app.isPackaged) return;
|
|
211
|
+
if (process.env.BOSUN_DESKTOP_AUTO_UPDATE !== "1") return;
|
|
212
|
+
try {
|
|
213
|
+
const { autoUpdater } = await import("electron-updater");
|
|
214
|
+
const feedUrl = process.env.BOSUN_DESKTOP_UPDATE_URL;
|
|
215
|
+
if (feedUrl) {
|
|
216
|
+
autoUpdater.setFeedURL({ url: feedUrl });
|
|
217
|
+
}
|
|
218
|
+
autoUpdater.autoDownload = true;
|
|
219
|
+
autoUpdater.checkForUpdatesAndNotify().catch(() => {});
|
|
220
|
+
} catch (err) {
|
|
221
|
+
console.warn("[desktop] auto-update unavailable", err?.message || err);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
81
225
|
async function shutdown(reason) {
|
|
82
226
|
if (shuttingDown) return;
|
|
83
227
|
shuttingDown = true;
|
|
@@ -87,7 +231,8 @@ async function shutdown(reason) {
|
|
|
87
231
|
}
|
|
88
232
|
|
|
89
233
|
try {
|
|
90
|
-
|
|
234
|
+
const api = await loadUiServerModule();
|
|
235
|
+
api.stopTelegramUiServer();
|
|
91
236
|
} catch (error) {
|
|
92
237
|
console.error("[desktop] failed to stop ui-server", error);
|
|
93
238
|
}
|
|
@@ -98,7 +243,9 @@ async function shutdown(reason) {
|
|
|
98
243
|
app.on("before-quit", () => {
|
|
99
244
|
shuttingDown = true;
|
|
100
245
|
try {
|
|
101
|
-
stopTelegramUiServer
|
|
246
|
+
if (uiApi?.stopTelegramUiServer) {
|
|
247
|
+
uiApi.stopTelegramUiServer();
|
|
248
|
+
}
|
|
102
249
|
} catch (error) {
|
|
103
250
|
console.error("[desktop] failed to stop ui-server", error);
|
|
104
251
|
}
|