Skip to content

Commit 44cff85

Browse files
rwang23Fei-Away
authored andcommitted
fix(windows): install a self-contained runtime
1 parent c2e9f40 commit 44cff85

6 files changed

Lines changed: 360 additions & 14 deletions

File tree

docs/platforms.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
| 用途 | 路径 |
2929
|------|------|
3030
| 源码(本整理包) | `Codex-Dream-Skin/windows/` |
31+
| 安装后的受管运行时 | `%LOCALAPPDATA%\CodexDreamSkin\engine` |
3132
| 状态 / 日志 | `%LOCALAPPDATA%\CodexDreamSkin` |
3233
| Codex 配置 | `%USERPROFILE%\.codex\config.toml` |
3334
| 默认 CDP 端口 | 首选 `9335`,冲突时自动选空闲口(Mac 包默认从 `9341` 起) |
@@ -74,7 +75,7 @@ Windows 启动、失败回滚与恢复重开均从已注册的 `OpenAI.Codex`
7475
### 平台差异
7576

7677
- macOS 的选图脚本会把这些字段写入主题库,可通过 `--appearance``--focus-x``--focus-y``--safe-area``--task-mode` 设置。
77-
- Windows 安装会初始化 `%LOCALAPPDATA%\CodexDreamSkin\active-theme``themes``images`,并把「桥本有菜」同时设为首次默认和可切换的已保存主题。系统托盘支持更换背景、保存当前主题、从「已保存主题」切换、暂停和恢复;图片与 `theme.json` 保存在主题仓库中,不写进 Codex 的 `config.toml`。安装会保留用户已有的 `appearanceTheme`;仅在识别到旧版精确托管的浅色三元组时按备份迁移。
78+
- Windows 安装会把运行所需的 `assets/``scripts/` 原子复制到 `%LOCALAPPDATA%\CodexDreamSkin\engine`,所有快捷方式均指向该受管副本,因此安装后可移动或删除源码目录。安装还会初始化 `active-theme``themes``images`,并把「桥本有菜」同时设为首次默认和可切换的已保存主题。系统托盘支持更换背景、保存当前主题、从「已保存主题」切换、暂停和恢复;图片与 `theme.json` 保存在主题仓库中,不写进 Codex 的 `config.toml`。安装会保留用户已有的 `appearanceTheme`;仅在识别到旧版精确托管的浅色三元组时按备份迁移。
7879
- Windows 渲染器仍支持在注入前用 `window.__CODEX_DREAM_SKIN_CONFIG__` 提供内存级可选覆盖(形状同上,颜色覆盖使用 `palette.accent`),但普通用户应优先使用持久化主题仓库与托盘。
7980

8081
## 预设与图片类型

windows/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### 新增
66

7+
- Windows 安装器会先校验并原子复制运行所需的 `assets/``scripts/``%LOCALAPPDATA%\CodexDreamSkin\engine`,启动、恢复和托盘快捷方式统一指向该受管副本;安装完成后可移动或删除源码克隆。重装前若旧托盘仍在运行,安装器会明确要求退出,避免新旧脚本混用。
78
- 渲染层支持通用自适应图像主题:本地 Canvas 采样图像亮度、主色、焦点和比例,为壁纸层提供自适应色彩与构图建议;支持 `appearance: auto | light | dark``art.focusX/focusY``0..1`)、`art.safeArea: auto | left | right | center | none``art.taskMode: auto | ambient | banner | off`。外观壳仍由显式主题或原生外观信号决定。
89
- 显式外观与艺术元数据优先于分析结果;超宽图默认任务横幅,普通比例图默认环境背景,`off` 可关闭任务页图像。分析完全在渲染器本地完成,不上传图片。
910
- Windows 发行 payload 直接读取受管 `theme.json`,完整支持与 macOS 一致的外观、焦点、安全区和任务页模式契约,不再依赖预先设置的 renderer 全局变量。

windows/SKILL.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ Apply a reversible renderer skin through Chromium DevTools Protocol while launch
99

1010
## Workflow
1111

12-
1. Install Node.js 22 or newer, close Codex, then run `scripts/install-dream-skin.ps1` once to preserve the user's native appearance settings, seed the Arina Hashimoto theme, and create launch/restore/tray shortcuts.
13-
2. Run `scripts/start-dream-skin.ps1`. The shortcut asks before restarting an already-open Codex app; CLI callers must explicitly add `-RestartExisting`.
12+
1. Install Node.js 22 or newer, close Codex, then run `scripts/install-dream-skin.ps1` once. The installer preserves the user's native appearance settings, seeds the Arina Hashimoto theme, copies the runtime to `%LOCALAPPDATA%\CodexDreamSkin\engine`, and creates launch/restore/tray shortcuts that do not depend on the source checkout.
13+
2. Use the `Codex Dream Skin` shortcut, or run `%LOCALAPPDATA%\CodexDreamSkin\engine\scripts\start-dream-skin.ps1`. The shortcut asks before restarting an already-open Codex app; CLI callers must explicitly add `-RestartExisting`.
1414
3. Run `scripts/verify-dream-skin.ps1 -ScreenshotPath <absolute-path>` after launch. Treat a missing continuous wallpaper, home shell, native composer, sidebar layer, or injection marker as failure. The native suggestion count is responsive and may be two to four.
1515
4. Inspect the screenshot against `references/qa-inventory.md`. Verify both the home screen and a normal task before signing off.
1616
5. Run `scripts/restore-dream-skin.ps1` to remove the live skin, close the saved CDP session, and reopen Codex normally. Add `-RestoreBaseTheme` to restore only saved appearance keys, `-RecoverConfigBackup` for explicit byte-for-byte recovery of a damaged config, or `-Uninstall` to delete shortcuts. A completed config restore archives that install's backup so a later install captures a fresh baseline.
@@ -33,6 +33,7 @@ Apply a reversible renderer skin through Chromium DevTools Protocol while launch
3333
- Loopback prevents LAN exposure, but Chromium CDP has no same-user authentication. Run only trusted local software while the skin is active, and use restore to close the debug session when it is no longer needed.
3434
- Preserve `config.toml` as strict UTF-8. Never use encoding-dependent whole-file PowerShell reads/writes, silently transcode UTF-16, or overwrite a file that changed after it was read. Ambiguous TOML shapes must fail before writing rather than receive a best-effort rewrite.
3535
- Keep install/start/restore/verify serialized with the per-user operation lock in `common-windows.ps1`.
36+
- Treat `%LOCALAPPDATA%\CodexDreamSkin\engine` as an installer-managed runtime. Exit the Dream Skin tray before reinstalling so the installer can replace that runtime atomically and update every shortcut to the same copy.
3637

3738
## Checks
3839

@@ -45,7 +46,7 @@ node --check assets\renderer-inject.js
4546
## Resources
4647

4748
- `scripts/injector.mjs`: CDP connection, renderer injection, verification, screenshot, and removal.
48-
- `scripts/common-windows.ps1`: Store-package discovery, Node validation, port ownership, state, and process identity safety.
49+
- `scripts/common-windows.ps1`: Store-package discovery, Node validation, managed runtime installation, port ownership, state, and process identity safety.
4950
- `scripts/config-utf8.ps1`: atomic UTF-8 configuration backup, selective restore, and explicit recovery.
5051
- `assets/dream-skin.css`: full visual layer.
5152
- `assets/renderer-inject.js`: idempotent DOM integration and cleanup.
@@ -55,4 +56,4 @@ node --check assets\renderer-inject.js
5556
- `scripts/tray-dream-skin.ps1`: Windows Forms tray for apply, pause, import, save, switch, and complete restore.
5657
- `references/qa-inventory.md`: required functional and visual signoff coverage.
5758
- `references/runtime-notes.md`: troubleshooting and update behavior.
58-
- `tests/run-tests.ps1`: configuration, state, recovery, payload, and CDP validation regression checks.
59+
- `tests/run-tests.ps1`: managed runtime, configuration, state, recovery, payload, and CDP validation regression checks. Use `-EngineOnly` for the source-independence contract.

windows/scripts/common-windows.ps1

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,196 @@ function Test-DreamSkinPathWithin {
4848
}
4949
}
5050

51+
function Get-DreamSkinRuntimeEnginePaths {
52+
param([string]$StateRoot = (Join-Path $env:LOCALAPPDATA 'CodexDreamSkin'))
53+
$root = Join-Path ([System.IO.Path]::GetFullPath($StateRoot)) 'engine'
54+
$scripts = Join-Path $root 'scripts'
55+
return [pscustomobject]@{
56+
Root = $root
57+
Scripts = $scripts
58+
Start = Join-Path $scripts 'start-dream-skin.ps1'
59+
Restore = Join-Path $scripts 'restore-dream-skin.ps1'
60+
Tray = Join-Path $scripts 'tray-dream-skin.ps1'
61+
}
62+
}
63+
64+
function Test-DreamSkinTrayActive {
65+
$sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
66+
$mutex = [System.Threading.Mutex]::new($false, "Local\CodexDreamSkin.$sid.Tray")
67+
$acquired = $false
68+
try {
69+
try { $acquired = $mutex.WaitOne(0) } catch [System.Threading.AbandonedMutexException] {
70+
$acquired = $true
71+
}
72+
if ($acquired) {
73+
$mutex.ReleaseMutex()
74+
$acquired = $false
75+
return $false
76+
}
77+
return $true
78+
} finally {
79+
if ($acquired) { try { $mutex.ReleaseMutex() } catch {} }
80+
$mutex.Dispose()
81+
}
82+
}
83+
84+
function Assert-DreamSkinRuntimeTree {
85+
param([Parameter(Mandatory = $true)][string]$Path)
86+
$root = [System.IO.Path]::GetFullPath($Path)
87+
if (-not (Test-Path -LiteralPath $root -PathType Container)) {
88+
throw "Dream Skin runtime directory does not exist: $root"
89+
}
90+
if (-not (Get-Command Assert-DreamSkinNoReparseComponents -ErrorAction SilentlyContinue)) {
91+
throw 'Dream Skin managed-path validation is unavailable.'
92+
}
93+
Assert-DreamSkinNoReparseComponents -Path $root
94+
foreach ($item in Get-ChildItem -LiteralPath $root -Recurse -Force -ErrorAction Stop) {
95+
if (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) {
96+
throw "Dream Skin runtime contains a junction or symbolic link: $($item.FullName)"
97+
}
98+
}
99+
}
100+
101+
function Remove-DreamSkinRuntimeTree {
102+
param(
103+
[Parameter(Mandatory = $true)][string]$Path,
104+
[Parameter(Mandatory = $true)][string]$StateRoot
105+
)
106+
$fullPath = [System.IO.Path]::GetFullPath($Path)
107+
$fullStateRoot = [System.IO.Path]::GetFullPath($StateRoot)
108+
if (-not (Test-DreamSkinPathWithin -Path $fullPath -Root $fullStateRoot)) {
109+
throw "Refusing to remove a runtime path outside the Dream Skin state root: $fullPath"
110+
}
111+
if (-not (Test-Path -LiteralPath $fullPath)) { return }
112+
Assert-DreamSkinRuntimeTree -Path $fullPath
113+
Remove-Item -LiteralPath $fullPath -Recurse -Force -ErrorAction Stop
114+
}
115+
116+
function Install-DreamSkinRuntimeEngine {
117+
param(
118+
[Parameter(Mandatory = $true)][string]$SkillRoot,
119+
[Parameter(Mandatory = $true)][string]$StateRoot
120+
)
121+
if (-not (Get-Command Ensure-DreamSkinManagedDirectory -ErrorAction SilentlyContinue)) {
122+
throw 'Dream Skin managed-directory validation is unavailable.'
123+
}
124+
125+
$sourceRoot = [System.IO.Path]::GetFullPath($SkillRoot)
126+
$fullStateRoot = [System.IO.Path]::GetFullPath($StateRoot)
127+
$engine = Get-DreamSkinRuntimeEnginePaths -StateRoot $fullStateRoot
128+
$required = @(
129+
'assets\dream-reference.jpg',
130+
'assets\dream-skin.css',
131+
'assets\renderer-inject.js',
132+
'assets\theme.json',
133+
'scripts\common-windows.ps1',
134+
'scripts\config-utf8.ps1',
135+
'scripts\image-metadata.mjs',
136+
'scripts\injector.mjs',
137+
'scripts\install-dream-skin.ps1',
138+
'scripts\restore-dream-skin.ps1',
139+
'scripts\start-dream-skin.ps1',
140+
'scripts\theme-windows.ps1',
141+
'scripts\tray-dream-skin.ps1',
142+
'scripts\verify-dream-skin.ps1'
143+
)
144+
foreach ($relative in $required) {
145+
if (-not (Test-Path -LiteralPath (Join-Path $sourceRoot $relative) -PathType Leaf)) {
146+
throw "Dream Skin runtime source is incomplete: $relative"
147+
}
148+
}
149+
foreach ($directoryName in @('assets', 'scripts')) {
150+
$sourceDirectory = Join-Path $sourceRoot $directoryName
151+
if ((Test-DreamSkinPathEqual -Left $fullStateRoot -Right $sourceDirectory) -or
152+
(Test-DreamSkinPathWithin -Path $fullStateRoot -Root $sourceDirectory)) {
153+
throw "Dream Skin state root cannot be created inside its runtime source: $fullStateRoot"
154+
}
155+
Assert-DreamSkinRuntimeTree -Path $sourceDirectory
156+
}
157+
158+
Ensure-DreamSkinManagedDirectory -Path $fullStateRoot -Root $fullStateRoot
159+
$token = [guid]::NewGuid().ToString('N')
160+
$stagingRoot = Join-Path $fullStateRoot ".engine-staging-$token"
161+
$backupRoot = Join-Path $fullStateRoot ".engine-backup-$token"
162+
Ensure-DreamSkinManagedDirectory -Path $stagingRoot -Root $fullStateRoot
163+
164+
try {
165+
foreach ($directoryName in @('assets', 'scripts')) {
166+
Copy-Item -LiteralPath (Join-Path $sourceRoot $directoryName) -Destination $stagingRoot `
167+
-Recurse -Force -ErrorAction Stop
168+
}
169+
Assert-DreamSkinRuntimeTree -Path $stagingRoot
170+
foreach ($relative in $required) {
171+
if (-not (Test-Path -LiteralPath (Join-Path $stagingRoot $relative) -PathType Leaf)) {
172+
throw "Staged Dream Skin runtime is incomplete: $relative"
173+
}
174+
}
175+
176+
$sourcePrefix = $sourceRoot.TrimEnd('\') + '\'
177+
$sourceFiles = @(
178+
Get-ChildItem -LiteralPath (Join-Path $sourceRoot 'assets'), (Join-Path $sourceRoot 'scripts') `
179+
-Recurse -File -Force -ErrorAction Stop
180+
)
181+
$stagedFiles = @(
182+
Get-ChildItem -LiteralPath (Join-Path $stagingRoot 'assets'), (Join-Path $stagingRoot 'scripts') `
183+
-Recurse -File -Force -ErrorAction Stop
184+
)
185+
if ($sourceFiles.Count -ne $stagedFiles.Count) {
186+
throw 'Staged Dream Skin runtime file count does not match its source.'
187+
}
188+
foreach ($sourceFile in $sourceFiles) {
189+
$relative = $sourceFile.FullName.Substring($sourcePrefix.Length)
190+
$stagedFile = Join-Path $stagingRoot $relative
191+
if (-not (Test-Path -LiteralPath $stagedFile -PathType Leaf) -or
192+
(Get-FileHash -Algorithm SHA256 -LiteralPath $sourceFile.FullName).Hash -cne
193+
(Get-FileHash -Algorithm SHA256 -LiteralPath $stagedFile).Hash) {
194+
throw "Staged Dream Skin runtime failed hash verification: $relative"
195+
}
196+
}
197+
198+
$hasBackup = $false
199+
if (Test-Path -LiteralPath $engine.Root) {
200+
Assert-DreamSkinRuntimeTree -Path $engine.Root
201+
Move-Item -LiteralPath $engine.Root -Destination $backupRoot -ErrorAction Stop
202+
$hasBackup = $true
203+
}
204+
try {
205+
Move-Item -LiteralPath $stagingRoot -Destination $engine.Root -ErrorAction Stop
206+
} catch {
207+
$installError = $_.Exception.Message
208+
if ($hasBackup -and -not (Test-Path -LiteralPath $engine.Root)) {
209+
try {
210+
Move-Item -LiteralPath $backupRoot -Destination $engine.Root -ErrorAction Stop
211+
$hasBackup = $false
212+
} catch {
213+
throw "Dream Skin runtime update failed and its previous engine could not be restored. Backup preserved at ${backupRoot}: $installError"
214+
}
215+
}
216+
throw
217+
}
218+
if ($hasBackup) {
219+
try { Remove-DreamSkinRuntimeTree -Path $backupRoot -StateRoot $fullStateRoot } catch {
220+
try {
221+
Write-Warning "Installed the new runtime but could not remove its previous backup: $($_.Exception.Message)"
222+
} catch {
223+
# Cleanup must never make a committed runtime update look unsuccessful.
224+
}
225+
}
226+
}
227+
return Get-DreamSkinRuntimeEnginePaths -StateRoot $fullStateRoot
228+
} finally {
229+
if (Test-Path -LiteralPath $stagingRoot) {
230+
try { Remove-DreamSkinRuntimeTree -Path $stagingRoot -StateRoot $fullStateRoot } catch {
231+
try {
232+
Write-Warning "Could not remove the staged Dream Skin runtime: $($_.Exception.Message)"
233+
} catch {
234+
# Cleanup must never mask the runtime installation result.
235+
}
236+
}
237+
}
238+
}
239+
}
240+
51241
function Test-DreamSkinCommandLineToken {
52242
param([string]$CommandLine, [string]$Token)
53243
if (-not $CommandLine -or -not $Token) { return $false }

windows/scripts/install-dream-skin.ps1

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ try {
3535
(Get-DreamSkinCodexProcesses -Codex $savedPathCandidate).Count -gt 0) {
3636
throw 'The saved Codex path is still running but no longer matches a registered Store package. Close it manually before installing.'
3737
}
38-
$null = Initialize-DreamSkinThemeStore -SkillRoot $SkillRoot -StateRoot $StateRoot
38+
if (Test-DreamSkinTrayActive) {
39+
throw 'Exit the Codex Dream Skin tray before reinstalling so every shortcut can move to the new runtime safely.'
40+
}
41+
$engine = Install-DreamSkinRuntimeEngine -SkillRoot $SkillRoot -StateRoot $StateRoot
42+
$null = Initialize-DreamSkinThemeStore -SkillRoot $engine.Root -StateRoot $StateRoot
3943
$ConfigPath = Join-Path $HOME '.codex\config.toml'
4044
$BackupPath = Join-Path $StateRoot 'config.before-dream-skin.toml'
4145
Install-DreamSkinBaseTheme -ConfigPath $ConfigPath -BackupPath $BackupPath
@@ -45,32 +49,32 @@ try {
4549
$desktop = [Environment]::GetFolderPath('Desktop')
4650
$startMenu = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs'
4751
$powershell = (Get-Command powershell.exe -ErrorAction Stop).Source
48-
$startScript = Join-Path $PSScriptRoot 'start-dream-skin.ps1'
49-
$restoreScript = Join-Path $PSScriptRoot 'restore-dream-skin.ps1'
50-
$trayScript = Join-Path $PSScriptRoot 'tray-dream-skin.ps1'
52+
$startScript = $engine.Start
53+
$restoreScript = $engine.Restore
54+
$trayScript = $engine.Tray
5155
$portArgument = if ($PortExplicit) { " -Port $Port" } else { '' }
5256

5357
foreach ($folder in @($desktop, $startMenu)) {
5458
$shortcut = $shell.CreateShortcut((Join-Path $folder 'Codex Dream Skin.lnk'))
5559
$shortcut.TargetPath = $powershell
5660
$shortcut.Arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$startScript`"$portArgument -PromptRestart"
57-
$shortcut.WorkingDirectory = $SkillRoot
61+
$shortcut.WorkingDirectory = $engine.Root
5862
$shortcut.Description = 'Launch the official Codex app with Codex Dream Skin'
5963
$shortcut.Save()
6064
}
6165

6266
$restore = $shell.CreateShortcut((Join-Path $desktop 'Codex Dream Skin - Restore.lnk'))
6367
$restore.TargetPath = $powershell
6468
$restore.Arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$restoreScript`"$portArgument -RestoreBaseTheme -PromptRestart"
65-
$restore.WorkingDirectory = $SkillRoot
69+
$restore.WorkingDirectory = $engine.Root
6670
$restore.Description = 'Restore the official Codex appearance and close the CDP session'
6771
$restore.Save()
6872

6973
foreach ($folder in @($desktop, $startMenu)) {
7074
$tray = $shell.CreateShortcut((Join-Path $folder 'Codex Dream Skin - Tray.lnk'))
7175
$tray.TargetPath = $powershell
7276
$tray.Arguments = "-NoProfile -STA -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$trayScript`"$portArgument"
73-
$tray.WorkingDirectory = $SkillRoot
77+
$tray.WorkingDirectory = $engine.Root
7478
$tray.Description = 'Open Codex Dream Skin status and theme controls in the system tray'
7579
$tray.Save()
7680
}
@@ -80,7 +84,7 @@ try {
8084
}
8185

8286
if ($NoShortcuts) {
83-
Write-Host 'Codex Dream Skin base theme installed. Run start-dream-skin.ps1 to launch it.'
87+
Write-Host "Codex Dream Skin base theme installed at $($engine.Root). Run $($engine.Start) to launch it."
8488
} else {
8589
Write-Host 'Codex Dream Skin installed. The launch shortcut asks before restarting an open Codex window.'
8690
}

0 commit comments

Comments
 (0)