b4fbd6fe9f
Deploy Site / deploy-vercel (push) Has been skipped
Deploy Site / deploy-docs (push) Has been skipped
Build Skills Index / build-index (push) Has been skipped
CI / Deny unrelated histories (push) Has been skipped
CI / Detect affected areas (push) Successful in 27m35s
CI / OSV scan (push) Failing after 4s
CI / Build&Test Docker image (push) Successful in 9s
CI / Supply-chain scan (push) Has been skipped
CI / Lint Docker scripts (push) Failing after 5m13s
CI / Check contributors (push) Failing after 12m8s
CI / Docs Site (push) Failing after 12m8s
CI / TypeScript (push) Failing after 12m8s
CI / Python lints (push) Failing after 12m9s
CI / Python tests (push) Failing after 12m9s
CI / Check uv.lock (push) Failing after 23m22s
CI / CI timing report (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
71 lines
2.1 KiB
TypeScript
71 lines
2.1 KiB
TypeScript
import { spawnSync } from 'node:child_process'
|
|
import { accessSync, constants, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { delimiter, join } from 'node:path'
|
|
|
|
import { withInkSuspended } from '@hermes/ink'
|
|
|
|
/**
|
|
* Editor fallback chain when neither $VISUAL nor $EDITOR is set. Mirrors
|
|
* prompt_toolkit's `Buffer.open_in_editor()` picker so the classic CLI and
|
|
* the TUI launch the same editor on a given box.
|
|
*/
|
|
const FALLBACKS = ['editor', 'nano', 'pico', 'vi', 'emacs']
|
|
|
|
const isExecutable = (path: string): boolean => {
|
|
try {
|
|
accessSync(path, constants.X_OK)
|
|
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve the editor invocation argv (without the file argument).
|
|
*
|
|
* 1. $VISUAL / $EDITOR, shell-tokenized so `EDITOR="code --wait"` works
|
|
* 2. on POSIX: first FALLBACKS entry resolvable on $PATH
|
|
* 3. on Windows: `notepad.exe`
|
|
* 4. literal `['vi']` as the last-resort POSIX floor
|
|
*/
|
|
export const resolveEditor = (
|
|
env: NodeJS.ProcessEnv = process.env,
|
|
platform: NodeJS.Platform = process.platform
|
|
): string[] => {
|
|
const explicit = env.VISUAL ?? env.EDITOR
|
|
|
|
if (explicit?.trim()) {
|
|
return explicit.trim().split(/\s+/)
|
|
}
|
|
|
|
if (platform === 'win32') {
|
|
return ['notepad.exe']
|
|
}
|
|
|
|
const dirs = (env.PATH ?? '').split(delimiter).filter(Boolean)
|
|
const found = FALLBACKS.flatMap(name => dirs.map(d => join(d, name))).find(isExecutable)
|
|
|
|
return [found ?? 'vi']
|
|
}
|
|
|
|
/** Suspend Ink, open ``initial`` in $EDITOR, return the edited text (null if aborted). */
|
|
export async function openInEditor(initial: string, suffix = '.txt'): Promise<null | string> {
|
|
const dir = mkdtempSync(join(tmpdir(), 'hermes-edit-'))
|
|
const file = join(dir, `edit${suffix}`)
|
|
writeFileSync(file, initial)
|
|
const [cmd, ...args] = resolveEditor()
|
|
let status: null | number = null
|
|
|
|
await withInkSuspended(async () => {
|
|
status = spawnSync(cmd!, [...args, file], { stdio: 'inherit' }).status
|
|
})
|
|
|
|
try {
|
|
return status === 0 ? readFileSync(file, 'utf8') : null
|
|
} finally {
|
|
rmSync(dir, { force: true, recursive: true })
|
|
}
|
|
}
|