chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

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

View 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("");

View 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();

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

View 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 projects .pnpm are hard links into the store."
echo " Real disk use ≈ store size + small symlink/metadata overhead."
echo " Often ~3050% 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"

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

View 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");

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env node
process.env.NODE_ENV = "development";
await import("../src/server.js");

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env node
process.env.NODE_ENV = "production";
await import("../src/server.js");

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env node
process.env.NODE_ENV = "test";
await import("../src/server.js");

View 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 "$@"