loop-task 2.1.0 → 2.1.1

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.
@@ -333,41 +333,46 @@ 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
- // Determine the command(s) to run
337
- const taskCommands = task?.commands?.length
338
- ? task.commands
339
- : [{
340
- command: task?.command ?? this.options.command,
341
- commandArgs: task?.commandArgs ?? this.options.commandArgs,
342
- commandRaw: task?.commandRaw ?? this.options.commandRaw,
343
- }];
344
- const shouldCaptureStdout = hasChainTasks || taskCommands.length > 1;
345
- const results = await Promise.allSettled(taskCommands.map((cmd) => executeCommand(cmd.command, cmd.commandArgs, cwd, this.logStream, AbortSignal.any([signal, this.runAbortController.signal]), this.runCount, shouldCaptureStdout)));
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
+ for (const r of stepResults) {
351
+ if (r.status === "fulfilled") {
352
+ totalDuration += r.value.duration;
353
+ if (r.value.stdout)
354
+ combinedStdout += (combinedStdout ? "\n" : "") + r.value.stdout;
355
+ }
356
+ }
357
+ const stepFailure = stepResults.some((r) => r.status === "rejected" || (r.status === "fulfilled" && r.value.exitCode !== 0));
358
+ if (stepFailure) {
359
+ const failed = stepResults.find((r) => r.status === "fulfilled" && r.value.exitCode !== 0);
360
+ exitCode = failed ? failed.value.exitCode : 1;
361
+ break;
362
+ }
363
+ }
346
364
  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");
365
+ if (shouldCaptureStdout && combinedStdout) {
366
+ const parsed = parseStdout(combinedStdout);
367
+ if (parsed !== null) {
368
+ Object.assign(chainContext, parsed);
369
+ }
370
+ }
360
371
  const result = {
361
372
  exitCode,
362
373
  duration: totalDuration,
363
374
  stdout: combinedStdout || undefined,
364
375
  };
365
- if (shouldCaptureStdout && result.stdout) {
366
- const parsed = parseStdout(result.stdout);
367
- if (parsed !== null) {
368
- Object.assign(chainContext, parsed);
369
- }
370
- }
371
376
  this.lastExitCode = result.exitCode;
372
377
  this.lastDuration = result.duration;
373
378
  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 . use && for parallel commands",
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",
@@ -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.commands?.length
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 commands = commandSegments.slice(0, 4).map(seg => {
114
- const joined = joinCommandLines(seg);
115
- const tokens = parseCommandLine(joined);
116
- return {
117
- command: tokens[0] ?? "",
118
- commandArgs: tokens.slice(1),
119
- commandRaw: seg,
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
- // First command also populates command/commandArgs for backward compat
128
+ const firstCmd = steps[0].commands[0];
123
129
  payload = {
124
130
  name,
125
- command: commands[0].command,
126
- commandArgs: commands[0].commandArgs,
127
- commandRaw: commands[0].commandRaw,
128
- commands,
131
+ command: firstCmd.command,
132
+ commandArgs: firstCmd.commandArgs,
133
+ commandRaw: firstCmd.commandRaw,
134
+ steps,
129
135
  onSuccessTaskId,
130
136
  onFailureTaskId,
131
137
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loop-task",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
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": {