loop-task 2.1.1 → 2.1.3
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",
|
|
@@ -4,7 +4,19 @@ import { formatDuration } from "../duration.js";
|
|
|
4
4
|
import { t } from "../i18n/index.js";
|
|
5
5
|
import { MAX_CONTEXT_STDOUT_BYTES } from "../config/constants.js";
|
|
6
6
|
function quoteArg(arg) {
|
|
7
|
-
|
|
7
|
+
if (arg.length === 0)
|
|
8
|
+
return '""';
|
|
9
|
+
// If the arg already starts with a single quote (from interpolate's
|
|
10
|
+
// shellEscape), it's already safely shell-quoted — pass through as-is.
|
|
11
|
+
if (arg.startsWith("'") && arg.endsWith("'"))
|
|
12
|
+
return arg;
|
|
13
|
+
// Safe characters: no quoting needed
|
|
14
|
+
if (/^[A-Za-z0-9_\-=:./,@]+$/.test(arg))
|
|
15
|
+
return arg;
|
|
16
|
+
// Otherwise: double-quote and escape inner double-quotes.
|
|
17
|
+
// Replace newlines with spaces (shell command line is single-line).
|
|
18
|
+
const cleaned = arg.replace(/[\n\r]/g, " ");
|
|
19
|
+
return `"${cleaned.replace(/"/g, '\\"')}"`;
|
|
8
20
|
}
|
|
9
21
|
function formatCommandLine(command, commandArgs) {
|
|
10
22
|
return [command, ...commandArgs.map((a) => quoteArg(a.replace(/[\n\r]/g, " ")))].join(" ").trim();
|
|
@@ -347,11 +347,19 @@ export class LoopController extends EventEmitter {
|
|
|
347
347
|
let combinedStdout = "";
|
|
348
348
|
for (const step of taskSteps) {
|
|
349
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 = "";
|
|
350
351
|
for (const r of stepResults) {
|
|
351
352
|
if (r.status === "fulfilled") {
|
|
352
353
|
totalDuration += r.value.duration;
|
|
353
354
|
if (r.value.stdout)
|
|
354
|
-
|
|
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);
|
|
355
363
|
}
|
|
356
364
|
}
|
|
357
365
|
const stepFailure = stepResults.some((r) => r.status === "rejected" || (r.status === "fulfilled" && r.value.exitCode !== 0));
|
|
@@ -362,12 +370,6 @@ export class LoopController extends EventEmitter {
|
|
|
362
370
|
}
|
|
363
371
|
}
|
|
364
372
|
this.runAbortController = null;
|
|
365
|
-
if (shouldCaptureStdout && combinedStdout) {
|
|
366
|
-
const parsed = parseStdout(combinedStdout);
|
|
367
|
-
if (parsed !== null) {
|
|
368
|
-
Object.assign(chainContext, parsed);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
373
|
const result = {
|
|
372
374
|
exitCode,
|
|
373
375
|
duration: totalDuration,
|
package/dist/core/template.js
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
function shellEscape(value) {
|
|
2
|
+
if (value.length === 0)
|
|
3
|
+
return '""';
|
|
4
|
+
// If the value is purely safe characters, pass it through unmodified
|
|
5
|
+
if (/^[A-Za-z0-9_\-=:./,@]+$/.test(value))
|
|
6
|
+
return value;
|
|
7
|
+
// Escape characters that are dangerous inside double quotes, then wrap
|
|
8
|
+
// the whole value in single quotes — the strongest shell quoting.
|
|
9
|
+
// Single quotes preserve everything literally (newlines, backticks,
|
|
10
|
+
// $, ", \, parens) except single quotes themselves.
|
|
11
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
12
|
+
}
|
|
1
13
|
export function interpolate(input, context) {
|
|
2
|
-
return input.replace(/{{(\w+)}}/g, (_, key) =>
|
|
14
|
+
return input.replace(/{{(\w+)}}/g, (_, key) => {
|
|
15
|
+
const raw = context[key];
|
|
16
|
+
if (raw === undefined || raw === null)
|
|
17
|
+
return "";
|
|
18
|
+
return shellEscape(String(raw));
|
|
19
|
+
});
|
|
3
20
|
}
|
|
@@ -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] ?? "";
|
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.3",
|
|
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": {
|