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.
- package/README.md +250 -195
- package/dist/board/App.js +225 -87
- package/dist/board/components/ActionButtons.js +28 -8
- package/dist/board/components/CreateForm.js +224 -70
- package/dist/board/components/CreateProjectModal.js +104 -0
- package/dist/board/components/DeleteProjectConfirm.js +53 -0
- package/dist/board/components/EditProjectModal.js +106 -0
- package/dist/board/components/FilterBar.js +5 -4
- package/dist/board/components/Footer.js +19 -6
- package/dist/board/components/Header.js +33 -3
- package/dist/board/components/HelpModal.js +11 -10
- package/dist/board/components/Inspector.js +1 -1
- package/dist/board/components/LogModal.js +39 -8
- package/dist/board/components/Navigator.js +30 -14
- package/dist/board/components/ProjectsModal.js +70 -0
- package/dist/board/components/ProjectsPage.js +105 -0
- package/dist/board/components/RunHistory.js +48 -9
- package/dist/board/components/TaskBrowser.js +95 -0
- package/dist/board/components/TaskFilterBar.js +6 -0
- package/dist/board/components/TaskForm.js +177 -0
- package/dist/board/daemon.js +56 -0
- package/dist/board/format.js +19 -12
- package/dist/board/hooks/useBoardKeybindings.js +264 -156
- package/dist/board/hooks/useTaskKeybindings.js +114 -0
- package/dist/board/router.js +16 -0
- package/dist/board/state.js +18 -14
- package/dist/cli.js +61 -6
- package/dist/client/commands.js +157 -1
- package/dist/config/constants.js +15 -0
- package/dist/config/paths.js +6 -0
- package/dist/core/command-runner.js +2 -0
- package/dist/core/foreground-loop.js +4 -1
- package/dist/core/loop-controller.js +210 -32
- package/dist/daemon/index.js +5 -2
- package/dist/daemon/manager.js +113 -34
- package/dist/daemon/projects.js +104 -0
- package/dist/daemon/server.js +177 -6
- package/dist/daemon/state.js +32 -1
- package/dist/daemon/task-manager.js +59 -0
- package/dist/i18n/en.json +139 -20
- package/dist/loop-config.js +13 -4
- package/dist/shared/clipboard.js +19 -0
- 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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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,
|
|
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");
|
package/dist/client/commands.js
CHANGED
|
@@ -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:
|
|
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
|
+
}
|
package/dist/config/constants.js
CHANGED
|
@@ -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
|
+
};
|
package/dist/config/paths.js
CHANGED
|
@@ -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(
|
|
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 }));
|