UNPKG

@dao-style/cli

Version:

CLI tool for DAO Style projects - providing project scaffolding, template generation and dependency management

1,454 lines (1,440 loc) 58.7 kB
// src/commands/create.ts import { execSync as execSync3 } from "node:child_process"; import { dirname as dirname2 } from "node:path"; import { fileURLToPath as fileURLToPath2 } from "node:url"; import * as path3 from "path"; import { checkbox } from "@inquirer/prompts"; import chalk3 from "chalk"; import * as fs2 from "fs-extra"; import { set } from "lodash-es"; import ora2 from "ora"; // package.json var version = "0.4.0"; // src/utils/file.ts import { fileURLToPath } from "node:url"; import { dirname } from "path"; import { outputFile } from "fs-extra"; function cliWriteFile(filePath, content) { return outputFile(filePath, content.content, { encoding: content.encoding }); } var __filename = fileURLToPath(import.meta.url); var __dirname = dirname(__filename); // src/utils/process-template.ts import path from "path"; import chalk from "chalk"; import ejs from "ejs"; import inquirer from "inquirer"; import { camelCase, cloneDeep, isEqual, mergeWith as mergeWith2 } from "lodash-es"; import { sortPackageJson } from "sort-package-json"; // src/utils/json.ts import { isArray, isObject, mergeWith } from "lodash-es"; function sortObjectKeys(obj) { if (Array.isArray(obj)) { return obj.map(sortObjectKeys); } if (isObject(obj)) { const sorted = {}; Object.keys(obj).sort().forEach((key) => { sorted[key] = sortObjectKeys(obj[key]); }); return sorted; } return obj; } function mergeJson(target, source) { return mergeWith({}, target, source, (objValue, srcValue) => { if (isArray(objValue) && isArray(srcValue)) { if (objValue.every((item) => !isObject(item)) && srcValue.every((item) => !isObject(item))) { return [.../* @__PURE__ */ new Set([...objValue, ...srcValue])]; } return srcValue; } }); } function tryParseJson(content) { try { return JSON.parse(content); } catch { return null; } } // src/utils/process-template.ts function resolveTemplateIdentifiers(template) { const identifiers = /* @__PURE__ */ new Set(); if (template.packageName) { identifiers.add(template.packageName); const scopedName = template.packageName.split("/").pop(); if (scopedName) { identifiers.add(scopedName); } } if (template.name) { identifiers.add(template.name); } identifiers.add("*"); identifiers.add("default"); return Array.from(identifiers); } function collectTemplatePromptAnswers(template, map) { if (!map) { return void 0; } const identifiers = resolveTemplateIdentifiers(template); const combined = {}; for (const key of identifiers) { const value = map[key]; if (!value || typeof value !== "object" || Array.isArray(value)) { continue; } Object.assign(combined, value); } return Object.keys(combined).length > 0 ? combined : void 0; } async function resolvePromptAnswers(prompt, options = {}) { const provided = options.providedAnswers && typeof options.providedAnswers === "object" && !Array.isArray(options.providedAnswers) ? options.providedAnswers : void 0; if (!prompt) { return provided; } const logProcessing = () => { if (options.title) { console.log(chalk.green(`Processing ${options.title} prompts...`)); } else { console.log(chalk.green("Processing prompts...")); } }; if (Array.isArray(prompt)) { const pendingQuestions = prompt.filter((question) => { if (!question || typeof question !== "object") { return true; } const name = question.name; if (name === void 0 || name === null) { return true; } if (!provided) { return true; } return !Object.prototype.hasOwnProperty.call(provided, name); }); if (pendingQuestions.length === 0) { return provided; } logProcessing(); const answers2 = await inquirer.prompt(pendingQuestions); return { ...provided, ...answers2 }; } const isObservable = typeof prompt?.subscribe === "function"; if (!isObservable) { const singleQuestion = prompt; const name = singleQuestion?.name; if (name !== void 0 && name !== null && provided && Object.prototype.hasOwnProperty.call(provided, name)) { return provided; } } logProcessing(); const answers = await inquirer.prompt(prompt); return { ...provided, ...answers }; } async function processTemplateFile(filePath, fileContent, data) { const processedPath = processFileName(filePath, data); return { path: processedPath, content: fileContent }; } async function processTemplate(template, data, existingFiles) { const transformedData = template.transform ? await template.transform(data) : data; const files = template.files || {}; const processedFiles = new Map(existingFiles); for (const [filePath, fileContent] of Object.entries(files)) { const processed = await processTemplateFile( filePath, fileContent, transformedData ); if (processed.path.endsWith(".json")) { const existingFile = processedFiles.get(processed.path); if (existingFile) { const existingJson = tryParseJson(existingFile.content.content); const newJson = tryParseJson(processed.content.content); if (existingJson !== null && newJson !== null) { const mergedJson = mergeJson(existingJson, newJson); const isPackageJSON = processed.path === "package.json"; const sortedJson = isPackageJSON ? sortPackageJson(mergedJson) : sortObjectKeys(mergedJson); processed.content.content = JSON.stringify(sortedJson, null, 2) + "\n"; } else { console.log(chalk.red(`Failed to merge JSON files: ${processed.path}`)); throw new Error(`Failed to merge JSON files: ${processed.path}`); } } } processedFiles.set(processed.path, processed); } return processedFiles; } async function renderProcessedTemplate(processed, data) { return renderTemplate( processed.content, data, processed.path ); } async function processTemplates(templates, data) { const processedFiles = /* @__PURE__ */ new Map(); for (const template of templates) { if (template.validate) { template.validate(data); } const files = await processTemplate(template, data, processedFiles); for (const [path6, file] of files) { processedFiles.set(path6, file); } } return Array.from(processedFiles.values()); } async function processTemplateData(templateData, templates, options = {}) { const result = cloneDeep(templateData); for (const template of templates) { const providedAnswers = collectTemplatePromptAnswers(template, options.promptAnswers); const promptAnswers = await resolvePromptAnswers(template.prompts, { providedAnswers, title: template.name }); if (template.transform) { const transformedData = await template.transform(result, promptAnswers); Object.assign(result, transformedData); } } return result; } var capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1); var pascalCase = (str) => capitalize(camelCase(str)); function createTemplateData(data) { return { ...data, helpers: { raw: (options) => options.fn(), capitalize, camelCase, formatDate: (date) => date.toLocaleDateString(), pascalCase } }; } function shouldRenderTemplate(filePath) { const ext = path.extname(filePath); return ext === ".ejs" || ext === ".html" || ext === ".json" || ext === ".js" || ext === ".ts" || ext === ".vue" || ext === ".md" || !ext; } function processPackageJson(content, data) { try { const pkg = JSON.parse(content); return JSON.stringify(sortPackageJson(mergeWith2( pkg, data.packageJSON )), null, 2) + "\n"; } catch (error) { console.error("Error processing package.json:", error); return content; } } async function renderTemplate(content, data, filePath) { if (!shouldRenderTemplate(filePath) || content.type !== "text" || content.encoding !== "utf8") { return content; } try { const rendered = await ejs.render(content.content, createTemplateData(data), { filename: filePath, async: true }); if (path.basename(filePath) === "package.json") { return { ...content, content: processPackageJson(rendered, data) }; } return { ...content, content: rendered }; } catch (error) { console.error(`Error rendering template ${filePath}:`, error); throw error; } } var normalizePromptQuestionEssential = (item) => { if (!item || typeof item !== "object") return item; const essential = { name: item.name, type: item.type, message: item.message, default: item.default }; if (item.choices) { const rawChoices = item.choices; const normalizedChoices = Array.isArray(rawChoices) ? rawChoices.map((c) => ({ value: c.value ?? c.name ?? c.key ?? c, name: c.name ?? c.value ?? c.key ?? c })).sort((a, b) => String(a.value).localeCompare(String(b.value))) : rawChoices; essential.choices = normalizedChoices; } Object.keys(essential).forEach((key) => { if (essential[key] === void 0) { delete essential[key]; } }); return essential; }; var normalizePromptSchema = (prompt) => { if (!prompt || !Array.isArray(prompt)) return []; const essentials = prompt.map(normalizePromptQuestionEssential); const dedup = /* @__PURE__ */ new Map(); for (const q of essentials) { const n = q && typeof q === "object" ? q.name : void 0; const key = n != null ? String(n) : void 0; if (key) { dedup.set(key, q); } } return Array.from(dedup.entries()).sort((a, b) => a[0].localeCompare(b[0])).map(([, v]) => v); }; var arePromptsEqual = (a, b) => { const na = normalizePromptSchema(a); const nb = normalizePromptSchema(b); return isEqual(na, nb); }; var filterPromptAnswersForTarget = (provided, originalPrompt, targetPrompt) => { if (!provided) return provided; if (!Array.isArray(originalPrompt) || !Array.isArray(targetPrompt)) return provided; const origList = normalizePromptSchema(originalPrompt); const targList = normalizePromptSchema(targetPrompt); const toMap = (list2) => { const m = /* @__PURE__ */ new Map(); for (const q of list2) { if (q && typeof q === "object" && q.name != null) { m.set(String(q.name), q); } } return m; }; const oMap = toMap(origList); const tMap = toMap(targList); const result = {}; for (const [k, v] of Object.entries(provided)) { const o = oMap.get(k); const t = tMap.get(k); if (!o || !t) { continue; } if (isEqual(o, t)) { result[k] = v; } } return result; }; function processFileName(fileName, data) { let processedName = fileName; if (processedName.startsWith("_")) { processedName = `.${processedName.slice(1)}`; } try { return ejs.render(processedName.replace(/_([^_]+)_/g, "<%= $1 %>"), data); } catch (error) { console.error(`Error processing filename ${fileName}:`, error); return processedName; } } // src/utils/prompt-answers.ts var PromptAnswersParseError = class extends Error { constructor(message, cause) { super(message); this.name = "PromptAnswersParseError"; this.cause = cause; } }; var stripWrappingQuotes = (value) => { if (value.startsWith("'") && value.endsWith("'") || value.startsWith('"') && value.endsWith('"')) { return value.slice(1, -1); } return value; }; var parseAnswerValue = (value) => { const trimmed = value.trim(); if (!trimmed) { return ""; } if (trimmed.startsWith("[") || trimmed.startsWith("{")) { try { const normalised = trimmed.replace(/'([^']*)'/g, (_, group) => `"${group}"`); return JSON.parse(normalised); } catch { } } const lower = trimmed.toLowerCase(); if (lower === "true") { return true; } if (lower === "false") { return false; } const numeric = Number(trimmed); if (!Number.isNaN(numeric) && trimmed === numeric.toString()) { return numeric; } const parts = trimmed.split(",").map((item) => stripWrappingQuotes(item.trim())).filter((item) => item.length > 0); if (parts.length > 1) { return parts; } return stripWrappingQuotes(trimmed); }; function parsePromptAnswerPairs(pairs) { if (!pairs || pairs.length === 0) { return void 0; } const result = {}; for (const pair of pairs) { const trimmed = pair.trim(); if (!trimmed) { continue; } const equalsIndex = trimmed.indexOf("="); if (equalsIndex === -1) { throw new PromptAnswersParseError(`Invalid prompt answer format "${pair}". Expected template.prompt=value.`); } const key = trimmed.slice(0, equalsIndex).trim(); const rawValue = trimmed.slice(equalsIndex + 1).trim(); if (!key) { throw new PromptAnswersParseError(`Prompt answer key is missing in "${pair}".`); } const segments = key.split(".").map((segment) => segment.trim()).filter(Boolean); if (segments.length < 2) { throw new PromptAnswersParseError(`Prompt answer must include template and prompt name, got "${pair}".`); } const templateKey = segments[0]; const promptName = segments.slice(1).join("."); const parsedValue = parseAnswerValue(rawValue); if (!result[templateKey]) { result[templateKey] = {}; } result[templateKey][promptName] = parsedValue; } return Object.keys(result).length > 0 ? result : void 0; } // src/utils/template-manager.ts import { exec, execSync as execSync2 } from "node:child_process"; import { readFile, rmdir, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { pathToFileURL } from "node:url"; import { promisify } from "node:util"; import * as path2 from "path"; import Arborist from "@npmcli/arborist"; import chalk2 from "chalk"; import fs from "fs-extra"; import ora from "ora"; import pacote2 from "pacote"; // src/constant.ts var BASE_TEMPLATE_NAME = "dao-style-template"; // src/utils/npm.ts import { execSync } from "child_process"; import pacote from "pacote"; async function getPackageVersions(packageName, options = {}) { const { registry = "https://registry.npmmirror.com" } = options; try { const manifest = await pacote.manifest(`${packageName}@latest`, { registry }); const versions = { latest: manifest.version }; if (options.includeLocal) { try { const localVersion = execSync(`pnpm ls -g ${packageName} --json --depth=0`).toString(); const parsed = JSON.parse(localVersion); const localPkg = parsed?.[0]?.dependencies?.[packageName]; if (localPkg) { versions.latest = localPkg.path ?? manifest.version; } } catch { } } return versions; } catch (error) { throw new Error(`Failed to fetch version for ${packageName}: ${error}`); } } async function searchOrganizationPackages(organization, options = {}) { const { registry = "https://registry.npmmirror.com" } = options; try { const command = `pnpm search ${organization} --json ${registry ? `--registry ${registry}` : ""}`; const resultStr = execSync(command).toString(); const result = JSON.parse(resultStr); const target = result.filter((item) => item.name.startsWith(organization) && !item.name.endsWith("shared")).map((item) => ({ name: item.name, version: item.version, description: item.description, author: item.author, keywords: item.keywords, maintainers: item.maintainers, "dist-tags": { latest: item["dist-tags"].latest }, time: { created: item.created, modified: item.modified } })); if (options.local) { const newTarget = [...target]; for (let i = 0; i < newTarget.length; i++) { const pkg = newTarget[i]; try { const localVersion = execSync(`pnpm ls -g ${pkg.name} --json --depth=0`).toString(); const parsed = JSON.parse(localVersion); const localPkg = parsed?.[0]?.dependencies?.[pkg.name]; if (localPkg) { pkg["dist-tags"] = { ...pkg["dist-tags"], local: localPkg.version }; pkg.version = localPkg.path ?? pkg.version; } } catch { } } return newTarget; } return target; } catch (error) { console.warn(`Failed to search organization packages: ${error}`); } return []; } // src/utils/template-manager.ts var execAsync = promisify(exec); var TemplateManager = class { /** * 获取可用的模板列表 */ static async getAvailableTemplates(local = false) { try { const packages = await searchOrganizationPackages(this.TEMPLATE_ORG, { local }); return packages; } catch (e) { console.warn(chalk2.yellow("Failed to fetch template list"), e); throw e; } } /** * 下载并缓存模板 */ static async downloadTemplate(templateName, version2) { const packageName = templateName; let targetVersion = version2 || (await getPackageVersions(packageName)).latest; if (targetVersion.startsWith("/")) { targetVersion = `file:${targetVersion}`; } const isLocalPath = targetVersion.startsWith("file:"); const templateDir = path2.join(this.TEMPLATE_CACHE_DIR, packageName, isLocalPath ? "local" : targetVersion); const isTemplateCompleteCache = await fs.pathExists(path2.join(templateDir, this.FULL_TEMPLATE_FLAG)); if (isLocalPath || !isTemplateCompleteCache) { await fs.remove(templateDir); } await fs.ensureDir(templateDir); const files = await fs.readdir(templateDir); if (files.length > 0) { return templateDir; } let resolvedVersion = `${packageName}@${targetVersion}`; const spinner = ora(`Downloading template ${resolvedVersion}...`).start(); try { await pacote2.extract(resolvedVersion, templateDir, { registry: "https://registry.npmmirror.com", Arborist }); await fs.writeFile(path2.join(templateDir, this.FULL_TEMPLATE_FLAG), ""); spinner.succeed(chalk2.green(`Template ${resolvedVersion} downloaded`)); return templateDir; } catch (error) { spinner.fail(chalk2.red(`Failed to download template ${resolvedVersion}`)); await fs.remove(templateDir); throw error; } } /** * 加载模板 */ static async loadTemplate(templateName, version2) { let templateDir; templateDir = await this.downloadTemplate(templateName, version2); const packageJsonPath = path2.join(templateDir, "package.json"); let packageJson2; if (await fs.pathExists(packageJsonPath)) { packageJson2 = JSON.parse(await readFile(packageJsonPath, "utf-8")); } else { packageJson2 = { version: version2 || "1.0.0" }; } const templateIndexPath = path2.join(templateDir, "dist", "index.js"); const templateModule = await import(pathToFileURL(templateIndexPath).href); const templateDefinition = templateModule.default?.default || templateModule.default || templateModule; if (!templateDefinition) { throw new Error(`Failed to load template definition from ${templateIndexPath}`); } templateDefinition.files = templateDefinition.files || {}; let targetVersion = packageJson2.version || "1.0.0"; if (version2?.startsWith("/")) { targetVersion = `file:${version2}`; } return { ...templateDefinition, packageName: templateName, version: targetVersion }; } /** * 获取已安装的模板版本 */ static async getInstalledTemplateVersions(projectPath) { try { const list2 = execSync2(`cd ${projectPath} && pnpm list --json`); const listJson = JSON.parse(list2.toString()); const templateVersions = {}; for (const [name, version2] of Object.entries(listJson?.[0]?.devDependencies || {})) { if (name.startsWith(this.TEMPLATE_ORG)) { templateVersions[name] = version2?.version; } } return templateVersions; } catch { return {}; } } /** * 检查模板更新 */ static async checkTemplateUpdates(projectPath, local = false, ignoreOrigin = false) { const installed = await this.getInstalledTemplateVersions(projectPath); const updates = {}; for (const [templateName, currentVersion] of Object.entries(installed)) { try { const packageName = templateName; const versions = await getPackageVersions(packageName, { includeLocal: local }); const effectiveCurrentVersion = ignoreOrigin ? "0.0.2" : currentVersion.replace("^", ""); if (ignoreOrigin || effectiveCurrentVersion !== versions.latest) { updates[templateName] = { current: effectiveCurrentVersion, latest: versions.latest }; } } catch { console.warn(chalk2.yellow(`Failed to check updates for ${templateName}`)); } } return updates; } /** * 执行三方合并 */ static async performThreeWayMerge(currentContent, baseContent, targetContent, filePath) { const tempDir = path2.join(process.cwd(), ".tmp-merge", filePath); await fs.ensureDir(tempDir); const baseFile = path2.join(tempDir, "base"); const currentFile = path2.join(tempDir, "current"); const targetFile = path2.join(tempDir, "target"); try { await writeFile(baseFile, baseContent); await writeFile(currentFile, currentContent); await writeFile(targetFile, targetContent); const { stderr } = await execAsync( `git merge-file ${currentFile} ${baseFile} ${targetFile}`, { cwd: tempDir } ); const mergedContent = await readFile(currentFile, "utf-8"); const hasConflicts = mergedContent.includes("<<<<<<< ") || mergedContent.includes(">>>>>>> ") || stderr.includes("conflict"); return { merged: mergedContent, hasConflicts }; } catch { const mergedContent = await readFile(currentFile, "utf-8"); const hasConflicts = true; return { merged: mergedContent, hasConflicts }; } finally { rmdir(tempDir, { recursive: true }); } } }; TemplateManager.TEMPLATE_ORG = "@" + BASE_TEMPLATE_NAME; TemplateManager.TEMPLATE_CACHE_DIR = path2.join(tmpdir(), ".dao-templates"); TemplateManager.FULL_TEMPLATE_FLAG = "__isFullTemplate"; // src/commands/create.ts var TemplateSelectionError = class extends Error { constructor(message) { super(message); this.name = "TemplateSelectionError"; } }; var groupName = "dao-style"; var __filename2 = fileURLToPath2(import.meta.url); var __dirname2 = dirname2(__filename2); async function create(name, { initialBranch, local, allTemplates, answer }) { const spinner = ora2("Creating project..."); try { const predefinedPromptAnswers = parsePromptAnswerPairs(answer); const packageName = `${name}-ui`; const templateData = { name, packageName, packageJSON: { devDependencies: { // 添加 cli 依赖,用于 post-install 任务 [`@${groupName}/cli`]: false ? getLocalCliPath() : version ?? "latest", "is-ci": "~4.1.0" }, scripts: { postinstall: "is-ci || dao post-install" } } }; spinner.text = "Listing templates..."; spinner.start(); const templates = await TemplateManager.getAvailableTemplates(local); spinner.stop(); const selectAll = Boolean(allTemplates); let selectedTemplateNames; if (selectAll) { selectedTemplateNames = templates.map((t) => ({ name: t.name, version: t.version })); } else { selectedTemplateNames = await checkbox( { message: "Select a template", choices: templates.map((t) => ({ name: `${t.name}@${t.version}`, value: { name: t.name, version: t.version }, checked: true })) } ); } spinner.text = "Loading templates..."; spinner.start(); const loaderHandler = selectedTemplateNames.map((t) => TemplateManager.loadTemplate(t.name, t.version)); const loadedTemplates = await Promise.all(loaderHandler); loadedTemplates.forEach((t) => { set(templateData, ["packageJSON", "devDependencies", t.packageName], `${t.version}`); }); spinner.stop(); spinner.succeed(chalk3.green("Templates loaded")); const mergedTemplateData = await processTemplateData(templateData, loadedTemplates, { promptAnswers: predefinedPromptAnswers }); const processedFiles = await processTemplates(loadedTemplates, mergedTemplateData); spinner.text = "Creating project directory"; spinner.start(); const projectPath = path3.resolve(process.cwd(), packageName); if (await fs2.pathExists(projectPath)) { spinner.fail(chalk3.red(`Directory ${packageName} already exists`)); process.exit(1); } await fs2.ensureDir(projectPath); spinner.succeed(chalk3.green("Project directory created")); for (const file of processedFiles) { const targetPath = path3.join(projectPath, file.path); await fs2.ensureDir(path3.dirname(targetPath)); const renderedContent = await renderProcessedTemplate(file, mergedTemplateData); await cliWriteFile(targetPath, renderedContent); } execSync3(`cd ${packageName} && git init -b ${initialBranch} && git add .`); execSync3(`cd ${packageName} && pnpm install`, { stdio: "inherit" }); execSync3(`cd ${packageName} && pnpm build`); execSync3(`cd ${packageName} && pnpm lint:fix`); execSync3(`cd ${packageName} && git add . && git commit -m "Initial commit" --no-verify`); spinner.succeed(chalk3.green(`Successfully created project ${packageName}`)); console.log("\nNext steps:"); console.log(chalk3.cyan(` cd ${packageName}`)); console.log(chalk3.cyan(" pnpm serve")); } catch (error) { if (error instanceof PromptAnswersParseError || error instanceof TemplateSelectionError) { spinner.fail(chalk3.red(error.message)); } else { spinner.fail(chalk3.red("Failed to create project")); } console.error(error); process.exit(1); } } // src/commands/upgrade.ts import { exec as exec2 } from "node:child_process"; import { readdir, readFile as readFile2, rm } from "node:fs/promises"; import { resolve as resolve2 } from "node:path"; import { promisify as promisify2 } from "node:util"; import * as path4 from "path"; import chalk4 from "chalk"; import { globby } from "globby"; import inquirer2 from "inquirer"; import { cloneDeep as cloneDeep2, isEqual as isEqual2, set as set2 } from "lodash-es"; var execAsync2 = promisify2(exec2); var binaryExtensions = /* @__PURE__ */ new Set([ ".ico", ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".zip", ".rar", ".tar", ".gz", ".7z", ".mp3", ".mp4", ".avi", ".mov", ".wav", ".ttf", ".woff", ".woff2", ".eot", ".exe", ".dmg", ".deb", ".rpm" ]); function isBinaryFile(filePath) { const ext = path4.extname(filePath).toLowerCase(); return binaryExtensions.has(ext); } async function determineFileAction(local, original, target, filePath, projectPath, ignoreOrigin = false) { const localExists = !!local; const originalExists = !!original; const targetExists = !!target; const equal = { local_original: localExists && originalExists && isEqual2(local, original), local_target: localExists && targetExists && isEqual2(local, target), original_target: originalExists && targetExists && isEqual2(original, target) }; const state = (localExists ? 1 : 0) | (originalExists ? 2 : 0) | (targetExists ? 4 : 0); switch (state) { // 000 — NONE case 0: return { action: "skip" /* SKIP */, reason: "nothing to do" }; // 001 — LOCAL_ONLY case 1: return { action: "skip" /* SKIP */, reason: "local project file" }; // 010 — ORIGINAL_ONLY case 2: return { action: "skip" /* SKIP */, reason: "obsolete template file" }; // 100 — TARGET_ONLY(模板新增文件) case 4: return { action: "add" /* ADD */, content: target }; // 011 — LOCAL + ORIGINAL(模板删除了这个文件) case 3: { if (equal.local_original) { await rm(path4.join(projectPath, filePath)); return { action: "delete" /* DELETE */ }; } return { action: "skip" /* SKIP */, warningType: "template-deleted-local-modified" /* TEMPLATE_DELETED_LOCAL_MODIFIED */ }; } // 110 — ORIGINAL + TARGET(本地删除了文件) case 6: { if (equal.original_target) { return { action: "skip" /* SKIP */, reason: "template unchanged and locally deleted", warningType: "local-deleted-template-unchanged" /* LOCAL_DELETED_TEMPLATE_UNCHANGED */ }; } return { action: "add" /* ADD */, content: target, reason: "locally deleted, but template updated", warningType: "local-deleted-template-updated" /* LOCAL_DELETED_TEMPLATE_UPDATED */ }; } // 101 — LOCAL + TARGET,没有 original(无法进行三方合并) case 5: { if (equal.local_target) { return { action: "skip" /* SKIP */, reason: "already up to date" }; } return { action: "skip" /* SKIP */, reason: "local project file", warningType: "local-target-conflict" /* LOCAL_TARGET_CONFLICT */ }; } // 111 — 三方都存在 → standard 3-way merge case 7: { if (equal.local_target) { return { action: "skip" /* SKIP */, reason: "already up to date" }; } if (equal.local_original) { return { action: "update" /* UPDATE */, content: target }; } const merged = ignoreOrigin ? await TemplateManager.performThreeWayMerge( local.content, "", target.content, filePath ) : await TemplateManager.performThreeWayMerge( local.content, original.content, target.content, filePath ); if (merged.merged === local.content) { return { action: "skip" /* SKIP */, reason: "merge resulted in no changes" }; } return { action: "merge" /* MERGE */, content: { ...target, content: merged.merged }, hasConflicts: merged.hasConflicts }; } default: return { action: "skip" /* SKIP */, reason: "unknown state" }; } } async function checkGitStatus() { try { const { stdout } = await execAsync2("git status --porcelain"); return stdout.length === 0; } catch (error) { console.error(chalk4.red("Check git status failed:"), error); throw new Error("Not a git repository"); } } async function promptForContinue() { const { continue: shouldContinue } = await inquirer2.prompt([ { type: "confirm", name: "continue", message: "Working directory is not clean. Continue anyway?", default: false } ]); return shouldContinue; } async function validateGitStatusAndGetConfirmation(options) { const isClean = await checkGitStatus(); if (!isClean && !options.force) { console.log(chalk4.yellow("Warning: You have uncommitted changes.")); const shouldContinue = await promptForContinue(); if (!shouldContinue) { console.log(chalk4.blue("Upgrade cancelled.")); return false; } } return true; } async function selectTemplates(availableTemplates) { const { templates } = await inquirer2.prompt([ { type: "checkbox", name: "templates", message: "Select templates to upgrade:", choices: availableTemplates.map((template) => ({ name: `${template.name} Template (${template.current || "N/A"} \u2192 ${template.version})`, value: template.name, checked: true })) } ]); return templates; } async function checkUpdatesAndSelectTemplates(projectPath, options) { const availableUpdates = await TemplateManager.checkTemplateUpdates(projectPath, options.local, options.ignoreOrigin); if (Object.keys(availableUpdates).length === 0) { console.log(chalk4.green("All templates are up to date!")); return null; } const selectedTemplateNames = await selectTemplates(Object.entries(availableUpdates).map(([templateName, update]) => ({ name: templateName, version: update.latest, current: update.current }))); if (selectedTemplateNames.length === 0) { console.log(chalk4.blue("No templates selected. Upgrade cancelled.")); return null; } return { selectedTemplateNames, availableUpdates }; } function createTemplateVersionMappings(availableUpdates, selectedTemplateNames) { const originalTemplateVersion = Object.keys(availableUpdates).reduce((acc, name) => { acc[name] = availableUpdates[name].current; return acc; }, {}); const targetTemplateVersion = Object.keys(availableUpdates).reduce((acc, name) => { if (!selectedTemplateNames.includes(name)) { acc[name] = availableUpdates[name].current; return acc; } acc[name] = availableUpdates[name].latest; return acc; }, {}); return { originalTemplateVersion, targetTemplateVersion }; } async function processTemplatePrompt(prompt, title, providedAnswers) { if (!prompt) { return; } const answers = await resolvePromptAnswers(prompt, { providedAnswers, title }); return answers; } async function getProjectPromptAnswers(template, projectPath, templateData) { if (!template.getProjectPromptAnswers) { return void 0; } try { const result = await template.getProjectPromptAnswers(projectPath, templateData); if (!result || typeof result !== "object" || Array.isArray(result)) { return void 0; } return result; } catch (error) { console.warn(chalk4.yellow(`Failed to resolve prompt defaults for template ${template.packageName ?? template.name}`), error); return void 0; } } async function transformTemplateData(template, templateData, promptAnswer) { if (!template.transform) { return templateData; } const result = cloneDeep2(templateData); const transformedData = await template.transform(result, promptAnswer); const assignedResult = Object.assign(result, transformedData); return assignedResult; } async function processTemplatePrompts(originalTemplates, targetTemplates, templateData, projectPath) { const originByName = originalTemplates.reduce((acc, t) => acc.set(t.name, t), /* @__PURE__ */ new Map()); const targetByName = targetTemplates.reduce((acc, t) => acc.set(t.name, t), /* @__PURE__ */ new Map()); const pairs = []; for (const [name, o] of originByName) { const t = targetByName.get(name); if (t) { pairs.push({ origin: o, target: t }); targetByName.delete(name); } else { pairs.push({ origin: o }); } } for (const [, t] of targetByName) { pairs.push({ target: t }); } const promptDefaults = /* @__PURE__ */ new Map(); for (const template of [...originalTemplates, ...targetTemplates]) { const key = template.packageName ?? template.name; if (!key || promptDefaults.has(key)) continue; const defaults = await getProjectPromptAnswers(template, projectPath, templateData); if (defaults) promptDefaults.set(key, defaults); } let originalTemplateData = { name: templateData.name, packageJSON: {} }; let targetTemplateData = { name: templateData.name, packageJSON: {} }; for (const pair of pairs) { const origin = pair.origin; const target = pair.target; const key = target?.packageName ?? target?.name ?? origin?.packageName ?? origin?.name ?? ""; const defaults = key ? promptDefaults.get(key) : void 0; if (origin && target && arePromptsEqual(origin.prompts, target.prompts)) { const answer = await processTemplatePrompt( target.prompts, target.name, defaults // ⚠️ defaults 是 partial answers,会被自动补全 ); originalTemplateData = await transformTemplateData(origin, originalTemplateData, answer); targetTemplateData = await transformTemplateData(target, targetTemplateData, answer); continue; } if (origin) { const originAnswer = await processTemplatePrompt( origin.prompts, origin.name, defaults // ⚠️ defaults 是 partial,可被补全 ); originalTemplateData = await transformTemplateData(origin, originalTemplateData, originAnswer); } if (target) { const provided = origin ? filterPromptAnswersForTarget(defaults, origin.prompts, target.prompts) : defaults; const targetAnswer = await processTemplatePrompt(target.prompts, target.name, provided); targetTemplateData = await transformTemplateData(target, targetTemplateData, targetAnswer); } } return { originalTemplateData, targetTemplateData }; } async function processTemplateFiles(selectedOriginalTemplates, selectedTargetTemplates, originalTemplateData, targetTemplateData) { const mergedOriginalFiles = await processTemplates(selectedOriginalTemplates, originalTemplateData); const originalRendererContent = /* @__PURE__ */ new Map(); for (const file of mergedOriginalFiles) { const renderedContent = await renderProcessedTemplate(file, originalTemplateData); originalRendererContent.set(file.path, renderedContent); } const mergedTargetFiles = await processTemplates(selectedTargetTemplates, targetTemplateData); const targetRendererContent = /* @__PURE__ */ new Map(); for (const file of mergedTargetFiles) { const renderedContent = await renderProcessedTemplate(file, targetTemplateData); targetRendererContent.set(file.path, renderedContent); } return { originalRendererContent, targetRendererContent, targetTemplateData }; } async function prepareFileContentsForMerging(originalTemplates, targetTemplates, originalTemplateData, targetTemplateData, projectPath) { const { originalRendererContent, targetRendererContent } = await processTemplateFiles( originalTemplates, targetTemplates, originalTemplateData, targetTemplateData ); const localFilesMap = await readLocalFiles(projectPath); return { originalRendererContent, targetRendererContent, localFilesMap }; } async function readLocalFiles(projectPath) { const gitIgnore = await readFile2(path4.join(projectPath, ".gitignore"), "utf-8"); const ignoredPatterns = gitIgnore.split("\n").filter((item) => item.trim() && !item.trim().startsWith("#") && item !== ".vscode"); const result = await globby(`**/*`, { ignore: [".git", ...ignoredPatterns], gitignore: true, dot: true }); const vscodeConfig = await readdir(resolve2(projectPath, ".vscode")); if (vscodeConfig.length > 0) { vscodeConfig.forEach((file) => { if (!result.includes(`.vscode/${file}`)) { result.push(`.vscode/${file}`); } }); } const readFileHandlers = []; const localFilesMap = result.reduce((acc, filePath) => { const fullPath = path4.join(projectPath, filePath); const isBinary = isBinaryFile(fullPath); const obj = { content: "", encoding: "utf8", type: "binary" }; if (isBinary) { obj.encoding = "base64"; obj.type = "binary"; readFileHandlers.push( readFile2(fullPath, "base64").then((content) => { obj.content = content; }) ); } else { readFileHandlers.push( readFile2(fullPath, "utf8").then((content) => { obj.content = content; }) ); } acc.set(filePath, obj); return acc; }, /* @__PURE__ */ new Map()); await Promise.all(readFileHandlers); return localFilesMap; } async function processFileMerging(originalRendererContent, targetRendererContent, localFilesMap, projectPath, ignoreOrigin = false) { const allFileNames = /* @__PURE__ */ new Set([...originalRendererContent.keys(), ...targetRendererContent.keys(), ...localFilesMap.keys()]); const resultFileMap = /* @__PURE__ */ new Map(); const warningTypes = { ["local-target-conflict" /* LOCAL_TARGET_CONFLICT */]: [], //keep local ["template-deleted-local-modified" /* TEMPLATE_DELETED_LOCAL_MODIFIED */]: [], //keep local ["local-deleted-template-unchanged" /* LOCAL_DELETED_TEMPLATE_UNCHANGED */]: [], // keep deleted ["local-deleted-template-updated" /* LOCAL_DELETED_TEMPLATE_UPDATED */]: [] // add back }; for (const filePath of allFileNames) { const action = await determineFileAction( localFilesMap.get(filePath), originalRendererContent.get(filePath), targetRendererContent.get(filePath), filePath, projectPath, ignoreOrigin ); if (action.warningType) { warningTypes[action.warningType].push(filePath); } switch (action.action) { case "update" /* UPDATE */: resultFileMap.set(filePath, { content: action.content }); console.log(chalk4.green(`Updated: ${filePath}`)); break; case "merge" /* MERGE */: resultFileMap.set(filePath, { content: action.content, hasConflicts: action.hasConflicts }); if (action.hasConflicts) { console.log(chalk4.yellow(`Merged with conflicts: ${filePath}`)); } else { console.log(chalk4.green(`Merged: ${filePath}`)); } break; case "add" /* ADD */: resultFileMap.set(filePath, { content: action.content }); console.log(chalk4.green(`Added: ${filePath}`)); break; case "delete" /* DELETE */: break; case "skip" /* SKIP */: break; } } const conflictFileNames = []; for (const [filePath, fileData] of resultFileMap) { if (fileData.hasConflicts) { conflictFileNames.push(filePath); } await cliWriteFile(path4.join(projectPath, filePath), fileData.content); } return { resultFileMap, conflictFileNames, warningTypes }; } async function handleConflicts(conflictFileNames) { if (conflictFileNames.length === 0) { return; } try { console.log(chalk4.blue("\nStaging changes...")); await execAsync2("git add ."); console.log(chalk4.blue("Unstaging conflict files...")); await execAsync2(`git reset HEAD -- ${conflictFileNames.map((f) => `"${f}"`).join(" ")}`); console.log(chalk4.green("Staged all files except conflicts.")); } catch (error) { console.warn(chalk4.yellow("Failed to stage files:"), error); } let isCodeCmdExist = false; try { await execAsync2("code -v"); isCodeCmdExist = true; } catch { isCodeCmdExist = false; } console.log(chalk4.red("\nUpgrade completed with conflicts in the below files. Please resolve them and commit the changes:")); if (isCodeCmdExist) { console.log(chalk4.yellow("\nOpening conflicts in VS Code...")); await execAsync2(`code -n ${conflictFileNames.join(" ")}`); } conflictFileNames.forEach((file) => { console.log(chalk4.red(`- ${file}`)); }); } function printWarningType(warningTypes) { const anyWarnings = Object.values(warningTypes).some((list2) => list2.length > 0); if (!anyWarnings) return; const displayMap = { ["template-deleted-local-modified" /* TEMPLATE_DELETED_LOCAL_MODIFIED */]: { symbol: "~", color: chalk4.yellow, legendLabel: "Modified/Review", header: "[Warning] Template deleted these files, but local changes exist. Kept local \u2014 review before keeping or deleting." }, ["local-target-conflict" /* LOCAL_TARGET_CONFLICT */]: { symbol: "\u26A0", color: chalk4.yellow, legendLabel: "Conflict", header: "[Warning] Local and template versions differ (no merge base). Kept local \u2014 review manually." }, ["local-deleted-template-unchanged" /* LOCAL_DELETED_TEMPLATE_UNCHANGED */]: { symbol: "-", color: chalk4.red, legendLabel: "Deleted", header: "[Warning] Locally deleted files are unchanged in template. Kept deleted." }, ["local-deleted-template-updated" /* LOCAL_DELETED_TEMPLATE_UPDATED */]: { symbol: "+", color: chalk4.green, legendLabel: "Added", header: "[Warning] Locally deleted files have template updates. Added them back \u2014 review." } }; const legendOrder = ["+", "-", "~", "\u26A0"]; const legendParts = []; for (const sym of legendOrder) { const entry = Object.values(displayMap).find((v) => v.symbol === sym); if (entry) legendParts.push(entry.color(`${entry.symbol} ${entry.legendLabel}`)); } console.log("\n"); console.log(chalk4.bold("Legend:") + " " + legendParts.join(" ")); for (const [warningType, filePaths] of Object.entries(warningTypes)) { if (filePaths.length === 0) continue; const entry = displayMap[warningType]; console.log(chalk4.yellow(entry.header)); filePaths.forEach((file) => console.log(entry.color(`${entry.symbol} ${file}`))); console.log("\n"); } } async function upgrade(options) { try { const shouldContinue = await validateGitStatusAndGetConfirmation(options); if (!shouldContinue) { return; } const projectPath = process.cwd(); const updateResult = await checkUpdatesAndSelectTemplates(projectPath, options); if (!updateResult) { return; } const { selectedTemplateNames, availableUpdates } = updateResult; const { originalTemplateVersion, targetTemplateVersion } = createTemplateVersionMappings( availableUpdates, selectedTemplateNames ); selectedTemplateNames.forEach((name) => { const update = availableUpdates[name]; console.log(`- ${name}: ${chalk4.red(update.current)} \u2192 ${chalk4.green(update.latest)}`); }); const originalTemplates = await Promise.all( Object.entries(originalTemplateVersion).filter(([name]) => selectedTemplateNames.includes(name)).map( ([name, version2]) => TemplateManager.loadTemplate(name, version2) ) ); const targetTemplates = await Promise.all( Object.entries(targetTemplateVersion).filter(([name]) => selectedTemplateNames.includes(name)).map( ([name, version2]) => TemplateManager.loadTemplate(name, version2) ) ); originalTemplates.sort((a, b) => (a.order || 0) - (b.order || 0)); targetTemplates.sort((a, b) => (a.order || 0) - (b.order || 0)); const packageJsonPath = path4.join(projectPath, "package.json"); const packageJson2 = JSON.parse(await readFile2(packageJsonPath, "utf-8")); const templateData = { name: packageJson2.name.replace(/-ui$/, ""), packageJSON: cloneDeep2(packageJson2) }; const { originalTemplateData, targetTemplateData } = await processTemplatePrompts( originalTemplates, targetTemplates, templateData, projectPath ); const originalCliVersion = packageJson2.devDependencies?.["@dao-style/cli"]; const upgradeDevDependencies = { "@dao-style/cli": false ? getLocalCliPath2() : version || originalCliVersion || "latest" }; for (const templateName of selectedTemplateNames) { const update = availableUpdates[templateName]; upgradeDevDependencies[templateName] = update.latest; } set2(targetTemplateData, ["packageJSON", "devDependencies"], { ...targetTemplateData.packageJSON?.devDependencies, ...upgradeDevDependencies }); const originalDevDependencies = {}; if (originalCliVersion) { originalDevDependencies["@dao-style/cli"] = originalCliVersion; } for (let [key, version2] of Object.entries(originalTemplateVersion)) { originalDevDependencies[key] = version2; } set2(originalTemplateData, ["packageJSON", "devDependencies"], { ...originalTemplateData.packageJSON?.devDependencies, ...originalDevDependencies }); const { originalRendererContent, targetRendererContent, localFilesMap } = await prepareFileContentsForMerging( originalTemplates, targetTemplates, originalTemplateData, targetTemplateData, projectPath ); const { conflictFileNames, warningTypes } = await processFileMerging( originalRendererContent, targetRendererContent, localFilesMap, projectPath, options.ignoreOrigin ?? false ); printWarningType(warningTypes); await handleConflicts(conflictFileNames); if (conflictFileNames.length > 0) { console.log(chalk4.yellow("\nPlease resolve the conflicts above and run " + chalk4.cyan("pnpm install") + " to finalize the upgrade.")); } else { await execAsync2("pnpm install"); console.log(chalk4.green("\nAll done! Dependencies are updated and installed.")); console.log(chalk4.green("\nUpgrade completed!")); } } catch (error) { console.error(chalk4.red("Upgrade failed:"), error); process.exit(1); } } // src/cli.ts import { readFileSync } from "node:fs"; import { dirname as dirname5, resolve as resolve3 } from "node:path"; import { fileURLToPath as fileURLToPath3 } from "node:url"; import { Command } from "commander"; // src/commands/list.ts import { readdir as readdir2 } from "node:fs/promises"; import chalk5 from "chalk"; import ora3 from "ora"; var templateOrg = BASE_TEMPLATE_NAME; async function list({ local }) { const spinner = ora3("Fetching template information...").start(); try { const projectPath = process.cwd(); const availableTemplates = await TemplateManager.getAvailableTemplates(local); const installedVersions = await TemplateManager.getInstalledTemplateVersions(projectPath); spinner.succeed(chalk5.green("Template information fetched")); console.log(chalk5.green(`\u{1F4E6} Available Templates from @${templateOrg}:`)); console.log(""); if (Object.keys(availableTemplates).length === 0) { console.log(chalk5.yel