loop-task 1.1.0 → 1.3.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.
Files changed (117) hide show
  1. package/README.md +132 -50
  2. package/dist/board/App.js +202 -0
  3. package/dist/board/board-log.js +18 -0
  4. package/dist/board/components/ActionButtons.js +25 -0
  5. package/dist/board/components/BoardButton.js +58 -0
  6. package/dist/board/components/ConfirmModal.js +36 -0
  7. package/dist/board/components/CreateForm.js +155 -0
  8. package/dist/board/components/DetailView.js +25 -0
  9. package/dist/board/components/FilterBar.js +15 -0
  10. package/dist/board/components/Footer.js +39 -0
  11. package/dist/board/components/Header.js +16 -0
  12. package/dist/board/components/HelpModal.js +35 -0
  13. package/dist/board/components/Inspector.js +12 -0
  14. package/dist/board/components/LogModal.js +27 -0
  15. package/dist/board/components/Navigator.js +68 -0
  16. package/dist/board/components/RunHistory.js +44 -0
  17. package/dist/board/components/Timeline.js +5 -0
  18. package/dist/board/daemon.js +61 -0
  19. package/dist/board/format.js +95 -0
  20. package/dist/board/hooks/useBoardKeybindings.js +191 -0
  21. package/dist/board/hooks/useBreakpoint.js +6 -0
  22. package/dist/board/hooks/useHoverState.js +11 -0
  23. package/dist/board/hooks/useLogStream.js +32 -0
  24. package/dist/board/hooks/useLoopPolling.js +28 -0
  25. package/dist/board/index.js +32 -0
  26. package/dist/board/state.js +95 -0
  27. package/dist/board/toast.js +65 -0
  28. package/dist/board/types.js +1 -0
  29. package/dist/cli.js +80 -94
  30. package/dist/cli.js.map +1 -1
  31. package/dist/client/commands.d.ts +11 -0
  32. package/dist/client/commands.d.ts.map +1 -0
  33. package/dist/client/commands.js +161 -0
  34. package/dist/client/commands.js.map +1 -0
  35. package/dist/client/ipc.d.ts +5 -0
  36. package/dist/client/ipc.d.ts.map +1 -0
  37. package/dist/client/ipc.js +96 -0
  38. package/dist/client/ipc.js.map +1 -0
  39. package/dist/config/constants.js +14 -0
  40. package/dist/config/paths.js +43 -0
  41. package/dist/core/command-runner.js +80 -0
  42. package/dist/core/foreground-loop.js +78 -0
  43. package/dist/core/log-parser.js +29 -0
  44. package/dist/core/log-rotator.js +25 -0
  45. package/dist/core/loop-controller.js +251 -0
  46. package/dist/daemon/daemon-log.js +11 -0
  47. package/dist/daemon/index.d.ts +2 -0
  48. package/dist/daemon/index.d.ts.map +1 -0
  49. package/dist/daemon/index.js +40 -0
  50. package/dist/daemon/index.js.map +1 -0
  51. package/dist/daemon/manager.d.ts +17 -0
  52. package/dist/daemon/manager.d.ts.map +1 -0
  53. package/dist/daemon/manager.js +181 -0
  54. package/dist/daemon/manager.js.map +1 -0
  55. package/dist/daemon/server.d.ts +15 -0
  56. package/dist/daemon/server.d.ts.map +1 -0
  57. package/dist/daemon/server.js +174 -0
  58. package/dist/daemon/server.js.map +1 -0
  59. package/dist/daemon/spawner.d.ts +3 -0
  60. package/dist/daemon/spawner.d.ts.map +1 -0
  61. package/dist/daemon/spawner.js +82 -0
  62. package/dist/daemon/spawner.js.map +1 -0
  63. package/dist/daemon/state.d.ts +15 -0
  64. package/dist/daemon/state.d.ts.map +1 -0
  65. package/dist/daemon/state.js +120 -0
  66. package/dist/daemon/state.js.map +1 -0
  67. package/dist/duration.js +4 -4
  68. package/dist/entry.js +11 -0
  69. package/dist/esm-loader.js +36 -0
  70. package/dist/i18n/en.json +267 -0
  71. package/dist/i18n/index.js +11 -0
  72. package/dist/ipc/handlers/logs-stream.js +65 -0
  73. package/dist/ipc/send.js +5 -0
  74. package/dist/logger.js +0 -1
  75. package/dist/loop-config.d.ts +13 -0
  76. package/dist/loop-config.d.ts.map +1 -0
  77. package/dist/loop-config.js +74 -0
  78. package/dist/loop-config.js.map +1 -0
  79. package/dist/loop.d.ts +43 -2
  80. package/dist/loop.d.ts.map +1 -1
  81. package/dist/loop.js +247 -2
  82. package/dist/loop.js.map +1 -1
  83. package/dist/open-board.d.ts +2 -0
  84. package/dist/open-board.d.ts.map +1 -0
  85. package/dist/open-board.js +19 -0
  86. package/dist/open-board.js.map +1 -0
  87. package/dist/shared/fs-utils.js +11 -0
  88. package/dist/shared/sleep.js +17 -0
  89. package/dist/shared/tail.js +4 -0
  90. package/dist/tui/app.d.ts +57 -0
  91. package/dist/tui/app.d.ts.map +1 -0
  92. package/dist/tui/app.js +167 -0
  93. package/dist/tui/app.js.map +1 -0
  94. package/dist/tui/create-loop.d.ts +3 -0
  95. package/dist/tui/create-loop.d.ts.map +1 -0
  96. package/dist/tui/create-loop.js +90 -0
  97. package/dist/tui/create-loop.js.map +1 -0
  98. package/dist/tui/dashboard.d.ts +2 -0
  99. package/dist/tui/dashboard.d.ts.map +1 -0
  100. package/dist/tui/dashboard.js +45 -0
  101. package/dist/tui/dashboard.js.map +1 -0
  102. package/dist/tui/events.d.ts +3 -0
  103. package/dist/tui/events.d.ts.map +1 -0
  104. package/dist/tui/events.js +244 -0
  105. package/dist/tui/events.js.map +1 -0
  106. package/dist/tui/popup.d.ts +3 -0
  107. package/dist/tui/popup.d.ts.map +1 -0
  108. package/dist/tui/popup.js +58 -0
  109. package/dist/tui/popup.js.map +1 -0
  110. package/dist/tui/render.d.ts +9 -0
  111. package/dist/tui/render.d.ts.map +1 -0
  112. package/dist/tui/render.js +305 -0
  113. package/dist/tui/render.js.map +1 -0
  114. package/dist/types.d.ts +74 -0
  115. package/dist/types.d.ts.map +1 -1
  116. package/dist/types.js +0 -1
  117. package/package.json +64 -55
@@ -0,0 +1,191 @@
1
+ import { useKeyboard } from "@opentui/react";
2
+ import { cycleSortMode, cycleStatusFilter } from "../state.js";
3
+ const PANEL_ORDER = ["search", "status", "sort", "new", "loops", "runs", "actions"];
4
+ const ACTION_COUNT = 4;
5
+ function nextPanel(current, direction) {
6
+ const idx = PANEL_ORDER.indexOf(current);
7
+ return PANEL_ORDER[(idx + (direction === "right" ? 1 : PANEL_ORDER.length - 1)) % PANEL_ORDER.length];
8
+ }
9
+ export function useBoardKeybindings(params) {
10
+ const { confirm, confirmChoice, setConfirm, setConfirmChoice, helpOpen, setHelpOpen, searchActive, setSearchActive, view, setView, setEditTarget, selected, visibleCount, setSelectedIndex, setFilters, setSort, onQuit, destroyLogSocket, logModalRun, setLogModalRun, selectedRunIndex, setSelectedRunIndex, selectedRunCount, focusedPanel, setFocusedPanel, selectedAction, setSelectedAction, onAction, onOpenRunLog, } = params;
11
+ useKeyboard((key) => {
12
+ const name = key.name;
13
+ if (confirm) {
14
+ if (name === "left" || name === "right" || name === "tab") {
15
+ setConfirmChoice((c) => (c === 1 ? 0 : 1));
16
+ return;
17
+ }
18
+ if (name === "y") {
19
+ const action = confirm.action;
20
+ setConfirm(null);
21
+ void action();
22
+ }
23
+ else if (name === "n" || name === "escape") {
24
+ setConfirm(null);
25
+ }
26
+ else if (name === "return" || name === "enter") {
27
+ if (confirmChoice === 1) {
28
+ const action = confirm.action;
29
+ setConfirm(null);
30
+ void action();
31
+ }
32
+ else {
33
+ setConfirm(null);
34
+ }
35
+ }
36
+ return;
37
+ }
38
+ if (logModalRun) {
39
+ if (name === "escape" || name === "q") {
40
+ setLogModalRun(null);
41
+ }
42
+ return;
43
+ }
44
+ if (helpOpen) {
45
+ if (name === "h" || name === "escape") {
46
+ setHelpOpen(false);
47
+ }
48
+ return;
49
+ }
50
+ if (searchActive) {
51
+ if (name === "escape") {
52
+ setSearchActive(false);
53
+ setFocusedPanel("search");
54
+ }
55
+ else if (name === "return" || name === "enter") {
56
+ setSearchActive(false);
57
+ setFocusedPanel("loops");
58
+ }
59
+ return;
60
+ }
61
+ if (view === "create") {
62
+ if (name === "escape") {
63
+ setEditTarget(null);
64
+ setView("board");
65
+ }
66
+ return;
67
+ }
68
+ if (name === "escape") {
69
+ destroyLogSocket();
70
+ onQuit();
71
+ return;
72
+ }
73
+ if (name === "h") {
74
+ setHelpOpen(true);
75
+ return;
76
+ }
77
+ if (name === "n") {
78
+ setEditTarget(null);
79
+ setView("create");
80
+ return;
81
+ }
82
+ if (name === "r") {
83
+ onAction("run");
84
+ return;
85
+ }
86
+ if (name === "p") {
87
+ onAction("pause-resume");
88
+ return;
89
+ }
90
+ if (name === "delete") {
91
+ onAction("delete");
92
+ return;
93
+ }
94
+ if (view === "board") {
95
+ if (name === "left" || name === "right") {
96
+ if (focusedPanel === "actions") {
97
+ setSelectedAction((i) => name === "right"
98
+ ? Math.min(ACTION_COUNT - 1, i + 1)
99
+ : Math.max(0, i - 1));
100
+ }
101
+ else {
102
+ setFocusedPanel((p) => nextPanel(p, name === "right" ? "right" : "left"));
103
+ }
104
+ return;
105
+ }
106
+ if (focusedPanel === "search") {
107
+ if (name === "return" || name === "enter") {
108
+ setSearchActive(true);
109
+ return;
110
+ }
111
+ }
112
+ else if (focusedPanel === "status") {
113
+ if (name === "return" || name === "enter") {
114
+ setFilters((prev) => ({ ...prev, status: cycleStatusFilter(prev.status) }));
115
+ return;
116
+ }
117
+ }
118
+ else if (focusedPanel === "sort") {
119
+ if (name === "return" || name === "enter") {
120
+ setSort((prev) => cycleSortMode(prev));
121
+ return;
122
+ }
123
+ }
124
+ else if (focusedPanel === "new") {
125
+ if (name === "return" || name === "enter") {
126
+ setEditTarget(null);
127
+ setView("create");
128
+ return;
129
+ }
130
+ }
131
+ else if (focusedPanel === "loops") {
132
+ if (name === "up" || name === "k") {
133
+ setSelectedIndex((i) => Math.max(0, i - 1));
134
+ return;
135
+ }
136
+ if (name === "down" || name === "j") {
137
+ setSelectedIndex((i) => Math.min(visibleCount - 1, i + 1));
138
+ return;
139
+ }
140
+ if ((name === "return" || name === "enter") && selected) {
141
+ setEditTarget(selected);
142
+ setView("create");
143
+ return;
144
+ }
145
+ }
146
+ else if (focusedPanel === "runs") {
147
+ if (name === "up" || name === "k") {
148
+ setSelectedRunIndex((i) => Math.max(0, i - 1));
149
+ return;
150
+ }
151
+ if (name === "down" || name === "j") {
152
+ setSelectedRunIndex((i) => Math.min(selectedRunCount - 1, i + 1));
153
+ return;
154
+ }
155
+ if ((name === "return" || name === "enter") && selected && selected.runHistory.length > 0) {
156
+ const runs = selected.runHistory;
157
+ const idx = Math.min(selectedRunIndex, runs.length - 1);
158
+ onOpenRunLog(runs[runs.length - 1 - idx]);
159
+ return;
160
+ }
161
+ }
162
+ else if (focusedPanel === "actions") {
163
+ if (name === "up" || name === "k") {
164
+ setSelectedAction((i) => Math.max(0, i - 1));
165
+ return;
166
+ }
167
+ if (name === "down" || name === "j") {
168
+ setSelectedAction((i) => Math.min(ACTION_COUNT - 1, i + 1));
169
+ return;
170
+ }
171
+ if (name === "return" || name === "enter") {
172
+ const actionKeys = ["pause-resume", "run", "edit", "delete"];
173
+ onAction(actionKeys[selectedAction] ?? "edit");
174
+ return;
175
+ }
176
+ }
177
+ if (name === "/") {
178
+ setSearchActive(true);
179
+ return;
180
+ }
181
+ if (name === "f") {
182
+ setFilters((prev) => ({ ...prev, status: cycleStatusFilter(prev.status) }));
183
+ return;
184
+ }
185
+ if (name === "s") {
186
+ setSort((prev) => cycleSortMode(prev));
187
+ return;
188
+ }
189
+ }
190
+ });
191
+ }
@@ -0,0 +1,6 @@
1
+ import { useTerminalDimensions } from "@opentui/react";
2
+ import { BOARD_BREAKPOINT_WIDTH } from "../../config/constants.js";
3
+ export function useBreakpoint() {
4
+ const { width } = useTerminalDimensions();
5
+ return width < BOARD_BREAKPOINT_WIDTH ? "narrow" : "wide";
6
+ }
@@ -0,0 +1,11 @@
1
+ import { useState } from "react";
2
+ export function useHoverState() {
3
+ const [isHovered, setIsHovered] = useState(false);
4
+ return {
5
+ isHovered,
6
+ hoverProps: {
7
+ onMouseOver: () => setIsHovered(true),
8
+ onMouseOut: () => setIsHovered(false),
9
+ },
10
+ };
11
+ }
@@ -0,0 +1,32 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { streamLogs } from "../daemon.js";
3
+ import { t } from "../../i18n/index.js";
4
+ import { LOG_LINES_MAX } from "../../config/constants.js";
5
+ export function useLogStream(selectedId, view, onError) {
6
+ const [logLines, setLogLines] = useState([]);
7
+ const logSocket = useRef(null);
8
+ useEffect(() => {
9
+ logSocket.current?.destroy();
10
+ logSocket.current = null;
11
+ setLogLines([]);
12
+ if (!selectedId || view !== "board") {
13
+ return;
14
+ }
15
+ setLogLines([t("board.logWaiting")]);
16
+ const socket = streamLogs(selectedId, (line) => setLogLines((prev) => {
17
+ const next = prev[0] === t("board.logWaiting") ? [] : prev;
18
+ return [...next, line].slice(-LOG_LINES_MAX);
19
+ }), onError);
20
+ logSocket.current = socket;
21
+ return () => {
22
+ socket.destroy();
23
+ if (logSocket.current === socket) {
24
+ logSocket.current = null;
25
+ }
26
+ };
27
+ }, [selectedId, view]);
28
+ return {
29
+ logLines,
30
+ destroy: () => logSocket.current?.destroy(),
31
+ };
32
+ }
@@ -0,0 +1,28 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { listLoops } from "../daemon.js";
3
+ import { POLL_MS } from "../../config/constants.js";
4
+ export function useLoopPolling() {
5
+ const [loops, setLoops] = useState([]);
6
+ const [daemonStatus, setDaemonStatus] = useState("starting");
7
+ const lastSerialized = useRef("");
8
+ const refresh = useCallback(async () => {
9
+ try {
10
+ const next = await listLoops();
11
+ const serialized = JSON.stringify(next);
12
+ if (serialized !== lastSerialized.current) {
13
+ lastSerialized.current = serialized;
14
+ setLoops(next);
15
+ }
16
+ setDaemonStatus("connected");
17
+ }
18
+ catch {
19
+ setDaemonStatus("error");
20
+ }
21
+ }, []);
22
+ useEffect(() => {
23
+ void refresh();
24
+ const timer = setInterval(() => void refresh(), POLL_MS);
25
+ return () => clearInterval(timer);
26
+ }, [refresh]);
27
+ return { loops, daemonStatus, refresh };
28
+ }
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx } from "@opentui/react/jsx-runtime";
2
+ import { createCliRenderer } from "@opentui/core";
3
+ import { createRoot } from "@opentui/react";
4
+ import { App } from "./App.js";
5
+ import { boardLog } from "./board-log.js";
6
+ export async function launchBoard() {
7
+ boardLog("board startup");
8
+ process.on("uncaughtException", (error) => {
9
+ boardLog("uncaught exception", error);
10
+ });
11
+ process.on("unhandledRejection", (reason) => {
12
+ boardLog("unhandled rejection", reason);
13
+ });
14
+ const renderer = await createCliRenderer({ exitOnCtrlC: false });
15
+ const quit = () => {
16
+ try {
17
+ renderer.destroy();
18
+ }
19
+ catch {
20
+ // ignore teardown errors
21
+ }
22
+ process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
23
+ process.exit(0);
24
+ };
25
+ try {
26
+ createRoot(renderer).render(_jsx(App, { onQuit: quit }));
27
+ }
28
+ catch (error) {
29
+ boardLog("render failure", error);
30
+ throw error;
31
+ }
32
+ }
@@ -0,0 +1,95 @@
1
+ export const defaultFilters = {
2
+ status: "all",
3
+ intervalBucket: "all",
4
+ recentActivity: "all",
5
+ query: "",
6
+ };
7
+ const statusOrder = {
8
+ running: 0,
9
+ waiting: 1,
10
+ paused: 2,
11
+ stopped: 3,
12
+ };
13
+ function intervalBucketOf(interval) {
14
+ if (interval <= 60_000)
15
+ return "short";
16
+ if (interval <= 3_600_000)
17
+ return "medium";
18
+ return "long";
19
+ }
20
+ function activityBucketOf(loop) {
21
+ const baseline = loop.lastRunAt ?? loop.createdAt;
22
+ const ageMs = Date.now() - new Date(baseline).getTime();
23
+ return ageMs <= 15 * 60_000 ? "active" : "stale";
24
+ }
25
+ function matches(loop, filters) {
26
+ if (filters.status !== "all" && loop.status !== filters.status) {
27
+ return false;
28
+ }
29
+ if (filters.intervalBucket !== "all" &&
30
+ intervalBucketOf(loop.interval) !== filters.intervalBucket) {
31
+ return false;
32
+ }
33
+ if (filters.recentActivity !== "all" &&
34
+ activityBucketOf(loop) !== filters.recentActivity) {
35
+ return false;
36
+ }
37
+ const query = filters.query.trim().toLowerCase();
38
+ if (!query) {
39
+ return true;
40
+ }
41
+ const haystack = [
42
+ loop.id,
43
+ loop.command,
44
+ ...loop.commandArgs,
45
+ loop.description ?? "",
46
+ loop.status,
47
+ loop.intervalHuman,
48
+ ]
49
+ .join(" ")
50
+ .toLowerCase();
51
+ return haystack.includes(query);
52
+ }
53
+ function describeLoop(loop) {
54
+ return loop.description?.trim() || [loop.command, ...loop.commandArgs].join(" ").trim();
55
+ }
56
+ function compare(left, right, sort) {
57
+ if (sort === "description") {
58
+ return describeLoop(left).localeCompare(describeLoop(right));
59
+ }
60
+ if (sort === "created") {
61
+ return right.createdAt.localeCompare(left.createdAt);
62
+ }
63
+ if (sort === "recent") {
64
+ return (right.lastRunAt ?? right.createdAt).localeCompare(left.lastRunAt ?? left.createdAt);
65
+ }
66
+ const byStatus = statusOrder[left.status] - statusOrder[right.status];
67
+ if (byStatus !== 0) {
68
+ return byStatus;
69
+ }
70
+ const leftNext = left.nextRunAt ?? left.lastRunAt ?? left.createdAt;
71
+ const rightNext = right.nextRunAt ?? right.lastRunAt ?? right.createdAt;
72
+ return rightNext.localeCompare(leftNext);
73
+ }
74
+ export function applyLoopFilters(loops, filters, sort) {
75
+ return loops
76
+ .filter((loop) => matches(loop, filters))
77
+ .sort((left, right) => compare(left, right, sort));
78
+ }
79
+ export function cycleSortMode(mode) {
80
+ return mode === "description" ? "status" : mode === "status" ? "recent" : mode === "recent" ? "created" : "description";
81
+ }
82
+ export function cycleStatusFilter(status) {
83
+ switch (status) {
84
+ case "all":
85
+ return "running";
86
+ case "running":
87
+ return "waiting";
88
+ case "waiting":
89
+ return "paused";
90
+ case "paused":
91
+ return "stopped";
92
+ default:
93
+ return "all";
94
+ }
95
+ }
@@ -0,0 +1,65 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
+ import { useCallback, useRef, useState } from "react";
3
+ import { TOAST_MAX, TOAST_TIMEOUT_MS } from "../config/constants.js";
4
+ export function useToasts() {
5
+ const [toasts, setToasts] = useState([]);
6
+ const idRef = useRef(0);
7
+ const timers = useRef(new Map());
8
+ const dismiss = useCallback((id) => {
9
+ setToasts((prev) => prev.filter((t) => t.id !== id));
10
+ const timer = timers.current.get(id);
11
+ if (timer) {
12
+ clearTimeout(timer);
13
+ timers.current.delete(id);
14
+ }
15
+ }, []);
16
+ const push = useCallback((kind, message) => {
17
+ const id = (idRef.current += 1);
18
+ setToasts((prev) => [...prev, { id, kind, message }].slice(-TOAST_MAX));
19
+ timers.current.set(id, setTimeout(() => dismiss(id), TOAST_TIMEOUT_MS));
20
+ }, [dismiss]);
21
+ return { toasts, push, dismiss };
22
+ }
23
+ function toastColor(kind) {
24
+ switch (kind) {
25
+ case "success":
26
+ return "#4ade80";
27
+ case "error":
28
+ return "#f87171";
29
+ default:
30
+ return "#38bdf8";
31
+ }
32
+ }
33
+ function toastIcon(kind) {
34
+ switch (kind) {
35
+ case "success":
36
+ return "✓";
37
+ case "error":
38
+ return "✗";
39
+ default:
40
+ return "ℹ";
41
+ }
42
+ }
43
+ export function ToastStack(props) {
44
+ if (props.toasts.length === 0) {
45
+ return null;
46
+ }
47
+ return (_jsx("box", { style: {
48
+ position: "absolute",
49
+ bottom: 1,
50
+ right: 2,
51
+ flexDirection: "column",
52
+ alignItems: "flex-end",
53
+ zIndex: 50,
54
+ }, children: props.toasts.map((toast) => {
55
+ const color = toastColor(toast.kind);
56
+ return (_jsx("box", { border: true, style: {
57
+ borderColor: color,
58
+ backgroundColor: "#0b1220",
59
+ paddingLeft: 1,
60
+ paddingRight: 1,
61
+ marginTop: 1,
62
+ minWidth: 24,
63
+ }, children: _jsxs("text", { children: [_jsxs("span", { fg: color, children: [toastIcon(toast.kind), " "] }), _jsx("span", { fg: "#e5e7eb", children: toast.message })] }) }, toast.id));
64
+ }) }));
65
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.js CHANGED
@@ -1,99 +1,85 @@
1
1
  #!/usr/bin/env node
2
- import { parseDuration } from "./duration.js";
2
+ import { Command } from "commander";
3
+ import { createRequire } from "node:module";
3
4
  import { Logger } from "./logger.js";
4
- import { runLoop } from "./loop.js";
5
- const args = process.argv.slice(2);
6
- const loopOptions = {
7
- immediate: false,
8
- maxRuns: null,
9
- verbose: false,
10
- };
11
- const commandParts = [];
12
- let i = 0;
13
- while (i < args.length) {
14
- const arg = args[i];
15
- if (arg === "--now") {
16
- loopOptions.immediate = true;
17
- i++;
5
+ import { runLoop } from "./core/foreground-loop.js";
6
+ import { buildLoopOptions } from "./loop-config.js";
7
+ import { startLoop } from "./client/commands.js";
8
+ import { t } from "./i18n/index.js";
9
+ const require = createRequire(import.meta.url);
10
+ const packageJson = require("../package.json");
11
+ const program = new Command();
12
+ program
13
+ .name("loop-task")
14
+ .description(t("cli.programDescription"))
15
+ .version(packageJson.version, "-V, --version");
16
+ program
17
+ .command("start")
18
+ .description(t("cli.startDescription"))
19
+ .action(async () => {
20
+ const { ensureDaemon } = await import("./daemon/spawner.js");
21
+ ensureDaemon();
22
+ console.log(t("cli.daemonStarted"));
23
+ });
24
+ program
25
+ .command("new")
26
+ .description(t("cli.newDescription"))
27
+ .argument("<interval>", t("cli.argInterval"))
28
+ .argument("<command...>", t("cli.argCommand"))
29
+ .option("--now", t("cli.optNow"), false)
30
+ .option("--max-runs <n>", t("cli.optMaxRuns"), parseInt)
31
+ .option("--verbose", t("cli.optVerbose"), false)
32
+ .option("--cwd <dir>", t("cli.optCwd"))
33
+ .action(async (intervalStr, cmdArgs, opts) => {
34
+ const built = buildLoopOptions(intervalStr, cmdArgs[0], cmdArgs.slice(1), {
35
+ ...opts,
36
+ cwd: opts.cwd ?? process.cwd(),
37
+ });
38
+ await startLoop(built.options, built.intervalHuman);
39
+ });
40
+ program
41
+ .command("run")
42
+ .description(t("cli.runDescription"))
43
+ .argument("<interval>", t("cli.argInterval"))
44
+ .argument("<command...>", t("cli.argCommand"))
45
+ .option("--now", t("cli.optNow"))
46
+ .option("--max-runs <n>", t("cli.optMaxRuns"))
47
+ .option("--verbose", t("cli.optVerbose"))
48
+ .option("--cwd <dir>", t("cli.optCwd"))
49
+ .action(async (intervalStr, cmdArgs, opts) => {
50
+ if (!intervalStr || !cmdArgs || cmdArgs.length === 0) {
51
+ program.help();
52
+ return;
18
53
  }
19
- else if (arg === "--max-runs") {
20
- i++;
21
- const n = parseInt(args[i], 10);
22
- if (isNaN(n) || n < 1) {
23
- console.error("Error: --max-runs must be a positive integer");
54
+ const logger = new Logger(opts.verbose ?? false);
55
+ const built = buildLoopOptions(intervalStr, cmdArgs[0], cmdArgs.slice(1), {
56
+ ...opts,
57
+ cwd: opts.cwd ?? process.cwd(),
58
+ });
59
+ const controller = new AbortController();
60
+ process.on("SIGINT", () => controller.abort());
61
+ process.on("SIGTERM", () => controller.abort());
62
+ await runLoop(built.options, logger, controller.signal);
63
+ process.exit(0);
64
+ });
65
+ program.action(async () => {
66
+ if (!process.execPath.includes("bun")) {
67
+ const { spawn } = await import("node:child_process");
68
+ const child = spawn("bun", [process.argv[1]], {
69
+ stdio: "inherit",
70
+ env: { ...process.env },
71
+ shell: true,
72
+ });
73
+ child.on("error", () => {
74
+ console.error("The board requires the Bun runtime for OpenTUI native FFI.\n" +
75
+ "Install Bun: npm install -g bun\n" +
76
+ "Then run: loop-task");
24
77
  process.exit(1);
25
- }
26
- loopOptions.maxRuns = n;
27
- i++;
78
+ });
79
+ child.on("exit", (code) => process.exit(code ?? 0));
80
+ return;
28
81
  }
29
- else if (arg === "--verbose") {
30
- loopOptions.verbose = true;
31
- i++;
32
- }
33
- else if (arg === "--help" || arg === "-h") {
34
- console.log(`
35
- Usage:
36
- loop-task [options] <interval> <command>
37
-
38
- Examples:
39
- loop-task 30m npm test
40
- loop-task 1h opencode --prompt '/ob-init'
41
- loop-task 30s --now -- echo hello
42
- loop-task --max-runs 5 5m npm test
43
-
44
- Options:
45
- --now Run immediately before waiting
46
- --max-runs <n> Stop after N executions
47
- --verbose Show execution details
48
- -h, --help Display help
49
- -V, --version Display version
50
- `);
51
- process.exit(0);
52
- }
53
- else if (arg === "--version" || arg === "-V") {
54
- console.log("1.0.0");
55
- process.exit(0);
56
- }
57
- else if (arg === "--") {
58
- i++;
59
- commandParts.push(...args.slice(i));
60
- break;
61
- }
62
- else {
63
- commandParts.push(arg);
64
- i++;
65
- }
66
- }
67
- if (commandParts.length < 2) {
68
- console.error("Error: missing required arguments");
69
- console.error("Usage: loop-task [options] <interval> <command>");
70
- process.exit(1);
71
- }
72
- const intervalStr = commandParts[0];
73
- const cmdArgs = commandParts.slice(1);
74
- const command = cmdArgs[0];
75
- const commandArgs = cmdArgs.slice(1);
76
- const logger = new Logger(loopOptions.verbose);
77
- let interval;
78
- try {
79
- interval = parseDuration(intervalStr);
80
- }
81
- catch (error) {
82
- const message = error instanceof Error ? error.message : String(error);
83
- logger.error(`Error: ${message}`);
84
- process.exit(1);
85
- }
86
- const options = {
87
- interval,
88
- command,
89
- commandArgs,
90
- immediate: loopOptions.immediate,
91
- maxRuns: loopOptions.maxRuns,
92
- verbose: loopOptions.verbose,
93
- };
94
- const controller = new AbortController();
95
- process.on("SIGINT", () => controller.abort());
96
- process.on("SIGTERM", () => controller.abort());
97
- await runLoop(options, logger, controller.signal);
98
- process.exit(0);
99
- //# sourceMappingURL=cli.js.map
82
+ const { launchBoard } = await import("./board/index.js");
83
+ await launchBoard();
84
+ });
85
+ await program.parseAsync(process.argv);
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}