@agentrhq/webcmd 0.2.5 → 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/_shared/site-auth.js +3 -3
- package/clis/_shared/site-auth.test.js +4 -4
- 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/slock/whoami.test.js +2 -2
- 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/daemon-client.d.ts +2 -1
- package/dist/src/browser/runtime/local-cloak/actions.js +4 -1
- 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/provider.test.js +36 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +10 -2
- package/dist/src/browser/runtime/local-cloak/session-manager.js +25 -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 +63 -110
- package/dist/src/cli.test.js +82 -6
- 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/commands/auth.js +3 -2
- package/dist/src/commands/auth.test.js +6 -6
- 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 +11 -1
- 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 +4 -0
- package/dist/src/hosted/args.js +4 -0
- package/dist/src/hosted/args.test.d.ts +1 -0
- package/dist/src/hosted/args.test.js +49 -0
- 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 +59 -0
- package/dist/src/hosted/client.js +510 -0
- package/dist/src/hosted/client.test.d.ts +1 -0
- package/dist/src/hosted/client.test.js +738 -0
- package/dist/src/hosted/config.d.ts +55 -0
- package/dist/src/hosted/config.js +112 -0
- package/dist/src/hosted/config.test.d.ts +1 -0
- package/dist/src/hosted/config.test.js +81 -0
- 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 +14 -0
- package/dist/src/hosted/manifest.js +47 -0
- package/dist/src/hosted/manifest.test.d.ts +1 -0
- package/dist/src/hosted/manifest.test.js +164 -0
- 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 +19 -0
- package/dist/src/hosted/runner.js +705 -0
- package/dist/src/hosted/runner.test.d.ts +1 -0
- package/dist/src/hosted/runner.test.js +1607 -0
- package/dist/src/hosted/setup.d.ts +10 -0
- package/dist/src/hosted/setup.js +79 -0
- package/dist/src/hosted/setup.test.d.ts +1 -0
- package/dist/src/hosted/setup.test.js +195 -0
- package/dist/src/hosted/types.d.ts +175 -0
- package/dist/src/hosted/types.js +1 -0
- package/dist/src/main.js +123 -97
- 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.d.ts +6 -1
- package/dist/src/release-notes.js +192 -4
- package/dist/src/release-notes.test.js +143 -4
- 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/generate-release-notes.ts +1 -1
- package/scripts/sync-community-plugins.ts +14 -0
- package/skills/smart-search/SKILL.md +14 -3
- package/skills/webcmd-autofix/SKILL.md +26 -37
- package/skills/webcmd-usage/SKILL.md +21 -7
- package/clis/antigravity/SKILL.md +0 -38
|
@@ -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>;
|
package/dist/src/utils.d.ts
CHANGED
|
@@ -54,4 +54,4 @@ export declare function parseJsonOrThrowLoginWall(response: Response, opts?: {
|
|
|
54
54
|
* return res;
|
|
55
55
|
*
|
|
56
56
|
* The Node side then calls `throwIfLoginWall(res, { url })` on the result. */
|
|
57
|
-
export declare const BROWSER_JSON_SNIFF_FN = "\nasync function fetchJsonOrLoginWall(input, init) {\n const r = await fetch(input, init);\n const contentType = r.headers.get('content-type') || '';\n const text = await r.text();\n const trimmed = text.replace(/^\\s+/, '');\n const looksLikeHtml =\n contentType.toLowerCase().includes('text/html')\n ||
|
|
57
|
+
export declare const BROWSER_JSON_SNIFF_FN = "\nasync function fetchJsonOrLoginWall(input, init) {\n const r = await fetch(input, init);\n const contentType = r.headers.get('content-type') || '';\n const text = await r.text();\n const trimmed = text.replace(/^\\s+/, '');\n const looksLikeHtml =\n contentType.toLowerCase().includes('text/html')\n || /^<(?:!doctype|html|head|body|title)(?:[\\s>/]|$)/i.test(trimmed);\n if (looksLikeHtml) {\n return {\n __loginWall: true,\n status: r.status,\n url: r.url || (typeof input === 'string' ? input : ''),\n contentType,\n bodyPreview: trimmed.slice(0, 100),\n };\n }\n if (!r.ok) {\n return { error: r.status };\n }\n try {\n return JSON.parse(text);\n } catch (err) {\n throw new Error(\n 'JSON parse failed (status=' + r.status + ', body[0..50]=' + JSON.stringify(trimmed.slice(0, 50)) + '): '\n + (err && err.message ? err.message : String(err))\n );\n }\n}\n";
|
package/dist/src/utils.js
CHANGED
|
@@ -85,10 +85,7 @@ export async function parseJsonOrThrowLoginWall(response, opts = {}) {
|
|
|
85
85
|
const text = await response.text();
|
|
86
86
|
const trimmed = text.trimStart();
|
|
87
87
|
const looksLikeHtml = contentType.toLowerCase().includes('text/html')
|
|
88
|
-
||
|
|
89
|
-
|| trimmed.startsWith('<!doctype')
|
|
90
|
-
|| trimmed.startsWith('<html')
|
|
91
|
-
|| trimmed.startsWith('<HTML');
|
|
88
|
+
|| /^<(?:!doctype|html|head|body|title)(?:[\s>/]|$)/i.test(trimmed);
|
|
92
89
|
if (looksLikeHtml) {
|
|
93
90
|
throw new LoginWallError(`Server returned HTML instead of JSON (status=${response.status}). `
|
|
94
91
|
+ `Likely a login wall, rate limit, or WAF challenge.`, response.status, opts.url || response.url || '', trimmed.slice(0, 100));
|
|
@@ -124,10 +121,7 @@ async function fetchJsonOrLoginWall(input, init) {
|
|
|
124
121
|
const trimmed = text.replace(/^\\s+/, '');
|
|
125
122
|
const looksLikeHtml =
|
|
126
123
|
contentType.toLowerCase().includes('text/html')
|
|
127
|
-
||
|
|
128
|
-
|| trimmed.startsWith('<!doctype')
|
|
129
|
-
|| trimmed.startsWith('<html')
|
|
130
|
-
|| trimmed.startsWith('<HTML');
|
|
124
|
+
|| /^<(?:!doctype|html|head|body|title)(?:[\\s>/]|$)/i.test(trimmed);
|
|
131
125
|
if (looksLikeHtml) {
|
|
132
126
|
return {
|
|
133
127
|
__loginWall: true,
|
package/dist/src/utils.test.js
CHANGED
|
@@ -51,6 +51,29 @@ describe('parseJsonOrThrowLoginWall', () => {
|
|
|
51
51
|
});
|
|
52
52
|
await expect(parseJsonOrThrowLoginWall(res)).rejects.toBeInstanceOf(LoginWallError);
|
|
53
53
|
});
|
|
54
|
+
it('detects mixed-case HTML tags when content-type claims JSON', async () => {
|
|
55
|
+
const res = new Response('<HtMl lang="en"><body>nope</body></HtMl>', {
|
|
56
|
+
status: 403,
|
|
57
|
+
headers: { 'content-type': 'application/json' },
|
|
58
|
+
});
|
|
59
|
+
await expect(parseJsonOrThrowLoginWall(res)).rejects.toBeInstanceOf(LoginWallError);
|
|
60
|
+
});
|
|
61
|
+
it('detects mixed-case HTML fragments without a top-level html tag', async () => {
|
|
62
|
+
for (const html of ['<BoDy>blocked</BoDy>', '<TiTlE>login</TiTlE>']) {
|
|
63
|
+
const res = new Response(html, {
|
|
64
|
+
status: 403,
|
|
65
|
+
headers: { 'content-type': 'application/json' },
|
|
66
|
+
});
|
|
67
|
+
await expect(parseJsonOrThrowLoginWall(res)).rejects.toBeInstanceOf(LoginWallError);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
it('does not treat arbitrary angle-prefixed non-HTML text as a login wall', async () => {
|
|
71
|
+
const res = new Response('<htmlish', {
|
|
72
|
+
status: 200,
|
|
73
|
+
headers: { 'content-type': 'application/json' },
|
|
74
|
+
});
|
|
75
|
+
await expect(parseJsonOrThrowLoginWall(res)).rejects.not.toBeInstanceOf(LoginWallError);
|
|
76
|
+
});
|
|
54
77
|
it('throws LoginWallError when body has leading whitespace before <!DOCTYPE', async () => {
|
|
55
78
|
const res = new Response(' \n\n<!DOCTYPE html><html></html>', {
|
|
56
79
|
status: 200,
|
|
@@ -152,4 +175,31 @@ describe('BROWSER_JSON_SNIFF_FN', () => {
|
|
|
152
175
|
// we CAN confirm the fragment parses cleanly when embedded inside an async IIFE.
|
|
153
176
|
expect(() => new Function(`(async () => { ${BROWSER_JSON_SNIFF_FN} })`)).not.toThrow();
|
|
154
177
|
});
|
|
178
|
+
it('detects mixed-case HTML tags in browser-side responses', async () => {
|
|
179
|
+
const fetchJsonOrLoginWall = new Function('fetch', `${BROWSER_JSON_SNIFF_FN}; return fetchJsonOrLoginWall;`)(async () => new Response('<HtMl><body>login</body></HtMl>', {
|
|
180
|
+
status: 403,
|
|
181
|
+
headers: { 'content-type': 'application/json' },
|
|
182
|
+
}));
|
|
183
|
+
await expect(fetchJsonOrLoginWall('/api')).resolves.toMatchObject({
|
|
184
|
+
__loginWall: true,
|
|
185
|
+
status: 403,
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
it('detects mixed-case HTML fragments in browser-side responses', async () => {
|
|
189
|
+
const fetchJsonOrLoginWall = new Function('fetch', `${BROWSER_JSON_SNIFF_FN}; return fetchJsonOrLoginWall;`)(async () => new Response('<BoDy>login</BoDy>', {
|
|
190
|
+
status: 403,
|
|
191
|
+
headers: { 'content-type': 'application/json' },
|
|
192
|
+
}));
|
|
193
|
+
await expect(fetchJsonOrLoginWall('/api')).resolves.toMatchObject({
|
|
194
|
+
__loginWall: true,
|
|
195
|
+
status: 403,
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
it('does not flag browser-side non-HTML angle-prefixed text', async () => {
|
|
199
|
+
const fetchJsonOrLoginWall = new Function('fetch', `${BROWSER_JSON_SNIFF_FN}; return fetchJsonOrLoginWall;`)(async () => new Response('<htmlish', {
|
|
200
|
+
status: 200,
|
|
201
|
+
headers: { 'content-type': 'application/json' },
|
|
202
|
+
}));
|
|
203
|
+
await expect(fetchJsonOrLoginWall('/api')).rejects.toThrow('JSON parse failed');
|
|
204
|
+
});
|
|
155
205
|
});
|