Files
afilmory/scripts/create-doc.ts
2025-09-11 18:42:47 +08:00

449 lines
8.9 KiB
JavaScript

#!/usr/bin/env node
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import * as clack from '@clack/prompts'
import { cancel, isCancel } from '@clack/prompts'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
interface DocOptions {
title: string
description?: string
category?: string
filename: string
template: 'basic' | 'guide' | 'api' | 'deployment'
}
/**
* Generate current timestamp in Asia/Shanghai timezone
*/
function getCurrentTimestamp(): string {
return new Date()
.toLocaleString('en-GB', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
})
.replace(
/(\d{2})\/(\d{2})\/(\d{4}), (\d{2}:\d{2}:\d{2})/,
'$3-$2-$1T$4+08:00',
)
}
/**
* Get available categories by scanning existing directories
*/
function getCategories(): string[] {
const contentsDir = join(__dirname, '..', 'packages', 'docs', 'contents')
try {
const { readdirSync, statSync } = require('node:fs')
return readdirSync(contentsDir)
.filter((item) => {
const fullPath = join(contentsDir, item)
return statSync(fullPath).isDirectory()
})
.sort()
} catch {
return ['deployment', 'guides', 'api', 'tutorial']
}
}
/**
* Generate content template based on type
*/
function generateTemplate(options: DocOptions): string {
const timestamp = getCurrentTimestamp()
const frontmatter = `---
title: ${options.title}${
options.description
? `
description: ${options.description}`
: ''
}
createdAt: ${timestamp}
lastModified: ${timestamp}
---`
switch (options.template) {
case 'guide': {
return `${frontmatter}
# ${options.title}
## Overview
Brief description of what this guide covers.
## Prerequisites
- Requirement 1
- Requirement 2
## Step 1: Getting Started
Description of the first step.
\`\`\`bash
# Example command
pnpm install
\`\`\`
## Step 2: Configuration
Description of configuration step.
\`\`\`json
{
"example": "configuration"
}
\`\`\`
## Step 3: Implementation
Implementation details.
## Troubleshooting
Common issues and solutions.
## Next Steps
- Link to related guides
- Additional resources
`
}
case 'api': {
return `${frontmatter}
# ${options.title}
## Overview
API documentation for ${options.title}.
## Authentication
Details about authentication requirements.
## Endpoints
### GET /api/example
Description of the endpoint.
**Parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| \`id\` | string | Yes | The unique identifier |
**Response:**
\`\`\`json
{
"success": true,
"data": {
"id": "example",
"name": "Example"
}
}
\`\`\`
## Error Handling
Common error responses and their meanings.
## Examples
Code examples for different programming languages.
`
}
case 'deployment': {
return `${frontmatter}
# ${options.title}
## Overview
Guide for deploying using ${options.title}.
## Prerequisites
- System requirements
- Account setup
## Installation
Step-by-step installation process.
\`\`\`bash
# Installation commands
\`\`\`
## Configuration
### Environment Variables
| Variable | Description | Required |
|----------|-------------|----------|
| \`EXAMPLE_VAR\` | Example variable | Yes |
### Configuration File
\`\`\`json
{
"example": "configuration"
}
\`\`\`
## Deployment Steps
1. Step one
2. Step two
3. Step three
## Verification
How to verify the deployment was successful.
## Troubleshooting
Common deployment issues and solutions.
`
}
default: {
return `${frontmatter}
# ${options.title}
## Introduction
Brief introduction to the topic.
## Content
Main content goes here.
## Examples
\`\`\`bash
# Example command
echo "Hello World"
\`\`\`
## Conclusion
Summary and next steps.
`
}
}
}
/**
* Validate filename
*/
function validateFilename(filename: string): string | undefined {
if (!filename.trim()) {
return 'Filename is required'
}
const cleanFilename = filename.trim().toLowerCase()
// Check for valid characters
if (!/^[a-z0-9-]+$/.test(cleanFilename)) {
return 'Filename can only contain lowercase letters, numbers, and hyphens'
}
// Check length
if (cleanFilename.length < 2 || cleanFilename.length > 50) {
return 'Filename must be between 2 and 50 characters'
}
}
/**
* Main CLI function
*/
async function main() {
clack.intro('📝 Create New Documentation')
// Get document title
const title = await clack.text({
message: 'What is the document title?',
placeholder: 'My Awesome Guide',
validate: (value) => {
if (!value.trim()) return 'Title is required'
},
})
if (isCancel(title)) {
cancel('Operation cancelled.')
process.exit(0)
}
// Get document description (optional)
const description = await clack.text({
message: 'Provide a brief description (optional):',
placeholder: 'A comprehensive guide to...',
})
if (isCancel(description)) {
cancel('Operation cancelled.')
process.exit(0)
}
// Get template type
const template = await clack.select({
message: 'Choose a template:',
options: [
{ value: 'basic', label: 'Basic - Simple document structure' },
{ value: 'guide', label: 'Guide - Step-by-step tutorial' },
{ value: 'api', label: 'API - API documentation' },
{ value: 'deployment', label: 'Deployment - Deployment guide' },
],
})
if (isCancel(template)) {
cancel('Operation cancelled.')
process.exit(0)
}
// Get existing categories
const categories = getCategories()
// Choose category or create new
const categoryChoice = await clack.select({
message: 'Choose a category:',
options: [
...categories.map((cat) => ({ value: cat, label: cat })),
{ value: '__new__', label: '✨ Create new category' },
{ value: '__root__', label: '📁 Root level (no category)' },
],
})
if (isCancel(categoryChoice)) {
cancel('Operation cancelled.')
process.exit(0)
}
let category: string | undefined
if (categoryChoice === '__new__') {
const newCategory = await clack.text({
message: 'Enter new category name:',
placeholder: 'my-category',
validate: (value) => validateFilename(value),
})
if (isCancel(newCategory)) {
cancel('Operation cancelled.')
process.exit(0)
}
category = newCategory.trim().toLowerCase()
} else if (categoryChoice !== '__root__') {
category = categoryChoice
}
// Get filename
const defaultFilename = title
.toLowerCase()
.replaceAll(/[^\w\s-]/g, '')
.replaceAll(/\s+/g, '-')
.replaceAll(/-+/g, '-')
.replaceAll(/^-|-$/g, '')
const filename = await clack.text({
message: 'Enter filename (without .mdx extension):',
placeholder: defaultFilename,
defaultValue: defaultFilename,
validate: validateFilename,
})
if (isCancel(filename)) {
cancel('Operation cancelled.')
process.exit(0)
}
// Confirm before creating
const confirm = await clack.confirm({
message: `Create document at ${category ? `${category}/` : ''}${filename}.mdx?`,
})
if (isCancel(confirm) || !confirm) {
cancel('Operation cancelled.')
process.exit(0)
}
// Create the document
const spinner = clack.spinner()
spinner.start('Creating document...')
try {
const contentsDir = join(__dirname, '..', 'packages', 'docs', 'contents')
const targetDir = category ? join(contentsDir, category) : contentsDir
const filePath = join(targetDir, `${filename}.mdx`)
// Create directory if it doesn't exist
if (!existsSync(targetDir)) {
mkdirSync(targetDir, { recursive: true })
}
// Check if file already exists
if (existsSync(filePath)) {
spinner.stop('File already exists!')
clack.log.error(
`Document ${category ? `${category}/` : ''}${filename}.mdx already exists`,
)
process.exit(1)
}
// Generate content
const options: DocOptions = {
title,
description: description || undefined,
category,
filename,
template: template as DocOptions['template'],
}
const content = generateTemplate(options)
// Write file
writeFileSync(filePath, content, 'utf-8')
spinner.stop('Document created successfully!')
clack.note(
`Location: packages/docs/contents/${category ? `${category}/` : ''}${filename}.mdx\n` +
`Template: ${template}\n` +
`Title: ${title}`,
'Document Details',
)
clack.outro('✨ Happy writing!')
} catch (error) {
spinner.stop('Failed to create document')
clack.log.error(
`Error: ${error instanceof Error ? error.message : String(error)}`,
)
process.exit(1)
}
}
// Run the CLI
main().catch((error) => {
console.error('Unexpected error:', error)
process.exit(1)
})