loop-task 1.4.2 → 1.4.4
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 +32 -39
- package/dist/board/components/FilterBar.js +4 -3
- package/dist/board/components/HelpModal.js +8 -5
- package/dist/board/components/ProjectsPage.js +9 -1
- package/dist/board/components/SearchBox.js +6 -0
- package/dist/board/components/TaskFilterBar.js +4 -4
- package/dist/board/components/TaskForm.js +19 -28
- package/dist/board/hooks/useBoardKeybindings.js +27 -28
- package/dist/board/hooks/useTaskKeybindings.js +24 -29
- package/dist/config/paths.js +9 -0
- package/dist/core/loop-controller.js +11 -7
- package/dist/daemon/index.js +3 -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,72 +86,65 @@ 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
|
});
|
|
95
|
+
key.preventDefault();
|
|
91
96
|
return;
|
|
92
97
|
}
|
|
93
|
-
if (key.name === "
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
}
|
|
98
|
+
if (key.name === "up" || key.name === "down") {
|
|
99
|
+
const field = filteredFields[focusIndex];
|
|
100
|
+
if (field === "project" && projectOptions.length > 1) {
|
|
101
|
+
const currentIdx = projectOptions.findIndex((p) => p.value === valuesRef.current.project);
|
|
102
|
+
const nextIdx = key.name === "down"
|
|
103
|
+
? (currentIdx + 1) % projectOptions.length
|
|
104
|
+
: (currentIdx - 1 + projectOptions.length) % projectOptions.length;
|
|
105
|
+
const next = projectOptions[nextIdx];
|
|
106
|
+
if (next)
|
|
107
|
+
updateValues({ ...valuesRef.current, project: next.value });
|
|
108
|
+
key.preventDefault();
|
|
118
109
|
return;
|
|
119
110
|
}
|
|
111
|
+
setFocusIndex((i) => {
|
|
112
|
+
const next = key.name === "up" ? i - 1 : i + 1;
|
|
113
|
+
if (next < 0)
|
|
114
|
+
return cancelIndex;
|
|
115
|
+
if (next > cancelIndex)
|
|
116
|
+
return 0;
|
|
117
|
+
return next;
|
|
118
|
+
});
|
|
119
|
+
key.preventDefault();
|
|
120
|
+
return;
|
|
120
121
|
}
|
|
121
122
|
if (key.name === "return" || key.name === "enter" || key.name === " ") {
|
|
122
123
|
const field = filteredFields[focusIndex];
|
|
123
124
|
if (field === "taskMode") {
|
|
124
125
|
const next = valuesRef.current.taskMode === TASK_MODE_INLINE ? TASK_MODE_EXISTING : TASK_MODE_INLINE;
|
|
125
126
|
updateValues({ ...valuesRef.current, taskMode: next });
|
|
127
|
+
key.preventDefault();
|
|
126
128
|
return;
|
|
127
129
|
}
|
|
128
130
|
if (field === "runNow") {
|
|
129
131
|
const next = valuesRef.current.runNow === "y" ? "n" : "y";
|
|
130
132
|
updateValues({ ...valuesRef.current, runNow: next });
|
|
133
|
+
key.preventDefault();
|
|
131
134
|
return;
|
|
132
135
|
}
|
|
133
136
|
if (focusIndex === chooseTaskIdx && !isInline) {
|
|
134
137
|
props.onChooseTask();
|
|
138
|
+
key.preventDefault();
|
|
135
139
|
return;
|
|
136
140
|
}
|
|
137
141
|
if (focusIndex === saveIndex) {
|
|
138
142
|
void submit(valuesRef.current);
|
|
143
|
+
key.preventDefault();
|
|
139
144
|
}
|
|
140
145
|
else if (focusIndex === cancelIndex) {
|
|
141
146
|
props.onCancel();
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
if (key.name === "up" || key.name === "down") {
|
|
145
|
-
const field = filteredFields[focusIndex];
|
|
146
|
-
if (field === "project" && projectOptions.length > 1) {
|
|
147
|
-
const currentIdx = projectOptions.findIndex((p) => p.value === valuesRef.current.project);
|
|
148
|
-
const nextIdx = key.name === "down"
|
|
149
|
-
? (currentIdx + 1) % projectOptions.length
|
|
150
|
-
: (currentIdx - 1 + projectOptions.length) % projectOptions.length;
|
|
151
|
-
const next = projectOptions[nextIdx];
|
|
152
|
-
if (next)
|
|
153
|
-
updateValues({ ...valuesRef.current, project: next.value });
|
|
154
|
-
return;
|
|
147
|
+
key.preventDefault();
|
|
155
148
|
}
|
|
156
149
|
}
|
|
157
150
|
});
|
|
@@ -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();
|
|
@@ -5,40 +5,43 @@ export function HelpModal(props) {
|
|
|
5
5
|
const { view } = props;
|
|
6
6
|
const { width } = useTerminalDimensions();
|
|
7
7
|
const loopRows = [
|
|
8
|
+
["tab/←→", "switch panels"],
|
|
9
|
+
["↑/↓", "navigate list items"],
|
|
10
|
+
["enter", t("board.helpEdit")],
|
|
8
11
|
["e", "edit loop"],
|
|
9
12
|
["d/del", "delete loop"],
|
|
10
13
|
["c", "clone loop"],
|
|
11
14
|
["f", "force run"],
|
|
12
15
|
["p", "pause/play (contextual)"],
|
|
13
16
|
["s", "stop loop (resets schedule)"],
|
|
14
|
-
[t("board.helpKeyEnter"), t("board.helpEdit")],
|
|
15
17
|
["n", t("board.helpCreate")],
|
|
16
18
|
["t", t("board.helpCreateTask")],
|
|
17
19
|
["o", t("board.helpCycleSort")],
|
|
18
20
|
["x", "cycle status filter"],
|
|
19
21
|
["r", "project filter"],
|
|
20
|
-
["←/→", t("board.helpSwitchPanel")],
|
|
21
22
|
["/", t("board.helpSearch")],
|
|
22
23
|
["h", t("board.helpToggleHelp")],
|
|
23
24
|
["esc", t("board.helpQuit")],
|
|
24
25
|
];
|
|
25
26
|
const taskRows = [
|
|
27
|
+
["tab/←→", "switch panels"],
|
|
28
|
+
["↑/↓", "navigate tasks"],
|
|
29
|
+
["enter", "focus actions panel"],
|
|
26
30
|
["e", "edit task"],
|
|
27
31
|
["d/del", "delete task"],
|
|
28
32
|
["s", "select task (when creating/editing a loop)"],
|
|
29
|
-
["enter", "focus actions panel"],
|
|
30
33
|
["n", t("board.helpCreateTask")],
|
|
31
|
-
["←/→", t("board.helpSwitchPanel")],
|
|
32
34
|
["/", t("board.helpSearch")],
|
|
33
35
|
["h", t("board.helpToggleHelp")],
|
|
34
36
|
["esc", t("board.helpQuit")],
|
|
35
37
|
];
|
|
36
38
|
const projectRows = [
|
|
39
|
+
["tab/←→", "switch panel (list/actions)"],
|
|
40
|
+
["↑/↓", "navigate projects"],
|
|
37
41
|
["enter", "focus actions"],
|
|
38
42
|
["n", "new project"],
|
|
39
43
|
["e", "edit project"],
|
|
40
44
|
["d", "delete project"],
|
|
41
|
-
["←/→", "switch panel (list/actions)"],
|
|
42
45
|
["h", t("board.helpToggleHelp")],
|
|
43
46
|
["esc", t("board.helpQuit")],
|
|
44
47
|
];
|
|
@@ -41,6 +41,7 @@ export function ProjectsPage(props) {
|
|
|
41
41
|
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
42
42
|
else
|
|
43
43
|
setSelectedAction((a) => Math.max(0, a - 1));
|
|
44
|
+
key.preventDefault();
|
|
44
45
|
return;
|
|
45
46
|
}
|
|
46
47
|
if (key.name === "down") {
|
|
@@ -48,10 +49,12 @@ export function ProjectsPage(props) {
|
|
|
48
49
|
setSelectedIndex((i) => Math.min(projects.length - 1, i + 1));
|
|
49
50
|
else
|
|
50
51
|
setSelectedAction((a) => Math.min(PROJECT_ACTION_COUNT - 1, a + 1));
|
|
52
|
+
key.preventDefault();
|
|
51
53
|
return;
|
|
52
54
|
}
|
|
53
|
-
if (key.name === "left" || key.name === "right") {
|
|
55
|
+
if (key.name === "left" || key.name === "right" || key.name === "tab") {
|
|
54
56
|
setFocusedPanel((p) => (p === "list" ? "actions" : "list"));
|
|
57
|
+
key.preventDefault();
|
|
55
58
|
return;
|
|
56
59
|
}
|
|
57
60
|
if (key.name === "return" || key.name === "enter") {
|
|
@@ -61,22 +64,27 @@ export function ProjectsPage(props) {
|
|
|
61
64
|
else {
|
|
62
65
|
runAction(selectedAction);
|
|
63
66
|
}
|
|
67
|
+
key.preventDefault();
|
|
64
68
|
return;
|
|
65
69
|
}
|
|
66
70
|
if (key.name === "n") {
|
|
67
71
|
setSubModal("create");
|
|
72
|
+
key.preventDefault();
|
|
68
73
|
return;
|
|
69
74
|
}
|
|
70
75
|
if (key.name === "e" && selectedProject && !selectedProject.isSystem) {
|
|
71
76
|
setSubModal("edit");
|
|
77
|
+
key.preventDefault();
|
|
72
78
|
return;
|
|
73
79
|
}
|
|
74
80
|
if (key.name === "d" && selectedProject && !selectedProject.isSystem) {
|
|
75
81
|
setSubModal("delete");
|
|
82
|
+
key.preventDefault();
|
|
76
83
|
return;
|
|
77
84
|
}
|
|
78
85
|
if (key.name === "escape") {
|
|
79
86
|
onClose();
|
|
87
|
+
key.preventDefault();
|
|
80
88
|
}
|
|
81
89
|
});
|
|
82
90
|
const actions = [
|
|
@@ -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,44 +40,35 @@ 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
|
});
|
|
49
|
+
key.preventDefault();
|
|
45
50
|
return;
|
|
46
51
|
}
|
|
47
|
-
if (key.name === "
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
}
|
|
52
|
+
if (key.name === "up" || key.name === "down") {
|
|
53
|
+
setFocusIndex((i) => {
|
|
54
|
+
const next = key.name === "up" ? i - 1 : i + 1;
|
|
55
|
+
if (next < 0)
|
|
56
|
+
return cancelIndex;
|
|
57
|
+
if (next > cancelIndex)
|
|
58
|
+
return 0;
|
|
59
|
+
return next;
|
|
60
|
+
});
|
|
61
|
+
key.preventDefault();
|
|
62
|
+
return;
|
|
74
63
|
}
|
|
75
64
|
if (key.name === "return" || key.name === "enter") {
|
|
76
65
|
if (focusIndex === saveIndex) {
|
|
77
66
|
void submit(valuesRef.current);
|
|
67
|
+
key.preventDefault();
|
|
78
68
|
}
|
|
79
69
|
else if (focusIndex === cancelIndex) {
|
|
80
70
|
props.onCancel();
|
|
71
|
+
key.preventDefault();
|
|
81
72
|
}
|
|
82
73
|
}
|
|
83
74
|
});
|
|
@@ -200,63 +200,63 @@ export function useBoardKeybindings(params) {
|
|
|
200
200
|
const name = key.name;
|
|
201
201
|
if (confirm) {
|
|
202
202
|
CONFIRM_KEYS[name]?.({ confirm, confirmChoice, setConfirm, setConfirmChoice });
|
|
203
|
+
key.preventDefault();
|
|
203
204
|
return;
|
|
204
205
|
}
|
|
205
206
|
if (logModalRun) {
|
|
206
207
|
if (name === "escape" || name === "q") {
|
|
207
208
|
OVERLAY_DISMISS.log({ setLogModalRun, setHelpOpen, setSearchActive, setFocusedPanel }, name);
|
|
209
|
+
key.preventDefault();
|
|
208
210
|
return;
|
|
209
211
|
}
|
|
210
212
|
if (key.ctrl && name === "c") {
|
|
211
213
|
copyToClipboard(logModalLines.join("\n"));
|
|
214
|
+
key.preventDefault();
|
|
212
215
|
return;
|
|
213
216
|
}
|
|
214
217
|
return;
|
|
215
218
|
}
|
|
216
219
|
if (helpOpen && (name === "h" || name === "escape")) {
|
|
217
220
|
OVERLAY_DISMISS.help({ setLogModalRun, setHelpOpen, setSearchActive, setFocusedPanel }, name);
|
|
221
|
+
key.preventDefault();
|
|
218
222
|
return;
|
|
219
223
|
}
|
|
220
224
|
if (helpOpen)
|
|
221
225
|
return;
|
|
222
|
-
if (searchActive && (name === "escape" || name === "return" || name === "enter")) {
|
|
223
|
-
OVERLAY_DISMISS.search({ setLogModalRun, setHelpOpen, setSearchActive, setFocusedPanel }, name);
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
226
|
if (searchActive) {
|
|
227
|
-
if (name === "
|
|
227
|
+
if (name === "escape") {
|
|
228
228
|
setSearchActive(false);
|
|
229
|
-
setFocusedPanel("
|
|
230
|
-
|
|
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 }));
|
|
229
|
+
setFocusedPanel("loops");
|
|
230
|
+
key.preventDefault();
|
|
239
231
|
return;
|
|
240
232
|
}
|
|
241
233
|
return;
|
|
242
234
|
}
|
|
243
|
-
if (
|
|
244
|
-
|
|
235
|
+
if (view !== "board" && name === "escape") {
|
|
236
|
+
VIEW_ESCAPE[view]?.({ setEditTarget, setEditTask, onBack: pop });
|
|
237
|
+
key.preventDefault();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (view !== "board")
|
|
241
|
+
return;
|
|
242
|
+
if (name === "left" || name === "right" || name === "tab") {
|
|
243
|
+
const direction = name === "left" || (name === "tab" && key.shift) ? "left" : "right";
|
|
244
|
+
if (focusedPanel === "actions" && name !== "tab") {
|
|
245
245
|
const actionCount = selected ? getActionCount(selected.status) : 0;
|
|
246
|
-
if (
|
|
246
|
+
if (direction === "left" && selectedAction === 0) {
|
|
247
247
|
setFocusedPanel((p) => nextPanel(p, "left"));
|
|
248
248
|
}
|
|
249
|
-
else if (
|
|
249
|
+
else if (direction === "right" && selectedAction === actionCount - 1) {
|
|
250
250
|
setFocusedPanel((p) => nextPanel(p, "right"));
|
|
251
251
|
}
|
|
252
252
|
else {
|
|
253
|
-
setSelectedAction((i) =>
|
|
253
|
+
setSelectedAction((i) => direction === "right"
|
|
254
254
|
? Math.min(actionCount - 1, i + 1)
|
|
255
255
|
: Math.max(0, i - 1));
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
258
|
else {
|
|
259
|
-
const next = nextPanel(focusedPanel,
|
|
259
|
+
const next = nextPanel(focusedPanel, direction);
|
|
260
260
|
if (next === "actions") {
|
|
261
261
|
setFocusedPanel("actions");
|
|
262
262
|
setSelectedAction(0);
|
|
@@ -265,17 +265,13 @@ export function useBoardKeybindings(params) {
|
|
|
265
265
|
setFocusedPanel(next);
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
|
+
key.preventDefault();
|
|
268
269
|
return;
|
|
269
270
|
}
|
|
270
|
-
if (view !== "board" && name === "escape") {
|
|
271
|
-
VIEW_ESCAPE[view]?.({ setEditTarget, setEditTask, onBack: pop });
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
if (view !== "board")
|
|
275
|
-
return;
|
|
276
271
|
const globalHandler = GLOBAL_KEYS[name];
|
|
277
272
|
if (globalHandler) {
|
|
278
273
|
globalHandler({ destroyLogSocket, onQuit, setHelpOpen, setEditTarget, setEditTask, push, onAction });
|
|
274
|
+
key.preventDefault();
|
|
279
275
|
return;
|
|
280
276
|
}
|
|
281
277
|
const panelHandler = panelHandlers[focusedPanel];
|
|
@@ -284,8 +280,10 @@ export function useBoardKeybindings(params) {
|
|
|
284
280
|
setSelectedIndex, setSelectedRunIndex, setFocusedPanel, selectedRunCount, selected,
|
|
285
281
|
selectedRunIndex, setSelectedAction, selectedAction, onAction, onOpenRunLog,
|
|
286
282
|
refreshTasks, onViewTasks, onViewProjects, onAddLoop,
|
|
287
|
-
}))
|
|
283
|
+
})) {
|
|
284
|
+
key.preventDefault();
|
|
288
285
|
return;
|
|
286
|
+
}
|
|
289
287
|
const shortcut = BOARD_SHORTCUTS[name];
|
|
290
288
|
if (shortcut) {
|
|
291
289
|
shortcut({
|
|
@@ -294,6 +292,7 @@ export function useBoardKeybindings(params) {
|
|
|
294
292
|
selectedRunIndex, setSelectedAction, selectedAction, onAction, onOpenRunLog,
|
|
295
293
|
onSelectProject,
|
|
296
294
|
});
|
|
295
|
+
key.preventDefault();
|
|
297
296
|
}
|
|
298
297
|
});
|
|
299
298
|
}
|
|
@@ -16,113 +16,108 @@ export function useTaskKeybindings(params) {
|
|
|
16
16
|
if (name === "escape") {
|
|
17
17
|
setTaskSearchActive(false);
|
|
18
18
|
setTaskFocusedPanel("tasks");
|
|
19
|
-
|
|
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);
|
|
19
|
+
key.preventDefault();
|
|
38
20
|
return;
|
|
39
21
|
}
|
|
40
22
|
return;
|
|
41
23
|
}
|
|
42
24
|
if (name === "escape") {
|
|
43
25
|
onCancel();
|
|
26
|
+
key.preventDefault();
|
|
44
27
|
return;
|
|
45
28
|
}
|
|
46
29
|
if (name === "n") {
|
|
47
30
|
onCreateTask();
|
|
31
|
+
key.preventDefault();
|
|
48
32
|
return;
|
|
49
33
|
}
|
|
50
34
|
if (name === "/" && taskFocusedPanel === "tasks") {
|
|
51
35
|
setTaskSearchActive(true);
|
|
36
|
+
key.preventDefault();
|
|
52
37
|
return;
|
|
53
38
|
}
|
|
54
|
-
if (name === "tab") {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (name === "left" || name === "right") {
|
|
59
|
-
if (taskFocusedPanel === "actions") {
|
|
60
|
-
if (name === "left" && taskSelectedAction === 0) {
|
|
39
|
+
if (name === "tab" || name === "left" || name === "right") {
|
|
40
|
+
const direction = name === "left" || (name === "tab" && key.shift) ? "left" : "right";
|
|
41
|
+
if (taskFocusedPanel === "actions" && name !== "tab") {
|
|
42
|
+
if (direction === "left" && taskSelectedAction === 0) {
|
|
61
43
|
setTaskFocusedPanel((p) => nextTaskPanel(p, "left"));
|
|
62
44
|
}
|
|
63
|
-
else if (
|
|
45
|
+
else if (direction === "right" && taskSelectedAction === taskActionCount - 1) {
|
|
64
46
|
setTaskFocusedPanel((p) => nextTaskPanel(p, "right"));
|
|
65
47
|
}
|
|
66
48
|
else {
|
|
67
|
-
setTaskSelectedAction((i) =>
|
|
49
|
+
setTaskSelectedAction((i) => direction === "right"
|
|
68
50
|
? Math.min(taskActionCount - 1, i + 1)
|
|
69
51
|
: Math.max(0, i - 1));
|
|
70
52
|
}
|
|
71
53
|
}
|
|
72
54
|
else {
|
|
73
|
-
setTaskFocusedPanel((p) => nextTaskPanel(p,
|
|
55
|
+
setTaskFocusedPanel((p) => nextTaskPanel(p, direction));
|
|
74
56
|
}
|
|
57
|
+
key.preventDefault();
|
|
75
58
|
return;
|
|
76
59
|
}
|
|
77
60
|
if (taskFocusedPanel === "tasks") {
|
|
78
61
|
if (name === "up" || name === "k") {
|
|
79
62
|
setTaskSelectedIndex((i) => Math.max(0, i - 1));
|
|
63
|
+
key.preventDefault();
|
|
80
64
|
return;
|
|
81
65
|
}
|
|
82
66
|
if (name === "down" || name === "j") {
|
|
83
67
|
setTaskSelectedIndex((i) => Math.min(tasks.length - 1, i + 1));
|
|
68
|
+
key.preventDefault();
|
|
84
69
|
return;
|
|
85
70
|
}
|
|
86
71
|
if (name === "return" || name === "enter") {
|
|
87
72
|
setTaskFocusedPanel("actions");
|
|
73
|
+
key.preventDefault();
|
|
88
74
|
return;
|
|
89
75
|
}
|
|
90
76
|
if (name === "e" && tasks[taskSelectedIndex]) {
|
|
91
77
|
onTaskAction("edit");
|
|
78
|
+
key.preventDefault();
|
|
92
79
|
return;
|
|
93
80
|
}
|
|
94
81
|
if (name === "d" && tasks[taskSelectedIndex]) {
|
|
95
82
|
onTaskAction("delete");
|
|
83
|
+
key.preventDefault();
|
|
96
84
|
return;
|
|
97
85
|
}
|
|
98
86
|
if (name === "delete" && tasks[taskSelectedIndex]) {
|
|
99
87
|
onTaskAction("delete");
|
|
88
|
+
key.preventDefault();
|
|
100
89
|
return;
|
|
101
90
|
}
|
|
102
91
|
if (selectable && name === "s" && tasks[taskSelectedIndex]) {
|
|
103
92
|
onTaskAction("select");
|
|
93
|
+
key.preventDefault();
|
|
104
94
|
return;
|
|
105
95
|
}
|
|
106
96
|
}
|
|
107
97
|
if (taskFocusedPanel === "actions") {
|
|
108
98
|
if (name === "up" || name === "k") {
|
|
109
99
|
setTaskSelectedAction((i) => Math.max(0, i - 1));
|
|
100
|
+
key.preventDefault();
|
|
110
101
|
return;
|
|
111
102
|
}
|
|
112
103
|
if (name === "down" || name === "j") {
|
|
113
104
|
setTaskSelectedAction((i) => Math.min(taskActionCount - 1, i + 1));
|
|
105
|
+
key.preventDefault();
|
|
114
106
|
return;
|
|
115
107
|
}
|
|
116
108
|
if (name === "return" || name === "enter") {
|
|
117
109
|
onTaskAction(taskActions[taskSelectedAction] ?? "select");
|
|
110
|
+
key.preventDefault();
|
|
118
111
|
return;
|
|
119
112
|
}
|
|
120
113
|
if (name === "d") {
|
|
121
114
|
onTaskAction("delete");
|
|
115
|
+
key.preventDefault();
|
|
122
116
|
return;
|
|
123
117
|
}
|
|
124
118
|
if (selectable && name === "s") {
|
|
125
119
|
onTaskAction("select");
|
|
120
|
+
key.preventDefault();
|
|
126
121
|
return;
|
|
127
122
|
}
|
|
128
123
|
}
|
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/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { LoopManager } from "./manager.js";
|
|
2
2
|
import { TaskManager } from "./task-manager.js";
|
|
3
3
|
import { IpcServer } from "./server.js";
|
|
4
|
-
import { writeDaemonPid, removeDaemonPid, writeDaemonSignature, removeDaemonSignature, computeCodeSignature, } from "./state.js";
|
|
4
|
+
import { writeDaemonPid, removeDaemonPid, writeDaemonSignature, removeDaemonSignature, computeCodeSignature, migrateTasksToJson, migrateLoopsToJson, } from "./state.js";
|
|
5
5
|
import { t } from "../i18n/index.js";
|
|
6
6
|
import { daemonLog } from "./daemon-log.js";
|
|
7
7
|
async function main() {
|
|
8
8
|
const taskManager = new TaskManager();
|
|
9
|
+
migrateLoopsToJson();
|
|
10
|
+
migrateTasksToJson();
|
|
9
11
|
taskManager.init();
|
|
10
12
|
const manager = new LoopManager(taskManager);
|
|
11
13
|
const server = new IpcServer(manager, taskManager);
|
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.4",
|
|
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": {
|