From 43c370f9de2a4ecaf04cd2fb0dcc601ddf8c1068 Mon Sep 17 00:00:00 2001 From: Kamil Listopad Date: Wed, 1 Apr 2026 10:40:28 +0200 Subject: [PATCH 1/8] refactor(cli): centralize process.exit through cliError/cliAbort Replace all direct process.exit() calls across 33 CLI files with centralized cliError() and cliAbort() functions from error.ts. - cliError(message?, exitCode?) for error exits (default code 1) - cliAbort(message?) for clean exits (code 0, SIGINT/cancellation) - checkConfigFile() now always exits on failure (removed exit param) - Added ESLint no-restricted-properties rule to ban future process.exit Made-with: Cursor --- cli/.eslintrc.json | 10 +++ cli/cli.ts | 61 ++++++--------- cli/commands/add.ts | 7 +- cli/commands/available-updates.ts | 6 +- cli/commands/bench.ts | 8 +- cli/commands/bump.ts | 14 +--- cli/commands/docs-coverage.ts | 3 +- cli/commands/docs.ts | 7 +- cli/commands/init.ts | 4 +- cli/commands/install/install-all.ts | 4 +- cli/commands/install/install-mops-dep.ts | 7 +- cli/commands/maintainer.ts | 26 ++----- cli/commands/outdated.ts | 4 +- cli/commands/owner.ts | 26 ++----- cli/commands/publish.ts | 91 +++++++---------------- cli/commands/remove.ts | 4 +- cli/commands/replica.ts | 3 +- cli/commands/self.ts | 8 +- cli/commands/sources.ts | 4 +- cli/commands/sync.ts | 4 +- cli/commands/test/test.ts | 24 +++--- cli/commands/toolchain/index.ts | 17 ++--- cli/commands/toolchain/lintoko.ts | 4 +- cli/commands/toolchain/moc.ts | 7 +- cli/commands/toolchain/pocket-ic.ts | 4 +- cli/commands/toolchain/toolchain-utils.ts | 11 +-- cli/commands/toolchain/wasmtime.ts | 4 +- cli/commands/update.ts | 4 +- cli/commands/user.ts | 5 +- cli/error.ts | 17 ++++- cli/integrity.ts | 41 ++++------ cli/mops.ts | 14 +--- cli/resolve-packages.ts | 7 +- package-lock.json | 2 +- 34 files changed, 177 insertions(+), 285 deletions(-) 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..aca2a539 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 } 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,7 +800,7 @@ docsCommand ).default(70), ) .action(async (options) => { - checkConfigFile(true); + checkConfigFile(); await docsCoverage(options); }); program.addCommand(docsCommand); diff --git a/cli/commands/add.ts b/cli/commands/add.ts index 4b8165fc..aa81d950 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) { @@ -105,7 +104,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/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/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..f98d6d5b 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; @@ -79,8 +80,7 @@ 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); + cliError("Error: " + text); } if ( text.includes("No such file or directory") || @@ -100,8 +100,7 @@ export async function docs(options: Partial = {}) { return; } if (code !== 0) { - console.log(chalk.red("Error:"), code, stderr); - process.exit(1); + cliError("Error: " + code + " " + stderr); } 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..ba99e90b 100644 --- a/cli/commands/install/install-mops-dep.ts +++ b/cli/commands/install/install-mops-dep.ts @@ -20,6 +20,7 @@ import { } from "../../api/downloadPackageFiles.js"; import { installDeps } from "./install-deps.js"; import { getDepName } from "../../helpers/get-dep-name.js"; +import { cliAbort } from "../../error.js"; type InstallMopsDepOptions = { verbose?: boolean; @@ -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,7 +101,7 @@ export async function installMopsDep( let onSigInt = () => { deleteSync([cacheDir], { force: true }); - process.exit(); + cliAbort(""); }; process.on("SIGINT", onSigInt); 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..bc0ce04c 100644 --- a/cli/commands/replica.ts +++ b/cli/commands/replica.ts @@ -21,6 +21,7 @@ import { } from "../helpers/pocket-ic-client.js"; import { toolchain } from "./toolchain/index.js"; import { getDfxVersion } from "../helpers/get-dfx-version.js"; +import { cliError } from "../error.js"; type StartOptions = { type?: "dfx" | "pocket-ic" | "dfx-pocket-ic"; @@ -93,7 +94,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"); - process.exit(11); + cliError(undefined, 11); } }); diff --git a/cli/commands/self.ts b/cli/commands/self.ts index d99d9fec..43a771d9 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 } 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"; @@ -53,8 +52,7 @@ export async function update() { proc.on("exit", (res) => { if (res !== 0) { - console.log(chalk.red("Failed to update.")); - process.exit(1); + cliError("Failed to update."); } console.log(chalk.green("Success")); }); 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..b18ec713 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 { cliAbort, 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"; } @@ -88,18 +86,17 @@ export async function test(filter = "", options: Partial = {}) { let sigint = false; process.on("SIGINT", () => { if (sigint) { - console.log("Force exit"); - process.exit(0); + cliAbort("Force exit"); } sigint = true; if (replicaStartPromise) { console.log("Stopping replica..."); replica.stop(true).then(() => { - process.exit(0); + cliAbort(); }); } else { - process.exit(0); + cliAbort(); } }); @@ -152,7 +149,7 @@ export async function test(filter = "", options: Partial = {}) { replicaType, ); if (!passed) { - process.exit(1); + cliError(); } } } @@ -361,12 +358,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 }); diff --git a/cli/commands/toolchain/index.ts b/cli/commands/toolchain/index.ts index b80ba127..5d36ec61 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 } 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,7 +89,7 @@ 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 {} @@ -119,7 +118,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 +321,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 +357,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 +389,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..5be72aa3 100644 --- a/cli/error.ts +++ b/cli/error.ts @@ -1,6 +1,17 @@ import chalk from "chalk"; -export function cliError(...args: unknown[]): never { - console.error(chalk.red(...args)); - process.exit(1); +export function cliError(message?: string, exitCode = 1): never { + if (message) { + console.error(chalk.red(message)); + } + // eslint-disable-next-line no-restricted-properties + process.exit(exitCode); +} + +export function cliAbort(message = "aborted"): never { + if (message !== "") { + console.log(message); + } + // eslint-disable-next-line no-restricted-properties + process.exit(0); } 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( diff --git a/package-lock.json b/package-lock.json index 9cca6501..c2d49b0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "mops", + "name": "aii", "lockfileVersion": 3, "requires": true, "packages": { From 83b3f5271194e339520d73ba8589864970e1aecc Mon Sep 17 00:00:00 2001 From: Kamil Listopad Date: Wed, 1 Apr 2026 11:32:27 +0200 Subject: [PATCH 2/8] refactor(cli): throw CliError instead of calling process.exit Replace the process.exit() calls in cliError/cliAbort with throwing a CliError exception. process.exit() is now only called in handleCliError (the top-level catch in cli.ts) and in SIGINT/detached event handlers where exceptions cannot propagate. This ensures finally clauses in called functions run when a CLI error is raised. - cliError/cliAbort throw CliError - program.parseAsync().catch(handleCliError) is the single exit point - docs.ts: Promise now uses reject(new CliError(...)) from event handlers - self.ts: proc.on("exit") wrapped in awaitable Promise with reject - replica.ts: detached stderr handler uses process.exit(11) + eslint-disable - Fix package-lock.json name field (worktree artifact) Made-with: Cursor --- cli/cli.ts | 4 +-- cli/commands/docs.ts | 11 +++++--- cli/commands/install/install-mops-dep.ts | 4 +-- cli/commands/replica.ts | 5 ++-- cli/commands/self.ts | 16 +++++++---- cli/commands/test/test.ts | 12 +++++--- cli/error.ts | 35 ++++++++++++++++++------ package-lock.json | 2 +- 8 files changed, 59 insertions(+), 30 deletions(-) diff --git a/cli/cli.ts b/cli/cli.ts index aca2a539..6ec53490 100755 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -4,7 +4,7 @@ import fs from "node:fs"; import process from "node:process"; import { resolve } from "node:path"; -import { cliError } from "./error.js"; +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"; @@ -805,4 +805,4 @@ docsCommand }); program.addCommand(docsCommand); -program.parse(); +program.parseAsync().catch(handleCliError); diff --git a/cli/commands/docs.ts b/cli/commands/docs.ts index f98d6d5b..5463a61b 100644 --- a/cli/commands/docs.ts +++ b/cli/commands/docs.ts @@ -9,7 +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 { CliError } from "../error.js"; import { toolchain } from "./toolchain/index.js"; let moDocPath: string; @@ -55,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}`, @@ -80,7 +80,9 @@ export async function docs(options: Partial = {}) { proc.stderr.on("data", (data) => { let text = data.toString().trim(); if (text.includes("syntax error")) { - cliError("Error: " + text); + proc.kill(); + reject(new CliError("Error: " + text)); + return; } if ( text.includes("No such file or directory") || @@ -100,7 +102,8 @@ export async function docs(options: Partial = {}) { return; } if (code !== 0) { - cliError("Error: " + code + " " + stderr); + reject(new CliError("Error: " + code + " " + stderr)); + return; } resolve(); }); diff --git a/cli/commands/install/install-mops-dep.ts b/cli/commands/install/install-mops-dep.ts index ba99e90b..44f9a0ea 100644 --- a/cli/commands/install/install-mops-dep.ts +++ b/cli/commands/install/install-mops-dep.ts @@ -20,7 +20,6 @@ import { } from "../../api/downloadPackageFiles.js"; import { installDeps } from "./install-deps.js"; import { getDepName } from "../../helpers/get-dep-name.js"; -import { cliAbort } from "../../error.js"; type InstallMopsDepOptions = { verbose?: boolean; @@ -101,7 +100,8 @@ export async function installMopsDep( let onSigInt = () => { deleteSync([cacheDir], { force: true }); - cliAbort(""); + // eslint-disable-next-line no-restricted-properties + process.exit(); }; process.on("SIGINT", onSigInt); diff --git a/cli/commands/replica.ts b/cli/commands/replica.ts index bc0ce04c..4c8c3e4a 100644 --- a/cli/commands/replica.ts +++ b/cli/commands/replica.ts @@ -21,8 +21,6 @@ import { } from "../helpers/pocket-ic-client.js"; import { toolchain } from "./toolchain/index.js"; import { getDfxVersion } from "../helpers/get-dfx-version.js"; -import { cliError } from "../error.js"; - type StartOptions = { type?: "dfx" | "pocket-ic" | "dfx-pocket-ic"; dir?: string; @@ -94,7 +92,8 @@ 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"); - cliError(undefined, 11); + // eslint-disable-next-line no-restricted-properties + process.exit(11); } }); diff --git a/cli/commands/self.ts b/cli/commands/self.ts index 43a771d9..d8cc6448 100644 --- a/cli/commands/self.ts +++ b/cli/commands/self.ts @@ -1,7 +1,7 @@ import child_process, { execSync } from "node:child_process"; import chalk from "chalk"; import { version, globalConfigDir } from "../mops.js"; -import { cliError } from "../error.js"; +import { CliError, cliError } from "../error.js"; import { cleanCache } from "../cache.js"; import { toolchain } from "./toolchain/index.js"; @@ -50,11 +50,15 @@ export async function update() { { stdio: "inherit", detached: false }, ); - proc.on("exit", (res) => { - if (res !== 0) { - cliError("Failed to update."); - } - 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/test/test.ts b/cli/commands/test/test.ts index b18ec713..57fe8735 100644 --- a/cli/commands/test/test.ts +++ b/cli/commands/test/test.ts @@ -33,7 +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 { cliAbort, cliError } from "../../error.js"; +import { cliError } from "../../error.js"; type ReporterName = "verbose" | "files" | "compact" | "silent"; type ReplicaName = "dfx" | "pocket-ic" | "dfx-pocket-ic"; @@ -86,17 +86,21 @@ export async function test(filter = "", options: Partial = {}) { let sigint = false; process.on("SIGINT", () => { if (sigint) { - cliAbort("Force exit"); + console.log("Force exit"); + // eslint-disable-next-line no-restricted-properties + process.exit(0); } sigint = true; if (replicaStartPromise) { console.log("Stopping replica..."); replica.stop(true).then(() => { - cliAbort(); + // eslint-disable-next-line no-restricted-properties + process.exit(0); }); } else { - cliAbort(); + // eslint-disable-next-line no-restricted-properties + process.exit(0); } }); diff --git a/cli/error.ts b/cli/error.ts index 5be72aa3..f6ee1e02 100644 --- a/cli/error.ts +++ b/cli/error.ts @@ -1,17 +1,36 @@ import chalk from "chalk"; -export function cliError(message?: string, exitCode = 1): never { - if (message) { - console.error(chalk.red(message)); +export class CliError extends Error { + exitCode: number; + + constructor(message?: string, exitCode = 1) { + super(message ?? ""); + this.name = "CliError"; + this.exitCode = exitCode; } - // eslint-disable-next-line no-restricted-properties - process.exit(exitCode); +} + +export function cliError(message?: string, exitCode = 1): never { + throw new CliError(message, exitCode); } export function cliAbort(message = "aborted"): never { - if (message !== "") { - console.log(message); + 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(0); + process.exit(1); } diff --git a/package-lock.json b/package-lock.json index c2d49b0f..9cca6501 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "aii", + "name": "mops", "lockfileVersion": 3, "requires": true, "packages": { From a35c6238c267c6840f70a203c3b232e7580f7834 Mon Sep 17 00:00:00 2001 From: Kamil Listopad Date: Wed, 1 Apr 2026 11:38:53 +0200 Subject: [PATCH 3/8] fix(cli): move cliError() outside empty catch{} in toolchain init The empty catch{} was there to handle 'which mocv' failing (not installed). Now that cliError() throws instead of calling process.exit(), the CliError was being silently swallowed by that same catch block. Fix by detecting mocv in the try/catch using a flag, then calling cliError() outside it. Made-with: Cursor --- cli/commands/toolchain/index.ts | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/cli/commands/toolchain/index.ts b/cli/commands/toolchain/index.ts index 5d36ec61..73f5e3fb 100644 --- a/cli/commands/toolchain/index.ts +++ b/cli/commands/toolchain/index.ts @@ -75,25 +75,30 @@ async function init({ reset = false, silent = false } = {}) { cliError("Windows is not supported. Please use WSL"); } + let mocvDetected = false; try { let res = execSync("which mocv").toString().trim(); if (res) { - console.error( - "Mops is not compatible with mocv. Please uninstall mocv and try again.", - ); - console.log("Steps to uninstall mocv:"); - console.log('1. Run "mocv reset"'); - console.log('2. Run "npm uninstall -g mocv"'); - console.log( - 'TIP: Alternative to "mocv use " is "mops toolchain use moc " (installs moc only for current project)', - ); - console.log("TIP: More details at https://docs.mops.one/cli/toolchain"); - if (!process.env.CI || !silent) { - cliError(); - } + mocvDetected = true; } } catch {} + if (mocvDetected) { + console.error( + "Mops is not compatible with mocv. Please uninstall mocv and try again.", + ); + console.log("Steps to uninstall mocv:"); + console.log('1. Run "mocv reset"'); + console.log('2. Run "npm uninstall -g mocv"'); + console.log( + 'TIP: Alternative to "mocv use " is "mops toolchain use moc " (installs moc only for current project)', + ); + console.log("TIP: More details at https://docs.mops.one/cli/toolchain"); + if (!process.env.CI || !silent) { + cliError(); + } + } + let zshrc = path.join(os.homedir(), ".zshrc"); let bashrc = path.join(os.homedir(), ".bashrc"); let bashProfile = path.join(os.homedir(), ".bash_profile"); From bee24287f303136a7fb9b6ea12957dd82e41d6e1 Mon Sep 17 00:00:00 2001 From: Kamil Listopad Date: Wed, 1 Apr 2026 11:44:00 +0200 Subject: [PATCH 4/8] fix(cli): re-throw CliError in subprocess catch blocks Catch blocks designed to handle subprocess/runtime errors (execa, spawn) were also swallowing CliError instances thrown by cliError() calls within the same try block. Now that cliError() throws instead of process.exit(), the CliError propagated into these handlers and got wrapped in a new 'Error while running X' message. Fix: add 'if (err instanceof CliError) throw err' at the top of each affected catch block so CliError passes through unchanged. Files affected: build.ts, check.ts, check-candid.ts, lint.ts All 63 tests pass after this fix. Made-with: Cursor --- cli/commands/build.ts | 7 +++++-- cli/commands/check-candid.ts | 5 ++++- cli/commands/check.ts | 5 ++++- cli/commands/lint.ts | 5 ++++- 4 files changed, 17 insertions(+), 5 deletions(-) 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/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/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}` : ""}`, ); From b0892b55bfae184195f6a551356ecf266090f784 Mon Sep 17 00:00:00 2001 From: Kamil Listopad Date: Wed, 1 Apr 2026 11:50:28 +0200 Subject: [PATCH 5/8] fix(cli): revert toolchain mocv detection restructure Use the simpler fix: re-throw CliError in the catch block, keeping the original try/catch structure unchanged. Made-with: Cursor --- cli/commands/toolchain/index.ts | 35 ++++++++++++++++----------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/cli/commands/toolchain/index.ts b/cli/commands/toolchain/index.ts index 73f5e3fb..3f8335d1 100644 --- a/cli/commands/toolchain/index.ts +++ b/cli/commands/toolchain/index.ts @@ -6,7 +6,7 @@ import { execSync } from "node:child_process"; import chalk from "chalk"; import prompts from "prompts"; import { createLogUpdate } from "log-update"; -import { cliError } from "../../error.js"; +import { CliError, cliError } from "../../error.js"; import { checkConfigFile, getClosestConfigFile, @@ -75,27 +75,26 @@ async function init({ reset = false, silent = false } = {}) { cliError("Windows is not supported. Please use WSL"); } - let mocvDetected = false; try { let res = execSync("which mocv").toString().trim(); if (res) { - mocvDetected = true; + console.error( + "Mops is not compatible with mocv. Please uninstall mocv and try again.", + ); + console.log("Steps to uninstall mocv:"); + console.log('1. Run "mocv reset"'); + console.log('2. Run "npm uninstall -g mocv"'); + console.log( + 'TIP: Alternative to "mocv use " is "mops toolchain use moc " (installs moc only for current project)', + ); + console.log("TIP: More details at https://docs.mops.one/cli/toolchain"); + if (!process.env.CI || !silent) { + cliError(); + } } - } catch {} - - if (mocvDetected) { - console.error( - "Mops is not compatible with mocv. Please uninstall mocv and try again.", - ); - console.log("Steps to uninstall mocv:"); - console.log('1. Run "mocv reset"'); - console.log('2. Run "npm uninstall -g mocv"'); - console.log( - 'TIP: Alternative to "mocv use " is "mops toolchain use moc " (installs moc only for current project)', - ); - console.log("TIP: More details at https://docs.mops.one/cli/toolchain"); - if (!process.env.CI || !silent) { - cliError(); + } catch (err) { + if (err instanceof CliError) { + throw err; } } From 6b7385148b411a77074bf22b5e787a1fe4b9fcb4 Mon Sep 17 00:00:00 2001 From: Kamil Listopad Date: Wed, 1 Apr 2026 12:03:58 +0200 Subject: [PATCH 6/8] fix(cli): propagate CliError through Promise chains and async catch blocks - Add `import process` to error.ts for consistency with rest of codebase - Fix unhandled rejection in test.ts wasi/replica chains: add reject param to outer Promise and use .then(resolve, reject) instead of .then(resolve) - Add CliError re-throw guards to install-mops-dep.ts catch blocks to prevent silent swallowing, consistent with build.ts/check.ts pattern Made-with: Cursor --- cli/commands/install/install-mops-dep.ts | 7 +++++++ cli/commands/test/test.ts | 6 +++--- cli/error.ts | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/cli/commands/install/install-mops-dep.ts b/cli/commands/install/install-mops-dep.ts index 44f9a0ea..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"; @@ -120,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; @@ -127,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/test/test.ts b/cli/commands/test/test.ts index 57fe8735..23ecafa8 100644 --- a/cli/commands/test/test.ts +++ b/cli/commands/test/test.ts @@ -295,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", @@ -380,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") { @@ -467,7 +467,7 @@ export async function testWithReporter( globalThis.mopsReplicaTestRunning = false; fs.rmSync(wasmFile, { force: true }); }) - .then(resolve); + .then(resolve, reject); } }); diff --git a/cli/error.ts b/cli/error.ts index f6ee1e02..0e1848c3 100644 --- a/cli/error.ts +++ b/cli/error.ts @@ -1,3 +1,4 @@ +import process from "node:process"; import chalk from "chalk"; export class CliError extends Error { From ba4fc4edaa399c79832f44ad31282f070ceb5d5b Mon Sep 17 00:00:00 2001 From: Kamil Listopad Date: Wed, 1 Apr 2026 12:09:13 +0200 Subject: [PATCH 7/8] fix(cli): propagate rejection in interpreter test branch Made-with: Cursor --- cli/commands/test/test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/commands/test/test.ts b/cli/commands/test/test.ts index 23ecafa8..c3a3e8fb 100644 --- a/cli/commands/test/test.ts +++ b/cli/commands/test/test.ts @@ -315,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") { From 17149715f1fd82e33d77514c901dfc0920bb7d92 Mon Sep 17 00:00:00 2001 From: Kamil Listopad Date: Wed, 1 Apr 2026 14:06:33 +0200 Subject: [PATCH 8/8] fix(cli): cliError on package not found in mops add Made-with: Cursor --- cli/commands/add.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cli/commands/add.ts b/cli/commands/add.ts index aa81d950..1be62e6a 100644 --- a/cli/commands/add.ts +++ b/cli/commands/add.ts @@ -86,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; }