@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
JavaScript
// 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