@a5c-ai/babysitter-observer-dashboard 1.0.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/LICENSE +21 -0
- package/README.md +490 -0
- package/next.config.mjs +25 -0
- package/package.json +104 -0
- package/postcss.config.mjs +8 -0
- package/src/app/actions/__tests__/approve-breakpoint.test.ts +246 -0
- package/src/app/actions/approve-breakpoint.ts +145 -0
- package/src/app/api/config/route.ts +137 -0
- package/src/app/api/digest/route.ts +45 -0
- package/src/app/api/runs/[runId]/events/route.ts +56 -0
- package/src/app/api/runs/[runId]/route.ts +84 -0
- package/src/app/api/runs/[runId]/tasks/[effectId]/route.ts +44 -0
- package/src/app/api/runs/route.ts +48 -0
- package/src/app/api/stream/route.ts +136 -0
- package/src/app/api/test/route.ts +1 -0
- package/src/app/api/version/route.ts +57 -0
- package/src/app/globals.css +555 -0
- package/src/app/icon.svg +20 -0
- package/src/app/layout.tsx +39 -0
- package/src/app/not-found.tsx +16 -0
- package/src/app/page.tsx +120 -0
- package/src/app/runs/[runId]/page.tsx +279 -0
- package/src/cli.ts +271 -0
- package/src/components/breakpoint/__tests__/breakpoint-approval.test.tsx +212 -0
- package/src/components/breakpoint/__tests__/breakpoint-panel.test.tsx +130 -0
- package/src/components/breakpoint/__tests__/file-preview.test.tsx +313 -0
- package/src/components/breakpoint/breakpoint-approval.tsx +138 -0
- package/src/components/breakpoint/breakpoint-panel.tsx +95 -0
- package/src/components/breakpoint/file-preview.tsx +215 -0
- package/src/components/dashboard/.gitkeep +0 -0
- package/src/components/dashboard/__tests__/breakpoint-banner.test.tsx +177 -0
- package/src/components/dashboard/__tests__/catch-up-banner.test.tsx +141 -0
- package/src/components/dashboard/__tests__/executive-summary-banner.test.tsx +164 -0
- package/src/components/dashboard/__tests__/kpi-grid.test.tsx +101 -0
- package/src/components/dashboard/__tests__/pagination-controls.test.tsx +125 -0
- package/src/components/dashboard/__tests__/project-accordion.test.tsx +97 -0
- package/src/components/dashboard/__tests__/project-list-view.test.tsx +174 -0
- package/src/components/dashboard/__tests__/project-search-input.test.tsx +110 -0
- package/src/components/dashboard/__tests__/project-section-header.test.tsx +91 -0
- package/src/components/dashboard/__tests__/project-section.test.tsx +151 -0
- package/src/components/dashboard/__tests__/run-card.test.tsx +164 -0
- package/src/components/dashboard/__tests__/run-filter-bar.test.tsx +109 -0
- package/src/components/dashboard/__tests__/run-list.test.tsx +123 -0
- package/src/components/dashboard/__tests__/search-filter.test.tsx +150 -0
- package/src/components/dashboard/__tests__/virtualized-run-list.test.tsx +179 -0
- package/src/components/dashboard/breakpoint-banner.tsx +301 -0
- package/src/components/dashboard/catch-up-banner.tsx +88 -0
- package/src/components/dashboard/executive-summary-banner.tsx +174 -0
- package/src/components/dashboard/global-search.tsx +323 -0
- package/src/components/dashboard/kpi-grid.tsx +140 -0
- package/src/components/dashboard/pagination-controls.tsx +100 -0
- package/src/components/dashboard/project-accordion.tsx +72 -0
- package/src/components/dashboard/project-health-card.tsx +536 -0
- package/src/components/dashboard/project-list-view.tsx +246 -0
- package/src/components/dashboard/project-search-input.tsx +41 -0
- package/src/components/dashboard/project-section-header.tsx +73 -0
- package/src/components/dashboard/project-section.tsx +89 -0
- package/src/components/dashboard/run-card.tsx +218 -0
- package/src/components/dashboard/run-filter-bar.tsx +100 -0
- package/src/components/dashboard/run-list.tsx +77 -0
- package/src/components/dashboard/search-filter.tsx +69 -0
- package/src/components/dashboard/virtualized-run-list.tsx +130 -0
- package/src/components/details/.gitkeep +0 -0
- package/src/components/details/__tests__/agent-panel.test.tsx +236 -0
- package/src/components/details/__tests__/json-tree.test.tsx +347 -0
- package/src/components/details/__tests__/log-viewer.test.tsx +168 -0
- package/src/components/details/__tests__/task-detail.test.tsx +212 -0
- package/src/components/details/__tests__/timing-panel.test.tsx +271 -0
- package/src/components/details/agent-panel.tsx +234 -0
- package/src/components/details/json-tree/categorize.ts +131 -0
- package/src/components/details/json-tree/index.tsx +120 -0
- package/src/components/details/json-tree/json-node.tsx +223 -0
- package/src/components/details/json-tree/smart-summary.tsx +596 -0
- package/src/components/details/json-tree/tree-controls.tsx +47 -0
- package/src/components/details/json-tree.tsx +9 -0
- package/src/components/details/log-viewer.tsx +140 -0
- package/src/components/details/task-detail.tsx +114 -0
- package/src/components/details/timing-panel.tsx +247 -0
- package/src/components/events/.gitkeep +0 -0
- package/src/components/events/__tests__/event-item.test.tsx +211 -0
- package/src/components/events/__tests__/event-stream.test.tsx +225 -0
- package/src/components/events/event-item.tsx +121 -0
- package/src/components/events/event-stream.tsx +260 -0
- package/src/components/notifications/.gitkeep +0 -0
- package/src/components/notifications/__tests__/notification-panel.test.tsx +287 -0
- package/src/components/notifications/__tests__/notification-provider.test.tsx +585 -0
- package/src/components/notifications/__tests__/toast-stack.test.tsx +217 -0
- package/src/components/notifications/notification-panel.tsx +124 -0
- package/src/components/notifications/notification-provider.tsx +175 -0
- package/src/components/notifications/toast-stack.tsx +75 -0
- package/src/components/pipeline/.gitkeep +0 -0
- package/src/components/pipeline/__tests__/parallel-group.test.tsx +88 -0
- package/src/components/pipeline/__tests__/pipeline-view.test.tsx +345 -0
- package/src/components/pipeline/__tests__/step-card.test.tsx +330 -0
- package/src/components/pipeline/parallel-group.tsx +39 -0
- package/src/components/pipeline/pipeline-view.tsx +197 -0
- package/src/components/pipeline/step-card.tsx +166 -0
- package/src/components/providers/event-stream-provider.tsx +29 -0
- package/src/components/providers.tsx +24 -0
- package/src/components/shared/.gitkeep +0 -0
- package/src/components/shared/__tests__/empty-state.test.tsx +49 -0
- package/src/components/shared/__tests__/friendly-id.test.tsx +47 -0
- package/src/components/shared/__tests__/kbd.test.tsx +45 -0
- package/src/components/shared/__tests__/kind-badge.test.tsx +71 -0
- package/src/components/shared/__tests__/metrics-row.test.tsx +74 -0
- package/src/components/shared/__tests__/outcome-banner.test.tsx +71 -0
- package/src/components/shared/__tests__/progress-bar.test.tsx +89 -0
- package/src/components/shared/__tests__/session-pill.test.tsx +62 -0
- package/src/components/shared/__tests__/settings-modal.test.tsx +201 -0
- package/src/components/shared/__tests__/shortcuts-help.test.tsx +103 -0
- package/src/components/shared/__tests__/status-badge.test.tsx +98 -0
- package/src/components/shared/__tests__/theme-provider.test.tsx +100 -0
- package/src/components/shared/__tests__/truncated-id.test.tsx +53 -0
- package/src/components/shared/app-footer.tsx +80 -0
- package/src/components/shared/app-header.tsx +160 -0
- package/src/components/shared/empty-state.tsx +18 -0
- package/src/components/shared/error-boundary.tsx +81 -0
- package/src/components/shared/friendly-id.tsx +48 -0
- package/src/components/shared/kbd.tsx +15 -0
- package/src/components/shared/kind-badge.tsx +51 -0
- package/src/components/shared/metrics-row.tsx +106 -0
- package/src/components/shared/outcome-banner.tsx +56 -0
- package/src/components/shared/progress-bar.tsx +42 -0
- package/src/components/shared/session-pill.tsx +69 -0
- package/src/components/shared/settings-modal.tsx +509 -0
- package/src/components/shared/shortcuts-help.tsx +113 -0
- package/src/components/shared/status-badge.tsx +110 -0
- package/src/components/shared/theme-provider.tsx +46 -0
- package/src/components/shared/truncated-id.tsx +51 -0
- package/src/components/ui/.gitkeep +0 -0
- package/src/components/ui/__tests__/accordion.test.tsx +96 -0
- package/src/components/ui/__tests__/badge.test.tsx +69 -0
- package/src/components/ui/__tests__/button.test.tsx +113 -0
- package/src/components/ui/__tests__/tabs.test.tsx +75 -0
- package/src/components/ui/__tests__/tooltip.test.tsx +90 -0
- package/src/components/ui/accordion.tsx +61 -0
- package/src/components/ui/badge.tsx +25 -0
- package/src/components/ui/button.tsx +40 -0
- package/src/components/ui/card.tsx +21 -0
- package/src/components/ui/scroll-area.tsx +35 -0
- package/src/components/ui/separator.tsx +24 -0
- package/src/components/ui/tabs.tsx +64 -0
- package/src/components/ui/tooltip.tsx +37 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/__tests__/use-animated-number.test.ts +184 -0
- package/src/hooks/__tests__/use-batched-updates.test.ts +315 -0
- package/src/hooks/__tests__/use-event-stream.test.ts +243 -0
- package/src/hooks/__tests__/use-keyboard.test.ts +217 -0
- package/src/hooks/__tests__/use-notifications.test.ts +230 -0
- package/src/hooks/__tests__/use-polling.test.ts +274 -0
- package/src/hooks/__tests__/use-project-runs.test.ts +163 -0
- package/src/hooks/__tests__/use-projects.test.ts +248 -0
- package/src/hooks/__tests__/use-run-dashboard.test.ts +168 -0
- package/src/hooks/__tests__/use-run-detail.test.ts +273 -0
- package/src/hooks/__tests__/use-smart-polling.test.ts +305 -0
- package/src/hooks/use-animated-number.ts +87 -0
- package/src/hooks/use-batched-updates.ts +150 -0
- package/src/hooks/use-event-stream.ts +150 -0
- package/src/hooks/use-keyboard.ts +45 -0
- package/src/hooks/use-notifications.ts +82 -0
- package/src/hooks/use-persisted-state.ts +60 -0
- package/src/hooks/use-polling.ts +60 -0
- package/src/hooks/use-project-runs.ts +51 -0
- package/src/hooks/use-projects.ts +26 -0
- package/src/hooks/use-run-dashboard.ts +207 -0
- package/src/hooks/use-run-detail.ts +77 -0
- package/src/hooks/use-smart-polling.ts +144 -0
- package/src/lib/.gitkeep +0 -0
- package/src/lib/__tests__/cn.test.ts +69 -0
- package/src/lib/__tests__/config-loader.test.ts +210 -0
- package/src/lib/__tests__/config.test.ts +561 -0
- package/src/lib/__tests__/error-handler.test.ts +143 -0
- package/src/lib/__tests__/fetcher.test.ts +517 -0
- package/src/lib/__tests__/global-registry.test.ts +214 -0
- package/src/lib/__tests__/parser.test.ts +1532 -0
- package/src/lib/__tests__/path-resolver.test.ts +112 -0
- package/src/lib/__tests__/run-cache.test.ts +591 -0
- package/src/lib/__tests__/server-init.test.ts +512 -0
- package/src/lib/__tests__/source-discovery.test.ts +246 -0
- package/src/lib/__tests__/utils.test.ts +160 -0
- package/src/lib/__tests__/watcher.test.ts +227 -0
- package/src/lib/cn.ts +6 -0
- package/src/lib/config-loader.ts +195 -0
- package/src/lib/config.ts +20 -0
- package/src/lib/error-handler.ts +76 -0
- package/src/lib/fetcher.ts +394 -0
- package/src/lib/global-registry.ts +117 -0
- package/src/lib/parser.ts +794 -0
- package/src/lib/path-resolver.ts +16 -0
- package/src/lib/run-cache.ts +404 -0
- package/src/lib/server-init.ts +226 -0
- package/src/lib/services/__tests__/run-query-service.test.ts +819 -0
- package/src/lib/services/run-query-service.ts +286 -0
- package/src/lib/source-discovery.ts +216 -0
- package/src/lib/utils.ts +103 -0
- package/src/lib/watcher.ts +265 -0
- package/src/test/fixtures.ts +269 -0
- package/src/test/mocks/handlers.ts +110 -0
- package/src/test/mocks/server.ts +17 -0
- package/src/test/setup.ts +200 -0
- package/src/test/test-utils.tsx +36 -0
- package/src/types/.gitkeep +0 -0
- package/src/types/breakpoint.ts +17 -0
- package/src/types/index.ts +214 -0
- package/tsconfig.json +50 -0
package/src/app/page.tsx
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useRunDashboard } from "@/hooks/use-run-dashboard";
|
|
3
|
+
import { BreakpointBanner } from "@/components/dashboard/breakpoint-banner";
|
|
4
|
+
import { CatchUpBanner } from "@/components/dashboard/catch-up-banner";
|
|
5
|
+
import { ExecutiveSummaryBanner } from "@/components/dashboard/executive-summary-banner";
|
|
6
|
+
import { KpiGrid } from "@/components/dashboard/kpi-grid";
|
|
7
|
+
import { RunFilterBar } from "@/components/dashboard/run-filter-bar";
|
|
8
|
+
import { ProjectListView } from "@/components/dashboard/project-list-view";
|
|
9
|
+
import { ErrorBoundary } from "@/components/shared/error-boundary";
|
|
10
|
+
import { GlobalSearch } from "@/components/dashboard/global-search";
|
|
11
|
+
|
|
12
|
+
export default function DashboardPage() {
|
|
13
|
+
const {
|
|
14
|
+
projects,
|
|
15
|
+
loading,
|
|
16
|
+
error,
|
|
17
|
+
metrics,
|
|
18
|
+
allBreakpointRuns,
|
|
19
|
+
summaryMetrics,
|
|
20
|
+
bannerFingerprint,
|
|
21
|
+
bannerDismissed,
|
|
22
|
+
filterCounts,
|
|
23
|
+
filteredProjects,
|
|
24
|
+
activeProjects,
|
|
25
|
+
historyProjects,
|
|
26
|
+
statusFilter,
|
|
27
|
+
sortMode,
|
|
28
|
+
historyCollapsed,
|
|
29
|
+
cardStatusFilter,
|
|
30
|
+
hasStaleRuns,
|
|
31
|
+
catchUp,
|
|
32
|
+
setStatusFilter,
|
|
33
|
+
setSortMode,
|
|
34
|
+
setHistoryCollapsed,
|
|
35
|
+
setDismissedFingerprint,
|
|
36
|
+
toggleMetricFilter,
|
|
37
|
+
handleHideProject,
|
|
38
|
+
} = useRunDashboard();
|
|
39
|
+
|
|
40
|
+
const showBanners = !loading && !error && projects.length > 0;
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="bg-gradient-brand flex-1">
|
|
44
|
+
<div className="mx-auto max-w-[1600px] px-6 py-6">
|
|
45
|
+
{/* Global Search */}
|
|
46
|
+
<GlobalSearch />
|
|
47
|
+
|
|
48
|
+
{/* Executive Summary Banner */}
|
|
49
|
+
{showBanners && (
|
|
50
|
+
<ErrorBoundary section="Executive Summary">
|
|
51
|
+
<ExecutiveSummaryBanner
|
|
52
|
+
metrics={summaryMetrics}
|
|
53
|
+
onFilterChange={setStatusFilter}
|
|
54
|
+
dismissed={bannerDismissed}
|
|
55
|
+
onDismiss={() => setDismissedFingerprint(bannerFingerprint)}
|
|
56
|
+
/>
|
|
57
|
+
</ErrorBoundary>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
{/* KPI Metrics Row */}
|
|
61
|
+
{showBanners && (
|
|
62
|
+
<ErrorBoundary section="KPI Metrics">
|
|
63
|
+
<KpiGrid
|
|
64
|
+
metrics={metrics}
|
|
65
|
+
statusFilter={statusFilter}
|
|
66
|
+
hasStaleRuns={hasStaleRuns}
|
|
67
|
+
onToggleFilter={toggleMetricFilter}
|
|
68
|
+
/>
|
|
69
|
+
</ErrorBoundary>
|
|
70
|
+
)}
|
|
71
|
+
|
|
72
|
+
{/* Catch-up mode banner — shown when burst of SSE updates detected */}
|
|
73
|
+
{catchUp.active && (
|
|
74
|
+
<CatchUpBanner
|
|
75
|
+
catchUp={catchUp}
|
|
76
|
+
summary={{
|
|
77
|
+
failedRuns: summaryMetrics.failedRuns,
|
|
78
|
+
completedRuns: summaryMetrics.completedRuns,
|
|
79
|
+
pendingBreakpoints: summaryMetrics.pendingBreakpoints,
|
|
80
|
+
}}
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
{/* Global Breakpoint Banner — pinned with sticky positioning */}
|
|
85
|
+
{!loading && !error && allBreakpointRuns.length > 0 && (
|
|
86
|
+
<ErrorBoundary section="Breakpoint Banner">
|
|
87
|
+
<div className="sticky top-0 z-40">
|
|
88
|
+
<BreakpointBanner breakpointRuns={allBreakpointRuns} />
|
|
89
|
+
</div>
|
|
90
|
+
</ErrorBoundary>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{/* Filter pills + sort toggle */}
|
|
94
|
+
<RunFilterBar
|
|
95
|
+
statusFilter={statusFilter}
|
|
96
|
+
onStatusFilterChange={setStatusFilter}
|
|
97
|
+
filterCounts={filterCounts}
|
|
98
|
+
sortMode={sortMode}
|
|
99
|
+
onSortModeToggle={() => setSortMode((prev) => prev === "status" ? "activity" : "status")}
|
|
100
|
+
filteredProjectCount={filteredProjects.length}
|
|
101
|
+
/>
|
|
102
|
+
|
|
103
|
+
{/* Project cards content */}
|
|
104
|
+
<ProjectListView
|
|
105
|
+
loading={loading}
|
|
106
|
+
error={error}
|
|
107
|
+
filteredProjects={filteredProjects}
|
|
108
|
+
activeProjects={activeProjects}
|
|
109
|
+
historyProjects={historyProjects}
|
|
110
|
+
statusFilter={statusFilter}
|
|
111
|
+
sortMode={sortMode}
|
|
112
|
+
cardStatusFilter={cardStatusFilter}
|
|
113
|
+
historyCollapsed={historyCollapsed}
|
|
114
|
+
onHistoryCollapsedChange={setHistoryCollapsed}
|
|
115
|
+
onHideProject={handleHideProject}
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState, useCallback, useMemo } from "react";
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import dynamic from "next/dynamic";
|
|
5
|
+
import { useRunDetail } from "@/hooks/use-run-detail";
|
|
6
|
+
import { useKeyboard } from "@/hooks/use-keyboard";
|
|
7
|
+
import { OutcomeBanner } from "@/components/shared/outcome-banner";
|
|
8
|
+
import { MetricsRow } from "@/components/shared/metrics-row";
|
|
9
|
+
import { useNotificationContext } from "@/components/notifications/notification-provider";
|
|
10
|
+
import { cn } from "@/lib/cn";
|
|
11
|
+
import { Loader2, X, ArrowLeft } from "lucide-react";
|
|
12
|
+
import type { JournalEvent, EffectRequestedPayload } from "@/types";
|
|
13
|
+
|
|
14
|
+
/* -------------------------------------------------------------------------- */
|
|
15
|
+
/* Loading skeletons for lazy-loaded route panels */
|
|
16
|
+
/* -------------------------------------------------------------------------- */
|
|
17
|
+
|
|
18
|
+
function PipelineSkeleton() {
|
|
19
|
+
return (
|
|
20
|
+
<div className="flex flex-col h-full animate-pulse p-4 space-y-3">
|
|
21
|
+
<div className="h-5 w-40 rounded bg-foreground-muted/10" />
|
|
22
|
+
<div className="h-2 w-full rounded-full bg-foreground-muted/10" />
|
|
23
|
+
<div className="space-y-2 mt-4">
|
|
24
|
+
{[1, 2, 3, 4].map((i) => (
|
|
25
|
+
<div key={i} className="h-16 rounded-lg bg-foreground-muted/10" />
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function EventStreamSkeleton() {
|
|
33
|
+
return (
|
|
34
|
+
<div className="flex flex-col h-full animate-pulse p-3 space-y-2">
|
|
35
|
+
<div className="h-4 w-24 rounded bg-foreground-muted/10" />
|
|
36
|
+
<div className="h-8 w-full rounded bg-foreground-muted/10" />
|
|
37
|
+
<div className="space-y-1.5 mt-2">
|
|
38
|
+
{[1, 2, 3, 4, 5].map((i) => (
|
|
39
|
+
<div key={i} className="h-10 rounded bg-foreground-muted/10" />
|
|
40
|
+
))}
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function DetailPanelSkeleton() {
|
|
47
|
+
return (
|
|
48
|
+
<div className="flex flex-col h-full animate-pulse p-4 space-y-3">
|
|
49
|
+
<div className="h-8 w-full rounded bg-foreground-muted/10" />
|
|
50
|
+
<div className="h-4 w-3/4 rounded bg-foreground-muted/10" />
|
|
51
|
+
<div className="h-4 w-1/2 rounded bg-foreground-muted/10" />
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* -------------------------------------------------------------------------- */
|
|
57
|
+
/* Lazy-loaded heavy route panels */
|
|
58
|
+
/* -------------------------------------------------------------------------- */
|
|
59
|
+
|
|
60
|
+
const PipelineView = dynamic(
|
|
61
|
+
() => import("@/components/pipeline/pipeline-view").then((mod) => ({ default: mod.PipelineView })),
|
|
62
|
+
{ ssr: false, loading: PipelineSkeleton }
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const EventStream = dynamic(
|
|
66
|
+
() => import("@/components/events/event-stream").then((mod) => ({ default: mod.EventStream })),
|
|
67
|
+
{ ssr: false, loading: EventStreamSkeleton }
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const TaskDetailPanel = dynamic(
|
|
71
|
+
() => import("@/components/details/task-detail").then((mod) => ({ default: mod.TaskDetailPanel })),
|
|
72
|
+
{ ssr: false, loading: DetailPanelSkeleton }
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
export default function RunDetailPage({ params }: { params: { runId: string } }) {
|
|
76
|
+
const { runId } = params;
|
|
77
|
+
const router = useRouter();
|
|
78
|
+
const { run, loading, error, hasBreakpointWaiting: _hasBreakpointWaiting } = useRunDetail(runId);
|
|
79
|
+
const { notifications: _notifications, dismiss: _dismiss, notify: _notify } = useNotificationContext();
|
|
80
|
+
const [selectedEffectId, setSelectedEffectId] = useState<string | null>(null);
|
|
81
|
+
const [showDetail, setShowDetail] = useState(false);
|
|
82
|
+
const [showEventStream, setShowEventStream] = useState(true);
|
|
83
|
+
const [activeTab, setActiveTab] = useState("agent");
|
|
84
|
+
|
|
85
|
+
const handleSelectEffect = useCallback((effectId: string) => {
|
|
86
|
+
setSelectedEffectId(effectId);
|
|
87
|
+
setShowDetail(true);
|
|
88
|
+
|
|
89
|
+
// Auto-switch to breakpoint tab when selecting a breakpoint task
|
|
90
|
+
const task = run?.tasks.find((t) => t.effectId === effectId);
|
|
91
|
+
if (task?.kind === "breakpoint") {
|
|
92
|
+
setActiveTab("breakpoint");
|
|
93
|
+
}
|
|
94
|
+
}, [run?.tasks]);
|
|
95
|
+
|
|
96
|
+
const handleEventClick = useCallback((event: JournalEvent) => {
|
|
97
|
+
const payload = event.payload as unknown as EffectRequestedPayload;
|
|
98
|
+
if (payload?.effectId) {
|
|
99
|
+
handleSelectEffect(payload.effectId);
|
|
100
|
+
}
|
|
101
|
+
}, [handleSelectEffect]);
|
|
102
|
+
|
|
103
|
+
const tasks = useMemo(() => run?.tasks || [], [run?.tasks]);
|
|
104
|
+
|
|
105
|
+
// Determine if the currently selected task is a waiting breakpoint
|
|
106
|
+
const selectedTask = useMemo(() => {
|
|
107
|
+
if (!selectedEffectId) return null;
|
|
108
|
+
return tasks.find((t) => t.effectId === selectedEffectId) || null;
|
|
109
|
+
}, [tasks, selectedEffectId]);
|
|
110
|
+
|
|
111
|
+
const moveDown = useCallback(() => {
|
|
112
|
+
if (!tasks.length) return;
|
|
113
|
+
const currentIdx = selectedEffectId
|
|
114
|
+
? tasks.findIndex((t) => t.effectId === selectedEffectId)
|
|
115
|
+
: -1;
|
|
116
|
+
const nextIdx = Math.min(currentIdx + 1, tasks.length - 1);
|
|
117
|
+
handleSelectEffect(tasks[nextIdx].effectId);
|
|
118
|
+
}, [tasks, selectedEffectId, handleSelectEffect]);
|
|
119
|
+
|
|
120
|
+
const moveUp = useCallback(() => {
|
|
121
|
+
if (!tasks.length) return;
|
|
122
|
+
const currentIdx = selectedEffectId
|
|
123
|
+
? tasks.findIndex((t) => t.effectId === selectedEffectId)
|
|
124
|
+
: tasks.length;
|
|
125
|
+
const prevIdx = Math.max(currentIdx - 1, 0);
|
|
126
|
+
handleSelectEffect(tasks[prevIdx].effectId);
|
|
127
|
+
}, [tasks, selectedEffectId, handleSelectEffect]);
|
|
128
|
+
|
|
129
|
+
const goBack = useCallback(() => {
|
|
130
|
+
if (showDetail) {
|
|
131
|
+
setShowDetail(false);
|
|
132
|
+
setSelectedEffectId(null);
|
|
133
|
+
} else {
|
|
134
|
+
router.push("/");
|
|
135
|
+
}
|
|
136
|
+
}, [showDetail, router]);
|
|
137
|
+
|
|
138
|
+
const toggleEventStream = useCallback(() => {
|
|
139
|
+
setShowEventStream((v) => !v);
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
const openSelected = useCallback(() => {
|
|
143
|
+
if (selectedEffectId && !showDetail) {
|
|
144
|
+
setShowDetail(true);
|
|
145
|
+
} else if (!selectedEffectId && tasks.length > 0) {
|
|
146
|
+
handleSelectEffect(tasks[0].effectId);
|
|
147
|
+
}
|
|
148
|
+
}, [selectedEffectId, showDetail, tasks, handleSelectEffect]);
|
|
149
|
+
|
|
150
|
+
const switchTab = useCallback((key: string) => {
|
|
151
|
+
if (!showDetail) return;
|
|
152
|
+
const tabKeys: Record<string, string> = {
|
|
153
|
+
"1": "agent",
|
|
154
|
+
"2": "timing",
|
|
155
|
+
"3": "logs",
|
|
156
|
+
"4": "data",
|
|
157
|
+
"5": "breakpoint",
|
|
158
|
+
};
|
|
159
|
+
const tab = tabKeys[key];
|
|
160
|
+
if (tab) {
|
|
161
|
+
if (tab === "breakpoint" && selectedTask?.kind !== "breakpoint") return;
|
|
162
|
+
setActiveTab(tab);
|
|
163
|
+
}
|
|
164
|
+
}, [selectedTask, showDetail]);
|
|
165
|
+
|
|
166
|
+
useKeyboard([
|
|
167
|
+
{ key: "j", action: moveDown, description: "Next item" },
|
|
168
|
+
{ key: "k", action: moveUp, description: "Previous item" },
|
|
169
|
+
{ key: "Enter", action: openSelected, description: "Open selected" },
|
|
170
|
+
{ key: "Escape", action: goBack, description: "Go back / Close" },
|
|
171
|
+
{ key: "e", action: toggleEventStream, description: "Toggle event stream" },
|
|
172
|
+
{ key: "1", action: () => switchTab("1"), description: "Agent tab" },
|
|
173
|
+
{ key: "2", action: () => switchTab("2"), description: "Timing tab" },
|
|
174
|
+
{ key: "3", action: () => switchTab("3"), description: "Logs tab" },
|
|
175
|
+
{ key: "4", action: () => switchTab("4"), description: "Data tab" },
|
|
176
|
+
{ key: "5", action: () => switchTab("5"), description: "Approval tab" },
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
if (loading && !run) {
|
|
180
|
+
return (
|
|
181
|
+
<div className="flex items-center justify-center flex-1">
|
|
182
|
+
<Loader2 className="h-6 w-6 animate-spin text-foreground-muted" />
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (error || !run) {
|
|
188
|
+
return (
|
|
189
|
+
<div className="flex items-center justify-center flex-1">
|
|
190
|
+
<div data-testid="run-error-message" className="rounded-lg border border-error/20 bg-error-muted p-4 text-sm text-error">
|
|
191
|
+
{error || "Run not found"}
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<div className="flex flex-col flex-1 bg-background">
|
|
199
|
+
{/* Navigation header with back button */}
|
|
200
|
+
<div className="flex items-center gap-3 px-4 py-2 border-b border-border bg-background-secondary/40">
|
|
201
|
+
<button
|
|
202
|
+
onClick={() => router.push("/")}
|
|
203
|
+
className="inline-flex items-center gap-1.5 rounded-md border border-border px-2.5 py-1 min-h-[44px] text-xs font-medium text-foreground-muted hover:text-foreground hover:border-primary/50 hover:shadow-neon-glow-primary-ring transition-all duration-200"
|
|
204
|
+
>
|
|
205
|
+
<ArrowLeft className="h-3.5 w-3.5" />
|
|
206
|
+
Dashboard
|
|
207
|
+
</button>
|
|
208
|
+
<span className="text-xs text-foreground-muted">/</span>
|
|
209
|
+
<span className="text-xs font-mono text-foreground-secondary">{run.runId.slice(0, 8)}...</span>
|
|
210
|
+
{run.processId && (
|
|
211
|
+
<>
|
|
212
|
+
<span className="text-xs text-foreground-muted">/</span>
|
|
213
|
+
<span className="text-xs text-foreground-secondary">{run.processId}</span>
|
|
214
|
+
</>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
{/* Outcome Banner - Top of page */}
|
|
219
|
+
<OutcomeBanner run={run} />
|
|
220
|
+
|
|
221
|
+
{/* Metrics Row - Summary stats for all run statuses */}
|
|
222
|
+
<MetricsRow run={run} />
|
|
223
|
+
|
|
224
|
+
<div className="flex flex-col lg:flex-row flex-1 overflow-hidden">
|
|
225
|
+
{/* Pipeline - Left panel */}
|
|
226
|
+
<div className={cn(
|
|
227
|
+
"border-b lg:border-b-0 lg:border-r border-border transition-panel bg-card/50 backdrop-blur-sm",
|
|
228
|
+
showDetail ? "lg:w-[35%]" : showEventStream ? "lg:w-[60%]" : "w-full",
|
|
229
|
+
)}>
|
|
230
|
+
<PipelineView
|
|
231
|
+
run={run}
|
|
232
|
+
selectedEffectId={selectedEffectId}
|
|
233
|
+
onSelectEffect={handleSelectEffect}
|
|
234
|
+
runStatus={run.status}
|
|
235
|
+
/>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
{/* Task Detail - Center panel (shown when task selected) */}
|
|
239
|
+
{showDetail && (
|
|
240
|
+
<div className={cn(
|
|
241
|
+
"border-b lg:border-b-0 lg:border-r border-border transition-panel bg-card/50 backdrop-blur-sm overflow-hidden flex flex-col",
|
|
242
|
+
showEventStream ? "lg:w-[30%]" : "lg:w-[65%]",
|
|
243
|
+
)}>
|
|
244
|
+
<div data-testid="task-detail-header" className="flex items-center justify-between p-3 border-b border-border bg-background-secondary/40">
|
|
245
|
+
<h3 className="text-xs font-medium text-foreground-muted uppercase tracking-wider">Task Detail</h3>
|
|
246
|
+
<button
|
|
247
|
+
data-testid="close-detail-btn"
|
|
248
|
+
onClick={() => { setShowDetail(false); setSelectedEffectId(null); }}
|
|
249
|
+
className="inline-flex items-center gap-1.5 rounded-md border border-primary/20 px-2.5 py-1 min-h-[44px] text-xs font-medium text-foreground-muted hover:text-primary hover:border-primary/50 hover:shadow-neon-glow-primary-ring transition-all duration-200"
|
|
250
|
+
>
|
|
251
|
+
<X className="h-3.5 w-3.5" />
|
|
252
|
+
Close
|
|
253
|
+
</button>
|
|
254
|
+
</div>
|
|
255
|
+
<TaskDetailPanel
|
|
256
|
+
runId={runId}
|
|
257
|
+
effectId={selectedEffectId}
|
|
258
|
+
activeTab={activeTab}
|
|
259
|
+
onTabChange={setActiveTab}
|
|
260
|
+
runDuration={run.duration}
|
|
261
|
+
allTasks={run.tasks}
|
|
262
|
+
/>
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
|
|
266
|
+
{/* Event Stream - Right panel */}
|
|
267
|
+
{showEventStream && (
|
|
268
|
+
<div className={cn(
|
|
269
|
+
"transition-panel bg-card/50 backdrop-blur-sm",
|
|
270
|
+
showDetail ? "lg:w-[35%]" : "lg:w-[40%]",
|
|
271
|
+
)}>
|
|
272
|
+
<EventStream events={run.events} onEventClick={handleEventClick} />
|
|
273
|
+
</div>
|
|
274
|
+
)}
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI entry point for the babysitter observer dashboard.
|
|
5
|
+
*
|
|
6
|
+
* Parses command-line flags, maps them to environment variables,
|
|
7
|
+
* then execs into `next dev` or `next start`.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npx ts-node src/cli.ts --port 4800 --watch-dir /tmp/runs --poll-interval 5000 --theme light
|
|
11
|
+
* npx ts-node src/cli.ts --production --port 4800
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { execSync } from "child_process";
|
|
15
|
+
import { readFileSync, existsSync } from "fs";
|
|
16
|
+
import { resolve } from "path";
|
|
17
|
+
|
|
18
|
+
interface CliOptions {
|
|
19
|
+
port?: string;
|
|
20
|
+
watchDir?: string;
|
|
21
|
+
pollInterval?: string;
|
|
22
|
+
theme?: string;
|
|
23
|
+
dev?: boolean;
|
|
24
|
+
help?: boolean;
|
|
25
|
+
version?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Injected at build time by esbuild define. Falls back to reading package.json
|
|
29
|
+
// at runtime for development (ts-node) usage.
|
|
30
|
+
declare const __CLI_VERSION__: string | undefined;
|
|
31
|
+
|
|
32
|
+
function getVersion(): string {
|
|
33
|
+
if (typeof __CLI_VERSION__ !== "undefined") {
|
|
34
|
+
return __CLI_VERSION__;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const pkgPath = resolve(__dirname, "..", "package.json");
|
|
38
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
39
|
+
return pkg.version || "unknown";
|
|
40
|
+
} catch {
|
|
41
|
+
return "unknown";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function printUsage(): void {
|
|
46
|
+
const usage = `
|
|
47
|
+
babysitter-observer-dashboard — CLI for the observer dashboard
|
|
48
|
+
|
|
49
|
+
Usage:
|
|
50
|
+
observer [options]
|
|
51
|
+
|
|
52
|
+
Options:
|
|
53
|
+
--port <number> Port to listen on (default: 4800)
|
|
54
|
+
--watch-dir <path> Directory to watch for .a5c/runs (default: cwd)
|
|
55
|
+
--poll-interval <ms> Polling interval in milliseconds (default: 2000)
|
|
56
|
+
--theme <dark|light> Default UI theme (default: dark)
|
|
57
|
+
--dev Run in dev mode (next dev) instead of production
|
|
58
|
+
--version, -v Show version number
|
|
59
|
+
--help Show this help message
|
|
60
|
+
|
|
61
|
+
Environment variable mapping:
|
|
62
|
+
--port -> OBSERVER_PORT
|
|
63
|
+
--watch-dir -> OBSERVER_WATCH_DIR
|
|
64
|
+
--poll-interval -> OBSERVER_POLL_INTERVAL
|
|
65
|
+
--theme -> OBSERVER_DEFAULT_THEME
|
|
66
|
+
|
|
67
|
+
Examples:
|
|
68
|
+
# Start dashboard on port 3002 watching a specific directory
|
|
69
|
+
observer --port 3002 --watch-dir /home/user/projects
|
|
70
|
+
|
|
71
|
+
# Start with light theme
|
|
72
|
+
observer --theme light
|
|
73
|
+
`.trim();
|
|
74
|
+
|
|
75
|
+
console.log(usage);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseArgs(argv: string[]): CliOptions {
|
|
79
|
+
const opts: CliOptions = {};
|
|
80
|
+
// Skip first two entries: node binary and script path
|
|
81
|
+
const args = argv.slice(2);
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < args.length; i++) {
|
|
84
|
+
const arg = args[i];
|
|
85
|
+
|
|
86
|
+
switch (arg) {
|
|
87
|
+
case "--port":
|
|
88
|
+
i++;
|
|
89
|
+
opts.port = args[i];
|
|
90
|
+
if (!opts.port || isNaN(Number(opts.port))) {
|
|
91
|
+
console.error("Error: --port requires a numeric value");
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
|
|
96
|
+
case "--watch-dir":
|
|
97
|
+
i++;
|
|
98
|
+
opts.watchDir = args[i];
|
|
99
|
+
if (!opts.watchDir) {
|
|
100
|
+
console.error("Error: --watch-dir requires a path value");
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
|
|
105
|
+
case "--poll-interval":
|
|
106
|
+
i++;
|
|
107
|
+
opts.pollInterval = args[i];
|
|
108
|
+
if (!opts.pollInterval || isNaN(Number(opts.pollInterval))) {
|
|
109
|
+
console.error("Error: --poll-interval requires a numeric value (ms)");
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
|
|
114
|
+
case "--theme":
|
|
115
|
+
i++;
|
|
116
|
+
opts.theme = args[i];
|
|
117
|
+
if (opts.theme !== "dark" && opts.theme !== "light") {
|
|
118
|
+
console.error('Error: --theme must be "dark" or "light"');
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
|
|
123
|
+
case "--dev":
|
|
124
|
+
opts.dev = true;
|
|
125
|
+
break;
|
|
126
|
+
|
|
127
|
+
case "--production":
|
|
128
|
+
// Legacy flag — production is now the default, so this is a no-op
|
|
129
|
+
break;
|
|
130
|
+
|
|
131
|
+
case "--version":
|
|
132
|
+
case "-v":
|
|
133
|
+
opts.version = true;
|
|
134
|
+
break;
|
|
135
|
+
|
|
136
|
+
case "--help":
|
|
137
|
+
case "-h":
|
|
138
|
+
opts.help = true;
|
|
139
|
+
break;
|
|
140
|
+
|
|
141
|
+
default:
|
|
142
|
+
console.error(`Unknown flag: ${arg}`);
|
|
143
|
+
console.error('Run with --help for usage information.');
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return opts;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Locate the `next` binary across all installation scenarios.
|
|
153
|
+
*
|
|
154
|
+
* npm may hoist dependencies to a parent node_modules (e.g. when this
|
|
155
|
+
* package is installed via npx or as a dependency of another project).
|
|
156
|
+
* We try several strategies before giving up:
|
|
157
|
+
*
|
|
158
|
+
* 1. require.resolve — honours Node's module resolution & hoisting
|
|
159
|
+
* 2. Local node_modules/.bin — classic non-hoisted layout
|
|
160
|
+
* 3. Walk up the directory tree — covers deep-nested workspaces
|
|
161
|
+
* 4. Bare "next" — last resort, assumes it is on $PATH
|
|
162
|
+
*/
|
|
163
|
+
function findNextBin(): string {
|
|
164
|
+
const projectRoot = resolve(__dirname, "..");
|
|
165
|
+
|
|
166
|
+
// Method 1: Use require.resolve to find next's package.json (handles hoisting).
|
|
167
|
+
// We derive the .bin/next wrapper path (not dist/bin/next) because the wrapper
|
|
168
|
+
// is cross-platform: on Unix it has a shebang, on Windows there is a .cmd peer.
|
|
169
|
+
try {
|
|
170
|
+
const nextPkgPath = require.resolve("next/package.json", {
|
|
171
|
+
paths: [projectRoot],
|
|
172
|
+
});
|
|
173
|
+
// nextPkgPath => .../node_modules/next/package.json
|
|
174
|
+
// Go up to the containing node_modules directory, then into .bin
|
|
175
|
+
const nodeModulesDir = resolve(nextPkgPath, "..", "..");
|
|
176
|
+
const binPath = resolve(nodeModulesDir, ".bin", "next");
|
|
177
|
+
if (existsSync(binPath) || existsSync(binPath + ".cmd")) {
|
|
178
|
+
return binPath;
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
// next is not resolvable from projectRoot — continue to fallbacks
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Method 2: Check the local node_modules/.bin (non-hoisted layout)
|
|
185
|
+
const localBin = resolve(projectRoot, "node_modules", ".bin", "next");
|
|
186
|
+
if (existsSync(localBin)) {
|
|
187
|
+
return localBin;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Method 3: Walk up the directory tree looking for node_modules/.bin/next
|
|
191
|
+
let current = projectRoot;
|
|
192
|
+
while (true) {
|
|
193
|
+
const candidate = resolve(current, "node_modules", ".bin", "next");
|
|
194
|
+
if (existsSync(candidate)) {
|
|
195
|
+
return candidate;
|
|
196
|
+
}
|
|
197
|
+
const parent = resolve(current, "..");
|
|
198
|
+
if (parent === current) {
|
|
199
|
+
break; // reached filesystem root
|
|
200
|
+
}
|
|
201
|
+
current = parent;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Method 4: Fall back to bare "next" and hope it is on PATH
|
|
205
|
+
return "next";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function main(): void {
|
|
209
|
+
const opts = parseArgs(process.argv);
|
|
210
|
+
|
|
211
|
+
if (opts.version) {
|
|
212
|
+
console.log(`babysitter-observer-dashboard v${getVersion()}`);
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (opts.help) {
|
|
217
|
+
printUsage();
|
|
218
|
+
process.exit(0);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Map CLI flags to environment variables
|
|
222
|
+
if (opts.port) {
|
|
223
|
+
process.env.OBSERVER_PORT = opts.port;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Default watch directory to the user's cwd (not the package root, which is
|
|
227
|
+
// where Next.js will run). Without this, the config falls back to
|
|
228
|
+
// process.cwd() inside the Next.js process, which points at the package
|
|
229
|
+
// root — useless for npx / global installs.
|
|
230
|
+
process.env.OBSERVER_WATCH_DIR = opts.watchDir || process.cwd();
|
|
231
|
+
|
|
232
|
+
if (opts.pollInterval) {
|
|
233
|
+
process.env.OBSERVER_POLL_INTERVAL = opts.pollInterval;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (opts.theme) {
|
|
237
|
+
process.env.OBSERVER_DEFAULT_THEME = opts.theme;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Determine the Next.js command — use local binary for global installs
|
|
241
|
+
const port = opts.port || process.env.OBSERVER_PORT || "4800";
|
|
242
|
+
const nextBin = findNextBin();
|
|
243
|
+
const nextCmd = opts.dev
|
|
244
|
+
? `"${nextBin}" dev --port ${port}`
|
|
245
|
+
: `"${nextBin}" start --port ${port}`;
|
|
246
|
+
|
|
247
|
+
console.log(`Starting observer: ${nextCmd}`);
|
|
248
|
+
|
|
249
|
+
if (opts.watchDir) {
|
|
250
|
+
console.log(` Watch directory: ${opts.watchDir}`);
|
|
251
|
+
}
|
|
252
|
+
if (opts.pollInterval) {
|
|
253
|
+
console.log(` Poll interval: ${opts.pollInterval}ms`);
|
|
254
|
+
}
|
|
255
|
+
if (opts.theme) {
|
|
256
|
+
console.log(` Theme: ${opts.theme}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
execSync(nextCmd, {
|
|
261
|
+
env: process.env,
|
|
262
|
+
stdio: "inherit",
|
|
263
|
+
cwd: resolve(__dirname, ".."),
|
|
264
|
+
});
|
|
265
|
+
} catch {
|
|
266
|
+
// next dev exits with non-zero on SIGINT/SIGTERM — that is normal
|
|
267
|
+
process.exit(0);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
main();
|