@bostonuniversity/buwp-local 0.7.2 → 0.7.4
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 +9 -0
- package/docs/CHANGELOG.md +13 -0
- package/docs/COMMANDS.md +76 -0
- package/docs/ROADMAP.md +57 -7
- package/lib/commands/watch-jobs.js +192 -0
- package/lib/compose-generator.js +2 -2
- package/package.json +1 -1
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';
|
|
@@ -108,6 +109,14 @@ program
|
|
|
108
109
|
.allowUnknownOption()
|
|
109
110
|
.action(wpCommand);
|
|
110
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
|
+
|
|
111
120
|
// Shell command
|
|
112
121
|
program
|
|
113
122
|
.command('shell')
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ 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.4]
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **Localhost-Only Port Binding for Database & Redis**
|
|
12
|
+
- Database and Redis services now bind to `127.0.0.1` instead of `0.0.0.0` for improved security and local development isolation
|
|
13
|
+
|
|
14
|
+
## [0.7.3]
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- `watch-jobs` command for automatic processing of site-manager jobs at regular intervals
|
|
18
|
+
- Configurable polling interval with `--interval` flag (default: 60 seconds, minimum: 10 seconds), and through `jobWatchInterval` in configuration file
|
|
19
|
+
- `--quiet` flag to suppress routine output for long-running background use
|
|
20
|
+
|
|
8
21
|
## [0.7.2]
|
|
9
22
|
|
|
10
23
|
### Breaking Changes
|
package/docs/COMMANDS.md
CHANGED
|
@@ -421,6 +421,82 @@ npx buwp-local keychain clear [--force]
|
|
|
421
421
|
|
|
422
422
|
---
|
|
423
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
|
+
|
|
424
500
|
## Global Options
|
|
425
501
|
|
|
426
502
|
These options work with all commands:
|
package/docs/ROADMAP.md
CHANGED
|
@@ -172,12 +172,65 @@ hostile.remove('127.0.0.1', config.hostname);
|
|
|
172
172
|
- Update command now properly releases volume locks before deletion
|
|
173
173
|
- Added `--preserve-wpbuild` flag for opt-out of WordPress volume refresh
|
|
174
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: 60 seconds)
|
|
180
|
+
- Runs as standalone process in terminal window
|
|
181
|
+
- Timestamped output for job processing visibility
|
|
182
|
+
- True quiet mode for long-running background monitoring
|
|
183
|
+
- Graceful shutdown (Ctrl+C)
|
|
184
|
+
|
|
185
|
+
**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.
|
|
186
|
+
|
|
187
|
+
**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.
|
|
188
|
+
|
|
189
|
+
### Shipped in v0.7.4
|
|
190
|
+
|
|
191
|
+
- **Localhost-Only Port Binding for Database & Redis** ✅
|
|
192
|
+
- Bind database (3306) and Redis (6379) ports to 127.0.0.1 only
|
|
193
|
+
- Prevents network exposure of confidential database content
|
|
194
|
+
- HTTP/HTTPS remain on all interfaces (0.0.0.0) for device testing
|
|
195
|
+
- Local database tools (TablePlus, Sequel Pro, etc.) still work perfectly
|
|
196
|
+
|
|
197
|
+
**Security Problem:** Default Docker port binding (`3306:3306`) exposes database on all network interfaces (0.0.0.0), including public WiFi. Confidential data accessible to anyone on the network.
|
|
198
|
+
|
|
199
|
+
**Solution:** Explicit localhost binding (`127.0.0.1:3306:3306`) restricts access to the laptop only. Network isolation provides defense-in-depth beyond password protection.
|
|
200
|
+
|
|
201
|
+
**Implementation:**
|
|
202
|
+
```javascript
|
|
203
|
+
// Database - localhost only (network isolated)
|
|
204
|
+
ports: [`127.0.0.1:${config.ports.db}:3306`]
|
|
205
|
+
|
|
206
|
+
// Redis - localhost only (session data protected)
|
|
207
|
+
ports: [`127.0.0.1:${config.ports.redis}:6379`]
|
|
208
|
+
|
|
209
|
+
// HTTP/HTTPS - all interfaces (device testing enabled)
|
|
210
|
+
ports: [`${config.ports.http}:80`, `${config.ports.https}:443`]
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Benefits:**
|
|
214
|
+
- Coffee shop/airport WiFi cannot reach database
|
|
215
|
+
- Brute-force attacks prevented by network isolation
|
|
216
|
+
- Zero performance impact
|
|
217
|
+
- Industry best practice (matching Laravel Sail, wp-env)
|
|
218
|
+
|
|
219
|
+
**Breaking Change Note:** Existing projects will need `buwp-local update` or restart to apply new port bindings. Database access from phones/tablets/other computers will no longer work (rare use case).
|
|
220
|
+
|
|
175
221
|
### Potential Features
|
|
176
222
|
|
|
177
|
-
- **
|
|
178
|
-
-
|
|
179
|
-
|
|
180
|
-
|
|
223
|
+
- **Ability to add custom WORDPRESS_CONFIG_EXTRA environment variables**
|
|
224
|
+
- Support for adding custom WP config snippets via env vars
|
|
225
|
+
|
|
226
|
+
- **Credential Export**
|
|
227
|
+
- Commands to export credentials to JSON file
|
|
228
|
+
- Useful for migrating between machines or sharing setup
|
|
229
|
+
|
|
230
|
+
- **Advanced Port Binding Configuration**
|
|
231
|
+
- Optional config to override localhost-only binding for database/Redis
|
|
232
|
+
- For advanced users who need network access to services
|
|
233
|
+
- Example: `"portBindings": { "db": "0.0.0.0", "redis": "127.0.0.1" }`
|
|
181
234
|
|
|
182
235
|
- **Xdebug Integration**
|
|
183
236
|
- Command to help generate Xdebug configuration for IDEs (VSCode, Zed)
|
|
@@ -207,9 +260,6 @@ hostile.remove('127.0.0.1', config.hostname);
|
|
|
207
260
|
- Credential issues → clear next steps
|
|
208
261
|
- Port conflicts → suggest alternatives
|
|
209
262
|
|
|
210
|
-
- **Multi project experience**
|
|
211
|
-
- 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.
|
|
212
|
-
|
|
213
263
|
- **Docker Volume management assistant**
|
|
214
264
|
- listing and cleanup of unused volumes
|
|
215
265
|
|
|
@@ -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;
|
package/lib/compose-generator.js
CHANGED
|
@@ -66,7 +66,7 @@ function generateDbService(config, dbVolumeName) {
|
|
|
66
66
|
MYSQL_PASSWORD: '${WORDPRESS_DB_PASSWORD:-password}',
|
|
67
67
|
MYSQL_ROOT_PASSWORD: '${DB_ROOT_PASSWORD:-rootpassword}'
|
|
68
68
|
},
|
|
69
|
-
ports: [
|
|
69
|
+
ports: [`127.0.0.1:${config.ports.db}:3306`],
|
|
70
70
|
networks: ['wp-network']
|
|
71
71
|
};
|
|
72
72
|
}
|
|
@@ -212,7 +212,7 @@ function generateRedisService(config) {
|
|
|
212
212
|
return {
|
|
213
213
|
image: 'redis:alpine',
|
|
214
214
|
restart: 'always',
|
|
215
|
-
ports: [
|
|
215
|
+
ports: [`127.0.0.1:${config.ports.redis}:6379`],
|
|
216
216
|
networks: ['wp-network']
|
|
217
217
|
};
|
|
218
218
|
}
|