Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 11:56:03 +08:00

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 })
}
}