kaven-utils
Version:
Utils for Node.js.
441 lines (440 loc) • 19.3 kB
JavaScript
/********************************************************************
* @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);
}));