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,23 +1,28 @@
1
1
  import crypto from "node:crypto";
2
- import { LoopController } from "../core/loop-controller.js";
3
- import { ProjectManager } from "./projects.js";
4
- import { saveLoop, loadAllLoops, deleteLoop as deleteLoopState, getLogPath, } from "./state.js";
5
- import { daemonLog } from "./daemon-log.js";
2
+ import { LoopController } from "../../core/loop/loop-controller.js";
3
+ import { ProjectManager } from "./project-manager.js";
4
+ import { saveLoop, loadAllLoops, deleteLoop as deleteLoopState, getLogPath, } from "../state/index.js";
5
+ import { daemonLog } from "../daemon-log.js";
6
+ import { createLoopEntry, metaToState } from "./loop-entry.js";
7
+ import { wireEvents, persistLoop, buildMeta } from "./loop-serialization.js";
6
8
  export class LoopManager {
7
- loops = new Map();
8
- lastSerialized = new Map();
9
- taskManager;
10
- projectManager;
11
9
  constructor(taskManager, projectManager) {
10
+ this.loops = new Map();
11
+ this.lastSerialized = new Map();
12
+ this.taskResolver = (taskId) => this.taskManager.get(taskId);
12
13
  this.taskManager = taskManager;
13
14
  this.projectManager = projectManager || new ProjectManager();
14
15
  }
15
- taskResolver = (taskId) => this.taskManager.get(taskId);
16
16
  getProjectDirectory(projectId) {
17
17
  return this.projectManager.get(projectId)?.directory;
18
18
  }
19
+ persist(id, controller, options, intervalHuman) {
20
+ persistLoop(id, controller, options, intervalHuman, this.taskManager, this.lastSerialized);
21
+ }
22
+ wireUp(id, entry) {
23
+ wireEvents(id, entry.controller, entry.options, entry.intervalHuman, () => this.persist(id, entry.controller, entry.options, entry.intervalHuman));
24
+ }
19
25
  init() {
20
- // Initialize projects first
21
26
  this.projectManager.init();
22
27
  const saved = loadAllLoops();
23
28
  let restarted = 0;
@@ -27,42 +32,11 @@ export class LoopManager {
27
32
  meta.projectId = "default";
28
33
  saveLoop(meta);
29
34
  }
30
- const options = {
31
- interval: meta.interval,
32
- taskId: meta.taskId,
33
- command: meta.command,
34
- commandArgs: meta.commandArgs,
35
- commandRaw: meta.commandRaw,
36
- cwd: meta.cwd ?? "",
37
- immediate: false,
38
- maxRuns: meta.maxRuns,
39
- verbose: meta.verbose,
40
- description: meta.description ?? "",
41
- projectId: meta.projectId ?? "default",
42
- offset: meta.offset ?? null,
43
- };
44
- const logPath = getLogPath(meta.id);
45
- const projectDir = this.getProjectDirectory(options.projectId ?? "default");
46
- const controller = new LoopController(meta.id, options, logPath, this.taskResolver, {
47
- status: meta.status,
48
- createdAt: meta.createdAt,
49
- runCount: meta.runCount,
50
- sessionStartedAt: meta.sessionStartedAt,
51
- lastRunAt: meta.lastRunAt,
52
- lastExitCode: meta.lastExitCode,
53
- lastDuration: meta.lastDuration,
54
- nextRunAt: meta.nextRunAt,
55
- remainingDelayMs: meta.remainingDelayMs,
56
- runHistory: meta.runHistory,
57
- }, projectDir);
58
- this.loops.set(meta.id, {
59
- controller,
60
- options,
61
- intervalHuman: meta.intervalHuman,
62
- });
63
- this.wireEvents(meta.id, controller, options, meta.intervalHuman);
35
+ const entry = createLoopEntry(meta, this.taskResolver, this.getProjectDirectory.bind(this), metaToState(meta));
36
+ this.loops.set(meta.id, entry);
37
+ this.wireUp(meta.id, entry);
64
38
  if (shouldAutoStart(meta.status)) {
65
- controller.start();
39
+ entry.controller.start();
66
40
  restarted += 1;
67
41
  }
68
42
  }
@@ -75,9 +49,10 @@ export class LoopManager {
75
49
  const logPath = getLogPath(id);
76
50
  const projectDir = this.getProjectDirectory(options.projectId ?? "default");
77
51
  const controller = new LoopController(id, options, logPath, this.taskResolver, undefined, projectDir);
78
- this.loops.set(id, { controller, options, intervalHuman });
52
+ const entry = { controller, options, intervalHuman };
53
+ this.loops.set(id, entry);
79
54
  controller.start();
80
- this.wireEvents(id, controller, options, intervalHuman);
55
+ this.wireUp(id, entry);
81
56
  this.persist(id, controller, options, intervalHuman);
82
57
  return id;
83
58
  }
@@ -110,7 +85,7 @@ export class LoopManager {
110
85
  list() {
111
86
  const result = [];
112
87
  for (const [id, entry] of this.loops) {
113
- result.push(this.buildMeta(id, entry));
88
+ result.push(buildMeta(id, entry, this.taskManager));
114
89
  }
115
90
  return result;
116
91
  }
@@ -136,7 +111,7 @@ export class LoopManager {
136
111
  const entry = this.loops.get(id);
137
112
  if (!entry)
138
113
  return null;
139
- return this.buildMeta(id, entry);
114
+ return buildMeta(id, entry, this.taskManager);
140
115
  }
141
116
  pause(id) {
142
117
  const entry = this.loops.get(id);
@@ -242,118 +217,23 @@ export class LoopManager {
242
217
  existing.options.cwd !== (meta.cwd ?? "");
243
218
  if (configChanged) {
244
219
  void existing.controller.stop().then(() => {
245
- const options = {
246
- interval: meta.interval,
247
- taskId: meta.taskId ?? null,
248
- command: meta.command,
249
- commandArgs: meta.commandArgs,
250
- cwd: meta.cwd ?? "",
251
- immediate: false,
252
- maxRuns: meta.maxRuns,
253
- verbose: meta.verbose,
254
- description: meta.description ?? "",
255
- projectId: meta.projectId ?? "default",
256
- offset: meta.offset ?? null,
257
- };
258
- const logPath = getLogPath(meta.id);
259
- const projectDir = this.getProjectDirectory(options.projectId ?? "default");
260
- const controller = new LoopController(meta.id, options, logPath, this.taskResolver, {
261
- status: meta.status,
262
- createdAt: meta.createdAt,
263
- runCount: meta.runCount,
264
- sessionStartedAt: meta.sessionStartedAt,
265
- lastRunAt: meta.lastRunAt,
266
- lastExitCode: meta.lastExitCode,
267
- lastDuration: meta.lastDuration,
268
- nextRunAt: meta.nextRunAt,
269
- remainingDelayMs: meta.remainingDelayMs,
270
- runHistory: meta.runHistory,
271
- }, projectDir);
272
- this.loops.set(meta.id, { controller, options, intervalHuman: meta.intervalHuman });
273
- this.wireEvents(meta.id, controller, options, meta.intervalHuman);
220
+ const entry = createLoopEntry(meta, this.taskResolver, this.getProjectDirectory.bind(this), metaToState(meta));
221
+ this.loops.set(meta.id, entry);
222
+ this.wireUp(meta.id, entry);
274
223
  if (shouldAutoStart(meta.status)) {
275
- controller.start();
224
+ entry.controller.start();
276
225
  }
277
226
  });
278
227
  }
279
228
  }
280
229
  else {
281
- const options = {
282
- interval: meta.interval,
283
- taskId: meta.taskId ?? null,
284
- command: meta.command,
285
- commandArgs: meta.commandArgs,
286
- cwd: meta.cwd ?? "",
287
- immediate: false,
288
- maxRuns: meta.maxRuns,
289
- verbose: meta.verbose,
290
- description: meta.description ?? "",
291
- projectId: meta.projectId ?? "default",
292
- offset: meta.offset ?? null,
293
- };
294
- const logPath = getLogPath(meta.id);
295
- const projectDir = this.getProjectDirectory(options.projectId ?? "default");
296
- const controller = new LoopController(meta.id, options, logPath, this.taskResolver, {
297
- status: meta.status,
298
- createdAt: meta.createdAt,
299
- runCount: meta.runCount,
300
- sessionStartedAt: meta.sessionStartedAt,
301
- lastRunAt: meta.lastRunAt,
302
- lastExitCode: meta.lastExitCode,
303
- lastDuration: meta.lastDuration,
304
- nextRunAt: meta.nextRunAt,
305
- remainingDelayMs: meta.remainingDelayMs,
306
- runHistory: meta.runHistory,
307
- }, projectDir);
308
- this.loops.set(meta.id, { controller, options, intervalHuman: meta.intervalHuman });
309
- this.wireEvents(meta.id, controller, options, meta.intervalHuman);
230
+ const entry = createLoopEntry(meta, this.taskResolver, this.getProjectDirectory.bind(this), metaToState(meta));
231
+ this.loops.set(meta.id, entry);
232
+ this.wireUp(meta.id, entry);
310
233
  if (shouldAutoStart(meta.status)) {
311
- controller.start();
234
+ entry.controller.start();
312
235
  }
313
236
  }
314
237
  }
315
238
  }
316
- wireEvents(id, controller, options, intervalHuman) {
317
- const persist = () => this.persist(id, controller, options, intervalHuman);
318
- controller.on("run:start", persist);
319
- controller.on("run:end", persist);
320
- controller.on("paused", persist);
321
- controller.on("resumed", persist);
322
- controller.on("triggered", persist);
323
- controller.on("waiting", persist);
324
- controller.on("stopped", persist);
325
- }
326
- persist(id, controller, options, intervalHuman) {
327
- const meta = this.toMeta(controller, options, intervalHuman);
328
- const serialized = JSON.stringify(meta);
329
- if (this.lastSerialized.get(id) === serialized) {
330
- return;
331
- }
332
- this.lastSerialized.set(id, serialized);
333
- saveLoop(meta);
334
- }
335
- buildMeta(id, entry) {
336
- return this.toMeta(entry.controller, entry.options, entry.intervalHuman);
337
- }
338
- toMeta(controller, options, intervalHuman) {
339
- const runtime = controller.getMeta();
340
- const task = options.taskId ? this.taskManager.get(options.taskId) : null;
341
- return {
342
- ...runtime,
343
- command: task?.command ?? options.command,
344
- commandArgs: task?.commandArgs ?? options.commandArgs,
345
- commandRaw: options.commandRaw,
346
- cwd: options.cwd || "",
347
- interval: options.interval,
348
- intervalHuman,
349
- immediate: options.immediate,
350
- maxRuns: options.maxRuns,
351
- verbose: options.verbose,
352
- description: options.description,
353
- remainingDelayMs: runtime.remainingDelayMs,
354
- pid: process.pid,
355
- projectId: options.projectId ?? "default",
356
- offset: options.offset,
357
- };
358
- }
359
239
  }
@@ -0,0 +1,16 @@
1
+ export function buildLoopOptions(meta) {
2
+ return {
3
+ interval: meta.interval,
4
+ taskId: meta.taskId ?? null,
5
+ command: meta.command,
6
+ commandArgs: meta.commandArgs,
7
+ commandRaw: meta.commandRaw,
8
+ cwd: meta.cwd ?? "",
9
+ immediate: false,
10
+ maxRuns: meta.maxRuns,
11
+ verbose: meta.verbose,
12
+ description: meta.description ?? "",
13
+ projectId: meta.projectId ?? "default",
14
+ offset: meta.offset ?? null,
15
+ };
16
+ }
@@ -0,0 +1,43 @@
1
+ import { saveLoop } from "../state/index.js";
2
+ export function wireEvents(id, controller, options, intervalHuman, persist) {
3
+ controller.on("run:start", persist);
4
+ controller.on("run:end", persist);
5
+ controller.on("paused", persist);
6
+ controller.on("resumed", persist);
7
+ controller.on("triggered", persist);
8
+ controller.on("waiting", persist);
9
+ controller.on("stopped", persist);
10
+ }
11
+ export function toMeta(controller, options, intervalHuman, taskManager) {
12
+ const runtime = controller.getMeta();
13
+ const task = options.taskId ? taskManager.get(options.taskId) : null;
14
+ return {
15
+ ...runtime,
16
+ command: task?.command ?? options.command,
17
+ commandArgs: task?.commandArgs ?? options.commandArgs,
18
+ commandRaw: options.commandRaw,
19
+ cwd: options.cwd || "",
20
+ interval: options.interval,
21
+ intervalHuman,
22
+ immediate: options.immediate,
23
+ maxRuns: options.maxRuns,
24
+ verbose: options.verbose,
25
+ description: options.description,
26
+ remainingDelayMs: runtime.remainingDelayMs,
27
+ pid: process.pid,
28
+ projectId: options.projectId ?? "default",
29
+ offset: options.offset,
30
+ };
31
+ }
32
+ export function persistLoop(id, controller, options, intervalHuman, taskManager, lastSerialized) {
33
+ const meta = toMeta(controller, options, intervalHuman, taskManager);
34
+ const serialized = JSON.stringify(meta);
35
+ if (lastSerialized.get(id) === serialized) {
36
+ return;
37
+ }
38
+ lastSerialized.set(id, serialized);
39
+ saveLoop(meta);
40
+ }
41
+ export function buildMeta(id, entry, taskManager) {
42
+ return toMeta(entry.controller, entry.options, entry.intervalHuman, taskManager);
43
+ }
@@ -1,11 +1,13 @@
1
1
  import fs from "node:fs";
2
- import { writeFileAtomic } from "../shared/fs-utils.js";
3
- import { daemonLog } from "./daemon-log.js";
2
+ import { writeFileAtomic } from "../../shared/fs-utils.js";
3
+ import { daemonLog } from "../daemon-log.js";
4
4
  import crypto from "node:crypto";
5
- import { getDataDir, projectsJson } from "../config/paths.js";
5
+ import { getDataDir, projectsJson } from "../../shared/config/paths.js";
6
6
  import path from "node:path";
7
7
  export class ProjectManager {
8
- projects = new Map();
8
+ constructor() {
9
+ this.projects = new Map();
10
+ }
9
11
  init() {
10
12
  const dataDir = getDataDir();
11
13
  fs.mkdirSync(dataDir, { recursive: true });
@@ -1,8 +1,10 @@
1
1
  import crypto from "node:crypto";
2
- import { saveTask, loadAllTasks, deleteTask as deleteTaskState } from "./state.js";
3
- import { daemonLog } from "./daemon-log.js";
2
+ import { saveTask, loadAllTasks, deleteTask as deleteTaskState } from "../state/index.js";
3
+ import { daemonLog } from "../daemon-log.js";
4
4
  export class TaskManager {
5
- tasks = new Map();
5
+ constructor() {
6
+ this.tasks = new Map();
7
+ }
6
8
  init() {
7
9
  const saved = loadAllTasks();
8
10
  for (const task of saved) {
@@ -0,0 +1,86 @@
1
+ import { handleStart, handleUpdate, handleList, handleStatus, handlePause, handleResume, handleStopLoop, handleStopAll, handlePlayLoop, handleTrigger, handleDelete, } from "./loop-handlers.js";
2
+ import { handleRunLog, handleRunLogStream, handleLogs, } from "./log-handlers.js";
3
+ import { handleTaskCreate, handleTaskUpdate, handleTaskList, handleTaskGet, handleTaskDelete, } from "./task-handlers.js";
4
+ import { handleProjectList, handleProjectCreate, handleProjectUpdate, handleProjectDelete, } from "./project-handlers.js";
5
+ export { handleStart, handleUpdate, handleList, handleStatus, handlePause, handleResume, handleStopLoop, handleStopAll, handlePlayLoop, handleTrigger, handleDelete, handleRunLog, handleRunLogStream, handleLogs, handleTaskCreate, handleTaskUpdate, handleTaskList, handleTaskGet, handleTaskDelete, handleProjectList, handleProjectCreate, handleProjectUpdate, handleProjectDelete, };
6
+ /**
7
+ * Dispatch an incoming IPC request to the appropriate handler.
8
+ * Returns true if the request was handled, false otherwise.
9
+ */
10
+ export async function dispatch(request, socket, ctx) {
11
+ switch (request.type) {
12
+ case "start":
13
+ handleStart(request, socket, ctx);
14
+ break;
15
+ case "update":
16
+ await handleUpdate(request, socket, ctx);
17
+ break;
18
+ case "list":
19
+ handleList(request, socket, ctx);
20
+ break;
21
+ case "status":
22
+ handleStatus(request, socket, ctx);
23
+ break;
24
+ case "pause":
25
+ handlePause(request, socket, ctx);
26
+ break;
27
+ case "resume":
28
+ handleResume(request, socket, ctx);
29
+ break;
30
+ case "stop-loop":
31
+ handleStopLoop(request, socket, ctx);
32
+ break;
33
+ case "stop-all":
34
+ handleStopAll(request, socket, ctx);
35
+ break;
36
+ case "play-loop":
37
+ handlePlayLoop(request, socket, ctx);
38
+ break;
39
+ case "trigger":
40
+ handleTrigger(request, socket, ctx);
41
+ break;
42
+ case "delete":
43
+ await handleDelete(request, socket, ctx);
44
+ break;
45
+ case "run-log":
46
+ handleRunLog(request, socket, ctx);
47
+ break;
48
+ case "run-log-stream":
49
+ handleRunLogStream(request, socket, ctx);
50
+ break;
51
+ case "attach":
52
+ case "logs":
53
+ handleLogs(request, socket, ctx);
54
+ break;
55
+ case "task-create":
56
+ handleTaskCreate(request, socket, ctx);
57
+ break;
58
+ case "task-update":
59
+ handleTaskUpdate(request, socket, ctx);
60
+ break;
61
+ case "task-list":
62
+ handleTaskList(request, socket, ctx);
63
+ break;
64
+ case "task-get":
65
+ handleTaskGet(request, socket, ctx);
66
+ break;
67
+ case "task-delete":
68
+ handleTaskDelete(request, socket, ctx);
69
+ break;
70
+ case "project-list":
71
+ handleProjectList(request, socket, ctx);
72
+ break;
73
+ case "project-create":
74
+ handleProjectCreate(request, socket, ctx);
75
+ break;
76
+ case "project-update":
77
+ handleProjectUpdate(request, socket, ctx);
78
+ break;
79
+ case "project-delete":
80
+ handleProjectDelete(request, socket, ctx);
81
+ break;
82
+ default:
83
+ return false;
84
+ }
85
+ return true;
86
+ }
@@ -0,0 +1,130 @@
1
+ import fs from "node:fs";
2
+ import { send } from "../../ipc/send.js";
3
+ import { t } from "../../../shared/i18n/index.js";
4
+ import { LOG_TAIL_DEFAULT } from "../../../shared/config/constants.js";
5
+ import { tail } from "../../../shared/tail.js";
6
+ import { streamLogFollow } from "../../ipc/logs-stream.js";
7
+ export function handleRunLog(request, socket, ctx) {
8
+ const { id, runNumber } = request.payload;
9
+ const logPath = ctx.manager.getLogPath(id);
10
+ if (!logPath || !fs.existsSync(logPath)) {
11
+ send(socket, { type: "ok", data: "" });
12
+ return;
13
+ }
14
+ const meta = ctx.manager.status(id);
15
+ if (!meta) {
16
+ send(socket, { type: "ok", data: "" });
17
+ return;
18
+ }
19
+ const records = meta.runHistory
20
+ .filter((r) => r.runNumber === runNumber)
21
+ .sort((a, b) => a.logOffset - b.logOffset);
22
+ if (records.length === 0) {
23
+ send(socket, { type: "ok", data: "" });
24
+ return;
25
+ }
26
+ const buffer = fs.readFileSync(logPath);
27
+ const allSorted = meta.runHistory.slice().sort((a, b) => a.logOffset - b.logOffset);
28
+ const parts = records.map((record) => {
29
+ const start = record.logOffset;
30
+ const idx = allSorted.indexOf(record);
31
+ const end = idx < allSorted.length - 1 ? allSorted[idx + 1].logOffset : buffer.length;
32
+ return buffer.toString("utf-8", start, end);
33
+ });
34
+ send(socket, { type: "ok", data: parts.join("") });
35
+ }
36
+ export function handleRunLogStream(request, socket, ctx) {
37
+ const { id, runNumber } = request.payload;
38
+ const meta = ctx.manager.status(id);
39
+ if (!meta) {
40
+ send(socket, { type: "error", message: t("errors.loopNotFound", { id }) });
41
+ return;
42
+ }
43
+ const records = meta.runHistory
44
+ .filter((r) => r.runNumber === runNumber)
45
+ .sort((a, b) => a.logOffset - b.logOffset);
46
+ if (records.length === 0) {
47
+ send(socket, { type: "ok", data: "" });
48
+ return;
49
+ }
50
+ const logPath = ctx.manager.getLogPath(id);
51
+ if (!logPath || !fs.existsSync(logPath)) {
52
+ send(socket, { type: "ok", data: "" });
53
+ return;
54
+ }
55
+ const firstOffset = records[0].logOffset;
56
+ const stat = fs.statSync(logPath);
57
+ if (stat.size > firstOffset) {
58
+ const fd = fs.openSync(logPath, "r");
59
+ const buf = Buffer.alloc(stat.size - firstOffset);
60
+ fs.readSync(fd, buf, 0, buf.length, firstOffset);
61
+ fs.closeSync(fd);
62
+ for (const line of buf.toString().split("\n")) {
63
+ if (line) {
64
+ send(socket, { type: "data", line });
65
+ }
66
+ }
67
+ }
68
+ const allCompleted = records.every((r) => r.status === "completed");
69
+ if (allCompleted) {
70
+ send(socket, { type: "end" });
71
+ return;
72
+ }
73
+ let fileSize = stat.size;
74
+ const watcher = fs.watch(logPath, (eventType) => {
75
+ if (eventType === "rename" && !fs.existsSync(logPath)) {
76
+ watcher.close();
77
+ send(socket, { type: "end" });
78
+ return;
79
+ }
80
+ if (eventType === "change") {
81
+ try {
82
+ const s = fs.statSync(logPath);
83
+ if (s.size > fileSize) {
84
+ const fd = fs.openSync(logPath, "r");
85
+ const buf = Buffer.alloc(s.size - fileSize);
86
+ fs.readSync(fd, buf, 0, buf.length, fileSize);
87
+ fs.closeSync(fd);
88
+ fileSize = s.size;
89
+ for (const line of buf.toString().split("\n")) {
90
+ if (line) {
91
+ send(socket, { type: "data", line });
92
+ }
93
+ }
94
+ }
95
+ }
96
+ catch {
97
+ watcher.close();
98
+ send(socket, { type: "end" });
99
+ }
100
+ }
101
+ });
102
+ socket.on("close", () => watcher.close());
103
+ socket.on("error", () => watcher.close());
104
+ }
105
+ export function handleLogs(request, socket, ctx) {
106
+ const id = request.payload.id;
107
+ const logPath = ctx.manager.getLogPath(id);
108
+ if (!logPath) {
109
+ send(socket, { type: "error", message: t("errors.loopNotFound", { id }) });
110
+ return;
111
+ }
112
+ if (!fs.existsSync(logPath)) {
113
+ if (request.type === "logs") {
114
+ send(socket, { type: "ok", data: "" });
115
+ }
116
+ else {
117
+ send(socket, { type: "error", message: t("errors.noLogsYet") });
118
+ }
119
+ return;
120
+ }
121
+ const follow = request.type === "attach" || (request.type === "logs" && request.payload.follow);
122
+ const tailCount = request.type === "logs" ? (request.payload.tail ?? LOG_TAIL_DEFAULT) : 0;
123
+ if (follow) {
124
+ streamLogFollow(logPath, socket, tailCount);
125
+ }
126
+ else {
127
+ const content = fs.readFileSync(logPath, "utf-8");
128
+ send(socket, { type: "ok", data: tail(content, tailCount).join("\n") });
129
+ }
130
+ }
@@ -0,0 +1,54 @@
1
+ import { send } from "../../ipc/send.js";
2
+ import { t } from "../../../shared/i18n/index.js";
3
+ export function handleStart(request, socket, ctx) {
4
+ const { intervalHuman, ...options } = request.payload;
5
+ const id = ctx.manager.start(options, intervalHuman);
6
+ send(socket, { type: "ok", data: { id } });
7
+ }
8
+ export async function handleUpdate(request, socket, ctx) {
9
+ const { id, intervalHuman, ...options } = request.payload;
10
+ const ok = await ctx.manager.update(id, options, intervalHuman);
11
+ ctx.respondOk(socket, ok, id, ok ? { id } : undefined);
12
+ }
13
+ export function handleList(_request, socket, ctx) {
14
+ send(socket, { type: "ok", data: ctx.manager.list() });
15
+ }
16
+ export function handleStatus(request, socket, ctx) {
17
+ const meta = ctx.manager.status(request.payload.id);
18
+ ctx.respondOk(socket, meta !== null, request.payload.id, meta ?? undefined);
19
+ }
20
+ export function handlePause(request, socket, ctx) {
21
+ ctx.respondOk(socket, ctx.manager.pause(request.payload.id), request.payload.id);
22
+ }
23
+ export function handleResume(request, socket, ctx) {
24
+ ctx.respondOk(socket, ctx.manager.resume(request.payload.id), request.payload.id);
25
+ }
26
+ export function handleStopLoop(request, socket, ctx) {
27
+ ctx.respondOk(socket, ctx.manager.stopLoop(request.payload.id), request.payload.id);
28
+ }
29
+ export function handleStopAll(_request, socket, ctx) {
30
+ const count = ctx.manager.stopAllLoops();
31
+ send(socket, { type: "ok", data: count });
32
+ }
33
+ export function handlePlayLoop(request, socket, ctx) {
34
+ if (ctx.manager.isMaxRunsBlocked(request.payload.id)) {
35
+ send(socket, { type: "error", message: t("errors.maxRunsReached") });
36
+ return;
37
+ }
38
+ ctx.respondOk(socket, ctx.manager.playLoop(request.payload.id), request.payload.id);
39
+ }
40
+ export function handleTrigger(request, socket, ctx) {
41
+ if (ctx.manager.isMaxRunsBlocked(request.payload.id)) {
42
+ send(socket, { type: "error", message: t("errors.maxRunsReached") });
43
+ return;
44
+ }
45
+ if (ctx.manager.isRunning(request.payload.id)) {
46
+ send(socket, { type: "error", message: t("errors.triggerWhileRunning") });
47
+ return;
48
+ }
49
+ ctx.respondOk(socket, ctx.manager.trigger(request.payload.id), request.payload.id);
50
+ }
51
+ export async function handleDelete(request, socket, ctx) {
52
+ const ok = await ctx.manager.delete(request.payload.id);
53
+ ctx.respondOk(socket, ok, request.payload.id);
54
+ }
@@ -0,0 +1,37 @@
1
+ import { send } from "../../ipc/send.js";
2
+ import { t } from "../../../shared/i18n/index.js";
3
+ export function handleProjectList(_request, socket, ctx) {
4
+ send(socket, { type: "ok", data: ctx.manager.listProjects() });
5
+ }
6
+ export function handleProjectCreate(request, socket, ctx) {
7
+ const { name, color, directory } = request.payload;
8
+ if (!name || !name.trim()) {
9
+ send(socket, { type: "error", message: t("project.error.nameRequired") });
10
+ return;
11
+ }
12
+ const project = ctx.manager.createProject(name.trim(), color, directory);
13
+ send(socket, { type: "ok", data: project });
14
+ }
15
+ export function handleProjectUpdate(request, socket, ctx) {
16
+ const { id, name, color, directory } = request.payload;
17
+ if (!name || !name.trim()) {
18
+ send(socket, { type: "error", message: t("project.error.nameEmpty") });
19
+ return;
20
+ }
21
+ try {
22
+ ctx.manager.updateProject(id, name.trim(), color, directory);
23
+ send(socket, { type: "ok" });
24
+ }
25
+ catch (err) {
26
+ send(socket, { type: "error", message: err instanceof Error ? err.message : String(err) });
27
+ }
28
+ }
29
+ export function handleProjectDelete(request, socket, ctx) {
30
+ try {
31
+ ctx.manager.deleteProject(request.payload.id);
32
+ send(socket, { type: "ok" });
33
+ }
34
+ catch (err) {
35
+ send(socket, { type: "error", message: err instanceof Error ? err.message : String(err) });
36
+ }
37
+ }
@@ -0,0 +1,20 @@
1
+ import { send } from "../../ipc/send.js";
2
+ export function handleTaskCreate(request, socket, ctx) {
3
+ const task = ctx.taskManager.create(request.payload);
4
+ send(socket, { type: "ok", data: task });
5
+ }
6
+ export function handleTaskUpdate(request, socket, ctx) {
7
+ const updated = ctx.taskManager.update(request.payload.id, request.payload);
8
+ ctx.respondOk(socket, updated !== null, request.payload.id, updated ?? undefined);
9
+ }
10
+ export function handleTaskList(_request, socket, ctx) {
11
+ send(socket, { type: "ok", data: ctx.taskManager.list() });
12
+ }
13
+ export function handleTaskGet(request, socket, ctx) {
14
+ const task = ctx.taskManager.get(request.payload.id);
15
+ ctx.respondOk(socket, task !== null, request.payload.id, task ?? undefined);
16
+ }
17
+ export function handleTaskDelete(request, socket, ctx) {
18
+ const ok = ctx.taskManager.delete(request.payload.id);
19
+ ctx.respondOk(socket, ok, request.payload.id);
20
+ }