@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @pugpigjs/create-wp-project
|
|
2
|
+
|
|
3
|
+
## 0.0.2-beta.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- c2f5db8: Initial release of CLI tool for scaffolding WordPress plugins and themes with interactive prompts, case-insensitive placeholder replacement, and commands for initialising, adding, removing, and updating projects from template repositories.
|
package/README.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# @pugpigjs/create-wp-project
|
|
2
|
+
|
|
3
|
+
CLI tool to scaffold WordPress plugins and themes from a template repo.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
The easiest way to scaffold a new WordPress project:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @pugpigjs/create-wp-project init --git-url <repository-url> --directory <target-directory>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**Note:** Both `--git-url` and `--directory` are required for all commands.
|
|
14
|
+
|
|
15
|
+
## How It Works
|
|
16
|
+
|
|
17
|
+
This CLI tool uses a **placeholder-based templating system** to scaffold WordPress projects:
|
|
18
|
+
|
|
19
|
+
1. **Clone template** - Fetches the template repository
|
|
20
|
+
2. **Interactive prompts** - Asks for namespace, project name, and description
|
|
21
|
+
3. **Placeholder replacement** - Automatically replaces `TPL_*`/`tpl_*` and `pkg_*` markers in all template files
|
|
22
|
+
4. **Generate config** - Creates a customised docker-compose.yml for your project
|
|
23
|
+
5. **Validation** - Ensures naming conventions and prevents common mistakes
|
|
24
|
+
|
|
25
|
+
### Template Placeholders
|
|
26
|
+
|
|
27
|
+
The template uses **case-insensitive** placeholder markers throughout all files. You can use either uppercase (`TPL_*`) or lowercase (`tpl_*`) variants:
|
|
28
|
+
|
|
29
|
+
- `TPL_NAMESPACE` / `tpl_namespace` - Namespace in PascalCase (e.g., "Pugpig")
|
|
30
|
+
- `TPL_NAMESPACE_LOWER` / `tpl_namespace_lower` - Lowercase namespace (e.g., "pugpig")
|
|
31
|
+
- `TPL_PROJECT_NAME` / `tpl_project_name` - Project name in snake_case (e.g., "my_plugin")
|
|
32
|
+
- `TPL_PROJECT_NAME_PASCAL` / `tpl_project_name_pascal` - PascalCase name (e.g., "MyPlugin")
|
|
33
|
+
- `TPL_PROJECT_NAME_SLUG` / `tpl_project_name_slug` - Kebab-case for NPM/URLs (e.g., "my-plugin")
|
|
34
|
+
- `TPL_PROJECT_NAME_FLAT` / `tpl_project_name_flat` - No separators (e.g., "myplugin")
|
|
35
|
+
- `TPL_FULL_PROJECT_NAME` / `tpl_full_project_name` - Full project name with namespace (e.g., "pugpig_my_plugin")
|
|
36
|
+
- `TPL_FULL_PROJECT_NAME_SLUG` / `tpl_full_project_name_slug` - Kebab-case full name (e.g., "pugpig-my-plugin")
|
|
37
|
+
- `TPL_FULL_PROJECT_NAME_TITLE` / `tpl_full_project_name_title` - Title Case full name (e.g., "Pugpig My Plugin")
|
|
38
|
+
- `TPL_FULL_PROJECT_NAME_UPPER` / `tpl_full_project_name_upper` - For plugin/theme headers (e.g., "PUGPIG MY PLUGIN")
|
|
39
|
+
- `TPL_DESCRIPTION` / `tpl_description` - Human-readable description
|
|
40
|
+
- `pkg_*` (wildcard) - Any package name starting with `pkg_` gets replaced with the full project name slug (e.g., `pkg_example_theme` → `pugpig-my-theme`)
|
|
41
|
+
|
|
42
|
+
**Usage Examples:**
|
|
43
|
+
```php
|
|
44
|
+
// PHP - uppercase convention
|
|
45
|
+
namespace TPL_NAMESPACE\TPL_PROJECT_NAME_PASCAL;
|
|
46
|
+
|
|
47
|
+
// composer.json - lowercase convention
|
|
48
|
+
"name": "tpl_namespace_lower/tpl_project_name_flat"
|
|
49
|
+
|
|
50
|
+
// package.json - wildcards
|
|
51
|
+
"name": "pkg_example_theme" // becomes "pugpig-my-theme"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The CLI automatically computes all these variations from your namespace and project name inputs.
|
|
55
|
+
|
|
56
|
+
## Commands
|
|
57
|
+
|
|
58
|
+
### `init` - Initialise a New Project
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx @pugpigjs/create-wp-project init --git-url <repository-url> --directory <target-directory>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This interactive CLI will:
|
|
65
|
+
1. Clone the template repository
|
|
66
|
+
2. Let you choose between Rich Plugin, Simple Plugin, or Theme templates
|
|
67
|
+
3. Prompt for namespace (e.g., "Pugpig"), project name (e.g., "my_plugin"), and description
|
|
68
|
+
4. Validate inputs (prevents namespace duplication, ensures snake_case format)
|
|
69
|
+
5. Copy configuration files to your directory
|
|
70
|
+
6. Generate a customised docker-compose.yml
|
|
71
|
+
7. Replace all `TPL_*`/`tpl_*` and `pkg_*` markers throughout the project with computed values
|
|
72
|
+
|
|
73
|
+
### `add` - Add Another Template to Existing Project
|
|
74
|
+
|
|
75
|
+
If you've already initialised a project and want to add another plugin or theme:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx @pugpigjs/create-wp-project add --git-url <repository-url> --directory <target-directory>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This will:
|
|
82
|
+
1. Clone the template repository
|
|
83
|
+
2. Let you choose which template to add
|
|
84
|
+
3. Prompt for namespace, project name, and description
|
|
85
|
+
4. Validate inputs and replace all placeholders
|
|
86
|
+
5. Copy the template to a new directory (e.g., `namespace_project_name`)
|
|
87
|
+
6. Update your docker-compose.yml with the new service
|
|
88
|
+
|
|
89
|
+
### `remove` - Remove a Template from Project
|
|
90
|
+
|
|
91
|
+
To remove a plugin or theme from your project:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npx @pugpigjs/create-wp-project remove --directory <target-directory>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This will:
|
|
98
|
+
1. Show you a list of existing projects in the directory
|
|
99
|
+
2. Ask you to confirm the removal
|
|
100
|
+
3. Remove the service from docker-compose.yml (with backup)
|
|
101
|
+
4. Delete the project directory
|
|
102
|
+
|
|
103
|
+
**Note:** This action cannot be undone. A backup of docker-compose.yml is created automatically.
|
|
104
|
+
|
|
105
|
+
### `update` - Update Root Configuration Files
|
|
106
|
+
|
|
107
|
+
To refresh configuration files (like bitbucket-pipelines.yml, renovate.json, etc.) from the latest template:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx @pugpigjs/create-wp-project update --git-url <repository-url> --directory <target-directory>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
This will:
|
|
114
|
+
1. Clone the latest template repository
|
|
115
|
+
2. Show you a list of available root configuration files
|
|
116
|
+
3. Let you select which files to update
|
|
117
|
+
4. Optionally create backups (.backup) of existing files before updating
|
|
118
|
+
5. Copy the selected files from the template
|
|
119
|
+
|
|
120
|
+
This is useful when:
|
|
121
|
+
- The template's pipeline configuration has been updated
|
|
122
|
+
- New ESLint rules or TypeScript configs are available
|
|
123
|
+
- Renovate configuration has changed
|
|
124
|
+
- You want to sync with the latest best practices
|
|
125
|
+
|
|
126
|
+
## CLI Options
|
|
127
|
+
|
|
128
|
+
All commands (`init`, `add`, and `update`) require:
|
|
129
|
+
|
|
130
|
+
- `--git-url <url>` - **(Required)** Git repository URL or local path to the template
|
|
131
|
+
- Example: `https://github.com/user/wp-template.git`
|
|
132
|
+
- Example: `/path/to/local/template`
|
|
133
|
+
- Example: `.` (current directory for local development)
|
|
134
|
+
|
|
135
|
+
- `--directory <path>` - **(Required)** Target/project directory
|
|
136
|
+
- **Must end with `-server`** for safety (e.g., `my-project-server`)
|
|
137
|
+
- Example: `./my-project-server`
|
|
138
|
+
- Example: `/absolute/path/to/project-server`
|
|
139
|
+
|
|
140
|
+
Optional:
|
|
141
|
+
|
|
142
|
+
- `--template-dir <path>` - Template source directory within the repository (defaults to `repo_template`)
|
|
143
|
+
|
|
144
|
+
## Examples
|
|
145
|
+
|
|
146
|
+
### Initialise from a remote repository
|
|
147
|
+
```bash
|
|
148
|
+
npx @pugpigjs/create-wp-project init \
|
|
149
|
+
--git-url https://github.com/your-org/your-wp-template.git \
|
|
150
|
+
--directory ./my-project-server
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Initialise from a local template (for development)
|
|
154
|
+
```bash
|
|
155
|
+
npx @pugpigjs/create-wp-project init \
|
|
156
|
+
--git-url . \
|
|
157
|
+
--directory ./test-server
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Add another template to existing project
|
|
161
|
+
```bash
|
|
162
|
+
npx @pugpigjs/create-wp-project add \
|
|
163
|
+
--git-url https://github.com/your-org/your-wp-template.git \
|
|
164
|
+
--directory ./my-project-server
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Update configuration files
|
|
168
|
+
```bash
|
|
169
|
+
npx @pugpigjs/create-wp-project update \
|
|
170
|
+
--git-url https://github.com/your-org/your-wp-template.git \
|
|
171
|
+
--directory ./my-project-server
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Safety Features
|
|
175
|
+
|
|
176
|
+
This CLI tool includes multiple safety checks to prevent accidental data loss:
|
|
177
|
+
|
|
178
|
+
- ✅ **Directory validation** - Target directory must end with `-server`
|
|
179
|
+
- ✅ **System directory protection** - Blocks operations in `/`, `/usr`, `/etc`, home directory, etc.
|
|
180
|
+
- ✅ **Sandboxed operations** - Temporary files only created within project directory
|
|
181
|
+
- ✅ **Backup creation** - Creates `.backup` files before modifying docker-compose.yml
|
|
182
|
+
- ✅ **File comparison** - Only updates files that have actually changed
|
|
183
|
+
- ✅ **Project directory check** - Won't overwrite existing project directories
|
|
184
|
+
|
|
185
|
+
## Requirements
|
|
186
|
+
|
|
187
|
+
- Node.js 14 or higher
|
|
188
|
+
- A directory ending with `-server` (e.g., `my-project-server`)
|
|
189
|
+
|
|
190
|
+
## Template Types
|
|
191
|
+
|
|
192
|
+
### Rich Plugin
|
|
193
|
+
Full-featured plugin with Vue.js, SCSS, and advanced build setup. Includes:
|
|
194
|
+
- Vite build system with HMR support
|
|
195
|
+
- Vue.js components
|
|
196
|
+
- SCSS preprocessing
|
|
197
|
+
- Composer dependencies with PHP-Scoper
|
|
198
|
+
- WordPress integration utilities
|
|
199
|
+
|
|
200
|
+
### Simple Plugin
|
|
201
|
+
Lightweight plugin with basic JavaScript and CSS. Includes:
|
|
202
|
+
- Simple Vite build setup
|
|
203
|
+
- Vanilla JavaScript
|
|
204
|
+
- Basic CSS
|
|
205
|
+
- Minimal dependencies
|
|
206
|
+
|
|
207
|
+
### Theme
|
|
208
|
+
WordPress theme with modern build tooling. Includes:
|
|
209
|
+
- Vite build system
|
|
210
|
+
- Modern CSS/JS compilation
|
|
211
|
+
- Theme template structure
|
|
212
|
+
|
|
213
|
+
## Development
|
|
214
|
+
|
|
215
|
+
For local development and testing from within this repository:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
# From the repository root
|
|
219
|
+
node packages/create-wp-project/src/cli.js init \
|
|
220
|
+
--git-url . \
|
|
221
|
+
--directory ./test-server
|
|
222
|
+
|
|
223
|
+
# Or test the add command
|
|
224
|
+
node packages/create-wp-project/src/cli.js add \
|
|
225
|
+
--git-url . \
|
|
226
|
+
--directory ./test-server
|
|
227
|
+
|
|
228
|
+
# Or test the update command
|
|
229
|
+
node packages/create-wp-project/src/cli.js update \
|
|
230
|
+
--git-url . \
|
|
231
|
+
--directory ./test-server
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## License
|
|
235
|
+
|
|
236
|
+
ISC
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pugpigjs/create-wp-project",
|
|
3
|
+
"version": "0.0.2-beta.0",
|
|
4
|
+
"description": "CLI tool to scaffold WordPress plugins and themes from a template repo.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-wp-project": "./src/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"CHANGELOG.md"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"wordpress",
|
|
15
|
+
"scaffold",
|
|
16
|
+
"cli",
|
|
17
|
+
"pugpig"
|
|
18
|
+
],
|
|
19
|
+
"author": "",
|
|
20
|
+
"license": "ISC",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"chalk": "^5.0.0",
|
|
23
|
+
"commander": "^12.0.0",
|
|
24
|
+
"fs-extra": "^11.0.0",
|
|
25
|
+
"inquirer": "^12.10.0",
|
|
26
|
+
"ora": "^8.0.0",
|
|
27
|
+
"simple-git": "^3.0.0"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander'
|
|
4
|
+
import { initProject } from './commands/init.js'
|
|
5
|
+
import { addTemplate } from './commands/add.js'
|
|
6
|
+
import { removeTemplate } from './commands/remove.js'
|
|
7
|
+
import { updateProject } from './commands/update.js'
|
|
8
|
+
|
|
9
|
+
const program = new Command()
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('create-wp-project')
|
|
13
|
+
.description('CLI tool to scaffold WordPress plugins and themes from a template repo.')
|
|
14
|
+
.version('0.0.1')
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.command('init')
|
|
18
|
+
.description('Initialise a new WordPress project from the template')
|
|
19
|
+
.requiredOption('-d, --directory <path>', 'Target directory (must end with -server)')
|
|
20
|
+
.requiredOption('--git-url <url>', 'Git repository URL or local path to template')
|
|
21
|
+
.option('--template-dir <path>', 'Template source directory', 'repo_template')
|
|
22
|
+
.action(initProject)
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.command('add')
|
|
26
|
+
.description('Add another plugin or theme to an existing project')
|
|
27
|
+
.requiredOption('-d, --directory <path>', 'Project directory (must end with -server)')
|
|
28
|
+
.requiredOption('--git-url <url>', 'Git repository URL or local path to template')
|
|
29
|
+
.option('--template-dir <path>', 'Template source directory', 'repo_template')
|
|
30
|
+
.action(addTemplate)
|
|
31
|
+
|
|
32
|
+
program
|
|
33
|
+
.command('remove')
|
|
34
|
+
.description('Remove a plugin or theme from the project')
|
|
35
|
+
.requiredOption('-d, --directory <path>', 'Project directory (must end with -server)')
|
|
36
|
+
.action(removeTemplate)
|
|
37
|
+
|
|
38
|
+
program
|
|
39
|
+
.command('update')
|
|
40
|
+
.description('Update root configuration files from the template')
|
|
41
|
+
.requiredOption('-d, --directory <path>', 'Project directory (must end with -server)')
|
|
42
|
+
.requiredOption('--git-url <url>', 'Git repository URL or local path to template')
|
|
43
|
+
.option('--template-dir <path>', 'Template source directory', 'repo_template')
|
|
44
|
+
.action(updateProject)
|
|
45
|
+
|
|
46
|
+
program.parse()
|
|
@@ -0,0 +1,151 @@
|
|
|
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 {
|
|
8
|
+
prompts,
|
|
9
|
+
validateTargetDirectory,
|
|
10
|
+
cloneRepo,
|
|
11
|
+
copyDirectory,
|
|
12
|
+
replacePlaceholdersInFiles,
|
|
13
|
+
cleanup,
|
|
14
|
+
displayNextSteps
|
|
15
|
+
} from '../utils/shared.js'
|
|
16
|
+
import { TEMPLATES } from '../utils/constants.js'
|
|
17
|
+
import { replacePlaceholders } from '../utils/docker-compose.js'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Add service to existing docker-compose.yml
|
|
21
|
+
*/
|
|
22
|
+
async function addDockerComposeService(targetDir, tempDir, templateType, projectName, directoryName) {
|
|
23
|
+
const spinner = ora('Updating docker-compose.yml...').start()
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const dockerComposePath = path.join(targetDir, 'docker-compose.yml')
|
|
27
|
+
const templateDir = path.join(tempDir, 'repo_template', '.templates', 'docker-compose')
|
|
28
|
+
const partialPath = path.join(templateDir, `${templateType}.partial.yml`)
|
|
29
|
+
|
|
30
|
+
// Check if docker-compose.yml exists
|
|
31
|
+
if (!await fse.pathExists(dockerComposePath)) {
|
|
32
|
+
spinner.warn('docker-compose.yml not found, skipping')
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check if partial exists
|
|
37
|
+
if (!await fse.pathExists(partialPath)) {
|
|
38
|
+
spinner.warn('Template partial not found, skipping docker-compose update')
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Create backup of docker-compose.yml
|
|
43
|
+
const backupPath = `${dockerComposePath}.backup`
|
|
44
|
+
await fse.copy(dockerComposePath, backupPath)
|
|
45
|
+
console.log(chalk.gray(` Created backup: ${backupPath}`))
|
|
46
|
+
|
|
47
|
+
// Read existing docker-compose and partial
|
|
48
|
+
let dockerComposeContent = await fse.readFile(dockerComposePath, 'utf-8')
|
|
49
|
+
let partial = await fse.readFile(partialPath, 'utf-8')
|
|
50
|
+
|
|
51
|
+
// Remove 'services:' from the beginning of partial if it exists (for add command)
|
|
52
|
+
partial = partial.replace(/^services:\s*\n/, '')
|
|
53
|
+
|
|
54
|
+
// Replace placeholders in partial
|
|
55
|
+
const serviceContent = replacePlaceholders(partial, directoryName)
|
|
56
|
+
|
|
57
|
+
// Insert service before the global networks section (not indented networks within services)
|
|
58
|
+
// Match 'networks:' at the start of a line (no leading spaces)
|
|
59
|
+
const networksMatch = dockerComposeContent.match(/^networks:/m)
|
|
60
|
+
if (networksMatch && networksMatch.index !== undefined) {
|
|
61
|
+
const networksIndex = networksMatch.index
|
|
62
|
+
dockerComposeContent =
|
|
63
|
+
dockerComposeContent.slice(0, networksIndex) +
|
|
64
|
+
serviceContent + '\n' +
|
|
65
|
+
dockerComposeContent.slice(networksIndex)
|
|
66
|
+
} else {
|
|
67
|
+
// No networks section, append to end
|
|
68
|
+
dockerComposeContent += '\n' + serviceContent
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await fse.writeFile(dockerComposePath, dockerComposeContent)
|
|
72
|
+
spinner.succeed('docker-compose.yml updated')
|
|
73
|
+
} catch (error) {
|
|
74
|
+
spinner.fail(`Failed to update docker-compose.yml: ${error.message}`)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Main add command handler
|
|
81
|
+
*/
|
|
82
|
+
export async function addTemplate(options) {
|
|
83
|
+
console.log(chalk.blue.bold('\n➕ Add Template to Existing Project\n'))
|
|
84
|
+
|
|
85
|
+
// Validate target directory is safe
|
|
86
|
+
const targetDir = await validateTargetDirectory(options.directory)
|
|
87
|
+
|
|
88
|
+
// Prompt for template selection and project name
|
|
89
|
+
// First get template and namespace
|
|
90
|
+
const initialAnswers = await inquirer.prompt([
|
|
91
|
+
prompts.template,
|
|
92
|
+
prompts.namespace
|
|
93
|
+
])
|
|
94
|
+
|
|
95
|
+
// Then get project name with validation based on namespace
|
|
96
|
+
const projectAnswers = await inquirer.prompt([
|
|
97
|
+
prompts.getProjectNamePrompt(initialAnswers),
|
|
98
|
+
prompts.description
|
|
99
|
+
])
|
|
100
|
+
|
|
101
|
+
const answers = { ...initialAnswers, ...projectAnswers }
|
|
102
|
+
|
|
103
|
+
const { template, namespace, projectName, description } = answers
|
|
104
|
+
// Use temp directory INSIDE the target directory - much safer!
|
|
105
|
+
// This ensures we can only delete files within the project scope
|
|
106
|
+
const uniqueId = crypto.randomBytes(4).toString('hex')
|
|
107
|
+
const tempDir = path.join(targetDir, `.temp-${uniqueId}`)
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// Step 1: Clone repository
|
|
111
|
+
await cloneRepo(options.gitUrl, tempDir)
|
|
112
|
+
|
|
113
|
+
// Step 2: Copy selected template
|
|
114
|
+
const spinner = ora(`Copying ${TEMPLATES[template].name} template...`).start()
|
|
115
|
+
const templateRoot = path.join(tempDir, options.templateDir)
|
|
116
|
+
const templateSource = path.join(templateRoot, TEMPLATES[template].directory)
|
|
117
|
+
const directoryName = `${namespace.toLowerCase()}_${projectName}`
|
|
118
|
+
const templateTarget = path.join(targetDir, directoryName)
|
|
119
|
+
|
|
120
|
+
// Check if target already exists
|
|
121
|
+
if (await fse.pathExists(templateTarget)) {
|
|
122
|
+
spinner.fail(`Directory ${directoryName} already exists`)
|
|
123
|
+
try {
|
|
124
|
+
await cleanup(tempDir)
|
|
125
|
+
} catch {}
|
|
126
|
+
process.exit(1)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await copyDirectory(templateSource, templateTarget)
|
|
130
|
+
spinner.succeed(`${TEMPLATES[template].name} template copied`)
|
|
131
|
+
|
|
132
|
+
// Step 3: Replace template placeholders
|
|
133
|
+
await replacePlaceholdersInFiles(targetDir, namespace, projectName, description)
|
|
134
|
+
|
|
135
|
+
// Step 4: Update docker-compose.yml
|
|
136
|
+
await addDockerComposeService(targetDir, tempDir, template, projectName, directoryName)
|
|
137
|
+
|
|
138
|
+
// Step 5: Cleanup
|
|
139
|
+
await cleanup(tempDir)
|
|
140
|
+
|
|
141
|
+
console.log(chalk.green.bold('\n✨ Template added successfully!\n'))
|
|
142
|
+
displayNextSteps(targetDir, directoryName, template, false)
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error(chalk.red('\n❌ Error adding template:'), error.message)
|
|
145
|
+
// Cleanup on error
|
|
146
|
+
try {
|
|
147
|
+
await cleanup(tempDir)
|
|
148
|
+
} catch {}
|
|
149
|
+
process.exit(1)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
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 {
|
|
8
|
+
prompts,
|
|
9
|
+
validateTargetDirectory,
|
|
10
|
+
cloneRepo,
|
|
11
|
+
copyDirectory,
|
|
12
|
+
replacePlaceholdersInFiles,
|
|
13
|
+
cleanup,
|
|
14
|
+
displayNextSteps
|
|
15
|
+
} from '../utils/shared.js'
|
|
16
|
+
import { TEMPLATES, ROOT_FILES } from '../utils/constants.js'
|
|
17
|
+
import { replacePlaceholders } from '../utils/docker-compose.js'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Copy root files from template to target directory
|
|
21
|
+
*/
|
|
22
|
+
async function copyRootFiles(templateRoot, targetDir, filesToCopy) {
|
|
23
|
+
const spinner = ora('Copying root configuration files...').start()
|
|
24
|
+
let copiedCount = 0
|
|
25
|
+
const overwrittenFiles = []
|
|
26
|
+
|
|
27
|
+
for (const file of filesToCopy) {
|
|
28
|
+
const sourcePath = path.join(templateRoot, file)
|
|
29
|
+
const targetPath = path.join(targetDir, file)
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
if (await fse.pathExists(sourcePath)) {
|
|
33
|
+
const targetExists = await fse.pathExists(targetPath)
|
|
34
|
+
if (targetExists) {
|
|
35
|
+
overwrittenFiles.push(file)
|
|
36
|
+
}
|
|
37
|
+
await fse.copy(sourcePath, targetPath)
|
|
38
|
+
copiedCount++
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.warn(chalk.yellow(` Warning: Could not copy ${file}`))
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
spinner.succeed(`Copied ${copiedCount} configuration files`)
|
|
46
|
+
|
|
47
|
+
if (overwrittenFiles.length > 0) {
|
|
48
|
+
console.log(chalk.yellow(` ⚠️ Overwrote ${overwrittenFiles.length} existing file${overwrittenFiles.length !== 1 ? 's' : ''}: ${overwrittenFiles.join(', ')}`))
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Generate docker-compose.yml based on selected template
|
|
54
|
+
*/
|
|
55
|
+
async function generateDockerCompose(targetDir, templateType, projectName, directoryName) {
|
|
56
|
+
const spinner = ora('Generating docker-compose.yml...').start()
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const templateDir = path.join(targetDir, '.templates', 'docker-compose')
|
|
60
|
+
const basePath = path.join(templateDir, 'base.yml')
|
|
61
|
+
const partialPath = path.join(templateDir, `${templateType}.partial.yml`)
|
|
62
|
+
|
|
63
|
+
let servicesContent = ''
|
|
64
|
+
let networksContent = ''
|
|
65
|
+
|
|
66
|
+
// Read the service partial
|
|
67
|
+
if (await fse.pathExists(partialPath)) {
|
|
68
|
+
const partial = await fse.readFile(partialPath, 'utf-8')
|
|
69
|
+
servicesContent = replacePlaceholders(partial, directoryName)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Read the networks base
|
|
73
|
+
if (await fse.pathExists(basePath)) {
|
|
74
|
+
networksContent = await fse.readFile(basePath, 'utf-8')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Combine with services first, then networks at the bottom
|
|
78
|
+
const dockerComposeContent = servicesContent + '\n' + networksContent
|
|
79
|
+
|
|
80
|
+
await fse.writeFile(path.join(targetDir, 'docker-compose.yml'), dockerComposeContent)
|
|
81
|
+
|
|
82
|
+
// Clean up templates directory
|
|
83
|
+
await fse.remove(path.join(targetDir, '.templates'))
|
|
84
|
+
|
|
85
|
+
spinner.succeed('docker-compose.yml generated')
|
|
86
|
+
} catch (error) {
|
|
87
|
+
spinner.warn('Could not generate docker-compose.yml (templates may not exist)')
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Main init command handler
|
|
93
|
+
*/
|
|
94
|
+
export async function initProject(options) {
|
|
95
|
+
console.log(chalk.blue.bold('\n🚀 Pugpig WordPress Project Scaffolder\n'))
|
|
96
|
+
|
|
97
|
+
// Validate target directory is safe
|
|
98
|
+
const targetDir = await validateTargetDirectory(options.directory)
|
|
99
|
+
|
|
100
|
+
// Prompt for template selection and project name
|
|
101
|
+
// First get template and namespace
|
|
102
|
+
const initialAnswers = await inquirer.prompt([
|
|
103
|
+
prompts.template,
|
|
104
|
+
prompts.namespace
|
|
105
|
+
])
|
|
106
|
+
|
|
107
|
+
// Then get project name with validation based on namespace
|
|
108
|
+
const projectAnswers = await inquirer.prompt([
|
|
109
|
+
prompts.getProjectNamePrompt(initialAnswers),
|
|
110
|
+
prompts.description,
|
|
111
|
+
{
|
|
112
|
+
type: 'input',
|
|
113
|
+
name: 'rootFiles',
|
|
114
|
+
message: 'Root files to copy (comma-separated, leave blank for defaults):',
|
|
115
|
+
default: ROOT_FILES.join(', '),
|
|
116
|
+
filter: (input) => input.split(',').map(f => f.trim()).filter(Boolean)
|
|
117
|
+
}
|
|
118
|
+
])
|
|
119
|
+
|
|
120
|
+
const answers = { ...initialAnswers, ...projectAnswers }
|
|
121
|
+
|
|
122
|
+
const { template, namespace, projectName, description, rootFiles } = answers
|
|
123
|
+
const directoryName = `${namespace.toLowerCase()}_${projectName}`
|
|
124
|
+
const projectTarget = path.join(targetDir, directoryName)
|
|
125
|
+
// Use temp directory INSIDE the target directory - much safer!
|
|
126
|
+
// This ensures we can only delete files within the project scope
|
|
127
|
+
const uniqueId = crypto.randomBytes(4).toString('hex')
|
|
128
|
+
const tempDir = path.join(targetDir, `.temp-${uniqueId}`)
|
|
129
|
+
|
|
130
|
+
// Safety check: Don't overwrite existing project directory
|
|
131
|
+
if (await fse.pathExists(projectTarget)) {
|
|
132
|
+
console.error(chalk.red(`\n❌ Directory ${directoryName} already exists in ${targetDir}`))
|
|
133
|
+
console.error(chalk.yellow('Please choose a different project name or remove the existing directory.'))
|
|
134
|
+
process.exit(1)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
// Step 1: Clone repository
|
|
139
|
+
await cloneRepo(options.gitUrl, tempDir)
|
|
140
|
+
|
|
141
|
+
// Step 2: Copy root files from repo_template directory
|
|
142
|
+
const templateRoot = path.join(tempDir, options.templateDir)
|
|
143
|
+
await copyRootFiles(templateRoot, targetDir, rootFiles)
|
|
144
|
+
|
|
145
|
+
// Step 3: Copy selected template
|
|
146
|
+
const spinner = ora(`Copying ${TEMPLATES[template].name} template...`).start()
|
|
147
|
+
const templateSource = path.join(templateRoot, TEMPLATES[template].directory)
|
|
148
|
+
const templateTarget = path.join(targetDir, directoryName)
|
|
149
|
+
await copyDirectory(templateSource, templateTarget)
|
|
150
|
+
spinner.succeed(`${TEMPLATES[template].name} template copied`)
|
|
151
|
+
|
|
152
|
+
// Step 4: Copy .templates directory for docker-compose generation
|
|
153
|
+
const templatesSource = path.join(templateRoot, '.templates')
|
|
154
|
+
const templatesTarget = path.join(targetDir, '.templates')
|
|
155
|
+
if (await fse.pathExists(templatesSource)) {
|
|
156
|
+
await copyDirectory(templatesSource, templatesTarget)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Step 5: Replace template placeholders (including root files for init)
|
|
160
|
+
const rootFilesToProcess = [
|
|
161
|
+
'package.json',
|
|
162
|
+
'docker-compose.yml',
|
|
163
|
+
'bitbucket-pipelines.yml'
|
|
164
|
+
]
|
|
165
|
+
await replacePlaceholdersInFiles(targetDir, namespace, projectName, description, rootFilesToProcess)
|
|
166
|
+
|
|
167
|
+
// Step 6: Generate docker-compose.yml
|
|
168
|
+
await generateDockerCompose(targetDir, template, projectName, directoryName)
|
|
169
|
+
|
|
170
|
+
// Step 7: Cleanup
|
|
171
|
+
await cleanup(tempDir)
|
|
172
|
+
|
|
173
|
+
console.log(chalk.green.bold('\n✨ Project initialised successfully!\n'))
|
|
174
|
+
displayNextSteps(targetDir, directoryName, template, true)
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error(chalk.red('\n❌ Error initialising project:'), error.message)
|
|
177
|
+
// Cleanup on error
|
|
178
|
+
try {
|
|
179
|
+
await cleanup(tempDir)
|
|
180
|
+
} catch {}
|
|
181
|
+
// Also cleanup .templates directory if it was copied
|
|
182
|
+
try {
|
|
183
|
+
await fse.remove(path.join(targetDir, '.templates'))
|
|
184
|
+
} catch {}
|
|
185
|
+
process.exit(1)
|
|
186
|
+
}
|
|
187
|
+
}
|