explorbot 0.5.3 → 0.5.4
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/boat/doc-collector/src/config.ts +1 -0
- package/boat/doc-collector/src/docbot.ts +10 -1
- package/boat/prima/src/envelope.ts +12 -6
- package/boat/prima/src/help.ts +4 -2
- package/boat/prima/src/prima.ts +37 -4
- package/boat/prima/src/session-log.ts +3 -3
- package/dist/boat/doc-collector/src/docbot.js +11 -1
- package/dist/boat/prima/src/envelope.js +12 -3
- package/dist/boat/prima/src/help.js +4 -2
- package/dist/boat/prima/src/prima.js +37 -4
- package/dist/boat/prima/src/session-log.js +3 -3
- package/dist/package.json +1 -1
- package/dist/rules/navigator/output.md +3 -3
- package/dist/src/action.js +4 -0
- package/dist/src/ai/driller.js +2 -28
- package/dist/src/ai/navigator.d.ts +5 -2
- package/dist/src/ai/navigator.js +15 -10
- package/dist/src/ai/pilot.d.ts +2 -7
- package/dist/src/ai/pilot.js +34 -7
- package/dist/src/ai/researcher.js +10 -8
- package/dist/src/ai/tools.js +6 -1
- package/dist/src/explorbot.js +1 -1
- package/dist/src/test-plan.d.ts +11 -0
- package/dist/src/test-plan.js +21 -0
- package/dist/src/utils/code-extractor.d.ts +1 -0
- package/dist/src/utils/code-extractor.js +40 -0
- package/dist/src/utils/html.d.ts +2 -0
- package/dist/src/utils/html.js +36 -1
- package/dist/src/utils/mdq/cli.js +3 -1
- package/dist/src/utils/web-sandbox.js +4 -4
- package/docs/doc-collection/basics.md +13 -0
- package/docs/web-testing/researcher.md +8 -0
- package/package.json +1 -1
- package/rules/navigator/output.md +3 -3
- package/src/action.ts +4 -0
- package/src/ai/driller.ts +2 -33
- package/src/ai/navigator.ts +15 -11
- package/src/ai/pilot.ts +35 -15
- package/src/ai/researcher.ts +10 -9
- package/src/ai/tools.ts +5 -1
- package/src/explorbot.ts +1 -1
- package/src/test-plan.ts +23 -0
- package/src/utils/code-extractor.ts +41 -0
- package/src/utils/html.ts +33 -1
- package/src/utils/mdq/cli.ts +3 -2
- package/src/utils/web-sandbox.ts +4 -4
package/src/ai/pilot.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type Explorer from '../explorer.ts';
|
|
|
8
8
|
import type { PlaywrightRecorder } from '../playwright-recorder.ts';
|
|
9
9
|
import type { StateManager } from '../state-manager.ts';
|
|
10
10
|
import { Stats } from '../stats.ts';
|
|
11
|
-
import { type Test, TestResult, TestStatus } from '../test-plan.ts';
|
|
11
|
+
import { SettledExpectation, type SettledStatus, type Test, TestResult, TestStatus } from '../test-plan.ts';
|
|
12
12
|
import { collectInteractiveNodes } from '../utils/aria.ts';
|
|
13
13
|
import { ErrorPageError } from '../utils/error-page.ts';
|
|
14
14
|
import { createDebug, tag } from '../utils/logger.ts';
|
|
@@ -39,6 +39,12 @@ const OUTCOME_STATUS: Record<string, SettledStatus> = {
|
|
|
39
39
|
'The run shows this outcome did not happen.': 'failed',
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
+
const CONTRADICTION_RESOLUTION: Record<string, { status: SettledStatus; resolution: string }> = {
|
|
43
|
+
'The page structure agrees with the screenshot, and the outcome happened.': { status: 'passed', resolution: 'the page structure agrees with the screenshot, so the run log was wrong' },
|
|
44
|
+
'The page structure agrees with the screenshot, and the outcome did not happen.': { status: 'failed', resolution: 'the page structure agrees with the screenshot, so the run log was wrong' },
|
|
45
|
+
'The page structure agrees with the run log: the page holds it, but the screenshot does not show it.': { status: 'contradiction', resolution: 'the page structure agrees with the run log: the page holds it but does not display it, a likely rendering bug' },
|
|
46
|
+
};
|
|
47
|
+
|
|
42
48
|
export class Pilot implements Agent {
|
|
43
49
|
emoji = '🧭';
|
|
44
50
|
private provider: Provider;
|
|
@@ -619,7 +625,7 @@ export class Pilot implements Agent {
|
|
|
619
625
|
let settledByJudge = new Map<string, SettledStatus>();
|
|
620
626
|
if (!image) settledByJudge = await this.settleByJudge(task, undecided);
|
|
621
627
|
undecided = undecided.filter((text) => !settledByJudge.has(text));
|
|
622
|
-
if (!undecided.length) return task.expected.map((text) => (
|
|
628
|
+
if (!undecided.length) return task.expected.map((text) => new SettledExpectation(text, settledByJudge.get(text) || decided(text)));
|
|
623
629
|
|
|
624
630
|
const schema = z.object({
|
|
625
631
|
outcomes: z.array(
|
|
@@ -691,14 +697,36 @@ export class Pilot implements Agent {
|
|
|
691
697
|
if (!response) response = await settle(userContent, this.provider.getAgenticModel('pilot'));
|
|
692
698
|
|
|
693
699
|
const judged = new Map((response?.object?.outcomes || []).map((outcome: any) => [outcome.expectation, outcome]));
|
|
694
|
-
|
|
700
|
+
const settled = task.expected.map((text) => {
|
|
695
701
|
const byJudge = settledByJudge.get(text);
|
|
696
|
-
if (byJudge) return
|
|
697
|
-
if (!undecided.includes(text)) return
|
|
702
|
+
if (byJudge) return new SettledExpectation(text, byJudge);
|
|
703
|
+
if (!undecided.includes(text)) return new SettledExpectation(text, decided(text));
|
|
698
704
|
const outcome = judged.get(text) as { status: SettledStatus; evidence?: string } | undefined;
|
|
699
|
-
if (!outcome) return
|
|
700
|
-
return
|
|
705
|
+
if (!outcome) return new SettledExpectation(text, 'unverified');
|
|
706
|
+
return new SettledExpectation(text, outcome.status || 'unverified', outcome.evidence);
|
|
701
707
|
});
|
|
708
|
+
return this.resolveContradictions(settled, finalState);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
private async resolveContradictions(outcomes: SettledExpectation[], finalState?: ActionResult): Promise<SettledExpectation[]> {
|
|
712
|
+
const judge = this.judge;
|
|
713
|
+
if (!judge || !finalState) return outcomes;
|
|
714
|
+
if (!outcomes.some((outcome) => outcome.contradicted)) return outcomes;
|
|
715
|
+
|
|
716
|
+
const page = finalState.getCompactARIA().slice(0, JUDGE_PAGE_CAP);
|
|
717
|
+
return Promise.all(
|
|
718
|
+
outcomes.map(async (outcome) => {
|
|
719
|
+
if (!outcome.contradicted) return outcome;
|
|
720
|
+
const decision = await judge.decide(`The screenshot and the run log disagree about the expected outcome: ${outcome.text}. Which side does the page structure support?`, [...Object.keys(CONTRADICTION_RESOLUTION), UNDECIDED], {
|
|
721
|
+
expectation: outcome.text,
|
|
722
|
+
disagreement: outcome.evidence || '',
|
|
723
|
+
page,
|
|
724
|
+
});
|
|
725
|
+
const resolved = CONTRADICTION_RESOLUTION[decision.value ?? ''];
|
|
726
|
+
if (!resolved) return outcome;
|
|
727
|
+
return outcome.resolve(resolved.status, resolved.resolution);
|
|
728
|
+
})
|
|
729
|
+
);
|
|
702
730
|
}
|
|
703
731
|
|
|
704
732
|
private async settleByJudge(task: Test, expectations: string[]): Promise<Map<string, SettledStatus>> {
|
|
@@ -1275,11 +1303,3 @@ export class Pilot implements Agent {
|
|
|
1275
1303
|
`;
|
|
1276
1304
|
}
|
|
1277
1305
|
}
|
|
1278
|
-
|
|
1279
|
-
export type SettledStatus = 'passed' | 'failed' | 'unverified' | 'contradiction';
|
|
1280
|
-
|
|
1281
|
-
export interface SettledExpectation {
|
|
1282
|
-
text: string;
|
|
1283
|
-
status: SettledStatus;
|
|
1284
|
-
evidence?: string;
|
|
1285
|
-
}
|
package/src/ai/researcher.ts
CHANGED
|
@@ -103,8 +103,8 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
103
103
|
`;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
async research(state: WebPageState, opts: { screenshot?: boolean; force?: boolean; deep?: boolean; data?: boolean; fix?: boolean; _retriesLeft?: number } = {}): Promise<string> {
|
|
107
|
-
const { screenshot = false, force = false, deep = false, data = false, fix = true } = opts;
|
|
106
|
+
async research(state: WebPageState, opts: { screenshot?: boolean; force?: boolean; deep?: boolean; data?: boolean; fix?: boolean; light?: boolean; _retriesLeft?: number } = {}): Promise<string> {
|
|
107
|
+
const { screenshot = false, force = false, deep = false, data = false, fix = true, light = false } = opts;
|
|
108
108
|
const maxRetries = this.settings.retries ?? 2;
|
|
109
109
|
let retriesLeft = opts._retriesLeft ?? maxRetries;
|
|
110
110
|
this.actionResult = ActionResult.fromState(state);
|
|
@@ -133,6 +133,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
133
133
|
return Observability.run(sessionName, { tags: ['researcher'], sessionId: stateHash }, async () => {
|
|
134
134
|
const displayUrl = state.fullUrl || state.url;
|
|
135
135
|
tag('info').log(`Researching ${displayUrl} to understand the context...`);
|
|
136
|
+
if (light) tag('info').log('Light research: single pass, locator verification skipped');
|
|
136
137
|
setActivity(`${this.emoji} Researching...`, 'action');
|
|
137
138
|
|
|
138
139
|
await this.ensureNavigated(displayUrl, screenshot && this.provider.hasVision());
|
|
@@ -171,7 +172,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
const isOnCurrentState = this.actionResult!.getStateHash() === this.stateManager.getCurrentState()?.hash;
|
|
174
|
-
this.hasScreenshotToAnalyze = screenshot && this.provider.hasVision() && isOnCurrentState;
|
|
175
|
+
this.hasScreenshotToAnalyze = !light && screenshot && this.provider.hasVision() && isOnCurrentState;
|
|
175
176
|
|
|
176
177
|
const conversation = this.provider.startConversation(this.getSystemMessage(), 'researcher');
|
|
177
178
|
|
|
@@ -205,13 +206,13 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
205
206
|
result.parseLocators();
|
|
206
207
|
debugLog(`Extracted ${result.locators.length} locators from research`);
|
|
207
208
|
|
|
208
|
-
if (!interrupted() && result.locators.length === 0 && retriesLeft > 0) {
|
|
209
|
+
if (!light && !interrupted() && result.locators.length === 0 && retriesLeft > 0) {
|
|
209
210
|
tag('warning').log(`No locators parsed, retrying research (${maxRetries - retriesLeft + 1}/${maxRetries})...`);
|
|
210
211
|
await new Promise((r) => setTimeout(r, 1000));
|
|
211
212
|
return this.research(state, { ...opts, force: true, _retriesLeft: retriesLeft - 1 } as any);
|
|
212
213
|
}
|
|
213
214
|
|
|
214
|
-
if (!interrupted()) {
|
|
215
|
+
if (!light && !interrupted()) {
|
|
215
216
|
const brokenContainers = await this.resolveContainers(result);
|
|
216
217
|
const containerCount = result.containers.length;
|
|
217
218
|
if (containerCount > 0 && brokenContainers.length === containerCount && retriesLeft > 0) {
|
|
@@ -232,7 +233,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
232
233
|
}
|
|
233
234
|
|
|
234
235
|
// Stage 3: Fix broken sections via AI conversation continuation
|
|
235
|
-
if (!interrupted() && fix && result.locators.some((l) => l.valid === false)) {
|
|
236
|
+
if (!light && !interrupted() && fix && result.locators.some((l) => l.valid === false)) {
|
|
236
237
|
await this.fixBrokenSections(result, activeConversation);
|
|
237
238
|
}
|
|
238
239
|
|
|
@@ -273,12 +274,12 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
273
274
|
}
|
|
274
275
|
|
|
275
276
|
// Stage 5: Backfill broken elements
|
|
276
|
-
if (!interrupted()) {
|
|
277
|
+
if (!light && !interrupted()) {
|
|
277
278
|
await this.backfillCoordinates(result);
|
|
278
279
|
await this.backfillBrokenLocators(result);
|
|
279
280
|
}
|
|
280
281
|
|
|
281
|
-
if (!interrupted()) {
|
|
282
|
+
if (!light && !interrupted()) {
|
|
282
283
|
await this.detectPagination(result);
|
|
283
284
|
}
|
|
284
285
|
|
|
@@ -309,7 +310,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
309
310
|
result.cleanup();
|
|
310
311
|
|
|
311
312
|
let researchFile: string | null = null;
|
|
312
|
-
if (stateHash) {
|
|
313
|
+
if (stateHash && !light) {
|
|
313
314
|
researchFile = saveResearch(researchState, result.text, combinedHtml);
|
|
314
315
|
}
|
|
315
316
|
|
package/src/ai/tools.ts
CHANGED
|
@@ -112,7 +112,7 @@ export function createCodeceptJSTools({ explorer, stateManager, judge }: ToolDep
|
|
|
112
112
|
let ambiguityError: Error | null = null;
|
|
113
113
|
|
|
114
114
|
for (let i = 0; i < commands.length; i++) {
|
|
115
|
-
|
|
115
|
+
let command = transformContainsCommand(commands[i]);
|
|
116
116
|
let success = await action.attempt(command, explanation);
|
|
117
117
|
|
|
118
118
|
const attempt: { command: string; success: boolean; error?: string } = { command, success };
|
|
@@ -125,10 +125,14 @@ export function createCodeceptJSTools({ explorer, stateManager, judge }: ToolDep
|
|
|
125
125
|
const labels = (await extractWebElements(ambiguityError))?.map((el) => el.label) || [];
|
|
126
126
|
const index = await judge.pick(PICK_ELEMENT_QUESTION, labels, { intent: explanation, task: task.description });
|
|
127
127
|
if (index) success = await action.attemptExactElementIndex(command, index, explanation);
|
|
128
|
+
if (success) command = action.executedSteps.map((step) => step.command).join('\n') || command;
|
|
128
129
|
}
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
if (success) {
|
|
133
|
+
attempt.command = command;
|
|
134
|
+
attempt.success = true;
|
|
135
|
+
attempt.error = undefined;
|
|
132
136
|
const toolResult = await ActionResult.fromState(stateManager.getCurrentState()!).toToolResult(previousState, command);
|
|
133
137
|
|
|
134
138
|
if (!hasObservablePageChange(toolResult)) {
|
package/src/explorbot.ts
CHANGED
|
@@ -155,7 +155,7 @@ export class ExplorBot {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
async visit(url: string): Promise<void> {
|
|
158
|
-
|
|
158
|
+
await this.agentNavigator().visit(this.configParser.resolveTargetPath(url));
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
async openTab(): Promise<void> {
|
package/src/test-plan.ts
CHANGED
|
@@ -66,6 +66,29 @@ export class ActiveNote {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
export type SettledStatus = 'passed' | 'failed' | 'unverified' | 'contradiction';
|
|
70
|
+
|
|
71
|
+
export class SettledExpectation {
|
|
72
|
+
constructor(
|
|
73
|
+
readonly text: string,
|
|
74
|
+
readonly status: SettledStatus,
|
|
75
|
+
readonly evidence?: string,
|
|
76
|
+
readonly resolution?: string
|
|
77
|
+
) {}
|
|
78
|
+
|
|
79
|
+
get failed(): boolean {
|
|
80
|
+
return this.status === 'failed';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get contradicted(): boolean {
|
|
84
|
+
return this.status === 'contradiction';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
resolve(status: SettledStatus, resolution: string): SettledExpectation {
|
|
88
|
+
return new SettledExpectation(this.text, status, this.evidence, resolution);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
69
92
|
export interface StepData {
|
|
70
93
|
text: string;
|
|
71
94
|
duration?: number;
|
|
@@ -3,6 +3,9 @@ import { createDebug } from './logger.js';
|
|
|
3
3
|
const debugLog = createDebug('explorbot:code-extractor');
|
|
4
4
|
|
|
5
5
|
const JS_LANGUAGES = new Set(['', 'js', 'javascript']);
|
|
6
|
+
const QUOTES = ["'", '"', '`'];
|
|
7
|
+
const OPENING_BRACKETS = ['(', '[', '{'];
|
|
8
|
+
const CLOSING_BRACKETS = [')', ']', '}'];
|
|
6
9
|
|
|
7
10
|
export function extractCodeBlocks(aiResponse: string): string[] {
|
|
8
11
|
const codeBlockRegex = /```([^\n`]*)\n([\s\S]*?)\n```/g;
|
|
@@ -24,3 +27,41 @@ export function extractCodeBlocks(aiResponse: string): string[] {
|
|
|
24
27
|
|
|
25
28
|
return codeBlocks;
|
|
26
29
|
}
|
|
30
|
+
|
|
31
|
+
export function splitTopLevel(code: string, separator: string): string[] {
|
|
32
|
+
const parts: string[] = [];
|
|
33
|
+
let current = '';
|
|
34
|
+
let depth = 0;
|
|
35
|
+
let quote = '';
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < code.length; i++) {
|
|
38
|
+
const char = code[i];
|
|
39
|
+
|
|
40
|
+
if (quote) {
|
|
41
|
+
current += char;
|
|
42
|
+
if (char === '\\') current += code[++i] || '';
|
|
43
|
+
if (char === quote) quote = '';
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (char === '/' && code[i + 1] === '/') {
|
|
48
|
+
while (i + 1 < code.length && code[i + 1] !== '\n') i++;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (char === separator && depth <= 0) {
|
|
53
|
+
parts.push(current);
|
|
54
|
+
current = '';
|
|
55
|
+
depth = 0;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
current += char;
|
|
60
|
+
if (QUOTES.includes(char)) quote = char;
|
|
61
|
+
if (OPENING_BRACKETS.includes(char)) depth++;
|
|
62
|
+
if (CLOSING_BRACKETS.includes(char)) depth--;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
parts.push(current);
|
|
66
|
+
return parts;
|
|
67
|
+
}
|
package/src/utils/html.ts
CHANGED
|
@@ -88,6 +88,7 @@ export const EXPLORBOT_ATTRS = {
|
|
|
88
88
|
coveredBy: 'data-explorbot-covered-by',
|
|
89
89
|
context: 'data-explorbot-context',
|
|
90
90
|
eidx: 'data-explorbot-eidx',
|
|
91
|
+
hidden: 'data-explorbot-hidden',
|
|
91
92
|
hit: 'data-explorbot-hit',
|
|
92
93
|
variant: 'data-explorbot-variant',
|
|
93
94
|
} as const;
|
|
@@ -189,6 +190,17 @@ export function captureHtmlForSnapshot(): string {
|
|
|
189
190
|
}
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
const liveElements = Array.from(document.body?.querySelectorAll('*') || []);
|
|
194
|
+
const clonedElements = Array.from(clone.querySelector('body')?.querySelectorAll('*') || []);
|
|
195
|
+
for (let i = 0; i < liveElements.length; i++) {
|
|
196
|
+
const source = liveElements[i];
|
|
197
|
+
if (source.matches('input, textarea, select, option, optgroup')) continue;
|
|
198
|
+
if (source.checkVisibility()) continue;
|
|
199
|
+
if (!source.parentElement?.checkVisibility()) continue;
|
|
200
|
+
if (getComputedStyle(source).display === 'contents') continue;
|
|
201
|
+
clonedElements[i]?.setAttribute('data-explorbot-hidden', 'true');
|
|
202
|
+
}
|
|
203
|
+
|
|
192
204
|
return clone.outerHTML;
|
|
193
205
|
}
|
|
194
206
|
|
|
@@ -1493,7 +1505,27 @@ function getAttribute(element: parse5TreeAdapter.Element, name: string): string
|
|
|
1493
1505
|
return attr?.value;
|
|
1494
1506
|
}
|
|
1495
1507
|
|
|
1508
|
+
function getImgAlt(element: parse5TreeAdapter.Element): string {
|
|
1509
|
+
let alt = '';
|
|
1510
|
+
|
|
1511
|
+
function processNode(node: parse5TreeAdapter.Node) {
|
|
1512
|
+
if (alt) return;
|
|
1513
|
+
if ('tagName' in node) {
|
|
1514
|
+
const el = node as parse5TreeAdapter.Element;
|
|
1515
|
+
if (el.tagName.toLowerCase() === 'img') {
|
|
1516
|
+
alt = getAttribute(el, 'alt') || '';
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
if ('childNodes' in node) node.childNodes.forEach(processNode);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
processNode(element);
|
|
1524
|
+
return alt;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1496
1527
|
function hasHiddenClass(element: parse5TreeAdapter.Element): boolean {
|
|
1528
|
+
if (getAttribute(element, EXPLORBOT_ATTRS.hidden)) return true;
|
|
1497
1529
|
const classAttr = element.attrs.find((attr) => attr.name === 'class');
|
|
1498
1530
|
if (!classAttr) return false;
|
|
1499
1531
|
|
|
@@ -1553,7 +1585,7 @@ export function extractLinks(html: string): ExtractedLink[] {
|
|
|
1553
1585
|
if (href) {
|
|
1554
1586
|
const shouldSkip = skipPrefixes.some((prefix) => href.startsWith(prefix));
|
|
1555
1587
|
if (!shouldSkip) {
|
|
1556
|
-
const rawTitle = getAttribute(element, 'aria-label') || getTextContent(element);
|
|
1588
|
+
const rawTitle = getAttribute(element, 'aria-label') || getTextContent(element) || getImgAlt(element);
|
|
1557
1589
|
const title = sanitizeLinkTitle(rawTitle);
|
|
1558
1590
|
if (title && title.length <= 100) {
|
|
1559
1591
|
const key = `${href}|${title}`;
|
package/src/utils/mdq/cli.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { Command } from 'commander';
|
|
2
|
+
import { Command, type CommanderError } from 'commander';
|
|
3
3
|
import { type MarkdownQuery, MdqError, mdq } from './query.ts';
|
|
4
4
|
|
|
5
5
|
const EDIT_FLAGS = ['remove', 'replace', 'insertBefore', 'insertAfter', 'prepend', 'append', 'addRow', 'addItem', 'set'] as const;
|
|
@@ -7,7 +7,7 @@ const EDIT_FLAGS = ['remove', 'replace', 'insertBefore', 'insertAfter', 'prepend
|
|
|
7
7
|
export async function runMdq(argv: string[], readStdin: StdinReader): Promise<CliResult> {
|
|
8
8
|
const program = new Command();
|
|
9
9
|
program
|
|
10
|
-
.name('mdq')
|
|
10
|
+
.name('mdq-cli')
|
|
11
11
|
.description('query and edit markdown')
|
|
12
12
|
.argument('[selector]', 'markdown selector')
|
|
13
13
|
.argument('[file]', 'file to read; stdin when omitted')
|
|
@@ -31,6 +31,7 @@ export async function runMdq(argv: string[], readStdin: StdinReader): Promise<Cl
|
|
|
31
31
|
try {
|
|
32
32
|
program.parse(argv, { from: 'user' });
|
|
33
33
|
} catch (error) {
|
|
34
|
+
if ((error as CommanderError).code === 'commander.helpDisplayed') return { output: program.helpInformation(), code: 0 };
|
|
34
35
|
return { output: String((error as Error).message), code: 2 };
|
|
35
36
|
}
|
|
36
37
|
|
package/src/utils/web-sandbox.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { faker } from '@faker-js/faker';
|
|
2
2
|
import { hopeThat, retryTo, tryTo, within } from 'codeceptjs/lib/effects';
|
|
3
3
|
import step from 'codeceptjs/steps';
|
|
4
|
+
import { splitTopLevel } from './code-extractor.ts';
|
|
4
5
|
|
|
5
6
|
const SHADOWED_GLOBALS = ['process', 'global', 'globalThis', 'fetch', 'Bun', 'require', 'module', 'exports'];
|
|
6
7
|
const ALLOWED_COMMAND_HEADS = ['I.', 'page.', 'await page.', 'await I.'];
|
|
@@ -9,10 +10,9 @@ const PLAYWRIGHT_ARG_NAMES = ['page'];
|
|
|
9
10
|
const CODECEPT_ARG_NAMES = ['I', 'tryTo', 'retryTo', 'within', 'hopeThat', 'step', 'faker'];
|
|
10
11
|
|
|
11
12
|
export function sanitizeCodeBlock(code: string): string {
|
|
12
|
-
return code
|
|
13
|
-
.
|
|
14
|
-
.
|
|
15
|
-
.filter((line) => ALLOWED_COMMAND_HEADS.some((head) => line.startsWith(head)))
|
|
13
|
+
return splitTopLevel(code, '\n')
|
|
14
|
+
.map((statement) => statement.trim())
|
|
15
|
+
.filter((statement) => ALLOWED_COMMAND_HEADS.some((head) => statement.startsWith(head)))
|
|
16
16
|
.join('\n');
|
|
17
17
|
}
|
|
18
18
|
|