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,267 @@
1
+ {
2
+ "cli.error": "Error: {message}",
3
+ "cli.programDescription": "Open the loop board or start loops in the background.",
4
+ "cli.startDescription": "Start the background daemon (restores persisted loops)",
5
+ "cli.newDescription": "Create a new loop in the background",
6
+ "cli.runDescription": "Run a loop in the foreground",
7
+ "cli.argInterval": "Interval between runs (e.g. 30s, 5m, 1h)",
8
+ "cli.argCommand": "Command to execute",
9
+ "cli.optNow": "Run immediately before waiting",
10
+ "cli.optMaxRuns": "Stop after N executions",
11
+ "cli.optVerbose": "Show execution details",
12
+ "cli.optCwd": "Working directory for the command",
13
+ "cli.daemonStarted": "Daemon started. Loops restored.",
14
+ "cli.startedTitle": "Loop started in background",
15
+ "cli.startedId": " ID: {id}",
16
+ "cli.startedCommand": " Command: {command}",
17
+ "cli.startedInterval": " Interval: {interval}",
18
+ "cli.startedStatus": " Status: running",
19
+ "cli.startedHint": " loop-task new ... # create another loop\n loop-task # open the board",
20
+ "cli.noLoops": "No background loops running.",
21
+ "cli.headerId": "ID",
22
+ "cli.headerCommand": "COMMAND",
23
+ "cli.headerInterval": "INTERVAL",
24
+ "cli.headerStatus": "STATUS",
25
+ "cli.headerRuns": "RUNS",
26
+ "cli.headerLastExit": "LAST EXIT",
27
+ "cli.statusTitle": "Loop: {id}",
28
+ "cli.statusCommand": " Command: {command}",
29
+ "cli.statusInterval": " Interval: {interval} ({duration})",
30
+ "cli.statusStatus": " Status: {status}",
31
+ "cli.statusRuns": " Runs: {runs} / {maxRuns}",
32
+ "cli.statusCreated": " Created: {created}",
33
+ "cli.statusLastRun": " Last run: {lastRun} {exit} {duration}",
34
+ "cli.statusNextRun": " Next run: {nextRun}",
35
+ "cli.exitInfo": "exit {code}",
36
+ "cli.unlimited": "∞",
37
+ "cli.paused": "Loop {id} paused.",
38
+ "cli.resumed": "Loop {id} resumed.",
39
+ "cli.deleted": "Loop {id} deleted.",
40
+ "cli.attaching": "Attaching to loop {id}... (Ctrl+C to detach)\n",
41
+ "cli.detached": "\nDetached.",
42
+ "loop.runHeader": "\n[Run #{runNumber} — {timestamp}]\n",
43
+ "loop.exitMarker": "[exit {code} · {duration}]\n",
44
+ "loop.cwdMissingLog": "[error] working directory does not exist: {cwd}\n",
45
+ "loop.cwdMissing": "Working directory does not exist: {cwd}",
46
+ "loop.executing": "Executing: {command}",
47
+ "loop.shuttingDown": "\nShutting down gracefully...",
48
+ "loop.completed": "Completed {runs} run(s). Exiting.",
49
+ "loop.waitingFirst": "Waiting {duration} before first run...",
50
+ "loop.runSeparator": "\n--- Run {run}{maxRuns} ---",
51
+ "loop.commandFailed": "Command failed with exit code {code}",
52
+ "loop.debugStartedAt": "Started at: {timestamp}",
53
+ "loop.debugEndedAt": "Ended at: {timestamp}",
54
+ "loop.debugExitCode": "Exit code: {code}",
55
+ "loop.debugDuration": "Duration: {duration}",
56
+ "loop.nextRun": "Next run in {duration} (at {time})",
57
+ "errors.durationEmpty": "Duration cannot be empty",
58
+ "errors.durationInvalid": "Invalid duration: \"{input}\". Use formats like 10s, 5m, 1h, 1d, 1w",
59
+ "errors.durationNotPositive": "Duration must be positive, got: \"{input}\"",
60
+ "errors.maxRunsInvalid": "--max-runs must be a positive integer",
61
+ "errors.unbalancedQuote": "Unbalanced quote in command",
62
+ "errors.commandEmpty": "Command cannot be empty",
63
+ "errors.loopNotFound": "Loop {id} not found",
64
+ "errors.invalidJson": "Invalid JSON",
65
+ "errors.noLogsYet": "No logs yet",
66
+ "errors.invalidResponse": "Invalid response from daemon",
67
+ "errors.connectionClosed": "Connection closed before response",
68
+ "errors.cannotConnect": "Cannot connect to daemon: {message}",
69
+ "errors.requestTimedOut": "Daemon request timed out",
70
+ "errors.daemonFailedToStart": "Daemon failed to start within 5 seconds",
71
+ "errors.daemonUncaught": "Daemon uncaught exception:",
72
+ "errors.daemonFailed": "Daemon failed to start:",
73
+ "errors.pauseFailed": "pause failed",
74
+ "errors.resumeFailed": "resume failed",
75
+ "errors.forceRunFailed": "force run failed",
76
+ "errors.deleteFailed": "delete failed",
77
+ "format.dash": "-",
78
+ "format.justNow": "just now",
79
+ "format.secsAgo": "{secs}s ago",
80
+ "format.minsAgo": "{mins}m ago",
81
+ "format.hrsAgo": "{hrs}h ago",
82
+ "format.daysAgo": "{days}d ago",
83
+ "format.secsAhead": "{secs}s",
84
+ "format.minsAhead": "{mins}m",
85
+ "format.hrsAhead": "{hrs}h",
86
+ "format.daysAhead": "{days}d",
87
+ "format.timingPaused": "paused",
88
+ "format.timingNext": "in {timeAgo}",
89
+ "format.timingLast": "last {timeAgo}",
90
+ "format.timingNew": "new",
91
+ "board.appName": "loop-task",
92
+ "board.appTagline": " background task loops",
93
+ "board.daemonLabel": "Daemon: ",
94
+ "board.loopsLabel": " Loops: ",
95
+ "board.runningLabel": " Running: ",
96
+ "board.waitingLabel": " Waiting: ",
97
+ "board.pausedLabel": " Paused: ",
98
+ "board.searchTitle": " Search / ",
99
+ "board.searchPlaceholder": "type to filter, enter to apply",
100
+ "board.searchEmpty": "press / to search",
101
+ "board.statusFilterTitle": " Status f ",
102
+ "board.sortTitle": " Sort s ",
103
+ "board.newLoopTitle": " New ",
104
+ "board.newLoopLabel": "+ New Loop",
105
+ "board.headerStatus": "STATUS",
106
+ "board.headerDescription": "DESCRIPTION",
107
+ "board.headerTiming": "TIMING",
108
+ "board.headerExitRuns": "EXIT RUNS",
109
+ "board.navigatorTitle": " Loops {visible}/{total} sort:{sort} status:{status} ",
110
+ "board.noMatch": " No loops match the current view",
111
+ "board.inspectorTitle": " Inspector ",
112
+ "board.inspectorEmpty": " Select a loop to view details",
113
+ "board.fieldId": "ID: ",
114
+ "board.fieldDesc": "Desc: ",
115
+ "board.fieldCommand": "Command: ",
116
+ "board.fieldDir": "Dir: ",
117
+ "board.fieldInterval": "Interval: ",
118
+ "board.fieldStatus": "Status: ",
119
+ "board.fieldRuns": "Runs: ",
120
+ "board.fieldLastRun": "Last run: ",
121
+ "board.fieldLastExit": "Last exit:",
122
+ "board.fieldNextRun": "Next run: ",
123
+ "board.fieldPid": "PID: ",
124
+ "board.inherit": "(inherit)",
125
+ "board.unlimited": "∞",
126
+ "board.timelineTitle": " Timeline ",
127
+ "board.timelineEmpty": " No output yet.",
128
+ "board.runHistoryTitle": " Run History ",
129
+ "board.runHistoryEmpty": " No runs yet.",
130
+ "board.runHistoryTime": "TIME",
131
+ "board.runHistoryDesc": "DESCRIPTION",
132
+ "board.runHistoryDuration": "DURATION",
133
+ "board.runHistorySize": "SIZE",
134
+ "board.logModalTitle": " Run Log {icon} {time} ",
135
+ "board.logModalLoading": " Loading...",
136
+ "board.logModalEmpty": " No output captured.",
137
+ "board.logModalEscClose": "esc to close",
138
+ "board.detailTitle": " Loop Detail ",
139
+ "board.detailFieldId": "ID: ",
140
+ "board.detailFieldDesc": "Desc: ",
141
+ "board.detailFieldCommand": "Command: ",
142
+ "board.detailFieldDir": "Dir: ",
143
+ "board.detailFieldInterval": "Interval: ",
144
+ "board.detailFieldStatus": "Status: ",
145
+ "board.detailFieldRuns": "Runs: ",
146
+ "board.detailFieldCreated": "Created: ",
147
+ "board.detailFieldLastRun": "Last run: ",
148
+ "board.detailFieldLastExit": "Last exit: ",
149
+ "board.detailFieldNextRun": "Next run: ",
150
+ "board.detailFieldPid": "PID: ",
151
+ "board.detailSummaryStatus": "status",
152
+ "board.detailSummaryTiming": "next",
153
+ "board.detailSummaryInterval": "every",
154
+ "board.detailSummaryRuns": "runs",
155
+ "board.detailSummaryExit": "exit",
156
+ "board.liveOutputTitle": " Live Output ",
157
+ "board.helpTitle": " Help ",
158
+ "board.helpMoveSelection": "move selection",
159
+ "board.helpToggleDetail": "toggle loop detail",
160
+ "board.helpCreate": "create loop",
161
+ "board.helpEdit": "edit loop (pauses it)",
162
+ "board.helpPauseResume": "pause/resume selected loop",
163
+ "board.helpForceRun": "force run selected loop now",
164
+ "board.helpDelete": "delete selected loop",
165
+ "board.helpSearch": "search loops",
166
+ "board.helpCycleFilter": "cycle status filter",
167
+ "board.helpCycleSort": "cycle sort mode",
168
+ "board.helpRunHistory": "navigate run history",
169
+ "board.helpOpenRunLog": "open run log",
170
+ "board.helpSwitchPanel": "switch panel focus",
171
+ "board.helpKeyLeftRight": "←/→",
172
+ "board.helpToggleHelp": "toggle help",
173
+ "board.helpBack": "back to board",
174
+ "board.helpQuit": "quit",
175
+ "board.labelInterval": "Interval",
176
+ "board.labelCommand": "Command",
177
+ "board.labelDescription": "Description",
178
+ "board.labelCwd": "Working dir",
179
+ "board.labelRunNow": "Run immediately?",
180
+ "board.labelMaxRuns": "Max runs",
181
+ "board.hintInterval": "How often to run, e.g. 30s, 5m, 30m, 1h, 1d",
182
+ "board.hintCommand": "Full command line. Quote args with spaces",
183
+ "board.hintDescription": "Short label shown in the list. Blank uses the command",
184
+ "board.hintCwd": "Directory the command runs in. Must exist at run time",
185
+ "board.hintRunNow": "run once now then every interval, or wait the first interval",
186
+ "board.hintMaxRuns": "Stop after N runs. Leave blank to run forever",
187
+ "board.exampleInterval": "30m",
188
+ "board.exampleCommand": "opencode run \"search missing translations and translate them, 3 maximum\" --model \"opencode/big-pickle\"",
189
+ "board.exampleDescription": "translate missing strings",
190
+ "board.runNowNo": "No - wait the first interval",
191
+ "board.runNowYes": "Yes - run now, then every interval",
192
+ "board.cwdMissing": "Working directory does not exist: {cwd}",
193
+ "board.createTitle": " New Loop ",
194
+ "board.editTitle": " Edit Loop (will pause) ",
195
+ "board.exampleHeading": "Full example of the loop you are building:",
196
+ "board.exampleFull": "loop-task start 30m --now -- opencode run \"search missing translations and translate them, 3 maximum\" --model \"opencode/big-pickle\"",
197
+ "board.placeholderExample": "e.g. {example}",
198
+ "board.placeholderBlank": "(blank)",
199
+ "board.saving": "Saving...",
200
+ "board.save": "Save",
201
+ "board.create": "Create",
202
+ "board.cancel": "Cancel",
203
+ "board.formNav": "Tab/Shift+Tab to move | Enter advances or activates | Esc cancels",
204
+ "board.badgeNormal": "DASHBOARD",
205
+ "board.badgeSearch": "SEARCH",
206
+ "board.badgeCreate": "CREATE",
207
+ "board.badgeHelp": "HELP",
208
+ "board.badgeDetail": "DETAIL",
209
+ "board.badgeConfirm": "CONFIRM",
210
+ "board.confirmTitle": " Confirm ",
211
+ "board.yes": "Yes",
212
+ "board.no": "No",
213
+ "board.actionResume": "Resume",
214
+ "board.actionPause": "Pause",
215
+ "board.actionResumed": "Resumed",
216
+ "board.actionPaused": "Paused",
217
+ "board.actionRun": "Run",
218
+ "board.actionEdit": "Edit",
219
+ "board.actionDelete": "Delete",
220
+ "board.noActions": "Select a loop to see actions",
221
+ "board.confirmPauseResume": "{action} loop {id}?",
222
+ "board.confirmDelete": "Delete loop {id}?",
223
+ "board.confirmForceRun": "Force run loop {id} now?",
224
+ "board.toastActionId": "{verb} {id}",
225
+ "board.toastDeleted": "Deleted {id}",
226
+ "board.toastTriggered": "Triggered {id}",
227
+ "board.toastUpdated": "Updated {id} (paused)",
228
+ "board.toastStarted": "Started {id}",
229
+ "board.logWaiting": " Waiting for live output...",
230
+ "board.logStreamError": "Log stream error: {message}",
231
+ "board.hintSearch": "search",
232
+ "board.hintRunHistory": "runs",
233
+ "board.hintOpenRun": "open log",
234
+ "board.hintSwitchPanel": "panel",
235
+ "board.hintHelp": "help",
236
+ "board.hintQuit": "quit",
237
+ "board.hintApply": "apply",
238
+ "board.hintCancel": "cancel",
239
+ "board.hintNextField": "next field",
240
+ "board.hintCreate": "create",
241
+ "board.hintBack": "back",
242
+ "board.hintChoose": "choose",
243
+ "board.hintConfirm": "confirm",
244
+ "board.hintYesNo": "yes/no",
245
+ "board.helpKeyMove": "up/down, j/k",
246
+ "board.helpKeyEnter": "enter",
247
+ "board.helpKeyN": "n",
248
+ "board.helpKeyE": "e",
249
+ "board.helpKeyP": "p",
250
+ "board.helpKeyX": "x",
251
+ "board.helpKeyD": "d",
252
+ "board.helpKeySlash": "/",
253
+ "board.helpKeyF": "f",
254
+ "board.helpKeyS": "s",
255
+ "board.helpKeyH": "h",
256
+ "board.helpKeyEsc": "esc",
257
+ "board.helpKeyQ": "q",
258
+ "board.hintKeySlash": "/",
259
+ "board.hintKeyH": "h",
260
+ "board.hintKeyQ": "q",
261
+ "board.hintKeyEnter": "enter",
262
+ "board.hintKeyEsc": "esc",
263
+ "board.hintKeyTab": "tab",
264
+ "board.hintKeyHelpEsc": "h/esc",
265
+ "board.hintKeyArrows": "←/→",
266
+ "board.hintKeyYN": "y/n"
267
+ }
@@ -0,0 +1,11 @@
1
+ import en from "./en.json" with { type: "json" };
2
+ const messages = en;
3
+ export function t(key, params) {
4
+ let result = messages[key] ?? key;
5
+ if (params) {
6
+ for (const name of Object.keys(params)) {
7
+ result = result.split(`{${name}}`).join(String(params[name]));
8
+ }
9
+ }
10
+ return result;
11
+ }
@@ -0,0 +1,65 @@
1
+ import fs from "node:fs";
2
+ import { send } from "../send.js";
3
+ import { tail } from "../../shared/tail.js";
4
+ const TAIL_WINDOW_BYTES = 64 * 1024;
5
+ function readInitialTail(logPath, tailCount) {
6
+ const size = fs.statSync(logPath).size;
7
+ if (tailCount <= 0 || size <= TAIL_WINDOW_BYTES) {
8
+ return tail(fs.readFileSync(logPath, "utf-8"), tailCount);
9
+ }
10
+ const fd = fs.openSync(logPath, "r");
11
+ const buf = Buffer.alloc(TAIL_WINDOW_BYTES);
12
+ fs.readSync(fd, buf, 0, TAIL_WINDOW_BYTES, size - TAIL_WINDOW_BYTES);
13
+ fs.closeSync(fd);
14
+ const windowLines = buf.toString("utf-8").split("\n");
15
+ windowLines.shift();
16
+ if (windowLines.length >= tailCount) {
17
+ return windowLines.slice(-tailCount);
18
+ }
19
+ return tail(fs.readFileSync(logPath, "utf-8"), tailCount);
20
+ }
21
+ export function streamLogFollow(logPath, socket, tailCount) {
22
+ if (fs.existsSync(logPath)) {
23
+ for (const line of readInitialTail(logPath, tailCount)) {
24
+ if (line) {
25
+ send(socket, { type: "data", line });
26
+ }
27
+ }
28
+ }
29
+ let fileSize = fs.existsSync(logPath) ? fs.statSync(logPath).size : 0;
30
+ const watcher = fs.watch(logPath, (eventType) => {
31
+ if (eventType === "rename" && !fs.existsSync(logPath)) {
32
+ watcher.close();
33
+ send(socket, { type: "end" });
34
+ return;
35
+ }
36
+ if (eventType === "change") {
37
+ try {
38
+ const stat = fs.statSync(logPath);
39
+ if (stat.size > fileSize) {
40
+ const fd = fs.openSync(logPath, "r");
41
+ const buf = Buffer.alloc(stat.size - fileSize);
42
+ fs.readSync(fd, buf, 0, buf.length, fileSize);
43
+ fs.closeSync(fd);
44
+ fileSize = stat.size;
45
+ const newContent = buf.toString();
46
+ for (const line of newContent.split("\n")) {
47
+ if (line) {
48
+ send(socket, { type: "data", line });
49
+ }
50
+ }
51
+ }
52
+ }
53
+ catch {
54
+ watcher.close();
55
+ send(socket, { type: "end" });
56
+ }
57
+ }
58
+ });
59
+ socket.on("close", () => {
60
+ watcher.close();
61
+ });
62
+ socket.on("error", () => {
63
+ watcher.close();
64
+ });
65
+ }
@@ -0,0 +1,5 @@
1
+ export function send(socket, message) {
2
+ if (!socket.destroyed) {
3
+ socket.write(JSON.stringify(message) + "\n");
4
+ }
5
+ }
package/dist/logger.js CHANGED
@@ -24,4 +24,3 @@ export class Logger {
24
24
  return new Date().toISOString();
25
25
  }
26
26
  }
27
- //# sourceMappingURL=logger.js.map
@@ -0,0 +1,13 @@
1
+ import type { LoopOptions } from "./types.js";
2
+ export interface LoopCommandOptionsInput {
3
+ now?: boolean;
4
+ maxRuns?: number | string | null;
5
+ verbose?: boolean;
6
+ }
7
+ export interface BuiltLoopOptions {
8
+ intervalHuman: string;
9
+ options: LoopOptions;
10
+ }
11
+ export declare function parseMaxRuns(value: number | string | null | undefined): number | null;
12
+ export declare function buildLoopOptions(intervalHuman: string, command: string, commandArgs: string[], input?: LoopCommandOptionsInput): BuiltLoopOptions;
13
+ //# sourceMappingURL=loop-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loop-config.d.ts","sourceRoot":"","sources":["../src/loop-config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,WAAW,uBAAuB;IACtC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACjC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAWrF;AAED,wBAAgB,gBAAgB,CAC9B,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EAAE,EACrB,KAAK,GAAE,uBAA4B,GAClC,gBAAgB,CAgBlB"}
@@ -0,0 +1,74 @@
1
+ import { parseDuration } from "./duration.js";
2
+ import { t } from "./i18n/index.js";
3
+ export function parseMaxRuns(value) {
4
+ if (value === null || value === undefined || value === "") {
5
+ return null;
6
+ }
7
+ const parsed = typeof value === "number" ? value : parseInt(value, 10);
8
+ if (Number.isNaN(parsed) || parsed < 1) {
9
+ throw new Error(t("errors.maxRunsInvalid"));
10
+ }
11
+ return parsed;
12
+ }
13
+ export function parseCommandLine(input) {
14
+ const tokens = [];
15
+ let current = "";
16
+ let quote = null;
17
+ let hasToken = false;
18
+ for (let i = 0; i < input.length; i += 1) {
19
+ const char = input[i];
20
+ if (quote) {
21
+ if (char === quote) {
22
+ quote = null;
23
+ }
24
+ else if (char === "\\" && quote === '"' && i + 1 < input.length) {
25
+ i += 1;
26
+ current += input[i];
27
+ }
28
+ else {
29
+ current += char;
30
+ }
31
+ continue;
32
+ }
33
+ if (char === '"' || char === "'") {
34
+ quote = char;
35
+ hasToken = true;
36
+ continue;
37
+ }
38
+ if (char === " " || char === "\t") {
39
+ if (hasToken) {
40
+ tokens.push(current);
41
+ current = "";
42
+ hasToken = false;
43
+ }
44
+ continue;
45
+ }
46
+ current += char;
47
+ hasToken = true;
48
+ }
49
+ if (quote) {
50
+ throw new Error(t("errors.unbalancedQuote"));
51
+ }
52
+ if (hasToken) {
53
+ tokens.push(current);
54
+ }
55
+ return tokens;
56
+ }
57
+ export function buildLoopOptions(intervalHuman, command, commandArgs, input = {}) {
58
+ if (!command.trim()) {
59
+ throw new Error(t("errors.commandEmpty"));
60
+ }
61
+ return {
62
+ intervalHuman,
63
+ options: {
64
+ interval: parseDuration(intervalHuman),
65
+ command,
66
+ commandArgs,
67
+ immediate: input.now ?? false,
68
+ maxRuns: parseMaxRuns(input.maxRuns),
69
+ verbose: input.verbose ?? false,
70
+ cwd: input.cwd?.trim() ?? "",
71
+ description: input.description?.trim() ?? "",
72
+ },
73
+ };
74
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loop-config.js","sourceRoot":"","sources":["../src/loop-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAc9C,MAAM,UAAU,YAAY,CAAC,KAAyC;IACpE,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;QAC1D,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACvE,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,aAAqB,EACrB,OAAe,EACf,WAAqB,EACrB,QAAiC,EAAE;IAEnC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC7C,CAAC;IAED,OAAO;QACL,aAAa;QACb,OAAO,EAAE;YACP,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC;YACtC,OAAO;YACP,WAAW;YACX,SAAS,EAAE,KAAK,CAAC,GAAG,IAAI,KAAK;YAC7B,OAAO,EAAE,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC;YACpC,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,KAAK;SAChC;KACF,CAAC;AACJ,CAAC"}
package/dist/loop.d.ts CHANGED
@@ -1,5 +1,46 @@
1
- import type { LoopOptions, ExecutionResult } from "./types.js";
1
+ import { EventEmitter } from "node:events";
2
+ import type { LoopOptions, ExecutionResult, LoopStatus, LoopMeta } from "./types.js";
2
3
  import { Logger } from "./logger.js";
3
- export declare function executeCommand(command: string, commandArgs: string[], logger: Logger): Promise<ExecutionResult>;
4
+ interface LoopControllerState {
5
+ status?: LoopStatus;
6
+ createdAt?: string;
7
+ runCount?: number;
8
+ lastRunAt?: string | null;
9
+ lastExitCode?: number | null;
10
+ lastDuration?: number | null;
11
+ nextRunAt?: string | null;
12
+ remainingDelayMs?: number | null;
13
+ }
14
+ export declare class LoopController extends EventEmitter {
15
+ private abortController;
16
+ private _paused;
17
+ private _status;
18
+ private resumeResolve;
19
+ private runCount;
20
+ private lastRunAt;
21
+ private lastExitCode;
22
+ private lastDuration;
23
+ private nextRunAt;
24
+ private readonly createdAt;
25
+ private readonly id;
26
+ private readonly options;
27
+ private readonly logPath;
28
+ private remainingDelayMs;
29
+ private logStream;
30
+ private loopPromise;
31
+ constructor(id: string, options: LoopOptions, logPath: string, state?: LoopControllerState);
32
+ get status(): LoopStatus;
33
+ start(): void;
34
+ private rotateLogIfNeeded;
35
+ pause(): void;
36
+ resume(): void;
37
+ stop(): Promise<void>;
38
+ getMeta(): Omit<LoopMeta, "command" | "commandArgs" | "interval" | "intervalHuman" | "immediate" | "maxRuns" | "verbose" | "pid">;
39
+ private waitForResume;
40
+ private waitForDelay;
41
+ private run;
42
+ }
43
+ export declare function executeCommandForeground(command: string, commandArgs: string[], logger: Logger): Promise<ExecutionResult>;
4
44
  export declare function runLoop(options: LoopOptions, logger: Logger, signal: AbortSignal): Promise<void>;
45
+ export {};
5
46
  //# sourceMappingURL=loop.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../src/loop.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAa,MAAM,YAAY,CAAC;AAC1E,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAwBrC,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EAAE,EACrB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,eAAe,CAAC,CAiC1B;AAED,wBAAsB,OAAO,CAC3B,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,IAAI,CAAC,CAyFf"}
1
+ {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../src/loop.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,UAAU,EACV,QAAQ,EAET,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAgErC,UAAU,mBAAmB;IAC3B,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,qBAAa,cAAe,SAAQ,YAAY;IAC9C,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,OAAO,CAAC,SAAS,CAA+B;IAChD,OAAO,CAAC,WAAW,CAA8B;gBAG/C,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,mBAAmB;IAkB7B,IAAI,MAAM,IAAI,UAAU,CAEvB;IAED,KAAK,IAAI,IAAI;IAKb,OAAO,CAAC,iBAAiB;IA8BzB,KAAK,IAAI,IAAI;IAQb,MAAM,IAAI,IAAI;IAaR,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAU3B,OAAO,IAAI,IAAI,CAAC,QAAQ,EAAE,SAAS,GAAG,aAAa,GAAG,UAAU,GAAG,eAAe,GAAG,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,KAAK,CAAC;YAcnH,aAAa;YAOb,YAAY;YAsCZ,GAAG;CA2ElB;AAED,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EAAE,EACrB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,eAAe,CAAC,CAgC1B;AAED,wBAAsB,OAAO,CAC3B,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,IAAI,CAAC,CAyFf"}