loop-task 1.1.0 → 1.4.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 (130) hide show
  1. package/README.md +250 -113
  2. package/dist/board/App.js +340 -0
  3. package/dist/board/board-log.js +18 -0
  4. package/dist/board/components/ActionButtons.js +45 -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 +309 -0
  8. package/dist/board/components/CreateProjectModal.js +104 -0
  9. package/dist/board/components/DeleteProjectConfirm.js +53 -0
  10. package/dist/board/components/DetailView.js +25 -0
  11. package/dist/board/components/EditProjectModal.js +106 -0
  12. package/dist/board/components/FilterBar.js +16 -0
  13. package/dist/board/components/Footer.js +52 -0
  14. package/dist/board/components/Header.js +46 -0
  15. package/dist/board/components/HelpModal.js +36 -0
  16. package/dist/board/components/Inspector.js +12 -0
  17. package/dist/board/components/LogModal.js +58 -0
  18. package/dist/board/components/Navigator.js +84 -0
  19. package/dist/board/components/ProjectsModal.js +70 -0
  20. package/dist/board/components/ProjectsPage.js +105 -0
  21. package/dist/board/components/RunHistory.js +83 -0
  22. package/dist/board/components/TaskBrowser.js +95 -0
  23. package/dist/board/components/TaskFilterBar.js +6 -0
  24. package/dist/board/components/TaskForm.js +177 -0
  25. package/dist/board/components/Timeline.js +5 -0
  26. package/dist/board/daemon.js +117 -0
  27. package/dist/board/format.js +102 -0
  28. package/dist/board/hooks/useBoardKeybindings.js +299 -0
  29. package/dist/board/hooks/useBreakpoint.js +6 -0
  30. package/dist/board/hooks/useHoverState.js +11 -0
  31. package/dist/board/hooks/useLogStream.js +32 -0
  32. package/dist/board/hooks/useLoopPolling.js +28 -0
  33. package/dist/board/hooks/useTaskKeybindings.js +114 -0
  34. package/dist/board/index.js +32 -0
  35. package/dist/board/router.js +16 -0
  36. package/dist/board/state.js +99 -0
  37. package/dist/board/toast.js +65 -0
  38. package/dist/board/types.js +1 -0
  39. package/dist/cli.js +134 -93
  40. package/dist/cli.js.map +1 -1
  41. package/dist/client/commands.d.ts +11 -0
  42. package/dist/client/commands.d.ts.map +1 -0
  43. package/dist/client/commands.js +317 -0
  44. package/dist/client/commands.js.map +1 -0
  45. package/dist/client/ipc.d.ts +5 -0
  46. package/dist/client/ipc.d.ts.map +1 -0
  47. package/dist/client/ipc.js +96 -0
  48. package/dist/client/ipc.js.map +1 -0
  49. package/dist/config/constants.js +29 -0
  50. package/dist/config/paths.js +49 -0
  51. package/dist/core/command-runner.js +82 -0
  52. package/dist/core/foreground-loop.js +81 -0
  53. package/dist/core/log-parser.js +29 -0
  54. package/dist/core/log-rotator.js +25 -0
  55. package/dist/core/loop-controller.js +429 -0
  56. package/dist/daemon/daemon-log.js +11 -0
  57. package/dist/daemon/index.d.ts +2 -0
  58. package/dist/daemon/index.d.ts.map +1 -0
  59. package/dist/daemon/index.js +43 -0
  60. package/dist/daemon/index.js.map +1 -0
  61. package/dist/daemon/manager.d.ts +17 -0
  62. package/dist/daemon/manager.d.ts.map +1 -0
  63. package/dist/daemon/manager.js +260 -0
  64. package/dist/daemon/manager.js.map +1 -0
  65. package/dist/daemon/projects.js +104 -0
  66. package/dist/daemon/server.d.ts +15 -0
  67. package/dist/daemon/server.d.ts.map +1 -0
  68. package/dist/daemon/server.js +345 -0
  69. package/dist/daemon/server.js.map +1 -0
  70. package/dist/daemon/spawner.d.ts +3 -0
  71. package/dist/daemon/spawner.d.ts.map +1 -0
  72. package/dist/daemon/spawner.js +82 -0
  73. package/dist/daemon/spawner.js.map +1 -0
  74. package/dist/daemon/state.d.ts +15 -0
  75. package/dist/daemon/state.d.ts.map +1 -0
  76. package/dist/daemon/state.js +151 -0
  77. package/dist/daemon/state.js.map +1 -0
  78. package/dist/daemon/task-manager.js +59 -0
  79. package/dist/duration.js +4 -4
  80. package/dist/entry.js +11 -0
  81. package/dist/esm-loader.js +36 -0
  82. package/dist/i18n/en.json +386 -0
  83. package/dist/i18n/index.js +11 -0
  84. package/dist/ipc/handlers/logs-stream.js +65 -0
  85. package/dist/ipc/send.js +5 -0
  86. package/dist/logger.js +0 -1
  87. package/dist/loop-config.d.ts +13 -0
  88. package/dist/loop-config.d.ts.map +1 -0
  89. package/dist/loop-config.js +83 -0
  90. package/dist/loop-config.js.map +1 -0
  91. package/dist/loop.d.ts +43 -2
  92. package/dist/loop.d.ts.map +1 -1
  93. package/dist/loop.js +247 -2
  94. package/dist/loop.js.map +1 -1
  95. package/dist/open-board.d.ts +2 -0
  96. package/dist/open-board.d.ts.map +1 -0
  97. package/dist/open-board.js +19 -0
  98. package/dist/open-board.js.map +1 -0
  99. package/dist/shared/clipboard.js +19 -0
  100. package/dist/shared/fs-utils.js +11 -0
  101. package/dist/shared/sleep.js +17 -0
  102. package/dist/shared/tail.js +4 -0
  103. package/dist/tui/app.d.ts +57 -0
  104. package/dist/tui/app.d.ts.map +1 -0
  105. package/dist/tui/app.js +167 -0
  106. package/dist/tui/app.js.map +1 -0
  107. package/dist/tui/create-loop.d.ts +3 -0
  108. package/dist/tui/create-loop.d.ts.map +1 -0
  109. package/dist/tui/create-loop.js +90 -0
  110. package/dist/tui/create-loop.js.map +1 -0
  111. package/dist/tui/dashboard.d.ts +2 -0
  112. package/dist/tui/dashboard.d.ts.map +1 -0
  113. package/dist/tui/dashboard.js +45 -0
  114. package/dist/tui/dashboard.js.map +1 -0
  115. package/dist/tui/events.d.ts +3 -0
  116. package/dist/tui/events.d.ts.map +1 -0
  117. package/dist/tui/events.js +244 -0
  118. package/dist/tui/events.js.map +1 -0
  119. package/dist/tui/popup.d.ts +3 -0
  120. package/dist/tui/popup.d.ts.map +1 -0
  121. package/dist/tui/popup.js +58 -0
  122. package/dist/tui/popup.js.map +1 -0
  123. package/dist/tui/render.d.ts +9 -0
  124. package/dist/tui/render.d.ts.map +1 -0
  125. package/dist/tui/render.js +305 -0
  126. package/dist/tui/render.js.map +1 -0
  127. package/dist/types.d.ts +74 -0
  128. package/dist/types.d.ts.map +1 -1
  129. package/dist/types.js +0 -1
  130. package/package.json +64 -55
@@ -0,0 +1,59 @@
1
+ import crypto from "node:crypto";
2
+ import { saveTask, loadAllTasks, deleteTask as deleteTaskState } from "./state.js";
3
+ import { daemonLog } from "./daemon-log.js";
4
+ export class TaskManager {
5
+ tasks = new Map();
6
+ init() {
7
+ const saved = loadAllTasks();
8
+ for (const task of saved) {
9
+ this.tasks.set(task.id, task);
10
+ }
11
+ if (saved.length > 0) {
12
+ daemonLog(`loaded ${saved.length} task(s) from persisted state`);
13
+ }
14
+ }
15
+ create(input) {
16
+ const task = {
17
+ ...input,
18
+ createdAt: new Date().toISOString(),
19
+ };
20
+ this.tasks.set(task.id, task);
21
+ saveTask(task);
22
+ return task;
23
+ }
24
+ get(id) {
25
+ return this.tasks.get(id) ?? null;
26
+ }
27
+ list() {
28
+ return [...this.tasks.values()];
29
+ }
30
+ update(id, input) {
31
+ const existing = this.tasks.get(id);
32
+ if (!existing)
33
+ return null;
34
+ const updated = { ...existing, ...input };
35
+ this.tasks.set(id, updated);
36
+ saveTask(updated);
37
+ return updated;
38
+ }
39
+ delete(id) {
40
+ if (!this.tasks.has(id))
41
+ return false;
42
+ this.tasks.delete(id);
43
+ deleteTaskState(id);
44
+ return true;
45
+ }
46
+ createInline(command, commandArgs, cwd) {
47
+ const id = crypto.randomUUID().slice(0, 8);
48
+ const name = [command, ...commandArgs].join(" ").slice(0, 40);
49
+ return this.create({
50
+ id,
51
+ name,
52
+ command,
53
+ commandArgs,
54
+ cwd,
55
+ onSuccessTaskId: null,
56
+ onFailureTaskId: null,
57
+ });
58
+ }
59
+ }
package/dist/duration.js CHANGED
@@ -1,19 +1,19 @@
1
1
  import ms from "ms";
2
+ import { t } from "./i18n/index.js";
2
3
  export function parseDuration(input) {
3
4
  const trimmed = input.trim();
4
5
  if (!trimmed) {
5
- throw new Error("Duration cannot be empty");
6
+ throw new Error(t("errors.durationEmpty"));
6
7
  }
7
8
  const result = ms(trimmed);
8
9
  if (typeof result !== "number" || isNaN(result)) {
9
- throw new Error(`Invalid duration: "${input}". Use formats like 10s, 5m, 1h, 1d, 1w`);
10
+ throw new Error(t("errors.durationInvalid", { input }));
10
11
  }
11
12
  if (result <= 0) {
12
- throw new Error(`Duration must be positive, got: "${input}"`);
13
+ throw new Error(t("errors.durationNotPositive", { input }));
13
14
  }
14
15
  return result;
15
16
  }
16
17
  export function formatDuration(value) {
17
18
  return ms(value, { long: true });
18
19
  }
19
- //# sourceMappingURL=duration.js.map
package/dist/entry.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { register } from "node:module";
4
+ import { pathToFileURL } from "node:url";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ register(pathToFileURL(join(__dirname, "esm-loader.js")).href);
10
+
11
+ import("./cli.js");
@@ -0,0 +1,36 @@
1
+ import { resolve as pathResolve, dirname } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import fs from "node:fs";
4
+
5
+ const EXTENSIONLESS_PATTERNS = [
6
+ /node_modules\/react-reconciler\/constants$/,
7
+ /node_modules\/react-reconciler\/reflection$/,
8
+ ];
9
+
10
+ export function resolve(specifier, context, nextResolve) {
11
+ for (const pattern of EXTENSIONLESS_PATTERNS) {
12
+ if (pattern.test(specifier)) {
13
+ return nextResolve(specifier + ".js", context);
14
+ }
15
+ }
16
+
17
+ if (specifier.includes("react-reconciler/constants") || specifier.includes("react-reconciler/reflection")) {
18
+ return nextResolve(specifier + ".js", context);
19
+ }
20
+
21
+ if (
22
+ context.parentURL &&
23
+ specifier.startsWith(".") &&
24
+ !specifier.endsWith(".js") &&
25
+ !specifier.endsWith(".json") &&
26
+ !specifier.endsWith(".node")
27
+ ) {
28
+ const parentDir = dirname(fileURLToPath(context.parentURL));
29
+ const candidate = pathResolve(parentDir, specifier + ".js");
30
+ if (fs.existsSync(candidate)) {
31
+ return nextResolve(specifier + ".js", context);
32
+ }
33
+ }
34
+
35
+ return nextResolve(specifier, context);
36
+ }
@@ -0,0 +1,386 @@
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
+ "cli.optProject": "Assign the loop to a project (name or id)",
43
+ "cli.projectDescription": "Manage projects",
44
+ "cli.projectListDescription": "List all projects",
45
+ "cli.projectNewDescription": "Create a new project",
46
+ "cli.projectRenameDescription": "Rename a project",
47
+ "cli.projectColorDescription": "Change a project's color",
48
+ "cli.projectDeleteDescription": "Delete a project (loops move to Default)",
49
+ "cli.optProjectColor": "Project color (name or #hex)",
50
+ "cli.projectArgName": "Project name",
51
+ "cli.projectArgNewName": "New project name",
52
+ "cli.projectArgIdOrName": "Project id or name",
53
+ "cli.projectArgColor": "Color name or #hex",
54
+ "cli.noProjects": "No projects.",
55
+ "cli.projectHeaderName": "NAME",
56
+ "cli.projectHeaderId": "ID",
57
+ "cli.projectHeaderColor": "COLOR",
58
+ "cli.projectHeaderLoops": "LOOPS",
59
+ "cli.projectHeaderSystem": "SYSTEM",
60
+ "cli.projectCreated": "Project \"{name}\" created (id: {id})",
61
+ "cli.projectRenamed": "Project renamed to \"{name}\"",
62
+ "cli.projectColorSet": "Project \"{name}\" color set to {color}",
63
+ "cli.projectDeleted": "Project \"{name}\" deleted (loops moved to Default)",
64
+ "cli.projectNotFound": "Project not found: {name}",
65
+ "cli.projectAmbiguous": "Multiple projects named \"{name}\" — use the id instead",
66
+ "cli.projectInvalidColor": "Invalid color \"{color}\". Valid names: {valid}, or a #rrggbb hex",
67
+ "loop.runHeader": "\n[Run #{runNumber} — {timestamp}]\n",
68
+ "loop.exitMarker": "[exit {code} · {duration}]\n",
69
+ "loop.cwdMissingLog": "[error] working directory does not exist: {cwd}\n",
70
+ "loop.cwdMissing": "Working directory does not exist: {cwd}",
71
+ "loop.executing": "Executing: {command}",
72
+ "loop.shuttingDown": "\nShutting down gracefully...",
73
+ "loop.completed": "Completed {runs} run(s). Exiting.",
74
+ "loop.waitingFirst": "Waiting {duration} before first run...",
75
+ "loop.runSeparator": "\n--- Run {run}{maxRuns} ---",
76
+ "loop.commandFailed": "Command failed with exit code {code}",
77
+ "loop.debugStartedAt": "Started at: {timestamp}",
78
+ "loop.debugEndedAt": "Ended at: {timestamp}",
79
+ "loop.debugExitCode": "Exit code: {code}",
80
+ "loop.debugDuration": "Duration: {duration}",
81
+ "loop.nextRun": "Next run in {duration} (at {time})",
82
+ "errors.durationEmpty": "Duration cannot be empty",
83
+ "errors.durationInvalid": "Invalid duration: \"{input}\". Use formats like 10s, 5m, 1h, 1d, 1w",
84
+ "errors.durationNotPositive": "Duration must be positive, got: \"{input}\"",
85
+ "errors.maxRunsInvalid": "--max-runs must be a positive integer",
86
+ "errors.unbalancedQuote": "Unbalanced quote in command",
87
+ "errors.commandEmpty": "Command cannot be empty",
88
+ "errors.descriptionEmpty": "Description cannot be empty",
89
+ "errors.loopNotFound": "Loop {id} not found",
90
+ "errors.invalidJson": "Invalid JSON",
91
+ "errors.noLogsYet": "No logs yet",
92
+ "errors.invalidResponse": "Invalid response from daemon",
93
+ "errors.connectionClosed": "Connection closed before response",
94
+ "errors.cannotConnect": "Cannot connect to daemon: {message}",
95
+ "errors.requestTimedOut": "Daemon request timed out",
96
+ "errors.daemonFailedToStart": "Daemon failed to start within 5 seconds",
97
+ "errors.daemonUncaught": "Daemon uncaught exception:",
98
+ "errors.daemonFailed": "Daemon failed to start:",
99
+ "errors.pauseFailed": "pause failed",
100
+ "errors.resumeFailed": "resume failed",
101
+ "errors.forceRunFailed": "force run failed",
102
+ "errors.deleteFailed": "delete failed",
103
+ "errors.maxRunsReached": "Max runs reached — edit the loop and increase 'Max runs' to continue",
104
+ "errors.triggerWhileRunning": "Loop is already running — force run skipped",
105
+ "format.dash": "-",
106
+ "format.justNow": "just now",
107
+ "format.secsAgo": "{secs}s ago",
108
+ "format.minsAgo": "{mins}m ago",
109
+ "format.hrsAgo": "{hrs}h ago",
110
+ "format.daysAgo": "{days}d ago",
111
+ "format.secsAhead": "{secs}s",
112
+ "format.minsAhead": "{mins}m",
113
+ "format.hrsAhead": "{hrs}h",
114
+ "format.daysAhead": "{days}d",
115
+ "format.timingPaused": "paused",
116
+ "format.timingIdle": "stopped",
117
+ "format.timingNext": "in {timeAgo}",
118
+ "format.timingLast": "last {timeAgo}",
119
+ "format.timingNew": "new",
120
+ "board.appName": "loop-task",
121
+ "board.appTagline": " background task loops",
122
+ "board.daemonLabel": "Daemon: ",
123
+ "board.loopsLabel": " Loops: ",
124
+ "board.runningLabel": " Running: ",
125
+ "board.waitingLabel": " Waiting: ",
126
+ "board.pausedLabel": " Paused: ",
127
+ "board.idleLabel": " Stopped: ",
128
+ "board.searchTitle": " Search / ",
129
+ "board.searchPlaceholder": "type to filter, enter to apply",
130
+ "board.searchEmpty": "press / to search",
131
+ "board.statusFilterTitle": " Status x ",
132
+ "board.sortTitle": " Order o ",
133
+ "board.newLoopTitle": "",
134
+ "board.newLoopLabel": "Add Loop",
135
+ "board.viewTasksTitle": "",
136
+ "board.viewTasksLabel": "View Tasks",
137
+ "board.viewLoopsTitle": " Loops ",
138
+ "board.viewLoopsLabel": "View Loops",
139
+ "board.headerSince": "SINCE",
140
+ "board.headerRuns": "RUNS",
141
+ "board.headerSkipped": "SKP",
142
+ "board.headerStatus": "STATUS",
143
+ "board.headerDescription": "DESCRIPTION",
144
+ "board.headerTiming": "TIMING",
145
+ "board.headerExitRuns": "EXIT RUNS",
146
+ "board.navigatorTitle": " Loops {visible}/{total} sort:{sort} status:{status} ",
147
+ "board.noMatch": " No loops match the current view",
148
+ "board.inspectorTitle": " Inspector ",
149
+ "board.inspectorEmpty": " Select a loop to view details",
150
+ "board.fieldId": "ID: ",
151
+ "board.fieldDesc": "Desc: ",
152
+ "board.fieldCommand": "Command: ",
153
+ "board.fieldTask": "Task: ",
154
+ "board.fieldDir": "Dir: ",
155
+ "board.fieldInterval": "Interval: ",
156
+ "board.fieldStatus": "Status: ",
157
+ "board.fieldRuns": "Runs: ",
158
+ "board.fieldLastRun": "Last run: ",
159
+ "board.fieldLastExit": "Last exit:",
160
+ "board.fieldNextRun": "Next run: ",
161
+ "board.fieldPid": "PID: ",
162
+ "board.inherit": "(inherit)",
163
+ "board.unlimited": "∞",
164
+ "board.timelineTitle": " Timeline ",
165
+ "board.timelineEmpty": " No output yet.",
166
+ "board.runHistoryTitle": " Run History ",
167
+ "board.runHistoryTitleHint": " Run History (Press Enter to view log) ",
168
+ "board.runHistoryEmpty": " No runs yet.",
169
+ "board.runHistoryTime": "TIME",
170
+ "board.runHistoryDesc": "DESCRIPTION",
171
+ "board.runHistoryDuration": "DURATION",
172
+ "board.runHistorySize": "SIZE",
173
+ "board.logModalTitle": " Run Log {icon} {time} ",
174
+ "board.logModalLoading": " Loading...",
175
+ "board.logModalEmpty": " No output captured.",
176
+ "board.logModalEscClose": "esc to close",
177
+ "board.logModalRunning": "running…",
178
+ "board.detailTitle": " Loop Detail ",
179
+ "board.detailFieldId": "ID: ",
180
+ "board.detailFieldDesc": "Desc: ",
181
+ "board.detailFieldCommand": "Command: ",
182
+ "board.detailFieldDir": "Dir: ",
183
+ "board.detailFieldInterval": "Interval: ",
184
+ "board.detailFieldStatus": "Status: ",
185
+ "board.detailFieldRuns": "Runs: ",
186
+ "board.detailFieldCreated": "Created: ",
187
+ "board.detailFieldLastRun": "Last run: ",
188
+ "board.detailFieldLastExit": "Last exit: ",
189
+ "board.detailFieldNextRun": "Next run: ",
190
+ "board.detailFieldPid": "PID: ",
191
+ "board.detailSummaryStatus": "status",
192
+ "board.detailSummaryTiming": "next",
193
+ "board.detailSummaryInterval": "every",
194
+ "board.detailSummaryRuns": "runs",
195
+ "board.detailSummaryExit": "exit",
196
+ "board.liveOutputTitle": " Live Output ",
197
+ "board.helpTitle": " Help ",
198
+ "board.helpMoveSelection": "move selection",
199
+ "board.helpToggleDetail": "toggle loop detail",
200
+ "board.helpCreate": "create loop",
201
+ "board.helpCreateTask": "create task",
202
+ "board.helpEdit": "edit loop (pauses it)",
203
+ "board.helpPauseResume": "pause/resume selected loop",
204
+ "board.helpStopPlay": "stop/play selected loop (resets schedule)",
205
+ "board.helpForceRun": "force run selected loop now",
206
+ "board.helpDelete": "delete selected loop",
207
+ "board.helpSearch": "search loops",
208
+ "board.helpCycleFilter": "cycle status filter",
209
+ "board.helpCycleSort": "cycle sort mode",
210
+ "board.helpRunHistory": "navigate run history",
211
+ "board.helpOpenRunLog": "open run log",
212
+ "board.helpSwitchPanel": "switch panel focus",
213
+ "board.helpKeyLeftRight": "←/→",
214
+ "board.helpToggleHelp": "toggle help",
215
+ "board.helpBack": "back to board",
216
+ "board.helpQuit": "quit",
217
+ "board.labelInterval": "Interval",
218
+ "board.labelCommand": "Command",
219
+ "board.labelDescription": "Description",
220
+ "board.labelCwd": "Working dir",
221
+ "board.labelRunNow": "Run immediately?",
222
+ "board.labelMaxRuns": "Max runs",
223
+ "board.labelTaskMode": "Task",
224
+ "board.labelTask": "Select task",
225
+ "board.taskModeInline": "Inline command",
226
+ "board.taskModeExisting": "Existing task",
227
+ "board.selectTask": "(select a task)",
228
+ "board.taskLabelName": "Name",
229
+ "board.taskLabelCommand": "Command",
230
+ "board.taskLabelOnSuccess": "On success",
231
+ "board.taskLabelOnFailure": "On failure",
232
+ "board.taskHintName": "A short name for this task",
233
+ "board.taskHintOnSuccess": "Task to run when this one succeeds (optional)",
234
+ "board.taskHintOnFailure": "Task to run when this one fails (optional)",
235
+ "board.taskNone": "(none)",
236
+ "board.taskCreateTitle": " New Task ",
237
+ "board.taskEditTitle": " Edit Task ",
238
+ "board.hintTaskMode": "type a command inline, or pick an existing task",
239
+ "board.hintInterval": "How often to run, e.g. 30s, 5m, 30m, 1h, 1d",
240
+ "board.hintCommand": "Full command line. Quote args with spaces",
241
+ "board.hintDescription": "Short label shown in the list. Blank uses the command",
242
+ "board.hintCwd": "Directory the command runs in. Must exist at run time",
243
+ "board.hintRunNow": "run once now then every interval, or wait the first interval",
244
+ "board.hintMaxRuns": "Stop after N runs. Leave blank to run forever",
245
+ "board.hintTask": "Pick a previously defined task",
246
+ "board.exampleInterval": "30m",
247
+ "board.exampleCommand": "opencode run \"search missing translations and translate them, 3 maximum\" --model \"opencode/big-pickle\"",
248
+ "board.exampleDescription": "translate missing strings",
249
+ "board.runNowNo": "No - wait the first interval",
250
+ "board.runNowYes": "Yes - run now, then every interval",
251
+ "board.cwdMissing": "Working directory does not exist: {cwd}",
252
+ "board.createTitle": " New Loop ",
253
+ "board.editTitle": " Edit Loop (will pause) ",
254
+ "board.exampleHeading": "Full example of the loop you are building:",
255
+ "board.exampleFull": "loop-task start 30m --now -- opencode run \"search missing translations and translate them, 3 maximum\" --model \"opencode/big-pickle\"",
256
+ "board.placeholderExample": "e.g. {example}",
257
+ "board.placeholderBlank": "(blank)",
258
+ "board.saving": "Saving...",
259
+ "board.save": "Save",
260
+ "board.create": "Create",
261
+ "board.cancel": "Cancel",
262
+ "board.formNav": "Tab/Shift+Tab to move | Enter advances or activates | Esc cancels",
263
+ "board.badgeNormal": "Loops",
264
+ "board.badgeSearch": "SEARCH",
265
+ "board.badgeCreate": "CREATE",
266
+ "board.badgeTask": "TASK",
267
+ "board.badgeHelp": "HELP",
268
+ "board.badgeDetail": "DETAIL",
269
+ "board.badgeConfirm": "CONFIRM",
270
+ "board.confirmTitle": " Confirm ",
271
+ "board.yes": "Yes",
272
+ "board.no": "No",
273
+ "board.actionEdit": "Edit",
274
+ "board.actionDelete": "Delete",
275
+ "board.actionPause": "Pause",
276
+ "board.actionStop": "Stop",
277
+ "board.actionPlay": "Play",
278
+ "board.actionTrigger": "Force Run",
279
+ "board.noActions": "Select a loop to see actions",
280
+ "board.confirmPause": "Pause loop \"{desc}\"?",
281
+ "board.confirmStop": "Stop loop \"{desc}\"? (schedule will reset on Play)",
282
+ "board.confirmPlay": "Play loop \"{desc}\"? (starts fresh interval from now)",
283
+ "board.confirmResume": "Resume loop \"{desc}\"?",
284
+ "board.confirmTrigger": "Force run loop \"{desc}\"?",
285
+ "board.confirmDelete": "Delete loop \"{desc}\"?",
286
+ "board.toastPaused": "Paused \"{desc}\"",
287
+ "board.toastStopped": "Stopped \"{desc}\"",
288
+ "board.toastPlayed": "Playing \"{desc}\"",
289
+ "board.toastResumed": "Resumed \"{desc}\"",
290
+ "board.toastDeleted": "Deleted \"{desc}\"",
291
+ "board.toastUpdated": "Updated \"{desc}\" (paused)",
292
+ "board.toastTriggered": "Triggered \"{desc}\"",
293
+ "board.toastStarted": "Started \"{desc}\"",
294
+ "board.toastTaskCreated": "Task created {id}",
295
+ "board.toastTaskUpdated": "Task updated {id}",
296
+ "board.toastTaskSelected": "Selected task \"{desc}\"",
297
+ "board.logWaiting": " Waiting for live output...",
298
+ "board.logStreamError": "Log stream error: {message}",
299
+ "board.hintSearch": "search",
300
+ "board.hintRunHistory": "runs",
301
+ "board.hintOpenRun": "select loop / open log",
302
+ "board.hintSwitchPanel": "panel",
303
+ "board.hintHelp": "help",
304
+ "board.hintQuit": "quit",
305
+ "board.hintApply": "apply",
306
+ "board.hintCancel": "cancel",
307
+ "board.hintNextField": "next field",
308
+ "board.hintCreate": "create",
309
+ "board.hintBack": "back",
310
+ "board.hintChoose": "choose",
311
+ "board.hintConfirm": "confirm",
312
+ "board.hintYesNo": "yes/no",
313
+ "board.helpKeyMove": "up/down, j/k",
314
+ "board.helpKeyEnter": "enter",
315
+ "board.helpKeyN": "n",
316
+ "board.helpKeyE": "e",
317
+ "board.helpKeyP": "p",
318
+ "board.helpKeyX": "x",
319
+ "board.helpKeyD": "d",
320
+ "board.helpKeySlash": "/",
321
+ "board.helpKeyF": "f",
322
+ "board.helpKeyS": "s",
323
+ "board.helpKeyH": "h",
324
+ "board.helpKeyEsc": "esc",
325
+ "board.helpKeyQ": "q",
326
+ "board.hintKeySlash": "/",
327
+ "board.hintKeyH": "h",
328
+ "board.hintKeyQ": "q",
329
+ "board.hintKeyEnter": "enter",
330
+ "board.hintKeyEsc": "esc",
331
+ "board.hintKeyTab": "tab",
332
+ "board.hintKeyHelpEsc": "h/esc",
333
+ "board.hintKeyArrows": "←/→",
334
+ "board.hintKeyYN": "y/n",
335
+ "board.taskBrowserTitle": " Tasks {visible}/{total} ",
336
+ "board.taskBrowserEmpty": " No tasks defined yet. Press n to create one.",
337
+ "board.taskHeaderName": "NAME",
338
+ "board.taskHeaderCommand": "COMMAND",
339
+ "board.taskHeaderChains": "CHAINS",
340
+ "board.taskActionSelect": "Select",
341
+ "board.taskActionNew": "+ New Task",
342
+ "board.newTaskTitle": " New ",
343
+ "board.taskActionEdit": "Edit",
344
+ "board.taskActionDelete": "Delete",
345
+ "board.confirmDeleteTask": "Delete task \"{desc}\"?",
346
+ "board.toastTaskDeleted": "Deleted task \"{desc}\"",
347
+ "board.chooseTask": "Choose Task",
348
+ "board.selectedTask": "Task: {name}",
349
+ "board.taskChainsNone": "-",
350
+ "board.taskChainsFormat": "✓:{success} ✗:{failure}",
351
+ "board.logModalCopied": "Log copied to clipboard",
352
+ "project.selectProject": " Select Project ",
353
+ "project.projectsLabel": "Projects",
354
+ "project.projectsTitle": " Projects ",
355
+ "project.filterTitle": " Project r ",
356
+ "project.newProjectLabel": "+ New Project",
357
+ "project.manageLabel": "View Projects",
358
+ "project.showAll": "Show All",
359
+ "project.showAllLoops": "All loops",
360
+ "project.manageTitle": "",
361
+ "project.createTitle": " New Project ",
362
+ "project.editTitle": " Edit Project ",
363
+ "project.deleteTitle": " Delete Project ",
364
+ "project.labelName": "Name",
365
+ "project.labelColor": "Color",
366
+ "project.hintName": "A short name for this project",
367
+ "project.hintColor": "Pick a color for this project",
368
+ "project.systemLabel": "System project",
369
+ "project.editProjectLabel": "Edit",
370
+ "project.deleteProjectLabel": "Delete",
371
+ "project.loopCount": "{count} loop(s)",
372
+ "project.noLoops": "No loops",
373
+ "project.deleteConfirm": "Delete project \"{name}\"?",
374
+ "project.deleteWarning": "This project has {count} loop(s). Loops will be moved to Default.",
375
+ "project.moveToDefault": "Yes, move to Default",
376
+ "project.keyNewHint": "n: new",
377
+ "project.keyEditHint": "e: edit",
378
+ "project.keyDeleteHint": "d: delete",
379
+ "project.error.nameRequired": "Project name is required",
380
+ "project.error.nameEmpty": "Project name cannot be empty",
381
+ "project.error.updateFailed": "Failed to update project",
382
+ "project.error.deleteFailed": "Failed to delete project",
383
+ "project.toastCreated": "Project \"{name}\" created",
384
+ "project.toastUpdated": "Project \"{name}\" updated",
385
+ "project.toastDeleted": "Project \"{name}\" deleted"
386
+ }
@@ -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"}