treemk
Version:
A CLI tool to generate project structures from text, JSON templates, file, with boilerplate and git integration
637 lines (558 loc) ⢠20.9 kB
JavaScript
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import chalk from 'chalk';
import { StructureGenerator } from './lib/structure-generator.js';
import {
saveTemplate,
loadTemplate,
listTemplates,
removeTemplate,
getBuiltInTemplate
} from './lib/templates.js';
import { loadConfig, mergeOptions } from './lib/config.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
async function readFromStdin() {
return new Promise((resolve, reject) => {
let data = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => {
data += chunk;
});
process.stdin.on('end', () => {
resolve(data);
});
process.stdin.on('error', reject);
// Timeout after 100ms if no data
setTimeout(() => {
if (!data) {
process.stdin.pause();
resolve(null);
}
}, 100);
});
}
function printHelp() {
console.log(chalk.bold('\nš¦ treemk') + ' - Generate folder structures from text, JSON, or trees\n');
console.log(chalk.bold('Usage:'));
console.log(' treemk [options]\n');
console.log(chalk.bold('Basic Options:'));
console.log(' -i, --input <file> Input file path (absolute or relative)');
console.log(' -o, --output <path> Output directory (default: ./output)');
console.log(' -b, --boilerplate Add context-aware boilerplate content');
console.log(' -d, --dry-run Show what would be created without writing');
console.log(' -j, --json Parse input as JSON');
console.log(' -h, --help Show this help message\n');
console.log(chalk.bold('Template Management:'));
console.log(' --template <name> Use built-in template (react|node|python)');
console.log(' --template-save <name> Save current input as reusable template');
console.log(' --template-use <name> Load and use a saved template');
console.log(' --template-list List all saved templates');
console.log(' --template-remove <name> Delete a saved template\n');
console.log(chalk.bold('Git Integration:'));
console.log(' -g, --git-init Initialize git repository');
console.log(' --git-commit Create initial commit');
console.log(' --git-push Push to GitHub (requires gh CLI)\n');
console.log(chalk.bold('Advanced Options:'));
console.log(' --install Auto-install dependencies (npm/pip)');
console.log(' --preview Preview structure as ASCII tree');
console.log(' -u, --from-url <url> Fetch structure from URL');
console.log(' --text <structure> Pass structure as text argument');
console.log(' --json-input <json> Pass JSON structure as argument');
console.log(' --setup Interactive setup wizard');
console.log(' --setup --run Setup and run immediately');
console.log(' --config Use treemk.config.json if present\n');
console.log(chalk.bold('Examples:'));
console.log(' # From file (any location)');
console.log(' treemk -i ~/Downloads/tree.txt -o ./myapp -b\n');
console.log(' # From stdin (pipe)');
console.log(' cat structure.txt | treemk -o ./myapp -b\n');
console.log(' # Inline text (no file needed!)');
console.log(' treemk --text "src/index.js\\nsrc/app.js\\nREADME.md" -o ./app\n');
console.log(' # Inline JSON (no file needed!)');
console.log(' treemk --json-input \'{"src":["index.js"]}\' -o ./app\n');
console.log(' # Interactive setup');
console.log(' treemk --setup --run\n');
console.log(' # Template workflow');
console.log(' treemk --template-use myproject -o ./app --git-init\n');
console.log(' # Full automation');
console.log(' treemk --template node -o ./api -b --install -g --git-commit\n');
console.log(' # Preview before creating');
console.log(' treemk -i structure.txt --preview\n');
console.log(chalk.bold('Config File (treemk.config.json):'));
console.log(' Place in your working directory for default options:');
console.log(' {');
console.log(' "template": "node",');
console.log(' "output": "./app",');
console.log(' "boilerplate": true,');
console.log(' "gitInit": true,');
console.log(' "install": true');
console.log(' }\n');
console.log(chalk.bold('Input Format Examples:'));
console.log('\n Tree format:');
console.log(' src/');
console.log(' āāā components/');
console.log(' ā āāā App.jsx');
console.log(' āāā index.js\n');
console.log(' Plain paths:');
console.log(' src/components/App.jsx');
console.log(' src/index.js');
console.log(' package.json\n');
console.log(' JSON format:');
console.log(' {');
console.log(' "src": {');
console.log(' "components": ["App.jsx"],');
console.log(' "index.js": null');
console.log(' }');
console.log(' }\n');
}
async function resolveInputPath(inputPath) {
if (!inputPath) {
return null;
}
// Try absolute path first
let resolved = path.resolve(inputPath);
try {
await fs.access(resolved);
return resolved;
} catch (e) {
// File doesn't exist at absolute path
}
// Try relative to current working directory
resolved = path.resolve(process.cwd(), inputPath);
try {
await fs.access(resolved);
return resolved;
} catch (e) {
// File doesn't exist at cwd
}
// Try relative to script location
resolved = path.resolve(__dirname, inputPath);
try {
await fs.access(resolved);
return resolved;
} catch (e) {
// File doesn't exist at script location
}
// Try common locations
const commonPaths = [
path.join(process.env.HOME || process.env.USERPROFILE, 'Downloads', inputPath),
path.join(process.env.HOME || process.env.USERPROFILE, 'Documents', inputPath),
path.join(process.env.HOME || process.env.USERPROFILE, 'Desktop', inputPath),
];
for (const p of commonPaths) {
try {
await fs.access(p);
return p;
} catch (e) {
// Continue to next path
}
}
return null;
}
async function setupDynamicConfig(autoRun = false) {
console.log(chalk.cyan.bold("\nš ļø treemk Setup Wizard"));
console.log(chalk.gray("--------------------------------\n"));
const readline = await import('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const question = (query) => new Promise((resolve) => rl.question(query, resolve));
try {
console.log(chalk.yellow('Answer the following questions (press Enter for defaults):\n'));
const template = await question(chalk.cyan('š¦ Choose template (node/react/python) or file path [node]: '));
const output = await question(chalk.cyan('š Output folder [./my-project]: '));
const boilerplateInput = await question(chalk.cyan('š§© Add boilerplate? (y/n) [y]: '));
const gitInitInput = await question(chalk.cyan('š Initialize Git? (y/n) [y]: '));
const installInput = await question(chalk.cyan('š¦ Auto-install dependencies? (y/n) [n]: '));
const templateValue = template.trim() || 'node';
const isBuiltInTemplate = ['node', 'react', 'python'].includes(templateValue);
const configData = {
...(isBuiltInTemplate ? { template: templateValue } : { input: templateValue }),
output: output.trim() || './my-project',
boilerplate: !boilerplateInput.trim() || boilerplateInput.toLowerCase().startsWith('y'),
gitInit: !gitInitInput.trim() || gitInitInput.toLowerCase().startsWith('y'),
install: installInput.toLowerCase().startsWith('y')
};
await fs.writeFile('treemk.config.json', JSON.stringify(configData, null, 2), 'utf8');
console.log(chalk.green("\nā
treemk.config.json created successfully!"));
console.log(chalk.cyan("\nConfig saved:"));
console.log(JSON.stringify(configData, null, 2));
rl.close();
if (autoRun) {
console.log(chalk.blue("\nš Running treemk with this config...\n"));
// Run main logic with the config
await runWithConfig(configData);
} else {
console.log(chalk.cyan("\nYou can now run:"));
console.log(chalk.yellow(" treemk\n"));
}
} catch (error) {
rl.close();
throw error;
}
}
async function runWithConfig(config) {
const options = {
output: config.output || './output',
boilerplate: config.boilerplate || false,
gitInit: config.gitInit || false,
install: config.install || false,
dryRun: false,
gitCommit: config.gitCommit || false,
gitPush: config.gitPush || false,
preview: false,
verbose: true,
};
let input = '';
// Handle built-in template
if (config.template) {
input = getBuiltInTemplate(config.template);
if (input) {
console.log(chalk.blue('ā¹') + ` Using built-in template: ${config.template}`);
} else {
console.error(chalk.red('ā') + ` Unknown built-in template: ${config.template}`);
console.log('Available templates: react, node, python');
process.exit(1);
}
}
// Handle file input
else if (config.input) {
const resolvedPath = await resolveInputPath(config.input);
if (!resolvedPath) {
console.error(chalk.red('ā') + ` Failed to find input file: ${config.input}`);
console.log(chalk.yellow('š” Tip: Make sure the file exists at the specified path'));
process.exit(1);
}
try {
console.log(chalk.blue('ā¹') + ` Reading from: ${resolvedPath}`);
input = await fs.readFile(resolvedPath, 'utf8');
} catch (error) {
console.error(chalk.red('ā') + ` Failed to read input file: ${error.message}`);
process.exit(1);
}
}
// If we have input, proceed with structure generation
if (input && input.trim()) {
try {
const generator = new StructureGenerator(options);
const paths = await generator.parseInput(input.trim());
if (paths.length === 0) {
console.error(chalk.red('ā') + ' No valid paths found in input');
process.exit(1);
}
await generator.createStructure(paths);
console.log('\n' + chalk.green('ā Success!') + ' Structure created at: ' + chalk.cyan(path.resolve(options.output)));
} catch (error) {
console.error(chalk.red('ā') + ` Error: ${error.message}`);
if (options.verbose) {
console.error(error.stack);
}
process.exit(1);
}
} else {
console.error(chalk.red('ā') + ' No valid input found');
console.log(chalk.yellow('š” Tip: Specify a valid template or input file in the config'));
process.exit(1);
}
}
async function main() {
const args = process.argv.slice(2);
// Parse arguments
const cliOptions = {
setup: false,
runAfterSetup: false,
input: null,
output: null,
boilerplate: false,
dryRun: false,
json: false,
gitInit: false,
gitCommit: false,
gitPush: false,
install: false,
preview: false,
help: false,
fromUrl: null,
template: null,
templateSave: null,
templateUse: null,
templateList: false,
templateRemove: null,
useConfig: true,
textInput: null,
jsonInput: null,
verbose: true,
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case '--setup':
cliOptions.setup = true;
break;
case '--run':
cliOptions.runAfterSetup = true;
break;
case '--input':
case '-i':
cliOptions.input = args[++i];
break;
case '--output':
case '-o':
cliOptions.output = args[++i];
break;
case '--boilerplate':
case '-b':
cliOptions.boilerplate = true;
break;
case '--dry-run':
case '-d':
cliOptions.dryRun = true;
break;
case '--json':
case '-j':
cliOptions.json = true;
break;
case '--git-init':
case '-g':
cliOptions.gitInit = true;
break;
case '--git-commit':
cliOptions.gitCommit = true;
break;
case '--git-push':
cliOptions.gitPush = true;
break;
case '--install':
cliOptions.install = true;
break;
case '--preview':
cliOptions.preview = true;
break;
case '--from-url':
case '-u':
cliOptions.fromUrl = args[++i];
break;
case '--template':
case '-t':
cliOptions.template = args[++i];
break;
case '--template-save':
cliOptions.templateSave = args[++i];
break;
case '--template-use':
cliOptions.templateUse = args[++i];
break;
case '--template-list':
cliOptions.templateList = true;
break;
case '--template-remove':
cliOptions.templateRemove = args[++i];
break;
case '--text':
cliOptions.textInput = args[++i];
break;
case '--json-input':
cliOptions.jsonInput = args[++i];
break;
case '--verbose':
cliOptions.verbose = true;
break;
case '--quiet':
cliOptions.verbose = false;
break;
case '--help':
case '-h':
cliOptions.help = true;
break;
default:
if (arg.startsWith('-')) {
console.warn(chalk.yellow('ā ') + ` Unknown option: ${arg}`);
} else if (!cliOptions.input) {
cliOptions.input = arg;
}
break;
}
}
// Handle setup flag (interactive config wizard)
if (cliOptions.setup) {
await setupDynamicConfig(cliOptions.runAfterSetup);
return;
}
// Handle help
if (cliOptions.help) {
printHelp();
return;
}
// Handle template list
if (cliOptions.templateList) {
await listTemplates();
return;
}
// Handle template remove
if (cliOptions.templateRemove) {
try {
await removeTemplate(cliOptions.templateRemove);
} catch (error) {
console.error(chalk.red('ā') + ` ${error.message}`);
process.exit(1);
}
return;
}
// Load config file (only from current working directory)
const configOptions = cliOptions.useConfig ? await loadConfig() : {};
// Merge options: config < cli (CLI takes precedence)
const options = mergeOptions(configOptions, cliOptions);
let input = '';
// Priority order for input sources:
// 1. Inline text argument (--text)
// 2. Inline JSON argument (--json-input)
// 3. Piped input (stdin)
// 4. URL fetch (--from-url)
// 5. Template use (--template-use)
// 6. File input (--input)
// 7. Built-in template (--template)
// Check for inline text input
if (options.textInput) {
input = options.textInput.replace(/\\n/g, '\n');
console.log(chalk.blue('ā¹') + ' Using inline text input');
}
// Check for inline JSON input
else if (options.jsonInput) {
try {
// Validate JSON
JSON.parse(options.jsonInput);
input = options.jsonInput;
console.log(chalk.blue('ā¹') + ' Using inline JSON input');
} catch (error) {
console.error(chalk.red('ā') + ` Invalid JSON input: ${error.message}`);
process.exit(1);
}
}
// Check for piped input (stdin)
else if (!process.stdin.isTTY) {
input = await readFromStdin();
if (input) {
console.log(chalk.blue('ā¹') + ' Reading from stdin (piped input)');
}
}
// Read from URL if specified
if (options.fromUrl && !input) {
try {
console.log(chalk.blue('ā¹') + ` Fetching from ${options.fromUrl}...`);
const response = await fetch(options.fromUrl);
input = await response.text();
console.log(chalk.green('ā') + ' Successfully fetched from URL');
} catch (error) {
console.error(chalk.red('ā') + ` Failed to fetch from URL: ${error.message}`);
process.exit(1);
}
}
// Read from template (user-saved)
if (options.templateUse && !input) {
try {
input = await loadTemplate(options.templateUse);
console.log(chalk.blue('ā¹') + ` Using saved template: ${options.templateUse}`);
} catch (error) {
console.error(chalk.red('ā') + ` ${error.message}`);
process.exit(1);
}
}
// Read from file if specified (with smart path resolution)
if (options.input && !input) {
const resolvedPath = await resolveInputPath(options.input);
if (!resolvedPath) {
console.error(chalk.red('ā') + ` Cannot find file: ${options.input}`);
console.log(chalk.yellow('š” Tip: File paths can be:'));
console.log(' - Absolute: /home/user/structure.txt');
console.log(' - Relative: ./structure.txt or ../structure.txt');
console.log(' - Or just filename if in common locations (Downloads, Documents, Desktop)');
process.exit(1);
}
try {
console.log(chalk.blue('ā¹') + ` Reading from: ${resolvedPath}`);
input = await fs.readFile(resolvedPath, 'utf8');
} catch (error) {
console.error(chalk.red('ā') + ` Failed to read input file: ${error.message}`);
process.exit(1);
}
}
// Use built-in template if specified
if (options.template && !input) {
input = getBuiltInTemplate(options.template);
if (!input) {
console.error(chalk.red('ā') + ` Unknown built-in template: ${options.template}`);
console.log('Available templates: react, node, python');
process.exit(1);
}
console.log(chalk.blue('ā¹') + ` Using built-in template: ${options.template}`);
}
// Handle template save - need input first
if (options.templateSave) {
if (!input) {
console.error(chalk.red('ā') + ' No input provided to save as template');
console.log('Use --input, --text, --json-input, or pipe data to save as template');
process.exit(1);
}
try {
await saveTemplate(options.templateSave, input);
// If template-save is the only action, exit
if (!options.output && !options.preview) {
console.log(chalk.green('ā') + ' Template saved successfully!');
console.log(chalk.blue('ā¹') + ` Use it with: treemk --template-use ${options.templateSave} -o ./myapp`);
return;
}
} catch (error) {
console.error(chalk.red('ā') + ` ${error.message}`);
process.exit(1);
}
}
if (!input || !input.trim()) {
console.error(chalk.red('ā') + ' No input provided.');
console.log('\n' + chalk.bold('Quick start options:'));
console.log(' 1. From file: treemk -i structure.txt -o ./app');
console.log(' 2. Inline text: treemk --text "src/index.js\\nREADME.md" -o ./app');
console.log(' 3. Inline JSON: treemk --json-input \'{"src":["index.js"]}\' -o ./app');
console.log(' 4. From pipe: cat structure.txt | treemk -o ./app');
console.log(' 5. Use template: treemk --template node -o ./app');
console.log(' 6. Interactive: treemk --setup --run');
console.log('\nRun ' + chalk.cyan('treemk --help') + ' for full documentation.');
process.exit(1);
}
// Create structure
const generator = new StructureGenerator({
output: options.output || './output',
boilerplate: options.boilerplate,
dryRun: options.dryRun,
gitInit: options.gitInit,
gitCommit: options.gitCommit,
gitPush: options.gitPush,
install: options.install,
preview: options.preview,
verbose: options.verbose,
});
try {
const paths = await generator.parseInput(input.trim());
if (paths.length === 0) {
console.error(chalk.red('ā') + ' No valid paths found in input');
process.exit(1);
}
await generator.createStructure(paths);
if (!options.preview && !options.dryRun) {
console.log('\n' + chalk.green('ā Success!') + ' Structure created at: ' + chalk.cyan(path.resolve(options.output || './output')));
}
} catch (error) {
console.error(chalk.red('ā') + ` Error: ${error.message}`);
if (options.verbose) {
console.error(error.stack);
}
process.exit(1);
}
}
main().catch(error => {
console.error(chalk.red('Fatal error:'), error);
process.exit(1);
});