mirror of
https://github.com/Afilmory/afilmory
synced 2026-04-24 23:05:05 +00:00
Afilmory Docs (#66)
This commit is contained in:
448
scripts/create-doc.ts
Normal file
448
scripts/create-doc.ts
Normal file
@@ -0,0 +1,448 @@
|
||||
#!/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)
|
||||
})
|
||||
156
scripts/update-lastmodified.ts
Normal file
156
scripts/update-lastmodified.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execSync } from 'node:child_process'
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
/**
|
||||
* Update lastModified field in MDX files
|
||||
*/
|
||||
function updateLastModified(filePath: string): boolean {
|
||||
try {
|
||||
const content = readFileSync(filePath, 'utf-8')
|
||||
|
||||
// Check if the file has frontmatter
|
||||
if (!content.startsWith('---')) {
|
||||
console.info(`Skipping ${filePath}: no frontmatter found`)
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse frontmatter
|
||||
const frontmatterEnd = content.indexOf('---', 3)
|
||||
if (frontmatterEnd === -1) {
|
||||
console.info(`Skipping ${filePath}: invalid frontmatter format`)
|
||||
return false
|
||||
}
|
||||
|
||||
const frontmatter = content.slice(0, frontmatterEnd + 3)
|
||||
const body = content.slice(frontmatterEnd + 3)
|
||||
|
||||
// Check if lastModified field exists
|
||||
if (!frontmatter.includes('lastModified:')) {
|
||||
console.info(`Skipping ${filePath}: no lastModified field found`)
|
||||
return false
|
||||
}
|
||||
|
||||
// Generate new timestamp
|
||||
const currentDate = 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',
|
||||
)
|
||||
|
||||
// Update lastModified field
|
||||
const updatedFrontmatter = frontmatter.replace(
|
||||
/lastModified:\s[^\n]+$/m,
|
||||
`lastModified: ${currentDate}`,
|
||||
)
|
||||
|
||||
const updatedContent = updatedFrontmatter + body
|
||||
writeFileSync(filePath, updatedContent, 'utf-8')
|
||||
|
||||
console.info(`✅ Updated ${filePath} lastModified to ${currentDate}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to update ${filePath}:`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get modified documentation files
|
||||
*/
|
||||
function getModifiedDocsFiles(): string[] {
|
||||
try {
|
||||
// Get staged files
|
||||
const stagedFiles = execSync('git diff --cached --name-only', {
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
|
||||
// Filter for md/mdx files in docs contents directory
|
||||
const docsFiles = stagedFiles.filter(
|
||||
(file) =>
|
||||
file.startsWith('packages/docs/contents/') &&
|
||||
(file.endsWith('.md') || file.endsWith('.mdx')),
|
||||
)
|
||||
|
||||
return docsFiles.map((file) => join(__dirname, '..', file))
|
||||
} catch (error) {
|
||||
console.error('Failed to get modified files:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
function main() {
|
||||
const args = process.argv.slice(2)
|
||||
|
||||
// If file paths are provided as arguments, process them directly
|
||||
if (args.length > 0) {
|
||||
let hasUpdates = false
|
||||
for (const filePath of args) {
|
||||
if (updateLastModified(filePath)) {
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
|
||||
if (hasUpdates) {
|
||||
console.info('\n📝 Please check updates and re-add to staging area:')
|
||||
console.info('git add packages/docs/contents/')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Get modified documentation files
|
||||
const modifiedFiles = getModifiedDocsFiles()
|
||||
|
||||
if (modifiedFiles.length === 0) {
|
||||
console.info('✨ No documentation files need updating')
|
||||
return
|
||||
}
|
||||
|
||||
console.info(`🔍 Found ${modifiedFiles.length} modified documentation files:`)
|
||||
modifiedFiles.forEach((file) => console.info(` - ${file}`))
|
||||
|
||||
let hasUpdates = false
|
||||
for (const filePath of modifiedFiles) {
|
||||
if (updateLastModified(filePath)) {
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
|
||||
if (hasUpdates) {
|
||||
console.info(
|
||||
'\n📝 Auto-updated lastModified fields, re-adding to staging area...',
|
||||
)
|
||||
try {
|
||||
execSync('git add packages/docs/contents/', { stdio: 'inherit' })
|
||||
console.info('✅ Successfully re-added to staging area')
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to re-add to staging area:', error)
|
||||
console.info('Please run manually: git add packages/docs/contents/')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run main function
|
||||
main()
|
||||
Reference in New Issue
Block a user