Skip to content

Commit 1bc02e7

Browse files
committed
feat: Added new spawn code using node-pty. Root and stdin are both supported now
1 parent 9e4e7cc commit 1bc02e7

File tree

12 files changed

+213
-167
lines changed

12 files changed

+213
-167
lines changed

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"ajv": "^8.12.0",
1616
"ajv-formats": "^3.0.1",
1717
"chalk": "^5.3.0",
18-
"codify-schemas": "^1.0.83",
18+
"codify-schemas": "1.0.86-beta5",
1919
"cors": "^2.8.5",
2020
"debug": "^4.3.4",
2121
"detect-indent": "^7.0.1",

src/commands/import.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import fs from 'node:fs/promises';
44

55
import { BaseCommand } from '../common/base-command.js';
66
import { ImportOrchestrator } from '../orchestrators/import.js';
7-
import { ShellUtils } from '../utils/shell.js';
7+
import { Shell, ShellUtils } from '../utils/shell.js';
88

99
export default class Import extends BaseCommand {
1010
static strict = false;
@@ -76,7 +76,7 @@ For more information, visit: https://docs.codifycli.com/commands/import`
7676

7777
private async cleanupZshStarExpansion(args: string[]): Promise<string[]> {
7878
const combinedArgs = args.join(' ');
79-
const zshStarExpansion = (await ShellUtils.isZshShell())
79+
const zshStarExpansion = (ShellUtils.getShell() === Shell.ZSH)
8080
? (await fs.readdir(process.cwd())).filter((name) => !name.startsWith('.')).join(' ')
8181
: ''
8282

src/commands/refresh.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
import chalk from 'chalk';
21
import fs from 'node:fs/promises';
3-
import path from 'node:path';
42

53
import { BaseCommand } from '../common/base-command.js';
6-
import { ImportOrchestrator } from '../orchestrators/import.js';
7-
import { ShellUtils } from '../utils/shell.js';
4+
import { Shell, ShellUtils } from '../utils/shell.js';
85
import { RefreshOrchestrator } from '../orchestrators/refresh.js';
96

107
export default class Refresh extends BaseCommand {
@@ -53,7 +50,7 @@ For more information, visit: https://docs.codifycli.com/commands/refresh`
5350

5451
private async cleanupZshStarExpansion(args: string[]): Promise<string[]> {
5552
const combinedArgs = args.join(' ');
56-
const zshStarExpansion = (await ShellUtils.isZshShell())
53+
const zshStarExpansion = (ShellUtils.getShell() === Shell.ZSH)
5754
? (await fs.readdir(process.cwd())).filter((name) => !name.startsWith('.')).join(' ')
5855
: ''
5956

src/common/base-command.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { Command, Flags } from '@oclif/core';
22
import { OutputFlags } from '@oclif/core/interfaces';
33
import chalk from 'chalk';
4-
import { PressKeyToContinueRequestData, SudoRequestData } from 'codify-schemas';
4+
import { CommandRequestData, PressKeyToContinueRequestData } from 'codify-schemas';
55
import createDebug from 'debug';
66

77
import { LoginHelper } from '../connect/login-helper.js';
88
import { Event, ctx } from '../events/context.js';
99
import { LoginOrchestrator } from '../orchestrators/login.js';
1010
import { Reporter, ReporterFactory, ReporterType } from '../ui/reporters/reporter.js';
11-
import { SudoUtils } from '../utils/sudo.js';
11+
import { spawnSafe } from '../utils/spawn.js';
1212
import { prettyPrintError } from './errors.js';
1313

1414
export abstract class BaseCommand extends Command {
@@ -48,16 +48,37 @@ export abstract class BaseCommand extends Command {
4848
console.log(chalk.blue('Running Codify in secure mode. Sudo will be prompted every time'));
4949
}
5050

51-
ctx.on(Event.SUDO_REQUEST, async (pluginName: string, data: SudoRequestData) => {
51+
ctx.on(Event.COMMAND_REQUEST, async (pluginName: string, data: CommandRequestData) => {
5252
try {
53-
const password = (flags.sudoPassword) ?? (await this.reporter.promptSudo(pluginName, data, flags.secure));
53+
const password = data.options.requiresRoot
54+
? (flags.sudoPassword) ?? (await this.reporter.promptSudo(pluginName, data, flags.secure))
55+
: undefined;
5456

55-
const result = await SudoUtils.runCommand(data.command, data.options, flags.secure, pluginName, password)
56-
ctx.sudoRequestGranted(pluginName, result);
57+
// We print that we used sudo everytime even if the user provides it in the beginning
58+
if (flags.sudoPassword && data.options.requiresRoot) {
59+
console.log(chalk.blue(`Plugin: "${pluginName}" requires root access to run command: "${data.command}"`));
60+
}
61+
62+
if (data.options.stdin) {
63+
await this.reporter.hide();
64+
console.log(chalk.blue(`Plugin "${pluginName}" is requesting stdin`));
65+
66+
// Raw mode is needed by stdin applications to function properly
67+
process.stdin.setRawMode(true);
68+
}
69+
70+
const result = await spawnSafe(data.command, pluginName, data.options, password)
71+
ctx.commandRequestCompleted(pluginName, result);
5772

5873
// This listener is outside of the base-command callstack. We have to manually catch the error.
5974
} catch (error) {
6075
this.catch(error as Error);
76+
} finally {
77+
// Always disable raw mode after
78+
if (data.options.stdin) {
79+
process.stdin.setRawMode(false);
80+
await this.reporter.displayProgress();
81+
}
6182
}
6283
});
6384

src/events/context.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import type { SudoRequestData, SudoRequestResponseData } from 'codify-schemas';
2-
1+
import { CommandRequestData, CommandRequestResponseData } from 'codify-schemas';
32
import { EventEmitter } from 'node:events';
43

54
export enum Event {
@@ -13,8 +12,8 @@ export enum Event {
1312
STDOUT = 'stdout',
1413
SUB_PROCESS_FINISH = 'sub_process_finish',
1514
SUB_PROCESS_START = 'sub_process_start',
16-
SUDO_REQUEST = 'sudo_request',
17-
SUDO_REQUEST_GRANTED = 'sudo_request_granted',
15+
COMMAND_REQUEST = 'command_request',
16+
COMMAND_REQUEST_GRANTED = 'command_request_granted',
1817
PRESS_KEY_TO_CONTINUE_REQUEST = 'press_key_to_continue_request',
1918
PRESS_KEY_TO_CONTINUE_COMPLETED = 'press_key_to_continue_completed',
2019
CODIFY_LOGIN_CREDENTIALS_REQUEST = 'codify_login_credentials_request',
@@ -103,12 +102,12 @@ export const ctx = new class {
103102
this.emitter.emit(Event.SUB_PROCESS_FINISH, name, additionalName);
104103
}
105104

106-
sudoRequested(pluginName: string, data: SudoRequestData) {
107-
this.emitter.emit(Event.SUDO_REQUEST, pluginName, data);
105+
commandRequested(pluginName: string, data: CommandRequestData) {
106+
this.emitter.emit(Event.COMMAND_REQUEST, pluginName, data);
108107
}
109108

110-
sudoRequestGranted(pluginName: string, data: SudoRequestResponseData) {
111-
this.emitter.emit(Event.SUDO_REQUEST_GRANTED, pluginName, data);
109+
commandRequestCompleted(pluginName: string, data: CommandRequestResponseData) {
110+
this.emitter.emit(Event.COMMAND_REQUEST_GRANTED, pluginName, data);
112111
}
113112

114113
pressToContinueRequested(pluginName: string, data: any) {

src/orchestrators/connect.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class ConnectOrchestrator {
2929
}
3030

3131

32-
private static tokenGenerate(bytes = 4): string {
32+
private static tokenGenerate(bytes = 16): string {
3333
return Buffer.from(randomBytes(bytes)).toString('hex')
3434
}
3535
}

src/plugins/plugin-process.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import {
1+
import {
2+
CommandRequestData,
3+
CommandRequestDataSchema,
4+
CommandRequestResponseData,
25
IpcMessageV2,
36
IpcMessageV2Schema,
47
MessageCmd,
58
PressKeyToContinueRequestData,
69
PressKeyToContinueRequestDataSchema,
7-
SudoRequestData,
8-
SudoRequestDataSchema
910
} from 'codify-schemas';
1011
import { ChildProcess, fork } from 'node:child_process';
1112
import { createRequire } from 'node:module';
@@ -16,7 +17,7 @@ import { sendIpcMessageForResult } from './message-sender.js';
1617
import { PluginMessage } from './plugin-message.js';
1718

1819
export const ipcMessageValidator = ajv.compile(IpcMessageV2Schema);
19-
export const sudoRequestValidator = ajv.compile(SudoRequestDataSchema);
20+
export const commandRequestValidator = ajv.compile(CommandRequestDataSchema);
2021
export const pressKeyToContinueRequestValidator = ajv.compile(PressKeyToContinueRequestDataSchema);
2122

2223
const DEFAULT_NODE_MODULES_DIR = '/usr/local/lib/codify/node_modules/'
@@ -57,6 +58,7 @@ export class PluginProcess {
5758
},
5859
);
5960

61+
// Note: stdin is not hooked up on purpose for security purposes. Interactive commands + sudo will have to run through the parent process.
6062
_process.stdout!.on('data', (message) => ctx.pluginStdout(name, message.toString('utf8')));
6163
_process.stderr!.on('data', (message) => ctx.pluginStderr(name, message.toString('utf8')));
6264
_process.on('exit', (code) => {
@@ -80,24 +82,24 @@ export class PluginProcess {
8082
throw new Error(`Invalid message from plugin. ${JSON.stringify(message, null, 2)}`);
8183
}
8284

83-
if (message.cmd === MessageCmd.SUDO_REQUEST) {
85+
if (message.cmd === MessageCmd.COMMAND_REQUEST) {
8486
const { data, requestId } = message;
85-
if (!sudoRequestValidator(data)) {
86-
throw new Error(`Invalid sudo request from plugin ${pluginName}. ${JSON.stringify(sudoRequestValidator.errors, null, 2)}`);
87+
if (!commandRequestValidator(data)) {
88+
throw new Error(`Invalid command request from plugin ${pluginName}. ${JSON.stringify(commandRequestValidator.errors, null, 2)}`);
8789
}
8890

89-
// Send out sudo granted events
90-
ctx.once(Event.SUDO_REQUEST_GRANTED, (_pluginName, data) => {
91+
// Send out command completed
92+
ctx.once(Event.COMMAND_REQUEST_GRANTED, (_pluginName, data) => {
9193
if (_pluginName === pluginName) {
9294
process.send({
93-
cmd: returnMessageCmd(MessageCmd.SUDO_REQUEST),
95+
cmd: returnMessageCmd(MessageCmd.COMMAND_REQUEST),
9496
requestId,
9597
data
9698
})
9799
}
98100
})
99101

100-
return ctx.sudoRequested(pluginName, data as unknown as SudoRequestData);
102+
return ctx.commandRequested(pluginName, data as unknown as CommandRequestData);
101103
}
102104

103105
if (message.cmd === MessageCmd.PRESS_KEY_TO_CONTINUE_REQUEST) {
@@ -106,7 +108,6 @@ export class PluginProcess {
106108
throw new Error(`Invalid press key to continue request from plugin ${pluginName}. ${JSON.stringify(pressKeyToContinueRequestValidator.errors, null, 2)}`);
107109
}
108110

109-
// Send out sudo granted events
110111
ctx.once(Event.PRESS_KEY_TO_CONTINUE_COMPLETED, (_pluginName) => {
111112
if (_pluginName === pluginName) {
112113
process.send({

src/utils/shell.ts

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,48 @@ import cp from 'node:child_process';
33

44
const exec = util.promisify(cp.exec);
55

6-
export class ShellUtils {
7-
static async isZshShell(): Promise<boolean> {
8-
try {
9-
await exec('echo $ZSH_VERSION');
10-
return true;
11-
} catch {
12-
return false;
6+
export enum Shell {
7+
ZSH = 'zsh',
8+
BASH = 'bash',
9+
SH = 'sh',
10+
KSH = 'ksh',
11+
CSH = 'csh',
12+
FISH = 'fish',
13+
}
14+
15+
16+
export const ShellUtils = {
17+
getShell(): Shell | undefined {
18+
const shell = process.env.SHELL || '';
19+
20+
if (shell.endsWith('bash')) {
21+
return Shell.BASH
22+
}
23+
24+
if (shell.endsWith('zsh')) {
25+
return Shell.ZSH
26+
}
27+
28+
if (shell.endsWith('sh')) {
29+
return Shell.SH
1330
}
14-
}
1531

32+
if (shell.endsWith('csh')) {
33+
return Shell.CSH
34+
}
35+
36+
if (shell.endsWith('ksh')) {
37+
return Shell.KSH
38+
}
39+
40+
if (shell.endsWith('fish')) {
41+
return Shell.FISH
42+
}
43+
44+
return undefined;
45+
},
46+
47+
getDefaultShell(): string {
48+
return process.env.SHELL!;
49+
}
1650
}

0 commit comments

Comments
 (0)