Skip to content

Commit 2a43158

Browse files
committed
feat: Added sudoAskpass support. Removed unnecessary log event emitter. Changed raw logs to not be cyan
1 parent 1cb1c10 commit 2a43158

7 files changed

Lines changed: 116 additions & 101 deletions

File tree

package-lock.json

Lines changed: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
},
66
"dependencies": {
77
"@codifycli/ink-form": "0.0.12",
8-
"@codifycli/schemas": "1.1.0-beta4",
8+
"@codifycli/schemas": "1.1.0-beta5",
99
"@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5",
1010
"@mischnic/json-sourcemap": "^0.1.1",
1111
"@oclif/core": "^4.0.8",
@@ -43,7 +43,7 @@
4343
},
4444
"description": "Codify is a configuration-as-code tool that declaratively installs and manages developer tools and applications. Check out https://dashboard.codifycli.com for an editor.",
4545
"devDependencies": {
46-
"@codifycli/plugin-core": "^1.0.0",
46+
"@codifycli/plugin-core": "^1.1.0-beta13",
4747
"@oclif/prettier-config": "^0.2.1",
4848
"@types/chalk": "^2.2.0",
4949
"@types/cors": "^2.8.19",
@@ -144,7 +144,7 @@
144144
"deploy": "npm run pkg && npm run notarize && npm run upload",
145145
"prepublishOnly": "npm run build"
146146
},
147-
"version": "1.1.0-beta",
147+
"version": "1.1.0-beta2",
148148
"bugs": "https://github.com/codifycli/codify/issues",
149149
"keywords": [
150150
"oclif",

src/common/base-command.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export abstract class BaseCommand extends Command {
6868
ctx.on(Event.COMMAND_REQUEST, async (pluginName: string, data: CommandRequestData) => {
6969
try {
7070
let password = undefined;
71-
if (data.options.requiresRoot) {
71+
if (data.options.requiresRoot || data.options.requiresSudoAskpass) {
7272
if (flags.secure || !cachedSudoPassword) {
7373
password = (await this.reporter.promptSudo(pluginName, data))
7474
} else {
@@ -80,6 +80,10 @@ export abstract class BaseCommand extends Command {
8080
await this.reporter.hide();
8181
console.log(chalk.blue(`Plugin "${pluginName}" is requesting stdin`));
8282

83+
if (this.reporter instanceof DefaultReporter) {
84+
this.reporter.rawOutput = true;
85+
}
86+
8387
// Raw mode is needed by stdin applications to function properly
8488
process.stdin.setRawMode(true);
8589
}
@@ -94,6 +98,11 @@ export abstract class BaseCommand extends Command {
9498
// Always disable raw mode after
9599
if (data.options.stdin) {
96100
process.stdin.setRawMode(false);
101+
102+
if (this.reporter instanceof DefaultReporter) {
103+
this.reporter.rawOutput = false;
104+
}
105+
97106
await this.reporter.displayProgress();
98107
}
99108
}

src/ui/components/default-component.tsx

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,6 @@ export function DefaultComponent(props: {
2828
const { emitter } = props
2929
const [{ status: renderStatus, data: renderData }] = useAtom(store.renderState);
3030

31-
// Use layoutEffect runs before the first render, whereas useEffect runs after
32-
useLayoutEffect(() => {
33-
const logListener = (log: string) => {
34-
console.log(chalk.cyan(log));
35-
};
36-
37-
emitter.on(RenderEvent.LOG, logListener);
38-
39-
return () => {
40-
emitter.off(RenderEvent.LOG, logListener);
41-
}
42-
}, []);
43-
4431
return <Box flexDirection="column">
4532
{
4633
renderStatus === RenderStatus.DISPLAY_MESSAGE && (

src/ui/reporters/default-reporter.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { DefaultComponent } from '../components/default-component.js';
1616
import { ProgressState, ProgressStatus } from '../components/progress/progress-display.js';
1717
import { RenderStatus, store } from '../store/index.js';
1818
import { PromptType, RenderEvent, Reporter } from './reporter.js';
19+
import chalk from 'chalk';
1920

2021
const ProgressLabelMapping = {
2122
[ProcessName.TEST]: 'Codify test',
@@ -48,6 +49,7 @@ export class DefaultReporter implements Reporter {
4849
private verbosityToggleCallback: (() => void) | null = null;
4950
private sudoPasswordSubmittedCallback: ((password: string) => Promise<boolean>) | null = null;
5051
silent = false;
52+
rawOutput = false;
5153

5254
constructor() {
5355
render(<DefaultComponent emitter={this.renderEmitter}/>);
@@ -249,10 +251,10 @@ export class DefaultReporter implements Reporter {
249251
void this.updateRenderState(RenderStatus.DISPLAY_FILE_MODIFICATION, diff);
250252
}
251253

252-
private log(args: string): void {
254+
private log(log: string): void {
253255
if (this.silent) return;
254256

255-
this.renderEmitter.emit(RenderEvent.LOG, args);
257+
console.log(this.rawOutput ? log : chalk.cyan(log));
256258
}
257259

258260
private onProcessStartEvent(name: ProcessName): void {

src/ui/reporters/reporter.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { PlainReporter } from './plain-reporter.js';
1212
import { StubReporter } from './stub-reporter.js';
1313

1414
export enum RenderEvent {
15-
LOG = 'log',
1615
PROGRESS_UPDATE = 'progressUpdate',
1716
PROMPT_RESULT = 'promptConfirmation',
1817
PROMPT_SUDO = 'promptSudo',

src/utils/spawn.ts

Lines changed: 87 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import * as pty from '@homebridge/node-pty-prebuilt-multiarch';
22
import { SpawnStatus } from '@codifycli/schemas';
3+
import { unlinkSync, writeFileSync } from 'node:fs';
4+
import { tmpdir } from 'node:os';
5+
import { join } from 'node:path';
36
import stripAnsi from 'strip-ansi';
47

58
import { SpawnError } from '../common/errors.js';
@@ -18,6 +21,7 @@ export interface SpawnOptions {
1821
env?: Record<string, unknown>,
1922
interactive?: boolean,
2023
requiresRoot?: boolean,
24+
requiresSudoAskpass?: boolean,
2125
stdin?: boolean,
2226
timeout?: number,
2327
allowSudoInCommand?: boolean,
@@ -34,7 +38,7 @@ export async function spawn(cmd: string, options?: SpawnOptions, pluginName?: st
3438
}
3539

3640
export async function spawnSafe(cmd: string, options?: SpawnOptions, pluginName?: string, password?: string): Promise<SpawnResult> {
37-
if (options?.requiresRoot && !password) {
41+
if ((options?.requiresRoot || options?.requiresSudoAskpass) && !password) {
3842
throw new Error('Password must be specified!');
3943
}
4044

@@ -43,88 +47,102 @@ export async function spawnSafe(cmd: string, options?: SpawnOptions, pluginName?
4347
}
4448

4549
if (pluginName) {
46-
ctx.pluginStdout(pluginName, `Running command: ${options?.requiresRoot ? 'sudo' : ''} ${cmd}` + (options?.cwd ? `(${options?.cwd})` : ''))
50+
ctx.pluginStdout(pluginName, `Running command: ${options?.requiresRoot ? 'sudo' : options?.requiresSudoAskpass ? 'sudo (askpass)' : ''} ${cmd}` + (options?.cwd ? `(${options?.cwd})` : ''))
4751
} else {
4852
ctx.log(`Running command: ${cmd}` + (options?.cwd ? `(${options?.cwd})` : '') + '\n');
4953
}
5054

51-
return new Promise((resolve) => {
52-
const output: string[] = [];
53-
const historyIgnore = ShellUtils.getShell() === Shell.ZSH ? { HISTORY_IGNORE: '*' } : { HISTIGNORE: '*' };
54-
55-
// If TERM_PROGRAM=Apple_Terminal is set then ANSI escape characters may be included
56-
// in the response.
57-
const env = {
58-
...process.env, ...options?.env,
59-
TERM_PROGRAM: 'codify',
60-
COMMAND_MODE: 'unix2003',
61-
COLORTERM: 'truecolor',
62-
...historyIgnore
63-
}
64-
65-
// Initial terminal dimensions
66-
const initialCols = process.stdout.columns ?? 80;
67-
const initialRows = process.stdout.rows ?? 24;
68-
69-
const command = options?.requiresRoot ? `sudo -k >/dev/null 2>&1; sudo -S <<< "${password}" -E ${ShellUtils.getDefaultShell()} ${options?.interactive ? '-i' : ''} -c "${cmd.replaceAll('"', '\\"')}"` : cmd;
70-
const args = options?.interactive ? ['-i', '-c', command] : ['-c', command]
71-
72-
// Run the command in a pty for interactivity
73-
const mPty = pty.spawn(ShellUtils.getDefaultShell(), args, {
74-
...options,
75-
cols: initialCols,
76-
rows: initialRows,
77-
env
78-
});
55+
let tmpFile: string | undefined;
56+
if (options?.requiresSudoAskpass && password) {
57+
tmpFile = join(tmpdir(), `codify-askpass-${Date.now()}.sh`);
58+
const escapedPassword = password.replace(/'/g, "'\\''");
59+
writeFileSync(tmpFile, `#!/bin/bash\necho '${escapedPassword}'`, { mode: 0o700 });
60+
}
7961

80-
mPty.onData((data) => {
81-
if (pluginName && !options?.stdin) {
82-
ctx.pluginStdout(pluginName, data)
83-
} else {
84-
ctx.log(data);
62+
try {
63+
return await new Promise<SpawnResult>((resolve) => {
64+
const output: string[] = [];
65+
const historyIgnore = ShellUtils.getShell() === Shell.ZSH ? { HISTORY_IGNORE: '*' } : { HISTIGNORE: '*' };
66+
67+
// If TERM_PROGRAM=Apple_Terminal is set then ANSI escape characters may be included
68+
// in the response.
69+
const env = {
70+
...process.env, ...options?.env,
71+
...(tmpFile ? { SUDO_ASKPASS: tmpFile } : {}),
72+
TERM_PROGRAM: 'codify',
73+
COMMAND_MODE: 'unix2003',
74+
COLORTERM: 'truecolor',
75+
...historyIgnore
8576
}
8677

87-
output.push(data.toString());
88-
})
89-
90-
const resizeListener = () => {
91-
const { columns, rows } = process.stdout;
92-
mPty.resize(columns, rows);
93-
}
78+
// Initial terminal dimensions
79+
const initialCols = process.stdout.columns ?? 80;
80+
const initialRows = process.stdout.rows ?? 24;
81+
82+
const command = options?.requiresRoot ? `sudo -k >/dev/null 2>&1; sudo -S <<< "${password}" -E ${ShellUtils.getDefaultShell()} ${options?.interactive ? '-i' : ''} -c "${cmd.replaceAll('"', '\\"')}"` : cmd;
83+
const args = options?.interactive ? ['-i', '-c', command] : ['-c', command]
84+
85+
// Run the command in a pty for interactivity
86+
const mPty = pty.spawn(ShellUtils.getDefaultShell(), args, {
87+
...options,
88+
cols: initialCols,
89+
rows: initialRows,
90+
env
91+
});
92+
93+
mPty.onData((data) => {
94+
if (pluginName && !options?.stdin) {
95+
ctx.pluginStdout(pluginName, data)
96+
} else {
97+
ctx.log(data);
98+
}
99+
100+
output.push(data.toString());
101+
})
94102

95-
const stdinListener = (data: Buffer | string) => {
96-
// console.log('stdinListener', data);
97-
mPty.write(data.toString());
98-
}
103+
const resizeListener = () => {
104+
const { columns, rows } = process.stdout;
105+
mPty.resize(columns, rows);
106+
}
99107

100-
// Listen to resize events for the terminal window;
101-
process.stdout.on('resize', resizeListener);
102-
if (options?.stdin) {
103-
process.stdin.on('data', stdinListener)
104-
}
108+
const stdinListener = (data: Buffer | string) => {
109+
// console.log('stdinListener', data);
110+
mPty.write(data.toString());
111+
}
105112

106-
mPty.onExit((result) => {
107-
process.stdout.off('resize', resizeListener);
113+
// Listen to resize events for the terminal window;
114+
process.stdout.on('resize', resizeListener);
108115
if (options?.stdin) {
109-
process.stdin.off('data', stdinListener);
116+
process.stdin.on('data', stdinListener)
110117
}
111118

112-
resolve({
113-
status: result.exitCode === 0 ? SpawnStatus.SUCCESS : SpawnStatus.ERROR,
114-
exitCode: result.exitCode,
115-
data: stripAnsi(output.join('\n').trim()),
116-
})
117-
});
119+
mPty.onExit((result) => {
120+
process.stdout.off('resize', resizeListener);
121+
if (options?.stdin) {
122+
process.stdin.off('data', stdinListener);
123+
}
118124

119-
if (options?.timeout) {
120-
setTimeout(() => {
121-
mPty.kill();
122125
resolve({
123-
status: SpawnStatus.ERROR,
124-
exitCode: -1,
125-
data: '',
126-
});
127-
}, options.timeout);
126+
status: result.exitCode === 0 ? SpawnStatus.SUCCESS : SpawnStatus.ERROR,
127+
exitCode: result.exitCode,
128+
data: stripAnsi(output.join('\n').trim()),
129+
})
130+
});
131+
132+
if (options?.timeout) {
133+
setTimeout(() => {
134+
mPty.kill();
135+
resolve({
136+
status: SpawnStatus.ERROR,
137+
exitCode: -1,
138+
data: '',
139+
});
140+
}, options.timeout);
141+
}
142+
});
143+
} finally {
144+
if (tmpFile) {
145+
try { unlinkSync(tmpFile); } catch { /* best effort */ }
128146
}
129-
})
147+
}
130148
}

0 commit comments

Comments
 (0)