github-actions-watcher 0.2.0 → 0.2.2
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/README.md +9 -1
- package/dist/app.js +4 -5
- package/dist/components/countdown.d.ts +7 -0
- package/dist/components/countdown.js +9 -0
- package/dist/components/footer.d.ts +3 -2
- package/dist/components/footer.js +4 -3
- package/dist/components/header.js +1 -2
- package/dist/components/run-detail.js +2 -3
- package/dist/hooks/use-countdown.d.ts +1 -2
- package/dist/hooks/use-countdown.js +3 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,12 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
A terminal UI for monitoring GitHub Actions workflow runs. Auto-detects the repo from your current directory, shows recent runs, and lets you drill into jobs and steps -- all without leaving the terminal.
|
|
4
4
|
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npx github-actions-watcher
|
|
9
|
+
```
|
|
10
|
+
|
|
5
11
|
## Install
|
|
6
12
|
|
|
7
|
-
```
|
|
13
|
+
```
|
|
8
14
|
npm install -g github-actions-watcher
|
|
9
15
|
```
|
|
10
16
|
|
|
17
|
+
Then run `ghaw` from any directory with a GitHub remote.
|
|
18
|
+
|
|
11
19
|
Requires the [GitHub CLI](https://cli.github.com/) (`gh`) to be installed and authenticated.
|
|
12
20
|
|
|
13
21
|
## Usage
|
package/dist/app.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useCallback, useEffect } from "react";
|
|
3
3
|
import { Box, Text, useApp, useInput } from "ink";
|
|
4
|
-
import Spinner from "ink-spinner";
|
|
5
4
|
import { fetchRuns, fetchRunDetail } from "./lib/gh.js";
|
|
6
5
|
import { useRepo } from "./hooks/use-repo.js";
|
|
7
6
|
import { usePolling } from "./hooks/use-polling.js";
|
|
8
|
-
import { useCountdown } from "./hooks/use-countdown.js";
|
|
9
7
|
import { Header } from "./components/header.js";
|
|
10
8
|
import { Footer } from "./components/footer.js";
|
|
11
9
|
import { RunList } from "./components/run-list.js";
|
|
@@ -28,7 +26,8 @@ export function App({ interval }) {
|
|
|
28
26
|
return fetchRuns(repo);
|
|
29
27
|
}, [repo]);
|
|
30
28
|
const { data: runs, setData: setRuns, loading: runsLoading, error: runsError, refresh: refreshRuns, } = usePolling(runsFetcher, interval, !!repo);
|
|
31
|
-
const
|
|
29
|
+
const [resetKey, setResetKey] = useState(0);
|
|
30
|
+
const resetCountdown = useCallback(() => setResetKey((k) => k + 1), []);
|
|
32
31
|
// Fetch detail
|
|
33
32
|
const fetchDetail = useCallback(async () => {
|
|
34
33
|
if (!repo || selectedRunId === null)
|
|
@@ -101,7 +100,7 @@ export function App({ interval }) {
|
|
|
101
100
|
}, []);
|
|
102
101
|
// Loading state
|
|
103
102
|
if (repoLoading) {
|
|
104
|
-
return (
|
|
103
|
+
return _jsx(Text, { dimColor: true, children: "Detecting repository..." });
|
|
105
104
|
}
|
|
106
105
|
if (repoError && !repo) {
|
|
107
106
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "GitHub Actions Watcher" }) }), _jsx(Text, { color: "yellow", children: "No GitHub repository detected in this directory." }), _jsx(Text, { dimColor: true, children: "To get started, either:" }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: [" cd into a directory with a GitHub remote and run ", _jsx(Text, { bold: true, children: "ghaw" })] }), _jsxs(Text, { children: [" or type a repo below (e.g. ", _jsx(Text, { bold: true, children: "owner/repo" }), ")"] })] }), _jsx(RepoInput, { currentRepo: "", onConfirm: (r) => {
|
|
@@ -111,5 +110,5 @@ export function App({ interval }) {
|
|
|
111
110
|
}
|
|
112
111
|
if (!repo)
|
|
113
112
|
return null;
|
|
114
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { repo: repo, loading: runsLoading || detailLoading }), runsError && view === "list" && (_jsxs(Text, { color: "red", children: ["Error: ", runsError] })), view === "list" && runs && (_jsx(RunList, { runs: runs, selectedIndex: selectedIndex, onSelect: setSelectedIndex, onEnter: handleDrillIn, isActive: view === "list" })), view === "detail" && (_jsx(RunDetailView, { detail: detail, loading: detailLoading, error: detailError, onBack: handleBack, onOpen: handleOpenInBrowser, onRefresh: fetchDetail, isActive: view === "detail", scrollOffset: detailScrollOffset, onScroll: setDetailScrollOffset })), view === "repo-input" && (_jsx(RepoInput, { currentRepo: repo, onConfirm: handleRepoConfirm, onCancel: handleRepoCancel, isActive: view === "repo-input" })), _jsx(Footer, { view: view,
|
|
113
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { repo: repo, loading: runsLoading || detailLoading }), runsError && view === "list" && (_jsxs(Text, { color: "red", children: ["Error: ", runsError] })), view === "list" && runs && (_jsx(RunList, { runs: runs, selectedIndex: selectedIndex, onSelect: setSelectedIndex, onEnter: handleDrillIn, isActive: view === "list" })), view === "detail" && (_jsx(RunDetailView, { detail: detail, loading: detailLoading, error: detailError, onBack: handleBack, onOpen: handleOpenInBrowser, onRefresh: fetchDetail, isActive: view === "detail", scrollOffset: detailScrollOffset, onScroll: setDetailScrollOffset })), view === "repo-input" && (_jsx(RepoInput, { currentRepo: repo, onConfirm: handleRepoConfirm, onCancel: handleRepoCancel, isActive: view === "repo-input" })), _jsx(Footer, { view: view, intervalMs: interval, resetKey: resetKey })] }));
|
|
115
114
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from "ink";
|
|
3
|
+
import { useCountdown } from "../hooks/use-countdown.js";
|
|
4
|
+
export function Countdown({ intervalMs, enabled, resetKey }) {
|
|
5
|
+
const { secondsLeft } = useCountdown(intervalMs, enabled, resetKey);
|
|
6
|
+
if (!enabled)
|
|
7
|
+
return null;
|
|
8
|
+
return _jsxs(Text, { dimColor: true, children: [" | refresh in ", secondsLeft, "s"] });
|
|
9
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { View } from "../lib/types.js";
|
|
2
2
|
interface Props {
|
|
3
3
|
view: View;
|
|
4
|
-
|
|
4
|
+
intervalMs: number;
|
|
5
|
+
resetKey: number;
|
|
5
6
|
}
|
|
6
|
-
export declare function Footer({ view,
|
|
7
|
+
export declare function Footer({ view, intervalMs, resetKey }: Props): import("react/jsx-runtime").JSX.Element;
|
|
7
8
|
export {};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
+
import { Countdown } from "./countdown.js";
|
|
3
4
|
const keys = {
|
|
4
5
|
list: "up/down: navigate | enter: details | s: switch repo | r: refresh | q: quit",
|
|
5
6
|
detail: "up/down: scroll | esc: back | o: open in browser | r: refresh | q: quit",
|
|
6
7
|
"repo-input": "enter: confirm | esc: cancel",
|
|
7
8
|
};
|
|
8
|
-
export function Footer({ view,
|
|
9
|
-
const showCountdown =
|
|
10
|
-
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: keys[view] }), showCountdown && (
|
|
9
|
+
export function Footer({ view, intervalMs, resetKey }) {
|
|
10
|
+
const showCountdown = view === "list" || view === "detail";
|
|
11
|
+
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: keys[view] }), showCountdown && (_jsx(Countdown, { intervalMs: intervalMs, enabled: true, resetKey: resetKey }))] }));
|
|
11
12
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import Spinner from "ink-spinner";
|
|
4
3
|
export function Header({ repo, loading }) {
|
|
5
|
-
return (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "GitHub Actions" }), _jsx(Text, { children: " - " }), _jsx(Text, { bold: true, children: repo }),
|
|
4
|
+
return (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "GitHub Actions" }), _jsx(Text, { children: " - " }), _jsx(Text, { bold: true, children: repo }), _jsx(Text, { dimColor: true, children: loading ? " fetching..." : "" })] }));
|
|
6
5
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
|
-
import Spinner from "ink-spinner";
|
|
4
3
|
import { StatusBadge } from "./status-badge.js";
|
|
5
4
|
import { duration } from "../lib/format.js";
|
|
6
5
|
export function RunDetailView({ detail, loading, error, onBack, onOpen, onRefresh, isActive, scrollOffset, onScroll, }) {
|
|
@@ -22,7 +21,7 @@ export function RunDetailView({ detail, loading, error, onBack, onOpen, onRefres
|
|
|
22
21
|
}
|
|
23
22
|
}, { isActive });
|
|
24
23
|
if (loading && !detail) {
|
|
25
|
-
return (
|
|
24
|
+
return _jsx(Text, { dimColor: true, children: "Loading run details..." });
|
|
26
25
|
}
|
|
27
26
|
if (error) {
|
|
28
27
|
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
@@ -33,7 +32,7 @@ export function RunDetailView({ detail, loading, error, onBack, onOpen, onRefres
|
|
|
33
32
|
// Build flat list of lines to render, then apply scroll offset
|
|
34
33
|
const lines = [];
|
|
35
34
|
lines.push(_jsxs(Box, { gap: 1, children: [_jsx(StatusBadge, { status: detail.status, conclusion: detail.conclusion }), _jsx(Text, { bold: true, children: detail.displayTitle })] }, "title"));
|
|
36
|
-
lines.push(_jsxs(Box, { gap: 1, children: [_jsxs(Text, { dimColor: true, children: [detail.workflowName, " #", detail.number, " on ", detail.headBranch, " (", detail.event, ")"] }), loading &&
|
|
35
|
+
lines.push(_jsxs(Box, { gap: 1, children: [_jsxs(Text, { dimColor: true, children: [detail.workflowName, " #", detail.number, " on ", detail.headBranch, " (", detail.event, ")"] }), loading && _jsx(Text, { dimColor: true, children: " fetching..." })] }, "meta"));
|
|
37
36
|
lines.push(_jsx(Text, { dimColor: true, children: "---" }, "sep"));
|
|
38
37
|
for (const job of detail.jobs) {
|
|
39
38
|
lines.push(_jsxs(Box, { gap: 1, marginTop: 1, children: [_jsx(StatusBadge, { status: job.status, conclusion: job.conclusion }), _jsx(Text, { bold: true, children: job.name }), job.startedAt && (_jsxs(Text, { dimColor: true, children: ["(", duration(job.startedAt, job.completedAt), ")"] }))] }, `job-${job.databaseId}`));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect, useRef } from "react";
|
|
2
|
-
export function useCountdown(intervalMs, enabled) {
|
|
2
|
+
export function useCountdown(intervalMs, enabled, resetKey) {
|
|
3
3
|
const [secondsLeft, setSecondsLeft] = useState(Math.floor(intervalMs / 1000));
|
|
4
4
|
const lastResetRef = useRef(Date.now());
|
|
5
5
|
useEffect(() => {
|
|
@@ -17,10 +17,6 @@ export function useCountdown(intervalMs, enabled) {
|
|
|
17
17
|
}
|
|
18
18
|
}, 1000);
|
|
19
19
|
return () => clearInterval(tick);
|
|
20
|
-
}, [intervalMs, enabled]);
|
|
21
|
-
|
|
22
|
-
lastResetRef.current = Date.now();
|
|
23
|
-
setSecondsLeft(Math.floor(intervalMs / 1000));
|
|
24
|
-
};
|
|
25
|
-
return { secondsLeft, reset };
|
|
20
|
+
}, [intervalMs, enabled, resetKey]);
|
|
21
|
+
return { secondsLeft };
|
|
26
22
|
}
|