Skip to content
Merged
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
6 changes: 2 additions & 4 deletions src/auth/commands/setup.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { Command } from "commander";
import { getRootOpts } from "../../cli/services/global-opts.js";
import {
CliUsageError,
handleCliError,
} from "../../platform/services/handle-cli-error.js";
import { CliUsageError } from "../../platform/errors/cli-usage-error.js";
import { handleCliError } from "../../platform/services/handle-cli-error.js";
import { promptHidden } from "../../platform/services/prompt-hidden.js";
import { validateAuthToken } from "../../scrape/services/auth-validation.js";
import { PLAYGROUND_URL } from "../constants.js";
Expand Down
4 changes: 4 additions & 0 deletions src/output/services/apply-request-defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ export function applyRequestDefaults(
target: string,
schema: DecodoSchema
): void {
if (body.headless === "png") {
return;
}

const properties = schema.getTargetParameterSchema(target)?.properties ?? {};

if (properties.parse !== undefined && body.parse === undefined) {
Expand Down
6 changes: 6 additions & 0 deletions src/platform/errors/cli-usage-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class CliUsageError extends Error {
constructor(message: string) {
super(message);
this.name = "CliUsageError";
}
}
41 changes: 37 additions & 4 deletions src/platform/services/handle-cli-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,38 @@ import {
import { PLAYGROUND_URL } from "../../auth/constants.js";
import { AuthRequiredError } from "../../auth/errors/auth-required-error.js";
import { EXIT } from "../constants.js";
import { CliUsageError } from "../errors/cli-usage-error.js";

const EXIT_SIGNAL_PREFIX = "process.exit:";

export class CliUsageError extends Error {
constructor(message: string) {
super(message);
this.name = "CliUsageError";
const NETWORK_ERROR_CODES = new Set([
"ENOTFOUND",
"ECONNREFUSED",
"ECONNRESET",
"ETIMEDOUT",
"EAI_AGAIN",
"EHOSTUNREACH",
"ENETUNREACH",
"EPIPE",
]);

function findNetworkCause(
err: unknown
): { code: string; message: string } | undefined {
const seen = new Set<unknown>();
let current: unknown = err;

while (current && typeof current === "object" && !seen.has(current)) {
seen.add(current);
const code = (current as { code?: unknown }).code;
if (typeof code === "string" && NETWORK_ERROR_CODES.has(code)) {
const message = (current as { message?: unknown }).message;
return { code, message: typeof message === "string" ? message : code };
}
current = (current as { cause?: unknown }).cause;
}

return;
}

export function resolveCliExitCode(err: unknown): number {
Expand Down Expand Up @@ -43,6 +67,10 @@ export function resolveCliExitCode(err: unknown): number {
return EXIT.NETWORK;
}

if (findNetworkCause(err)) {
return EXIT.NETWORK;
}

return EXIT.ERROR;
}

Expand Down Expand Up @@ -104,6 +132,11 @@ export function handleCliError(

console.error(`Error: ${message}`);

const networkCause = findNetworkCause(err);
if (networkCause) {
console.error(`Cause: ${networkCause.code} (${networkCause.message})`);
}

if (err instanceof ValidationError) {
const details = extractValidationDetails(err);
if (details.length > 0) {
Expand Down
48 changes: 40 additions & 8 deletions src/platform/services/prompt-hidden.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { stdin, stdout } from "node:process";
import { createInterface } from "node:readline/promises";
import { CliUsageError } from "../errors/cli-usage-error.js";

const CHAR_ETX = 3;
const CHAR_EOT = 4;
const CHAR_DEL = 127;
const CHAR_BACKSPACE = 8;

interface HiddenPromptState {
cleanup: () => void;
Expand All @@ -9,21 +15,30 @@ interface HiddenPromptState {
}

function handleHiddenPromptChar(char: string, state: HiddenPromptState): void {
if (char === "\u0003") {
const code = char.charCodeAt(0);

if (code === CHAR_ETX) {
state.cleanup();
stdout.write("\n");
state.reject(new Error("Cancelled."));
return;
}

if (code === CHAR_EOT) {
state.cleanup();
stdout.write("\n");
state.reject(new CliUsageError("No auth token provided on stdin."));
return;
}

if (char === "\r" || char === "\n") {
state.cleanup();
stdout.write("\n");
state.resolve(state.input.trim());
return;
}

if (char === "\u007f" || char === "\b") {
if (code === CHAR_DEL || code === CHAR_BACKSPACE) {
if (state.input.length > 0) {
state.input = state.input.slice(0, -1);
stdout.write("\b \b");
Expand All @@ -34,14 +49,23 @@ function handleHiddenPromptChar(char: string, state: HiddenPromptState): void {
state.input += char;
}

async function promptViaReadline(message: string): Promise<string> {
const rl = createInterface({ input: stdin, output: stdout });
try {
return await new Promise<string>((resolve, reject) => {
rl.question(message).then((answer) => resolve(answer.trim()), reject);
rl.once("close", () => {
reject(new CliUsageError("No auth token provided on stdin."));
});
});
} finally {
rl.close();
}
}

export async function promptHidden(message: string): Promise<string> {
if (!stdin.isTTY) {
const rl = createInterface({ input: stdin, output: stdout });
try {
return (await rl.question(message)).trim();
} finally {
rl.close();
}
return await promptViaReadline(message);
}

stdout.write(message);
Expand All @@ -64,12 +88,20 @@ export async function promptHidden(message: string): Promise<string> {
}
};

const onEnd = (): void => {
state.cleanup();
stdout.write("\n");
reject(new CliUsageError("No auth token provided on stdin."));
};

state.cleanup = (): void => {
stdin.setRawMode(false);
stdin.pause();
stdin.removeListener("data", onData);
stdin.removeListener("end", onEnd);
};

stdin.on("data", onData);
stdin.on("end", onEnd);
});
}
3 changes: 2 additions & 1 deletion src/platform/services/write-binary.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { CliUsageError, handleCliError } from "./handle-cli-error.js";
import { CliUsageError } from "../errors/cli-usage-error.js";
import { handleCliError } from "./handle-cli-error.js";
import { resolveOutputFilePath } from "./resolve-output-file.js";

export const BINARY_TTY_ERROR =
Expand Down
27 changes: 26 additions & 1 deletion src/scrape/services/command-builder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { DecodoSchema } from "@decodo/sdk-ts";
import { type Command, Option } from "commander";
import { type Command, InvalidArgumentError, Option } from "commander";
import type { JSONSchema4 } from "json-schema";
import { attachScrapeOutputOptions } from "../../output/commands/attach-output-options.js";
import { applyRequestDefaults } from "../../output/services/apply-request-defaults.js";
Expand All @@ -25,9 +25,29 @@ function formatOptionHelp(propertySchema: JSONSchema4): string {
return `${propertySchema.type}${bounds}`;
}

if (propertySchema.type === "array") {
return "JSON array";
}

if (propertySchema.type === "object") {
return "JSON object";
}

return String(propertySchema.type ?? "value");
}

function parseJsonArg(field: string) {
return (value: string): unknown => {
try {
return JSON.parse(value);
} catch {
throw new InvalidArgumentError(
`--${snakeToKebab(field)} expects valid JSON.`
);
}
};
}

function addPropertyOption(
command: Command,
field: string,
Expand Down Expand Up @@ -64,6 +84,11 @@ function addPropertyOption(
return;
}

if (propertySchema.type === "array" || propertySchema.type === "object") {
command.option(`--${kebabFlag} <json>`, help, parseJsonArg(field));
return;
}

command.option(`--${kebabFlag} <value>`, help);
}

Expand Down
23 changes: 22 additions & 1 deletion src/scrape/services/run-target-scrape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getRootOpts } from "../../cli/services/global-opts.js";
import { verboseLog } from "../../cli/services/verbose-log.js";
import { writeScrapeResponse } from "../../output/services/write-scrape-response.js";
import type { OutputOptions } from "../../output/types/output-options.js";
import type { WriteScrapeResponseContext } from "../../output/types/write-scrape-response.js";
import { handleCliError } from "../../platform/services/handle-cli-error.js";
import type {
ExecuteScrapeOptions,
Expand Down Expand Up @@ -39,6 +40,22 @@ async function executeScrape({
});
}

function resolveOutputContext(
explicit: Partial<WriteScrapeResponseContext> | undefined,
body: Record<string, unknown>,
input: string | undefined
): Partial<WriteScrapeResponseContext> | undefined {
if (explicit?.binary) {
return explicit;
}

if (body.headless === "png") {
return { ...explicit, binary: { kind: "png" }, input };
}

return explicit;
}

export function createTargetAction(
target: string,
schema: DecodoSchema,
Expand Down Expand Up @@ -79,7 +96,11 @@ export function createTargetAction(

const body = resolveBody(input, options);
verboseLog(verbose, formatScrapeRequestLog(body));
const outputContext = getOutputContext?.(input, options);
const outputContext = resolveOutputContext(
getOutputContext?.(input, options),
body,
input
);
await executeScrape({
token: auth.token,
schema,
Expand Down
Loading
Loading