@duanluan/codex-plus-plus-launcher 0.1.11 → 0.1.12

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/README.md CHANGED
@@ -7,7 +7,7 @@ Windows 和 macOS 安装后会得到两个入口:
7
7
  - `Codex++`:静默启动入口,负责启动 Codex 并注入增强功能。
8
8
  - `Codex++ 管理工具`:上游 Tauri 控制面板,用于检查、修复、更新和管理增强功能。
9
9
 
10
- Linux 暂不提供上游桌面 sidecar。Linux 用户可以直接使用上游项目提供的方案,例如 Arch Linux 的 [codex-plus-plus](https://aur.archlinux.org/packages/codex-plus-plus) AUR 包。
10
+ Linux 支持需要先安装 [ilysenko/codex-desktop-linux](https://github.com/ilysenko/codex-desktop-linux)。npm 包会内置上游 Codex++ 静默 launcher,并为 `codex-desktop-linux` 生成一个 `codex.exe` shim,让 Codex++ 可以通过 `--app-path` 启动 Linux 版 Codex Desktop;Linux 暂不内置 Codex++ 管理工具。
11
11
 
12
12
  ## 安装
13
13
 
@@ -19,7 +19,7 @@ npm install -g @duanluan/codex-plus-plus-launcher
19
19
 
20
20
  - Windows:复制到 `%LOCALAPPDATA%\Programs\Codex++`,创建桌面和开始菜单的 `Codex++`、`Codex++ 管理工具` 快捷方式。
21
21
  - macOS:优先创建 `/Applications/Codex++.app` 和 `/Applications/Codex++ 管理工具.app`,无权限时退到 `~/Applications`。
22
- - Linux:命令可安装,但上游桌面 sidecar 当前标记为 unsupported。
22
+ - Linux:如果已安装 `codex-desktop-linux`,复制 `codex-plus-plus` 静默 launcher,生成 `codex.exe` shim,并创建 `~/.local/share/applications/codex-plus-plus.desktop`。如果安装 npm 包时还没装 `codex-desktop-linux`,postinstall 会跳过集成且不报错;之后运行 `cxpp install-app` 或 `cxpp repair-app` 即可补装入口。
23
23
 
24
24
  postinstall 只安装入口和快捷方式,不会自动启动 Codex++、管理工具或 Codex 进程。
25
25
 
@@ -45,6 +45,7 @@ Windows/npm 路线默认把“更新 Codex++”收敛成“更新这个 wrapper
45
45
  - `install_root`:已复制 sidecar 和入口的本机目录。
46
46
  - `silent_binary_state` / `manager_binary_state`:两个 sidecar 是否已安装。
47
47
  - `silent_entrypoint_state` / `manager_entrypoint_state`:两个桌面入口是否已安装。
48
+ - Linux 还会显示 `linux_codex_desktop_state`、`linux_codex_desktop_start` 和 `linux_codex_shim_state`;`manager_binary_state` / `manager_entrypoint_state` 在 Linux 上为 `unsupported`。
48
49
 
49
50
  ## 本地开发验证
50
51
 
@@ -54,7 +55,7 @@ Windows/npm 路线默认把“更新 Codex++”收敛成“更新这个 wrapper
54
55
  npm run build:sidecars-local
55
56
  ```
56
57
 
57
- 发布包验证要求 `upstream-bin/win32-x64`、`upstream-bin/darwin-x64`、`upstream-bin/darwin-arm64` 都包含上游二进制、图标和 `upstream-release.json`:
58
+ 发布包验证要求 `upstream-bin/win32-x64`、`upstream-bin/darwin-x64`、`upstream-bin/darwin-arm64` 都包含上游二进制和图标,`upstream-bin/linux-x64` 包含上游静默 launcher,并包含 `upstream-release.json`:
58
59
 
59
60
  ```bash
60
61
  npm run prepack
package/README_EN.md CHANGED
@@ -7,7 +7,7 @@ Windows and macOS installs provide two entry points:
7
7
  - `Codex++`: the silent launcher entry.
8
8
  - `Codex++ Manager`: the upstream Tauri control panel for checks, repair, updates, and enhancement management.
9
9
 
10
- Linux does not ship the upstream desktop sidecar in this npm package yet. Linux users should use the upstream project distribution path where available, such as the Arch Linux [codex-plus-plus](https://aur.archlinux.org/packages/codex-plus-plus) AUR package.
10
+ Linux support requires [ilysenko/codex-desktop-linux](https://github.com/ilysenko/codex-desktop-linux) to be installed first. The npm package bundles the upstream Codex++ silent launcher and generates a `codex.exe` shim for `codex-desktop-linux`, so Codex++ can launch the Linux Codex Desktop through `--app-path`; Codex++ Manager is not bundled on Linux yet.
11
11
 
12
12
  ## Install
13
13
 
@@ -19,7 +19,7 @@ The npm install adds the `cxpp`/`codexpp` commands and copies the bundled upstre
19
19
 
20
20
  - Windows: copies to `%LOCALAPPDATA%\Programs\Codex++` and creates Desktop and Start Menu shortcuts for `Codex++` and `Codex++ Manager`.
21
21
  - macOS: creates `/Applications/Codex++.app` and `/Applications/Codex++ 管理工具.app`, falling back to `~/Applications` if `/Applications` is not writable.
22
- - Linux: commands can be installed, but upstream desktop sidecar mode is reported as unsupported.
22
+ - Linux: if `codex-desktop-linux` is installed, copies the `codex-plus-plus` silent launcher, generates a `codex.exe` shim, and creates `~/.local/share/applications/codex-plus-plus.desktop`. If `codex-desktop-linux` is missing during npm install, postinstall skips integration without failing; run `cxpp install-app` or `cxpp repair-app` after installing it.
23
23
 
24
24
  The postinstall step only installs entry points and shortcuts. It does not auto-launch Codex++, the manager UI, or any Codex process.
25
25
 
@@ -45,6 +45,7 @@ Important `doctor --json` fields:
45
45
  - `install_root`: the local sidecar and entrypoint install directory.
46
46
  - `silent_binary_state` / `manager_binary_state`: whether each sidecar is installed.
47
47
  - `silent_entrypoint_state` / `manager_entrypoint_state`: whether each desktop entry point is installed.
48
+ - Linux also reports `linux_codex_desktop_state`, `linux_codex_desktop_start`, and `linux_codex_shim_state`; `manager_binary_state` / `manager_entrypoint_state` are `unsupported` on Linux.
48
49
 
49
50
  ## Local Verification
50
51
 
@@ -54,7 +55,7 @@ Build the upstream sidecars for the current platform:
54
55
  npm run build:sidecars-local
55
56
  ```
56
57
 
57
- Package verification expects `upstream-bin/win32-x64`, `upstream-bin/darwin-x64`, and `upstream-bin/darwin-arm64` to contain the upstream binaries, icons, and `upstream-release.json`:
58
+ Package verification expects `upstream-bin/win32-x64`, `upstream-bin/darwin-x64`, and `upstream-bin/darwin-arm64` to contain the upstream binaries and icons, `upstream-bin/linux-x64` to contain the upstream silent launcher, and `upstream-release.json` to be present:
58
59
 
59
60
  ```bash
60
61
  npm run prepack
@@ -1 +1 @@
1
- __version__ = "0.1.11"
1
+ __version__ = "0.1.12"
@@ -27,6 +27,9 @@ function buildTargetArgs(key) {
27
27
  if (key === 'darwin-arm64') {
28
28
  return ['--target', 'aarch64-apple-darwin'];
29
29
  }
30
+ if (key === 'linux-x64') {
31
+ return [];
32
+ }
30
33
  return [];
31
34
  }
32
35
 
@@ -35,6 +38,10 @@ function cargoTargetForKey(key) {
35
38
  return args.length ? args[1] : '';
36
39
  }
37
40
 
41
+ function packageBuildArgs(key) {
42
+ return key === 'linux-x64' ? ['-p', 'codex-plus-launcher'] : [];
43
+ }
44
+
38
45
  function main() {
39
46
  const root = packageRoot();
40
47
  const upstreamDir = process.env.CODEXPP_UPSTREAM_DIR || path.join(os.tmpdir(), 'CodexPlusPlus-upstream-build');
@@ -49,20 +56,27 @@ function main() {
49
56
  run('git', ['checkout', 'FETCH_HEAD'], { cwd: upstreamDir });
50
57
  }
51
58
 
52
- run('npm', ['install', '--package-lock=false'], { cwd: path.join(upstreamDir, 'apps', 'codex-plus-manager'), shell: process.platform === 'win32' });
53
- run('npm', ['run', 'vite:build'], { cwd: path.join(upstreamDir, 'apps', 'codex-plus-manager'), shell: process.platform === 'win32' });
54
- run('cargo', ['build', '--release', ...buildTargetArgs(key)], { cwd: upstreamDir });
59
+ if (key !== 'linux-x64') {
60
+ run('npm', ['install', '--package-lock=false'], { cwd: path.join(upstreamDir, 'apps', 'codex-plus-manager'), shell: process.platform === 'win32' });
61
+ run('npm', ['run', 'vite:build'], { cwd: path.join(upstreamDir, 'apps', 'codex-plus-manager'), shell: process.platform === 'win32' });
62
+ }
63
+ run('cargo', ['build', '--release', ...buildTargetArgs(key), ...packageBuildArgs(key)], { cwd: upstreamDir });
55
64
 
56
65
  const cargoTarget = cargoTargetForKey(key);
57
66
  const targetDir = path.join(upstreamDir, 'target', ...(cargoTarget ? [cargoTarget] : []), 'release');
58
67
  fs.mkdirSync(outDir, { recursive: true });
59
68
  const exe = key.startsWith('win32') ? '.exe' : '';
60
69
  fs.copyFileSync(path.join(targetDir, `codex-plus-plus${exe}`), path.join(outDir, `codex-plus-plus${exe}`));
61
- fs.copyFileSync(path.join(targetDir, `codex-plus-plus-manager${exe}`), path.join(outDir, `codex-plus-plus-manager${exe}`));
70
+ const managerSource = path.join(targetDir, `codex-plus-plus-manager${exe}`);
71
+ if (fs.existsSync(managerSource)) {
72
+ fs.copyFileSync(managerSource, path.join(outDir, `codex-plus-plus-manager${exe}`));
73
+ }
62
74
  const iconSource = key.startsWith('darwin')
63
75
  ? path.join(upstreamDir, 'apps', 'codex-plus-manager', 'src-tauri', 'icons', 'icon.png')
64
76
  : path.join(upstreamDir, 'apps', 'codex-plus-manager', 'src-tauri', 'icons', 'icon.ico');
65
- fs.copyFileSync(iconSource, path.join(outDir, key.startsWith('darwin') ? 'codex-plus-plus.png' : 'codex-plus-plus.ico'));
77
+ if (fs.existsSync(iconSource)) {
78
+ fs.copyFileSync(iconSource, path.join(outDir, key.startsWith('darwin') ? 'codex-plus-plus.png' : 'codex-plus-plus.ico'));
79
+ }
66
80
 
67
81
  const cargoToml = fs.readFileSync(path.join(upstreamDir, 'Cargo.toml'), 'utf8');
68
82
  const versionMatch = cargoToml.match(/^\s*version\s*=\s*"([^"]+)"/m);
@@ -86,4 +100,4 @@ if (require.main === module) {
86
100
  }
87
101
  }
88
102
 
89
- module.exports = { buildTargetArgs, cargoTargetForKey, main, platformKey };
103
+ module.exports = { buildTargetArgs, cargoTargetForKey, main, packageBuildArgs, platformKey };
package/npm/i18n.js CHANGED
@@ -3,8 +3,10 @@ const messages = {
3
3
  missingPython: '缺少命令:python3、python 或 py',
4
4
  missingBinary: '缺少随 npm 包安装的 cxpp 二进制程序',
5
5
  missingSidecar: '缺少随 npm 包内置的 Codex++ 上游程序',
6
+ missingLinuxCodexDesktop: '未找到 ilysenko/codex-desktop-linux 安装。请先安装 codex-desktop-linux,或设置 CODEXPP_LINUX_CODEX_START 指向 start.sh',
6
7
  missingPip: '当前 Python 缺少 pip,请先安装 pip',
7
8
  unsupportedPlatform: '当前 Codex++ 桌面版 npm 包暂不支持该平台',
9
+ unsupportedLinuxManager: 'Linux 暂不提供 Codex++ 管理工具;请使用 cxpp launch 启动 Codex++',
8
10
  installingWrapper: '正在安装 codex-plus-plus-launcher',
9
11
  installingCodexPlusPlus: '正在安装 Codex++',
10
12
  installDone: '安装完成',
@@ -21,8 +23,10 @@ const messages = {
21
23
  missingPython: 'missing command: python3, python, or py',
22
24
  missingBinary: 'missing bundled cxpp binary installed by the npm package',
23
25
  missingSidecar: 'missing bundled upstream Codex++ program installed by the npm package',
26
+ missingLinuxCodexDesktop: 'could not find an ilysenko/codex-desktop-linux install; install codex-desktop-linux first or set CODEXPP_LINUX_CODEX_START to start.sh',
24
27
  missingPip: 'pip is not available for the selected Python runtime',
25
28
  unsupportedPlatform: 'the bundled Codex++ desktop npm package does not support this platform yet',
29
+ unsupportedLinuxManager: 'Codex++ Manager is not bundled for Linux yet; use cxpp launch to start Codex++',
26
30
  installingWrapper: 'installing codex-plus-plus-launcher',
27
31
  installingCodexPlusPlus: 'installing Codex++',
28
32
  installDone: 'installation completed',
package/npm/launcher.js CHANGED
@@ -5,11 +5,14 @@ const readline = require('node:readline');
5
5
  const { spawn, spawnSync } = require('node:child_process');
6
6
  const { t } = require('./i18n.js');
7
7
 
8
- const SUPPORTED_PLATFORMS = new Set(['win32-x64', 'darwin-x64', 'darwin-arm64']);
8
+ const SUPPORTED_PLATFORMS = new Set(['win32-x64', 'darwin-x64', 'darwin-arm64', 'linux-x64']);
9
9
  const SILENT_BINARY = 'codex-plus-plus';
10
10
  const MANAGER_BINARY = 'codex-plus-plus-manager';
11
11
  const SILENT_NAME = 'Codex++';
12
12
  const MANAGER_NAME = 'Codex++ 管理工具';
13
+ const LINUX_SHIM_DIR_NAME = 'codex-desktop-linux-shim';
14
+ const LINUX_SHIM_BINARY = 'codex.exe';
15
+ const LINUX_DESKTOP_ENTRY = 'codex-plus-plus.desktop';
13
16
 
14
17
  function optionValue(options, key, fallback) {
15
18
  const value = options[key];
@@ -39,6 +42,15 @@ function executableName(name, platform = process.platform) {
39
42
  return platform === 'win32' ? `${name}.exe` : name;
40
43
  }
41
44
 
45
+ function requiredSidecarKinds(options = {}) {
46
+ const platform = optionValue(options, 'platform', process.platform);
47
+ return platform === 'linux' ? ['silent'] : ['silent', 'manager'];
48
+ }
49
+
50
+ function sidecarKindSupported(kind, options = {}) {
51
+ return requiredSidecarKinds(options).includes(kind);
52
+ }
53
+
42
54
  function upstreamBinDir(options = {}) {
43
55
  const root = optionValue(options, 'packageRoot', packageRoot);
44
56
  const platform = optionValue(options, 'platform', process.platform);
@@ -107,7 +119,7 @@ function assertSupportedPlatform(options = {}) {
107
119
  function assertBundledSidecars(options = {}) {
108
120
  const fsImpl = options.fs || fs;
109
121
  assertSupportedPlatform(options);
110
- const missing = [bundledSidecarPath('silent', options), bundledSidecarPath('manager', options)].filter((candidate) => !fsImpl.existsSync(candidate));
122
+ const missing = requiredSidecarKinds(options).map((kind) => bundledSidecarPath(kind, options)).filter((candidate) => !fsImpl.existsSync(candidate));
111
123
  if (missing.length > 0) {
112
124
  const error = new Error(`${t('missingSidecar', options.env || process.env)}: ${missing.join(', ')}`);
113
125
  error.code = 'CODEXPP_MISSING_SIDECAR';
@@ -136,6 +148,16 @@ function fallbackMacInstallRoot(options = {}) {
136
148
  return optionValue(options, 'fallbackInstallRoot', () => path.join(optionValue(options, 'homeDir', () => os.homedir()), 'Applications'));
137
149
  }
138
150
 
151
+ function xdgDataHome(options = {}) {
152
+ const env = options.env || process.env;
153
+ return env.XDG_DATA_HOME || path.join(optionValue(options, 'homeDir', () => os.homedir()), '.local', 'share');
154
+ }
155
+
156
+ function xdgStateHome(options = {}) {
157
+ const env = options.env || process.env;
158
+ return env.XDG_STATE_HOME || path.join(optionValue(options, 'homeDir', () => os.homedir()), '.local', 'state');
159
+ }
160
+
139
161
  function installedSidecarPath(kind, options = {}) {
140
162
  const platform = optionValue(options, 'platform', process.platform);
141
163
  const root = optionValue(options, 'installRoot', () => detectedInstallRoot(options));
@@ -162,6 +184,18 @@ function detectedInstallRoot(options = {}) {
162
184
  return requested;
163
185
  }
164
186
 
187
+ function linuxShimRoot(options = {}) {
188
+ return optionValue(options, 'linuxShimRoot', () => path.join(xdgDataHome(options), 'Codex++', LINUX_SHIM_DIR_NAME));
189
+ }
190
+
191
+ function linuxEntrypointPath(options = {}) {
192
+ return optionValue(options, 'linuxEntrypointPath', () => path.join(xdgDataHome(options), 'applications', LINUX_DESKTOP_ENTRY));
193
+ }
194
+
195
+ function linuxInstallStatePath(options = {}) {
196
+ return optionValue(options, 'linuxInstallStatePath', () => path.join(xdgStateHome(options), 'codex-plus-plus-launcher', 'linux-install.json'));
197
+ }
198
+
165
199
  function isPermissionError(error) {
166
200
  return error && (error.code === 'EPERM' || error.code === 'EACCES');
167
201
  }
@@ -425,7 +459,7 @@ async function installSidecars(options = {}) {
425
459
  const installRoot = installSidecarRoot(options);
426
460
  fsImpl.mkdirSync(installRoot, { recursive: true });
427
461
  const installed = {};
428
- for (const kind of ['silent', 'manager']) {
462
+ for (const kind of requiredSidecarKinds(options)) {
429
463
  const source = bundledSidecarPath(kind, options);
430
464
  const target = installedSidecarPath(kind, { ...options, installRoot });
431
465
  await copyReplacingChangedFile(source, target, { ...options, target });
@@ -441,6 +475,293 @@ async function installSidecars(options = {}) {
441
475
  return installed;
442
476
  }
443
477
 
478
+ function normalizeExecutablePath(candidate) {
479
+ return path.resolve(String(candidate || ''));
480
+ }
481
+
482
+ function linuxStartScriptFromBinary(binaryPath, options = {}) {
483
+ const fsImpl = options.fs || fs;
484
+ const normalized = normalizeExecutablePath(binaryPath);
485
+ const basename = path.basename(normalized);
486
+ const parent = path.dirname(normalized);
487
+ const candidates = [];
488
+
489
+ if (basename === 'start.sh') {
490
+ candidates.push(normalized);
491
+ }
492
+ candidates.push(path.join(parent, 'start.sh'));
493
+ if (basename === 'codex-desktop') {
494
+ candidates.push('/opt/codex-desktop/start.sh');
495
+ candidates.push(path.join(parent, '..', 'opt', 'codex-desktop', 'start.sh'));
496
+ candidates.push(path.join(parent, '..', 'opt', 'codex-desktop-linux', 'lib', 'codex-desktop-linux', 'codex-app', 'start.sh'));
497
+ }
498
+ if (normalized.includes(`${path.sep}codex-desktop-linux${path.sep}`) || normalized.includes('/codex-desktop-linux/')) {
499
+ candidates.push(path.join(parent, '..', 'lib', 'codex-desktop-linux', 'codex-app', 'start.sh'));
500
+ candidates.push(path.join(parent, '..', 'codex-app', 'start.sh'));
501
+ }
502
+
503
+ for (const candidate of candidates) {
504
+ const resolved = path.resolve(candidate);
505
+ if (fsImpl.existsSync(resolved)) {
506
+ return resolved;
507
+ }
508
+ }
509
+ return null;
510
+ }
511
+
512
+ function linuxStartScriptCandidates(options = {}) {
513
+ const env = options.env || process.env;
514
+ const homeDir = optionValue(options, 'homeDir', () => os.homedir());
515
+ const candidates = [];
516
+ for (const value of [env.CODEXPP_LINUX_CODEX_START, env.CODEX_DESKTOP_LINUX_START, env.CODEX_DESKTOP_START]) {
517
+ if (value) {
518
+ candidates.push(value);
519
+ }
520
+ }
521
+ for (const value of [env.CODEXPP_LINUX_APPDIR, env.CODEX_DESKTOP_LINUX_APPDIR, env.APPDIR]) {
522
+ if (value) {
523
+ candidates.push(path.join(value, 'opt', 'codex-desktop', 'start.sh'));
524
+ }
525
+ }
526
+ for (const value of [env.CODEXPP_LINUX_CODEX_BIN, env.CODEX_DESKTOP_LINUX_BIN]) {
527
+ if (value) {
528
+ const start = linuxStartScriptFromBinary(value, options);
529
+ if (start) {
530
+ candidates.push(start);
531
+ }
532
+ }
533
+ }
534
+ const which = options.which || ((name) => {
535
+ const result = (options.spawnSync || spawnSync)('which', [name], { encoding: 'utf8' });
536
+ return result.status === 0 ? String(result.stdout || '').trim().split(/\r?\n/)[0] : '';
537
+ });
538
+ for (const name of ['codex-desktop', 'openai-codex-desktop']) {
539
+ try {
540
+ const found = which(name);
541
+ if (found) {
542
+ const start = linuxStartScriptFromBinary(found, options);
543
+ if (start) {
544
+ candidates.push(start);
545
+ }
546
+ }
547
+ } catch (_error) {}
548
+ }
549
+ candidates.push('/opt/codex-desktop/start.sh');
550
+ candidates.push(path.join(homeDir, '.local', 'opt', 'codex-desktop-linux', 'lib', 'codex-desktop-linux', 'codex-app', 'start.sh'));
551
+ candidates.push(path.join(homeDir, 'codex-desktop-linux', 'codex-app', 'start.sh'));
552
+
553
+ const unique = [];
554
+ const seen = new Set();
555
+ for (const candidate of candidates) {
556
+ if (!candidate) {
557
+ continue;
558
+ }
559
+ const resolved = path.resolve(candidate);
560
+ if (!seen.has(resolved)) {
561
+ seen.add(resolved);
562
+ unique.push(resolved);
563
+ }
564
+ }
565
+ return unique;
566
+ }
567
+
568
+ function detectLinuxCodexDesktop(options = {}) {
569
+ const fsImpl = options.fs || fs;
570
+ for (const startScript of linuxStartScriptCandidates(options)) {
571
+ if (fsImpl.existsSync(startScript)) {
572
+ const appRoot = path.dirname(startScript);
573
+ return {
574
+ kind: 'codex-desktop-linux',
575
+ startScript,
576
+ appRoot,
577
+ resourcesDir: path.join(appRoot, 'resources'),
578
+ };
579
+ }
580
+ }
581
+ return null;
582
+ }
583
+
584
+ function shellSingleQuote(value) {
585
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
586
+ }
587
+
588
+ function linuxCodexShimScript(startScript) {
589
+ return [
590
+ '#!/usr/bin/env bash',
591
+ 'set -euo pipefail',
592
+ `exec ${shellSingleQuote(startScript)} -- "$@"`,
593
+ '',
594
+ ].join('\n');
595
+ }
596
+
597
+ function writeLinuxCodexShim(installation, options = {}) {
598
+ const fsImpl = options.fs || fs;
599
+ const shimRoot = linuxShimRoot(options);
600
+ fsImpl.mkdirSync(shimRoot, { recursive: true });
601
+ const shimPath = path.join(shimRoot, LINUX_SHIM_BINARY);
602
+ fsImpl.writeFileSync(shimPath, linuxCodexShimScript(installation.startScript), 'utf8');
603
+ fsImpl.chmodSync(shimPath, 0o755);
604
+ return { shimRoot, shimPath };
605
+ }
606
+
607
+ function linuxDesktopEntry({ name, execPath, iconPath, comment }) {
608
+ const iconLine = iconPath ? `Icon=${iconPath}\n` : '';
609
+ return `[Desktop Entry]
610
+ Name=${name}
611
+ Comment=${comment}
612
+ Exec=${execPath}
613
+ ${iconLine}Terminal=false
614
+ Type=Application
615
+ Categories=Development;
616
+ StartupNotify=true
617
+ `;
618
+ }
619
+
620
+ function installLinuxEntrypoints(installed, options = {}) {
621
+ const fsImpl = options.fs || fs;
622
+ const desktopPath = linuxEntrypointPath(options);
623
+ fsImpl.mkdirSync(path.dirname(desktopPath), { recursive: true });
624
+ const command = `${desktopExecArg(installed.silent)} --app-path ${desktopExecArg(installed.linuxShimRoot)}`;
625
+ fsImpl.writeFileSync(
626
+ desktopPath,
627
+ linuxDesktopEntry({
628
+ name: SILENT_NAME,
629
+ execPath: command,
630
+ iconPath: installed.icon || '',
631
+ comment: 'Launch Codex++ with Codex Desktop for Linux',
632
+ }),
633
+ 'utf8',
634
+ );
635
+ return {
636
+ silent: desktopPath,
637
+ manager: 'unsupported',
638
+ };
639
+ }
640
+
641
+ function desktopExecArg(value) {
642
+ const raw = String(value);
643
+ if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(raw)) {
644
+ return raw.replace(/%/g, '%%');
645
+ }
646
+ return `"${raw.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$').replace(/%/g, '%%')}"`;
647
+ }
648
+
649
+ function linuxShellArg(value) {
650
+ const raw = String(value);
651
+ if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(raw)) {
652
+ return raw;
653
+ }
654
+ return shellSingleQuote(raw);
655
+ }
656
+
657
+ function linuxHasExplicitAppPath(args) {
658
+ for (let index = 0; index < args.length; index += 1) {
659
+ if (args[index] === '--app-path' && args[index + 1]) {
660
+ return true;
661
+ }
662
+ }
663
+ return false;
664
+ }
665
+
666
+ function linuxShimPathFromRoot(root) {
667
+ return path.join(root, LINUX_SHIM_BINARY);
668
+ }
669
+
670
+ function linuxStateShimRoots(state) {
671
+ const roots = [];
672
+ if (state && state.codex_shim_root) {
673
+ roots.push(state.codex_shim_root);
674
+ }
675
+ if (state && state.codex_shim) {
676
+ roots.push(path.dirname(state.codex_shim));
677
+ }
678
+ return roots;
679
+ }
680
+
681
+ function ensureLinuxCodexShimRoot(options = {}) {
682
+ const fsImpl = options.fs || fs;
683
+ const state = readJsonFile(linuxInstallStatePath(options), fsImpl) || {};
684
+ const roots = [...linuxStateShimRoots(state), linuxShimRoot(options)];
685
+ const seen = new Set();
686
+ for (const root of roots) {
687
+ if (!root || seen.has(root)) {
688
+ continue;
689
+ }
690
+ seen.add(root);
691
+ if (fsImpl.existsSync(linuxShimPathFromRoot(root))) {
692
+ return root;
693
+ }
694
+ }
695
+
696
+ const installation = detectLinuxCodexDesktop(options);
697
+ if (!installation) {
698
+ return null;
699
+ }
700
+ const shim = writeLinuxCodexShim(installation, options);
701
+ const installedSilent = installedSidecarPath('silent', options);
702
+ const silent = fsImpl.existsSync(installedSilent) ? installedSilent : bundledSidecarPath('silent', options);
703
+ writeLinuxInstallState(
704
+ {
705
+ silent,
706
+ installRoot: path.dirname(silent),
707
+ linuxShim: shim.shimPath,
708
+ linuxShimRoot: shim.shimRoot,
709
+ },
710
+ installation,
711
+ options,
712
+ );
713
+ return shim.shimRoot;
714
+ }
715
+
716
+ function linuxSilentLaunchArgs(args, options = {}) {
717
+ if (linuxHasExplicitAppPath(args)) {
718
+ return args;
719
+ }
720
+ const shimRoot = ensureLinuxCodexShimRoot(options);
721
+ if (!shimRoot) {
722
+ const error = new Error(t('missingLinuxCodexDesktop', options.env || process.env));
723
+ error.code = 'CODEXPP_MISSING_LINUX_CODEX_DESKTOP';
724
+ throw error;
725
+ }
726
+ return ['--app-path', shimRoot, ...args];
727
+ }
728
+
729
+ function writeLinuxInstallState(installed, installation, options = {}) {
730
+ const fsImpl = options.fs || fs;
731
+ const statePath = linuxInstallStatePath(options);
732
+ fsImpl.mkdirSync(path.dirname(statePath), { recursive: true });
733
+ const payload = {
734
+ mode: 'codex_desktop_linux',
735
+ app_integration_state: 'installed',
736
+ codex_desktop_linux_start: installation.startScript,
737
+ codex_desktop_linux_app_root: installation.appRoot,
738
+ codex_shim: installed.linuxShim,
739
+ codex_shim_root: installed.linuxShimRoot,
740
+ silent_binary: installed.silent,
741
+ upstream_version: bundledUpstreamVersion(options) || null,
742
+ installed_at: new Date().toISOString(),
743
+ };
744
+ fsImpl.writeFileSync(statePath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
745
+ return statePath;
746
+ }
747
+
748
+ async function installLinuxApp(options = {}) {
749
+ const installation = detectLinuxCodexDesktop(options);
750
+ if (!installation) {
751
+ const error = new Error(t('missingLinuxCodexDesktop', options.env || process.env));
752
+ error.code = 'CODEXPP_MISSING_LINUX_CODEX_DESKTOP';
753
+ throw error;
754
+ }
755
+ const installed = await installSidecars(options);
756
+ const shim = writeLinuxCodexShim(installation, options);
757
+ installed.linuxShim = shim.shimPath;
758
+ installed.linuxShimRoot = shim.shimRoot;
759
+ installed.linuxCodexStart = installation.startScript;
760
+ const entrypoints = await installEntrypoints(installed, { ...options, installRoot: installed.installRoot });
761
+ const statePath = writeLinuxInstallState(installed, installation, options);
762
+ return { installed, entrypoints, linux: installation, statePath };
763
+ }
764
+
444
765
  function installSidecarRoot(options = {}) {
445
766
  const platform = optionValue(options, 'platform', process.platform);
446
767
  const fsImpl = options.fs || fs;
@@ -606,10 +927,17 @@ async function installEntrypoints(installed, options = {}) {
606
927
  if (platform === 'darwin') {
607
928
  return installMacEntrypoints(installed, options);
608
929
  }
930
+ if (platform === 'linux') {
931
+ return installLinuxEntrypoints(installed, options);
932
+ }
609
933
  return { unsupported: true };
610
934
  }
611
935
 
612
936
  async function installApp(options = {}) {
937
+ const platform = optionValue(options, 'platform', process.platform);
938
+ if (platform === 'linux') {
939
+ return installLinuxApp(options);
940
+ }
613
941
  const installed = await installSidecars(options);
614
942
  const entrypoints = await installEntrypoints(installed, { ...options, installRoot: installed.installRoot });
615
943
  return { installed, entrypoints };
@@ -641,6 +969,15 @@ function inspectEntrypoints(options = {}) {
641
969
  manager_path: fsImpl.existsSync(managerPrimary) ? managerPrimary : managerFallback,
642
970
  };
643
971
  }
972
+ if (platform === 'linux') {
973
+ const desktopPath = linuxEntrypointPath(options);
974
+ return {
975
+ silent: fsImpl.existsSync(desktopPath) ? 'installed' : 'missing',
976
+ manager: 'unsupported',
977
+ silent_path: desktopPath,
978
+ manager_path: '',
979
+ };
980
+ }
644
981
  return { silent: 'unsupported', manager: 'unsupported' };
645
982
  }
646
983
 
@@ -648,11 +985,12 @@ function installedSidecars(options = {}) {
648
985
  const fsImpl = options.fs || fs;
649
986
  const silent = installedSidecarPath('silent', options);
650
987
  const manager = installedSidecarPath('manager', options);
988
+ const managerSupported = sidecarKindSupported('manager', options);
651
989
  return {
652
990
  silent,
653
991
  manager,
654
992
  silent_state: fsImpl.existsSync(silent) ? 'installed' : 'missing',
655
- manager_state: fsImpl.existsSync(manager) ? 'installed' : 'missing',
993
+ manager_state: managerSupported ? (fsImpl.existsSync(manager) ? 'installed' : 'missing') : 'unsupported',
656
994
  };
657
995
  }
658
996
 
@@ -663,7 +1001,7 @@ function doctorReport(options = {}) {
663
1001
  const sidecars = installedSidecars(options);
664
1002
  const entrypoints = inspectEntrypoints(options);
665
1003
  const supported = SUPPORTED_PLATFORMS.has(platformKey(platform, arch));
666
- return {
1004
+ const report = {
667
1005
  platform,
668
1006
  arch,
669
1007
  supported: supported ? 'yes' : 'no',
@@ -681,6 +1019,16 @@ function doctorReport(options = {}) {
681
1019
  silent_entrypoint: entrypoints.silent_path || '',
682
1020
  manager_entrypoint: entrypoints.manager_path || '',
683
1021
  };
1022
+ if (platform === 'linux') {
1023
+ const linux = detectLinuxCodexDesktop(options);
1024
+ const state = readJsonFile(linuxInstallStatePath(options), options.fs || fs) || {};
1025
+ report.linux_codex_desktop_state = linux ? 'found' : 'missing';
1026
+ report.linux_codex_desktop_start = (linux && linux.startScript) || state.codex_desktop_linux_start || '';
1027
+ report.linux_codex_shim = state.codex_shim || path.join(linuxShimRoot(options), LINUX_SHIM_BINARY);
1028
+ report.linux_codex_shim_state = (options.fs || fs).existsSync(report.linux_codex_shim) ? 'installed' : 'missing';
1029
+ report.install_mode = state.mode || (linux ? 'codex_desktop_linux' : 'unsupported');
1030
+ }
1031
+ return report;
684
1032
  }
685
1033
 
686
1034
  function printDoctor(jsonMode, options = {}) {
@@ -698,6 +1046,16 @@ function printDoctor(jsonMode, options = {}) {
698
1046
  function spawnSidecar(kind, args = [], options = {}) {
699
1047
  assertSupportedPlatform(options);
700
1048
  const platform = optionValue(options, 'platform', process.platform);
1049
+ if (platform === 'linux' && kind === 'manager') {
1050
+ return { status: 1, error: new Error(t('unsupportedLinuxManager', options.env || process.env)) };
1051
+ }
1052
+ if (platform === 'linux' && kind === 'silent') {
1053
+ try {
1054
+ args = linuxSilentLaunchArgs(args, options);
1055
+ } catch (error) {
1056
+ return { status: 1, error };
1057
+ }
1058
+ }
701
1059
  const cwd = options.cwd || os.homedir();
702
1060
  const installed = installedSidecarPath(kind, options);
703
1061
  const bundled = bundledSidecarPath(kind, options);
@@ -785,12 +1143,26 @@ async function runLauncher(args = [], options = {}) {
785
1143
  }
786
1144
  return { status: 1, error: new Error(message) };
787
1145
  }
788
- const result = await installApp(options);
1146
+ let result;
1147
+ try {
1148
+ result = await installApp(options);
1149
+ } catch (error) {
1150
+ if (command === 'npm-postinstall' && error && error.code === 'CODEXPP_MISSING_LINUX_CODEX_DESKTOP') {
1151
+ console.log(`install_mode=unsupported`);
1152
+ console.log(`message=${error.message || String(error)}`);
1153
+ return { status: 0 };
1154
+ }
1155
+ throw error;
1156
+ }
789
1157
  console.log(`install_mode=sidecar`);
790
1158
  console.log(`upstream_version=${bundledUpstreamVersion(options) || 'missing'}`);
791
1159
  console.log(`install_root=${result.installed.installRoot}`);
792
1160
  console.log(`silent_binary=${result.installed.silent}`);
793
- console.log(`manager_binary=${result.installed.manager}`);
1161
+ console.log(`manager_binary=${result.installed.manager || 'unsupported'}`);
1162
+ if (result.installed.linuxShim) {
1163
+ console.log(`linux_codex_shim=${result.installed.linuxShim}`);
1164
+ console.log(`linux_codex_desktop_start=${result.installed.linuxCodexStart}`);
1165
+ }
794
1166
  console.log(`entrypoints=${result.entrypoints.skipped ? 'skipped' : 'installed'}`);
795
1167
  return { status: 0 };
796
1168
  }
@@ -7,6 +7,7 @@ const REQUIRED_PLATFORMS = {
7
7
  'win32-x64': ['codex-plus-plus.exe', 'codex-plus-plus-manager.exe'],
8
8
  'darwin-x64': ['codex-plus-plus', 'codex-plus-plus-manager'],
9
9
  'darwin-arm64': ['codex-plus-plus', 'codex-plus-plus-manager'],
10
+ 'linux-x64': ['codex-plus-plus'],
10
11
  };
11
12
 
12
13
  const REQUIRED_ICONS = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duanluan/codex-plus-plus-launcher",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Install and launch Codex++ from npm",
5
5
  "bin": {
6
6
  "cxpp": "npm/cxpp.js",
@@ -1,16 +1,16 @@
1
- {
2
- "version": "v1.1.5",
3
- "html_url": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/tag/v1.1.5",
4
- "asset_name": "CodexPlusPlus-1.1.5-macos-arm64.dmg",
5
- "asset_url": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/download/v1.1.5/CodexPlusPlus-1.1.5-macos-arm64.dmg",
6
- "windows_asset_name": "CodexPlusPlus-1.1.5-windows-x64-setup.exe",
7
- "windows_asset_url": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/download/v1.1.5/CodexPlusPlus-1.1.5-windows-x64-setup.exe",
8
- "macos_x64_asset_name": "CodexPlusPlus-1.1.5-macos-x64.dmg",
9
- "macos_x64_asset_url": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/download/v1.1.5/CodexPlusPlus-1.1.5-macos-x64.dmg",
10
- "macos_arm64_asset_name": "CodexPlusPlus-1.1.5-macos-arm64.dmg",
11
- "macos_arm64_asset_url": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/download/v1.1.5/CodexPlusPlus-1.1.5-macos-arm64.dmg",
12
- "source_zip_url": "https://github.com/BigPizzaV3/CodexPlusPlus/archive/refs/tags/v1.1.5.zip",
13
- "install_spec": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/download/v1.1.5/CodexPlusPlus-1.1.5-macos-arm64.dmg",
14
- "commit": "f6b69a3692e2833e29909c14164d4f6d309363b3",
15
- "repository": "BigPizzaV3/CodexPlusPlus"
16
- }
1
+ {
2
+ "version": "v1.1.5",
3
+ "html_url": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/tag/v1.1.5",
4
+ "asset_name": "CodexPlusPlus-1.1.5-macos-arm64.dmg",
5
+ "asset_url": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/download/v1.1.5/CodexPlusPlus-1.1.5-macos-arm64.dmg",
6
+ "windows_asset_name": "CodexPlusPlus-1.1.5-windows-x64-setup.exe",
7
+ "windows_asset_url": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/download/v1.1.5/CodexPlusPlus-1.1.5-windows-x64-setup.exe",
8
+ "macos_x64_asset_name": "CodexPlusPlus-1.1.5-macos-x64.dmg",
9
+ "macos_x64_asset_url": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/download/v1.1.5/CodexPlusPlus-1.1.5-macos-x64.dmg",
10
+ "macos_arm64_asset_name": "CodexPlusPlus-1.1.5-macos-arm64.dmg",
11
+ "macos_arm64_asset_url": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/download/v1.1.5/CodexPlusPlus-1.1.5-macos-arm64.dmg",
12
+ "source_zip_url": "https://github.com/BigPizzaV3/CodexPlusPlus/archive/refs/tags/v1.1.5.zip",
13
+ "install_spec": "https://github.com/BigPizzaV3/CodexPlusPlus/releases/download/v1.1.5/CodexPlusPlus-1.1.5-macos-arm64.dmg",
14
+ "commit": "f6b69a3692e2833e29909c14164d4f6d309363b3",
15
+ "repository": "BigPizzaV3/CodexPlusPlus"
16
+ }