/** * Pure launcher generation: the PowerShell / POSIX launcher bodies, the * Windows shortcut installer, and the desktop file names for the three * supported platforms. No filesystem or process access — everything here is * testable without touching disk. */ /** Desktop platforms the launcher can generate an icon for. */ export type LauncherPlatform = 'win32' | 'darwin' | 'linux' /** Launcher behavior, resolved from plugin config. */ export interface LauncherSpec { /** Command that starts dsh (must be on PATH when the launcher runs). */ dshCommand: string /** Base URL of the dsh web GUI. */ url: string /** Optional profile started as `dsh --profile --no-open`. */ profile?: string /** Optional icon file (.ico/.png) for the desktop icon; empty uses the bundled dsh icon. */ iconPath?: string } /** Default dsh command. */ export const DEFAULT_DSH_COMMAND = 'dsh' /** Default GUI URL. */ export const DEFAULT_URL = 'http://127.0.0.1:3080' /** * Fill defaults from a partial config (schema defaults may be absent in * hand-built test contexts). An empty profile means "no --profile flag". * @param config - partial launcher config. * @returns the resolved spec. */ export function resolveLauncherSpec(config: { dshCommand?: string url?: string profile?: string iconPath?: string }): LauncherSpec { return { dshCommand: config.dshCommand ?? DEFAULT_DSH_COMMAND, url: config.url ?? DEFAULT_URL, ...(config.profile === undefined || config.profile === '' ? {} : { profile: config.profile }), ...(config.iconPath === undefined || config.iconPath === '' ? {} : { iconPath: config.iconPath }), } } /** File name of the launcher script under $DSH_HOME/desktop-launcher/. */ export function scriptFileName(platform: LauncherPlatform): string { switch (platform) { case 'win32': return 'launcher.ps1' case 'darwin': return 'launcher.command' case 'linux': return 'launcher.sh' } } /** File name of the icon placed on the Desktop. */ export function desktopFileName(platform: LauncherPlatform): string { switch (platform) { case 'win32': return 'DeepSeek-Harness.lnk' case 'darwin': return 'DeepSeek-Harness.command' case 'linux': return 'deepseek-harness.desktop' } } /** Single-quote a value for PowerShell (embedded quotes are doubled). */ function psSingle(value: string): string { return `'${value.replaceAll("'", "''")}'` } /** Single-quote a value for POSIX sh (embedded quotes are escaped). */ function shSingle(value: string): string { return `'${value.replaceAll("'", "'\\''")}'` } /** PowerShell launcher: probes the URL, starts dsh hidden, polls, opens the GUI. */ /** PowerShell launcher: shows a DeepSeek Harness styled popup while it probes, starts dsh hidden, waits for the GUI. */ function renderPowerShell(spec: LauncherSpec): string { return [ '# DeepSeek Harness launcher (generated by dsh-desktop-launcher)', "$ErrorActionPreference = 'SilentlyContinue'", `$dshCommand = ${psSingle(spec.dshCommand)}`, `$url = ${psSingle(spec.url)}`, `$profile = ${psSingle(spec.profile ?? '')}`, '', 'function Test-DshUrl {', ' try {', ' $response = Invoke-WebRequest -UseBasicParsing -Uri $url -TimeoutSec 2', ' return $response.StatusCode -ge 200 -and $response.StatusCode -lt 500', ' } catch {', ' if ($_.Exception.Response -and $_.Exception.Response.StatusCode) {', ' $code = [int]$_.Exception.Response.StatusCode', ' return $code -ge 200 -and $code -lt 500', ' }', ' return $false', ' }', '}', '', 'function Open-DshUrl {', ' try {', " Start-Process -FilePath 'explorer.exe' -ArgumentList $url", ' } catch {', ' Start-Process -FilePath $url', ' }', '}', '', 'if (Test-DshUrl) {', ' Open-DshUrl', ' exit 0', '}', '', '$commands = @(Get-Command $dshCommand -All -ErrorAction SilentlyContinue)', "$command = $commands | Where-Object { $_.CommandType -eq 'Application' -and $_.Source -match '\\.(?:cmd|exe|bat|com)$' } | Select-Object -First 1", "if ($null -eq $command) { $command = $commands | Where-Object { $_.CommandType -eq 'Application' } | Select-Object -First 1 }", "if ($null -eq $command) { $command = $commands | Where-Object { $_.CommandType -eq 'ExternalScript' } | Select-Object -First 1 }", '$dshMissing = ($null -eq $command)', '', '# ---- startup popup: borderless WPF window in DeepSeek Harness style ----', 'Add-Type -AssemblyName PresentationFramework', "[xml]$popupXaml = @'", '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', "'@", '', '$popup = [System.Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $popupXaml))', '$iconImage = $popup.FindName(\'IconImage\')', '$iconPath = Join-Path $PSScriptRoot \'dsh.png\'', "if (-not (Test-Path $iconPath)) { $iconPath = Join-Path $PSScriptRoot 'dsh.ico' }", 'if (Test-Path $iconPath) { $iconImage.Source = New-Object System.Windows.Media.Imaging.BitmapImage([uri]$iconPath) }', '$statusText = $popup.FindName(\'StatusText\')', '$detailText = $popup.FindName(\'DetailText\')', '$closeButton = $popup.FindName(\'CloseButton\')', '', 'function Pump-Popup {', ' [System.Windows.Threading.Dispatcher]::CurrentDispatcher.Invoke([Action]{}, [System.Windows.Threading.DispatcherPriority]::Background) | Out-Null', '}', '', '$closeButton.Add_Click({ $popup.Close() })', '', 'function Show-PopupError($status, $detail) {', ' $statusText.Text = $status', ' $statusText.Foreground = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(0xE5, 0x4D, 0x4D))', ' $detailText.Text = $detail', " $closeButton.Visibility = 'Visible'", ' $popup.Show()', ' while ($popup.IsVisible) {', ' Start-Sleep -Milliseconds 100', ' Pump-Popup', ' }', '}', '', 'if ($dshMissing) {', " Show-PopupError '启动失败' (\"找不到 dsh 命令:\" + $dshCommand)", ' exit 1', '}', '', '$popup.Show()', '', "$arguments = if ($profile -eq '') { @('web', '--no-open') } else { @('--profile', $profile, '--no-open') }", '$dshProcess = if ($command.CommandType -eq \'ExternalScript\' -or $command.Source -match \'\\.ps1$\') {', " $arguments = @('-NoProfile', '-File', $command.Source) + $arguments", " Start-Process -FilePath 'powershell.exe' -ArgumentList $arguments -WindowStyle Hidden -PassThru", '} else {', ' Start-Process -FilePath $command.Source -ArgumentList $arguments -WindowStyle Hidden -PassThru', '}', '', '$deadline = (Get-Date).AddSeconds(60)', 'while ((Get-Date) -lt $deadline) {', ' Start-Sleep -Milliseconds 250', ' Pump-Popup', ' if (Test-DshUrl) {', " $statusText.Text = '启动完成'", " $detailText.Text = '正在打开浏览器…'", ' Pump-Popup', ' Start-Sleep -Milliseconds 400', ' $popup.Close()', ' Open-DshUrl', ' exit 0', ' }', ' if ($null -ne $dshProcess -and $dshProcess.HasExited) {', " Show-PopupError '启动失败' (\"DSH 进程已异常退出(退出码:\" + $dshProcess.ExitCode + \")\")", ' exit 1', ' }', '}', "Show-PopupError '启动超时' (\"60 秒内未就绪:\" + $url)", 'exit 1', '', ].join('\n') } /** POSIX launcher (macOS .command / Linux .sh) with the platform open command. */ function renderPosix(platform: 'darwin' | 'linux', spec: LauncherSpec): string { const open = platform === 'darwin' ? 'open' : 'xdg-open' const alert = platform === 'darwin' ? "osascript -e 'display dialog \"dsh command not found: '\"$DASH\"'\" with title \"DSH Launcher\" with icon caution' 2>/dev/null || echo \"dsh command not found: $DASH\" >&2" : 'zenity --error --title="DSH Launcher" --text="dsh command not found: $DASH" 2>/dev/null || echo "dsh command not found: $DASH" >&2' return [ '#!/bin/bash', '# DSH web launcher (generated by dsh-desktop-launcher)', `DASH=${shSingle(spec.dshCommand)}`, `URL=${shSingle(spec.url)}`, `PROFILE=${shSingle(spec.profile ?? '')}`, '', 'probe() {', ' curl -fsS --max-time 2 "$URL" >/dev/null 2>&1', '}', '', 'if probe; then', ` ${open} "$URL"`, ' exit 0', 'fi', '', 'if ! command -v "$DASH" >/dev/null 2>&1; then', ` ${alert}`, ' exit 1', 'fi', '', 'if [ -n "$PROFILE" ]; then', ' "$DASH" --profile "$PROFILE" --no-open >/dev/null 2>&1 &', 'else', ' "$DASH" web --no-open >/dev/null 2>&1 &', 'fi', 'DASH_PID=$!', '', 'for i in $(seq 1 30); do', ' if probe; then', ` ${open} "$URL"`, ' exit 0', ' fi', ' if ! kill -0 "$DASH_PID" 2>/dev/null; then', ' echo "dsh process exited unexpectedly" >&2', ' exit 1', ' fi', ' sleep 1', 'done', '', 'echo "dsh web did not start within 30 seconds: $URL" >&2', 'exit 1', '', ].join('\n') } /** * Render the launcher script for one platform. * @param platform - target desktop platform. * @param spec - resolved launcher behavior. * @returns the script body. */ export function renderLauncherScript(platform: LauncherPlatform, spec: LauncherSpec): string { switch (platform) { case 'win32': return renderPowerShell(spec) case 'darwin': case 'linux': return renderPosix(platform, spec) } } /** * Render the Linux desktop entry (the macOS launcher doubles as the desktop * file; Windows uses a .lnk created by the installer script). * @param launcherPath - absolute path of the launcher script. * @returns the .desktop file body. */ export function renderDesktopEntry(launcherPath: string, iconPath?: string): string { const iconLine = iconPath === undefined ? 'Icon=utilities-terminal' : `Icon=${iconPath}` return [ '[Desktop Entry]', 'Type=Application', 'Version=1.0', 'Name=DeepSeek Harness', 'Comment=Launch DeepSeek Harness Web GUI', `Exec="${launcherPath}"`, iconLine, 'Terminal=true', 'Categories=Development;', '', ].join('\n') } /** * Render the Windows shortcut installer: a PowerShell script that creates * the Desktop .lnk pointing at the launcher.ps1, run hidden by the host. * @param opts - the launcher, icon, and working-directory paths. * @returns the installer body. */ export function renderShortcutInstaller(opts: { /** Absolute path of launcher.ps1. */ launcherPath: string /** Absolute path of the DSH.lnk to create. */ desktopPath: string /** Working directory of the shortcut (defaults to launcher script directory). */ workingDirectory?: string /** Legacy alias for workingDirectory. */ homeDir?: string /** Icon the shortcut shows (an .ico/.png path, or a shell-exe icon spec). */ iconLocation: string }): string { const { launcherPath, desktopPath, workingDirectory, homeDir, iconLocation } = opts const targetWorkingDir = workingDirectory ?? homeDir ?? '' return [ '# DSH desktop shortcut installer (generated by dsh-desktop-launcher)', "$ErrorActionPreference = 'Stop'", '$ws = New-Object -ComObject WScript.Shell', `$shortcut = $ws.CreateShortcut(${psSingle(desktopPath)})`, `$shortcut.TargetPath = ${psSingle('powershell.exe')}`, `$shortcut.Arguments = ${psSingle(`-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File ${launcherPath}`)}`, `$shortcut.WorkingDirectory = ${psSingle(targetWorkingDir)}`, `$shortcut.IconLocation = ${psSingle(iconLocation)}`, `$shortcut.Description = ${psSingle('Launch DeepSeek Harness Web GUI')}`, '$shortcut.Save()', '', ].join('\n') }