loop-task 1.4.2 → 1.4.3
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/dist/board/App.js +1 -1
- package/dist/board/components/CreateForm.js +5 -29
- package/dist/board/components/FilterBar.js +4 -3
- package/dist/board/components/SearchBox.js +6 -0
- package/dist/board/components/TaskFilterBar.js +4 -4
- package/dist/board/components/TaskForm.js +5 -29
- package/dist/board/hooks/useBoardKeybindings.js +2 -15
- package/dist/board/hooks/useTaskKeybindings.js +0 -19
- package/dist/config/paths.js +9 -0
- package/dist/core/loop-controller.js +11 -7
- package/dist/daemon/manager.js +4 -1
- package/dist/daemon/projects.js +47 -26
- package/dist/daemon/state.js +83 -32
- package/package.json +1 -1
package/dist/board/App.js
CHANGED
|
@@ -345,7 +345,7 @@ export function App(props) {
|
|
|
345
345
|
}
|
|
346
346
|
else {
|
|
347
347
|
push("projects");
|
|
348
|
-
} } }), view === "board" ? (_jsx(FilterBar, { filters: filters, sort: sort, searchActive: searchActive, focusedPanel: focusedPanel, onStatusCycle: () => setFilters((prev) => ({ ...prev, status: cycleStatusFilter(prev.status) })), onSortCycle: () => setSort(cycleSortMode(sort)), onSelectProject: () => setProjectsModalOpen(true), currentProjectName: currentProjectId === "all" ? t("project.showAll") : (projects.find(p => p.id === currentProjectId)?.name ?? "Default") })) : view === "task-list" ? (_jsx(TaskFilterBar, { query: taskQuery, searchActive: taskSearchActive, focusedPanel: taskFocusedPanel })) : null, _jsx("box", { style: { flexGrow: 1, backgroundColor: "#0b0b0b" }, children: view === "create" ? (_jsx(CreateView, { mode: editTarget && !cloneMode ? "edit" : "create", editId: editTarget && !cloneMode ? editTarget.id : null, initial: createInitialValues(editTarget, currentProjectId), selectedTaskId: pendingTaskSelection?.id ?? null, selectedTaskName: pendingTaskSelection?.name ?? null, projects: projects, currentProjectId: currentProjectId, onCancel: cancelCreate, onDone: onCreateDone, onChooseTask: handleChooseTask })) : TASK_FORM_VIEWS.has(view) ? (_jsx(TaskForm, { mode: view === "task-edit" ? "edit" : "create", editTask: editTask, onCancel: cancelTask, onDone: onTaskDone })) : view === "task-list" ? (_jsxs("box", { style: { flexDirection: breakpoint === "narrow" ? "column" : "row", flexGrow: 1, backgroundColor: "#0b0b0b" }, children: [_jsx(TaskNavigator, { visible: filteredTasks, total: tasks.length, selectedIndex: taskClampedIndex, focused: taskFocusedPanel === "tasks", query: taskQuery, onSelect: (index) => { setTaskSelectedIndex(index); setTaskFocusedPanel("tasks"); }, onActivate: (index) => { setTaskSelectedIndex(index); setEditTask(filteredTasks[index] ?? null); push("task-edit"); } }), _jsxs("box", { style: { flexDirection: "column", flexGrow: 1, backgroundColor: "#0b0b0b", overflow: "hidden" }, children: [_jsx(TaskInspector, { task: selectedTask }, `ti-${selectedTask?.id}`), _jsx(TaskActionButtons, { task: selectedTask, focused: taskFocusedPanel === "actions", selectedAction: taskSelectedAction, selectable: stack.includes("create") || stack.includes("task-edit"), onAction: handleTaskAction }, `tab-${selectedTask?.id}`)] })] })) : view === "projects" ? (_jsx(ProjectsPage, { projects: projects, loops: loops, onClose: () => pop(), onRefresh: refreshProjects, onOpenCreate: (trigger) => { createProjectTriggerRef.current = trigger; } })) : (_jsxs("box", { style: { flexDirection: breakpoint === "narrow" ? "column" : "row", flexGrow: 1, backgroundColor: "#0b0b0b" }, children: [_jsx(Navigator, { visible: visible, total: loops.length, selectedIndex: clampedIndex, filters: filters, sort: sort, breakpoint: breakpoint, focused: focusedPanel === "loops", projects: projects, onSelect: (index) => { setSelectedIndex(index); setFocusedPanel("loops"); }, onActivate: (index) => { setSelectedIndex(index); const loop = visible[index]; if (loop) {
|
|
348
|
+
} } }), view === "board" ? (_jsx(FilterBar, { filters: filters, sort: sort, searchActive: searchActive, focusedPanel: focusedPanel, onStatusCycle: () => setFilters((prev) => ({ ...prev, status: cycleStatusFilter(prev.status) })), onSortCycle: () => setSort(cycleSortMode(sort)), onSelectProject: () => setProjectsModalOpen(true), currentProjectName: currentProjectId === "all" ? t("project.showAll") : (projects.find(p => p.id === currentProjectId)?.name ?? "Default"), onQueryChange: (value) => setFilters((prev) => ({ ...prev, query: value })), onSearchActivate: () => setSearchActive(true), onSearchDismiss: () => { setSearchActive(false); setFocusedPanel("loops"); } })) : view === "task-list" ? (_jsx(TaskFilterBar, { query: taskQuery, searchActive: taskSearchActive, focusedPanel: taskFocusedPanel, onQueryChange: setTaskQuery, onSearchActivate: () => setTaskSearchActive(true), onSearchDismiss: () => { setTaskSearchActive(false); setTaskFocusedPanel("tasks"); } })) : null, _jsx("box", { style: { flexGrow: 1, backgroundColor: "#0b0b0b" }, children: view === "create" ? (_jsx(CreateView, { mode: editTarget && !cloneMode ? "edit" : "create", editId: editTarget && !cloneMode ? editTarget.id : null, initial: createInitialValues(editTarget, currentProjectId), selectedTaskId: pendingTaskSelection?.id ?? null, selectedTaskName: pendingTaskSelection?.name ?? null, projects: projects, currentProjectId: currentProjectId, onCancel: cancelCreate, onDone: onCreateDone, onChooseTask: handleChooseTask })) : TASK_FORM_VIEWS.has(view) ? (_jsx(TaskForm, { mode: view === "task-edit" ? "edit" : "create", editTask: editTask, onCancel: cancelTask, onDone: onTaskDone })) : view === "task-list" ? (_jsxs("box", { style: { flexDirection: breakpoint === "narrow" ? "column" : "row", flexGrow: 1, backgroundColor: "#0b0b0b" }, children: [_jsx(TaskNavigator, { visible: filteredTasks, total: tasks.length, selectedIndex: taskClampedIndex, focused: taskFocusedPanel === "tasks", query: taskQuery, onSelect: (index) => { setTaskSelectedIndex(index); setTaskFocusedPanel("tasks"); }, onActivate: (index) => { setTaskSelectedIndex(index); setEditTask(filteredTasks[index] ?? null); push("task-edit"); } }), _jsxs("box", { style: { flexDirection: "column", flexGrow: 1, backgroundColor: "#0b0b0b", overflow: "hidden" }, children: [_jsx(TaskInspector, { task: selectedTask }, `ti-${selectedTask?.id}`), _jsx(TaskActionButtons, { task: selectedTask, focused: taskFocusedPanel === "actions", selectedAction: taskSelectedAction, selectable: stack.includes("create") || stack.includes("task-edit"), onAction: handleTaskAction }, `tab-${selectedTask?.id}`)] })] })) : view === "projects" ? (_jsx(ProjectsPage, { projects: projects, loops: loops, onClose: () => pop(), onRefresh: refreshProjects, onOpenCreate: (trigger) => { createProjectTriggerRef.current = trigger; } })) : (_jsxs("box", { style: { flexDirection: breakpoint === "narrow" ? "column" : "row", flexGrow: 1, backgroundColor: "#0b0b0b" }, children: [_jsx(Navigator, { visible: visible, total: loops.length, selectedIndex: clampedIndex, filters: filters, sort: sort, breakpoint: breakpoint, focused: focusedPanel === "loops", projects: projects, onSelect: (index) => { setSelectedIndex(index); setFocusedPanel("loops"); }, onActivate: (index) => { setSelectedIndex(index); const loop = visible[index]; if (loop) {
|
|
349
349
|
setEditTarget(loop);
|
|
350
350
|
push("create");
|
|
351
351
|
} } }), _jsxs("box", { style: { flexDirection: "column", flexGrow: 1, backgroundColor: "#0b0b0b", overflow: "hidden" }, children: [_jsx(Inspector, { loop: selected }), _jsx(RunHistory, { loop: selected, selectedRunIndex: selectedRunIndex, focused: focusedPanel === "runs", onSelectRun: (index) => { setSelectedRunIndex(index); setFocusedPanel("runs"); }, onOpenRun: handleOpenRunLog }), _jsx(ActionButtons, { loop: selected, focused: focusedPanel === "actions", selectedAction: selectedAction, onAction: handleAction })] })] })) }, viewKey(view, editTarget, editTask)), _jsx(Footer, { mode: mode }), confirm ? (_jsx(ConfirmModal, { message: confirm.message, choice: confirmChoice, onYes: () => { const action = confirm.action; setConfirm(null); void action(); }, onNo: () => setConfirm(null) })) : null, helpOpen ? _jsx(HelpModal, { view: view }) : null, logModalRun ? (_jsx(LogModal, { loopId: selectedId, run: logModalRun, logLines: logModalLines, loading: logModalLoading, onClose: () => setLogModalRun(null) })) : null, projectsModalOpen ? (_jsx(ProjectsModal, { projects: projects, loops: loops, currentProjectId: currentProjectId, onSelect: (id) => { setCurrentProjectId(id); setProjectsModalOpen(false); }, onClose: () => setProjectsModalOpen(false) })) : null, _jsx(ToastStack, { toasts: toasts })] }));
|
|
@@ -86,38 +86,14 @@ export function CreateView(props) {
|
|
|
86
86
|
if (key.name === "tab") {
|
|
87
87
|
setFocusIndex((i) => {
|
|
88
88
|
const next = key.shift ? i - 1 : i + 1;
|
|
89
|
-
|
|
89
|
+
if (next < 0)
|
|
90
|
+
return cancelIndex;
|
|
91
|
+
if (next > cancelIndex)
|
|
92
|
+
return 0;
|
|
93
|
+
return next;
|
|
90
94
|
});
|
|
91
95
|
return;
|
|
92
96
|
}
|
|
93
|
-
if (key.name === "left" || key.name === "right") {
|
|
94
|
-
if (focusIndex === saveIndex) {
|
|
95
|
-
setFocusIndex(key.name === "right" ? cancelIndex : focusIndex - 1 >= 0 ? filteredFields.length - 1 : 0);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
if (focusIndex === cancelIndex) {
|
|
99
|
-
setFocusIndex(key.name === "right" ? 0 : saveIndex);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
if (key.name === "right") {
|
|
103
|
-
if (focusIndex + 1 < filteredFields.length) {
|
|
104
|
-
setFocusIndex((i) => i + 1);
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
setFocusIndex(saveIndex);
|
|
108
|
-
}
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
if (key.name === "left") {
|
|
112
|
-
if (focusIndex > 0) {
|
|
113
|
-
setFocusIndex((i) => i - 1);
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
setFocusIndex(cancelIndex);
|
|
117
|
-
}
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
97
|
if (key.name === "return" || key.name === "enter" || key.name === " ") {
|
|
122
98
|
const field = filteredFields[focusIndex];
|
|
123
99
|
if (field === "taskMode") {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
2
|
import { t } from "../../i18n/index.js";
|
|
3
3
|
import { useHoverState } from "../hooks/useHoverState.js";
|
|
4
4
|
import { HOVER_BG } from "../../config/constants.js";
|
|
5
|
+
import { SearchBox } from "./SearchBox.js";
|
|
5
6
|
export function FilterBar(props) {
|
|
6
|
-
const { filters, sort, searchActive, focusedPanel, onStatusCycle, onSortCycle, onSelectProject, currentProjectName } = props;
|
|
7
|
+
const { filters, sort, searchActive, focusedPanel, onStatusCycle, onSortCycle, onSelectProject, currentProjectName, onQueryChange, onSearchActivate, onSearchDismiss } = props;
|
|
7
8
|
const statusDisplay = filters.status === "waiting" ? "waiting" : filters.status;
|
|
8
|
-
return (_jsxs("box", { style: { flexDirection: "row", height: 3, paddingLeft: 1, paddingRight: 1, backgroundColor: "#0b0b0b" }, children: [_jsx(
|
|
9
|
+
return (_jsxs("box", { style: { flexDirection: "row", height: 3, paddingLeft: 1, paddingRight: 1, backgroundColor: "#0b0b0b" }, children: [_jsx(SearchBox, { query: filters.query, searchActive: searchActive, focused: focusedPanel === "search", onQueryChange: onQueryChange, onActivate: onSearchActivate, onDismiss: onSearchDismiss }), onSelectProject ? (_jsx(ClickableBadge, { title: t("project.filterTitle"), text: currentProjectName ?? "Default", textColor: "#f59e0b", focused: focusedPanel === "project-filter", onMouseDown: onSelectProject, marginRight: 1 })) : null, _jsx(ClickableBadge, { title: t("board.statusFilterTitle"), text: statusDisplay, textColor: "#38bdf8", focused: focusedPanel === "status", onMouseDown: onStatusCycle, marginRight: 1 }), _jsx(ClickableBadge, { title: t("board.sortTitle"), text: sort, textColor: "#a3e635", focused: focusedPanel === "sort", onMouseDown: onSortCycle })] }));
|
|
9
10
|
}
|
|
10
11
|
function ClickableBadge(props) {
|
|
11
12
|
const { isHovered, hoverProps } = useHoverState();
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { t } from "../../i18n/index.js";
|
|
3
|
+
export function SearchBox(props) {
|
|
4
|
+
const { query, searchActive, focused, onQueryChange, onActivate, onDismiss } = props;
|
|
5
|
+
return (_jsx("box", { title: t("board.searchTitle"), border: true, borderColor: focused ? "#38bdf8" : undefined, style: { width: "25%", height: 3, marginRight: 1, paddingLeft: 1, backgroundColor: focused ? "#1e2a4a" : "#0b0b0b" }, children: searchActive ? (_jsx("input", { focused: true, value: query, placeholder: t("board.searchPlaceholder"), onInput: (value) => onQueryChange(value), onSubmit: () => onDismiss() })) : (_jsx("text", { fg: query ? "#e5e7eb" : "#6b7280", onMouseDown: onActivate, children: query || t("board.searchEmpty") })) }));
|
|
6
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { SearchBox } from "./SearchBox.js";
|
|
3
3
|
export function TaskFilterBar(props) {
|
|
4
|
-
const { query, searchActive, focusedPanel } = props;
|
|
5
|
-
return (_jsx("box", { style: { flexDirection: "row", height: 3, paddingLeft: 1, paddingRight: 1, backgroundColor: "#0b0b0b" }, children: _jsx("box", {
|
|
4
|
+
const { query, searchActive, focusedPanel, onQueryChange, onSearchActivate, onSearchDismiss } = props;
|
|
5
|
+
return (_jsx("box", { style: { flexDirection: "row", height: 3, paddingLeft: 1, paddingRight: 1, backgroundColor: "#0b0b0b" }, children: _jsx("box", { style: { flexGrow: 1 }, children: _jsx(SearchBox, { query: query, searchActive: searchActive, focused: focusedPanel === "search", onQueryChange: onQueryChange, onActivate: onSearchActivate, onDismiss: onSearchDismiss }) }) }));
|
|
6
6
|
}
|
|
@@ -40,38 +40,14 @@ export function TaskForm(props) {
|
|
|
40
40
|
if (key.name === "tab") {
|
|
41
41
|
setFocusIndex((i) => {
|
|
42
42
|
const next = key.shift ? i - 1 : i + 1;
|
|
43
|
-
|
|
43
|
+
if (next < 0)
|
|
44
|
+
return cancelIndex;
|
|
45
|
+
if (next > cancelIndex)
|
|
46
|
+
return 0;
|
|
47
|
+
return next;
|
|
44
48
|
});
|
|
45
49
|
return;
|
|
46
50
|
}
|
|
47
|
-
if (key.name === "left" || key.name === "right") {
|
|
48
|
-
if (focusIndex === saveIndex) {
|
|
49
|
-
setFocusIndex(key.name === "right" ? cancelIndex : taskFields.length - 1);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
if (focusIndex === cancelIndex) {
|
|
53
|
-
setFocusIndex(key.name === "right" ? 0 : saveIndex);
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
if (key.name === "right") {
|
|
57
|
-
if (focusIndex + 1 < taskFields.length) {
|
|
58
|
-
setFocusIndex((i) => i + 1);
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
setFocusIndex(saveIndex);
|
|
62
|
-
}
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
if (key.name === "left") {
|
|
66
|
-
if (focusIndex > 0) {
|
|
67
|
-
setFocusIndex((i) => i - 1);
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
setFocusIndex(cancelIndex);
|
|
71
|
-
}
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
51
|
if (key.name === "return" || key.name === "enter") {
|
|
76
52
|
if (focusIndex === saveIndex) {
|
|
77
53
|
void submit(valuesRef.current);
|
|
@@ -219,23 +219,10 @@ export function useBoardKeybindings(params) {
|
|
|
219
219
|
}
|
|
220
220
|
if (helpOpen)
|
|
221
221
|
return;
|
|
222
|
-
if (searchActive && (name === "escape" || name === "return" || name === "enter")) {
|
|
223
|
-
OVERLAY_DISMISS.search({ setLogModalRun, setHelpOpen, setSearchActive, setFocusedPanel }, name);
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
222
|
if (searchActive) {
|
|
227
|
-
if (name === "
|
|
223
|
+
if (name === "escape") {
|
|
228
224
|
setSearchActive(false);
|
|
229
|
-
setFocusedPanel("
|
|
230
|
-
setSelectedAction((selected ? getActionCount(selected.status) : 0) - 1);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
if (name === "backspace") {
|
|
234
|
-
setFilters((prev) => ({ ...prev, query: prev.query.slice(0, -1) }));
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
if (key.sequence && key.sequence.length === 1 && key.sequence >= " " && key.sequence <= "~") {
|
|
238
|
-
setFilters((prev) => ({ ...prev, query: prev.query + key.sequence }));
|
|
225
|
+
setFocusedPanel("loops");
|
|
239
226
|
return;
|
|
240
227
|
}
|
|
241
228
|
return;
|
|
@@ -18,25 +18,6 @@ export function useTaskKeybindings(params) {
|
|
|
18
18
|
setTaskFocusedPanel("tasks");
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
|
-
if (name === "return" || name === "enter") {
|
|
22
|
-
setTaskSearchActive(false);
|
|
23
|
-
setTaskFocusedPanel("tasks");
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
if (name === "left") {
|
|
27
|
-
setTaskSearchActive(false);
|
|
28
|
-
setTaskFocusedPanel("actions");
|
|
29
|
-
setTaskSelectedAction(taskActionCount - 1);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
if (name === "backspace") {
|
|
33
|
-
setTaskQuery((q) => q.slice(0, -1));
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
if (key.sequence && key.sequence.length === 1 && key.sequence >= " " && key.sequence <= "~") {
|
|
37
|
-
setTaskQuery((q) => q + key.sequence);
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
21
|
return;
|
|
41
22
|
}
|
|
42
23
|
if (name === "escape") {
|
package/dist/config/paths.js
CHANGED
|
@@ -21,6 +21,15 @@ export function loopFile(id) {
|
|
|
21
21
|
export function taskFile(id) {
|
|
22
22
|
return path.join(getTasksDir(), `${id}.json`);
|
|
23
23
|
}
|
|
24
|
+
export function loopsJson() {
|
|
25
|
+
return path.join(getDataDir(), "loops.json");
|
|
26
|
+
}
|
|
27
|
+
export function tasksJson() {
|
|
28
|
+
return path.join(getDataDir(), "tasks.json");
|
|
29
|
+
}
|
|
30
|
+
export function projectsJson() {
|
|
31
|
+
return path.join(getDataDir(), "projects.json");
|
|
32
|
+
}
|
|
24
33
|
export function logFile(id) {
|
|
25
34
|
return path.join(getLogsDir(), `${id}.log`);
|
|
26
35
|
}
|
|
@@ -335,12 +335,15 @@ export class LoopController extends EventEmitter {
|
|
|
335
335
|
}
|
|
336
336
|
const chainTargetId = result.exitCode === 0 ? task?.onSuccessTaskId : task?.onFailureTaskId;
|
|
337
337
|
if (chainTargetId) {
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
338
|
+
const chainGroupId = crypto.randomUUID().slice(0, 8);
|
|
339
|
+
const mainRecord = this.runHistory[this.runHistory.length - 1];
|
|
340
|
+
if (mainRecord)
|
|
341
|
+
mainRecord.chainGroupId = chainGroupId;
|
|
342
|
+
let currentTargetId = chainTargetId;
|
|
343
|
+
while (currentTargetId) {
|
|
344
|
+
const chainTask = this.taskResolver(currentTargetId);
|
|
345
|
+
if (!chainTask)
|
|
346
|
+
break;
|
|
344
347
|
const chainStartedAt = new Date().toISOString();
|
|
345
348
|
const chainOffset = fs.existsSync(this.logPath) ? fs.statSync(this.logPath).size : 0;
|
|
346
349
|
this.runHistory.push({
|
|
@@ -356,7 +359,7 @@ export class LoopController extends EventEmitter {
|
|
|
356
359
|
});
|
|
357
360
|
const chainResult = await executeCommand(chainTask.command, chainTask.commandArgs, this.options.cwd, this.logStream, signal, this.runCount);
|
|
358
361
|
const chainLogSize = fs.existsSync(this.logPath) ? fs.statSync(this.logPath).size - chainOffset : 0;
|
|
359
|
-
const chainRecord = this.runHistory.find((r) => r.chainGroupId === chainGroupId && r.status === "running");
|
|
362
|
+
const chainRecord = this.runHistory.find((r) => r.chainGroupId === chainGroupId && r.status === "running" && r.chainName === chainTask.name);
|
|
360
363
|
if (chainRecord) {
|
|
361
364
|
chainRecord.exitCode = chainResult.exitCode;
|
|
362
365
|
chainRecord.duration = chainResult.duration;
|
|
@@ -365,6 +368,7 @@ export class LoopController extends EventEmitter {
|
|
|
365
368
|
}
|
|
366
369
|
this.lastExitCode = chainResult.exitCode;
|
|
367
370
|
this.lastDuration = (this.lastDuration ?? 0) + chainResult.duration;
|
|
371
|
+
currentTargetId = (chainResult.exitCode === 0 ? chainTask.onSuccessTaskId : chainTask.onFailureTaskId) ?? null;
|
|
368
372
|
}
|
|
369
373
|
}
|
|
370
374
|
if (this.runHistory.length > 50) {
|
package/dist/daemon/manager.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { LoopController } from "../core/loop-controller.js";
|
|
3
3
|
import { ProjectManager } from "./projects.js";
|
|
4
|
-
import { saveLoop, loadAllLoops, deleteLoop as deleteLoopState, getLogPath, } from "./state.js";
|
|
4
|
+
import { saveLoop, loadAllLoops, deleteLoop as deleteLoopState, getLogPath, migrateLoopsToJson, migrateTasksToJson, } from "./state.js";
|
|
5
5
|
import { daemonLog } from "./daemon-log.js";
|
|
6
6
|
export class LoopManager {
|
|
7
7
|
loops = new Map();
|
|
@@ -16,6 +16,9 @@ export class LoopManager {
|
|
|
16
16
|
init() {
|
|
17
17
|
// Initialize projects first
|
|
18
18
|
this.projectManager.init();
|
|
19
|
+
// Migrate from individual files to consolidated JSON
|
|
20
|
+
migrateLoopsToJson();
|
|
21
|
+
migrateTasksToJson();
|
|
19
22
|
const saved = loadAllLoops();
|
|
20
23
|
let restarted = 0;
|
|
21
24
|
let migrated = 0;
|
package/dist/daemon/projects.js
CHANGED
|
@@ -1,53 +1,76 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { writeFileAtomic } from "../shared/fs-utils.js";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
3
|
import { daemonLog } from "./daemon-log.js";
|
|
6
4
|
import crypto from "node:crypto";
|
|
5
|
+
import { getDataDir, projectsJson } from "../config/paths.js";
|
|
6
|
+
import path from "node:path";
|
|
7
7
|
export class ProjectManager {
|
|
8
8
|
projects = new Map();
|
|
9
|
-
projectsDir;
|
|
10
|
-
constructor(loopCliHome) {
|
|
11
|
-
const baseDir = loopCliHome || join(homedir(), ".loop-cli");
|
|
12
|
-
this.projectsDir = join(baseDir, "projects");
|
|
13
|
-
}
|
|
14
9
|
init() {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
const dataDir = getDataDir();
|
|
11
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
12
|
+
this.migrateProjectsToJson();
|
|
18
13
|
this.loadProjects();
|
|
19
14
|
if (!this.projects.has("default")) {
|
|
20
15
|
this.createDefaultProject();
|
|
21
16
|
}
|
|
22
17
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (
|
|
18
|
+
migrateProjectsToJson() {
|
|
19
|
+
const jsonFile = projectsJson();
|
|
20
|
+
if (fs.existsSync(jsonFile))
|
|
21
|
+
return;
|
|
22
|
+
const oldDir = path.join(getDataDir(), "projects");
|
|
23
|
+
if (!fs.existsSync(oldDir))
|
|
26
24
|
return;
|
|
27
25
|
try {
|
|
28
|
-
const files = fs.readdirSync(
|
|
26
|
+
const files = fs.readdirSync(oldDir).filter((f) => f.endsWith(".json"));
|
|
27
|
+
if (files.length === 0)
|
|
28
|
+
return;
|
|
29
|
+
const projects = [];
|
|
29
30
|
for (const file of files) {
|
|
30
|
-
if (!file.endsWith(".json"))
|
|
31
|
-
continue;
|
|
32
31
|
try {
|
|
33
|
-
const raw = fs.readFileSync(join(
|
|
32
|
+
const raw = fs.readFileSync(path.join(oldDir, file), "utf-8");
|
|
34
33
|
const content = JSON.parse(raw);
|
|
35
34
|
if (content.id)
|
|
36
|
-
|
|
35
|
+
projects.push(content);
|
|
37
36
|
}
|
|
38
|
-
catch
|
|
39
|
-
|
|
37
|
+
catch {
|
|
38
|
+
// skip corrupted files
|
|
40
39
|
}
|
|
41
40
|
}
|
|
41
|
+
writeFileAtomic(jsonFile, JSON.stringify(projects, null, 2));
|
|
42
|
+
daemonLog(`migrated ${projects.length} project(s) to projects.json`);
|
|
42
43
|
}
|
|
43
44
|
catch (err) {
|
|
44
|
-
daemonLog(`Failed to
|
|
45
|
+
daemonLog(`Failed to migrate projects: ${err}`);
|
|
45
46
|
}
|
|
46
47
|
}
|
|
48
|
+
loadProjects() {
|
|
49
|
+
this.projects.clear();
|
|
50
|
+
const jsonFile = projectsJson();
|
|
51
|
+
if (!fs.existsSync(jsonFile))
|
|
52
|
+
return;
|
|
53
|
+
try {
|
|
54
|
+
const raw = fs.readFileSync(jsonFile, "utf-8");
|
|
55
|
+
const parsed = JSON.parse(raw);
|
|
56
|
+
if (!Array.isArray(parsed))
|
|
57
|
+
return;
|
|
58
|
+
for (const item of parsed) {
|
|
59
|
+
if (item && item.id)
|
|
60
|
+
this.projects.set(item.id, item);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
daemonLog(`Failed to load projects.json: ${err}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
saveAllProjects() {
|
|
68
|
+
const all = Array.from(this.projects.values());
|
|
69
|
+
writeFileAtomic(projectsJson(), JSON.stringify(all, null, 2));
|
|
70
|
+
}
|
|
47
71
|
saveProject(project) {
|
|
48
|
-
const filePath = join(this.projectsDir, `${project.id}.json`);
|
|
49
|
-
writeFileAtomic(filePath, JSON.stringify(project, null, 2));
|
|
50
72
|
this.projects.set(project.id, project);
|
|
73
|
+
this.saveAllProjects();
|
|
51
74
|
}
|
|
52
75
|
createDefaultProject() {
|
|
53
76
|
const defaultProject = {
|
|
@@ -96,9 +119,7 @@ export class ProjectManager {
|
|
|
96
119
|
throw new Error(`Project ${id} not found`);
|
|
97
120
|
if (project.isSystem)
|
|
98
121
|
throw new Error("Cannot delete system project");
|
|
99
|
-
const filePath = join(this.projectsDir, `${id}.json`);
|
|
100
|
-
if (fs.existsSync(filePath))
|
|
101
|
-
fs.rmSync(filePath);
|
|
102
122
|
this.projects.delete(id);
|
|
123
|
+
this.saveAllProjects();
|
|
103
124
|
}
|
|
104
125
|
}
|
package/dist/daemon/state.js
CHANGED
|
@@ -3,27 +3,39 @@ import path from "node:path";
|
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { removeIfExists, writeFileAtomic } from "../shared/fs-utils.js";
|
|
6
|
-
import { getLoopsDir, getTasksDir, getLogsDir,
|
|
6
|
+
import { getLoopsDir, getTasksDir, getLogsDir, logFile, getPidFile, getSignatureFile, getSocketPath, loopsJson, tasksJson, getDataDir, } from "../config/paths.js";
|
|
7
7
|
export { getDataDir, getPidFile, getSocketPath } from "../config/paths.js";
|
|
8
8
|
function ensureDirs() {
|
|
9
|
-
fs.mkdirSync(
|
|
9
|
+
fs.mkdirSync(getDataDir(), { recursive: true });
|
|
10
10
|
fs.mkdirSync(getLogsDir(), { recursive: true });
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
function readJsonArray(filePath) {
|
|
13
|
+
if (!fs.existsSync(filePath))
|
|
14
|
+
return [];
|
|
15
|
+
try {
|
|
16
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
17
|
+
const parsed = JSON.parse(raw);
|
|
18
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
15
23
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return null;
|
|
20
|
-
const raw = fs.readFileSync(file, "utf-8");
|
|
21
|
-
return JSON.parse(raw);
|
|
24
|
+
function writeJsonArray(filePath, items) {
|
|
25
|
+
ensureDirs();
|
|
26
|
+
writeFileAtomic(filePath, JSON.stringify(items, null, 2));
|
|
22
27
|
}
|
|
23
|
-
export function
|
|
28
|
+
export function migrateLoopsToJson() {
|
|
24
29
|
ensureDirs();
|
|
30
|
+
const jsonFile = loopsJson();
|
|
31
|
+
if (fs.existsSync(jsonFile))
|
|
32
|
+
return;
|
|
25
33
|
const dir = getLoopsDir();
|
|
34
|
+
if (!fs.existsSync(dir))
|
|
35
|
+
return;
|
|
26
36
|
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
37
|
+
if (files.length === 0)
|
|
38
|
+
return;
|
|
27
39
|
const loops = [];
|
|
28
40
|
for (const file of files) {
|
|
29
41
|
try {
|
|
@@ -34,28 +46,20 @@ export function loadAllLoops() {
|
|
|
34
46
|
// skip corrupted files
|
|
35
47
|
}
|
|
36
48
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
export function deleteLoop(id) {
|
|
40
|
-
removeIfExists(loopFile(id));
|
|
41
|
-
removeIfExists(logFile(id));
|
|
42
|
-
}
|
|
43
|
-
export function saveTask(task) {
|
|
44
|
-
const dir = getTasksDir();
|
|
45
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
46
|
-
writeFileAtomic(taskFile(task.id), JSON.stringify(task, null, 2));
|
|
47
|
-
}
|
|
48
|
-
export function loadTask(id) {
|
|
49
|
-
const file = taskFile(id);
|
|
50
|
-
if (!fs.existsSync(file))
|
|
51
|
-
return null;
|
|
52
|
-
const raw = fs.readFileSync(file, "utf-8");
|
|
53
|
-
return JSON.parse(raw);
|
|
49
|
+
loops.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
50
|
+
writeJsonArray(jsonFile, loops);
|
|
54
51
|
}
|
|
55
|
-
export function
|
|
52
|
+
export function migrateTasksToJson() {
|
|
53
|
+
ensureDirs();
|
|
54
|
+
const jsonFile = tasksJson();
|
|
55
|
+
if (fs.existsSync(jsonFile))
|
|
56
|
+
return;
|
|
56
57
|
const dir = getTasksDir();
|
|
57
|
-
fs.
|
|
58
|
+
if (!fs.existsSync(dir))
|
|
59
|
+
return;
|
|
58
60
|
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
61
|
+
if (files.length === 0)
|
|
62
|
+
return;
|
|
59
63
|
const tasks = [];
|
|
60
64
|
for (const file of files) {
|
|
61
65
|
try {
|
|
@@ -66,10 +70,57 @@ export function loadAllTasks() {
|
|
|
66
70
|
// skip corrupted files
|
|
67
71
|
}
|
|
68
72
|
}
|
|
73
|
+
tasks.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
74
|
+
writeJsonArray(jsonFile, tasks);
|
|
75
|
+
}
|
|
76
|
+
export function saveLoop(meta) {
|
|
77
|
+
const loops = readJsonArray(loopsJson());
|
|
78
|
+
const idx = loops.findIndex((l) => l.id === meta.id);
|
|
79
|
+
if (idx >= 0)
|
|
80
|
+
loops[idx] = meta;
|
|
81
|
+
else
|
|
82
|
+
loops.push(meta);
|
|
83
|
+
writeJsonArray(loopsJson(), loops);
|
|
84
|
+
}
|
|
85
|
+
export function loadLoop(id) {
|
|
86
|
+
const loops = readJsonArray(loopsJson());
|
|
87
|
+
return loops.find((l) => l.id === id) ?? null;
|
|
88
|
+
}
|
|
89
|
+
export function loadAllLoops() {
|
|
90
|
+
const loops = readJsonArray(loopsJson());
|
|
91
|
+
return loops.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
92
|
+
}
|
|
93
|
+
export function deleteLoop(id) {
|
|
94
|
+
const loops = readJsonArray(loopsJson());
|
|
95
|
+
const filtered = loops.filter((l) => l.id !== id);
|
|
96
|
+
if (filtered.length !== loops.length) {
|
|
97
|
+
writeJsonArray(loopsJson(), filtered);
|
|
98
|
+
}
|
|
99
|
+
removeIfExists(logFile(id));
|
|
100
|
+
}
|
|
101
|
+
export function saveTask(task) {
|
|
102
|
+
const tasks = readJsonArray(tasksJson());
|
|
103
|
+
const idx = tasks.findIndex((t) => t.id === task.id);
|
|
104
|
+
if (idx >= 0)
|
|
105
|
+
tasks[idx] = task;
|
|
106
|
+
else
|
|
107
|
+
tasks.push(task);
|
|
108
|
+
writeJsonArray(tasksJson(), tasks);
|
|
109
|
+
}
|
|
110
|
+
export function loadTask(id) {
|
|
111
|
+
const tasks = readJsonArray(tasksJson());
|
|
112
|
+
return tasks.find((t) => t.id === id) ?? null;
|
|
113
|
+
}
|
|
114
|
+
export function loadAllTasks() {
|
|
115
|
+
const tasks = readJsonArray(tasksJson());
|
|
69
116
|
return tasks.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
70
117
|
}
|
|
71
118
|
export function deleteTask(id) {
|
|
72
|
-
|
|
119
|
+
const tasks = readJsonArray(tasksJson());
|
|
120
|
+
const filtered = tasks.filter((t) => t.id !== id);
|
|
121
|
+
if (filtered.length !== tasks.length) {
|
|
122
|
+
writeJsonArray(tasksJson(), filtered);
|
|
123
|
+
}
|
|
73
124
|
}
|
|
74
125
|
export function getLogPath(id) {
|
|
75
126
|
ensureDirs();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loop-task",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"description": "Loop engineering toolkit. Run any command on a cadence, in the background, managed from a terminal board. Schedule tests, builds, syncs, or agent prompts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|