UNPKG

kaven-utils

Version:

Utils for Node.js.

441 lines (440 loc) 19.3 kB
#!/usr/bin/env node /******************************************************************** * @author: Kaven * @email: kaven@wuwenkai.com * @website: http://blog.kaven.xyz * @file: [Kaven-Utils] /bin.ts * @create: 2018-11-05 14:59:09.639 * @modify: 2025-10-24 16:47:27.883 * @version: 6.1.2 * @times: 245 * @lines: 536 * @copyright: Copyright © 2018-2025 Kaven. All Rights Reserved. * @description: [description] * @license: [license] ********************************************************************/ import { Command } from "commander"; import { AddQueryParameterToURL, CombinePath, ConsoleLogger, FormatCurrentDate, LoggingAgent } from "kaven-basic"; import { promises } from "node:fs"; import { basename, dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { AppendPathToDirectory, ContinuousIntegrationForDocuments, CopyFileOrDirectory, CopyToDirectory, Delete, DeleteFilesByExtension, DockerRegistry, Execute, FindAndReplaceInFiles, GenerateCertificate, GetExternalIp, GetExternalIpByUPnP, GetFileStats, IsFile, IsPathExistSync, IsWin32, LoadJsonFile, Minify, MinifyCss, SaveJsonConfig, StartProxy, } from "./index.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const packageJson = join(__dirname, "package.json"); const logger = new LoggingAgent(new ConsoleLogger(true)); LoadJsonFile(packageJson).then(data => { const program = new Command(); program .version(data.version, "-v, --version", "Output the version number") .helpOption("-h, --help", "Display help") .helpCommand("help [command]", "Display help for [command]") .name("kaven-utils|ku"); // copy /// ///////////////////////////////////////////////////////////////////////////////// program .command("copy [destination] [sourceFiles...]") .alias("cp") .description("Copy files or directories to a specified destination") .option("-c, --config <config>", "Specify a JSON configuration file for advanced copying options") .action(async (destination, sourceFiles, options) => { // Check if a configuration file is provided if (options.config) { const configFile = resolve(options.config); // Check if the configuration file exists if (await IsFile(configFile)) { logger.Info(`Loading configuration from file: ${configFile}`); const config = await LoadJsonFile(configFile); // Process each entry in the configuration if (config.entries) { for (const entry of config.entries) { const override = entry.override ?? config.override ?? false; const options = { overwrite: override, logger, }; // Copy files or directories based on entry type if (typeof entry.src === "string") { await CopyFileOrDirectory(entry.src, entry.dest, options); } else if (Array.isArray(entry.src)) { await CopyToDirectory(entry.src, entry.dest, options); } } } } } // Copy additional source files if provided if (sourceFiles && sourceFiles.length > 0) { await CopyToDirectory(sourceFiles, destination, { logger }); } }); // remove /// ///////////////////////////////////////////////////////////////////////////////// program .command("remove <file> [otherFiles...]") .alias("rm") .alias("delete") .description("Remove files or directories") .option("-r, --recursive", "Remove directories and their contents recursively") .action(async (file, otherFiles, options) => { // Concatenate all files including the first one const allFiles = [file].concat(otherFiles); // If the recursive option is provided, delete files and directories recursively if (options.recursive) { await Delete(...allFiles); } else { // Iterate through the provided files and remove them individually for (const item of allFiles) { // Get file stats to determine if it's a directory or file const stats = await GetFileStats(item); // If stats are undefined, continue to the next item if (stats === undefined) { continue; } // Check if it's a directory and remove it using promises.rmdir if (stats.isDirectory()) { await promises.rmdir(item); } else { // If it's a file, remove it using promises.unlink await promises.unlink(item); } } } }); // remove-files /// ///////////////////////////////////////////////////////////////////////////////// program .command("remove-files <dir> <ext> [otherExtensions...]") .alias("remove_files") .alias("rm-files") .alias("rm_files") .alias("delete-files") .alias("delete_files") .description("Remove files or directories") .option("--caseSensitive") .option("--ignore, --ignoreFolderNames [ignoreFolderNames...]", "Specify a folder name to exclude when searching") .action(async (dir, ext, otherExtensions, options) => { await DeleteFilesByExtension(dir, [ext].concat(otherExtensions), { caseSensitive: options.caseSensitive, ignoreFolderNames: options.ignoreFolderNames, }); }); // run /// ///////////////////////////////////////////////////////////////////////////////// program .command("run <command> [otherCommands...]") .description("Run multiple scripts concurrently or sequentially") .option("-s, --sequential", "Run commands sequentially") .option("-e, --encoding <encoding>", "Specify the decoding encoding using iconv-lite") .option("--spawn", "Use spawn instead of the default exec for command execution") .option("--shell [shell]", "Specify an alternative shell for command execution") .option("--win32commands <win32commands...>", "Specify an alternative execution command on Windows. This overrides the default command on Windows") .action(async (command, otherCommands, options) => { // Concatenate all commands including the first one const allCommands = (IsWin32 && options.win32commands) ? options.win32commands : [command].concat(otherCommands); const executeOptions = { logger: logger, decoderEncoding: options.encoding, spawn: !!options.spawn, spawnOptions: { shell: (typeof options.shell === "string" || typeof options.shell === "boolean") ? options.shell : undefined, }, execOptions: { shell: typeof options.shell === "string" ? options.shell : undefined, }, }; // If running sequentially, iterate through and run each command one by one if (options.sequential) { for (const c of allCommands) { await Execute(c, executeOptions); } } else { // If not running sequentially, execute all commands concurrently for (const c of allCommands) { Execute(c, executeOptions); } } }); // gen-cert /// ///////////////////////////////////////////////////////////////////////////////// program .command("gen-cert") .alias("gen_cert") .description("Generate SSL/TLS certificates for secure communication") .option("--dir [dir]", "Specify the target directory for certificate generation") .option("--bits [bits]", "Specify the key size in bits for the generated certificates") .option("--days [days]", "Specify the validity period in days for the generated certificates") .option("--cn [cn]", "Specify the common name (CN) for the certificate subject") .option("--openssl [openssl]", "Specify the location of the OpenSSL executable") .option("--verbose", "Enable verbose logging for detailed information") .action(async (options) => { // Configure certificate generation options based on command-line inputs const certOptions = {}; if (options.dir) { certOptions.certGenerateDir = options.dir; } else { certOptions.certGenerateDir = "./generated"; } if (options.bits) { certOptions.size = Number(options.bits); } if (options.days) { certOptions.days = Number(options.days); } if (options.cn) { certOptions.serverSubj = { CN: options.cn, }; } if (options.openssl) { certOptions.openssl = options.openssl; } if (options.verbose) { certOptions.logger = logger; } // Generate the certificate and save the result to a JSON file const result = await GenerateCertificate(certOptions); result.Save(CombinePath(certOptions.certGenerateDir, "cert.json")); }); // proxy /// ///////////////////////////////////////////////////////////////////////////////// program .command("proxy [dirOrFile]") .description("Start a local proxy server for forwarding HTTP requests") .action(async (dirOrFile) => { try { // If no directory or file is provided, default to the current directory if (!dirOrFile) { dirOrFile = process.cwd(); } // Start the proxy server with the specified directory or file await StartProxy(dirOrFile); } catch (ex) { logger.Error(ex); } }); // ip /// ///////////////////////////////////////////////////////////////////////////////// program .command("ip") .description("Retrieve the external IP address of the current network") .option("--UPnP", "Use UPnP to discover the external IP address") .option("--verbose", "Enable verbose mode for additional information") .action(async (options) => { // Destructure options for clarity const { UPnP, verbose } = options; // Set trace based on verbose option const trace = !!verbose; if (UPnP) { logger.Info(await GetExternalIpByUPnP({ logger: trace ? logger : undefined }) ?? "", { Raw: true }); } else { logger.Info(await GetExternalIp({ logger: trace ? logger : undefined }), { Raw: true }); } }); // ci /// ///////////////////////////////////////////////////////////////////////////////// program .command("ci") .description("Run continuous integration for documentation updates") .option("--config <config>", "Specify a JSON configuration file for advanced options") .option("--variables <variables...>", "Variables to use in the format key:value") .action(async (options) => { let cwd = process.cwd(); let config = { sourceDocumentFileOrDirectory: "./docs", sourceVersionFile: "./dist/package.json", targetRootDirectory: "../Kaven-Documents", targetDirectoryName: basename(cwd), }; if (options.config) { const configFile = AppendPathToDirectory(cwd, options.config); if (IsPathExistSync(configFile)) { logger.Info(`Loading configuration from file: ${configFile}`); const json = await LoadJsonFile(configFile); config = Object.assign(config, json); cwd = dirname(configFile); } else { throw new Error(`Config file not exists: ${configFile}`); } } if (options.variables && options.variables.length > 0) { config.variables = {}; for (const variable of options.variables) { const items = variable.split(":"); if (items.length === 2) { config.variables[items[0].trim()] = items[1].trim(); } } } logger.Info(`Current Working Directory: ${cwd}`); config.sourceDocumentFileOrDirectory = AppendPathToDirectory(cwd, config.sourceDocumentFileOrDirectory); config.sourceVersionFile = AppendPathToDirectory(cwd, config.sourceVersionFile); config.targetRootDirectory = AppendPathToDirectory(cwd, config.targetRootDirectory); if (config.commands) { for (const command of config.commands) { if (command.options?.execOptions?.cwd) { command.options.execOptions.cwd = AppendPathToDirectory(cwd, command.options.execOptions.cwd); } } } config.updateReadmeFile ??= true; config.gitCommit ??= true; await ContinuousIntegrationForDocuments(config); }); // docker /// ///////////////////////////////////////////////////////////////////////////////// program .command("docker <server> [command]") .option("-u, --username <username>", "username") .option("-p, --password <password>", "password") .action(async (server, command, options) => { const { username, password } = options; const registry = new DockerRegistry(server, (username && password) ? { username, password } : undefined); if (command) { // TODO } else { const list = await registry.ListImageWithTags(); for (const item of list) { logger.Info(`${item.name}: ${item.tags.join(", ")}`, { Raw: true }); } } }); // where /// ///////////////////////////////////////////////////////////////////////////////// program .command("where") .description("Show the location of the command") .action(() => { logger.Info(__filename, { Raw: true }); }); // minify /// ///////////////////////////////////////////////////////////////////////////////// program .command("minify [file] [otherFiles...]") .description("JavaScript minifier") .option("--config <config>", "Read or generate a JSON configuration file.") .action(async (file, otherFiles, options) => { let cwd = process.cwd(); // Concatenate all files including the first one const allFiles = []; if (file) { allFiles.push(file); } if (otherFiles && Array.isArray(otherFiles)) { allFiles.push(...otherFiles); } let minifyOptions = { src: allFiles, deleteSourceMap: false, setSourceMappingURL: true, deleteTypeScriptDeclarationFile: true, includeNodeModules: false, terserOptions: { format: { ecma: 2015, comments: false, }, }, }; if (options.config) { const configFile = AppendPathToDirectory(cwd, options.config); if (IsPathExistSync(configFile)) { logger.Info(`Loading configuration from file: ${configFile}`); const json = await LoadJsonFile(configFile); minifyOptions = Object.assign(minifyOptions, json); cwd = dirname(configFile); process.chdir(cwd); } else { logger.Info(`Generate configuration file: ${configFile}`); await SaveJsonConfig(minifyOptions, configFile); return; } } minifyOptions.logger = logger; await Minify(minifyOptions); }); // minify-css /// ///////////////////////////////////////////////////////////////////////////////// program .command("minify-css [file] [otherFiles...]") .description("CSS minifier") .option("--config <config>", "Read or generate a JSON configuration file.") .action(async (file, otherFiles, options) => { let cwd = process.cwd(); // Concatenate all files including the first one const allFiles = []; if (file) { allFiles.push(file); } if (otherFiles && Array.isArray(otherFiles)) { allFiles.push(...otherFiles); } let minifyOptions = { src: allFiles, includeNodeModules: false, }; if (options.config) { const configFile = AppendPathToDirectory(cwd, options.config); if (IsPathExistSync(configFile)) { logger.Info(`Loading configuration from file: ${configFile}`); const json = await LoadJsonFile(configFile); minifyOptions = Object.assign(minifyOptions, json); cwd = dirname(configFile); process.chdir(cwd); } else { logger.Info(`Generate configuration file: ${configFile}`); await SaveJsonConfig(minifyOptions, configFile); return; } } minifyOptions.logger = logger; await MinifyCss(minifyOptions); }); // update-pug-resource-version /// ///////////////////////////////////////////////////////////////////////////////// program .command("update-pug-resource-version [dir]") /* cSpell:disable */ .alias("uprv") /* cSpell:enable */ .description("Update the version of resources in Pug files by appending a query parameter to URLs") .option("--name [name]", "Specify the query parameter name to append to resource URLs") .option("--version [version]", "Specify the version to append to resource URLs") .option("--verbose", "Enable verbose mode for detailed output") .action(async (dir, options) => { const cwd = process.cwd(); dir = AppendPathToDirectory(cwd, dir ?? "./"); const name = options.name ?? "version"; const val = options.version || FormatCurrentDate("YYYYMMDD"); if (options.verbose) { logger.Info(`Searching in directory: ${dir}, name: ${name}, version: ${val}`); } await FindAndReplaceInFiles(dir, { newStrMethod: (url) => AddQueryParameterToURL(url, name, val), logger, }); }); program .command("*", { hidden: true, }) .action((env) => { logger.Error(`error: unknown command '${env}'`); program.outputHelp(); }); program.parse(process.argv); }).catch((err => { logger.Error(err); process.exit(-1); }));