|
1 | 1 | import { FormProps, FormReturnValue } from '@codifycli/ink-form'; |
2 | | -import chalk from 'chalk'; |
3 | 2 | import { CommandRequestData } from '@codifycli/schemas'; |
4 | 3 | import { render } from 'ink'; |
5 | 4 | import { EventEmitter } from 'node:events'; |
@@ -76,8 +75,8 @@ export class DefaultReporter implements Reporter { |
76 | 75 | this.sudoPasswordSubmittedCallback = callback; |
77 | 76 | } |
78 | 77 |
|
79 | | - notifySudoPasswordPreSupplied(): void { |
80 | | - setImmediate(() => this.renderEmitter.emit(RenderEvent.SUDO_PASSWORD_PRE_SUPPLIED)); |
| 78 | + setSudoPasswordCached(): void { |
| 79 | + store.set(store.isSudoPasswordCached, true); |
81 | 80 | } |
82 | 81 |
|
83 | 82 | async promptPressKeyToContinue(message?: string): Promise<void> { |
@@ -198,18 +197,11 @@ export class DefaultReporter implements Reporter { |
198 | 197 | void this.updateRenderState(RenderStatus.DISPLAY_IMPORT_RESULT, { importResult, showConfigs }); |
199 | 198 | } |
200 | 199 |
|
201 | | - async promptSudo(pluginName: string, data: CommandRequestData, secureMode: boolean): Promise<string | undefined> { |
| 200 | + async promptSudo(pluginName: string, data: CommandRequestData): Promise<string | undefined> { |
202 | 201 | ctx.log(`Plugin: "${pluginName}" requires root access to run command: "sudo ${data.command}"`); |
203 | 202 | const title = `Plugin: "${pluginName}" requires root access to run command: "sudo ${data.command}"`; |
204 | 203 |
|
205 | | - let password; |
206 | | - |
207 | | - // Password is only needed outside of sudo timeout. Pass password in as undefined if not needed. |
208 | | - if (secureMode || !(await SudoUtils.validate())) { |
209 | | - password = await this.getUserPassword(title); |
210 | | - } |
211 | | - |
212 | | - return password; |
| 204 | + return this.promptSudoPassword({ title, cancellable: false }); |
213 | 205 | } |
214 | 206 |
|
215 | 207 | displayPlan(plan: Plan): void { |
@@ -315,71 +307,53 @@ export class DefaultReporter implements Reporter { |
315 | 307 | } |
316 | 308 |
|
317 | 309 | private async handleInlineSudoPassword(): Promise<void> { |
| 310 | + ctx.log('Prompting sudo to save password for rest of requests'); |
| 311 | + const password = await this.promptSudoPassword({ cancellable: true }); |
| 312 | + |
| 313 | + if (password != null) { |
| 314 | + const isValid = await (this.sudoPasswordSubmittedCallback?.(password) ?? Promise.resolve(false)); |
| 315 | + if (isValid) { |
| 316 | + store.set(store.isSudoPasswordCached, true); |
| 317 | + } |
| 318 | + } |
| 319 | + |
| 320 | + await this.displayProgress(); |
| 321 | + } |
| 322 | + |
| 323 | + // Shared prompt loop for both inline (cancellable) and blocking (non-cancellable) sudo password entry. |
| 324 | + // Returns the validated password, or undefined if the user cancelled (only possible when cancellable=true). |
| 325 | + private async promptSudoPassword(opts: { title?: string; cancellable: boolean }): Promise<string | undefined> { |
| 326 | + const { title, cancellable } = opts; |
| 327 | + let hasError = false; |
318 | 328 | let attemptCount = 0; |
319 | 329 |
|
320 | | - while (attemptCount < 3) { |
| 330 | + while (true) { |
| 331 | + attemptCount++; |
321 | 332 | const result = (await Promise.all([ |
322 | | - this.updateRenderState(RenderStatus.SUDO_PROMPT, { attemptCount, cancellable: true }), |
| 333 | + this.updateRenderState(RenderStatus.SUDO_PROMPT, { attemptCount, hasError, cancellable, title }), |
323 | 334 | Promise.race([ |
324 | 335 | this.awaitEvent<string>(RenderEvent.SUDO_PROMPT_RESULT), |
325 | | - this.awaitEvent<'cancel'>(RenderEvent.SUDO_PASSWORD_CANCEL).then(() => Symbol.for('cancel')), |
| 336 | + ...(cancellable |
| 337 | + ? [this.awaitEvent<'cancel'>(RenderEvent.SUDO_PASSWORD_CANCEL).then(() => Symbol.for('cancel'))] |
| 338 | + : []), |
326 | 339 | ]), |
327 | 340 | ])).at(1) as string | symbol; |
328 | 341 |
|
329 | 342 | if (result === Symbol.for('cancel')) { |
330 | | - ctx.log('Sudo password cancelled'); |
331 | | - break; |
332 | | - } else { |
333 | | - ctx.log('Sudo password attempt'); |
| 343 | + return undefined; |
334 | 344 | } |
335 | 345 |
|
336 | | - const isValid = await (this.sudoPasswordSubmittedCallback?.(result as string) ?? Promise.resolve(false)); |
| 346 | + const password = result as string; |
| 347 | + const isValid = await SudoUtils.validate(password); |
| 348 | + |
337 | 349 | if (isValid) { |
338 | 350 | ctx.log('Sudo password successful!'); |
339 | | - |
340 | | - await this.displayProgress(); |
341 | | - this.renderEmitter.emit(RenderEvent.SUDO_PASSWORD_PRE_SUPPLIED); |
342 | | - return; |
343 | | - } |
344 | | - |
345 | | - ctx.log('Sudo password failed'); |
346 | | - attemptCount++; |
347 | | - } |
348 | | - |
349 | | - // Cancelled or all attempts exhausted — restore progress display |
350 | | - await this.displayProgress(); |
351 | | - } |
352 | | - |
353 | | - private async getUserPassword(title?: string): Promise<string> { |
354 | | - let attemptCount = 0; |
355 | | - |
356 | | - while (attemptCount < 3) { |
357 | | - const passwordAttempt = await this.updateStateAndAwaitEvent<string>( |
358 | | - () => this.updateRenderState(RenderStatus.SUDO_PROMPT, { attemptCount, cancellable: false, title }), |
359 | | - RenderEvent.SUDO_PROMPT_RESULT, |
360 | | - ); |
361 | | - |
362 | | - // Validates that the password works |
363 | | - if (await SudoUtils.validate(passwordAttempt)) { |
364 | | - // Drop the sudo session so it is not cached for future prompts. |
365 | | - // The inline sudo path (handleInlineSudoPassword) caches intentionally; |
366 | | - // this path (promptSudo) must not. |
367 | 351 | await SudoUtils.invalidate(); |
368 | | - await this.displayProgress(); |
369 | | - return passwordAttempt; |
| 352 | + return password; |
370 | 353 | } |
371 | 354 |
|
372 | | - if (attemptCount + 1 < 3) { |
373 | | - ctx.log('Password:') |
374 | | - ctx.log(chalk.red(`Sorry, try again. (${attemptCount + 1}/3)`)) |
375 | | - } |
376 | | - |
377 | | - attemptCount++; |
| 355 | + hasError = true; |
378 | 356 | } |
379 | | - |
380 | | - this.updateRenderState(null) |
381 | | - store.set(store.renderState, { status: null }); |
382 | | - throw new Error('sudo: 3 incorrect password attempts') |
383 | 357 | } |
384 | 358 |
|
385 | 359 | private getRenderState(): { status: RenderStatus, data: any } { |
|
0 commit comments