@bostonuniversity/buwp-local 0.1.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.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Logs command - View logs from the WordPress environment
3
+ */
4
+
5
+ const chalk = require('chalk');
6
+ const { execSync } = require('child_process');
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+ const { loadConfig } = require('../config');
10
+
11
+ async function logsCommand(options) {
12
+ console.log(chalk.blue('šŸ“‹ Viewing logs...\n'));
13
+
14
+ try {
15
+ const projectPath = process.cwd();
16
+ const composePath = path.join(projectPath, '.buwp-local', 'docker-compose.yml');
17
+
18
+ // Check if docker-compose.yml exists
19
+ if (!fs.existsSync(composePath)) {
20
+ console.log(chalk.yellow('āš ļø No running environment found.'));
21
+ console.log(chalk.gray('Run "buwp-local start" to create an environment.\n'));
22
+ return;
23
+ }
24
+
25
+ // Load config to get project name
26
+ const config = loadConfig(projectPath);
27
+ const projectName = config.projectName || 'buwp-local';
28
+
29
+ // Check if Docker is running
30
+ try {
31
+ execSync('docker info', { stdio: 'ignore' });
32
+ } catch (err) {
33
+ console.error(chalk.red('āŒ Docker is not running.'));
34
+ process.exit(1);
35
+ }
36
+
37
+ // Build docker compose logs command
38
+ const composeDir = path.dirname(composePath);
39
+ let command = `docker compose -p ${projectName} -f ${composePath} logs`;
40
+
41
+ if (options.follow) {
42
+ command += ' -f';
43
+ }
44
+
45
+ if (options.service) {
46
+ command += ` ${options.service}`;
47
+ }
48
+
49
+ // Execute logs command
50
+ try {
51
+ execSync(command, {
52
+ cwd: composeDir,
53
+ stdio: 'inherit'
54
+ });
55
+ } catch (err) {
56
+ // User likely pressed Ctrl+C to exit, which is expected
57
+ console.log(chalk.gray('\nLogs closed.\n'));
58
+ }
59
+
60
+ } catch (err) {
61
+ console.error(chalk.red('\nāŒ Error:'), err.message);
62
+ process.exit(1);
63
+ }
64
+ }
65
+
66
+ module.exports = logsCommand;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Start command - Starts the local WordPress environment
3
+ */
4
+
5
+ const chalk = require('chalk');
6
+ const { execSync } = require('child_process');
7
+ const path = require('path');
8
+ const { loadConfig, validateConfig } = require('../config');
9
+ const { generateComposeFile } = require('../compose-generator');
10
+
11
+ async function startCommand(options) {
12
+ console.log(chalk.blue('šŸš€ Starting BU WordPress local environment...\n'));
13
+
14
+ try {
15
+ // Load configuration
16
+ const projectPath = process.cwd();
17
+ let config = loadConfig(projectPath);
18
+
19
+ // Apply command-line options
20
+ if (options.xdebug) {
21
+ config.env = config.env || {};
22
+ config.env.XDEBUG = true;
23
+ }
24
+
25
+ if (options.s3 === false) {
26
+ config.services.s3proxy = false;
27
+ }
28
+
29
+ if (options.redis === false) {
30
+ config.services.redis = false;
31
+ }
32
+
33
+ // Validate configuration
34
+ console.log(chalk.gray('Validating configuration...'));
35
+ const validation = validateConfig(config);
36
+
37
+ if (!validation.valid) {
38
+ console.error(chalk.red('\nāŒ Configuration validation failed:'));
39
+ validation.errors.forEach(error => {
40
+ console.error(chalk.red(` - ${error}`));
41
+ });
42
+ process.exit(1);
43
+ }
44
+
45
+ // Generate docker-compose.yml
46
+ console.log(chalk.gray('Generating docker-compose.yml...'));
47
+ const composePath = generateComposeFile(config, projectPath);
48
+ console.log(chalk.green(`āœ“ Generated ${composePath}\n`));
49
+
50
+ // Check if Docker is running
51
+ try {
52
+ execSync('docker info', { stdio: 'ignore' });
53
+ } catch (err) {
54
+ console.error(chalk.red('āŒ Docker is not running. Please start Docker Desktop and try again.'));
55
+ process.exit(1);
56
+ }
57
+
58
+ // Start Docker Compose
59
+ console.log(chalk.gray('Starting Docker containers...\n'));
60
+ const composeDir = path.dirname(composePath);
61
+ const projectName = config.projectName || 'buwp-local';
62
+
63
+ try {
64
+ execSync(
65
+ `docker compose -p ${projectName} -f ${composePath} up -d`,
66
+ {
67
+ cwd: composeDir,
68
+ stdio: 'inherit'
69
+ }
70
+ );
71
+ } catch (err) {
72
+ console.error(chalk.red('\nāŒ Failed to start Docker containers'));
73
+ process.exit(1);
74
+ }
75
+
76
+ // Success message
77
+ console.log(chalk.green('\nāœ… Environment started successfully!\n'));
78
+ console.log(chalk.cyan(`Project: ${projectName}`));
79
+ console.log(chalk.cyan('Access your site at:'));
80
+ console.log(chalk.white(` http://${config.hostname}`));
81
+ console.log(chalk.white(` https://${config.hostname}\n`));
82
+
83
+ console.log(chalk.gray('Useful commands:'));
84
+ console.log(chalk.white(' buwp-local logs - View logs'));
85
+ console.log(chalk.white(' buwp-local stop - Stop environment'));
86
+ console.log(chalk.white(' buwp-local destroy - Remove environment\n'));
87
+
88
+ // Reminder about /etc/hosts
89
+ console.log(chalk.yellow('āš ļø Remember to add this to your /etc/hosts file:'));
90
+ console.log(chalk.white(` 127.0.0.1 ${config.hostname}\n`));
91
+
92
+ } catch (err) {
93
+ console.error(chalk.red('\nāŒ Error:'), err.message);
94
+ process.exit(1);
95
+ }
96
+ }
97
+
98
+ module.exports = startCommand;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Stop command - Stops the local WordPress environment
3
+ */
4
+
5
+ const chalk = require('chalk');
6
+ const { execSync } = require('child_process');
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+ const { loadConfig } = require('../config');
10
+
11
+ async function stopCommand() {
12
+ console.log(chalk.blue('šŸ›‘ Stopping BU WordPress local environment...\n'));
13
+
14
+ try {
15
+ const projectPath = process.cwd();
16
+ const composePath = path.join(projectPath, '.buwp-local', 'docker-compose.yml');
17
+
18
+ // Check if docker-compose.yml exists
19
+ if (!fs.existsSync(composePath)) {
20
+ console.log(chalk.yellow('āš ļø No running environment found.'));
21
+ console.log(chalk.gray('Run "buwp-local start" to create an environment.\n'));
22
+ return;
23
+ }
24
+
25
+ // Load config to get project name
26
+ const config = loadConfig(projectPath);
27
+ const projectName = config.projectName || 'buwp-local';
28
+
29
+ // Check if Docker is running
30
+ try {
31
+ execSync('docker info', { stdio: 'ignore' });
32
+ } catch (err) {
33
+ console.error(chalk.red('āŒ Docker is not running.'));
34
+ process.exit(1);
35
+ }
36
+
37
+ // Stop Docker Compose
38
+ const composeDir = path.dirname(composePath);
39
+
40
+ try {
41
+ execSync(
42
+ `docker compose -p ${projectName} -f ${composePath} stop`,
43
+ {
44
+ cwd: composeDir,
45
+ stdio: 'inherit'
46
+ }
47
+ );
48
+ } catch (err) {
49
+ console.error(chalk.red('\nāŒ Failed to stop Docker containers'));
50
+ process.exit(1);
51
+ }
52
+
53
+ console.log(chalk.green('\nāœ… Environment stopped successfully!\n'));
54
+ console.log(chalk.gray('Use "buwp-local start" to start it again.\n'));
55
+
56
+ } catch (err) {
57
+ console.error(chalk.red('\nāŒ Error:'), err.message);
58
+ process.exit(1);
59
+ }
60
+ }
61
+
62
+ module.exports = stopCommand;
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Docker Compose file generator
3
+ * Generates docker-compose.yml from configuration using js-yaml
4
+ */
5
+
6
+ const yaml = require('js-yaml');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const os = require('os');
10
+
11
+ /**
12
+ * Generate docker-compose configuration from buwp-local config
13
+ * @param {object} config - buwp-local configuration
14
+ * @returns {object} Docker Compose configuration object
15
+ */
16
+ function generateComposeConfig(config) {
17
+ // Use project name for unique volume naming
18
+ const projectName = config.projectName || 'buwp-local';
19
+ const dbVolumeName = `${projectName}_db_data`;
20
+ const wpVolumeName = `${projectName}_wp_build`;
21
+
22
+ const composeConfig = {
23
+ services: {},
24
+ networks: {
25
+ 'wp-network': {
26
+ driver: 'bridge'
27
+ }
28
+ },
29
+ volumes: {
30
+ [dbVolumeName]: null,
31
+ [wpVolumeName]: null
32
+ }
33
+ };
34
+
35
+ // Database service
36
+ composeConfig.services.db = generateDbService(config, dbVolumeName);
37
+
38
+ // WordPress service
39
+ composeConfig.services.wordpress = generateWordPressService(config, wpVolumeName);
40
+
41
+ // S3 proxy service (if enabled)
42
+ if (config.services.s3proxy) {
43
+ composeConfig.services.s3proxy = generateS3ProxyService(config);
44
+ }
45
+
46
+ // Redis service (if enabled)
47
+ if (config.services.redis) {
48
+ composeConfig.services.redis = generateRedisService(config);
49
+ }
50
+
51
+ return composeConfig;
52
+ }
53
+
54
+ /**
55
+ * Generate database service configuration
56
+ * @param {object} config - buwp-local configuration
57
+ * @param {string} dbVolumeName - Name of the database volume
58
+ * @returns {object} Database service config
59
+ */
60
+ function generateDbService(config, dbVolumeName) {
61
+ const dbPassword = config.db?.password || 'password';
62
+ const rootPassword = config.db?.rootPassword || 'rootpassword';
63
+
64
+ return {
65
+ image: 'mariadb:latest',
66
+ restart: 'always',
67
+ volumes: [`${dbVolumeName}:/var/lib/mysql`],
68
+ environment: {
69
+ MYSQL_DATABASE: 'wordpress',
70
+ MYSQL_USER: 'wordpress',
71
+ MYSQL_PASSWORD: dbPassword,
72
+ MYSQL_ROOT_PASSWORD: rootPassword
73
+ },
74
+ ports: [`${config.ports.db}:3306`],
75
+ networks: ['wp-network']
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Generate WordPress service configuration
81
+ * @param {object} config - buwp-local configuration
82
+ * @param {string} wpVolumeName - Name of the WordPress volume
83
+ * @returns {object} WordPress service config
84
+ */
85
+ function generateWordPressService(config, wpVolumeName) {
86
+ const depends_on = ['db'];
87
+
88
+ if (config.services.s3proxy) depends_on.push('s3proxy');
89
+ if (config.services.redis) depends_on.push('redis');
90
+
91
+ // Build environment variables
92
+ const environment = {
93
+ WORDPRESS_DB_HOST: 'db:3306',
94
+ WORDPRESS_DB_USER: 'wordpress',
95
+ WORDPRESS_DB_PASSWORD: config.db?.password || 'password',
96
+ WORDPRESS_DB_NAME: 'wordpress',
97
+ WORDPRESS_DEBUG: config.env?.WP_DEBUG || '0',
98
+ SERVER_NAME: config.hostname,
99
+ HTTP_HOST: config.hostname,
100
+ MULTISITE: config.multisite ? 'true' : 'false',
101
+ XDEBUG: config.env?.XDEBUG || 'false',
102
+ WP_CLI_ALLOW_ROOT: 'true',
103
+ TZ: 'America/New_York'
104
+ };
105
+
106
+ // Add Shibboleth config if enabled
107
+ if (config.services.shibboleth) {
108
+ environment.SP_ENTITY_ID = config.shibboleth?.entityId || '';
109
+ environment.IDP_ENTITY_ID = config.shibboleth?.idpEntityId || '';
110
+ environment.SHIB_IDP_LOGOUT = config.shibboleth?.idpLogout || '';
111
+ environment.SHIB_SP_KEY = config.shibboleth?.spKey || '';
112
+ environment.SHIB_SP_CERT = config.shibboleth?.spCert || '';
113
+ }
114
+
115
+ // Add S3 config if enabled
116
+ if (config.services.s3proxy) {
117
+ environment.S3PROXY_HOST = 'http://s3proxy:8080';
118
+ }
119
+
120
+ // Add Redis config if enabled
121
+ if (config.services.redis) {
122
+ environment.REDIS_HOST = 'redis';
123
+ environment.REDIS_PORT = '6379';
124
+ }
125
+
126
+ // Build WordPress config extra
127
+ let wpConfigExtra = '';
128
+
129
+ if (config.multisite) {
130
+ wpConfigExtra += "define('MULTISITE', true);\n";
131
+ wpConfigExtra += "define('SUBDOMAIN_INSTALL', false);\n";
132
+ }
133
+
134
+ if (config.services.s3proxy && config.s3) {
135
+ wpConfigExtra += `define('S3_UPLOADS_BUCKET', '${config.s3.bucket || ''}');\n`;
136
+ wpConfigExtra += `define('S3_UPLOADS_REGION', '${config.s3.region || 'us-east-1'}');\n`;
137
+ wpConfigExtra += `define('S3_UPLOADS_KEY', '${config.s3.accessKeyId || ''}');\n`;
138
+ wpConfigExtra += `define('S3_UPLOADS_SECRET', '${config.s3.secretAccessKey || ''}');\n`;
139
+ wpConfigExtra += `define('ACCESS_RULES_TABLE', '${config.s3.accessRulesTable || ''}');\n`;
140
+ wpConfigExtra += "define('S3_UPLOADS_OBJECT_ACL', null);\n";
141
+ wpConfigExtra += "define('S3_UPLOADS_AUTOENABLE', true);\n";
142
+ wpConfigExtra += "define('S3_UPLOADS_DISABLE_REPLACE_UPLOAD_URL', true);\n";
143
+ }
144
+
145
+ // Add BU_INCLUDES_PATH definition; this seems bad and should be unwound, but various things depend on it right now.
146
+ wpConfigExtra += `define( 'BU_INCLUDES_PATH', '/var/www/html/bu-includes' );\n`;
147
+
148
+ // Add custom env vars
149
+ Object.entries(config.env || {}).forEach(([key, value]) => {
150
+ if (!environment[key]) {
151
+ environment[key] = String(value);
152
+ }
153
+ });
154
+
155
+ if (wpConfigExtra) {
156
+ environment.WORDPRESS_CONFIG_EXTRA = wpConfigExtra;
157
+ }
158
+
159
+ // Build volumes array
160
+ const volumes = [`${wpVolumeName}:/var/www/html`];
161
+
162
+ // Add custom mappings
163
+ if (config.mappings && Array.isArray(config.mappings)) {
164
+ config.mappings.forEach(mapping => {
165
+ const localPath = path.resolve(mapping.local);
166
+ volumes.push(`${localPath}:${mapping.container}`);
167
+ });
168
+ }
169
+
170
+ return {
171
+ image: config.image,
172
+ depends_on,
173
+ restart: 'always',
174
+ ports: [
175
+ `${config.ports.http}:80`,
176
+ `${config.ports.https}:443`
177
+ ],
178
+ hostname: config.hostname,
179
+ environment,
180
+ volumes,
181
+ networks: ['wp-network']
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Generate S3 proxy service configuration
187
+ * @param {object} config - buwp-local configuration
188
+ * @returns {object} S3 proxy service config
189
+ */
190
+ function generateS3ProxyService(config) {
191
+ const region = config.olap?.region || config.s3?.region || 'us-east-1';
192
+ const olapName = config.olap?.name || '';
193
+ const olapAcctNbr = config.olap?.accountNumber || '';
194
+
195
+ return {
196
+ image: 'public.ecr.aws/bostonuniversity-nonprod/aws-sigv4-proxy',
197
+ restart: 'always',
198
+ command: [
199
+ '-v',
200
+ '--name',
201
+ 's3-object-lambda',
202
+ '--region',
203
+ region,
204
+ '--no-verify-ssl',
205
+ '--host',
206
+ `${olapName}-${olapAcctNbr}.s3-object-lambda.${region}.amazonaws.com`
207
+ ],
208
+ environment: {
209
+ healthcheck_path: '/s3proxy-healthcheck',
210
+ AWS_ACCESS_KEY_ID: config.s3?.accessKeyId || '',
211
+ AWS_SECRET_ACCESS_KEY: config.s3?.secretAccessKey || '',
212
+ REGION: region
213
+ },
214
+ networks: ['wp-network']
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Generate Redis service configuration
220
+ * @param {object} config - buwp-local configuration
221
+ * @returns {object} Redis service config
222
+ */
223
+ function generateRedisService(config) {
224
+ return {
225
+ image: 'redis:alpine',
226
+ restart: 'always',
227
+ ports: [`${config.ports.redis}:6379`],
228
+ networks: ['wp-network']
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Write docker-compose.yml file
234
+ * @param {object} composeConfig - Docker Compose configuration
235
+ * @param {string} outputPath - Path to write docker-compose.yml
236
+ * @returns {string} Path to written file
237
+ */
238
+ function writeComposeFile(composeConfig, outputPath) {
239
+ const yamlContent = yaml.dump(composeConfig, {
240
+ indent: 2,
241
+ lineWidth: -1,
242
+ noRefs: true
243
+ });
244
+
245
+ const fileContent = `# Generated by buwp-local
246
+ # Do not edit this file directly - it will be overwritten
247
+ # Edit .buwp-local.json instead
248
+
249
+ ${yamlContent}`;
250
+
251
+ fs.writeFileSync(outputPath, fileContent, 'utf8');
252
+
253
+ return outputPath;
254
+ }
255
+
256
+ /**
257
+ * Generate and write docker-compose.yml from config
258
+ * @param {object} config - buwp-local configuration
259
+ * @param {string} projectPath - Project directory path
260
+ * @returns {string} Path to generated docker-compose.yml
261
+ */
262
+ function generateComposeFile(config, projectPath = process.cwd()) {
263
+ // Create .buwp-local directory if it doesn't exist
264
+ const stateDir = path.join(projectPath, '.buwp-local');
265
+ if (!fs.existsSync(stateDir)) {
266
+ fs.mkdirSync(stateDir, { recursive: true });
267
+ }
268
+
269
+ const composePath = path.join(stateDir, 'docker-compose.yml');
270
+ const composeConfig = generateComposeConfig(config);
271
+
272
+ return writeComposeFile(composeConfig, composePath);
273
+ }
274
+
275
+ module.exports = {
276
+ generateComposeConfig,
277
+ generateComposeFile,
278
+ writeComposeFile
279
+ };