Skip to content

Commit a0f241c

Browse files
committed
feat: Added toggle for adding sudo password during apply
1 parent c644025 commit a0f241c

4 files changed

Lines changed: 113 additions & 11 deletions

File tree

src/common/base-command.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import createDebug from 'debug';
77
import { LoginHelper } from '../connect/login-helper.js';
88
import { ctx, Event } from '../events/context.js';
99
import { LoginOrchestrator } from '../orchestrators/login.js';
10+
import { DefaultReporter } from '../ui/reporters/default-reporter.js';
1011
import { Reporter, ReporterFactory, ReporterType } from '../ui/reporters/reporter.js';
1112
import { spawnSafe } from '../utils/spawn.js';
13+
import { SudoUtils } from '../utils/sudo.js';
1214
import { prettyPrintError } from './errors.js';
1315

1416
export abstract class BaseCommand extends Command {
@@ -43,21 +45,32 @@ export abstract class BaseCommand extends Command {
4345
const reporterType = this.getReporterType(flags)
4446
this.reporter = ReporterFactory.create(reporterType)
4547

48+
let cachedSudoPassword: string | null = flags.sudoPassword ?? null;
49+
50+
if (this.reporter instanceof DefaultReporter) {
51+
if (cachedSudoPassword !== null) {
52+
this.reporter.notifySudoPasswordPreSupplied();
53+
}
54+
55+
this.reporter.onSudoPasswordSubmitted(async (password: string) => {
56+
const isValid = SudoUtils.validate(password);
57+
if (isValid) {
58+
cachedSudoPassword = password;
59+
}
60+
(this.reporter as DefaultReporter).notifySudoPasswordResult(isValid);
61+
});
62+
}
63+
4664
if (flags.secure) {
4765
console.log(chalk.blue('Running Codify in secure mode. Sudo will be prompted every time'));
4866
}
4967

5068
ctx.on(Event.COMMAND_REQUEST, async (pluginName: string, data: CommandRequestData) => {
5169
try {
5270
const password = data.options.requiresRoot
53-
? (flags.sudoPassword) ?? (await this.reporter.promptSudo(pluginName, data, flags.secure))
71+
? cachedSudoPassword ?? (await this.reporter.promptSudo(pluginName, data, flags.secure))
5472
: undefined;
5573

56-
// We print that we used sudo everytime even if the user provides it in the beginning
57-
if (flags.sudoPassword && data.options.requiresRoot) {
58-
console.log(chalk.blue(`Plugin: "${pluginName}" requires root access to run command: "sudo ${data.command}"`));
59-
}
60-
6174
if (data.options.stdin) {
6275
await this.reporter.hide();
6376
console.log(chalk.blue(`Plugin "${pluginName}" is requesting stdin`));

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

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { PasswordInput } from '@inkjs/ui';
12
import { Box, Text, useInput } from 'ink';
23
import { useAtom } from 'jotai';
34
import { EventEmitter } from 'node:events';
4-
import React, { useState } from 'react';
5+
import React, { useLayoutEffect, useState } from 'react';
56

67
import { ProcessName } from '../../../events/context.js';
78
import { RenderEvent } from '../../reporters/reporter.js';
@@ -28,15 +29,59 @@ export function ProgressDisplay(props: { emitter: EventEmitter }) {
2829
const { emitter } = props;
2930
const [progress] = useAtom(store.progressState);
3031
const [isVerbose, setIsVerbose] = useState(false);
32+
const [isEnteringPassword, setIsEnteringPassword] = useState(false);
33+
const [passwordError, setPasswordError] = useState(false);
34+
const [passwordAttempts, setPasswordAttempts] = useState(0);
35+
const [passwordSaved, setPasswordSaved] = useState(false);
36+
const [passwordInputKey, setPasswordInputKey] = useState(0);
3137

3238
const isApplyOrDestroy = progress?.name === ProcessName.APPLY || progress?.name === ProcessName.DESTROY;
3339

40+
useLayoutEffect(() => {
41+
const onResult = ({ success }: { success: boolean }) => {
42+
if (success) {
43+
setPasswordSaved(true);
44+
setIsEnteringPassword(false);
45+
setPasswordError(false);
46+
setPasswordAttempts(0);
47+
} else {
48+
setPasswordAttempts((prev) => {
49+
const next = prev + 1;
50+
if (next >= 3) {
51+
setIsEnteringPassword(false);
52+
setPasswordError(false);
53+
return 0;
54+
}
55+
setPasswordError(true);
56+
setPasswordInputKey((k) => k + 1);
57+
return next;
58+
});
59+
}
60+
};
61+
62+
const onPreSupplied = () => setPasswordSaved(true);
63+
64+
emitter.on(RenderEvent.SUDO_PASSWORD_RESULT, onResult);
65+
emitter.on(RenderEvent.SUDO_PASSWORD_PRE_SUPPLIED, onPreSupplied);
66+
67+
return () => {
68+
emitter.off(RenderEvent.SUDO_PASSWORD_RESULT, onResult);
69+
emitter.off(RenderEvent.SUDO_PASSWORD_PRE_SUPPLIED, onPreSupplied);
70+
};
71+
}, []);
72+
3473
useInput((input) => {
3574
if (!isApplyOrDestroy) return;
3675
if (input === 'v') {
3776
setIsVerbose((prev) => !prev);
3877
emitter.emit(RenderEvent.TOGGLE_VERBOSITY);
3978
}
79+
if (input === 'p' && !passwordSaved) {
80+
setIsEnteringPassword((prev) => !prev);
81+
setPasswordError(false);
82+
setPasswordAttempts(0);
83+
setPasswordInputKey((k) => k + 1);
84+
}
4085
});
4186

4287
if (!progress) {
@@ -51,11 +96,35 @@ export function ProgressDisplay(props: { emitter: EventEmitter }) {
5196
? <Spinner label={label} type="dots" />
5297
: <Text><Text color='greenBright'></Text> {label}</Text>
5398
}
54-
<Box flexDirection="column" marginLeft={2}>
55-
<SubProgressDisplay subProgresses={subProgresses}/>
56-
</Box>
99+
100+
{!isEnteringPassword && (
101+
<Box flexDirection="column" marginLeft={2}>
102+
<SubProgressDisplay subProgresses={subProgresses}/>
103+
</Box>
104+
)}
105+
106+
{isEnteringPassword && (
107+
<Box flexDirection="column" marginTop={1}>
108+
<Text color={passwordError ? 'red' : 'cyan'}>{'─'.repeat(40)}</Text>
109+
<Box>
110+
<Text> Password: </Text>
111+
<PasswordInput key={passwordInputKey} onSubmit={(pw) => emitter.emit(RenderEvent.SUDO_PASSWORD_SUBMITTED, pw)} />
112+
</Box>
113+
{passwordError && (
114+
<Text color="red">{` Incorrect password, try again (${passwordAttempts}/3)`}</Text>
115+
)}
116+
<Text color={passwordError ? 'red' : 'cyan'}>{'─'.repeat(40)}</Text>
117+
</Box>
118+
)}
119+
57120
{isApplyOrDestroy && (
58-
<Text dimColor>{isVerbose ? '[v] Hide verbose logs' : '[v] Show verbose logs'}</Text>
121+
<Box flexDirection="column">
122+
<Text dimColor>{isVerbose ? '[v] Hide verbose logs' : '[v] Show verbose logs'}</Text>
123+
{passwordSaved
124+
? <Text color="green">✓ sudo password</Text>
125+
: <Text dimColor>{isEnteringPassword ? '[p] Cancel' : '[p] Enter sudo password'}</Text>
126+
}
127+
</Box>
59128
)}
60129
</Box>
61130
}

src/ui/reporters/default-reporter.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export class DefaultReporter implements Reporter {
4747
private renderEmitter = new EventEmitter();
4848
private progressState: ProgressState | null = null
4949
private verbosityToggleCallback: (() => void) | null = null;
50+
private sudoPasswordSubmittedCallback: ((password: string) => void) | null = null;
5051
silent = false;
5152

5253
constructor() {
@@ -61,12 +62,28 @@ export class DefaultReporter implements Reporter {
6162
this.renderEmitter.on(RenderEvent.TOGGLE_VERBOSITY, () => {
6263
this.verbosityToggleCallback?.();
6364
});
65+
66+
this.renderEmitter.on(RenderEvent.SUDO_PASSWORD_SUBMITTED, (password: string) => {
67+
this.sudoPasswordSubmittedCallback?.(password);
68+
});
6469
}
6570

6671
onVerbosityToggle(callback: () => void): void {
6772
this.verbosityToggleCallback = callback;
6873
}
6974

75+
onSudoPasswordSubmitted(callback: (password: string) => void): void {
76+
this.sudoPasswordSubmittedCallback = callback;
77+
}
78+
79+
notifySudoPasswordResult(success: boolean): void {
80+
this.renderEmitter.emit(RenderEvent.SUDO_PASSWORD_RESULT, { success });
81+
}
82+
83+
notifySudoPasswordPreSupplied(): void {
84+
setImmediate(() => this.renderEmitter.emit(RenderEvent.SUDO_PASSWORD_PRE_SUPPLIED));
85+
}
86+
7087
async promptPressKeyToContinue(message?: string): Promise<void> {
7188
const previousRenderState = this.getRenderState();
7289

src/ui/reporters/reporter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ export enum RenderEvent {
2424
SUDO_PROMPT_RESULT = 'promptSudoResult',
2525
STATE_TRANSITION = 'stateTransition',
2626
TOGGLE_VERBOSITY = 'toggleVerbosity',
27+
SUDO_PASSWORD_SUBMITTED = 'sudoPasswordSubmitted',
28+
SUDO_PASSWORD_RESULT = 'sudoPasswordResult',
29+
SUDO_PASSWORD_PRE_SUPPLIED = 'sudoPasswordPreSupplied',
2730
}
2831

2932
/**

0 commit comments

Comments
 (0)