fix(cli): avoid shell for file manager actions

This commit is contained in:
digua authored and digua committed 2026-05-26 23:19:47 +08:00
1 parent 19a45f9e5f
commit 8ae8ff6cda
4 files changed
+83 -18

No files matched your search

@@ -0,0 +1,29 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { openDirectoryPath, showPathInFolder } from './cache'
describe('CLI Web cache route helpers', () => {
it('opens directories with execFile arguments instead of shell commands', async () => {
const calls: Array<{ file: string; args: string[] }> = []
const dirPath = '/tmp/chat"lab; touch /tmp/pwned'
await openDirectoryPath(dirPath, 'darwin', (file, args, callback) => {
calls.push({ file, args })
callback(null)
})
assert.deepEqual(calls, [{ file: 'open', args: [dirPath] }])
})
it('reveals files with execFile arguments instead of shell commands', async () => {
const calls: Array<{ file: string; args: string[] }> = []
const filePath = '/tmp/chat"lab; touch /tmp/pwned/export.json'
await showPathInFolder(filePath, 'darwin', (file, args, callback) => {
calls.push({ file, args })
callback(null)
})
assert.deepEqual(calls, [{ file: 'open', args: ['-R', filePath] }])
})
})
+51 -18
View File
@@ -1,10 +1,53 @@
import * as fs from 'fs'
import * as fsp from 'fs/promises'
import * as path from 'path'
import { execFile } from 'child_process'
import type { FastifyInstance } from 'fastify'
import type { PathProvider } from '@openchatlab/core'
import { loadConfig } from '@openchatlab/config'
type ExecFileRunner = (file: string, args: string[], callback: (error: Error | null) => void) => unknown
function runExecFile(file: string, args: string[], runner: ExecFileRunner = execFile): Promise<void> {
return new Promise((resolve, reject) => {
runner(file, args, (error) => {
if (error) {
reject(error)
return
}
resolve()
})
})
}
export async function openDirectoryPath(
dirPath: string,
platform: NodeJS.Platform = process.platform,
runner?: ExecFileRunner
): Promise<void> {
if (platform === 'darwin') {
await runExecFile('open', [dirPath], runner)
} else if (platform === 'win32') {
await runExecFile('explorer.exe', [dirPath], runner)
} else {
await runExecFile('xdg-open', [dirPath], runner)
}
}
export async function showPathInFolder(
filePath: string,
platform: NodeJS.Platform = process.platform,
runner?: ExecFileRunner
): Promise<void> {
if (platform === 'darwin') {
await runExecFile('open', ['-R', filePath], runner)
} else if (platform === 'win32') {
await runExecFile('explorer.exe', [`/select,${filePath}`], runner)
} else {
await runExecFile('xdg-open', [path.dirname(filePath)], runner)
}
}
async function getDirSize(dirPath: string): Promise<number> {
let totalSize = 0
try {
@@ -142,14 +185,10 @@ export function registerCacheRoutes(server: FastifyInstance, pathProvider: PathP
await fsp.mkdir(dirPath, { recursive: true })
}
const { exec } = await import('child_process')
const platform = process.platform
if (platform === 'darwin') {
exec(`open "${dirPath}"`)
} else if (platform === 'win32') {
exec(`explorer "${dirPath}"`)
} else {
exec(`xdg-open "${dirPath}"`)
try {
await openDirectoryPath(dirPath)
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) }
}
return { success: true }
})
@@ -216,16 +255,10 @@ export function registerCacheRoutes(server: FastifyInstance, pathProvider: PathP
return { success: false, error: 'filePath is required' }
}
const { exec } = await import('child_process')
const platform = process.platform
const dir = path.dirname(filePath)
if (platform === 'darwin') {
exec(`open -R "${filePath}"`)
} else if (platform === 'win32') {
exec(`explorer /select,"${filePath}"`)
} else {
exec(`xdg-open "${dir}"`)
try {
await showPathInFolder(filePath)
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) }
}
return { success: true }
+1
View File
@@ -53,6 +53,7 @@ function isNewerVersion(latest: string, current: string): boolean {
if (l.minor !== c.minor) return l.minor > c.minor
if (l.patch !== c.patch) return l.patch > c.patch
// Same core version: stable > pre-release
// Design note: prerelease-to-prerelease updates are intentionally not surfaced.
if (c.pre && !l.pre) return true
return false
}
+2
View File
@@ -55,6 +55,8 @@ function isNewerVersion(latest: string, current: string): boolean {
if (l.major !== c.major) return l.major > c.major
if (l.minor !== c.minor) return l.minor > c.minor
if (l.patch !== c.patch) return l.patch > c.patch
// Design note: prerelease users are not prompted for prerelease-to-prerelease updates.
// Only a stable release with the same core version should supersede a prerelease build.
if (c.pre && !l.pre) return true
return false
}