feat: unwrap 11 util namespaces to flat exports + barrel (#22739)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import path from "path"
|
||||
import { Process } from "./process"
|
||||
import { Process } from "."
|
||||
|
||||
export async function extractZip(zipPath: string, destDir: string) {
|
||||
if (process.platform === "win32") {
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
export namespace Color {
|
||||
export function isValidHex(hex?: string): hex is string {
|
||||
if (!hex) return false
|
||||
return /^#[0-9a-fA-F]{6}$/.test(hex)
|
||||
}
|
||||
|
||||
export function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
||||
const r = parseInt(hex.slice(1, 3), 16)
|
||||
const g = parseInt(hex.slice(3, 5), 16)
|
||||
const b = parseInt(hex.slice(5, 7), 16)
|
||||
return { r, g, b }
|
||||
}
|
||||
|
||||
export function hexToAnsiBold(hex?: string): string | undefined {
|
||||
if (!isValidHex(hex)) return undefined
|
||||
const { r, g, b } = hexToRgb(hex)
|
||||
return `\x1b[38;2;${r};${g};${b}m\x1b[1m`
|
||||
}
|
||||
export function isValidHex(hex?: string): hex is string {
|
||||
if (!hex) return false
|
||||
return /^#[0-9a-fA-F]{6}$/.test(hex)
|
||||
}
|
||||
|
||||
export function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
||||
const r = parseInt(hex.slice(1, 3), 16)
|
||||
const g = parseInt(hex.slice(3, 5), 16)
|
||||
const b = parseInt(hex.slice(5, 7), 16)
|
||||
return { r, g, b }
|
||||
}
|
||||
|
||||
export function hexToAnsiBold(hex?: string): string | undefined {
|
||||
if (!isValidHex(hex)) return undefined
|
||||
const { r, g, b } = hexToRgb(hex)
|
||||
return `\x1b[38;2;${r};${g};${b}m\x1b[1m`
|
||||
}
|
||||
|
||||
@@ -7,239 +7,237 @@ import { Readable } from "stream"
|
||||
import { pipeline } from "stream/promises"
|
||||
import { Glob } from "@opencode-ai/shared/util/glob"
|
||||
|
||||
export namespace Filesystem {
|
||||
// Fast sync version for metadata checks
|
||||
export async function exists(p: string): Promise<boolean> {
|
||||
return existsSync(p)
|
||||
}
|
||||
// Fast sync version for metadata checks
|
||||
export async function exists(p: string): Promise<boolean> {
|
||||
return existsSync(p)
|
||||
}
|
||||
|
||||
export async function isDir(p: string): Promise<boolean> {
|
||||
try {
|
||||
return statSync(p).isDirectory()
|
||||
} catch {
|
||||
return false
|
||||
export async function isDir(p: string): Promise<boolean> {
|
||||
try {
|
||||
return statSync(p).isDirectory()
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function stat(p: string): ReturnType<typeof statSync> | undefined {
|
||||
return statSync(p, { throwIfNoEntry: false }) ?? undefined
|
||||
}
|
||||
|
||||
export async function statAsync(p: string): Promise<ReturnType<typeof statSync> | undefined> {
|
||||
return statFile(p).catch((e) => {
|
||||
if (isEnoent(e)) return undefined
|
||||
throw e
|
||||
})
|
||||
}
|
||||
|
||||
export async function size(p: string): Promise<number> {
|
||||
const s = stat(p)?.size ?? 0
|
||||
return typeof s === "bigint" ? Number(s) : s
|
||||
}
|
||||
|
||||
export async function readText(p: string): Promise<string> {
|
||||
return readFile(p, "utf-8")
|
||||
}
|
||||
|
||||
export async function readJson<T = any>(p: string): Promise<T> {
|
||||
return JSON.parse(await readFile(p, "utf-8"))
|
||||
}
|
||||
|
||||
export async function readBytes(p: string): Promise<Buffer> {
|
||||
return readFile(p)
|
||||
}
|
||||
|
||||
export async function readArrayBuffer(p: string): Promise<ArrayBuffer> {
|
||||
const buf = await readFile(p)
|
||||
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer
|
||||
}
|
||||
|
||||
function isEnoent(e: unknown): e is { code: "ENOENT" } {
|
||||
return typeof e === "object" && e !== null && "code" in e && (e as { code: string }).code === "ENOENT"
|
||||
}
|
||||
|
||||
export async function write(p: string, content: string | Buffer | Uint8Array, mode?: number): Promise<void> {
|
||||
try {
|
||||
if (mode) {
|
||||
await writeFile(p, content, { mode })
|
||||
} else {
|
||||
await writeFile(p, content)
|
||||
}
|
||||
}
|
||||
|
||||
export function stat(p: string): ReturnType<typeof statSync> | undefined {
|
||||
return statSync(p, { throwIfNoEntry: false }) ?? undefined
|
||||
}
|
||||
|
||||
export async function statAsync(p: string): Promise<ReturnType<typeof statSync> | undefined> {
|
||||
return statFile(p).catch((e) => {
|
||||
if (isEnoent(e)) return undefined
|
||||
throw e
|
||||
})
|
||||
}
|
||||
|
||||
export async function size(p: string): Promise<number> {
|
||||
const s = stat(p)?.size ?? 0
|
||||
return typeof s === "bigint" ? Number(s) : s
|
||||
}
|
||||
|
||||
export async function readText(p: string): Promise<string> {
|
||||
return readFile(p, "utf-8")
|
||||
}
|
||||
|
||||
export async function readJson<T = any>(p: string): Promise<T> {
|
||||
return JSON.parse(await readFile(p, "utf-8"))
|
||||
}
|
||||
|
||||
export async function readBytes(p: string): Promise<Buffer> {
|
||||
return readFile(p)
|
||||
}
|
||||
|
||||
export async function readArrayBuffer(p: string): Promise<ArrayBuffer> {
|
||||
const buf = await readFile(p)
|
||||
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer
|
||||
}
|
||||
|
||||
function isEnoent(e: unknown): e is { code: "ENOENT" } {
|
||||
return typeof e === "object" && e !== null && "code" in e && (e as { code: string }).code === "ENOENT"
|
||||
}
|
||||
|
||||
export async function write(p: string, content: string | Buffer | Uint8Array, mode?: number): Promise<void> {
|
||||
try {
|
||||
} catch (e) {
|
||||
if (isEnoent(e)) {
|
||||
await mkdir(dirname(p), { recursive: true })
|
||||
if (mode) {
|
||||
await writeFile(p, content, { mode })
|
||||
} else {
|
||||
await writeFile(p, content)
|
||||
}
|
||||
} catch (e) {
|
||||
if (isEnoent(e)) {
|
||||
await mkdir(dirname(p), { recursive: true })
|
||||
if (mode) {
|
||||
await writeFile(p, content, { mode })
|
||||
} else {
|
||||
await writeFile(p, content)
|
||||
}
|
||||
return
|
||||
}
|
||||
throw e
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeJson(p: string, data: unknown, mode?: number): Promise<void> {
|
||||
return write(p, JSON.stringify(data, null, 2), mode)
|
||||
}
|
||||
|
||||
export async function writeStream(
|
||||
p: string,
|
||||
stream: ReadableStream<Uint8Array> | Readable,
|
||||
mode?: number,
|
||||
): Promise<void> {
|
||||
const dir = dirname(p)
|
||||
if (!existsSync(dir)) {
|
||||
await mkdir(dir, { recursive: true })
|
||||
}
|
||||
|
||||
const nodeStream = stream instanceof ReadableStream ? Readable.fromWeb(stream as any) : stream
|
||||
const writeStream = createWriteStream(p)
|
||||
await pipeline(nodeStream, writeStream)
|
||||
|
||||
if (mode) {
|
||||
await chmod(p, mode)
|
||||
}
|
||||
}
|
||||
|
||||
export function mimeType(p: string): string {
|
||||
return lookup(p) || "application/octet-stream"
|
||||
}
|
||||
|
||||
/**
|
||||
* On Windows, normalize a path to its canonical casing using the filesystem.
|
||||
* This is needed because Windows paths are case-insensitive but LSP servers
|
||||
* may return paths with different casing than what we send them.
|
||||
*/
|
||||
export function normalizePath(p: string): string {
|
||||
if (process.platform !== "win32") return p
|
||||
const resolved = win32.normalize(win32.resolve(windowsPath(p)))
|
||||
try {
|
||||
return realpathSync.native(resolved)
|
||||
} catch {
|
||||
return resolved
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizePathPattern(p: string): string {
|
||||
if (process.platform !== "win32") return p
|
||||
if (p === "*") return p
|
||||
const match = p.match(/^(.*)[\\/]\*$/)
|
||||
if (!match) return normalizePath(p)
|
||||
const dir = /^[A-Za-z]:$/.test(match[1]) ? match[1] + "\\" : match[1]
|
||||
return join(normalizePath(dir), "*")
|
||||
}
|
||||
|
||||
// We cannot rely on path.resolve() here because git.exe may come from Git Bash, Cygwin, or MSYS2, so we need to translate these paths at the boundary.
|
||||
// Also resolves symlinks so that callers using the result as a cache key
|
||||
// always get the same canonical path for a given physical directory.
|
||||
export function resolve(p: string): string {
|
||||
const resolved = pathResolve(windowsPath(p))
|
||||
try {
|
||||
return normalizePath(realpathSync(resolved))
|
||||
} catch (e) {
|
||||
if (isEnoent(e)) return normalizePath(resolved)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export function windowsPath(p: string): string {
|
||||
if (process.platform !== "win32") return p
|
||||
return (
|
||||
p
|
||||
.replace(/^\/([a-zA-Z]):(?:[\\/]|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
|
||||
// Git Bash for Windows paths are typically /<drive>/...
|
||||
.replace(/^\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
|
||||
// Cygwin git paths are typically /cygdrive/<drive>/...
|
||||
.replace(/^\/cygdrive\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
|
||||
// WSL paths are typically /mnt/<drive>/...
|
||||
.replace(/^\/mnt\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
|
||||
)
|
||||
}
|
||||
export function overlaps(a: string, b: string) {
|
||||
const relA = relative(a, b)
|
||||
const relB = relative(b, a)
|
||||
return !relA || !relA.startsWith("..") || !relB || !relB.startsWith("..")
|
||||
}
|
||||
|
||||
export function contains(parent: string, child: string) {
|
||||
return !relative(parent, child).startsWith("..")
|
||||
}
|
||||
|
||||
export async function findUp(
|
||||
target: string,
|
||||
start: string,
|
||||
stop?: string,
|
||||
options?: { rootFirst?: boolean },
|
||||
): Promise<string[]>
|
||||
export async function findUp(
|
||||
target: string[],
|
||||
start: string,
|
||||
stop?: string,
|
||||
options?: { rootFirst?: boolean },
|
||||
): Promise<string[]>
|
||||
export async function findUp(
|
||||
target: string | string[],
|
||||
start: string,
|
||||
stop?: string,
|
||||
options?: { rootFirst?: boolean },
|
||||
) {
|
||||
const dirs = [start]
|
||||
let current = start
|
||||
while (true) {
|
||||
if (stop === current) break
|
||||
const parent = dirname(current)
|
||||
if (parent === current) break
|
||||
dirs.push(parent)
|
||||
current = parent
|
||||
}
|
||||
|
||||
const targets = Array.isArray(target) ? target : [target]
|
||||
const result = []
|
||||
for (const dir of options?.rootFirst ? dirs.toReversed() : dirs) {
|
||||
for (const item of targets) {
|
||||
const search = join(dir, item)
|
||||
if (await exists(search)) result.push(search)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export async function* up(options: { targets: string[]; start: string; stop?: string }) {
|
||||
const { targets, start, stop } = options
|
||||
let current = start
|
||||
while (true) {
|
||||
for (const target of targets) {
|
||||
const search = join(current, target)
|
||||
if (await exists(search)) yield search
|
||||
}
|
||||
if (stop === current) break
|
||||
const parent = dirname(current)
|
||||
if (parent === current) break
|
||||
current = parent
|
||||
}
|
||||
}
|
||||
|
||||
export async function globUp(pattern: string, start: string, stop?: string) {
|
||||
let current = start
|
||||
const result = []
|
||||
while (true) {
|
||||
try {
|
||||
const matches = await Glob.scan(pattern, {
|
||||
cwd: current,
|
||||
absolute: true,
|
||||
include: "file",
|
||||
dot: true,
|
||||
})
|
||||
result.push(...matches)
|
||||
} catch {
|
||||
// Skip invalid glob patterns
|
||||
}
|
||||
if (stop === current) break
|
||||
const parent = dirname(current)
|
||||
if (parent === current) break
|
||||
current = parent
|
||||
}
|
||||
return result
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeJson(p: string, data: unknown, mode?: number): Promise<void> {
|
||||
return write(p, JSON.stringify(data, null, 2), mode)
|
||||
}
|
||||
|
||||
export async function writeStream(
|
||||
p: string,
|
||||
stream: ReadableStream<Uint8Array> | Readable,
|
||||
mode?: number,
|
||||
): Promise<void> {
|
||||
const dir = dirname(p)
|
||||
if (!existsSync(dir)) {
|
||||
await mkdir(dir, { recursive: true })
|
||||
}
|
||||
|
||||
const nodeStream = stream instanceof ReadableStream ? Readable.fromWeb(stream as any) : stream
|
||||
const writeStream = createWriteStream(p)
|
||||
await pipeline(nodeStream, writeStream)
|
||||
|
||||
if (mode) {
|
||||
await chmod(p, mode)
|
||||
}
|
||||
}
|
||||
|
||||
export function mimeType(p: string): string {
|
||||
return lookup(p) || "application/octet-stream"
|
||||
}
|
||||
|
||||
/**
|
||||
* On Windows, normalize a path to its canonical casing using the filesystem.
|
||||
* This is needed because Windows paths are case-insensitive but LSP servers
|
||||
* may return paths with different casing than what we send them.
|
||||
*/
|
||||
export function normalizePath(p: string): string {
|
||||
if (process.platform !== "win32") return p
|
||||
const resolved = win32.normalize(win32.resolve(windowsPath(p)))
|
||||
try {
|
||||
return realpathSync.native(resolved)
|
||||
} catch {
|
||||
return resolved
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizePathPattern(p: string): string {
|
||||
if (process.platform !== "win32") return p
|
||||
if (p === "*") return p
|
||||
const match = p.match(/^(.*)[\\/]\*$/)
|
||||
if (!match) return normalizePath(p)
|
||||
const dir = /^[A-Za-z]:$/.test(match[1]) ? match[1] + "\\" : match[1]
|
||||
return join(normalizePath(dir), "*")
|
||||
}
|
||||
|
||||
// We cannot rely on path.resolve() here because git.exe may come from Git Bash, Cygwin, or MSYS2, so we need to translate these paths at the boundary.
|
||||
// Also resolves symlinks so that callers using the result as a cache key
|
||||
// always get the same canonical path for a given physical directory.
|
||||
export function resolve(p: string): string {
|
||||
const resolved = pathResolve(windowsPath(p))
|
||||
try {
|
||||
return normalizePath(realpathSync(resolved))
|
||||
} catch (e) {
|
||||
if (isEnoent(e)) return normalizePath(resolved)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export function windowsPath(p: string): string {
|
||||
if (process.platform !== "win32") return p
|
||||
return (
|
||||
p
|
||||
.replace(/^\/([a-zA-Z]):(?:[\\/]|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
|
||||
// Git Bash for Windows paths are typically /<drive>/...
|
||||
.replace(/^\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
|
||||
// Cygwin git paths are typically /cygdrive/<drive>/...
|
||||
.replace(/^\/cygdrive\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
|
||||
// WSL paths are typically /mnt/<drive>/...
|
||||
.replace(/^\/mnt\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
|
||||
)
|
||||
}
|
||||
export function overlaps(a: string, b: string) {
|
||||
const relA = relative(a, b)
|
||||
const relB = relative(b, a)
|
||||
return !relA || !relA.startsWith("..") || !relB || !relB.startsWith("..")
|
||||
}
|
||||
|
||||
export function contains(parent: string, child: string) {
|
||||
return !relative(parent, child).startsWith("..")
|
||||
}
|
||||
|
||||
export async function findUp(
|
||||
target: string,
|
||||
start: string,
|
||||
stop?: string,
|
||||
options?: { rootFirst?: boolean },
|
||||
): Promise<string[]>
|
||||
export async function findUp(
|
||||
target: string[],
|
||||
start: string,
|
||||
stop?: string,
|
||||
options?: { rootFirst?: boolean },
|
||||
): Promise<string[]>
|
||||
export async function findUp(
|
||||
target: string | string[],
|
||||
start: string,
|
||||
stop?: string,
|
||||
options?: { rootFirst?: boolean },
|
||||
) {
|
||||
const dirs = [start]
|
||||
let current = start
|
||||
while (true) {
|
||||
if (stop === current) break
|
||||
const parent = dirname(current)
|
||||
if (parent === current) break
|
||||
dirs.push(parent)
|
||||
current = parent
|
||||
}
|
||||
|
||||
const targets = Array.isArray(target) ? target : [target]
|
||||
const result = []
|
||||
for (const dir of options?.rootFirst ? dirs.toReversed() : dirs) {
|
||||
for (const item of targets) {
|
||||
const search = join(dir, item)
|
||||
if (await exists(search)) result.push(search)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export async function* up(options: { targets: string[]; start: string; stop?: string }) {
|
||||
const { targets, start, stop } = options
|
||||
let current = start
|
||||
while (true) {
|
||||
for (const target of targets) {
|
||||
const search = join(current, target)
|
||||
if (await exists(search)) yield search
|
||||
}
|
||||
if (stop === current) break
|
||||
const parent = dirname(current)
|
||||
if (parent === current) break
|
||||
current = parent
|
||||
}
|
||||
}
|
||||
|
||||
export async function globUp(pattern: string, start: string, stop?: string) {
|
||||
let current = start
|
||||
const result = []
|
||||
while (true) {
|
||||
try {
|
||||
const matches = await Glob.scan(pattern, {
|
||||
cwd: current,
|
||||
absolute: true,
|
||||
include: "file",
|
||||
dot: true,
|
||||
})
|
||||
result.push(...matches)
|
||||
} catch {
|
||||
// Skip invalid glob patterns
|
||||
}
|
||||
if (stop === current) break
|
||||
const parent = dirname(current)
|
||||
if (parent === current) break
|
||||
current = parent
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1 +1,12 @@
|
||||
export * as Archive from "./archive"
|
||||
export * as Color from "./color"
|
||||
export * as Filesystem from "./filesystem"
|
||||
export * as Keybind from "./keybind"
|
||||
export * as LocalContext from "./local-context"
|
||||
export * as Locale from "./locale"
|
||||
export * as Lock from "./lock"
|
||||
export * as Log from "./log"
|
||||
export * as Process from "./process"
|
||||
export * as Rpc from "./rpc"
|
||||
export * as Token from "./token"
|
||||
export * as Wildcard from "./wildcard"
|
||||
|
||||
@@ -1,103 +1,101 @@
|
||||
import { isDeepEqual } from "remeda"
|
||||
import type { ParsedKey } from "@opentui/core"
|
||||
|
||||
export namespace Keybind {
|
||||
/**
|
||||
* Keybind info derived from OpenTUI's ParsedKey with our custom `leader` field.
|
||||
* This ensures type compatibility and catches missing fields at compile time.
|
||||
*/
|
||||
export type Info = Pick<ParsedKey, "name" | "ctrl" | "meta" | "shift" | "super"> & {
|
||||
leader: boolean // our custom field
|
||||
}
|
||||
/**
|
||||
* Keybind info derived from OpenTUI's ParsedKey with our custom `leader` field.
|
||||
* This ensures type compatibility and catches missing fields at compile time.
|
||||
*/
|
||||
export type Info = Pick<ParsedKey, "name" | "ctrl" | "meta" | "shift" | "super"> & {
|
||||
leader: boolean // our custom field
|
||||
}
|
||||
|
||||
export function match(a: Info | undefined, b: Info): boolean {
|
||||
if (!a) return false
|
||||
const normalizedA = { ...a, super: a.super ?? false }
|
||||
const normalizedB = { ...b, super: b.super ?? false }
|
||||
return isDeepEqual(normalizedA, normalizedB)
|
||||
}
|
||||
export function match(a: Info | undefined, b: Info): boolean {
|
||||
if (!a) return false
|
||||
const normalizedA = { ...a, super: a.super ?? false }
|
||||
const normalizedB = { ...b, super: b.super ?? false }
|
||||
return isDeepEqual(normalizedA, normalizedB)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert OpenTUI's ParsedKey to our Keybind.Info format.
|
||||
* This helper ensures all required fields are present and avoids manual object creation.
|
||||
*/
|
||||
export function fromParsedKey(key: ParsedKey, leader = false): Info {
|
||||
return {
|
||||
name: key.name === " " ? "space" : key.name,
|
||||
ctrl: key.ctrl,
|
||||
meta: key.meta,
|
||||
shift: key.shift,
|
||||
super: key.super ?? false,
|
||||
leader,
|
||||
}
|
||||
}
|
||||
|
||||
export function toString(info: Info | undefined): string {
|
||||
if (!info) return ""
|
||||
const parts: string[] = []
|
||||
|
||||
if (info.ctrl) parts.push("ctrl")
|
||||
if (info.meta) parts.push("alt")
|
||||
if (info.super) parts.push("super")
|
||||
if (info.shift) parts.push("shift")
|
||||
if (info.name) {
|
||||
if (info.name === "delete") parts.push("del")
|
||||
else parts.push(info.name)
|
||||
}
|
||||
|
||||
let result = parts.join("+")
|
||||
|
||||
if (info.leader) {
|
||||
result = result ? `<leader> ${result}` : `<leader>`
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function parse(key: string): Info[] {
|
||||
if (key === "none") return []
|
||||
|
||||
return key.split(",").map((combo) => {
|
||||
// Handle <leader> syntax by replacing with leader+
|
||||
const normalized = combo.replace(/<leader>/g, "leader+")
|
||||
const parts = normalized.toLowerCase().split("+")
|
||||
const info: Info = {
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
leader: false,
|
||||
name: "",
|
||||
}
|
||||
|
||||
for (const part of parts) {
|
||||
switch (part) {
|
||||
case "ctrl":
|
||||
info.ctrl = true
|
||||
break
|
||||
case "alt":
|
||||
case "meta":
|
||||
case "option":
|
||||
info.meta = true
|
||||
break
|
||||
case "super":
|
||||
info.super = true
|
||||
break
|
||||
case "shift":
|
||||
info.shift = true
|
||||
break
|
||||
case "leader":
|
||||
info.leader = true
|
||||
break
|
||||
case "esc":
|
||||
info.name = "escape"
|
||||
break
|
||||
default:
|
||||
info.name = part
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
})
|
||||
/**
|
||||
* Convert OpenTUI's ParsedKey to our Keybind.Info format.
|
||||
* This helper ensures all required fields are present and avoids manual object creation.
|
||||
*/
|
||||
export function fromParsedKey(key: ParsedKey, leader = false): Info {
|
||||
return {
|
||||
name: key.name === " " ? "space" : key.name,
|
||||
ctrl: key.ctrl,
|
||||
meta: key.meta,
|
||||
shift: key.shift,
|
||||
super: key.super ?? false,
|
||||
leader,
|
||||
}
|
||||
}
|
||||
|
||||
export function toString(info: Info | undefined): string {
|
||||
if (!info) return ""
|
||||
const parts: string[] = []
|
||||
|
||||
if (info.ctrl) parts.push("ctrl")
|
||||
if (info.meta) parts.push("alt")
|
||||
if (info.super) parts.push("super")
|
||||
if (info.shift) parts.push("shift")
|
||||
if (info.name) {
|
||||
if (info.name === "delete") parts.push("del")
|
||||
else parts.push(info.name)
|
||||
}
|
||||
|
||||
let result = parts.join("+")
|
||||
|
||||
if (info.leader) {
|
||||
result = result ? `<leader> ${result}` : `<leader>`
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function parse(key: string): Info[] {
|
||||
if (key === "none") return []
|
||||
|
||||
return key.split(",").map((combo) => {
|
||||
// Handle <leader> syntax by replacing with leader+
|
||||
const normalized = combo.replace(/<leader>/g, "leader+")
|
||||
const parts = normalized.toLowerCase().split("+")
|
||||
const info: Info = {
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
leader: false,
|
||||
name: "",
|
||||
}
|
||||
|
||||
for (const part of parts) {
|
||||
switch (part) {
|
||||
case "ctrl":
|
||||
info.ctrl = true
|
||||
break
|
||||
case "alt":
|
||||
case "meta":
|
||||
case "option":
|
||||
info.meta = true
|
||||
break
|
||||
case "super":
|
||||
info.super = true
|
||||
break
|
||||
case "shift":
|
||||
info.shift = true
|
||||
break
|
||||
case "leader":
|
||||
info.leader = true
|
||||
break
|
||||
case "esc":
|
||||
info.name = "escape"
|
||||
break
|
||||
default:
|
||||
info.name = part
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
import { AsyncLocalStorage } from "async_hooks"
|
||||
|
||||
export namespace LocalContext {
|
||||
export class NotFound extends Error {
|
||||
constructor(public override readonly name: string) {
|
||||
super(`No context found for ${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function create<T>(name: string) {
|
||||
const storage = new AsyncLocalStorage<T>()
|
||||
return {
|
||||
use() {
|
||||
const result = storage.getStore()
|
||||
if (!result) {
|
||||
throw new NotFound(name)
|
||||
}
|
||||
return result
|
||||
},
|
||||
provide<R>(value: T, fn: () => R) {
|
||||
return storage.run(value, fn)
|
||||
},
|
||||
}
|
||||
export class NotFound extends Error {
|
||||
constructor(public override readonly name: string) {
|
||||
super(`No context found for ${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function create<T>(name: string) {
|
||||
const storage = new AsyncLocalStorage<T>()
|
||||
return {
|
||||
use() {
|
||||
const result = storage.getStore()
|
||||
if (!result) {
|
||||
throw new NotFound(name)
|
||||
}
|
||||
return result
|
||||
},
|
||||
provide<R>(value: T, fn: () => R) {
|
||||
return storage.run(value, fn)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,81 +1,79 @@
|
||||
export namespace Locale {
|
||||
export function titlecase(str: string) {
|
||||
return str.replace(/\b\w/g, (c) => c.toUpperCase())
|
||||
}
|
||||
export function titlecase(str: string) {
|
||||
return str.replace(/\b\w/g, (c) => c.toUpperCase())
|
||||
}
|
||||
|
||||
export function time(input: number): string {
|
||||
const date = new Date(input)
|
||||
return date.toLocaleTimeString(undefined, { timeStyle: "short" })
|
||||
}
|
||||
export function time(input: number): string {
|
||||
const date = new Date(input)
|
||||
return date.toLocaleTimeString(undefined, { timeStyle: "short" })
|
||||
}
|
||||
|
||||
export function datetime(input: number): string {
|
||||
const date = new Date(input)
|
||||
const localTime = time(input)
|
||||
const localDate = date.toLocaleDateString()
|
||||
return `${localTime} · ${localDate}`
|
||||
}
|
||||
export function datetime(input: number): string {
|
||||
const date = new Date(input)
|
||||
const localTime = time(input)
|
||||
const localDate = date.toLocaleDateString()
|
||||
return `${localTime} · ${localDate}`
|
||||
}
|
||||
|
||||
export function todayTimeOrDateTime(input: number): string {
|
||||
const date = new Date(input)
|
||||
const now = new Date()
|
||||
const isToday =
|
||||
date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate()
|
||||
export function todayTimeOrDateTime(input: number): string {
|
||||
const date = new Date(input)
|
||||
const now = new Date()
|
||||
const isToday =
|
||||
date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate()
|
||||
|
||||
if (isToday) {
|
||||
return time(input)
|
||||
} else {
|
||||
return datetime(input)
|
||||
}
|
||||
}
|
||||
|
||||
export function number(num: number): string {
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + "M"
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + "K"
|
||||
}
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
export function duration(input: number) {
|
||||
if (input < 1000) {
|
||||
return `${input}ms`
|
||||
}
|
||||
if (input < 60000) {
|
||||
return `${(input / 1000).toFixed(1)}s`
|
||||
}
|
||||
if (input < 3600000) {
|
||||
const minutes = Math.floor(input / 60000)
|
||||
const seconds = Math.floor((input % 60000) / 1000)
|
||||
return `${minutes}m ${seconds}s`
|
||||
}
|
||||
if (input < 86400000) {
|
||||
const hours = Math.floor(input / 3600000)
|
||||
const minutes = Math.floor((input % 3600000) / 60000)
|
||||
return `${hours}h ${minutes}m`
|
||||
}
|
||||
const hours = Math.floor(input / 3600000)
|
||||
const days = Math.floor((input % 3600000) / 86400000)
|
||||
return `${days}d ${hours}h`
|
||||
}
|
||||
|
||||
export function truncate(str: string, len: number): string {
|
||||
if (str.length <= len) return str
|
||||
return str.slice(0, len - 1) + "…"
|
||||
}
|
||||
|
||||
export function truncateMiddle(str: string, maxLength: number = 35): string {
|
||||
if (str.length <= maxLength) return str
|
||||
|
||||
const ellipsis = "…"
|
||||
const keepStart = Math.ceil((maxLength - ellipsis.length) / 2)
|
||||
const keepEnd = Math.floor((maxLength - ellipsis.length) / 2)
|
||||
|
||||
return str.slice(0, keepStart) + ellipsis + str.slice(-keepEnd)
|
||||
}
|
||||
|
||||
export function pluralize(count: number, singular: string, plural: string): string {
|
||||
const template = count === 1 ? singular : plural
|
||||
return template.replace("{}", count.toString())
|
||||
if (isToday) {
|
||||
return time(input)
|
||||
} else {
|
||||
return datetime(input)
|
||||
}
|
||||
}
|
||||
|
||||
export function number(num: number): string {
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + "M"
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + "K"
|
||||
}
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
export function duration(input: number) {
|
||||
if (input < 1000) {
|
||||
return `${input}ms`
|
||||
}
|
||||
if (input < 60000) {
|
||||
return `${(input / 1000).toFixed(1)}s`
|
||||
}
|
||||
if (input < 3600000) {
|
||||
const minutes = Math.floor(input / 60000)
|
||||
const seconds = Math.floor((input % 60000) / 1000)
|
||||
return `${minutes}m ${seconds}s`
|
||||
}
|
||||
if (input < 86400000) {
|
||||
const hours = Math.floor(input / 3600000)
|
||||
const minutes = Math.floor((input % 3600000) / 60000)
|
||||
return `${hours}h ${minutes}m`
|
||||
}
|
||||
const hours = Math.floor(input / 3600000)
|
||||
const days = Math.floor((input % 3600000) / 86400000)
|
||||
return `${days}d ${hours}h`
|
||||
}
|
||||
|
||||
export function truncate(str: string, len: number): string {
|
||||
if (str.length <= len) return str
|
||||
return str.slice(0, len - 1) + "…"
|
||||
}
|
||||
|
||||
export function truncateMiddle(str: string, maxLength: number = 35): string {
|
||||
if (str.length <= maxLength) return str
|
||||
|
||||
const ellipsis = "…"
|
||||
const keepStart = Math.ceil((maxLength - ellipsis.length) / 2)
|
||||
const keepEnd = Math.floor((maxLength - ellipsis.length) / 2)
|
||||
|
||||
return str.slice(0, keepStart) + ellipsis + str.slice(-keepEnd)
|
||||
}
|
||||
|
||||
export function pluralize(count: number, singular: string, plural: string): string {
|
||||
const template = count === 1 ? singular : plural
|
||||
return template.replace("{}", count.toString())
|
||||
}
|
||||
|
||||
@@ -1,54 +1,62 @@
|
||||
export namespace Lock {
|
||||
const locks = new Map<
|
||||
string,
|
||||
{
|
||||
readers: number
|
||||
writer: boolean
|
||||
waitingReaders: (() => void)[]
|
||||
waitingWriters: (() => void)[]
|
||||
}
|
||||
>()
|
||||
const locks = new Map<
|
||||
string,
|
||||
{
|
||||
readers: number
|
||||
writer: boolean
|
||||
waitingReaders: (() => void)[]
|
||||
waitingWriters: (() => void)[]
|
||||
}
|
||||
>()
|
||||
|
||||
function get(key: string) {
|
||||
if (!locks.has(key)) {
|
||||
locks.set(key, {
|
||||
readers: 0,
|
||||
writer: false,
|
||||
waitingReaders: [],
|
||||
waitingWriters: [],
|
||||
function get(key: string) {
|
||||
if (!locks.has(key)) {
|
||||
locks.set(key, {
|
||||
readers: 0,
|
||||
writer: false,
|
||||
waitingReaders: [],
|
||||
waitingWriters: [],
|
||||
})
|
||||
}
|
||||
return locks.get(key)!
|
||||
}
|
||||
|
||||
function process(key: string) {
|
||||
const lock = locks.get(key)
|
||||
if (!lock || lock.writer || lock.readers > 0) return
|
||||
|
||||
// Prioritize writers to prevent starvation
|
||||
if (lock.waitingWriters.length > 0) {
|
||||
const nextWriter = lock.waitingWriters.shift()!
|
||||
nextWriter()
|
||||
return
|
||||
}
|
||||
|
||||
// Wake up all waiting readers
|
||||
while (lock.waitingReaders.length > 0) {
|
||||
const nextReader = lock.waitingReaders.shift()!
|
||||
nextReader()
|
||||
}
|
||||
|
||||
// Clean up empty locks
|
||||
if (lock.readers === 0 && !lock.writer && lock.waitingReaders.length === 0 && lock.waitingWriters.length === 0) {
|
||||
locks.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
export async function read(key: string): Promise<Disposable> {
|
||||
const lock = get(key)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
if (!lock.writer && lock.waitingWriters.length === 0) {
|
||||
lock.readers++
|
||||
resolve({
|
||||
[Symbol.dispose]: () => {
|
||||
lock.readers--
|
||||
process(key)
|
||||
},
|
||||
})
|
||||
}
|
||||
return locks.get(key)!
|
||||
}
|
||||
|
||||
function process(key: string) {
|
||||
const lock = locks.get(key)
|
||||
if (!lock || lock.writer || lock.readers > 0) return
|
||||
|
||||
// Prioritize writers to prevent starvation
|
||||
if (lock.waitingWriters.length > 0) {
|
||||
const nextWriter = lock.waitingWriters.shift()!
|
||||
nextWriter()
|
||||
return
|
||||
}
|
||||
|
||||
// Wake up all waiting readers
|
||||
while (lock.waitingReaders.length > 0) {
|
||||
const nextReader = lock.waitingReaders.shift()!
|
||||
nextReader()
|
||||
}
|
||||
|
||||
// Clean up empty locks
|
||||
if (lock.readers === 0 && !lock.writer && lock.waitingReaders.length === 0 && lock.waitingWriters.length === 0) {
|
||||
locks.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
export async function read(key: string): Promise<Disposable> {
|
||||
const lock = get(key)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
if (!lock.writer && lock.waitingWriters.length === 0) {
|
||||
} else {
|
||||
lock.waitingReaders.push(() => {
|
||||
lock.readers++
|
||||
resolve({
|
||||
[Symbol.dispose]: () => {
|
||||
@@ -56,25 +64,25 @@ export namespace Lock {
|
||||
process(key)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
lock.waitingReaders.push(() => {
|
||||
lock.readers++
|
||||
resolve({
|
||||
[Symbol.dispose]: () => {
|
||||
lock.readers--
|
||||
process(key)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function write(key: string): Promise<Disposable> {
|
||||
const lock = get(key)
|
||||
export async function write(key: string): Promise<Disposable> {
|
||||
const lock = get(key)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
if (!lock.writer && lock.readers === 0) {
|
||||
return new Promise((resolve) => {
|
||||
if (!lock.writer && lock.readers === 0) {
|
||||
lock.writer = true
|
||||
resolve({
|
||||
[Symbol.dispose]: () => {
|
||||
lock.writer = false
|
||||
process(key)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
lock.waitingWriters.push(() => {
|
||||
lock.writer = true
|
||||
resolve({
|
||||
[Symbol.dispose]: () => {
|
||||
@@ -82,17 +90,7 @@ export namespace Lock {
|
||||
process(key)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
lock.waitingWriters.push(() => {
|
||||
lock.writer = true
|
||||
resolve({
|
||||
[Symbol.dispose]: () => {
|
||||
lock.writer = false
|
||||
process(key)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,183 +5,181 @@ import { Global } from "../global"
|
||||
import z from "zod"
|
||||
import { Glob } from "@opencode-ai/shared/util/glob"
|
||||
|
||||
export namespace Log {
|
||||
export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" })
|
||||
export type Level = z.infer<typeof Level>
|
||||
export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" })
|
||||
export type Level = z.infer<typeof Level>
|
||||
|
||||
const levelPriority: Record<Level, number> = {
|
||||
DEBUG: 0,
|
||||
INFO: 1,
|
||||
WARN: 2,
|
||||
ERROR: 3,
|
||||
}
|
||||
const keep = 10
|
||||
const levelPriority: Record<Level, number> = {
|
||||
DEBUG: 0,
|
||||
INFO: 1,
|
||||
WARN: 2,
|
||||
ERROR: 3,
|
||||
}
|
||||
const keep = 10
|
||||
|
||||
let level: Level = "INFO"
|
||||
let level: Level = "INFO"
|
||||
|
||||
function shouldLog(input: Level): boolean {
|
||||
return levelPriority[input] >= levelPriority[level]
|
||||
}
|
||||
function shouldLog(input: Level): boolean {
|
||||
return levelPriority[input] >= levelPriority[level]
|
||||
}
|
||||
|
||||
export type Logger = {
|
||||
debug(message?: any, extra?: Record<string, any>): void
|
||||
info(message?: any, extra?: Record<string, any>): void
|
||||
error(message?: any, extra?: Record<string, any>): void
|
||||
warn(message?: any, extra?: Record<string, any>): void
|
||||
tag(key: string, value: string): Logger
|
||||
clone(): Logger
|
||||
time(
|
||||
message: string,
|
||||
extra?: Record<string, any>,
|
||||
): {
|
||||
stop(): void
|
||||
[Symbol.dispose](): void
|
||||
}
|
||||
}
|
||||
|
||||
const loggers = new Map<string, Logger>()
|
||||
|
||||
export const Default = create({ service: "default" })
|
||||
|
||||
export interface Options {
|
||||
print: boolean
|
||||
dev?: boolean
|
||||
level?: Level
|
||||
}
|
||||
|
||||
let logpath = ""
|
||||
export function file() {
|
||||
return logpath
|
||||
}
|
||||
let write = (msg: any) => {
|
||||
process.stderr.write(msg)
|
||||
return msg.length
|
||||
}
|
||||
|
||||
export async function init(options: Options) {
|
||||
if (options.level) level = options.level
|
||||
cleanup(Global.Path.log)
|
||||
if (options.print) return
|
||||
logpath = path.join(
|
||||
Global.Path.log,
|
||||
options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
|
||||
)
|
||||
await fs.truncate(logpath).catch(() => {})
|
||||
const stream = createWriteStream(logpath, { flags: "a" })
|
||||
write = async (msg: any) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.write(msg, (err) => {
|
||||
if (err) reject(err)
|
||||
else resolve(msg.length)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanup(dir: string) {
|
||||
const files = (
|
||||
await Glob.scan("????-??-??T??????.log", {
|
||||
cwd: dir,
|
||||
absolute: false,
|
||||
include: "file",
|
||||
}).catch(() => [])
|
||||
)
|
||||
.filter((file) => path.basename(file) === file)
|
||||
.sort()
|
||||
if (files.length <= keep) return
|
||||
|
||||
const doomed = files.slice(0, -keep)
|
||||
await Promise.all(doomed.map((file) => fs.unlink(path.join(dir, file)).catch(() => {})))
|
||||
}
|
||||
|
||||
function formatError(error: Error, depth = 0): string {
|
||||
const result = error.message
|
||||
return error.cause instanceof Error && depth < 10
|
||||
? result + " Caused by: " + formatError(error.cause, depth + 1)
|
||||
: result
|
||||
}
|
||||
|
||||
let last = Date.now()
|
||||
export function create(tags?: Record<string, any>) {
|
||||
tags = tags || {}
|
||||
|
||||
const service = tags["service"]
|
||||
if (service && typeof service === "string") {
|
||||
const cached = loggers.get(service)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
|
||||
function build(message: any, extra?: Record<string, any>) {
|
||||
const prefix = Object.entries({
|
||||
...tags,
|
||||
...extra,
|
||||
})
|
||||
.filter(([_, value]) => value !== undefined && value !== null)
|
||||
.map(([key, value]) => {
|
||||
const prefix = `${key}=`
|
||||
if (value instanceof Error) return prefix + formatError(value)
|
||||
if (typeof value === "object") return prefix + JSON.stringify(value)
|
||||
return prefix + value
|
||||
})
|
||||
.join(" ")
|
||||
const next = new Date()
|
||||
const diff = next.getTime() - last
|
||||
last = next.getTime()
|
||||
return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n"
|
||||
}
|
||||
const result: Logger = {
|
||||
debug(message?: any, extra?: Record<string, any>) {
|
||||
if (shouldLog("DEBUG")) {
|
||||
write("DEBUG " + build(message, extra))
|
||||
}
|
||||
},
|
||||
info(message?: any, extra?: Record<string, any>) {
|
||||
if (shouldLog("INFO")) {
|
||||
write("INFO " + build(message, extra))
|
||||
}
|
||||
},
|
||||
error(message?: any, extra?: Record<string, any>) {
|
||||
if (shouldLog("ERROR")) {
|
||||
write("ERROR " + build(message, extra))
|
||||
}
|
||||
},
|
||||
warn(message?: any, extra?: Record<string, any>) {
|
||||
if (shouldLog("WARN")) {
|
||||
write("WARN " + build(message, extra))
|
||||
}
|
||||
},
|
||||
tag(key: string, value: string) {
|
||||
if (tags) tags[key] = value
|
||||
return result
|
||||
},
|
||||
clone() {
|
||||
return Log.create({ ...tags })
|
||||
},
|
||||
time(message: string, extra?: Record<string, any>) {
|
||||
const now = Date.now()
|
||||
result.info(message, { status: "started", ...extra })
|
||||
function stop() {
|
||||
result.info(message, {
|
||||
status: "completed",
|
||||
duration: Date.now() - now,
|
||||
...extra,
|
||||
})
|
||||
}
|
||||
return {
|
||||
stop,
|
||||
[Symbol.dispose]() {
|
||||
stop()
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
if (service && typeof service === "string") {
|
||||
loggers.set(service, result)
|
||||
}
|
||||
|
||||
return result
|
||||
export type Logger = {
|
||||
debug(message?: any, extra?: Record<string, any>): void
|
||||
info(message?: any, extra?: Record<string, any>): void
|
||||
error(message?: any, extra?: Record<string, any>): void
|
||||
warn(message?: any, extra?: Record<string, any>): void
|
||||
tag(key: string, value: string): Logger
|
||||
clone(): Logger
|
||||
time(
|
||||
message: string,
|
||||
extra?: Record<string, any>,
|
||||
): {
|
||||
stop(): void
|
||||
[Symbol.dispose](): void
|
||||
}
|
||||
}
|
||||
|
||||
const loggers = new Map<string, Logger>()
|
||||
|
||||
export const Default = create({ service: "default" })
|
||||
|
||||
export interface Options {
|
||||
print: boolean
|
||||
dev?: boolean
|
||||
level?: Level
|
||||
}
|
||||
|
||||
let logpath = ""
|
||||
export function file() {
|
||||
return logpath
|
||||
}
|
||||
let write = (msg: any) => {
|
||||
process.stderr.write(msg)
|
||||
return msg.length
|
||||
}
|
||||
|
||||
export async function init(options: Options) {
|
||||
if (options.level) level = options.level
|
||||
cleanup(Global.Path.log)
|
||||
if (options.print) return
|
||||
logpath = path.join(
|
||||
Global.Path.log,
|
||||
options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
|
||||
)
|
||||
await fs.truncate(logpath).catch(() => {})
|
||||
const stream = createWriteStream(logpath, { flags: "a" })
|
||||
write = async (msg: any) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.write(msg, (err) => {
|
||||
if (err) reject(err)
|
||||
else resolve(msg.length)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanup(dir: string) {
|
||||
const files = (
|
||||
await Glob.scan("????-??-??T??????.log", {
|
||||
cwd: dir,
|
||||
absolute: false,
|
||||
include: "file",
|
||||
}).catch(() => [])
|
||||
)
|
||||
.filter((file) => path.basename(file) === file)
|
||||
.sort()
|
||||
if (files.length <= keep) return
|
||||
|
||||
const doomed = files.slice(0, -keep)
|
||||
await Promise.all(doomed.map((file) => fs.unlink(path.join(dir, file)).catch(() => {})))
|
||||
}
|
||||
|
||||
function formatError(error: Error, depth = 0): string {
|
||||
const result = error.message
|
||||
return error.cause instanceof Error && depth < 10
|
||||
? result + " Caused by: " + formatError(error.cause, depth + 1)
|
||||
: result
|
||||
}
|
||||
|
||||
let last = Date.now()
|
||||
export function create(tags?: Record<string, any>) {
|
||||
tags = tags || {}
|
||||
|
||||
const service = tags["service"]
|
||||
if (service && typeof service === "string") {
|
||||
const cached = loggers.get(service)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
|
||||
function build(message: any, extra?: Record<string, any>) {
|
||||
const prefix = Object.entries({
|
||||
...tags,
|
||||
...extra,
|
||||
})
|
||||
.filter(([_, value]) => value !== undefined && value !== null)
|
||||
.map(([key, value]) => {
|
||||
const prefix = `${key}=`
|
||||
if (value instanceof Error) return prefix + formatError(value)
|
||||
if (typeof value === "object") return prefix + JSON.stringify(value)
|
||||
return prefix + value
|
||||
})
|
||||
.join(" ")
|
||||
const next = new Date()
|
||||
const diff = next.getTime() - last
|
||||
last = next.getTime()
|
||||
return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n"
|
||||
}
|
||||
const result: Logger = {
|
||||
debug(message?: any, extra?: Record<string, any>) {
|
||||
if (shouldLog("DEBUG")) {
|
||||
write("DEBUG " + build(message, extra))
|
||||
}
|
||||
},
|
||||
info(message?: any, extra?: Record<string, any>) {
|
||||
if (shouldLog("INFO")) {
|
||||
write("INFO " + build(message, extra))
|
||||
}
|
||||
},
|
||||
error(message?: any, extra?: Record<string, any>) {
|
||||
if (shouldLog("ERROR")) {
|
||||
write("ERROR " + build(message, extra))
|
||||
}
|
||||
},
|
||||
warn(message?: any, extra?: Record<string, any>) {
|
||||
if (shouldLog("WARN")) {
|
||||
write("WARN " + build(message, extra))
|
||||
}
|
||||
},
|
||||
tag(key: string, value: string) {
|
||||
if (tags) tags[key] = value
|
||||
return result
|
||||
},
|
||||
clone() {
|
||||
return create({ ...tags })
|
||||
},
|
||||
time(message: string, extra?: Record<string, any>) {
|
||||
const now = Date.now()
|
||||
result.info(message, { status: "started", ...extra })
|
||||
function stop() {
|
||||
result.info(message, {
|
||||
status: "completed",
|
||||
duration: Date.now() - now,
|
||||
...extra,
|
||||
})
|
||||
}
|
||||
return {
|
||||
stop,
|
||||
[Symbol.dispose]() {
|
||||
stop()
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
if (service && typeof service === "string") {
|
||||
loggers.set(service, result)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -3,174 +3,172 @@ import launch from "cross-spawn"
|
||||
import { buffer } from "node:stream/consumers"
|
||||
import { errorMessage } from "./error"
|
||||
|
||||
export namespace Process {
|
||||
export type Stdio = "inherit" | "pipe" | "ignore"
|
||||
export type Shell = boolean | string
|
||||
export type Stdio = "inherit" | "pipe" | "ignore"
|
||||
export type Shell = boolean | string
|
||||
|
||||
export interface Options {
|
||||
cwd?: string
|
||||
env?: NodeJS.ProcessEnv | null
|
||||
stdin?: Stdio
|
||||
stdout?: Stdio
|
||||
stderr?: Stdio
|
||||
shell?: Shell
|
||||
abort?: AbortSignal
|
||||
kill?: NodeJS.Signals | number
|
||||
timeout?: number
|
||||
}
|
||||
export interface Options {
|
||||
cwd?: string
|
||||
env?: NodeJS.ProcessEnv | null
|
||||
stdin?: Stdio
|
||||
stdout?: Stdio
|
||||
stderr?: Stdio
|
||||
shell?: Shell
|
||||
abort?: AbortSignal
|
||||
kill?: NodeJS.Signals | number
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
export interface RunOptions extends Omit<Options, "stdout" | "stderr"> {
|
||||
nothrow?: boolean
|
||||
}
|
||||
export interface RunOptions extends Omit<Options, "stdout" | "stderr"> {
|
||||
nothrow?: boolean
|
||||
}
|
||||
|
||||
export interface Result {
|
||||
code: number
|
||||
stdout: Buffer
|
||||
stderr: Buffer
|
||||
}
|
||||
export interface Result {
|
||||
code: number
|
||||
stdout: Buffer
|
||||
stderr: Buffer
|
||||
}
|
||||
|
||||
export interface TextResult extends Result {
|
||||
text: string
|
||||
}
|
||||
export interface TextResult extends Result {
|
||||
text: string
|
||||
}
|
||||
|
||||
export class RunFailedError extends Error {
|
||||
readonly cmd: string[]
|
||||
readonly code: number
|
||||
readonly stdout: Buffer
|
||||
readonly stderr: Buffer
|
||||
export class RunFailedError extends Error {
|
||||
readonly cmd: string[]
|
||||
readonly code: number
|
||||
readonly stdout: Buffer
|
||||
readonly stderr: Buffer
|
||||
|
||||
constructor(cmd: string[], code: number, stdout: Buffer, stderr: Buffer) {
|
||||
const text = stderr.toString().trim()
|
||||
super(
|
||||
text
|
||||
? `Command failed with code ${code}: ${cmd.join(" ")}\n${text}`
|
||||
: `Command failed with code ${code}: ${cmd.join(" ")}`,
|
||||
)
|
||||
this.name = "ProcessRunFailedError"
|
||||
this.cmd = [...cmd]
|
||||
this.code = code
|
||||
this.stdout = stdout
|
||||
this.stderr = stderr
|
||||
}
|
||||
}
|
||||
|
||||
export type Child = ChildProcess & { exited: Promise<number> }
|
||||
|
||||
export function spawn(cmd: string[], opts: Options = {}): Child {
|
||||
if (cmd.length === 0) throw new Error("Command is required")
|
||||
opts.abort?.throwIfAborted()
|
||||
|
||||
const proc = launch(cmd[0], cmd.slice(1), {
|
||||
cwd: opts.cwd,
|
||||
shell: opts.shell,
|
||||
env: opts.env === null ? {} : opts.env ? { ...process.env, ...opts.env } : undefined,
|
||||
stdio: [opts.stdin ?? "ignore", opts.stdout ?? "ignore", opts.stderr ?? "ignore"],
|
||||
windowsHide: process.platform === "win32",
|
||||
})
|
||||
|
||||
let closed = false
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
const abort = () => {
|
||||
if (closed) return
|
||||
if (proc.exitCode !== null || proc.signalCode !== null) return
|
||||
closed = true
|
||||
|
||||
proc.kill(opts.kill ?? "SIGTERM")
|
||||
|
||||
const ms = opts.timeout ?? 5_000
|
||||
if (ms <= 0) return
|
||||
timer = setTimeout(() => proc.kill("SIGKILL"), ms)
|
||||
}
|
||||
|
||||
const exited = new Promise<number>((resolve, reject) => {
|
||||
const done = () => {
|
||||
opts.abort?.removeEventListener("abort", abort)
|
||||
if (timer) clearTimeout(timer)
|
||||
}
|
||||
|
||||
proc.once("exit", (code, signal) => {
|
||||
done()
|
||||
resolve(code ?? (signal ? 1 : 0))
|
||||
})
|
||||
|
||||
proc.once("error", (error) => {
|
||||
done()
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
void exited.catch(() => undefined)
|
||||
|
||||
if (opts.abort) {
|
||||
opts.abort.addEventListener("abort", abort, { once: true })
|
||||
if (opts.abort.aborted) abort()
|
||||
}
|
||||
|
||||
const child = proc as Child
|
||||
child.exited = exited
|
||||
return child
|
||||
}
|
||||
|
||||
export async function run(cmd: string[], opts: RunOptions = {}): Promise<Result> {
|
||||
const proc = spawn(cmd, {
|
||||
cwd: opts.cwd,
|
||||
env: opts.env,
|
||||
stdin: opts.stdin,
|
||||
shell: opts.shell,
|
||||
abort: opts.abort,
|
||||
kill: opts.kill,
|
||||
timeout: opts.timeout,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
|
||||
if (!proc.stdout || !proc.stderr) throw new Error("Process output not available")
|
||||
|
||||
const out = await Promise.all([proc.exited, buffer(proc.stdout), buffer(proc.stderr)])
|
||||
.then(([code, stdout, stderr]) => ({
|
||||
code,
|
||||
stdout,
|
||||
stderr,
|
||||
}))
|
||||
.catch((err: unknown) => {
|
||||
if (!opts.nothrow) throw err
|
||||
return {
|
||||
code: 1,
|
||||
stdout: Buffer.alloc(0),
|
||||
stderr: Buffer.from(errorMessage(err)),
|
||||
}
|
||||
})
|
||||
if (out.code === 0 || opts.nothrow) return out
|
||||
throw new RunFailedError(cmd, out.code, out.stdout, out.stderr)
|
||||
}
|
||||
|
||||
// Duplicated in `packages/sdk/js/src/process.ts` because the SDK cannot import
|
||||
// `opencode` without creating a cycle. Keep both copies in sync.
|
||||
export async function stop(proc: ChildProcess) {
|
||||
if (proc.exitCode !== null || proc.signalCode !== null) return
|
||||
|
||||
if (process.platform !== "win32" || !proc.pid) {
|
||||
proc.kill()
|
||||
return
|
||||
}
|
||||
|
||||
const out = await run(["taskkill", "/pid", String(proc.pid), "/T", "/F"], {
|
||||
nothrow: true,
|
||||
})
|
||||
|
||||
if (out.code === 0) return
|
||||
proc.kill()
|
||||
}
|
||||
|
||||
export async function text(cmd: string[], opts: RunOptions = {}): Promise<TextResult> {
|
||||
const out = await run(cmd, opts)
|
||||
return {
|
||||
...out,
|
||||
text: out.stdout.toString(),
|
||||
}
|
||||
}
|
||||
|
||||
export async function lines(cmd: string[], opts: RunOptions = {}): Promise<string[]> {
|
||||
return (await text(cmd, opts)).text.split(/\r?\n/).filter(Boolean)
|
||||
constructor(cmd: string[], code: number, stdout: Buffer, stderr: Buffer) {
|
||||
const text = stderr.toString().trim()
|
||||
super(
|
||||
text
|
||||
? `Command failed with code ${code}: ${cmd.join(" ")}\n${text}`
|
||||
: `Command failed with code ${code}: ${cmd.join(" ")}`,
|
||||
)
|
||||
this.name = "ProcessRunFailedError"
|
||||
this.cmd = [...cmd]
|
||||
this.code = code
|
||||
this.stdout = stdout
|
||||
this.stderr = stderr
|
||||
}
|
||||
}
|
||||
|
||||
export type Child = ChildProcess & { exited: Promise<number> }
|
||||
|
||||
export function spawn(cmd: string[], opts: Options = {}): Child {
|
||||
if (cmd.length === 0) throw new Error("Command is required")
|
||||
opts.abort?.throwIfAborted()
|
||||
|
||||
const proc = launch(cmd[0], cmd.slice(1), {
|
||||
cwd: opts.cwd,
|
||||
shell: opts.shell,
|
||||
env: opts.env === null ? {} : opts.env ? { ...process.env, ...opts.env } : undefined,
|
||||
stdio: [opts.stdin ?? "ignore", opts.stdout ?? "ignore", opts.stderr ?? "ignore"],
|
||||
windowsHide: process.platform === "win32",
|
||||
})
|
||||
|
||||
let closed = false
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
const abort = () => {
|
||||
if (closed) return
|
||||
if (proc.exitCode !== null || proc.signalCode !== null) return
|
||||
closed = true
|
||||
|
||||
proc.kill(opts.kill ?? "SIGTERM")
|
||||
|
||||
const ms = opts.timeout ?? 5_000
|
||||
if (ms <= 0) return
|
||||
timer = setTimeout(() => proc.kill("SIGKILL"), ms)
|
||||
}
|
||||
|
||||
const exited = new Promise<number>((resolve, reject) => {
|
||||
const done = () => {
|
||||
opts.abort?.removeEventListener("abort", abort)
|
||||
if (timer) clearTimeout(timer)
|
||||
}
|
||||
|
||||
proc.once("exit", (code, signal) => {
|
||||
done()
|
||||
resolve(code ?? (signal ? 1 : 0))
|
||||
})
|
||||
|
||||
proc.once("error", (error) => {
|
||||
done()
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
void exited.catch(() => undefined)
|
||||
|
||||
if (opts.abort) {
|
||||
opts.abort.addEventListener("abort", abort, { once: true })
|
||||
if (opts.abort.aborted) abort()
|
||||
}
|
||||
|
||||
const child = proc as Child
|
||||
child.exited = exited
|
||||
return child
|
||||
}
|
||||
|
||||
export async function run(cmd: string[], opts: RunOptions = {}): Promise<Result> {
|
||||
const proc = spawn(cmd, {
|
||||
cwd: opts.cwd,
|
||||
env: opts.env,
|
||||
stdin: opts.stdin,
|
||||
shell: opts.shell,
|
||||
abort: opts.abort,
|
||||
kill: opts.kill,
|
||||
timeout: opts.timeout,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
|
||||
if (!proc.stdout || !proc.stderr) throw new Error("Process output not available")
|
||||
|
||||
const out = await Promise.all([proc.exited, buffer(proc.stdout), buffer(proc.stderr)])
|
||||
.then(([code, stdout, stderr]) => ({
|
||||
code,
|
||||
stdout,
|
||||
stderr,
|
||||
}))
|
||||
.catch((err: unknown) => {
|
||||
if (!opts.nothrow) throw err
|
||||
return {
|
||||
code: 1,
|
||||
stdout: Buffer.alloc(0),
|
||||
stderr: Buffer.from(errorMessage(err)),
|
||||
}
|
||||
})
|
||||
if (out.code === 0 || opts.nothrow) return out
|
||||
throw new RunFailedError(cmd, out.code, out.stdout, out.stderr)
|
||||
}
|
||||
|
||||
// Duplicated in `packages/sdk/js/src/process.ts` because the SDK cannot import
|
||||
// `opencode` without creating a cycle. Keep both copies in sync.
|
||||
export async function stop(proc: ChildProcess) {
|
||||
if (proc.exitCode !== null || proc.signalCode !== null) return
|
||||
|
||||
if (process.platform !== "win32" || !proc.pid) {
|
||||
proc.kill()
|
||||
return
|
||||
}
|
||||
|
||||
const out = await run(["taskkill", "/pid", String(proc.pid), "/T", "/F"], {
|
||||
nothrow: true,
|
||||
})
|
||||
|
||||
if (out.code === 0) return
|
||||
proc.kill()
|
||||
}
|
||||
|
||||
export async function text(cmd: string[], opts: RunOptions = {}): Promise<TextResult> {
|
||||
const out = await run(cmd, opts)
|
||||
return {
|
||||
...out,
|
||||
text: out.stdout.toString(),
|
||||
}
|
||||
}
|
||||
|
||||
export async function lines(cmd: string[], opts: RunOptions = {}): Promise<string[]> {
|
||||
return (await text(cmd, opts)).text.split(/\r?\n/).filter(Boolean)
|
||||
}
|
||||
|
||||
@@ -1,66 +1,64 @@
|
||||
export namespace Rpc {
|
||||
type Definition = {
|
||||
[method: string]: (input: any) => any
|
||||
}
|
||||
type Definition = {
|
||||
[method: string]: (input: any) => any
|
||||
}
|
||||
|
||||
export function listen(rpc: Definition) {
|
||||
onmessage = async (evt) => {
|
||||
const parsed = JSON.parse(evt.data)
|
||||
if (parsed.type === "rpc.request") {
|
||||
const result = await rpc[parsed.method](parsed.input)
|
||||
postMessage(JSON.stringify({ type: "rpc.result", result, id: parsed.id }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function emit(event: string, data: unknown) {
|
||||
postMessage(JSON.stringify({ type: "rpc.event", event, data }))
|
||||
}
|
||||
|
||||
export function client<T extends Definition>(target: {
|
||||
postMessage: (data: string) => void | null
|
||||
onmessage: ((this: Worker, ev: MessageEvent<any>) => any) | null
|
||||
}) {
|
||||
const pending = new Map<number, (result: any) => void>()
|
||||
const listeners = new Map<string, Set<(data: any) => void>>()
|
||||
let id = 0
|
||||
target.onmessage = async (evt) => {
|
||||
const parsed = JSON.parse(evt.data)
|
||||
if (parsed.type === "rpc.result") {
|
||||
const resolve = pending.get(parsed.id)
|
||||
if (resolve) {
|
||||
resolve(parsed.result)
|
||||
pending.delete(parsed.id)
|
||||
}
|
||||
}
|
||||
if (parsed.type === "rpc.event") {
|
||||
const handlers = listeners.get(parsed.event)
|
||||
if (handlers) {
|
||||
for (const handler of handlers) {
|
||||
handler(parsed.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
call<Method extends keyof T>(method: Method, input: Parameters<T[Method]>[0]): Promise<ReturnType<T[Method]>> {
|
||||
const requestId = id++
|
||||
return new Promise((resolve) => {
|
||||
pending.set(requestId, resolve)
|
||||
target.postMessage(JSON.stringify({ type: "rpc.request", method, input, id: requestId }))
|
||||
})
|
||||
},
|
||||
on<Data>(event: string, handler: (data: Data) => void) {
|
||||
let handlers = listeners.get(event)
|
||||
if (!handlers) {
|
||||
handlers = new Set()
|
||||
listeners.set(event, handlers)
|
||||
}
|
||||
handlers.add(handler)
|
||||
return () => {
|
||||
handlers!.delete(handler)
|
||||
}
|
||||
},
|
||||
export function listen(rpc: Definition) {
|
||||
onmessage = async (evt) => {
|
||||
const parsed = JSON.parse(evt.data)
|
||||
if (parsed.type === "rpc.request") {
|
||||
const result = await rpc[parsed.method](parsed.input)
|
||||
postMessage(JSON.stringify({ type: "rpc.result", result, id: parsed.id }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function emit(event: string, data: unknown) {
|
||||
postMessage(JSON.stringify({ type: "rpc.event", event, data }))
|
||||
}
|
||||
|
||||
export function client<T extends Definition>(target: {
|
||||
postMessage: (data: string) => void | null
|
||||
onmessage: ((this: Worker, ev: MessageEvent<any>) => any) | null
|
||||
}) {
|
||||
const pending = new Map<number, (result: any) => void>()
|
||||
const listeners = new Map<string, Set<(data: any) => void>>()
|
||||
let id = 0
|
||||
target.onmessage = async (evt) => {
|
||||
const parsed = JSON.parse(evt.data)
|
||||
if (parsed.type === "rpc.result") {
|
||||
const resolve = pending.get(parsed.id)
|
||||
if (resolve) {
|
||||
resolve(parsed.result)
|
||||
pending.delete(parsed.id)
|
||||
}
|
||||
}
|
||||
if (parsed.type === "rpc.event") {
|
||||
const handlers = listeners.get(parsed.event)
|
||||
if (handlers) {
|
||||
for (const handler of handlers) {
|
||||
handler(parsed.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
call<Method extends keyof T>(method: Method, input: Parameters<T[Method]>[0]): Promise<ReturnType<T[Method]>> {
|
||||
const requestId = id++
|
||||
return new Promise((resolve) => {
|
||||
pending.set(requestId, resolve)
|
||||
target.postMessage(JSON.stringify({ type: "rpc.request", method, input, id: requestId }))
|
||||
})
|
||||
},
|
||||
on<Data>(event: string, handler: (data: Data) => void) {
|
||||
let handlers = listeners.get(event)
|
||||
if (!handlers) {
|
||||
handlers = new Set()
|
||||
listeners.set(event, handlers)
|
||||
}
|
||||
handlers.add(handler)
|
||||
return () => {
|
||||
handlers!.delete(handler)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
export namespace Token {
|
||||
const CHARS_PER_TOKEN = 4
|
||||
const CHARS_PER_TOKEN = 4
|
||||
|
||||
export function estimate(input: string) {
|
||||
return Math.max(0, Math.round((input || "").length / CHARS_PER_TOKEN))
|
||||
}
|
||||
export function estimate(input: string) {
|
||||
return Math.max(0, Math.round((input || "").length / CHARS_PER_TOKEN))
|
||||
}
|
||||
|
||||
@@ -1,59 +1,57 @@
|
||||
import { sortBy, pipe } from "remeda"
|
||||
|
||||
export namespace Wildcard {
|
||||
export function match(str: string, pattern: string) {
|
||||
if (str) str = str.replaceAll("\\", "/")
|
||||
if (pattern) pattern = pattern.replaceAll("\\", "/")
|
||||
let escaped = pattern
|
||||
.replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape special regex chars
|
||||
.replace(/\*/g, ".*") // * becomes .*
|
||||
.replace(/\?/g, ".") // ? becomes .
|
||||
export function match(str: string, pattern: string) {
|
||||
if (str) str = str.replaceAll("\\", "/")
|
||||
if (pattern) pattern = pattern.replaceAll("\\", "/")
|
||||
let escaped = pattern
|
||||
.replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape special regex chars
|
||||
.replace(/\*/g, ".*") // * becomes .*
|
||||
.replace(/\?/g, ".") // ? becomes .
|
||||
|
||||
// If pattern ends with " *" (space + wildcard), make the trailing part optional
|
||||
// This allows "ls *" to match both "ls" and "ls -la"
|
||||
if (escaped.endsWith(" .*")) {
|
||||
escaped = escaped.slice(0, -3) + "( .*)?"
|
||||
}
|
||||
|
||||
const flags = process.platform === "win32" ? "si" : "s"
|
||||
return new RegExp("^" + escaped + "$", flags).test(str)
|
||||
// If pattern ends with " *" (space + wildcard), make the trailing part optional
|
||||
// This allows "ls *" to match both "ls" and "ls -la"
|
||||
if (escaped.endsWith(" .*")) {
|
||||
escaped = escaped.slice(0, -3) + "( .*)?"
|
||||
}
|
||||
|
||||
export function all(input: string, patterns: Record<string, any>) {
|
||||
const sorted = pipe(patterns, Object.entries, sortBy([([key]) => key.length, "asc"], [([key]) => key, "asc"]))
|
||||
let result = undefined
|
||||
for (const [pattern, value] of sorted) {
|
||||
if (match(input, pattern)) {
|
||||
result = value
|
||||
continue
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function allStructured(input: { head: string; tail: string[] }, patterns: Record<string, any>) {
|
||||
const sorted = pipe(patterns, Object.entries, sortBy([([key]) => key.length, "asc"], [([key]) => key, "asc"]))
|
||||
let result = undefined
|
||||
for (const [pattern, value] of sorted) {
|
||||
const parts = pattern.split(/\s+/)
|
||||
if (!match(input.head, parts[0])) continue
|
||||
if (parts.length === 1 || matchSequence(input.tail, parts.slice(1))) {
|
||||
result = value
|
||||
continue
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function matchSequence(items: string[], patterns: string[]): boolean {
|
||||
if (patterns.length === 0) return true
|
||||
const [pattern, ...rest] = patterns
|
||||
if (pattern === "*") return matchSequence(items, rest)
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (match(items[i], pattern) && matchSequence(items.slice(i + 1), rest)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
const flags = process.platform === "win32" ? "si" : "s"
|
||||
return new RegExp("^" + escaped + "$", flags).test(str)
|
||||
}
|
||||
|
||||
export function all(input: string, patterns: Record<string, any>) {
|
||||
const sorted = pipe(patterns, Object.entries, sortBy([([key]) => key.length, "asc"], [([key]) => key, "asc"]))
|
||||
let result = undefined
|
||||
for (const [pattern, value] of sorted) {
|
||||
if (match(input, pattern)) {
|
||||
result = value
|
||||
continue
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function allStructured(input: { head: string; tail: string[] }, patterns: Record<string, any>) {
|
||||
const sorted = pipe(patterns, Object.entries, sortBy([([key]) => key.length, "asc"], [([key]) => key, "asc"]))
|
||||
let result = undefined
|
||||
for (const [pattern, value] of sorted) {
|
||||
const parts = pattern.split(/\s+/)
|
||||
if (!match(input.head, parts[0])) continue
|
||||
if (parts.length === 1 || matchSequence(input.tail, parts.slice(1))) {
|
||||
result = value
|
||||
continue
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function matchSequence(items: string[], patterns: string[]): boolean {
|
||||
if (patterns.length === 0) return true
|
||||
const [pattern, ...rest] = patterns
|
||||
if (pattern === "*") return matchSequence(items, rest)
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (match(items[i], pattern) && matchSequence(items.slice(i + 1), rest)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user