loop-task 2.1.4 → 2.1.5

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 (148) hide show
  1. package/dist/app/App.js +119 -0
  2. package/dist/{tui → app}/index.js +4 -5
  3. package/dist/app/providers/index.js +1 -0
  4. package/dist/cli/import-writer.js +1 -1
  5. package/dist/cli.js +9 -9
  6. package/dist/client/cli-format.js +7 -0
  7. package/dist/client/commands.js +2 -164
  8. package/dist/client/ipc.js +3 -3
  9. package/dist/client/project-commands.js +159 -0
  10. package/dist/core/{command-runner.js → command/command-runner.js} +3 -3
  11. package/dist/core/{foreground-loop.js → foreground/index.js} +4 -4
  12. package/dist/core/{log-rotator.js → logging/log-rotator.js} +1 -1
  13. package/dist/core/loop/chain-executor.js +67 -0
  14. package/dist/core/loop/delay-utils.js +65 -0
  15. package/dist/core/loop/loop-controller.js +191 -0
  16. package/dist/core/loop/loop-runner.js +133 -0
  17. package/dist/core/loop/run-executor.js +90 -0
  18. package/dist/core/loop/types.js +1 -0
  19. package/dist/daemon/daemon-log.js +1 -1
  20. package/dist/daemon/http/helpers.js +81 -0
  21. package/dist/daemon/http/openapi.js +92 -0
  22. package/dist/daemon/http/route-loops.js +218 -0
  23. package/dist/daemon/http/route-misc.js +36 -0
  24. package/dist/daemon/http/route-projects.js +55 -0
  25. package/dist/daemon/http/route-tasks.js +53 -0
  26. package/dist/daemon/http/routes.js +17 -0
  27. package/dist/daemon/http/server.js +64 -0
  28. package/dist/daemon/http/sse.js +36 -0
  29. package/dist/daemon/index.js +7 -9
  30. package/dist/{ipc/handlers → daemon/ipc}/logs-stream.js +1 -1
  31. package/dist/daemon/managers/loop-entry.js +24 -0
  32. package/dist/daemon/{manager.js → managers/loop-manager.js} +32 -152
  33. package/dist/daemon/managers/loop-options.js +16 -0
  34. package/dist/daemon/managers/loop-serialization.js +43 -0
  35. package/dist/daemon/{projects.js → managers/project-manager.js} +6 -4
  36. package/dist/daemon/{task-manager.js → managers/task-manager.js} +5 -3
  37. package/dist/daemon/server/handlers/index.js +86 -0
  38. package/dist/daemon/server/handlers/log-handlers.js +130 -0
  39. package/dist/daemon/server/handlers/loop-handlers.js +54 -0
  40. package/dist/daemon/server/handlers/project-handlers.js +37 -0
  41. package/dist/daemon/server/handlers/task-handlers.js +20 -0
  42. package/dist/daemon/server/index.js +113 -0
  43. package/dist/daemon/{spawner.js → spawner/index.js} +4 -4
  44. package/dist/daemon/{state.js → state/index.js} +4 -4
  45. package/dist/daemon/{file-watcher.js → watcher/index.js} +8 -6
  46. package/dist/duration.js +1 -1
  47. package/dist/{tui/state.js → entities/loops/filters.js} +0 -76
  48. package/dist/entities/loops/index.js +1 -0
  49. package/dist/entities/projects/filters.js +69 -0
  50. package/dist/entities/projects/index.js +1 -0
  51. package/dist/entities/tasks/filters.js +41 -0
  52. package/dist/entities/tasks/index.js +1 -0
  53. package/dist/{tui/components → features/chain-editor}/ChainEditor.js +7 -5
  54. package/dist/features/code-editor/CodeEditorModal.js +84 -0
  55. package/dist/{tui/components → features/code-editor}/CodeEditorPreview.js +3 -3
  56. package/dist/features/code-editor/useEditorKeyboard.js +147 -0
  57. package/dist/features/code-editor/useModalDimensions.js +9 -0
  58. package/dist/{tui → features/commands}/commands.js +2 -2
  59. package/dist/features/commands/useCommandHandlers.js +147 -0
  60. package/dist/features/commands/useContextualActions.js +75 -0
  61. package/dist/features/commands/useGlobalShortcuts.js +152 -0
  62. package/dist/features/forms/FormRouter.js +44 -0
  63. package/dist/{tui/components → features/overlays}/ContextHelpModal.js +3 -3
  64. package/dist/{tui/components → features/overlays}/ExportModal.js +3 -3
  65. package/dist/{tui/components → features/overlays}/HelpGuideModal.js +3 -3
  66. package/dist/{tui/components → features/overlays}/HelpModal.js +3 -3
  67. package/dist/features/overlays/OverlayStack.js +9 -0
  68. package/dist/{tui/components → features/overlays}/ProjectsModal.js +2 -2
  69. package/dist/{tui/components → features/overlays}/TaskPickerModal.js +2 -2
  70. package/dist/{tui/components → features/overlays}/WelcomeScreen.js +5 -3
  71. package/dist/features/overlays/useOverlayStack.js +51 -0
  72. package/dist/features/state/useAppState.js +150 -0
  73. package/dist/logger.js +0 -1
  74. package/dist/loop-config.js +9 -4
  75. package/dist/{config → shared/config}/constants.js +1 -1
  76. package/dist/shared/container/index.js +22 -0
  77. package/dist/{tui → shared}/hooks/useBreakpoint.js +1 -1
  78. package/dist/shared/hooks/useInject.js +6 -0
  79. package/dist/{tui → shared}/hooks/useLogStream.js +7 -5
  80. package/dist/{hooks → shared/hooks}/useLoopFormValidation.js +2 -2
  81. package/dist/{tui → shared}/hooks/useLoopPolling.js +6 -4
  82. package/dist/shared/{useUndoRedo.js → hooks/useUndoRedo.js} +2 -4
  83. package/dist/shared/providers/InversifyProvider.js +6 -0
  84. package/dist/shared/services/export-service.js +43 -0
  85. package/dist/shared/services/log-service.js +28 -0
  86. package/dist/shared/services/loop-service.js +78 -0
  87. package/dist/shared/services/project-service.js +39 -0
  88. package/dist/shared/services/task-service.js +41 -0
  89. package/dist/shared/services/types.js +7 -0
  90. package/dist/{tui/components → shared/ui}/Button.js +1 -1
  91. package/dist/{tui/components → shared/ui}/DebugPanel.js +1 -1
  92. package/dist/{tui/components → shared/ui}/FocusableButton.js +1 -1
  93. package/dist/{tui/components → shared/ui}/FocusableInput.js +1 -1
  94. package/dist/{tui/components → shared/ui}/FocusableList.js +1 -1
  95. package/dist/{tui/components → shared/ui}/Modal.js +1 -1
  96. package/dist/{tui/components → shared/ui}/SelectModal.js +2 -2
  97. package/dist/{tui/components → shared/ui}/Toast.js +1 -1
  98. package/dist/shared/ui/state.js +9 -0
  99. package/dist/{tui → shared}/utils/paste.js +2 -2
  100. package/dist/{tui → shared}/utils/validation.js +1 -1
  101. package/dist/widgets/command-input/CommandDropdown.js +69 -0
  102. package/dist/widgets/command-input/CommandInput.js +129 -0
  103. package/dist/widgets/command-input/HintBar.js +9 -0
  104. package/dist/widgets/command-input/InputModes.js +68 -0
  105. package/dist/{tui/components → widgets/commands-browser}/CommandsBrowserModal.js +3 -3
  106. package/dist/{tui/components → widgets/header}/Header.js +3 -3
  107. package/dist/{tui/components → widgets/header}/TabBar.js +1 -1
  108. package/dist/{tui/components → widgets/left-panel}/LeftPanel.js +3 -3
  109. package/dist/{tui/components → widgets/left-panel}/Navigator.js +3 -3
  110. package/dist/{tui/components → widgets/left-panel}/ProjectsPage.js +9 -34
  111. package/dist/widgets/left-panel/ProjectsPageParts.js +34 -0
  112. package/dist/{tui/components → widgets/left-panel}/TaskBrowser.js +4 -4
  113. package/dist/{tui/components → widgets/log-modal}/LogModal.js +8 -6
  114. package/dist/widgets/loop-form/CreateForm.js +75 -0
  115. package/dist/widgets/loop-form/TextField.js +9 -0
  116. package/dist/{tui/components → widgets/loop-form}/WizardForm.js +5 -10
  117. package/dist/widgets/loop-form/useCreateSteps.js +107 -0
  118. package/dist/widgets/loop-form/useHandleComplete.js +74 -0
  119. package/dist/{tui/components → widgets/project-form}/ProjectForm.js +9 -7
  120. package/dist/{tui/components → widgets/right-panel}/Inspector.js +4 -4
  121. package/dist/{tui/components → widgets/right-panel}/RightPanel.js +4 -4
  122. package/dist/{tui/components → widgets/right-panel}/RunHistory.js +52 -4
  123. package/dist/{tui/components → widgets/task-form}/TaskForm.js +11 -9
  124. package/package.json +2 -1
  125. package/dist/core/loop-controller.js +0 -511
  126. package/dist/daemon/http-server.js +0 -599
  127. package/dist/daemon/server.js +0 -374
  128. package/dist/tui/App.js +0 -680
  129. package/dist/tui/components/CodeEditorModal.js +0 -244
  130. package/dist/tui/components/CommandInput.js +0 -273
  131. package/dist/tui/components/CreateForm.js +0 -224
  132. package/dist/tui/daemon.js +0 -142
  133. package/dist/tui/format.js +0 -111
  134. /package/dist/{tui/router.js → app/router/index.js} +0 -0
  135. /package/dist/{tui → app}/types.js +0 -0
  136. /package/dist/core/{resolve-cwd.js → command/resolve-cwd.js} +0 -0
  137. /package/dist/core/{context-parser.js → context/context-parser.js} +0 -0
  138. /package/dist/core/{template.js → context/template.js} +0 -0
  139. /package/dist/core/{log-parser.js → logging/log-parser.js} +0 -0
  140. /package/dist/core/{scheduling.js → scheduling/index.js} +0 -0
  141. /package/dist/{ipc → daemon/ipc}/send.js +0 -0
  142. /package/dist/{config → shared/config}/paths.js +0 -0
  143. /package/dist/{i18n → shared/i18n}/en.json +0 -0
  144. /package/dist/{i18n → shared/i18n}/index.js +0 -0
  145. /package/dist/{board → shared/ui}/format.js +0 -0
  146. /package/dist/{tui → shared/ui}/hooks/useHoverState.js +0 -0
  147. /package/dist/{tui → shared/ui}/theme.js +0 -0
  148. /package/dist/{tui → shared}/utils/syntax.js +0 -0
@@ -1,224 +0,0 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo, useCallback, useState, useRef } from "react";
3
- import { createLoop, updateLoop } from "../daemon.js";
4
- import { t } from "../../i18n/index.js";
5
- import { WizardForm } from "./WizardForm.js";
6
- import { TaskPickerModal } from "./TaskPickerModal.js";
7
- import { SelectModal, SelectValueField } from "./SelectModal.js";
8
- import { CodeEditorPreview } from "./CodeEditorPreview.js";
9
- import { CodeEditorModal } from "./CodeEditorModal.js";
10
- import { parseDuration } from "../../duration.js";
11
- import { parseCommandLine, joinCommandLines } from "../../loop-config.js";
12
- export function CreateView(props) {
13
- const { mode, editId, initial, selectedTaskId, selectedTaskName, tasks, currentProjectId, onCancel, onDone, onChooseTask, } = props;
14
- const [taskPickerOpen, setTaskPickerOpen] = useState(false);
15
- const [openSelect, setOpenSelect] = useState(null);
16
- const [commandEditorOpen, setCommandEditorOpen] = useState(false);
17
- const [commandValue, setCommandValue] = useState(initial.command ?? "");
18
- // renderCustom is invoked on every WizardForm render for every field, so this
19
- // ref always holds the latest onChange/onAdvance closures per field key —
20
- // letting the modal (rendered outside WizardForm) drive a field's value.
21
- const fieldCallbacksRef = useRef({});
22
- const taskModeInitial = initial.taskMode === "existing" ? "Existing task" : "Inline command";
23
- // Resolve task display name on edit mode: if the user hasn't picked a task
24
- // via the picker (selectedTaskName is null), look it up from the tasks list.
25
- // Always format as "<name> (<short id>)" for consistency.
26
- const resolvedTaskName = useMemo(() => {
27
- const tid = selectedTaskId ?? initial.taskId;
28
- if (!tid)
29
- return null;
30
- const displayName = selectedTaskName ?? tasks.find((t) => t.id === tid)?.name;
31
- if (displayName)
32
- return `${displayName} (${tid.slice(0, 8)})`;
33
- return `${tid.slice(0, 8)}`;
34
- }, [selectedTaskName, selectedTaskId, initial.taskId, tasks]);
35
- const steps = useMemo(() => {
36
- const list = [
37
- {
38
- key: "interval",
39
- prompt: t("wizard.intervalPrompt"),
40
- hint: t("wizard.intervalHint"),
41
- required: true,
42
- inputType: "text",
43
- defaultValue: initial.interval ?? undefined,
44
- },
45
- {
46
- key: "taskMode",
47
- prompt: t("wizard.taskModePrompt"),
48
- hint: t("board.hintTaskMode"),
49
- required: true,
50
- defaultValue: taskModeInitial,
51
- onActivate: () => setOpenSelect("taskMode"),
52
- renderCustom: ({ value, isActive, onChange, onAdvance }) => {
53
- fieldCallbacksRef.current.taskMode = { value, onChange, onAdvance };
54
- return (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.taskModePrompt"), isActive: isActive }));
55
- },
56
- },
57
- {
58
- key: "taskId",
59
- prompt: resolvedTaskName
60
- ? t("board.selectedTask", { name: resolvedTaskName })
61
- : t("board.chooseTask"),
62
- hint: t("board.hintTask"),
63
- required: true,
64
- inputType: "text",
65
- defaultValue: selectedTaskId ?? initial.taskId ?? undefined,
66
- skip: (values) => !values.taskMode?.includes("Existing"),
67
- onActivate: () => setTaskPickerOpen(true),
68
- renderCustom: ({ isActive }) => (_jsx(SelectValueField, { label: resolvedTaskName, placeholder: t("board.chooseTask"), isActive: isActive })),
69
- },
70
- {
71
- key: "command",
72
- prompt: t("wizard.commandPrompt"),
73
- hint: t("wizard.commandHint"),
74
- required: true,
75
- inputType: "text",
76
- defaultValue: commandValue || undefined,
77
- skip: (values) => !!values.taskMode?.includes("Existing"),
78
- onActivate: () => setCommandEditorOpen(true),
79
- renderCustom: ({ isActive }) => (_jsx(CodeEditorPreview, { value: commandValue, hint: t("wizard.commandHint"), isActive: isActive, onActivate: () => setCommandEditorOpen(true) })),
80
- },
81
- {
82
- key: "runNow",
83
- prompt: t("wizard.runNowPrompt"),
84
- hint: t("board.hintRunNow"),
85
- required: true,
86
- defaultValue: initial.runNow === "true" || initial.runNow === "yes"
87
- ? t("wizard.runNowNow")
88
- : t("wizard.runNowWait"),
89
- onActivate: () => setOpenSelect("runNow"),
90
- renderCustom: ({ value, isActive, onChange, onAdvance }) => {
91
- fieldCallbacksRef.current.runNow = { value, onChange, onAdvance };
92
- return (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.runNowPrompt"), isActive: isActive }));
93
- },
94
- },
95
- {
96
- key: "cwd",
97
- prompt: t("wizard.cwdPrompt"),
98
- hint: t("wizard.cwdHint"),
99
- required: false,
100
- inputType: "text",
101
- defaultValue: initial.cwd ?? undefined,
102
- },
103
- {
104
- key: "description",
105
- prompt: t("wizard.descriptionPrompt"),
106
- hint: t("wizard.descriptionHint"),
107
- required: true,
108
- inputType: "text",
109
- defaultValue: initial.description ?? undefined,
110
- },
111
- {
112
- key: "maxRuns",
113
- prompt: t("wizard.maxRunsPrompt"),
114
- hint: t("wizard.maxRunsHint"),
115
- required: false,
116
- inputType: "text",
117
- defaultValue: initial.maxRuns ?? undefined,
118
- },
119
- {
120
- key: "project",
121
- prompt: t("wizard.projectPrompt"),
122
- hint: t("wizard.projectHint"),
123
- required: false,
124
- defaultValue: props.projects.find((p) => p.id === (initial.project ?? "default"))?.name ?? props.projects[0]?.name,
125
- onActivate: () => setOpenSelect("project"),
126
- renderCustom: ({ value, isActive, onChange, onAdvance }) => {
127
- fieldCallbacksRef.current.project = { value, onChange, onAdvance };
128
- return (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.projectPrompt"), isActive: isActive }));
129
- },
130
- },
131
- ];
132
- return list;
133
- }, [taskModeInitial, selectedTaskId, resolvedTaskName, initial, commandValue, props.projects]);
134
- const handleComplete = useCallback((values) => {
135
- const intervalInput = values.interval ?? "";
136
- if (!intervalInput.trim())
137
- return;
138
- let interval;
139
- try {
140
- interval = parseDuration(intervalInput.trim());
141
- }
142
- catch {
143
- return;
144
- }
145
- const intervalHuman = intervalInput.trim();
146
- const isExistingTask = !!values.taskMode?.includes("Existing");
147
- if (isExistingTask && !selectedTaskId && !values.taskId?.trim())
148
- return;
149
- const cmdValue = values.command ?? commandValue;
150
- if (!isExistingTask && !cmdValue.trim())
151
- return;
152
- const cmd = isExistingTask
153
- ? ""
154
- : joinCommandLines(cmdValue);
155
- let cmdOnly = "";
156
- let args = [];
157
- if (cmd.trim()) {
158
- try {
159
- const tokens = parseCommandLine(cmd.trim());
160
- cmdOnly = tokens[0] ?? "";
161
- args = tokens.slice(1);
162
- }
163
- catch {
164
- return;
165
- }
166
- }
167
- const runNowValue = values.runNow === t("wizard.runNowNow");
168
- const projectName = values.project ?? "";
169
- const project = props.projects.find((p) => p.name === projectName);
170
- const projectId = project?.id ?? currentProjectId;
171
- const options = {
172
- interval,
173
- taskId: isExistingTask
174
- ? (selectedTaskId ?? values.taskId?.trim() ?? null)
175
- : null,
176
- command: cmdOnly,
177
- commandArgs: args,
178
- commandRaw: isExistingTask ? undefined : cmdValue,
179
- cwd: (values.cwd ?? "").trim() || process.cwd(),
180
- immediate: runNowValue,
181
- maxRuns: (values.maxRuns ?? "").trim()
182
- ? parseInt(values.maxRuns, 10)
183
- : null,
184
- verbose: false,
185
- description: (values.description ?? "").trim(),
186
- projectId,
187
- offset: null,
188
- };
189
- const desc = (values.description ?? "").trim() || [cmdOnly, ...args].join(" ").trim();
190
- if (mode === "edit" && editId) {
191
- updateLoop(editId, options, intervalHuman)
192
- .then((id) => onDone(true, id, desc))
193
- .catch(() => { });
194
- }
195
- else {
196
- createLoop(options, intervalHuman)
197
- .then((id) => onDone(false, id, desc))
198
- .catch(() => { });
199
- }
200
- }, [selectedTaskId, mode, editId, currentProjectId, onDone, commandValue, props.projects]);
201
- const selectOptionsFor = useCallback((field) => {
202
- if (field === "taskMode") {
203
- return [t("wizard.taskModeInline"), t("wizard.taskModeExisting")].map((v) => ({ value: v, label: v }));
204
- }
205
- if (field === "runNow") {
206
- return [t("wizard.runNowWait"), t("wizard.runNowNow")].map((v) => ({ value: v, label: v }));
207
- }
208
- return props.projects.map((p) => ({ value: p.name, label: p.name }));
209
- }, [props.projects]);
210
- const selectTitleFor = (field) => field === "taskMode" ? t("wizard.taskModePrompt")
211
- : field === "runNow" ? t("wizard.runNowPrompt")
212
- : t("wizard.projectPrompt");
213
- return (_jsxs(_Fragment, { children: [_jsx(WizardForm, { title: mode === "edit" ? t("wizard.editLoop") : t("wizard.newLoop"), steps: steps, onComplete: handleComplete, onCancel: onCancel, disabled: taskPickerOpen || openSelect !== null || commandEditorOpen }), taskPickerOpen ? (_jsx(TaskPickerModal, { tasks: tasks, onSelect: (task) => {
214
- onChooseTask({ id: task.id, name: task.name });
215
- setTaskPickerOpen(false);
216
- }, onClose: () => setTaskPickerOpen(false) })) : null, openSelect ? (_jsx(SelectModal, { title: selectTitleFor(openSelect), options: selectOptionsFor(openSelect), initialValue: fieldCallbacksRef.current[openSelect]?.value, onSelect: (option) => {
217
- fieldCallbacksRef.current[openSelect]?.onChange(option.value);
218
- fieldCallbacksRef.current[openSelect]?.onAdvance();
219
- setOpenSelect(null);
220
- }, onClose: () => setOpenSelect(null) })) : null, commandEditorOpen ? (_jsx(CodeEditorModal, { initialValue: commandValue, onSave: (v) => {
221
- setCommandValue(v);
222
- setCommandEditorOpen(false);
223
- }, onCancel: () => setCommandEditorOpen(false) })) : null] }));
224
- }
@@ -1,142 +0,0 @@
1
- import { sendRequest, streamRequest } from "../client/ipc.js";
2
- import { t } from "../i18n/index.js";
3
- import { LOG_TAIL_DEFAULT } from "../config/constants.js";
4
- import fs from "node:fs/promises";
5
- import path from "node:path";
6
- import { getDataDir } from "../config/paths.js";
7
- function expectOk(message, type) {
8
- if (type !== "ok") {
9
- throw new Error(message);
10
- }
11
- }
12
- export async function listLoops() {
13
- const response = await sendRequest({ type: "list" });
14
- if (response.type !== "ok") {
15
- throw new Error(response.message);
16
- }
17
- return response.data;
18
- }
19
- export async function pauseLoop(id) {
20
- const response = await sendRequest({ type: "pause", payload: { id } });
21
- expectOk(response.message ?? t("errors.pauseFailed"), response.type);
22
- }
23
- export async function resumeLoop(id) {
24
- const response = await sendRequest({ type: "resume", payload: { id } });
25
- expectOk(response.message ?? t("errors.resumeFailed"), response.type);
26
- }
27
- export async function stopLoop(id) {
28
- const response = await sendRequest({ type: "stop-loop", payload: { id } });
29
- expectOk(response.message ?? "Stop failed", response.type);
30
- }
31
- export async function playLoop(id) {
32
- const response = await sendRequest({ type: "play-loop", payload: { id } });
33
- expectOk(response.message ?? "Play failed", response.type);
34
- }
35
- export async function triggerLoop(id) {
36
- const response = await sendRequest({ type: "trigger", payload: { id } });
37
- expectOk(response.message ?? t("errors.forceRunFailed"), response.type);
38
- }
39
- export async function deleteLoop(id) {
40
- const response = await sendRequest({ type: "delete", payload: { id } });
41
- expectOk(response.message ?? t("errors.deleteFailed"), response.type);
42
- }
43
- export async function createLoop(options, intervalHuman) {
44
- const response = await sendRequest({
45
- type: "start",
46
- payload: { ...options, intervalHuman },
47
- });
48
- if (response.type !== "ok") {
49
- throw new Error(response.message);
50
- }
51
- return response.data.id;
52
- }
53
- export async function updateLoop(id, options, intervalHuman) {
54
- const response = await sendRequest({
55
- type: "update",
56
- payload: { id, ...options, intervalHuman },
57
- });
58
- if (response.type !== "ok") {
59
- throw new Error(response.message);
60
- }
61
- return response.data.id;
62
- }
63
- export function streamLogs(id, onLine, onError) {
64
- return streamRequest({ type: "logs", payload: { id, follow: true, tail: LOG_TAIL_DEFAULT } }, onLine, () => { }, onError);
65
- }
66
- export async function fetchRunLog(id, runNumber) {
67
- const response = await sendRequest({ type: "run-log", payload: { id, runNumber } });
68
- if (response.type !== "ok") {
69
- throw new Error(response.message ?? "Failed to fetch run log");
70
- }
71
- return response.data ?? "";
72
- }
73
- export function streamRunLog(id, runNumber, onLine, onEnd, onError) {
74
- return streamRequest({ type: "run-log-stream", payload: { id, runNumber } }, onLine, onEnd, onError);
75
- }
76
- export async function listTasks() {
77
- const response = await sendRequest({ type: "task-list" });
78
- if (response.type !== "ok") {
79
- throw new Error(response.message);
80
- }
81
- return response.data;
82
- }
83
- export async function createTask(payload) {
84
- const response = await sendRequest({ type: "task-create", payload });
85
- if (response.type !== "ok") {
86
- throw new Error(response.message);
87
- }
88
- return response.data;
89
- }
90
- export async function updateTask(id, payload) {
91
- const response = await sendRequest({ type: "task-update", payload: { id, ...payload } });
92
- if (response.type !== "ok") {
93
- throw new Error(response.message);
94
- }
95
- return response.data;
96
- }
97
- export async function deleteTask(id) {
98
- const response = await sendRequest({ type: "task-delete", payload: { id } });
99
- expectOk(response.message ?? "Task delete failed", response.type);
100
- }
101
- export async function listProjects() {
102
- const response = await sendRequest({ type: "project-list" });
103
- if (response.type !== "ok")
104
- throw new Error(response.message);
105
- return response.data;
106
- }
107
- export async function createProject(name, color, directory) {
108
- const response = await sendRequest({ type: "project-create", payload: { name, color, directory } });
109
- if (response.type !== "ok")
110
- throw new Error(response.message);
111
- return response.data;
112
- }
113
- export async function updateProject(id, name, color, directory) {
114
- const response = await sendRequest({ type: "project-update", payload: { id, name, color, directory } });
115
- expectOk(response.message ?? t("project.error.updateFailed"), response.type);
116
- }
117
- export async function exportConfig() {
118
- const [loops, tasks, projects] = await Promise.all([
119
- listLoops().catch(() => []),
120
- listTasks().catch(() => []),
121
- listProjects().catch(() => []),
122
- ]);
123
- const exportData = {
124
- version: 2,
125
- exportedAt: new Date().toISOString(),
126
- loops: loops.map(({ runHistory: _ignored, ...rest }) => rest),
127
- tasks,
128
- projects,
129
- };
130
- const json = JSON.stringify(exportData, null, 2);
131
- const dataDir = getDataDir();
132
- const exportsDir = path.join(dataDir, "exports");
133
- await fs.mkdir(exportsDir, { recursive: true });
134
- const ts = new Date().toISOString().replace(/[:.]/g, "-");
135
- const filePath = path.join(exportsDir, `loop-export-${ts}.json`);
136
- await fs.writeFile(filePath, json, "utf-8");
137
- return { json, filePath };
138
- }
139
- export async function deleteProject(id) {
140
- const response = await sendRequest({ type: "project-delete", payload: { id } });
141
- expectOk(response.message ?? t("project.error.deleteFailed"), response.type);
142
- }
@@ -1,111 +0,0 @@
1
- import { t } from "../i18n/index.js";
2
- export function unescapeCommand(str) {
3
- return str
4
- .replace(/\\\\/g, "\x00")
5
- .replace(/\\"/g, '"')
6
- .replace(/\x00/g, "\\");
7
- }
8
- export function quoteArg(arg) {
9
- return /[\s"]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
10
- }
11
- export function commandLine(command, args) {
12
- return [unescapeCommand(command), ...args.map((a) => quoteArg(unescapeCommand(a)))].join(" ").trim();
13
- }
14
- export function formatCmd(command, args, max = 24) {
15
- const full = commandLine(command, args);
16
- return full.length > max ? full.slice(0, max - 3) + "..." : full;
17
- }
18
- export function describeLoop(loop) {
19
- return loop.description?.trim() || commandLine(loop.command, loop.commandArgs);
20
- }
21
- export function truncate(text, max) {
22
- return text.length > max ? text.slice(0, max - 3) + "..." : text;
23
- }
24
- export function timeAgo(iso) {
25
- if (!iso)
26
- return t("format.dash");
27
- const diff = Date.now() - new Date(iso).getTime();
28
- const secs = Math.floor(diff / 1000);
29
- if (secs < 5)
30
- return t("format.justNow");
31
- if (secs < 60)
32
- return t("format.secsAgo", { secs });
33
- const mins = Math.floor(secs / 60);
34
- if (mins < 60)
35
- return t("format.minsAgo", { mins });
36
- const hrs = Math.floor(mins / 60);
37
- if (hrs < 24)
38
- return t("format.hrsAgo", { hrs });
39
- return t("format.daysAgo", { days: Math.floor(hrs / 24) });
40
- }
41
- export function timeUntil(iso) {
42
- if (!iso)
43
- return t("format.dash");
44
- const diff = Math.max(0, new Date(iso).getTime() - Date.now());
45
- const secs = Math.floor(diff / 1000);
46
- if (secs < 5)
47
- return t("format.justNow");
48
- if (secs < 60)
49
- return t("format.secsAhead", { secs });
50
- const mins = Math.floor(secs / 60);
51
- if (mins < 60)
52
- return t("format.minsAhead", { mins });
53
- const hrs = Math.floor(mins / 60);
54
- if (hrs < 24)
55
- return t("format.hrsAhead", { hrs });
56
- return t("format.daysAhead", { days: Math.floor(hrs / 24) });
57
- }
58
- const STATUS_COLORS = {
59
- running: "#4ade80",
60
- waiting: "#6b7280",
61
- paused: "#facc15",
62
- idle: "#fb923c",
63
- stopped: "#f87171",
64
- };
65
- export function statusColor(status) {
66
- return STATUS_COLORS[status] ?? "#ffffff";
67
- }
68
- export function timingLabel(loop) {
69
- if (loop.status === "paused")
70
- return t("format.timingPaused");
71
- if (loop.status === "idle")
72
- return t("format.timingIdle");
73
- if (loop.nextRunAt)
74
- return t("format.timingNext", { timeAgo: timeUntil(loop.nextRunAt) });
75
- if (loop.lastRunAt)
76
- return t("format.timingLast", { timeAgo: timeAgo(loop.lastRunAt) });
77
- return t("format.timingNew");
78
- }
79
- export function statusLabel(status) {
80
- return status === "waiting" ? "waiting" : status;
81
- }
82
- export function formatFileSize(bytes) {
83
- if (bytes < 1024)
84
- return `${bytes} B`;
85
- if (bytes < 1024 * 1024)
86
- return `${Math.round(bytes / 1024)} KB`;
87
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
88
- }
89
- export function formatRunDuration(ms) {
90
- if (ms < 1000)
91
- return `${ms}ms`;
92
- if (ms < 60000)
93
- return `${(ms / 1000).toFixed(1)}s`;
94
- return `${Math.floor(ms / 60000)}m${Math.floor((ms % 60000) / 1000)}s`;
95
- }
96
- export function formatRunTime(iso) {
97
- const d = new Date(iso);
98
- return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false });
99
- }
100
- export function formatDate(iso) {
101
- const d = new Date(iso);
102
- const date = d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
103
- const time = d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false });
104
- return `${date} ${time}`;
105
- }
106
- export function sinceLabel(loop) {
107
- const ts = loop.sessionStartedAt ?? loop.createdAt;
108
- if (!ts)
109
- return t("format.dash");
110
- return formatDate(ts);
111
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes