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
2 changes: 1 addition & 1 deletion lib/src/commands/scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export const scan = async (
const {
candidatesSignedS3Url,
record: { _id: recordId },
} = await initiateScan(path);
} = await initiateScan(resolvedInput);
await uploadCandidatesToS3(candidates, candidatesSignedS3Url);
await initiateClassify(recordId);
logExtractSummary(extractSummary);
Expand Down
39 changes: 39 additions & 0 deletions lib/src/http/scan.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,42 @@
import axios, { AxiosError } from "axios";
import chalk from "chalk";
import getHttpClient from "./client";
import { IInitiateScanResponse, ZInitiateScanResponse } from "./types";
import { DittoScanCandidate } from "../scan/types";
import DittoError, { ErrorType } from "../utils/DittoError";
import { Blob } from "buffer";

// Deep-links to the home page with the billing/upgrade modal open.
const BILLING_URL = "https://app.dittowords.com/home?openBillingModal=true";

// OSC 8 hyperlink: renders `label` as a clickable link to `href` in terminals
// that support it.
function terminalLink(label: string, href: string): string {
const OSC = "\u001B]8;;";
const BEL = "\u0007";
return `${OSC}${href}${BEL}${chalk.blueBright.underline(label)}${OSC}${BEL}`;
}

// Turns the server's plan-limit response into a message with concrete next
// steps: upgrade, or scan a smaller path.
function scanLimitError(serverMessage: string): DittoError<ErrorType.ScanError> {
const message = [
serverMessage,
"",
"To scan more strings, you can either:",
` • Upgrade your plan at ${terminalLink("app.dittowords.com", BILLING_URL)}`,
" • Scan a smaller subdirectory, e.g. `npx @dittowords/cli scan ./src`",
].join("\n");

return new DittoError({
type: ErrorType.ScanError,
message,
exitCode: 1,
expected: true,
data: { rawErrorMessage: serverMessage },
});
}

export async function initiateScan(
path: string
): Promise<IInitiateScanResponse> {
Expand Down Expand Up @@ -31,6 +64,12 @@ export async function initiateClassify(scanId: string): Promise<void> {
"Sorry! We're having trouble reaching the Ditto API. Please try again later."
);
}

const data = e.response?.data;
if (data?.code === "SCAN_CANDIDATE_LIMIT_EXCEEDED") {
throw scanLimitError(data.message);
}
if (data?.message) throw new Error(data.message);
throw e;
}
}
Expand Down
12 changes: 8 additions & 4 deletions lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ const handleCommandError = async (error: any) => {
errorText = `${error.data.messagePrefix}\n\n${error.data.formattedError}`;
}

if (error.expected) {
return await quit(logger.errorText(errorText), exitCode);
}

sentryOptions = {
extra: { message: errorText, ...(error.data || {}) },
};
Expand Down Expand Up @@ -73,9 +77,9 @@ const appEntry = async () => {

// ditto scan
program
.command("scan <path>")
.command("scan [path]")
.description(
"Run extract + classify + infer end-to-end; emits schema.json and stagings.ndjson to --out-dir"
"Scan a codebase for user-facing strings and send them to Ditto. Defaults to the current directory if no path is given."
)
.option(
"--local",
Expand All @@ -86,11 +90,11 @@ const appEntry = async () => {
.option("--prefix <prefix>", "prefix for output files", "")
.action(
async (
inputPath: string,
inputPath: string | undefined,
opts: { local: boolean; outDir: string; prefix?: string }
) => {
try {
return await scan(inputPath, opts);
return await scan(inputPath ?? ".", opts);
} catch (error) {
handleCommandError(error);
}
Expand Down
5 changes: 5 additions & 0 deletions lib/src/utils/DittoError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { z } from "zod";
export default class DittoError<T extends ErrorType> extends Error {
exitCode: number | undefined;
type: ErrorType;
expected: boolean;
// Note: if you see the type error "Type 'T' cannot be used to index type 'ErrorDataMap'",
// a value is missing from the ErrorDataMap defined below
data: ErrorDataMap[T];
Expand All @@ -18,17 +19,20 @@ export default class DittoError<T extends ErrorType> extends Error {
* @param type The type of error, from the ErrorType enum
* @param message Optional: error message to display to the user
* @param exitCode Optional: exit code to return to the shell.
* @param expected Optional: whether this is an expected, user-actionable error
* @param data Optional: additional data to pass along with the error
*/
constructor({
type,
message,
exitCode,
expected = false,
data,
}: {
type: T;
message?: string;
exitCode?: number;
expected?: boolean;
data: ErrorDataMap[T];
}) {
const errorMessage =
Expand All @@ -39,6 +43,7 @@ export default class DittoError<T extends ErrorType> extends Error {

this.exitCode = exitCode;
this.type = type;
this.expected = expected;
this.data = data;
}
}
Expand Down
Loading