Files
kit.program/kit/src/kitfunction.js
T
2026-06-17 10:52:30 +08:00

558 lines
16 KiB
JavaScript

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
};