@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.
@@ -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
+ }