loop-task 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config/constants.js
CHANGED
|
@@ -74,6 +74,7 @@ export const CTRL_SHORTCUT_DELETE = "Ctrl+D";
|
|
|
74
74
|
export const CODE_EDITOR_MAX_VISIBLE = 2;
|
|
75
75
|
export const CODE_EDITOR_MODAL_HEIGHT = 40;
|
|
76
76
|
export const CODE_EDITOR_MODAL_WIDTH = 120;
|
|
77
|
+
export const CODE_EDITOR_WRAP_LENGTH = 40;
|
|
77
78
|
export const CODE_EDITOR_UNDO_LIMIT = 50;
|
|
78
79
|
export const CODE_EDITOR_SYNTAX_COLORS = {
|
|
79
80
|
flag: "#38bdf8",
|
|
@@ -333,41 +333,48 @@ export class LoopController extends EventEmitter {
|
|
|
333
333
|
const cwd = resolveEffectiveCwd(this.options.cwd, this.projectDirectory);
|
|
334
334
|
const chainContext = {};
|
|
335
335
|
const hasChainTasks = !!(task?.onSuccessTaskId || task?.onFailureTaskId);
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
:
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const shouldCaptureStdout = hasChainTasks ||
|
|
345
|
-
|
|
336
|
+
const singleCommandFallback = {
|
|
337
|
+
command: task?.command ?? this.options.command,
|
|
338
|
+
commandArgs: task?.commandArgs ?? this.options.commandArgs,
|
|
339
|
+
commandRaw: task?.commandRaw ?? this.options.commandRaw,
|
|
340
|
+
};
|
|
341
|
+
const taskSteps = task?.steps?.length
|
|
342
|
+
? task.steps
|
|
343
|
+
: [{ commands: [singleCommandFallback] }];
|
|
344
|
+
const shouldCaptureStdout = hasChainTasks || taskSteps.length > 1 || taskSteps[0].commands.length > 1;
|
|
345
|
+
let exitCode = 0;
|
|
346
|
+
let totalDuration = 0;
|
|
347
|
+
let combinedStdout = "";
|
|
348
|
+
for (const step of taskSteps) {
|
|
349
|
+
const stepResults = await Promise.allSettled(step.commands.map((cmd) => executeCommand(interpolate(cmd.command, chainContext), cmd.commandArgs.map(a => interpolate(a, chainContext)), cwd, this.logStream, AbortSignal.any([signal, this.runAbortController.signal]), this.runCount, shouldCaptureStdout)));
|
|
350
|
+
let stepStdout = "";
|
|
351
|
+
for (const r of stepResults) {
|
|
352
|
+
if (r.status === "fulfilled") {
|
|
353
|
+
totalDuration += r.value.duration;
|
|
354
|
+
if (r.value.stdout)
|
|
355
|
+
stepStdout += (stepStdout ? "\n" : "") + r.value.stdout;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
combinedStdout += (combinedStdout && stepStdout ? "\n" : "") + stepStdout;
|
|
359
|
+
if (shouldCaptureStdout && stepStdout) {
|
|
360
|
+
const parsed = parseStdout(stepStdout);
|
|
361
|
+
if (parsed !== null) {
|
|
362
|
+
Object.assign(chainContext, parsed);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const stepFailure = stepResults.some((r) => r.status === "rejected" || (r.status === "fulfilled" && r.value.exitCode !== 0));
|
|
366
|
+
if (stepFailure) {
|
|
367
|
+
const failed = stepResults.find((r) => r.status === "fulfilled" && r.value.exitCode !== 0);
|
|
368
|
+
exitCode = failed ? failed.value.exitCode : 1;
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
346
372
|
this.runAbortController = null;
|
|
347
|
-
// Find first failure (if any) — stop-immediately policy
|
|
348
|
-
const firstFailure = results.find((r) => r.status === "rejected");
|
|
349
|
-
const firstNonZero = results.find((r) => r.status === "fulfilled" && r.value.exitCode !== 0);
|
|
350
|
-
const exitCode = firstFailure ? 1 : (firstNonZero?.value.exitCode ?? 0);
|
|
351
|
-
const totalDuration = results.reduce((sum, r) => {
|
|
352
|
-
if (r.status === "fulfilled")
|
|
353
|
-
return sum + r.value.duration;
|
|
354
|
-
return sum;
|
|
355
|
-
}, 0);
|
|
356
|
-
const combinedStdout = results
|
|
357
|
-
.filter((r) => r.status === "fulfilled" && !!r.value.stdout)
|
|
358
|
-
.map((r) => r.value.stdout)
|
|
359
|
-
.join("\n");
|
|
360
373
|
const result = {
|
|
361
374
|
exitCode,
|
|
362
375
|
duration: totalDuration,
|
|
363
376
|
stdout: combinedStdout || undefined,
|
|
364
377
|
};
|
|
365
|
-
if (shouldCaptureStdout && result.stdout) {
|
|
366
|
-
const parsed = parseStdout(result.stdout);
|
|
367
|
-
if (parsed !== null) {
|
|
368
|
-
Object.assign(chainContext, parsed);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
378
|
this.lastExitCode = result.exitCode;
|
|
372
379
|
this.lastDuration = result.duration;
|
|
373
380
|
const logSize = fs.existsSync(this.logPath) ? fs.statSync(this.logPath).size - this.currentRunStartOffset : 0;
|
package/dist/i18n/en.json
CHANGED
|
@@ -279,7 +279,7 @@
|
|
|
279
279
|
"cmdEditor.lines": "lines",
|
|
280
280
|
"cmdEditor.inlineHint": "enter: new line . ctrl+v: paste . ctrl+y: copy . tab: next field . ctrl+s: save",
|
|
281
281
|
"codeEditor.title": "Command Editor",
|
|
282
|
-
"codeEditor.hint": "ctrl+s save . esc cancel . ctrl+z/ctrl+shift+z undo/redo .
|
|
282
|
+
"codeEditor.hint": "ctrl+s save . esc cancel . ctrl+z/ctrl+shift+z undo/redo . && sequential . || parallel",
|
|
283
283
|
"codeEditor.buttonCopy": "Copy",
|
|
284
284
|
"codeEditor.buttonPaste": "Paste",
|
|
285
285
|
"codeEditor.buttonClear": "Clear",
|
|
@@ -7,7 +7,7 @@ import { useUndoRedo } from "../../shared/useUndoRedo.js";
|
|
|
7
7
|
import { highlightSegments } from "../utils/syntax.js";
|
|
8
8
|
import { joinCommandLines } from "../../loop-config.js";
|
|
9
9
|
import { copyToClipboard, readFromClipboard } from "../../shared/clipboard.js";
|
|
10
|
-
import { sanitizeMultilinePaste } from "../utils/paste.js";
|
|
10
|
+
import { sanitizeMultilinePaste, wrapCommand } from "../utils/paste.js";
|
|
11
11
|
import { CODE_EDITOR_MODAL_HEIGHT, CODE_EDITOR_MODAL_WIDTH, CODE_EDITOR_UNDO_LIMIT, CODE_EDITOR_SYNTAX_COLORS, } from "../../config/constants.js";
|
|
12
12
|
// Cap modal to terminal size: never exceed (terminal - 2) so the border
|
|
13
13
|
// and a 1-row margin are always visible. Ideal max is the constant.
|
|
@@ -62,7 +62,7 @@ export function CodeEditorModal(props) {
|
|
|
62
62
|
// Must be detected BEFORE the escape check — the leading ESC trips
|
|
63
63
|
// key.escape and would close the modal before paste is handled.
|
|
64
64
|
if (input.includes("\x1b[200~")) {
|
|
65
|
-
const pasted = sanitizeMultilinePaste(input);
|
|
65
|
+
const pasted = wrapCommand(sanitizeMultilinePaste(input));
|
|
66
66
|
if (pasted) {
|
|
67
67
|
const next = [...lines];
|
|
68
68
|
const line = next[cursorRow] ?? "";
|
|
@@ -197,7 +197,7 @@ export function CodeEditorModal(props) {
|
|
|
197
197
|
// Multi-char printable input = unbracketed paste (e.g. right-click
|
|
198
198
|
// in terminals that don't support DECSET 2004). Preserve newlines.
|
|
199
199
|
if (input.length > 1 && !key.meta) {
|
|
200
|
-
const pasted = sanitizeMultilinePaste(input);
|
|
200
|
+
const pasted = wrapCommand(sanitizeMultilinePaste(input));
|
|
201
201
|
if (pasted) {
|
|
202
202
|
const next = [...lines];
|
|
203
203
|
const line = next[cursorRow] ?? "";
|
|
@@ -24,8 +24,8 @@ export function TaskForm(props) {
|
|
|
24
24
|
const { mode, editTask, onCancel, onDone } = props;
|
|
25
25
|
const [tasks, setTasks] = useState([]);
|
|
26
26
|
const [commandValue, setCommandValue] = useState(editTask
|
|
27
|
-
? (editTask.
|
|
28
|
-
? editTask.commands.map(c => c.commandRaw ?? [c.command, ...c.commandArgs].join(" ")).join("\n&&\n")
|
|
27
|
+
? (editTask.steps?.length
|
|
28
|
+
? editTask.steps.map(step => step.commands.map(c => c.commandRaw ?? [c.command, ...c.commandArgs].join(" ")).join("\n||\n")).join("\n&&\n")
|
|
29
29
|
: (editTask.commandRaw ?? joinCommand(editTask)))
|
|
30
30
|
: "");
|
|
31
31
|
useEffect(() => {
|
|
@@ -102,30 +102,36 @@ export function TaskForm(props) {
|
|
|
102
102
|
return;
|
|
103
103
|
const onSuccessTaskId = resolveChainId(values.onSuccess ?? "");
|
|
104
104
|
const onFailureTaskId = resolveChainId(values.onFailure ?? "");
|
|
105
|
-
// Split by && to build parallel commands. If only one command, keep
|
|
106
|
-
// backward compat (command + commandArgs, no commands[]).
|
|
107
105
|
const commandSegments = commandValue
|
|
108
106
|
.split(/\n&&\n|\n&&|&&\n|&&/)
|
|
109
107
|
.map(s => s.trim())
|
|
110
108
|
.filter(s => s.length > 0);
|
|
111
109
|
let payload;
|
|
112
|
-
if (commandSegments.length > 1) {
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
110
|
+
if (commandSegments.length > 1 || commandValue.includes("||")) {
|
|
111
|
+
const steps = commandSegments.slice(0, 8).map(seg => {
|
|
112
|
+
const parallelCmds = seg
|
|
113
|
+
.split(/\n\|\|\n|\n\|\||\|\|\n|\|\|/)
|
|
114
|
+
.map(s => s.trim())
|
|
115
|
+
.filter(s => s.length > 0)
|
|
116
|
+
.slice(0, 4)
|
|
117
|
+
.map(cmdSeg => {
|
|
118
|
+
const joined = joinCommandLines(cmdSeg);
|
|
119
|
+
const tokens = parseCommandLine(joined);
|
|
120
|
+
return {
|
|
121
|
+
command: tokens[0] ?? "",
|
|
122
|
+
commandArgs: tokens.slice(1),
|
|
123
|
+
commandRaw: cmdSeg,
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
return { commands: parallelCmds };
|
|
121
127
|
});
|
|
122
|
-
|
|
128
|
+
const firstCmd = steps[0].commands[0];
|
|
123
129
|
payload = {
|
|
124
130
|
name,
|
|
125
|
-
command:
|
|
126
|
-
commandArgs:
|
|
127
|
-
commandRaw:
|
|
128
|
-
|
|
131
|
+
command: firstCmd.command,
|
|
132
|
+
commandArgs: firstCmd.commandArgs,
|
|
133
|
+
commandRaw: firstCmd.commandRaw,
|
|
134
|
+
steps,
|
|
129
135
|
onSuccessTaskId,
|
|
130
136
|
onFailureTaskId,
|
|
131
137
|
};
|
package/dist/tui/utils/paste.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PASTE_MAX_CHARS } from "../../config/constants.js";
|
|
2
|
+
import { CODE_EDITOR_WRAP_LENGTH } from "../../config/constants.js";
|
|
2
3
|
export function sanitizePaste(raw) {
|
|
3
4
|
return raw
|
|
4
5
|
.replace(/\x1b\[20[01]~/g, "")
|
|
@@ -14,3 +15,23 @@ export function sanitizeMultilinePaste(raw) {
|
|
|
14
15
|
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
|
|
15
16
|
.slice(0, PASTE_MAX_CHARS);
|
|
16
17
|
}
|
|
18
|
+
export function wrapCommand(text, maxLen = CODE_EDITOR_WRAP_LENGTH) {
|
|
19
|
+
const words = text.split(" ");
|
|
20
|
+
const lines = [];
|
|
21
|
+
let current = "";
|
|
22
|
+
for (const word of words) {
|
|
23
|
+
if (current.length === 0) {
|
|
24
|
+
current = word;
|
|
25
|
+
}
|
|
26
|
+
else if (current.length + 1 + word.length <= maxLen) {
|
|
27
|
+
current += " " + word;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
lines.push(current);
|
|
31
|
+
current = word;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (current)
|
|
35
|
+
lines.push(current);
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loop-task",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "Loop engineering toolkit. Run any command on a cadence, in the background, managed from a terminal board. Schedule tests, builds, syncs, or agent prompts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|