diff --git a/cli/.eslintrc.json b/cli/.eslintrc.json index 41756d26..378f871d 100644 --- a/cli/.eslintrc.json +++ b/cli/.eslintrc.json @@ -4,5 +4,15 @@ }, "parserOptions": { "sourceType": "module" + }, + "rules": { + "no-restricted-properties": [ + "error", + { + "object": "process", + "property": "exit", + "message": "Use cliError() or cliAbort() from error.ts instead of process.exit()" + } + ] } } diff --git a/cli/cli.ts b/cli/cli.ts index 7a9285da..6ec53490 100755 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -4,6 +4,7 @@ import fs from "node:fs"; import process from "node:process"; import { resolve } from "node:path"; +import { cliError, handleCliError } from "./error.js"; import { getNetwork } from "./api/network.js"; import { cacheSize, cleanCache, show } from "./cache.js"; import { add } from "./commands/add.js"; @@ -122,9 +123,7 @@ program ]), ) .action(async (pkg, options) => { - if (!checkConfigFile()) { - process.exit(1); - } + checkConfigFile(); await add(pkg, options); }); @@ -143,9 +142,7 @@ program ]), ) .action(async (pkg, options) => { - if (!checkConfigFile()) { - process.exit(1); - } + checkConfigFile(); await remove(pkg, options); }); @@ -164,9 +161,7 @@ program ]), ) .action(async (options) => { - if (!checkConfigFile()) { - process.exit(1); - } + checkConfigFile(); let compatible = await checkApiCompatibility(); if (!compatible) { @@ -187,7 +182,7 @@ program await resolvePackages({ conflicts: "warning" }); if (!ok) { - process.exit(1); + cliError(); } }); @@ -200,9 +195,7 @@ program .option("--no-bench", "Do not run benchmarks") .option("--verbose") .action(async (options) => { - if (!checkConfigFile()) { - process.exit(1); - } + checkConfigFile(); let compatible = await checkApiCompatibility(); if (compatible) { await publish(options); @@ -242,9 +235,7 @@ program .default("warning"), ) .action(async (options) => { - if (!checkConfigFile()) { - process.exit(1); - } + checkConfigFile(); if (options.install) { await installAll({ silent: true, @@ -263,7 +254,7 @@ program .command("moc-args") .description("Print global moc compiler flags from [moc] config section") .action(async () => { - checkConfigFile(true); + checkConfigFile(); let config = readConfig(); let args = getGlobalMocArgs(config); if (args.length) { @@ -304,7 +295,7 @@ program .addOption(new Option("--output, -o ", "Output directory")) .allowUnknownOption(true) // TODO: restrict unknown before "--" .action(async (canisters, options) => { - checkConfigFile(true); + checkConfigFile(); const { extraArgs, args } = parseExtraArgs(canisters); await installAll({ silent: true, @@ -333,7 +324,7 @@ program ) .allowUnknownOption(true) .action(async (files, options) => { - checkConfigFile(true); + checkConfigFile(); const { extraArgs, args: fileList } = parseExtraArgs(files); await installAll({ silent: true, @@ -351,7 +342,7 @@ program .command("check-candid ") .description("Check Candid interface compatibility between two Candid files") .action(async (newCandid, originalCandid) => { - checkConfigFile(true); + checkConfigFile(); await installAll({ silent: true, lock: "ignore", @@ -369,7 +360,7 @@ program .option("--verbose", "Verbose console output") .allowUnknownOption(true) .action(async (oldFile, canister, options) => { - checkConfigFile(true); + checkConfigFile(); const { extraArgs } = parseExtraArgs(); await installAll({ silent: true, @@ -408,7 +399,7 @@ program .option("-w, --watch", "Enable watch mode") .option("--verbose", "Verbose output") .action(async (filter, options) => { - checkConfigFile(true); + checkConfigFile(); await installAll({ silent: true, lock: "ignore", @@ -444,7 +435,7 @@ program // .addOption(new Option('--force-gc', 'Force GC')) .addOption(new Option("--verbose", "Show more information")) .action(async (filter, options) => { - checkConfigFile(true); + checkConfigFile(); await installAll({ silent: true, lock: "ignore", @@ -458,9 +449,7 @@ program .command("template") .description("Apply template") .action(async () => { - if (!checkConfigFile()) { - process.exit(1); - } + checkConfigFile(); await template(); }); @@ -659,9 +648,7 @@ toolchainCommand .addArgument(new Argument("").choices(TOOLCHAINS)) .addArgument(new Argument("[version]")) .action(async (tool, version) => { - if (!checkConfigFile()) { - process.exit(1); - } + checkConfigFile(); await toolchain.use(tool, version); }); @@ -672,9 +659,7 @@ toolchainCommand ) .addArgument(new Argument("[tool]").choices(TOOLCHAINS)) .action(async (tool?: Tool) => { - if (!checkConfigFile()) { - process.exit(1); - } + checkConfigFile(); await toolchain.update(tool); }); @@ -732,7 +717,7 @@ program .option("-g, --generate", "Generate declarations for Motoko canisters") .option("-d, --deploy", "Deploy Motoko canisters") .action(async (options) => { - checkConfigFile(true); + checkConfigFile(); await watch(options); }); @@ -745,10 +730,10 @@ program new Option("--check", "Check code formatting (do not change source files)"), ) .action(async (filter, options) => { - checkConfigFile(true); + checkConfigFile(); let { ok } = await format(filter, options); if (!ok) { - process.exit(1); + cliError(); } }); @@ -766,7 +751,7 @@ program ) .allowUnknownOption(true) .action(async (filter, options) => { - checkConfigFile(true); + checkConfigFile(); const { extraArgs } = parseExtraArgs(); await lint(filter, { ...options, @@ -790,7 +775,7 @@ docsCommand .choices(["md", "adoc", "html"]), ) .action(async (options) => { - checkConfigFile(true); + checkConfigFile(); await docs(options); }); @@ -815,9 +800,9 @@ docsCommand ).default(70), ) .action(async (options) => { - checkConfigFile(true); + checkConfigFile(); await docsCoverage(options); }); program.addCommand(docsCommand); -program.parse(); +program.parseAsync().catch(handleCliError); diff --git a/cli/commands/add.ts b/cli/commands/add.ts index 4b8165fc..1be62e6a 100644 --- a/cli/commands/add.ts +++ b/cli/commands/add.ts @@ -17,6 +17,7 @@ import { checkRequirements } from "../check-requirements.js"; import { syncLocalCache } from "./install/sync-local-cache.js"; import { notifyInstalls } from "../notify-installs.js"; import { resolvePackages } from "../resolve-packages.js"; +import { cliError } from "../error.js"; type AddOptions = { verbose?: boolean; @@ -30,9 +31,7 @@ export async function add( { verbose = false, dev = false, lock }: AddOptions = {}, asName?: string, ) { - if (!checkConfigFile()) { - return; - } + checkConfigFile(); let config = readConfig(); if (dev) { @@ -87,8 +86,7 @@ export async function add( } else { let versionRes = await getHighestVersion(name); if ("err" in versionRes) { - console.log(chalk.red("Error: ") + versionRes.err); - return; + cliError(versionRes.err); } ver = versionRes.ok; } @@ -105,7 +103,7 @@ export async function add( verbose: verbose, }); if (!res) { - process.exit(1); + cliError(); } } else if (!pkgDetails.path) { let res = await installMopsDep(pkgDetails.name, pkgDetails.version, { diff --git a/cli/commands/available-updates.ts b/cli/commands/available-updates.ts index d4a8ae9a..4df67a73 100644 --- a/cli/commands/available-updates.ts +++ b/cli/commands/available-updates.ts @@ -1,9 +1,8 @@ -import process from "node:process"; -import chalk from "chalk"; import { mainActor } from "../api/actors.js"; import { Config } from "../types.js"; import { getDepName, getDepPinnedVersion } from "../helpers/get-dep-name.js"; import { SemverPart } from "../declarations/main/main.did.js"; +import { cliError } from "../error.js"; // [pkg, oldVersion, newVersion] export async function getAvailableUpdates( @@ -52,8 +51,7 @@ export async function getAvailableUpdates( ); if ("err" in res) { - console.log(chalk.red("Error:"), res.err); - process.exit(1); + cliError("Error: " + res.err); } return res.ok diff --git a/cli/commands/bench.ts b/cli/commands/bench.ts index c9d574d4..37f9398b 100644 --- a/cli/commands/bench.ts +++ b/cli/commands/bench.ts @@ -25,6 +25,7 @@ import { getDfxVersion } from "../helpers/get-dfx-version.js"; import { getMocPath } from "../helpers/get-moc-path.js"; import { sources } from "./sources.js"; import { MOTOKO_GLOB_CONFIG } from "../constants.js"; +import { cliError } from "../error.js"; import { Benchmark, Benchmarks } from "../declarations/main/main.did.js"; import { BenchResult, _SERVICE } from "../declarations/bench/bench.did.js"; @@ -75,12 +76,9 @@ export async function bench( if (replicaType === "pocket-ic" && !config.toolchain?.["pocket-ic"]) { let dfxVersion = getDfxVersion(); if (!dfxVersion || new SemVer(dfxVersion).compare("0.24.1") < 0) { - console.log( - chalk.red( - "Please update dfx to the version >=0.24.1 or specify pocket-ic version in mops.toml", - ), + cliError( + "Please update dfx to the version >=0.24.1 or specify pocket-ic version in mops.toml", ); - process.exit(1); } else { replicaType = "dfx-pocket-ic"; } diff --git a/cli/commands/build.ts b/cli/commands/build.ts index bc3f48e1..2d7ea039 100644 --- a/cli/commands/build.ts +++ b/cli/commands/build.ts @@ -3,7 +3,7 @@ import { execa } from "execa"; import { exists } from "fs-extra"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import { cliError } from "../error.js"; +import { CliError, cliError } from "../error.js"; import { isCandidCompatible } from "../helpers/is-candid-compatible.js"; import { resolveCanisterConfigs } from "../helpers/resolve-canisters.js"; import { CanisterConfig, Config } from "../types.js"; @@ -147,6 +147,9 @@ export async function build( ); } } catch (err: any) { + if (err instanceof CliError) { + throw err; + } cliError( `Error during Candid compatibility check for canister ${canisterName}${err?.message ? `\n${err.message}` : ""}`, ); @@ -173,7 +176,7 @@ export async function build( ); await writeFile(wasmPath, newWasm); } catch (err: any) { - if (err.message?.includes("Build failed for canister")) { + if (err instanceof CliError) { throw err; } cliError( diff --git a/cli/commands/bump.ts b/cli/commands/bump.ts index a18fd0e9..c7318cfd 100644 --- a/cli/commands/bump.ts +++ b/cli/commands/bump.ts @@ -1,25 +1,19 @@ -import process from "node:process"; import prompts from "prompts"; import chalk from "chalk"; import { checkConfigFile, readConfig, writeConfig } from "../mops.js"; +import { cliError } from "../error.js"; export async function bump(part: string) { - if (!checkConfigFile()) { - return; - } + checkConfigFile(); if (part && !["major", "minor", "patch"].includes(part)) { - console.log( - chalk.red("Unknown version part. Available parts: major, minor, patch"), - ); - process.exit(1); + cliError("Unknown version part. Available parts: major, minor, patch"); } let config = readConfig(); if (!config.package) { - console.log(chalk.red("No [package] section found in mops.toml.")); - process.exit(1); + cliError("No [package] section found in mops.toml."); } console.log(`Current version: ${chalk.yellow.bold(config.package.version)}`); diff --git a/cli/commands/check-candid.ts b/cli/commands/check-candid.ts index 378984c9..8094c847 100644 --- a/cli/commands/check-candid.ts +++ b/cli/commands/check-candid.ts @@ -1,6 +1,6 @@ import chalk from "chalk"; import { isCandidCompatible } from "../helpers/is-candid-compatible.js"; -import { cliError } from "../error.js"; +import { CliError, cliError } from "../error.js"; export interface CheckCandidOptions { verbose?: boolean; @@ -17,6 +17,9 @@ export async function checkCandid( } console.log(chalk.green("✓ Candid compatibility check passed")); } catch (error: any) { + if (error instanceof CliError) { + throw error; + } cliError( `Error while checking Candid compatibility${error?.message ? `\n${error.message}` : ""}`, ); diff --git a/cli/commands/check.ts b/cli/commands/check.ts index aac2f5a6..b5e602b1 100644 --- a/cli/commands/check.ts +++ b/cli/commands/check.ts @@ -2,7 +2,7 @@ import path from "node:path"; import { existsSync } from "node:fs"; import chalk from "chalk"; import { execa } from "execa"; -import { cliError } from "../error.js"; +import { CliError, cliError } from "../error.js"; import { getGlobalMocArgs, getRootDir, @@ -134,6 +134,9 @@ export async function check( console.log(chalk.green(`✓ ${file}`)); } catch (err: any) { + if (err instanceof CliError) { + throw err; + } cliError( `Error while checking ${file}${err?.message ? `\n${err.message}` : ""}`, ); diff --git a/cli/commands/docs-coverage.ts b/cli/commands/docs-coverage.ts index 03d848c6..902b93cc 100644 --- a/cli/commands/docs-coverage.ts +++ b/cli/commands/docs-coverage.ts @@ -2,6 +2,7 @@ import { readFileSync } from "node:fs"; import chalk from "chalk"; import { globSync } from "glob"; import { docs } from "./docs.js"; +import { cliError } from "../error.js"; export type DocsCoverageReporter = | "compact" @@ -80,7 +81,7 @@ export async function docsCoverage(options: Partial = {}) { } if (threshold > 0 && totalCoverage < threshold) { - process.exit(1); + cliError(); } return totalCoverage; diff --git a/cli/commands/docs.ts b/cli/commands/docs.ts index a95dd60f..5463a61b 100644 --- a/cli/commands/docs.ts +++ b/cli/commands/docs.ts @@ -9,6 +9,7 @@ import { create as createTar } from "tar"; import streamToPromise from "stream-to-promise"; import { getRootDir } from "../mops.js"; +import { CliError } from "../error.js"; import { toolchain } from "./toolchain/index.js"; let moDocPath: string; @@ -54,7 +55,7 @@ export async function docs(options: Partial = {}) { } // generate docs - await new Promise((resolve) => { + await new Promise((resolve, reject) => { let proc = spawn(moDocPath, [ `--source=${path.join(rootDir, source)}`, `--output=${docsDirRelative}`, @@ -79,8 +80,9 @@ export async function docs(options: Partial = {}) { proc.stderr.on("data", (data) => { let text = data.toString().trim(); if (text.includes("syntax error")) { - console.log(chalk.red("Error:"), text); - process.exit(1); + proc.kill(); + reject(new CliError("Error: " + text)); + return; } if ( text.includes("No such file or directory") || @@ -100,8 +102,8 @@ export async function docs(options: Partial = {}) { return; } if (code !== 0) { - console.log(chalk.red("Error:"), code, stderr); - process.exit(1); + reject(new CliError("Error: " + code + " " + stderr)); + return; } resolve(); }); diff --git a/cli/commands/init.ts b/cli/commands/init.ts index 43cf7e5a..11fdbb1b 100644 --- a/cli/commands/init.ts +++ b/cli/commands/init.ts @@ -12,6 +12,7 @@ import { VesselConfig, readVesselConfig } from "../vessel.js"; import { Config, Dependencies } from "../types.js"; import { template } from "./template.js"; import { kebabCase } from "change-case"; +import { cliAbort } from "../error.js"; export async function init({ yes = false } = {}) { let configFile = path.join(process.cwd(), "mops.toml"); @@ -63,8 +64,7 @@ export async function init({ yes = false } = {}) { let promptsConfig = { onCancel() { - console.log("aborted"); - process.exit(0); + cliAbort(); }, }; diff --git a/cli/commands/install/install-all.ts b/cli/commands/install/install-all.ts index 8f6e0c0d..1e91e6fd 100644 --- a/cli/commands/install/install-all.ts +++ b/cli/commands/install/install-all.ts @@ -27,9 +27,7 @@ export async function installAll({ lock, installFromLockFile, }: InstallAllOptions = {}): Promise { - if (!checkConfigFile()) { - return false; - } + checkConfigFile(); let config = readConfig(); let deps = Object.values(config.dependencies || {}); diff --git a/cli/commands/install/install-mops-dep.ts b/cli/commands/install/install-mops-dep.ts index 53b71431..38cfe514 100644 --- a/cli/commands/install/install-mops-dep.ts +++ b/cli/commands/install/install-mops-dep.ts @@ -5,6 +5,7 @@ import { Buffer } from "node:buffer"; import { createLogUpdate } from "log-update"; import chalk from "chalk"; import { deleteSync } from "del"; +import { CliError } from "../../error.js"; import { checkConfigFile, progressBar, readConfig } from "../../mops.js"; import { getHighestVersion } from "../../api/getHighestVersion.js"; import { storageActor } from "../../api/actors.js"; @@ -43,9 +44,7 @@ export async function installMopsDep( threads = threads || 12; let depName = getDepName(pkg); - if (!checkConfigFile()) { - return false; - } + checkConfigFile(); let logUpdate = createLogUpdate(process.stdout, { showCursor: true }); // progress @@ -102,6 +101,7 @@ export async function installMopsDep( let onSigInt = () => { deleteSync([cacheDir], { force: true }); + // eslint-disable-next-line no-restricted-properties process.exit(); }; process.on("SIGINT", onSigInt); @@ -121,6 +121,9 @@ export async function installMopsDep( }), ); } catch (err) { + if (err instanceof CliError) { + throw err; + } console.error(chalk.red("Error: ") + err); deleteSync([cacheDir], { force: true }); return false; @@ -128,6 +131,9 @@ export async function installMopsDep( process.off("SIGINT", onSigInt); } catch (err) { + if (err instanceof CliError) { + throw err; + } console.error(chalk.red("Error: ") + err); return false; } diff --git a/cli/commands/lint.ts b/cli/commands/lint.ts index c9ad0fea..f3203abd 100644 --- a/cli/commands/lint.ts +++ b/cli/commands/lint.ts @@ -2,7 +2,7 @@ import chalk from "chalk"; import { execa } from "execa"; import { globSync } from "glob"; import path from "node:path"; -import { cliError } from "../error.js"; +import { CliError, cliError } from "../error.js"; import { formatDir, formatGithubDir, @@ -182,6 +182,9 @@ export async function lint( console.log(chalk.green("✓ Lint succeeded")); } } catch (err: any) { + if (err instanceof CliError) { + throw err; + } cliError( `Error while running lintoko${err?.message ? `\n${err.message}` : ""}`, ); diff --git a/cli/commands/maintainer.ts b/cli/commands/maintainer.ts index afc6b9c5..541e32c6 100644 --- a/cli/commands/maintainer.ts +++ b/cli/commands/maintainer.ts @@ -1,14 +1,12 @@ -import process from "node:process"; import chalk from "chalk"; import { checkConfigFile, getIdentity, readConfig } from "../mops.js"; import { mainActor } from "../api/actors.js"; import { Principal } from "@icp-sdk/core/principal"; import prompts from "prompts"; +import { cliAbort, cliError } from "../error.js"; export async function printMaintainers() { - if (!checkConfigFile()) { - return; - } + checkConfigFile(); let config = readConfig(); let actor = await mainActor(); @@ -23,9 +21,7 @@ export async function printMaintainers() { } export async function addMaintainer(maintainer: string, yes = false) { - if (!checkConfigFile()) { - return; - } + checkConfigFile(); let config = readConfig(); let principal = Principal.fromText(maintainer); @@ -33,8 +29,7 @@ export async function addMaintainer(maintainer: string, yes = false) { if (!yes) { let promptsConfig = { onCancel() { - console.log("aborted"); - process.exit(0); + cliAbort(); }, }; @@ -63,15 +58,12 @@ export async function addMaintainer(maintainer: string, yes = false) { `Added maintainer ${chalk.bold(maintainer)} to package ${chalk.bold(config.package?.name)}`, ); } else { - console.error(chalk.red("Error: ") + res.err); - process.exit(1); + cliError("Error: " + res.err); } } export async function removeMaintainer(maintainer: string, yes = false) { - if (!checkConfigFile()) { - return; - } + checkConfigFile(); let config = readConfig(); let principal = Principal.fromText(maintainer); @@ -79,8 +71,7 @@ export async function removeMaintainer(maintainer: string, yes = false) { if (!yes) { let promptsConfig = { onCancel() { - console.log("aborted"); - process.exit(0); + cliAbort(); }, }; @@ -109,7 +100,6 @@ export async function removeMaintainer(maintainer: string, yes = false) { `Removed maintainer ${chalk.bold(maintainer)} from package ${chalk.bold(config.package?.name)}`, ); } else { - console.error(chalk.red("Error: ") + res.err); - process.exit(1); + cliError("Error: " + res.err); } } diff --git a/cli/commands/outdated.ts b/cli/commands/outdated.ts index 7faebf15..81874fe3 100644 --- a/cli/commands/outdated.ts +++ b/cli/commands/outdated.ts @@ -4,9 +4,7 @@ import { getAvailableUpdates } from "./available-updates.js"; import { getDepName, getDepPinnedVersion } from "../helpers/get-dep-name.js"; export async function outdated() { - if (!checkConfigFile()) { - return; - } + checkConfigFile(); let config = readConfig(); let available = await getAvailableUpdates(config); diff --git a/cli/commands/owner.ts b/cli/commands/owner.ts index 10df7a78..7fabc2e6 100644 --- a/cli/commands/owner.ts +++ b/cli/commands/owner.ts @@ -1,14 +1,12 @@ -import process from "node:process"; import chalk from "chalk"; import { checkConfigFile, getIdentity, readConfig } from "../mops.js"; import { mainActor } from "../api/actors.js"; import { Principal } from "@icp-sdk/core/principal"; import prompts from "prompts"; +import { cliAbort, cliError } from "../error.js"; export async function printOwners() { - if (!checkConfigFile()) { - return; - } + checkConfigFile(); let config = readConfig(); let actor = await mainActor(); @@ -21,9 +19,7 @@ export async function printOwners() { } export async function addOwner(owner: string, yes = false) { - if (!checkConfigFile()) { - return; - } + checkConfigFile(); let config = readConfig(); let principal = Principal.fromText(owner); @@ -31,8 +27,7 @@ export async function addOwner(owner: string, yes = false) { if (!yes) { let promptsConfig = { onCancel() { - console.log("aborted"); - process.exit(0); + cliAbort(); }, }; @@ -61,15 +56,12 @@ export async function addOwner(owner: string, yes = false) { `Added owner ${chalk.bold(owner)} to package ${chalk.bold(config.package?.name)}`, ); } else { - console.error(chalk.red("Error: ") + res.err); - process.exit(1); + cliError("Error: " + res.err); } } export async function removeOwner(owner: string, yes = false) { - if (!checkConfigFile()) { - return; - } + checkConfigFile(); let config = readConfig(); let principal = Principal.fromText(owner); @@ -77,8 +69,7 @@ export async function removeOwner(owner: string, yes = false) { if (!yes) { let promptsConfig = { onCancel() { - console.log("aborted"); - process.exit(0); + cliAbort(); }, }; @@ -107,7 +98,6 @@ export async function removeOwner(owner: string, yes = false) { `Removed owner ${chalk.bold(owner)} from package ${chalk.bold(config.package?.name)}`, ); } else { - console.error(chalk.red("Error: ") + res.err); - process.exit(1); + cliError("Error: " + res.err); } } diff --git a/cli/commands/publish.ts b/cli/commands/publish.ts index 79814c71..bbd1a736 100644 --- a/cli/commands/publish.ts +++ b/cli/commands/publish.ts @@ -29,6 +29,7 @@ import { SilentReporter } from "./test/reporters/silent-reporter.js"; import { findChangelogEntry } from "../helpers/find-changelog-entry.js"; import { bench } from "./bench.js"; import { docsCoverage } from "./docs-coverage.js"; +import { cliError } from "../error.js"; export async function publish( options: { @@ -38,9 +39,7 @@ export async function publish( verbose?: boolean; } = {}, ) { - if (!checkConfigFile()) { - return; - } + checkConfigFile(); let rootDir = getRootDir(); let config = readConfig(); @@ -58,27 +57,20 @@ export async function publish( "requirements", ].includes(key) ) { - console.log(chalk.red("Error: ") + `Unknown config section [${key}]`); - process.exit(1); + cliError(`Error: Unknown config section [${key}]`); } } // required fields if (!config.package) { - console.log( - chalk.red("Error: ") + - "Please specify [package] section in your mops.toml", - ); - process.exit(1); + cliError("Error: Please specify [package] section in your mops.toml"); } for (let key of ["name", "version"]) { // @ts-ignore if (!config.package[key]) { - console.log( - chalk.red("Error: ") + - `Please specify "${key}" in [package] section in your mops.toml`, + cliError( + `Error: Please specify "${key}" in [package] section in your mops.toml`, ); - process.exit(1); } } @@ -115,16 +107,14 @@ export async function publish( ]; for (let key of Object.keys(config.package)) { if (!packageKeys.includes(key)) { - console.log(chalk.red("Error: ") + `Unknown config key 'package.${key}'`); - process.exit(1); + cliError(`Error: Unknown config key 'package.${key}'`); } } // disabled fields for (let key of ["dfx", "moc", "homepage", "documentation", "donation"]) { if ((config.package as any)[key]) { - console.log(chalk.red("Error: ") + `package.${key} is not supported yet`); - process.exit(1); + cliError(`Error: package.${key} is not supported yet`); } } @@ -149,54 +139,41 @@ export async function publish( for (let [key, max] of Object.entries(keysMax)) { // @ts-ignore if (config.package[key] && config.package[key].length > max) { - console.log( - chalk.red("Error: ") + `package.${key} value max length is ${max}`, - ); - process.exit(1); + cliError(`Error: package.${key} value max length is ${max}`); } } if (config.dependencies) { if (Object.keys(config.dependencies).length > 100) { - console.log(chalk.red("Error: ") + "max dependencies is 100"); - process.exit(1); + cliError("Error: max dependencies is 100"); } for (let dep of Object.values(config.dependencies)) { if (dep.path) { - console.log( - chalk.red("Error: ") + - "you can't publish packages with local dependencies", - ); - process.exit(1); + cliError("Error: you can't publish packages with local dependencies"); } delete dep.path; } for (let dep of Object.values(config.dependencies)) { if (dep.repo) { - console.log( - chalk.red("Error: ") + - "GitHub dependencies are no longer supported.\nIf you are the owner of the dependency, please publish it to the Mops registry.", + cliError( + "Error: GitHub dependencies are no longer supported.\nIf you are the owner of the dependency, please publish it to the Mops registry.", ); - process.exit(1); } } } if (config["dev-dependencies"]) { if (Object.keys(config["dev-dependencies"]).length > 100) { - console.log(chalk.red("Error: ") + "max dev-dependencies is 100"); - process.exit(1); + cliError("Error: max dev-dependencies is 100"); } for (let dep of Object.values(config["dev-dependencies"])) { if (dep.path) { - console.log( - chalk.red("Error: ") + - "you can't publish packages with local dev-dependencies", + cliError( + "Error: you can't publish packages with local dev-dependencies", ); - process.exit(1); } delete dep.path; } @@ -310,12 +287,10 @@ export async function publish( // check required files if (!files.includes("mops.toml")) { - console.log(chalk.red("Error: ") + " please add mops.toml file"); - process.exit(1); + cliError("Error: please add mops.toml file"); } if (!files.includes("README.md")) { - console.log(chalk.red("Error: ") + " please add README.md file"); - process.exit(1); + cliError("Error: please add README.md file"); } // check allowed exts @@ -326,22 +301,18 @@ export async function publish( !file.toLowerCase().endsWith("notice") && file !== docsFile ) { - console.log( - chalk.red("Error: ") + - `file ${file} has unsupported extension. Allowed: .mo, .did, .md, .toml`, + cliError( + `Error: file ${file} has unsupported extension. Allowed: .mo, .did, .md, .toml`, ); - process.exit(1); } } // pre-flight file count check (must match MAX_PACKAGE_FILES in PackagePublisher.mo) const FILE_LIMIT = 1000; if (files.length > FILE_LIMIT) { - console.log( - chalk.red("Error: ") + - `Too many files (${files.length}). Maximum is ${FILE_LIMIT}.`, + cliError( + `Error: Too many files (${files.length}). Maximum is ${FILE_LIMIT}.`, ); - process.exit(1); } // parse changelog @@ -373,8 +344,7 @@ export async function publish( config.toolchain?.["pocket-ic"] ? "pocket-ic" : "dfx", ); if (reporter.failed > 0) { - console.log(chalk.red("Error: ") + "tests failed"); - process.exit(1); + cliError("Error: tests failed"); } } @@ -391,8 +361,7 @@ export async function publish( }); } catch (err) { console.error(err); - console.log(chalk.red("Error: ") + "benchmarks failed"); - process.exit(1); + cliError("Error: benchmarks failed"); } } @@ -411,8 +380,7 @@ export async function publish( progress(); let publishing = await actor.startPublish(backendPkgConfig); if ("err" in publishing) { - console.log(chalk.red("Error: ") + publishing.err); - process.exit(1); + cliError("Error: " + publishing.err); } let publishingId = publishing.ok; @@ -460,8 +428,7 @@ export async function publish( firstChunk, ); if ("err" in res) { - console.log(chalk.red("Error: ") + res.err); - process.exit(1); + cliError("Error: " + res.err); } let fileId = res.ok; @@ -475,8 +442,7 @@ export async function publish( chunk, ); if ("err" in res) { - console.log(chalk.red("Error: ") + res.err); - process.exit(1); + cliError("Error: " + res.err); } } }); @@ -492,8 +458,7 @@ export async function publish( let res = await actor.finishPublish(publishingId); if ("err" in res) { - console.log(chalk.red("Error: ") + res.err); - process.exit(1); + cliError("Error: " + res.err); } console.log( diff --git a/cli/commands/remove.ts b/cli/commands/remove.ts index 0e1517cd..aec649e6 100644 --- a/cli/commands/remove.ts +++ b/cli/commands/remove.ts @@ -25,9 +25,7 @@ export async function remove( name: string, { dev = false, verbose = false, dryRun = false, lock }: RemoveOptions = {}, ) { - if (!checkConfigFile()) { - return; - } + checkConfigFile(); function getTransitiveDependencies(config: Config, exceptPkgId: string) { let deps = Object.values(config.dependencies || {}); diff --git a/cli/commands/replica.ts b/cli/commands/replica.ts index 28c44ec6..4c8c3e4a 100644 --- a/cli/commands/replica.ts +++ b/cli/commands/replica.ts @@ -21,7 +21,6 @@ import { } from "../helpers/pocket-ic-client.js"; import { toolchain } from "./toolchain/index.js"; import { getDfxVersion } from "../helpers/get-dfx-version.js"; - type StartOptions = { type?: "dfx" | "pocket-ic" | "dfx-pocket-ic"; dir?: string; @@ -93,6 +92,7 @@ export class Replica { if (data.toString().includes("Failed to bind socket to")) { console.error(chalk.red(data.toString())); console.log("Please try again after some time"); + // eslint-disable-next-line no-restricted-properties process.exit(11); } }); diff --git a/cli/commands/self.ts b/cli/commands/self.ts index d99d9fec..d8cc6448 100644 --- a/cli/commands/self.ts +++ b/cli/commands/self.ts @@ -1,7 +1,7 @@ -import process from "node:process"; import child_process, { execSync } from "node:child_process"; import chalk from "chalk"; import { version, globalConfigDir } from "../mops.js"; +import { CliError, cliError } from "../error.js"; import { cleanCache } from "../cache.js"; import { toolchain } from "./toolchain/index.js"; @@ -13,8 +13,7 @@ function detectPackageManager() { res = execSync("which mops").toString(); } catch (e) {} if (!res) { - console.error(chalk.red("Couldn't detect package manager")); - process.exit(1); + cliError("Couldn't detect package manager"); } if (res.includes("pnpm/")) { return "pnpm"; @@ -51,12 +50,15 @@ export async function update() { { stdio: "inherit", detached: false }, ); - proc.on("exit", (res) => { - if (res !== 0) { - console.log(chalk.red("Failed to update.")); - process.exit(1); - } - console.log(chalk.green("Success")); + await new Promise((resolve, reject) => { + proc.on("exit", (res) => { + if (res !== 0) { + reject(new CliError("Failed to update.")); + } else { + console.log(chalk.green("Success")); + resolve(); + } + }); }); } } diff --git a/cli/commands/sources.ts b/cli/commands/sources.ts index 7256ecb2..66aaa555 100644 --- a/cli/commands/sources.ts +++ b/cli/commands/sources.ts @@ -14,9 +14,7 @@ export async function sourcesArgs({ conflicts = "ignore" as "warning" | "error" | "ignore", cwd = process.cwd(), } = {}): Promise { - if (!checkConfigFile()) { - return []; - } + checkConfigFile(); let resolvedPackages = await resolvePackages({ conflicts }); diff --git a/cli/commands/sync.ts b/cli/commands/sync.ts index f6595a5e..6c579e0a 100644 --- a/cli/commands/sync.ts +++ b/cli/commands/sync.ts @@ -15,9 +15,7 @@ type SyncOptions = { }; export async function sync({ lock }: SyncOptions = {}) { - if (!checkConfigFile()) { - return; - } + checkConfigFile(); let missing = await getMissingPackages(); let unused = await getUnusedPackages(); diff --git a/cli/commands/test/test.ts b/cli/commands/test/test.ts index 1f5f4350..c3a3e8fb 100644 --- a/cli/commands/test/test.ts +++ b/cli/commands/test/test.ts @@ -33,6 +33,7 @@ import { Replica } from "../replica.js"; import { TestMode } from "../../types.js"; import { getDfxVersion } from "../../helpers/get-dfx-version.js"; import { MOTOKO_GLOB_CONFIG, MOTOKO_IGNORE_PATTERNS } from "../../constants.js"; +import { cliError } from "../../error.js"; type ReporterName = "verbose" | "files" | "compact" | "silent"; type ReplicaName = "dfx" | "pocket-ic" | "dfx-pocket-ic"; @@ -68,12 +69,9 @@ export async function test(filter = "", options: Partial = {}) { if (replicaType === "pocket-ic" && !config.toolchain?.["pocket-ic"]) { let dfxVersion = getDfxVersion(); if (!dfxVersion || new SemVer(dfxVersion).compare("0.24.1") < 0) { - console.log( - chalk.red( - "Please update dfx to the version >=0.24.1 or specify pocket-ic version in mops.toml", - ), + cliError( + "Please update dfx to the version >=0.24.1 or specify pocket-ic version in mops.toml", ); - process.exit(1); } else { replicaType = "dfx-pocket-ic"; } @@ -89,6 +87,7 @@ export async function test(filter = "", options: Partial = {}) { process.on("SIGINT", () => { if (sigint) { console.log("Force exit"); + // eslint-disable-next-line no-restricted-properties process.exit(0); } sigint = true; @@ -96,9 +95,11 @@ export async function test(filter = "", options: Partial = {}) { if (replicaStartPromise) { console.log("Stopping replica..."); replica.stop(true).then(() => { + // eslint-disable-next-line no-restricted-properties process.exit(0); }); } else { + // eslint-disable-next-line no-restricted-properties process.exit(0); } }); @@ -152,7 +153,7 @@ export async function test(filter = "", options: Partial = {}) { replicaType, ); if (!passed) { - process.exit(1); + cliError(); } } } @@ -294,7 +295,7 @@ export async function testWithReporter( // print logs immediately for replica tests because we run them one-by-one let mmf = new MMF1(mode === "replica" ? "print" : "store", absToRel(file)); - let promise = new Promise((resolve) => { + let promise = new Promise((resolve, reject) => { let mocArgs = [ "--hide-warnings", "--error-detail=2", @@ -314,7 +315,7 @@ export async function testWithReporter( } throw error; }); - pipeMMF(proc, mmf).then(resolve); + pipeMMF(proc, mmf).then(resolve, reject); } // build and run wasm else if (mode === "wasi") { @@ -361,12 +362,9 @@ export async function testWithReporter( wasmFile, ]; } else { - console.error( - chalk.red( - "Minimum wasmtime version is 14.0.0. Please update wasmtime to the latest version", - ), + cliError( + "Minimum wasmtime version is 14.0.0. Please update wasmtime to the latest version", ); - process.exit(1); } let proc = spawn(wasmtimePath, wasmtimeArgs, { signal }); @@ -382,7 +380,7 @@ export async function testWithReporter( .finally(() => { fs.rmSync(wasmFile, { force: true }); }) - .then(resolve); + .then(resolve, reject); } // build and execute in replica else if (mode === "replica") { @@ -469,7 +467,7 @@ export async function testWithReporter( globalThis.mopsReplicaTestRunning = false; fs.rmSync(wasmFile, { force: true }); }) - .then(resolve); + .then(resolve, reject); } }); diff --git a/cli/commands/toolchain/index.ts b/cli/commands/toolchain/index.ts index b80ba127..3f8335d1 100644 --- a/cli/commands/toolchain/index.ts +++ b/cli/commands/toolchain/index.ts @@ -6,6 +6,7 @@ import { execSync } from "node:child_process"; import chalk from "chalk"; import prompts from "prompts"; import { createLogUpdate } from "log-update"; +import { CliError, cliError } from "../../error.js"; import { checkConfigFile, getClosestConfigFile, @@ -32,8 +33,7 @@ function getToolUtils(tool: Tool) { } else if (tool === "lintoko") { return lintoko; } else { - console.error(`Unknown tool '${tool}'`); - process.exit(1); + cliError(`Unknown tool '${tool}'`); } } @@ -72,8 +72,7 @@ async function checkToolchainInited({ strict = false } = {}): Promise { // update shell config files to set DFX_MOC_PATH to moc-wrapper async function init({ reset = false, silent = false } = {}) { if (process.platform == "win32") { - console.error("Windows is not supported. Please use WSL"); - process.exit(1); + cliError("Windows is not supported. Please use WSL"); } try { @@ -90,10 +89,14 @@ async function init({ reset = false, silent = false } = {}) { ); console.log("TIP: More details at https://docs.mops.one/cli/toolchain"); if (!process.env.CI || !silent) { - process.exit(1); + cliError(); } } - } catch {} + } catch (err) { + if (err instanceof CliError) { + throw err; + } + } let zshrc = path.join(os.homedir(), ".zshrc"); let bashrc = path.join(os.homedir(), ".bashrc"); @@ -119,7 +122,7 @@ async function init({ reset = false, silent = false } = {}) { console.log( 'TIP: You can add "export DFX_MOC_PATH=moc-wrapper" to your shell config file manually to initialize Mops toolchain', ); - process.exit(1); + cliError(); } // update all existing shell config files @@ -322,10 +325,9 @@ async function update(tool?: Tool) { for (let tool of tools) { if (!config.toolchain[tool]) { - console.error( + cliError( `Tool '${tool}' is not defined in [toolchain] section in mops.toml`, ); - process.exit(1); } let toolUtils = getToolUtils(tool); @@ -359,7 +361,6 @@ async function bin(tool: Tool, { fallback = false } = {}): Promise { return execSync("dfx cache show").toString().trim() + "/moc"; } checkConfigFile(); - process.exit(1); } let config = readConfig(); @@ -392,7 +393,7 @@ async function bin(tool: Tool, { fallback = false } = {}): Promise { console.log( `Run ${chalk.green(`mops toolchain use ${tool}`)} to install it`, ); - process.exit(1); + cliError(); } } diff --git a/cli/commands/toolchain/lintoko.ts b/cli/commands/toolchain/lintoko.ts index cab88029..5c01fd5b 100644 --- a/cli/commands/toolchain/lintoko.ts +++ b/cli/commands/toolchain/lintoko.ts @@ -2,6 +2,7 @@ import process from "node:process"; import path from "node:path"; import fs from "node:fs"; +import { cliError } from "../../error.js"; import { globalCacheDir } from "../../mops.js"; import * as toolchainUtils from "./toolchain-utils.js"; @@ -27,8 +28,7 @@ export let download = async ( { silent = false, verbose = false } = {}, ) => { if (!version) { - console.error("version is not defined"); - process.exit(1); + cliError("version is not defined"); } if (isCached(version)) { if (verbose) { diff --git a/cli/commands/toolchain/moc.ts b/cli/commands/toolchain/moc.ts index d933f06d..460b49de 100644 --- a/cli/commands/toolchain/moc.ts +++ b/cli/commands/toolchain/moc.ts @@ -3,6 +3,7 @@ import path from "node:path"; import fs from "fs-extra"; import { SemVer } from "semver"; +import { cliError } from "../../error.js"; import { globalCacheDir } from "../../mops.js"; import * as toolchainUtils from "./toolchain-utils.js"; @@ -28,12 +29,10 @@ export let download = async ( { silent = false, verbose = false } = {}, ) => { if (process.platform == "win32") { - console.error("Windows is not supported. Please use WSL"); - process.exit(1); + cliError("Windows is not supported. Please use WSL"); } if (!version) { - console.error("version is not defined"); - process.exit(1); + cliError("version is not defined"); } const destDir = path.join(cacheDir, version); diff --git a/cli/commands/toolchain/pocket-ic.ts b/cli/commands/toolchain/pocket-ic.ts index 896b1441..fb92e40b 100644 --- a/cli/commands/toolchain/pocket-ic.ts +++ b/cli/commands/toolchain/pocket-ic.ts @@ -2,6 +2,7 @@ import process from "node:process"; import path from "node:path"; import fs from "node:fs"; +import { cliError } from "../../error.js"; import { globalCacheDir } from "../../mops.js"; import * as toolchainUtils from "./toolchain-utils.js"; @@ -27,8 +28,7 @@ export let download = async ( { silent = false, verbose = false } = {}, ) => { if (!version) { - console.error("version is not defined"); - process.exit(1); + cliError("version is not defined"); } if (isCached(version)) { if (verbose) { diff --git a/cli/commands/toolchain/toolchain-utils.ts b/cli/commands/toolchain/toolchain-utils.ts index 05b3e9cb..7b51be1f 100644 --- a/cli/commands/toolchain/toolchain-utils.ts +++ b/cli/commands/toolchain/toolchain-utils.ts @@ -1,4 +1,3 @@ -import process from "node:process"; import path from "node:path"; import { Buffer } from "node:buffer"; import { unzipSync } from "node:zlib"; @@ -10,6 +9,7 @@ import { deleteSync } from "del"; import { Octokit } from "octokit"; import { extract as extractTar } from "tar"; +import { cliError } from "../../error.js"; import { getRootDir } from "../../mops.js"; export const TOOLCHAINS = ["moc", "wasmtime", "pocket-ic", "lintoko"]; @@ -34,8 +34,7 @@ export let downloadAndExtract = async ( let res = await fetch(url); if (res.status !== 200) { - console.error(`ERROR ${res.status} ${url}`); - process.exit(1); + cliError(`ERROR ${res.status} ${url}`); } let arrayBuffer = await res.arrayBuffer(); @@ -81,8 +80,7 @@ export let getLatestReleaseTag = async (repo: string): Promise => { (release: any) => !release.prerelease && !release.draft, ); if (!release?.tag_name) { - console.error(`Failed to fetch latest release tag for ${repo}`); - process.exit(1); + cliError(`Failed to fetch latest release tag for ${repo}`); } return release.tag_name.replace(/^v/, ""); }; @@ -96,8 +94,7 @@ export let getReleases = async (repo: string) => { }, }); if (res.status !== 200) { - console.log("Releases fetch error"); - process.exit(1); + cliError("Releases fetch error"); } return res.data.map((release: any) => { return { diff --git a/cli/commands/toolchain/wasmtime.ts b/cli/commands/toolchain/wasmtime.ts index ac9e9098..0ebef637 100644 --- a/cli/commands/toolchain/wasmtime.ts +++ b/cli/commands/toolchain/wasmtime.ts @@ -2,6 +2,7 @@ import process from "node:process"; import path from "node:path"; import fs from "fs-extra"; +import { cliError } from "../../error.js"; import { globalCacheDir } from "../../mops.js"; import * as toolchainUtils from "./toolchain-utils.js"; @@ -27,8 +28,7 @@ export let download = async ( { silent = false, verbose = false } = {}, ) => { if (!version) { - console.error("version is not defined"); - process.exit(1); + cliError("version is not defined"); } if (isCached(version)) { if (verbose) { diff --git a/cli/commands/update.ts b/cli/commands/update.ts index 86620874..61cd7681 100644 --- a/cli/commands/update.ts +++ b/cli/commands/update.ts @@ -17,9 +17,7 @@ type UpdateOptions = { }; export async function update(pkg?: string, { lock }: UpdateOptions = {}) { - if (!checkConfigFile()) { - return; - } + checkConfigFile(); let config = readConfig(); if ( diff --git a/cli/commands/user.ts b/cli/commands/user.ts index 6bcce2bf..35aea32d 100644 --- a/cli/commands/user.ts +++ b/cli/commands/user.ts @@ -1,4 +1,3 @@ -import process from "node:process"; import chalk from "chalk"; import fs from "node:fs"; import path from "node:path"; @@ -8,13 +7,13 @@ import { deleteSync } from "del"; import { mainActor } from "../api/actors.js"; import { getIdentity, globalConfigDir } from "../mops.js"; import { encrypt } from "../pem.js"; +import { cliError } from "../error.js"; export async function getUserProp(prop: string) { let actor = await mainActor(); let identity = await getIdentity(); if (!identity) { - console.log(chalk.red("Error: ") + "No identity found"); - process.exit(1); + cliError("Error: No identity found"); } let res = await actor.getUser(identity.getPrincipal()); // @ts-ignore diff --git a/cli/error.ts b/cli/error.ts index 18f3467c..0e1848c3 100644 --- a/cli/error.ts +++ b/cli/error.ts @@ -1,6 +1,37 @@ +import process from "node:process"; import chalk from "chalk"; -export function cliError(...args: unknown[]): never { - console.error(chalk.red(...args)); +export class CliError extends Error { + exitCode: number; + + constructor(message?: string, exitCode = 1) { + super(message ?? ""); + this.name = "CliError"; + this.exitCode = exitCode; + } +} + +export function cliError(message?: string, exitCode = 1): never { + throw new CliError(message, exitCode); +} + +export function cliAbort(message = "aborted"): never { + throw new CliError(message, 0); +} + +export function handleCliError(err: unknown): never { + if (err instanceof CliError) { + if (err.message) { + if (err.exitCode === 0) { + console.log(err.message); + } else { + console.error(chalk.red(err.message)); + } + } + // eslint-disable-next-line no-restricted-properties + process.exit(err.exitCode); + } + console.error(err); + // eslint-disable-next-line no-restricted-properties process.exit(1); } diff --git a/cli/integrity.ts b/cli/integrity.ts index 181f223d..32b5cfd0 100644 --- a/cli/integrity.ts +++ b/cli/integrity.ts @@ -1,8 +1,8 @@ -import process from "node:process"; import fs from "node:fs"; import path from "node:path"; import { sha256 } from "@noble/hashes/sha256"; import { bytesToHex } from "@noble/hashes/utils"; +import { cliError } from "./error.js"; import { getDependencyType, getRootDir, readConfig } from "./mops.js"; import { mainActor } from "./api/actors.js"; import { resolvePackages } from "./resolve-packages.js"; @@ -71,8 +71,7 @@ export function getLocalFileHash(fileId: string): string { let rootDir = getRootDir(); let file = path.join(rootDir, ".mops", fileId); if (!fs.existsSync(file)) { - console.error(`Missing file ${fileId} in .mops dir`); - process.exit(1); + cliError(`Missing file ${fileId} in .mops dir`); } let fileData = fs.readFileSync(file); return bytesToHex(sha256(fileData)); @@ -117,11 +116,10 @@ export async function checkRemote() { let localHash = getLocalFileHash(fileId); if (localHash !== bytesToHex(remoteHash)) { - console.error("Integrity check failed."); console.error( `Mismatched hash for ${fileId}: ${localHash} vs ${bytesToHex(remoteHash)}`, ); - process.exit(1); + cliError("Integrity check failed."); } } } @@ -134,10 +132,9 @@ export function readLockFile(): LockFile | null { try { return JSON.parse(fs.readFileSync(lockFile).toString()) as LockFile; } catch { - console.error( + cliError( "mops.lock is corrupted. Delete it and run `mops install` to regenerate.", ); - process.exit(1); } } return null; @@ -207,8 +204,7 @@ export async function checkLockFile(force = false) { // check if lock file exists if (!fs.existsSync(lockFile)) { if (force) { - console.error("Missing lock file. Run `mops install` to generate it."); - process.exit(1); + cliError("Missing lock file. Run `mops install` to generate it."); } return; } @@ -220,11 +216,10 @@ export async function checkLockFile(force = false) { // check lock file version if (!supportedVersions.includes(lockFileJsonGeneric.version)) { - console.error("Integrity check failed"); console.error( `Invalid lock file version: ${lockFileJsonGeneric.version}. Supported versions: ${supportedVersions.join(", ")}`, ); - process.exit(1); + cliError("Integrity check failed"); } let lockFileJson = lockFileJsonGeneric as LockFile; @@ -232,22 +227,20 @@ export async function checkLockFile(force = false) { // V1: check mops.toml hash if (lockFileJson.version === 1) { if (lockFileJson.mopsTomlHash !== getMopsTomlHash()) { - console.error("Integrity check failed"); console.error("Mismatched mops.toml hash"); console.error(`Locked hash: ${lockFileJson.mopsTomlHash}`); console.error(`Actual hash: ${getMopsTomlHash()}`); - process.exit(1); + cliError("Integrity check failed"); } } // V2, V3: check mops.toml deps hash if (lockFileJson.version === 2 || lockFileJson.version === 3) { if (lockFileJson.mopsTomlDepsHash !== getMopsTomlDepsHash()) { - console.error("Integrity check failed"); console.error("Mismatched mops.toml dependencies hash"); console.error(`Locked hash: ${lockFileJson.mopsTomlDepsHash}`); console.error(`Actual hash: ${getMopsTomlDepsHash()}`); - process.exit(1); + cliError("Integrity check failed"); } } @@ -258,61 +251,55 @@ export async function checkLockFile(force = false) { for (let name of Object.keys(resolvedDeps)) { if (lockedDeps[name] !== resolvedDeps[name]) { - console.error("Integrity check failed"); console.error(`Mismatched package ${name}`); console.error(`Locked: ${lockedDeps[name]}`); console.error(`Actual: ${resolvedDeps[name]}`); - process.exit(1); + cliError("Integrity check failed"); } } } // check number of packages if (Object.keys(lockFileJson.hashes).length !== packageIds.length) { - console.error("Integrity check failed"); console.error( `Mismatched number of resolved packages: ${JSON.stringify(Object.keys(lockFileJson.hashes).length)} vs ${JSON.stringify(packageIds.length)}`, ); - process.exit(1); + cliError("Integrity check failed"); } // check if resolved packages are in the lock file for (let packageId of packageIds) { if (!(packageId in lockFileJson.hashes)) { - console.error("Integrity check failed"); console.error(`Missing package ${packageId} in lock file`); - process.exit(1); + cliError("Integrity check failed"); } } for (let [packageId, hashes] of Object.entries(lockFileJson.hashes)) { // check if package is in resolved packages if (!packageIds.includes(packageId)) { - console.error("Integrity check failed"); console.error( `Package ${packageId} in lock file but not in resolved packages`, ); - process.exit(1); + cliError("Integrity check failed"); } for (let [fileId, lockedHash] of Object.entries(hashes)) { // check if file belongs to package if (!fileId.startsWith(packageId + "/")) { - console.error("Integrity check failed"); console.error( `File ${fileId} in lock file does not belong to package ${packageId}`, ); - process.exit(1); + cliError("Integrity check failed"); } // local file hash vs hash from lock file let localHash = getLocalFileHash(fileId); if (lockedHash !== localHash) { - console.error("Integrity check failed"); console.error(`Mismatched hash for ${fileId}`); console.error(`Locked hash: ${lockedHash}`); console.error(`Actual hash: ${localHash}`); - process.exit(1); + cliError("Integrity check failed"); } } } diff --git a/cli/mops.ts b/cli/mops.ts index 8f26c657..c62fee5c 100644 --- a/cli/mops.ts +++ b/cli/mops.ts @@ -71,8 +71,7 @@ export let getIdentity = async (): Promise => { try { return decodeFile(identityPemEncrypted, res.value); } catch (e) { - console.log(chalk.red("Error: ") + "Invalid password"); - process.exit(1); + cliError("Error: Invalid password"); } } if (fs.existsSync(identityPem)) { @@ -108,17 +107,12 @@ export function resolveConfigPath(configPath: string): string { return path.relative(process.cwd(), path.resolve(getRootDir(), configPath)); } -export function checkConfigFile(exit = false) { +export function checkConfigFile(): true { let configFile = getClosestConfigFile(); if (!configFile) { - console.log( - chalk.red("Error: ") + - `Config file 'mops.toml' not found. Please run ${chalk.green("mops init")} first`, + cliError( + `Config file 'mops.toml' not found. Please run ${chalk.green("mops init")} first`, ); - if (exit) { - process.exit(1); - } - return false; } return true; } diff --git a/cli/resolve-packages.ts b/cli/resolve-packages.ts index 1885919c..e28db45f 100644 --- a/cli/resolve-packages.ts +++ b/cli/resolve-packages.ts @@ -13,13 +13,12 @@ import { Config, Dependency } from "./types.js"; import { getDepCacheDir, getDepCacheName } from "./cache.js"; import { getPackageId } from "./helpers/get-package-id.js"; import { checkLockFileLight, readLockFile } from "./integrity.js"; +import { cliError } from "./error.js"; export async function resolvePackages({ conflicts = "ignore" as "warning" | "error" | "ignore", } = {}): Promise> { - if (!checkConfigFile()) { - return {}; - } + checkConfigFile(); if (checkLockFileLight()) { let lockFileJson = readLockFile(); @@ -208,7 +207,7 @@ export async function resolvePackages({ } if (conflicts === "error" && hasConflicts) { - process.exit(1); + cliError(); } return Object.fromEntries(