@agentrhq/webcmd 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/cli-manifest.json +217 -13
- package/clis/chatgpt/ask.js +1 -1
- package/clis/chatgpt/ask.test.js +11 -0
- package/clis/chatgpt/commands.test.js +102 -1
- package/clis/chatgpt/deep-research-result.js +45 -3
- package/clis/chatgpt/image.js +22 -2
- package/clis/chatgpt/project-file-add.js +24 -1
- package/clis/chatgpt/utils.js +258 -52
- package/clis/chatgpt/utils.test.js +259 -1
- package/clis/claude/ask.js +21 -1
- package/clis/facebook/feed.js +110 -24
- package/clis/facebook/feed.test.js +62 -0
- package/clis/facebook/search.js +185 -38
- package/clis/facebook/search.test.js +95 -50
- package/clis/instagram/explore.js +30 -12
- package/clis/instagram/explore.test.js +77 -0
- package/clis/instagram/post.js +14 -1
- package/clis/instagram/reel.js +13 -1
- package/clis/linkedin/company.js +153 -0
- package/clis/linkedin/company.test.js +111 -0
- package/clis/linkedin/connections.js +135 -0
- package/clis/linkedin/connections.test.js +141 -0
- package/clis/mercury/reimbursement-draft.js +12 -1
- package/clis/twitter/article-evaluate.test.js +27 -0
- package/clis/twitter/article.js +73 -16
- package/clis/twitter/article.test.js +209 -0
- package/clis/twitter/download.js +6 -1
- package/clis/twitter/post.js +14 -1
- package/clis/twitter/profile.js +1 -1
- package/clis/twitter/profile.test.js +8 -0
- package/clis/twitter/quote.js +11 -1
- package/clis/twitter/reply.js +11 -1
- package/dist/src/browser/base-page.d.ts +26 -0
- package/dist/src/browser/base-page.js +23 -8
- package/dist/src/browser/base-page.test.js +45 -12
- package/dist/src/browser/command-catalog.d.ts +6 -0
- package/dist/src/browser/command-catalog.js +259 -0
- package/dist/src/browser/command-catalog.test.d.ts +1 -0
- package/dist/src/browser/command-catalog.test.js +79 -0
- package/dist/src/browser/runtime/local-cloak/darwin-background-launch.d.ts +17 -0
- package/dist/src/browser/runtime/local-cloak/darwin-background-launch.js +106 -0
- package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.js +65 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +7 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.js +23 -9
- package/dist/src/browser/runtime/local-cloak/session-manager.test.js +36 -0
- package/dist/src/browser/target-resolver.d.ts +1 -0
- package/dist/src/browser/target-resolver.js +99 -4
- package/dist/src/browser/target-resolver.test.js +141 -1
- package/dist/src/build-manifest.d.ts +6 -0
- package/dist/src/build-manifest.js +18 -1
- package/dist/src/build-manifest.test.js +57 -1
- package/dist/src/builtin-command-surface.d.ts +9 -0
- package/dist/src/builtin-command-surface.js +16 -0
- package/dist/src/check-hosted-contract.test.d.ts +1 -0
- package/dist/src/check-hosted-contract.test.js +56 -0
- package/dist/src/cli.js +58 -106
- package/dist/src/cli.test.js +71 -1
- package/dist/src/command-presentation.d.ts +95 -0
- package/dist/src/command-presentation.js +486 -0
- package/dist/src/command-presentation.test.d.ts +1 -0
- package/dist/src/command-presentation.test.js +97 -0
- package/dist/src/command-surface.d.ts +43 -0
- package/dist/src/command-surface.js +205 -0
- package/dist/src/command-surface.test.d.ts +1 -0
- package/dist/src/command-surface.test.js +406 -0
- package/dist/src/commanderAdapter.d.ts +5 -1
- package/dist/src/commanderAdapter.js +12 -55
- package/dist/src/commanderAdapter.test.js +15 -3
- package/dist/src/community-plugin-sync.d.ts +11 -0
- package/dist/src/community-plugin-sync.js +138 -0
- package/dist/src/community-plugin-sync.test.d.ts +1 -0
- package/dist/src/community-plugin-sync.test.js +123 -0
- package/dist/src/completion-fast.d.ts +4 -4
- package/dist/src/completion-fast.js +15 -34
- package/dist/src/completion-shared.d.ts +4 -0
- package/dist/src/completion-shared.js +38 -0
- package/dist/src/completion.js +3 -27
- package/dist/src/completion.test.js +31 -2
- package/dist/src/docs-sync-review-cli.test.d.ts +1 -0
- package/dist/src/docs-sync-review-cli.test.js +322 -0
- package/dist/src/docs-sync-review.d.ts +117 -0
- package/dist/src/docs-sync-review.js +475 -0
- package/dist/src/docs-sync-review.test.d.ts +1 -0
- package/dist/src/docs-sync-review.test.js +324 -0
- package/dist/src/errors.d.ts +12 -3
- package/dist/src/errors.js +11 -7
- package/dist/src/errors.test.js +14 -1
- package/dist/src/execution.d.ts +1 -2
- package/dist/src/execution.js +4 -45
- package/dist/src/generate-release-notes-cli.test.js +7 -2
- package/dist/src/help.d.ts +4 -0
- package/dist/src/help.js +50 -255
- package/dist/src/help.test.js +27 -1
- package/dist/src/hosted/args.d.ts +2 -7
- package/dist/src/hosted/args.js +2 -99
- package/dist/src/hosted/args.test.js +15 -1
- package/dist/src/hosted/availability.d.ts +13 -0
- package/dist/src/hosted/availability.js +16 -0
- package/dist/src/hosted/availability.test.d.ts +1 -0
- package/dist/src/hosted/availability.test.js +180 -0
- package/dist/src/hosted/browser-args.d.ts +18 -0
- package/dist/src/hosted/browser-args.js +152 -0
- package/dist/src/hosted/browser-args.test.d.ts +1 -0
- package/dist/src/hosted/browser-args.test.js +182 -0
- package/dist/src/hosted/client.d.ts +31 -2
- package/dist/src/hosted/client.js +441 -53
- package/dist/src/hosted/client.test.js +651 -32
- package/dist/src/hosted/config.d.ts +7 -2
- package/dist/src/hosted/config.js +26 -4
- package/dist/src/hosted/config.test.js +34 -1
- package/dist/src/hosted/contract.d.ts +94 -0
- package/dist/src/hosted/contract.js +208 -0
- package/dist/src/hosted/contract.test.d.ts +1 -0
- package/dist/src/hosted/contract.test.js +361 -0
- package/dist/src/hosted/credentials.d.ts +38 -0
- package/dist/src/hosted/credentials.js +248 -0
- package/dist/src/hosted/credentials.test.d.ts +1 -0
- package/dist/src/hosted/credentials.test.js +93 -0
- package/dist/src/hosted/file-contract.test.d.ts +1 -0
- package/dist/src/hosted/file-contract.test.js +174 -0
- package/dist/src/hosted/files.d.ts +29 -0
- package/dist/src/hosted/files.js +296 -0
- package/dist/src/hosted/files.test.d.ts +1 -0
- package/dist/src/hosted/files.test.js +231 -0
- package/dist/src/hosted/main-lifecycle.test.d.ts +1 -0
- package/dist/src/hosted/main-lifecycle.test.js +213 -0
- package/dist/src/hosted/manifest.d.ts +5 -0
- package/dist/src/hosted/manifest.js +21 -66
- package/dist/src/hosted/manifest.test.js +120 -2
- package/dist/src/hosted/output-parity.test.d.ts +1 -0
- package/dist/src/hosted/output-parity.test.js +108 -0
- package/dist/src/hosted/root-command-surface.test.d.ts +1 -0
- package/dist/src/hosted/root-command-surface.test.js +646 -0
- package/dist/src/hosted/runner.d.ts +7 -0
- package/dist/src/hosted/runner.js +554 -253
- package/dist/src/hosted/runner.test.js +1471 -53
- package/dist/src/hosted/setup.d.ts +3 -2
- package/dist/src/hosted/setup.js +45 -15
- package/dist/src/hosted/setup.test.js +133 -6
- package/dist/src/hosted/types.d.ts +101 -23
- package/dist/src/main.js +120 -108
- package/dist/src/manifest-types.d.ts +2 -0
- package/dist/src/output.d.ts +17 -2
- package/dist/src/output.js +88 -81
- package/dist/src/output.test.js +141 -51
- package/dist/src/pipeline/executor.test.js +1 -0
- package/dist/src/pipeline/steps/download.test.js +1 -0
- package/dist/src/plugin-create-cli.test.d.ts +1 -0
- package/dist/src/plugin-create-cli.test.js +37 -0
- package/dist/src/plugin-manifest.d.ts +12 -0
- package/dist/src/plugin-manifest.js +16 -0
- package/dist/src/plugin-manifest.test.js +19 -1
- package/dist/src/plugin-scaffold.d.ts +4 -1
- package/dist/src/plugin-scaffold.js +4 -1
- package/dist/src/plugin-scaffold.test.js +23 -8
- package/dist/src/plugin.js +4 -1
- package/dist/src/plugin.test.js +13 -0
- package/dist/src/registry.d.ts +2 -0
- package/dist/src/release-notes.js +12 -4
- package/dist/src/release-notes.test.js +27 -15
- package/dist/src/root-command-surface.d.ts +31 -0
- package/dist/src/root-command-surface.js +106 -0
- package/dist/src/serialization.d.ts +1 -16
- package/dist/src/serialization.js +5 -55
- package/dist/src/stream-write.d.ts +12 -0
- package/dist/src/stream-write.js +91 -0
- package/dist/src/stream-write.test.d.ts +1 -0
- package/dist/src/stream-write.test.js +186 -0
- package/dist/src/types.d.ts +5 -0
- package/dist/src/utils.d.ts +1 -1
- package/dist/src/utils.js +2 -8
- package/dist/src/utils.test.js +50 -0
- package/hosted-contract.json +36748 -0
- package/package.json +6 -1
- package/scripts/check-hosted-contract.mjs +108 -0
- package/scripts/docs-sync-review.ts +332 -0
- package/scripts/sync-community-plugins.ts +14 -0
- package/skills/webcmd-autofix/SKILL.md +26 -37
- package/skills/webcmd-usage/SKILL.md +7 -0
|
@@ -20,8 +20,11 @@ describe('release notes helpers', () => {
|
|
|
20
20
|
it('detects major releases from semver tag ranges', () => {
|
|
21
21
|
expect(isMajorRelease({ tag: 'webcmd-v2.0.0', previousTag: 'webcmd-v1.9.9' })).toBe(true);
|
|
22
22
|
expect(isMajorRelease({ tag: 'v3.1.0', previousTag: 'v2.9.9' })).toBe(true);
|
|
23
|
+
expect(isMajorRelease({ tag: 'webcmd-v0.3.0', previousTag: 'webcmd-v0.2.5' })).toBe(true);
|
|
24
|
+
expect(isMajorRelease({ tag: 'webcmd-v0.1.0', previousTag: 'webcmd-v0.0.9' })).toBe(true);
|
|
23
25
|
expect(isMajorRelease({ tag: 'webcmd-v2.1.0', previousTag: 'webcmd-v2.0.0' })).toBe(false);
|
|
24
|
-
expect(isMajorRelease({ tag: 'webcmd-v0.3.
|
|
26
|
+
expect(isMajorRelease({ tag: 'webcmd-v0.3.1', previousTag: 'webcmd-v0.3.0' })).toBe(false);
|
|
27
|
+
expect(isMajorRelease({ tag: 'webcmd-v0.0.1', previousTag: 'webcmd-v0.0.0' })).toBe(false);
|
|
25
28
|
});
|
|
26
29
|
it('deduplicates PR author contributors and excludes service accounts', () => {
|
|
27
30
|
const prs = [
|
|
@@ -83,11 +86,19 @@ describe('release notes helpers', () => {
|
|
|
83
86
|
},
|
|
84
87
|
{
|
|
85
88
|
number: 43,
|
|
89
|
+
title: 'feat: improve release presentation',
|
|
90
|
+
author: { login: 'bob' },
|
|
91
|
+
labels: [{ name: 'feature' }],
|
|
92
|
+
files: [{ path: 'src/release-notes.ts' }],
|
|
93
|
+
url: 'https://github.com/agentrhq/webcmd/pull/43',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
number: 44,
|
|
86
97
|
title: 'chore: release automation',
|
|
87
98
|
author: { login: 'github-actions[bot]' },
|
|
88
99
|
labels: [],
|
|
89
100
|
files: [{ path: '.github/workflows/release.yml' }],
|
|
90
|
-
url: 'https://github.com/agentrhq/webcmd/pull/
|
|
101
|
+
url: 'https://github.com/agentrhq/webcmd/pull/44',
|
|
91
102
|
},
|
|
92
103
|
],
|
|
93
104
|
};
|
|
@@ -100,15 +111,16 @@ describe('release notes helpers', () => {
|
|
|
100
111
|
const normalized = normalizeReleaseNotes(raw, { context });
|
|
101
112
|
expect(normalized).toContain('# webcmd v2.0.0: The Command Surface Opens');
|
|
102
113
|
expect(normalized).toContain('## Contributors');
|
|
103
|
-
expect(normalized).toContain('<img src="https://github.com/alice.png?size=
|
|
114
|
+
expect(normalized).toContain('<img src="https://github.com/alice.png?size=40" width="40" height="40" alt="@alice" /></a> <a href="https://github.com/bob"');
|
|
104
115
|
expect(normalized).toContain('[@alice](https://github.com/alice)');
|
|
116
|
+
expect(normalized).toContain('[@alice](https://github.com/alice) | [@bob](https://github.com/bob)');
|
|
105
117
|
expect(normalized).not.toContain('github-actions');
|
|
106
118
|
});
|
|
107
119
|
it('uses a deterministic major release title fallback when the model omits one', () => {
|
|
108
120
|
const context = {
|
|
109
|
-
tag: 'webcmd-
|
|
110
|
-
previousTag: 'webcmd-
|
|
111
|
-
currentRef: 'webcmd-
|
|
121
|
+
tag: 'webcmd-v0.3.0',
|
|
122
|
+
previousTag: 'webcmd-v0.2.5',
|
|
123
|
+
currentRef: 'webcmd-v0.3.0',
|
|
112
124
|
pullRequests: [
|
|
113
125
|
{
|
|
114
126
|
number: 42,
|
|
@@ -121,7 +133,7 @@ describe('release notes helpers', () => {
|
|
|
121
133
|
],
|
|
122
134
|
};
|
|
123
135
|
const normalized = normalizeReleaseNotes('## Highlights\n- Added a GitHub adapter.', { context });
|
|
124
|
-
expect(normalized).toContain('# webcmd
|
|
136
|
+
expect(normalized).toContain('# webcmd v0.3.0: The Command Surface Expands');
|
|
125
137
|
});
|
|
126
138
|
it('does not add major release title treatment to minor releases', () => {
|
|
127
139
|
const context = {
|
|
@@ -172,7 +184,7 @@ describe('release notes helpers', () => {
|
|
|
172
184
|
'- Improved district checkout.',
|
|
173
185
|
'',
|
|
174
186
|
'## Contributors',
|
|
175
|
-
'<a href="https://github.com/alice" title="@alice"><img src="https://github.com/alice.png?size=
|
|
187
|
+
'<a href="https://github.com/alice" title="@alice"><img src="https://github.com/alice.png?size=40" width="40" height="40" alt="@alice" /></a>',
|
|
176
188
|
'',
|
|
177
189
|
'[@alice](https://github.com/alice)',
|
|
178
190
|
].join('\n');
|
|
@@ -189,8 +201,8 @@ describe('release notes helpers', () => {
|
|
|
189
201
|
});
|
|
190
202
|
it('builds a prompt grounded in the exact release range and PR list', () => {
|
|
191
203
|
const context = {
|
|
192
|
-
tag: 'v0.2.
|
|
193
|
-
previousTag: 'v0.
|
|
204
|
+
tag: 'v0.2.1',
|
|
205
|
+
previousTag: 'v0.2.0',
|
|
194
206
|
currentRef: 'abc123',
|
|
195
207
|
pullRequests: [
|
|
196
208
|
{
|
|
@@ -206,7 +218,7 @@ describe('release notes helpers', () => {
|
|
|
206
218
|
],
|
|
207
219
|
};
|
|
208
220
|
const prompt = buildReleaseNotesPrompt(context);
|
|
209
|
-
expect(prompt).toContain('v0.
|
|
221
|
+
expect(prompt).toContain('v0.2.0...abc123');
|
|
210
222
|
expect(prompt).toContain('PR #42: feat: add docs scaffold');
|
|
211
223
|
expect(prompt).toContain('docs/docs.json');
|
|
212
224
|
expect(prompt).toContain('## Highlights');
|
|
@@ -222,9 +234,9 @@ describe('release notes helpers', () => {
|
|
|
222
234
|
});
|
|
223
235
|
it('asks for a tasteful H1 title only when building major release notes', () => {
|
|
224
236
|
const context = {
|
|
225
|
-
tag: 'webcmd-
|
|
226
|
-
previousTag: 'webcmd-
|
|
227
|
-
currentRef: 'webcmd-
|
|
237
|
+
tag: 'webcmd-v0.3.0',
|
|
238
|
+
previousTag: 'webcmd-v0.2.5',
|
|
239
|
+
currentRef: 'webcmd-v0.3.0',
|
|
228
240
|
pullRequests: [
|
|
229
241
|
{
|
|
230
242
|
number: 42,
|
|
@@ -240,7 +252,7 @@ describe('release notes helpers', () => {
|
|
|
240
252
|
};
|
|
241
253
|
const prompt = buildReleaseNotesPrompt(context);
|
|
242
254
|
expect(prompt).toContain('This is a major release');
|
|
243
|
-
expect(prompt).toContain('# webcmd
|
|
255
|
+
expect(prompt).toContain('# webcmd v0.3.0: <Elegant Release Title>');
|
|
244
256
|
expect(prompt).toContain('grand enough to feel memorable, but polished rather than loud');
|
|
245
257
|
});
|
|
246
258
|
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
export declare const ROOT_PROFILE_FLAGS = "--profile <name>";
|
|
3
|
+
export declare const ROOT_PROFILE_DESCRIPTION = "Chrome profile/context alias for browser runtime commands";
|
|
4
|
+
export declare const COMPLETION_SENTINEL = "--get-completions";
|
|
5
|
+
/**
|
|
6
|
+
* Configure the structural root options shared by the local and hosted CLI.
|
|
7
|
+
* Keeping this in one place makes Commander the source of truth for attached
|
|
8
|
+
* values, short-option clusters, missing values, and help/version precedence.
|
|
9
|
+
*/
|
|
10
|
+
export declare function configureRootCommandSurface(program: Command): Command;
|
|
11
|
+
export type HostedRootCommandSurface = {
|
|
12
|
+
kind: 'help';
|
|
13
|
+
exitCode: number;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'version';
|
|
16
|
+
output: string;
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'completion';
|
|
19
|
+
argv: string[];
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'dispatch';
|
|
22
|
+
argv: string[];
|
|
23
|
+
profile?: string;
|
|
24
|
+
literal: boolean;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Parse only the root command surface without registering or discovering local
|
|
28
|
+
* commands. The completion/version checks reproduce the entry-point fast paths
|
|
29
|
+
* in main.ts; all remaining root grammar is delegated to Commander itself.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseHostedRootCommandSurface(argv: readonly string[]): HostedRootCommandSurface;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Command, CommanderError } from 'commander';
|
|
2
|
+
import { CommanderStructuralError } from './command-surface.js';
|
|
3
|
+
import { PKG_VERSION } from './version.js';
|
|
4
|
+
export const ROOT_PROFILE_FLAGS = '--profile <name>';
|
|
5
|
+
export const ROOT_PROFILE_DESCRIPTION = 'Chrome profile/context alias for browser runtime commands';
|
|
6
|
+
export const COMPLETION_SENTINEL = '--get-completions';
|
|
7
|
+
/**
|
|
8
|
+
* Configure the structural root options shared by the local and hosted CLI.
|
|
9
|
+
* Keeping this in one place makes Commander the source of truth for attached
|
|
10
|
+
* values, short-option clusters, missing values, and help/version precedence.
|
|
11
|
+
*/
|
|
12
|
+
export function configureRootCommandSurface(program) {
|
|
13
|
+
return program
|
|
14
|
+
.version(PKG_VERSION)
|
|
15
|
+
.option(ROOT_PROFILE_FLAGS, ROOT_PROFILE_DESCRIPTION)
|
|
16
|
+
.enablePositionalOptions();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parse only the root command surface without registering or discovering local
|
|
20
|
+
* commands. The completion/version checks reproduce the entry-point fast paths
|
|
21
|
+
* in main.ts; all remaining root grammar is delegated to Commander itself.
|
|
22
|
+
*/
|
|
23
|
+
export function parseHostedRootCommandSurface(argv) {
|
|
24
|
+
const input = [...argv];
|
|
25
|
+
// main.ts checks an exact first-token version before its completion scan.
|
|
26
|
+
if (input[0] === '--version' || input[0] === '-V') {
|
|
27
|
+
return { kind: 'version', output: `${PKG_VERSION}\n` };
|
|
28
|
+
}
|
|
29
|
+
// The local completion path scans the complete raw argv before discovery or
|
|
30
|
+
// Commander parsing, including after `--` and malformed root options.
|
|
31
|
+
if (input.includes(COMPLETION_SENTINEL)) {
|
|
32
|
+
return { kind: 'completion', argv: input };
|
|
33
|
+
}
|
|
34
|
+
let stdout = '';
|
|
35
|
+
let stderr = '';
|
|
36
|
+
const boundary = findRootCommandBoundary(input);
|
|
37
|
+
const root = configureRootCommandSurface(new Command('webcmd'))
|
|
38
|
+
.exitOverride()
|
|
39
|
+
.configureOutput({
|
|
40
|
+
writeOut: value => { stdout += value; },
|
|
41
|
+
writeErr: value => { stderr += value; },
|
|
42
|
+
});
|
|
43
|
+
if (boundary.commandIndex !== undefined) {
|
|
44
|
+
// Register only the token Commander needs in order to stop root option
|
|
45
|
+
// parsing. This is structural scaffolding, not adapter discovery.
|
|
46
|
+
root.command(input[boundary.commandIndex])
|
|
47
|
+
.helpOption(false)
|
|
48
|
+
.allowUnknownOption(true)
|
|
49
|
+
.allowExcessArguments(true)
|
|
50
|
+
.argument('[commandArgs...]')
|
|
51
|
+
.exitOverride()
|
|
52
|
+
.configureOutput({ writeOut: () => undefined, writeErr: () => undefined });
|
|
53
|
+
}
|
|
54
|
+
const structuralArgv = boundary.commandIndex !== undefined
|
|
55
|
+
? input
|
|
56
|
+
: boundary.separatorIndex !== undefined
|
|
57
|
+
? input.slice(0, boundary.separatorIndex + 1)
|
|
58
|
+
: input;
|
|
59
|
+
try {
|
|
60
|
+
root.parse(structuralArgv, { from: 'user' });
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (!(error instanceof CommanderError))
|
|
64
|
+
throw error;
|
|
65
|
+
if (error.code === 'commander.helpDisplayed')
|
|
66
|
+
return { kind: 'help', exitCode: error.exitCode };
|
|
67
|
+
if (error.code === 'commander.version') {
|
|
68
|
+
return { kind: 'version', output: stdout || `${PKG_VERSION}\n` };
|
|
69
|
+
}
|
|
70
|
+
throw new CommanderStructuralError(stderr || `${error.message}\n`, error.exitCode);
|
|
71
|
+
}
|
|
72
|
+
const profile = root.opts().profile;
|
|
73
|
+
if (boundary.commandIndex === undefined && boundary.separatorIndex === undefined)
|
|
74
|
+
return { kind: 'help', exitCode: 1 };
|
|
75
|
+
const literal = boundary.separatorIndex !== undefined;
|
|
76
|
+
const parsedArgv = boundary.commandIndex !== undefined
|
|
77
|
+
? input.slice(boundary.commandIndex)
|
|
78
|
+
: input.slice(boundary.separatorIndex + 1);
|
|
79
|
+
if (parsedArgv.length === 0)
|
|
80
|
+
return { kind: 'help', exitCode: 1 };
|
|
81
|
+
return {
|
|
82
|
+
kind: 'dispatch',
|
|
83
|
+
argv: parsedArgv,
|
|
84
|
+
...(profile !== undefined ? { profile } : {}),
|
|
85
|
+
literal,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Locates the undiscovered command token while respecting root value options. */
|
|
89
|
+
function findRootCommandBoundary(argv) {
|
|
90
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
91
|
+
const token = argv[index];
|
|
92
|
+
if (token === '--profile') {
|
|
93
|
+
// Commander requires and consumes the next token even when it is `--` or
|
|
94
|
+
// starts with a dash. Structural failures have already been reported.
|
|
95
|
+
index += 1;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (token.startsWith('--profile='))
|
|
99
|
+
continue;
|
|
100
|
+
if (token === '--')
|
|
101
|
+
return { separatorIndex: index };
|
|
102
|
+
if (!token.startsWith('-') || token === '-')
|
|
103
|
+
return { commandIndex: index };
|
|
104
|
+
}
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
@@ -18,22 +18,7 @@ export type SerializedArg = {
|
|
|
18
18
|
/** Stable arg schema — every field is always present (no sparse objects). */
|
|
19
19
|
export declare function serializeArg(a: Arg): SerializedArg;
|
|
20
20
|
/** Full command metadata for structured output (json/yaml). */
|
|
21
|
-
export declare function serializeCommand(cmd: CliCommand):
|
|
22
|
-
command: string;
|
|
23
|
-
site: string;
|
|
24
|
-
name: string;
|
|
25
|
-
aliases: string[];
|
|
26
|
-
description: string;
|
|
27
|
-
access: import("./registry.js").CommandAccess;
|
|
28
|
-
strategy: string;
|
|
29
|
-
browser: boolean;
|
|
30
|
-
args: SerializedArg[];
|
|
31
|
-
columns: string[];
|
|
32
|
-
domain: string | null;
|
|
33
|
-
example: string;
|
|
34
|
-
defaultFormat: "table" | "plain" | "json" | "yaml" | "yml" | "md" | "markdown" | "csv" | null;
|
|
35
|
-
siteSession: import("./registry.js").SiteSessionMode | null;
|
|
36
|
-
};
|
|
21
|
+
export declare function serializeCommand(cmd: CliCommand): Record<string, unknown>;
|
|
37
22
|
/** Human-readable arg summary: `<required> [optional]` style. */
|
|
38
23
|
export declare function formatArgSummary(args: Arg[]): string;
|
|
39
24
|
/** Agent-facing canonical invocation. Adapter authors may override with `example`. */
|
|
@@ -4,78 +4,28 @@
|
|
|
4
4
|
* Used by the `list` command, Commander --help, and build-manifest.
|
|
5
5
|
* Separated from registry.ts to keep the registry focused on types + registration.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import { CLI_COMMAND } from './brand.js';
|
|
7
|
+
import { commandListRows, formatArgumentSummary, formatPresentableCommandExample, serializePresentableArg, toPresentableCommand, } from './command-presentation.js';
|
|
9
8
|
/** Stable arg schema — every field is always present (no sparse objects). */
|
|
10
9
|
export function serializeArg(a) {
|
|
11
|
-
return
|
|
12
|
-
name: a.name,
|
|
13
|
-
type: a.type ?? 'string',
|
|
14
|
-
required: !!a.required,
|
|
15
|
-
valueRequired: !!a.valueRequired,
|
|
16
|
-
positional: !!a.positional,
|
|
17
|
-
choices: a.choices ?? [],
|
|
18
|
-
default: a.default ?? null,
|
|
19
|
-
help: a.help ?? '',
|
|
20
|
-
};
|
|
10
|
+
return serializePresentableArg(a);
|
|
21
11
|
}
|
|
22
12
|
/** Full command metadata for structured output (json/yaml). */
|
|
23
13
|
export function serializeCommand(cmd) {
|
|
24
|
-
return
|
|
25
|
-
command: fullName(cmd),
|
|
26
|
-
site: cmd.site,
|
|
27
|
-
name: cmd.name,
|
|
28
|
-
aliases: cmd.aliases ?? [],
|
|
29
|
-
description: cmd.description,
|
|
30
|
-
access: cmd.access,
|
|
31
|
-
strategy: strategyLabel(cmd),
|
|
32
|
-
browser: !!cmd.browser,
|
|
33
|
-
args: cmd.args.map(serializeArg),
|
|
34
|
-
columns: cmd.columns ?? [],
|
|
35
|
-
domain: cmd.domain ?? null,
|
|
36
|
-
example: formatCommandExample(cmd),
|
|
37
|
-
defaultFormat: cmd.defaultFormat ?? null,
|
|
38
|
-
siteSession: cmd.siteSession ?? null,
|
|
39
|
-
};
|
|
14
|
+
return commandListRows([toPresentableCommand(cmd)], true)[0];
|
|
40
15
|
}
|
|
41
16
|
// ── Formatting ──────────────────────────────────────────────────────────────
|
|
42
17
|
/** Human-readable arg summary: `<required> [optional]` style. */
|
|
43
18
|
export function formatArgSummary(args) {
|
|
44
|
-
return args
|
|
45
|
-
.map(a => {
|
|
46
|
-
if (a.positional)
|
|
47
|
-
return a.required ? `<${a.name}>` : `[${a.name}]`;
|
|
48
|
-
return a.required ? `--${a.name}` : `[--${a.name}]`;
|
|
49
|
-
})
|
|
50
|
-
.join(' ');
|
|
19
|
+
return formatArgumentSummary(args);
|
|
51
20
|
}
|
|
52
21
|
function summarizeChoices(choices) {
|
|
53
22
|
if (choices.length <= 4)
|
|
54
23
|
return choices.join(', ');
|
|
55
24
|
return `${choices.slice(0, 4).join(', ')}, ... (+${choices.length - 4} more)`;
|
|
56
25
|
}
|
|
57
|
-
function formatValuePlaceholder(name) {
|
|
58
|
-
return `<${name}>`;
|
|
59
|
-
}
|
|
60
26
|
/** Agent-facing canonical invocation. Adapter authors may override with `example`. */
|
|
61
27
|
export function formatCommandExample(cmd) {
|
|
62
|
-
|
|
63
|
-
return cmd.example.trim();
|
|
64
|
-
const parts = [CLI_COMMAND, cmd.site, cmd.name];
|
|
65
|
-
for (const arg of cmd.args) {
|
|
66
|
-
if (arg.positional && arg.required) {
|
|
67
|
-
parts.push(formatValuePlaceholder(arg.name));
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
for (const arg of cmd.args) {
|
|
71
|
-
if (arg.positional || !arg.required)
|
|
72
|
-
continue;
|
|
73
|
-
parts.push(`--${arg.name}`);
|
|
74
|
-
if (arg.type !== 'bool' && arg.type !== 'boolean')
|
|
75
|
-
parts.push(formatValuePlaceholder(arg.name));
|
|
76
|
-
}
|
|
77
|
-
parts.push('-f', 'yaml');
|
|
78
|
-
return parts.join(' ');
|
|
28
|
+
return formatPresentableCommandExample(toPresentableCommand(cmd));
|
|
79
29
|
}
|
|
80
30
|
/** Generate the --help appendix showing registry metadata not exposed by Commander. */
|
|
81
31
|
export function formatRegistryHelpText(cmd) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** A write failure that must not be mistaken for a command/runtime failure. */
|
|
2
|
+
export declare class StreamWriteError extends Error {
|
|
3
|
+
readonly cause: unknown;
|
|
4
|
+
constructor(cause: unknown);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Write one complete chunk without taking ownership of the stream.
|
|
8
|
+
*
|
|
9
|
+
* Resolution requires both the write callback and, when write() reports
|
|
10
|
+
* backpressure, the matching drain event. The stream is never ended.
|
|
11
|
+
*/
|
|
12
|
+
export declare function writeToStream(stream: NodeJS.WritableStream, chunk: string | Uint8Array): Promise<void>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** A write failure that must not be mistaken for a command/runtime failure. */
|
|
2
|
+
export class StreamWriteError extends Error {
|
|
3
|
+
cause;
|
|
4
|
+
constructor(cause) {
|
|
5
|
+
super(cause instanceof Error ? cause.message : String(cause));
|
|
6
|
+
this.name = 'StreamWriteError';
|
|
7
|
+
this.cause = cause;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Write one complete chunk without taking ownership of the stream.
|
|
12
|
+
*
|
|
13
|
+
* Resolution requires both the write callback and, when write() reports
|
|
14
|
+
* backpressure, the matching drain event. The stream is never ended.
|
|
15
|
+
*/
|
|
16
|
+
export function writeToStream(stream, chunk) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const prematureCloseError = new Error('Writable stream closed before the write completed');
|
|
19
|
+
const streamState = stream;
|
|
20
|
+
if (streamState.closed || streamState.destroyed) {
|
|
21
|
+
reject(new StreamWriteError(prematureCloseError));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
let writeReturned = false;
|
|
25
|
+
let callbackComplete = false;
|
|
26
|
+
let requiresDrain = false;
|
|
27
|
+
let drainSeen = false;
|
|
28
|
+
let settled = false;
|
|
29
|
+
let pendingCallbackError;
|
|
30
|
+
let pendingCallbackFailure;
|
|
31
|
+
const cleanup = () => {
|
|
32
|
+
stream.removeListener('error', onError);
|
|
33
|
+
stream.removeListener('drain', onDrain);
|
|
34
|
+
stream.removeListener('close', onClose);
|
|
35
|
+
if (pendingCallbackFailure) {
|
|
36
|
+
clearImmediate(pendingCallbackFailure);
|
|
37
|
+
pendingCallbackFailure = undefined;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const fail = (error) => {
|
|
41
|
+
if (settled)
|
|
42
|
+
return;
|
|
43
|
+
settled = true;
|
|
44
|
+
cleanup();
|
|
45
|
+
reject(error instanceof StreamWriteError ? error : new StreamWriteError(error));
|
|
46
|
+
};
|
|
47
|
+
const failFromCallback = (error) => {
|
|
48
|
+
if (settled)
|
|
49
|
+
return;
|
|
50
|
+
pendingCallbackError ??= error;
|
|
51
|
+
// A real Node Writable reports an _write callback failure to the public
|
|
52
|
+
// write callback and then emits `error`. Keep the error listener until
|
|
53
|
+
// that paired event has had a chance to arrive, while still bounding
|
|
54
|
+
// non-standard streams that only invoke the callback.
|
|
55
|
+
pendingCallbackFailure ??= setImmediate(() => fail(pendingCallbackError));
|
|
56
|
+
};
|
|
57
|
+
const finish = () => {
|
|
58
|
+
if (settled || !writeReturned || !callbackComplete || (requiresDrain && !drainSeen))
|
|
59
|
+
return;
|
|
60
|
+
settled = true;
|
|
61
|
+
cleanup();
|
|
62
|
+
resolve();
|
|
63
|
+
};
|
|
64
|
+
const onError = (error) => fail(error);
|
|
65
|
+
const onClose = () => fail(pendingCallbackError ?? prematureCloseError);
|
|
66
|
+
const onDrain = () => {
|
|
67
|
+
drainSeen = true;
|
|
68
|
+
finish();
|
|
69
|
+
};
|
|
70
|
+
stream.once('error', onError);
|
|
71
|
+
stream.once('drain', onDrain);
|
|
72
|
+
stream.once('close', onClose);
|
|
73
|
+
try {
|
|
74
|
+
requiresDrain = stream.write(chunk, (error) => {
|
|
75
|
+
if (error) {
|
|
76
|
+
failFromCallback(error);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
callbackComplete = true;
|
|
80
|
+
finish();
|
|
81
|
+
}) === false;
|
|
82
|
+
writeReturned = true;
|
|
83
|
+
if (!requiresDrain)
|
|
84
|
+
stream.removeListener('drain', onDrain);
|
|
85
|
+
finish();
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
fail(error);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { Writable } from 'node:stream';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { writeToStream } from './stream-write.js';
|
|
5
|
+
class ManualWritable extends Writable {
|
|
6
|
+
chunks = [];
|
|
7
|
+
callbacks = [];
|
|
8
|
+
constructor() {
|
|
9
|
+
super({ highWaterMark: 1 });
|
|
10
|
+
}
|
|
11
|
+
_write(chunk, _encoding, callback) {
|
|
12
|
+
this.chunks.push(Buffer.from(chunk));
|
|
13
|
+
this.callbacks.push(callback);
|
|
14
|
+
}
|
|
15
|
+
release(error) {
|
|
16
|
+
const callback = this.callbacks.shift();
|
|
17
|
+
if (!callback)
|
|
18
|
+
throw new Error('No manual write callback is pending');
|
|
19
|
+
callback(error);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
class SyntheticBackpressureStream extends EventEmitter {
|
|
23
|
+
callback;
|
|
24
|
+
end = vi.fn();
|
|
25
|
+
write(_chunk, callback) {
|
|
26
|
+
this.callback = callback;
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
class SyntheticBackpressureWritable extends Writable {
|
|
31
|
+
callback;
|
|
32
|
+
write(_chunk, encodingOrCallback, callback) {
|
|
33
|
+
this.callback = typeof encodingOrCallback === 'function' ? encodingOrCallback : callback;
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
_write(_chunk, _encoding, callback) {
|
|
37
|
+
callback();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function within(promise, milliseconds = 500) {
|
|
41
|
+
let timer;
|
|
42
|
+
try {
|
|
43
|
+
return await Promise.race([
|
|
44
|
+
promise,
|
|
45
|
+
new Promise((_resolve, reject) => {
|
|
46
|
+
timer = setTimeout(() => reject(new Error(`promise did not settle within ${milliseconds}ms`)), milliseconds);
|
|
47
|
+
}),
|
|
48
|
+
]);
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
if (timer)
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
describe('writeToStream', () => {
|
|
56
|
+
it('waits for both the write callback and backpressure drain', async () => {
|
|
57
|
+
const stream = new SyntheticBackpressureStream();
|
|
58
|
+
let settled = false;
|
|
59
|
+
const write = writeToStream(stream, 'payload').then(() => {
|
|
60
|
+
settled = true;
|
|
61
|
+
});
|
|
62
|
+
stream.callback?.();
|
|
63
|
+
await Promise.resolve();
|
|
64
|
+
expect(settled).toBe(false);
|
|
65
|
+
stream.emit('drain');
|
|
66
|
+
await write;
|
|
67
|
+
expect(settled).toBe(true);
|
|
68
|
+
expect(stream.end).not.toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
it('preserves ordering across multiple awaited writes', async () => {
|
|
71
|
+
const stream = new ManualWritable();
|
|
72
|
+
let settled = false;
|
|
73
|
+
const writes = (async () => {
|
|
74
|
+
await writeToStream(stream, 'first');
|
|
75
|
+
await writeToStream(stream, 'second');
|
|
76
|
+
settled = true;
|
|
77
|
+
})();
|
|
78
|
+
expect(stream.callbacks).toHaveLength(1);
|
|
79
|
+
stream.release();
|
|
80
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
81
|
+
expect(stream.callbacks).toHaveLength(1);
|
|
82
|
+
expect(Buffer.concat(stream.chunks).toString('utf8')).toBe('firstsecond');
|
|
83
|
+
expect(settled).toBe(false);
|
|
84
|
+
stream.release();
|
|
85
|
+
await writes;
|
|
86
|
+
expect(settled).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
it('writes and drains a value larger than one MiB', async () => {
|
|
89
|
+
const stream = new ManualWritable();
|
|
90
|
+
const payload = Buffer.alloc(1024 * 1024 + 17, 0x61);
|
|
91
|
+
const write = writeToStream(stream, payload);
|
|
92
|
+
expect(stream.chunks[0]).toHaveLength(payload.length);
|
|
93
|
+
stream.release();
|
|
94
|
+
await write;
|
|
95
|
+
expect(Buffer.concat(stream.chunks).equals(payload)).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
it('rejects an error emitted before drain without hanging', async () => {
|
|
98
|
+
const stream = new SyntheticBackpressureStream();
|
|
99
|
+
const write = writeToStream(stream, 'payload');
|
|
100
|
+
stream.emit('error', new Error('stream failed before drain'));
|
|
101
|
+
await expect(write).rejects.toThrow('stream failed before drain');
|
|
102
|
+
});
|
|
103
|
+
it('rejects a real Writable callback error without an uncaught error event', async () => {
|
|
104
|
+
const stream = new ManualWritable();
|
|
105
|
+
const write = writeToStream(stream, 'payload');
|
|
106
|
+
stream.release(new Error('real writable callback failed'));
|
|
107
|
+
await expect(write).rejects.toThrow('real writable callback failed');
|
|
108
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
109
|
+
});
|
|
110
|
+
it('rejects a callback error delivered after drain without double resolution', async () => {
|
|
111
|
+
const stream = new SyntheticBackpressureStream();
|
|
112
|
+
const write = writeToStream(stream, 'payload');
|
|
113
|
+
stream.emit('drain');
|
|
114
|
+
stream.callback?.(new Error('stream failed after drain'));
|
|
115
|
+
await expect(write).rejects.toThrow('stream failed after drain');
|
|
116
|
+
});
|
|
117
|
+
it('rejects when a real Writable is destroyed before its write callback', async () => {
|
|
118
|
+
const stream = new ManualWritable();
|
|
119
|
+
const write = writeToStream(stream, 'payload');
|
|
120
|
+
stream.destroy();
|
|
121
|
+
await expect(within(write)).rejects.toThrow('closed before the write completed');
|
|
122
|
+
expect(stream.listenerCount('close')).toBe(0);
|
|
123
|
+
expect(stream.listenerCount('drain')).toBe(0);
|
|
124
|
+
expect(stream.listenerCount('error')).toBe(0);
|
|
125
|
+
});
|
|
126
|
+
it('rejects when close occurs after callback but before required drain', async () => {
|
|
127
|
+
const stream = new SyntheticBackpressureWritable();
|
|
128
|
+
const write = writeToStream(stream, 'payload');
|
|
129
|
+
stream.callback?.();
|
|
130
|
+
stream.destroy();
|
|
131
|
+
await expect(within(write)).rejects.toThrow('closed before the write completed');
|
|
132
|
+
expect(stream.listenerCount('close')).toBe(0);
|
|
133
|
+
expect(stream.listenerCount('drain')).toBe(0);
|
|
134
|
+
expect(stream.listenerCount('error')).toBe(0);
|
|
135
|
+
});
|
|
136
|
+
it('rejects an already-destroyed Writable without attempting a write', async () => {
|
|
137
|
+
const stream = new ManualWritable();
|
|
138
|
+
stream.destroy();
|
|
139
|
+
await new Promise(resolve => stream.once('close', resolve));
|
|
140
|
+
await expect(within(writeToStream(stream, 'payload'))).rejects.toThrow('closed before the write completed');
|
|
141
|
+
expect(stream.chunks).toHaveLength(0);
|
|
142
|
+
expect(stream.listenerCount('close')).toBe(0);
|
|
143
|
+
expect(stream.listenerCount('drain')).toBe(0);
|
|
144
|
+
expect(stream.listenerCount('error')).toBe(0);
|
|
145
|
+
});
|
|
146
|
+
it('ignores a late callback after close has already rejected the write', async () => {
|
|
147
|
+
const stream = new ManualWritable();
|
|
148
|
+
const write = writeToStream(stream, 'payload');
|
|
149
|
+
stream.destroy();
|
|
150
|
+
await expect(within(write)).rejects.toThrow('closed before the write completed');
|
|
151
|
+
expect(() => {
|
|
152
|
+
stream.emit('drain');
|
|
153
|
+
stream.release(new Error('late callback failure'));
|
|
154
|
+
}).not.toThrow();
|
|
155
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
156
|
+
expect(stream.listenerCount('close')).toBe(0);
|
|
157
|
+
expect(stream.listenerCount('drain')).toBe(0);
|
|
158
|
+
expect(stream.listenerCount('error')).toBe(0);
|
|
159
|
+
});
|
|
160
|
+
it('settles once with the original error when error and close race', async () => {
|
|
161
|
+
const stream = new ManualWritable();
|
|
162
|
+
const write = writeToStream(stream, 'payload');
|
|
163
|
+
stream.destroy(new Error('destroy race failed'));
|
|
164
|
+
await expect(within(write)).rejects.toThrow('destroy race failed');
|
|
165
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
166
|
+
expect(stream.listenerCount('close')).toBe(0);
|
|
167
|
+
expect(stream.listenerCount('drain')).toBe(0);
|
|
168
|
+
expect(stream.listenerCount('error')).toBe(0);
|
|
169
|
+
});
|
|
170
|
+
it('does not let close after success change the resolved result', async () => {
|
|
171
|
+
const stream = new SyntheticBackpressureWritable();
|
|
172
|
+
const end = vi.spyOn(stream, 'end');
|
|
173
|
+
const destroy = vi.spyOn(stream, 'destroy');
|
|
174
|
+
const write = writeToStream(stream, 'payload');
|
|
175
|
+
stream.emit('drain');
|
|
176
|
+
stream.callback?.();
|
|
177
|
+
await expect(within(write)).resolves.toBeUndefined();
|
|
178
|
+
expect(end).not.toHaveBeenCalled();
|
|
179
|
+
expect(destroy).not.toHaveBeenCalled();
|
|
180
|
+
stream.destroy();
|
|
181
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
182
|
+
expect(stream.listenerCount('close')).toBe(0);
|
|
183
|
+
expect(stream.listenerCount('drain')).toBe(0);
|
|
184
|
+
expect(stream.listenerCount('error')).toBe(0);
|
|
185
|
+
});
|
|
186
|
+
});
|
package/dist/src/types.d.ts
CHANGED
|
@@ -87,6 +87,9 @@ export interface IPage {
|
|
|
87
87
|
}): Promise<{
|
|
88
88
|
matches_n: number;
|
|
89
89
|
match_level: 'exact' | 'stable' | 'reidentified';
|
|
90
|
+
click_method?: 'cdp' | 'js' | 'ax';
|
|
91
|
+
hit?: 'target' | 'ancestor' | 'other' | 'none';
|
|
92
|
+
retargeted?: boolean;
|
|
90
93
|
}>;
|
|
91
94
|
dblClick?(ref: string, opts?: {
|
|
92
95
|
nth?: number;
|
|
@@ -178,6 +181,8 @@ export interface IPage {
|
|
|
178
181
|
}): Promise<any>;
|
|
179
182
|
getFormState(): Promise<any>;
|
|
180
183
|
wait(options: number | WaitOptions): Promise<void>;
|
|
184
|
+
/** Pure client-side sleep (bare setTimeout, no page evaluation). */
|
|
185
|
+
sleep(seconds: number): Promise<void>;
|
|
181
186
|
waitForDownload?(pattern?: string, timeoutMs?: number): Promise<BrowserDownloadWaitResult>;
|
|
182
187
|
tabs(): Promise<any>;
|
|
183
188
|
closeTab?(target?: number | string): Promise<void>;
|