diff --git a/src/cli/cmd-add.ts b/src/cli/cmd-add.ts index 9e27e231..7ba574ec 100644 --- a/src/cli/cmd-add.ts +++ b/src/cli/cmd-add.ts @@ -1,54 +1,51 @@ +import { Argument, Command, Option } from "@commander-js/extra-typings"; import { Logger } from "npmlog"; import { addDependenciesUsing } from "../app/add-dependencies"; import { determineEditorVersionUsing } from "../app/determine-editor-version"; import { loadRegistryAuthUsing } from "../app/get-registry-auth"; import { DebugLog } from "../domain/logging"; -import { - makePackageReference, - PackageReference, -} from "../domain/package-reference"; +import { makePackageReference } from "../domain/package-reference"; import { recordEntries } from "../domain/record-utils"; import { getHomePathFromEnv } from "../domain/special-paths"; import { getUserUpmConfigPathFor } from "../domain/upm-config"; import type { ReadTextFile, WriteTextFile } from "../io/fs"; import type { GetRegistryPackument } from "../io/registry"; import type { CheckUrlExists } from "../io/www"; -import { CmdOptions } from "./options"; +import { eachValue } from "./cli-parsing"; +import { withErrorLogger } from "./error-logging"; +import type { GlobalOptions } from "./options"; import { parseEnvUsing } from "./parse-env"; -import { ResultCodes } from "./result-codes"; +import { mustBePackageReference } from "./validators"; -/** - * Options passed to the add command. - */ -export type AddOptions = CmdOptions<{ - /** - * Whether to also add the packages to testables. - */ - test?: boolean; - /** - * Whether to run with force. This will add packages even if validation - * was not possible. - */ - force?: boolean; -}>; +const pkgArg = new Argument( + "", + "Reference to the package that should be added" +).argParser(mustBePackageReference); -/** - * The different command result codes for the add command. - */ -export type AddResultCode = ResultCodes.Ok | ResultCodes.Error; +const otherPkgsArg = new Argument( + "[otherPkgs...]", + "References to additional packages that should be added" +).argParser(eachValue(mustBePackageReference)); -/** - * Cmd-handler for adding packages. - * @param pkgs One or multiple references to packages to add. - * @param options Options specifying how to add the packages. - */ -type AddCmd = ( - pkgs: PackageReference | PackageReference[], - options: AddOptions -) => Promise; +const addTestableOpt = new Option( + "-t, --test", + "add package as testable" +).default(false); + +const forceOpt = new Option( + "-f, --force", + "force add package if missing deps or editor version is not qualified" +).default(false); /** - * Makes a {@link AddCmd} function. + * Makes the `openupm add` cli command with the given dependencies. + * @param checkUrlExists IO function to check whether a url exists. + * @param fetchPackument IO function for fetching a packument. + * @param readTextFile IO function for reading a text file. + * @param writeTextFile IO function for writing a text file. + * @param log Logger for cli output. + * @param debugLog IO function for debug-logs. + * @returns The command. */ export function makeAddCmd( checkUrlExists: CheckUrlExists, @@ -57,78 +54,97 @@ export function makeAddCmd( writeTextFile: WriteTextFile, log: Logger, debugLog: DebugLog -): AddCmd { - return async (pkgs, options) => { - if (!Array.isArray(pkgs)) pkgs = [pkgs]; +) { + return new Command("add") + .aliases(["install", "i"]) + .addArgument(pkgArg) + .addArgument(otherPkgsArg) + .addOption(addTestableOpt) + .addOption(forceOpt) + .description( + `add package to manifest json +openupm add [otherPkgs...] +openupm add @ [otherPkgs...]` + ) + .action( + withErrorLogger(log, async function (pkg, otherPkgs, addOptions, cmd) { + const globalOptions = cmd.optsWithGlobals(); - // parse env - const env = await parseEnvUsing(log, process.env, process.cwd(), options); + const pkgs = [pkg].concat(otherPkgs); - const editorVersion = await determineEditorVersionUsing( - readTextFile, - debugLog, - env.cwd - ); + // parse env + const env = await parseEnvUsing( + log, + process.env, + process.cwd(), + globalOptions + ); - if (typeof editorVersion === "string") - log.warn( - "editor.version", - `${editorVersion} is unknown, the editor version check is disabled` - ); + const editorVersion = await determineEditorVersionUsing( + readTextFile, + debugLog, + env.cwd + ); - const projectDirectory = env.cwd; + if (typeof editorVersion === "string") + log.warn( + "editor.version", + `${editorVersion} is unknown, the editor version check is disabled` + ); - const homePath = getHomePathFromEnv(process.env); - const upmConfigPath = getUserUpmConfigPathFor( - process.env, - homePath, - env.systemUser - ); + const projectDirectory = env.cwd; - const primaryRegistry = await loadRegistryAuthUsing( - readTextFile, - debugLog, - upmConfigPath, - env.primaryRegistryUrl - ); + const homePath = getHomePathFromEnv(process.env); + const upmConfigPath = getUserUpmConfigPathFor( + process.env, + homePath, + env.systemUser + ); - const addResults = await addDependenciesUsing( - readTextFile, - writeTextFile, - fetchPackument, - checkUrlExists, - debugLog, - projectDirectory, - typeof editorVersion === "string" ? null : editorVersion, - primaryRegistry, - env.upstream, - options.force === true, - options.test === true, - pkgs - ); + const primaryRegistry = await loadRegistryAuthUsing( + readTextFile, + debugLog, + upmConfigPath, + env.primaryRegistryUrl + ); - recordEntries(addResults) - .map(([packageName, addResult]) => { - switch (addResult.type) { - case "added": - return `added ${makePackageReference( - packageName, - addResult.version - )}`; - case "upgraded": - return `modified ${packageName} ${addResult.fromVersion} => ${addResult.toVersion}`; - case "noChange": - return `existed ${makePackageReference( - packageName, - addResult.version - )}`; - } - }) - .forEach((message) => { - log.notice("", message); - }); + const addResults = await addDependenciesUsing( + readTextFile, + writeTextFile, + fetchPackument, + checkUrlExists, + debugLog, + projectDirectory, + typeof editorVersion === "string" ? null : editorVersion, + primaryRegistry, + env.upstream, + addOptions.force, + addOptions.test, + pkgs + ); - log.notice("", "please open Unity project to apply changes."); - return ResultCodes.Ok; - }; + recordEntries(addResults) + .map(([packageName, addResult]) => { + switch (addResult.type) { + case "added": + return `added ${makePackageReference( + packageName, + addResult.version + )}`; + case "upgraded": + return `modified ${packageName} ${addResult.fromVersion} => ${addResult.toVersion}`; + case "noChange": + return `existed ${makePackageReference( + packageName, + addResult.version + )}`; + } + }) + .forEach((message) => { + log.notice("", message); + }); + + log.notice("", "please open Unity project to apply changes."); + }) + ); } diff --git a/src/cli/cmd-deps.ts b/src/cli/cmd-deps.ts index a9898f10..1b27e7a8 100644 --- a/src/cli/cmd-deps.ts +++ b/src/cli/cmd-deps.ts @@ -1,3 +1,4 @@ +import { Argument, Command, Option } from "@commander-js/extra-typings"; import chalk from "chalk"; import { Logger } from "npmlog"; import os from "os"; @@ -9,7 +10,6 @@ import { PackumentNotFoundError } from "../domain/common-errors"; import { DebugLog } from "../domain/logging"; import { makePackageReference, - PackageReference, splitPackageReference, } from "../domain/package-reference"; import { PackageUrl } from "../domain/package-url"; @@ -22,37 +22,28 @@ import type { ReadTextFile } from "../io/fs"; import type { GetRegistryPackument } from "../io/registry"; import type { CheckUrlExists } from "../io/www"; import { stringifyDependencyGraph } from "./dependency-logging"; -import { CmdOptions } from "./options"; +import { withErrorLogger } from "./error-logging"; +import { GlobalOptions } from "./options"; import { parseEnvUsing } from "./parse-env"; import { ResultCodes } from "./result-codes"; +import { mustBePackageReference } from "./validators"; -/** - * Options passed to the deps command. - */ -export type DepsOptions = CmdOptions<{ - /** - * Whether to print only direct or deep dependencies. - */ - deep?: boolean; -}>; - -/** - * The possible result codes with which the deps command can exit. - */ -export type DepsResultCode = ResultCodes.Ok | ResultCodes.Error; +const pkgArg = new Argument("", "Reference to a package").argParser( + mustBePackageReference +); -/** - * Cmd-handler for listing dependencies for a package. - * @param pkg Reference to a package. - * @param options Command options. - */ -export type DepsCmd = ( - pkg: PackageReference, - options: DepsOptions -) => Promise; +const deepOpt = new Option( + "-d, --deep", + "view package dependencies recursively" +).default(false); /** - * Makes a {@link DepsCmd} function. + * Makes the `openupm deps` command with the given dependencies. + * @param readTextFile IO function for reading text files. + * @param fetchPackument IO function for fetching remote packuments. + * @param checkUrlExists IO function for checking whether a url exists. + * @param log Logger for printing output. + * @param debugLog IO function for printing debug logs. */ export function makeDepsCmd( readTextFile: ReadTextFile, @@ -60,68 +51,90 @@ export function makeDepsCmd( checkUrlExists: CheckUrlExists, log: Logger, debugLog: DebugLog -): DepsCmd { - return async (pkg, options) => { - // parse env - const env = await parseEnvUsing(log, process.env, process.cwd(), options); +) { + return new Command("deps") + .alias("dep") + .addArgument(pkgArg) + .addOption(deepOpt) + .description( + `view package dependencies +openupm deps +openupm deps @` + ) + .action( + withErrorLogger(log, async function (pkg, depsOptions, cmd) { + const globalOptions = cmd.optsWithGlobals(); - const homePath = getHomePathFromEnv(process.env); - const upmConfigPath = getUserUpmConfigPathFor( - process.env, - homePath, - env.systemUser - ); - const primaryRegistry = await loadRegistryAuthUsing( - readTextFile, - debugLog, - upmConfigPath, - env.primaryRegistryUrl - ); - const sources = [primaryRegistry, unityRegistry]; + // parse env + const env = await parseEnvUsing( + log, + process.env, + process.cwd(), + globalOptions + ); - const [packageName, requestedVersion] = splitPackageReference(pkg); + const homePath = getHomePathFromEnv(process.env); + const upmConfigPath = getUserUpmConfigPathFor( + process.env, + homePath, + env.systemUser + ); + const primaryRegistry = await loadRegistryAuthUsing( + readTextFile, + debugLog, + upmConfigPath, + env.primaryRegistryUrl + ); + const sources = [primaryRegistry, unityRegistry]; - if (requestedVersion !== undefined && isZod(requestedVersion, PackageUrl)) { - log.error("", "cannot get dependencies for url-version"); - return ResultCodes.Error; - } + const [packageName, requestedVersion] = splitPackageReference(pkg); - const latestVersion = - requestedVersion !== undefined && isZod(requestedVersion, SemanticVersion) - ? requestedVersion - : ( - await queryAllRegistriesLazy(sources, (source) => - fetchLatestPackumentVersionUsing( - fetchPackument, - source, - packageName - ) - ) - )?.value ?? null; + if ( + requestedVersion !== undefined && + isZod(requestedVersion, PackageUrl) + ) { + log.error("", "cannot get dependencies for url-version"); + return process.exit(ResultCodes.Error); + } - if (latestVersion === null) throw new PackumentNotFoundError(packageName); + const latestVersion = + requestedVersion !== undefined && + isZod(requestedVersion, SemanticVersion) + ? requestedVersion + : ( + await queryAllRegistriesLazy(sources, (source) => + fetchLatestPackumentVersionUsing( + fetchPackument, + source, + packageName + ) + ) + )?.value ?? null; - const deep = options.deep || false; - await debugLog( - `fetch: ${makePackageReference(packageName, latestVersion)}, deep=${deep}` - ); - const dependencyGraph = await resolveDependenciesUsing( - checkUrlExists, - fetchPackument, - sources, - packageName, - latestVersion, - deep - ); + if (latestVersion === null) + throw new PackumentNotFoundError(packageName); - const output = stringifyDependencyGraph( - dependencyGraph, - packageName, - latestVersion, - chalk - ).join(os.EOL); - log.notice("", output); + await debugLog( + `fetch: ${makePackageReference(packageName, latestVersion)}, deep=${ + depsOptions.deep + }` + ); + const dependencyGraph = await resolveDependenciesUsing( + checkUrlExists, + fetchPackument, + sources, + packageName, + latestVersion, + depsOptions.deep + ); - return ResultCodes.Ok; - }; + const output = stringifyDependencyGraph( + dependencyGraph, + packageName, + latestVersion, + chalk + ).join(os.EOL); + log.notice("", output); + }) + ); } diff --git a/src/cli/cmd-login.ts b/src/cli/cmd-login.ts index a185c4c7..2a00f607 100644 --- a/src/cli/cmd-login.ts +++ b/src/cli/cmd-login.ts @@ -1,3 +1,4 @@ +import { Command, Option } from "@commander-js/extra-typings"; import { Logger } from "npmlog"; import { loginUsing } from "../app/login"; import { partialApply } from "../domain/fp-utils"; @@ -7,7 +8,8 @@ import { getHomePathFromEnv } from "../domain/special-paths"; import { getUserUpmConfigPathFor } from "../domain/upm-config"; import type { ReadTextFile, WriteTextFile } from "../io/fs"; import type { GetAuthToken } from "../io/registry"; -import { CmdOptions } from "./options"; +import { withErrorLogger } from "./error-logging"; +import { GlobalOptions } from "./options"; import { parseEnvUsing } from "./parse-env"; import { promptEmail, @@ -15,46 +17,28 @@ import { promptRegistryUrl, promptUsername, } from "./prompts"; -import { ResultCodes } from "./result-codes"; -/** - * Options for logging in a user. These come from the CLI. - * All properties are optional. If missing they will either be prompted - * from the user or get default values. - */ -export type LoginOptions = CmdOptions<{ - /** - * The username to log in with. - */ - username?: string; - /** - * The password to log in with. - */ - password?: string; - /** - * The email to log in with. - */ - email?: string; - /** - * Whether to use basic or token-based authentication. - */ - basicAuth?: boolean; - /** - * Whether to always authenticate. - */ - alwaysAuth?: boolean; -}>; +const usernameOpt = new Option("-u, --username ", "username").default( + null +); -/** - * The possible result codes with which the login command can exit. - */ -export type LoginResultCode = ResultCodes.Ok | ResultCodes.Error; +const passwordOpt = new Option("-p, --password ", "password").default( + null +); -/** - * Cmd-handler for logging in users. - * @param options Options for logging in. - */ -export type LoginCmd = (options: LoginOptions) => Promise; +const emailOpt = new Option("-e, --email ", "email address").default( + null +); + +const basicAuthOpt = new Option( + "--basic-auth", + "use basic authentication instead of token" +).default(false); + +const alwaysAuthOpt = new Option( + "--always-auth", + "always auth for tarball hosted on a different domain" +).default(false); /** * Makes a {@link LoginCmd} function. @@ -66,7 +50,7 @@ export function makeLoginCmd( writeTextFile: WriteTextFile, debugLog: DebugLog, log: Logger -): LoginCmd { +) { const login = partialApply( loginUsing, homePath, @@ -76,41 +60,55 @@ export function makeLoginCmd( debugLog ); - return async (options) => { - // parse env - const env = await parseEnvUsing(log, process.env, process.cwd(), options); + return new Command("login") + .aliases(["add-user", "adduser"]) + .addOption(usernameOpt) + .addOption(passwordOpt) + .addOption(emailOpt) + .addOption(basicAuthOpt) + .addOption(alwaysAuthOpt) + .description("authenticate with a scoped registry") + .action( + withErrorLogger(log, async function (loginOptions, cmd) { + const globalOptions = cmd.optsWithGlobals(); - const homePath = getHomePathFromEnv(process.env); - const upmConfigPath = getUserUpmConfigPathFor( - process.env, - homePath, - env.systemUser - ); + // parse env + const env = await parseEnvUsing( + log, + process.env, + process.cwd(), + globalOptions + ); - // query parameters - const username = options.username ?? (await promptUsername()); - const password = options.password ?? (await promptPassword()); - const email = options.email ?? (await promptEmail()); + const homePath = getHomePathFromEnv(process.env); + const upmConfigPath = getUserUpmConfigPathFor( + process.env, + homePath, + env.systemUser + ); - const loginRegistry = - options.registry !== undefined - ? coerceRegistryUrl(options.registry) - : await promptRegistryUrl(); + // query parameters + const username = loginOptions.username ?? (await promptUsername()); + const password = loginOptions.password ?? (await promptPassword()); + const email = loginOptions.email ?? (await promptEmail()); - const alwaysAuth = options.alwaysAuth || false; + const loginRegistry = + globalOptions.registry !== undefined + ? coerceRegistryUrl(globalOptions.registry) + : await promptRegistryUrl(); - await login( - username, - password, - email, - alwaysAuth, - loginRegistry, - upmConfigPath, - options.basicAuth ? "basic" : "token" - ); + await login( + username, + password, + email, + loginOptions.alwaysAuth, + loginRegistry, + upmConfigPath, + loginOptions.basicAuth ? "basic" : "token" + ); - log.notice("auth", `you are authenticated as '${username}'`); - log.notice("config", "saved unity config at " + upmConfigPath); - return ResultCodes.Ok; - }; + log.notice("auth", `you are authenticated as '${username}'`); + log.notice("config", "saved unity config at " + upmConfigPath); + }) + ); } diff --git a/src/cli/cmd-remove.ts b/src/cli/cmd-remove.ts index 367eadd9..55c4b338 100644 --- a/src/cli/cmd-remove.ts +++ b/src/cli/cmd-remove.ts @@ -1,44 +1,40 @@ +import { Argument, Command } from "@commander-js/extra-typings"; import { Logger } from "npmlog"; import { removeDependenciesUsing } from "../app/remove-dependencies"; -import { DomainName } from "../domain/domain-name"; +import { partialApply } from "../domain/fp-utils"; +import type { DebugLog } from "../domain/logging"; import { makePackageReference } from "../domain/package-reference"; import type { ReadTextFile, WriteTextFile } from "../io/fs"; -import type { DebugLog } from "../domain/logging"; -import { partialApply } from "../domain/fp-utils"; -import { logError } from "./error-logging"; -import { CmdOptions } from "./options"; +import { eachValue } from "./cli-parsing"; +import { logError, withErrorLogger } from "./error-logging"; +import { GlobalOptions } from "./options"; import { parseEnvUsing } from "./parse-env"; import { ResultCodes } from "./result-codes"; +import { mustBeDomainName } from "./validators"; -/** - * The possible result codes with which the remove command can exit. - */ -export type RemoveResultCode = ResultCodes.Ok | ResultCodes.Error; +const pkgArg = new Argument("", "Name of the package to remove").argParser( + mustBeDomainName +); -/** - * Options passed to the remove command. - */ -export type RemoveOptions = CmdOptions; - -/** - * Cmd-handler for removing packages. - * @param pkgs One or multiple packages to remove. - * @param options Command options. - */ -export type RemoveCmd = ( - pkgs: ReadonlyArray, - options: RemoveOptions -) => Promise; +const otherPkgsArg = new Argument( + "[otherPkgs...]", + "Names of additional packages to remove" +).argParser(eachValue(mustBeDomainName)); /** - * Makes a {@link RemoveCmd} function. + * Makes the `openupm remove` cli command with the given dependencies. + * @param readTextFile IO function for reading a text file. + * @param writeTextFile IO function for writing a text file. + * @param debugLog IO function for debug-logs. + * @param log Logger for cli output. + * @returns The command. */ export function makeRemoveCmd( readTextFile: ReadTextFile, writeTextFile: WriteTextFile, debugLog: DebugLog, log: Logger -): RemoveCmd { +) { const removeDependencies = partialApply( removeDependenciesUsing, readTextFile, @@ -46,30 +42,46 @@ export function makeRemoveCmd( debugLog ); - return async (pkgs, options) => { - // parse env - const env = await parseEnvUsing(log, process.env, process.cwd(), options); + return new Command("remove") + .aliases(["rm", "uninstall"]) + .addArgument(pkgArg) + .addArgument(otherPkgsArg) + .description("remove package from manifest json") + .action( + withErrorLogger( + log, + async function (packageName, otherPackageNames, _, cmd) { + const globalOptions = cmd.optsWithGlobals(); + const pkgs = [packageName, ...otherPackageNames]; - const removeResult = await removeDependencies(env.cwd, pkgs).promise; - if (removeResult.isErr()) { - logError(log, removeResult.error); - return ResultCodes.Error; - } - const removedPackages = removeResult.value; + // parse env + const env = await parseEnvUsing( + log, + process.env, + process.cwd(), + globalOptions + ); - removedPackages.forEach((removedPackage) => { - log.notice( - "", - `Removed "${makePackageReference( - removedPackage.name, - removedPackage.version - )}".` - ); - }); + const removeResult = await removeDependencies(env.cwd, pkgs).promise; + if (removeResult.isErr()) { + logError(log, removeResult.error); + return process.exit(ResultCodes.Error); + } + const removedPackages = removeResult.value; - // print manifest notice - log.notice("", "please open Unity project to apply changes"); + removedPackages.forEach((removedPackage) => { + log.notice( + "", + `Removed "${makePackageReference( + removedPackage.name, + removedPackage.version + )}".` + ); + }); - return ResultCodes.Ok; - }; + // print manifest notice + log.notice("", "please open Unity project to apply changes"); + } + ) + ); } diff --git a/src/cli/cmd-search.ts b/src/cli/cmd-search.ts index 44a6696d..d5bade8e 100644 --- a/src/cli/cmd-search.ts +++ b/src/cli/cmd-search.ts @@ -1,3 +1,4 @@ +import { Command } from "@commander-js/extra-typings"; import { Logger } from "npmlog"; import * as os from "os"; import { loadRegistryAuthUsing } from "../app/get-registry-auth"; @@ -8,33 +9,22 @@ import { getHomePathFromEnv } from "../domain/special-paths"; import { getUserUpmConfigPathFor } from "../domain/upm-config"; import type { ReadTextFile } from "../io/fs"; import type { GetAllRegistryPackuments, SearchRegistry } from "../io/registry"; -import { CmdOptions } from "./options"; +import { withErrorLogger } from "./error-logging"; +import { GlobalOptions } from "./options"; import { formatAsTable } from "./output-formatting"; import { parseEnvUsing } from "./parse-env"; import { ResultCodes } from "./result-codes"; /** - * The possible result codes with which the search command can exit. - */ -export type SearchResultCode = ResultCodes.Ok | ResultCodes.Error; - -/** - * Options passed to the search command. - */ -export type SearchOptions = CmdOptions; - -/** - * Cmd-handler for searching the registry. - * @param keyword The keyword to search for. - * @param options Command options. - */ -export type SearchCmd = ( - keyword: string, - options: SearchOptions -) => Promise; - -/** - * Makes a {@link SearchCmd} function. + * Makes the `openupm search` cli command with the given dependencies. + * @param readTextFile IO function for reading a text file. + * @param searchRegistry IO function for sending a search request to a + * registry. + * @param fetchAllPackuments IO function for getting all packages from a + * registry. + * @param log Logger for cli output. + * @param debugLog IO function for debug-logs. + * @returns The command. */ export function makeSearchCmd( readTextFile: ReadTextFile, @@ -42,46 +32,60 @@ export function makeSearchCmd( fetchAllPackuments: GetAllRegistryPackuments, log: Logger, debugLog: DebugLog -): SearchCmd { +) { const searchPackages = partialApply( searchPackagesUsing, searchRegistry, fetchAllPackuments, debugLog ); + return new Command("search") + .argument("", "The keyword to search") + .aliases(["s", "se", "find"]) + .description("Search package by keyword") + .action( + withErrorLogger(log, async function (keyword, _, cmd) { + const globalOptions = cmd.optsWithGlobals(); - return async (keyword, options) => { - // parse env - const env = await parseEnvUsing(log, process.env, process.cwd(), options); - const homePath = getHomePathFromEnv(process.env); - const upmConfigPath = getUserUpmConfigPathFor( - process.env, - homePath, - env.systemUser - ); + // parse env + const env = await parseEnvUsing( + log, + process.env, + process.cwd(), + globalOptions + ); + const homePath = getHomePathFromEnv(process.env); + const upmConfigPath = getUserUpmConfigPathFor( + process.env, + homePath, + env.systemUser + ); - const primaryRegistry = await loadRegistryAuthUsing( - readTextFile, - debugLog, - upmConfigPath, - env.primaryRegistryUrl - ); + const primaryRegistry = await loadRegistryAuthUsing( + readTextFile, + debugLog, + upmConfigPath, + env.primaryRegistryUrl + ); - let usedEndpoint = "npmsearch"; - const results = await searchPackages(primaryRegistry, keyword, () => { - usedEndpoint = "endpoint.all"; - log.warn("", "fast search endpoint is not available, using old search."); - }); + let usedEndpoint = "npmsearch"; + const results = await searchPackages(primaryRegistry, keyword, () => { + usedEndpoint = "endpoint.all"; + log.warn( + "", + "fast search endpoint is not available, using old search." + ); + }); - if (results.length === 0) { - log.notice("", `No matches found for "${keyword}"`); - return ResultCodes.Ok; - } + if (results.length === 0) { + log.notice("", `No matches found for "${keyword}"`); + return process.exit(ResultCodes.Ok); + } - await debugLog( - `${usedEndpoint}: ${results.map((it) => it.name).join(os.EOL)}` + await debugLog( + `${usedEndpoint}: ${results.map((it) => it.name).join(os.EOL)}` + ); + log.notice("", formatAsTable(results)); + }) ); - log.notice("", formatAsTable(results)); - return ResultCodes.Ok; - }; } diff --git a/src/cli/cmd-view.ts b/src/cli/cmd-view.ts index 4455fc60..2c20f21e 100644 --- a/src/cli/cmd-view.ts +++ b/src/cli/cmd-view.ts @@ -1,88 +1,91 @@ +import { Command } from "@commander-js/extra-typings"; import { Logger } from "npmlog"; import { EOL } from "os"; import { loadRegistryAuthUsing } from "../app/get-registry-auth"; import { queryAllRegistriesLazy } from "../app/query-registries"; import { PackumentNotFoundError } from "../domain/common-errors"; import type { DebugLog } from "../domain/logging"; -import { - hasVersion, - PackageReference, - splitPackageReference, -} from "../domain/package-reference"; +import { hasVersion, splitPackageReference } from "../domain/package-reference"; import { unityRegistry } from "../domain/registry"; import { getHomePathFromEnv } from "../domain/special-paths"; import { getUserUpmConfigPathFor } from "../domain/upm-config"; import type { ReadTextFile } from "../io/fs"; import type { GetRegistryPackument } from "../io/registry"; -import { CmdOptions } from "./options"; +import { withErrorLogger } from "./error-logging"; +import { GlobalOptions } from "./options"; import { formatPackumentInfo } from "./output-formatting"; import { parseEnvUsing } from "./parse-env"; import { ResultCodes } from "./result-codes"; +import { mustBePackageReference } from "./validators"; /** - * Options passed to the view command. - */ -export type ViewOptions = CmdOptions; - -/** - * The possible result codes with which the view command can exit. - */ -export type ViewResultCode = ResultCodes.Ok | ResultCodes.Error; - -/** - * Cmd-handler for viewing package information. - * @param pkg Reference to the package to view. - * @param options Command options. - */ -export type ViewCmd = ( - pkg: PackageReference, - options: ViewOptions -) => Promise; - -/** - * Makes a {@link ViewCmd} function. + * Makes the `openupm view` cli command with the given dependencies. + * @param getRegistryPackument IO function for fetching registry packages. + * @param readTextFile IO function for reading a text file. + * @param debugLog IO function for debug-logs. + * @param log Logger for cli output. + * @returns The command. */ export function makeViewCmd( getRegistryPackument: GetRegistryPackument, readTextFile: ReadTextFile, debugLog: DebugLog, log: Logger -): ViewCmd { - return async (pkg, options) => { - // parse env - const env = await parseEnvUsing(log, process.env, process.cwd(), options); - const homePath = getHomePathFromEnv(process.env); - const upmConfigPath = getUserUpmConfigPathFor( - process.env, - homePath, - env.systemUser - ); +) { + return new Command("view") + .argument("", "Reference to a package", mustBePackageReference) + .aliases(["v", "info", "show"]) + .description("view package information") + .action( + withErrorLogger(log, async function (pkg, _, cmd) { + const globalOptions = cmd.optsWithGlobals(); - const primaryRegistry = await loadRegistryAuthUsing( - readTextFile, - debugLog, - upmConfigPath, - env.primaryRegistryUrl - ); + // parse env + const env = await parseEnvUsing( + log, + process.env, + process.cwd(), + globalOptions + ); + const homePath = getHomePathFromEnv(process.env); + const upmConfigPath = getUserUpmConfigPathFor( + process.env, + homePath, + env.systemUser + ); - // parse name - if (hasVersion(pkg)) { - const [name] = splitPackageReference(pkg); - log.warn("", `please do not specify a version (Write only '${name}').`); - return ResultCodes.Error; - } + const primaryRegistry = await loadRegistryAuthUsing( + readTextFile, + debugLog, + upmConfigPath, + env.primaryRegistryUrl + ); - // verify name - const sources = [primaryRegistry, ...(env.upstream ? [unityRegistry] : [])]; - const packumentFromRegistry = await queryAllRegistriesLazy( - sources, - (source) => getRegistryPackument(source, pkg) - ); - const packument = packumentFromRegistry?.value ?? null; - if (packument === null) throw new PackumentNotFoundError(pkg); + // parse name + if (hasVersion(pkg)) { + const [name] = splitPackageReference(pkg); + log.warn( + "", + `please do not specify a version (Write only '${name}').` + ); + return process.exit(ResultCodes.Error); + } + + // verify name + const sources = [ + primaryRegistry, + ...(env.upstream ? [unityRegistry] : []), + ]; + const packumentFromRegistry = await queryAllRegistriesLazy( + sources, + (source) => getRegistryPackument(source, pkg) + ); + const packument = packumentFromRegistry?.value ?? null; + if (packument === null) throw new PackumentNotFoundError(pkg); - const output = formatPackumentInfo(packument, EOL); - log.notice("", output); - return ResultCodes.Ok; - }; + const output = formatPackumentInfo(packument, EOL); + log.notice("", output); + return process.exit(ResultCodes.Ok); + }) + ); } diff --git a/src/cli/error-logging.ts b/src/cli/error-logging.ts index c80ac780..225acf54 100644 --- a/src/cli/error-logging.ts +++ b/src/cli/error-logging.ts @@ -26,13 +26,13 @@ import { type ResolvePackumentVersionError, } from "../domain/packument"; import { recordEntries } from "../domain/record-utils"; -import { NoSystemUserProfilePath } from "../domain/upm-config"; -import { RegistryAuthenticationError } from "../io/common-errors"; import { NoHomePathError, OSNotSupportedError, VersionNotSupportedOnOsError, } from "../domain/special-paths"; +import { NoSystemUserProfilePath } from "../domain/upm-config"; +import { RegistryAuthenticationError } from "../io/common-errors"; import { RegistryAuthLoadError } from "./parse-env"; import { ResultCodes } from "./result-codes"; @@ -166,16 +166,16 @@ export function logError(log: Logger, error: unknown) { * @returns A new function that has the same behaviour as the original but with * error logging. */ -export function withErrorLogger< - TArgs extends unknown[], - TOut extends ResultCodes ->( +export function withErrorLogger( log: Logger, cmd: (...args: TArgs) => Promise ): (...args: TArgs) => Promise { - return (...args) => - cmd(...args).catch((error) => { + return async (...args) => { + try { + return await cmd(...args); + } catch (error) { logError(log, error); process.exit(ResultCodes.Error); - }); + } + }; } diff --git a/src/cli/index.ts b/src/cli/index.ts index 5b08f45b..7de98009 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1,255 +1,108 @@ import { createCommand } from "@commander-js/extra-typings"; -import RegClient from "another-npm-registry-client"; -import npmlog from "npmlog"; +import type { Logger } from "npmlog"; import pkginfo from "pkginfo"; -import updateNotifier from "update-notifier"; -import pkg from "../../package.json"; -import { DebugLog } from "../domain/logging"; +import type { DebugLog } from "../domain/logging"; import { getHomePathFromEnv } from "../domain/special-paths"; import { readTextFile, writeTextFile } from "../io/fs"; import { - getAllRegistryPackumentsUsing, - getAuthTokenUsing, - getRegistryPackumentUsing, - searchRegistryUsing, + type GetAllRegistryPackuments, + type GetAuthToken, + type GetRegistryPackument, + type SearchRegistry, } from "../io/registry"; import { fetchCheckUrlExists } from "../io/www"; -import { eachValue } from "./cli-parsing"; import { makeAddCmd } from "./cmd-add"; import { makeDepsCmd } from "./cmd-deps"; import { makeLoginCmd } from "./cmd-login"; import { makeRemoveCmd } from "./cmd-remove"; import { makeSearchCmd } from "./cmd-search"; import { makeViewCmd } from "./cmd-view"; -import { withErrorLogger } from "./error-logging"; -import { CmdOptions } from "./options"; -import { - mustBeDomainName, - mustBePackageReference, - mustBeRegistryUrl, -} from "./validators"; - -// Composition root - -const log = npmlog; - -/** - * {@link DebugLog} function which uses {@link npmlog} to print logs to - * the console. - */ -const debugLogToConsole: DebugLog = async function (message, context) { - const contextMessage = - context !== undefined - ? `\n${ - context instanceof Error - ? context.toString() - : JSON.stringify(context, null, 2) - }` - : ""; - return log.verbose("", `${message}${contextMessage}`); -}; - -const homePath = getHomePathFromEnv(process.env); -const registryClient = new RegClient({ log }); -const fetchPackument = getRegistryPackumentUsing( - registryClient, - debugLogToConsole -); -const searchRegistry = searchRegistryUsing(debugLogToConsole); -const fetchAllPackuments = getAllRegistryPackumentsUsing(debugLogToConsole); - -const addCmd = makeAddCmd( - fetchCheckUrlExists, - fetchPackument, - readTextFile, - writeTextFile, - log, - debugLogToConsole -); -const loginCmd = makeLoginCmd( - homePath, - getAuthTokenUsing(registryClient, debugLogToConsole), - readTextFile, - writeTextFile, - debugLogToConsole, - log -); -const searchCmd = makeSearchCmd( - readTextFile, - searchRegistry, - fetchAllPackuments, - log, - debugLogToConsole -); -const depsCmd = makeDepsCmd( - readTextFile, - fetchPackument, - fetchCheckUrlExists, - log, - debugLogToConsole -); -const removeCmd = makeRemoveCmd( - readTextFile, - writeTextFile, - debugLogToConsole, - log -); -const viewCmd = makeViewCmd( - getRegistryPackumentUsing(registryClient, debugLogToConsole), - readTextFile, - debugLogToConsole, - log -); - -// update-notifier - -pkginfo(module); -const notifier = updateNotifier({ pkg }); -notifier.notify(); - -const program = createCommand() - .version(module.exports.version) - .option("-c, --chdir ", "change the working directory") - .option("-r, --registry ", "specify registry url", mustBeRegistryUrl) - .option("-v, --verbose", "output extra debugging") - .option("--system-user", "auth for Windows system user") - .option("--no-upstream", "don't use upstream unity registry") - .option("--no-color", "disable color"); +import { mustBeRegistryUrl } from "./validators"; /** - * Creates a CmdOptions object by adding global options to the given - * specific options. - * @param specificOptions The specific options. + * Makes the openupm cli app with the given dependencies. + * @param fetchPackument IO function for fetching registry packuments. + * @param searchRegistry IO function for using the search API on a registry. + * @param fetchAllPackuments IO function for getting all packuments from + * a registry. + * @param getAuthToken IO function for getting a users auth token from a + * registry. + * @param log Logger for printing output. + * @param debugLog IO function for printing debug logs. + * @returns Root command. */ -function makeCmdOptions>( - specificOptions: T -): CmdOptions { - return { ...specificOptions, ...program.opts() }; -} - -program - .command("add") - .argument( - "", - "Reference to the package that should be added", - mustBePackageReference - ) - .argument( - "[otherPkgs...]", - "References to additional packages that should be added", - eachValue(mustBePackageReference) - ) - .aliases(["install", "i"]) - .option("-t, --test", "add package as testable") - .option( - "-f, --force", - "force add package if missing deps or editor version is not qualified" - ) - .description( - `add package to manifest json -openupm add [otherPkgs...] -openupm add @ [otherPkgs...]` - ) - .action( - withErrorLogger(log, async function (pkg, otherPkgs, options) { - const pkgs = [pkg].concat(otherPkgs); - const resultCode = await addCmd(pkgs, makeCmdOptions(options)); - process.exit(resultCode); - }) - ); - -program - .command("remove") - .argument("", "Name of the package to remove", mustBeDomainName) - .argument( - "[otherPkgs...]", - "Names of additional packages to remove", - eachValue(mustBeDomainName) - ) - .aliases(["rm", "uninstall"]) - .description("remove package from manifest json") - .action( - withErrorLogger( +export function makeOpenupmCli( + fetchPackument: GetRegistryPackument, + searchRegistry: SearchRegistry, + fetchAllPackuments: GetAllRegistryPackuments, + getAuthToken: GetAuthToken, + log: Logger, + debugLog: DebugLog +) { + const homePath = getHomePathFromEnv(process.env); + + pkginfo(module); + const program = createCommand() + .version(module.exports.version) + .option("-c, --chdir ", "change the working directory") + .option("-r, --registry ", "specify registry url", mustBeRegistryUrl) + .option("-v, --verbose", "output extra debugging") + .option("--system-user", "auth for Windows system user") + .option("--no-upstream", "don't use upstream unity registry") + .option("--no-color", "disable color"); + + program.addCommand( + makeAddCmd( + fetchCheckUrlExists, + fetchPackument, + readTextFile, + writeTextFile, log, - async function (packageName, otherPackageNames, options) { - const packageNames = [packageName].concat(otherPackageNames); - const resultCode = await removeCmd( - packageNames, - makeCmdOptions(options) - ); - process.exit(resultCode); - } + debugLog ) ); -program - .command("search") - .argument("", "The keyword to search") - .aliases(["s", "se", "find"]) - .description("Search package by keyword") - .action( - withErrorLogger(log, async function (keyword, options) { - const resultCode = await searchCmd(keyword, makeCmdOptions(options)); - process.exit(resultCode); - }) - ); + program.addCommand(makeRemoveCmd(readTextFile, writeTextFile, debugLog, log)); -program - .command("view") - .argument("", "Reference to a package", mustBePackageReference) - .aliases(["v", "info", "show"]) - .description("view package information") - .action( - withErrorLogger(log, async function (pkg, options) { - const resultCode = await viewCmd(pkg, makeCmdOptions(options)); - process.exit(resultCode); - }) + program.addCommand( + makeSearchCmd( + readTextFile, + searchRegistry, + fetchAllPackuments, + log, + debugLog + ) ); -program - .command("deps") - .argument("", "Reference to a package", mustBePackageReference) - .alias("dep") - .option("-d, --deep", "view package dependencies recursively") - .description( - `view package dependencies -openupm deps -openupm deps @` - ) - .action( - withErrorLogger(log, async function (pkg, options) { - const resultCode = await depsCmd(pkg, makeCmdOptions(options)); - process.exit(resultCode); - }) - ); + program.addCommand(makeViewCmd(fetchPackument, readTextFile, debugLog, log)); -program - .command("login") - .aliases(["add-user", "adduser"]) - .option("-u, --username ", "username") - .option("-p, --password ", "password") - .option("-e, --email ", "email address") - .option("--basic-auth", "use basic authentication instead of token") - .option( - "--always-auth", - "always auth for tarball hosted on a different domain" - ) - .description("authenticate with a scoped registry") - .action( - withErrorLogger(log, async function (options) { - const resultCode = await loginCmd(makeCmdOptions(options)); - process.exit(resultCode); - }) + program.addCommand( + makeDepsCmd( + readTextFile, + fetchPackument, + fetchCheckUrlExists, + log, + debugLog + ) ); -// prompt for invalid command -program.on("command:*", function () { - log.warn("", `unknown command: ${program.args.join(" ")}`); - log.warn("", "see --help for a list of available commands"); - process.exit(1); -}); + program.addCommand( + makeLoginCmd( + homePath, + getAuthToken, + readTextFile, + writeTextFile, + debugLog, + log + ) + ); -program.parse(process.argv); + // prompt for invalid command + program.on("command:*", function () { + log.warn("", `unknown command: ${program.args.join(" ")}`); + log.warn("", "see --help for a list of available commands"); + process.exit(1); + }); -// print help if no command is given -if (!program.args.length) program.help(); + return program; +} diff --git a/src/cli/options.ts b/src/cli/options.ts index 0a23834c..3bb477df 100644 --- a/src/cli/options.ts +++ b/src/cli/options.ts @@ -1,7 +1,7 @@ /** * Options which are shared between commands. */ -type GlobalOptions = Readonly<{ +export type GlobalOptions = Readonly<{ /** * Override package registry to use. */ diff --git a/src/index.ts b/src/index.ts index bd16bc1e..a5500af5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,60 @@ #!/usr/bin/env node -import "./cli"; +import RegClient from "another-npm-registry-client"; +import npmlog from "npmlog"; +import updateNotifier from "update-notifier"; +import pkg from "../package.json"; +import { makeOpenupmCli } from "./cli"; +import type { DebugLog } from "./domain/logging"; +import { + getAllRegistryPackumentsUsing, + getAuthTokenUsing, + getRegistryPackumentUsing, + searchRegistryUsing, +} from "./io/registry"; + +// Composition root + +const log = npmlog; + +const debugLogToConsole: DebugLog = async function (message, context) { + const contextMessage = + context !== undefined + ? `\n${ + context instanceof Error + ? context.toString() + : JSON.stringify(context, null, 2) + }` + : ""; + return log.verbose("", `${message}${contextMessage}`); +}; + +const registryClient = new RegClient({ log }); +const fetchPackument = getRegistryPackumentUsing( + registryClient, + debugLogToConsole +); +const searchRegistry = searchRegistryUsing(debugLogToConsole); +const fetchAllPackuments = getAllRegistryPackumentsUsing(debugLogToConsole); +const getAuthToken = getAuthTokenUsing(registryClient, debugLogToConsole); + +const openupmCli = makeOpenupmCli( + fetchPackument, + searchRegistry, + fetchAllPackuments, + getAuthToken, + log, + debugLogToConsole +); + +// Update in case of update + +const notifier = updateNotifier({ pkg }); +notifier.notify(); + +// Run app + +openupmCli.parse(process.argv); + +// print help if no command is given +if (openupmCli.args.length === 0) openupmCli.help();