@inquirer/external-editor
Advanced tools
| export declare class CreateFileError extends Error { | ||
| name: string; | ||
| originalError: unknown; | ||
| constructor(originalError: unknown); | ||
| } | ||
| export declare class LaunchEditorError extends Error { | ||
| name: string; | ||
| originalError: unknown; | ||
| constructor(originalError: unknown); | ||
| } | ||
| export declare class ReadFileError extends Error { | ||
| name: string; | ||
| originalError: unknown; | ||
| constructor(originalError: unknown); | ||
| } | ||
| export declare class RemoveFileError extends Error { | ||
| name: string; | ||
| originalError: unknown; | ||
| constructor(originalError: unknown); | ||
| } |
| export class CreateFileError extends Error { | ||
| name = 'CreateFileError'; | ||
| originalError; | ||
| constructor(originalError) { | ||
| super(`Failed to create temporary file.${originalError instanceof Error ? ` ${originalError.message}` : ''}`, { cause: originalError }); | ||
| this.originalError = originalError; | ||
| } | ||
| } | ||
| export class LaunchEditorError extends Error { | ||
| name = 'LaunchEditorError'; | ||
| originalError; | ||
| constructor(originalError) { | ||
| super(`Failed to launch editor.${originalError instanceof Error ? ` ${originalError.message}` : ''}`, { cause: originalError }); | ||
| this.originalError = originalError; | ||
| } | ||
| } | ||
| export class ReadFileError extends Error { | ||
| name = 'ReadFileError'; | ||
| originalError; | ||
| constructor(originalError) { | ||
| super(`Failed to read temporary file.${originalError instanceof Error ? ` ${originalError.message}` : ''}`, { cause: originalError }); | ||
| this.originalError = originalError; | ||
| } | ||
| } | ||
| export class RemoveFileError extends Error { | ||
| name = 'RemoveFileError'; | ||
| originalError; | ||
| constructor(originalError) { | ||
| super(`Failed to remove temporary file.${originalError instanceof Error ? ` ${originalError.message}` : ''}`, { cause: originalError }); | ||
| this.originalError = originalError; | ||
| } | ||
| } |
| export type EditorParams = { | ||
| args: string[]; | ||
| bin: string; | ||
| }; | ||
| export declare function parseEditorCommand(editor: string): EditorParams; |
| export function parseEditorCommand(editor) { | ||
| let bin; | ||
| let rest; | ||
| if (editor.startsWith('"')) { | ||
| const closeQuote = editor.indexOf('"', 1); | ||
| if (closeQuote === -1) { | ||
| // Unmatched quote — treat the whole string as the binary | ||
| bin = editor.slice(1); | ||
| rest = ''; | ||
| } | ||
| else { | ||
| bin = editor.substring(1, closeQuote); | ||
| rest = editor.substring(closeQuote + 1).trim(); | ||
| } | ||
| } | ||
| else { | ||
| const firstSpace = editor.indexOf(' '); | ||
| if (firstSpace === -1) { | ||
| bin = editor; | ||
| rest = ''; | ||
| } | ||
| else { | ||
| bin = editor.substring(0, firstSpace); | ||
| rest = editor.substring(firstSpace + 1).trim(); | ||
| } | ||
| } | ||
| return { bin, args: rest ? rest.split(/\s+/) : [] }; | ||
| } |
+21
-27
@@ -1,10 +0,5 @@ | ||
| import { CreateFileError } from './errors/CreateFileError.ts'; | ||
| import { LaunchEditorError } from './errors/LaunchEditorError.ts'; | ||
| import { ReadFileError } from './errors/ReadFileError.ts'; | ||
| import { RemoveFileError } from './errors/RemoveFileError.ts'; | ||
| export interface IEditorParams { | ||
| args: string[]; | ||
| bin: string; | ||
| } | ||
| export interface IFileOptions { | ||
| import { CreateFileError, LaunchEditorError, ReadFileError, RemoveFileError } from './errors.ts'; | ||
| import { type EditorParams } from './parse-editor-command.ts'; | ||
| type StringCallback = (err: Error | undefined, result: string | undefined) => void; | ||
| export type FileOptions = { | ||
| prefix?: string; | ||
@@ -15,26 +10,25 @@ postfix?: string; | ||
| dir?: string; | ||
| } | ||
| export type StringCallback = (err: Error | undefined, result: string | undefined) => void; | ||
| export type VoidCallback = () => void; | ||
| }; | ||
| /** @deprecated Use FileOptions */ | ||
| export type IFileOptions = FileOptions; | ||
| export { CreateFileError, LaunchEditorError, ReadFileError, RemoveFileError }; | ||
| export declare function edit(text?: string, fileOptions?: IFileOptions): string; | ||
| export declare function editAsync(text: string | undefined, callback: StringCallback, fileOptions?: IFileOptions): void; | ||
| export declare function edit(text?: string, fileOptions?: FileOptions): string; | ||
| type EditAsync = { | ||
| /** @deprecated Use editAsync(text, options) returning a Promise instead */ | ||
| (text: string, callback: StringCallback, fileOptions?: FileOptions): Promise<string>; | ||
| (text?: string, fileOptions?: FileOptions): Promise<string>; | ||
| }; | ||
| export declare const editAsync: EditAsync; | ||
| export declare class ExternalEditor { | ||
| text: string; | ||
| tempFile: string; | ||
| editor: IEditorParams; | ||
| editor: EditorParams; | ||
| lastExitStatus: number; | ||
| private text; | ||
| private tempFile; | ||
| private fileOptions; | ||
| get temp_file(): string; | ||
| get last_exit_status(): number; | ||
| constructor(text?: string, fileOptions?: IFileOptions); | ||
| constructor(text?: string, fileOptions?: FileOptions); | ||
| run(): string; | ||
| runAsync(callback: StringCallback): void; | ||
| cleanup(): void; | ||
| private determineEditor; | ||
| private createTemporaryFile; | ||
| runAsync(callback?: StringCallback): Promise<string>; | ||
| private cleanup; | ||
| private createTempFile; | ||
| private readTemporaryFile; | ||
| private removeTemporaryFile; | ||
| private launchEditor; | ||
| private launchEditorAsync; | ||
| } |
+65
-123
| import { detect } from 'chardet'; | ||
| import { spawn, spawnSync } from 'child_process'; | ||
| import { readFileSync, unlinkSync, writeFileSync } from 'fs'; | ||
| import { spawn, spawnSync } from 'node:child_process'; | ||
| import { readFileSync, unlinkSync, writeFileSync } from 'node:fs'; | ||
| import path from 'node:path'; | ||
@@ -8,30 +8,13 @@ import os from 'node:os'; | ||
| import iconv from 'iconv-lite'; | ||
| import { CreateFileError } from "./errors/CreateFileError.js"; | ||
| import { LaunchEditorError } from "./errors/LaunchEditorError.js"; | ||
| import { ReadFileError } from "./errors/ReadFileError.js"; | ||
| import { RemoveFileError } from "./errors/RemoveFileError.js"; | ||
| import { CreateFileError, LaunchEditorError, ReadFileError, RemoveFileError, } from "./errors.js"; | ||
| import { parseEditorCommand } from "./parse-editor-command.js"; | ||
| export { CreateFileError, LaunchEditorError, ReadFileError, RemoveFileError }; | ||
| export function edit(text = '', fileOptions) { | ||
| const editor = new ExternalEditor(text, fileOptions); | ||
| editor.run(); | ||
| editor.cleanup(); | ||
| return editor.text; | ||
| return new ExternalEditor(text, fileOptions).run(); | ||
| } | ||
| export function editAsync(text = '', callback, fileOptions) { | ||
| const editor = new ExternalEditor(text, fileOptions); | ||
| editor.runAsync((err, result) => { | ||
| if (err) { | ||
| setImmediate(callback, err, undefined); | ||
| } | ||
| else { | ||
| try { | ||
| editor.cleanup(); | ||
| setImmediate(callback, undefined, result); | ||
| } | ||
| catch (cleanupError) { | ||
| setImmediate(callback, cleanupError, undefined); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| export const editAsync = (text, callbackOrOptions, fileOptions) => { | ||
| const callback = typeof callbackOrOptions === 'function' ? callbackOrOptions : undefined; | ||
| const options = typeof callbackOrOptions === 'function' ? fileOptions : callbackOrOptions; | ||
| return new ExternalEditor(text, options).runAsync(callback); | ||
| }; | ||
| function sanitizeAffix(affix) { | ||
@@ -42,82 +25,70 @@ if (!affix) | ||
| } | ||
| function splitStringBySpace(str) { | ||
| const pieces = []; | ||
| let currentString = ''; | ||
| for (let strIndex = 0; strIndex < str.length; strIndex++) { | ||
| const currentLetter = str.charAt(strIndex); | ||
| if (strIndex > 0 && | ||
| currentLetter === ' ' && | ||
| str[strIndex - 1] !== '\\' && | ||
| currentString.length > 0) { | ||
| pieces.push(currentString); | ||
| currentString = ''; | ||
| } | ||
| else { | ||
| currentString = `${currentString}${currentLetter}`; | ||
| } | ||
| } | ||
| if (currentString.length > 0) { | ||
| pieces.push(currentString); | ||
| } | ||
| return pieces; | ||
| } | ||
| export class ExternalEditor { | ||
| text = ''; | ||
| tempFile; | ||
| editor; | ||
| lastExitStatus = 0; | ||
| text = ''; | ||
| tempFile = ''; | ||
| fileOptions = {}; | ||
| get temp_file() { | ||
| console.log('DEPRECATED: temp_file. Use tempFile moving forward.'); | ||
| return this.tempFile; | ||
| } | ||
| get last_exit_status() { | ||
| console.log('DEPRECATED: last_exit_status. Use lastExitStatus moving forward.'); | ||
| return this.lastExitStatus; | ||
| } | ||
| constructor(text = '', fileOptions) { | ||
| constructor(text = '', fileOptions = {}) { | ||
| this.text = text; | ||
| if (fileOptions) { | ||
| this.fileOptions = fileOptions; | ||
| } | ||
| this.determineEditor(); | ||
| this.createTemporaryFile(); | ||
| this.fileOptions = fileOptions; | ||
| this.editor = parseEditorCommand(process.env['VISUAL'] ?? | ||
| process.env['EDITOR'] ?? | ||
| (process.platform.startsWith('win') ? 'notepad' : 'vim')); | ||
| } | ||
| run() { | ||
| this.launchEditor(); | ||
| this.readTemporaryFile(); | ||
| return this.text; | ||
| this.createTempFile(); | ||
| try { | ||
| try { | ||
| const editorProcess = spawnSync(this.editor.bin, this.editor.args.concat([this.tempFile]), { stdio: 'inherit' }); | ||
| this.lastExitStatus = editorProcess.status ?? 0; | ||
| } | ||
| catch (launchError) { | ||
| throw new LaunchEditorError(launchError); | ||
| } | ||
| this.readTemporaryFile(); | ||
| return this.text; | ||
| } | ||
| finally { | ||
| this.cleanup(); | ||
| } | ||
| } | ||
| runAsync(callback) { | ||
| this.createTempFile(); | ||
| const promise = new Promise((resolve, reject) => { | ||
| try { | ||
| const editorProcess = spawn(this.editor.bin, this.editor.args.concat([this.tempFile]), { stdio: 'inherit' }); | ||
| editorProcess.on('exit', (code) => { | ||
| this.lastExitStatus = code; | ||
| resolve(); | ||
| }); | ||
| } | ||
| catch (launchError) { | ||
| reject(new LaunchEditorError(launchError)); | ||
| } | ||
| }) | ||
| .then(() => { | ||
| this.readTemporaryFile(); | ||
| return this.text; | ||
| }) | ||
| .finally(() => { | ||
| this.cleanup(); | ||
| }); | ||
| if (callback) { | ||
| promise.then((text) => callback(undefined, text), (err) => callback(err instanceof Error ? err : new Error(String(err)), undefined)); | ||
| } | ||
| return promise; | ||
| } | ||
| cleanup() { | ||
| if (!this.tempFile) | ||
| return; | ||
| try { | ||
| this.launchEditorAsync(() => { | ||
| try { | ||
| this.readTemporaryFile(); | ||
| setImmediate(callback, undefined, this.text); | ||
| } | ||
| catch (readError) { | ||
| setImmediate(callback, readError, undefined); | ||
| } | ||
| }); | ||
| unlinkSync(this.tempFile); | ||
| this.tempFile = ''; | ||
| } | ||
| catch (launchError) { | ||
| setImmediate(callback, launchError, undefined); | ||
| catch (removeFileError) { | ||
| throw new RemoveFileError(removeFileError); | ||
| } | ||
| } | ||
| cleanup() { | ||
| this.removeTemporaryFile(); | ||
| } | ||
| determineEditor() { | ||
| const editor = process.env['VISUAL'] | ||
| ? process.env['VISUAL'] | ||
| : process.env['EDITOR'] | ||
| ? process.env['EDITOR'] | ||
| : process.platform.startsWith('win') | ||
| ? 'notepad' | ||
| : 'vim'; | ||
| const editorOpts = splitStringBySpace(editor).map((piece) => piece.replace('\\ ', ' ')); | ||
| const bin = editorOpts.shift(); | ||
| this.editor = { args: editorOpts, bin }; | ||
| } | ||
| createTemporaryFile() { | ||
| createTempFile() { | ||
| try { | ||
@@ -164,31 +135,2 @@ const baseDir = this.fileOptions.dir ?? os.tmpdir(); | ||
| } | ||
| removeTemporaryFile() { | ||
| try { | ||
| unlinkSync(this.tempFile); | ||
| } | ||
| catch (removeFileError) { | ||
| throw new RemoveFileError(removeFileError); | ||
| } | ||
| } | ||
| launchEditor() { | ||
| try { | ||
| const editorProcess = spawnSync(this.editor.bin, this.editor.args.concat([this.tempFile]), { stdio: 'inherit' }); | ||
| this.lastExitStatus = editorProcess.status ?? 0; | ||
| } | ||
| catch (launchError) { | ||
| throw new LaunchEditorError(launchError); | ||
| } | ||
| } | ||
| launchEditorAsync(callback) { | ||
| try { | ||
| const editorProcess = spawn(this.editor.bin, this.editor.args.concat([this.tempFile]), { stdio: 'inherit' }); | ||
| editorProcess.on('exit', (code) => { | ||
| this.lastExitStatus = code; | ||
| setImmediate(callback); | ||
| }); | ||
| } | ||
| catch (launchError) { | ||
| throw new LaunchEditorError(launchError); | ||
| } | ||
| } | ||
| } |
+4
-3
| { | ||
| "name": "@inquirer/external-editor", | ||
| "version": "2.0.4", | ||
| "version": "3.0.0", | ||
| "description": "Edit a string with the users preferred text editor using $VISUAL or $ENVIRONMENT", | ||
@@ -80,3 +80,4 @@ "keywords": [ | ||
| "@types/chardet": "^1.0.0", | ||
| "typescript": "^5.9.3" | ||
| "@types/node": "^25.5.2", | ||
| "typescript": "^6.0.2" | ||
| }, | ||
@@ -96,3 +97,3 @@ "peerDependencies": { | ||
| "types": "./dist/index.d.ts", | ||
| "gitHead": "b218fcc4afe888a58957aa78c9a032f9bd2d60cb" | ||
| "gitHead": "e68fe01d65359e083581c48c4a18cd8f97d88842" | ||
| } |
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
19079
6.46%17
30.77%343
6.52%2
-60%0
-100%3
50%