@bostonuniversity/buwp-local 0.7.1 → 0.7.3

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
@@ -27,6 +27,7 @@ import destroyCommand from '../lib/commands/destroy.js';
27
27
  import updateCommand from '../lib/commands/update.js';
28
28
  import logsCommand from '../lib/commands/logs.js';
29
29
  import wpCommand from '../lib/commands/wp.js';
30
+ import watchJobsCommand from '../lib/commands/watch-jobs.js';
30
31
  import shellCommand from '../lib/commands/shell.js';
31
32
  import configCommand from '../lib/commands/config.js';
32
33
  import initCommand from '../lib/commands/init.js';
@@ -66,6 +67,7 @@ program
66
67
  .command('update')
67
68
  .description('Update Docker images and recreate containers')
68
69
  .option('--all', 'Update all service images (default: WordPress only)')
70
+ .option('--preserve-wpbuild', 'Preserve existing WordPress volume (prevents core file updates)')
69
71
  .action(updateCommand);
70
72
 
71
73
  // Logs command
@@ -107,6 +109,14 @@ program
107
109
  .allowUnknownOption()
108
110
  .action(wpCommand);
109
111
 
112
+ // Watch Jobs command
113
+ program
114
+ .command('watch-jobs')
115
+ .description('Watch and automatically process site-manager jobs')
116
+ .option('--interval <seconds>', `Polling interval in seconds (default: 300, min: 30)`)
117
+ .option('--quiet', 'Suppress output unless jobs are found')
118
+ .action(watchJobsCommand);
119
+
110
120
  // Shell command
111
121
  program
112
122
  .command('shell')
@@ -465,16 +465,78 @@ This architecture provides clean boundaries:
465
465
 
466
466
  | Concern | Location | Update Mechanism |
467
467
  |---------|----------|------------------|
468
- | WordPress core | Docker image | `docker pull` |
469
- | BU plugins | Docker image | `docker pull` |
470
- | BU theme | Docker image | `docker pull` |
468
+ | WordPress core | Docker image | `buwp-local update` |
469
+ | BU plugins | Docker image | `buwp-local update` |
470
+ | BU theme | Docker image | `buwp-local update` |
471
471
  | **Your plugin** | **Local filesystem** | **Your editor** |
472
472
  | **Your theme** | **Local filesystem** | **Your editor** |
473
- | Database | Docker volume | Persists across updates |
474
- | Uploads | Docker volume | Persists across updates |
473
+ | Database | Docker volume (db_data) | Persists across updates |
474
+ | **Uploads** | **S3 bucket** | **Via s3proxy** |
475
475
 
476
476
  Updates to WordPress never touch your development code. Updates to your code never require rebuilding WordPress.
477
477
 
478
+ ### S3 Upload Architecture
479
+
480
+ A critical design decision enables safe WordPress core updates: **all media uploads are stored in S3, not in the container filesystem**.
481
+
482
+ ```
483
+ WordPress Upload Flow:
484
+ ┌──────────────┐
485
+ │ User │ Uploads file via WP admin
486
+ └──────┬───────┘
487
+
488
+
489
+ ┌──────────────────────────────────┐
490
+ │ WordPress Container │
491
+ │ │
492
+ │ BU S3 Uploads Plugin │
493
+ │ (intercepts upload) │
494
+ └──────┬───────────────────────────┘
495
+
496
+
497
+ ┌──────────────────────────────────┐
498
+ │ s3proxy Container │
499
+ │ (AWS SigV4 authentication) │
500
+ └──────┬───────────────────────────┘
501
+
502
+
503
+ ┌──────────────────────────────────┐
504
+ │ S3 Bucket │
505
+ │ (permanent storage) │
506
+ └──────────────────────────────────┘
507
+ ```
508
+
509
+ **Key Configuration** (from `compose-generator.js`):
510
+
511
+ ```javascript
512
+ if (config.services.s3proxy) {
513
+ wpConfigExtra += "define('S3_UPLOADS_AUTOENABLE', true);\n";
514
+ wpConfigExtra += "define('S3_UPLOADS_DISABLE_REPLACE_UPLOAD_URL', true);\n";
515
+ }
516
+ ```
517
+
518
+ **What this means for updates:**
519
+
520
+ The `wp_build` volume contains **zero user data**:
521
+ - ✅ WordPress core files (disposable - from image)
522
+ - ✅ BU plugins/themes (disposable - from image)
523
+ - ❌ **No media uploads** (in S3 bucket)
524
+ - ❌ **No database** (separate db_data volume)
525
+ - ❌ **No custom code** (mapped from host filesystem)
526
+
527
+ **Result:** The `wp_build` volume is purely infrastructure and can be safely wiped during updates to refresh WordPress core files.
528
+
529
+ ```bash
530
+ # This is safe because:
531
+ # - Database preserved (db_data volume)
532
+ # - Uploads preserved (S3 bucket)
533
+ # - Your code preserved (local filesystem)
534
+ npx buwp-local update # Wipes wp_build, recreates from image
535
+ ```
536
+
537
+ **Contrast with traditional WordPress:**
538
+ In a standard WordPress installation, `wp-content/uploads/` contains irreplaceable user media files. In buwp-local, that directory is empty or contains only regenerable thumbnails/cache.
539
+
478
540
  For detailed migration guidance, see [MIGRATION_FROM_VM.md](MIGRATION_FROM_VM.md).
479
541
 
480
542
  ## Security Model
package/docs/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ 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.3]
9
+
10
+ ### Added
11
+ - `watch-jobs` command for automatic processing of site-manager jobs at regular intervals
12
+ - Configurable polling interval with `--interval` flag (default: 60 seconds, minimum: 10 seconds), and through `jobWatchInterval` in configuration file
13
+ - `--quiet` flag to suppress routine output for long-running background use
14
+
15
+ ## [0.7.2]
16
+
17
+ ### Breaking Changes
18
+ - **Volume Naming Fix:** Corrected double-prefixing bug in Docker volume names
19
+ - Old: `projectname_projectname_wp_build`
20
+ - New: `projectname_wp_build`
21
+ - **Migration required:** See [Migration Guide](docs/temp-breaking-0-7-2.md)
22
+
23
+ ### Added
24
+ - `buwp-local update` now properly refreshes WordPress core files from new images
25
+ - `--preserve-wpbuild` flag to opt-out of WordPress volume refresh during updates
26
+
27
+ ### Fixed
28
+ - Volume deletion during update now works correctly (containers released first)
29
+ - Docker Compose auto-prefixing no longer causes duplicated volume names
30
+
8
31
  ## [0.7.1]
9
32
 
10
33
  Documentation only release.
package/docs/COMMANDS.md CHANGED
@@ -117,7 +117,7 @@ npx buwp-local destroy --force
117
117
 
118
118
  ### `update`
119
119
 
120
- Update Docker images and recreate containers without losing data.
120
+ Update Docker images and refresh WordPress core files.
121
121
 
122
122
  ```bash
123
123
  npx buwp-local update [options]
@@ -125,33 +125,53 @@ npx buwp-local update [options]
125
125
 
126
126
  **Options:**
127
127
  - `--all` - Update all service images (default: WordPress image only)
128
+ - `--preserve-wpbuild` - Keep existing WordPress volume (prevents core file updates)
128
129
 
129
130
  **Examples:**
130
131
  ```bash
131
- # Update WordPress image only (recommended)
132
+ # Update WordPress core from new image (recommended)
132
133
  npx buwp-local update
133
134
 
134
135
  # Update all service images (Redis, S3 proxy, etc.)
135
136
  npx buwp-local update --all
137
+
138
+ # Preserve WordPress volume (skip core file refresh)
139
+ npx buwp-local update --preserve-wpbuild
136
140
  ```
137
141
 
138
142
  **What it does:**
139
143
  - Checks if environment exists and Docker is running
140
144
  - Pulls latest Docker images from registry
141
- - Recreates containers with new images using `--force-recreate`
145
+ - **Removes wp_build volume** to get fresh WordPress core files (unless `--preserve-wpbuild`)
146
+ - Recreates containers with new images
142
147
  - Loads credentials from Keychain and/or `.env.local`
143
- - **Preserves volumes** - Database and WordPress files untouched
144
- - Shows success message confirming what was preserved
148
+ - **Always preserves database** (separate volume)
149
+ - **Always preserves custom mapped code** (your local files)
150
+ - **Uploads safe** (stored in S3, not in container)
151
+
152
+ **What gets updated:**
153
+ - ✅ WordPress core files (wp-admin, wp-includes, core PHP files)
154
+ - ✅ BU plugins and themes bundled in image
155
+ - ✅ PHP/Apache configuration from image
156
+
157
+ **What's preserved:**
158
+ - ✅ Database content (posts, users, settings) - separate volume
159
+ - ✅ Your custom mapped code - lives on your Mac
160
+ - ✅ Media uploads - stored in S3 bucket
145
161
 
146
162
  **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
163
+ - Get latest WordPress security patches
164
+ - Pull updated BU plugins/themes from new image
165
+ - Test code against newer WordPress version
166
+ - Refresh environment without losing development work
150
167
 
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)
168
+ **Key difference from other commands:**
169
+ - `stop` → `start`: Reuses containers and volumes (no updates)
170
+ - `update`: Refreshes WordPress from image, preserves database
171
+ - `destroy`: Removes everything including database (nuclear option)
172
+
173
+ **Why it's safe:**
174
+ Because buwp-local uses S3 for media uploads (via s3proxy), the WordPress volume contains no user data - only WordPress core and BU infrastructure code. Your custom development code is mapped from your local filesystem and never touched.
155
175
 
156
176
  ---
157
177
 
@@ -401,6 +421,82 @@ npx buwp-local keychain clear [--force]
401
421
 
402
422
  ---
403
423
 
424
+ ### `watch-jobs`
425
+
426
+ Watch for and automatically process site-manager jobs at regular intervals.
427
+
428
+ ```bash
429
+ npx buwp-local watch-jobs [options]
430
+ ```
431
+
432
+ **Options:**
433
+ - `--interval <seconds>` - Polling interval in seconds (default: 60, min: 10)
434
+ - `--quiet` - Suppress all routine output (for long-running background use)
435
+
436
+ **Examples:**
437
+ ```bash
438
+ # Watch with default 1 minute interval
439
+ npx buwp-local watch-jobs
440
+
441
+ # Watch with custom 2 minute interval
442
+ npx buwp-local watch-jobs --interval 120
443
+
444
+ # Quiet mode (silent operation, check web UI for job status)
445
+ npx buwp-local watch-jobs --quiet
446
+
447
+ # Quiet mode with short interval for active monitoring
448
+ npx buwp-local watch-jobs --interval 20 --quiet
449
+ ```
450
+
451
+ **What it does:**
452
+ - Runs `wp site-manager process-jobs` inside the WordPress container at configured intervals
453
+ - **Default mode**: Shows timestamped output for all checks and job results
454
+ - **Quiet mode**: Silently processes jobs (check site-manager web UI for status)
455
+ - Continues running until stopped (Ctrl+C)
456
+ - Mirrors production cron/EventBridge behavior locally
457
+
458
+ **Use cases:**
459
+ - Automatic processing of jobs created via web UI
460
+
461
+ **Configuration:**
462
+
463
+ Optionally configure default interval in `.buwp-local.json`:
464
+ ```json
465
+ {
466
+ "projectName": "my-project",
467
+ "jobWatchInterval": 200
468
+ }
469
+ ```
470
+
471
+ Command-line `--interval` flag overrides config file setting.
472
+
473
+ **Requirements:**
474
+ - WordPress container must be running (`start` first)
475
+ - Site-manager plugin must be installed and activated
476
+
477
+ **Quiet mode behavior:**
478
+ - **Startup**: Shows configuration banner
479
+ - **Routine operation**: Completely silent - no output for checks or job processing
480
+ - **Critical errors**: Shows only failures requiring intervention (Docker stopped, environment missing)
481
+ - **Job status**: Check site-manager plugin web UI to see job results and history
482
+ - **Best for**: Long-running background monitoring (hours/days) without terminal noise
483
+
484
+ **Error handling:**
485
+ - **Default mode**: Shows all errors and warnings with timestamps
486
+ - **Quiet mode**: Silently retries transient errors (container restarts), only shows critical failures
487
+ - Graceful shutdown on SIGINT/SIGTERM
488
+
489
+ **Tips:**
490
+ - Run in separate terminal window for visibility
491
+ - Use **default mode** during active development to see what's happening
492
+ - Use **quiet mode** for set-it-and-forget-it background monitoring
493
+ - Reduce interval during active testing (e.g., `--interval 60`)
494
+ - Increase interval for background monitoring (e.g., `--interval 600`)
495
+
496
+ **⚠️ Note:** This is a development tool. Production environments should continue using cron/AWS EventBridge for job processing.
497
+
498
+ ---
499
+
404
500
  ## Global Options
405
501
 
406
502
  These options work with all commands:
package/docs/ROADMAP.md CHANGED
@@ -154,20 +154,69 @@ hostile.remove('127.0.0.1', config.hostname);
154
154
  **Focus:** Ease of use and visibility
155
155
 
156
156
  ### Shipped in v0.7.0
157
- - **Docker Image Update Command** 🎯 (Proposed for v0.7.0)
158
- - **Problem:** Stopping and restarting containers reuses existing images; newer images aren't pulled
159
- - **Solution:** Add `buwp-local update` command that:
160
- - Pulls latest Docker images from registry
161
- - Recreates containers with new images
162
- - Preserves volumes (database, WordPress data)
163
- - **Benefit:** Safe, explicit way to apply WordPress/service updates without `destroy`
164
- - **Implementation:** Wrapper around `docker-compose pull && docker-compose up -d --force-recreate`
157
+ - **Docker Image Update Command**
158
+ - Pull latest Docker images from registry
159
+ - Recreate containers with new images
160
+ - Preserve volumes (database, WordPress data)
161
+ - Safely apply WordPress and service updates without `destroy`
162
+ - Implementation: Stop containers, conditionally remove wp_build volume, start with new images
165
163
 
166
164
  ### Shipped in v0.7.1
167
165
  - **Documentation Improvements**
168
166
 
167
+ ### Shipped in v0.7.2
168
+ - **Volume Naming Fix** ✅
169
+ - Fixed double-prefixing bug in Docker volume names
170
+ - Old: `projectname_projectname_wp_build` → New: `projectname_wp_build`
171
+ - Corrected `compose-generator.js` to use simple names (Docker Compose handles prefixing)
172
+ - Update command now properly releases volume locks before deletion
173
+ - Added `--preserve-wpbuild` flag for opt-out of WordPress volume refresh
174
+
175
+ ### Shipped in v0.7.3
176
+
177
+ - **Job Watcher Command** 🚧
178
+ - New `watch-jobs` command to periodically run `wp site-manager process-jobs`
179
+ - Configurable polling interval (default: 5 minutes)
180
+ - Runs as standalone process in terminal window
181
+ - Timestamped output for job processing visibility
182
+ - Graceful shutdown (Ctrl+C)
183
+
184
+ **Problem:** Production environments use cron/AWS EventBridge to automatically process site-manager jobs (content migration, deployments). Local developers currently must manually run `npx buwp-local wp site-manager process-jobs` to see queued jobs complete.
185
+
186
+ **Solution:** Standalone `watch-jobs` command that runs indefinitely, polling for jobs at configurable intervals. Mirrors production behavior without requiring cron setup. Enables developers to use the site-manager web UI for content operations and see jobs complete automatically.
187
+
188
+ **Implementation location:** `lib/commands/watch-jobs.js`
189
+
190
+ **Configuration support:**
191
+ ```json
192
+ {
193
+ "jobWatchInterval": 60 // seconds, default 60 seconds
194
+ }
195
+ ```
196
+
197
+ **Command syntax:**
198
+ ```bash
199
+ buwp-local watch-jobs [--interval 200] [--quiet]
200
+ ```
201
+
202
+ **Technical considerations:**
203
+ - Requires WordPress container to be running
204
+ - Uses `docker compose exec` to run WP-CLI command
205
+ - Handles container stop/restart gracefully
206
+ - Minimal resource usage (sleeps between checks)
207
+ - Output includes timestamps for audit trail
208
+
209
+ **Future enhancement (v0.8.0+):** If widely adopted, consider adding `--watch-jobs` flag to `start` command for automatic background execution.
210
+
169
211
  ### Potential Features
170
212
 
213
+ - **Ability to add custom WORDPRESS_CONFIG_EXTRA environment variables**
214
+ - Support for adding custom WP config snippets via env vars
215
+
216
+ - **Credential Export**
217
+ - Commands to export credentials to JSON file
218
+ - Useful for migrating between machines or sharing setup
219
+
171
220
  - **Database Security**
172
221
  - Check database access on db port (e.g. `localhost:3306`)
173
222
  - Consider more stringent default database passwords
@@ -201,9 +250,6 @@ hostile.remove('127.0.0.1', config.hostname);
201
250
  - Credential issues → clear next steps
202
251
  - Port conflicts → suggest alternatives
203
252
 
204
- - **Multi project experience**
205
- - There is a problem when starting a new project when an existing project exists in docker but is stopped. When starting the new project, docker first starts the container for the stopped project for unknown reasons. If the new project uses the same ports, this causes conflicts. Need to investigate and resolve, projects should be isolated and not interfere with each other.
206
-
207
253
  - **Docker Volume management assistant**
208
254
  - listing and cleanup of unused volumes
209
255
 
@@ -269,6 +315,54 @@ Will be informed by feedback from initial small group of users and actual pain p
269
315
  ### Quality
270
316
  - Standardized help for all CLI commands
271
317
 
318
+ ### Code Structure
319
+
320
+ #### Refactor credential handling to avoid duplication
321
+ Suggested refactor:
322
+ ```javascript
323
+ // lib/docker-helpers.js (new file)
324
+ export function prepareDockerComposeEnv(projectPath, projectName) {
325
+ const credentials = loadKeychainCredentials();
326
+ let tempEnvPath = null;
327
+
328
+ if (Object.keys(credentials).length > 0) {
329
+ try {
330
+ tempEnvPath = createSecureTempEnvFile(credentials, projectName);
331
+ } catch (err) {
332
+ console.warn(chalk.yellow(`⚠️ Could not load keychain credentials: ${err.message}`));
333
+ }
334
+ }
335
+
336
+ const envFilePath = path.join(projectPath, ENV_FILE_NAME);
337
+ const envFileFlag = fs.existsSync(envFilePath) ? `--env-file ${envFilePath}` : '';
338
+ const tempEnvFileFlag = tempEnvPath ? `--env-file ${tempEnvPath}` : '';
339
+
340
+ return {
341
+ flags: `${tempEnvFileFlag} ${envFileFlag}`.trim(),
342
+ tempEnvPath,
343
+ cleanup: () => tempEnvPath && secureDeleteTempEnvFile(tempEnvPath)
344
+ };
345
+ }
346
+ ```
347
+
348
+ #### Centralize volume naming logic
349
+
350
+ Suggested refactor:
351
+ ```javascript
352
+ // lib/volume-naming.js (new file)
353
+ export function getVolumeNames(projectName) {
354
+ return {
355
+ db: `${projectName}_db_data`,
356
+ wp: `${projectName}_wp_build`
357
+ };
358
+ }
359
+
360
+ // Or simpler - just add to config.js
361
+ export function getWpVolumeName(projectName) {
362
+ return `${projectName}_wp_build`;
363
+ }
364
+ ```
365
+
272
366
  ---
273
367
 
274
368
  ## Decision Framework
@@ -0,0 +1,37 @@
1
+ # Temporary Breaking Changes in v0.7.2
2
+
3
+ Version 0.7.2 unwinds a problem where the project name was being duplicated in the volume names, e.g. `projectname_projectname_wp_build`. This requires a one-time manual migration of your Docker volumes from the old names to the new names.
4
+
5
+ ## Option 1: Simple Destruction and Recreation
6
+
7
+ If you don't need to preserve your database or any data in your WordPress volume, you can simply destroy and recreate your environment:
8
+
9
+ ```bash
10
+ npx buwp-local destroy
11
+ npx buwp-local start
12
+ ```
13
+
14
+ ## Manual Migration Steps
15
+
16
+ If you want to preserve your database and WordPress volume, follow these steps to rename your Docker volumes:
17
+
18
+ ```bash
19
+ # Stop environment
20
+ npx buwp-local stop
21
+
22
+ # Rename volumes manually
23
+ docker volume create projectname_db_data
24
+ docker volume create projectname_wp_build
25
+
26
+ # Copy data from old to new
27
+ docker run --rm -v projectname_projectname_db_data:/from -v projectname_db_data:/to alpine ash -c "cd /from && cp -av . /to"
28
+ docker run --rm -v projectname_projectname_wp_build:/from -v projectname_wp_build:/to alpine ash -c "cd /from && cp -av . /to"
29
+
30
+ # Update buwp-local and start
31
+ npm update @bostonuniversity/buwp-local
32
+ npx buwp-local start
33
+
34
+ # Verify, then remove old volumes
35
+ docker volume rm projectname_projectname_db_data projectname_projectname_wp_build
36
+
37
+ ```
@@ -57,9 +57,52 @@ async function updateCommand(options = {}) {
57
57
  process.exit(1);
58
58
  }
59
59
 
60
- // Step 2: Recreate containers with new images (preserves volumes)
60
+ // Step 2: Stop and remove containers to ensure new images are used
61
+ // Note: We use 'down' without -v flag to preserve all volumes
62
+ console.log(chalk.cyan('\n🛑 Stopping containers...'));
63
+ try {
64
+ // Remove containers but preserve volumes (no -v flag)
65
+ execSync(
66
+ `docker compose -p ${projectName} -f "${composePath}" down`,
67
+ {
68
+ cwd: composeDir,
69
+ stdio: 'inherit'
70
+ }
71
+ );
72
+ } catch (err) {
73
+ console.error(chalk.red('\n❌ Failed to stop containers'));
74
+ process.exit(1);
75
+ }
76
+
77
+ // Step 3: Conditionally remove wp_build volume to get fresh WordPress core
78
+ const preserveWpBuild = options.preserveWpbuild || false;
79
+
80
+ if (!preserveWpBuild) {
81
+ // Docker Compose prefixes volume names with project name
82
+ const wpVolumeName = `${projectName}_wp_build`;
83
+
84
+ console.log(chalk.cyan('\n🗑️ Removing WordPress volume to get fresh core files...'));
85
+ try {
86
+ execSync(
87
+ `docker volume rm ${wpVolumeName}`,
88
+ {
89
+ cwd: composeDir,
90
+ stdio: 'ignore'
91
+ }
92
+ );
93
+ console.log(chalk.green('✓ WordPress volume removed\n'));
94
+ } catch (err) {
95
+ // Volume might not exist - that's okay
96
+ console.log(chalk.yellow('⚠️ WordPress volume not found (will be created fresh)\n'));
97
+ }
98
+ } else {
99
+ console.log(chalk.yellow('\n⚠️ Preserving existing WordPress volume'));
100
+ console.log(chalk.gray('WordPress core files will NOT be updated from the image.\n'));
101
+ }
102
+
103
+ // Step 4: Start containers with new images
61
104
  // Need to pass environment variables just like start command does
62
- console.log(chalk.cyan('\n🔨 Recreating containers with new images...'));
105
+ console.log(chalk.cyan('🔨 Starting containers with new images...'));
63
106
 
64
107
  // Load keychain credentials and create secure temp env file if available
65
108
  let tempEnvPath = null;
@@ -80,14 +123,14 @@ async function updateCommand(options = {}) {
80
123
 
81
124
  try {
82
125
  execSync(
83
- `docker compose -p ${projectName} ${tempEnvFileFlag} ${envFileFlag} -f "${composePath}" up -d --force-recreate`,
126
+ `docker compose -p ${projectName} ${tempEnvFileFlag} ${envFileFlag} -f "${composePath}" up -d`,
84
127
  {
85
128
  cwd: composeDir,
86
129
  stdio: 'inherit'
87
130
  }
88
131
  );
89
132
  } catch (err) {
90
- console.error(chalk.red('\n❌ Failed to recreate containers'));
133
+ console.error(chalk.red('\n❌ Failed to start containers'));
91
134
  process.exit(1);
92
135
  } finally {
93
136
  // Always clean up temp env file
@@ -98,9 +141,17 @@ async function updateCommand(options = {}) {
98
141
 
99
142
  // Success message
100
143
  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'));
144
+
145
+ if (!preserveWpBuild) {
146
+ console.log(chalk.cyan('Updated:'));
147
+ console.log(chalk.gray(' ✓ WordPress core files (from new image)'));
148
+ console.log(chalk.gray(' ✓ BU plugins and themes (from new image)\n'));
149
+ }
150
+
151
+ console.log(chalk.cyan('Preserved:'));
152
+ console.log(chalk.gray(' ✓ Database (all content and settings)'));
153
+ console.log(chalk.gray(' ✓ Custom mapped code (your local files)'));
154
+ console.log(chalk.gray(' ✓ Uploads (stored in S3)\n'));
104
155
  console.log(chalk.cyan('Access your site at:'));
105
156
  console.log(chalk.white(` https://${config.hostname}\n`));
106
157
 
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Watch Jobs command - Periodically process site-manager jobs
3
+ * Mirrors production cron/EventBridge behavior for local development
4
+ */
5
+
6
+ import chalk from 'chalk';
7
+ import { execSync, exec } from 'child_process';
8
+ import path from 'path';
9
+ import fs from 'fs';
10
+ import { loadConfig } from '../config.js';
11
+
12
+ const DEFAULT_INTERVAL = 60; // 1 minute in seconds
13
+ const MIN_INTERVAL = 10; // Minimum 10 seconds
14
+
15
+ /**
16
+ * Format timestamp for log output
17
+ */
18
+ function timestamp() {
19
+ const now = new Date();
20
+ return now.toLocaleString('en-US', {
21
+ year: 'numeric',
22
+ month: '2-digit',
23
+ day: '2-digit',
24
+ hour: '2-digit',
25
+ minute: '2-digit',
26
+ second: '2-digit',
27
+ hour12: false
28
+ }).replace(',', '');
29
+ }
30
+
31
+ /**
32
+ * Check if container is running
33
+ */
34
+ function isContainerRunning(projectName, composePath) {
35
+ try {
36
+ const result = execSync(
37
+ `docker compose -p ${projectName} -f "${composePath}" ps --status running --services`,
38
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }
39
+ );
40
+ return result.includes('wordpress');
41
+ } catch (err) {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Process site-manager jobs
48
+ */
49
+ async function processJobs(projectName, composePath, quiet) {
50
+ return new Promise((resolve) => {
51
+ const composeDir = path.dirname(composePath);
52
+ const command = `docker compose -p ${projectName} -f "${composePath}" exec -T wordpress wp site-manager process-jobs`;
53
+
54
+ if (!quiet) {
55
+ console.log(chalk.gray(`[${timestamp()}] Checking for jobs...`));
56
+ }
57
+
58
+ exec(command, { cwd: composeDir }, (error, stdout, stderr) => {
59
+ if (error) {
60
+ // In quiet mode, silently retry on transient errors (container might restart)
61
+ // Only verbose mode shows these errors
62
+ if (!quiet) {
63
+ if (stderr.includes('container') || stderr.includes('not running')) {
64
+ console.log(chalk.yellow(`[${timestamp()}] ⚠️ Container not running. Waiting...`));
65
+ } else {
66
+ console.log(chalk.red(`[${timestamp()}] ❌ Error executing command:`));
67
+ console.log(chalk.red(stderr || error.message));
68
+ }
69
+ }
70
+ resolve(false);
71
+ return;
72
+ }
73
+
74
+ const output = stdout.trim();
75
+
76
+ // Check if jobs were found and processed
77
+ if (output && output.length > 0) {
78
+ // In quiet mode, suppress all output (user checks web UI for job status)
79
+ // In verbose mode, show full job output
80
+ if (!quiet) {
81
+ console.log(chalk.green(`[${timestamp()}] ✓ Processing jobs:`));
82
+ console.log(output);
83
+ }
84
+ resolve(true);
85
+ } else if (!quiet) {
86
+ // No jobs found - only show in verbose mode
87
+ console.log(chalk.gray(`[${timestamp()}] No jobs found.`));
88
+ resolve(false);
89
+ } else {
90
+ // Quiet mode and no jobs - silent
91
+ resolve(false);
92
+ }
93
+ });
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Watch jobs command
99
+ */
100
+ async function watchJobsCommand(options) {
101
+ try {
102
+ const projectPath = process.cwd();
103
+ const composePath = path.join(projectPath, '.buwp-local', 'docker-compose.yml');
104
+
105
+ // Check if docker-compose.yml exists
106
+ if (!fs.existsSync(composePath)) {
107
+ console.log(chalk.yellow('⚠️ No running environment found.'));
108
+ console.log(chalk.gray('Run "buwp-local start" to create an environment.\n'));
109
+ return;
110
+ }
111
+
112
+ // Load config to get project name and optional interval setting
113
+ const config = loadConfig(projectPath);
114
+ const projectName = config.projectName || 'buwp-local';
115
+
116
+ // Determine interval (priority: CLI flag > config file > default)
117
+ let interval = DEFAULT_INTERVAL;
118
+ if (options.interval) {
119
+ interval = parseInt(options.interval, 10);
120
+ if (isNaN(interval) || interval < MIN_INTERVAL) {
121
+ console.log(chalk.red(`❌ Invalid interval. Minimum is ${MIN_INTERVAL} seconds.`));
122
+ process.exit(1);
123
+ }
124
+ } else if (config.jobWatchInterval) {
125
+ interval = config.jobWatchInterval;
126
+ if (interval < MIN_INTERVAL) {
127
+ console.log(chalk.yellow(`⚠️ Config interval too low. Using minimum: ${MIN_INTERVAL}s`));
128
+ interval = MIN_INTERVAL;
129
+ }
130
+ }
131
+
132
+ const quiet = options.quiet || false;
133
+
134
+ // Check if Docker is running
135
+ try {
136
+ execSync('docker info', { stdio: 'ignore' });
137
+ } catch (err) {
138
+ console.error(chalk.red('❌ Docker is not running.'));
139
+ console.log(chalk.gray('Start Docker Desktop and try again.'));
140
+ process.exit(1);
141
+ }
142
+
143
+ // Check if container is running initially
144
+ if (!isContainerRunning(projectName, composePath)) {
145
+ console.log(chalk.yellow('⚠️ WordPress container is not running.'));
146
+ console.log(chalk.gray('Run "buwp-local start" first, then try again.\n'));
147
+ return;
148
+ }
149
+
150
+ // Display startup message
151
+ console.log(chalk.cyan('🔍 Watching for site-manager jobs...'));
152
+ console.log(chalk.gray(` Interval: ${interval}s`));
153
+ console.log(chalk.gray(` Project: ${projectName}`));
154
+ console.log(chalk.gray(` Mode: ${quiet ? 'quiet' : 'verbose'}`));
155
+ console.log(chalk.gray('\nPress Ctrl+C to stop\n'));
156
+
157
+ // Set up graceful shutdown
158
+ let isShuttingDown = false;
159
+ const shutdown = () => {
160
+ if (isShuttingDown) return;
161
+ isShuttingDown = true;
162
+ console.log(chalk.cyan('\n\n👋 Stopping job watcher...'));
163
+ process.exit(0);
164
+ };
165
+
166
+ process.on('SIGINT', shutdown);
167
+ process.on('SIGTERM', shutdown);
168
+
169
+ // Main watch loop
170
+ const watch = async () => {
171
+ // Check if we should stop
172
+ if (isShuttingDown) return;
173
+
174
+ // Process jobs
175
+ await processJobs(projectName, composePath, quiet);
176
+
177
+ // Schedule next check
178
+ if (!isShuttingDown) {
179
+ setTimeout(watch, interval * 1000);
180
+ }
181
+ };
182
+
183
+ // Start watching
184
+ await watch();
185
+
186
+ } catch (err) {
187
+ console.error(chalk.red('\n❌ Error:'), err.message);
188
+ process.exit(1);
189
+ }
190
+ }
191
+
192
+ export default watchJobsCommand;
@@ -13,10 +13,9 @@ import path from 'path';
13
13
  * @returns {object} Docker Compose configuration object
14
14
  */
15
15
  function generateComposeConfig(config) {
16
- // Use project name for unique volume naming
17
- const projectName = config.projectName || 'buwp-local';
18
- const dbVolumeName = `${projectName}_db_data`;
19
- const wpVolumeName = `${projectName}_wp_build`;
16
+ // Volume names without project prefix - Docker Compose will add the prefix automatically
17
+ const dbVolumeName = 'db_data';
18
+ const wpVolumeName = 'wp_build';
20
19
 
21
20
  const composeConfig = {
22
21
  services: {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bostonuniversity/buwp-local",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Local WordPress development environment for Boston University projects",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",