@ailuntz/minilink 0.9.7 → 0.9.8
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 +7 -8
- package/bin/minilink.mjs +2 -2
- package/package.json +2 -3
- package/src/config.mjs +35 -4
- package/src/install.mjs +94 -3
- package/src/lifecycle.mjs +4 -6
- package/src/platform/windows/MiniLinkLauncher.cs +50 -0
- package/src/update.mjs +2 -1
package/README.md
CHANGED
|
@@ -15,18 +15,16 @@
|
|
|
15
15
|
|
|
16
16
|
需 Node.js ≥22.16,并已安装 Desktop。
|
|
17
17
|
|
|
18
|
-
macOS:
|
|
18
|
+
macOS / Windows / Linux:
|
|
19
19
|
|
|
20
20
|
```sh
|
|
21
|
-
npm install -g @ailuntz/minilink
|
|
21
|
+
npm install -g @ailuntz/minilink
|
|
22
22
|
minilink on
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
完全退出并重开 Desktop。
|
|
26
26
|
|
|
27
|
-
npm
|
|
28
|
-
|
|
29
|
-
Windows / Linux 的模型路由和 Desktop 连接已通过手动启动桥验证;当前 npm 包的自动安装脚本仅实现 macOS,不能使用上述命令完成 Windows / Linux 的桌面接入。
|
|
27
|
+
`npm install` 只安装命令;`minilink on` 才接入 Desktop。更新包后再次运行 `minilink on`。
|
|
30
28
|
|
|
31
29
|
## 命令
|
|
32
30
|
|
|
@@ -34,12 +32,12 @@ Windows / Linux 的模型路由和 Desktop 连接已通过手动启动桥验证
|
|
|
34
32
|
| --- | --- |
|
|
35
33
|
| `minilink on` | 开启 |
|
|
36
34
|
| `minilink link` | 选择模型与推理强度 |
|
|
37
|
-
| `minilink off` |
|
|
35
|
+
| `minilink off` | 关闭,并撤销后续 Desktop 启动的 MiniLink 覆盖 |
|
|
38
36
|
| `minilink doctor` | 检查配置与运行状态 |
|
|
39
37
|
|
|
40
38
|
通过 `minilink link` 保存选择后,完全退出并重开 Desktop。
|
|
41
39
|
|
|
42
|
-
|
|
40
|
+
`off` 会让后续启动直接使用 Desktop 自带的 Codex;`on` 会重新接入 MiniLink。已打开的 Desktop 需要完全退出并重开,才会切换启动方式。
|
|
43
41
|
|
|
44
42
|
## 配置
|
|
45
43
|
|
|
@@ -51,9 +49,10 @@ macOS 上 `off` 会让后续启动直接使用 Desktop 自带的 Codex;`on`
|
|
|
51
49
|
|
|
52
50
|
## 卸载
|
|
53
51
|
|
|
54
|
-
|
|
52
|
+
先运行 `minilink off`,再卸载:
|
|
55
53
|
|
|
56
54
|
```sh
|
|
55
|
+
minilink off
|
|
57
56
|
npm uninstall -g @ailuntz/minilink
|
|
58
57
|
```
|
|
59
58
|
|
package/bin/minilink.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { startUpdate, updateStatus } from '../src/update.mjs';
|
|
|
6
6
|
import { runProxy } from '../src/proxy.mjs';
|
|
7
7
|
import { pickerEndpoints } from '../src/picker.mjs';
|
|
8
8
|
import { selectModel, text, language } from '../src/terminal.mjs';
|
|
9
|
-
import { installation, environmentStatus, syncLaunchEnvironment } from '../src/install.mjs';
|
|
9
|
+
import { installation, environmentStatus, syncLaunchEnvironment, ensureInstalled } from '../src/install.mjs';
|
|
10
10
|
|
|
11
11
|
const self = fileURLToPath(import.meta.url), args = process.argv.slice(2);
|
|
12
12
|
const commands = ['on', 'link', 'off', 'doctor'];
|
|
@@ -19,7 +19,7 @@ async function main() {
|
|
|
19
19
|
if (!command || ['--help', '-h'].includes(command)) return output(text('help'));
|
|
20
20
|
if (commands.includes(command) && args.length !== 1) throw new Error(language() === 'zh' ? `${command} 不接受额外参数。` : `${command} does not accept extra arguments.`);
|
|
21
21
|
if (command === 'on' || command === 'off') {
|
|
22
|
-
if (command === 'on') discoverProfiles();
|
|
22
|
+
if (command === 'on') { discoverProfiles(); ensureInstalled(); }
|
|
23
23
|
atomicJSON(configPath(), { version: 1, enabled: command === 'on' });
|
|
24
24
|
syncLaunchEnvironment({ enabled: command === 'on' });
|
|
25
25
|
return output(text(command));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ailuntz/minilink",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8",
|
|
4
4
|
"description": "Use your Codex profiles in the Desktop model menu.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT AND Apache-2.0",
|
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
"LICENSE"
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
|
-
"test": "node --test test/*.test.mjs"
|
|
22
|
-
"postinstall": "node src/lifecycle.mjs postinstall"
|
|
21
|
+
"test": "node --test test/*.test.mjs"
|
|
23
22
|
},
|
|
24
23
|
"dependencies": {
|
|
25
24
|
"smol-toml": "1.8.0"
|
package/src/config.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { readFileSync, readdirSync, mkdirSync, writeFileSync, renameSync, rmSync, existsSync, realpathSync, accessSync, constants } from 'node:fs';
|
|
1
|
+
import { readFileSync, readdirSync, mkdirSync, writeFileSync, renameSync, rmSync, existsSync, realpathSync, accessSync, constants, statSync, copyFileSync, mkdtempSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { resolve, join, dirname, isAbsolute } from 'node:path';
|
|
4
|
-
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
5
6
|
import { parseEnv } from 'node:util';
|
|
6
7
|
import { parse } from 'smol-toml';
|
|
7
8
|
import { setting } from './legacy.mjs';
|
|
@@ -65,6 +66,37 @@ export function discoverProfiles(home = codexHome()) {
|
|
|
65
66
|
|
|
66
67
|
export const bundledCodexRelativePath = 'Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex';
|
|
67
68
|
export const bundledCodex = app => join(app, bundledCodexRelativePath);
|
|
69
|
+
const windowsExecutables = ['codex.exe', 'codex-code-mode-host.exe', 'codex-windows-sandbox-setup.exe', 'codex-command-runner.exe'];
|
|
70
|
+
function windowsCodex() {
|
|
71
|
+
const packageRoot = execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', '(Get-AppxPackage OpenAI.Codex).InstallLocation'], { encoding: 'utf8' }).trim();
|
|
72
|
+
if (!packageRoot) throw new Error('找不到 ChatGPT Windows 应用。');
|
|
73
|
+
const resources = join(packageRoot, 'app/resources');
|
|
74
|
+
const files = windowsExecutables.map(name => ({ name, source: join(resources, name) }));
|
|
75
|
+
const digest = createHash('sha256');
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
if (!statSync(file.source).isFile()) throw new Error('ChatGPT 内置 Codex 文件无效。');
|
|
78
|
+
file.hash = createHash('sha256').update(readFileSync(file.source)).digest('hex');
|
|
79
|
+
digest.update(file.name).update('\0').update(file.hash).update('\0');
|
|
80
|
+
}
|
|
81
|
+
const root = join(process.env.LOCALAPPDATA || join(homedir(), 'AppData/Local'), 'OpenAI/Codex/bin');
|
|
82
|
+
const version = digest.digest('hex').slice(0, 16), destination = join(root, version);
|
|
83
|
+
if (!files.every(file => existsSync(join(destination, file.name)) && createHash('sha256').update(readFileSync(join(destination, file.name))).digest('hex') === file.hash)) {
|
|
84
|
+
mkdirSync(root, { recursive: true });
|
|
85
|
+
if (existsSync(destination)) rmSync(destination, { recursive: true });
|
|
86
|
+
const staging = mkdtempSync(join(root, `.staging-${version}-`));
|
|
87
|
+
try {
|
|
88
|
+
for (const file of files) copyFileSync(file.source, join(staging, file.name));
|
|
89
|
+
renameSync(staging, destination);
|
|
90
|
+
} finally { rmSync(staging, { recursive: true, force: true }); }
|
|
91
|
+
}
|
|
92
|
+
return join(destination, 'codex.exe');
|
|
93
|
+
}
|
|
94
|
+
export function officialCodex(platform = process.platform) {
|
|
95
|
+
if (platform === 'darwin') return bundledCodex(process.env.CODEX_DESKTOP_APP || '/Applications/ChatGPT.app');
|
|
96
|
+
if (platform === 'linux') return join(process.env.CODEX_DESKTOP_RESOURCES || '/usr/lib/chatgpt/resources', 'codex');
|
|
97
|
+
if (platform === 'win32') return windowsCodex();
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
68
100
|
|
|
69
101
|
export function findApp() {
|
|
70
102
|
const app = process.env.CODEX_DESKTOP_APP || '/Applications/ChatGPT.app';
|
|
@@ -72,8 +104,7 @@ export function findApp() {
|
|
|
72
104
|
}
|
|
73
105
|
|
|
74
106
|
export function resolveRealCodex(selfPath) {
|
|
75
|
-
const
|
|
76
|
-
const candidate = setting('REAL_CODEX') || (app && bundledCodex(app));
|
|
107
|
+
const candidate = setting('REAL_CODEX') || officialCodex();
|
|
77
108
|
if (!candidate || !isAbsolute(candidate)) throw new Error('找不到内置 Codex;可用 MINILINK_REAL_CODEX 指定绝对路径。');
|
|
78
109
|
let actual;
|
|
79
110
|
try { actual = realpathSync(candidate); accessSync(actual, constants.X_OK); } catch { throw new Error('真正的 Codex 不存在或不可执行。'); }
|
package/src/install.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { activeRuns } from './runtime.mjs';
|
|
7
7
|
import { legacyInstallation } from './legacy.mjs';
|
|
8
|
-
import { atomicJSON, configPath, codexHome, readConfig, bundledCodexRelativePath } from './config.mjs';
|
|
8
|
+
import { VERSION, atomicJSON, configPath, codexHome, readConfig, resolveRealCodex, bundledCodexRelativePath } from './config.mjs';
|
|
9
9
|
|
|
10
10
|
const label = 'com.ailuntz.minilink';
|
|
11
11
|
export const installRoot = () => join(homedir(), '.local/share/minilink');
|
|
@@ -18,9 +18,26 @@ export function installation(root = installRoot()) {
|
|
|
18
18
|
try { return JSON.parse(readFileSync(join(root, 'installation.json'), 'utf8')); } catch (e) { if (e.code === 'ENOENT') return null; throw new Error('安装状态损坏,请检查 installation.json。'); }
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export function ensureInstalled({ state = installation(), source = sourceRoot, activate = installDesktop } = {}) {
|
|
22
|
+
try {
|
|
23
|
+
if (state?.source === source && JSON.parse(readFileSync(join(state.runtime, 'package.json'), 'utf8')).version === VERSION) return state;
|
|
24
|
+
} catch {}
|
|
25
|
+
return activate();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function npmDetails() {
|
|
29
|
+
try {
|
|
30
|
+
const npm = execFileSync('/usr/bin/which', ['npm'], { encoding: 'utf8' }).trim();
|
|
31
|
+
const prefix = execFileSync(npm, ['prefix', '-g'], { encoding: 'utf8' }).trim();
|
|
32
|
+
return { npm, prefix };
|
|
33
|
+
} catch { return {}; }
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
// A disabled integration must not remain in Desktop's launch path. The login
|
|
22
37
|
// agent reapplies this state in the Aqua session when the command ran over SSH.
|
|
23
38
|
export function syncLaunchEnvironment({ state = installation(), run = launchctl, enabled, previousWrapper } = {}) {
|
|
39
|
+
if (process.platform === 'linux') return syncLinux({ state, enabled });
|
|
40
|
+
if (process.platform === 'win32') return syncWindows({ state, enabled });
|
|
24
41
|
if (process.platform !== 'darwin' || !state?.wrapper) return;
|
|
25
42
|
if (enabled == null) {
|
|
26
43
|
try { enabled = readConfig(state.configPath).enabled; } catch { enabled = false; }
|
|
@@ -40,11 +57,80 @@ function atomicFile(path, text, mode) {
|
|
|
40
57
|
finally { rmSync(temporary, { force: true }); }
|
|
41
58
|
}
|
|
42
59
|
|
|
60
|
+
export function installDesktop(options) {
|
|
61
|
+
if (process.platform === 'darwin') return install(options);
|
|
62
|
+
if (process.platform === 'linux') return installLinux(options);
|
|
63
|
+
if (process.platform === 'win32') return installWindows(options);
|
|
64
|
+
throw new Error('不支持此操作系统。');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const linuxLauncher = () => join(homedir(), '.local/share/applications/chatgpt.desktop');
|
|
68
|
+
const linuxOwner = '# MiniLink desktop integration';
|
|
69
|
+
|
|
70
|
+
function syncLinux({ state = installation(), enabled } = {}) {
|
|
71
|
+
if (!state?.wrapper) return;
|
|
72
|
+
if (enabled == null) enabled = readConfig(state.configPath).enabled;
|
|
73
|
+
const launcher = linuxLauncher();
|
|
74
|
+
const current = existsSync(launcher) ? readFileSync(launcher, 'utf8') : null;
|
|
75
|
+
if (current && !current.startsWith(linuxOwner)) throw new Error('用户级 ChatGPT 启动器已存在,无法接入 MiniLink。');
|
|
76
|
+
if (!enabled) { if (current) rmSync(launcher); return; }
|
|
77
|
+
const base = readFileSync('/usr/share/applications/chatgpt.desktop', 'utf8');
|
|
78
|
+
if (!/^Exec=chatgpt %U$/m.test(base)) throw new Error('ChatGPT 启动器格式已改变。');
|
|
79
|
+
const wrapper = state.wrapper.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
|
|
80
|
+
const content = `${linuxOwner}\n${base.replace(/^Exec=chatgpt %U$/m, `Exec=env CODEX_CLI_PATH="${wrapper}" /usr/bin/chatgpt %U`)}`;
|
|
81
|
+
mkdirSync(dirname(launcher), { recursive: true });
|
|
82
|
+
atomicFile(launcher, content, 0o644);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function installLinux({ root = installRoot(), source = sourceRoot, home = codexHome(), config = configPath(), node = process.execPath } = {}) {
|
|
86
|
+
const native = resolveRealCodex(join(source, 'bin/minilink.mjs'));
|
|
87
|
+
const launcher = linuxLauncher();
|
|
88
|
+
if (existsSync(launcher) && !readFileSync(launcher, 'utf8').startsWith(linuxOwner)) throw new Error('用户级 ChatGPT 启动器已存在,无法接入 MiniLink。');
|
|
89
|
+
const wrapper = join(root, 'minilink');
|
|
90
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
91
|
+
atomicFile(wrapper, `#!/bin/sh\nif [ ! -f ${shellQuote(join(source, 'package.json'))} ]; then exec ${shellQuote(native)} "$@"; fi\nexport CODEX_HOME=${shellQuote(home)}\nexport MINILINK_CONFIG=${shellQuote(config)}\nexec ${shellQuote(node)} ${shellQuote(join(source, 'bin/minilink.mjs'))} "$@"\n`, 0o755);
|
|
92
|
+
const state = { version: 3, platform: 'linux', source, runtime: source, wrapper, node, codexHome: home, configPath: config, installedAt: new Date().toISOString() };
|
|
93
|
+
atomicJSON(join(root, 'installation.json'), state);
|
|
94
|
+
return state;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const psQuote = value => "'" + value.replaceAll("'", "''") + "'";
|
|
98
|
+
const powershell = command => execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', command], { encoding: 'utf8' }).trim();
|
|
99
|
+
const windowsEnvironment = () => powershell("[Environment]::GetEnvironmentVariable('CODEX_CLI_PATH','User')") || null;
|
|
100
|
+
function setWindowsEnvironment(value) {
|
|
101
|
+
const broadcast = 'using System; using System.Runtime.InteropServices; public class MiniLinkBroadcast { [DllImport("user32.dll", CharSet=CharSet.Auto)] public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, IntPtr wParam, string lParam, uint flags, uint timeout, out IntPtr result); }';
|
|
102
|
+
powershell(`[Environment]::SetEnvironmentVariable('CODEX_CLI_PATH',${value == null ? '$null' : psQuote(value)},'User'); Add-Type -TypeDefinition ${psQuote(broadcast)}; $result=[IntPtr]::Zero; [MiniLinkBroadcast]::SendMessageTimeout([IntPtr]65535,26,[IntPtr]::Zero,'Environment',2,5000,[ref]$result) | Out-Null`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function syncWindows({ state = installation(), enabled } = {}) {
|
|
106
|
+
if (!state?.wrapper) return;
|
|
107
|
+
if (enabled == null) enabled = readConfig(state.configPath).enabled;
|
|
108
|
+
const current = windowsEnvironment();
|
|
109
|
+
if (enabled) {
|
|
110
|
+
if (current && current !== state.wrapper) throw new Error('CODEX_CLI_PATH 已指向其他程序。');
|
|
111
|
+
if (!current) setWindowsEnvironment(state.wrapper);
|
|
112
|
+
} else if (current === state.wrapper) setWindowsEnvironment(state.previousEnv || null);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function installWindows({ root = installRoot(), source = sourceRoot, home = codexHome(), config = configPath(), node = process.execPath } = {}) {
|
|
116
|
+
const native = resolveRealCodex(join(source, 'bin/minilink.mjs'));
|
|
117
|
+
const previous = installation(root), wrapper = join(root, 'MiniLinkLauncher.exe');
|
|
118
|
+
const current = windowsEnvironment();
|
|
119
|
+
if (current && current !== wrapper) throw new Error('CODEX_CLI_PATH 已指向其他程序。');
|
|
120
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
121
|
+
if (!existsSync(wrapper)) powershell(`Add-Type -Path ${psQuote(join(source, 'src/platform/windows/MiniLinkLauncher.cs'))} -OutputAssembly ${psQuote(wrapper)} -OutputType ConsoleApplication`);
|
|
122
|
+
atomicFile(join(root, 'launcher.paths'), [node, join(source, 'bin/minilink.mjs'), native, home, config].join('\n') + '\n', 0o600);
|
|
123
|
+
const state = { version: 3, platform: 'win32', source, runtime: source, wrapper, node, codexHome: home, configPath: config, previousEnv: previous?.previousEnv || null, installedAt: new Date().toISOString() };
|
|
124
|
+
atomicJSON(join(root, 'installation.json'), state);
|
|
125
|
+
return state;
|
|
126
|
+
}
|
|
127
|
+
|
|
43
128
|
// Each installation has its own files: active Desktop processes keep their
|
|
44
129
|
// original modules and picker assets while the next launch uses the new copy.
|
|
45
130
|
export function install({ root = installRoot(), plist = defaultPlist(), source = sourceRoot, home = codexHome(), config = configPath(), node = process.execPath, npm = process.env.npm_execpath, prefix = process.env.npm_config_prefix, run = launchctl, legacy = root === installRoot() ? legacyInstallation() : null } = {}) {
|
|
46
131
|
if (process.platform !== 'darwin') throw new Error('Desktop 接入目前仅支持 macOS。');
|
|
47
132
|
const previous = installation(root), wrapper = join(root, 'minilink');
|
|
133
|
+
const packageManager = npm && prefix ? { npm, prefix } : previous?.npm && previous?.prefix ? { npm: previous.npm, prefix: previous.prefix } : npmDetails();
|
|
48
134
|
if (existsSync(plist) && !previous) throw new Error('同名 LaunchAgent 已存在且不属于本次安装。');
|
|
49
135
|
const env = run(['getenv', 'CODEX_CLI_PATH'], false) || null;
|
|
50
136
|
if (env && env !== wrapper && env !== legacy?.state.wrapper) throw new Error('CODEX_CLI_PATH 已指向其他程序,请先移除该覆盖。');
|
|
@@ -75,7 +161,7 @@ export function install({ root = installRoot(), plist = defaultPlist(), source =
|
|
|
75
161
|
}
|
|
76
162
|
if (JSON.parse(readFileSync(join(dependency, 'package.json'), 'utf8')).name !== 'smol-toml') throw new Error('smol-toml 依赖路径无效。');
|
|
77
163
|
cpSync(dependency, join(runtime, 'node_modules/smol-toml'), { recursive: true });
|
|
78
|
-
const state = { version: 2, source, runtime, wrapper, node, npm: npm || previous?.npm, prefix: prefix || previous?.prefix, codexHome: home, configPath: config, previousEnv: previous?.previousEnv || legacy?.state.previousEnv || null, installedAt: new Date().toISOString() };
|
|
164
|
+
const state = { version: 2, source, runtime, wrapper, node, npm: packageManager.npm || previous?.npm, prefix: packageManager.prefix || previous?.prefix, codexHome: home, configPath: config, previousEnv: previous?.previousEnv || legacy?.state.previousEnv || null, installedAt: new Date().toISOString() };
|
|
79
165
|
// A Desktop process can retain this launcher after npm removes the state.
|
|
80
166
|
const fallback = `if [ ! -f ${shellQuote(stateFile)} ]; then\n exec "\${CODEX_DESKTOP_APP:-/Applications/ChatGPT.app}/${bundledCodexRelativePath}" "$@"\nfi`;
|
|
81
167
|
atomicFile(wrapper, `#!/bin/sh\n${fallback}\nexport CODEX_HOME=${shellQuote(home)}\nexport MINILINK_CONFIG=${shellQuote(config)}\nexec ${shellQuote(node)} ${shellQuote(join(runtime, 'bin/minilink.mjs'))} "$@"\n`, 0o755);
|
|
@@ -149,4 +235,9 @@ export function reconcile({ root = installRoot(), plist = defaultPlist(), run =
|
|
|
149
235
|
run(['bootout', `gui/${process.getuid()}/${label}`], false);
|
|
150
236
|
}
|
|
151
237
|
|
|
152
|
-
export function environmentStatus() {
|
|
238
|
+
export function environmentStatus() {
|
|
239
|
+
if (process.platform === 'darwin') return launchctl(['getenv', 'CODEX_CLI_PATH'], false) || null;
|
|
240
|
+
if (process.platform === 'linux') return existsSync(linuxLauncher()) && readFileSync(linuxLauncher(), 'utf8').startsWith(linuxOwner) ? installation()?.wrapper : null;
|
|
241
|
+
if (process.platform === 'win32') return windowsEnvironment();
|
|
242
|
+
return null;
|
|
243
|
+
}
|
package/src/lifecycle.mjs
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { installDesktop, reconcile } from './install.mjs';
|
|
2
2
|
|
|
3
|
-
if (process.argv[2] === '
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
catch (error) { process.stderr.write(`minilink: ${error.message}\n`); process.exitCode = 1; }
|
|
7
|
-
}
|
|
3
|
+
if (process.argv[2] === 'activate') {
|
|
4
|
+
try { installDesktop(); }
|
|
5
|
+
catch (error) { process.stderr.write(`minilink: ${error.message}\n`); process.exitCode = 1; }
|
|
8
6
|
} else {
|
|
9
7
|
try { reconcile(); } catch { /* Login and uninstall checks stay silent. */ }
|
|
10
8
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Diagnostics;
|
|
3
|
+
using System.IO;
|
|
4
|
+
using System.Linq;
|
|
5
|
+
using System.Threading.Tasks;
|
|
6
|
+
|
|
7
|
+
class MiniLinkLauncher {
|
|
8
|
+
static void Pump(Stream input, Stream output) {
|
|
9
|
+
byte[] buffer = new byte[4096];
|
|
10
|
+
int count;
|
|
11
|
+
while ((count = input.Read(buffer, 0, buffer.Length)) > 0) {
|
|
12
|
+
output.Write(buffer, 0, count);
|
|
13
|
+
output.Flush();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static string Quote(string arg) {
|
|
18
|
+
return "\"" + System.Text.RegularExpressions.Regex.Replace(arg, "(\\\\*)\"", "$1$1\\\"").TrimEnd('\\')
|
|
19
|
+
+ new string('\\', arg.Reverse().TakeWhile(c => c == '\\').Count() * 2) + "\"";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static int Main(string[] args) {
|
|
23
|
+
try {
|
|
24
|
+
string root = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
|
|
25
|
+
string[] paths = File.ReadAllLines(Path.Combine(root, "launcher.paths"));
|
|
26
|
+
bool linked = File.Exists(paths[1]);
|
|
27
|
+
var info = new ProcessStartInfo(linked ? paths[0] : paths[2]);
|
|
28
|
+
info.Arguments = String.Join(" ", (linked ? new[] { paths[1] }.Concat(args) : args).Select(Quote));
|
|
29
|
+
info.UseShellExecute = false;
|
|
30
|
+
info.CreateNoWindow = true;
|
|
31
|
+
info.RedirectStandardInput = true;
|
|
32
|
+
info.RedirectStandardOutput = true;
|
|
33
|
+
info.RedirectStandardError = true;
|
|
34
|
+
info.EnvironmentVariables["CODEX_HOME"] = paths[3];
|
|
35
|
+
info.EnvironmentVariables["MINILINK_CONFIG"] = paths[4];
|
|
36
|
+
if (!linked) info.EnvironmentVariables.Remove("CODEX_CLI_PATH");
|
|
37
|
+
using (var process = Process.Start(info)) {
|
|
38
|
+
Task.Run(() => { try { Pump(Console.OpenStandardInput(), process.StandardInput.BaseStream); process.StandardInput.Close(); } catch {} });
|
|
39
|
+
var output = Task.Run(() => Pump(process.StandardOutput.BaseStream, Console.OpenStandardOutput()));
|
|
40
|
+
var error = Task.Run(() => Pump(process.StandardError.BaseStream, Console.OpenStandardError()));
|
|
41
|
+
process.WaitForExit();
|
|
42
|
+
Task.WaitAll(output, error);
|
|
43
|
+
return process.ExitCode;
|
|
44
|
+
}
|
|
45
|
+
} catch (Exception error) {
|
|
46
|
+
Console.Error.WriteLine("minilink launcher: " + error.Message);
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/update.mjs
CHANGED
|
@@ -61,7 +61,8 @@ export async function upgrade(latest, state, { root = installRoot(), exec = exec
|
|
|
61
61
|
// A manual uninstall during the download must not be undone by the worker.
|
|
62
62
|
if (!existsSync(state.source) || installation(root)?.installedAt !== state.installedAt) return false;
|
|
63
63
|
installing = true;
|
|
64
|
-
await npm(['install', '-g', '--prefix', state.prefix, candidate, '--offline', '--engine-strict',
|
|
64
|
+
await npm(['install', '-g', '--prefix', state.prefix, candidate, '--offline', '--engine-strict', '--ignore-scripts']);
|
|
65
|
+
await exec(state.node, [join(state.source, 'src/lifecycle.mjs'), 'activate'], { env, cwd: directory, timeout: 30000 });
|
|
65
66
|
const active = installation(root);
|
|
66
67
|
if (!active?.runtime || JSON.parse(readFileSync(join(active.runtime, 'package.json'), 'utf8')).version !== latest) throw new Error('Desktop activation failed');
|
|
67
68
|
return true;
|