All files / cli / prompt_secret.ts

96.43% Branches 27/28
97.40% Lines 75/77
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
 
 
x1
x1
x1
x1
x1
x1
x1
x1
x1
x1
 
 
 
 
x2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x1
x1
x1
 
x14
 
x14
x15
x15
 
x26
x26
 
x26
x14
x81
x81
x81
 
x81
x105
x105
 
x105
 
 
x81
 
x92
x92
 
x94
x94
x81
 
x126
x167
x167
x126
 
x81
 
x81
x14
 
x14
 
x14
x14
x14
x14
x26
x27
x26
x37
x37
x26
x26
x14
 
 
 
 
 
x13
x13
x13
 
x13
x85
x85
x87
x87
x85
x95
x95
x85
x96
x85
x134
x134
x85
x85
x13
x13













I


















































































































// Copyright 2018-2025 the Deno authors. MIT license.

const input = Deno.stdin;
const output = Deno.stdout;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const LF = "\n".charCodeAt(0); // ^J - Enter on Linux
const CR = "\r".charCodeAt(0); // ^M - Enter on macOS and Windows (CRLF)
const BS = "\b".charCodeAt(0); // ^H - Backspace on Linux and Windows
const DEL = 0x7f; // ^? - Backspace on macOS
const CLR = encoder.encode("\r\u001b[K"); // Clear the current line
const MOVE_LINE_UP = encoder.encode("\r\u001b[1F"); // Move to previous line

// The `cbreak` option is not supported on Windows
const setRawOptions = Deno.build.os === "windows"
  ? undefined
  : { cbreak: true };

/** Options for {@linkcode promptSecret}. */
export type PromptSecretOptions = {
  /** A character to print instead of the user's input. */
  mask?: string;
  /** Clear the current line after the user's input. */
  clear?: boolean;
};

/**
 * Shows the given message and waits for the user's input. Returns the user's input as string.
 * This is similar to `prompt()` but it print user's input as `*` to prevent password from being shown.
 * Use an empty `mask` if you don't want to show any character.
 *
 * @param message The prompt message to show to the user.
 * @param options The options for the prompt.
 * @returns The string that was entered or `null` if stdin is not a TTY.
 *
 * @example Usage
 * ```ts ignore
 * import { promptSecret } from "@std/cli/prompt-secret";
 *
 * const password = promptSecret("Please provide the password:");
 * if (password !== "some-password") {
 *   throw new Error("Access denied");
 * }
 * ```
 */
export function promptSecret(
  message = "Secret",
  options?: PromptSecretOptions,
): string | null {
  const { mask = "*", clear } = options ?? {};

  if (!input.isTerminal()) {
    return null;
  }

  const { columns } = Deno.consoleSize();
  let previousLength = 0;
  // Make the output consistent with the built-in prompt()
  message += " ";
  const callback = !mask ? undefined : (n: number) => {
    let line = `${message}${mask.repeat(n)}`;
    const currentLength = line.length;
    const charsPastLineLength = line.length % columns;

    if (line.length > columns) {
      line = line.slice(
        -1 * (charsPastLineLength === 0 ? columns : charsPastLineLength),
      );
    }

    // If the user has deleted a character
    if (currentLength < previousLength) {
      // Then clear the current line.
      output.writeSync(CLR);
      if (charsPastLineLength === 0) {
        // And if there's no characters on the current line, return to previous line.
        output.writeSync(MOVE_LINE_UP);
      }
    } else {
      // Always jump the cursor back to the beginning of the line unless it's the first character.
      if (charsPastLineLength !== 1) {
        output.writeSync(CLR);
      }
    }

    output.writeSync(encoder.encode(line));

    previousLength = currentLength;
  };

  output.writeSync(encoder.encode(message));

  Deno.stdin.setRaw(true, setRawOptions);
  try {
    return readLineFromStdinSync(callback);
  } finally {
    if (clear) {
      output.writeSync(CLR);
    } else {
      output.writeSync(encoder.encode("\n"));
    }
    Deno.stdin.setRaw(false);
  }
}

// Slightly modified from Deno's runtime/js/41_prompt.js
// This implementation immediately break on CR or LF and accept callback.
// The original version waits LF when CR is received.
// https://github.com/denoland/deno/blob/e4593873a9c791238685dfbb45e64b4485884174/runtime/js/41_prompt.js#L52-L77
function readLineFromStdinSync(callback?: (n: number) => void): string {
  const c = new Uint8Array(1);
  const buf = [];

  while (true) {
    const n = input.readSync(c);
    if (n === null || n === 0) {
      break;
    }
    if (c[0] === CR || c[0] === LF) {
      break;
    }
    if (c[0] === BS || c[0] === DEL) {
      buf.pop();
    } else {
      buf.push(c[0]!);
    }
    if (callback) callback(buf.length);
  }
  return decoder.decode(new Uint8Array(buf));
}