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
JavaScript
#!/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