mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 22:48:17 +00:00
449 lines
8.9 KiB
JavaScript
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)
|
|
})
|