import fs from 'fs'; import path from 'path'; import glob from 'fast-glob'; import { program, Command } from '@/program.ts'; import { getConfig, query } from '@/module/index.ts'; import { fileIsExist } from '@/uitls/file.ts'; import { chalk } from '@/module/chalk.ts'; import * as backServices from '@/query/services/index.ts'; import { input } from '@inquirer/prompts'; import { logger } from '@/module/logger.ts'; // 查找文件(忽略大小写) async function findFileInsensitive(targetFile: string): Promise { const files = fs.readdirSync('.'); const matchedFile = files.find((file) => file.toLowerCase() === targetFile.toLowerCase()); return matchedFile || null; } // 递归收集文件信息 async function collectFileInfo(filePath: string, baseDir = '.'): Promise { const stats = fs.statSync(filePath); const relativePath = path.relative(baseDir, filePath); if (stats.isFile()) { return [{ path: relativePath, size: stats.size }]; } if (stats.isDirectory()) { const files = fs.readdirSync(filePath); const results = await Promise.all(files.map((file) => collectFileInfo(path.join(filePath, file), baseDir))); return results.flat(); } return []; } /** * 复制文件到 pack-dist * @param files 文件列表, 或者文件夹列表 * @param cwd 当前工作目录 * @param packDist 打包目录 pack-dist * @param mergeDist 是否合并 dist 目录到 pack-dist 中 */ export const copyFilesToPackDist = async (files: string[], cwd: string, packDist = 'pack-dist', mergeDist = true) => { const packDistPath = path.join(cwd, packDist); if (!fileIsExist(packDistPath)) { fs.mkdirSync(packDistPath, { recursive: true }); } else { fs.rmSync(packDistPath, { recursive: true, force: true }); } files.forEach((file) => { const stat = fs.statSync(path.join(cwd, file)); let outputFile = file; if (mergeDist) { if (file.startsWith('dist/')) { outputFile = file.replace(/^dist\//, ''); } else if (file === 'dist') { outputFile = ''; } } if (stat.isDirectory()) { fs.cpSync(path.join(cwd, file), path.join(packDistPath, outputFile), { recursive: true }); } else { fs.copyFileSync(path.join(cwd, file), path.join(packDistPath, outputFile)); } }); const packageInfo = await getPackageInfo(); // 根据所有文件,生成一个index.html const indexHtmlPath = path.join(packDistPath, 'index.html'); const collectionFiles = (await Promise.all(files.map((file) => collectFileInfo(file)))).flat(); const prettifySize = (size: number) => { if (size < 1024) { return `${size}B`; } if (size < 1024 * 1024) { return `${(size / 1024).toFixed(2)}kB`; } return `${(size / 1024 / 1024).toFixed(2)}MB`; }; const filesString = collectionFiles.map((file) => `
  • ${file.path}${prettifySize(file.size)}
  • `).join('\n'); const indexHtmlContent = ` ${packageInfo.name}

    ${packageInfo.name}

    ${JSON.stringify(packageInfo, null, 2)}
    `; if (!fileIsExist(indexHtmlPath)) { fs.writeFileSync(indexHtmlPath, indexHtmlContent); } }; export const pack = async (opts: { packDist?: string, mergeDist?: boolean }) => { const cwd = process.cwd(); const collection: Record = {}; const mergeDist = opts.mergeDist !== false; const packageJsonPath = path.join(cwd, 'package.json'); if (!fileIsExist(packageJsonPath)) { console.error('package.json not found'); return; } let packageJson; try { const packageContent = fs.readFileSync(packageJsonPath, 'utf-8'); packageJson = JSON.parse(packageContent); } catch (error) { console.error('Invalid package.json:', error); return; } let files = packageJson.files; // 从 package.json 的 files 字段收集文件 const filesToInclude = files ? await glob(files, { cwd: cwd, dot: true, // 包括隐藏文件 onlyFiles: false, // 包括目录 followSymbolicLinks: true, // 处理符号链接 ignore: ['node_modules/**', ".git/**", opts.packDist ? opts.packDist + '/**' : ''], }) : []; // 确保 README.md 和 dist 存在(忽略大小写检测 README.md) const readmeFile = await findFileInsensitive('README.md'); if (readmeFile && !filesToInclude.includes(readmeFile)) { filesToInclude.push(readmeFile); } const packageFile = await findFileInsensitive('package.json'); if (packageFile && !filesToInclude.includes(packageFile)) { filesToInclude.push(packageFile); } const allFiles = (await Promise.all(filesToInclude.map((file) => collectFileInfo(file)))).flat(); // 输出文件详细信息 logger.debug('文件列表:'); allFiles.forEach((file) => { logger.debug(`${file.size}B ${file.path}`); }); const totalSize = allFiles.reduce((sum, file) => sum + file.size, 0); collection.files = allFiles; collection.packageJson = packageJson; collection.totalSize = totalSize; collection.tags = packageJson.app?.tags || packageJson.keywords || []; logger.debug('\n基本信息'); logger.debug(`name: ${packageJson.name}`); logger.debug(`version: ${packageJson.version}`); logger.debug(`total files: ${allFiles.length}`); try { copyFilesToPackDist(filesToInclude, cwd, opts.packDist, mergeDist); } catch (error) { console.error('Error creating tarball:', error); } const readme = await findFileInsensitive('README.md'); if (readme) { const readmeContent = fs.readFileSync(readme, 'utf-8'); collection.readme = readmeContent; } return { collection, dir: cwd }; }; export const getPackageInfo = async () => { const cwd = process.cwd(); const packageJsonPath = path.join(cwd, 'package.json'); try { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); return packageJson; } catch (error) { console.error('Invalid package.json:', error); return {}; } }; const publishCommand = new Command('publish') .description('发布应用') .option('-k, --key ', '应用 key') .option('-v, --version ', '应用版本') .action(async (options) => { const { key, version } = options; const config = await getConfig(); console.log('发布逻辑实现', { key, version, config }); }); const deployLoadFn = async (id: string, fileKey: string, force = true, install = false) => { if (!id) { console.error(chalk.red('id is required')); return; } let appKey = ''; let version = ''; if (id && id.includes('/')) { const [a, b] = id.split('/'); if (a) { appKey = b || '1.0.0'; version = a; id = ''; } else { console.error(chalk.red('id format error, please use "version/appKey" format')); return; } } const res = await query.post({ path: 'micro-app', key: 'deploy', data: { id: id, version: version, appKey: appKey, key: fileKey, force: force, install: !!install, }, }); if (res.code === 200) { console.log('deploy-load success. current version:', res.data?.pkg?.version); console.log('run: ', 'envision services -s', res.data?.showAppInfo?.key); } else { console.error('deploy-load 失败', res.message); } return res; }; const packCommand = new Command('pack') .description('打包应用, 使用 package.json 中的 files 字段,如果是 dist 的路径,直接复制到 pack-dist 的根目录') .option('-p, --publish', '打包并发布') .option('-u, --update', '发布后显示更新命令, show command for deploy to server') .option('-d, --packDist ', '打包到的目录') .option('-m, --mergeDist ', '合并 dist 目录到 pack-dist 中', "true") .option('-y, --yes ', '确定,直接打包', "true") .option('-c, --clean', '清理 package.json中的 devDependencies') .action(async (opts) => { const packDist = opts.packDist || 'pack-dist'; const mergeDist = opts.mergeDist === "true"; const yes = opts.yes === "true"; const packageInfo = await getPackageInfo(); if (!packageInfo) { console.error('Invalid package.json:'); return; } let basename = packageInfo.basename || ''; let appKey: string | undefined; let version = packageInfo.version || ''; if (!version) { version = await input({ message: 'Enter your version:', }); } if (basename) { if (basename.startsWith('/')) { basename = basename.slice(1); } const basenameArr = basename.split('/'); if (basenameArr.length !== 2) { console.error(chalk.red('basename is error, 请输入正确的路径, packages.json中basename例如 root/appKey')); return; } appKey = basenameArr[1] || ''; } if (!appKey) { appKey = await input({ message: 'Enter your appKey:', }); } let value = await pack({ packDist, mergeDist }); if (opts?.clean) { const newPackageJson = { ...packageInfo }; delete newPackageJson.devDependencies; fs.writeFileSync(path.join(process.cwd(), 'pack-dist', 'package.json'), JSON.stringify(newPackageJson, null, 2)); } if (opts.publish) { // 运行 deploy 命令 // const runDeployCommand = 'envision pack-deploy ' + value.outputFilePath + ' -k ' + appKey; const [_app, _command] = process.argv; let deployDist = packDist; const deployCommand = [_app, _command, 'deploy', deployDist, '-k', appKey, '-v', version, '-u', '-d']; if (opts.org) { deployCommand.push('-o', opts.org); } if (opts.update) { deployCommand.push('-s'); } if (yes) { deployCommand.push('-y', 'yes'); } logger.debug(chalk.blue('deploy doing: '), deployCommand.slice(2).join(' '), '\n'); // console.log('pack deploy services', chalk.blue('example: '), runDeployCommand); program.parse(deployCommand); } }); const packDeployCommand = new Command('pack-deploy') .argument('', 'id') .option('-k, --key ', 'fileKey, 服务器的部署文件夹的列表') .option('-i, --install ', 'install dependencies') .action(async (id, opts) => { let { key, install } = opts || {}; const res = await deployLoadFn(id, key, true, install); }); program.addCommand(packDeployCommand); program.addCommand(publishCommand); program.addCommand(packCommand); enum AppType { /** * run in (import way) */ SystemApp = 'system-app', /** * fork 执行 */ MicroApp = 'micro-app', GatewayApp = 'gateway-app', /** * pm2 启动 */ Pm2SystemApp = 'pm2-system-app', /** * 脚本应用 */ ScriptApp = 'script-app', } type ServiceItem = { key: string; status: 'inactive' | 'running' | 'stop' | 'error' | 'unknown'; type: AppType; description: string; version: string; }; const servicesCommand = new Command('services') .description('服务器registry当中的服务管理') .option('-l, --list', 'list services') .option('-r, --restart ', 'restart services') .option('-s, --start ', 'start services') .option('-t, --stop ', 'stop services') .option('-i, --info ', 'info services') .option('-d, --delete ', 'delete services') .action(async (opts) => { // if (opts.list) { const res = await backServices.queryServiceList(); if (res.code === 200) { // console.log('res', JSON.stringify(res.data, null, 2)); const data = res.data as ServiceItem[]; console.log('services list'); const getMaxLengths = (data) => { const lengths = { key: 0, status: 0, type: 0, description: 0, version: 0 }; data.forEach((item) => { lengths.key = Math.max(lengths.key, item.key.length); lengths.status = Math.max(lengths.status, item.status.length); lengths.type = Math.max(lengths.type, item.type.length); lengths.description = Math.max(lengths.description, item.description.length); lengths.version = Math.max(lengths.version, item.version.length); }); return lengths; }; const lengths = getMaxLengths(data); const padString = (str, length) => str + ' '.repeat(Math.max(length - str.length, 0)); try { console.log( chalk.blue(padString('Key', lengths.key)), chalk.green(padString('Status', lengths.status)), chalk.yellow(padString('Type', lengths.type)), chalk.red(padString('Version', lengths.version)), ); } catch (error) { console.error('error', error); } data.forEach((item) => { console.log( chalk.blue(padString(item.key, lengths.key)), chalk.green(padString(item.status, lengths.status)), chalk.blue(padString(item.type, lengths.type)), chalk.green(padString(item.version, lengths.version)), ); }); } else { console.log('error', chalk.red(res.message || '获取列表失败')); } return; } if (opts.restart) { const res = await backServices.queryServiceOperate(opts.restart, 'restart'); if (res.code === 200) { console.log('restart success'); } else { console.error('restart failed', res.message); } return; } if (opts.start) { const res = await backServices.queryServiceOperate(opts.start, 'start'); if (res.code === 200) { console.log('start success'); } else { console.error('start failed', res.message); } return; } if (opts.stop) { const res = await backServices.queryServiceOperate(opts.stop, 'stop'); if (res.code === 200) { console.log('stop success'); } else { console.log(chalk.red('stop failed'), res.message); } return; } if (opts.info) { const res = await backServices.queryServiceList(); if (res.code === 200) { const data = res.data as ServiceItem[]; const item = data.find((item) => item.key === opts.info); if (!item) { console.log('not found'); return; } console.log(chalk.blue(item.key), chalk.green(item.status), chalk.yellow(item.type), chalk.red(item.version)); console.log('description:', chalk.blue(item.description)); } else { console.log(chalk.red(res.message || '获取列表失败')); } } if (opts.delete) { // const res = await backServices.queryServiceOperate(opts.delete, 'delete'); const res = await backServices.queryServiceDelect(opts.delete); if (res.code === 200) { console.log('delete success'); } else { console.log(chalk.red('delete failed'), res.message); } } }); const detectCommand = new Command('detect').description('检测服务, 当返回内容不为true,则是有新增的内容').action(async () => { const res = await backServices.queryServiceDetect(); console.log('detect', res); }); program.addCommand(servicesCommand); servicesCommand.addCommand(detectCommand);