|
1 | 1 | // @ts-nocheck |
2 | 2 | import fs from 'node:fs/promises'; |
| 3 | +import path from 'node:path'; |
| 4 | +import { emitKeypressEvents } from 'node:readline'; |
3 | 5 | import { |
4 | 6 | generateCompletionScript, |
5 | 7 | installCompletionScript, |
@@ -41,6 +43,7 @@ import { readPackageVersion } from './state.mjs'; |
41 | 43 | const EXIT_SUCCESS = 0; |
42 | 44 | const EXIT_FAILURE = 1; |
43 | 45 | const EXIT_USAGE = 2; |
| 46 | +const RUNTIME_DB_FILENAME = 'coder-studio.db'; |
44 | 47 |
|
45 | 48 | class CliError extends Error { |
46 | 49 | constructor(message, { exitCode = EXIT_FAILURE, helpTopic = null } = {}) { |
@@ -575,6 +578,127 @@ async function readSecretFromStdin() { |
575 | 578 | return Buffer.concat(chunks).toString('utf8').trimEnd(); |
576 | 579 | } |
577 | 580 |
|
| 581 | +async function pathExists(filePath) { |
| 582 | + try { |
| 583 | + await fs.access(filePath); |
| 584 | + return true; |
| 585 | + } catch { |
| 586 | + return false; |
| 587 | + } |
| 588 | +} |
| 589 | + |
| 590 | +async function promptHiddenInput(label) { |
| 591 | + if (!process.stdin.isTTY || !process.stdout.isTTY || typeof process.stdin.setRawMode !== 'function') { |
| 592 | + throw new CliError('interactive password setup requires a TTY', { exitCode: EXIT_FAILURE }); |
| 593 | + } |
| 594 | + |
| 595 | + const stdin = process.stdin; |
| 596 | + const stdout = process.stdout; |
| 597 | + const wasRaw = Boolean(stdin.isRaw); |
| 598 | + |
| 599 | + emitKeypressEvents(stdin); |
| 600 | + stdin.resume(); |
| 601 | + if (!wasRaw) { |
| 602 | + stdin.setRawMode(true); |
| 603 | + } |
| 604 | + |
| 605 | + stdout.write(label); |
| 606 | + |
| 607 | + return await new Promise((resolve, reject) => { |
| 608 | + let value = ''; |
| 609 | + |
| 610 | + const cleanup = () => { |
| 611 | + stdin.off('keypress', onKeypress); |
| 612 | + if (!wasRaw) { |
| 613 | + stdin.setRawMode(false); |
| 614 | + } |
| 615 | + stdout.write('\n'); |
| 616 | + }; |
| 617 | + |
| 618 | + const finish = (callback) => { |
| 619 | + cleanup(); |
| 620 | + callback(); |
| 621 | + }; |
| 622 | + |
| 623 | + const onKeypress = (chunk, key = {}) => { |
| 624 | + if (key.ctrl && (key.name === 'c' || key.name === 'd')) { |
| 625 | + finish(() => reject(new CliError('initial password setup cancelled', { exitCode: EXIT_FAILURE }))); |
| 626 | + return; |
| 627 | + } |
| 628 | + |
| 629 | + if (key.name === 'return' || key.name === 'enter') { |
| 630 | + finish(() => resolve(value)); |
| 631 | + return; |
| 632 | + } |
| 633 | + |
| 634 | + if (key.name === 'backspace' || key.name === 'delete') { |
| 635 | + value = value.slice(0, -1); |
| 636 | + return; |
| 637 | + } |
| 638 | + |
| 639 | + if (typeof chunk === 'string' && chunk.length > 0 && !key.ctrl && !key.meta) { |
| 640 | + value += chunk; |
| 641 | + } |
| 642 | + }; |
| 643 | + |
| 644 | + stdin.on('keypress', onKeypress); |
| 645 | + }); |
| 646 | +} |
| 647 | + |
| 648 | +async function ensureInitialPasswordConfigured(context, flags) { |
| 649 | + const status = await getStatus(context.options); |
| 650 | + if (runtimeIsActive(status)) { |
| 651 | + return context; |
| 652 | + } |
| 653 | + |
| 654 | + const needsPassword = context.config.values.auth.publicMode && !context.config.values.auth.passwordConfigured; |
| 655 | + if (!needsPassword) { |
| 656 | + return context; |
| 657 | + } |
| 658 | + |
| 659 | + const dbPath = path.join(context.config.paths.dataDir, RUNTIME_DB_FILENAME); |
| 660 | + if (await pathExists(dbPath)) { |
| 661 | + return context; |
| 662 | + } |
| 663 | + |
| 664 | + if (flags.json || !process.stdin.isTTY || !process.stdout.isTTY) { |
| 665 | + throw new CliError( |
| 666 | + 'first launch requires configuring auth.password before start; run `coder-studio config password set --stdin` and retry', |
| 667 | + { exitCode: EXIT_FAILURE }, |
| 668 | + ); |
| 669 | + } |
| 670 | + |
| 671 | + console.log('First launch detected. Set an access password before starting Coder Studio.'); |
| 672 | + |
| 673 | + while (true) { |
| 674 | + const password = await promptHiddenInput('New password: '); |
| 675 | + if (!password.trim()) { |
| 676 | + console.log('Password cannot be empty.'); |
| 677 | + continue; |
| 678 | + } |
| 679 | + |
| 680 | + const confirmation = await promptHiddenInput('Confirm password: '); |
| 681 | + if (password !== confirmation) { |
| 682 | + console.log('Passwords do not match. Try again.'); |
| 683 | + continue; |
| 684 | + } |
| 685 | + |
| 686 | + await updateLocalConfig( |
| 687 | + { stateDir: context.config.paths.stateDir, dataDir: context.config.paths.dataDir }, |
| 688 | + { 'auth.password': password }, |
| 689 | + ); |
| 690 | + |
| 691 | + console.log('Password saved. Starting Coder Studio...'); |
| 692 | + return { |
| 693 | + ...context, |
| 694 | + config: await loadLocalConfig({ |
| 695 | + stateDir: context.config.paths.stateDir, |
| 696 | + dataDir: context.config.paths.dataDir, |
| 697 | + }), |
| 698 | + }; |
| 699 | + } |
| 700 | +} |
| 701 | + |
578 | 702 | async function applyConfigUpdate(context, key, rawValue, { unset = false } = {}) { |
579 | 703 | const status = await getStatus(context.options); |
580 | 704 | if (runtimeIsActive(status) && status.managed && isRuntimeConfigKey(key)) { |
@@ -1049,10 +1173,12 @@ export async function runCli(argv = process.argv.slice(2)) { |
1049 | 1173 | return await handleAuthCommand(positionals, flags, context); |
1050 | 1174 | } |
1051 | 1175 |
|
1052 | | - const context = await resolveCommandContext(flags); |
1053 | | - const options = context.options; |
| 1176 | + let context = await resolveCommandContext(flags); |
| 1177 | + let options = context.options; |
1054 | 1178 |
|
1055 | 1179 | if (command === 'start') { |
| 1180 | + context = await ensureInitialPasswordConfigured(context, flags); |
| 1181 | + options = context.options; |
1056 | 1182 | const result = await startRuntime({ |
1057 | 1183 | ...options, |
1058 | 1184 | foreground: Boolean(flags.foreground), |
@@ -1084,6 +1210,8 @@ export async function runCli(argv = process.argv.slice(2)) { |
1084 | 1210 | } |
1085 | 1211 |
|
1086 | 1212 | if (command === 'restart') { |
| 1213 | + context = await ensureInitialPasswordConfigured(context, flags); |
| 1214 | + options = context.options; |
1087 | 1215 | const result = await restartRuntime(options); |
1088 | 1216 | if (flags.json) printJson(result); |
1089 | 1217 | else { |
@@ -1112,6 +1240,8 @@ export async function runCli(argv = process.argv.slice(2)) { |
1112 | 1240 | } |
1113 | 1241 |
|
1114 | 1242 | if (command === 'open') { |
| 1243 | + context = await ensureInitialPasswordConfigured(context, flags); |
| 1244 | + options = context.options; |
1115 | 1245 | const result = await openRuntime(options); |
1116 | 1246 | if (flags.json) printJson(result); |
1117 | 1247 | else console.log(`opened: ${result.endpoint}`); |
|
0 commit comments