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 resolvePlainPassword(params) { if (params.plainPwd !== undefined) { return params.plainPwd; } if (params.plainPassword !== undefined) { return params.plainPassword; } if (params.plaintextPwd !== undefined) { return params.plaintextPwd; } if (params.plaintextPassword !== undefined) { return params.plaintextPassword; } return undefined; } function resolveScpPassword(params) { const plainPassword = resolvePlainPassword(params); if (plainPassword !== undefined) { return plainPassword; } return resolveValue(params.pwd || params.password); } function scp(from, to, params = {}) { if (!from || !to) { throw new Error("scp from and to are required"); } const pwd = resolveScpPassword(params); platform.scp(from, to, Object.assign({}, params, { pwd, password: pwd, key: resolveValue(params.key || params.identityFile), identityFile: resolveValue(params.identityFile || params.key) })); } function loadSsh2Client() { try { return require("ssh2").Client; } catch (e) { throw new Error("remoteShell requires pure JS dependency ssh2. Please run npm install ssh2 in kit project."); } } function normalizeRemoteShellConfig(config = {}) { const host = config.host || config.hostname; if (!host) { throw new Error("remoteShell host is required"); } const username = config.username || config.user; if (!username) { throw new Error("remoteShell username is required"); } const password = resolveScpPassword(config); const privateKey = config.privateKey || config.keyText || (config.keyFile ? fs.readFileSync(path.resolve(config.keyFile), "utf-8") : undefined); const connectConfig = Object.assign({}, config, { host, username, port: config.port || 22, password, privateKey, passphrase: resolveValue(config.passphrase) }); delete connectConfig.user; delete connectConfig.hostname; delete connectConfig.pwd; delete connectConfig.plainPwd; delete connectConfig.plainPassword; delete connectConfig.plaintextPwd; delete connectConfig.plaintextPassword; delete connectConfig.keyText; delete connectConfig.keyFile; return connectConfig; } function remoteShell(config = {}) { const Client = loadSsh2Client(); const connectConfig = normalizeRemoteShellConfig(config); let client = null; let connectPromise = null; function connect() { if (client) { return Promise.resolve(client); } if (connectPromise) { return connectPromise; } connectPromise = new Promise((resolve, reject) => { const conn = new Client(); conn.once("ready", () => { client = conn; resolve(conn); }); conn.once("error", (e) => { connectPromise = null; reject(e); }); conn.once("close", () => { client = null; connectPromise = null; }); conn.connect(connectConfig); }); return connectPromise; } async function exec(cmd, options = {}) { if (!cmd) { throw new Error("remoteShell exec cmd is required"); } const conn = await connect(); return new Promise((resolve, reject) => { const execOptions = Object.assign({}, options); delete execOptions.stdio; delete execOptions.rejectOnError; conn.exec(cmd, execOptions, (err, stream) => { if (err) { reject(err); return; } let stdout = ""; let stderr = ""; stream.on("error", reject); stream.on("close", (code, signal) => { const result = { code, signal, stdout, stderr }; if (options.rejectOnError === false || code === 0) { resolve(result); } else { const error = new Error(`Remote command failed with code ${code}: ${cmd}`); error.result = result; reject(error); } }); stream.on("data", (data) => { const text = data.toString(); stdout += text; if (options.stdio === "inherit") { process.stdout.write(text); } }); stream.stderr.on("data", (data) => { const text = data.toString(); stderr += text; if (options.stdio === "inherit") { process.stderr.write(text); } }); }); }); } async function execText(cmd, options = {}) { const result = await exec(cmd, options); return result.stdout; } async function call(cmd, options = {}) { return exec(cmd, Object.assign({ stdio: "inherit" }, options)); } function close() { if (client) { client.end(); } client = null; connectPromise = null; } return { connect, exec, execText, call, close }; } function exit(code = 0) { process.exit(code); } function isSecretLogKey(key) { return /^(pwd|password|plainPwd|plainPassword|plaintextPwd|plaintextPassword|privateKey|passphrase|keyText)$/i.test(key); } function redactLogValue(value) { if (Array.isArray(value)) { return value.map(redactLogValue); } if (value && typeof value === "object") { const result = {}; for (const key of Object.keys(value)) { result[key] = isSecretLogKey(key) ? "******" : redactLogValue(value[key]); } return result; } return value; } 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(redactLogValue(value)); } catch (e) { return String(value); } } function printCallLog(name, args, result) { console.log(`调用 ${name} 功能`); console.log(`入参 ${args.map(stringifyLogValue).join(" ")}`); if (result === undefined) { console.log("执行结束"); } else { 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), remoteShell: withCallLog("remoteShell", remoteShell), exit: withCallLog("exit", exit), setVar: withCallLog("setVar", setVar), getVar: withCallLog("getVar", getVar), vars };