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 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
- ```bash
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 { secondsLeft, reset: resetCountdown } = useCountdown(interval, !!repo);
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 (_jsxs(Text, { children: [_jsx(Spinner, { type: "dots" }), " Detecting repository..."] }));
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, secondsLeft: secondsLeft })] }));
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,7 @@
1
+ interface Props {
2
+ intervalMs: number;
3
+ enabled: boolean;
4
+ resetKey: number;
5
+ }
6
+ export declare function Countdown({ intervalMs, enabled, resetKey }: Props): import("react/jsx-runtime").JSX.Element | null;
7
+ export {};
@@ -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
- secondsLeft?: number;
4
+ intervalMs: number;
5
+ resetKey: number;
5
6
  }
6
- export declare function Footer({ view, secondsLeft }: Props): import("react/jsx-runtime").JSX.Element;
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, secondsLeft }) {
9
- const showCountdown = secondsLeft !== undefined && (view === "list" || view === "detail");
10
- return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: keys[view] }), showCountdown && (_jsxs(Text, { dimColor: true, children: [" | refresh in ", secondsLeft, "s"] }))] }));
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 }), loading && (_jsxs(Text, { children: [" ", _jsx(Spinner, { type: "dots" })] }))] }));
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 (_jsxs(Text, { children: [_jsx(Spinner, { type: "dots" }), " Loading run details..."] }));
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 && (_jsx(Text, { children: _jsx(Spinner, { type: "dots" }) }))] }, "meta"));
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,4 +1,3 @@
1
- export declare function useCountdown(intervalMs: number, enabled: boolean): {
1
+ export declare function useCountdown(intervalMs: number, enabled: boolean, resetKey: number): {
2
2
  secondsLeft: number;
3
- reset: () => void;
4
3
  };
@@ -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
- const reset = () => {
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-actions-watcher",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Terminal TUI for monitoring GitHub Actions workflow runs",
5
5
  "type": "module",
6
6
  "bin": {