From b46cddc7cff4cfc68448068ab4b6e7b857d3cbbc Mon Sep 17 00:00:00 2001 From: cheney Date: Tue, 16 Jun 2026 09:59:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=20cicd=20=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kit/src/kitcommand.js | 268 ++++++++++++------------ kit/src/kitfunction.js | 361 +++++++++++++++++++++++++++++++++ kit/src/linux/kitfunction.js | 70 +++++++ kit/src/windows/kitfunction.js | 69 +++++++ 4 files changed, 635 insertions(+), 133 deletions(-) create mode 100644 kit/src/kitfunction.js create mode 100644 kit/src/linux/kitfunction.js create mode 100644 kit/src/windows/kitfunction.js diff --git a/kit/src/kitcommand.js b/kit/src/kitcommand.js index 42c43f8..714fbd7 100644 --- a/kit/src/kitcommand.js +++ b/kit/src/kitcommand.js @@ -1,133 +1,135 @@ -const shell = require("./shell"); -const os = require("os"); - - -module.exports = { - init : function () { - return this; - }, - regTo: function (parser) { - - parser.addCmdLine("help", "打开在线帮助;", async function () { - const uConfig = await $context.get("uConfig") - if (os.type() === 'Windows_NT') { - shell.execSync("start " + uConfig.get("man")) - } else { - console.log("请用浏览器访问 " + uConfig.get("man")) - } - }); - parser.addCmdLine("manu", "人工维护配置;", function () { - if (os.type() === 'Windows_NT') { - shell.execSync("start " + $sConfig.getUserConfigPath()) - } else { - console.log("cd " + $sConfig.getUserConfigPath()) - } - }); - parser.addCmdLine("run [--file] [func]", "运行项目脚本;", function (cli) { - $logger.info("kit 当前执行目录 " + process.cwd()); - const fs = require("fs"); - - let step = cli.getParamValue("func"); - if (!step) { - step = "main" - } - - let file = cli.getParamValue("file"); - if (!file) { - file = "kit" - } - - const stat = fs.existsSync(`./${file}.js`) - if (!stat) { - if (!cli.getParamValue("file")) { - fs.writeFileSync("./kit.js", ` -async function main(){ - console.log("hello kit!") -} -`); - } else { - $logger.info(`文件 ${file} 不存在`); - return - } - } - - - const projectScript = fs.readFileSync("./kit.js", "utf-8"); - const vm = require('vm'); - const script = new vm.Script(projectScript + ";" + step + "()"); - const context = { - animal: 'cat', count: 2, $logger, shell - }; - - script.runInNewContext(context, { - timeout: 10 * 60 * 1000, breakOnSigint: true, - }); - $logger.info("执行结束"); - }); - - parser.addCmdLine("config [value]", "查询/设置配置;", async function (cli) { - let key = cli.getParamValue("key"); - let value = cli.getParamValue("value"); - const uConfig = await $context.get("uConfig") - if (null != key && null == value) { - let v = uConfig.get(key); - $logger.info("{} : {}", key, v); - } else if (null != key && null != value) { - uConfig.set(key, value); - $logger.info("set {} : {}", key, value); - } else { - $logger.error("参数异常"); - } - }); - - parser.addCmdLine("remote", "验证远程仓库;", async function () { - let remote = await $context.get("remote") - if (!(await remote.check())) { - $logger.error("远程仓库不可用") - return - } else { - $logger.info("远程仓库可用") - } - }); - - parser.addCmdLine("publish [localFile] [remotePath]", "发布到远程仓库;", async function (cli) { - const fs = require("fs"); - let localFile = cli.getParamValue("localFile"); - if ( ! fs.existsSync(localFile) ) { - $logger.error("本地文件不存在") - return - } - - let remotePath = cli.getParamValue("remotePath"); - if ( ! remotePath ) { - $logger.info("使用本地配置路径") - if ( ! fs.existsSync("meta.json") ) { - $logger.error("没有找到本地配置") - return - } - } else { - $logger.info("发布到远程路径" + remotePath) - } - let remote = await $context.get("remote") - if (!(await remote.check())) { - $logger.error("远程仓库不可用") - return - } else { - $logger.info("远程仓库可用") - } - await remote.publish(localFile, remotePath) - $logger.info("发布成功") - }); - parser.addCmdLine("install [module]", "安装/更新模块;", async function (cli) { - let remote = await $context.get("remote") - if (!(await remote.check())) { - $logger.error("远程仓库不可用") - return - } else { - $logger.info("远程仓库可用") - } - remote.install(cli.getParamValue("module")) - }); - - } -} +const shell = require("./shell"); +const os = require("os"); +const kitfunction = require("./kitfunction"); + +module.exports = { + init : function () { + return this; + }, + regTo: function (parser) { + parser.addCmdLine("help", "打开在线帮助;", async function () { + const uConfig = await $context.get("uConfig") + if (os.type() === 'Windows_NT') { + shell.execSync("start " + uConfig.get("man")) + } else { + console.log("请用浏览器访问 " + uConfig.get("man")) + } + }); + + parser.addCmdLine("manu", "人工维护配置;", function () { + if (os.type() === 'Windows_NT') { + shell.execSync("start " + $sConfig.getUserConfigPath()) + } else { + console.log("cd " + $sConfig.getUserConfigPath()) + } + }); + + parser.addCmdLine("run [--file] [func]", "运行项目脚本;", function (cli) { + $logger.info("kit 当前执行目录 " + process.cwd()); + const fs = require("fs"); + + let step = cli.getParamValue("func"); + if (!step) { + step = "main" + } + + let file = cli.getParamValue("file"); + if (!file) { + file = "kit" + } + + const scriptFile = `./${file}.js`; + const stat = fs.existsSync(scriptFile) + if (!stat) { + if (!cli.getParamValue("file")) { + fs.writeFileSync("./kit.js", ` +async function main(){ + console.log("hello kit!") +} +`); + } else { + $logger.info(`文件 ${file} 不存在`); + return + } + } + + const projectScript = fs.readFileSync(scriptFile, "utf-8"); + const vm = require('vm'); + const script = new vm.Script(projectScript + ";" + step + "()"); + + // 注入全局变量 + const kitfunction = require("./kitfunction") + const context = kitfunction; + + script.runInNewContext(context, { + timeout: 10 * 60 * 1000, breakOnSigint: true, + }); + $logger.info("执行结束"); + }); + + parser.addCmdLine("config [value]", "查询/设置配置;", async function (cli) { + let key = cli.getParamValue("key"); + let value = cli.getParamValue("value"); + const uConfig = await $context.get("uConfig") + if (null != key && null == value) { + let v = uConfig.get(key); + $logger.info("{} : {}", key, v); + } else if (null != key && null != value) { + uConfig.set(key, value); + $logger.info("set {} : {}", key, value); + } else { + $logger.error("参数异常"); + } + }); + + parser.addCmdLine("remote", "验证远程仓库;", async function () { + let remote = await $context.get("remote") + if (!(await remote.check())) { + $logger.error("远程仓库不可用") + return + } else { + $logger.info("远程仓库可用") + } + }); + + parser.addCmdLine("publish [localFile] [remotePath]", "发布到远程仓库", async function (cli) { + const fs = require("fs"); + let localFile = cli.getParamValue("localFile"); + if (!fs.existsSync(localFile)) { + $logger.error("本地文件不存在") + return + } + + let remotePath = cli.getParamValue("remotePath"); + if (!remotePath) { + $logger.info("使用本地配置路径") + if (!fs.existsSync("meta.json")) { + $logger.error("没有找到本地配置") + return + } + } else { + $logger.info("发布到远程路径 " + remotePath) + } + let remote = await $context.get("remote") + if (!(await remote.check())) { + $logger.error("远程仓库不可用") + return + } else { + $logger.info("远程仓库可用") + } + await remote.publish(localFile, remotePath) + $logger.info("发布成功") + }); + + parser.addCmdLine("install [module]", "安装/更新模块;", async function (cli) { + let remote = await $context.get("remote") + if (!(await remote.check())) { + $logger.error("远程仓库不可用") + return + } else { + $logger.info("远程仓库可用") + } + remote.install(cli.getParamValue("module")) + }); + } +} diff --git a/kit/src/kitfunction.js b/kit/src/kitfunction.js new file mode 100644 index 0000000..ccadfc8 --- /dev/null +++ b/kit/src/kitfunction.js @@ -0,0 +1,361 @@ +const crypto = require("crypto"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); + +const platform = os.type() === "Windows_NT" ? require("./windows/kitfunction") : require("./linux/kitfunction"); +const vars = Object.create(null); + +function setVar(name, value) { + if (name) { + vars[name] = value; + } + return value; +} + +function getVar(name) { + return vars[name]; +} + +function resolveValue(value) { + if (typeof value !== "string") { + return value; + } + + const varMatch = value.match(/^%([A-Za-z_][A-Za-z0-9_]*)%$/); + if (varMatch) { + return getVar(varMatch[1]); + } + + const randomMatch = value.match(/^%random\((number),(\d+)\)%$/); + if (randomMatch) { + const length = Number(randomMatch[2]); + let result = ""; + while (result.length < length) { + result += crypto.randomInt(0, 10).toString(); + } + return result; + } + + return value; +} + +function info(msg) { + const text = String(resolveValue(msg) ?? ""); + const line = "=".repeat(Math.max(24, text.length + 8)); + console.log(`\n\x1b[36m${line}\x1b[0m`); + console.log(`\x1b[1m\x1b[33m>>> ${text}\x1b[0m`); + console.log(`\x1b[36m${line}\x1b[0m\n`); +} + +function mkdir(dirPath) { + if (!dirPath) { + throw new Error("mkdir path is required"); + } + fs.mkdirSync(path.resolve(dirPath), { recursive: true }); +} + +function rm(targetPath) { + if (!targetPath) { + throw new Error("rm path is required"); + } + fs.rmSync(path.resolve(targetPath), { recursive: true, force: true }); +} + +function copyDir(from, to) { + fs.mkdirSync(to, { recursive: true }); + for (const item of fs.readdirSync(from, { withFileTypes: true })) { + const source = path.join(from, item.name); + const destination = path.join(to, item.name); + if (item.isDirectory()) { + copyDir(source, destination); + } else if (item.isSymbolicLink()) { + fs.rmSync(destination, { recursive: true, force: true }); + fs.symlinkSync(fs.readlinkSync(source), destination); + } else { + fs.mkdirSync(path.dirname(destination), { recursive: true }); + fs.copyFileSync(source, destination); + } + } +} + +function cp(from, to) { + if (!from || !to) { + throw new Error("cp from and to are required"); + } + + const source = path.resolve(from); + if (!fs.existsSync(source)) { + return; + } + + let destination = path.resolve(to); + const stat = fs.statSync(source); + if (!stat.isDirectory() && fs.existsSync(destination) && fs.statSync(destination).isDirectory()) { + destination = path.join(destination, path.basename(source)); + } + fs.mkdirSync(path.dirname(destination), { recursive: true }); + + if (stat.isDirectory()) { + rm(destination); + copyDir(source, destination); + } else { + fs.copyFileSync(source, destination); + } +} + +function mv(from, to) { + if (!from || !to) { + throw new Error("mv from and to are required"); + } + + const source = path.resolve(from); + if (!fs.existsSync(source)) { + return; + } + + let destination = path.resolve(to); + const stat = fs.statSync(source); + if (!stat.isDirectory() && fs.existsSync(destination) && fs.statSync(destination).isDirectory()) { + destination = path.join(destination, path.basename(source)); + } + fs.mkdirSync(path.dirname(destination), { recursive: true }); + rm(destination); + + try { + fs.renameSync(source, destination); + } catch (e) { + cp(source, destination); + rm(source); + } +} + +function normalizeList(value) { + if (!value) { + return []; + } + return Array.isArray(value) ? value : [value]; +} + +function normalizeSlash(value) { + return value.replace(/\\/g, "/"); +} + +function matchRule(relativePath, rule) { + if (typeof rule === "function") { + return rule(relativePath); + } + if (rule instanceof RegExp) { + return rule.test(relativePath); + } + if (typeof rule === "string") { + const text = normalizeSlash(rule); + return relativePath === text || relativePath.startsWith(text.endsWith("/") ? text : `${text}/`) || relativePath.includes(text); + } + return false; +} + +function shouldInclude(relativePath, params) { + const normalized = normalizeSlash(relativePath); + const filters = normalizeList(params.filter); + const excludes = normalizeList(params.execute || params.exclude); + + if (filters.length > 0 && !filters.some((rule) => matchRule(normalized, rule))) { + return false; + } + + if (excludes.some((rule) => matchRule(normalized, rule))) { + return false; + } + + return true; +} + +function copyFiltered(source, tempRoot, params) { + const sourceStat = fs.statSync(source); + const baseName = path.basename(source); + const root = sourceStat.isDirectory() ? source : path.dirname(source); + + function walk(current) { + const relative = path.relative(root, current) || baseName; + const target = path.join(tempRoot, relative); + const stat = fs.statSync(current); + + if (!shouldInclude(relative, params)) { + return; + } + + if (stat.isDirectory()) { + fs.mkdirSync(target, { recursive: true }); + for (const name of fs.readdirSync(current)) { + walk(path.join(current, name)); + } + } else { + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.copyFileSync(current, target); + } + } + + walk(source); +} + +function zip(from, to, params = {}) { + if (!from || !to) { + throw new Error("zip from and to are required"); + } + + const source = path.resolve(from); + if (!fs.existsSync(source)) { + return; + } + + const options = Object.assign({}, params, { + type: params.type || "zip", + password: resolveValue(params.password) + }); + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "kit-zip-")); + + try { + copyFiltered(source, tempRoot, options); + platform.zip(tempRoot, to, options); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } +} + +function md5(filePath, params = {}) { + const target = filePath ? path.resolve(filePath) : ""; + let value = "0000000000000000"; + + if (target && fs.existsSync(target) && fs.statSync(target).isFile()) { + value = crypto.createHash("md5").update(fs.readFileSync(target)).digest("hex"); + } + + return setVar(params.var, value); +} + +function parseKey(key) { + if (!key) { + return []; + } + return String(key).split(".").filter((item) => item.length > 0).map((item) => /^\d+$/.test(item) ? Number(item) : item); +} + +function readJsonValue(data, key) { + let current = data; + for (const item of parseKey(key)) { + if (current == null) { + return undefined; + } + current = current[item]; + } + return current; +} + +function ensureContainer(parent, key, nextKey) { + if (parent[key] == null || typeof parent[key] !== "object") { + parent[key] = typeof nextKey === "number" ? [] : {}; + } +} + +function writeJsonValue(data, key, value) { + const keys = parseKey(key); + if (keys.length === 0) { + return value; + } + + let current = data; + for (let index = 0; index < keys.length - 1; index++) { + ensureContainer(current, keys[index], keys[index + 1]); + current = current[keys[index]]; + } + current[keys[keys.length - 1]] = value; + return data; +} + +function readJson(filePath, key, params = {}) { + if (!filePath) { + throw new Error("readJson filePath is required"); + } + const data = JSON.parse(fs.readFileSync(path.resolve(filePath), "utf-8")); + return setVar(params.var, readJsonValue(data, key)); +} + +function writeJson(filePath, key, value) { + if (!filePath) { + throw new Error("writeJson filePath is required"); + } + + const target = path.resolve(filePath); + let data = {}; + if (fs.existsSync(target)) { + data = JSON.parse(fs.readFileSync(target, "utf-8")); + } + + const nextData = writeJsonValue(data, key, resolveValue(value)); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, JSON.stringify(nextData, null, 4), "utf-8"); + return nextData; +} + +function scp(from, to, params = {}) { + if (!from || !to) { + throw new Error("scp from and to are required"); + } + platform.scp(from, to, Object.assign({}, params, { pwd: resolveValue(params.pwd) })); +} + +function exit(code = 0) { + process.exit(code); +} + +function stringifyLogValue(value) { + if (typeof value === "string") { + return value; + } + if (value === undefined) { + return "undefined"; + } + if (typeof value === "function") { + return `[Function ${value.name || "anonymous"}]`; + } + try { + return JSON.stringify(value); + } catch (e) { + return String(value); + } +} + +function printCallLog(name, args, result) { + console.log(`调用 ${name} 功能`); + console.log(`入参 ${args.map(stringifyLogValue).join(" ")}`); + console.log(`结果 ${stringifyLogValue(result)}`); +} + +function withCallLog(name, fn) { + return function (...args) { + const result = fn.apply(this, args); + printCallLog(name, args, result); + return result; + }; +} + +module.exports = { + info: withCallLog("info", info), + mkdir: withCallLog("mkdir", mkdir), + rm: withCallLog("rm", rm), + cp: withCallLog("cp", cp), + mv: withCallLog("mv", mv), + zip: withCallLog("zip", zip), + md5: withCallLog("md5", md5), + readJson: withCallLog("readJson", readJson), + writeJson: withCallLog("writeJson", writeJson), + scp: withCallLog("scp", scp), + exit: withCallLog("exit", exit), + setVar: withCallLog("setVar", setVar), + getVar: withCallLog("getVar", getVar), + vars +}; + + + diff --git a/kit/src/linux/kitfunction.js b/kit/src/linux/kitfunction.js new file mode 100644 index 0000000..a61c00e --- /dev/null +++ b/kit/src/linux/kitfunction.js @@ -0,0 +1,70 @@ +const { execFileSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +function commandExists(command) { + try { + execFileSync("which", [command], { stdio: "ignore" }); + return true; + } catch (e) { + return false; + } +} + +function run(command, args, options = {}) { + return execFileSync(command, args, Object.assign({ stdio: "inherit" }, options)); +} + +function addToZip(sourceDir, to, password) { + fs.mkdirSync(path.dirname(path.resolve(to)), { recursive: true }); + + if (commandExists("zip")) { + const args = ["-r"]; + if (password) { + args.push("-P", password); + } + args.push(path.resolve(to), "."); + run("zip", args, { cwd: sourceDir, stdio: "inherit" }); + return; + } + + if (password) { + throw new Error("Linux password zip requires zip in PATH"); + } + + const script = `import zipfile, pathlib\nroot = pathlib.Path(${JSON.stringify(path.resolve(sourceDir))})\nout = pathlib.Path(${JSON.stringify(path.resolve(to))})\nout.parent.mkdir(parents=True, exist_ok=True)\nmode = 'a' if out.exists() else 'w'\nwith zipfile.ZipFile(out, mode, zipfile.ZIP_DEFLATED) as z:\n for p in root.rglob('*'):\n if p.is_file():\n z.write(p, p.relative_to(root).as_posix())`; + run("python3", ["-c", script]); +} + +function addToTar(sourceDir, to) { + fs.mkdirSync(path.dirname(path.resolve(to)), { recursive: true }); + const args = fs.existsSync(to) ? ["-rf", path.resolve(to), "."] : ["-cf", path.resolve(to), "."]; + run("tar", args, { cwd: sourceDir, stdio: "inherit" }); +} + +function zip(sourceDir, to, params = {}) { + if (params.type === "tar") { + addToTar(sourceDir, to); + } else { + addToZip(sourceDir, to, params.password); + } +} + +function scp(from, to, params = {}) { + const source = path.resolve(from); + if (!fs.existsSync(source)) { + return; + } + + if (params.pwd) { + if (!commandExists("sshpass")) { + throw new Error("Linux scp with pwd requires sshpass in PATH"); + } + run("sshpass", ["-p", params.pwd, "scp", "-r", source, to]); + return; + } + + run("scp", ["-r", source, to]); +} + +module.exports = { zip, scp }; diff --git a/kit/src/windows/kitfunction.js b/kit/src/windows/kitfunction.js new file mode 100644 index 0000000..87ab3b9 --- /dev/null +++ b/kit/src/windows/kitfunction.js @@ -0,0 +1,69 @@ +const { execFileSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +function commandExists(command) { + try { + execFileSync("where.exe", [command], { stdio: "ignore" }); + return true; + } catch (e) { + return false; + } +} + +function run(command, args, options = {}) { + return execFileSync(command, args, Object.assign({ stdio: "inherit" }, options)); +} + +function addToZip(sourceDir, to, password) { + fs.mkdirSync(path.dirname(path.resolve(to)), { recursive: true }); + + if (password) { + if (!commandExists("7z.exe") && !commandExists("7z")) { + throw new Error("Windows password zip requires 7z in PATH"); + } + const bin = commandExists("7z.exe") ? "7z.exe" : "7z"; + run(bin, ["a", "-tzip", `-p${password}`, "-y", path.resolve(to), "."], { cwd: sourceDir, stdio: "inherit" }); + return; + } + + const destination = path.resolve(to).replace(/'/g, "''"); + const source = path.resolve(sourceDir, "*").replace(/'/g, "''"); + const update = fs.existsSync(to) ? " -Update" : ""; + const script = `Compress-Archive -Path '${source}' -DestinationPath '${destination}'${update} -Force`; + run("powershell.exe", ["-NoProfile", "-Command", script]); +} + +function addToTar(sourceDir, to) { + fs.mkdirSync(path.dirname(path.resolve(to)), { recursive: true }); + const args = fs.existsSync(to) ? ["-rf", path.resolve(to), "."] : ["-cf", path.resolve(to), "."]; + run("tar.exe", args, { cwd: sourceDir, stdio: "inherit" }); +} + +function zip(sourceDir, to, params = {}) { + if (params.type === "tar") { + addToTar(sourceDir, to); + } else { + addToZip(sourceDir, to, params.password); + } +} + +function scp(from, to, params = {}) { + const source = path.resolve(from); + if (!fs.existsSync(source)) { + return; + } + + if (params.pwd) { + if (!commandExists("pscp.exe") && !commandExists("pscp")) { + throw new Error("Windows scp with pwd requires pscp in PATH"); + } + const bin = commandExists("pscp.exe") ? "pscp.exe" : "pscp"; + run(bin, ["-pw", params.pwd, "-r", source, to]); + return; + } + + run("scp.exe", ["-r", source, to]); +} + +module.exports = { zip, scp };