@@ -69,9 +69,15 @@ export async function runInit(deps: InitDeps): Promise<number> {
6969 deps . out ( `Password stored in ${ where } ` ) ;
7070 deps . out ( `Settings written to ${ path } ` ) ;
7171 deps . out ( '' ) ;
72- deps . out ( 'Add the server to your agent:' ) ;
73- deps . out ( ' Codex: codex mcp add keenetic -- npx -y keenetic-mcp' ) ;
74- deps . out ( ' Others: {"command": "npx", "args": ["-y", "keenetic-mcp"]}' ) ;
72+ deps . out ( 'Add it to your agent. The plugin brings the skills along with the server:' ) ;
73+ deps . out ( '' ) ;
74+ deps . out ( ' Claude Code /plugin marketplace add salatmaster/keenetic-mcp' ) ;
75+ deps . out ( ' /plugin install keenetic@keenetic' ) ;
76+ deps . out ( '' ) ;
77+ deps . out ( ' Codex codex plugin marketplace add salatmaster/keenetic-mcp' ) ;
78+ deps . out ( ' codex plugin add keenetic@keenetic' ) ;
79+ deps . out ( '' ) ;
80+ deps . out ( ' Anything else {"command": "npx", "args": ["-y", "keenetic-mcp"]}' ) ;
7581 return 0 ;
7682}
7783
@@ -93,34 +99,85 @@ async function readGateway(): Promise<string | null> {
9399 }
94100}
95101
96- // Required-but-nullable rather than optional: exactOptionalPropertyTypes
97- // forbids assigning undefined back to an optional property.
98- type MutableReadline = { _writeToOutput : ( ( text : string ) => void ) | undefined } ;
99-
100102interface LineSource {
101103 ask ( question : string , echo : boolean ) : Promise < string > ;
102104 close ( ) : void ;
103105}
104106
105- /** Reads whole lines from a terminal, hiding the echo when asked to. */
107+ /**
108+ * Reads a secret from the terminal without echoing it.
109+ *
110+ * Raw mode rather than readline: in terminal mode readline redraws the current
111+ * line, which erases a prompt written straight to stdout, and muting its
112+ * private `_writeToOutput` does not suppress the echo on current Node. Raw mode
113+ * turns the echo off at the terminal, so nothing can leak it.
114+ */
115+ function readSecret ( question : string ) : Promise < string > {
116+ return new Promise ( ( resolve , reject ) => {
117+ const { stdin, stdout } = process ;
118+ stdout . write ( question ) ;
119+
120+ const wasRaw = stdin . isRaw === true ;
121+ if ( stdin . isTTY ) stdin . setRawMode ( true ) ;
122+ stdin . resume ( ) ;
123+
124+ let secret = '' ;
125+
126+ const stop = ( ) : void => {
127+ stdin . off ( 'data' , onData ) ;
128+ if ( stdin . isTTY ) stdin . setRawMode ( wasRaw ) ;
129+ stdin . pause ( ) ;
130+ stdout . write ( '\n' ) ;
131+ } ;
132+
133+ const onData = ( chunk : Buffer ) : void => {
134+ for ( const char of chunk . toString ( 'utf8' ) ) {
135+ switch ( char ) {
136+ case '\r' :
137+ case '\n' :
138+ case '\u0004' : // Ctrl-D, an empty password
139+ stop ( ) ;
140+ resolve ( secret ) ;
141+ return ;
142+ case '\u0003' : // Ctrl-C
143+ stop ( ) ;
144+ reject ( new Error ( 'cancelled' ) ) ;
145+ return ;
146+ case '\u007f' : // Delete
147+ case '\b' :
148+ secret = secret . slice ( 0 , - 1 ) ;
149+ break ;
150+ default :
151+ // Skip the rest of the control range, including escape sequences
152+ // from arrow keys, which would otherwise land in the password.
153+ if ( char >= ' ' && char !== '\u007f' ) secret += char ;
154+ }
155+ }
156+ } ;
157+
158+ stdin . on ( 'data' , onData ) ;
159+ } ) ;
160+ }
161+
162+ /**
163+ * Reads from a terminal.
164+ *
165+ * A readline interface is opened per visible question and closed again, so it
166+ * is never competing with the raw-mode reader for stdin.
167+ */
106168function terminalSource ( ) : LineSource {
107- const rl = createInterface ( { input : process . stdin , output : process . stdout } ) ;
108169 return {
109170 async ask ( question , echo ) {
110- if ( echo ) return ( await rl . question ( question ) ) . trim ( ) ;
171+ if ( ! echo ) return readSecret ( question ) ;
111172
112- const mutable = rl as unknown as MutableReadline ;
113- const original = mutable . _writeToOutput ;
114- process . stdout . write ( question ) ;
115- mutable . _writeToOutput = ( ) => { } ;
173+ const rl = createInterface ( { input : process . stdin , output : process . stdout } ) ;
116174 try {
117- return await rl . question ( '' ) ;
175+ return ( await rl . question ( question ) ) . trim ( ) ;
118176 } finally {
119- mutable . _writeToOutput = original ;
120- process . stdout . write ( '\n' ) ;
177+ rl . close ( ) ;
121178 }
122179 } ,
123- close : ( ) => rl . close ( )
180+ close : ( ) => { }
124181 } ;
125182}
126183
0 commit comments