@bostonuniversity/buwp-local 0.4.0 → 0.5.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,613 @@
1
+ /**
2
+ * Keychain command - Manage credentials in macOS keychain
3
+ */
4
+
5
+ import chalk from 'chalk';
6
+ import prompts from 'prompts';
7
+ import fs from 'fs';
8
+ import {
9
+ isPlatformSupported,
10
+ setCredential,
11
+ getCredential,
12
+ hasCredential,
13
+ listCredentials,
14
+ clearAllCredentials,
15
+ isMultilineCredential,
16
+ parseCredentialsFile,
17
+ CREDENTIAL_KEYS,
18
+ CREDENTIAL_GROUPS,
19
+ CREDENTIAL_DESCRIPTIONS,
20
+ MULTILINE_CREDENTIALS
21
+ } from '../keychain.js';
22
+
23
+ /**
24
+ * Main keychain command handler
25
+ * @param {string} subcommand - Subcommand to execute
26
+ * @param {string[]} args - Additional arguments
27
+ * @param {object} options - Command options
28
+ */
29
+ async function keychainCommand(subcommand, args, options) {
30
+ // Check platform support first
31
+ if (!isPlatformSupported()) {
32
+ console.log(chalk.yellow('⚠️ Keychain integration is only available on macOS.\n'));
33
+ console.log(chalk.gray('On this platform, please use .env.local for credential storage.\n'));
34
+ process.exit(1);
35
+ }
36
+
37
+ try {
38
+ switch (subcommand) {
39
+ case 'setup':
40
+ await setupCommand(options);
41
+ break;
42
+ case 'set':
43
+ await setCommand(args, options);
44
+ break;
45
+ case 'get':
46
+ await getCommand(args);
47
+ break;
48
+ case 'list':
49
+ await listCommand();
50
+ break;
51
+ case 'clear':
52
+ await clearCommand(options);
53
+ break;
54
+ case 'status':
55
+ await statusCommand();
56
+ break;
57
+ default:
58
+ showHelp();
59
+ }
60
+ } catch (err) {
61
+ console.error(chalk.red('\n❌ Error:'), err.message);
62
+ process.exit(1);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Interactive setup - prompts for all credentials
68
+ * @param {object} options - Command options
69
+ */
70
+ async function setupCommand(options) {
71
+ console.log(chalk.blue('🔐 Keychain Credential Setup\n'));
72
+
73
+ // Check if bulk import from file
74
+ if (options.file) {
75
+ await bulkImportFromFile(options.file, options.force);
76
+ return;
77
+ }
78
+
79
+ // Interactive mode
80
+ console.log(chalk.yellow('⚠️ macOS may prompt you to allow Node.js access to your keychain.'));
81
+ console.log(chalk.yellow(' Click "Always Allow" to avoid repeated prompts.\n'));
82
+ console.log(chalk.gray('This will store credentials securely in your macOS keychain.'));
83
+ console.log(chalk.gray('All buwp-local projects will use these credentials by default.\n'));
84
+
85
+ // Check for existing credentials
86
+ const existingKeys = listCredentials();
87
+ if (existingKeys.length > 0) {
88
+ console.log(chalk.yellow(`Found ${existingKeys.length} existing credential(s) in keychain.`));
89
+ const { shouldOverwrite } = await prompts({
90
+ type: 'confirm',
91
+ name: 'shouldOverwrite',
92
+ message: 'Overwrite existing credentials?',
93
+ initial: false
94
+ });
95
+
96
+ if (!shouldOverwrite) {
97
+ console.log(chalk.gray('\nSetup cancelled.\n'));
98
+ return;
99
+ }
100
+ console.log('');
101
+ }
102
+
103
+ // Prompt for each credential group
104
+ const credentials = {};
105
+ let totalStored = 0;
106
+
107
+ for (const [groupName, keys] of Object.entries(CREDENTIAL_GROUPS)) {
108
+ console.log(chalk.cyan(`\n📋 ${groupName.toUpperCase()} Credentials`));
109
+ console.log(chalk.gray('━'.repeat(50)));
110
+
111
+ for (const key of keys) {
112
+ const description = CREDENTIAL_DESCRIPTIONS[key];
113
+ const existing = hasCredential(key);
114
+ const isMultiline = isMultilineCredential(key);
115
+
116
+ const prompt = existing
117
+ ? `${description} (currently stored)`
118
+ : description;
119
+
120
+ if (isMultiline) {
121
+ // Handle multiline credentials with file path input only
122
+ console.log(chalk.yellow(`\n⚠️ ${key} is a multiline credential (cryptographic key/certificate).`));
123
+ console.log(chalk.gray('Provide the file path to your key/certificate file.\n'));
124
+
125
+ const { filePath } = await prompts({
126
+ type: 'text',
127
+ name: 'filePath',
128
+ message: `File path for ${description}`,
129
+ validate: val => {
130
+ if (!val || !val.trim()) return 'File path cannot be empty';
131
+ const trimmed = val.trim();
132
+ if (!fs.existsSync(trimmed)) return `File not found: ${trimmed}`;
133
+ try {
134
+ fs.accessSync(trimmed, fs.constants.R_OK);
135
+ return true;
136
+ } catch {
137
+ return 'File is not readable';
138
+ }
139
+ }
140
+ });
141
+
142
+ if (filePath) {
143
+ try {
144
+ const fileContent = fs.readFileSync(filePath.trim(), 'utf8');
145
+ setCredential(key, fileContent);
146
+ credentials[key] = true;
147
+ totalStored++;
148
+ const lineCount = fileContent.split('\n').length;
149
+ console.log(chalk.green(` ✓ Stored ${key} from file (${lineCount} lines)`));
150
+ } catch (err) {
151
+ console.log(chalk.red(` ✗ Failed to read file: ${err.message}`));
152
+ }
153
+ }
154
+ } else {
155
+ // Handle single-line credentials with regular prompt
156
+ const { value } = await prompts({
157
+ type: 'text',
158
+ name: 'value',
159
+ message: prompt,
160
+ validate: val => val.trim().length > 0 || 'Value cannot be empty'
161
+ });
162
+
163
+ if (value) {
164
+ setCredential(key, value.trim());
165
+ credentials[key] = true;
166
+ totalStored++;
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ console.log(chalk.green(`\n✅ Successfully stored ${totalStored} credential(s) in keychain\n`));
173
+ console.log(chalk.gray('These credentials will be used automatically by all buwp-local projects.'));
174
+ console.log(chalk.gray('You can override specific credentials per-project using .env.local files.\n'));
175
+ }
176
+
177
+ /**
178
+ * Bulk import credentials from JSON file
179
+ * @param {string} filePath - Path to credentials JSON file
180
+ * @param {boolean} force - Skip confirmation prompts
181
+ */
182
+ async function bulkImportFromFile(filePath, force) {
183
+ console.log(chalk.yellow('⚠️ macOS may prompt you to allow Node.js access to your keychain.'));
184
+ console.log(chalk.yellow(' Click "Always Allow" to avoid repeated prompts.\n'));
185
+
186
+ // Parse the credentials file
187
+ let result;
188
+ try {
189
+ result = parseCredentialsFile(filePath);
190
+ } catch (err) {
191
+ console.log(chalk.red(`❌ Failed to parse credentials file: ${err.message}\n`));
192
+ process.exit(1);
193
+ }
194
+
195
+ const { parsed, unknown, metadata } = result;
196
+ const credentialCount = Object.keys(parsed).length;
197
+
198
+ if (credentialCount === 0) {
199
+ console.log(chalk.yellow('⚠️ No valid credentials found in file.\n'));
200
+ process.exit(1);
201
+ }
202
+
203
+ // Show import summary
204
+ console.log(chalk.cyan('📄 Credentials File Summary:\n'));
205
+ if (metadata.source !== 'unknown') {
206
+ console.log(chalk.gray(` Source: ${metadata.source}`));
207
+ }
208
+ if (metadata.version !== 'unknown') {
209
+ console.log(chalk.gray(` Version: ${metadata.version}`));
210
+ }
211
+ if (metadata.exported) {
212
+ console.log(chalk.gray(` Exported: ${metadata.exported}`));
213
+ }
214
+ console.log('');
215
+
216
+ // Show credentials by group
217
+ console.log(chalk.cyan('Credentials to import:\n'));
218
+ for (const [groupName, keys] of Object.entries(CREDENTIAL_GROUPS)) {
219
+ const groupCreds = keys.filter(k => parsed[k]);
220
+ if (groupCreds.length > 0) {
221
+ console.log(chalk.white(` ${groupName.toUpperCase()}:`));
222
+ groupCreds.forEach(key => {
223
+ const value = parsed[key];
224
+ const lineCount = value.split('\n').length;
225
+ const info = lineCount > 1 ? ` (${lineCount} lines)` : '';
226
+ console.log(chalk.green(` ✓ ${key}${info}`));
227
+ });
228
+ }
229
+ }
230
+ console.log('');
231
+
232
+ // Show unknown keys if any
233
+ if (unknown.length > 0) {
234
+ console.log(chalk.yellow(`⚠️ Found ${unknown.length} unknown or invalid credential(s):\n`));
235
+ unknown.forEach(({ key, reason }) => {
236
+ console.log(chalk.gray(` - ${key}: ${reason}`));
237
+ });
238
+ console.log('');
239
+ }
240
+
241
+ console.log(chalk.cyan(`Total: ${credentialCount} credential(s) to import\n`));
242
+
243
+ // Check for existing credentials
244
+ const existingKeys = listCredentials();
245
+ const willOverwrite = Object.keys(parsed).filter(k => existingKeys.includes(k));
246
+
247
+ if (willOverwrite.length > 0 && !force) {
248
+ console.log(chalk.yellow(`⚠️ ${willOverwrite.length} credential(s) already exist and will be overwritten:\n`));
249
+ willOverwrite.forEach(key => {
250
+ console.log(chalk.gray(` - ${key}`));
251
+ });
252
+ console.log('');
253
+
254
+ const { shouldContinue } = await prompts({
255
+ type: 'confirm',
256
+ name: 'shouldContinue',
257
+ message: 'Continue with import?',
258
+ initial: false
259
+ });
260
+
261
+ if (!shouldContinue) {
262
+ console.log(chalk.gray('\nImport cancelled.\n'));
263
+ return;
264
+ }
265
+ }
266
+
267
+ // Import credentials
268
+ console.log(chalk.gray('\nImporting credentials...\n'));
269
+ let successCount = 0;
270
+ let failCount = 0;
271
+
272
+ for (const [key, value] of Object.entries(parsed)) {
273
+ try {
274
+ setCredential(key, value);
275
+ successCount++;
276
+ console.log(chalk.green(` ✓ ${key}`));
277
+ } catch (err) {
278
+ failCount++;
279
+ console.log(chalk.red(` ✗ ${key}: ${err.message}`));
280
+ }
281
+ }
282
+
283
+ console.log('');
284
+ if (failCount === 0) {
285
+ console.log(chalk.green(`✅ Successfully imported ${successCount} credential(s) into keychain\n`));
286
+ } else {
287
+ console.log(chalk.yellow(`⚠️ Imported ${successCount} credential(s), ${failCount} failed\n`));
288
+ }
289
+
290
+ console.log(chalk.gray('These credentials will be used automatically by all buwp-local projects.'));
291
+ console.log(chalk.gray('You can override specific credentials per-project using .env.local files.\n'));
292
+ }
293
+
294
+ /**
295
+ * Set a single credential
296
+ */
297
+ async function setCommand(args, options) {
298
+ if (args.length === 0) {
299
+ console.log(chalk.red('❌ Missing credential key\n'));
300
+ console.log(chalk.gray('Usage:'));
301
+ console.log(chalk.gray(' buwp-local keychain set <KEY> [value]'));
302
+ console.log(chalk.gray(' buwp-local keychain set <KEY> --file <path>'));
303
+ console.log(chalk.gray(' cat file.pem | buwp-local keychain set <KEY> --stdin\n'));
304
+ console.log(chalk.gray('Available keys:'));
305
+ CREDENTIAL_KEYS.forEach(key => {
306
+ const isMultiline = MULTILINE_CREDENTIALS.includes(key);
307
+ console.log(chalk.gray(` - ${key}${isMultiline ? ' (multiline)' : ''}`));
308
+ });
309
+ console.log('');
310
+ process.exit(1);
311
+ }
312
+
313
+ const key = args[0];
314
+ let value = args.slice(1).join(' ');
315
+
316
+ if (!CREDENTIAL_KEYS.includes(key)) {
317
+ console.log(chalk.red(`❌ Invalid credential key: ${key}\n`));
318
+ console.log(chalk.gray('Available keys:'));
319
+ CREDENTIAL_KEYS.forEach(k => {
320
+ console.log(chalk.gray(` - ${k}`));
321
+ });
322
+ console.log('');
323
+ process.exit(1);
324
+ }
325
+
326
+ // Check if credential already exists
327
+ const existing = hasCredential(key);
328
+ if (existing && !options.force) {
329
+ console.log(chalk.yellow(`⚠️ Credential ${key} already exists in keychain.\n`));
330
+ const { shouldOverwrite } = await prompts({
331
+ type: 'confirm',
332
+ name: 'shouldOverwrite',
333
+ message: 'Overwrite existing value?',
334
+ initial: false
335
+ });
336
+
337
+ if (!shouldOverwrite) {
338
+ console.log(chalk.gray('\nOperation cancelled.\n'));
339
+ return;
340
+ }
341
+ }
342
+
343
+ // Handle different input methods
344
+ if (options.file) {
345
+ // Read from file
346
+ if (!fs.existsSync(options.file)) {
347
+ console.log(chalk.red(`❌ File not found: ${options.file}\n`));
348
+ process.exit(1);
349
+ }
350
+ try {
351
+ value = fs.readFileSync(options.file, 'utf8');
352
+ console.log(chalk.green(`📄 Read ${value.split('\n').length} line(s) from ${options.file}`));
353
+ } catch (err) {
354
+ console.log(chalk.red(`❌ Failed to read file: ${err.message}\n`));
355
+ process.exit(1);
356
+ }
357
+ } else if (options.stdin) {
358
+ // Read from stdin
359
+ try {
360
+ const chunks = [];
361
+ for await (const chunk of process.stdin) {
362
+ chunks.push(chunk);
363
+ }
364
+ value = Buffer.concat(chunks).toString('utf8');
365
+ if (!value.trim()) {
366
+ console.log(chalk.red('❌ No input received from stdin\n'));
367
+ process.exit(1);
368
+ }
369
+ console.log(chalk.green(`📄 Read ${value.split('\n').length} line(s) from stdin`));
370
+ } catch (err) {
371
+ console.log(chalk.red(`❌ Failed to read from stdin: ${err.message}\n`));
372
+ process.exit(1);
373
+ }
374
+ } else if (!value) {
375
+ // Interactive prompt
376
+ const isMultiline = isMultilineCredential(key);
377
+
378
+ if (isMultiline) {
379
+ console.log(chalk.red(`❌ ${key} is a multiline credential (cryptographic key/certificate).\n`));
380
+ console.log(chalk.yellow('Multiline credentials must be provided via file or stdin:\n'));
381
+ console.log(chalk.gray(` buwp-local keychain set ${key} --file path/to/key.pem`));
382
+ console.log(chalk.gray(` cat key.pem | buwp-local keychain set ${key} --stdin\n`));
383
+ process.exit(1);
384
+ }
385
+
386
+ // Regular single-line prompt
387
+ console.log(chalk.yellow('⚠️ macOS may prompt you to allow keychain access.\n'));
388
+ const description = CREDENTIAL_DESCRIPTIONS[key];
389
+
390
+ const response = await prompts({
391
+ type: 'text',
392
+ name: 'value',
393
+ message: description,
394
+ validate: val => val.trim().length > 0 || 'Value cannot be empty'
395
+ });
396
+
397
+ if (!response.value) {
398
+ console.log(chalk.gray('\nOperation cancelled.\n'));
399
+ return;
400
+ }
401
+
402
+ value = response.value.trim();
403
+ }
404
+
405
+ // Validate and store the credential
406
+ if (!value || !value.trim()) {
407
+ console.log(chalk.red('❌ Empty value not allowed\n'));
408
+ process.exit(1);
409
+ }
410
+
411
+ setCredential(key, value.trim());
412
+ const lines = value.trim().split('\n').length;
413
+ const lineText = lines === 1 ? 'line' : 'lines';
414
+ console.log(chalk.green(`\n✅ Stored ${key} in keychain (${lines} ${lineText})\n`));
415
+ }
416
+
417
+ /**
418
+ * Get a credential value (for debugging)
419
+ */
420
+ async function getCommand(args) {
421
+ if (args.length === 0) {
422
+ console.log(chalk.red('❌ Missing credential key\n'));
423
+ console.log(chalk.gray('Usage: buwp-local keychain get <KEY>\n'));
424
+ process.exit(1);
425
+ }
426
+
427
+ const key = args[0];
428
+
429
+ if (!CREDENTIAL_KEYS.includes(key)) {
430
+ console.log(chalk.red(`❌ Invalid credential key: ${key}\n`));
431
+ process.exit(1);
432
+ }
433
+
434
+ const value = getCredential(key);
435
+
436
+ if (value === null) {
437
+ console.log(chalk.yellow(`⚠️ Credential ${key} not found in keychain\n`));
438
+ process.exit(1);
439
+ }
440
+
441
+ // Mask the value for security (show first/last 4 chars)
442
+ let masked = value;
443
+ if (value.length > 8) {
444
+ const first = value.substring(0, 4);
445
+ const last = value.substring(value.length - 4);
446
+ const middle = '*'.repeat(Math.min(value.length - 8, 20));
447
+ masked = `${first}${middle}${last}`;
448
+ } else {
449
+ masked = '*'.repeat(value.length);
450
+ }
451
+
452
+ console.log(chalk.cyan(`\n${key}:`));
453
+ console.log(chalk.white(` ${masked}`));
454
+ console.log(chalk.gray(` (length: ${value.length} characters)\n`));
455
+ }
456
+
457
+ /**
458
+ * List all stored credentials
459
+ */
460
+ async function listCommand() {
461
+ console.log(chalk.blue('🔐 Stored Credentials\n'));
462
+
463
+ const storedKeys = listCredentials();
464
+
465
+ if (storedKeys.length === 0) {
466
+ console.log(chalk.yellow('No credentials stored in keychain.\n'));
467
+ console.log(chalk.gray('Run "buwp-local keychain setup" to configure credentials.\n'));
468
+ return;
469
+ }
470
+
471
+ // Group by category
472
+ for (const [groupName, keys] of Object.entries(CREDENTIAL_GROUPS)) {
473
+ const groupKeys = keys.filter(k => storedKeys.includes(k));
474
+
475
+ if (groupKeys.length > 0) {
476
+ console.log(chalk.cyan(`\n${groupName.toUpperCase()}:`));
477
+ groupKeys.forEach(key => {
478
+ console.log(chalk.green(` ✓ ${key}`));
479
+ });
480
+ } else {
481
+ console.log(chalk.cyan(`\n${groupName.toUpperCase()}:`));
482
+ console.log(chalk.gray(' (none stored)'));
483
+ }
484
+ }
485
+
486
+ console.log(chalk.gray(`\nTotal: ${storedKeys.length} credential(s) stored\n`));
487
+ }
488
+
489
+ /**
490
+ * Clear all credentials
491
+ */
492
+ async function clearCommand(options) {
493
+ console.log(chalk.red('⚠️ Clear All Credentials\n'));
494
+
495
+ const storedKeys = listCredentials();
496
+
497
+ if (storedKeys.length === 0) {
498
+ console.log(chalk.yellow('No credentials stored in keychain.\n'));
499
+ return;
500
+ }
501
+
502
+ console.log(chalk.gray(`This will remove ${storedKeys.length} credential(s) from your keychain:\n`));
503
+ storedKeys.forEach(key => {
504
+ console.log(chalk.gray(` - ${key}`));
505
+ });
506
+ console.log('');
507
+
508
+ if (!options.force) {
509
+ const { confirmed } = await prompts({
510
+ type: 'confirm',
511
+ name: 'confirmed',
512
+ message: 'Are you sure you want to clear all credentials?',
513
+ initial: false
514
+ });
515
+
516
+ if (!confirmed) {
517
+ console.log(chalk.gray('\nOperation cancelled.\n'));
518
+ return;
519
+ }
520
+ }
521
+
522
+ const deletedCount = clearAllCredentials();
523
+ console.log(chalk.green(`\n✅ Removed ${deletedCount} credential(s) from keychain\n`));
524
+ }
525
+
526
+ /**
527
+ * Show keychain status
528
+ */
529
+ async function statusCommand() {
530
+ console.log(chalk.blue('🔐 Keychain Status\n'));
531
+
532
+ console.log(chalk.cyan('Platform:'));
533
+ console.log(chalk.white(` ${process.platform} ${isPlatformSupported() ? '(supported ✓)' : '(not supported ✗)'}\n`));
534
+
535
+ if (!isPlatformSupported()) {
536
+ console.log(chalk.yellow('Keychain integration is only available on macOS.\n'));
537
+ return;
538
+ }
539
+
540
+ const storedKeys = listCredentials();
541
+ const totalKeys = CREDENTIAL_KEYS.length;
542
+
543
+ console.log(chalk.cyan('Credentials:'));
544
+ console.log(chalk.white(` ${storedKeys.length} of ${totalKeys} stored (${Math.round(storedKeys.length / totalKeys * 100)}%)\n`));
545
+
546
+ // Show completeness by group
547
+ for (const [groupName, keys] of Object.entries(CREDENTIAL_GROUPS)) {
548
+ const storedInGroup = keys.filter(k => storedKeys.includes(k)).length;
549
+ const emoji = storedInGroup === keys.length ? '✓' : storedInGroup > 0 ? '⚠' : '✗';
550
+ console.log(chalk.gray(` ${emoji} ${groupName}: ${storedInGroup}/${keys.length}`));
551
+ }
552
+
553
+ console.log('');
554
+
555
+ if (storedKeys.length === 0) {
556
+ console.log(chalk.yellow('No credentials configured yet.\n'));
557
+ console.log(chalk.gray('Run "buwp-local keychain setup" to get started.\n'));
558
+ } else if (storedKeys.length < totalKeys) {
559
+ console.log(chalk.yellow('Some credentials are missing.\n'));
560
+ console.log(chalk.gray('Run "buwp-local keychain setup" to configure all credentials.\n'));
561
+ } else {
562
+ console.log(chalk.green('All credentials configured! ✓\n'));
563
+ }
564
+ }
565
+
566
+ /**
567
+ * Show help message
568
+ */
569
+ function showHelp() {
570
+ console.log(chalk.blue('🔐 Keychain Command\n'));
571
+ console.log('Manage credentials in macOS keychain for secure storage.\n');
572
+ console.log(chalk.cyan('Usage:'));
573
+ console.log(' buwp-local keychain <subcommand> [options]\n');
574
+ console.log(chalk.cyan('Subcommands:'));
575
+ console.log(' setup Interactive credential setup (all credentials)');
576
+ console.log(' set <KEY> Set a single credential');
577
+ console.log(' get <KEY> Get a credential value (masked)');
578
+ console.log(' list List all stored credentials');
579
+ console.log(' clear Remove all credentials');
580
+ console.log(' status Show keychain status\n');
581
+ console.log(chalk.cyan('Setup Command Options:'));
582
+ console.log(' --file <path> Bulk import credentials from JSON file\n');
583
+ console.log(chalk.cyan('Set Command Options:'));
584
+ console.log(' --file <path> Read credential from file (required for multiline credentials)');
585
+ console.log(' --stdin Read credential from stdin (for piping)\n');
586
+ console.log(chalk.cyan('Examples:'));
587
+ console.log(' # Interactive setup (prompts for each credential)');
588
+ console.log(' buwp-local keychain setup\n');
589
+ console.log(' # Bulk import from JSON file');
590
+ console.log(' buwp-local keychain setup --file .buwp-credentials.json\n');
591
+ console.log(' # Interactive prompt for single-line credential');
592
+ console.log(' buwp-local keychain set WORDPRESS_DB_PASSWORD\n');
593
+ console.log(' # Set single-line credential directly');
594
+ console.log(' buwp-local keychain set WORDPRESS_DB_PASSWORD mypassword\n');
595
+ console.log(' # Set multiline credential from file (required for keys/certificates)');
596
+ console.log(' buwp-local keychain set SHIB_SP_KEY --file private-key.pem\n');
597
+ console.log(' # Pipe credential from file or command');
598
+ console.log(' cat certificate.pem | buwp-local keychain set SHIB_SP_CERT --stdin\n');
599
+ console.log(chalk.cyan('Global Options:'));
600
+ console.log(' -f, --force Skip confirmation prompts\n');
601
+ console.log(chalk.cyan('Credentials File Format (JSON):'));
602
+ console.log(chalk.gray(' {'));
603
+ console.log(chalk.gray(' "version": "1.0",'));
604
+ console.log(chalk.gray(' "source": "dev-server.bu.edu",'));
605
+ console.log(chalk.gray(' "credentials": {'));
606
+ console.log(chalk.gray(' "WORDPRESS_DB_PASSWORD": "password123",'));
607
+ console.log(chalk.gray(' "SHIB_SP_KEY": "-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----",'));
608
+ console.log(chalk.gray(' "S3_UPLOADS_BUCKET": "my-bucket"'));
609
+ console.log(chalk.gray(' }'));
610
+ console.log(chalk.gray(' }\n'));
611
+ }
612
+
613
+ export default keychainCommand;
@@ -5,7 +5,8 @@
5
5
  import chalk from 'chalk';
6
6
  import { execSync } from 'child_process';
7
7
  import path from 'path';
8
- import { loadConfig, validateConfig } from '../config.js';
8
+ import fs from 'fs';
9
+ import { loadConfig, validateConfig, ENV_FILE_NAME, loadKeychainCredentials, createSecureTempEnvFile, secureDeleteTempEnvFile } from '../config.js';
9
10
  import { generateComposeFile } from '../compose-generator.js';
10
11
 
11
12
  async function startCommand(options) {
@@ -60,9 +61,28 @@ async function startCommand(options) {
60
61
  const composeDir = path.dirname(composePath);
61
62
  const projectName = config.projectName || 'buwp-local';
62
63
 
64
+ // Load keychain credentials and create secure temp env file if available
65
+ let tempEnvPath = null;
66
+ const keychainCredentials = loadKeychainCredentials();
67
+ const keychainCredCount = Object.keys(keychainCredentials).length;
68
+
69
+ if (keychainCredCount > 0) {
70
+ try {
71
+ tempEnvPath = createSecureTempEnvFile(keychainCredentials, projectName);
72
+ console.log(chalk.gray(`✓ Loaded ${keychainCredCount} credentials from keychain\n`));
73
+ } catch (err) {
74
+ console.warn(chalk.yellow(`⚠️ Could not load keychain credentials: ${err.message}`));
75
+ }
76
+ }
77
+
78
+ // Check if .env.local exists and build env-file flags
79
+ const envFilePath = path.join(projectPath, ENV_FILE_NAME);
80
+ const envFileFlag = fs.existsSync(envFilePath) ? `--env-file ${envFilePath}` : '';
81
+ const tempEnvFileFlag = tempEnvPath ? `--env-file ${tempEnvPath}` : '';
82
+
63
83
  try {
64
84
  execSync(
65
- `docker compose -p ${projectName} -f ${composePath} up -d`,
85
+ `docker compose -p ${projectName} ${tempEnvFileFlag} ${envFileFlag} -f ${composePath} up -d`,
66
86
  {
67
87
  cwd: composeDir,
68
88
  stdio: 'inherit'
@@ -71,6 +91,11 @@ async function startCommand(options) {
71
91
  } catch (err) {
72
92
  console.error(chalk.red('\n❌ Failed to start Docker containers'));
73
93
  process.exit(1);
94
+ } finally {
95
+ // Always clean up temp env file, even if Docker Compose failed
96
+ if (tempEnvPath) {
97
+ secureDeleteTempEnvFile(tempEnvPath);
98
+ }
74
99
  }
75
100
 
76
101
  // Success message