astro-template/init-template.js
Nicholai d384abea33 feat: add initial scaffolding, configuration, and documentation
- Create .gitignore, .env.example, README.md, LICENSE
- Add astro.config.mjs and template.config.json
- Scaffold .vscode extensions, launch, and settings
- Add CLAUDE.md with development commands and guidelines
- Include public assets, fonts, and media files
- Add src components, layouts, pages, and utils
- Add cursor rules and worktrees configuration
- Add package.json, pnpm-lock.yaml, and tsconfig.json
2025-12-27 04:37:55 -07:00

261 lines
9.2 KiB
JavaScript

#!/usr/bin/env node
/**
* Template Initialization Script
*
* This script personalizes the Astro portfolio template with your information.
*
* Usage:
* node init-template.js # Interactive mode (prompts for info)
* node init-template.js --config # Config mode (reads template.config.json)
* node init-template.js --help # Show help
*/
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import readline from 'readline';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// ANSI color codes for better terminal output
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
dim: '\x1b[2m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
cyan: '\x1b[36m',
red: '\x1b[31m'
};
function log(message, color = 'reset') {
console.log(`${colors[color]}${message}${colors.reset}`);
}
function prompt(question) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => {
rl.question(`${colors.cyan}${question}${colors.reset} `, (answer) => {
rl.close();
resolve(answer.trim());
});
});
}
async function loadConfig() {
try {
const configPath = path.join(__dirname, 'template.config.json');
const configData = await fs.readFile(configPath, 'utf-8');
return JSON.parse(configData);
} catch (error) {
log('Warning: Could not load template.config.json', 'yellow');
return null;
}
}
async function interactiveMode() {
log('\n=== Astro Portfolio Template Setup ===\n', 'bright');
log('This wizard will help you personalize your template.\n', 'dim');
const config = {
site: {},
branding: {},
personal: {},
social: {},
seo: {},
cloudflare: {}
};
// Site information
log('\n--- Site Information ---\n', 'blue');
config.personal.fullName = await prompt('Your full name:') || 'Your Name';
config.personal.firstName = config.personal.fullName.split(' ')[0];
config.personal.lastName = config.personal.fullName.split(' ').slice(1).join(' ') || 'Name';
config.personal.jobTitle = await prompt('Your profession/job title:') || 'Your Profession';
config.site.title = `${config.personal.fullName}${config.personal.jobTitle}`;
config.site.description = await prompt('Brief description of what you do:') || 'Professional portfolio and blog';
config.site.url = await prompt('Your site URL (e.g., https://example.com):') || 'https://yoursite.com';
config.site.author = config.personal.fullName;
// Branding
log('\n--- Branding ---\n', 'blue');
const defaultInitials = config.personal.firstName[0] + (config.personal.lastName[0] || 'N');
config.branding.initials = await prompt(`Your initials (default: ${defaultInitials}):`) || defaultInitials;
config.branding.year = await prompt('Portfolio year (default: 2025):') || '2025';
config.branding.backgroundText = await prompt(`Large background text (default: ${config.personal.lastName.toUpperCase()}):`) || config.personal.lastName.toUpperCase();
config.branding.companyName = await prompt('Company name (optional):') || 'Your Company';
// Contact information
log('\n--- Contact Information ---\n', 'blue');
config.personal.email = await prompt('Email address:') || 'your@email.com';
config.personal.location = await prompt('Location (e.g., San Francisco, CA):') || 'Your City, State';
config.personal.locationCountry = await prompt('Country:') || 'Your Country';
config.personal.bio = await prompt('Professional bio (1-2 sentences):') || 'Professional bio here.';
// Social links
log('\n--- Social Links ---\n', 'blue');
config.social.linkedin = await prompt('LinkedIn URL (full URL):') || 'https://linkedin.com/in/yourprofile';
config.social.github = await prompt('GitHub URL:') || 'https://github.com/yourusername';
config.social.twitter = await prompt('Twitter URL:') || 'https://twitter.com/yourhandle';
const twitterHandle = config.social.twitter.split('/').pop();
config.social.twitterHandle = twitterHandle.startsWith('@') ? twitterHandle : `@${twitterHandle}`;
config.social.website = config.site.url;
// Cloudflare
log('\n--- Deployment ---\n', 'blue');
const defaultProjectName = config.site.url.replace(/https?:\/\//, '').replace(/[^a-z0-9-]/gi, '-').toLowerCase();
config.cloudflare.projectName = await prompt(`Cloudflare project name (default: ${defaultProjectName}):`) || defaultProjectName;
// SEO
config.seo.keywords = ['Portfolio', 'Blog', config.personal.jobTitle];
config.seo.serviceTypes = ['Service 1', 'Service 2', 'Service 3'];
config.seo.companyUrl = config.branding.companyName !== 'Your Company'
? await prompt('Company website URL (optional):')
: 'https://example.com';
return config;
}
async function applyConfig(config) {
log('\n🔧 Applying configuration...\n', 'bright');
const replacements = {
// Site-wide
'Your Name — Your Profession': config.site.title,
'Your Name': config.personal.fullName,
'Your Profession': config.personal.jobTitle,
'Your professional description here. Describe what you do, who you work with, and what makes you unique.': config.site.description,
'your professional description': config.site.description,
'https://yoursite.com': config.site.url,
'your@email.com': config.personal.email,
// Branding
'YN / 2025': `${config.branding.initials} / ${config.branding.year}`,
'NAME': config.branding.backgroundText,
// Personal
'Your': config.personal.firstName,
'Your City, State': config.personal.location,
'Your Country': config.personal.locationCountry,
'Your Company': config.branding.companyName,
// Social
'https://linkedin.com/in/yourprofile': config.social.linkedin,
'https://github.com/yourusername': config.social.github,
'https://twitter.com/yourhandle': config.social.twitter,
'@yourhandle': config.social.twitterHandle,
// Cloudflare
'astro-portfolio-template': config.cloudflare.projectName
};
const filesToUpdate = [
'src/consts.ts',
'src/components/BaseHead.astro',
'src/components/Navigation.astro',
'src/components/Footer.astro',
'src/content/sections/hero.mdx',
'src/content/sections/experience.mdx',
'src/content/pages/contact.mdx',
'astro.config.mjs',
'wrangler.jsonc',
'package.json'
];
for (const file of filesToUpdate) {
try {
const filePath = path.join(__dirname, file);
let content = await fs.readFile(filePath, 'utf-8');
for (const [search, replace] of Object.entries(replacements)) {
content = content.replaceAll(search, replace);
}
await fs.writeFile(filePath, content, 'utf-8');
log(`✓ Updated ${file}`, 'green');
} catch (error) {
log(`✗ Failed to update ${file}: ${error.message}`, 'red');
}
}
// Save config for reference
try {
const configPath = path.join(__dirname, 'template.config.json');
await fs.writeFile(
configPath,
JSON.stringify(config, null, 2),
'utf-8'
);
log('✓ Saved configuration to template.config.json', 'green');
} catch (error) {
log(`✗ Failed to save config: ${error.message}`, 'red');
}
log('\n✨ Template personalization complete!\n', 'bright');
log('Next steps:', 'cyan');
log(' 1. Review the changes made to your files', 'dim');
log(' 2. Replace placeholder images in src/assets/ and public/media/', 'dim');
log(' 3. Update content in src/content/sections/ with your info', 'dim');
log(' 4. Run `pnpm dev` to preview your site', 'dim');
log(' 5. Run `pnpm deploy` when ready to publish\n', 'dim');
}
async function showHelp() {
log('\n=== Astro Portfolio Template Setup ===\n', 'bright');
log('Usage:', 'cyan');
log(' node init-template.js Interactive mode (prompts for info)', 'dim');
log(' node init-template.js --config Config mode (reads template.config.json)', 'dim');
log(' node init-template.js --help Show this help message\n', 'dim');
log('Interactive mode will ask you questions and personalize the template.', 'dim');
log('Config mode reads from template.config.json file.\n', 'dim');
}
async function main() {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
await showHelp();
return;
}
if (args.includes('--config')) {
log('\n📄 Running in config mode...\n', 'bright');
const config = await loadConfig();
if (!config) {
log('Error: template.config.json not found or invalid.', 'red');
log('Create one or run without --config flag for interactive mode.\n', 'yellow');
process.exit(1);
}
await applyConfig(config);
} else {
// Interactive mode
const config = await interactiveMode();
log('\n📋 Configuration summary:', 'yellow');
console.log(JSON.stringify(config, null, 2));
const confirm = await prompt('\nApply this configuration? (yes/no):');
if (confirm.toLowerCase() === 'yes' || confirm.toLowerCase() === 'y') {
await applyConfig(config);
} else {
log('\nSetup cancelled. No changes were made.\n', 'yellow');
}
}
}
main().catch((error) => {
log(`\n❌ Error: ${error.message}\n`, 'red');
console.error(error);
process.exit(1);
});