@handykit/cli
Version:
A collection of handy CLI tools
205 lines • 9.04 kB
JavaScript
import { askUser } from "../utils/common/index.js";
import * as fs from "fs/promises";
import * as path from "path";
import * as os from "os";
import { fileURLToPath } from "url";
import { createFoldersFromTemplate, runInteractiveWizard, } from "../utils/scaffold/index.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* Creates a directory structure from a template.
* The template is a JSON object, where each key is a folder name, and the value is either
* another JSON object (representing a subfolder) or a string (representing a file).
* @param {Object} options - Options for creating the directory structure.
* @param {string} [options.entry] - The folder to create the directory structure in. Defaults to "src".
* @param {string} [options.templateName] - The name of the template to use. Defaults to "react-default".
* @param {string} [options.customFile] - The path to a custom template file.
* @param {boolean} [options.interactive] - Whether to use interactive mode.
*/
export const scaffoldDir = async ({ entry, templateName, customFile, interactive = false, non_interactive = false, force = false, }) => {
if (interactive && non_interactive) {
console.error("❌ Cannot use interactive mode in non-interactive mode.");
return;
}
// 1. ENTRY
if (!entry) {
if (non_interactive) {
console.error("❌ Entry folder is required in non-interactive mode.");
return;
}
entry = (await askUser("Enter entry folder (default 'src'): ")) || "src";
}
let template;
const defaultTemplateDir = path.resolve(__dirname, "../../assets/templates/dir");
const userTemplateDir = path.resolve(os.homedir(), ".scaffold-cli/templates/dir");
await fs.mkdir(userTemplateDir, { recursive: true });
// 2. INTERACTIVE MODE
if (interactive) {
if (non_interactive) {
console.error("❌ Cannot use interactive mode in non-interactive mode.");
return;
}
template = await runInteractiveWizard();
if (!non_interactive) {
const save = await askUser("Save this structure as a reusable template? [y/N]: ");
if (["y", "yes"].includes(save.toLowerCase())) {
const name = await askUser("Enter template name (e.g. my-template): ");
const savePath = path.resolve(userTemplateDir, `${name}.json`);
await fs.writeFile(savePath, JSON.stringify(template, null, 2), "utf8");
console.log(`✅ Template saved at ${savePath}`);
}
}
}
// 3. CUSTOM FILE
else if (customFile) {
try {
const fileContent = await fs.readFile(path.resolve(customFile), "utf-8");
template = JSON.parse(fileContent);
}
catch (err) {
console.error("❌ Error reading custom template:", err);
return;
}
}
// 4. TEMPLATE SELECTION
else {
const [defaultTemplates, userTemplates] = await Promise.all([
fs.readdir(defaultTemplateDir),
fs.readdir(userTemplateDir),
]);
const allTemplates = [
...defaultTemplates.map((name) => ({
name,
type: "default",
path: path.join(defaultTemplateDir, name),
})),
...userTemplates.map((name) => ({
name,
type: "user",
path: path.join(userTemplateDir, name),
})),
];
if (!templateName) {
if (non_interactive) {
console.error("❌ Template name is required in non-interactive mode.");
return;
}
const templateOptions = [
...allTemplates.map((t, i) => ({
label: `${i + 1}. ${t.type === "user" ? "📦 user" : "📦 default"} - ${t.name.replace(".json", "")}`,
value: t.name.replace(".json", ""),
index: i,
})),
{
label: `${allTemplates.length + 1}. 🎨 interactive - create from scratch`,
value: "interactive",
index: allTemplates.length,
},
];
const selection = await askUser(`Choose one of the following templates:\n${templateOptions
.map((opt) => opt.label)
.join("\n")}\nEnter template number or name: `);
const selectedByIndex = parseInt(selection);
const selectedTemplate = templateOptions.find((opt) => opt.value === selection) ||
templateOptions.find((opt) => opt.index === selectedByIndex - 1);
if (!selectedTemplate) {
console.error("❌ Invalid selection.");
return;
}
if (selectedTemplate.value === "interactive") {
template = await runInteractiveWizard();
const save = await askUser("Save this structure as a reusable template? [y/N]: ");
if (["y", "yes"].includes(save.toLowerCase())) {
const name = await askUser("Enter template name (e.g. my-template): ");
const savePath = path.resolve(userTemplateDir, `${name}.json`);
await fs.writeFile(savePath, JSON.stringify(template, null, 2), "utf8");
console.log(`✅ Template saved at ${savePath}`);
}
}
else {
try {
const fileContent = await fs.readFile(allTemplates[selectedTemplate.index].path, "utf-8");
template = JSON.parse(fileContent);
}
catch (err) {
console.error("❌ Error reading template:", err);
return;
}
}
}
else {
const selectedTemplate = allTemplates.find((t) => t.name.replace(".json", "") === templateName);
if (!selectedTemplate) {
console.error("❌ Template not found:", templateName);
return;
}
try {
const fileContent = await fs.readFile(selectedTemplate.path, "utf-8");
template = JSON.parse(fileContent);
}
catch (err) {
console.error("❌ Error reading template:", err);
return;
}
}
}
// 5. PREVIEW
function preview(templateObj, indent = 0) {
const indentStr = " ".repeat(indent);
for (const key of Object.keys(templateObj)) {
if (key === "files") {
const files = templateObj[key];
if (Array.isArray(files.paths)) {
for (const fullPath of files.paths) {
const fileName = path.basename(fullPath);
console.log(`${indentStr}📄 ${fileName} (copied from ${fullPath})`);
}
}
for (const fileName of Object.keys(files)) {
if (fileName === "paths")
continue;
const file = files[fileName];
const ext = file.type ? `.${file.type}` : "";
console.log(`${indentStr}📄 ${fileName}${ext}`);
}
}
else if (key === "paths") {
const paths = templateObj[key];
for (const filePath of paths) {
const fileName = path.basename(filePath);
console.log(`${indentStr}📄 ${fileName} (top-level path)`);
}
}
else {
const value = templateObj[key];
const hasFiles = typeof value === "object" &&
(value.files || value.paths || Object.keys(value).length > 0);
const icon = hasFiles ? "📂" : "📁";
console.log(`${indentStr}${icon} ${key}/`);
if (typeof value === "object")
preview(value, indent + 1);
}
}
}
if (!non_interactive) {
console.log("\n🪟 Folder structure preview:");
preview(template);
console.log(`\nWill be created under: '${path.resolve(process.cwd(), entry)}'\n`);
if (!force) {
const confirm = await askUser("Do you want to proceed? [y/N]: ");
if (!["y", "yes"].includes(confirm.toLowerCase())) {
console.log("❌ Aborted by user.");
return;
}
}
}
// 6. CREATE
try {
await createFoldersFromTemplate(template, path.resolve(process.cwd(), entry), { force });
console.log(`✅ Folder structure created at '${path.resolve(process.cwd(), entry)}'`);
}
catch (err) {
console.error("❌ Error creating folders:", err);
}
};
//# sourceMappingURL=scaffold-dir.js.map