template-syncer
Version:
智能模板同步工具 - 让你的项目与模板仓库保持同步,支持智能合并、差异对比和交互式更新
388 lines (387 loc) • 14 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.TemplateSyncer = void 0;
const path = __importStar(require("path"));
const utils_1 = require("./utils");
const ui_1 = require("./ui");
const CONFIG_FILE = '.template-sync.json';
/**
* 模板同步器
*/
class TemplateSyncer {
constructor(options = {}) {
this.options = {
tempDir: '.temp-template',
verbose: false,
...options
};
this.rules = {
deleteOrphans: false,
deletePatterns: [],
protectPatterns: [],
autoBackup: true,
defaultMergeStrategy: 'overwrite',
...options.rules
};
this.git = new utils_1.Git(this.options.verbose);
this.scanner = new utils_1.Scanner([...(options.ignore || []), `${this.options.tempDir}/**`], options.categories);
this.merger = new utils_1.Merger(options.mergers);
}
/**
* 加载配置
*/
loadConfig() {
const config = utils_1.platform.readJson(CONFIG_FILE) || {};
// 合并 rules
if (config.rules) {
this.rules = { ...this.rules, ...config.rules };
}
return config;
}
/**
* 保存配置
*/
saveConfig(config) {
utils_1.platform.writeJson(CONFIG_FILE, config);
}
/**
* 获取模板仓库
*/
async getRepo() {
if (this.options.repo)
return this.options.repo;
const config = this.loadConfig();
if (config.repo) {
this.options.repo = config.repo;
if (config.branch && !this.options.branch) {
this.options.branch = config.branch;
}
return config.repo;
}
const repo = await ui_1.prompts.inputRepo();
this.options.repo = repo;
this.saveConfig({ ...config, repo });
return repo;
}
/**
* 克隆模板
*/
async cloneTemplate() {
const repo = await this.getRepo();
ui_1.logger.step('测试仓库连接...');
if (!this.git.testConnection(repo)) {
throw new Error('无法连接到模板仓库');
}
ui_1.logger.success('仓库连接成功');
ui_1.logger.step('克隆模板...');
this.git.clone(repo, this.options.tempDir);
// 选择分支
if (!this.options.branch) {
const branches = this.git.getBranches(this.options.tempDir);
if (branches.length > 1) {
console.log(`\n发现 ${branches.length} 个分支`);
this.options.branch = await ui_1.prompts.selectBranch(branches);
}
else {
this.options.branch = branches[0] || 'main';
}
}
// 切换分支
if (this.options.branch && !['main', 'master'].includes(this.options.branch)) {
ui_1.logger.step(`切换到分支: ${this.options.branch}`);
this.git.checkout(this.options.branch, this.options.tempDir);
}
this.git.removeGitDir(this.options.tempDir);
ui_1.logger.success('模板克隆完成');
}
/**
* 扫描变更
*/
async scanChanges() {
ui_1.logger.step('扫描文件差异...');
const detectOrphans = this.rules.deleteOrphans ||
(this.rules.deletePatterns && this.rules.deletePatterns.length > 0);
let changes = await this.scanner.compare(this.options.tempDir, process.cwd(), detectOrphans);
// 过滤要删除的文件
if (detectOrphans) {
const orphans = changes.filter(c => c.status === 'deleted');
const others = changes.filter(c => c.status !== 'deleted');
// 如果 deletePatterns 为空数组,使用默认值 ['**'] 匹配所有文件
const patterns = this.rules.deletePatterns && this.rules.deletePatterns.length > 0
? this.rules.deletePatterns
: ['**'];
const filteredOrphans = this.scanner.filterOrphans(orphans, patterns, this.rules.protectPatterns || []);
changes = [...others, ...filteredOrphans];
}
return changes;
}
/**
* 应用变更
*/
async applyChanges(changes) {
const result = { success: [], skipped: [], failed: [] };
console.log(`\n开始处理 ${changes.length} 个文件...\n`);
// 并发处理所有文件
await Promise.allSettled(changes.map(async (file) => {
try {
if (file.status === 'deleted') {
utils_1.platform.removeFile(file.currentPath);
result.success.push(file.path);
ui_1.logger.file('🗑️', file.path, '已删除');
}
else {
const targetPath = path.join(process.cwd(), file.path);
const mergeResult = await this.merger.merge(file.templatePath, file.currentPath, targetPath);
if (mergeResult.success) {
result.success.push(file.path);
}
else {
result.skipped.push(file.path);
}
ui_1.logger.file(file.icon, file.path, mergeResult.message);
}
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
result.failed.push({ path: file.path, error: message });
ui_1.logger.file(file.icon, file.path, `失败: ${message}`);
}
}));
return result;
}
/**
* 生成推荐
*/
generateRecommendations(changes) {
if (changes.length === 0)
return [];
const high = [];
const medium = [];
const low = [];
// 核心配置文件
const coreFiles = new Set([
'package.json', 'tsconfig.json', 'jsconfig.json',
'vite.config.ts', 'vite.config.js', 'vite.config.mjs'
]);
// 开发工具配置
const devCategories = new Set([
'代码质量', '代码格式化', '构建配置', 'TypeScript', '测试配置'
]);
for (const file of changes) {
const fileName = path.basename(file.path);
if (coreFiles.has(fileName)) {
high.push(file);
}
else if (devCategories.has(file.category)) {
medium.push(file);
}
else {
low.push(file);
}
}
const recommendations = [];
if (high.length > 0) {
recommendations.push({
priority: 'high',
title: '核心配置更新',
description: '更新项目核心配置文件',
files: high
});
}
if (medium.length > 0) {
recommendations.push({
priority: 'medium',
title: '开发工具配置',
description: '更新代码质量和构建工具配置',
files: medium
});
}
if (low.length > 0) {
recommendations.push({
priority: 'low',
title: '其他文件',
description: '更新文档、样式等其他文件',
files: low
});
}
return recommendations;
}
/**
* 清理临时文件
*/
cleanup() {
utils_1.platform.removeDir(this.options.tempDir);
ui_1.logger.step('清理临时文件完成');
}
/**
* 主同步流程
*/
async sync() {
try {
ui_1.logger.info('🚀 开始模板同步...');
// 创建备份
ui_1.logger.step('创建 Git 备份...');
if (this.git.backup()) {
ui_1.logger.success('备份已创建');
}
else {
ui_1.logger.warn('Git 备份失败,请确保有变更需要备份');
}
// 克隆模板
await this.cloneTemplate();
// 扫描变更
const changes = await this.scanChanges();
if (changes.length === 0) {
ui_1.logger.success('没有发现任何变更,项目已是最新');
return;
}
// 显示变更统计
ui_1.prompts.showChangeSummary(changes);
// 选择处理方式
const action = await ui_1.prompts.selectAction();
if (action === 'cancel') {
ui_1.logger.warn('操作已取消');
return;
}
let selected;
if (action === 'all') {
selected = changes;
}
else if (action === 'category') {
selected = await ui_1.prompts.selectByCategory(changes);
}
else {
selected = await ui_1.prompts.selectIndividually(changes);
}
if (selected.length === 0) {
ui_1.logger.warn('没有选择任何文件');
return;
}
// 确认
const confirmed = await ui_1.prompts.confirm(`确定要处理 ${selected.length} 个文件吗?`);
if (!confirmed) {
ui_1.logger.warn('操作已取消');
return;
}
// 应用变更
const result = await this.applyChanges(selected);
ui_1.logger.summary(result);
ui_1.logger.info('🎉 模板同步完成!');
}
finally {
this.cleanup();
}
}
async batch() {
try {
ui_1.logger.info('🔄 批量处理模式');
await this.cloneTemplate();
const changes = await this.scanChanges();
if (changes.length === 0) {
ui_1.logger.success('没有发现需要处理的文件');
return;
}
const recommendations = this.generateRecommendations(changes);
const selected = await ui_1.prompts.selectRecommendations(recommendations);
const files = selected.flatMap(r => r.files);
if (files.length > 0) {
const result = await this.applyChanges(files);
ui_1.logger.summary(result);
}
}
finally {
this.cleanup();
}
}
/**
* 预览模式
*/
async preview() {
try {
ui_1.logger.info('🔍 预览模式');
await this.cloneTemplate();
const changes = await this.scanChanges();
if (changes.length === 0) {
ui_1.logger.success('没有发现任何差异');
return;
}
ui_1.prompts.showChangeSummary(changes);
// 显示完整文件树
console.log('📁 文件结构:\n');
console.log((0, ui_1.formatFileTree)(changes));
}
finally {
this.cleanup();
}
}
async smart() {
try {
ui_1.logger.info('🤖 智能同步模式');
await this.cloneTemplate();
const changes = await this.scanChanges();
if (changes.length === 0) {
ui_1.logger.success('项目已是最新,无需同步');
return;
}
const recommendations = this.generateRecommendations(changes);
const selected = await ui_1.prompts.selectRecommendations(recommendations);
const files = selected.flatMap(r => r.files);
if (files.length > 0) {
const confirmed = await ui_1.prompts.confirm(`确定要处理 ${files.length} 个文件吗?`);
if (confirmed) {
const result = await this.applyChanges(files);
ui_1.logger.summary(result);
}
}
}
finally {
this.cleanup();
}
}
async init() {
ui_1.logger.info('🔧 初始化配置向导\n');
const answers = await ui_1.prompts.initConfig();
const config = {
repo: answers.repo || undefined,
branch: answers.branch || undefined,
ignore: answers.ignore.length > 0 ? answers.ignore : undefined,
lastSync: new Date().toISOString()
};
this.saveConfig(config);
ui_1.logger.success(`配置已保存到 ${CONFIG_FILE}`);
}
}
exports.TemplateSyncer = TemplateSyncer;