Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/fetch/command.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it, vi } from 'vitest';
import { formatWebFetchMarkdown, runClientOwnedWebFetch } from './command.js';
import { CliError } from '../errors.js';

describe('web fetch command', () => {
it('renders fetch metadata before content', () => {
Expand All @@ -10,4 +11,25 @@ describe('web fetch command', () => {
await runClientOwnedWebFetch(['web', 'fetch', '--url', 'https://a'], { webFetch, stdout: { write: vi.fn() } as never });
expect(webFetch).toHaveBeenCalledOnce();
});
it('formats a thrown CliError instead of letting it escape as a raw stack trace', async () => {
const webFetch = vi.fn().mockRejectedValue(new CliError('FETCH_BLOCKED', 'The site blocked non-browser fetches.', 'Use webcmd web fetch-browser for this URL.', 1));
const write = vi.fn();
const priorExitCode = process.exitCode;
await runClientOwnedWebFetch(['web', 'fetch', '--url', 'https://a'], { webFetch, stderr: { write } as never });
const output = write.mock.calls.map((call) => String(call[0])).join('');
expect(output).toContain('FETCH_BLOCKED');
expect(output).toContain('Use webcmd web fetch-browser for this URL.');
expect(process.exitCode).toBe(1);
process.exitCode = priorExitCode;
});
it('formats an ArgumentError from bad flags instead of letting it escape as a raw stack trace', async () => {
const write = vi.fn();
const priorExitCode = process.exitCode;
await runClientOwnedWebFetch(['web', 'fetch', '--url', 'not-a-url'], { stderr: { write } as never });
const output = write.mock.calls.map((call) => String(call[0])).join('');
expect(output).toContain('ARGUMENT');
expect(output).toContain('--url must be an http or https URL');
expect(process.exitCode).toBe(2);
process.exitCode = priorExitCode;
});
});
14 changes: 10 additions & 4 deletions src/fetch/command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { cli, Strategy } from '../registry.js';
import { ArgumentError } from '../errors.js';
import { ArgumentError, CliError, EXIT_CODES, toEnvelope } from '../errors.js';
import { webFetch, type WebFetchOptions, type WebFetchResult } from './client.js';

export const webFetchCommand = cli({
Expand Down Expand Up @@ -31,7 +31,13 @@ function clientOptions(argv: readonly string[]): WebFetchOptions {
return { url: values.url, timeoutSeconds: int('timeout', 30), maxChars: int('max-chars', 50000), allowPrivate: values['allow-private'] === true || values['allow-private'] === 'true' };
}

export async function runClientOwnedWebFetch(argv: readonly string[], dependencies: { webFetch?: typeof webFetch; stdout?: NodeJS.WritableStream } = {}): Promise<void> {
const result = await (dependencies.webFetch ?? webFetch)(clientOptions(argv));
(dependencies.stdout ?? process.stdout).write(`${formatWebFetchMarkdown(result)}\n`);
export async function runClientOwnedWebFetch(argv: readonly string[], dependencies: { webFetch?: typeof webFetch; stdout?: NodeJS.WritableStream; stderr?: NodeJS.WritableStream } = {}): Promise<void> {
try {
const result = await (dependencies.webFetch ?? webFetch)(clientOptions(argv));
(dependencies.stdout ?? process.stdout).write(`${formatWebFetchMarkdown(result)}\n`);
} catch (err) {
const { formatErrorEnvelope } = await import('../output.js');
(dependencies.stderr ?? process.stderr).write(formatErrorEnvelope(toEnvelope(err), { cmdName: 'web/fetch' }));
process.exitCode = err instanceof CliError ? err.exitCode : EXIT_CODES.GENERIC_ERROR;
}
}