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