@bostonuniversity/buwp-local 0.4.1 → 0.5.1
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/IMPLEMENTATION_NOTES_V0.5.0_PHASE3.md +240 -0
- package/KEYCHAIN_IMPLEMENTATION.md +140 -0
- package/bin/buwp-local.js +12 -0
- package/lib/commands/init.js +3 -3
- package/lib/commands/keychain.js +613 -0
- package/lib/commands/start.js +23 -4
- package/lib/config.js +92 -0
- package/lib/index.js +15 -0
- package/lib/keychain.js +337 -0
- package/package.json +2 -2
- package/readme.md +79 -0
- package/.buwp-local.json +0 -22
- package/plan-environmentBasedCredentials.prompt.md +0 -57
|
@@ -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;
|
package/lib/commands/start.js
CHANGED
|
@@ -6,7 +6,7 @@ import chalk from 'chalk';
|
|
|
6
6
|
import { execSync } from 'child_process';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import fs from 'fs';
|
|
9
|
-
import { loadConfig, validateConfig, ENV_FILE_NAME } from '../config.js';
|
|
9
|
+
import { loadConfig, validateConfig, ENV_FILE_NAME, loadKeychainCredentials, createSecureTempEnvFile, secureDeleteTempEnvFile } from '../config.js';
|
|
10
10
|
import { generateComposeFile } from '../compose-generator.js';
|
|
11
11
|
|
|
12
12
|
async function startCommand(options) {
|
|
@@ -61,13 +61,28 @@ async function startCommand(options) {
|
|
|
61
61
|
const composeDir = path.dirname(composePath);
|
|
62
62
|
const projectName = config.projectName || 'buwp-local';
|
|
63
63
|
|
|
64
|
-
//
|
|
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
|
|
65
79
|
const envFilePath = path.join(projectPath, ENV_FILE_NAME);
|
|
66
80
|
const envFileFlag = fs.existsSync(envFilePath) ? `--env-file ${envFilePath}` : '';
|
|
81
|
+
const tempEnvFileFlag = tempEnvPath ? `--env-file ${tempEnvPath}` : '';
|
|
67
82
|
|
|
68
83
|
try {
|
|
69
84
|
execSync(
|
|
70
|
-
`docker compose -p ${projectName} ${envFileFlag} -f ${composePath} up -d`,
|
|
85
|
+
`docker compose -p ${projectName} ${tempEnvFileFlag} ${envFileFlag} -f ${composePath} up -d`,
|
|
71
86
|
{
|
|
72
87
|
cwd: composeDir,
|
|
73
88
|
stdio: 'inherit'
|
|
@@ -76,13 +91,17 @@ async function startCommand(options) {
|
|
|
76
91
|
} catch (err) {
|
|
77
92
|
console.error(chalk.red('\n❌ Failed to start Docker containers'));
|
|
78
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
|
+
}
|
|
79
99
|
}
|
|
80
100
|
|
|
81
101
|
// Success message
|
|
82
102
|
console.log(chalk.green('\n✅ Environment started successfully!\n'));
|
|
83
103
|
console.log(chalk.cyan(`Project: ${projectName}`));
|
|
84
104
|
console.log(chalk.cyan('Access your site at:'));
|
|
85
|
-
console.log(chalk.white(` http://${config.hostname}`));
|
|
86
105
|
console.log(chalk.white(` https://${config.hostname}\n`));
|
|
87
106
|
|
|
88
107
|
console.log(chalk.gray('Useful commands:'));
|