@bostonuniversity/buwp-local 0.6.3 → 0.7.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/bin/buwp-local.js CHANGED
@@ -24,6 +24,7 @@ const packageJson = JSON.parse(
24
24
  import startCommand from '../lib/commands/start.js';
25
25
  import stopCommand from '../lib/commands/stop.js';
26
26
  import destroyCommand from '../lib/commands/destroy.js';
27
+ import updateCommand from '../lib/commands/update.js';
27
28
  import logsCommand from '../lib/commands/logs.js';
28
29
  import wpCommand from '../lib/commands/wp.js';
29
30
  import shellCommand from '../lib/commands/shell.js';
@@ -60,6 +61,13 @@ program
60
61
  .option('-f, --force', 'Skip confirmation prompt')
61
62
  .action(destroyCommand);
62
63
 
64
+ // Update command
65
+ program
66
+ .command('update')
67
+ .description('Update Docker images and recreate containers')
68
+ .option('--all', 'Update all service images (default: WordPress only)')
69
+ .action(updateCommand);
70
+
63
71
  // Logs command
64
72
  program
65
73
  .command('logs')
@@ -88,6 +96,7 @@ program
88
96
  .option('--plugin', 'Non-interactive: initialize as plugin')
89
97
  .option('--mu-plugin', 'Non-interactive: initialize as mu-plugin')
90
98
  .option('--theme', 'Non-interactive: initialize as theme')
99
+ .option('--sandbox', 'Non-interactive: initialize as sandbox')
91
100
  .option('-f, --force', 'Overwrite existing configuration')
92
101
  .action(initCommand);
93
102
 
package/docs/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to buwp-local will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.7.0]
9
+
10
+ ### Added
11
+ - **`update` command** - Pull latest Docker images and recreate containers without losing data
12
+ - Pull WordPress image only by default (typical use case)
13
+ - `--all` flag to update all service images (Redis, S3 proxy, etc.)
14
+ - Safely applies security updates and WordPress core updates
15
+ - Data preserved: Database, WordPress files, volume mappings
16
+
17
+ - **Non-interactive sandbox initialization** - Add `--sandbox` flag to `init` command for scripted setup of sandbox-type projects
18
+
19
+ ### Changed
20
+ - **`init` command** - Now supports `--sandbox` flag for non-interactive initialization
21
+
8
22
  ## [0.6.3]
9
23
 
10
24
  ### Added
package/docs/COMMANDS.md CHANGED
@@ -115,6 +115,46 @@ npx buwp-local destroy --force
115
115
 
116
116
  ---
117
117
 
118
+ ### `update`
119
+
120
+ Update Docker images and recreate containers without losing data.
121
+
122
+ ```bash
123
+ npx buwp-local update [options]
124
+ ```
125
+
126
+ **Options:**
127
+ - `--all` - Update all service images (default: WordPress image only)
128
+
129
+ **Examples:**
130
+ ```bash
131
+ # Update WordPress image only (recommended)
132
+ npx buwp-local update
133
+
134
+ # Update all service images (Redis, S3 proxy, etc.)
135
+ npx buwp-local update --all
136
+ ```
137
+
138
+ **What it does:**
139
+ - Checks if environment exists and Docker is running
140
+ - Pulls latest Docker images from registry
141
+ - Recreates containers with new images using `--force-recreate`
142
+ - Loads credentials from Keychain and/or `.env.local`
143
+ - **Preserves volumes** - Database and WordPress files untouched
144
+ - Shows success message confirming what was preserved
145
+
146
+ **Use cases:**
147
+ - Pull latest WordPress updates without losing development work
148
+ - Update only WordPress image (typical use case) or all services
149
+ - Safe alternative to `destroy` when you only want to refresh images
150
+
151
+ **Key difference from `stop/start`:**
152
+ - `stop` → `start`: Reuses existing containers (no new images)
153
+ - `update`: Pulls new images and recreates containers (gets updates)
154
+ - `destroy`: Removes everything including volumes (fresh start)
155
+
156
+ ---
157
+
118
158
  ### `logs`
119
159
 
120
160
  View logs from Docker containers.
@@ -224,7 +224,7 @@ Check volume mappings in `.buwp-local.json`:
224
224
  # Verify Keychain entries
225
225
  npx buwp-local keychain list
226
226
 
227
- # Or use .env.local fallback
227
+ # Or use .env.local fallback --- this is a nice idea, but the actual command does not yet exist
228
228
  npx buwp-local keychain export > .env.local
229
229
  ```
230
230
 
package/docs/ROADMAP.md CHANGED
@@ -160,6 +160,15 @@ hostile.remove('127.0.0.1', config.hostname);
160
160
 
161
161
  ### Potential Features
162
162
 
163
+ - **Docker Image Update Command** šŸŽÆ (Proposed for v0.7.0)
164
+ - **Problem:** Stopping and restarting containers reuses existing images; newer images aren't pulled
165
+ - **Solution:** Add `buwp-local update` command that:
166
+ - Pulls latest Docker images from registry
167
+ - Recreates containers with new images
168
+ - Preserves volumes (database, WordPress data)
169
+ - **Benefit:** Safe, explicit way to apply WordPress/service updates without `destroy`
170
+ - **Implementation:** Wrapper around `docker-compose pull && docker-compose up -d --force-recreate`
171
+
163
172
  - **Database Security**
164
173
  - Check database access on db port (e.g. `localhost:3306`)
165
174
  - Consider more stringent default database passwords
@@ -169,6 +178,10 @@ hostile.remove('127.0.0.1', config.hostname);
169
178
  - Command to help generate Xdebug configuration for IDEs (VSCode, Zed)
170
179
  - Documentation on usage patterns
171
180
 
181
+ - **Interactive setup assistant for adding volume mappings**
182
+ - Guided prompts to add common volume mappings post-initialization
183
+ - Suggestions based on detected project structure
184
+
172
185
  - **Improved Windows and Linux support**
173
186
  - Multiplatform /etc/hosts hostname guide
174
187
  - Evaluate credential storage solutions for non-macOS platforms (https://www.npmjs.com/package/keytar)
@@ -195,6 +208,10 @@ hostile.remove('127.0.0.1', config.hostname);
195
208
  - **Docker Volume management assistant**
196
209
  - listing and cleanup of unused volumes
197
210
 
211
+ - **Unit tests**
212
+ - Core modules (config, keychain, docker-compose)
213
+ - Command tests (init, start, stop, destroy, wp)
214
+
198
215
  ### Prioritization
199
216
  Will be informed by feedback from initial small group of users and actual pain points observed during rollout.
200
217
 
@@ -209,7 +226,8 @@ Will be informed by feedback from initial small group of users and actual pain p
209
226
 
210
227
  - **Cross-Platform Support** - Windows WSL2 and Linux credential storage
211
228
  - **SSL Certificate Generation** - Local HTTPS with mkcert
212
- - **Performance Monitoring** -
229
+ - **Real support for runnning on ports other than 443**
230
+ - **Potential GUI from Electron or SwiftUI**
213
231
 
214
232
  **Note:** Automatic `/etc/hosts` management deferred pending user feedback. See "Lessons Learned" section above for details on the `hostile` library approach.
215
233
 
@@ -85,7 +85,7 @@ function confirmDestroy(projectName) {
85
85
  console.log(chalk.yellow(`This will destroy project: ${chalk.bold(projectName)}`));
86
86
  console.log(chalk.yellow(' - Stop all containers'));
87
87
  console.log(chalk.yellow(' - Remove all containers'));
88
- console.log(chalk.yellow(' - Delete all volumes (including database data) (except maybe not, please fix)\n'));
88
+ console.log(chalk.yellow(' - Delete all volumes (including database data)\n'));
89
89
 
90
90
  rl.question(chalk.red('Are you sure you want to continue? (yes/no): '), (answer) => {
91
91
  rl.close();
@@ -9,6 +9,17 @@ import fs from 'fs';
9
9
  import os from 'os';
10
10
  import { initConfig, CONFIG_FILE_NAME } from '../config.js';
11
11
 
12
+ /**
13
+ * Container path templates for project types
14
+ * Defines where each project type maps to in the WordPress container
15
+ */
16
+ const MAPPING_TEMPLATES = {
17
+ 'plugin': '/var/www/html/wp-content/plugins/{name}',
18
+ 'mu-plugin': '/var/www/html/wp-content/mu-plugins/{name}',
19
+ 'theme': '/var/www/html/wp-content/themes/{name}',
20
+ 'sandbox': null // Sandbox has no default mapping
21
+ };
22
+
12
23
  /**
13
24
  * Detect project type from package.json or directory structure
14
25
  * @param {string} projectPath - Path to project directory
@@ -84,6 +95,7 @@ async function initCommand(options) {
84
95
  if (options.plugin) initOptions.plugin = true;
85
96
  if (options.muPlugin) initOptions.muPlugin = true;
86
97
  if (options.theme) initOptions.theme = true;
98
+ if (options.sandbox) initOptions.sandbox = true;
87
99
 
88
100
  const configPath = initConfig(projectPath, initOptions);
89
101
  console.log(chalk.green(`āœ… Created configuration file: ${configPath}\n`));
@@ -101,11 +113,26 @@ async function initCommand(options) {
101
113
  console.log(chalk.cyan(`ā„¹ļø Detected project type: ${detectedType}\n`));
102
114
  }
103
115
 
104
- // Determine default project type index
105
- let defaultTypeIndex = 0;
106
- if (detectedType === 'plugin') defaultTypeIndex = 0;
107
- else if (detectedType === 'mu-plugin') defaultTypeIndex = 1;
108
- else if (detectedType === 'theme') defaultTypeIndex = 2;
116
+ // Define project type choices
117
+ const projectTypeChoices = [
118
+ { title: 'Plugin', value: 'plugin', description: 'WordPress plugin development' },
119
+ { title: 'MU Plugin', value: 'mu-plugin', description: 'Must-use plugin development' },
120
+ { title: 'Theme', value: 'theme', description: 'WordPress theme development' },
121
+ { title: 'Sandbox', value: 'sandbox', description: 'Base WordPress environment (add code mappings later)' }
122
+ ];
123
+
124
+ // Determine default project type with priority: CLI flag > detected type > default
125
+ const typeFromFlag = options.plugin ? 'plugin'
126
+ : options.muPlugin ? 'mu-plugin'
127
+ : options.theme ? 'theme'
128
+ : options.sandbox ? 'sandbox'
129
+ : null;
130
+
131
+ const preferredType = typeFromFlag || detectedType || 'plugin';
132
+
133
+ const defaultTypeIndex = projectTypeChoices.findIndex(
134
+ choice => choice.value === preferredType
135
+ );
109
136
 
110
137
  const questions = [
111
138
  {
@@ -119,13 +146,7 @@ async function initCommand(options) {
119
146
  type: 'select',
120
147
  name: 'projectType',
121
148
  message: 'Project type',
122
- choices: [
123
- { title: 'Plugin', value: 'plugin', description: 'WordPress plugin development' },
124
- { title: 'MU Plugin', value: 'mu-plugin', description: 'Must-use plugin development' },
125
- { title: 'Theme', value: 'theme', description: 'WordPress theme development' },
126
- { title: 'Sandbox', value: 'sandbox', description: 'Base WordPress environment (add code mappings later)' },
127
- { title: 'Custom', value: 'custom', description: 'Custom mapping configuration' }
128
- ],
149
+ choices: projectTypeChoices,
129
150
  initial: defaultTypeIndex
130
151
  },
131
152
  {
@@ -245,31 +266,12 @@ async function initCommand(options) {
245
266
  }
246
267
  };
247
268
 
248
- // Add mapping based on project type
249
- if (answers.projectType === 'plugin') {
250
- config.mappings.push({
251
- local: './',
252
- container: `/var/www/html/wp-content/plugins/${answers.projectName}`
253
- });
254
- } else if (answers.projectType === 'mu-plugin') {
255
- config.mappings.push({
256
- local: './',
257
- container: `/var/www/html/wp-content/mu-plugins/${answers.projectName}`
258
- });
259
- } else if (answers.projectType === 'theme') {
260
- config.mappings.push({
261
- local: './',
262
- container: `/var/www/html/wp-content/themes/${answers.projectName}`
263
- });
264
- } else if (answers.projectType === 'sandbox') {
265
- // Sandbox type: no initial mappings
266
- // User can add mappings manually to config.mappings array
267
- } else {
268
- // Custom type
269
+ // Add mapping based on project type using template
270
+ const template = MAPPING_TEMPLATES[answers.projectType];
271
+ if (template) {
269
272
  config.mappings.push({
270
273
  local: './',
271
- container: '/var/www/html/wp-content/plugins/my-plugin',
272
- comment: 'Customize this mapping for your project'
274
+ container: template.replace('{name}', answers.projectName)
273
275
  });
274
276
  }
275
277
 
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Update command - Updates Docker images and recreates containers
3
+ */
4
+
5
+ import chalk from 'chalk';
6
+ import { execSync } from 'child_process';
7
+ import path from 'path';
8
+ import fs from 'fs';
9
+ import { loadConfig, loadKeychainCredentials, createSecureTempEnvFile, secureDeleteTempEnvFile, ENV_FILE_NAME } from '../config.js';
10
+
11
+ async function updateCommand(options = {}) {
12
+ console.log(chalk.blue('šŸ”„ Updating Docker images...\n'));
13
+
14
+ try {
15
+ const projectPath = process.cwd();
16
+ const composePath = path.join(projectPath, '.buwp-local', 'docker-compose.yml');
17
+ const composeDir = path.dirname(composePath);
18
+ const envFilePath = path.join(projectPath, ENV_FILE_NAME);
19
+
20
+ // Check if docker-compose.yml exists
21
+ if (!fs.existsSync(composePath)) {
22
+ console.log(chalk.yellow('āš ļø No environment found.'));
23
+ console.log(chalk.gray('Run "buwp-local start" to create an environment.\n'));
24
+ return;
25
+ }
26
+
27
+ // Load config to get project name
28
+ const config = loadConfig(projectPath);
29
+ const projectName = config.projectName || 'buwp-local';
30
+
31
+ // Check if Docker is running
32
+ try {
33
+ execSync('docker info', { stdio: 'ignore' });
34
+ } catch (err) {
35
+ console.error(chalk.red('āŒ Docker is not running'));
36
+ console.log(chalk.gray('Please start Docker Desktop and try again.\n'));
37
+ process.exit(1);
38
+ }
39
+
40
+ // Determine which services to pull
41
+ const pullAll = options.all || false;
42
+ const imageFilter = pullAll ? '' : 'wordpress';
43
+
44
+ // Step 1: Pull images (WordPress only by default, or all with --all flag)
45
+ console.log(chalk.cyan(pullAll ? 'šŸ“„ Pulling all Docker images...' : 'šŸ“„ Pulling WordPress image...'));
46
+ try {
47
+ execSync(
48
+ `docker compose -p ${projectName} -f "${composePath}" pull ${imageFilter}`,
49
+ {
50
+ cwd: composeDir,
51
+ stdio: 'inherit'
52
+ }
53
+ );
54
+ } catch (err) {
55
+ console.error(chalk.red('\nāŒ Failed to pull Docker images'));
56
+ console.log(chalk.gray('Check your Docker registry credentials and network connection.\n'));
57
+ process.exit(1);
58
+ }
59
+
60
+ // Step 2: Recreate containers with new images (preserves volumes)
61
+ // Need to pass environment variables just like start command does
62
+ console.log(chalk.cyan('\nšŸ”Ø Recreating containers with new images...'));
63
+
64
+ // Load keychain credentials and create secure temp env file if available
65
+ let tempEnvPath = null;
66
+ const finalKeychainCredentials = loadKeychainCredentials();
67
+ const keychainCredCount = Object.keys(finalKeychainCredentials).length;
68
+
69
+ if (keychainCredCount > 0) {
70
+ try {
71
+ tempEnvPath = createSecureTempEnvFile(finalKeychainCredentials, projectName);
72
+ } catch (err) {
73
+ console.warn(chalk.yellow(`āš ļø Could not load keychain credentials: ${err.message}`));
74
+ }
75
+ }
76
+
77
+ // Build env-file flags
78
+ const envFileFlag = fs.existsSync(envFilePath) ? `--env-file ${envFilePath}` : '';
79
+ const tempEnvFileFlag = tempEnvPath ? `--env-file ${tempEnvPath}` : '';
80
+
81
+ try {
82
+ execSync(
83
+ `docker compose -p ${projectName} ${tempEnvFileFlag} ${envFileFlag} -f "${composePath}" up -d --force-recreate`,
84
+ {
85
+ cwd: composeDir,
86
+ stdio: 'inherit'
87
+ }
88
+ );
89
+ } catch (err) {
90
+ console.error(chalk.red('\nāŒ Failed to recreate containers'));
91
+ process.exit(1);
92
+ } finally {
93
+ // Always clean up temp env file
94
+ if (tempEnvPath) {
95
+ secureDeleteTempEnvFile(tempEnvPath);
96
+ }
97
+ }
98
+
99
+ // Success message
100
+ console.log(chalk.green('\nāœ… Update complete!\n'));
101
+ console.log(chalk.gray('Preserved:'));
102
+ console.log(chalk.gray(' āœ“ Database and WordPress data'));
103
+ console.log(chalk.gray(' āœ“ Volume mappings and configuration\n'));
104
+ console.log(chalk.cyan('Access your site at:'));
105
+ console.log(chalk.white(` https://${config.hostname}\n`));
106
+
107
+ } catch (err) {
108
+ console.error(chalk.red('\nāŒ Error:'), err.message);
109
+ process.exit(1);
110
+ }
111
+ }
112
+
113
+ export default updateCommand;
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@bostonuniversity/buwp-local",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "description": "Local WordPress development environment for Boston University projects",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "bin": {
8
- "buwp-local": "./bin/buwp-local.js"
8
+ "buwp-local": "bin/buwp-local.js"
9
9
  },
10
10
  "scripts": {
11
11
  "test": "echo \"Error: no test specified\" && exit 1",
package/.buwp-local.json DELETED
@@ -1,22 +0,0 @@
1
- {
2
- "projectName": "buwp-local",
3
- "image": "bu-wordpress:latest",
4
- "hostname": "jaydub.local",
5
- "multisite": true,
6
- "services": {
7
- "redis": true,
8
- "s3proxy": true,
9
- "shibboleth": true
10
- },
11
- "ports": {
12
- "http": 80,
13
- "https": 443,
14
- "db": 3306,
15
- "redis": 6379
16
- },
17
- "mappings": [],
18
- "env": {
19
- "WP_DEBUG": true,
20
- "XDEBUG": false
21
- }
22
- }