@pugpigjs/create-wp-project 0.0.2-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +236 -0
- package/package.json +32 -0
- package/src/cli.js +46 -0
- package/src/commands/add.js +151 -0
- package/src/commands/init.js +187 -0
- package/src/commands/remove.js +123 -0
- package/src/commands/update.js +229 -0
- package/src/utils/constants.js +68 -0
- package/src/utils/docker-compose.js +25 -0
- package/src/utils/shared.js +359 -0
- package/src/utils/templating.js +167 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fse from 'fs-extra'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import inquirer from 'inquirer'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
import ora from 'ora'
|
|
6
|
+
import {
|
|
7
|
+
validateTargetDirectory
|
|
8
|
+
} from '../utils/shared.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Remove service from docker-compose.yml
|
|
12
|
+
*/
|
|
13
|
+
async function removeDockerComposeService(targetDir, projectName) {
|
|
14
|
+
const spinner = ora('Updating docker-compose.yml...').start()
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const dockerComposePath = path.join(targetDir, 'docker-compose.yml')
|
|
18
|
+
|
|
19
|
+
// Check if docker-compose.yml exists
|
|
20
|
+
if (!await fse.pathExists(dockerComposePath)) {
|
|
21
|
+
spinner.warn('docker-compose.yml not found, skipping')
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Create backup of docker-compose.yml
|
|
26
|
+
const backupPath = `${dockerComposePath}.backup`
|
|
27
|
+
await fse.copy(dockerComposePath, backupPath)
|
|
28
|
+
console.log(chalk.gray(` Created backup: ${backupPath}`))
|
|
29
|
+
|
|
30
|
+
// Read existing docker-compose
|
|
31
|
+
let dockerComposeContent = await fse.readFile(dockerComposePath, 'utf-8')
|
|
32
|
+
|
|
33
|
+
// Find and remove the service section
|
|
34
|
+
// Service name format is: build-{projectName}:
|
|
35
|
+
const serviceName = `build-${projectName}`
|
|
36
|
+
|
|
37
|
+
// Match the entire service block from service name to the next service or networks/end
|
|
38
|
+
// Also match any blank lines before the service
|
|
39
|
+
const servicePattern = new RegExp(
|
|
40
|
+
`\\n* ${serviceName}:[\\s\\S]*?(?=\\n build-[a-zA-Z_]|\\nnetworks:|$)`,
|
|
41
|
+
'g'
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const originalContent = dockerComposeContent
|
|
45
|
+
dockerComposeContent = dockerComposeContent.replace(servicePattern, '')
|
|
46
|
+
|
|
47
|
+
if (dockerComposeContent === originalContent) {
|
|
48
|
+
spinner.warn(`Service "${projectName}" not found in docker-compose.yml`)
|
|
49
|
+
} else {
|
|
50
|
+
await fse.writeFile(dockerComposePath, dockerComposeContent)
|
|
51
|
+
spinner.succeed('docker-compose.yml updated')
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
spinner.fail(`Failed to update docker-compose.yml: ${error.message}`)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Main remove command handler
|
|
60
|
+
*/
|
|
61
|
+
export async function removeTemplate(options) {
|
|
62
|
+
console.log(chalk.blue.bold('\n🗑️ Remove Template from Project\n'))
|
|
63
|
+
|
|
64
|
+
// Validate target directory is safe
|
|
65
|
+
const targetDir = await validateTargetDirectory(options.directory)
|
|
66
|
+
|
|
67
|
+
// Get list of directories in target (potential projects to remove)
|
|
68
|
+
const entries = await fse.readdir(targetDir, { withFileTypes: true })
|
|
69
|
+
const directories = entries
|
|
70
|
+
.filter(entry => entry.isDirectory())
|
|
71
|
+
.filter(entry => !entry.name.startsWith('.')) // Exclude hidden directories
|
|
72
|
+
.filter(entry => !['node_modules', 'vendor', 'dist', 'build'].includes(entry.name))
|
|
73
|
+
.map(entry => entry.name)
|
|
74
|
+
|
|
75
|
+
if (directories.length === 0) {
|
|
76
|
+
console.log(chalk.yellow('\n⚠️ No project directories found to remove'))
|
|
77
|
+
process.exit(0)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Prompt for which project to remove
|
|
81
|
+
const answers = await inquirer.prompt([
|
|
82
|
+
{
|
|
83
|
+
type: 'list',
|
|
84
|
+
name: 'projectName',
|
|
85
|
+
message: 'Select a project to remove:',
|
|
86
|
+
choices: directories
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
type: 'confirm',
|
|
90
|
+
name: 'confirm',
|
|
91
|
+
message: (answers) => chalk.red(`Are you sure you want to remove "${answers.projectName}"? This cannot be undone.`),
|
|
92
|
+
default: false
|
|
93
|
+
}
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
const { projectName, confirm } = answers
|
|
97
|
+
|
|
98
|
+
if (!confirm) {
|
|
99
|
+
console.log(chalk.yellow('\n❌ Removal cancelled'))
|
|
100
|
+
process.exit(0)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const projectPath = path.join(targetDir, projectName)
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
// Step 1: Remove from docker-compose.yml
|
|
107
|
+
await removeDockerComposeService(targetDir, projectName)
|
|
108
|
+
|
|
109
|
+
// Step 2: Remove the project directory
|
|
110
|
+
const spinner = ora(`Removing ${projectName} directory...`).start()
|
|
111
|
+
await fse.remove(projectPath)
|
|
112
|
+
spinner.succeed(`${projectName} directory removed`)
|
|
113
|
+
|
|
114
|
+
console.log(chalk.green.bold('\n✨ Template removed successfully!\n'))
|
|
115
|
+
console.log(chalk.cyan('Next steps:'))
|
|
116
|
+
console.log(chalk.white(` 1. Review docker-compose.yml to ensure it's correct`))
|
|
117
|
+
console.log(chalk.white(` 2. If needed, restore from docker-compose.yml.backup`))
|
|
118
|
+
console.log()
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error(chalk.red('\n❌ Error removing template:'), error.message)
|
|
121
|
+
process.exit(1)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import fse from 'fs-extra'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import crypto from 'crypto'
|
|
4
|
+
import inquirer from 'inquirer'
|
|
5
|
+
import chalk from 'chalk'
|
|
6
|
+
import ora from 'ora'
|
|
7
|
+
import { validateTargetDirectory, cloneRepo, cleanup } from '../utils/shared.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Default root files that can be updated
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_ROOT_FILES = [
|
|
13
|
+
'.eslintrc.json',
|
|
14
|
+
'.gitignore',
|
|
15
|
+
'bitbucket-pipelines.yml',
|
|
16
|
+
'renovate.json',
|
|
17
|
+
'tsconfig.json'
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Files that typically shouldn't be overwritten
|
|
22
|
+
*/
|
|
23
|
+
const PROTECTED_FILES = [
|
|
24
|
+
'package.json',
|
|
25
|
+
'docker-compose.yml',
|
|
26
|
+
'init-template.sh',
|
|
27
|
+
'.git',
|
|
28
|
+
'.env',
|
|
29
|
+
'vendor',
|
|
30
|
+
'node_modules'
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create a backup of a file
|
|
35
|
+
*/
|
|
36
|
+
async function backupFile(filePath) {
|
|
37
|
+
const backupPath = `${filePath}.backup`
|
|
38
|
+
try {
|
|
39
|
+
await fse.copy(filePath, backupPath)
|
|
40
|
+
return backupPath
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get list of available root files from template
|
|
48
|
+
*/
|
|
49
|
+
async function getAvailableRootFiles(templateRoot) {
|
|
50
|
+
try {
|
|
51
|
+
const entries = await fse.readdir(templateRoot, { withFileTypes: true })
|
|
52
|
+
return entries
|
|
53
|
+
.filter(entry => entry.isFile() && !PROTECTED_FILES.includes(entry.name))
|
|
54
|
+
.map(entry => entry.name)
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return []
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Update root configuration files
|
|
62
|
+
*/
|
|
63
|
+
async function updateRootFiles(templateRoot, targetDir, filesToUpdate, createBackups) {
|
|
64
|
+
const spinner = ora('Updating configuration files...').start()
|
|
65
|
+
let updatedCount = 0
|
|
66
|
+
let backupCount = 0
|
|
67
|
+
let unchangedCount = 0
|
|
68
|
+
const updatedFiles = []
|
|
69
|
+
const skippedFiles = []
|
|
70
|
+
const unchangedFiles = []
|
|
71
|
+
|
|
72
|
+
for (const file of filesToUpdate) {
|
|
73
|
+
const sourcePath = path.join(templateRoot, file)
|
|
74
|
+
const targetPath = path.join(targetDir, file)
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
if (!await fse.pathExists(sourcePath)) {
|
|
78
|
+
skippedFiles.push({ file, reason: 'Not found in template' })
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const targetExists = await fse.pathExists(targetPath)
|
|
83
|
+
|
|
84
|
+
// Compare file contents if target exists
|
|
85
|
+
if (targetExists) {
|
|
86
|
+
const sourceContent = await fse.readFile(sourcePath, 'utf-8')
|
|
87
|
+
const targetContent = await fse.readFile(targetPath, 'utf-8')
|
|
88
|
+
|
|
89
|
+
if (sourceContent === targetContent) {
|
|
90
|
+
unchangedFiles.push(file)
|
|
91
|
+
unchangedCount++
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Create backup if file exists, has changed, and backups are enabled
|
|
96
|
+
if (createBackups) {
|
|
97
|
+
const backupPath = await backupFile(targetPath)
|
|
98
|
+
if (backupPath) {
|
|
99
|
+
backupCount++
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Copy the file
|
|
105
|
+
await fse.copy(sourcePath, targetPath)
|
|
106
|
+
updatedCount++
|
|
107
|
+
updatedFiles.push(file)
|
|
108
|
+
} catch (error) {
|
|
109
|
+
skippedFiles.push({ file, reason: error.message })
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
spinner.succeed(`Updated ${updatedCount} configuration file${updatedCount !== 1 ? 's' : ''}`)
|
|
114
|
+
|
|
115
|
+
if (unchangedCount > 0) {
|
|
116
|
+
console.log(chalk.gray(` ${unchangedCount} file${unchangedCount !== 1 ? 's' : ''} already up to date`))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (backupCount > 0) {
|
|
120
|
+
console.log(chalk.gray(` Created ${backupCount} backup file${backupCount !== 1 ? 's' : ''} (.backup)`))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (skippedFiles.length > 0) {
|
|
124
|
+
console.log(chalk.yellow('\n Skipped files:'))
|
|
125
|
+
skippedFiles.forEach(({ file, reason }) => {
|
|
126
|
+
console.log(chalk.gray(` - ${file}: ${reason}`))
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { updatedFiles, skippedFiles, unchangedFiles, backupCount }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Main update command handler
|
|
135
|
+
*/
|
|
136
|
+
export async function updateProject(options) {
|
|
137
|
+
console.log(chalk.blue.bold('\n🔄 Update Project Configuration Files\n'))
|
|
138
|
+
|
|
139
|
+
// Validate target directory is safe
|
|
140
|
+
const targetDir = await validateTargetDirectory(options.directory)
|
|
141
|
+
|
|
142
|
+
// Use temp directory INSIDE the target directory - much safer!
|
|
143
|
+
// This ensures we can only delete files within the project scope
|
|
144
|
+
const uniqueId = crypto.randomBytes(4).toString('hex')
|
|
145
|
+
const tempDir = path.join(targetDir, `.temp-${uniqueId}`)
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
// Clone repository to get latest templates
|
|
149
|
+
await cloneRepo(options.gitUrl, tempDir)
|
|
150
|
+
|
|
151
|
+
const templateRoot = path.join(tempDir, options.templateDir)
|
|
152
|
+
|
|
153
|
+
// Get available files
|
|
154
|
+
const availableFiles = await getAvailableRootFiles(templateRoot)
|
|
155
|
+
const defaultFiles = DEFAULT_ROOT_FILES.filter(f => availableFiles.includes(f))
|
|
156
|
+
|
|
157
|
+
// Prompt user for options
|
|
158
|
+
const answers = await inquirer.prompt([
|
|
159
|
+
{
|
|
160
|
+
type: 'checkbox',
|
|
161
|
+
name: 'filesToUpdate',
|
|
162
|
+
message: 'Select files to update:',
|
|
163
|
+
choices: availableFiles.map(file => ({
|
|
164
|
+
name: file,
|
|
165
|
+
value: file,
|
|
166
|
+
checked: defaultFiles.includes(file)
|
|
167
|
+
})),
|
|
168
|
+
validate: (input) => {
|
|
169
|
+
if (input.length === 0) return 'Please select at least one file'
|
|
170
|
+
return true
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
type: 'confirm',
|
|
175
|
+
name: 'createBackups',
|
|
176
|
+
message: 'Create backups of existing files before updating?',
|
|
177
|
+
default: true
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
type: 'confirm',
|
|
181
|
+
name: 'proceed',
|
|
182
|
+
message: (answers) => {
|
|
183
|
+
const count = answers.filesToUpdate.length
|
|
184
|
+
return chalk.yellow(`\nThis will update ${count} file${count !== 1 ? 's' : ''}. Continue?`)
|
|
185
|
+
},
|
|
186
|
+
default: true
|
|
187
|
+
}
|
|
188
|
+
])
|
|
189
|
+
|
|
190
|
+
if (!answers.proceed) {
|
|
191
|
+
console.log(chalk.gray('\nUpdate cancelled'))
|
|
192
|
+
try {
|
|
193
|
+
await cleanup(tempDir)
|
|
194
|
+
} catch {}
|
|
195
|
+
process.exit(0)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Update the files
|
|
199
|
+
const result = await updateRootFiles(
|
|
200
|
+
templateRoot,
|
|
201
|
+
targetDir,
|
|
202
|
+
answers.filesToUpdate,
|
|
203
|
+
answers.createBackups
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
// Cleanup
|
|
207
|
+
await cleanup(tempDir)
|
|
208
|
+
|
|
209
|
+
console.log(chalk.green.bold('\n✨ Configuration files updated successfully!\n'))
|
|
210
|
+
|
|
211
|
+
if (result.backupCount > 0) {
|
|
212
|
+
console.log(chalk.cyan('Backup files were created with .backup extension'))
|
|
213
|
+
console.log(chalk.gray('You can restore them if needed, or delete them once you verify the changes\n'))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
console.log(chalk.cyan('Next steps:'))
|
|
217
|
+
console.log(chalk.white(' 1. Review the updated files'))
|
|
218
|
+
console.log(chalk.white(' 2. Test your build'))
|
|
219
|
+
console.log(chalk.white(' 3. Delete backup files when satisfied: rm *.backup'))
|
|
220
|
+
console.log()
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error(chalk.red('\n❌ Error updating project:'), error.message)
|
|
223
|
+
// Cleanup on error
|
|
224
|
+
try {
|
|
225
|
+
await cleanup(tempDir)
|
|
226
|
+
} catch {}
|
|
227
|
+
process.exit(1)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template types and their configurations
|
|
3
|
+
*/
|
|
4
|
+
export const TEMPLATES = {
|
|
5
|
+
rich: {
|
|
6
|
+
name: 'Rich Plugin',
|
|
7
|
+
directory: 'example_rich_plugin',
|
|
8
|
+
description: 'Full-featured plugin with Vue.js, SCSS, and advanced build setup',
|
|
9
|
+
command: 'serve'
|
|
10
|
+
},
|
|
11
|
+
simple: {
|
|
12
|
+
name: 'Simple Plugin',
|
|
13
|
+
directory: 'example_simple_plugin',
|
|
14
|
+
description: 'Lightweight plugin with basic JavaScript and CSS',
|
|
15
|
+
command: 'dev'
|
|
16
|
+
},
|
|
17
|
+
theme: {
|
|
18
|
+
name: 'Theme',
|
|
19
|
+
directory: 'example_theme',
|
|
20
|
+
description: 'WordPress theme with modern build tooling',
|
|
21
|
+
command: 'dev'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Files to copy from the template root to the target directory (for init command)
|
|
27
|
+
*/
|
|
28
|
+
export const ROOT_FILES = [
|
|
29
|
+
'.gitignore',
|
|
30
|
+
'.env',
|
|
31
|
+
'.editorconfig',
|
|
32
|
+
'bitbucket-pipelines.yml',
|
|
33
|
+
'renovate.json',
|
|
34
|
+
'README.md',
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Directories and files to exclude when copying
|
|
39
|
+
*/
|
|
40
|
+
export const EXCLUDED_ITEMS = [
|
|
41
|
+
'.git',
|
|
42
|
+
'node_modules',
|
|
43
|
+
'dist',
|
|
44
|
+
'build',
|
|
45
|
+
'vendor',
|
|
46
|
+
'.DS_Store'
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Patterns to exclude when scanning for files to process
|
|
51
|
+
*/
|
|
52
|
+
export const EXCLUDE_PATTERNS = [
|
|
53
|
+
'node_modules',
|
|
54
|
+
'.git',
|
|
55
|
+
'.temp-',
|
|
56
|
+
'vendor',
|
|
57
|
+
'dist',
|
|
58
|
+
'build'
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Files to exclude from placeholder replacement
|
|
63
|
+
* These files should be copied as-is without any template processing
|
|
64
|
+
*/
|
|
65
|
+
export const SKIP_PLACEHOLDER_REPLACEMENT = [
|
|
66
|
+
'README.md',
|
|
67
|
+
'init-template.sh'
|
|
68
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for Docker Compose file manipulation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { replaceTemplatePlaceholders } from './templating.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Replace placeholders in Docker Compose template content
|
|
9
|
+
*
|
|
10
|
+
* Wraps the main templating system for docker-compose files.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} content - The template content with placeholders
|
|
13
|
+
* @param {string} fullProjectName - The full project name (namespace_projectName)
|
|
14
|
+
* @returns {string} Content with placeholders replaced
|
|
15
|
+
*/
|
|
16
|
+
export function replacePlaceholders(content, fullProjectName) {
|
|
17
|
+
const namespace = fullProjectName.split('_')[0]
|
|
18
|
+
const actualProjectName = fullProjectName.substring(namespace.length + 1)
|
|
19
|
+
|
|
20
|
+
return replaceTemplatePlaceholders(content, {
|
|
21
|
+
namespace: namespace.charAt(0).toUpperCase() + namespace.slice(1),
|
|
22
|
+
projectName: actualProjectName,
|
|
23
|
+
description: '' // Docker compose doesn't use description
|
|
24
|
+
})
|
|
25
|
+
}
|