opencode-deepseek-chan 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 daemon1s
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # DeepSeek-chan theme suite for OpenCode Desktop
2
+
3
+ Unofficial community theme: cyber-dark and glassmorphism visual theme inspired by DeepSeek-chan for OpenCode Desktop, with an interactive terminal installer that applies and restores the theme safely — no manual code editing required.
4
+
5
+ 非官方社区主题:受 DeepSeek-chan 启发的赛博暗黑玻璃拟态主题,适用于 OpenCode Desktop,附带交互式终端安装器,可安全地安装与恢复主题,无需手动修改代码。
6
+
7
+ ![Version](https://img.shields.io/badge/version-1.0.0-4D6BFE)
8
+ ![License](https://img.shields.io/badge/license-MIT-70C0FF)
9
+ ![Platform](https://img.shields.io/badge/platform-Windows-0B1528)
10
+ ![Node](https://img.shields.io/badge/node-%3E%3D18-4D6BFE)
11
+ ![GitHub Stars](https://img.shields.io/github/stars/daemon1s/opencode-deepseek-chan)
12
+ ![GitHub Release](https://img.shields.io/github/v/release/daemon1s/opencode-deepseek-chan)
13
+
14
+ ## Gallery / 主题预览
15
+
16
+ | Féishā | Maid whale | Midnight | Artwork |
17
+ | --- | --- | --- | --- |
18
+ | ![Féishā preview](./assets/previews/feisha_preview.gif) | ![Maid whale preview](./assets/previews/doncella_preview.gif) | ![Midnight preview](./assets/previews/midnight_preview.gif) | ![Artwork preview](./assets/previews/artwork_preview.png) |
19
+ | 绯莎 | 深海女仆 | 深夜 | 插画 |
20
+
21
+ ## Presets / 预设
22
+
23
+ The installer downloads the background assets on demand from GitHub Releases and caches them locally in `~/.opencode-deepseek-cache`, keeping the repository light.
24
+
25
+ 安装器按需从 GitHub Releases 下载背景资源,并缓存到本地 `~/.opencode-deepseek-cache`,保持仓库轻量。
26
+
27
+ | # | Preset / 预设 | Type / 类型 | Size / 大小 |
28
+ | --- | --- | --- | --- |
29
+ | 1 | DeepSeek Féishā / DeepSeek 绯莎 | Video 1080p / 视频 1080p | ~6.4 MB |
30
+ | 2 | DeepSeek Maid Whale (ZipZip) / DeepSeek 深海女仆 (ZipZip) | Video 1080p / 视频 1080p | ~11.4 MB |
31
+ | 3 | DeepSeek Midnight / DeepSeek 深夜 | Video 1080p / 视频 1080p | ~20.2 MB |
32
+ | 4 | DeepSeek Artwork / DeepSeek 插画 | Image 4K / 图片 4K | ~9.6 MB |
33
+
34
+ ## Features / 功能特性
35
+
36
+ - **Professional CLI installer**: no suspicious executables (.bat or .exe); runs transparently and audibly through Node.js.
37
+ - **DeepSeek Cyber palette**: electric blue (`#4D6BFE`), neon cyan (`#70C0FF`) and deep midnight blue (`#0B1528`).
38
+ - **Translucent glassmorphism**: `backdrop-filter` blur on panels, chat bar and floating menus.
39
+ - **Intact contrast and syntax**: high-specificity selectors that never touch Monaco editor syntax colors or text readability.
40
+ - **Native CSP bypass**: the background is Base64-encoded and injected into the Electron preload cycle, avoiding local security directive blocks (`oc://renderer/index.html`).
41
+ - **Automatic backup and safe uninstall**: creates an `app.asar.bak` backup before modifying resources, so the original app can always be restored.
42
+
43
+ - **专业的 CLI 安装器**:不含可疑的可执行文件(.bat 或 .exe),全程通过 Node.js 透明、可审计地运行。
44
+ - **DeepSeek 赛博配色**:电光蓝(`#4D6BFE`)、霓虹青(`#70C0FF`)与深午夜蓝(`#0B1528`)。
45
+ - **半透明玻璃拟态**:面板、聊天栏和悬浮菜单均采用 `backdrop-filter` 模糊效果。
46
+ - **保持对比度与语法高亮**:高特异性选择器不会影响 Monaco 编辑器的语法着色与文字可读性。
47
+ - **原生绕过 CSP**:背景以 Base64 编码并注入 Electron 预加载流程,避免本地安全策略(`oc://renderer/index.html`)拦截。
48
+ - **自动备份与安全卸载**:修改资源前自动生成 `app.asar.bak` 备份,随时可恢复原始应用。
49
+
50
+ ## Installation / 安装
51
+
52
+ ### Option 1: run directly with npx (no clone needed) / 直接使用 npx(无需克隆)
53
+
54
+ ```bash
55
+ npx opencode-deepseek-chan
56
+ ```
57
+
58
+ ### Option 2: clone the repository / 克隆仓库
59
+
60
+ ```bash
61
+ git clone https://github.com/daemon1s/opencode-deepseek-chan.git
62
+ cd opencode-deepseek-chan
63
+ npm install
64
+ npm start
65
+ ```
66
+
67
+ The interactive menu shows the ready-to-use presets:
68
+
69
+ 交互式菜单会展示预设选项:
70
+
71
+ ```text
72
+ Select a background preset:
73
+ 1) DeepSeek Féishā (1080p)
74
+ 2) DeepSeek Maid Whale (ZipZip)
75
+ 3) DeepSeek Midnight (1080p)
76
+ 4) DeepSeek Artwork (4K)
77
+ 5) Restore original OpenCode design
78
+ 6) Exit
79
+ ```
80
+
81
+ ## Direct CLI usage / 命令行用法
82
+
83
+ Apply a preset or restore without the interactive menu:
84
+
85
+ 无需交互菜单即可直接应用预设或恢复:
86
+
87
+ ```bash
88
+ node bin/cli.js --preset feisha-1080p # Video 1080p / 视频 1080p
89
+ node bin/cli.js --preset zipzip-1080p # Video 1080p / 视频 1080p
90
+ node bin/cli.js --preset midnight-1080p # Video 1080p / 视频 1080p
91
+ node bin/cli.js --preset static-4k # Image 4K / 图片 4K
92
+ node bin/cli.js --install /path/to/file.mp4 # Custom image or video / 自定义图片或视频
93
+ node bin/cli.js --restore # Restore factory state / 恢复出厂状态
94
+ node bin/cli.js --lang zh # Force language: en | zh | es / 指定语言
95
+ ```
96
+
97
+ ## Uninstall / 卸载
98
+
99
+ ```bash
100
+ npm run uninstall-theme
101
+ ```
102
+
103
+ Or select option **5** in the interactive menu.
104
+
105
+ 或直接在交互菜单中选择选项 **5**。
106
+
107
+ ## Project structure / 项目结构
108
+
109
+ - `bin/cli.js`: interactive CLI entry point / 交互式 CLI 入口。
110
+ - `src/engine.js`: theme CSS generation and install logic / 主题 CSS 生成与安装逻辑。
111
+ - `src/presets.js`: preset catalog with remote URLs and integrity checks / 预设目录(远程地址与完整性校验)。
112
+ - `src/downloader.js`: on-demand downloads with mirror fallback and local cache / 按需下载(镜像回退与本地缓存)。
113
+ - `src/i18n.js`: multilingual strings (en / zh / es) / 多语言文案(en / zh / es)。
114
+ - `test/`: automated tests / 自动化测试。
115
+
116
+ ## Credits / 致谢与许可
117
+
118
+ All background assets are adapted from community artworks, used strictly for non-commercial open-source customization under the original licenses (CC BY-NC-SA 4.0). Redistributions and forks must keep this attribution intact.
119
+
120
+ 所有背景资源改编自社区作品,仅用于非商业的开源个性化定制,遵循原作者许可协议(CC BY-NC-SA 4.0)。再分发与分支必须保留本署名。
121
+
122
+ | Preset / 预设 | Original artwork / 原作 | Source / 来源 |
123
+ | --- | --- | --- |
124
+ | Féishā / 绯莎 | `[绯莎]蓝色大肥鱼 deepseek娘化` by `@无菌洗手液Eva ofmaster` (Bilibili), published by `Always` | [Steam Workshop ID 3790716675](https://steamcommunity.com/sharedfiles/filedetails/?id=3790716675) |
125
+ | Maid whale / 深海女仆 | `DeepSeek 蓝色大肥鱼 深海女仆 鲸鱼娘` by `ZipZipPipe` (Bilibili), base design by `上善无形` (Bilibili), published by `HDHKVT` | [Steam Workshop ID 3787993441](https://steamcommunity.com/sharedfiles/filedetails/?id=3787993441) |
126
+ | Midnight / 深夜 | `[deepseek娘]深夜的大肥鱼` by `樱小路露娜` | [Steam Workshop ID 3787353838](https://steamcommunity.com/sharedfiles/filedetails/?id=3787353838) |
127
+ | Artwork / 插画 | AI-generated by `@daemon1s`, upscaled to 4K with [Upscayl](https://upscayl.org) / 由 `@daemon1s` 使用 AI 生成,并经 Upscayl 放大至 4K | Repository original artwork / 仓库原创插画 |
128
+
129
+ License terms / 许可条款:
130
+
131
+ - **Non-commercial use only / 仅限非商业使用**: none of the assets may be sold, bundled for profit or included in paid commercial software.
132
+ - **Continuous attribution / 持续署名**: any fork or redistribution must keep this credits table recognizing the original authors (`@无菌洗手液Eva ofmaster`, `ZipZipPipe`, `上善无形`, `樱小路露娜`, `Always`, `HDHKVT`).
133
+ - **Share alike / 相同方式共享**: derivatives must be licensed under CC BY-NC-SA 4.0.
package/bin/cli.js ADDED
@@ -0,0 +1,316 @@
1
+ #!/usr/bin/env node
2
+
3
+ const path = require("path");
4
+ const readline = require("readline");
5
+ const { presets, getPresetById } = require("../src/presets");
6
+ const { detectLocale, getLocaleStrings } = require("../src/i18n");
7
+ const {
8
+ resolveAssetPath,
9
+ getUserCacheDirectory,
10
+ downloadWithFallback
11
+ } = require("../src/downloader");
12
+ const {
13
+ isOpenCodeRunning,
14
+ resolveOpenCodePath,
15
+ installTheme,
16
+ restoreTheme
17
+ } = require("../src/engine");
18
+ const { renderStaticBanner, startBannerLoop, renderProgressBar } = require("../src/banner");
19
+
20
+ const colors = {
21
+ reset: "\x1b[0m",
22
+ cyan: "\x1b[36m",
23
+ blue: "\x1b[34m",
24
+ green: "\x1b[32m",
25
+ yellow: "\x1b[33m",
26
+ red: "\x1b[31m",
27
+ bold: "\x1b[1m"
28
+ };
29
+
30
+ const printBanner = () => {
31
+ process.stdout.write(renderStaticBanner() + "\n");
32
+ };
33
+
34
+ const parseCliArguments = () => {
35
+ const args = process.argv.slice(2);
36
+ const parsed = {
37
+ help: false,
38
+ lang: null,
39
+ preset: null,
40
+ install: null,
41
+ restore: false,
42
+ noAnimation: false
43
+ };
44
+
45
+ for (let index = 0; index < args.length; index += 1) {
46
+ const current = args[index];
47
+ if (current === "--help" || current === "-h") {
48
+ parsed.help = true;
49
+ } else if (current === "--no-animation") {
50
+ parsed.noAnimation = true;
51
+ } else if (current === "--lang" || current === "-l") {
52
+ parsed.lang = args[index + 1] || null;
53
+ index += 1;
54
+ } else if (current === "--preset" || current === "-p") {
55
+ parsed.preset = args[index + 1] || null;
56
+ index += 1;
57
+ } else if (current === "--install" || current === "-i") {
58
+ parsed.install = args[index + 1] || true;
59
+ if (args[index + 1] && !args[index + 1].startsWith("-")) {
60
+ index += 1;
61
+ }
62
+ } else if (
63
+ current === "--uninstall" ||
64
+ current === "-u" ||
65
+ current === "--restore" ||
66
+ current === "-r"
67
+ ) {
68
+ parsed.restore = true;
69
+ }
70
+ }
71
+
72
+ return parsed;
73
+ };
74
+
75
+ const formatBytes = (bytes) => {
76
+ if (bytes < 1024) return `${bytes} B`;
77
+ if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
78
+ return `${(bytes / 1048576).toFixed(1)} MB`;
79
+ };
80
+
81
+ const obtainAssetFile = async (preset, localeStrings) => {
82
+ const workspaceAssetsDir = path.join(__dirname, "..", "assets");
83
+ const userCacheDir = getUserCacheDirectory();
84
+
85
+ const existingFile = resolveAssetPath(
86
+ preset,
87
+ workspaceAssetsDir,
88
+ userCacheDir
89
+ );
90
+
91
+ if (existingFile) {
92
+ return existingFile;
93
+ }
94
+
95
+ const destinationPath = path.join(userCacheDir, preset.filename);
96
+ console.log(`${colors.cyan}${localeStrings.downloading}: ${preset.filename}${colors.reset}`);
97
+
98
+ let previousRenderLength = 0;
99
+ const onProgress = (downloaded, total) => {
100
+ const ratio = total > 0 ? downloaded / total : 0;
101
+ const percentage = Math.round(ratio * 100);
102
+ const progressBar = renderProgressBar(ratio);
103
+ const progressText = ` ${progressBar} ${String(percentage).padStart(3)}% ${formatBytes(downloaded)} / ${formatBytes(total || preset.expectedSize)}`;
104
+ process.stdout.write(`\r${progressText}\x1b[K`);
105
+ previousRenderLength = progressText.length;
106
+ };
107
+
108
+ const onFallback = () => {
109
+ process.stdout.write("\n");
110
+ console.log(`${colors.yellow}${localeStrings.downloadFallback}${colors.reset}`);
111
+ };
112
+
113
+ await downloadWithFallback(preset, destinationPath, onProgress, onFallback);
114
+ process.stdout.write("\n\n");
115
+ return destinationPath;
116
+ };
117
+
118
+ const executeInstall = async (itemType, filePath, localeStrings) => {
119
+ if (isOpenCodeRunning()) {
120
+ console.log(`${colors.red}${localeStrings.opencodeRunning}${colors.reset}`);
121
+ process.exit(1);
122
+ }
123
+
124
+ const openCodePath = resolveOpenCodePath();
125
+ if (!openCodePath) {
126
+ console.log(`${colors.red}${localeStrings.pathNotFound}${colors.reset}`);
127
+ process.exit(1);
128
+ }
129
+
130
+ try {
131
+ await installTheme(itemType, filePath);
132
+ console.log(`${colors.green}${colors.bold}${localeStrings.installSuccess}${colors.reset}\n`);
133
+ } catch (error) {
134
+ console.log(`${colors.red}${localeStrings.error}: ${error.message}${colors.reset}`);
135
+ process.exit(1);
136
+ }
137
+ };
138
+
139
+ const executeRestore = (localeStrings) => {
140
+ if (isOpenCodeRunning()) {
141
+ console.log(`${colors.red}${localeStrings.opencodeRunning}${colors.reset}`);
142
+ process.exit(1);
143
+ }
144
+
145
+ try {
146
+ restoreTheme();
147
+ console.log(`${colors.green}${colors.bold}${localeStrings.restoreSuccess}${colors.reset}\n`);
148
+ } catch (error) {
149
+ console.log(`${colors.red}${localeStrings.error}: ${error.message}${colors.reset}`);
150
+ process.exit(1);
151
+ }
152
+ };
153
+
154
+ const promptLanguageSelection = (readlineInterface, localeStrings, cliArgs, onSelected) => {
155
+ const langBannerLoop = startBannerLoop({ noAnimation: cliArgs?.noAnimation });
156
+ console.log(`\n${colors.cyan}${localeStrings.selectLangPrompt}${colors.reset}\n`);
157
+ console.log(` ${colors.bold}1)${colors.reset} English (en)`);
158
+ console.log(` ${colors.bold}2)${colors.reset} 中文 (zh)`);
159
+ console.log(` ${colors.bold}3)${colors.reset} Español (es)`);
160
+ console.log(` ${colors.bold}4)${colors.reset} ${localeStrings.cancelOption}\n`);
161
+
162
+ readlineInterface.question(`> `, (answer) => {
163
+ langBannerLoop.stop();
164
+ const trimmed = answer.trim();
165
+ if (trimmed === "1") {
166
+ onSelected("en");
167
+ } else if (trimmed === "2") {
168
+ onSelected("zh");
169
+ } else if (trimmed === "3") {
170
+ onSelected("es");
171
+ } else {
172
+ onSelected(null);
173
+ }
174
+ });
175
+ };
176
+
177
+ const runInteractiveMenu = async (localeStrings, currentLocale = "en", cliArgs = {}) => {
178
+ if (process.stdout.isTTY && !cliArgs.noAnimation) {
179
+ process.stdout.write("\x1b[2J\x1b[1;1H");
180
+ }
181
+ printBanner();
182
+
183
+ const bannerLoop = startBannerLoop({ noAnimation: cliArgs.noAnimation });
184
+
185
+ const readlineInterface = readline.createInterface({
186
+ input: process.stdin,
187
+ output: process.stdout
188
+ });
189
+
190
+ const changeLangOptionNumber = presets.length + 1;
191
+ const restoreOptionNumber = presets.length + 2;
192
+ const exitOptionNumber = presets.length + 3;
193
+
194
+ console.log(`${localeStrings.selectPrompt}\n`);
195
+ presets.forEach((preset, index) => {
196
+ const title = localeStrings[preset.titleKey] || preset.id;
197
+ console.log(` ${colors.bold}${index + 1})${colors.reset} ${title}`);
198
+ });
199
+ console.log(` ${colors.bold}${changeLangOptionNumber})${colors.reset} ${localeStrings.changeLangOption}`);
200
+ console.log(` ${colors.bold}${restoreOptionNumber})${colors.reset} ${localeStrings.restoreOption}`);
201
+ console.log(` ${colors.bold}${exitOptionNumber})${colors.reset} ${localeStrings.exitOption}\n`);
202
+
203
+ readlineInterface.question(`> `, async (answer) => {
204
+ bannerLoop.stop();
205
+ const trimmed = answer.trim();
206
+ const numericChoice = parseInt(trimmed, 10);
207
+
208
+ if (numericChoice >= 1 && numericChoice <= presets.length) {
209
+ readlineInterface.close();
210
+ const chosenPreset = presets[numericChoice - 1];
211
+ const title = localeStrings[chosenPreset.titleKey] || chosenPreset.id;
212
+ console.log(`\n${colors.cyan}${title}${colors.reset}\n`);
213
+ const assetPath = await obtainAssetFile(chosenPreset, localeStrings);
214
+ await executeInstall(chosenPreset.type, assetPath, localeStrings);
215
+ } else if (numericChoice === changeLangOptionNumber) {
216
+ promptLanguageSelection(readlineInterface, localeStrings, cliArgs, async (selectedLanguage) => {
217
+ readlineInterface.close();
218
+ if (selectedLanguage && selectedLanguage !== currentLocale) {
219
+ const updatedStrings = getLocaleStrings(selectedLanguage);
220
+ await runInteractiveMenu(updatedStrings, selectedLanguage, cliArgs);
221
+ } else {
222
+ await runInteractiveMenu(localeStrings, currentLocale, cliArgs);
223
+ }
224
+ });
225
+ } else if (numericChoice === restoreOptionNumber) {
226
+ readlineInterface.close();
227
+ executeRestore(localeStrings);
228
+ } else {
229
+ readlineInterface.close();
230
+ process.exit(0);
231
+ }
232
+ });
233
+ };
234
+
235
+ const main = async () => {
236
+ const cliArgs = parseCliArguments();
237
+ const currentLocale = detectLocale(cliArgs.lang);
238
+ const localeStrings = getLocaleStrings(currentLocale);
239
+
240
+ if (cliArgs.help) {
241
+ printBanner();
242
+ console.log("Usage:");
243
+ console.log(" npx opencode-deepseek-chan");
244
+ console.log(" npx opencode-deepseek-chan --preset <id|1-4>");
245
+ console.log(" npx opencode-deepseek-chan --install <file-path>");
246
+ console.log(" npx opencode-deepseek-chan --restore");
247
+ console.log(" npx opencode-deepseek-chan --lang <en|zh|es>");
248
+ console.log(" npx opencode-deepseek-chan --no-animation\n");
249
+ console.log("Available presets:");
250
+ presets.forEach((preset, index) => {
251
+ const title = localeStrings[preset.titleKey] || preset.id;
252
+ console.log(` ${index + 1}) ${preset.id.padEnd(16)} ${title}`);
253
+ });
254
+ console.log("");
255
+ return;
256
+ }
257
+
258
+ if (cliArgs.restore) {
259
+ printBanner();
260
+ executeRestore(localeStrings);
261
+ return;
262
+ }
263
+
264
+ if (cliArgs.preset) {
265
+ printBanner();
266
+ const numericIndex = parseInt(cliArgs.preset, 10);
267
+ let chosenPreset = null;
268
+
269
+ if (!isNaN(numericIndex) && numericIndex >= 1 && numericIndex <= presets.length) {
270
+ chosenPreset = presets[numericIndex - 1];
271
+ } else {
272
+ chosenPreset = getPresetById(cliArgs.preset);
273
+ }
274
+
275
+ if (!chosenPreset) {
276
+ console.log(`${colors.red}${localeStrings.error}: Unknown preset ${cliArgs.preset}${colors.reset}\n`);
277
+ return;
278
+ }
279
+
280
+ const title = localeStrings[chosenPreset.titleKey] || chosenPreset.id;
281
+ console.log(`${colors.cyan}${title}${colors.reset}\n`);
282
+ const assetPath = await obtainAssetFile(chosenPreset, localeStrings);
283
+ await executeInstall(chosenPreset.type, assetPath, localeStrings);
284
+ return;
285
+ }
286
+
287
+ if (cliArgs.install) {
288
+ printBanner();
289
+ if (typeof cliArgs.install === "string") {
290
+ const cleaned = cliArgs.install.trim().replace(/^["']|["']$/g, "");
291
+ const extension = path.extname(cleaned).toLowerCase();
292
+ const isVideo = [".mp4", ".webm", ".mkv"].includes(extension);
293
+ await executeInstall(isVideo ? "video" : "image", cleaned, localeStrings);
294
+ } else {
295
+ const defaultPreset = presets[0];
296
+ const assetPath = await obtainAssetFile(defaultPreset, localeStrings);
297
+ await executeInstall(defaultPreset.type, assetPath, localeStrings);
298
+ }
299
+ return;
300
+ }
301
+
302
+ await runInteractiveMenu(localeStrings, currentLocale, cliArgs);
303
+ };
304
+
305
+ if (require.main === module) {
306
+ main().catch((error) => {
307
+ console.error(`\x1b[31m${error.message}\x1b[0m`);
308
+ process.exit(1);
309
+ });
310
+ }
311
+
312
+ module.exports = {
313
+ parseCliArguments,
314
+ runInteractiveMenu,
315
+ main
316
+ };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "opencode-deepseek-chan",
3
+ "version": "1.0.0",
4
+ "description": "Animated video backgrounds & cyber glassmorphism theme for OpenCode Desktop (DeepSeek-chan). 适用于 OpenCode Desktop 的 DeepSeek 赛博动态壁纸主题。",
5
+ "main": "bin/cli.js",
6
+ "bin": {
7
+ "opencode-deepseek-chan": "./bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node ./bin/cli.js",
11
+ "test": "node --test test/*.test.js",
12
+ "install-theme": "node ./bin/cli.js --install",
13
+ "uninstall-theme": "node ./bin/cli.js --uninstall"
14
+ },
15
+ "files": [
16
+ "bin",
17
+ "src",
18
+ "README.md"
19
+ ],
20
+ "keywords": [
21
+ "opencode",
22
+ "opencode-desktop",
23
+ "theme",
24
+ "deepseek",
25
+ "dark-theme",
26
+ "glassmorphism",
27
+ "cli",
28
+ "deepseek-chan",
29
+ "video-background",
30
+ "electron-theme",
31
+ "cli-installer"
32
+ ],
33
+ "author": "daemon1s",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/daemon1s/opencode-deepseek-chan.git"
38
+ },
39
+ "homepage": "https://github.com/daemon1s/opencode-deepseek-chan",
40
+ "bugs": {
41
+ "url": "https://github.com/daemon1s/opencode-deepseek-chan/issues"
42
+ },
43
+ "dependencies": {
44
+ "@electron/asar": "^3.2.14"
45
+ }
46
+ }
package/src/banner.js ADDED
@@ -0,0 +1,131 @@
1
+ const deepseekPalette = [
2
+ { r: 13, g: 37, b: 90 },
3
+ { r: 29, g: 99, b: 255 },
4
+ { r: 0, g: 210, b: 255 },
5
+ { r: 139, g: 233, b: 253 },
6
+ { r: 240, g: 248, b: 255 },
7
+ { r: 0, g: 210, b: 255 },
8
+ { r: 29, g: 99, b: 255 }
9
+ ];
10
+
11
+ const bannerLines = [
12
+ " ____ ____ _ _ ",
13
+ " | _ \\ ___ ___ _ __ / ___| ___ ___| | __ ___| |__ __ _ _ __ ",
14
+ " | | | |/ _ \\/ _ \\ '_ \\\\___ \\ / _ \\/ _ \\ |/ / _____ / __| '_ \\ / _` | '_ \\ ",
15
+ " | |_| | __/ __/ |_) |___) | __/ __/ < |_____| (__| | | | (_| | | | |",
16
+ " |____/ \\___|\\___| .__/|____/ \\___|\\___|_|\\_\\ \\___|_| |_|\\__,_|_| |_|",
17
+ " |_| "
18
+ ];
19
+
20
+ const interpolateColor = (colorA, colorB, factor) => {
21
+ const clampedFactor = Math.max(0, Math.min(1, factor));
22
+ return {
23
+ r: Math.round(colorA.r + (colorB.r - colorA.r) * clampedFactor),
24
+ g: Math.round(colorA.g + (colorB.g - colorA.g) * clampedFactor),
25
+ b: Math.round(colorA.b + (colorB.b - colorA.b) * clampedFactor)
26
+ };
27
+ };
28
+
29
+ const getMultiStopColor = (palette, progress) => {
30
+ const normalized = ((progress % 1) + 1) % 1;
31
+ const segments = palette.length - 1;
32
+ const position = normalized * segments;
33
+ const index = Math.floor(position);
34
+ const nextIndex = Math.min(index + 1, palette.length - 1);
35
+ const localFactor = position - index;
36
+ return interpolateColor(palette[index], palette[nextIndex], localFactor);
37
+ };
38
+
39
+ const colorTextRgb = (text, r, g, b) => {
40
+ return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
41
+ };
42
+
43
+ const renderProgressBar = (progress, width = 24) => {
44
+ const clampedProgress = Math.max(0, Math.min(1, progress));
45
+ const filledCount = Math.round(clampedProgress * width);
46
+ let bar = "[";
47
+ for (let index = 0; index < width; index += 1) {
48
+ if (index < filledCount) {
49
+ const color = getMultiStopColor(deepseekPalette, index / width);
50
+ bar += `\x1b[38;2;${color.r};${color.g};${color.b}m█`;
51
+ } else {
52
+ bar += "\x1b[90m░\x1b[0m";
53
+ }
54
+ }
55
+ bar += "\x1b[0m]";
56
+ return bar;
57
+ };
58
+
59
+ const generateBannerFrame = (offset = 0) => {
60
+ const maxWidth = bannerLines.reduce((max, line) => Math.max(max, line.length), 0);
61
+ const coloredLines = bannerLines.map((line) => {
62
+ let coloredLine = "";
63
+ for (let charIndex = 0; charIndex < line.length; charIndex += 1) {
64
+ const char = line[charIndex];
65
+ if (char === " ") {
66
+ coloredLine += " ";
67
+ } else {
68
+ const progress = (charIndex / maxWidth + offset) % 1;
69
+ const color = getMultiStopColor(deepseekPalette, progress);
70
+ coloredLine += `\x1b[38;2;${color.r};${color.g};${color.b}m${char}`;
71
+ }
72
+ }
73
+ return `${coloredLine}\x1b[0m`;
74
+ });
75
+ return coloredLines.join("\n");
76
+ };
77
+
78
+ const renderStaticBanner = () => {
79
+ const asciiBanner = generateBannerFrame(0.15);
80
+ const cyanCode = "\x1b[36m";
81
+ const grayCode = "\x1b[90m";
82
+ const whiteCode = "\x1b[97m";
83
+ const resetCode = "\x1b[0m";
84
+
85
+ const englishTagline = ` ${cyanCode}›${resetCode} ${whiteCode}DeepSeek-chan theme suite for OpenCode Desktop${resetCode}`;
86
+ const chineseTagline = ` ${cyanCode}›${resetCode} ${grayCode}OpenCode Desktop 的 DeepSeek-chan (深度求索娘) 主题套件${resetCode}`;
87
+ const authorLine = ` ${cyanCode}›${resetCode} ${grayCode}Author: ${whiteCode}@daemon1s${resetCode} ${grayCode}(https://github.com/daemon1s)${resetCode}`;
88
+
89
+ return `${asciiBanner}\n\n${englishTagline}\n${chineseTagline}\n${authorLine}\n`;
90
+ };
91
+
92
+ const startBannerLoop = (options = {}) => {
93
+ const isInteractiveTty = Boolean(process.stdout.isTTY) && !options.noAnimation;
94
+ if (!isInteractiveTty) {
95
+ return {
96
+ stop: () => {}
97
+ };
98
+ }
99
+
100
+ let offset = 0;
101
+ const frameInterval = 60;
102
+
103
+ const timer = setInterval(() => {
104
+ offset = (offset + 0.025) % 1;
105
+ const frameLines = generateBannerFrame(offset).split("\n");
106
+ let buffer = "\x1b7";
107
+ for (let index = 0; index < frameLines.length; index += 1) {
108
+ buffer += `\x1b[${index + 1};1H${frameLines[index]}\x1b[K`;
109
+ }
110
+ buffer += "\x1b8";
111
+ process.stdout.write(buffer);
112
+ }, frameInterval);
113
+
114
+ return {
115
+ stop: () => {
116
+ clearInterval(timer);
117
+ }
118
+ };
119
+ };
120
+
121
+ module.exports = {
122
+ deepseekPalette,
123
+ bannerLines,
124
+ interpolateColor,
125
+ getMultiStopColor,
126
+ colorTextRgb,
127
+ generateBannerFrame,
128
+ renderStaticBanner,
129
+ startBannerLoop,
130
+ renderProgressBar
131
+ };
@@ -0,0 +1,205 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const os = require("os");
4
+ const https = require("https");
5
+ const http = require("http");
6
+ const crypto = require("crypto");
7
+
8
+ const getFallbackUrls = (url) => {
9
+ return [
10
+ `https://ghproxy.net/${url}`,
11
+ `https://mirror.ghproxy.com/${url}`
12
+ ];
13
+ };
14
+
15
+ const verifyFileSize = (filePath, expectedSize) => {
16
+ if (!fs.existsSync(filePath)) {
17
+ return false;
18
+ }
19
+ const fileStats = fs.statSync(filePath);
20
+ return fileStats.size === expectedSize;
21
+ };
22
+
23
+ const verifyFileIntegrity = (filePath, expectedSha256) => {
24
+ if (!expectedSha256 || !fs.existsSync(filePath)) {
25
+ return false;
26
+ }
27
+ const fileBuffer = fs.readFileSync(filePath);
28
+ const fileHash = crypto.createHash("sha256").update(fileBuffer).digest("hex");
29
+ return fileHash === expectedSha256.toLowerCase();
30
+ };
31
+
32
+ const resolveAssetPath = (preset, localAssetsDir, userCacheDir) => {
33
+ if (localAssetsDir) {
34
+ const candidatePath = path.join(localAssetsDir, preset.filename);
35
+ if (verifyFileSize(candidatePath, preset.expectedSize)) {
36
+ return candidatePath;
37
+ }
38
+ const rootCandidatePath = path.join(localAssetsDir, "..", preset.filename);
39
+ if (verifyFileSize(rootCandidatePath, preset.expectedSize)) {
40
+ return rootCandidatePath;
41
+ }
42
+ }
43
+
44
+ if (userCacheDir) {
45
+ const cacheCandidatePath = path.join(userCacheDir, preset.filename);
46
+ if (verifyFileSize(cacheCandidatePath, preset.expectedSize)) {
47
+ return cacheCandidatePath;
48
+ }
49
+ }
50
+
51
+ return null;
52
+ };
53
+
54
+ const getUserCacheDirectory = () => {
55
+ const homeDirectory = os.homedir();
56
+ const cachePath = path.join(homeDirectory, ".opencode-deepseek-cache");
57
+ if (!fs.existsSync(cachePath)) {
58
+ fs.mkdirSync(cachePath, { recursive: true });
59
+ }
60
+ return cachePath;
61
+ };
62
+
63
+ const downloadStream = (requestUrl, destinationPath, onProgress, timeoutMs, redirectHops = 0) => {
64
+ return new Promise((resolve, reject) => {
65
+ if (redirectHops > 5) {
66
+ reject(new Error("Too many redirects"));
67
+ return;
68
+ }
69
+
70
+ const parsedUrl = new URL(requestUrl);
71
+ const client = parsedUrl.protocol === "https:" ? https : http;
72
+
73
+ const request = client.get(
74
+ requestUrl,
75
+ {
76
+ timeout: timeoutMs,
77
+ headers: {
78
+ "User-Agent": "opencode-deepseek-downloader"
79
+ }
80
+ },
81
+ (response) => {
82
+ if ([301, 302, 303, 307, 308].includes(response.statusCode)) {
83
+ const redirectLocation = response.headers.location;
84
+ if (!redirectLocation) {
85
+ reject(new Error("Redirect header missing"));
86
+ return;
87
+ }
88
+ const nextUrl = new URL(redirectLocation, requestUrl).toString();
89
+ downloadStream(nextUrl, destinationPath, onProgress, timeoutMs, redirectHops + 1)
90
+ .then(resolve)
91
+ .catch(reject);
92
+ return;
93
+ }
94
+
95
+ if (response.statusCode !== 200) {
96
+ reject(new Error(`Server returned HTTP ${response.statusCode}`));
97
+ return;
98
+ }
99
+
100
+ const totalBytes = parseInt(response.headers["content-length"] || "0", 10);
101
+ let downloadedBytes = 0;
102
+
103
+ const fileStream = fs.createWriteStream(destinationPath);
104
+
105
+ response.on("data", (chunk) => {
106
+ downloadedBytes += chunk.length;
107
+ if (onProgress && typeof onProgress === "function") {
108
+ onProgress(downloadedBytes, totalBytes);
109
+ }
110
+ });
111
+
112
+ response.pipe(fileStream);
113
+
114
+ fileStream.on("finish", () => {
115
+ fileStream.close(() => resolve());
116
+ });
117
+
118
+ fileStream.on("error", (error) => {
119
+ if (fs.existsSync(destinationPath)) {
120
+ fs.unlinkSync(destinationPath);
121
+ }
122
+ reject(error);
123
+ });
124
+ }
125
+ );
126
+
127
+ request.on("timeout", () => {
128
+ request.destroy();
129
+ if (fs.existsSync(destinationPath)) {
130
+ fs.unlinkSync(destinationPath);
131
+ }
132
+ reject(new Error("Download connection timed out"));
133
+ });
134
+
135
+ request.on("error", (error) => {
136
+ if (fs.existsSync(destinationPath)) {
137
+ fs.unlinkSync(destinationPath);
138
+ }
139
+ reject(error);
140
+ });
141
+ });
142
+ };
143
+
144
+ const downloadWithFallback = async (preset, targetPath, onProgress, onFallbackTriggered) => {
145
+ const temporaryDownloadPath = `${targetPath}.download`;
146
+ const initialTimeoutMs = 15000;
147
+ const standardTimeoutMs = 30000;
148
+
149
+ const candidateUrls = [preset.remoteUrl, ...getFallbackUrls(preset.remoteUrl)];
150
+
151
+ let downloadSucceeded = false;
152
+ let finalError = null;
153
+
154
+ for (let index = 0; index < candidateUrls.length; index += 1) {
155
+ const currentUrl = candidateUrls[index];
156
+ const isFallbackAttempt = index > 0;
157
+ const currentTimeout = isFallbackAttempt ? standardTimeoutMs : initialTimeoutMs;
158
+
159
+ if (isFallbackAttempt && onFallbackTriggered) {
160
+ onFallbackTriggered(currentUrl);
161
+ }
162
+
163
+ try {
164
+ await downloadStream(
165
+ currentUrl,
166
+ temporaryDownloadPath,
167
+ onProgress,
168
+ currentTimeout
169
+ );
170
+
171
+ const isValidSize = verifyFileSize(temporaryDownloadPath, preset.expectedSize);
172
+ const isValidHash = verifyFileIntegrity(temporaryDownloadPath, preset.sha256);
173
+ if (!isValidSize || !isValidHash) {
174
+ if (fs.existsSync(temporaryDownloadPath)) {
175
+ fs.unlinkSync(temporaryDownloadPath);
176
+ }
177
+ throw new Error("Downloaded asset failed integrity verification");
178
+ }
179
+
180
+ fs.renameSync(temporaryDownloadPath, targetPath);
181
+ downloadSucceeded = true;
182
+ break;
183
+ } catch (error) {
184
+ finalError = error;
185
+ if (fs.existsSync(temporaryDownloadPath)) {
186
+ fs.unlinkSync(temporaryDownloadPath);
187
+ }
188
+ }
189
+ }
190
+
191
+ if (!downloadSucceeded) {
192
+ throw finalError || new Error("Failed to download asset from all available mirrors");
193
+ }
194
+
195
+ return targetPath;
196
+ };
197
+
198
+ module.exports = {
199
+ getFallbackUrls,
200
+ verifyFileSize,
201
+ verifyFileIntegrity,
202
+ resolveAssetPath,
203
+ getUserCacheDirectory,
204
+ downloadWithFallback
205
+ };
package/src/engine.js ADDED
@@ -0,0 +1,353 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const os = require("os");
4
+ const childProcess = require("child_process");
5
+ const asar = require("@electron/asar");
6
+
7
+ const isOpenCodeRunning = () => {
8
+ try {
9
+ const isWindows = process.platform === "win32";
10
+ const command = isWindows
11
+ ? 'tasklist /NH /FI "IMAGENAME eq opencode.exe"'
12
+ : "pgrep -l -i opencode";
13
+
14
+ const output = childProcess.execSync(command, {
15
+ encoding: "utf8",
16
+ stdio: ["pipe", "pipe", "ignore"]
17
+ });
18
+
19
+ return /opencode/i.test(output);
20
+ } catch {
21
+ return false;
22
+ }
23
+ };
24
+
25
+ const resolveOpenCodePath = () => {
26
+ const localAppData = process.env.LOCALAPPDATA || "";
27
+ const candidatePaths = [
28
+ path.join(localAppData, "Programs", "@opencode-aidesktop"),
29
+ path.join(localAppData, "Programs", "OpenCode"),
30
+ path.join(localAppData, "OpenCode")
31
+ ];
32
+
33
+ for (const candidate of candidatePaths) {
34
+ if (fs.existsSync(candidate)) {
35
+ return candidate;
36
+ }
37
+ }
38
+
39
+ return null;
40
+ };
41
+
42
+ const encodeImageToBase64 = (filePath) => {
43
+ const extension = path.extname(filePath).toLowerCase();
44
+ const mimeTypes = {
45
+ ".jpg": "image/jpeg",
46
+ ".jpeg": "image/jpeg",
47
+ ".png": "image/png",
48
+ ".webp": "image/webp",
49
+ ".svg": "image/svg+xml"
50
+ };
51
+ const mimeType = mimeTypes[extension] || "image/png";
52
+ const buffer = fs.readFileSync(filePath);
53
+ return `data:${mimeType};base64,${buffer.toString("base64")}`;
54
+ };
55
+
56
+ const generateThemeCss = (isVideo, base64Url) => {
57
+ const backgroundRule = isVideo
58
+ ? `
59
+ #opencode-bg-video {
60
+ position: fixed !important;
61
+ top: 0 !important;
62
+ left: 0 !important;
63
+ width: 100vw !important;
64
+ height: 100vh !important;
65
+ object-fit: cover !important;
66
+ z-index: -2 !important;
67
+ pointer-events: none !important;
68
+ }
69
+ `
70
+ : `
71
+ body::before {
72
+ content: "" !important;
73
+ position: fixed !important;
74
+ inset: 0 !important;
75
+ background-image: url('${base64Url}') !important;
76
+ background-size: cover !important;
77
+ background-position: center !important;
78
+ background-repeat: no-repeat !important;
79
+ image-rendering: high-quality !important;
80
+ z-index: -2 !important;
81
+ pointer-events: none !important;
82
+ }
83
+ `;
84
+
85
+ return `
86
+ :root {
87
+ --background-base: transparent !important;
88
+ --background-weak: rgba(16, 26, 48, 0.65) !important;
89
+ --background-strong: rgba(11, 21, 40, 0.75) !important;
90
+ --background-stronger: rgba(11, 21, 40, 0.85) !important;
91
+ --v2-background-bg-base: transparent !important;
92
+ --v2-background-bg-deep: rgba(11, 21, 40, 0.5) !important;
93
+ --v2-background-bg-layer-01: rgba(16, 26, 48, 0.6) !important;
94
+ --v2-background-bg-layer-02: rgba(22, 32, 53, 0.7) !important;
95
+ --v2-background-bg-layer-03: rgba(30, 42, 68, 0.8) !important;
96
+ --surface-raised-stronger-non-alpha: #0e172a !important;
97
+ --surface-raised-base-hover: rgba(77, 107, 254, 0.35) !important;
98
+ }
99
+ ${backgroundRule}
100
+ html,
101
+ body,
102
+ #root {
103
+ background-color: transparent !important;
104
+ background-image: none !important;
105
+ color: #e2e8f0 !important;
106
+ }
107
+
108
+ #root > div,
109
+ #root > div * {
110
+ background-color: transparent !important;
111
+ }
112
+
113
+ #root > div {
114
+ background-color: rgba(10, 14, 26, 0.70) !important;
115
+ z-index: 1 !important;
116
+ position: relative !important;
117
+ }
118
+
119
+ header,
120
+ nav,
121
+ aside,
122
+ [data-slot="titlebar-v2"],
123
+ [data-component="tabs"],
124
+ [data-titlebar-tab],
125
+ .home-session-group-header:after {
126
+ background-color: transparent !important;
127
+ background-image: none !important;
128
+ }
129
+
130
+ [data-titlebar-tab] {
131
+ background: rgba(16, 26, 48, 0.6) !important;
132
+ border: 1px solid rgba(77, 107, 254, 0.25) !important;
133
+ border-radius: 6px !important;
134
+ }
135
+
136
+ [data-titlebar-tab][data-active="true"] {
137
+ background: rgba(30, 45, 75, 0.85) !important;
138
+ border-color: rgba(112, 192, 255, 0.45) !important;
139
+ }
140
+
141
+ [data-role="user"],
142
+ [data-message-author="user"] {
143
+ background-color: rgba(77, 107, 254, 0.22) !important;
144
+ border: 1px solid rgba(77, 107, 254, 0.35) !important;
145
+ border-radius: 10px !important;
146
+ padding: 8px 12px !important;
147
+ }
148
+
149
+ [data-role="assistant"],
150
+ [data-message-author="assistant"] {
151
+ background-color: rgba(16, 26, 48, 0.75) !important;
152
+ border: 1px solid rgba(112, 192, 255, 0.25) !important;
153
+ border-radius: 10px !important;
154
+ padding: 8px 12px !important;
155
+ }
156
+
157
+ form,
158
+ textarea,
159
+ input,
160
+ select {
161
+ background-color: rgba(20, 27, 45, 0.85) !important;
162
+ border: 1px solid rgba(77, 107, 254, 0.35) !important;
163
+ color: #f8fafc !important;
164
+ border-radius: 8px !important;
165
+ }
166
+
167
+ .monaco-editor,
168
+ .monaco-editor-background,
169
+ .monaco-editor .margin,
170
+ .monaco-editor .overflow-guard,
171
+ pre {
172
+ background-color: rgba(11, 21, 40, 0.9) !important;
173
+ }
174
+
175
+ button {
176
+ background-color: rgba(22, 32, 53, 0.8) !important;
177
+ border: 1px solid rgba(77, 107, 254, 0.35) !important;
178
+ color: #f1f5f9 !important;
179
+ border-radius: 8px !important;
180
+ }
181
+
182
+ button:hover {
183
+ background-color: rgba(77, 107, 254, 0.4) !important;
184
+ border-color: #70c0ff !important;
185
+ }
186
+
187
+ [data-component="context-menu-content"],
188
+ [data-component="context-menu-sub-content"],
189
+ [data-component="dropdown-menu-content"],
190
+ [data-component="dropdown-menu-sub-content"],
191
+ [data-component="menu-v2-content"],
192
+ [role="menu"] {
193
+ background-color: rgba(14, 23, 42, 0.94) !important;
194
+ border: 1px solid rgba(77, 107, 254, 0.35) !important;
195
+ border-radius: 8px !important;
196
+ backdrop-filter: blur(16px) !important;
197
+ -webkit-backdrop-filter: blur(16px) !important;
198
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.65), 0 0 16px rgba(77, 107, 254, 0.25) !important;
199
+ color: #f1f5f9 !important;
200
+ }
201
+
202
+ [data-slot="context-menu-item"],
203
+ [data-slot="context-menu-sub-trigger"],
204
+ [data-slot="dropdown-menu-item"],
205
+ [data-slot="dropdown-menu-sub-trigger"],
206
+ [data-component="menu-v2-item"],
207
+ [role="menuitem"] {
208
+ color: #e2e8f0 !important;
209
+ border-radius: 4px !important;
210
+ }
211
+
212
+ [data-slot="context-menu-item"]:hover,
213
+ [data-slot="context-menu-sub-trigger"]:hover,
214
+ [data-slot="dropdown-menu-item"]:hover,
215
+ [data-slot="dropdown-menu-sub-trigger"]:hover,
216
+ [data-component="menu-v2-item"]:hover,
217
+ [role="menuitem"]:hover,
218
+ [data-slot="context-menu-sub-trigger"][data-expanded],
219
+ [data-slot="dropdown-menu-sub-trigger"][data-expanded],
220
+ [data-highlighted] {
221
+ background-color: rgba(77, 107, 254, 0.35) !important;
222
+ color: #ffffff !important;
223
+ }
224
+
225
+ [data-slot="context-menu-separator"],
226
+ [data-slot="dropdown-menu-separator"],
227
+ [data-slot="menu-v2-separator"] {
228
+ background-color: rgba(77, 107, 254, 0.25) !important;
229
+ }
230
+ `;
231
+ };
232
+
233
+ const installTheme = async (itemType, filePath, logFunction) => {
234
+ const log = logFunction || console.log;
235
+
236
+ if (isOpenCodeRunning()) {
237
+ throw new Error("OPENCODE_RUNNING");
238
+ }
239
+
240
+ const openCodeBasePath = resolveOpenCodePath();
241
+ if (!openCodeBasePath) {
242
+ throw new Error("PATH_NOT_FOUND");
243
+ }
244
+
245
+ const resourcesDirectory = path.join(openCodeBasePath, "resources");
246
+ const appAsarPath = path.join(resourcesDirectory, "app.asar");
247
+ const appAsarBackupPath = path.join(resourcesDirectory, "app.asar.bak");
248
+
249
+ if (!fs.existsSync(appAsarPath) && !fs.existsSync(appAsarBackupPath)) {
250
+ throw new Error(`app.asar not found in ${resourcesDirectory}`);
251
+ }
252
+
253
+ if (fs.existsSync(appAsarBackupPath)) {
254
+ fs.copyFileSync(appAsarBackupPath, appAsarPath);
255
+ } else {
256
+ fs.copyFileSync(appAsarPath, appAsarBackupPath);
257
+ }
258
+
259
+ if (!filePath || !fs.existsSync(filePath)) {
260
+ throw new Error(`Background asset not found at: ${filePath}`);
261
+ }
262
+
263
+ const isVideo = itemType === "video";
264
+ let base64Url = "";
265
+ if (!isVideo) {
266
+ base64Url = encodeImageToBase64(filePath);
267
+ }
268
+
269
+ const themeCss = generateThemeCss(isVideo, base64Url);
270
+
271
+ const temporaryExtractionPath = path.join(
272
+ os.tmpdir(),
273
+ `opencode_patch_${Date.now()}`
274
+ );
275
+
276
+ asar.extractAll(appAsarPath, temporaryExtractionPath);
277
+
278
+ const indexHtmlPath = path.join(
279
+ temporaryExtractionPath,
280
+ "out",
281
+ "renderer",
282
+ "index.html"
283
+ );
284
+
285
+ if (!fs.existsSync(indexHtmlPath)) {
286
+ fs.rmSync(temporaryExtractionPath, { recursive: true, force: true });
287
+ throw new Error("index.html not found inside app.asar");
288
+ }
289
+
290
+ let indexHtmlContent = fs.readFileSync(indexHtmlPath, "utf8");
291
+
292
+ indexHtmlContent = indexHtmlContent.replace(
293
+ /<video id="opencode-bg-video"[^>]*><\/video>\s*/gi,
294
+ ""
295
+ );
296
+
297
+ if (isVideo) {
298
+ const rendererAssetsDir = path.join(
299
+ temporaryExtractionPath,
300
+ "out",
301
+ "renderer",
302
+ "assets"
303
+ );
304
+ if (!fs.existsSync(rendererAssetsDir)) {
305
+ fs.mkdirSync(rendererAssetsDir, { recursive: true });
306
+ }
307
+ const targetVideoPath = path.join(rendererAssetsDir, "deepseek-bg.mp4");
308
+ fs.copyFileSync(filePath, targetVideoPath);
309
+
310
+ const videoTag = `<video id="opencode-bg-video" autoplay loop muted playsinline src="./assets/deepseek-bg.mp4"></video>`;
311
+ indexHtmlContent = indexHtmlContent.replace(
312
+ /(<body[^>]*>)/i,
313
+ `$1\n ${videoTag}`
314
+ );
315
+ }
316
+
317
+ const styleBlock = `<style id="opencode-bg-correct-override">${themeCss}</style>\n</head>`;
318
+ indexHtmlContent = indexHtmlContent.replace("</head>", styleBlock);
319
+ fs.writeFileSync(indexHtmlPath, indexHtmlContent, "utf8");
320
+
321
+ await asar.createPackage(temporaryExtractionPath, appAsarPath);
322
+ fs.rmSync(temporaryExtractionPath, { recursive: true, force: true });
323
+ };
324
+
325
+ const restoreTheme = () => {
326
+ if (isOpenCodeRunning()) {
327
+ throw new Error("OPENCODE_RUNNING");
328
+ }
329
+
330
+ const openCodeBasePath = resolveOpenCodePath();
331
+ if (!openCodeBasePath) {
332
+ throw new Error("PATH_NOT_FOUND");
333
+ }
334
+
335
+ const resourcesDirectory = path.join(openCodeBasePath, "resources");
336
+ const appAsarPath = path.join(resourcesDirectory, "app.asar");
337
+ const appAsarBackupPath = path.join(resourcesDirectory, "app.asar.bak");
338
+
339
+ if (!fs.existsSync(appAsarBackupPath)) {
340
+ throw new Error("BACKUP_NOT_FOUND");
341
+ }
342
+
343
+ fs.copyFileSync(appAsarBackupPath, appAsarPath);
344
+ };
345
+
346
+ module.exports = {
347
+ isOpenCodeRunning,
348
+ resolveOpenCodePath,
349
+ encodeImageToBase64,
350
+ generateThemeCss,
351
+ installTheme,
352
+ restoreTheme
353
+ };
package/src/i18n.js ADDED
@@ -0,0 +1,84 @@
1
+ const locales = {
2
+ en: {
3
+ bannerTitle: "Cyber Theme for OpenCode Desktop",
4
+ selectPrompt: "Select a background preset:",
5
+ feisha1080: "DeepSeek Féishā (1080p)",
6
+ zipzip1080: "DeepSeek Maid Whale (ZipZip)",
7
+ midnight1080: "DeepSeek Midnight (1080p)",
8
+ static4k: "DeepSeek Artwork (4K)",
9
+ restoreOption: "Restore original OpenCode design",
10
+ exitOption: "Exit",
11
+ downloading: "Downloading asset",
12
+ downloadFallback: "Retrying using mirror accelerator...",
13
+ opencodeRunning: "OpenCode Desktop is currently running. Please close it before proceeding.",
14
+ installSuccess: "DeepSeek Cyber theme installed successfully.",
15
+ restoreSuccess: "OpenCode Desktop restored to factory state.",
16
+ pathNotFound: "OpenCode Desktop installation path not found.",
17
+ error: "Error",
18
+ changeLangOption: "Change language / 切换语言 / Cambiar idioma",
19
+ selectLangPrompt: "Select a language:",
20
+ cancelOption: "Cancel and return to main menu"
21
+ },
22
+ zh: {
23
+ bannerTitle: "OpenCode Desktop DeepSeek 赛博主题",
24
+ selectPrompt: "请选择背景预设:",
25
+ feisha1080: "DeepSeek 绯莎 (1080p)",
26
+ zipzip1080: "DeepSeek 深海女仆 (ZipZip)",
27
+ midnight1080: "DeepSeek 深夜 (1080p)",
28
+ static4k: "DeepSeek 插画 (4K)",
29
+ restoreOption: "恢复 OpenCode 原始默认样式",
30
+ exitOption: "退出",
31
+ downloading: "正在下载背景资源",
32
+ downloadFallback: "正在尝试使用国内镜像加速下载...",
33
+ opencodeRunning: "检测到 OpenCode Desktop 正在运行,请先关闭该程序再执行安装。",
34
+ installSuccess: "DeepSeek 赛博主题安装成功!",
35
+ restoreSuccess: "OpenCode Desktop 已成功恢复出厂默认状态。",
36
+ pathNotFound: "未找到 OpenCode Desktop 安装路径。",
37
+ error: "错误",
38
+ changeLangOption: "切换语言 / Change language / Cambiar idioma",
39
+ selectLangPrompt: "请选择语言:",
40
+ cancelOption: "取消并返回主菜单"
41
+ },
42
+ es: {
43
+ bannerTitle: "Tema DeepSeek Cyber para OpenCode Desktop",
44
+ selectPrompt: "Selecciona un preset de fondo:",
45
+ feisha1080: "DeepSeek Féishā (1080p)",
46
+ zipzip1080: "DeepSeek doncella marina (ZipZip)",
47
+ midnight1080: "DeepSeek medianoche (1080p)",
48
+ static4k: "DeepSeek artwork (4K)",
49
+ restoreOption: "Restaurar diseno original de fabrica",
50
+ exitOption: "Salir",
51
+ downloading: "Descargando recurso",
52
+ downloadFallback: "Reintentando mediante espejo acelerado...",
53
+ opencodeRunning: "OpenCode Desktop esta en ejecucion. Cierralo antes de continuar.",
54
+ installSuccess: "Tema DeepSeek Cyber instalado con exito.",
55
+ restoreSuccess: "OpenCode Desktop ha sido restaurado a su estado original.",
56
+ pathNotFound: "No se detecto la ruta de OpenCode Desktop.",
57
+ error: "Error",
58
+ changeLangOption: "Cambiar idioma / Switch language / 切换语言",
59
+ selectLangPrompt: "Selecciona un idioma:",
60
+ cancelOption: "Cancelar y volver al menu principal"
61
+ }
62
+ };
63
+
64
+ const detectLocale = (explicitLang) => {
65
+ if (explicitLang && typeof explicitLang === "string" && explicitLang.trim().length > 0) {
66
+ const normalized = explicitLang.trim().toLowerCase();
67
+ if (["en", "zh", "es"].includes(normalized)) {
68
+ return normalized;
69
+ }
70
+ return "en";
71
+ }
72
+
73
+ return "en";
74
+ };
75
+
76
+ const getLocaleStrings = (locale) => {
77
+ return locales[locale] || locales.en;
78
+ };
79
+
80
+ module.exports = {
81
+ locales,
82
+ detectLocale,
83
+ getLocaleStrings
84
+ };
package/src/presets.js ADDED
@@ -0,0 +1,51 @@
1
+ const presets = [
2
+ {
3
+ id: "feisha-1080p",
4
+ titleKey: "feisha1080",
5
+ type: "video",
6
+ filename: "feisha-1080p.mp4",
7
+ expectedSize: 6445227,
8
+ sha256: "cce0a170d91fb93f6d97199d5c98078b7c275928a5b852cded1dd582fd5173b5",
9
+ remoteUrl: "https://github.com/daemon1s/opencode-deepseek-chan/releases/download/v1.0.0/feisha-1080p.mp4"
10
+ },
11
+ {
12
+ id: "zipzip-1080p",
13
+ titleKey: "zipzip1080",
14
+ type: "video",
15
+ filename: "zipzip-1080p.mp4",
16
+ expectedSize: 11418597,
17
+ sha256: "4f7e026b2b7bda9b527ab261b896464197bf1fe103897c172404b8431fce2af7",
18
+ remoteUrl: "https://github.com/daemon1s/opencode-deepseek-chan/releases/download/v1.0.0/zipzip-1080p.mp4"
19
+ },
20
+ {
21
+ id: "midnight-1080p",
22
+ titleKey: "midnight1080",
23
+ type: "video",
24
+ filename: "midnight-1080p.mp4",
25
+ expectedSize: 20221147,
26
+ sha256: "f44d1e271cd2e66846b88feefe3bc5478e46c204a7d9d40810dc02c706be98c4",
27
+ remoteUrl: "https://github.com/daemon1s/opencode-deepseek-chan/releases/download/v1.0.0/midnight-1080p.mp4"
28
+ },
29
+ {
30
+ id: "static-4k",
31
+ titleKey: "static4k",
32
+ type: "image",
33
+ filename: "artwork-4k.png",
34
+ expectedSize: 9552989,
35
+ sha256: "fe13cd17bbd9aa48bb6be47cc31ed2f2ab50d74c93d58ccc9af5599322d8f133",
36
+ remoteUrl: "https://github.com/daemon1s/opencode-deepseek-chan/releases/download/v1.0.0/artwork-4k.png"
37
+ }
38
+ ];
39
+
40
+ const getPresetById = (id) => {
41
+ if (!id || typeof id !== "string") {
42
+ return null;
43
+ }
44
+ const normalizedId = id.trim().toLowerCase();
45
+ return presets.find((preset) => preset.id.toLowerCase() === normalizedId) || null;
46
+ };
47
+
48
+ module.exports = {
49
+ presets,
50
+ getPresetById
51
+ };