Skip to content

Commit 1cb1c10

Browse files
committed
Refactoring to fix structural issues and improvements
1 parent 02cf2ef commit 1cb1c10

8 files changed

Lines changed: 51 additions & 82 deletions

File tree

src/common/base-command.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export abstract class BaseCommand extends Command {
4949

5050
if (this.reporter instanceof DefaultReporter) {
5151
if (cachedSudoPassword !== null) {
52-
this.reporter.notifySudoPasswordPreSupplied();
52+
this.reporter.setSudoPasswordCached();
5353
}
5454

5555
this.reporter.onSudoPasswordSubmitted(async (password: string) => {
@@ -67,9 +67,14 @@ export abstract class BaseCommand extends Command {
6767

6868
ctx.on(Event.COMMAND_REQUEST, async (pluginName: string, data: CommandRequestData) => {
6969
try {
70-
const password = data.options.requiresRoot
71-
? cachedSudoPassword ?? (await this.reporter.promptSudo(pluginName, data, flags.secure))
72-
: undefined;
70+
let password = undefined;
71+
if (data.options.requiresRoot) {
72+
if (flags.secure || !cachedSudoPassword) {
73+
password = (await this.reporter.promptSudo(pluginName, data))
74+
} else {
75+
password = cachedSudoPassword
76+
}
77+
}
7378

7479
if (data.options.stdin) {
7580
await this.reporter.hide();

src/ui/components/default-component.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ export function DefaultComponent(props: {
8484
{
8585
renderStatus === RenderStatus.SUDO_PROMPT && (
8686
<SudoPasswordInput
87-
key={(renderData as { attemptCount: number }).attemptCount}
87+
key={ (renderData as { attemptCount: number }).attemptCount}
8888
title={(renderData as { title?: string }).title}
89-
hasError={(renderData as { attemptCount: number }).attemptCount > 0}
89+
hasError={(renderData as { hasError: boolean }).hasError}
9090
cancellable={(renderData as { cancellable: boolean }).cancellable}
9191
onSubmit={(password) => emitter.emit(RenderEvent.SUDO_PROMPT_RESULT, password)}
9292
onCancel={() => emitter.emit(RenderEvent.SUDO_PASSWORD_CANCEL)}

src/ui/components/progress/progress-display.tsx

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,10 @@ export function ProgressDisplay(props: { emitter: EventEmitter }) {
2828
const { emitter } = props;
2929
const [progress] = useAtom(store.progressState);
3030
const [isVerbose, setIsVerbose] = useState(false);
31-
const [passwordSaved, setPasswordSaved] = useState(false);
31+
const [passwordSaved] = useAtom(store.isSudoPasswordCached);
3232

3333
const isApplyOrDestroy = progress?.name === ProcessName.APPLY || progress?.name === ProcessName.DESTROY;
3434

35-
useLayoutEffect(() => {
36-
const onPreSupplied = () => setPasswordSaved(true);
37-
38-
emitter.on(RenderEvent.SUDO_PASSWORD_PRE_SUPPLIED, onPreSupplied);
39-
40-
return () => {
41-
emitter.off(RenderEvent.SUDO_PASSWORD_PRE_SUPPLIED, onPreSupplied);
42-
};
43-
}, []);
44-
4535
useInput((input) => {
4636
if (!isApplyOrDestroy) return;
4737
if (input === 'v') {

src/ui/components/widgets/SudoPasswordInput.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ export function SudoPasswordInput(props: {
5757
{isChecking ? (
5858
<Spinner label="Checking password..." />
5959
) : (
60-
<Box gap={1}>
61-
<Text bold color={borderColor}>Sudo Password:</Text>
60+
<Box>
61+
<Text bold color={borderColor}>Sudo Password: </Text>
6262
<Text>{value.replace(/./g, '*')}</Text>
6363
<Text inverse> </Text>
6464
</Box>

src/ui/reporters/default-reporter.tsx

Lines changed: 34 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { FormProps, FormReturnValue } from '@codifycli/ink-form';
2-
import chalk from 'chalk';
32
import { CommandRequestData } from '@codifycli/schemas';
43
import { render } from 'ink';
54
import { EventEmitter } from 'node:events';
@@ -76,8 +75,8 @@ export class DefaultReporter implements Reporter {
7675
this.sudoPasswordSubmittedCallback = callback;
7776
}
7877

79-
notifySudoPasswordPreSupplied(): void {
80-
setImmediate(() => this.renderEmitter.emit(RenderEvent.SUDO_PASSWORD_PRE_SUPPLIED));
78+
setSudoPasswordCached(): void {
79+
store.set(store.isSudoPasswordCached, true);
8180
}
8281

8382
async promptPressKeyToContinue(message?: string): Promise<void> {
@@ -198,18 +197,11 @@ export class DefaultReporter implements Reporter {
198197
void this.updateRenderState(RenderStatus.DISPLAY_IMPORT_RESULT, { importResult, showConfigs });
199198
}
200199

201-
async promptSudo(pluginName: string, data: CommandRequestData, secureMode: boolean): Promise<string | undefined> {
200+
async promptSudo(pluginName: string, data: CommandRequestData): Promise<string | undefined> {
202201
ctx.log(`Plugin: "${pluginName}" requires root access to run command: "sudo ${data.command}"`);
203202
const title = `Plugin: "${pluginName}" requires root access to run command: "sudo ${data.command}"`;
204203

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 });
213205
}
214206

215207
displayPlan(plan: Plan): void {
@@ -315,71 +307,53 @@ export class DefaultReporter implements Reporter {
315307
}
316308

317309
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;
318328
let attemptCount = 0;
319329

320-
while (attemptCount < 3) {
330+
while (true) {
331+
attemptCount++;
321332
const result = (await Promise.all([
322-
this.updateRenderState(RenderStatus.SUDO_PROMPT, { attemptCount, cancellable: true }),
333+
this.updateRenderState(RenderStatus.SUDO_PROMPT, { attemptCount, hasError, cancellable, title }),
323334
Promise.race([
324335
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+
: []),
326339
]),
327340
])).at(1) as string | symbol;
328341

329342
if (result === Symbol.for('cancel')) {
330-
ctx.log('Sudo password cancelled');
331-
break;
332-
} else {
333-
ctx.log('Sudo password attempt');
343+
return undefined;
334344
}
335345

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+
337349
if (isValid) {
338350
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.
367351
await SudoUtils.invalidate();
368-
await this.displayProgress();
369-
return passwordAttempt;
352+
return password;
370353
}
371354

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;
378356
}
379-
380-
this.updateRenderState(null)
381-
store.set(store.renderState, { status: null });
382-
throw new Error('sudo: 3 incorrect password attempts')
383357
}
384358

385359
private getRenderState(): { status: RenderStatus, data: any } {

src/ui/reporters/plain-reporter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export class PlainReporter implements Reporter {
128128
}
129129
}
130130

131-
async promptSudo(pluginName: string, data: CommandRequestData, secureMode: boolean): Promise<string | undefined> {
131+
async promptSudo(pluginName: string, data: CommandRequestData): Promise<string | undefined> {
132132
ctx.log(chalk.blue(`Plugin: "${pluginName}" requires root access to run command: "${data.command}"`));
133133
return undefined;
134134
}

src/ui/reporters/reporter.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ export enum RenderEvent {
2626
TOGGLE_VERBOSITY = 'toggleVerbosity',
2727
SUDO_PASSWORD_TOGGLE = 'sudoPasswordToggle',
2828
SUDO_PASSWORD_CANCEL = 'sudoPasswordCancel',
29-
SUDO_PASSWORD_PRE_SUPPLIED = 'sudoPasswordPreSupplied',
3029
}
3130

3231
/**
@@ -66,7 +65,7 @@ export interface Reporter {
6665

6766
promptOptions(message: string, options: string[]): Promise<number>;
6867

69-
promptSudo(pluginName: string, data: CommandRequestData, secureMode: boolean): Promise<string | undefined>;
68+
promptSudo(pluginName: string, data: CommandRequestData): Promise<string | undefined>;
7069

7170
promptUserForValues(resources: Array<ResourceInfo>, promptType: PromptType): Promise<ResourceConfig[]>;
7271

src/ui/store/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export const store = new class {
3333
renderData = atom((get) => get(this.renderState).data)
3434

3535
progressState = atom(null as ProgressState | null)
36+
isSudoPasswordCached = atom(false)
3637

3738
get<Value>(atom: Atom<Value>): Value {
3839
return this.internal.get(atom);

0 commit comments

Comments
 (0)