loop-task 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +250 -195
  2. package/dist/board/App.js +225 -87
  3. package/dist/board/components/ActionButtons.js +28 -8
  4. package/dist/board/components/CreateForm.js +224 -70
  5. package/dist/board/components/CreateProjectModal.js +104 -0
  6. package/dist/board/components/DeleteProjectConfirm.js +53 -0
  7. package/dist/board/components/EditProjectModal.js +106 -0
  8. package/dist/board/components/FilterBar.js +5 -4
  9. package/dist/board/components/Footer.js +19 -6
  10. package/dist/board/components/Header.js +33 -3
  11. package/dist/board/components/HelpModal.js +11 -10
  12. package/dist/board/components/Inspector.js +1 -1
  13. package/dist/board/components/LogModal.js +39 -8
  14. package/dist/board/components/Navigator.js +30 -14
  15. package/dist/board/components/ProjectsModal.js +70 -0
  16. package/dist/board/components/ProjectsPage.js +105 -0
  17. package/dist/board/components/RunHistory.js +48 -9
  18. package/dist/board/components/TaskBrowser.js +95 -0
  19. package/dist/board/components/TaskFilterBar.js +6 -0
  20. package/dist/board/components/TaskForm.js +177 -0
  21. package/dist/board/daemon.js +56 -0
  22. package/dist/board/format.js +19 -12
  23. package/dist/board/hooks/useBoardKeybindings.js +264 -156
  24. package/dist/board/hooks/useTaskKeybindings.js +114 -0
  25. package/dist/board/router.js +16 -0
  26. package/dist/board/state.js +18 -14
  27. package/dist/cli.js +61 -6
  28. package/dist/client/commands.js +157 -1
  29. package/dist/config/constants.js +15 -0
  30. package/dist/config/paths.js +6 -0
  31. package/dist/core/command-runner.js +2 -0
  32. package/dist/core/foreground-loop.js +4 -1
  33. package/dist/core/loop-controller.js +210 -32
  34. package/dist/daemon/index.js +5 -2
  35. package/dist/daemon/manager.js +113 -34
  36. package/dist/daemon/projects.js +104 -0
  37. package/dist/daemon/server.js +177 -6
  38. package/dist/daemon/state.js +32 -1
  39. package/dist/daemon/task-manager.js +59 -0
  40. package/dist/i18n/en.json +139 -20
  41. package/dist/loop-config.js +13 -4
  42. package/dist/shared/clipboard.js +19 -0
  43. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -5,6 +5,7 @@ import { Logger } from "./logger.js";
5
5
  import { runLoop } from "./core/foreground-loop.js";
6
6
  import { buildLoopOptions } from "./loop-config.js";
7
7
  import { startLoop } from "./client/commands.js";
8
+ import { listProjectsCli, createProjectCli, renameProjectCli, setProjectColorCli, deleteProjectCli, resolveProjectId, } from "./client/commands.js";
8
9
  import { t } from "./i18n/index.js";
9
10
  const require = createRequire(import.meta.url);
10
11
  const packageJson = require("../package.json");
@@ -30,12 +31,26 @@ program
30
31
  .option("--max-runs <n>", t("cli.optMaxRuns"), parseInt)
31
32
  .option("--verbose", t("cli.optVerbose"), false)
32
33
  .option("--cwd <dir>", t("cli.optCwd"))
34
+ .option("--project <name>", t("cli.optProject"))
33
35
  .action(async (intervalStr, cmdArgs, opts) => {
34
- const built = buildLoopOptions(intervalStr, cmdArgs[0], cmdArgs.slice(1), {
35
- ...opts,
36
- cwd: opts.cwd ?? process.cwd(),
37
- });
38
- await startLoop(built.options, built.intervalHuman);
36
+ try {
37
+ const projectId = opts.project
38
+ ? await resolveProjectId(opts.project)
39
+ : undefined;
40
+ const built = buildLoopOptions(intervalStr, {
41
+ ...opts,
42
+ command: cmdArgs[0],
43
+ commandArgs: cmdArgs.slice(1),
44
+ cwd: opts.cwd ?? process.cwd(),
45
+ projectId,
46
+ });
47
+ await startLoop(built.options, built.intervalHuman);
48
+ }
49
+ catch (error) {
50
+ const message = error instanceof Error ? error.message : String(error);
51
+ console.error(t("cli.error", { message }));
52
+ process.exit(1);
53
+ }
39
54
  });
40
55
  program
41
56
  .command("run")
@@ -52,8 +67,10 @@ program
52
67
  return;
53
68
  }
54
69
  const logger = new Logger(opts.verbose ?? false);
55
- const built = buildLoopOptions(intervalStr, cmdArgs[0], cmdArgs.slice(1), {
70
+ const built = buildLoopOptions(intervalStr, {
56
71
  ...opts,
72
+ command: cmdArgs[0],
73
+ commandArgs: cmdArgs.slice(1),
57
74
  cwd: opts.cwd ?? process.cwd(),
58
75
  });
59
76
  const controller = new AbortController();
@@ -62,6 +79,44 @@ program
62
79
  await runLoop(built.options, logger, controller.signal);
63
80
  process.exit(0);
64
81
  });
82
+ const projectCmd = program.command("project").description(t("cli.projectDescription"));
83
+ projectCmd
84
+ .command("list")
85
+ .description(t("cli.projectListDescription"))
86
+ .action(async () => {
87
+ await listProjectsCli();
88
+ });
89
+ projectCmd
90
+ .command("new")
91
+ .description(t("cli.projectNewDescription"))
92
+ .argument("<name>", t("cli.projectArgName"))
93
+ .option("--color <color>", t("cli.optProjectColor"))
94
+ .action(async (name, opts) => {
95
+ await createProjectCli(name, opts.color);
96
+ });
97
+ projectCmd
98
+ .command("rename")
99
+ .description(t("cli.projectRenameDescription"))
100
+ .argument("<id-or-name>", t("cli.projectArgIdOrName"))
101
+ .argument("<new-name>", t("cli.projectArgNewName"))
102
+ .action(async (idOrName, newName) => {
103
+ await renameProjectCli(idOrName, newName);
104
+ });
105
+ projectCmd
106
+ .command("color")
107
+ .description(t("cli.projectColorDescription"))
108
+ .argument("<id-or-name>", t("cli.projectArgIdOrName"))
109
+ .argument("<color>", t("cli.projectArgColor"))
110
+ .action(async (idOrName, color) => {
111
+ await setProjectColorCli(idOrName, color);
112
+ });
113
+ projectCmd
114
+ .command("delete")
115
+ .description(t("cli.projectDeleteDescription"))
116
+ .argument("<id-or-name>", t("cli.projectArgIdOrName"))
117
+ .action(async (idOrName) => {
118
+ await deleteProjectCli(idOrName);
119
+ });
65
120
  program.action(async () => {
66
121
  if (!process.execPath.includes("bun")) {
67
122
  const { spawn } = await import("node:child_process");
@@ -1,4 +1,5 @@
1
1
  import { formatDuration } from "../duration.js";
2
+ import { PROJECT_COLORS } from "../config/constants.js";
2
3
  import { t } from "../i18n/index.js";
3
4
  import { sendRequest, streamRequest } from "./ipc.js";
4
5
  export async function createBackgroundLoop(options, intervalHuman) {
@@ -19,7 +20,7 @@ export async function startLoop(options, intervalHuman) {
19
20
  });
20
21
  console.log(t("cli.startedTitle"));
21
22
  console.log(t("cli.startedId", { id }));
22
- console.log(t("cli.startedCommand", { command: `${options.command} ${options.commandArgs.join(" ")}` }));
23
+ console.log(t("cli.startedCommand", { command: options.command }));
23
24
  console.log(t("cli.startedInterval", { interval: intervalHuman }));
24
25
  console.log(t("cli.startedStatus"));
25
26
  console.log();
@@ -159,3 +160,158 @@ function formatCmd(command, args) {
159
160
  function pad(str, width) {
160
161
  return str.length >= width ? str + " " : str + " ".repeat(width - str.length);
161
162
  }
163
+ async function fetchProjects() {
164
+ const response = await sendRequest({ type: "project-list" });
165
+ if (response.type !== "ok") {
166
+ throw new Error(response.message);
167
+ }
168
+ return response.data;
169
+ }
170
+ export function resolveColor(input) {
171
+ if (PROJECT_COLORS[input])
172
+ return PROJECT_COLORS[input];
173
+ if (/^#[0-9a-fA-F]{6}$/.test(input))
174
+ return input;
175
+ throw new Error(t("cli.projectInvalidColor", {
176
+ color: input,
177
+ valid: Object.keys(PROJECT_COLORS).join(", "),
178
+ }));
179
+ }
180
+ export async function resolveProjectId(idOrName) {
181
+ const projects = await fetchProjects();
182
+ const byId = projects.find((p) => p.id === idOrName);
183
+ if (byId)
184
+ return byId.id;
185
+ const matches = projects.filter((p) => p.name.toLowerCase() === idOrName.toLowerCase());
186
+ if (matches.length === 1)
187
+ return matches[0].id;
188
+ if (matches.length === 0)
189
+ throw new Error(t("cli.projectNotFound", { name: idOrName }));
190
+ throw new Error(t("cli.projectAmbiguous", { name: idOrName }));
191
+ }
192
+ export async function listProjectsCli() {
193
+ try {
194
+ const projects = await fetchProjects();
195
+ if (projects.length === 0) {
196
+ console.log(t("cli.noProjects"));
197
+ process.exit(0);
198
+ }
199
+ const loopResponse = await sendRequest({ type: "list" });
200
+ const loops = loopResponse.type === "ok" ? loopResponse.data : [];
201
+ const counts = new Map();
202
+ for (const loop of loops) {
203
+ counts.set(loop.projectId, (counts.get(loop.projectId) ?? 0) + 1);
204
+ }
205
+ const nameW = Math.max(t("cli.projectHeaderName").length, ...projects.map((p) => p.name.length)) + 2;
206
+ const idW = Math.max(t("cli.projectHeaderId").length, ...projects.map((p) => p.id.length)) + 2;
207
+ const colorW = Math.max(t("cli.projectHeaderColor").length, ...projects.map((p) => p.color.length)) + 2;
208
+ const loopsW = t("cli.projectHeaderLoops").length + 2;
209
+ const header = pad(t("cli.projectHeaderName"), nameW) +
210
+ pad(t("cli.projectHeaderId"), idW) +
211
+ pad(t("cli.projectHeaderColor"), colorW) +
212
+ pad(t("cli.projectHeaderLoops"), loopsW) +
213
+ t("cli.projectHeaderSystem");
214
+ console.log(header);
215
+ console.log("-".repeat(header.length));
216
+ for (const project of projects) {
217
+ console.log(pad(project.name, nameW) +
218
+ pad(project.id, idW) +
219
+ pad(project.color, colorW) +
220
+ pad(String(counts.get(project.id) ?? 0), loopsW) +
221
+ (project.isSystem ? "yes" : ""));
222
+ }
223
+ process.exit(0);
224
+ }
225
+ catch (error) {
226
+ const message = error instanceof Error ? error.message : String(error);
227
+ console.error(t("cli.error", { message }));
228
+ process.exit(1);
229
+ }
230
+ }
231
+ export async function createProjectCli(name, colorInput) {
232
+ try {
233
+ const color = colorInput ? resolveColor(colorInput) : PROJECT_COLORS.cyan;
234
+ const response = await sendRequest({
235
+ type: "project-create",
236
+ payload: { name, color },
237
+ });
238
+ if (response.type !== "ok") {
239
+ throw new Error(response.message);
240
+ }
241
+ const id = response.data.id;
242
+ console.log(t("cli.projectCreated", { name, id }));
243
+ process.exit(0);
244
+ }
245
+ catch (error) {
246
+ const message = error instanceof Error ? error.message : String(error);
247
+ console.error(t("cli.error", { message }));
248
+ process.exit(1);
249
+ }
250
+ }
251
+ export async function renameProjectCli(idOrName, newName) {
252
+ try {
253
+ const id = await resolveProjectId(idOrName);
254
+ const response = await sendRequest({
255
+ type: "project-update",
256
+ payload: { id, name: newName },
257
+ });
258
+ if (response.type !== "ok") {
259
+ throw new Error(response.message);
260
+ }
261
+ console.log(t("cli.projectRenamed", { name: newName }));
262
+ process.exit(0);
263
+ }
264
+ catch (error) {
265
+ const message = error instanceof Error ? error.message : String(error);
266
+ console.error(t("cli.error", { message }));
267
+ process.exit(1);
268
+ }
269
+ }
270
+ export async function setProjectColorCli(idOrName, colorInput) {
271
+ try {
272
+ const projects = await fetchProjects();
273
+ const byId = projects.find((p) => p.id === idOrName);
274
+ const matches = byId
275
+ ? [byId]
276
+ : projects.filter((p) => p.name.toLowerCase() === idOrName.toLowerCase());
277
+ if (matches.length === 0)
278
+ throw new Error(t("cli.projectNotFound", { name: idOrName }));
279
+ if (matches.length > 1)
280
+ throw new Error(t("cli.projectAmbiguous", { name: idOrName }));
281
+ const project = matches[0];
282
+ const color = resolveColor(colorInput);
283
+ const response = await sendRequest({
284
+ type: "project-update",
285
+ payload: { id: project.id, name: project.name, color },
286
+ });
287
+ if (response.type !== "ok") {
288
+ throw new Error(response.message);
289
+ }
290
+ console.log(t("cli.projectColorSet", { name: project.name, color }));
291
+ process.exit(0);
292
+ }
293
+ catch (error) {
294
+ const message = error instanceof Error ? error.message : String(error);
295
+ console.error(t("cli.error", { message }));
296
+ process.exit(1);
297
+ }
298
+ }
299
+ export async function deleteProjectCli(idOrName) {
300
+ try {
301
+ const id = await resolveProjectId(idOrName);
302
+ const response = await sendRequest({
303
+ type: "project-delete",
304
+ payload: { id },
305
+ });
306
+ if (response.type !== "ok") {
307
+ throw new Error(response.message);
308
+ }
309
+ console.log(t("cli.projectDeleted", { name: idOrName }));
310
+ process.exit(0);
311
+ }
312
+ catch (error) {
313
+ const message = error instanceof Error ? error.message : String(error);
314
+ console.error(t("cli.error", { message }));
315
+ process.exit(1);
316
+ }
317
+ }
@@ -12,3 +12,18 @@ export const LOG_TAIL_DEFAULT = 50;
12
12
  export const BOARD_BREAKPOINT_WIDTH = 80;
13
13
  export const HEADER_COMPACT_WIDTH = 60;
14
14
  export const HOVER_BG = "#1e3a5f";
15
+ export const PROJECT_COLORS = {
16
+ white: "#ffffff",
17
+ cyan: "#06b6d4",
18
+ green: "#4ade80",
19
+ yellow: "#facc15",
20
+ orange: "#fb923c",
21
+ pink: "#f472b6",
22
+ };
23
+ export const PROJECT_COLOR_KEYS = Object.keys(PROJECT_COLORS);
24
+ // Entity theme colors — used for header action buttons and view accents
25
+ export const ENTITY_COLORS = {
26
+ loop: "#38bdf8", // cyan/blue
27
+ task: "#a78bfa", // purple
28
+ project: "#34d399", // green
29
+ };
@@ -9,12 +9,18 @@ export function getDataDir() {
9
9
  export function getLoopsDir() {
10
10
  return path.join(getDataDir(), "loops");
11
11
  }
12
+ export function getTasksDir() {
13
+ return path.join(getDataDir(), "tasks");
14
+ }
12
15
  export function getLogsDir() {
13
16
  return path.join(getDataDir(), "logs");
14
17
  }
15
18
  export function loopFile(id) {
16
19
  return path.join(getLoopsDir(), `${id}.json`);
17
20
  }
21
+ export function taskFile(id) {
22
+ return path.join(getTasksDir(), `${id}.json`);
23
+ }
18
24
  export function logFile(id) {
19
25
  return path.join(getLogsDir(), `${id}.log`);
20
26
  }
@@ -23,6 +23,7 @@ export async function executeCommand(command, commandArgs, cwd, logStream, signa
23
23
  stdin: "ignore",
24
24
  cwd: cwd || undefined,
25
25
  cancelSignal: signal,
26
+ shell: true,
26
27
  });
27
28
  child.stdout.on("data", (chunk) => {
28
29
  logStream.write(chunk);
@@ -59,6 +60,7 @@ export async function executeCommandForeground(command, commandArgs, logger, cwd
59
60
  stderr: "inherit",
60
61
  stdin: "inherit",
61
62
  cwd: cwd || undefined,
63
+ shell: true,
62
64
  });
63
65
  const endedAt = new Date();
64
66
  return {
@@ -14,6 +14,9 @@ export async function runLoop(options, logger, signal) {
14
14
  };
15
15
  process.on("SIGINT", onSignal);
16
16
  process.on("SIGTERM", onSignal);
17
+ const command = options.command;
18
+ const commandArgs = options.commandArgs;
19
+ const cwd = options.cwd;
17
20
  try {
18
21
  let isFirstRun = true;
19
22
  while (!state.shuttingDown) {
@@ -40,7 +43,7 @@ export async function runLoop(options, logger, signal) {
40
43
  run: state.runCount,
41
44
  maxRuns: options.maxRuns !== null ? `/${options.maxRuns}` : "",
42
45
  }));
43
- const result = await executeCommandForeground(options.command, options.commandArgs, logger, options.cwd);
46
+ const result = await executeCommandForeground(command, commandArgs, logger, cwd);
44
47
  state.running = false;
45
48
  if (result.exitCode !== 0) {
46
49
  logger.error(t("loop.commandFailed", { code: result.exitCode }));