mirror of
https://github.com/waynesutton/markdown-site.git
synced 2026-01-12 04:09:14 +00:00
new: npx create-markdown-sync CLI , ui , related post thumbnails features
This commit is contained in:
66
packages/create-markdown-sync/README.md
Normal file
66
packages/create-markdown-sync/README.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# create-markdown-sync
|
||||
|
||||
Create a markdown-sync site with a single command.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
npx create-markdown-sync my-site
|
||||
```
|
||||
|
||||
This interactive CLI will:
|
||||
|
||||
1. Clone the markdown-sync framework
|
||||
2. Walk through configuration (site name, URL, features, etc.)
|
||||
3. Install dependencies
|
||||
4. Set up Convex backend
|
||||
5. Run initial content sync
|
||||
6. Open your site in the browser
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Create a new project
|
||||
npx create-markdown-sync my-blog
|
||||
|
||||
# With specific package manager
|
||||
npx create-markdown-sync my-blog --pm yarn
|
||||
|
||||
# Skip Convex setup (configure later)
|
||||
npx create-markdown-sync my-blog --skip-convex
|
||||
```
|
||||
|
||||
## What You Get
|
||||
|
||||
A fully configured markdown-sync site with:
|
||||
|
||||
- Real-time content sync via Convex
|
||||
- Markdown-based blog posts and pages
|
||||
- Full-text and semantic search
|
||||
- RSS feeds and sitemap
|
||||
- AI integrations (Claude, GPT-4, Gemini)
|
||||
- Newsletter subscriptions (via AgentMail)
|
||||
- MCP server for AI tool integration
|
||||
- Dashboard for content management
|
||||
- Deploy-ready for Netlify
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 18 or higher
|
||||
- npm, yarn, pnpm, or bun
|
||||
|
||||
## After Setup
|
||||
|
||||
```bash
|
||||
cd my-site
|
||||
npm run dev # Start dev server at localhost:5173
|
||||
npm run sync # Sync content changes to Convex
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
Full documentation at [markdown.fast/docs](https://www.markdown.fast/docs)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
54
packages/create-markdown-sync/package.json
Normal file
54
packages/create-markdown-sync/package.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "create-markdown-sync",
|
||||
"version": "0.1.0",
|
||||
"description": "Create a markdown-sync site with a single command",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"create-markdown-sync": "dist/index.js"
|
||||
},
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"keywords": [
|
||||
"markdown",
|
||||
"blog",
|
||||
"convex",
|
||||
"cli",
|
||||
"scaffolding",
|
||||
"create",
|
||||
"static-site",
|
||||
"cms"
|
||||
],
|
||||
"author": "Wayne Sutton",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/waynesutton/markdown-site"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/waynesutton/markdown-site/issues"
|
||||
},
|
||||
"homepage": "https://github.com/waynesutton/markdown-site#readme",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"execa": "^8.0.1",
|
||||
"giget": "^1.2.3",
|
||||
"kleur": "^4.1.5",
|
||||
"open": "^10.1.0",
|
||||
"ora": "^8.0.1",
|
||||
"prompts": "^2.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
76
packages/create-markdown-sync/src/clone.ts
Normal file
76
packages/create-markdown-sync/src/clone.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { downloadTemplate } from 'giget';
|
||||
import { existsSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
import ora from 'ora';
|
||||
import { log } from './utils.js';
|
||||
|
||||
const TEMPLATE_REPO = 'github:waynesutton/markdown-site';
|
||||
|
||||
export interface CloneOptions {
|
||||
projectName: string;
|
||||
cwd?: string;
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
export async function cloneTemplate(options: CloneOptions): Promise<string> {
|
||||
const { projectName, cwd = process.cwd(), force = false } = options;
|
||||
const targetDir = resolve(cwd, projectName);
|
||||
|
||||
// Check if directory already exists
|
||||
if (existsSync(targetDir) && !force) {
|
||||
throw new Error(
|
||||
`Directory "${projectName}" already exists. Use --force to overwrite.`
|
||||
);
|
||||
}
|
||||
|
||||
const spinner = ora('Cloning markdown-sync template...').start();
|
||||
|
||||
try {
|
||||
await downloadTemplate(TEMPLATE_REPO, {
|
||||
dir: targetDir,
|
||||
force,
|
||||
preferOffline: false,
|
||||
});
|
||||
|
||||
spinner.succeed('Template cloned successfully');
|
||||
return targetDir;
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to clone template');
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('404')) {
|
||||
log.error('Template repository not found. Please check the repository URL.');
|
||||
} else if (error.message.includes('network')) {
|
||||
log.error('Network error. Please check your internet connection.');
|
||||
} else {
|
||||
log.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove files that shouldn't be in the cloned project
|
||||
export async function cleanupClonedFiles(projectDir: string): Promise<void> {
|
||||
const { rm } = await import('fs/promises');
|
||||
const { join } = await import('path');
|
||||
|
||||
const filesToRemove = [
|
||||
// Remove CLI package from cloned repo (it's installed via npm)
|
||||
'packages',
|
||||
// Remove any existing fork-config.json (will be regenerated)
|
||||
'fork-config.json',
|
||||
// Remove git history for fresh start
|
||||
'.git',
|
||||
];
|
||||
|
||||
for (const file of filesToRemove) {
|
||||
const filePath = join(projectDir, file);
|
||||
try {
|
||||
await rm(filePath, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Ignore if file doesn't exist
|
||||
}
|
||||
}
|
||||
}
|
||||
302
packages/create-markdown-sync/src/configure.ts
Normal file
302
packages/create-markdown-sync/src/configure.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { execa } from 'execa';
|
||||
import ora from 'ora';
|
||||
import type { WizardAnswers } from './wizard.js';
|
||||
import { log, extractDomain } from './utils.js';
|
||||
|
||||
// Fix template siteConfig.ts to have clean values before running configure-fork.ts
|
||||
// This is needed because the template may have values with embedded quotes that break regex
|
||||
function fixTemplateSiteConfig(projectDir: string): void {
|
||||
const siteConfigPath = join(projectDir, 'src/config/siteConfig.ts');
|
||||
let content = readFileSync(siteConfigPath, 'utf-8');
|
||||
|
||||
// Fix name field - replace any problematic value with a clean placeholder
|
||||
// Match: name: 'anything' or name: "anything" (properly handling escaped quotes)
|
||||
content = content.replace(
|
||||
/name: ['"].*?["'](?:.*?['"])?,/,
|
||||
'name: "markdown-sync",'
|
||||
);
|
||||
|
||||
// Also try a more aggressive fix for malformed values
|
||||
content = content.replace(
|
||||
/name: '.*?framework',/,
|
||||
'name: "markdown-sync",'
|
||||
);
|
||||
|
||||
writeFileSync(siteConfigPath, content, 'utf-8');
|
||||
}
|
||||
|
||||
// Create empty auth config when user doesn't need authentication
|
||||
// This prevents WorkOS env var errors from blocking Convex setup
|
||||
function disableAuthConfig(projectDir: string): void {
|
||||
const authConfigPath = join(projectDir, 'convex/auth.config.ts');
|
||||
|
||||
// Replace with empty auth config (no providers)
|
||||
const emptyAuthConfig = `// Auth configuration (WorkOS disabled)
|
||||
// To enable WorkOS authentication, see: https://docs.convex.dev/auth/authkit/
|
||||
//
|
||||
// 1. Create a WorkOS account at https://workos.com
|
||||
// 2. Set WORKOS_CLIENT_ID in your Convex dashboard environment variables
|
||||
// 3. Replace this file with the WorkOS auth config
|
||||
|
||||
const authConfig = {
|
||||
providers: [],
|
||||
};
|
||||
|
||||
export default authConfig;
|
||||
`;
|
||||
|
||||
writeFileSync(authConfigPath, emptyAuthConfig, 'utf-8');
|
||||
}
|
||||
|
||||
// Convert wizard answers to fork-config.json structure
|
||||
function buildForkConfig(answers: WizardAnswers): Record<string, unknown> {
|
||||
return {
|
||||
siteName: answers.siteName,
|
||||
siteTitle: answers.siteTitle,
|
||||
siteDescription: answers.siteDescription,
|
||||
siteUrl: answers.siteUrl,
|
||||
siteDomain: extractDomain(answers.siteUrl),
|
||||
githubUsername: answers.githubUsername,
|
||||
githubRepo: answers.githubRepo,
|
||||
contactEmail: answers.contactEmail,
|
||||
creator: {
|
||||
name: answers.creatorName,
|
||||
twitter: answers.twitter,
|
||||
linkedin: answers.linkedin,
|
||||
github: answers.github,
|
||||
},
|
||||
bio: answers.bio,
|
||||
gitHubRepoConfig: {
|
||||
owner: answers.githubUsername,
|
||||
repo: answers.githubRepo,
|
||||
branch: answers.branch,
|
||||
contentPath: answers.contentPath,
|
||||
},
|
||||
logoGallery: {
|
||||
enabled: answers.logoGalleryEnabled,
|
||||
title: 'Built with',
|
||||
scrolling: answers.logoGalleryScrolling,
|
||||
maxItems: 4,
|
||||
},
|
||||
gitHubContributions: {
|
||||
enabled: answers.githubContributionsEnabled,
|
||||
username: answers.githubContributionsUsername,
|
||||
showYearNavigation: true,
|
||||
linkToProfile: true,
|
||||
title: 'GitHub Activity',
|
||||
},
|
||||
visitorMap: {
|
||||
enabled: answers.visitorMapEnabled,
|
||||
title: 'Live Visitors',
|
||||
},
|
||||
blogPage: {
|
||||
enabled: answers.blogPageEnabled,
|
||||
showInNav: true,
|
||||
title: answers.blogPageTitle,
|
||||
description: 'All posts from the blog, sorted by date.',
|
||||
order: 2,
|
||||
},
|
||||
postsDisplay: {
|
||||
showOnHome: answers.showPostsOnHome,
|
||||
showOnBlogPage: true,
|
||||
homePostsLimit: answers.homePostsLimit || undefined,
|
||||
homePostsReadMore: answers.homePostsReadMoreEnabled
|
||||
? {
|
||||
enabled: true,
|
||||
text: answers.homePostsReadMoreText,
|
||||
link: answers.homePostsReadMoreLink,
|
||||
}
|
||||
: {
|
||||
enabled: false,
|
||||
text: 'Read more blog posts',
|
||||
link: '/blog',
|
||||
},
|
||||
},
|
||||
featuredViewMode: answers.featuredViewMode,
|
||||
showViewToggle: answers.showViewToggle,
|
||||
theme: answers.theme,
|
||||
fontFamily: answers.fontFamily,
|
||||
homepage: {
|
||||
type: answers.homepageType,
|
||||
slug: null,
|
||||
originalHomeRoute: '/home',
|
||||
},
|
||||
rightSidebar: {
|
||||
enabled: true,
|
||||
minWidth: 1135,
|
||||
},
|
||||
footer: {
|
||||
enabled: answers.footerEnabled,
|
||||
showOnHomepage: true,
|
||||
showOnPosts: true,
|
||||
showOnPages: true,
|
||||
showOnBlogPage: true,
|
||||
defaultContent: answers.footerDefaultContent,
|
||||
},
|
||||
socialFooter: {
|
||||
enabled: answers.socialFooterEnabled,
|
||||
showOnHomepage: true,
|
||||
showOnPosts: true,
|
||||
showOnPages: true,
|
||||
showOnBlogPage: true,
|
||||
showInHeader: answers.socialFooterShowInHeader,
|
||||
socialLinks: [
|
||||
{
|
||||
platform: 'github',
|
||||
url: `https://github.com/${answers.githubUsername}/${answers.githubRepo}`,
|
||||
},
|
||||
{
|
||||
platform: 'twitter',
|
||||
url: answers.twitter,
|
||||
},
|
||||
{
|
||||
platform: 'linkedin',
|
||||
url: answers.linkedin,
|
||||
},
|
||||
],
|
||||
copyright: {
|
||||
siteName: answers.copyrightSiteName,
|
||||
showYear: true,
|
||||
},
|
||||
},
|
||||
aiChat: {
|
||||
enabledOnWritePage: false,
|
||||
enabledOnContent: false,
|
||||
},
|
||||
aiDashboard: {
|
||||
enableImageGeneration: true,
|
||||
defaultTextModel: 'claude-sonnet-4-20250514',
|
||||
textModels: [
|
||||
{ id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', provider: 'anthropic' },
|
||||
{ id: 'gpt-4o', name: 'GPT-4o', provider: 'openai' },
|
||||
{ id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash', provider: 'google' },
|
||||
],
|
||||
imageModels: [
|
||||
{ id: 'gemini-2.0-flash-exp-image-generation', name: 'Nano Banana', provider: 'google' },
|
||||
{ id: 'imagen-3.0-generate-002', name: 'Nano Banana Pro', provider: 'google' },
|
||||
],
|
||||
},
|
||||
newsletter: {
|
||||
enabled: answers.newsletterEnabled,
|
||||
agentmail: {
|
||||
inbox: 'newsletter@mail.agentmail.to',
|
||||
},
|
||||
signup: {
|
||||
home: {
|
||||
enabled: answers.newsletterHomeEnabled,
|
||||
position: 'above-footer',
|
||||
title: 'Stay Updated',
|
||||
description: 'Get new posts delivered to your inbox.',
|
||||
},
|
||||
blogPage: {
|
||||
enabled: answers.newsletterEnabled,
|
||||
position: 'above-footer',
|
||||
title: 'Subscribe',
|
||||
description: 'Get notified when new posts are published.',
|
||||
},
|
||||
posts: {
|
||||
enabled: answers.newsletterEnabled,
|
||||
position: 'below-content',
|
||||
title: 'Enjoyed this post?',
|
||||
description: 'Subscribe for more updates.',
|
||||
},
|
||||
},
|
||||
},
|
||||
contactForm: {
|
||||
enabled: answers.contactFormEnabled,
|
||||
title: answers.contactFormTitle,
|
||||
description: 'Send us a message and we\'ll get back to you.',
|
||||
},
|
||||
newsletterAdmin: {
|
||||
enabled: false,
|
||||
showInNav: false,
|
||||
},
|
||||
newsletterNotifications: {
|
||||
enabled: false,
|
||||
newSubscriberAlert: false,
|
||||
weeklyStatsSummary: false,
|
||||
},
|
||||
weeklyDigest: {
|
||||
enabled: false,
|
||||
dayOfWeek: 0,
|
||||
subject: 'Weekly Digest',
|
||||
},
|
||||
statsPage: {
|
||||
enabled: answers.statsPageEnabled,
|
||||
showInNav: answers.statsPageEnabled,
|
||||
},
|
||||
mcpServer: {
|
||||
enabled: answers.mcpServerEnabled,
|
||||
endpoint: '/mcp',
|
||||
publicRateLimit: 50,
|
||||
authenticatedRateLimit: 1000,
|
||||
requireAuth: false,
|
||||
},
|
||||
imageLightbox: {
|
||||
enabled: answers.imageLightboxEnabled,
|
||||
},
|
||||
dashboard: {
|
||||
enabled: answers.dashboardEnabled,
|
||||
requireAuth: answers.dashboardRequireAuth,
|
||||
},
|
||||
semanticSearch: {
|
||||
enabled: answers.semanticSearchEnabled,
|
||||
},
|
||||
twitter: {
|
||||
site: answers.twitterSite,
|
||||
creator: answers.twitterCreator,
|
||||
},
|
||||
askAI: {
|
||||
enabled: answers.askAIEnabled,
|
||||
defaultModel: 'claude-sonnet-4-20250514',
|
||||
models: [
|
||||
{ id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', provider: 'anthropic' },
|
||||
{ id: 'gpt-4o', name: 'GPT-4o', provider: 'openai' },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function configureProject(
|
||||
projectDir: string,
|
||||
answers: WizardAnswers
|
||||
): Promise<void> {
|
||||
const spinner = ora('Generating configuration...').start();
|
||||
|
||||
try {
|
||||
// 1. Fix template siteConfig.ts to have clean values (fixes embedded quote issues)
|
||||
fixTemplateSiteConfig(projectDir);
|
||||
|
||||
// 2. Disable auth config if user doesn't need authentication
|
||||
// This prevents WorkOS env var errors from blocking Convex setup
|
||||
if (!answers.dashboardRequireAuth) {
|
||||
disableAuthConfig(projectDir);
|
||||
}
|
||||
|
||||
// 3. Build fork-config.json content
|
||||
const forkConfig = buildForkConfig(answers);
|
||||
|
||||
// 4. Write fork-config.json
|
||||
const configPath = join(projectDir, 'fork-config.json');
|
||||
writeFileSync(configPath, JSON.stringify(forkConfig, null, 2));
|
||||
spinner.text = 'Running configuration script...';
|
||||
|
||||
// 5. Run existing configure-fork.ts script with --silent flag
|
||||
await execa('npx', ['tsx', 'scripts/configure-fork.ts', '--silent'], {
|
||||
cwd: projectDir,
|
||||
stdio: 'pipe', // Capture output
|
||||
});
|
||||
|
||||
spinner.succeed('Site configured successfully');
|
||||
} catch (error) {
|
||||
spinner.fail('Configuration failed');
|
||||
|
||||
if (error instanceof Error) {
|
||||
log.error(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
107
packages/create-markdown-sync/src/convex-setup.ts
Normal file
107
packages/create-markdown-sync/src/convex-setup.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { execa } from 'execa';
|
||||
import ora from 'ora';
|
||||
import { log } from './utils.js';
|
||||
|
||||
export async function setupConvex(
|
||||
projectDir: string,
|
||||
projectName: string
|
||||
): Promise<boolean> {
|
||||
const spinner = ora('Setting up Convex...').start();
|
||||
|
||||
try {
|
||||
// Check if user is logged in to Convex
|
||||
spinner.text = 'Checking Convex authentication...';
|
||||
|
||||
const { stdout: whoami } = await execa('npx', ['convex', 'whoami'], {
|
||||
cwd: projectDir,
|
||||
reject: false,
|
||||
});
|
||||
|
||||
// If not logged in, prompt for login
|
||||
if (!whoami || whoami.includes('Not logged in')) {
|
||||
spinner.text = 'Opening browser for Convex login...';
|
||||
|
||||
await execa('npx', ['convex', 'login'], {
|
||||
cwd: projectDir,
|
||||
stdio: 'inherit', // Show login flow
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize Convex project
|
||||
// Stop spinner to allow interactive prompts from Convex CLI
|
||||
spinner.stop();
|
||||
console.log('');
|
||||
log.step('Initializing Convex project...');
|
||||
console.log('');
|
||||
|
||||
// Use convex dev --once to set up project without running in watch mode
|
||||
// This creates .env.local with CONVEX_URL
|
||||
// stdio: 'inherit' allows user to respond to Convex prompts (new project vs existing)
|
||||
await execa('npx', ['convex', 'dev', '--once'], {
|
||||
cwd: projectDir,
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
// Set project name if needed
|
||||
CONVEX_PROJECT_NAME: projectName,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('');
|
||||
log.success('Convex project initialized');
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Spinner may already be stopped, so use log.error instead
|
||||
spinner.stop();
|
||||
|
||||
// Check if .env.local was created despite the error
|
||||
// This happens when Convex project is created but auth config has missing env vars
|
||||
const fs = await import('fs');
|
||||
const path = await import('path');
|
||||
const envLocalPath = path.join(projectDir, '.env.local');
|
||||
|
||||
if (fs.existsSync(envLocalPath)) {
|
||||
const envContent = fs.readFileSync(envLocalPath, 'utf-8');
|
||||
if (envContent.includes('CONVEX_DEPLOYMENT') || envContent.includes('VITE_CONVEX_URL')) {
|
||||
// Convex was set up, just auth config had issues (missing WORKOS_CLIENT_ID etc)
|
||||
console.log('');
|
||||
log.success('Convex project created');
|
||||
log.warn('Auth config has missing environment variables (optional)');
|
||||
log.info('Set them up later in the Convex dashboard if you want authentication');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
log.error('Convex setup failed');
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('ENOENT')) {
|
||||
log.error('Convex CLI not found. Install with: npm install -g convex');
|
||||
} else {
|
||||
log.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
log.warn('You can set up Convex later with: npx convex dev');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deployConvexFunctions(projectDir: string): Promise<void> {
|
||||
const spinner = ora('Deploying Convex functions...').start();
|
||||
|
||||
try {
|
||||
await execa('npx', ['convex', 'deploy'], {
|
||||
cwd: projectDir,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
|
||||
spinner.succeed('Convex functions deployed');
|
||||
} catch (error) {
|
||||
spinner.fail('Convex deployment failed');
|
||||
|
||||
if (error instanceof Error) {
|
||||
log.warn('You can deploy later with: npx convex deploy');
|
||||
}
|
||||
}
|
||||
}
|
||||
117
packages/create-markdown-sync/src/index.ts
Normal file
117
packages/create-markdown-sync/src/index.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { printBanner, printSuccess, log } from './utils.js';
|
||||
import { runWizard } from './wizard.js';
|
||||
import { cloneTemplate, cleanupClonedFiles } from './clone.js';
|
||||
import { configureProject } from './configure.js';
|
||||
import { installDependencies, runInitialSync, startDevServer } from './install.js';
|
||||
import { setupConvex } from './convex-setup.js';
|
||||
|
||||
const VERSION = '0.1.0';
|
||||
|
||||
async function main(): Promise<void> {
|
||||
// Parse command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
const projectName = args.find(arg => !arg.startsWith('-'));
|
||||
const force = args.includes('--force') || args.includes('-f');
|
||||
const skipConvex = args.includes('--skip-convex');
|
||||
const skipOpen = args.includes('--skip-open');
|
||||
const showHelp = args.includes('--help') || args.includes('-h');
|
||||
const showVersion = args.includes('--version') || args.includes('-v');
|
||||
|
||||
// Handle --version
|
||||
if (showVersion) {
|
||||
console.log(`create-markdown-sync v${VERSION}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Handle --help
|
||||
if (showHelp) {
|
||||
console.log(`
|
||||
create-markdown-sync v${VERSION}
|
||||
|
||||
Create a markdown-sync site with a single command.
|
||||
|
||||
Usage:
|
||||
npx create-markdown-sync [project-name] [options]
|
||||
|
||||
Options:
|
||||
-f, --force Overwrite existing directory
|
||||
--skip-convex Skip Convex setup
|
||||
--skip-open Don't open browser after setup
|
||||
-h, --help Show this help message
|
||||
-v, --version Show version number
|
||||
|
||||
Examples:
|
||||
npx create-markdown-sync my-blog
|
||||
npx create-markdown-sync my-site --force
|
||||
npx create-markdown-sync my-app --skip-convex
|
||||
|
||||
Documentation: https://www.markdown.fast/docs
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Print welcome banner
|
||||
printBanner(VERSION);
|
||||
|
||||
try {
|
||||
// Run interactive wizard
|
||||
const answers = await runWizard(projectName);
|
||||
|
||||
if (!answers) {
|
||||
log.error('Setup cancelled');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
log.step(`Creating project in ./${answers.projectName}`);
|
||||
console.log('');
|
||||
|
||||
// Step 1: Clone template
|
||||
const projectDir = await cloneTemplate({
|
||||
projectName: answers.projectName,
|
||||
force,
|
||||
});
|
||||
|
||||
// Step 2: Clean up cloned files
|
||||
await cleanupClonedFiles(projectDir);
|
||||
|
||||
// Step 3: Configure project
|
||||
await configureProject(projectDir, answers);
|
||||
|
||||
// Step 4: Install dependencies
|
||||
await installDependencies(projectDir, answers.packageManager);
|
||||
|
||||
// Step 5: Setup Convex (if not skipped)
|
||||
let convexSetup = false;
|
||||
if (answers.initConvex && !skipConvex) {
|
||||
convexSetup = await setupConvex(projectDir, answers.convexProjectName);
|
||||
}
|
||||
|
||||
// Step 6: Run initial sync (only if Convex is set up)
|
||||
if (convexSetup) {
|
||||
await runInitialSync(projectDir, answers.packageManager);
|
||||
}
|
||||
|
||||
// Step 7: Start dev server and open browser
|
||||
if (!skipOpen) {
|
||||
await startDevServer(projectDir, answers.packageManager);
|
||||
}
|
||||
|
||||
// Print success message
|
||||
printSuccess(answers.projectName);
|
||||
} catch (error) {
|
||||
console.log('');
|
||||
|
||||
if (error instanceof Error) {
|
||||
log.error(error.message);
|
||||
} else {
|
||||
log.error('An unexpected error occurred');
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
92
packages/create-markdown-sync/src/install.ts
Normal file
92
packages/create-markdown-sync/src/install.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { execa } from 'execa';
|
||||
import ora from 'ora';
|
||||
import open from 'open';
|
||||
import { log, sleep, getInstallCommand, getRunCommand } from './utils.js';
|
||||
|
||||
export async function installDependencies(
|
||||
projectDir: string,
|
||||
packageManager: string
|
||||
): Promise<void> {
|
||||
const spinner = ora('Installing dependencies...').start();
|
||||
|
||||
try {
|
||||
const [cmd, ...args] = getInstallCommand(packageManager);
|
||||
|
||||
await execa(cmd, args, {
|
||||
cwd: projectDir,
|
||||
stdio: 'pipe', // Suppress output
|
||||
});
|
||||
|
||||
spinner.succeed('Dependencies installed');
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to install dependencies');
|
||||
|
||||
if (error instanceof Error) {
|
||||
log.error(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function runInitialSync(
|
||||
projectDir: string,
|
||||
packageManager: string
|
||||
): Promise<void> {
|
||||
const spinner = ora('Running initial content sync...').start();
|
||||
|
||||
try {
|
||||
const [cmd, ...args] = getRunCommand(packageManager, 'sync');
|
||||
|
||||
await execa(cmd, args, {
|
||||
cwd: projectDir,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
|
||||
spinner.succeed('Initial sync completed');
|
||||
} catch {
|
||||
// Sync often fails on first run because Convex functions need to be deployed first
|
||||
// This is expected and not an error - just inform the user what to do
|
||||
spinner.stop();
|
||||
log.warn('Sync requires Convex functions to be deployed first');
|
||||
log.info('After setup completes, run: npx convex dev');
|
||||
log.info('Then in another terminal: npm run sync');
|
||||
}
|
||||
}
|
||||
|
||||
export async function startDevServer(
|
||||
projectDir: string,
|
||||
packageManager: string
|
||||
): Promise<void> {
|
||||
const spinner = ora('Starting development server...').start();
|
||||
|
||||
try {
|
||||
const [cmd, ...args] = getRunCommand(packageManager, 'dev');
|
||||
|
||||
// Start dev server in detached mode (won't block)
|
||||
const devProcess = execa(cmd, args, {
|
||||
cwd: projectDir,
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
|
||||
// Unref to allow parent process to exit
|
||||
devProcess.unref();
|
||||
|
||||
spinner.succeed('Development server starting');
|
||||
|
||||
// Wait for server to be ready
|
||||
log.info('Waiting for server to start...');
|
||||
await sleep(3000);
|
||||
|
||||
// Open browser
|
||||
await open('http://localhost:5173');
|
||||
log.success('Opened browser at http://localhost:5173');
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to start development server');
|
||||
|
||||
if (error instanceof Error) {
|
||||
log.warn('You can start the server manually with: npm run dev');
|
||||
}
|
||||
}
|
||||
}
|
||||
133
packages/create-markdown-sync/src/utils.ts
Normal file
133
packages/create-markdown-sync/src/utils.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import kleur from 'kleur';
|
||||
|
||||
// Sleep helper for async delays
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
export function isValidEmail(email: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
// Validate URL format
|
||||
export function isValidUrl(url: string): boolean {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract domain from URL
|
||||
export function extractDomain(url: string): string {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
return parsed.hostname;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// Validate GitHub username (alphanumeric and hyphens)
|
||||
export function isValidGitHubUsername(username: string): boolean {
|
||||
const usernameRegex = /^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$/;
|
||||
return usernameRegex.test(username);
|
||||
}
|
||||
|
||||
// Extract GitHub username from URL
|
||||
export function extractGitHubUsername(url: string): string {
|
||||
const match = url.match(/github\.com\/([^\/]+)/);
|
||||
return match ? match[1] : '';
|
||||
}
|
||||
|
||||
// Extract Twitter handle from URL
|
||||
export function extractTwitterHandle(url: string): string {
|
||||
const match = url.match(/(?:twitter\.com|x\.com)\/([^\/]+)/);
|
||||
return match ? `@${match[1]}` : '';
|
||||
}
|
||||
|
||||
// Logging helpers with colors
|
||||
export const log = {
|
||||
info: (msg: string) => console.log(kleur.blue('i') + ' ' + msg),
|
||||
success: (msg: string) => console.log(kleur.green('✓') + ' ' + msg),
|
||||
warn: (msg: string) => console.log(kleur.yellow('!') + ' ' + msg),
|
||||
error: (msg: string) => console.log(kleur.red('✗') + ' ' + msg),
|
||||
step: (msg: string) => console.log(kleur.cyan('→') + ' ' + msg),
|
||||
dim: (msg: string) => console.log(kleur.dim(msg)),
|
||||
};
|
||||
|
||||
// Print a section header
|
||||
export function printSection(title: string, current: number, total: number): void {
|
||||
console.log('');
|
||||
console.log(kleur.bold().cyan(`[${current}/${total}] ${title}`));
|
||||
console.log(kleur.dim('─'.repeat(40)));
|
||||
}
|
||||
|
||||
// Print welcome banner
|
||||
export function printBanner(version: string): void {
|
||||
console.log('');
|
||||
console.log(kleur.bold().cyan('create-markdown-sync') + kleur.dim(` v${version}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Print success message with next steps
|
||||
export function printSuccess(projectName: string): void {
|
||||
console.log('');
|
||||
console.log(kleur.bold().green('Success!') + ' Your site is ready.');
|
||||
console.log('');
|
||||
console.log('Next steps:');
|
||||
console.log(kleur.cyan(` cd ${projectName}`));
|
||||
console.log(kleur.cyan(' npx convex dev') + kleur.dim(' # Start Convex (required first time)'));
|
||||
console.log(kleur.cyan(' npm run sync') + kleur.dim(' # Sync content (in another terminal)'));
|
||||
console.log(kleur.cyan(' npm run dev') + kleur.dim(' # Start dev server'));
|
||||
console.log('');
|
||||
console.log('Resources:');
|
||||
console.log(kleur.dim(' Docs: https://www.markdown.fast/docs'));
|
||||
console.log(kleur.dim(' Deployment: https://www.markdown.fast/docs-deployment'));
|
||||
console.log(kleur.dim(' WorkOS: https://www.markdown.fast/how-to-setup-workos'));
|
||||
console.log('');
|
||||
console.log(kleur.dim('To remove and start over:'));
|
||||
console.log(kleur.dim(` rm -rf ${projectName}`));
|
||||
console.log(kleur.dim(` npx create-markdown-sync ${projectName}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Detect available package manager
|
||||
export function detectPackageManager(): 'npm' | 'yarn' | 'pnpm' | 'bun' {
|
||||
const userAgent = process.env.npm_config_user_agent || '';
|
||||
if (userAgent.includes('yarn')) return 'yarn';
|
||||
if (userAgent.includes('pnpm')) return 'pnpm';
|
||||
if (userAgent.includes('bun')) return 'bun';
|
||||
return 'npm';
|
||||
}
|
||||
|
||||
// Get install command for package manager
|
||||
export function getInstallCommand(pm: string): string[] {
|
||||
switch (pm) {
|
||||
case 'yarn':
|
||||
return ['yarn'];
|
||||
case 'pnpm':
|
||||
return ['pnpm', 'install'];
|
||||
case 'bun':
|
||||
return ['bun', 'install'];
|
||||
default:
|
||||
return ['npm', 'install'];
|
||||
}
|
||||
}
|
||||
|
||||
// Get run command for package manager
|
||||
export function getRunCommand(pm: string, script: string): string[] {
|
||||
switch (pm) {
|
||||
case 'yarn':
|
||||
return ['yarn', script];
|
||||
case 'pnpm':
|
||||
return ['pnpm', script];
|
||||
case 'bun':
|
||||
return ['bun', 'run', script];
|
||||
default:
|
||||
return ['npm', 'run', script];
|
||||
}
|
||||
}
|
||||
604
packages/create-markdown-sync/src/wizard.ts
Normal file
604
packages/create-markdown-sync/src/wizard.ts
Normal file
@@ -0,0 +1,604 @@
|
||||
import prompts from 'prompts';
|
||||
import {
|
||||
printSection,
|
||||
isValidEmail,
|
||||
isValidUrl,
|
||||
extractDomain,
|
||||
extractGitHubUsername,
|
||||
extractTwitterHandle,
|
||||
detectPackageManager,
|
||||
} from './utils.js';
|
||||
|
||||
// Wizard answers interface matching fork-config.json structure
|
||||
export interface WizardAnswers {
|
||||
// Section 1: Project Setup
|
||||
projectName: string;
|
||||
packageManager: 'npm' | 'yarn' | 'pnpm' | 'bun';
|
||||
|
||||
// Section 2: Site Identity
|
||||
siteName: string;
|
||||
siteTitle: string;
|
||||
siteDescription: string;
|
||||
siteUrl: string;
|
||||
contactEmail: string;
|
||||
|
||||
// Section 3: Creator Info
|
||||
creatorName: string;
|
||||
twitter: string;
|
||||
linkedin: string;
|
||||
github: string;
|
||||
|
||||
// Section 4: GitHub Repository
|
||||
githubUsername: string;
|
||||
githubRepo: string;
|
||||
branch: string;
|
||||
contentPath: string;
|
||||
|
||||
// Section 5: Appearance
|
||||
theme: 'dark' | 'light' | 'tan' | 'cloud';
|
||||
fontFamily: 'serif' | 'sans' | 'monospace';
|
||||
bio: string;
|
||||
|
||||
// Section 6: Homepage & Featured
|
||||
homepageType: 'default' | 'page' | 'post';
|
||||
featuredViewMode: 'cards' | 'list';
|
||||
featuredTitle: string;
|
||||
showViewToggle: boolean;
|
||||
|
||||
// Section 7: Blog & Posts
|
||||
blogPageEnabled: boolean;
|
||||
blogPageTitle: string;
|
||||
showPostsOnHome: boolean;
|
||||
homePostsLimit: number;
|
||||
homePostsReadMoreEnabled: boolean;
|
||||
homePostsReadMoreText: string;
|
||||
homePostsReadMoreLink: string;
|
||||
|
||||
// Section 8: Features
|
||||
logoGalleryEnabled: boolean;
|
||||
logoGalleryScrolling: boolean;
|
||||
githubContributionsEnabled: boolean;
|
||||
githubContributionsUsername: string;
|
||||
visitorMapEnabled: boolean;
|
||||
statsPageEnabled: boolean;
|
||||
imageLightboxEnabled: boolean;
|
||||
|
||||
// Section 9: Footer & Social
|
||||
footerEnabled: boolean;
|
||||
footerDefaultContent: string;
|
||||
socialFooterEnabled: boolean;
|
||||
socialFooterShowInHeader: boolean;
|
||||
copyrightSiteName: string;
|
||||
|
||||
// Section 10: Newsletter & Contact
|
||||
newsletterEnabled: boolean;
|
||||
newsletterHomeEnabled: boolean;
|
||||
contactFormEnabled: boolean;
|
||||
contactFormTitle: string;
|
||||
|
||||
// Section 11: Advanced Features
|
||||
mcpServerEnabled: boolean;
|
||||
semanticSearchEnabled: boolean;
|
||||
askAIEnabled: boolean;
|
||||
dashboardEnabled: boolean;
|
||||
dashboardRequireAuth: boolean;
|
||||
|
||||
// Section 12: Twitter/X Config
|
||||
twitterSite: string;
|
||||
twitterCreator: string;
|
||||
|
||||
// Section 13: Convex Setup
|
||||
initConvex: boolean;
|
||||
convexProjectName: string;
|
||||
}
|
||||
|
||||
const TOTAL_SECTIONS = 13;
|
||||
|
||||
export async function runWizard(initialProjectName?: string): Promise<WizardAnswers | null> {
|
||||
const answers: Partial<WizardAnswers> = {};
|
||||
|
||||
// Handle cancellation
|
||||
const onCancel = () => {
|
||||
console.log('\nSetup cancelled.');
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
// SECTION 1: Project Setup
|
||||
printSection('Project Setup', 1, TOTAL_SECTIONS);
|
||||
|
||||
const section1 = await prompts([
|
||||
{
|
||||
type: 'text',
|
||||
name: 'projectName',
|
||||
message: 'Project name (directory)',
|
||||
initial: initialProjectName || 'my-markdown-site',
|
||||
validate: (value: string) => {
|
||||
if (!value.trim()) return 'Project name is required';
|
||||
if (!/^[a-zA-Z0-9-_]+$/.test(value)) return 'Only letters, numbers, hyphens, and underscores';
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'packageManager',
|
||||
message: 'Package manager',
|
||||
choices: [
|
||||
{ title: 'npm', value: 'npm' },
|
||||
{ title: 'yarn', value: 'yarn' },
|
||||
{ title: 'pnpm', value: 'pnpm' },
|
||||
{ title: 'bun', value: 'bun' },
|
||||
],
|
||||
initial: ['npm', 'yarn', 'pnpm', 'bun'].indexOf(detectPackageManager()),
|
||||
},
|
||||
], { onCancel });
|
||||
|
||||
Object.assign(answers, section1);
|
||||
|
||||
// SECTION 2: Site Identity
|
||||
printSection('Site Identity', 2, TOTAL_SECTIONS);
|
||||
|
||||
const section2 = await prompts([
|
||||
{
|
||||
type: 'text',
|
||||
name: 'siteName',
|
||||
message: 'Site name',
|
||||
initial: 'My Site',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'siteTitle',
|
||||
message: 'Tagline',
|
||||
initial: 'A markdown-powered site',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'siteDescription',
|
||||
message: 'Description (one sentence)',
|
||||
initial: 'A site built with markdown-sync framework.',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'siteUrl',
|
||||
message: 'Site URL',
|
||||
initial: `https://${answers.projectName}.netlify.app`,
|
||||
validate: (value: string) => isValidUrl(value) || 'Enter a valid URL',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'contactEmail',
|
||||
message: 'Contact email',
|
||||
validate: (value: string) => !value || isValidEmail(value) || 'Enter a valid email',
|
||||
},
|
||||
], { onCancel });
|
||||
|
||||
Object.assign(answers, section2);
|
||||
|
||||
// SECTION 3: Creator Info
|
||||
printSection('Creator Info', 3, TOTAL_SECTIONS);
|
||||
|
||||
const section3 = await prompts([
|
||||
{
|
||||
type: 'text',
|
||||
name: 'creatorName',
|
||||
message: 'Your name',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'twitter',
|
||||
message: 'Twitter/X URL',
|
||||
initial: 'https://x.com/',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'linkedin',
|
||||
message: 'LinkedIn URL',
|
||||
initial: 'https://linkedin.com/in/',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'github',
|
||||
message: 'GitHub URL',
|
||||
initial: 'https://github.com/',
|
||||
},
|
||||
], { onCancel });
|
||||
|
||||
Object.assign(answers, section3);
|
||||
|
||||
// SECTION 4: GitHub Repository
|
||||
printSection('GitHub Repository', 4, TOTAL_SECTIONS);
|
||||
|
||||
const section4 = await prompts([
|
||||
{
|
||||
type: 'text',
|
||||
name: 'githubUsername',
|
||||
message: 'GitHub username',
|
||||
initial: extractGitHubUsername(answers.github || ''),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'githubRepo',
|
||||
message: 'Repository name',
|
||||
initial: answers.projectName,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'branch',
|
||||
message: 'Default branch',
|
||||
initial: 'main',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'contentPath',
|
||||
message: 'Content path',
|
||||
initial: 'public/raw',
|
||||
},
|
||||
], { onCancel });
|
||||
|
||||
Object.assign(answers, section4);
|
||||
|
||||
// SECTION 5: Appearance
|
||||
printSection('Appearance', 5, TOTAL_SECTIONS);
|
||||
|
||||
const section5 = await prompts([
|
||||
{
|
||||
type: 'select',
|
||||
name: 'theme',
|
||||
message: 'Default theme',
|
||||
choices: [
|
||||
{ title: 'Tan (warm)', value: 'tan' },
|
||||
{ title: 'Light', value: 'light' },
|
||||
{ title: 'Dark', value: 'dark' },
|
||||
{ title: 'Cloud (blue-gray)', value: 'cloud' },
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'fontFamily',
|
||||
message: 'Font family',
|
||||
choices: [
|
||||
{ title: 'Sans-serif (system fonts)', value: 'sans' },
|
||||
{ title: 'Serif (New York)', value: 'serif' },
|
||||
{ title: 'Monospace (IBM Plex Mono)', value: 'monospace' },
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'bio',
|
||||
message: 'Bio text',
|
||||
initial: 'Your content is instantly available to browsers, LLMs, and AI agents.',
|
||||
},
|
||||
], { onCancel });
|
||||
|
||||
Object.assign(answers, section5);
|
||||
|
||||
// SECTION 6: Homepage & Featured
|
||||
printSection('Homepage & Featured', 6, TOTAL_SECTIONS);
|
||||
|
||||
const section6 = await prompts([
|
||||
{
|
||||
type: 'select',
|
||||
name: 'homepageType',
|
||||
message: 'Homepage type',
|
||||
choices: [
|
||||
{ title: 'Default (standard homepage)', value: 'default' },
|
||||
{ title: 'Page (use a static page)', value: 'page' },
|
||||
{ title: 'Post (use a blog post)', value: 'post' },
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'featuredViewMode',
|
||||
message: 'Featured section view',
|
||||
choices: [
|
||||
{ title: 'Cards (grid with excerpts)', value: 'cards' },
|
||||
{ title: 'List (bullet list)', value: 'list' },
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'featuredTitle',
|
||||
message: 'Featured section title',
|
||||
initial: 'Get started:',
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'showViewToggle',
|
||||
message: 'Show view toggle button?',
|
||||
initial: true,
|
||||
},
|
||||
], { onCancel });
|
||||
|
||||
Object.assign(answers, section6);
|
||||
|
||||
// SECTION 7: Blog & Posts
|
||||
printSection('Blog & Posts', 7, TOTAL_SECTIONS);
|
||||
|
||||
const section7 = await prompts([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'blogPageEnabled',
|
||||
message: 'Enable dedicated /blog page?',
|
||||
initial: true,
|
||||
},
|
||||
{
|
||||
type: (prev: boolean) => prev ? 'text' : null,
|
||||
name: 'blogPageTitle',
|
||||
message: 'Blog page title',
|
||||
initial: 'Blog',
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'showPostsOnHome',
|
||||
message: 'Show posts on homepage?',
|
||||
initial: true,
|
||||
},
|
||||
{
|
||||
type: (prev: boolean) => prev ? 'number' : null,
|
||||
name: 'homePostsLimit',
|
||||
message: 'Posts limit on homepage (0 for all)',
|
||||
initial: 5,
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
{
|
||||
type: (prev: number, values: { showPostsOnHome: boolean }) =>
|
||||
values.showPostsOnHome && prev > 0 ? 'confirm' : null,
|
||||
name: 'homePostsReadMoreEnabled',
|
||||
message: 'Show "read more" link?',
|
||||
initial: true,
|
||||
},
|
||||
{
|
||||
type: (prev: boolean) => prev ? 'text' : null,
|
||||
name: 'homePostsReadMoreText',
|
||||
message: 'Read more text',
|
||||
initial: 'Read more blog posts',
|
||||
},
|
||||
{
|
||||
type: (prev: string) => prev ? 'text' : null,
|
||||
name: 'homePostsReadMoreLink',
|
||||
message: 'Read more link URL',
|
||||
initial: '/blog',
|
||||
},
|
||||
], { onCancel });
|
||||
|
||||
// Set defaults for skipped questions (spread first, then defaults)
|
||||
Object.assign(answers, {
|
||||
...section7,
|
||||
blogPageTitle: section7.blogPageTitle || 'Blog',
|
||||
homePostsLimit: section7.homePostsLimit ?? 5,
|
||||
homePostsReadMoreEnabled: section7.homePostsReadMoreEnabled ?? true,
|
||||
homePostsReadMoreText: section7.homePostsReadMoreText || 'Read more blog posts',
|
||||
homePostsReadMoreLink: section7.homePostsReadMoreLink || '/blog',
|
||||
});
|
||||
|
||||
// SECTION 8: Features
|
||||
printSection('Features', 8, TOTAL_SECTIONS);
|
||||
|
||||
const section8 = await prompts([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'logoGalleryEnabled',
|
||||
message: 'Enable logo gallery?',
|
||||
initial: true,
|
||||
},
|
||||
{
|
||||
type: (prev: boolean) => prev ? 'confirm' : null,
|
||||
name: 'logoGalleryScrolling',
|
||||
message: 'Scrolling marquee?',
|
||||
initial: false,
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'githubContributionsEnabled',
|
||||
message: 'Enable GitHub contributions graph?',
|
||||
initial: true,
|
||||
},
|
||||
{
|
||||
type: (prev: boolean) => prev ? 'text' : null,
|
||||
name: 'githubContributionsUsername',
|
||||
message: 'GitHub username for contributions',
|
||||
initial: answers.githubUsername,
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'visitorMapEnabled',
|
||||
message: 'Enable visitor map on stats page?',
|
||||
initial: false,
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'statsPageEnabled',
|
||||
message: 'Enable public stats page?',
|
||||
initial: true,
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'imageLightboxEnabled',
|
||||
message: 'Enable image lightbox (click to magnify)?',
|
||||
initial: true,
|
||||
},
|
||||
], { onCancel });
|
||||
|
||||
Object.assign(answers, {
|
||||
...section8,
|
||||
logoGalleryScrolling: section8.logoGalleryScrolling ?? false,
|
||||
githubContributionsUsername: section8.githubContributionsUsername || answers.githubUsername,
|
||||
});
|
||||
|
||||
// SECTION 9: Footer & Social
|
||||
printSection('Footer & Social', 9, TOTAL_SECTIONS);
|
||||
|
||||
const section9 = await prompts([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'footerEnabled',
|
||||
message: 'Enable footer?',
|
||||
initial: true,
|
||||
},
|
||||
{
|
||||
type: (prev: boolean) => prev ? 'text' : null,
|
||||
name: 'footerDefaultContent',
|
||||
message: 'Footer content (markdown)',
|
||||
initial: 'Built with [Convex](https://convex.dev) for real-time sync.',
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'socialFooterEnabled',
|
||||
message: 'Enable social footer (icons + copyright)?',
|
||||
initial: true,
|
||||
},
|
||||
{
|
||||
type: (prev: boolean) => prev ? 'confirm' : null,
|
||||
name: 'socialFooterShowInHeader',
|
||||
message: 'Show social icons in header?',
|
||||
initial: true,
|
||||
},
|
||||
{
|
||||
type: (prev: boolean, values: { socialFooterEnabled: boolean }) =>
|
||||
values.socialFooterEnabled ? 'text' : null,
|
||||
name: 'copyrightSiteName',
|
||||
message: 'Copyright site name',
|
||||
initial: answers.siteName,
|
||||
},
|
||||
], { onCancel });
|
||||
|
||||
Object.assign(answers, {
|
||||
...section9,
|
||||
footerDefaultContent: section9.footerDefaultContent || '',
|
||||
socialFooterShowInHeader: section9.socialFooterShowInHeader ?? true,
|
||||
copyrightSiteName: section9.copyrightSiteName || answers.siteName,
|
||||
});
|
||||
|
||||
// SECTION 10: Newsletter & Contact
|
||||
printSection('Newsletter & Contact', 10, TOTAL_SECTIONS);
|
||||
|
||||
const section10 = await prompts([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'newsletterEnabled',
|
||||
message: 'Enable newsletter signups?',
|
||||
initial: false,
|
||||
hint: 'Requires AgentMail setup',
|
||||
},
|
||||
{
|
||||
type: (prev: boolean) => prev ? 'confirm' : null,
|
||||
name: 'newsletterHomeEnabled',
|
||||
message: 'Show signup on homepage?',
|
||||
initial: true,
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'contactFormEnabled',
|
||||
message: 'Enable contact form?',
|
||||
initial: false,
|
||||
hint: 'Requires AgentMail setup',
|
||||
},
|
||||
{
|
||||
type: (prev: boolean) => prev ? 'text' : null,
|
||||
name: 'contactFormTitle',
|
||||
message: 'Contact form title',
|
||||
initial: 'Get in Touch',
|
||||
},
|
||||
], { onCancel });
|
||||
|
||||
Object.assign(answers, {
|
||||
...section10,
|
||||
newsletterHomeEnabled: section10.newsletterHomeEnabled ?? false,
|
||||
contactFormTitle: section10.contactFormTitle || 'Get in Touch',
|
||||
});
|
||||
|
||||
// SECTION 11: Advanced Features
|
||||
printSection('Advanced Features', 11, TOTAL_SECTIONS);
|
||||
|
||||
const section11 = await prompts([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'mcpServerEnabled',
|
||||
message: 'Enable MCP server for AI tools?',
|
||||
initial: true,
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'semanticSearchEnabled',
|
||||
message: 'Enable semantic search?',
|
||||
initial: false,
|
||||
hint: 'Requires OpenAI API key',
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'askAIEnabled',
|
||||
message: 'Enable Ask AI header button?',
|
||||
initial: false,
|
||||
hint: 'Requires semantic search + LLM API key',
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'dashboardEnabled',
|
||||
message: 'Enable admin dashboard?',
|
||||
initial: true,
|
||||
},
|
||||
{
|
||||
type: (prev: boolean) => prev ? 'confirm' : null,
|
||||
name: 'dashboardRequireAuth',
|
||||
message: 'Require authentication for dashboard?',
|
||||
initial: false,
|
||||
hint: 'Requires WorkOS setup',
|
||||
},
|
||||
], { onCancel });
|
||||
|
||||
Object.assign(answers, {
|
||||
...section11,
|
||||
dashboardRequireAuth: section11.dashboardRequireAuth ?? false,
|
||||
});
|
||||
|
||||
// SECTION 12: Twitter/X Config
|
||||
printSection('Twitter/X Config', 12, TOTAL_SECTIONS);
|
||||
|
||||
const section12 = await prompts([
|
||||
{
|
||||
type: 'text',
|
||||
name: 'twitterSite',
|
||||
message: 'Twitter site handle',
|
||||
initial: extractTwitterHandle(answers.twitter || ''),
|
||||
hint: 'For Twitter Cards',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'twitterCreator',
|
||||
message: 'Twitter creator handle',
|
||||
initial: extractTwitterHandle(answers.twitter || ''),
|
||||
},
|
||||
], { onCancel });
|
||||
|
||||
Object.assign(answers, section12);
|
||||
|
||||
// SECTION 13: Convex Setup
|
||||
printSection('Convex Setup', 13, TOTAL_SECTIONS);
|
||||
|
||||
const section13 = await prompts([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'initConvex',
|
||||
message: 'Initialize Convex project now?',
|
||||
initial: true,
|
||||
hint: 'Opens browser for login',
|
||||
},
|
||||
{
|
||||
type: (prev: boolean) => prev ? 'text' : null,
|
||||
name: 'convexProjectName',
|
||||
message: 'Convex project name',
|
||||
initial: answers.projectName,
|
||||
},
|
||||
], { onCancel });
|
||||
|
||||
Object.assign(answers, {
|
||||
...section13,
|
||||
convexProjectName: section13.convexProjectName || answers.projectName,
|
||||
});
|
||||
|
||||
return answers as WizardAnswers;
|
||||
}
|
||||
20
packages/create-markdown-sync/tsconfig.json
Normal file
20
packages/create-markdown-sync/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user