chore: initialize qiming workspace repository
This commit is contained in:
217
qiming-file-server/scripts/build.js
Normal file
217
qiming-file-server/scripts/build.js
Normal file
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* qiming-file-server 发 npm 前编译脚本(ESM)
|
||||
*
|
||||
* 使用方法:
|
||||
* - 发布(默认): node scripts/build.js
|
||||
* - 本地调试: node scripts/build.js --all
|
||||
*
|
||||
* env 文件位于 src/,构建时复制 env.development、env.production 到 dist/
|
||||
* 使用 --all 参数会额外复制其余 src/env.*(如 env.test)
|
||||
*/
|
||||
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const projectRoot = path.resolve(__dirname, "..");
|
||||
const distRoot = path.join(projectRoot, "dist");
|
||||
|
||||
const pkgPath = path.join(projectRoot, "package.json");
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
||||
const version = pkg.version;
|
||||
|
||||
// 获取命令行参数
|
||||
const args = process.argv.slice(2);
|
||||
const debugMode = args.includes("--all");
|
||||
|
||||
/**
|
||||
* 递归查找目录下所有 .js 文件
|
||||
*/
|
||||
function findJsFiles(dir, files = []) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const e of entries) {
|
||||
const fullPath = path.join(dir, e.name);
|
||||
if (e.isDirectory()) findJsFiles(fullPath, files);
|
||||
else if (e.name.endsWith(".js")) files.push(fullPath);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 esbuild 压缩 JS 文件
|
||||
* @param {string} dir 目标目录
|
||||
* @param {object} esbuild esbuild 实例
|
||||
*/
|
||||
async function compressJsFiles(esbuild, dir) {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
|
||||
const jsFiles = findJsFiles(dir);
|
||||
let totalSaved = 0;
|
||||
|
||||
for (const file of jsFiles) {
|
||||
const code = fs.readFileSync(file, "utf8");
|
||||
const result = await esbuild.transform(code, {
|
||||
minify: true,
|
||||
target: "node22",
|
||||
});
|
||||
fs.writeFileSync(file, result.code, "utf8");
|
||||
const originalSize = Buffer.byteLength(code, "utf8");
|
||||
const compressedSize = Buffer.byteLength(result.code, "utf8");
|
||||
const saved = originalSize - compressedSize;
|
||||
totalSaved += saved;
|
||||
console.log(`[build] 压缩 ${path.relative(distRoot, file)}: ${(originalSize / 1024).toFixed(1)}KB -> ${(compressedSize / 1024).toFixed(1)}KB`);
|
||||
}
|
||||
console.log(`[build] 压缩 ${path.relative(distRoot, dir)} 完成,节省 ${(totalSaved / 1024).toFixed(1)}KB`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制单个文件
|
||||
*/
|
||||
function copyFileSync(src, dest) {
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.copyFileSync(src, dest);
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制目录内容(不包含顶层目录)
|
||||
* srcDir/abc/xyz.js -> destDir/abc/xyz.js
|
||||
*/
|
||||
function copyDirContentsSync(srcDir, destDir) {
|
||||
if (!fs.existsSync(srcDir)) return;
|
||||
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
||||
|
||||
for (const e of entries) {
|
||||
const srcPath = path.join(srcDir, e.name);
|
||||
const destPath = path.join(destDir, e.name);
|
||||
|
||||
if (e.isDirectory()) {
|
||||
copyDirContentsSync(srcPath, destPath);
|
||||
} else if (e.isFile() && e.name.endsWith(".js")) {
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function build() {
|
||||
const esbuild = (await import("esbuild")).default;
|
||||
|
||||
// ---------- 0. 清理旧目录 ----------
|
||||
const oldSrcDir = path.join(distRoot, "src");
|
||||
if (fs.existsSync(oldSrcDir)) {
|
||||
fs.rmSync(oldSrcDir, { recursive: true, force: true });
|
||||
console.log("[build] Old src/ directory deleted");
|
||||
}
|
||||
|
||||
// ---------- 1. 打包 CLI ----------
|
||||
await esbuild.build({
|
||||
entryPoints: [path.join(projectRoot, "src", "cli.js")],
|
||||
bundle: true,
|
||||
platform: "node",
|
||||
target: "node22",
|
||||
outfile: path.join(distRoot, "cli.js"),
|
||||
format: "esm",
|
||||
external: ["commander", "cross-spawn", "fs-extra", "tree-kill"],
|
||||
define: { __BUILD_VERSION__: JSON.stringify(version) },
|
||||
minify: true,
|
||||
sourcemap: false,
|
||||
});
|
||||
console.log("[build] dist/cli.js output");
|
||||
|
||||
// ---------- 2. 复制 src 下的子目录到 dist 根目录 ----------
|
||||
// appConfig/* -> dist/appConfig/* 应用配置(环境变量等)
|
||||
// config/* -> dist/config/* Swagger/API 文档
|
||||
// routes/* -> dist/routes/*
|
||||
// scheduler/* -> dist/scheduler/*
|
||||
// service/* -> dist/service/*
|
||||
// utils/* -> dist/utils/*
|
||||
// server.js -> dist/server.js(单独复制)
|
||||
|
||||
const srcRoot = path.join(projectRoot, "src");
|
||||
|
||||
// 复制子目录(与 src 结构同步:appConfig、routes、scheduler、service、utils)
|
||||
const subdirs = ["appConfig", "routes", "scheduler", "service", "utils"];
|
||||
for (const subdir of subdirs) {
|
||||
const srcPath = path.join(srcRoot, subdir);
|
||||
const destPath = path.join(distRoot, subdir);
|
||||
copyDirContentsSync(srcPath, destPath);
|
||||
console.log(`[build] ${subdir}/ -> dist/${subdir}/ copied`);
|
||||
}
|
||||
|
||||
// 单独复制 src/config/ 下的 swagger 相关文件(移除遗留的 dist/config/index.js,应用配置已迁至 appConfig)
|
||||
const srcConfigPath = path.join(srcRoot, "config");
|
||||
const destConfigPath = path.join(distRoot, "config");
|
||||
const legacyConfigIndex = path.join(destConfigPath, "index.js");
|
||||
if (fs.existsSync(legacyConfigIndex)) fs.rmSync(legacyConfigIndex);
|
||||
copyDirContentsSync(srcConfigPath, destConfigPath);
|
||||
console.log(`[build] config/ -> dist/config/ copied`);
|
||||
|
||||
// 单独复制 server.js 到 dist 根目录(无需修改导入路径,源码已用 ./appConfig)
|
||||
const serverJsSrc = path.join(srcRoot, "server.js");
|
||||
const serverJsDest = path.join(distRoot, "server.js");
|
||||
copyFileSync(serverJsSrc, serverJsDest);
|
||||
console.log("[build] server.js -> dist/ copied");
|
||||
|
||||
// ---------- 3. 压缩 dist 下所有 JS 文件(CLI 除外) ----------
|
||||
const distJsFiles = findJsFiles(distRoot);
|
||||
let totalSaved = 0;
|
||||
|
||||
for (const file of distJsFiles) {
|
||||
// 跳过已压缩的 cli.js
|
||||
if (path.basename(file) === "cli.js") continue;
|
||||
|
||||
const code = fs.readFileSync(file, "utf8");
|
||||
const result = await esbuild.transform(code, {
|
||||
minify: true,
|
||||
target: "node22",
|
||||
});
|
||||
fs.writeFileSync(file, result.code, "utf8");
|
||||
const originalSize = Buffer.byteLength(code, "utf8");
|
||||
const compressedSize = Buffer.byteLength(result.code, "utf8");
|
||||
const saved = originalSize - compressedSize;
|
||||
totalSaved += saved;
|
||||
console.log(`[build] 压缩 ${path.relative(distRoot, file)}: ${(originalSize / 1024).toFixed(1)}KB -> ${(compressedSize / 1024).toFixed(1)}KB`);
|
||||
}
|
||||
console.log(`[build] Compression completed, saving ${(totalSaved / 1024).toFixed(1)}KB`);
|
||||
|
||||
// ---------- 4. 复制 env 文件(从 src/ 到 dist/) ----------
|
||||
// 发布包需包含 development + production,以便 CLI --env production 能正常启动
|
||||
const envToShip = ["env.development", "env.production"];
|
||||
for (const envFile of envToShip) {
|
||||
const src = path.join(srcRoot, envFile);
|
||||
if (fs.existsSync(src)) {
|
||||
fs.copyFileSync(src, path.join(distRoot, envFile));
|
||||
console.log(`[build] ${envFile} -> dist/ copied`);
|
||||
}
|
||||
}
|
||||
|
||||
// 调试模式下额外复制其余 env.*(如 env.test)
|
||||
if (debugMode) {
|
||||
const envFiles = fs.readdirSync(srcRoot).filter((f) => f.startsWith("env."));
|
||||
for (const envFile of envFiles) {
|
||||
if (!envToShip.includes(envFile)) {
|
||||
fs.copyFileSync(path.join(srcRoot, envFile), path.join(distRoot, envFile));
|
||||
console.log(`[build] ${envFile} -> dist/ copied`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 5. 输出启动说明 ----------
|
||||
console.log("");
|
||||
console.log("[build] =========================================");
|
||||
console.log("[build] Build completed!");
|
||||
console.log("[build] =========================================");
|
||||
console.log("[build] Start service: cd dist && node server.js");
|
||||
console.log("[build] Or use CLI: node cli.js start");
|
||||
console.log("");
|
||||
}
|
||||
|
||||
build().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
287
qiming-file-server/scripts/check-package-in-store.js
Normal file
287
qiming-file-server/scripts/check-package-in-store.js
Normal file
@@ -0,0 +1,287 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 检查指定包是否在 pnpm store 中,以及下载原因诊断工具
|
||||
*
|
||||
* 使用方法:
|
||||
* node scripts/check-package-in-store.js @radix-ui/react-toggle
|
||||
* node scripts/check-package-in-store.js @radix-ui/react-toggle 1.0.3
|
||||
*/
|
||||
|
||||
import { execSync, spawn } from "child_process";
|
||||
import path from "path";
|
||||
import { readFileSync } from "fs";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const packageName = process.argv[2];
|
||||
const packageVersion = process.argv[3];
|
||||
|
||||
if (!packageName) {
|
||||
console.error("❌ Please provide package name");
|
||||
console.log("\nUsage:");
|
||||
console.log(" node scripts/check-package-in-store.js <package-name> [version]");
|
||||
console.log("\nExamples:");
|
||||
console.log(" node scripts/check-package-in-store.js @radix-ui/react-toggle");
|
||||
console.log(" node scripts/check-package-in-store.js @radix-ui/react-toggle 1.0.3");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("======================================");
|
||||
console.log("pnpm Store package diagnosis tool");
|
||||
console.log("======================================");
|
||||
console.log("");
|
||||
|
||||
// 1. 获取 store 路径
|
||||
console.log("1️⃣ Check pnpm Store path:");
|
||||
console.log("----------------------------------------");
|
||||
let storePath;
|
||||
try {
|
||||
storePath = execSync("pnpm store path", {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
}).trim();
|
||||
console.log(`✅ Store path: ${storePath}`);
|
||||
|
||||
// 检查 store 结构
|
||||
const filesDir = path.join(storePath, "files");
|
||||
const indexDir = path.join(storePath, "index");
|
||||
if (filesDir && indexDir) {
|
||||
console.log(` Store structure: pnpm v10 (hash sharding storage)`);
|
||||
console.log(` - files/ directory: store hash files`);
|
||||
console.log(` - index/ directory: store index information`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to get store path: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("");
|
||||
|
||||
// 2. 检查 store 中是否有该包
|
||||
console.log("2️⃣ Check if the package exists in the Store:");
|
||||
console.log("----------------------------------------");
|
||||
let foundDirs = "";
|
||||
let packageFound = false;
|
||||
|
||||
try {
|
||||
// pnpm v10 store structure: store/v10/files/ and store/v10/index/
|
||||
// files/ directory is the hash sharding directory (00-ff), each sharding directory is the hash named file
|
||||
// index/ directory stores index information
|
||||
const encodedPackageName = packageName
|
||||
.replace(/@/g, "%40")
|
||||
.replace(/\//g, "%2f");
|
||||
|
||||
const filesDir = path.join(storePath, "files");
|
||||
const indexDir = path.join(storePath, "index");
|
||||
|
||||
console.log(` Search package: ${packageName}`);
|
||||
console.log(` Encoded package name: ${encodedPackageName}`);
|
||||
console.log(` Store files directory: ${filesDir}`);
|
||||
console.log(` Store index directory: ${indexDir}`);
|
||||
console.log(" (Note: pnpm v10 uses hash file storage, packages are named by hash values)");
|
||||
console.log("");
|
||||
|
||||
// 方法1: 使用 pnpm store status 检查(最可靠的方法)
|
||||
console.log(" Method 1: Use pnpm store status check...");
|
||||
try {
|
||||
const statusOutput = execSync("pnpm store status", {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// 检查输出中是否包含该包的信息
|
||||
if (statusOutput.includes(packageName) || statusOutput.includes(encodedPackageName)) {
|
||||
console.log(" ✅ pnpm store status shows the package related information:");
|
||||
const relevantLines = statusOutput
|
||||
.split("\n")
|
||||
.filter(line => line.includes(packageName) || line.includes(encodedPackageName))
|
||||
.slice(0, 5);
|
||||
relevantLines.forEach(line => console.log(` ${line}`));
|
||||
packageFound = true;
|
||||
} else {
|
||||
console.log(" ⚠️ pnpm store status did not find the direct information of the package");
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(` ⚠️ Failed to execute pnpm store status: ${e.message}`);
|
||||
}
|
||||
console.log("");
|
||||
|
||||
// 方法2: 在 index 目录中查找(索引可能包含包名信息)
|
||||
console.log(" Method 2: Find in the index directory...");
|
||||
try {
|
||||
const findCommand = `find "${indexDir}" -type f -exec grep -l "${encodedPackageName}" {} \\; 2>/dev/null | head -10`;
|
||||
const indexFiles = execSync(findCommand, {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
}).trim();
|
||||
|
||||
if (indexFiles) {
|
||||
console.log(" ✅ Found related files in the index:");
|
||||
indexFiles.split("\n").slice(0, 5).forEach(file => {
|
||||
console.log(` ${file}`);
|
||||
});
|
||||
packageFound = true;
|
||||
} else {
|
||||
console.log(" ⚠️ index directory did not find related files");
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(` ⚠️ Failed to search index directory: ${e.message}`);
|
||||
}
|
||||
console.log("");
|
||||
|
||||
// 方法3: 尝试在整个 store 中查找包含包名的任何内容
|
||||
console.log(" Method 3: Search for any content containing the package name in the store...");
|
||||
try {
|
||||
const findCommand = `grep -r "${encodedPackageName}" "${indexDir}" 2>/dev/null | head -5`;
|
||||
const grepResults = execSync(findCommand, {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
}).trim();
|
||||
|
||||
if (grepResults) {
|
||||
console.log(" ✅ Found content containing the package name:");
|
||||
grepResults.split("\n").forEach(line => {
|
||||
const parts = line.split(":");
|
||||
if (parts.length > 1) {
|
||||
console.log(` File: ${parts[0]}`);
|
||||
console.log(` Content: ${parts.slice(1).join(":").substring(0, 100)}...`);
|
||||
}
|
||||
});
|
||||
packageFound = true;
|
||||
}
|
||||
} catch (e) {
|
||||
// grep 没找到结果时会返回非零退出码,这是正常的
|
||||
if (e.status !== 1) {
|
||||
console.log(` ⚠️ Failed to search: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 总结查找结果
|
||||
console.log(" 📊 Summary of search results:");
|
||||
if (packageFound) {
|
||||
console.log(" ✅ Found the related information of the package in the store");
|
||||
} else {
|
||||
console.log(` ❌ Did not find the package in the store: ${packageName}`);
|
||||
console.log(" 💡 This may be the reason for the download: the package is not in the store");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`⚠️ Failed during check: ${error.message}`);
|
||||
}
|
||||
console.log("");
|
||||
|
||||
// 3. 检查 store 状态
|
||||
console.log("3️⃣ Check Store status:");
|
||||
console.log("----------------------------------------");
|
||||
try {
|
||||
const statusOutput = execSync("pnpm store status", {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
console.log(statusOutput);
|
||||
} catch (error) {
|
||||
console.log(`⚠️ Failed to get store status: ${error.message}`);
|
||||
if (error.message.includes("ENOENT")) {
|
||||
console.log(" 💡 Store index may be damaged,建议运行: pnpm store prune");
|
||||
}
|
||||
}
|
||||
console.log("");
|
||||
|
||||
// 4. 检查包的依赖信息(如果提供了项目路径)
|
||||
console.log("4️⃣ Diagnose possible reasons:");
|
||||
console.log("----------------------------------------");
|
||||
const reasons = [];
|
||||
|
||||
if (!packageFound) {
|
||||
reasons.push({
|
||||
reason: "Package not in Store",
|
||||
description: "This is the first time to install the package, or the package has never been used by other projects",
|
||||
solution: "This is a normal behavior, after installation, the package will be added to the store, and subsequent projects can reuse it",
|
||||
});
|
||||
} else if (packageVersion) {
|
||||
const dirs = foundDirs.split("\n").filter(Boolean);
|
||||
const hasExactVersion = dirs.some((dir) => {
|
||||
try {
|
||||
const pkgJsonPath = path.join(dir, "package.json");
|
||||
const pkgJsonContent = readFileSync(pkgJsonPath, "utf8");
|
||||
const pkgJson = JSON.parse(pkgJsonContent);
|
||||
return pkgJson.version === packageVersion;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasExactVersion) {
|
||||
reasons.push({
|
||||
reason: "Version mismatch",
|
||||
description: `Store has the package, but the version is not ${packageVersion}`,
|
||||
solution: "Different projects use different versions of the package, this is normal",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 检查镜像源配置
|
||||
try {
|
||||
const registry = execSync("pnpm config get registry", {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
}).trim();
|
||||
console.log(`📦 Current mirror source: ${registry}`);
|
||||
|
||||
if (registry.includes("npmmirror.com")) {
|
||||
reasons.push({
|
||||
reason: "Mirror source problem",
|
||||
description: "When using the domestic mirror source, some packages may need to be re-downloaded",
|
||||
solution: "This is normal, the mirror source synchronization may have a delay",
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略
|
||||
}
|
||||
|
||||
if (reasons.length === 0) {
|
||||
console.log("✅ No obvious problems found");
|
||||
console.log(" 💡 If still downloading, it may be:");
|
||||
console.log(" - Package integrity verification failed");
|
||||
console.log(" - Dependency resolution requires a specific version");
|
||||
console.log(" - Hard link failed, fallback to download");
|
||||
} else {
|
||||
console.log("Possible reasons:");
|
||||
reasons.forEach((item, index) => {
|
||||
console.log(`\n${index + 1}. ${item.reason}`);
|
||||
console.log(` Description: ${item.description}`);
|
||||
console.log(` Solution: ${item.solution}`);
|
||||
});
|
||||
}
|
||||
console.log("");
|
||||
|
||||
// 5. 建议的进一步检查
|
||||
console.log("5️⃣ Further checks recommended:");
|
||||
console.log("----------------------------------------");
|
||||
const encodedPackageNameForHelp = packageName
|
||||
.replace(/@/g, "%40")
|
||||
.replace(/\//g, "%2f");
|
||||
console.log("1. View detailed installation logs (add --loglevel=debug):");
|
||||
console.log(` pnpm install --loglevel=debug 2>&1 | grep -i "${packageName}"`);
|
||||
console.log("");
|
||||
console.log("2. Check the lock file of the project:");
|
||||
console.log(` cat pnpm-lock.yaml | grep -A 5 "${packageName}"`);
|
||||
console.log("");
|
||||
console.log("3. Manually check the store content (traverse all hash shards):");
|
||||
console.log(` # 方法1: 使用 find 递归查找`);
|
||||
console.log(` find "${storePath}/files" -type d -name "*${encodedPackageNameForHelp}*" 2>/dev/null`);
|
||||
console.log(` find "${storePath}/index" -type f -name "*${encodedPackageNameForHelp}*" 2>/dev/null`);
|
||||
console.log("");
|
||||
console.log(` # Method 2: Traverse the hash sharding directory (if you know the approximate hash value):`);
|
||||
console.log(` for dir in "${storePath}/files"/{00..ff}; do`);
|
||||
console.log(` [ -d "$dir" ] && ls -la "$dir" 2>/dev/null | grep -q "${encodedPackageNameForHelp}" && echo "找到在: $dir";`);
|
||||
console.log(` done`);
|
||||
console.log("");
|
||||
console.log("4. Check the dependency of the package:");
|
||||
console.log(` pnpm why ${packageName}`);
|
||||
console.log("");
|
||||
console.log("5. Use pnpm command to check (if the package is installed):");
|
||||
console.log(` pnpm list ${packageName}`);
|
||||
console.log(` pnpm store status | grep -i "${packageName}"`);
|
||||
console.log("");
|
||||
199
qiming-file-server/scripts/pnpm-check-store-usage.js
Normal file
199
qiming-file-server/scripts/pnpm-check-store-usage.js
Normal file
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 检查 pnpm store 使用情况
|
||||
* 验证环境变量是否正确传递给子进程
|
||||
*/
|
||||
|
||||
import { spawn, exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
|
||||
const execPromise = promisify(exec);
|
||||
|
||||
console.log("========================================");
|
||||
console.log("pnpm Store usage check");
|
||||
console.log("========================================\n");
|
||||
|
||||
async function checkPnpmConfig() {
|
||||
console.log("1. Check pnpm configuration:");
|
||||
console.log("----------------------------------------");
|
||||
|
||||
try {
|
||||
console.log("📦 Current Node.js process pnpm related environment variables:");
|
||||
const pnpmEnvVars = Object.keys(process.env)
|
||||
.filter(
|
||||
(key) =>
|
||||
key.toLowerCase().includes("pnpm") ||
|
||||
key === "PATH" ||
|
||||
key === "HOME"
|
||||
)
|
||||
.sort();
|
||||
|
||||
if (pnpmEnvVars.length === 0) {
|
||||
console.log(" (No pnpm related environment variables found)");
|
||||
} else {
|
||||
pnpmEnvVars.forEach((key) => {
|
||||
const value = process.env[key];
|
||||
const displayValue =
|
||||
value.length > 100 ? value.substring(0, 100) + "..." : value;
|
||||
console.log(` ${key} = ${displayValue}`);
|
||||
});
|
||||
}
|
||||
console.log("");
|
||||
|
||||
console.log(
|
||||
"📦 Get pnpm configuration through spawn (inherit process.env):"
|
||||
);
|
||||
await new Promise((resolve, reject) => {
|
||||
const child = spawn("sh", ["-c", "pnpm config list"], {
|
||||
env: process.env,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let output = "";
|
||||
child.stdout.on("data", (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
child.on("exit", (code) => {
|
||||
if (code === 0) {
|
||||
console.log(output);
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Exit code: ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log("📦 Get pnpm configuration through spawn (no env):");
|
||||
await new Promise((resolve, reject) => {
|
||||
const child = spawn("sh", ["-c", "pnpm config list"], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let output = "";
|
||||
child.stdout.on("data", (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
child.on("exit", (code) => {
|
||||
if (code === 0) {
|
||||
console.log(output);
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Exit code: ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(` ❌ 错误: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkStoreLocation() {
|
||||
console.log("\n2. 检查 Store 位置:");
|
||||
console.log("----------------------------------------");
|
||||
|
||||
try {
|
||||
const { stdout } = await execPromise("pnpm store path", {
|
||||
env: process.env,
|
||||
});
|
||||
const storePath = stdout.trim();
|
||||
console.log(`📁 Store 路径: ${storePath}`);
|
||||
|
||||
try {
|
||||
const { stdout: sizeOutput } = await execPromise(
|
||||
`du -sh "${storePath}" 2>/dev/null || echo "无法计算"`
|
||||
);
|
||||
const size = sizeOutput.split("\t")[0];
|
||||
console.log(`💾 Store 大小: ${size}`);
|
||||
} catch (e) {
|
||||
console.log("💾 Store 大小: 无法计算");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`❌ Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function testSpawnWithEnv() {
|
||||
console.log("\n3. Test spawn environment variable passing:");
|
||||
console.log("----------------------------------------");
|
||||
|
||||
console.log("✅ Test 1: spawn pass env: process.env");
|
||||
await new Promise((resolve) => {
|
||||
const child = spawn(
|
||||
"sh",
|
||||
["-c", 'echo "Registry: $(pnpm config get registry)"'],
|
||||
{
|
||||
env: process.env,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
}
|
||||
);
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
console.log(` ${data.toString().trim()}`);
|
||||
});
|
||||
|
||||
child.on("exit", () => resolve());
|
||||
});
|
||||
|
||||
console.log("\n❌ Test 2: spawn no env");
|
||||
await new Promise((resolve) => {
|
||||
const child = spawn(
|
||||
"sh",
|
||||
["-c", 'echo "Registry: $(pnpm config get registry)"'],
|
||||
{
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
}
|
||||
);
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
console.log(` ${data.toString().trim()}`);
|
||||
});
|
||||
|
||||
child.on("exit", () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
async function analyzeInstallProgress() {
|
||||
console.log("\n4. pnpm install progress indicator description:");
|
||||
console.log("----------------------------------------");
|
||||
|
||||
console.log(`
|
||||
📊 Progress indicator meaning:
|
||||
- resolved: Total number of resolved dependencies (determined version number and dependency relationship)
|
||||
- reused: Number of packages reused through hard links from the store
|
||||
- downloaded: Number of packages downloaded from the central repository
|
||||
- added: Number of packages added to node_modules
|
||||
|
||||
✅ Ideal state: downloaded = 0, means completely using local store
|
||||
|
||||
❓ Why resolved != reused?
|
||||
Different packages may be:
|
||||
1. Virtual packages (virtual references of peer dependencies)
|
||||
2. Symbolic links (links to other packages)
|
||||
3. Local packages (packages in workspace)
|
||||
4. Optional dependencies (packages skipped based on platform conditions)
|
||||
|
||||
💡 Key indicators:
|
||||
- downloaded = 0 → ✅ Environment variables take effect, using store
|
||||
- downloaded > 0 → ❌ Some packages downloaded from the network
|
||||
`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
await checkPnpmConfig();
|
||||
await checkStoreLocation();
|
||||
await testSpawnWithEnv();
|
||||
await analyzeInstallProgress();
|
||||
|
||||
console.log("\n========================================");
|
||||
console.log("Check completed!");
|
||||
console.log("========================================\n");
|
||||
} catch (error) {
|
||||
console.error("Execution error:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
37
qiming-file-server/scripts/pnpm-check-wrapper.js
Normal file
37
qiming-file-server/scripts/pnpm-check-wrapper.js
Normal file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* pnpm-check 脚本的 Node.js 包装器
|
||||
* 自动加载项目配置并传递给 bash 脚本
|
||||
*/
|
||||
|
||||
import { spawn } from "child_process";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const config = (await import("../src/appConfig/index.js")).default;
|
||||
|
||||
const projectSourceDir = config.PROJECT_SOURCE_DIR;
|
||||
const scriptPath = path.join(__dirname, "pnpm-check.sh");
|
||||
|
||||
console.log(`🔧 Current environment: ${config.NODE_ENV}`);
|
||||
console.log(`📂 Project directory: ${projectSourceDir}`);
|
||||
console.log("");
|
||||
|
||||
const child = spawn("bash", [scriptPath, projectSourceDir], {
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
PROJECT_SOURCE_DIR: projectSourceDir,
|
||||
NODE_ENV: config.NODE_ENV,
|
||||
},
|
||||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
console.error("❌ Execution failed:", error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
child.on("exit", (code) => {
|
||||
process.exit(code || 0);
|
||||
});
|
||||
423
qiming-file-server/scripts/pnpm-check.sh
Normal file
423
qiming-file-server/scripts/pnpm-check.sh
Normal file
@@ -0,0 +1,423 @@
|
||||
#!/bin/bash
|
||||
# pnpm disk usage inspection script
|
||||
|
||||
echo "======================================"
|
||||
echo "pnpm disk usage analyzer"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Ensure pnpm is installed
|
||||
if ! command -v pnpm &> /dev/null; then
|
||||
echo "❌ pnpm not found; install it first"
|
||||
echo " npm install -g pnpm"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ pnpm version: $(pnpm --version)"
|
||||
echo ""
|
||||
|
||||
# Read PROJECT_SOURCE_DIR from env file
|
||||
get_project_dir_from_env() {
|
||||
local env_name=${1:-"development"}
|
||||
local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
local project_root="$(dirname "$script_dir")"
|
||||
local env_file="$project_root/env.$env_name"
|
||||
|
||||
if [ -f "$env_file" ]; then
|
||||
local project_dir=$(grep "^PROJECT_SOURCE_DIR=" "$env_file" | cut -d'=' -f2)
|
||||
if [ -n "$project_dir" ]; then
|
||||
echo "$project_dir"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Store path
|
||||
echo "📁 pnpm store path:"
|
||||
STORE_PATH=$(pnpm store path 2>/dev/null)
|
||||
STORE_FILESYSTEM=""
|
||||
if [ -n "$STORE_PATH" ]; then
|
||||
echo " $STORE_PATH"
|
||||
|
||||
if [ -d "$STORE_PATH" ]; then
|
||||
STORE_SIZE=$(du -sh "$STORE_PATH" 2>/dev/null | awk '{print $1}')
|
||||
echo " Store size: $STORE_SIZE"
|
||||
|
||||
STORE_FILESYSTEM=$(df "$STORE_PATH" 2>/dev/null | tail -n 1 | awk '{print $1}')
|
||||
if [ -n "$STORE_FILESYSTEM" ]; then
|
||||
echo " Filesystem: $STORE_FILESYSTEM"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo " ⚠️ Could not resolve store path"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Store status
|
||||
echo "📊 pnpm store status:"
|
||||
STORE_STATUS_OUTPUT=$(pnpm store status 2>&1)
|
||||
STORE_STATUS_EXIT_CODE=$?
|
||||
|
||||
if [ $STORE_STATUS_EXIT_CODE -eq 0 ]; then
|
||||
echo "$STORE_STATUS_OUTPUT"
|
||||
else
|
||||
if echo "$STORE_STATUS_OUTPUT" | grep -q "ENOENT"; then
|
||||
echo " ⚠️ Store index missing or corrupted"
|
||||
echo " 💡 Try:"
|
||||
echo " pnpm store prune"
|
||||
else
|
||||
echo " ⚠️ Could not read store status"
|
||||
echo " Error: $(echo "$STORE_STATUS_OUTPUT" | head -n 1)"
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Project directory resolution
|
||||
# Priority: 1) CLI arg 2) PROJECT_SOURCE_DIR 3) env file 4) prompt
|
||||
PROJECT_DIR=""
|
||||
AUTO_DETECTED_DIR=""
|
||||
SOURCE_INFO=""
|
||||
|
||||
if [ -n "$1" ]; then
|
||||
AUTO_DETECTED_DIR="$1"
|
||||
SOURCE_INFO="command-line argument"
|
||||
elif [ -n "$PROJECT_SOURCE_DIR" ]; then
|
||||
AUTO_DETECTED_DIR="$PROJECT_SOURCE_DIR"
|
||||
SOURCE_INFO="env PROJECT_SOURCE_DIR"
|
||||
else
|
||||
ENV_NAME="${NODE_ENV:-development}"
|
||||
AUTO_DETECTED_DIR=$(get_project_dir_from_env "$ENV_NAME")
|
||||
|
||||
if [ -z "$AUTO_DETECTED_DIR" ]; then
|
||||
for env in development production test; do
|
||||
AUTO_DETECTED_DIR=$(get_project_dir_from_env "$env")
|
||||
if [ -n "$AUTO_DETECTED_DIR" ]; then
|
||||
ENV_NAME="$env"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -n "$AUTO_DETECTED_DIR" ]; then
|
||||
SOURCE_INFO="config file env.$ENV_NAME"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$AUTO_DETECTED_DIR" ]; then
|
||||
echo ""
|
||||
echo "📂 Detected project directory (source: $SOURCE_INFO)"
|
||||
echo " Path: $AUTO_DETECTED_DIR"
|
||||
echo ""
|
||||
read -p "👉 Press Enter to use this path, or type a different path: " USER_INPUT
|
||||
|
||||
if [ -n "$USER_INPUT" ]; then
|
||||
PROJECT_DIR="$USER_INPUT"
|
||||
echo "📝 Using entered project directory: $PROJECT_DIR"
|
||||
else
|
||||
PROJECT_DIR="$AUTO_DETECTED_DIR"
|
||||
echo "✅ Using detected project directory"
|
||||
fi
|
||||
else
|
||||
echo ""
|
||||
echo "⚠️ Could not auto-detect project directory"
|
||||
echo ""
|
||||
echo "💡 You can:"
|
||||
echo " - Pass a path: $0 /path/to/projects"
|
||||
echo " - Set env: PROJECT_SOURCE_DIR=/path/to/projects $0"
|
||||
echo " - Set NODE_ENV: NODE_ENV=development $0"
|
||||
echo ""
|
||||
|
||||
read -p "📝 Project directory path (Enter to skip project scan): " USER_INPUT
|
||||
|
||||
if [ -n "$USER_INPUT" ]; then
|
||||
PROJECT_DIR="$USER_INPUT"
|
||||
echo "📂 Using entered project directory"
|
||||
else
|
||||
echo ""
|
||||
echo "Skipping project scan..."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$PROJECT_DIR" ] && [ ! -d "$PROJECT_DIR" ]; then
|
||||
echo ""
|
||||
echo "⚠️ Project directory does not exist: $PROJECT_DIR"
|
||||
echo ""
|
||||
echo "Skipping project scan..."
|
||||
PROJECT_DIR=""
|
||||
fi
|
||||
|
||||
if [ -n "$PROJECT_DIR" ]; then
|
||||
echo ""
|
||||
echo "🔍 Scanning project directory: $PROJECT_DIR"
|
||||
echo ""
|
||||
|
||||
if [ -d "$PROJECT_DIR" ]; then
|
||||
PROJECT_FILESYSTEM=$(df "$PROJECT_DIR" 2>/dev/null | tail -n 1 | awk '{print $1}')
|
||||
if [ -n "$PROJECT_FILESYSTEM" ]; then
|
||||
echo "💾 Filesystem check:"
|
||||
echo " Project filesystem: $PROJECT_FILESYSTEM"
|
||||
if [ -n "$STORE_FILESYSTEM" ]; then
|
||||
echo " Store filesystem: $STORE_FILESYSTEM"
|
||||
echo ""
|
||||
if [ "$PROJECT_FILESYSTEM" = "$STORE_FILESYSTEM" ]; then
|
||||
echo " ✅ Project and store are on the same filesystem"
|
||||
echo " 💡 Hard links work; disk savings apply"
|
||||
else
|
||||
echo " ⚠️ Project and store are on different filesystems"
|
||||
echo " ❌ Hard links cannot cross filesystems; pnpm will copy files"
|
||||
fi
|
||||
else
|
||||
echo " ⚠️ Could not determine store filesystem"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "📦 Per-project node_modules (apparent size):"
|
||||
echo " ⚠️ Note: du double-counts hard links; real usage is lower"
|
||||
TOTAL_SIZE_KB=0
|
||||
COUNT=0
|
||||
MAX_DISPLAY=5
|
||||
|
||||
while IFS= read -r dir; do
|
||||
SIZE_HUMAN=$(du -sh "$dir" 2>/dev/null | awk '{print $1}')
|
||||
SIZE_KB=$(du -sk "$dir" 2>/dev/null | awk '{print $1}')
|
||||
PROJECT_NAME=$(echo "$dir" | sed "s|$PROJECT_DIR/||" | sed 's|/node_modules||')
|
||||
|
||||
if [ $COUNT -lt $MAX_DISPLAY ]; then
|
||||
echo " [$PROJECT_NAME] $SIZE_HUMAN (includes hard-link double counting)"
|
||||
fi
|
||||
|
||||
TOTAL_SIZE_KB=$((TOTAL_SIZE_KB + SIZE_KB))
|
||||
((COUNT++))
|
||||
done < <(find "$PROJECT_DIR" -name "node_modules" -type d -maxdepth 3 2>/dev/null)
|
||||
|
||||
if [ $COUNT -gt $MAX_DISPLAY ]; then
|
||||
echo " ... $((COUNT - MAX_DISPLAY)) more not shown"
|
||||
fi
|
||||
|
||||
if [ $TOTAL_SIZE_KB -gt 0 ]; then
|
||||
if [ $TOTAL_SIZE_KB -gt 1048576 ]; then
|
||||
TOTAL_SIZE_HUMAN=$(awk "BEGIN {printf \"%.1fG\", $TOTAL_SIZE_KB/1048576}")
|
||||
elif [ $TOTAL_SIZE_KB -gt 1024 ]; then
|
||||
TOTAL_SIZE_HUMAN=$(awk "BEGIN {printf \"%.1fM\", $TOTAL_SIZE_KB/1024}")
|
||||
else
|
||||
TOTAL_SIZE_HUMAN="${TOTAL_SIZE_KB}K"
|
||||
fi
|
||||
echo " Total apparent size: $TOTAL_SIZE_HUMAN (real usage is lower)"
|
||||
fi
|
||||
echo " Found $COUNT node_modules directories"
|
||||
echo ""
|
||||
|
||||
echo "🗂️ Per-project .pnpm folders (apparent size):"
|
||||
echo " ⚠️ Note: entries under .pnpm are hard links; little extra space per copy"
|
||||
PNPM_COUNT=0
|
||||
PNPM_TOTAL_SIZE_KB=0
|
||||
|
||||
while IFS= read -r dir; do
|
||||
SIZE_HUMAN=$(du -sh "$dir" 2>/dev/null | awk '{print $1}')
|
||||
SIZE_KB=$(du -sk "$dir" 2>/dev/null | awk '{print $1}')
|
||||
PROJECT_NAME=$(echo "$dir" | sed "s|$PROJECT_DIR/||" | sed 's|/node_modules/.pnpm||')
|
||||
|
||||
if [ $PNPM_COUNT -lt $MAX_DISPLAY ]; then
|
||||
echo " [$PROJECT_NAME] $SIZE_HUMAN (hard links, shared in store)"
|
||||
fi
|
||||
|
||||
PNPM_TOTAL_SIZE_KB=$((PNPM_TOTAL_SIZE_KB + SIZE_KB))
|
||||
((PNPM_COUNT++))
|
||||
done < <(find "$PROJECT_DIR" -type d -path "*/node_modules/.pnpm" -maxdepth 4 2>/dev/null)
|
||||
|
||||
if [ $PNPM_COUNT -gt $MAX_DISPLAY ]; then
|
||||
echo " ... $((PNPM_COUNT - MAX_DISPLAY)) more not shown"
|
||||
fi
|
||||
|
||||
if [ $PNPM_TOTAL_SIZE_KB -gt 0 ]; then
|
||||
if [ $PNPM_TOTAL_SIZE_KB -gt 1048576 ]; then
|
||||
PNPM_TOTAL_SIZE_HUMAN=$(awk "BEGIN {printf \"%.1fG\", $PNPM_TOTAL_SIZE_KB/1048576}")
|
||||
elif [ $PNPM_TOTAL_SIZE_KB -gt 1024 ]; then
|
||||
PNPM_TOTAL_SIZE_HUMAN=$(awk "BEGIN {printf \"%.1fM\", $PNPM_TOTAL_SIZE_KB/1024}")
|
||||
else
|
||||
PNPM_TOTAL_SIZE_HUMAN="${PNPM_TOTAL_SIZE_KB}K"
|
||||
fi
|
||||
echo " Total apparent size: $PNPM_TOTAL_SIZE_HUMAN (all hard-linked to store)"
|
||||
fi
|
||||
echo " Found $PNPM_COUNT .pnpm directories"
|
||||
echo ""
|
||||
|
||||
echo "💾 Filesystem usage (df):"
|
||||
echo " Whole filesystem is more accurate than summing du:"
|
||||
df -h "$PROJECT_DIR" | tail -n 1 | awk '{print " Filesystem: "$1, "| Used: "$3, "| Avail: "$4, "| Use: "$5}'
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "======================================"
|
||||
echo "💡 Important:"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
echo "⚠️ du counts hard-linked files multiple times!"
|
||||
echo " Files under each project’s .pnpm are hard links into the store."
|
||||
echo " Real disk use ≈ store size + small symlink/metadata overhead."
|
||||
echo " Often ~30–50% of what naive du sums suggest."
|
||||
echo ""
|
||||
echo "✅ Ways to see real usage:"
|
||||
echo " 1. pnpm store size (shown above)"
|
||||
echo " 2. df -h for the whole filesystem"
|
||||
echo " 3. Compare df before and after installs"
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "💡 Recommendations:"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
echo "1. Prune unused packages from the store:"
|
||||
echo " pnpm store prune"
|
||||
echo ""
|
||||
echo "2. Prefer one store and projects on the same filesystem:"
|
||||
echo " Keep projects and the store on the same volume when possible"
|
||||
echo ""
|
||||
echo "3. Tune per-project .npmrc for your registry and link strategy"
|
||||
echo ""
|
||||
|
||||
if [ -n "$PROJECT_DIR" ] && [ -d "$PROJECT_DIR" ]; then
|
||||
echo ""
|
||||
read -p "🔍 Verify hard links between two projects? (y to start, Enter to skip): " VERIFY_HARDLINK
|
||||
|
||||
if [[ "$VERIFY_HARDLINK" =~ ^[Yy]$ ]]; then
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "🔗 Hard link verification"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
echo "💡 Project layout: $PROJECT_DIR/{projectId}"
|
||||
echo ""
|
||||
|
||||
echo "📋 Project IDs under this directory (up to 5):"
|
||||
PROJECT_IDS=$(find "$PROJECT_DIR" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; 2>/dev/null | head -5)
|
||||
if [ -n "$PROJECT_IDS" ]; then
|
||||
echo "$PROJECT_IDS" | while read -r pid; do
|
||||
echo " - $pid"
|
||||
done
|
||||
else
|
||||
echo " (none)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
read -p "👉 First project ID: " PROJECT_1_ID
|
||||
|
||||
if [ -z "$PROJECT_1_ID" ]; then
|
||||
echo "❌ Project ID cannot be empty"
|
||||
else
|
||||
read -p "👉 Second project ID: " PROJECT_2_ID
|
||||
|
||||
if [ -z "$PROJECT_2_ID" ]; then
|
||||
echo "❌ Project ID cannot be empty"
|
||||
elif [ "$PROJECT_1_ID" = "$PROJECT_2_ID" ]; then
|
||||
echo "❌ Enter two different project IDs"
|
||||
else
|
||||
echo ""
|
||||
|
||||
PROJECT_1="$PROJECT_DIR/$PROJECT_1_ID"
|
||||
PROJECT_2="$PROJECT_DIR/$PROJECT_2_ID"
|
||||
PROJECT_1_NAME="$PROJECT_1_ID"
|
||||
PROJECT_2_NAME="$PROJECT_2_ID"
|
||||
|
||||
if [ ! -d "$PROJECT_1" ]; then
|
||||
echo "❌ Project directory not found: $PROJECT_1"
|
||||
elif [ ! -d "$PROJECT_2" ]; then
|
||||
echo "❌ Project directory not found: $PROJECT_2"
|
||||
else
|
||||
|
||||
echo "🔍 Comparing projects:"
|
||||
echo " • $PROJECT_1_NAME"
|
||||
echo " • $PROJECT_2_NAME"
|
||||
echo ""
|
||||
|
||||
PNPM_DIR_1="$PROJECT_1/node_modules/.pnpm"
|
||||
PNPM_DIR_2="$PROJECT_2/node_modules/.pnpm"
|
||||
|
||||
if [ ! -d "$PNPM_DIR_1" ]; then
|
||||
echo "⚠️ $PROJECT_1_NAME: no node_modules/.pnpm"
|
||||
echo " Path: $PNPM_DIR_1"
|
||||
elif [ ! -d "$PNPM_DIR_2" ]; then
|
||||
echo "⚠️ $PROJECT_2_NAME: no node_modules/.pnpm"
|
||||
echo " Path: $PNPM_DIR_2"
|
||||
else
|
||||
echo "🔎 Looking for overlapping dependencies..."
|
||||
|
||||
PACKAGES_1=($(find "$PNPM_DIR_1" -maxdepth 1 -type d -name "*@*" 2>/dev/null | xargs -I {} basename {}))
|
||||
PACKAGES_2=($(find "$PNPM_DIR_2" -maxdepth 1 -type d -name "*@*" 2>/dev/null | xargs -I {} basename {}))
|
||||
|
||||
COMMON_PACKAGES=()
|
||||
for pkg1 in "${PACKAGES_1[@]}"; do
|
||||
for pkg2 in "${PACKAGES_2[@]}"; do
|
||||
if [ "$pkg1" = "$pkg2" ]; then
|
||||
COMMON_PACKAGES+=("$pkg1")
|
||||
break
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
if [ ${#COMMON_PACKAGES[@]} -eq 0 ]; then
|
||||
echo "⚠️ No shared packages between the two projects"
|
||||
else
|
||||
echo "📦 Found ${#COMMON_PACKAGES[@]} shared package(s); verifying..."
|
||||
echo ""
|
||||
|
||||
VERIFIED_COUNT=0
|
||||
SAME_INODE_COUNT=0
|
||||
|
||||
for pkg in "${COMMON_PACKAGES[@]:0:5}"; do
|
||||
PKG_FILE_1=$(find "$PNPM_DIR_1/$pkg" -name "package.json" -path "*/node_modules/*/package.json" 2>/dev/null | head -n 1)
|
||||
PKG_FILE_2=$(find "$PNPM_DIR_2/$pkg" -name "package.json" -path "*/node_modules/*/package.json" 2>/dev/null | head -n 1)
|
||||
|
||||
if [ -n "$PKG_FILE_1" ] && [ -f "$PKG_FILE_1" ] && [ -n "$PKG_FILE_2" ] && [ -f "$PKG_FILE_2" ]; then
|
||||
INODE_1=$(ls -i "$PKG_FILE_1" | awk '{print $1}')
|
||||
INODE_2=$(ls -i "$PKG_FILE_2" | awk '{print $1}')
|
||||
|
||||
PKG_DISPLAY=$(echo "$pkg" | sed 's/@[^@]*$//')
|
||||
|
||||
if [ "$INODE_1" = "$INODE_2" ]; then
|
||||
echo " ✅ $PKG_DISPLAY: inode=$INODE_1 (same)"
|
||||
((SAME_INODE_COUNT++))
|
||||
else
|
||||
echo " ❌ $PKG_DISPLAY: $INODE_1 vs $INODE_2 (different)"
|
||||
fi
|
||||
|
||||
((VERIFIED_COUNT++))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
|
||||
if [ $VERIFIED_COUNT -eq 0 ]; then
|
||||
echo "⚠️ Could not verify package files"
|
||||
elif [ $SAME_INODE_COUNT -eq $VERIFIED_COUNT ]; then
|
||||
echo "✅ Hard links look good"
|
||||
echo " Checked $VERIFIED_COUNT package(s); all inodes match"
|
||||
echo " Same physical files → disk savings from deduplication"
|
||||
elif [ $SAME_INODE_COUNT -eq 0 ]; then
|
||||
echo "❌ Hard links not shared"
|
||||
echo " Checked $VERIFIED_COUNT package(s); inodes all differ"
|
||||
echo ""
|
||||
echo " Possible causes:"
|
||||
echo " - Projects on different filesystems"
|
||||
echo " - Filesystem without working hard links (e.g. some network mounts)"
|
||||
echo " - pnpm / store configuration"
|
||||
else
|
||||
echo "⚠️ Mixed results"
|
||||
echo " Checked $VERIFIED_COUNT package(s); $SAME_INODE_COUNT inode match(es)"
|
||||
echo " May indicate inconsistent config or partial reinstalls"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "✅ Done"
|
||||
|
||||
34
qiming-file-server/scripts/pnpm-prune-manual.js
Normal file
34
qiming-file-server/scripts/pnpm-prune-manual.js
Normal file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 手动执行 pnpm store prune 的脚本
|
||||
*
|
||||
* 使用方法:
|
||||
* node scripts/pnpm-prune-manual.js
|
||||
*/
|
||||
|
||||
import { executePruneManually } from "../src/scheduler/pnpmPruneScheduler.js";
|
||||
|
||||
async function main() {
|
||||
console.log("====================================");
|
||||
console.log("Manually execute pnpm store prune");
|
||||
console.log("====================================");
|
||||
console.log("");
|
||||
|
||||
try {
|
||||
await executePruneManually();
|
||||
console.log("");
|
||||
console.log("✅ Execution completed");
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("");
|
||||
console.error("❌ Execution failed:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// If running this script directly
|
||||
if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/").replace(/^.*[\/\\]/, ""))) {
|
||||
main();
|
||||
}
|
||||
|
||||
export { main };
|
||||
10
qiming-file-server/scripts/start-cli.js
Normal file
10
qiming-file-server/scripts/start-cli.js
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* CLI 环境启动脚本(ESM)
|
||||
* 由 qiming-file-server CLI 命令调用
|
||||
*/
|
||||
|
||||
if (!process.env.NODE_ENV) process.env.NODE_ENV = "production";
|
||||
console.log("[CLI] 启动服务 - 环境: " + process.env.NODE_ENV);
|
||||
await import("../src/server.js");
|
||||
3
qiming-file-server/scripts/start-dev.js
Normal file
3
qiming-file-server/scripts/start-dev.js
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
process.env.NODE_ENV = "development";
|
||||
await import("../src/server.js");
|
||||
3
qiming-file-server/scripts/start-prod.js
Normal file
3
qiming-file-server/scripts/start-prod.js
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
process.env.NODE_ENV = "production";
|
||||
await import("../src/server.js");
|
||||
3
qiming-file-server/scripts/start-test.js
Normal file
3
qiming-file-server/scripts/start-test.js
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
process.env.NODE_ENV = "test";
|
||||
await import("../src/server.js");
|
||||
436
qiming-file-server/scripts/test-cli.sh
Normal file
436
qiming-file-server/scripts/test-cli.sh
Normal file
@@ -0,0 +1,436 @@
|
||||
#!/bin/bash
|
||||
|
||||
#===============================================================================
|
||||
# qiming-file-server CLI 自动化测试脚本
|
||||
#
|
||||
# 功能: 自动化测试 qiming-file-server CLI 的启动、状态查询、健康检查、重启、停止等功能
|
||||
#
|
||||
# 使用方法:
|
||||
# chmod +x test-cli.sh
|
||||
# ./test-cli.sh # 模式一: pnpm link 全局命令
|
||||
# ./test-cli.sh --direct # 模式二: 直接用 node 运行 dist 目录
|
||||
# ./test-cli.sh --installed # 模式三: 已全局安装,跳过所有安装步骤
|
||||
#
|
||||
# 环境要求:
|
||||
# - bash shell
|
||||
# - curl
|
||||
# - jq (用于格式化 JSON 输出)
|
||||
# - pnpm
|
||||
# - node (v22+)
|
||||
#
|
||||
# 脚本行为:
|
||||
# 模式一 (默认): pnpm install + pnpm link 后测试
|
||||
# 模式二 (--direct): 直接用 node 运行 dist/,无需全局链接
|
||||
# 模式三 (--installed): 已全局安装,直接测试,跳过所有安装步骤
|
||||
#===============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# 项目根目录(脚本位于 scripts/ 下,根目录为上级目录)
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
DIST_DIR="${PROJECT_ROOT}/dist"
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 测试模式: "link", "direct" 或 "installed"
|
||||
TEST_MODE="link"
|
||||
|
||||
# 日志函数
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
log_section() {
|
||||
echo ""
|
||||
echo -e "${CYAN}========================================${NC}"
|
||||
echo -e "${CYAN} $1${NC}"
|
||||
echo -e "${CYAN}========================================${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# 模式一: 本地安装 - 安装依赖并全局链接,使 qiming-file-server 命令可用
|
||||
#-------------------------------------------------------------------------------
|
||||
do_local_install() {
|
||||
log_info "模式一: 本地安装模式"
|
||||
log_info "进入项目根目录 ${PROJECT_ROOT}"
|
||||
cd "${PROJECT_ROOT}"
|
||||
|
||||
log_info "执行 pnpm install..."
|
||||
pnpm install
|
||||
|
||||
log_info "执行 pnpm run build(编译 CLI 到 dist/)..."
|
||||
pnpm run build
|
||||
|
||||
log_info "执行 pnpm link --global(全局链接)..."
|
||||
pnpm link --global
|
||||
|
||||
log_success "本地安装完成,qiming-file-server 命令已可用"
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# 模式二: 直接运行 - 检查 dist 目录是否存在,直接用 node 运行
|
||||
#-------------------------------------------------------------------------------
|
||||
check_direct_mode() {
|
||||
if [ ! -d "${DIST_DIR}" ]; then
|
||||
log_error "dist 目录不存在,请先运行: npm run build"
|
||||
log_info "或者使用默认模式: ./test-cli.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "${DIST_DIR}/cli.js" ]; then
|
||||
log_error "dist/cli.js 不存在,请先运行: npm run build"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "模式二: 直接运行模式"
|
||||
log_info "将使用 'node ${DIST_DIR}/cli.js' 执行测试"
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# 模式三: 已安装 - 跳过所有安装步骤,直接使用全局命令测试
|
||||
#-------------------------------------------------------------------------------
|
||||
check_installed_mode() {
|
||||
if ! command -v qiming-file-server &> /dev/null; then
|
||||
log_error "未找到 qiming-file-server 命令,请先全局安装:"
|
||||
log_info " npm install -g qiming-file-server"
|
||||
log_info "或者使用其他模式:"
|
||||
log_info " ./test-cli.sh # 模式一: pnpm link"
|
||||
log_info " ./test-cli.sh --direct # 模式二: node 直接运行"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "模式三: 已安装模式"
|
||||
log_info "检测到 qiming-file-server 全局命令"
|
||||
log_info "将直接使用 'qiming-file-server' 执行测试"
|
||||
}
|
||||
|
||||
# 测试函数
|
||||
test_command() {
|
||||
local cmd=$1
|
||||
local description=$2
|
||||
log_info "测试: ${description}"
|
||||
log_info "命令: ${cmd}"
|
||||
eval "${cmd}"
|
||||
local exit_code=$?
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
log_success "✓ 完成: ${description}"
|
||||
return 0
|
||||
else
|
||||
log_error "✗ 失败: ${description} (退出码: ${exit_code})"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 默认配置
|
||||
PORT=${PORT:-60000}
|
||||
HEALTH_URL="http://localhost:${PORT}/health"
|
||||
PROJECT_DIR=${PROJECT_DIR:-./test-projects}
|
||||
NGINX_DIR=${NGINX_DIR:-./test-nginx}
|
||||
UPLOAD_DIR=${UPLOAD_DIR:-./test-uploads}
|
||||
|
||||
# 显示配置
|
||||
show_config() {
|
||||
log_section "测试配置"
|
||||
echo " 测试模式: ${TEST_MODE_LABEL}"
|
||||
echo " 端口: ${PORT}"
|
||||
echo " 健康检查: ${HEALTH_URL}"
|
||||
echo " 项目目录: ${PROJECT_DIR}"
|
||||
echo " Nginx目录: ${NGINX_DIR}"
|
||||
echo " 上传目录: ${UPLOAD_DIR}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 获取模式标签
|
||||
get_mode_label() {
|
||||
case "${TEST_MODE}" in
|
||||
link)
|
||||
TEST_MODE_LABEL="pnpm link 全局命令"
|
||||
;;
|
||||
direct)
|
||||
TEST_MODE_LABEL="node 直接运行"
|
||||
;;
|
||||
installed)
|
||||
TEST_MODE_LABEL="已全局安装"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 检查依赖
|
||||
check_dependencies() {
|
||||
log_info "检查依赖..."
|
||||
|
||||
local missing_deps=()
|
||||
|
||||
if ! command -v curl &> /dev/null; then
|
||||
missing_deps+=("curl")
|
||||
fi
|
||||
|
||||
if ! command -v jq &> /dev/null; then
|
||||
missing_deps+=("jq")
|
||||
fi
|
||||
|
||||
# 模式一和模式二需要 pnpm
|
||||
if [ "${TEST_MODE}" != "installed" ]; then
|
||||
if ! command -v pnpm &> /dev/null; then
|
||||
missing_deps+=("pnpm")
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! command -v node &> /dev/null; then
|
||||
missing_deps+=("node")
|
||||
fi
|
||||
|
||||
if [ ${#missing_deps[@]} -gt 0 ]; then
|
||||
log_error "缺少依赖: ${missing_deps[*]}"
|
||||
log_info "请安装后再运行,例如: brew install ${missing_deps[*]}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "依赖已满足"
|
||||
}
|
||||
|
||||
# 获取当前模式下的命令前缀
|
||||
get_cmd_prefix() {
|
||||
case "${TEST_MODE}" in
|
||||
link)
|
||||
echo "qiming-file-server"
|
||||
;;
|
||||
direct)
|
||||
echo "node ${DIST_DIR}/cli.js"
|
||||
;;
|
||||
installed)
|
||||
echo "qiming-file-server"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 清理函数:停止服务
|
||||
cleanup() {
|
||||
log_info "清理环境(停止服务)..."
|
||||
local cmd_prefix=$(get_cmd_prefix)
|
||||
${cmd_prefix} stop 2>/dev/null || true
|
||||
sleep 1
|
||||
}
|
||||
|
||||
# 退出时清理
|
||||
cleanup_exit() {
|
||||
local exit_code=$?
|
||||
|
||||
# 只在模式一下取消全局链接
|
||||
if [ "${TEST_MODE}" = "link" ]; then
|
||||
log_info "取消全局链接: pnpm unlink --global"
|
||||
(cd "${PROJECT_ROOT}" && pnpm unlink --global) 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ $exit_code -ne 0 ]; then
|
||||
exit $exit_code
|
||||
fi
|
||||
}
|
||||
|
||||
# 测试健康检查端点
|
||||
test_health_endpoint() {
|
||||
log_info "测试健康检查端点..."
|
||||
|
||||
local response=$(curl -s "${HEALTH_URL}")
|
||||
local status=$(echo "${response}" | jq -r '.status' 2>/dev/null || echo "error")
|
||||
|
||||
if [ "${status}" = "ok" ]; then
|
||||
log_success "健康检查通过"
|
||||
log_info "响应: ${response}"
|
||||
return 0
|
||||
else
|
||||
log_error "健康检查失败,状态: ${status}"
|
||||
log_info "响应: ${response}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 测试服务状态查询
|
||||
test_status_command() {
|
||||
log_info "测试服务状态查询..."
|
||||
local cmd_prefix=$(get_cmd_prefix)
|
||||
${cmd_prefix} status
|
||||
log_success "状态查询完成"
|
||||
}
|
||||
|
||||
# 主测试流程
|
||||
run_tests() {
|
||||
get_mode_label
|
||||
show_config
|
||||
check_dependencies
|
||||
|
||||
log_section "开始测试"
|
||||
|
||||
# 根据模式执行不同的初始化
|
||||
case "${TEST_MODE}" in
|
||||
link)
|
||||
do_local_install
|
||||
;;
|
||||
direct)
|
||||
check_direct_mode
|
||||
;;
|
||||
installed)
|
||||
check_installed_mode
|
||||
;;
|
||||
esac
|
||||
|
||||
# 清理环境(停止可能残留的服务)
|
||||
cleanup
|
||||
|
||||
# ========== 测试 1: 启动服务 ==========
|
||||
log_section "测试 1: 启动服务"
|
||||
local cmd_prefix=$(get_cmd_prefix)
|
||||
test_command "${cmd_prefix} start --env production --port ${PORT}" "启动服务"
|
||||
sleep 3
|
||||
|
||||
# ========== 测试 2: 健康检查 ==========
|
||||
log_section "测试 2: 健康检查"
|
||||
test_health_endpoint
|
||||
|
||||
# ========== 测试 3: 状态查询 ==========
|
||||
log_section "测试 3: 状态查询"
|
||||
test_status_command
|
||||
|
||||
# ========== 测试 4: 重启服务 ==========
|
||||
log_section "测试 4: 重启服务"
|
||||
test_command "${cmd_prefix} restart" "重启服务"
|
||||
sleep 3
|
||||
|
||||
# ========== 测试 5: 重启后健康检查 ==========
|
||||
log_section "测试 5: 重启后健康检查"
|
||||
test_health_endpoint
|
||||
|
||||
# ========== 测试 6: 停止服务 ==========
|
||||
log_section "测试 6: 停止服务"
|
||||
test_command "${cmd_prefix} stop" "停止服务"
|
||||
|
||||
# ========== 完成 ==========
|
||||
log_section "测试完成"
|
||||
log_success "所有 CLI 测试通过!"
|
||||
|
||||
# 根据模式显示不同的后续提示
|
||||
echo ""
|
||||
case "${TEST_MODE}" in
|
||||
link)
|
||||
echo "已测试全局命令 'qiming-file-server'"
|
||||
;;
|
||||
direct)
|
||||
echo "已测试 'node ${DIST_DIR}/cli.js' 直接运行"
|
||||
;;
|
||||
installed)
|
||||
echo "已测试全局安装版本"
|
||||
;;
|
||||
esac
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 显示帮助信息
|
||||
show_help() {
|
||||
echo "qiming-file-server CLI 测试脚本"
|
||||
echo ""
|
||||
echo "用法: $0 [选项]"
|
||||
echo ""
|
||||
echo "选项:"
|
||||
echo " -p, --port <端口> 指定测试端口 (默认: 60000)"
|
||||
echo " --direct 模式二: 直接用 node 运行 dist/"
|
||||
echo " --installed 模式三: 已全局安装,跳过所有安装步骤"
|
||||
echo " --project-dir <目录> 指定项目目录"
|
||||
echo " --nginx-dir <目录> 指定 Nginx 目录"
|
||||
echo " --upload-dir <目录> 指定上传目录"
|
||||
echo " -h, --help 显示帮助信息"
|
||||
echo ""
|
||||
echo "三种测试模式:"
|
||||
echo " 1. 默认模式 (pnpm link): ./test-cli.sh"
|
||||
echo " - 执行 pnpm install + pnpm run build + pnpm link"
|
||||
echo " - 使用全局命令 'qiming-file-server'"
|
||||
echo ""
|
||||
echo " 2. 直接运行模式: ./test-cli.sh --direct"
|
||||
echo " - 无需 pnpm link"
|
||||
echo " - 使用 'node dist/cli.js' 运行"
|
||||
echo ""
|
||||
echo " 3. 已安装模式: ./test-cli.sh --installed"
|
||||
echo " - 跳过所有安装步骤"
|
||||
echo " - 直接使用已全局安装的命令"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " $0 # 模式一: pnpm link 全局命令"
|
||||
echo " $0 --direct # 模式二: node 直接运行"
|
||||
echo " $0 --installed # 模式三: 已全局安装"
|
||||
echo " $0 --port 60001 # 指定端口"
|
||||
echo " $0 --installed -p 60001 # 组合使用"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 解析命令行参数
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-p|--port)
|
||||
PORT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--direct)
|
||||
TEST_MODE="direct"
|
||||
shift
|
||||
;;
|
||||
--installed)
|
||||
TEST_MODE="installed"
|
||||
shift
|
||||
;;
|
||||
--project-dir)
|
||||
PROJECT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--nginx-dir)
|
||||
NGINX_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--upload-dir)
|
||||
UPLOAD_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "未知参数: $1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# 脚本入口
|
||||
main() {
|
||||
parse_args "$@"
|
||||
|
||||
# 无论正常结束、失败或 Ctrl+C,退出时都执行清理
|
||||
trap 'cleanup_exit' EXIT
|
||||
|
||||
run_tests
|
||||
}
|
||||
|
||||
# 执行主函数
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user