Skip to content

Commit 4cee759

Browse files
committed
feat: remove the need for repeated sudo passwords
1 parent 26909ed commit 4cee759

5 files changed

Lines changed: 43 additions & 21 deletions

File tree

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@
150150
"prepublishOnly": "npm run build && npm run build:npm",
151151
"postpublish": "npm run restore-npm"
152152
},
153-
"version": "1.2.3",
153+
"version": "1.3.0-beta.2",
154154
"bugs": "https://github.com/codifycli/codify/issues",
155155
"keywords": [
156156
"oclif",

‎src/common/base-command.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ export abstract class BaseCommand extends Command {
4747

4848
let cachedSudoPassword: string | null = flags.sudoPassword ?? null;
4949

50-
if (this.reporter instanceof DefaultReporter) {
50+
// Secure mode never caches the sudo password, so no cache is registered with the reporter
51+
if (this.reporter instanceof DefaultReporter && !flags.secure) {
5152
if (cachedSudoPassword !== null) {
5253
this.reporter.setSudoPasswordCached();
5354
}

‎src/ui/components/default-component.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ export function DefaultComponent(props: {
101101
title={(renderData as { title?: string }).title}
102102
hasError={(renderData as { hasError: boolean }).hasError}
103103
cancellable={(renderData as { cancellable: boolean }).cancellable}
104-
onSubmit={(password) => emitter.emit(RenderEvent.SUDO_PROMPT_RESULT, password)}
104+
savable={(renderData as { savable: boolean }).savable}
105+
onSubmit={(password, save) => emitter.emit(RenderEvent.SUDO_PROMPT_RESULT, { password, save })}
105106
onCancel={() => emitter.emit(RenderEvent.SUDO_PASSWORD_CANCEL)}
106107
/>
107108
)

‎src/ui/components/widgets/SudoPasswordInput.tsx‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ export function SudoPasswordInput(props: {
77
title?: string;
88
hasError: boolean;
99
cancellable: boolean;
10-
onSubmit: (password: string) => void;
10+
savable: boolean;
11+
onSubmit: (password: string, save: boolean) => void;
1112
onCancel: () => void;
1213
}) {
13-
const { title, hasError, cancellable, onSubmit, onCancel } = props;
14+
const { title, hasError, cancellable, savable, onSubmit, onCancel } = props;
1415
const [value, setValue] = useState('');
1516
const [isChecking, setIsChecking] = useState(false);
1617

@@ -24,9 +25,10 @@ export function SudoPasswordInput(props: {
2425
return;
2526
}
2627

27-
if (key.return) {
28+
// Enter saves the password for the rest of the session (when savable). Tab submits without saving.
29+
if (key.return || (key.tab && savable)) {
2830
setIsChecking(true);
29-
onSubmit(value);
31+
onSubmit(value, savable && key.return);
3032
setValue('');
3133
return;
3234
}
@@ -64,7 +66,12 @@ export function SudoPasswordInput(props: {
6466
</Box>
6567
)}
6668
{hasError && <Text color="red">Incorrect password, try again</Text>}
67-
{!isChecking && <Text dimColor>Enter to confirm{cancellable ? ' · Esc to cancel' : ''}</Text>}
69+
{!isChecking && (
70+
<Text dimColor>
71+
{savable ? 'Enter to confirm and save for this session · Tab to confirm without saving' : 'Enter to confirm'}
72+
{cancellable ? ' · Esc to cancel' : ''}
73+
</Text>
74+
)}
6875
</Box>
6976
);
7077
}

‎src/ui/reporters/default-reporter.tsx‎

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -248,9 +248,19 @@ export class DefaultReporter implements Reporter {
248248
ctx.log(`Plugin: "${pluginName}" requires root access to run command: "sudo ${data.command}"`);
249249
const title = `Plugin: "${pluginName}" requires root access to run command: "sudo ${data.command}"`;
250250

251-
const password = await this.promptSudoPassword({ title, cancellable: false });
251+
// Saving is only offered when a cache is registered (i.e. not in secure mode)
252+
const savable = this.sudoPasswordSubmittedCallback !== null;
253+
const result = await this.promptSudoPassword({ title, cancellable: false, savable });
254+
255+
if (result?.save) {
256+
const isValid = await this.sudoPasswordSubmittedCallback!(result.password);
257+
if (isValid) {
258+
store.set(store.isSudoPasswordCached, true);
259+
}
260+
}
261+
252262
await this.displayProgress();
253-
return password;
263+
return result?.password;
254264
}
255265

256266
async displayPlan(plan: Plan): Promise<void> {
@@ -380,10 +390,10 @@ export class DefaultReporter implements Reporter {
380390

381391
private async handleInlineSudoPassword(): Promise<void> {
382392
ctx.log('Prompting sudo to save password for rest of requests');
383-
const password = await this.promptSudoPassword({ cancellable: true });
393+
const result = await this.promptSudoPassword({ cancellable: true, savable: false });
384394

385-
if (password != null) {
386-
const isValid = await (this.sudoPasswordSubmittedCallback?.(password) ?? Promise.resolve(false));
395+
if (result != null) {
396+
const isValid = await (this.sudoPasswordSubmittedCallback?.(result.password) ?? Promise.resolve(false));
387397
if (isValid) {
388398
store.set(store.isSudoPasswordCached, true);
389399
}
@@ -393,35 +403,38 @@ export class DefaultReporter implements Reporter {
393403
}
394404

395405
// Shared prompt loop for both inline (cancellable) and blocking (non-cancellable) sudo password entry.
396-
// Returns the validated password, or undefined if the user cancelled (only possible when cancellable=true).
397-
private async promptSudoPassword(opts: { title?: string; cancellable: boolean }): Promise<string | undefined> {
398-
const { title, cancellable } = opts;
406+
// Returns the validated password and whether the user chose to save it, or undefined if the user cancelled
407+
// (only possible when cancellable=true).
408+
private async promptSudoPassword(
409+
opts: { title?: string; cancellable: boolean; savable: boolean }
410+
): Promise<{ password: string; save: boolean } | undefined> {
411+
const { title, cancellable, savable } = opts;
399412
let hasError = false;
400413
let attemptCount = 0;
401414

402415
while (true) {
403416
attemptCount++;
404417
const result = (await Promise.all([
405-
this.updateRenderState(RenderStatus.SUDO_PROMPT, { attemptCount, hasError, cancellable, title }),
418+
this.updateRenderState(RenderStatus.SUDO_PROMPT, { attemptCount, hasError, cancellable, savable, title }),
406419
Promise.race([
407-
this.awaitEvent<string>(RenderEvent.SUDO_PROMPT_RESULT),
420+
this.awaitEvent<{ password: string; save: boolean }>(RenderEvent.SUDO_PROMPT_RESULT),
408421
...(cancellable
409422
? [this.awaitEvent<'cancel'>(RenderEvent.SUDO_PASSWORD_CANCEL).then(() => Symbol.for('cancel'))]
410423
: []),
411424
]),
412-
])).at(1) as string | symbol;
425+
])).at(1) as { password: string; save: boolean } | symbol;
413426

414427
if (result === Symbol.for('cancel')) {
415428
return undefined;
416429
}
417430

418-
const password = result as string;
431+
const { password, save } = result as { password: string; save: boolean };
419432
const isValid = await SudoUtils.validate(password);
420433

421434
if (isValid) {
422435
ctx.log('Sudo password successful!');
423436
await SudoUtils.invalidate();
424-
return password;
437+
return { password, save };
425438
}
426439

427440
hasError = true;

0 commit comments

Comments
 (0)