@molecule/api-ai-tools 1.8.1 → 1.8.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/README.md +174 -18
- package/dist/command-position.d.ts +68 -2
- package/dist/command-position.d.ts.map +1 -1
- package/dist/command-position.js +309 -43
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/sleep-peek.d.ts +51 -0
- package/dist/sleep-peek.d.ts.map +1 -0
- package/dist/sleep-peek.js +76 -0
- package/dist/tools.d.ts +56 -3
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +285 -48
- package/dist/trace-artifacts.d.ts.map +1 -1
- package/dist/trace-artifacts.js +24 -19
- package/dist/utilities.d.ts +38 -9
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +150 -51
- package/package.json +1 -1
package/dist/command-position.js
CHANGED
|
@@ -21,8 +21,14 @@
|
|
|
21
21
|
* Not command position, and therefore not an invocation: a runner named as an argument
|
|
22
22
|
* (`head -30 run-api-tests-hurl.sh`, `grep -n playwright e2e/README.md`,
|
|
23
23
|
* `git show HEAD:run-api-tests.sh`), and a runner named in a heredoc body — a file a command
|
|
24
|
-
* WRITES is
|
|
25
|
-
*
|
|
24
|
+
* WRITES is not a command it runs, so the bodies are blanked to spaces before anything is split
|
|
25
|
+
* ({@link blankHeredocBodies}); offsets stay exact, because the blanking preserves every position.
|
|
26
|
+
*
|
|
27
|
+
* The parser also answers WHERE each statement sits ({@link locatedCommandSegments}): `start`,
|
|
28
|
+
* `end`, `programOffset` and `assignmentOffset` are the offsets a caller splices at. They are
|
|
29
|
+
* token positions from this walk, never a search for re-joined text — the words are joined back
|
|
30
|
+
* with single spaces for matching, so `command.indexOf(segment.text)` fails on irregular spacing,
|
|
31
|
+
* on grouping braces, and on anything a heredoc body shifts (xr-molecule-flash-m105 +08:12).
|
|
26
32
|
*
|
|
27
33
|
* @module
|
|
28
34
|
*/
|
|
@@ -380,6 +386,123 @@ function commandStart(words) {
|
|
|
380
386
|
}
|
|
381
387
|
return { at, nested: null, timeoutSeconds };
|
|
382
388
|
}
|
|
389
|
+
/**
|
|
390
|
+
* A command with every heredoc BODY blanked to spaces, so its length and every offset in it are
|
|
391
|
+
* unchanged.
|
|
392
|
+
*
|
|
393
|
+
* A heredoc body is data the command writes, not a command it runs: `cat > playwright.config.ts
|
|
394
|
+
* <<'EOF' … import { defineConfig, devices } from '@playwright/test' … EOF` writes a file and runs
|
|
395
|
+
* `cat`. Read as shell, though, every line of the body is a statement — and the words joined back
|
|
396
|
+
* for matching are not the words written, so a caller that placed a statement by searching for
|
|
397
|
+
* them found nothing, in a command whose real statements sat all along. On X1
|
|
398
|
+
* xr-molecule-flash-m105 (+08:12) the body's `import { defineConfig, devices } …` alone made
|
|
399
|
+
* `suiteRunPipedAfterRunner` answer "piped" for a run with no pipe left, and the run's exit code
|
|
400
|
+
* was dropped for the rest of the session.
|
|
401
|
+
*
|
|
402
|
+
* The body runs from the line after the operator to the line whose whole content is the delimiter
|
|
403
|
+
* (leading TABS stripped for `<<-`, as `man bash` defines it); the terminator line is blanked too,
|
|
404
|
+
* and its newline kept, so the statements around it still split at the same places. Newlines
|
|
405
|
+
* inside the body are kept for the same reason. `<<<` is a herestring, not a heredoc, and a `<<`
|
|
406
|
+
* inside quotes or a comment opens nothing.
|
|
407
|
+
*
|
|
408
|
+
* @param command - The shell command.
|
|
409
|
+
* @returns The command with the bodies blanked to spaces, the same length as the input.
|
|
410
|
+
*/
|
|
411
|
+
function blankHeredocBodies(command) {
|
|
412
|
+
if (!command.includes('<<'))
|
|
413
|
+
return command;
|
|
414
|
+
const chars = command.split('');
|
|
415
|
+
/** Overwrite a range with spaces, keeping its newlines so statements still split there. */
|
|
416
|
+
const blank = (from, to) => {
|
|
417
|
+
for (let i = from; i < to; i++)
|
|
418
|
+
if (chars[i] !== '\n')
|
|
419
|
+
chars[i] = ' ';
|
|
420
|
+
};
|
|
421
|
+
let lineStart = 0;
|
|
422
|
+
while (lineStart < command.length) {
|
|
423
|
+
const lineEndRaw = command.indexOf('\n', lineStart);
|
|
424
|
+
const lineEnd = lineEndRaw < 0 ? command.length : lineEndRaw;
|
|
425
|
+
const openers = [];
|
|
426
|
+
let quote = null;
|
|
427
|
+
for (let i = lineStart; i < lineEnd; i++) {
|
|
428
|
+
const char = command[i] ?? '';
|
|
429
|
+
if (quote !== null) {
|
|
430
|
+
if (char === quote)
|
|
431
|
+
quote = null;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (char === '"' || char === "'") {
|
|
435
|
+
quote = char;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
// A `#` that starts a word opens a comment: the rest of the line is not shell words.
|
|
439
|
+
if (char === '#' && (i === lineStart || /\s/.test(command[i - 1] ?? '')))
|
|
440
|
+
break;
|
|
441
|
+
if (char !== '<' || command[i + 1] !== '<')
|
|
442
|
+
continue;
|
|
443
|
+
// `<<<` is a herestring: it opens no body.
|
|
444
|
+
if (command[i + 2] === '<') {
|
|
445
|
+
i += 2;
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
let at = i + 2;
|
|
449
|
+
const stripTabs = command[at] === '-';
|
|
450
|
+
if (stripTabs)
|
|
451
|
+
at += 1;
|
|
452
|
+
while (command[at] === ' ' || command[at] === '\t')
|
|
453
|
+
at += 1;
|
|
454
|
+
let delimiter;
|
|
455
|
+
const opener = command[at];
|
|
456
|
+
if (opener === '"' || opener === "'") {
|
|
457
|
+
const close = command.indexOf(opener, at + 1);
|
|
458
|
+
const wordEnd = close < 0 || close > lineEnd ? lineEnd : close;
|
|
459
|
+
delimiter = command.slice(at + 1, wordEnd);
|
|
460
|
+
at = wordEnd + 1;
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
let wordEnd = at;
|
|
464
|
+
while (wordEnd < lineEnd && !/\s/.test(command[wordEnd] ?? ''))
|
|
465
|
+
wordEnd += 1;
|
|
466
|
+
delimiter = command.slice(at, wordEnd);
|
|
467
|
+
at = wordEnd;
|
|
468
|
+
}
|
|
469
|
+
if (delimiter !== '')
|
|
470
|
+
openers.push({ delimiter, stripTabs });
|
|
471
|
+
i = at - 1;
|
|
472
|
+
}
|
|
473
|
+
if (openers.length === 0) {
|
|
474
|
+
if (lineEndRaw < 0)
|
|
475
|
+
break;
|
|
476
|
+
lineStart = lineEnd + 1;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
// The bodies follow the line that opened them, in the order the operators were written.
|
|
480
|
+
let cursor = lineEndRaw < 0 ? command.length : lineEnd + 1;
|
|
481
|
+
for (const heredoc of openers) {
|
|
482
|
+
let bodyEnd = command.length;
|
|
483
|
+
let after = command.length;
|
|
484
|
+
let from = cursor;
|
|
485
|
+
while (from < command.length) {
|
|
486
|
+
const nl = command.indexOf('\n', from);
|
|
487
|
+
const line = command.slice(from, nl < 0 ? command.length : nl);
|
|
488
|
+
const compared = heredoc.stripTabs ? line.replace(/^\t+/, '') : line;
|
|
489
|
+
if (compared === heredoc.delimiter) {
|
|
490
|
+
bodyEnd = from;
|
|
491
|
+
after = nl < 0 ? command.length : nl + 1;
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
if (nl < 0)
|
|
495
|
+
break;
|
|
496
|
+
from = nl + 1;
|
|
497
|
+
}
|
|
498
|
+
blank(cursor, bodyEnd);
|
|
499
|
+
blank(bodyEnd, after);
|
|
500
|
+
cursor = after;
|
|
501
|
+
}
|
|
502
|
+
lineStart = cursor;
|
|
503
|
+
}
|
|
504
|
+
return chars.join('');
|
|
505
|
+
}
|
|
383
506
|
/**
|
|
384
507
|
* A command's statements and pipeline stages, split where the shell splits them — inside quotes
|
|
385
508
|
* and inside a command substitution, a `|`, `;` or `&&` belongs to that command, not to this one.
|
|
@@ -400,16 +523,21 @@ function commandStart(words) {
|
|
|
400
523
|
* no runner, and every run of it reads as nothing).
|
|
401
524
|
*
|
|
402
525
|
* @param command - The shell command.
|
|
403
|
-
* @returns Each
|
|
526
|
+
* @returns Each statement's text, the offset its first character sits at, and the separator that
|
|
527
|
+
* ended it (`|`, `&&`, `;`, a newline, … — '' after the last one), in order, with empty ones
|
|
528
|
+
* dropped.
|
|
404
529
|
*/
|
|
405
530
|
function splitSegments(command) {
|
|
531
|
+
const source = blankHeredocBodies(command);
|
|
406
532
|
const parts = [];
|
|
407
533
|
let buffer = '';
|
|
534
|
+
/** The offset in the command of the current buffer's first character. */
|
|
535
|
+
let bufferStart = 0;
|
|
408
536
|
/** The nestings this position sits inside, outermost first. */
|
|
409
537
|
const stack = [];
|
|
410
|
-
for (let i = 0; i <
|
|
411
|
-
const char =
|
|
412
|
-
const next =
|
|
538
|
+
for (let i = 0; i < source.length; i++) {
|
|
539
|
+
const char = source[i] ?? '';
|
|
540
|
+
const next = source[i + 1];
|
|
413
541
|
const top = stack[stack.length - 1];
|
|
414
542
|
// A single-quoted string expands nothing at all: no escape, no substitution, and no quote
|
|
415
543
|
// but its own ends it.
|
|
@@ -466,25 +594,46 @@ function splitSegments(command) {
|
|
|
466
594
|
continue;
|
|
467
595
|
}
|
|
468
596
|
if ((char === '&' && next === '&') || (char === '|' && next === '|')) {
|
|
469
|
-
parts.push(buffer);
|
|
597
|
+
parts.push({ text: buffer, start: bufferStart, after: char + next });
|
|
470
598
|
buffer = '';
|
|
471
599
|
i += 1;
|
|
600
|
+
bufferStart = i + 1;
|
|
472
601
|
continue;
|
|
473
602
|
}
|
|
474
603
|
if (char === ';' || char === '|' || char === '\n') {
|
|
475
|
-
parts.push(buffer);
|
|
604
|
+
parts.push({ text: buffer, start: bufferStart, after: char });
|
|
476
605
|
buffer = '';
|
|
606
|
+
bufferStart = i + 1;
|
|
477
607
|
continue;
|
|
478
608
|
}
|
|
479
|
-
if (char === '&' &&
|
|
480
|
-
parts.push(buffer);
|
|
609
|
+
if (char === '&' && source[i - 1] !== '>' && next !== '>') {
|
|
610
|
+
parts.push({ text: buffer, start: bufferStart, after: '&' });
|
|
481
611
|
buffer = '';
|
|
612
|
+
bufferStart = i + 1;
|
|
482
613
|
continue;
|
|
483
614
|
}
|
|
484
615
|
buffer += char;
|
|
485
616
|
}
|
|
486
|
-
parts.push(buffer);
|
|
487
|
-
|
|
617
|
+
parts.push({ text: buffer, start: bufferStart, after: '' });
|
|
618
|
+
const out = [];
|
|
619
|
+
// The separator of the last part, kept across the empty ones so a statement's own separator
|
|
620
|
+
// survives the blank lines around it (`a &&\n b` and a heredoc's blanked body alike).
|
|
621
|
+
let lastAfter = null;
|
|
622
|
+
for (const part of parts) {
|
|
623
|
+
const text = part.text.trim();
|
|
624
|
+
if (text === '') {
|
|
625
|
+
lastAfter = part.after;
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
out.push({
|
|
629
|
+
text,
|
|
630
|
+
start: part.start + (part.text.length - part.text.trimStart().length),
|
|
631
|
+
after: part.after,
|
|
632
|
+
before: lastAfter ?? '',
|
|
633
|
+
});
|
|
634
|
+
lastAfter = part.after;
|
|
635
|
+
}
|
|
636
|
+
return out;
|
|
488
637
|
}
|
|
489
638
|
/**
|
|
490
639
|
* One segment's words: quotes kept whole, grouping parentheses dropped, and a `(` written
|
|
@@ -506,6 +655,38 @@ function segmentWords(text) {
|
|
|
506
655
|
}
|
|
507
656
|
return cleaned;
|
|
508
657
|
}
|
|
658
|
+
/** {@link WORD} anchored at an offset, for walking a statement's raw words. */
|
|
659
|
+
const WORD_AT = new RegExp(WORD.source, 'y');
|
|
660
|
+
/**
|
|
661
|
+
* A statement's own leading `NAME=value` words, and where they end.
|
|
662
|
+
*
|
|
663
|
+
* The shell binds these to the command the statement runs, so they are the environment THAT
|
|
664
|
+
* program sees — which is why a caller writing a variable for a run has to write it here and not
|
|
665
|
+
* at the start of the whole command.
|
|
666
|
+
*
|
|
667
|
+
* @param text - One statement's text.
|
|
668
|
+
* @returns The assignments as written, and the offset in `text` just before the word that runs.
|
|
669
|
+
*/
|
|
670
|
+
function leadingAssignments(text) {
|
|
671
|
+
const words = [];
|
|
672
|
+
let at = 0;
|
|
673
|
+
for (let guard = 0; guard < MAX_WRAPPERS; guard++) {
|
|
674
|
+
while (at < text.length && /\s/.test(text[at] ?? ''))
|
|
675
|
+
at += 1;
|
|
676
|
+
// Grouping in front of the statement (`(npx …)`, `{ …`): not a word the shell assigns with.
|
|
677
|
+
if (text[at] === '(' || text[at] === '{') {
|
|
678
|
+
at += 1;
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
WORD_AT.lastIndex = at;
|
|
682
|
+
const match = WORD_AT.exec(text);
|
|
683
|
+
if (!match || !ENV_ASSIGNMENT.test(match[0]))
|
|
684
|
+
return { words, end: at };
|
|
685
|
+
words.push(match[0]);
|
|
686
|
+
at += match[0].length;
|
|
687
|
+
}
|
|
688
|
+
return { words, end: at };
|
|
689
|
+
}
|
|
509
690
|
/**
|
|
510
691
|
* The segments of a command, each with its program and arguments — the answer every classifier
|
|
511
692
|
* that reads a command as "this runs the suite" is asking for.
|
|
@@ -514,66 +695,151 @@ function segmentWords(text) {
|
|
|
514
695
|
* @returns One entry per statement or pipeline stage that runs something, in order.
|
|
515
696
|
*/
|
|
516
697
|
export function commandSegments(command) {
|
|
517
|
-
return
|
|
698
|
+
return locatedSegmentsOf(command, 0).flatMap((located) => located.segment === null ? [] : [located.segment]);
|
|
518
699
|
}
|
|
519
700
|
/**
|
|
520
|
-
*
|
|
521
|
-
* under (`timeout 280 hurl --test …` → 280), or null when it carries none.
|
|
701
|
+
* Where a statement's word sits in its own raw text, as {@link segmentWords} counts the words.
|
|
522
702
|
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
* (`grep -n 'timeout 280' notes.md`) bounds nothing, and `-k 5` is the grace before the kill, not
|
|
527
|
-
* the bound ({@link WRAPPER_VALUE_FLAGS}).
|
|
703
|
+
* The words are matched for READING (quotes whole, grouping braces dropped, a leading `(` taken
|
|
704
|
+
* off the first word); a caller that writes into the statement needs the offset it was written
|
|
705
|
+
* at. Both come from this one walk, so the two can never disagree about which word is which.
|
|
528
706
|
*
|
|
529
|
-
* @param
|
|
530
|
-
* @
|
|
707
|
+
* @param text - One statement's text.
|
|
708
|
+
* @param index - The word's index in {@link segmentWords}'s list.
|
|
709
|
+
* @returns The word's offset in `text`, or null when there is no such word.
|
|
531
710
|
*/
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
711
|
+
function wordOffset(text, index) {
|
|
712
|
+
const placement = new RegExp(WORD.source, 'g');
|
|
713
|
+
let kept = 0;
|
|
714
|
+
let first = true;
|
|
715
|
+
for (let match = placement.exec(text); match !== null; match = placement.exec(text)) {
|
|
716
|
+
if (GROUPING.has(match[0]))
|
|
536
717
|
continue;
|
|
537
|
-
|
|
718
|
+
if (first) {
|
|
719
|
+
first = false;
|
|
720
|
+
const bare = match[0].replace(/^[({]+/, '');
|
|
721
|
+
// `segmentWords` shifts a first word that was nothing but grouping away, so the NEXT word
|
|
722
|
+
// is not stripped and is counted from zero.
|
|
723
|
+
if (bare === '')
|
|
724
|
+
continue;
|
|
725
|
+
if (kept === index)
|
|
726
|
+
return match.index + (match[0].length - bare.length);
|
|
727
|
+
kept += 1;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (kept === index)
|
|
731
|
+
return match.index;
|
|
732
|
+
kept += 1;
|
|
538
733
|
}
|
|
539
|
-
return
|
|
734
|
+
return null;
|
|
540
735
|
}
|
|
541
736
|
/**
|
|
542
|
-
*
|
|
543
|
-
*
|
|
737
|
+
* Every statement of a command, with its own assignments and where it sits — assignment-only
|
|
738
|
+
* statements included, because the variables those persist are exactly what a later statement of
|
|
739
|
+
* the same command reads.
|
|
740
|
+
*
|
|
741
|
+
* @param command - The shell command.
|
|
742
|
+
* @returns One entry per statement or pipeline stage, in order.
|
|
743
|
+
*/
|
|
744
|
+
export function locatedCommandSegments(command) {
|
|
745
|
+
return locatedSegmentsOf(command, 0);
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* {@link locatedCommandSegments}'s recursion, so a `sh -c '<command>'` is opened as far as
|
|
749
|
+
* {@link MAX_NESTING} allows. A nested statement's offsets are cleared: they are positions in
|
|
750
|
+
* the nested string, not in the command this walk started from.
|
|
544
751
|
*
|
|
545
752
|
* @param command - The shell command.
|
|
546
753
|
* @param depth - How many `sh -c` strings this command already sits inside.
|
|
547
|
-
* @returns The
|
|
754
|
+
* @returns The located statements.
|
|
548
755
|
*/
|
|
549
|
-
function
|
|
756
|
+
function locatedSegmentsOf(command, depth) {
|
|
550
757
|
if (typeof command !== 'string' || depth > MAX_NESTING)
|
|
551
758
|
return [];
|
|
552
759
|
const out = [];
|
|
553
|
-
for (const
|
|
554
|
-
const words = segmentWords(text);
|
|
555
|
-
if (words.length === 0)
|
|
556
|
-
continue;
|
|
760
|
+
for (const part of splitSegments(command)) {
|
|
761
|
+
const words = segmentWords(part.text);
|
|
557
762
|
const start = commandStart(words);
|
|
763
|
+
const leading = leadingAssignments(part.text);
|
|
764
|
+
/** Every offset in this statement is an offset in the command it was written in. */
|
|
765
|
+
const placed = {
|
|
766
|
+
start: part.start,
|
|
767
|
+
end: part.start + part.text.length,
|
|
768
|
+
assignmentOffset: part.start + leading.end,
|
|
769
|
+
programOffset: words[start.at] === undefined ? null : part.start + (wordOffset(part.text, start.at) ?? 0),
|
|
770
|
+
};
|
|
558
771
|
if (start.nested !== null) {
|
|
559
|
-
|
|
772
|
+
const inner = locatedSegmentsOf(start.nested, depth + 1);
|
|
773
|
+
for (let i = 0; i < inner.length; i += 1) {
|
|
774
|
+
const entry = inner[i];
|
|
775
|
+
if (!entry)
|
|
776
|
+
continue;
|
|
777
|
+
out.push({
|
|
778
|
+
...entry,
|
|
779
|
+
// A nested statement's positions are positions in the nested string, not in this
|
|
780
|
+
// command. The FIRST one keeps this statement's own separator: the shell reads the
|
|
781
|
+
// nested string where this statement stands.
|
|
782
|
+
start: null,
|
|
783
|
+
end: null,
|
|
784
|
+
assignmentOffset: null,
|
|
785
|
+
programOffset: null,
|
|
786
|
+
separator: i === 0 ? part.before : entry.separator,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
560
789
|
continue;
|
|
561
790
|
}
|
|
562
791
|
const word = words[start.at];
|
|
563
|
-
|
|
564
|
-
|
|
792
|
+
if (word === undefined) {
|
|
793
|
+
// Only wrappers and assignments: nothing this statement runs, but the variables it sets
|
|
794
|
+
// persist in the shell the rest of the command runs in.
|
|
795
|
+
if (leading.words.length > 0)
|
|
796
|
+
out.push({
|
|
797
|
+
segment: null,
|
|
798
|
+
assignments: leading.words,
|
|
799
|
+
...placed,
|
|
800
|
+
programOffset: null,
|
|
801
|
+
separator: part.before,
|
|
802
|
+
});
|
|
565
803
|
continue;
|
|
804
|
+
}
|
|
566
805
|
const program = programName(word);
|
|
567
806
|
const args = words.slice(start.at + 1);
|
|
568
807
|
out.push({
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
808
|
+
segment: {
|
|
809
|
+
program,
|
|
810
|
+
args,
|
|
811
|
+
text: [program, ...args].join(' '),
|
|
812
|
+
timeoutSeconds: start.timeoutSeconds,
|
|
813
|
+
},
|
|
814
|
+
assignments: leading.words,
|
|
815
|
+
...placed,
|
|
816
|
+
separator: part.before,
|
|
573
817
|
});
|
|
574
818
|
}
|
|
575
819
|
return out;
|
|
576
820
|
}
|
|
821
|
+
/**
|
|
822
|
+
* The bound a command puts on its own run: the largest `timeout <N>` any of its segments runs
|
|
823
|
+
* under (`timeout 280 hurl --test …` → 280), or null when it carries none.
|
|
824
|
+
*
|
|
825
|
+
* This is the bound `man timeout` enforces by KILLING the command — so a run that used it did not
|
|
826
|
+
* finish, whatever the pipeline's own exit status says. It is read here, from the same parse every
|
|
827
|
+
* other command reader uses, rather than matched as text: `timeout` in an argument
|
|
828
|
+
* (`grep -n 'timeout 280' notes.md`) bounds nothing, and `-k 5` is the grace before the kill, not
|
|
829
|
+
* the bound ({@link WRAPPER_VALUE_FLAGS}).
|
|
830
|
+
*
|
|
831
|
+
* @param command - The shell command.
|
|
832
|
+
* @returns The seconds the command allows itself, or null.
|
|
833
|
+
*/
|
|
834
|
+
export function commandTimeoutSeconds(command) {
|
|
835
|
+
let bound = null;
|
|
836
|
+
for (const segment of commandSegments(command)) {
|
|
837
|
+
if (segment.timeoutSeconds === null)
|
|
838
|
+
continue;
|
|
839
|
+
bound = bound === null ? segment.timeoutSeconds : Math.max(bound, segment.timeoutSeconds);
|
|
840
|
+
}
|
|
841
|
+
return bound;
|
|
842
|
+
}
|
|
577
843
|
/**
|
|
578
844
|
* Whether any segment of the command runs a program whose command-position text matches
|
|
579
845
|
* `pattern` — the one question a runner-kind table asks.
|
package/dist/index.d.ts
CHANGED
|
@@ -137,6 +137,7 @@ export * from './cut-run.js';
|
|
|
137
137
|
export * from './input-normalizer.js';
|
|
138
138
|
export * from './no-test-run.js';
|
|
139
139
|
export * from './schemas.js';
|
|
140
|
+
export * from './sleep-peek.js';
|
|
140
141
|
export * from './suite-overlap.js';
|
|
141
142
|
export * from './system-prompt.js';
|
|
142
143
|
export * from './tools.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkIG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA;AAC5B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,sBAAsB,CAAA;AACpC,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkIG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,sBAAsB,CAAA;AACpC,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -137,6 +137,7 @@ export * from './cut-run.js';
|
|
|
137
137
|
export * from './input-normalizer.js';
|
|
138
138
|
export * from './no-test-run.js';
|
|
139
139
|
export * from './schemas.js';
|
|
140
|
+
export * from './sleep-peek.js';
|
|
140
141
|
export * from './suite-overlap.js';
|
|
141
142
|
export * from './system-prompt.js';
|
|
142
143
|
export * from './tools.js';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A leading `sleep` in an executor's command: the seconds it would have waited, and the command
|
|
3
|
+
* that follows it.
|
|
4
|
+
*
|
|
5
|
+
* X1 xr-molecule-flash-m105 (+08:49 → +10:53, 2026-10-17) spent 225 s of its 60-minute clock
|
|
6
|
+
* inside `exec_command` calls of the shape `sleep N; tail <task log>`. The platform's own launch
|
|
7
|
+
* note said "peek with exec_command: tail <log>" beside "never sleep", and the only concrete
|
|
8
|
+
* instruction was the peek, so the executor built the poll by hand. Two of its three sleeps were
|
|
9
|
+
* provably dead — the task had ended 0.1 s in (`playwright: not found`, exit 127) and another had
|
|
10
|
+
* already printed `Error: No tests found` — and the third spent 120 s asleep beside a live
|
|
11
|
+
* 139-test run whose failures were already in the log.
|
|
12
|
+
*
|
|
13
|
+
* The platform does not have to answer "what is the task doing" from a sleep: `wait_for_task`
|
|
14
|
+
* already returns the moment the task ends, early on a proven repeating cause, and with the
|
|
15
|
+
* progress and failures the log holds. This module answers only the parsing question — is the
|
|
16
|
+
* command's FIRST statement a `sleep` of {@link MIN_PEEK_SLEEP_SECONDS} seconds or more, and what
|
|
17
|
+
* follows it — from the package's own shell parse ({@link locatedCommandSegments}), so a `sleep`
|
|
18
|
+
* named in an argument (`grep -n 'sleep 60' notes.md`) or written after another statement
|
|
19
|
+
* (`cd x && sleep 60`) is not one.
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* The shortest leading sleep the platform drops: below it the wall clock it costs is noise, and a
|
|
25
|
+
* short settle (`sleep 2; curl …`) is a legitimate thing to run.
|
|
26
|
+
*/
|
|
27
|
+
export declare const MIN_PEEK_SLEEP_SECONDS = 5;
|
|
28
|
+
/** The leading `sleep` a command opens with, and the command that follows it. */
|
|
29
|
+
export interface LeadingSleep {
|
|
30
|
+
/** How many seconds the statement would have slept (GNU sleep sums its operands). */
|
|
31
|
+
seconds: number;
|
|
32
|
+
/** What follows the leading statement, trimmed — '' when the sleep was the whole command. */
|
|
33
|
+
rest: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The leading `sleep` of a command, when the first statement is one and it asks for at least
|
|
37
|
+
* {@link MIN_PEEK_SLEEP_SECONDS}.
|
|
38
|
+
*
|
|
39
|
+
* The statement is read by {@link locatedCommandSegments} — the same parse every other command
|
|
40
|
+
* classifier uses — so only a `sleep` in command position of the FIRST statement counts. The end
|
|
41
|
+
* of that statement is then found by walking forward from its own start: the statement is
|
|
42
|
+
* `sleep <duration>`, whose words carry no quotes and open no substitution, so the first unquoted
|
|
43
|
+
* `;`, newline or `&` ends it. A `|` is refused: dropping the left side of a pipeline would hand
|
|
44
|
+
* the right side its stdin instead, and a `||` fallback would run on a success that never did.
|
|
45
|
+
*
|
|
46
|
+
* @param command - The shell command the executor sent.
|
|
47
|
+
* @returns The seconds and the remaining command, or null when the command does not open with a
|
|
48
|
+
* long `sleep`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function leadingSleepPeek(command: string): LeadingSleep | null;
|
|
51
|
+
//# sourceMappingURL=sleep-peek.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sleep-peek.d.ts","sourceRoot":"","sources":["../src/sleep-peek.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH;;;GAGG;AACH,eAAO,MAAM,sBAAsB,IAAI,CAAA;AAEvC,iFAAiF;AACjF,MAAM,WAAW,YAAY;IAC3B,qFAAqF;IACrF,OAAO,EAAE,MAAM,CAAA;IACf,6FAA6F;IAC7F,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAyBrE"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A leading `sleep` in an executor's command: the seconds it would have waited, and the command
|
|
3
|
+
* that follows it.
|
|
4
|
+
*
|
|
5
|
+
* X1 xr-molecule-flash-m105 (+08:49 → +10:53, 2026-10-17) spent 225 s of its 60-minute clock
|
|
6
|
+
* inside `exec_command` calls of the shape `sleep N; tail <task log>`. The platform's own launch
|
|
7
|
+
* note said "peek with exec_command: tail <log>" beside "never sleep", and the only concrete
|
|
8
|
+
* instruction was the peek, so the executor built the poll by hand. Two of its three sleeps were
|
|
9
|
+
* provably dead — the task had ended 0.1 s in (`playwright: not found`, exit 127) and another had
|
|
10
|
+
* already printed `Error: No tests found` — and the third spent 120 s asleep beside a live
|
|
11
|
+
* 139-test run whose failures were already in the log.
|
|
12
|
+
*
|
|
13
|
+
* The platform does not have to answer "what is the task doing" from a sleep: `wait_for_task`
|
|
14
|
+
* already returns the moment the task ends, early on a proven repeating cause, and with the
|
|
15
|
+
* progress and failures the log holds. This module answers only the parsing question — is the
|
|
16
|
+
* command's FIRST statement a `sleep` of {@link MIN_PEEK_SLEEP_SECONDS} seconds or more, and what
|
|
17
|
+
* follows it — from the package's own shell parse ({@link locatedCommandSegments}), so a `sleep`
|
|
18
|
+
* named in an argument (`grep -n 'sleep 60' notes.md`) or written after another statement
|
|
19
|
+
* (`cd x && sleep 60`) is not one.
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
import { durationSeconds, locatedCommandSegments } from './command-position.js';
|
|
24
|
+
/**
|
|
25
|
+
* The shortest leading sleep the platform drops: below it the wall clock it costs is noise, and a
|
|
26
|
+
* short settle (`sleep 2; curl …`) is a legitimate thing to run.
|
|
27
|
+
*/
|
|
28
|
+
export const MIN_PEEK_SLEEP_SECONDS = 5;
|
|
29
|
+
/**
|
|
30
|
+
* The leading `sleep` of a command, when the first statement is one and it asks for at least
|
|
31
|
+
* {@link MIN_PEEK_SLEEP_SECONDS}.
|
|
32
|
+
*
|
|
33
|
+
* The statement is read by {@link locatedCommandSegments} — the same parse every other command
|
|
34
|
+
* classifier uses — so only a `sleep` in command position of the FIRST statement counts. The end
|
|
35
|
+
* of that statement is then found by walking forward from its own start: the statement is
|
|
36
|
+
* `sleep <duration>`, whose words carry no quotes and open no substitution, so the first unquoted
|
|
37
|
+
* `;`, newline or `&` ends it. A `|` is refused: dropping the left side of a pipeline would hand
|
|
38
|
+
* the right side its stdin instead, and a `||` fallback would run on a success that never did.
|
|
39
|
+
*
|
|
40
|
+
* @param command - The shell command the executor sent.
|
|
41
|
+
* @returns The seconds and the remaining command, or null when the command does not open with a
|
|
42
|
+
* long `sleep`.
|
|
43
|
+
*/
|
|
44
|
+
export function leadingSleepPeek(command) {
|
|
45
|
+
if (typeof command !== 'string')
|
|
46
|
+
return null;
|
|
47
|
+
const first = locatedCommandSegments(command)[0];
|
|
48
|
+
if (!first || first.start === null || first.segment === null)
|
|
49
|
+
return null;
|
|
50
|
+
if (first.segment.program !== 'sleep')
|
|
51
|
+
return null;
|
|
52
|
+
// GNU sleep sums its operands (`sleep 45 15`); every one of them has to be a duration.
|
|
53
|
+
let seconds = 0;
|
|
54
|
+
for (const arg of first.segment.args) {
|
|
55
|
+
const part = durationSeconds(arg);
|
|
56
|
+
if (part === null)
|
|
57
|
+
return null;
|
|
58
|
+
seconds += part;
|
|
59
|
+
}
|
|
60
|
+
if (seconds < MIN_PEEK_SLEEP_SECONDS)
|
|
61
|
+
return null;
|
|
62
|
+
let end = command.length;
|
|
63
|
+
for (let i = first.start; i < command.length; i++) {
|
|
64
|
+
const char = command[i] ?? '';
|
|
65
|
+
if (char === '|')
|
|
66
|
+
return null;
|
|
67
|
+
if (char === ';' || char === '\n' || char === '&') {
|
|
68
|
+
end = i;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
let at = end;
|
|
73
|
+
while (at < command.length && /[;&\s]/.test(command[at] ?? ''))
|
|
74
|
+
at += 1;
|
|
75
|
+
return { seconds, rest: command.slice(at).trim() };
|
|
76
|
+
}
|