peezy-cli
Version:
Production-ready CLI for scaffolding modern applications with curated full-stack templates, intelligent migrations, and enterprise security.
821 lines • 32.8 kB
JavaScript
import { Command } from "commander";
import { registry, getOrderedTemplates, isValidTemplate } from "./registry.js";
import { scaffold } from "./actions/scaffold.js";
import { installDeps, getRecommendedPackageManager, } from "./actions/install.js";
import { initGit } from "./actions/git.js";
import { log } from "./utils/logger.js";
import { validateVersions } from "./utils/version-check.js";
import { VersionMonitoringService } from "./plugins/version-scrubbing/services/version-monitoring.js";
import { loadConfig } from "./plugins/version-scrubbing/config/plugin-config.js";
import { createSuccessOutput, createErrorOutput, outputJson, OutputCapture, } from "./utils/json-output.js";
import { createVerifyCommand, createTrustCommand, createAuditCommand, } from "./commands/security.js";
import { migrateCommand } from "./commands/migrate.js";
import { addFiles } from "./commands/addfile.js";
const program = new Command();
program
.name("peezy")
.description("Initialize projects across runtimes — instantly")
.version("1.0.3");
/**
* List command - show all available templates
*/
program
.command("list")
.description("List available templates")
.option("--remote", "Include remote templates", false)
.option("--json", "Output in JSON format")
.action(async (options) => {
const outputCapture = new OutputCapture();
if (options.json) {
outputCapture.start();
}
try {
if (options.remote) {
const { getAllTemplates } = await import("./registry.js");
const { local, remote } = await getAllTemplates();
if (options.json) {
const { warnings, errors } = outputCapture.stop();
const localTemplates = local.map((key) => ({
name: key,
title: registry[key].title,
popular: registry[key].popular || false,
type: "local",
}));
const remoteTemplates = remote.map((template) => ({
name: template.name,
latest: template.latest,
versions: Object.keys(template.versions),
tags: template.versions[template.latest]?.tags || [],
type: "remote",
}));
const output = createSuccessOutput({
local: localTemplates,
remote: remoteTemplates,
}, warnings);
outputJson(output);
}
else {
log.info("Local templates:");
console.log();
local.forEach((key) => {
const template = registry[key];
const indicator = template.popular ? log.popular("") : " ";
const title = template.popular
? log.highlight(template.title)
: template.title;
console.log(`${indicator}${key.padEnd(20)} ${title}`);
});
if (remote.length > 0) {
console.log();
log.info("Remote templates:");
console.log();
remote.forEach((template) => {
const tags = template.versions[template.latest]?.tags?.join(", ") || "";
console.log(` ${template.name.padEnd(30)} ${template.latest.padEnd(10)} ${tags}`);
});
}
console.log();
log.info("Popular templates are marked with ⭐");
log.info("Use 'peezy add @org/template@version' to cache remote templates");
}
}
else {
const orderedTemplates = getOrderedTemplates();
if (options.json) {
const { warnings, errors } = outputCapture.stop();
const templates = orderedTemplates.map((key) => ({
name: key,
title: registry[key].title,
popular: registry[key].popular || false,
type: "local",
}));
const output = createSuccessOutput({ templates }, warnings);
outputJson(output);
}
else {
log.info("Available templates:");
console.log();
orderedTemplates.forEach((key) => {
const template = registry[key];
const indicator = template.popular ? log.popular("") : " ";
const title = template.popular
? log.highlight(template.title)
: template.title;
console.log(`${indicator}${key.padEnd(20)} ${title}`);
});
console.log();
log.info("Popular templates are marked with ⭐");
log.info("Use --remote to see remote templates");
}
}
}
catch (error) {
if (options.json) {
const { warnings } = outputCapture.stop();
const output = createErrorOutput([error instanceof Error ? error.message : String(error)], warnings);
outputJson(output);
}
else {
log.err(`Failed to list templates: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
});
/**
* New command - create a new project
*/
program
.command("new")
.argument("[template]", "Template to use")
.argument("[name]", "Project name")
.option("-p, --pm <pm>", "Package manager (bun|npm|pnpm|yarn)")
.option("--no-install", "Skip dependency installation")
.option("--no-git", "Skip git initialization")
.option("--databases <databases>", "Comma-separated list of databases (postgresql,mysql,sqlite,mongodb)")
.option("--redis", "Include Redis for caching/sessions")
.option("--search", "Include Elasticsearch for search functionality")
.option("--orm <orm>", "ORM to configure (prisma|drizzle|both)")
.option("--volumes <type>", "Volume configuration (preconfigured|custom)", "preconfigured")
.option("--json", "Output in JSON format")
.description("Create a new project from a template")
.action(async (templateArg, nameArg, opts) => {
const outputCapture = new OutputCapture();
if (opts?.json) {
outputCapture.start();
}
try {
// Validate version requirements
const versionCheck = validateVersions(templateArg);
// Show errors and exit if critical issues
if (!versionCheck.valid) {
versionCheck.errors.forEach((error) => log.err(error));
process.exit(1);
}
// Show warnings but continue
if (versionCheck.warnings.length > 0) {
versionCheck.warnings.forEach((warning) => log.warn(warning));
console.log(); // Add spacing
}
// Get ordered templates for prompts
const orderedTemplates = getOrderedTemplates();
// Interactive prompts for missing arguments (skip in JSON mode)
let answers = {};
if (!opts?.json) {
const { getEnhancedProjectConfig, showConfigSummary } = await import("./utils/enhanced-prompts.js");
answers = await getEnhancedProjectConfig(templateArg, nameArg, opts);
}
// Parse database options
const databases = opts?.databases
? opts.databases.split(",").map((db) => db.trim())
: answers.databases
? typeof answers.databases === "string"
? answers.databases.split(",").map((db) => db.trim())
: answers.databases
: undefined;
// Merge arguments and prompt answers
const config = {
template: templateArg ?? answers.template,
name: nameArg ?? answers.name,
pm: opts?.pm ?? answers.pm ?? "bun", // Default to bun in JSON mode
install: opts?.install ?? answers.install ?? true, // Default to true in JSON mode
git: opts?.git ?? answers.git ?? true, // Default to true in JSON mode
databases,
includeRedis: opts?.redis ?? answers.includeRedis,
includeSearch: opts?.search,
orm: opts?.orm ?? answers.orm,
volumes: opts?.volumes ?? answers.volumes,
json: opts?.json,
};
// Show configuration summary for interactive mode
if (!opts?.json && Object.keys(answers).length > 0) {
const { showConfigSummary } = await import("./utils/enhanced-prompts.js");
showConfigSummary(config);
}
// Validate required fields
if (!config.template || !config.name) {
log.err("Template and name are required. Try `peezy new` and follow the prompts.");
process.exit(1);
}
// Re-validate versions now that we know the template
if (!templateArg) {
const templateVersionCheck = validateVersions(config.template);
if (templateVersionCheck.warnings.length > 0) {
templateVersionCheck.warnings.forEach((warning) => log.warn(warning));
console.log(); // Add spacing
}
}
// Validate project name (same validation as in prompts)
if (config.name) {
const reserved = [
"node_modules",
"package.json",
"package-lock.json",
".git",
".env",
];
if (reserved.includes(config.name.toLowerCase())) {
log.err(`"${config.name}" is a reserved name and cannot be used`);
process.exit(1);
}
if (config.name.startsWith(".")) {
log.err("Project name cannot start with a dot");
process.exit(1);
}
if (!/^[a-zA-Z0-9_-]+$/.test(config.name)) {
log.err("Project name can only contain letters, numbers, hyphens, and underscores");
process.exit(1);
}
if (config.name.startsWith("-") || config.name.endsWith("-")) {
log.err("Project name cannot start or end with hyphens");
process.exit(1);
}
if (config.name.length > 214) {
log.err("Project name must be less than 214 characters");
process.exit(1);
}
}
// Validate template (allow remote templates to pass through)
const { isRemoteTemplate } = await import("./registry.js");
if (!isValidTemplate(config.template) &&
!isRemoteTemplate(config.template)) {
log.err(`Unknown template: "${config.template}"`);
console.log();
log.info("Available templates:");
getOrderedTemplates().forEach((key) => {
const template = registry[key];
const indicator = template.popular ? "⭐ " : " ";
console.log(`${indicator}${key} — ${template.title}`);
});
console.log();
log.info("Use `peezy list --remote` to see remote templates");
log.info("Use `peezy add @org/template@version` to add remote templates");
process.exit(1);
}
// Validate package manager if provided
if (config.pm && !["bun", "npm", "pnpm", "yarn"].includes(config.pm)) {
log.err(`Unknown package manager: "${config.pm}"`);
log.info("Supported package managers: bun, npm, pnpm, yarn");
process.exit(1);
}
// Start scaffolding
log.info(`Scaffolding ${log.highlight(config.template)} → ${log.highlight(config.name)}`);
const scaffoldResult = await scaffold(config.template, config.name, config);
log.ok(`Files created in ./${config.name}`);
// Install dependencies
if (config.install !== false) {
try {
const pm = config.pm ?? (await getRecommendedPackageManager());
log.info(`Installing dependencies with ${pm}...`);
await installDeps(pm, scaffoldResult.projectPath);
log.ok("Dependencies installed");
}
catch (error) {
log.warn(`Dependency installation failed: ${error instanceof Error ? error.message : String(error)}`);
log.info("You can install dependencies manually later");
}
}
// Initialize git
if (config.git !== false) {
try {
log.info("Initializing git repository...");
await initGit(scaffoldResult.projectPath);
log.ok("Git repository initialized");
}
catch (error) {
log.warn(`Git initialization failed: ${error instanceof Error ? error.message : String(error)}`);
log.info("You can initialize git manually later");
}
}
// Show educational next steps
if (!opts?.json) {
const { showEducationalNextSteps } = await import("./utils/enhanced-prompts.js");
showEducationalNextSteps(config);
}
else {
// Show basic next steps for JSON mode
console.log();
log.info("Next steps:");
console.log(` cd ${config.name}`);
const devCommand = config.pm === "bun"
? "bun run dev"
: config.pm === "yarn"
? "yarn dev"
: config.pm === "pnpm"
? "pnpm dev"
: "npm run dev";
console.log(` ${devCommand}`);
console.log();
}
// Handle JSON output
if (opts?.json) {
const { warnings, errors } = outputCapture.stop();
const devCommand = config.pm === "bun"
? "bun run dev"
: config.pm === "yarn"
? "yarn dev"
: config.pm === "pnpm"
? "pnpm dev"
: "npm run dev";
const output = createSuccessOutput({
project: {
name: config.name,
path: scaffoldResult.projectPath,
template: scaffoldResult.templateInfo,
},
options: config,
nextSteps: [`cd ${config.name}`, devCommand],
}, warnings);
outputJson(output);
}
}
catch (error) {
if (opts?.json) {
const { warnings } = outputCapture.stop();
const output = createErrorOutput([error instanceof Error ? error.message : String(error)], warnings);
outputJson(output);
}
else {
log.err(`Failed to create project: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
});
/**
* Version checking command
*/
program
.command("doctor")
.description("Environment & project health checks")
.option("--fix-lint", "Attempt to autofix lint issues", false)
.option("--fix-env-examples", "Autofix .env.example issues", false)
.option("--ports <ports>", "Comma-separated list of ports to check", "3000,5173,8000")
.option("--json", "Output in JSON format")
.action(async (options) => {
const outputCapture = new OutputCapture();
if (options.json) {
outputCapture.start();
}
try {
const { doctor } = await import("./commands/doctor.js");
const ports = String(options.ports)
.split(",")
.map((s) => parseInt(s.trim(), 10))
.filter((n) => !Number.isNaN(n));
const result = await doctor({
fixLint: !!options.fixLint,
fixEnvExamples: !!options.fixEnvExamples,
ports,
json: !!options.json,
});
if (options.json) {
const { warnings, errors } = outputCapture.stop();
if (typeof result === "number") {
// Legacy return format
const output = result === 0
? createSuccessOutput({ healthy: true }, warnings)
: createErrorOutput(["Health checks failed"], warnings);
outputJson(output);
}
else {
// New structured format
const output = result.ok
? createSuccessOutput(result, warnings)
: createErrorOutput(result.errors, warnings.concat(result.warnings));
outputJson(output);
}
}
else {
const code = typeof result === "number" ? result : result.ok ? 0 : 1;
process.exit(code);
}
}
catch (error) {
if (options.json) {
const { warnings } = outputCapture.stop();
const output = createErrorOutput([error instanceof Error ? error.message : String(error)], warnings);
outputJson(output);
}
else {
log.err(`Doctor failed: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
});
program
.command("env")
.description("Typed env management: check, diff, generate, pull:railway, push:railway")
.argument("<subcommand>")
.option("--schema <path>", "Path to env schema JSON (required/optional keys)")
.option("--json", "Output in JSON format")
.action(async (subcommand, options) => {
const outputCapture = new OutputCapture();
if (options.json) {
outputCapture.start();
}
try {
const { runEnv } = await import("./commands/env.js");
const result = await runEnv(subcommand, {
schema: options.schema,
json: options.json,
});
if (options.json) {
const { warnings, errors } = outputCapture.stop();
const output = createSuccessOutput(result, warnings);
outputJson(output);
}
}
catch (error) {
if (options.json) {
const { warnings } = outputCapture.stop();
const output = createErrorOutput([error instanceof Error ? error.message : String(error)], warnings);
outputJson(output);
}
else {
log.err(`Env command failed: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
});
program
.command("readme")
.description("Generate README and/or CHANGELOG with badges")
.option("--name <name>")
.option("--no-badges")
.option("--changelog", "Also generate CHANGELOG.md", false)
.option("--json", "Output in JSON format")
.action(async (options) => {
const outputCapture = new OutputCapture();
if (options.json) {
outputCapture.start();
}
try {
const { generateReadme, generateChangelog } = await import("./commands/readme-changelog.js");
const readmeResult = await generateReadme({
name: options.name,
badges: options.badges,
json: options.json,
});
let changelogResult;
if (options.changelog) {
changelogResult = await generateChangelog({ json: options.json });
}
if (options.json) {
const { warnings, errors } = outputCapture.stop();
const output = createSuccessOutput({
readme: readmeResult,
changelog: changelogResult,
}, warnings);
outputJson(output);
}
}
catch (error) {
if (options.json) {
const { warnings } = outputCapture.stop();
const output = createErrorOutput([error instanceof Error ? error.message : String(error)], warnings);
outputJson(output);
}
else {
log.err(`README generation failed: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
});
program
.command("upgrade")
.description("Check for updates to templates/plugins and preview diffs")
.option("--dry-run", "Preview changes only", false)
.option("--json", "Output in JSON format")
.action(async (options) => {
const outputCapture = new OutputCapture();
if (options.json) {
outputCapture.start();
}
try {
const { upgrade } = await import("./commands/upgrade.js");
const result = await upgrade({
dryRun: !!options.dryRun,
json: options.json,
});
if (options.json) {
const { warnings, errors } = outputCapture.stop();
const output = createSuccessOutput(result, warnings);
outputJson(output);
}
}
catch (error) {
if (options.json) {
const { warnings } = outputCapture.stop();
const output = createErrorOutput([error instanceof Error ? error.message : String(error)], warnings);
outputJson(output);
}
else {
log.err(`Upgrade failed: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
});
/**
* Add command - add remote templates to cache
*/
program
.command("add")
.argument("<template>", "Template to add (@org/template@version)")
.option("--force", "Force re-download if already cached", false)
.option("--version <version>", "Specific version to download")
.option("--json", "Output in JSON format")
.description("Add a remote template to the local cache")
.action(async (templateName, options) => {
const outputCapture = new OutputCapture();
if (options.json) {
outputCapture.start();
}
try {
const { addTemplate } = await import("./commands/add.js");
await addTemplate(templateName, {
force: options.force,
version: options.version,
});
if (options.json) {
const { warnings, errors } = outputCapture.stop();
const output = createSuccessOutput({
template: templateName,
cached: true,
}, warnings);
outputJson(output);
}
}
catch (error) {
if (options.json) {
const { warnings } = outputCapture.stop();
const output = createErrorOutput([error instanceof Error ? error.message : String(error)], warnings);
outputJson(output);
}
else {
log.err(`Failed to add template: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
});
/**
* Cache command - manage template cache
*/
program
.command("cache")
.description("Manage template cache")
.argument("[action]", "Action to perform (list, clear)", "list")
.option("--json", "Output in JSON format")
.action(async (action, options) => {
const outputCapture = new OutputCapture();
if (options.json) {
outputCapture.start();
}
try {
const { listCachedTemplates, clearCache, getCacheInfo } = await import("./commands/add.js");
switch (action) {
case "list":
if (options.json) {
const cacheInfo = await getCacheInfo();
const { warnings, errors } = outputCapture.stop();
const output = createSuccessOutput(cacheInfo, warnings);
outputJson(output);
}
else {
await listCachedTemplates();
}
break;
case "clear":
await clearCache();
if (options.json) {
const { warnings, errors } = outputCapture.stop();
const output = createSuccessOutput({ cleared: true }, warnings);
outputJson(output);
}
break;
default:
if (options.json) {
const { warnings } = outputCapture.stop();
const output = createErrorOutput([`Unknown cache action: ${action}`], warnings);
outputJson(output);
}
else {
log.err(`Unknown cache action: ${action}`);
log.info("Available actions: list, clear");
process.exit(1);
}
}
}
catch (error) {
if (options.json) {
const { warnings } = outputCapture.stop();
const output = createErrorOutput([error instanceof Error ? error.message : String(error)], warnings);
outputJson(output);
}
else {
log.err(`Cache operation failed: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
});
/**
* Reproduce command - recreate project from lock file
*/
program
.command("reproduce")
.argument("<name>", "Name for the reproduced project")
.option("--lock-file <path>", "Path to peezy.lock.json file")
.option("--verify", "Verify checksums after reproduction", false)
.option("--json", "Output in JSON format")
.description("Reproduce a project from peezy.lock.json")
.action(async (projectName, options) => {
const outputCapture = new OutputCapture();
if (options.json) {
outputCapture.start();
}
try {
const { reproduceProject } = await import("./commands/reproduce.js");
const result = await reproduceProject(projectName, {
lockFile: options.lockFile,
verify: options.verify,
json: options.json,
});
if (options.json) {
const { warnings, errors } = outputCapture.stop();
const output = createSuccessOutput({
project: projectName,
reproduced: result.success,
lockFile: result.lockFile,
verification: result.verification,
}, warnings);
outputJson(output);
}
}
catch (error) {
if (options.json) {
const { warnings } = outputCapture.stop();
const output = createErrorOutput([error instanceof Error ? error.message : String(error)], warnings);
outputJson(output);
}
else {
log.err(`Failed to reproduce project: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
});
/**
* Verify command - check project against lock file
*/
program
.command("verify")
.option("--project-path <path>", "Path to project directory", process.cwd())
.option("--json", "Output in JSON format")
.description("Verify project matches its peezy.lock.json")
.action(async (options) => {
const outputCapture = new OutputCapture();
if (options.json) {
outputCapture.start();
}
try {
const { verifyProject } = await import("./commands/reproduce.js");
const result = await verifyProject(options.projectPath);
if (options.json) {
const { warnings, errors } = outputCapture.stop();
const output = createSuccessOutput(result, warnings);
outputJson(output);
}
}
catch (error) {
if (options.json) {
const { warnings } = outputCapture.stop();
const output = createErrorOutput([error instanceof Error ? error.message : String(error)], warnings);
outputJson(output);
}
else {
log.err(`Failed to verify project: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
});
program
.command("check-versions")
.description("Check for latest versions of runtimes, frameworks, and dependencies")
.option("-f, --format <format>", "Output format (json, markdown, console)", "console")
.option("-t, --technologies <technologies>", "Comma-separated list of technologies to check")
.option("--include-prerelease", "Include prerelease versions", false)
.option("--security-only", "Only show security-related updates", false)
.option("--json", "Output in standardized JSON format (overrides --format)")
.action(async (options) => {
const outputCapture = new OutputCapture();
if (options.json) {
outputCapture.start();
}
try {
const config = loadConfig();
const versionService = new VersionMonitoringService(config);
const technologies = options.technologies
? options.technologies.split(",").map((t) => t.trim())
: undefined;
if (options.securityOnly) {
const advisories = await versionService.getSecurityAdvisories(technologies || config.monitoring.runtimes, "medium");
if (options.json) {
const { warnings, errors } = outputCapture.stop();
const output = createSuccessOutput({
type: "security-advisories",
advisories,
}, warnings);
outputJson(output);
}
else if (options.format === "json") {
console.log(JSON.stringify(advisories, null, 2));
}
else {
console.log("🔒 Security Advisories");
console.log("=".repeat(30));
for (const [tech, techAdvisories] of Object.entries(advisories)) {
if (techAdvisories.length > 0) {
console.log(`\n${tech}:`);
techAdvisories.forEach((advisory) => {
console.log(` ⚠️ ${advisory.severity.toUpperCase()}: ${advisory.description}`);
});
}
}
}
}
else {
const report = await versionService.generateReport(options.json
? "json"
: options.format, true);
if (options.json) {
const { warnings, errors } = outputCapture.stop();
const output = createSuccessOutput({
type: "version-report",
report: options.format === "json" ? JSON.parse(report) : report,
}, warnings);
outputJson(output);
}
else {
console.log(report);
}
}
}
catch (error) {
if (options.json) {
const { warnings } = outputCapture.stop();
const output = createErrorOutput([error instanceof Error ? error.message : String(error)], warnings);
outputJson(output);
}
else {
log.err(`Failed to check versions: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
});
// Add security commands
program.addCommand(createVerifyCommand().name("verify-template"));
program.addCommand(createTrustCommand());
program.addCommand(createAuditCommand());
// Add migration command
program.addCommand(migrateCommand);
/**
* Add File command - intelligently add configuration files to existing projects
*/
program
.command("addfile")
.alias("add-file")
.description("Add configuration files to your existing project")
.option("--json", "Output in JSON format")
.option("--force", "Overwrite existing files")
.option("--category <category>", "Filter by file category")
.action(async (options) => {
const outputCapture = new OutputCapture();
if (options.json) {
outputCapture.start();
}
try {
await addFiles(process.cwd(), {
json: options.json,
force: options.force,
category: options.category,
});
if (options.json) {
const { warnings, errors } = outputCapture.stop();
// Success output is handled within addFiles function
}
}
catch (error) {
if (options.json) {
const { warnings } = outputCapture.stop();
const output = createErrorOutput([error instanceof Error ? error.message : String(error)], warnings);
outputJson(output);
}
else {
log.err(`Failed to add files: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
});
// Parse command line arguments
await program.parseAsync(process.argv).catch((error) => {
log.err(`CLI error: ${error.message}`);
process.exit(1);
});
//# sourceMappingURL=index.js.map