@molecule/api-ai-tools 1.8.2 → 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 +26 -2
- package/dist/command-position.d.ts.map +1 -1
- package/dist/command-position.js +221 -24
- 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/README.md
CHANGED
|
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
|
3
3
|
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
4
|
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
5
|
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
-
Generated: 2026-10-
|
|
6
|
+
Generated: 2026-10-17T05:02:46.912Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/api-ai-tools
|
|
@@ -115,6 +115,24 @@ interface LocatedCommandSegment {
|
|
|
115
115
|
* inside a nested `sh -c` string, for the same reason as {@link start}.
|
|
116
116
|
*/
|
|
117
117
|
assignmentOffset: number | null
|
|
118
|
+
/**
|
|
119
|
+
* The offset in the command one past the statement's last word — where an option of this
|
|
120
|
+
* statement's program belongs, before whatever closes it. Null inside a nested `sh -c` string.
|
|
121
|
+
*/
|
|
122
|
+
end: number | null
|
|
123
|
+
/**
|
|
124
|
+
* The offset in the command of the word in command position — the program, once every wrapper
|
|
125
|
+
* in front of it is taken away (`npx --prefix /app <here>playwright test`). Null inside a
|
|
126
|
+
* nested `sh -c` string, and for a statement that only sets variables.
|
|
127
|
+
*/
|
|
128
|
+
programOffset: number | null
|
|
129
|
+
/**
|
|
130
|
+
* The separator the shell split at immediately before this statement — `|`, `||`, `&&`, `;`,
|
|
131
|
+
* a newline or `&` — or '' when it is the first statement of the command. It is the parse's own
|
|
132
|
+
* answer, so it is known even where the offsets are not, and a caller deciding whether two
|
|
133
|
+
* statements are one pipeline (`|`, never `;`) asks it instead of reading the text between them.
|
|
134
|
+
*/
|
|
135
|
+
separator: string
|
|
118
136
|
}
|
|
119
137
|
```
|
|
120
138
|
|
|
@@ -235,6 +253,19 @@ interface ToolSchema {
|
|
|
235
253
|
}
|
|
236
254
|
```
|
|
237
255
|
|
|
256
|
+
#### `LeadingSleep`
|
|
257
|
+
|
|
258
|
+
The leading `sleep` a command opens with, and the command that follows it.
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
interface LeadingSleep {
|
|
262
|
+
/** How many seconds the statement would have slept (GNU sleep sums its operands). */
|
|
263
|
+
seconds: number
|
|
264
|
+
/** What follows the leading statement, trimmed — '' when the sleep was the whole command. */
|
|
265
|
+
rest: string
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
238
269
|
#### `SuiteRunInFlight`
|
|
239
270
|
|
|
240
271
|
One suite run, as the rule that decides whether a second may start beside it reads it.
|
|
@@ -258,6 +289,20 @@ interface SuiteRunInFlight {
|
|
|
258
289
|
}
|
|
259
290
|
```
|
|
260
291
|
|
|
292
|
+
#### `InstalledPackageFacts`
|
|
293
|
+
|
|
294
|
+
What the filesystem says about a write target under `node_modules` — the half
|
|
295
|
+
of the decision a path's spelling cannot make.
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
interface InstalledPackageFacts {
|
|
299
|
+
/** Whether the `node_modules` entry the path goes through is a symbolic link. */
|
|
300
|
+
readonly symlink: boolean
|
|
301
|
+
/** Whether a package is really installed there: a `package.json` resolves under the entry. */
|
|
302
|
+
readonly installed: boolean
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
261
306
|
#### `TraceRequest`
|
|
262
307
|
|
|
263
308
|
One request a traced page made, as `*.network` records it.
|
|
@@ -553,6 +598,27 @@ interface SuiteFilterStrip {
|
|
|
553
598
|
}
|
|
554
599
|
```
|
|
555
600
|
|
|
601
|
+
#### `SuiteRunPipelinePlacement`
|
|
602
|
+
|
|
603
|
+
Whether a finished run's exit code belongs to a stage after the suite runner.
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
interface SuiteRunPipelinePlacement {
|
|
607
|
+
/**
|
|
608
|
+
* True when a pipe this tool must not drop still follows the suite runner: the code in the
|
|
609
|
+
* result is that stage's, not the run's.
|
|
610
|
+
*/
|
|
611
|
+
piped: boolean
|
|
612
|
+
/**
|
|
613
|
+
* True when the suite (or a stage of its pipeline) sits inside a nested `sh -c '<command>'`
|
|
614
|
+
* string, so the parse cannot place it in the command's own text. The caller then says so
|
|
615
|
+
* plainly instead of implying a stage it can point at — and keeps the command's own code when
|
|
616
|
+
* no pipe follows the runner, because on that command the pipeline's last stage IS the run.
|
|
617
|
+
*/
|
|
618
|
+
unplaced: boolean
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
556
622
|
#### `FailedTest`
|
|
557
623
|
|
|
558
624
|
One failed test, as a running suite's log names it.
|
|
@@ -658,6 +724,19 @@ Re-export SkillEntry as DiscoveredSkill for backwards compatibility.
|
|
|
658
724
|
type DiscoveredSkill = SkillEntry
|
|
659
725
|
```
|
|
660
726
|
|
|
727
|
+
#### `InstalledPackageProbe`
|
|
728
|
+
|
|
729
|
+
Looks at the filesystem for {@link InstalledPackageFacts}.
|
|
730
|
+
|
|
731
|
+
The guard cannot `stat` the path itself: the tools act through a backend (a
|
|
732
|
+
sandbox over the network for the fleet), so the caller — which holds that
|
|
733
|
+
backend — supplies the look. {@link shellInstalledPackageProbe} builds one
|
|
734
|
+
from any shell runner.
|
|
735
|
+
|
|
736
|
+
```typescript
|
|
737
|
+
type InstalledPackageProbe = (path: string) => Promise<InstalledPackageFacts>
|
|
738
|
+
```
|
|
739
|
+
|
|
661
740
|
### Classes
|
|
662
741
|
|
|
663
742
|
#### `ToolInputError`
|
|
@@ -1029,6 +1108,26 @@ function commandRunsNoTests(command: string): boolean
|
|
|
1029
1108
|
|
|
1030
1109
|
**Returns:** True when the command invokes a runner only to print.
|
|
1031
1110
|
|
|
1111
|
+
#### `leadingSleepPeek(command)`
|
|
1112
|
+
|
|
1113
|
+
The leading `sleep` of a command, when the first statement is one and it asks for at least
|
|
1114
|
+
{@link MIN_PEEK_SLEEP_SECONDS}.
|
|
1115
|
+
|
|
1116
|
+
The statement is read by {@link locatedCommandSegments} — the same parse every other command
|
|
1117
|
+
classifier uses — so only a `sleep` in command position of the FIRST statement counts. The end
|
|
1118
|
+
of that statement is then found by walking forward from its own start: the statement is
|
|
1119
|
+
`sleep <duration>`, whose words carry no quotes and open no substitution, so the first unquoted
|
|
1120
|
+
`;`, newline or `&` ends it. A `|` is refused: dropping the left side of a pipeline would hand
|
|
1121
|
+
the right side its stdin instead, and a `||` fallback would run on a success that never did.
|
|
1122
|
+
|
|
1123
|
+
```typescript
|
|
1124
|
+
function leadingSleepPeek(command: string): LeadingSleep | null
|
|
1125
|
+
```
|
|
1126
|
+
|
|
1127
|
+
- `command` — The shell command the executor sent.
|
|
1128
|
+
|
|
1129
|
+
**Returns:** The seconds and the remaining command, or null when the command does not open with a long `sleep`.
|
|
1130
|
+
|
|
1032
1131
|
#### `withoutHeredocs(command)`
|
|
1033
1132
|
|
|
1034
1133
|
A command with its heredoc bodies removed: the runs, kills and hosts it names in a file it
|
|
@@ -1213,10 +1312,26 @@ function moleculeDistReadNote(path: string): string | null
|
|
|
1213
1312
|
|
|
1214
1313
|
**Returns:** The note, or null when the path is not inside a package's `dist`.
|
|
1215
1314
|
|
|
1216
|
-
#### `
|
|
1315
|
+
#### `shellInstalledPackageProbe(run)`
|
|
1217
1316
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1317
|
+
Build an {@link InstalledPackageProbe} from a shell runner (a backend's
|
|
1318
|
+
`run`, or a sandbox's `exec`): one command reports whether the entry is a
|
|
1319
|
+
link and whether a `package.json` resolves under it.
|
|
1320
|
+
|
|
1321
|
+
```typescript
|
|
1322
|
+
function shellInstalledPackageProbe(
|
|
1323
|
+
run: (command: string) => Promise<{ stdout?: string } | null | undefined>,
|
|
1324
|
+
): InstalledPackageProbe
|
|
1325
|
+
```
|
|
1326
|
+
|
|
1327
|
+
- `run` — Runs a command where the path lives.
|
|
1328
|
+
|
|
1329
|
+
**Returns:** The probe.
|
|
1330
|
+
|
|
1331
|
+
#### `installedPackageWriteRefusal(path, probe)`
|
|
1332
|
+
|
|
1333
|
+
The refusal for a write whose target is inside an INSTALLED package, or null
|
|
1334
|
+
when the path is not under `node_modules` or nothing is installed there.
|
|
1220
1335
|
|
|
1221
1336
|
`node_modules` is not the project's code: nothing written there survives the
|
|
1222
1337
|
next install, no deploy carries it, and a sandbox whose dependency tree is
|
|
@@ -1229,17 +1344,29 @@ answered `ok: true`, and 18 s went to noticing, `npm pack`-ing the package and
|
|
|
1229
1344
|
putting the file back. Reading these files stays allowed (that is how the
|
|
1230
1345
|
declarations above are read at all).
|
|
1231
1346
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1347
|
+
What is protected is the INSTALL, not the spelling: the decision comes from
|
|
1348
|
+
{@link InstalledPackageFacts}, never from the path alone (m106 above). Three
|
|
1349
|
+
things are the run's own to remove or replace —
|
|
1350
|
+
|
|
1351
|
+
- a `node_modules` that is a SYMLINK: `rm`/`unlink` removes the link, never the
|
|
1352
|
+
tree it points at, so a link the run made cannot clobber anything;
|
|
1353
|
+
- anything under the run's own `.mol-scratch/` directory;
|
|
1354
|
+
- a `node_modules` with no `package.json` under it (nothing was ever installed).
|
|
1355
|
+
|
|
1356
|
+
A path INSIDE the link is still decided by what resolves there: a link into a
|
|
1357
|
+
real tree is refused, because removing or writing through it reaches the
|
|
1358
|
+
|
|
1359
|
+
_(description truncated — read the full text in `node_modules/@molecule/api-ai-tools/dist/tools.d.ts`.)_
|
|
1237
1360
|
|
|
1238
1361
|
```typescript
|
|
1239
|
-
function installedPackageWriteRefusal(
|
|
1362
|
+
function installedPackageWriteRefusal(
|
|
1363
|
+
path: string,
|
|
1364
|
+
probe?: InstalledPackageProbe,
|
|
1365
|
+
): Promise<string | null>
|
|
1240
1366
|
```
|
|
1241
1367
|
|
|
1242
1368
|
- `path` — An absolute path a tool is about to write.
|
|
1369
|
+
- `probe` — Looks at the filesystem where the path lives.
|
|
1243
1370
|
|
|
1244
1371
|
**Returns:** The one-line refusal naming the package, or null when nothing is installed there.
|
|
1245
1372
|
|
|
@@ -1602,23 +1729,40 @@ function stripTrailingSuiteFilters(command: string): SuiteFilterStrip
|
|
|
1602
1729
|
|
|
1603
1730
|
**Returns:** The rewritten command and the stages dropped.
|
|
1604
1731
|
|
|
1732
|
+
#### `suiteRunPipelinePlacement(command)`
|
|
1733
|
+
|
|
1734
|
+
Where the suite runner's exit code comes from: the runner itself, or a stage piped after it.
|
|
1735
|
+
|
|
1736
|
+
{@link suiteRunPipedAfterRunner}'s one question, with the honest third answer kept apart. The
|
|
1737
|
+
code a finished run reports is its LAST pipeline stage's, so a stage after the runner makes the
|
|
1738
|
+
code the stage's — but only when such a stage EXISTS. The platform used to report "piped" for
|
|
1739
|
+
any run whose statements it could not place in the text, which one heredoc body was enough to
|
|
1740
|
+
cause: on X1 xr-molecule-flash-m105 (+08:12) a config written in the same `exec_command` made
|
|
1741
|
+
the run come back with no exit code at all, and the note under it claimed a stage after the
|
|
1742
|
+
runner that was not there. The parse now places every statement (`locatedCommandSegments`), so
|
|
1743
|
+
"unplaced" is the rare, named case rather than a guess dressed as an answer.
|
|
1744
|
+
|
|
1745
|
+
```typescript
|
|
1746
|
+
function suiteRunPipelinePlacement(command: string): SuiteRunPipelinePlacement
|
|
1747
|
+
```
|
|
1748
|
+
|
|
1749
|
+
- `command` — The shell command, after the streaming rewrite when one was applied.
|
|
1750
|
+
|
|
1751
|
+
**Returns:** Whether a stage follows the runner, and whether the run could be placed at all.
|
|
1752
|
+
|
|
1605
1753
|
#### `suiteRunPipedAfterRunner(command)`
|
|
1606
1754
|
|
|
1607
1755
|
Whether the command's last step runs a suite whose output still passes through a stage after
|
|
1608
1756
|
the runner — one {@link stripTrailingSuiteFilters} could not prove is output-only (`| tee log`,
|
|
1609
1757
|
`| xargs …`). The pipeline's exit status is that stage's, not the run's.
|
|
1610
1758
|
|
|
1611
|
-
A command whose stages cannot be placed in the text (irregular whitespace inside the run) is
|
|
1612
|
-
reported as piped too: whether a pipe follows the runner is then UNKNOWN, and an unknown exit
|
|
1613
|
-
code that gets trusted is a failing run read as a pass — the failure this rule exists to stop.
|
|
1614
|
-
|
|
1615
1759
|
```typescript
|
|
1616
1760
|
function suiteRunPipedAfterRunner(command: string): boolean
|
|
1617
1761
|
```
|
|
1618
1762
|
|
|
1619
1763
|
- `command` — The shell command, after the streaming rewrite when one was applied.
|
|
1620
1764
|
|
|
1621
|
-
**Returns:** True when a pipe this tool must not drop still follows the suite runner
|
|
1765
|
+
**Returns:** True when a pipe this tool must not drop still follows the suite runner.
|
|
1622
1766
|
|
|
1623
1767
|
#### `suiteRunPriorityLaunch(command, launch)`
|
|
1624
1768
|
|
|
@@ -2231,9 +2375,12 @@ executor reads, so one round of fixes covers all of them rather than the one cau
|
|
|
2231
2375
|
stopped early happened to reach.
|
|
2232
2376
|
|
|
2233
2377
|
A test the request excludes is not one of the causes — it is dropped and named once, at the
|
|
2234
|
-
end, so a result never asks to fix a test it also says to ignore.
|
|
2235
|
-
|
|
2236
|
-
|
|
2378
|
+
end, so a result never asks to fix a test it also says to ignore. A test the runner itself
|
|
2379
|
+
declared `flaky` (it failed once and passed on its config retry) is not a cause either: it is
|
|
2380
|
+
named apart from them, once, as what the tally called it ({@link flakyTestsDeclared}). Where
|
|
2381
|
+
the caller knows how the run was started, the note also hands over the narrowed re-run of
|
|
2382
|
+
exactly those tests ({@link rerunOfRunningSuite}) instead of leaving the executor to re-run the
|
|
2383
|
+
whole suite.
|
|
2237
2384
|
|
|
2238
2385
|
```typescript
|
|
2239
2386
|
function failureCausesNote(
|
|
@@ -2414,6 +2561,15 @@ Canonical tool schemas shared by the agent runtime and documentation.
|
|
|
2414
2561
|
const TOOL_SCHEMAS: Record<string, ToolSchema>
|
|
2415
2562
|
```
|
|
2416
2563
|
|
|
2564
|
+
#### `MIN_PEEK_SLEEP_SECONDS`
|
|
2565
|
+
|
|
2566
|
+
The shortest leading sleep the platform drops: below it the wall clock it costs is noise, and a
|
|
2567
|
+
short settle (`sleep 2; curl …`) is a legitimate thing to run.
|
|
2568
|
+
|
|
2569
|
+
```typescript
|
|
2570
|
+
const MIN_PEEK_SLEEP_SECONDS: 5
|
|
2571
|
+
```
|
|
2572
|
+
|
|
2417
2573
|
#### `SPLIT_WORK_ADVICE`
|
|
2418
2574
|
|
|
2419
2575
|
The closing advice on an over-budget message for a command that is not a test
|
|
@@ -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
|
*/
|
|
@@ -90,6 +96,24 @@ export interface LocatedCommandSegment {
|
|
|
90
96
|
* inside a nested `sh -c` string, for the same reason as {@link start}.
|
|
91
97
|
*/
|
|
92
98
|
assignmentOffset: number | null;
|
|
99
|
+
/**
|
|
100
|
+
* The offset in the command one past the statement's last word — where an option of this
|
|
101
|
+
* statement's program belongs, before whatever closes it. Null inside a nested `sh -c` string.
|
|
102
|
+
*/
|
|
103
|
+
end: number | null;
|
|
104
|
+
/**
|
|
105
|
+
* The offset in the command of the word in command position — the program, once every wrapper
|
|
106
|
+
* in front of it is taken away (`npx --prefix /app <here>playwright test`). Null inside a
|
|
107
|
+
* nested `sh -c` string, and for a statement that only sets variables.
|
|
108
|
+
*/
|
|
109
|
+
programOffset: number | null;
|
|
110
|
+
/**
|
|
111
|
+
* The separator the shell split at immediately before this statement — `|`, `||`, `&&`, `;`,
|
|
112
|
+
* a newline or `&` — or '' when it is the first statement of the command. It is the parse's own
|
|
113
|
+
* answer, so it is known even where the offsets are not, and a caller deciding whether two
|
|
114
|
+
* statements are one pipeline (`|`, never `;`) asks it instead of reading the text between them.
|
|
115
|
+
*/
|
|
116
|
+
separator: string;
|
|
93
117
|
}
|
|
94
118
|
/**
|
|
95
119
|
* Every statement of a command, with its own assignments and where it sits — assignment-only
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command-position.d.ts","sourceRoot":"","sources":["../src/command-position.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"command-position.d.ts","sourceRoot":"","sources":["../src/command-position.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,uFAAuF;AACvF,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAA;IACf,qFAAqF;IACrF,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AA2MD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM3D;AA+cD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAIjE;AAmCD;;;;;;;;;GASG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,OAAO,EAAE,cAAc,GAAG,IAAI,CAAA;IAC9B,0FAA0F;IAC1F,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B;;;;OAIG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B;;;OAGG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;;;OAIG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB,EAAE,CAE/E;AA4ED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOpE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAK/E"}
|
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,19 +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
|
|
404
|
-
* order, with empty ones
|
|
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.
|
|
405
529
|
*/
|
|
406
530
|
function splitSegments(command) {
|
|
531
|
+
const source = blankHeredocBodies(command);
|
|
407
532
|
const parts = [];
|
|
408
533
|
let buffer = '';
|
|
409
534
|
/** The offset in the command of the current buffer's first character. */
|
|
410
535
|
let bufferStart = 0;
|
|
411
536
|
/** The nestings this position sits inside, outermost first. */
|
|
412
537
|
const stack = [];
|
|
413
|
-
for (let i = 0; i <
|
|
414
|
-
const char =
|
|
415
|
-
const next =
|
|
538
|
+
for (let i = 0; i < source.length; i++) {
|
|
539
|
+
const char = source[i] ?? '';
|
|
540
|
+
const next = source[i + 1];
|
|
416
541
|
const top = stack[stack.length - 1];
|
|
417
542
|
// A single-quoted string expands nothing at all: no escape, no substitution, and no quote
|
|
418
543
|
// but its own ends it.
|
|
@@ -469,33 +594,46 @@ function splitSegments(command) {
|
|
|
469
594
|
continue;
|
|
470
595
|
}
|
|
471
596
|
if ((char === '&' && next === '&') || (char === '|' && next === '|')) {
|
|
472
|
-
parts.push({ text: buffer, start: bufferStart });
|
|
597
|
+
parts.push({ text: buffer, start: bufferStart, after: char + next });
|
|
473
598
|
buffer = '';
|
|
474
599
|
i += 1;
|
|
475
600
|
bufferStart = i + 1;
|
|
476
601
|
continue;
|
|
477
602
|
}
|
|
478
603
|
if (char === ';' || char === '|' || char === '\n') {
|
|
479
|
-
parts.push({ text: buffer, start: bufferStart });
|
|
604
|
+
parts.push({ text: buffer, start: bufferStart, after: char });
|
|
480
605
|
buffer = '';
|
|
481
606
|
bufferStart = i + 1;
|
|
482
607
|
continue;
|
|
483
608
|
}
|
|
484
|
-
if (char === '&' &&
|
|
485
|
-
parts.push({ text: buffer, start: bufferStart });
|
|
609
|
+
if (char === '&' && source[i - 1] !== '>' && next !== '>') {
|
|
610
|
+
parts.push({ text: buffer, start: bufferStart, after: '&' });
|
|
486
611
|
buffer = '';
|
|
487
612
|
bufferStart = i + 1;
|
|
488
613
|
continue;
|
|
489
614
|
}
|
|
490
615
|
buffer += char;
|
|
491
616
|
}
|
|
492
|
-
parts.push({ text: buffer, start: bufferStart });
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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;
|
|
499
637
|
}
|
|
500
638
|
/**
|
|
501
639
|
* One segment's words: quotes kept whole, grouping parentheses dropped, and a `(` written
|
|
@@ -559,6 +697,42 @@ function leadingAssignments(text) {
|
|
|
559
697
|
export function commandSegments(command) {
|
|
560
698
|
return locatedSegmentsOf(command, 0).flatMap((located) => located.segment === null ? [] : [located.segment]);
|
|
561
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* Where a statement's word sits in its own raw text, as {@link segmentWords} counts the words.
|
|
702
|
+
*
|
|
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.
|
|
706
|
+
*
|
|
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.
|
|
710
|
+
*/
|
|
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]))
|
|
717
|
+
continue;
|
|
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;
|
|
733
|
+
}
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
562
736
|
/**
|
|
563
737
|
* Every statement of a command, with its own assignments and where it sits — assignment-only
|
|
564
738
|
* statements included, because the variables those persist are exactly what a later statement of
|
|
@@ -587,9 +761,31 @@ function locatedSegmentsOf(command, depth) {
|
|
|
587
761
|
const words = segmentWords(part.text);
|
|
588
762
|
const start = commandStart(words);
|
|
589
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
|
+
};
|
|
590
771
|
if (start.nested !== null) {
|
|
591
|
-
|
|
592
|
-
|
|
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
|
+
}
|
|
593
789
|
continue;
|
|
594
790
|
}
|
|
595
791
|
const word = words[start.at];
|
|
@@ -600,8 +796,9 @@ function locatedSegmentsOf(command, depth) {
|
|
|
600
796
|
out.push({
|
|
601
797
|
segment: null,
|
|
602
798
|
assignments: leading.words,
|
|
603
|
-
|
|
604
|
-
|
|
799
|
+
...placed,
|
|
800
|
+
programOffset: null,
|
|
801
|
+
separator: part.before,
|
|
605
802
|
});
|
|
606
803
|
continue;
|
|
607
804
|
}
|
|
@@ -615,8 +812,8 @@ function locatedSegmentsOf(command, depth) {
|
|
|
615
812
|
timeoutSeconds: start.timeoutSeconds,
|
|
616
813
|
},
|
|
617
814
|
assignments: leading.words,
|
|
618
|
-
|
|
619
|
-
|
|
815
|
+
...placed,
|
|
816
|
+
separator: part.before,
|
|
620
817
|
});
|
|
621
818
|
}
|
|
622
819
|
return out;
|
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';
|