@oss-autopilot/core 0.42.6 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.bundle.cjs +185 -11
- package/dist/commands/dashboard-lifecycle.d.ts +22 -0
- package/dist/commands/dashboard-lifecycle.js +87 -0
- package/dist/commands/dashboard-server.d.ts +14 -0
- package/dist/commands/dashboard-server.js +85 -1
- package/dist/commands/dashboard.d.ts +5 -0
- package/dist/commands/dashboard.js +1 -1
- package/dist/commands/startup.d.ts +1 -1
- package/dist/commands/startup.js +44 -12
- package/dist/formatters/json.d.ts +3 -1
- package/package.json +1 -1
package/dist/cli.bundle.cjs
CHANGED
|
@@ -16301,8 +16301,79 @@ var init_dashboard_templates = __esm({
|
|
|
16301
16301
|
// src/commands/dashboard-server.ts
|
|
16302
16302
|
var dashboard_server_exports = {};
|
|
16303
16303
|
__export(dashboard_server_exports, {
|
|
16304
|
-
|
|
16304
|
+
findRunningDashboardServer: () => findRunningDashboardServer,
|
|
16305
|
+
getDashboardPidPath: () => getDashboardPidPath,
|
|
16306
|
+
isDashboardServerRunning: () => isDashboardServerRunning,
|
|
16307
|
+
readDashboardServerInfo: () => readDashboardServerInfo,
|
|
16308
|
+
removeDashboardServerInfo: () => removeDashboardServerInfo,
|
|
16309
|
+
startDashboardServer: () => startDashboardServer,
|
|
16310
|
+
writeDashboardServerInfo: () => writeDashboardServerInfo
|
|
16305
16311
|
});
|
|
16312
|
+
function getDashboardPidPath() {
|
|
16313
|
+
return path5.join(getDataDir(), "dashboard-server.pid");
|
|
16314
|
+
}
|
|
16315
|
+
function writeDashboardServerInfo(info2) {
|
|
16316
|
+
fs5.writeFileSync(getDashboardPidPath(), JSON.stringify(info2), { mode: 384 });
|
|
16317
|
+
}
|
|
16318
|
+
function readDashboardServerInfo() {
|
|
16319
|
+
try {
|
|
16320
|
+
const content = fs5.readFileSync(getDashboardPidPath(), "utf-8");
|
|
16321
|
+
const parsed = JSON.parse(content);
|
|
16322
|
+
if (typeof parsed !== "object" || parsed === null || typeof parsed.pid !== "number" || typeof parsed.port !== "number" || typeof parsed.startedAt !== "string") {
|
|
16323
|
+
console.error("[DASHBOARD] PID file has invalid structure, ignoring");
|
|
16324
|
+
return null;
|
|
16325
|
+
}
|
|
16326
|
+
return parsed;
|
|
16327
|
+
} catch (err) {
|
|
16328
|
+
const code = err.code;
|
|
16329
|
+
if (code !== "ENOENT") {
|
|
16330
|
+
console.error(`[DASHBOARD] Failed to read PID file: ${err.message}`);
|
|
16331
|
+
}
|
|
16332
|
+
return null;
|
|
16333
|
+
}
|
|
16334
|
+
}
|
|
16335
|
+
function removeDashboardServerInfo() {
|
|
16336
|
+
try {
|
|
16337
|
+
fs5.unlinkSync(getDashboardPidPath());
|
|
16338
|
+
} catch (err) {
|
|
16339
|
+
const code = err.code;
|
|
16340
|
+
if (code !== "ENOENT") {
|
|
16341
|
+
console.error(`[DASHBOARD] Failed to remove PID file: ${err.message}`);
|
|
16342
|
+
}
|
|
16343
|
+
}
|
|
16344
|
+
}
|
|
16345
|
+
function isDashboardServerRunning(port) {
|
|
16346
|
+
return new Promise((resolve5) => {
|
|
16347
|
+
const req = http.get(`http://127.0.0.1:${port}/api/data`, { timeout: 2e3 }, (res) => {
|
|
16348
|
+
res.resume();
|
|
16349
|
+
resolve5(res.statusCode === 200);
|
|
16350
|
+
});
|
|
16351
|
+
req.on("error", () => resolve5(false));
|
|
16352
|
+
req.on("timeout", () => {
|
|
16353
|
+
req.destroy();
|
|
16354
|
+
resolve5(false);
|
|
16355
|
+
});
|
|
16356
|
+
});
|
|
16357
|
+
}
|
|
16358
|
+
async function findRunningDashboardServer() {
|
|
16359
|
+
const info2 = readDashboardServerInfo();
|
|
16360
|
+
if (!info2) return null;
|
|
16361
|
+
try {
|
|
16362
|
+
process.kill(info2.pid, 0);
|
|
16363
|
+
} catch (err) {
|
|
16364
|
+
const code = err.code;
|
|
16365
|
+
if (code !== "ESRCH" && code !== "EPERM") {
|
|
16366
|
+
console.error(`[DASHBOARD] Unexpected error checking PID ${info2.pid}: ${err.message}`);
|
|
16367
|
+
}
|
|
16368
|
+
removeDashboardServerInfo();
|
|
16369
|
+
return null;
|
|
16370
|
+
}
|
|
16371
|
+
if (await isDashboardServerRunning(info2.port)) {
|
|
16372
|
+
return { port: info2.port, url: `http://localhost:${info2.port}` };
|
|
16373
|
+
}
|
|
16374
|
+
removeDashboardServerInfo();
|
|
16375
|
+
return null;
|
|
16376
|
+
}
|
|
16306
16377
|
function buildDashboardJson(digest, state, commentedIssues) {
|
|
16307
16378
|
const prsByRepo = computePRsByRepo(digest, state);
|
|
16308
16379
|
const topRepos = computeTopRepos(prsByRepo);
|
|
@@ -16583,6 +16654,7 @@ async function startDashboardServer(options) {
|
|
|
16583
16654
|
process.exit(1);
|
|
16584
16655
|
}
|
|
16585
16656
|
}
|
|
16657
|
+
writeDashboardServerInfo({ pid: process.pid, port: actualPort, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
16586
16658
|
const serverUrl = `http://localhost:${actualPort}`;
|
|
16587
16659
|
console.error(`Dashboard server running at ${serverUrl}`);
|
|
16588
16660
|
if (open) {
|
|
@@ -16612,6 +16684,7 @@ async function startDashboardServer(options) {
|
|
|
16612
16684
|
}
|
|
16613
16685
|
const shutdown = () => {
|
|
16614
16686
|
console.error("\nShutting down dashboard server...");
|
|
16687
|
+
removeDashboardServerInfo();
|
|
16615
16688
|
server.close(() => {
|
|
16616
16689
|
process.exit(0);
|
|
16617
16690
|
});
|
|
@@ -16649,6 +16722,7 @@ var init_dashboard_server = __esm({
|
|
|
16649
16722
|
// src/commands/dashboard.ts
|
|
16650
16723
|
var dashboard_exports = {};
|
|
16651
16724
|
__export(dashboard_exports, {
|
|
16725
|
+
resolveAssetsDir: () => resolveAssetsDir,
|
|
16652
16726
|
runDashboard: () => runDashboard,
|
|
16653
16727
|
serveDashboard: () => serveDashboard,
|
|
16654
16728
|
writeDashboardFromState: () => writeDashboardFromState
|
|
@@ -17176,6 +17250,78 @@ var init_local_repos = __esm({
|
|
|
17176
17250
|
}
|
|
17177
17251
|
});
|
|
17178
17252
|
|
|
17253
|
+
// src/commands/dashboard-lifecycle.ts
|
|
17254
|
+
function sleep(ms) {
|
|
17255
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
17256
|
+
}
|
|
17257
|
+
async function launchDashboardServer(options) {
|
|
17258
|
+
const assetsDir = resolveAssetsDir();
|
|
17259
|
+
if (!assetsDir) {
|
|
17260
|
+
return null;
|
|
17261
|
+
}
|
|
17262
|
+
const existing = await findRunningDashboardServer();
|
|
17263
|
+
if (existing) {
|
|
17264
|
+
return { url: existing.url, port: existing.port, alreadyRunning: true };
|
|
17265
|
+
}
|
|
17266
|
+
const port = options?.port ?? DEFAULT_PORT;
|
|
17267
|
+
const cliPath = process.argv[1];
|
|
17268
|
+
const child = (0, import_child_process5.spawn)("node", [cliPath, "dashboard", "serve", "--port", String(port), "--no-open"], {
|
|
17269
|
+
detached: true,
|
|
17270
|
+
stdio: "ignore"
|
|
17271
|
+
});
|
|
17272
|
+
let spawnFailed = false;
|
|
17273
|
+
let childExited = false;
|
|
17274
|
+
child.on("error", (err) => {
|
|
17275
|
+
spawnFailed = true;
|
|
17276
|
+
console.error(`[STARTUP] Failed to spawn dashboard server: ${err.message}`);
|
|
17277
|
+
});
|
|
17278
|
+
child.on("exit", (code) => {
|
|
17279
|
+
childExited = true;
|
|
17280
|
+
if (code !== 0 && code !== null) {
|
|
17281
|
+
console.error(`[STARTUP] Dashboard server exited prematurely (code ${code})`);
|
|
17282
|
+
}
|
|
17283
|
+
});
|
|
17284
|
+
child.unref();
|
|
17285
|
+
for (let i = 0; i < MAX_POLL_ATTEMPTS; i++) {
|
|
17286
|
+
await sleep(POLL_INTERVAL_MS);
|
|
17287
|
+
if (spawnFailed || childExited) {
|
|
17288
|
+
return null;
|
|
17289
|
+
}
|
|
17290
|
+
const info2 = readDashboardServerInfo();
|
|
17291
|
+
if (info2) {
|
|
17292
|
+
if (await isDashboardServerRunning(info2.port)) {
|
|
17293
|
+
return { url: `http://localhost:${info2.port}`, port: info2.port, alreadyRunning: false };
|
|
17294
|
+
}
|
|
17295
|
+
}
|
|
17296
|
+
}
|
|
17297
|
+
console.error("[STARTUP] Dashboard server failed to start within 5 seconds");
|
|
17298
|
+
if (child.pid) {
|
|
17299
|
+
try {
|
|
17300
|
+
process.kill(child.pid, "SIGTERM");
|
|
17301
|
+
} catch (err) {
|
|
17302
|
+
const code = err.code;
|
|
17303
|
+
if (code !== "ESRCH") {
|
|
17304
|
+
console.error(
|
|
17305
|
+
`[STARTUP] Failed to kill orphan dashboard process (PID ${child.pid}): ${err.message}`
|
|
17306
|
+
);
|
|
17307
|
+
}
|
|
17308
|
+
}
|
|
17309
|
+
}
|
|
17310
|
+
return null;
|
|
17311
|
+
}
|
|
17312
|
+
var import_child_process5, DEFAULT_PORT, POLL_INTERVAL_MS, MAX_POLL_ATTEMPTS;
|
|
17313
|
+
var init_dashboard_lifecycle = __esm({
|
|
17314
|
+
"src/commands/dashboard-lifecycle.ts"() {
|
|
17315
|
+
"use strict";
|
|
17316
|
+
import_child_process5 = require("child_process");
|
|
17317
|
+
init_dashboard_server();
|
|
17318
|
+
init_dashboard();
|
|
17319
|
+
DEFAULT_PORT = 3e3;
|
|
17320
|
+
POLL_INTERVAL_MS = 200;
|
|
17321
|
+
MAX_POLL_ATTEMPTS = 25;
|
|
17322
|
+
}
|
|
17323
|
+
});
|
|
17324
|
+
|
|
17179
17325
|
// src/commands/startup.ts
|
|
17180
17326
|
var startup_exports = {};
|
|
17181
17327
|
__export(startup_exports, {
|
|
@@ -17243,10 +17389,23 @@ function detectIssueList() {
|
|
|
17243
17389
|
}
|
|
17244
17390
|
}
|
|
17245
17391
|
function openInBrowser(filePath) {
|
|
17246
|
-
|
|
17247
|
-
|
|
17248
|
-
|
|
17249
|
-
|
|
17392
|
+
let openCmd;
|
|
17393
|
+
let args;
|
|
17394
|
+
switch (process.platform) {
|
|
17395
|
+
case "darwin":
|
|
17396
|
+
openCmd = "open";
|
|
17397
|
+
args = [filePath];
|
|
17398
|
+
break;
|
|
17399
|
+
case "win32":
|
|
17400
|
+
openCmd = "cmd";
|
|
17401
|
+
args = ["/c", "start", "", filePath];
|
|
17402
|
+
break;
|
|
17403
|
+
default:
|
|
17404
|
+
openCmd = "xdg-open";
|
|
17405
|
+
args = [filePath];
|
|
17406
|
+
break;
|
|
17407
|
+
}
|
|
17408
|
+
(0, import_child_process6.execFile)(openCmd, args, (error) => {
|
|
17250
17409
|
if (error) {
|
|
17251
17410
|
console.error(`[STARTUP] Failed to open dashboard in browser: ${error.message}`);
|
|
17252
17411
|
}
|
|
@@ -17268,15 +17427,28 @@ async function runStartup() {
|
|
|
17268
17427
|
}
|
|
17269
17428
|
const daily = await executeDailyCheck(token);
|
|
17270
17429
|
let dashboardPath;
|
|
17271
|
-
let dashboardOpened = false;
|
|
17272
17430
|
try {
|
|
17273
17431
|
dashboardPath = writeDashboardFromState();
|
|
17274
|
-
|
|
17432
|
+
} catch (error) {
|
|
17433
|
+
console.error("[STARTUP] Dashboard generation failed:", errorMessage(error));
|
|
17434
|
+
}
|
|
17435
|
+
let dashboardUrl;
|
|
17436
|
+
let dashboardOpened = false;
|
|
17437
|
+
if (daily.digest.summary.totalActivePRs > 0) {
|
|
17438
|
+
let spaResult = null;
|
|
17439
|
+
try {
|
|
17440
|
+
spaResult = await launchDashboardServer();
|
|
17441
|
+
} catch (error) {
|
|
17442
|
+
console.error("[STARTUP] SPA dashboard launch failed:", errorMessage(error));
|
|
17443
|
+
}
|
|
17444
|
+
if (spaResult) {
|
|
17445
|
+
dashboardUrl = spaResult.url;
|
|
17446
|
+
openInBrowser(spaResult.url);
|
|
17447
|
+
dashboardOpened = true;
|
|
17448
|
+
} else if (dashboardPath) {
|
|
17275
17449
|
openInBrowser(dashboardPath);
|
|
17276
17450
|
dashboardOpened = true;
|
|
17277
17451
|
}
|
|
17278
|
-
} catch (error) {
|
|
17279
|
-
console.error("[STARTUP] Dashboard generation failed:", errorMessage(error));
|
|
17280
17452
|
}
|
|
17281
17453
|
if (dashboardOpened) {
|
|
17282
17454
|
daily.briefSummary += " | Dashboard opened in browser";
|
|
@@ -17287,19 +17459,21 @@ async function runStartup() {
|
|
|
17287
17459
|
setupComplete: true,
|
|
17288
17460
|
daily,
|
|
17289
17461
|
dashboardPath,
|
|
17462
|
+
dashboardUrl,
|
|
17290
17463
|
issueList
|
|
17291
17464
|
};
|
|
17292
17465
|
}
|
|
17293
|
-
var fs9,
|
|
17466
|
+
var fs9, import_child_process6;
|
|
17294
17467
|
var init_startup = __esm({
|
|
17295
17468
|
"src/commands/startup.ts"() {
|
|
17296
17469
|
"use strict";
|
|
17297
17470
|
fs9 = __toESM(require("fs"), 1);
|
|
17298
|
-
|
|
17471
|
+
import_child_process6 = require("child_process");
|
|
17299
17472
|
init_core();
|
|
17300
17473
|
init_errors();
|
|
17301
17474
|
init_daily();
|
|
17302
17475
|
init_dashboard();
|
|
17476
|
+
init_dashboard_lifecycle();
|
|
17303
17477
|
}
|
|
17304
17478
|
});
|
|
17305
17479
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard server lifecycle management.
|
|
3
|
+
* Handles launching the interactive SPA dashboard as a background process
|
|
4
|
+
* and detecting whether a server is already running.
|
|
5
|
+
*/
|
|
6
|
+
export interface LaunchResult {
|
|
7
|
+
url: string;
|
|
8
|
+
port: number;
|
|
9
|
+
alreadyRunning: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Launch the interactive dashboard SPA server as a detached background process.
|
|
13
|
+
*
|
|
14
|
+
* Returns the server URL if launched successfully, or null if the SPA assets
|
|
15
|
+
* are not available (caller should fall back to static HTML).
|
|
16
|
+
*
|
|
17
|
+
* If a server is already running (detected via PID file + health probe),
|
|
18
|
+
* returns its URL without launching a new one.
|
|
19
|
+
*/
|
|
20
|
+
export declare function launchDashboardServer(options?: {
|
|
21
|
+
port?: number;
|
|
22
|
+
}): Promise<LaunchResult | null>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard server lifecycle management.
|
|
3
|
+
* Handles launching the interactive SPA dashboard as a background process
|
|
4
|
+
* and detecting whether a server is already running.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import { findRunningDashboardServer, isDashboardServerRunning, readDashboardServerInfo } from './dashboard-server.js';
|
|
8
|
+
import { resolveAssetsDir } from './dashboard.js';
|
|
9
|
+
const DEFAULT_PORT = 3000;
|
|
10
|
+
const POLL_INTERVAL_MS = 200;
|
|
11
|
+
const MAX_POLL_ATTEMPTS = 25; // 5 seconds total
|
|
12
|
+
function sleep(ms) {
|
|
13
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Launch the interactive dashboard SPA server as a detached background process.
|
|
17
|
+
*
|
|
18
|
+
* Returns the server URL if launched successfully, or null if the SPA assets
|
|
19
|
+
* are not available (caller should fall back to static HTML).
|
|
20
|
+
*
|
|
21
|
+
* If a server is already running (detected via PID file + health probe),
|
|
22
|
+
* returns its URL without launching a new one.
|
|
23
|
+
*/
|
|
24
|
+
export async function launchDashboardServer(options) {
|
|
25
|
+
// 1. Check if SPA assets exist
|
|
26
|
+
const assetsDir = resolveAssetsDir();
|
|
27
|
+
if (!assetsDir) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
// 2. Check if a server is already running
|
|
31
|
+
const existing = await findRunningDashboardServer();
|
|
32
|
+
if (existing) {
|
|
33
|
+
return { url: existing.url, port: existing.port, alreadyRunning: true };
|
|
34
|
+
}
|
|
35
|
+
// 3. Launch as detached child process
|
|
36
|
+
const port = options?.port ?? DEFAULT_PORT;
|
|
37
|
+
// process.argv[1] is the CLI entry point (cli.bundle.cjs in production, cli.ts in dev)
|
|
38
|
+
const cliPath = process.argv[1];
|
|
39
|
+
const child = spawn('node', [cliPath, 'dashboard', 'serve', '--port', String(port), '--no-open'], {
|
|
40
|
+
detached: true,
|
|
41
|
+
stdio: 'ignore',
|
|
42
|
+
});
|
|
43
|
+
// Track spawn failures and early exits so the polling loop can bail early
|
|
44
|
+
let spawnFailed = false;
|
|
45
|
+
let childExited = false;
|
|
46
|
+
child.on('error', (err) => {
|
|
47
|
+
spawnFailed = true;
|
|
48
|
+
console.error(`[STARTUP] Failed to spawn dashboard server: ${err.message}`);
|
|
49
|
+
});
|
|
50
|
+
child.on('exit', (code) => {
|
|
51
|
+
childExited = true;
|
|
52
|
+
if (code !== 0 && code !== null) {
|
|
53
|
+
console.error(`[STARTUP] Dashboard server exited prematurely (code ${code})`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
child.unref();
|
|
57
|
+
// 4. Poll for PID file to appear (server writes it after binding)
|
|
58
|
+
for (let i = 0; i < MAX_POLL_ATTEMPTS; i++) {
|
|
59
|
+
await sleep(POLL_INTERVAL_MS);
|
|
60
|
+
// Bail early if spawn failed or child crashed before writing PID file
|
|
61
|
+
if (spawnFailed || childExited) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const info = readDashboardServerInfo();
|
|
65
|
+
if (info) {
|
|
66
|
+
// PID file appeared — verify the server is responding
|
|
67
|
+
if (await isDashboardServerRunning(info.port)) {
|
|
68
|
+
return { url: `http://localhost:${info.port}`, port: info.port, alreadyRunning: false };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// 5. Timeout — server didn't start in time; kill orphan process
|
|
73
|
+
console.error('[STARTUP] Dashboard server failed to start within 5 seconds');
|
|
74
|
+
if (child.pid) {
|
|
75
|
+
try {
|
|
76
|
+
process.kill(child.pid, 'SIGTERM');
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
// ESRCH = process already exited, which is fine
|
|
80
|
+
const code = err.code;
|
|
81
|
+
if (code !== 'ESRCH') {
|
|
82
|
+
console.error(`[STARTUP] Failed to kill orphan dashboard process (PID ${child.pid}): ${err.message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
@@ -11,4 +11,18 @@ export interface DashboardServerOptions {
|
|
|
11
11
|
token: string | null;
|
|
12
12
|
open: boolean;
|
|
13
13
|
}
|
|
14
|
+
export interface DashboardServerInfo {
|
|
15
|
+
pid: number;
|
|
16
|
+
port: number;
|
|
17
|
+
startedAt: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function getDashboardPidPath(): string;
|
|
20
|
+
export declare function writeDashboardServerInfo(info: DashboardServerInfo): void;
|
|
21
|
+
export declare function readDashboardServerInfo(): DashboardServerInfo | null;
|
|
22
|
+
export declare function removeDashboardServerInfo(): void;
|
|
23
|
+
export declare function isDashboardServerRunning(port: number): Promise<boolean>;
|
|
24
|
+
export declare function findRunningDashboardServer(): Promise<{
|
|
25
|
+
port: number;
|
|
26
|
+
url: string;
|
|
27
|
+
} | null>;
|
|
14
28
|
export declare function startDashboardServer(options: DashboardServerOptions): Promise<void>;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import * as http from 'http';
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
|
-
import { getStateManager, getGitHubToken } from '../core/index.js';
|
|
11
|
+
import { getStateManager, getGitHubToken, getDataDir } from '../core/index.js';
|
|
12
12
|
import { errorMessage, ValidationError } from '../core/errors.js';
|
|
13
13
|
import { validateUrl, validateGitHubUrl, validateMessage, PR_URL_PATTERN } from './validation.js';
|
|
14
14
|
import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData } from './dashboard-data.js';
|
|
@@ -25,6 +25,87 @@ const MIME_TYPES = {
|
|
|
25
25
|
'.png': 'image/png',
|
|
26
26
|
'.ico': 'image/x-icon',
|
|
27
27
|
};
|
|
28
|
+
// ── PID File Management ──────────────────────────────────────────────────────
|
|
29
|
+
export function getDashboardPidPath() {
|
|
30
|
+
return path.join(getDataDir(), 'dashboard-server.pid');
|
|
31
|
+
}
|
|
32
|
+
export function writeDashboardServerInfo(info) {
|
|
33
|
+
fs.writeFileSync(getDashboardPidPath(), JSON.stringify(info), { mode: 0o600 });
|
|
34
|
+
}
|
|
35
|
+
export function readDashboardServerInfo() {
|
|
36
|
+
try {
|
|
37
|
+
const content = fs.readFileSync(getDashboardPidPath(), 'utf-8');
|
|
38
|
+
const parsed = JSON.parse(content);
|
|
39
|
+
if (typeof parsed !== 'object' ||
|
|
40
|
+
parsed === null ||
|
|
41
|
+
typeof parsed.pid !== 'number' ||
|
|
42
|
+
typeof parsed.port !== 'number' ||
|
|
43
|
+
typeof parsed.startedAt !== 'string') {
|
|
44
|
+
console.error('[DASHBOARD] PID file has invalid structure, ignoring');
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return parsed;
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
const code = err.code;
|
|
51
|
+
if (code !== 'ENOENT') {
|
|
52
|
+
console.error(`[DASHBOARD] Failed to read PID file: ${err.message}`);
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function removeDashboardServerInfo() {
|
|
58
|
+
try {
|
|
59
|
+
fs.unlinkSync(getDashboardPidPath());
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
const code = err.code;
|
|
63
|
+
if (code !== 'ENOENT') {
|
|
64
|
+
console.error(`[DASHBOARD] Failed to remove PID file: ${err.message}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// ── Health Probe ─────────────────────────────────────────────────────────────
|
|
69
|
+
export function isDashboardServerRunning(port) {
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
const req = http.get(`http://127.0.0.1:${port}/api/data`, { timeout: 2000 }, (res) => {
|
|
72
|
+
// Consume response data to free up memory
|
|
73
|
+
res.resume();
|
|
74
|
+
resolve(res.statusCode === 200);
|
|
75
|
+
});
|
|
76
|
+
req.on('error', () => resolve(false));
|
|
77
|
+
req.on('timeout', () => {
|
|
78
|
+
req.destroy();
|
|
79
|
+
resolve(false);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export async function findRunningDashboardServer() {
|
|
84
|
+
const info = readDashboardServerInfo();
|
|
85
|
+
if (!info)
|
|
86
|
+
return null;
|
|
87
|
+
// Check if process is alive (signal 0 = existence check only)
|
|
88
|
+
try {
|
|
89
|
+
process.kill(info.pid, 0);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
const code = err.code;
|
|
93
|
+
if (code !== 'ESRCH' && code !== 'EPERM') {
|
|
94
|
+
console.error(`[DASHBOARD] Unexpected error checking PID ${info.pid}: ${err.message}`);
|
|
95
|
+
}
|
|
96
|
+
// ESRCH = no process at that PID; EPERM = PID recycled to another user's process
|
|
97
|
+
// Either way, our dashboard server is no longer running — clean up stale PID file
|
|
98
|
+
removeDashboardServerInfo();
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
// Process exists — verify it's actually our server via HTTP probe
|
|
102
|
+
if (await isDashboardServerRunning(info.port)) {
|
|
103
|
+
return { port: info.port, url: `http://localhost:${info.port}` };
|
|
104
|
+
}
|
|
105
|
+
// Process exists but not responding on expected port — stale
|
|
106
|
+
removeDashboardServerInfo();
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
28
109
|
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
29
110
|
/**
|
|
30
111
|
* Build the JSON payload that the SPA expects from GET /api/data.
|
|
@@ -359,6 +440,8 @@ export async function startDashboardServer(options) {
|
|
|
359
440
|
process.exit(1);
|
|
360
441
|
}
|
|
361
442
|
}
|
|
443
|
+
// Write PID file so other processes can detect this running server
|
|
444
|
+
writeDashboardServerInfo({ pid: process.pid, port: actualPort, startedAt: new Date().toISOString() });
|
|
362
445
|
const serverUrl = `http://localhost:${actualPort}`;
|
|
363
446
|
console.error(`Dashboard server running at ${serverUrl}`);
|
|
364
447
|
// ── Open browser ─────────────────────────────────────────────────────────
|
|
@@ -390,6 +473,7 @@ export async function startDashboardServer(options) {
|
|
|
390
473
|
// ── Clean shutdown ───────────────────────────────────────────────────────
|
|
391
474
|
const shutdown = () => {
|
|
392
475
|
console.error('\nShutting down dashboard server...');
|
|
476
|
+
removeDashboardServerInfo();
|
|
393
477
|
server.close(() => {
|
|
394
478
|
process.exit(0);
|
|
395
479
|
});
|
|
@@ -19,5 +19,10 @@ interface ServeOptions {
|
|
|
19
19
|
port: number;
|
|
20
20
|
open: boolean;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the SPA assets directory from packages/dashboard/dist/.
|
|
24
|
+
* Tries multiple strategies to locate it across dev (tsx) and bundled (cjs) modes.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveAssetsDir(): string | null;
|
|
22
27
|
export declare function serveDashboard(options: ServeOptions): Promise<void>;
|
|
23
28
|
export {};
|
|
@@ -138,7 +138,7 @@ export function writeDashboardFromState() {
|
|
|
138
138
|
* Resolve the SPA assets directory from packages/dashboard/dist/.
|
|
139
139
|
* Tries multiple strategies to locate it across dev (tsx) and bundled (cjs) modes.
|
|
140
140
|
*/
|
|
141
|
-
function resolveAssetsDir() {
|
|
141
|
+
export function resolveAssetsDir() {
|
|
142
142
|
// Strategy 1: relative to this source file (works in dev with tsx)
|
|
143
143
|
const devPath = path.resolve(__dirname, '../../dashboard/dist');
|
|
144
144
|
if (fs.existsSync(path.join(devPath, 'index.html'))) {
|
|
@@ -31,7 +31,7 @@ export declare function detectIssueList(): IssueListInfo | undefined;
|
|
|
31
31
|
* Returns StartupOutput with one of three shapes:
|
|
32
32
|
* 1. Setup incomplete: { version, setupComplete: false }
|
|
33
33
|
* 2. Auth failure: { version, setupComplete: true, authError: "..." }
|
|
34
|
-
* 3. Success: { version, setupComplete: true, daily, dashboardPath?, issueList? }
|
|
34
|
+
* 3. Success: { version, setupComplete: true, daily, dashboardPath?, dashboardUrl?, issueList? }
|
|
35
35
|
*
|
|
36
36
|
* Errors from the daily check propagate to the caller.
|
|
37
37
|
*/
|
package/dist/commands/startup.js
CHANGED
|
@@ -12,6 +12,7 @@ import { getStateManager, getGitHubToken, getCLIVersion } from '../core/index.js
|
|
|
12
12
|
import { errorMessage } from '../core/errors.js';
|
|
13
13
|
import { executeDailyCheck } from './daily.js';
|
|
14
14
|
import { writeDashboardFromState } from './dashboard.js';
|
|
15
|
+
import { launchDashboardServer } from './dashboard-lifecycle.js';
|
|
15
16
|
/**
|
|
16
17
|
* Parse issueListPath from a config file's YAML frontmatter.
|
|
17
18
|
* Returns the path string or undefined if not found.
|
|
@@ -93,9 +94,22 @@ export function detectIssueList() {
|
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
function openInBrowser(filePath) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
let openCmd;
|
|
98
|
+
let args;
|
|
99
|
+
switch (process.platform) {
|
|
100
|
+
case 'darwin':
|
|
101
|
+
openCmd = 'open';
|
|
102
|
+
args = [filePath];
|
|
103
|
+
break;
|
|
104
|
+
case 'win32':
|
|
105
|
+
openCmd = 'cmd';
|
|
106
|
+
args = ['/c', 'start', '', filePath];
|
|
107
|
+
break;
|
|
108
|
+
default:
|
|
109
|
+
openCmd = 'xdg-open';
|
|
110
|
+
args = [filePath];
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
99
113
|
execFile(openCmd, args, (error) => {
|
|
100
114
|
if (error) {
|
|
101
115
|
console.error(`[STARTUP] Failed to open dashboard in browser: ${error.message}`);
|
|
@@ -107,7 +121,7 @@ function openInBrowser(filePath) {
|
|
|
107
121
|
* Returns StartupOutput with one of three shapes:
|
|
108
122
|
* 1. Setup incomplete: { version, setupComplete: false }
|
|
109
123
|
* 2. Auth failure: { version, setupComplete: true, authError: "..." }
|
|
110
|
-
* 3. Success: { version, setupComplete: true, daily, dashboardPath?, issueList? }
|
|
124
|
+
* 3. Success: { version, setupComplete: true, daily, dashboardPath?, dashboardUrl?, issueList? }
|
|
111
125
|
*
|
|
112
126
|
* Errors from the daily check propagate to the caller.
|
|
113
127
|
*/
|
|
@@ -129,31 +143,49 @@ export async function runStartup() {
|
|
|
129
143
|
}
|
|
130
144
|
// 3. Run daily check
|
|
131
145
|
const daily = await executeDailyCheck(token);
|
|
132
|
-
// 4. Generate
|
|
133
|
-
// Skip opening on first run (0 PRs) — the welcome flow handles onboarding
|
|
146
|
+
// 4. Generate static HTML dashboard (always — serves as fallback + snapshot)
|
|
134
147
|
let dashboardPath;
|
|
135
|
-
let dashboardOpened = false;
|
|
136
148
|
try {
|
|
137
149
|
dashboardPath = writeDashboardFromState();
|
|
138
|
-
if (daily.digest.summary.totalActivePRs > 0) {
|
|
139
|
-
openInBrowser(dashboardPath);
|
|
140
|
-
dashboardOpened = true;
|
|
141
|
-
}
|
|
142
150
|
}
|
|
143
151
|
catch (error) {
|
|
144
152
|
console.error('[STARTUP] Dashboard generation failed:', errorMessage(error));
|
|
145
153
|
}
|
|
154
|
+
// 5. Launch interactive SPA dashboard (preferred) with static HTML fallback
|
|
155
|
+
// Skip opening on first run (0 PRs) — the welcome flow handles onboarding
|
|
156
|
+
let dashboardUrl;
|
|
157
|
+
let dashboardOpened = false;
|
|
158
|
+
if (daily.digest.summary.totalActivePRs > 0) {
|
|
159
|
+
let spaResult = null;
|
|
160
|
+
try {
|
|
161
|
+
spaResult = await launchDashboardServer();
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
console.error('[STARTUP] SPA dashboard launch failed:', errorMessage(error));
|
|
165
|
+
}
|
|
166
|
+
if (spaResult) {
|
|
167
|
+
dashboardUrl = spaResult.url;
|
|
168
|
+
openInBrowser(spaResult.url);
|
|
169
|
+
dashboardOpened = true;
|
|
170
|
+
}
|
|
171
|
+
else if (dashboardPath) {
|
|
172
|
+
// SPA unavailable (assets not built) — fall back to static HTML
|
|
173
|
+
openInBrowser(dashboardPath);
|
|
174
|
+
dashboardOpened = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
146
177
|
// Append dashboard status to brief summary (only startup opens the browser, not daily)
|
|
147
178
|
if (dashboardOpened) {
|
|
148
179
|
daily.briefSummary += ' | Dashboard opened in browser';
|
|
149
180
|
}
|
|
150
|
-
//
|
|
181
|
+
// 6. Detect issue list
|
|
151
182
|
const issueList = detectIssueList();
|
|
152
183
|
return {
|
|
153
184
|
version,
|
|
154
185
|
setupComplete: true,
|
|
155
186
|
daily,
|
|
156
187
|
dashboardPath,
|
|
188
|
+
dashboardUrl,
|
|
157
189
|
issueList,
|
|
158
190
|
};
|
|
159
191
|
}
|
|
@@ -200,7 +200,7 @@ export interface IssueListInfo {
|
|
|
200
200
|
* Three valid shapes:
|
|
201
201
|
* 1. Setup incomplete: { version, setupComplete: false }
|
|
202
202
|
* 2. Auth failure: { version, setupComplete: true, authError: "..." }
|
|
203
|
-
* 3. Success: { version, setupComplete: true, daily, dashboardPath?, issueList? }
|
|
203
|
+
* 3. Success: { version, setupComplete: true, daily, dashboardPath?, dashboardUrl?, issueList? }
|
|
204
204
|
*/
|
|
205
205
|
export interface StartupOutput {
|
|
206
206
|
version: string;
|
|
@@ -208,6 +208,8 @@ export interface StartupOutput {
|
|
|
208
208
|
authError?: string;
|
|
209
209
|
daily?: DailyOutput;
|
|
210
210
|
dashboardPath?: string;
|
|
211
|
+
/** URL of the interactive SPA dashboard server, when running (e.g., "http://localhost:3000") */
|
|
212
|
+
dashboardUrl?: string;
|
|
211
213
|
issueList?: IssueListInfo;
|
|
212
214
|
}
|
|
213
215
|
/** A single parsed issue from a markdown list (#82) */
|