UNPKG

scaffolder-toolkit

Version:

🚀 A universal command-line tool for developers to automate project scaffolding and streamline their workflows.

1,592 lines (1,499 loc) • 63.4 kB
#!/usr/bin/env node import { Command } from 'commander'; import { existsSync as existsSync$2, promises } from 'fs'; import path, { dirname } from 'path'; import { promisify } from 'node:util'; import childProcess, { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'url'; import os, { homedir } from 'os'; import ora from 'ora'; import chalk from 'chalk'; import { select } from '@inquirer/prompts'; function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } var cjs; var hasRequiredCjs; function requireCjs () { if (hasRequiredCjs) return cjs; hasRequiredCjs = 1; var isMergeableObject = function isMergeableObject(value) { return isNonNullObject(value) && !isSpecial(value) }; function isNonNullObject(value) { return !!value && typeof value === 'object' } function isSpecial(value) { var stringValue = Object.prototype.toString.call(value); return stringValue === '[object RegExp]' || stringValue === '[object Date]' || isReactElement(value) } // see https://github.com/facebook/react/blob/b5ac963fb791d1298e7f396236383bc955f916c1/src/isomorphic/classic/element/ReactElement.js#L21-L25 var canUseSymbol = typeof Symbol === 'function' && Symbol.for; var REACT_ELEMENT_TYPE = canUseSymbol ? Symbol.for('react.element') : 0xeac7; function isReactElement(value) { return value.$$typeof === REACT_ELEMENT_TYPE } function emptyTarget(val) { return Array.isArray(val) ? [] : {} } function cloneUnlessOtherwiseSpecified(value, options) { return (options.clone !== false && options.isMergeableObject(value)) ? deepmerge(emptyTarget(value), value, options) : value } function defaultArrayMerge(target, source, options) { return target.concat(source).map(function(element) { return cloneUnlessOtherwiseSpecified(element, options) }) } function getMergeFunction(key, options) { if (!options.customMerge) { return deepmerge } var customMerge = options.customMerge(key); return typeof customMerge === 'function' ? customMerge : deepmerge } function getEnumerableOwnPropertySymbols(target) { return Object.getOwnPropertySymbols ? Object.getOwnPropertySymbols(target).filter(function(symbol) { return Object.propertyIsEnumerable.call(target, symbol) }) : [] } function getKeys(target) { return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target)) } function propertyIsOnObject(object, property) { try { return property in object } catch(_) { return false } } // Protects from prototype poisoning and unexpected merging up the prototype chain. function propertyIsUnsafe(target, key) { return propertyIsOnObject(target, key) // Properties are safe to merge if they don't exist in the target yet, && !(Object.hasOwnProperty.call(target, key) // unsafe if they exist up the prototype chain, && Object.propertyIsEnumerable.call(target, key)) // and also unsafe if they're nonenumerable. } function mergeObject(target, source, options) { var destination = {}; if (options.isMergeableObject(target)) { getKeys(target).forEach(function(key) { destination[key] = cloneUnlessOtherwiseSpecified(target[key], options); }); } getKeys(source).forEach(function(key) { if (propertyIsUnsafe(target, key)) { return } if (propertyIsOnObject(target, key) && options.isMergeableObject(source[key])) { destination[key] = getMergeFunction(key, options)(target[key], source[key], options); } else { destination[key] = cloneUnlessOtherwiseSpecified(source[key], options); } }); return destination } function deepmerge(target, source, options) { options = options || {}; options.arrayMerge = options.arrayMerge || defaultArrayMerge; options.isMergeableObject = options.isMergeableObject || isMergeableObject; // cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge() // implementations can use it. The caller may not replace it. options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified; var sourceIsArray = Array.isArray(source); var targetIsArray = Array.isArray(target); var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray; if (!sourceAndTargetTypesMatch) { return cloneUnlessOtherwiseSpecified(source, options) } else if (sourceIsArray) { return options.arrayMerge(target, source, options) } else { return mergeObject(target, source, options) } } deepmerge.all = function deepmergeAll(array, options) { if (!Array.isArray(array)) { throw new Error('first argument should be an array') } return array.reduce(function(prev, next) { return deepmerge(prev, next, options) }, {}) }; var deepmerge_1 = deepmerge; cjs = deepmerge_1; return cjs; } var cjsExports = requireCjs(); var deepmerge = /*@__PURE__*/getDefaultExportFromCjs(cjsExports); async function readJson(filePath) { const fileContent = await promises.readFile(filePath, { encoding: "utf8", }); return JSON.parse(fileContent); } async function writeJson(filePath, data) { const jsonString = JSON.stringify(data, null, 2); await promises.writeFile(filePath, jsonString); } const existsSync$1 = existsSync$2; async function copy(source, destination, options = {}) { const filterFunction = options.filter || (() => true); const stats = await promises.stat(source); if (stats.isDirectory()) { if (!filterFunction(source)) { return; } await promises.mkdir(destination, { recursive: true }); const entries = await promises.readdir(source); for (const entry of entries) { const srcPath = path.join(source, entry); const destPath = path.join(destination, entry); await copy(srcPath, destPath, options); } } else if (stats.isFile()) { if (!filterFunction(source)) { return; } await promises.copyFile(source, destination); } } async function pathExists(filePath) { try { await promises.access(filePath); return true; // oxlint-disable-next-line no-unused-vars } catch (error) { return false; } } async function stat(filePath) { return await promises.stat(filePath); } async function ensureDir(dirPath) { await promises.mkdir(dirPath, { recursive: true }); } async function remove(path) { await promises.rm(path, { recursive: true, force: true }); } const writeFile = writeJson; var fs = { readJson, writeJson, existsSync: existsSync$1, copy, pathExists, stat, ensureDir, remove, writeFile, }; const JavascriptPackageManagers = { Bun: "bun", Npm: "npm", Yarn: "yarn", Deno: "deno", Pnpm: "pnpm", }; const PackageManagers = { ...JavascriptPackageManagers, }; const VALID_PACKAGE_MANAGERS = Object.seal(Object.values(PackageManagers)); const VALID_CACHE_STRATEGIES = [ "always-refresh", "never-refresh", "daily", ]; const TextLanguages = { English: "en", French: "fr", }; const SUPPORTED_LANGUAGES = Object.seal(Object.values(TextLanguages)); const defaultCliConfig = { templates: { javascript: { templates: { vue: { description: "An official Vue.js project.", location: "{pm} create vue@latest", cacheStrategy: "always-refresh", }, nuxt: { description: "An official Nuxt.js project.", location: "{pm} create nuxt@latest", alias: "nx", }, nest: { description: "An official Nest.js project.", location: "{pm} install -g @nestjs/cli && nest new", }, nextjs: { description: "An official Next.js project.", location: "{pm} create next-app@latest", alias: "next", }, express: { description: "A simple Express.js boilerplate from its generator.", location: "https://github.com/expressjs/express-generator.git", alias: "ex", }, fastify: { description: "A highly performant Fastify web framework boilerplate.", location: "https://github.com/fastify/fastify-cli.git", alias: "fy", }, koa: { description: "A Koa.js web framework boilerplate.", location: "https://github.com/koajs/koa-generator.git", }, adonis: { description: "A full-stack Node.js framework (AdonisJS).", location: "{pm} create adonisjs", alias: "ad", }, sails: { description: "A real-time, MVC framework (Sails.js).", location: "{pm} install -g sails && sails new", }, angular: { description: "An official Angular project.", location: "{pm} install -g @angular/cli && ng new", alias: "ng", }, "angular-vite": { description: "An Angular project using Vite via AnalogJS.", location: "{pm} create analog@latest", alias: "ng-v", }, react: { description: "A React project using the recommended Vite setup.", location: "{pm} create vite@latest -- --template react", alias: "rt", }, svelte: { description: "A Svelte project using SvelteKit.", location: "{pm} create svelte@latest", }, qwik: { description: "An official Qwik project.", location: "{pm} create qwik@latest", }, astro: { description: "A new Astro project.", location: "{pm} create astro@latest", }, solid: { description: "An official SolidJS project.", location: "{pm} create solid@latest", }, remix: { description: "An official Remix project.", location: "{pm} create remix@latest", }, }, }, }, settings: { defaultPackageManager: PackageManagers.Bun, cacheStrategy: "daily", language: TextLanguages.English, }, }; const CONFIG_FILE_NAMES = [".devkitrc", ".devkit.json"]; const FILE_NAMES = { packageJson: "package.json", node_modules: "node_modules", common: { git: ".git", }, javascript: { lockFiles: [ "package-lock.json", "bun.lockb", "yarn.lock", "pnpm-lock.yaml", ], }, }; var lcid$1 = {}; var invertKv; var hasRequiredInvertKv; function requireInvertKv () { if (hasRequiredInvertKv) return invertKv; hasRequiredInvertKv = 1; invertKv = object => { if (typeof object !== 'object' || object === null) { throw new TypeError('Expected an object'); } const result = {}; for (const [key, value] of Object.entries(object)) { result[value] = key; } for (const symbol of Object.getOwnPropertySymbols(object)) { const value = object[symbol]; result[value] = symbol; } return result; }; return invertKv; } var require$$1 = { "4": "zh_CHS", "1025": "ar_SA", "1026": "bg_BG", "1027": "ca_ES", "1028": "zh_TW", "1029": "cs_CZ", "1030": "da_DK", "1031": "de_DE", "1032": "el_GR", "1033": "en_US", "1034": "es_ES", "1035": "fi_FI", "1036": "fr_FR", "1037": "he_IL", "1038": "hu_HU", "1039": "is_IS", "1040": "it_IT", "1041": "ja_JP", "1042": "ko_KR", "1043": "nl_NL", "1044": "nb_NO", "1045": "pl_PL", "1046": "pt_BR", "1047": "rm_CH", "1048": "ro_RO", "1049": "ru_RU", "1050": "hr_HR", "1051": "sk_SK", "1052": "sq_AL", "1053": "sv_SE", "1054": "th_TH", "1055": "tr_TR", "1056": "ur_PK", "1057": "id_ID", "1058": "uk_UA", "1059": "be_BY", "1060": "sl_SI", "1061": "et_EE", "1062": "lv_LV", "1063": "lt_LT", "1064": "tg_TJ", "1065": "fa_IR", "1066": "vi_VN", "1067": "hy_AM", "1069": "eu_ES", "1070": "wen_DE", "1071": "mk_MK", "1074": "tn_ZA", "1076": "xh_ZA", "1077": "zu_ZA", "1078": "af_ZA", "1079": "ka_GE", "1080": "fo_FO", "1081": "hi_IN", "1082": "mt_MT", "1083": "se_NO", "1086": "ms_MY", "1087": "kk_KZ", "1088": "ky_KG", "1089": "sw_KE", "1090": "tk_TM", "1092": "tt_RU", "1093": "bn_IN", "1094": "pa_IN", "1095": "gu_IN", "1096": "or_IN", "1097": "ta_IN", "1098": "te_IN", "1099": "kn_IN", "1100": "ml_IN", "1101": "as_IN", "1102": "mr_IN", "1103": "sa_IN", "1104": "mn_MN", "1105": "bo_CN", "1106": "cy_GB", "1107": "kh_KH", "1108": "lo_LA", "1109": "my_MM", "1110": "gl_ES", "1111": "kok_IN", "1114": "syr_SY", "1115": "si_LK", "1118": "am_ET", "1121": "ne_NP", "1122": "fy_NL", "1123": "ps_AF", "1124": "fil_PH", "1125": "div_MV", "1128": "ha_NG", "1130": "yo_NG", "1131": "quz_BO", "1132": "ns_ZA", "1133": "ba_RU", "1134": "lb_LU", "1135": "kl_GL", "1144": "ii_CN", "1146": "arn_CL", "1148": "moh_CA", "1150": "br_FR", "1152": "ug_CN", "1153": "mi_NZ", "1154": "oc_FR", "1155": "co_FR", "1156": "gsw_FR", "1157": "sah_RU", "1158": "qut_GT", "1159": "rw_RW", "1160": "wo_SN", "1164": "gbz_AF", "2049": "ar_IQ", "2052": "zh_CN", "2055": "de_CH", "2057": "en_GB", "2058": "es_MX", "2060": "fr_BE", "2064": "it_CH", "2067": "nl_BE", "2068": "nn_NO", "2070": "pt_PT", "2077": "sv_FI", "2080": "ur_IN", "2092": "az_AZ", "2094": "dsb_DE", "2107": "se_SE", "2108": "ga_IE", "2110": "ms_BN", "2115": "uz_UZ", "2128": "mn_CN", "2129": "bo_BT", "2141": "iu_CA", "2143": "tmz_DZ", "2155": "quz_EC", "3073": "ar_EG", "3076": "zh_HK", "3079": "de_AT", "3081": "en_AU", "3082": "es_ES", "3084": "fr_CA", "3098": "sr_SP", "3131": "se_FI", "3179": "quz_PE", "4097": "ar_LY", "4100": "zh_SG", "4103": "de_LU", "4105": "en_CA", "4106": "es_GT", "4108": "fr_CH", "4122": "hr_BA", "4155": "smj_NO", "5121": "ar_DZ", "5124": "zh_MO", "5127": "de_LI", "5129": "en_NZ", "5130": "es_CR", "5132": "fr_LU", "5179": "smj_SE", "6145": "ar_MA", "6153": "en_IE", "6154": "es_PA", "6156": "fr_MC", "6203": "sma_NO", "7169": "ar_TN", "7177": "en_ZA", "7178": "es_DO", "7194": "sr_BA", "7227": "sma_SE", "8193": "ar_OM", "8201": "en_JA", "8202": "es_VE", "8218": "bs_BA", "8251": "sms_FI", "9217": "ar_YE", "9225": "en_CB", "9226": "es_CO", "9275": "smn_FI", "10241": "ar_SY", "10249": "en_BZ", "10250": "es_PE", "11265": "ar_JO", "11273": "en_TT", "11274": "es_AR", "12289": "ar_LB", "12297": "en_ZW", "12298": "es_EC", "13313": "ar_KW", "13321": "en_PH", "13322": "es_CL", "14337": "ar_AE", "14346": "es_UR", "15361": "ar_BH", "15370": "es_PY", "16385": "ar_QA", "16394": "es_BO", "17417": "en_MY", "17418": "es_SV", "18441": "en_IN", "18442": "es_HN", "19466": "es_NI", "20490": "es_PR", "21514": "es_US", "31748": "zh_CHT" }; var hasRequiredLcid; function requireLcid () { if (hasRequiredLcid) return lcid$1; hasRequiredLcid = 1; const invertKv = requireInvertKv(); const all = require$$1; const inverted = invertKv(all); lcid$1.from = lcidCode => { if (typeof lcidCode !== 'number') { throw new TypeError('Expected a number'); } return all[lcidCode]; }; lcid$1.to = localeId => { if (typeof localeId !== 'string') { throw new TypeError('Expected a string'); } const lcidCode = inverted[localeId]; if (lcidCode) { return Number(inverted[localeId]); } }; lcid$1.all = new Proxy( inverted, { get(target, name) { const lcid = target[name]; if (lcid) { return Number(lcid); } } } ); return lcid$1; } var lcidExports = requireLcid(); var lcid = /*@__PURE__*/getDefaultExportFromCjs(lcidExports); // Mini wrapper around `child_process` to make it behave a little like `execa`. const execFile = promisify(childProcess.execFile); /** @param {string} command @param {string[]} arguments_ @returns {Promise<import('child_process').ChildProcess>} */ async function exec(command, arguments_) { const subprocess = await execFile(command, arguments_, {encoding: 'utf8'}); subprocess.stdout = subprocess.stdout.trim(); return subprocess; } const defaultOptions = {spawn: true}; const defaultLocale = 'en-US'; async function getStdOut(command, args) { return (await exec(command, args)).stdout; } function getEnvLocale(env = process.env) { return env.LC_ALL || env.LC_MESSAGES || env.LANG || env.LANGUAGE; } function parseLocale(string) { const env = {}; for (const definition of string.split('\n')) { const [key, value] = definition.split('='); env[key] = value.replace(/^"|"$/g, ''); } return getEnvLocale(env); } function getLocale(string) { return (string && string.replace(/[.:].*/, '')); } async function getLocales() { return getStdOut('locale', ['-a']); } function getSupportedLocale(locale, locales = '') { return locales.includes(locale) ? locale : defaultLocale; } async function getAppleLocale() { const results = await Promise.all([ getStdOut('defaults', ['read', '-globalDomain', 'AppleLocale']), getLocales(), ]); return getSupportedLocale(results[0], results[1]); } async function getUnixLocale() { return getLocale(parseLocale(await getStdOut('locale'))); } async function getWinLocale() { const stdout = await getStdOut('wmic', ['os', 'get', 'locale']); const lcidCode = Number.parseInt(stdout.replace('Locale', ''), 16); return lcid.from(lcidCode); } function normalise(input) { return input.replace(/_/, '-'); } const cache$1 = new Map(); async function osLocale(options = defaultOptions) { if (cache$1.has(options.spawn)) { return cache$1.get(options.spawn); } let locale; try { const envLocale = getEnvLocale(); if (envLocale || options.spawn === false) { locale = getLocale(envLocale); } else if (process.platform === 'win32') { locale = await getWinLocale(); } else if (process.platform === 'darwin') { locale = await getAppleLocale(); } else { locale = await getUnixLocale(); } } catch {} const normalised = normalise(locale || defaultLocale); cache$1.set(options.spawn, normalised); return normalised; } class DevkitError extends Error { constructor(message, options) { super(message, options); this.name = "DevkitError"; } } class ConfigError extends DevkitError { filePath; constructor(message, filePath, options) { super(message, options); this.filePath = filePath; this.name = "ConfigError"; } } class GitError extends DevkitError { url; constructor(message, url, options) { super(message, options); this.url = url; this.name = "GitError"; } } async function findUp({ files, cwd, limit, }) { let currentDir = path.resolve(cwd ?? process.cwd()); const filesToFind = Array.isArray(files) ? files : [files]; while (true) { for (const file of filesToFind) { const filePath = path.join(currentDir, file); try { const stats = await fs.stat(filePath); if (stats.isDirectory() || stats.isFile()) { return filePath; } // oxlint-disable-next-line no-unused-vars } catch (e) { // File does not exist, continue search } } if (currentDir === limit) { break; } const parentDir = path.dirname(currentDir); if (parentDir === currentDir || currentDir === homedir()) { break; } currentDir = parentDir; } return null; } async function findFileInDirectory(directory, fileNames) { for (const fileName of fileNames) { const filePath = path.join(directory, fileName); if (await fs.pathExists(filePath)) { return filePath; } } return null; } const MONOREPO_INDICATORS = [ "pnpm-workspace.yaml", "lerna.json", ]; const NODE_MODULES = "node_modules"; async function findMonorepoRoot() { const foundFile = await findUp({ files: [...MONOREPO_INDICATORS, NODE_MODULES], }); if (!foundFile) { return null; } const rootDir = path.dirname(foundFile); const fileName = path.basename(foundFile); if (MONOREPO_INDICATORS.includes(fileName) || fileName === NODE_MODULES) { return rootDir; } return null; } async function findProjectRoot() { const filePath = await findUp({ files: [NODE_MODULES], }); if (!filePath) { return null; } return path.dirname(filePath); } async function findPackageRoot() { const __filename = fileURLToPath(import.meta.url); const startDir = dirname(__filename); const filePath = await findUp({ files: [FILE_NAMES.packageJson], cwd: startDir, }); if (!filePath) { throw new DevkitError("Package root not found. Cannot determine the root of the devkit."); } return path.dirname(filePath); } async function findLocalesDir() { const packageRoot = await findPackageRoot(); return path.join(packageRoot, "locales"); } let translations = {}; function getSupportedLanguage(lang) { if (!lang) return undefined; const supportedLanguages = Object.values(TextLanguages); const validatedLang = lang?.split(/[_.-]/)[0]?.toLowerCase(); return supportedLanguages.includes(validatedLang) ? validatedLang : undefined; } async function loadTranslations(configLang) { const userLang = getSupportedLanguage(configLang); const rawSystemLocale = await osLocale(); const systemLang = getSupportedLanguage(rawSystemLocale); const languageToLoad = userLang || systemLang || "en"; try { const localesDir = await findLocalesDir(); const filePath = path.join(localesDir, `${languageToLoad}.json`); translations = await fs.readJson(filePath); // oxlint-disable-next-line no-unused-vars } catch (error) { const localesDir = await findLocalesDir(); const fallbackPath = path.join(localesDir, "en.json"); try { translations = await fs.readJson(fallbackPath); } catch (e) { throw new DevkitError(`Failed to load translations from both ${languageToLoad}.json and the fallback en.json`, { cause: e }); } } } function resolveNestedKey(obj, key) { const parts = key.split("."); let current = obj; for (const part of parts) { if (current && typeof current === "object" && part in current) { current = current[part]; } else { return undefined; } } return typeof current === "string" ? current : undefined; } function t(key, variables = {}) { const translatedString = resolveNestedKey(translations, key) || key; let result = translatedString; for (const [varName, varValue] of Object.entries(variables)) { result = result.replace(`{${varName}}`, varValue); } return result; } const allConfigFiles = [...CONFIG_FILE_NAMES]; async function findGlobalConfigFile() { const homeDir = os.homedir(); return findFileInDirectory(homeDir, allConfigFiles); } async function findLocalConfigFile() { const monorepoRoot = await findMonorepoRoot(); const projectRoot = await findProjectRoot(); const searchLimit = monorepoRoot || projectRoot; if (!searchLimit) { return findUp({ files: [...CONFIG_FILE_NAMES], cwd: process.cwd(), limit: process.cwd(), }); } return findUp({ files: [...CONFIG_FILE_NAMES], cwd: process.cwd(), limit: searchLimit, }); } async function getConfigFilepath(isGlobal = false) { const allConfigFiles = [...CONFIG_FILE_NAMES]; if (isGlobal) { return (await findGlobalConfigFile()) || ""; } const localConfigPath = await findUp({ files: [...allConfigFiles], cwd: process.cwd(), }); if (localConfigPath) { return localConfigPath; } return path.join(process.cwd(), allConfigFiles[1]); } const { existsSync } = fs; async function readConfigAtPath(filePath) { if (!existsSync(filePath)) { return null; } try { return await fs.readJson(filePath); } catch (error) { throw new Error(`Failed to read or parse config file at ${filePath}`, { cause: error, }); } } async function readLocalConfig() { const filePath = await getConfigFilepath(false); if (!filePath || !existsSync(filePath)) { return null; } const config = await readConfigAtPath(filePath); if (!config) return null; return { config, filePath, source: "local" }; } async function readGlobalConfig() { const filePath = await getConfigFilepath(true); if (!filePath || !existsSync(filePath)) { return null; } const config = await readConfigAtPath(filePath); if (!config) return null; return { config, filePath, source: "global" }; } async function getLocaleFromConfigMinimal() { const localConfigPath = await findLocalConfigFile(); if (localConfigPath) { try { const config = await readConfigAtPath(localConfigPath); if (config?.settings?.language && SUPPORTED_LANGUAGES.includes(config.settings.language)) { return config.settings.language; } } catch (error) { if (error.code !== "ENOENT") { throw new ConfigError("Failed to read local config for locale.", localConfigPath, { cause: error }); } } } const globalConfigPath = await getConfigFilepath(true); try { const config = await readConfigAtPath(globalConfigPath); if (config?.settings?.language && SUPPORTED_LANGUAGES.includes(config.settings.language)) { return config.settings.language; } } catch (error) { if (error.code !== "ENOENT") { throw new ConfigError("Failed to read global config for locale.", globalConfigPath, { cause: error }); } } return defaultCliConfig.settings.language; } async function loadUserConfig(spinner) { let finalConfig = { ...defaultCliConfig }; let source = "default"; if (spinner) { spinner.text = t("config.check.global"); } const globalConfigPath = await getConfigFilepath(true); const globalConfig = await readConfigAtPath(globalConfigPath); if (globalConfig) { if (source === "default") { source = "global"; } finalConfig = deepmerge(finalConfig, globalConfig); } if (spinner) { spinner.text = t("config.check.local"); } const localConfigPath = await getConfigFilepath(); const localConfig = await readConfigAtPath(localConfigPath); if (localConfig) { finalConfig = deepmerge(finalConfig, localConfig); source = "local"; } return { config: finalConfig, source }; } async function readAndMergeConfigs(options = {}) { let finalConfig = JSON.parse(JSON.stringify(defaultCliConfig)); let source = "default"; let configPath = null; if (!options.forceGlobal) { configPath = await findLocalConfigFile(); if (configPath) { source = "local"; } } if (source === "default") { configPath = await findGlobalConfigFile(); if (configPath && (await fs.pathExists(configPath))) { source = "global"; } } if (configPath && (await fs.pathExists(configPath))) { try { const foundConfig = await fs.readJson(configPath); finalConfig = deepmerge(finalConfig, foundConfig, { arrayMerge: (_, sourceArray) => sourceArray, }); // oxlint-disable-next-line no-unused-vars } catch (e) { console.error(`Warning: Invalid configuration file found at ${configPath}. Using default settings.`); source = "default"; } } return { config: finalConfig, source }; } async function getProjectVersion() { try { const packageRoot = await findPackageRoot(); if (!packageRoot) { throw new Error(t("error.package.root.not_found")); } const packageJsonPath = path.join(packageRoot, FILE_NAMES.packageJson); const packageJson = await fs.readJson(packageJsonPath); return packageJson.version; } catch (error) { console.error(chalk.red(t("error.version.read_fail")), error); return "0.0.0"; } } function handleErrorAndExit(error, spinner) { spinner.stop(); if (error instanceof ConfigError) { console.error(chalk.red(`\n${t("error.config.generic")}: ${error.message}`)); if (error.filePath) { console.error(chalk.red(`File path: ${error.filePath}`)); } } else if (error instanceof GitError) { console.error(chalk.red(`\n${t("error.git.generic")}: ${error.message}`)); if (error.url) { console.error(chalk.red(`Repository URL: ${error.url}`)); } } else if (error instanceof Error) { console.error(chalk.red(`\n${t("error.unexpected")}: ${error.message}`)); } else { console.error(chalk.red(`\n${t("error.unknown")}`)); } process.exit(1); } function setupNewCommand(options) { const { program, config } = options; program .command("new") .alias("nw") .description(t("new.command.description")) .argument("<language>", t("new.project.language.argument")) .argument("<projectName>", t("new.project.name.argument")) .requiredOption("-t, --template <string>", t("new.project.template.option.description")) .action(async (language, projectName, cmdOptions) => { const { template } = cmdOptions; const scaffoldSpinner = ora(chalk.cyan(t("new.project.scaffolding", { projectName, template: template, }))).start(); try { const languageTemplates = config.templates[language]; if (!languageTemplates) { throw new DevkitError(t("error.language_config_not_found", { language })); } const templateConfig = languageTemplates.templates[template] || Object.values(languageTemplates.templates).find((t) => t.alias === template); if (!templateConfig) { throw new DevkitError(t("error.template.not_found", { template })); } const { scaffoldProject } = await import(`#scaffolding/${language}.js`); await scaffoldProject({ projectName, templateConfig, packageManager: templateConfig.packageManager || config.settings.defaultPackageManager, cacheStrategy: templateConfig.cacheStrategy || config.settings.cacheStrategy || "daily", }); scaffoldSpinner.succeed(chalk.green(t("new.project.success", { projectName }))); } catch (error) { handleErrorAndExit(error, scaffoldSpinner); } }); } const SCHEMA_PATH = "./node_modules/scaffolder-toolkit/devkit-schema.json"; async function saveConfig$1(config, filePath) { try { await fs.writeJson(filePath, { $schema: SCHEMA_PATH, ...config, }); } catch (error) { throw new DevkitError(t("error.config.save", { file: filePath }), { cause: error, }); } } async function saveCliConfig(config, isGlobal = false) { const filePath = await getConfigFilepath(isGlobal); await saveConfig$1(config, filePath); } async function saveGlobalConfig(config) { const targetPath = await getConfigFilepath(true); await saveConfig$1(config, targetPath); } async function saveLocalConfig(config) { const targetPath = await getConfigFilepath(); await saveConfig$1(config, targetPath); } function validateConfigValue(key, value) { if (key === "defaultPackageManager") { const validPackageManagers = Object.values(PackageManagers); if (!validPackageManagers.includes(value)) { throw new DevkitError(t("error.invalid.value", { key, options: validPackageManagers.join(", "), })); } } else if (key === "cacheStrategy") { const validStrategies = VALID_CACHE_STRATEGIES; if (!validStrategies.includes(value)) { throw new DevkitError(t("error.invalid.value", { key, options: validStrategies.join(", "), })); } } else if (key === "language") { const validLanguages = Object.values(TextLanguages); if (!validLanguages.includes(value)) { throw new DevkitError(t("error.invalid.value", { key, options: validLanguages.join(", "), })); } } } const configAliases = { pm: "defaultPackageManager", packageManager: "defaultPackageManager", cache: "cacheStrategy", cacheStrategy: "cacheStrategy", language: "language", lg: "language", }; function setupConfigSetCommand(options) { const { program } = options; const setCommandDescription = t("config.set.command.description", { pmValues: Object.values(PackageManagers).join(", "), }); program .command("set") .description(setCommandDescription) .argument("<settings...>", t("config.set.argument.description")) .option("-g, --global", t("config.set.option.global"), false) .action(async (settings, cmdOptions) => { const spinner = ora(chalk.cyan(t("config.set.updating"))).start(); try { const { config, source } = await readAndMergeConfigs({ forceGlobal: cmdOptions.global, }); if (source === "default") { throw new DevkitError(t("error.config.no_file_found")); } if (settings.length % 2 !== 0) { throw new DevkitError(t("error.command.set.invalid_arguments_count")); } for (let i = 0; i < settings.length; i += 2) { const key = settings[i]; const value = settings[i + 1]; const canonicalKey = configAliases[key]; if (!canonicalKey) { throw new DevkitError(t("error.invalid.key", { key, keys: Object.keys(configAliases).join(", "), })); } validateConfigValue(canonicalKey, value); config.settings[canonicalKey] = value; } if (cmdOptions.global) { await saveGlobalConfig(config); } else { await saveLocalConfig(config); } spinner.succeed(chalk.bold.green(t("config.set.success"))); } catch (error) { handleErrorAndExit(error, spinner); } }); } function setupConfigGetCommand(options) { const { program } = options; program .command("get") .description(t("config.get.command.description")) .argument("[key]", t("config.get.argument.description"), "") .option("-g, --global", t("config.get.option.global"), false) .action(async (key, cmdOptions) => { const spinner = ora(chalk.cyan(t("config.get.loading"))).start(); let activeConfig; let sourceMessage = null; let configSource; try { const { config, source } = await readAndMergeConfigs({ forceGlobal: cmdOptions.global, forceLocal: !cmdOptions.global, }); activeConfig = config.settings; configSource = source; if (cmdOptions.global) { if (configSource === "default") { sourceMessage = t("config.get.fallback.global"); } else { sourceMessage = t("config.get.source.global"); } } else { if (configSource === "default") { sourceMessage = t("config.get.fallback.local"); } else if (configSource === "global") { sourceMessage = t("config.get.fallback.local_to_global"); } else { sourceMessage = t("config.get.source.local"); } } spinner.succeed(chalk.bold.green(t("config.get.success"))); if (sourceMessage) { console.log(chalk.bold.yellow(sourceMessage)); } if (key) { printConfigValue(activeConfig, key); } else { printConfig(activeConfig); } } catch (error) { handleErrorAndExit(error, spinner); } }); } function printConfig(activeConfig) { console.log(`\n${chalk.bold.blue("Current Configuration:")}`); for (const [key, value] of Object.entries(activeConfig)) { if (value === null || value === undefined) { continue; } if (typeof value === "object" && !Array.isArray(value)) { console.log(` - ${chalk.green(key)}:`); for (const [subKey, subValue] of Object.entries(value)) { console.log(` - ${chalk.yellow(subKey)}: ${chalk.white(subValue)}`); } } else { console.log(` - ${chalk.green(key)}: ${chalk.white(value)}`); } } console.log(""); } function printConfigValue(activeConfig, key) { const canonicalKey = configAliases[key] || key; const value = activeConfig[canonicalKey]; if (value === undefined) { console.log(chalk.red(t("config.get.not_found", { key: canonicalKey }))); return; } const outputValue = typeof value === "object" ? JSON.stringify(value, null, 2) : value; console.log(`\n${chalk.cyan(canonicalKey)}: ${chalk.white(outputValue)}\n`); } function setupConfigCommand(options) { const { program} = options; const configCommand = program .command("config") .alias("cf") .description(t("config.command.description")); setupConfigSetCommand({ program: configCommand}); setupConfigGetCommand({ program: configCommand}); } async function getConfigsToDisplay(opts, spinner) { const { global, local, all } = opts; const configs = []; const addTemplates = (config) => { if (config) { configs.push({ templates: config.config.templates }); } }; const getAndAddLocalConfig = async () => { const localConfig = await readLocalConfig(); addTemplates(localConfig); return localConfig; }; const getAndAddGlobalConfig = async () => { const globalConfig = await readGlobalConfig(); addTemplates(globalConfig); return globalConfig; }; if (all) { await getAndAddLocalConfig(); await getAndAddGlobalConfig(); } else if (global) { const globalConfig = await getAndAddGlobalConfig(); if (globalConfig) { spinner.info(t("list.templates.using_global")).start(); } } else if (local) { const localConfig = await getAndAddLocalConfig(); if (localConfig) { spinner.info(t("list.templates.using_local")).start(); } } else { const localConfig = await getAndAddLocalConfig(); if (!localConfig) { const globalConfig = await getAndAddGlobalConfig(); if (globalConfig) { spinner.info(t("list.templates.using_global_fallback")).start(); } } } return configs; } function setupListCommand(options) { const { program } = options; program .command("list") .alias("ls") .description(t("list.command.description")) .argument("[language]", t("list.command.language.argument"), "") .option("-g, --global", t("list.command.global.option")) .option("-l, --local", t("list.command.local.option")) .option("-a, --all", t("list.command.all.option")) .action(async (language, opts) => { const spinner = ora(t("list.templates.loading")).start(); try { const configsToDisplay = await getConfigsToDisplay(opts, spinner); const allTemplatesFound = configsToDisplay.some((config) => Object.keys(config.templates).length > 0); if (!allTemplatesFound) { spinner.succeed(chalk.yellow(t("list.templates.not_found"))); return; } spinner.stop(); console.log("\n", chalk.bold(t("list.templates.header"))); if (language) { const foundTemplates = configsToDisplay.flatMap((configSource) => Object.entries(configSource.templates) .filter(([lang]) => lang === language) .map(([_, langTemplates]) => langTemplates)); if (foundTemplates.length === 0) { throw new DevkitError(t("error.language_config_not_found", { language })); } foundTemplates.forEach((langTemplates) => { printTemplates(language, langTemplates.templates); }); } else { for (const configSource of configsToDisplay) { for (const [lang, langTemplates] of Object.entries(configSource.templates)) { printTemplates(lang, langTemplates.templates); } } } } catch (error) { handleErrorAndExit(error, spinner); } }); } function printTemplates(language, templates) { console.log(`\n${chalk.blue.bold(language.toUpperCase())}:`); for (const [templateName, templateConfig] of Object.entries(templates)) { const alias = templateConfig.alias ? chalk.dim(`(alias: ${templateConfig.alias})`) : ""; const description = templateConfig.description ? `\n ${chalk.dim("Description:")} ${templateConfig.description}` : ""; const location = templateConfig.location ? `\n ${chalk.dim("Location:")} ${templateConfig.location}` : ""; const cacheStrategy = templateConfig.cacheStrategy ? `\n ${chalk.dim("Cache Strategy:")} ${templateConfig.cacheStrategy}` : ""; console.log(` - ${chalk.green(templateName)} ${alias}${description}${location}${cacheStrategy}\n`); } } function setupRemoveTemplateCommand(options) { const { program, config, source } = options; program .command("remove-template") .alias("rt") .description(t("remove_template.command.description")) .argument("<language>", t("remove_template.language.argument")) .argument("<templateName>", t("remove_template.name.argument")) .option("-g, --global", t("remove_template.option.global"), false) .action(async (language, templateName, commandOptions) => { const { global: isGlobal } = commandOptions; const spinner = ora(t("remove_template.start")).start(); let targetConfig; try { if (source === "default") { throw new DevkitError(t("error.config.no_file_found")); } if (isGlobal) { if (source === "global") { targetConfig = config; } else { const globalConfigPath = await getConfigFilepath(true); const globalConfig = await readConfigAtPath(globalConfigPath); if (!globalConfig) { throw new DevkitError(t("error.config.global.not.found")); } targetConfig = globalConfig; } } else { if (source !== "local") { throw new DevkitError(t("error.config.local.not.found")); } targetConfig = config; } const languageTemplates = targetConfig.templates[language]; if (!languageTemplates) { throw new DevkitError(t("error.language_config_not_found", { language })); } let templateToRemove = templateName; let templateConfig = languageTemplates.templates[templateToRemove]; if (!templateConfig) { const foundTemplateName = Object.keys(languageTemplates.templates).find((key) => languageTemplates.templates[key]?.alias === templateName); if (foundTemplateName) { templateToRemove = foundTemplateName; templateConfig = languageTemplates.templates[foundTemplateName]; } } if (!templateConfig) { throw new DevkitError(t("error.template.not_found", { template: templateName })); } const updatedTemplates = { ...languageTemplates.templates }; const { [templateToRemove]: _, ...restOfTemplates } = updatedTemplates; languageTemplates.templates = restOfTemplates; if (isGlobal) { await saveGlobalConfig(targetConfig); } else { await saveLocalConfig(targetConfig); } spinner.succeed(t("remove_template.success", { templateName, language })); } catch (error) { handleErrorAndExit(error, spinner); } }); } function setupAddTemplateCommand(options) { const { program, config, source } = options; program .command("add-template <language> <templateName> <location>") .description(t("cli.add_template.description")) .alias("at") .requiredOption("--description <string>", t("cli.add_template.options.description")) .option("--alias <string>", t("cli.add_template.options.alias")) .option("--cache-strategy <string>", t("cli.add_template.options.cache")) .option("--package-manager <string>", t("cli.add_template.options.package_manager")) .option("-g, --global", t("config.set.option.global"), false) .action(async (language, templateName, location, cmdOptions) => { const addSpinner = ora(chalk.cyan(t("cli.add_template.adding"))).start(); try { if (source === "default") { throw new DevkitError(t("error.config.not.found")); } let targetConfig; const isGlobal = !!cmdOptions.global; if (isGlobal) { const globalConfigPath = await getConfigFilepath(true); const existingGlobalConfig = await readConfigAtPath(globalConfigPath); if (!existingGlobalConfig) { throw new DevkitError(t("error.config.global.not.found")); } targetConfig = deepmerge({}, existingGlobalConfig); } else { if (source === "global") { throw new DevkitError(t("error.config.local.not.found")); } targetConfig = deepmerge({}, config); } if (!targetConfig.templates[language]) { throw new DevkitError(t("error.language_config_not_found", { language })); } const languageConfig = targetConfig.templates[language]; if (languageConfig.templates[templateName]) { throw new DevkitError(t("error.template.exists", { template: templateName })); } if (cmdOptions.alias) { const aliasExists = Object.values(languageConfig.templates).some((t) => t.alias === cmdOptions.alias); if (aliasExists) { throw new DevkitError(t("error.alias.exists", { alias: cmdOptions.alias })); } } if (cmdOptions.cacheStrategy && !VALID_CACHE_STRATEGIES.includes(cmdOptions.cacheStrategy)) { throw new DevkitError(t("error.invalid.cache_strategy", { value: cmdOptions.cacheStrategy, options: VALID_CACHE_STRATEGIES.join(", "), })); } if (cmdOptions.packageManager && !VALID_PACKAGE_MANAGERS.includes(cmdOptions.packageManager)) { throw new DevkitError(t("error.invalid.package_manager", { value: cmdOptions.packageManager, options: VALID_PACKAGE_MANAGERS.join(", "), })); } const newTemplate = { description: cmdOptions.description, location: location, alias: cmdOptions.alias, cacheStrategy: cmdOptions.cacheStrategy, packageManager: cmdOp