Skip to content

Commit 7cd8df1

Browse files
committed
2 parents 9c5c513 + a4321b0 commit 7cd8df1

17 files changed

Lines changed: 759 additions & 63 deletions

File tree

.gitattributes

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Default: let Git decide (text auto-detect, normalize to LF in repo).
2+
* text=auto
3+
4+
# Windows launchers MUST stay CRLF regardless of client core.autocrlf.
5+
# cmd.exe is picky about line endings, and PowerShell on Windows expects
6+
# them too. Without this, a Mac contributor's commit can silently strip
7+
# CR bytes and break start.bat on every Windows machine.
8+
*.bat text eol=crlf
9+
*.cmd text eol=crlf
10+
*.ps1 text eol=crlf
11+
12+
# macOS / Linux launchers MUST stay LF. Double-click .command files break
13+
# on macOS if they ever get CRLF endings.
14+
*.command text eol=lf
15+
*.sh text eol=lf
16+
17+
# Binary types - never touch.
18+
*.png binary
19+
*.jpg binary
20+
*.jpeg binary
21+
*.gif binary
22+
*.ico binary
23+
*.pdf binary
24+
*.zip binary
25+
*.tar binary
26+
*.gz binary
27+
*.node binary
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
name: Windows smoke test
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
workflow_dispatch:
9+
10+
jobs:
11+
smoke:
12+
name: start.bat end-to-end on Windows
13+
runs-on: windows-latest
14+
timeout-minutes: 15
15+
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v4
19+
20+
- name: Setup Node.js
21+
uses: actions/setup-node@v4
22+
with:
23+
node-version: "22"
24+
25+
- name: Install dependencies
26+
shell: pwsh
27+
run: npm ci
28+
29+
- name: Lint Windows launchers (.bat / .ps1)
30+
shell: pwsh
31+
run: |
32+
$ErrorActionPreference = "Stop"
33+
$targets = @(
34+
"start.bat", "stop.bat",
35+
"scripts/dev/start-local-agent.ps1",
36+
"scripts/dev/stop-local-agent.ps1",
37+
"scripts/windows/capture-screen.ps1"
38+
)
39+
$failed = $false
40+
foreach ($target in $targets) {
41+
if (-not (Test-Path $target)) {
42+
Write-Host "skip missing: $target"
43+
continue
44+
}
45+
$bytes = [System.IO.File]::ReadAllBytes($target)
46+
$text = [System.Text.Encoding]::ASCII.GetString($bytes)
47+
48+
# 1. Pure ASCII. cmd.exe on zh-CN runs code page 936 and will
49+
# misdecode UTF-8 multi-byte sequences in .bat files,
50+
# producing '文件名、目录名或卷标语法不正确'. Applying the
51+
# same rule to .ps1 keeps us consistent and protects
52+
# Windows PowerShell 5.1 hosts that can't read BOM-less
53+
# UTF-8 reliably.
54+
foreach ($b in $bytes) {
55+
if ($b -gt 0x7f) {
56+
Write-Error "$target contains non-ASCII byte 0x$("{0:X2}" -f $b) - use ASCII-only for Windows compatibility"
57+
$failed = $true
58+
break
59+
}
60+
}
61+
62+
# 2. CRLF line endings. LF-only .bat files confuse some cmd.exe
63+
# versions, and Windows PowerShell 5.1 parses CRLF more
64+
# reliably than LF.
65+
if ($text -notmatch "`r`n") {
66+
Write-Error "$target has no CRLF line endings"
67+
$failed = $true
68+
}
69+
70+
# 3. No Unix-style /dev/null redirection. cmd.exe would create
71+
# an actual file named 'null' under a 'dev' directory.
72+
if ($text -match "/dev/null") {
73+
Write-Error "$target contains Unix-style '/dev/null' - use 'NUL' instead"
74+
$failed = $true
75+
}
76+
}
77+
if ($failed) { exit 1 }
78+
Write-Host "All Windows launchers passed ASCII / CRLF / NUL lint."
79+
80+
- name: Run start.bat end-to-end
81+
shell: cmd
82+
run: |
83+
start.bat
84+
85+
- name: Capture PID before stop.bat
86+
shell: pwsh
87+
run: |
88+
$pidFile = "runtime/agent/agent.pid"
89+
if (-not (Test-Path $pidFile)) {
90+
Write-Error "agent.pid missing after start.bat — agent never wrote it"
91+
exit 1
92+
}
93+
$capturedPid = (Get-Content $pidFile -Raw).Trim()
94+
Write-Host "agent PID before stop: $capturedPid"
95+
"AGENT_PID_BEFORE_STOP=$capturedPid" | Out-File -FilePath $env:GITHUB_ENV -Append
96+
97+
- name: Run stop.bat end-to-end
98+
shell: cmd
99+
run: |
100+
stop.bat
101+
102+
- name: Verify stop.bat killed the full process tree
103+
shell: pwsh
104+
run: |
105+
$ErrorActionPreference = "Stop"
106+
107+
# 1. The tracked node.exe must be gone.
108+
$tracked = [int]$env:AGENT_PID_BEFORE_STOP
109+
try {
110+
$null = Get-Process -Id $tracked -ErrorAction Stop
111+
Write-Error "stop.bat left node.exe PID $tracked running"
112+
exit 1
113+
} catch {
114+
Write-Host "tracked node.exe PID $tracked is gone"
115+
}
116+
117+
# 2. The pid / port files must be cleaned up.
118+
if (Test-Path "runtime/agent/agent.pid") {
119+
Write-Error "stop.bat left runtime/agent/agent.pid behind"
120+
exit 1
121+
}
122+
if (Test-Path "runtime/agent/agent.port") {
123+
Write-Error "stop.bat left runtime/agent/agent.port behind"
124+
exit 1
125+
}
126+
127+
# 3. The runtime/agent folder must be deletable — i.e. no
128+
# wrapper powershell.exe is still holding agent.log open.
129+
try {
130+
Copy-Item -Recurse -Force "runtime/agent" "runtime/agent.stop-test-copy"
131+
Remove-Item -Recurse -Force "runtime/agent"
132+
Write-Host "runtime/agent deleted cleanly — no leaked handles"
133+
Move-Item "runtime/agent.stop-test-copy" "runtime/agent"
134+
} catch {
135+
Write-Error "runtime/agent could not be deleted after stop.bat: $_"
136+
exit 1
137+
}
138+
139+
- name: Start agent via start-local-agent.ps1 (foreground)
140+
shell: pwsh
141+
run: |
142+
$ErrorActionPreference = "Continue"
143+
Write-Host "Running start-local-agent.ps1 in foreground to surface errors..."
144+
& "$env:GITHUB_WORKSPACE\scripts\dev\start-local-agent.ps1"
145+
Write-Host "start-local-agent.ps1 exit code: $LASTEXITCODE"
146+
147+
- name: Wait for agent to be ready
148+
shell: pwsh
149+
run: |
150+
$ErrorActionPreference = "Stop"
151+
$portFile = "runtime/agent/agent.port"
152+
$port = $null
153+
154+
for ($i = 0; $i -lt 90; $i++) {
155+
if (Test-Path $portFile) {
156+
$port = (Get-Content $portFile -Raw).Trim()
157+
if ($port) { break }
158+
}
159+
Start-Sleep -Seconds 2
160+
}
161+
162+
if (-not $port) {
163+
Write-Error "agent.port file never appeared — start.bat failed to start the agent"
164+
exit 1
165+
}
166+
167+
Write-Host "Agent port: $port"
168+
"AGENT_PORT=$port" | Out-File -FilePath $env:GITHUB_ENV -Append
169+
170+
$url = "http://127.0.0.1:$port/api/config"
171+
for ($i = 0; $i -lt 60; $i++) {
172+
try {
173+
$resp = Invoke-WebRequest -UseBasicParsing -Uri $url -TimeoutSec 3
174+
if ($resp.StatusCode -eq 200) {
175+
Write-Host "GET /api/config -> 200 OK"
176+
Write-Host $resp.Content
177+
exit 0
178+
}
179+
} catch {
180+
Start-Sleep -Seconds 2
181+
}
182+
}
183+
Write-Error "Agent never responded on /api/config within 2 minutes"
184+
exit 1
185+
186+
- name: Smoke test /desktop route
187+
shell: pwsh
188+
run: |
189+
$ErrorActionPreference = "Stop"
190+
$url = "http://127.0.0.1:$env:AGENT_PORT/desktop"
191+
$resp = Invoke-WebRequest -UseBasicParsing -Uri $url -TimeoutSec 10
192+
if ($resp.StatusCode -ne 200) {
193+
Write-Error "/desktop returned $($resp.StatusCode)"
194+
exit 1
195+
}
196+
Write-Host "/desktop -> 200 OK ($($resp.Content.Length) bytes)"
197+
198+
- name: Smoke test login (POST /api/pair)
199+
shell: pwsh
200+
run: |
201+
$ErrorActionPreference = "Stop"
202+
203+
$tokenFile = "runtime/agent/app_data/pairing-token.txt"
204+
if (-not (Test-Path $tokenFile)) {
205+
Write-Error "pairing-token.txt missing — agent never initialized session store"
206+
exit 1
207+
}
208+
$token = (Get-Content $tokenFile -Raw).Trim()
209+
Write-Host "Pairing token: $token"
210+
211+
$url = "http://127.0.0.1:$env:AGENT_PORT/api/pair"
212+
213+
# 1. Wrong token -> 401
214+
try {
215+
$bad = Invoke-WebRequest -UseBasicParsing -Uri $url -Method POST `
216+
-ContentType "application/json" `
217+
-Body '{"token":"wrong-token"}' -TimeoutSec 5
218+
Write-Error "Wrong token should have failed, got $($bad.StatusCode)"
219+
exit 1
220+
} catch {
221+
if ($_.Exception.Response.StatusCode.value__ -ne 401) {
222+
Write-Error "Wrong token expected 401, got $($_.Exception.Response.StatusCode.value__)"
223+
exit 1
224+
}
225+
Write-Host "Wrong token -> 401 (as expected)"
226+
}
227+
228+
# 2. Correct token -> 200 { ok: true }
229+
$body = @{ token = $token } | ConvertTo-Json -Compress
230+
$ok = Invoke-WebRequest -UseBasicParsing -Uri $url -Method POST `
231+
-ContentType "application/json" `
232+
-Body $body -TimeoutSec 5
233+
if ($ok.StatusCode -ne 200) {
234+
Write-Error "/api/pair returned $($ok.StatusCode)"
235+
exit 1
236+
}
237+
Write-Host "/api/pair -> 200 OK: $($ok.Content)"
238+
239+
- name: Run unit tests
240+
shell: pwsh
241+
run: npm test
242+
243+
- name: Stop agent
244+
if: always()
245+
shell: pwsh
246+
run: |
247+
$pidFile = "runtime/agent/agent.pid"
248+
if (Test-Path $pidFile) {
249+
$agentPid = (Get-Content $pidFile -Raw).Trim()
250+
try {
251+
Stop-Process -Id $agentPid -Force -ErrorAction Stop
252+
Write-Host "Stopped agent PID $agentPid"
253+
} catch {
254+
Write-Host "Agent PID $agentPid already gone"
255+
}
256+
}
257+
258+
- name: Upload agent log
259+
if: always()
260+
uses: actions/upload-artifact@v4
261+
with:
262+
name: agent-log
263+
path: |
264+
runtime/agent/agent.log
265+
runtime/agent/agent.port
266+
runtime/agent/agent.pid
267+
if-no-files-found: warn

apps/mac-web/public/app.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,10 @@ async function init() {
185185
wireEvents();
186186
state.config = await fetchJson("/api/config");
187187
serviceState.textContent = `${state.config.serviceName} 已连接,当前桌面网页控制台可用。`;
188+
const versionEl = document.querySelector("#server-version");
189+
if (versionEl) {
190+
versionEl.textContent = `version: ${state.config.serverVersion || "unknown"}`;
191+
}
188192
populateProviderOptions(state.config.modelProviders || [], state.config.defaults?.modelProvider || "codex");
189193
populateModelOptions(codexModelSelect, state.config.codexModels || [], state.config.defaults?.codexModel || "gpt-5.4");
190194
populateModelOptions(

apps/mac-web/public/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<div class="brand-block">
1313
<p class="eyebrow">Desktop Console</p>
1414
<h1>Screen Pilot</h1>
15+
<p id="server-version" class="server-version">version: &hellip;</p>
1516
<p class="muted-copy">面向 Mac 与 Windows 的网页控制台,用来做配置、认证、抓屏测试、模型测试和历史查看。</p>
1617
</div>
1718

apps/mac-web/public/styles.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,18 @@ img { display: block; max-width: 100%; }
8585
line-height: 1.5;
8686
}
8787

88+
.brand-block .server-version {
89+
margin: 0 0 10px;
90+
padding: 2px 8px;
91+
display: inline-block;
92+
border-radius: 999px;
93+
background: rgba(0, 0, 0, 0.28);
94+
color: rgba(255, 255, 255, 0.95);
95+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
96+
font-size: 0.72rem;
97+
letter-spacing: 0.02em;
98+
}
99+
88100
.brand-block .eyebrow {
89101
color: rgba(255, 255, 255, 0.7);
90102
}

core/agent/src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
loadLocalVisionModelCatalog,
2323
loadModelProviderCatalog
2424
} from "./model-catalog.js";
25+
import { resolveServerVersion } from "./server-version.js";
2526
import { SessionStore } from "./session-store.js";
2627
import { SettingsStore } from "./settings-store.js";
2728
import type {
@@ -251,6 +252,7 @@ async function routeRequest(input: {
251252
if (method === "GET" && pathname === "/api/config") {
252253
const payload: AgentConfigPayload = {
253254
serviceName: input.config.serviceName,
255+
serverVersion: resolveServerVersion(input.config.workspaceRoot),
254256
auth: {
255257
pairingRequired: true
256258
},

0 commit comments

Comments
 (0)