codex-multi-auth 2.19.1 → 2.21.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/.codex-plugin/plugin.json +9 -9
- package/LICENSE +21 -21
- package/README.md +149 -148
- package/assets/codex-multi-auth-icon.svg +39 -39
- package/assets/readme-hero.svg +31 -31
- package/config/README.md +31 -31
- package/config/codex-legacy.json +1021 -1021
- package/config/codex-modern.json +347 -347
- package/config/minimal-codex.json +15 -15
- package/dist/index.js +1 -1
- package/dist/lib/accounts/rate-limits.js +5 -0
- package/dist/lib/accounts.js +5 -4
- package/dist/lib/auth/account-access.js +2 -2
- package/dist/lib/codex-cli/version.d.ts +22 -0
- package/dist/lib/codex-cli/version.js +78 -0
- package/dist/lib/codex-manager/commands/limits.d.ts +2 -0
- package/dist/lib/codex-manager/commands/limits.js +31 -0
- package/dist/lib/codex-manager/commands/status.js +1 -1
- package/dist/lib/codex-manager.js +2 -0
- package/dist/lib/constants.d.ts +2 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/context-budget/model-context-windows.d.ts +1 -1
- package/dist/lib/context-budget/model-context-windows.js +4 -0
- package/dist/lib/context-budget-response.js +8 -8
- package/dist/lib/context-overflow.js +10 -10
- package/dist/lib/luna-reserve.d.ts +26 -0
- package/dist/lib/luna-reserve.js +68 -0
- package/dist/lib/oauth-success.html +125 -125
- package/dist/lib/policy/runtime-policy.js +14 -0
- package/dist/lib/prompts/codex-host-bridge.js +138 -138
- package/dist/lib/prompts/codex.js +79 -79
- package/dist/lib/request/helpers/model-map.d.ts +1 -1
- package/dist/lib/request/helpers/model-map.js +12 -0
- package/dist/lib/request/response-outcome.d.ts +1 -1
- package/dist/lib/request/response-outcome.js +4 -1
- package/dist/lib/runtime/account-status.js +2 -1
- package/dist/lib/runtime/luna-reserve-usage.d.ts +9 -0
- package/dist/lib/runtime/luna-reserve-usage.js +68 -0
- package/dist/lib/runtime/model-discovery-status.d.ts +6 -2
- package/dist/lib/runtime/model-discovery-status.js +13 -5
- package/dist/lib/runtime/reset-credit-routing.js +4 -1
- package/dist/lib/runtime/rotation-server-types.d.ts +2 -0
- package/dist/lib/runtime/runtime-observability.d.ts +2 -0
- package/dist/lib/runtime/runtime-observability.js +1 -0
- package/dist/lib/runtime-rotation-proxy.js +200 -32
- package/dist/lib/usage/pricing.d.ts +1 -1
- package/dist/lib/usage/pricing.js +4 -0
- package/dist/lib/usage/usage-extraction.d.ts +2 -0
- package/dist/lib/usage/usage-extraction.js +25 -1
- package/dist/lib/utils.d.ts +1 -1
- package/dist/lib/utils.js +1 -1
- package/node_modules/@codex-ai/plugin/dist/index.d.ts +2 -2
- package/node_modules/@codex-ai/plugin/dist/index.js +2 -2
- package/node_modules/@codex-ai/plugin/dist/tool.d.ts +42 -42
- package/node_modules/@codex-ai/plugin/dist/tool.js +29 -29
- package/node_modules/@codex-ai/plugin/package.json +9 -9
- package/node_modules/@codex-ai/sdk/dist/index.d.ts +4 -4
- package/node_modules/@codex-ai/sdk/dist/index.js +2 -2
- package/node_modules/@codex-ai/sdk/package.json +8 -8
- package/package.json +207 -207
- package/scripts/codex-app-launcher.js +653 -653
- package/scripts/codex-app-router.js +374 -373
- package/scripts/codex-bin-resolver.js +332 -332
- package/scripts/codex-multi-auth.js +65 -65
- package/scripts/codex-routing.js +62 -62
- package/scripts/codex.js +7006 -7005
- package/scripts/install-codex-auth-utils.js +213 -213
- package/scripts/mcodex.js +367 -367
- package/scripts/postinstall.js +114 -114
- package/scripts/preuninstall.js +265 -265
- package/skills/codex-auth-setup/SKILL.md +73 -73
- package/vendor/codex-ai-plugin/dist/index.d.ts +2 -2
- package/vendor/codex-ai-plugin/dist/index.js +2 -2
- package/vendor/codex-ai-plugin/dist/tool.d.ts +42 -42
- package/vendor/codex-ai-plugin/dist/tool.js +29 -29
- package/vendor/codex-ai-plugin/package.json +9 -9
- package/vendor/codex-ai-sdk/dist/index.d.ts +4 -4
- package/vendor/codex-ai-sdk/dist/index.js +2 -2
- package/vendor/codex-ai-sdk/package.json +8 -8
|
@@ -1,653 +1,653 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// @ts-check
|
|
4
|
-
|
|
5
|
-
import { chmod, mkdir, rm, writeFile } from "node:fs/promises";
|
|
6
|
-
import { homedir } from "node:os";
|
|
7
|
-
import { dirname, join, resolve } from "node:path";
|
|
8
|
-
import process from "node:process";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { spawn } from "node:child_process";
|
|
11
|
-
import { withFileOperationRetry } from "./install-codex-auth-utils.js";
|
|
12
|
-
|
|
13
|
-
const OFFICIAL_LAUNCHER_NAME = "Codex";
|
|
14
|
-
const MANAGED_LAUNCHER_NAME = "Codex Multi Auth";
|
|
15
|
-
const WINDOWS_SHORTCUT_NAME = `${OFFICIAL_LAUNCHER_NAME}.lnk`;
|
|
16
|
-
const LINUX_DESKTOP_FILE_NAME = "codex-multi-auth.desktop";
|
|
17
|
-
const MACOS_APP_NAME = `${MANAGED_LAUNCHER_NAME}.app`;
|
|
18
|
-
const WINDOWS_BACKUP_FILE_NAME = "app-shortcuts.json";
|
|
19
|
-
const MANAGED_SHORTCUT_DESCRIPTION =
|
|
20
|
-
"Launch Codex through codex-multi-auth runtime rotation";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* @param {string} value
|
|
24
|
-
*/
|
|
25
|
-
function quotePowerShellSingle(value) {
|
|
26
|
-
return `'${value.replace(/'/g, "''")}'`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @param {string} value
|
|
31
|
-
*/
|
|
32
|
-
function encodePowerShellCommand(value) {
|
|
33
|
-
return Buffer.from(value, "utf16le").toString("base64");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* @param {boolean} value
|
|
38
|
-
*/
|
|
39
|
-
function quotePowerShellBoolean(value) {
|
|
40
|
-
return value ? "$true" : "$false";
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* @param {string[]} values
|
|
45
|
-
*/
|
|
46
|
-
function quotePowerShellArray(values) {
|
|
47
|
-
if (values.length === 0) {
|
|
48
|
-
return "@()";
|
|
49
|
-
}
|
|
50
|
-
return `@(${values.map(quotePowerShellSingle).join(", ")})`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* @param {string} value
|
|
55
|
-
*/
|
|
56
|
-
function quoteDesktopExec(value) {
|
|
57
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* @param {string} value
|
|
62
|
-
*/
|
|
63
|
-
function quotePosixShell(value) {
|
|
64
|
-
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* @param {string[]} values
|
|
69
|
-
*/
|
|
70
|
-
function uniqueStrings(values) {
|
|
71
|
-
return [...new Set(values.filter((value) => value.trim().length > 0))];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* @param {NodeJS.ProcessEnv} env
|
|
76
|
-
* @param {string} home
|
|
77
|
-
*/
|
|
78
|
-
function resolveWindowsStartMenuDir(env, home) {
|
|
79
|
-
const appData = (env.APPDATA ?? "").trim() || join(home, "AppData", "Roaming");
|
|
80
|
-
return join(appData, "Microsoft", "Windows", "Start Menu", "Programs");
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* @param {NodeJS.ProcessEnv} env
|
|
85
|
-
*/
|
|
86
|
-
function resolveWindowsPowerShellPath(env) {
|
|
87
|
-
const systemRoot =
|
|
88
|
-
(env.SystemRoot ?? env.SYSTEMROOT ?? "").trim() || "C:\\Windows";
|
|
89
|
-
return join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* @param {NodeJS.ProcessEnv} env
|
|
94
|
-
* @param {string} home
|
|
95
|
-
*/
|
|
96
|
-
function resolveWindowsTaskbarPinnedDir(env, home) {
|
|
97
|
-
const appData = (env.APPDATA ?? "").trim() || join(home, "AppData", "Roaming");
|
|
98
|
-
return join(
|
|
99
|
-
appData,
|
|
100
|
-
"Microsoft",
|
|
101
|
-
"Internet Explorer",
|
|
102
|
-
"Quick Launch",
|
|
103
|
-
"User Pinned",
|
|
104
|
-
"TaskBar",
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* @param {NodeJS.ProcessEnv} env
|
|
110
|
-
* @param {string} home
|
|
111
|
-
*/
|
|
112
|
-
function resolveWindowsDesktopDirs(env, home) {
|
|
113
|
-
const configured = (env.CODEX_MULTI_AUTH_APP_LAUNCHER_WINDOWS_DESKTOP_DIR ?? "").trim();
|
|
114
|
-
const onedriveRoots = [
|
|
115
|
-
env.OneDrive,
|
|
116
|
-
env.OneDriveConsumer,
|
|
117
|
-
env.OneDriveCommercial,
|
|
118
|
-
].filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
119
|
-
return uniqueStrings([
|
|
120
|
-
configured,
|
|
121
|
-
...onedriveRoots.map((root) => join(String(root), "Desktop")),
|
|
122
|
-
join(home, "Desktop"),
|
|
123
|
-
]);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* @param {NodeJS.ProcessEnv} env
|
|
128
|
-
* @param {string} home
|
|
129
|
-
*/
|
|
130
|
-
function resolveCodexMultiAuthDir(env, home) {
|
|
131
|
-
return (env.CODEX_MULTI_AUTH_DIR ?? "").trim() || join(home, ".codex", "multi-auth");
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* @param {NodeJS.ProcessEnv} env
|
|
136
|
-
* @param {string} home
|
|
137
|
-
*/
|
|
138
|
-
function resolveLinuxApplicationsDir(env, home) {
|
|
139
|
-
const dataHome = (env.XDG_DATA_HOME ?? "").trim() || join(home, ".local", "share");
|
|
140
|
-
return join(dataHome, "applications");
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* @param {NodeJS.ProcessEnv} env
|
|
145
|
-
* @param {string} home
|
|
146
|
-
*/
|
|
147
|
-
function resolveMacApplicationsDir(env, home) {
|
|
148
|
-
return (env.CODEX_MULTI_AUTH_APP_LAUNCHER_MACOS_DIR ?? "").trim() || join(home, "Applications");
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* @param {string} moduleUrl
|
|
153
|
-
*/
|
|
154
|
-
function resolveCurrentScriptPath(moduleUrl) {
|
|
155
|
-
return fileURLToPath(moduleUrl);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* @param {{
|
|
160
|
-
* nodePath: string,
|
|
161
|
-
* codexScriptPath: string,
|
|
162
|
-
* workingDirectory: string,
|
|
163
|
-
* }} params
|
|
164
|
-
*/
|
|
165
|
-
function createWindowsLauncherCommandArgs(params) {
|
|
166
|
-
const command = [
|
|
167
|
-
"$ErrorActionPreference = 'Stop'",
|
|
168
|
-
`Set-Location -LiteralPath ${quotePowerShellSingle(params.workingDirectory)}`,
|
|
169
|
-
`& ${quotePowerShellSingle(params.nodePath)} ${quotePowerShellSingle(params.codexScriptPath)} app`,
|
|
170
|
-
].join("; ");
|
|
171
|
-
return `-NoProfile -ExecutionPolicy Bypass -EncodedCommand ${encodePowerShellCommand(command)}`;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* @param {{
|
|
176
|
-
* env?: NodeJS.ProcessEnv,
|
|
177
|
-
* platform?: NodeJS.Platform,
|
|
178
|
-
* home?: string,
|
|
179
|
-
* moduleUrl?: string,
|
|
180
|
-
* }} [options]
|
|
181
|
-
*/
|
|
182
|
-
export function resolveAppLauncherPlan(options = {}) {
|
|
183
|
-
const env = options.env ?? process.env;
|
|
184
|
-
const platform = options.platform ?? process.platform;
|
|
185
|
-
const home = options.home ?? homedir();
|
|
186
|
-
const moduleUrl = options.moduleUrl ?? import.meta.url;
|
|
187
|
-
const scriptPath = resolveCurrentScriptPath(moduleUrl);
|
|
188
|
-
const codexScriptPath = join(dirname(scriptPath), "codex.js");
|
|
189
|
-
const nodePath = process.execPath;
|
|
190
|
-
const commandArgv = [codexScriptPath, "app"];
|
|
191
|
-
|
|
192
|
-
if (platform === "win32") {
|
|
193
|
-
const startMenuDir = resolveWindowsStartMenuDir(env, home);
|
|
194
|
-
return {
|
|
195
|
-
platform,
|
|
196
|
-
mode: "route-existing",
|
|
197
|
-
launcherPath: join(startMenuDir, WINDOWS_SHORTCUT_NAME),
|
|
198
|
-
shortcutRoots: [
|
|
199
|
-
startMenuDir,
|
|
200
|
-
resolveWindowsTaskbarPinnedDir(env, home),
|
|
201
|
-
...resolveWindowsDesktopDirs(env, home),
|
|
202
|
-
],
|
|
203
|
-
backupPath: join(resolveCodexMultiAuthDir(env, home), WINDOWS_BACKUP_FILE_NAME),
|
|
204
|
-
commandPath: resolveWindowsPowerShellPath(env),
|
|
205
|
-
commandArgs: createWindowsLauncherCommandArgs({
|
|
206
|
-
nodePath,
|
|
207
|
-
codexScriptPath,
|
|
208
|
-
workingDirectory: home,
|
|
209
|
-
}),
|
|
210
|
-
commandArgv,
|
|
211
|
-
workingDirectory: home,
|
|
212
|
-
iconPath: nodePath,
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (platform === "darwin") {
|
|
217
|
-
const appPath = join(resolveMacApplicationsDir(env, home), MACOS_APP_NAME);
|
|
218
|
-
return {
|
|
219
|
-
platform,
|
|
220
|
-
mode: "create-managed",
|
|
221
|
-
launcherPath: appPath,
|
|
222
|
-
commandPath: nodePath,
|
|
223
|
-
commandArgs: `"${codexScriptPath}" app`,
|
|
224
|
-
commandArgv,
|
|
225
|
-
workingDirectory: home,
|
|
226
|
-
iconPath: nodePath,
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const desktopPath = join(resolveLinuxApplicationsDir(env, home), LINUX_DESKTOP_FILE_NAME);
|
|
231
|
-
return {
|
|
232
|
-
platform,
|
|
233
|
-
mode: "create-managed",
|
|
234
|
-
launcherPath: desktopPath,
|
|
235
|
-
commandPath: nodePath,
|
|
236
|
-
commandArgs: `"${codexScriptPath}" app %F`,
|
|
237
|
-
commandArgv: [codexScriptPath, "app", "%F"],
|
|
238
|
-
workingDirectory: home,
|
|
239
|
-
iconPath: "utilities-terminal",
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* @param {ReturnType<typeof resolveAppLauncherPlan>} plan
|
|
245
|
-
* @param {{ dryRun?: boolean, remove?: boolean }} [options]
|
|
246
|
-
*/
|
|
247
|
-
export function createWindowsShortcutPowerShellScript(plan, options = {}) {
|
|
248
|
-
const shortcutRoots = Array.isArray(plan.shortcutRoots) ? plan.shortcutRoots : [];
|
|
249
|
-
const backupPath = typeof plan.backupPath === "string" ? plan.backupPath : "";
|
|
250
|
-
const dryRun = options.dryRun === true;
|
|
251
|
-
const remove = options.remove === true;
|
|
252
|
-
|
|
253
|
-
if (remove) {
|
|
254
|
-
return [
|
|
255
|
-
"$ErrorActionPreference = 'Stop'",
|
|
256
|
-
`$DryRun = ${quotePowerShellBoolean(dryRun)}`,
|
|
257
|
-
`$BackupPath = ${quotePowerShellSingle(backupPath)}`,
|
|
258
|
-
"$Restored = @()",
|
|
259
|
-
"$Skipped = @()",
|
|
260
|
-
"if (Test-Path -LiteralPath $BackupPath) {",
|
|
261
|
-
" $Raw = Get-Content -LiteralPath $BackupPath -Raw -Encoding UTF8",
|
|
262
|
-
" $Backups = @($Raw | ConvertFrom-Json)",
|
|
263
|
-
" $Shell = New-Object -ComObject WScript.Shell",
|
|
264
|
-
" foreach ($Backup in $Backups) {",
|
|
265
|
-
" if ($null -eq $Backup.Path -or -not (Test-Path -LiteralPath $Backup.Path)) {",
|
|
266
|
-
" if ($null -ne $Backup.Path) { $Skipped += [string]$Backup.Path }",
|
|
267
|
-
" continue",
|
|
268
|
-
" }",
|
|
269
|
-
" if (-not $DryRun) {",
|
|
270
|
-
" $Shortcut = $Shell.CreateShortcut([string]$Backup.Path)",
|
|
271
|
-
" $Shortcut.TargetPath = [string]$Backup.TargetPath",
|
|
272
|
-
" $Shortcut.Arguments = [string]$Backup.Arguments",
|
|
273
|
-
" $Shortcut.WorkingDirectory = [string]$Backup.WorkingDirectory",
|
|
274
|
-
" $Shortcut.IconLocation = [string]$Backup.IconLocation",
|
|
275
|
-
" $Shortcut.Description = [string]$Backup.Description",
|
|
276
|
-
" $Shortcut.Save()",
|
|
277
|
-
" }",
|
|
278
|
-
" $Restored += [string]$Backup.Path",
|
|
279
|
-
" }",
|
|
280
|
-
" if (-not $DryRun) { Remove-Item -LiteralPath $BackupPath -Force -ErrorAction SilentlyContinue }",
|
|
281
|
-
"}",
|
|
282
|
-
"$Result = [ordered]@{ action = 'restore'; dryRun = $DryRun; backupPath = $BackupPath; restored = @($Restored); skipped = @($Skipped) }",
|
|
283
|
-
"$Result | ConvertTo-Json -Depth 6 -Compress",
|
|
284
|
-
].join("\r\n");
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return [
|
|
288
|
-
"$ErrorActionPreference = 'Stop'",
|
|
289
|
-
`$DryRun = ${quotePowerShellBoolean(dryRun)}`,
|
|
290
|
-
`$ShortcutRoots = ${quotePowerShellArray(shortcutRoots)}`,
|
|
291
|
-
"$ShellDesktop = [Environment]::GetFolderPath('Desktop')",
|
|
292
|
-
"if (-not [string]::IsNullOrWhiteSpace($ShellDesktop)) { $ShortcutRoots = @($ShortcutRoots + $ShellDesktop) | Sort-Object -Unique }",
|
|
293
|
-
`$BackupPath = ${quotePowerShellSingle(backupPath)}`,
|
|
294
|
-
`$ShortcutName = ${quotePowerShellSingle(OFFICIAL_LAUNCHER_NAME)}`,
|
|
295
|
-
`$TargetPath = ${quotePowerShellSingle(plan.commandPath)}`,
|
|
296
|
-
`$Arguments = ${quotePowerShellSingle(plan.commandArgs)}`,
|
|
297
|
-
`$WorkingDirectory = ${quotePowerShellSingle(plan.workingDirectory)}`,
|
|
298
|
-
`$ManagedDescription = ${quotePowerShellSingle(MANAGED_SHORTCUT_DESCRIPTION)}`,
|
|
299
|
-
"$Candidates = @()",
|
|
300
|
-
"$PackagedApps = @()",
|
|
301
|
-
"foreach ($Root in $ShortcutRoots) {",
|
|
302
|
-
" if (-not (Test-Path -LiteralPath $Root)) { continue }",
|
|
303
|
-
" $Candidates += Get-ChildItem -LiteralPath $Root -Filter '*.lnk' -File -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.BaseName -ieq $ShortcutName } | ForEach-Object { $_.FullName }",
|
|
304
|
-
"}",
|
|
305
|
-
"$Candidates = @($Candidates | Sort-Object -Unique)",
|
|
306
|
-
"try {",
|
|
307
|
-
" $AppsFolder = (New-Object -ComObject Shell.Application).Namespace('shell:AppsFolder')",
|
|
308
|
-
" if ($null -ne $AppsFolder) {",
|
|
309
|
-
" $PackagedApps = @($AppsFolder.Items() | Where-Object { $_.Name -ieq $ShortcutName } | ForEach-Object { [ordered]@{ Name = [string]$_.Name; Path = [string]$_.Path } })",
|
|
310
|
-
" }",
|
|
311
|
-
"} catch { $PackagedApps = @() }",
|
|
312
|
-
"$Shell = New-Object -ComObject WScript.Shell",
|
|
313
|
-
"$ExistingBackups = @()",
|
|
314
|
-
"if (Test-Path -LiteralPath $BackupPath) {",
|
|
315
|
-
" try {",
|
|
316
|
-
" $Raw = Get-Content -LiteralPath $BackupPath -Raw -Encoding UTF8",
|
|
317
|
-
" if ($Raw.Trim().Length -gt 0) { $ExistingBackups = @($Raw | ConvertFrom-Json) }",
|
|
318
|
-
" } catch { $ExistingBackups = @() }",
|
|
319
|
-
"}",
|
|
320
|
-
"$BackupByPath = @{}",
|
|
321
|
-
"$BackupsToWrite = New-Object System.Collections.ArrayList",
|
|
322
|
-
"foreach ($Backup in $ExistingBackups) {",
|
|
323
|
-
" if ($null -eq $Backup.Path) { continue }",
|
|
324
|
-
" $BackupByPath[[string]$Backup.Path] = $Backup",
|
|
325
|
-
" [void]$BackupsToWrite.Add($Backup)",
|
|
326
|
-
"}",
|
|
327
|
-
"$Routed = @()",
|
|
328
|
-
"$Skipped = @()",
|
|
329
|
-
"foreach ($Path in $Candidates) {",
|
|
330
|
-
" $Shortcut = $Shell.CreateShortcut($Path)",
|
|
331
|
-
" $ShortcutText = (($Shortcut.TargetPath, $Shortcut.Arguments, $Shortcut.Description) -join ' ')",
|
|
332
|
-
" if ($ShortcutText -notmatch '(?i)codex') {",
|
|
333
|
-
" $Skipped += $Path",
|
|
334
|
-
" continue",
|
|
335
|
-
" }",
|
|
336
|
-
" $AlreadyManaged = (([string]$Shortcut.Description) -eq $ManagedDescription) -or ((([string]$Shortcut.TargetPath) -ieq $TargetPath) -and (([string]$Shortcut.Arguments) -ieq $Arguments))",
|
|
337
|
-
" if (-not $BackupByPath.ContainsKey($Path) -and -not $AlreadyManaged) {",
|
|
338
|
-
" $IconLocation = [string]$Shortcut.IconLocation",
|
|
339
|
-
" if ([string]::IsNullOrWhiteSpace($IconLocation)) { $IconLocation = [string]$Shortcut.TargetPath }",
|
|
340
|
-
" $Backup = [ordered]@{",
|
|
341
|
-
" Path = [string]$Path",
|
|
342
|
-
" TargetPath = [string]$Shortcut.TargetPath",
|
|
343
|
-
" Arguments = [string]$Shortcut.Arguments",
|
|
344
|
-
" WorkingDirectory = [string]$Shortcut.WorkingDirectory",
|
|
345
|
-
" IconLocation = $IconLocation",
|
|
346
|
-
" Description = [string]$Shortcut.Description",
|
|
347
|
-
" }",
|
|
348
|
-
" [void]$BackupsToWrite.Add($Backup)",
|
|
349
|
-
" $BackupByPath[$Path] = $Backup",
|
|
350
|
-
" }",
|
|
351
|
-
" if (-not $DryRun) {",
|
|
352
|
-
" $Backup = $BackupByPath[$Path]",
|
|
353
|
-
" $Shortcut.TargetPath = $TargetPath",
|
|
354
|
-
" $Shortcut.Arguments = $Arguments",
|
|
355
|
-
" $Shortcut.WorkingDirectory = $WorkingDirectory",
|
|
356
|
-
" if ($null -ne $Backup -and $null -ne $Backup.IconLocation) { $Shortcut.IconLocation = [string]$Backup.IconLocation }",
|
|
357
|
-
" $Shortcut.Description = $ManagedDescription",
|
|
358
|
-
" $Shortcut.Save()",
|
|
359
|
-
" }",
|
|
360
|
-
" $Routed += $Path",
|
|
361
|
-
"}",
|
|
362
|
-
"if (-not $DryRun -and $Routed.Count -gt 0) {",
|
|
363
|
-
" New-Item -ItemType Directory -Force -Path (Split-Path -Parent $BackupPath) | Out-Null",
|
|
364
|
-
" @($BackupsToWrite) | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $BackupPath -Encoding UTF8",
|
|
365
|
-
"}",
|
|
366
|
-
"$Result = [ordered]@{ action = 'route'; dryRun = $DryRun; backupPath = $BackupPath; candidates = @($Candidates); packagedApps = @($PackagedApps); routed = @($Routed); skipped = @($Skipped); targetPath = $TargetPath; arguments = $Arguments }",
|
|
367
|
-
"$Result | ConvertTo-Json -Depth 6 -Compress",
|
|
368
|
-
].join("\r\n");
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* @param {ReturnType<typeof resolveAppLauncherPlan>} plan
|
|
373
|
-
*/
|
|
374
|
-
function createLinuxDesktopFile(plan) {
|
|
375
|
-
return [
|
|
376
|
-
"[Desktop Entry]",
|
|
377
|
-
"Type=Application",
|
|
378
|
-
`Name=${MANAGED_LAUNCHER_NAME}`,
|
|
379
|
-
"Comment=Launch Codex through codex-multi-auth runtime rotation",
|
|
380
|
-
`Exec=${quoteDesktopExec(plan.commandPath)} ${plan.commandArgs}`,
|
|
381
|
-
`Path=${plan.workingDirectory}`,
|
|
382
|
-
`Icon=${plan.iconPath}`,
|
|
383
|
-
"Terminal=false",
|
|
384
|
-
"Categories=Development;",
|
|
385
|
-
"StartupNotify=true",
|
|
386
|
-
"",
|
|
387
|
-
].join("\n");
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* @param {ReturnType<typeof resolveAppLauncherPlan>} plan
|
|
392
|
-
*/
|
|
393
|
-
function createMacInfoPlist(plan) {
|
|
394
|
-
return [
|
|
395
|
-
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
396
|
-
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
397
|
-
'<plist version="1.0">',
|
|
398
|
-
"<dict>",
|
|
399
|
-
" <key>CFBundleExecutable</key>",
|
|
400
|
-
" <string>Codex</string>",
|
|
401
|
-
" <key>CFBundleIdentifier</key>",
|
|
402
|
-
" <string>com.ndycode.codex-multi-auth.launcher</string>",
|
|
403
|
-
" <key>CFBundleName</key>",
|
|
404
|
-
` <string>${MANAGED_LAUNCHER_NAME}</string>`,
|
|
405
|
-
" <key>CFBundlePackageType</key>",
|
|
406
|
-
" <string>APPL</string>",
|
|
407
|
-
"</dict>",
|
|
408
|
-
"</plist>",
|
|
409
|
-
"",
|
|
410
|
-
].join("\n");
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* @param {ReturnType<typeof resolveAppLauncherPlan>} plan
|
|
415
|
-
*/
|
|
416
|
-
function createMacLauncherScript(plan) {
|
|
417
|
-
const args = Array.isArray(plan.commandArgv)
|
|
418
|
-
? plan.commandArgv.map(quotePosixShell).join(" ")
|
|
419
|
-
: plan.commandArgs;
|
|
420
|
-
return [
|
|
421
|
-
"#!/bin/sh",
|
|
422
|
-
`cd ${quotePosixShell(plan.workingDirectory)}`,
|
|
423
|
-
`exec ${quotePosixShell(plan.commandPath)} ${args}`,
|
|
424
|
-
"",
|
|
425
|
-
].join("\n");
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* @param {string} command
|
|
430
|
-
* @param {string[]} args
|
|
431
|
-
* @param {NodeJS.ProcessEnv} env
|
|
432
|
-
*/
|
|
433
|
-
function runCommand(command, args, env) {
|
|
434
|
-
return new Promise((resolve, reject) => {
|
|
435
|
-
const child = spawn(command, args, {
|
|
436
|
-
env,
|
|
437
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
438
|
-
windowsHide: true,
|
|
439
|
-
});
|
|
440
|
-
let stdout = "";
|
|
441
|
-
let stderr = "";
|
|
442
|
-
child.stdout?.setEncoding("utf8");
|
|
443
|
-
child.stderr?.setEncoding("utf8");
|
|
444
|
-
child.stdout?.on("data", (chunk) => {
|
|
445
|
-
stdout += chunk;
|
|
446
|
-
});
|
|
447
|
-
child.stderr?.on("data", (chunk) => {
|
|
448
|
-
stderr += chunk;
|
|
449
|
-
});
|
|
450
|
-
child.once("error", reject);
|
|
451
|
-
child.once("close", (code) => {
|
|
452
|
-
if (code === 0) {
|
|
453
|
-
resolve({ stdout, stderr });
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
reject(new Error(`${command} exited with ${code ?? "unknown"}: ${stderr.trim()}`));
|
|
457
|
-
});
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* @param {ReturnType<typeof resolveAppLauncherPlan>} plan
|
|
463
|
-
* @param {{ env: NodeJS.ProcessEnv, dryRun?: boolean, remove?: boolean }} options
|
|
464
|
-
*/
|
|
465
|
-
async function installWindowsShortcut(plan, options) {
|
|
466
|
-
const script = createWindowsShortcutPowerShellScript(plan, {
|
|
467
|
-
dryRun: options.dryRun,
|
|
468
|
-
remove: options.remove,
|
|
469
|
-
});
|
|
470
|
-
const powershell =
|
|
471
|
-
(options.env.SystemRoot ?? options.env.SYSTEMROOT ?? "C:\\Windows") +
|
|
472
|
-
"\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
|
|
473
|
-
const result = await runCommand(
|
|
474
|
-
powershell,
|
|
475
|
-
[
|
|
476
|
-
"-NoProfile",
|
|
477
|
-
"-ExecutionPolicy",
|
|
478
|
-
"Bypass",
|
|
479
|
-
"-Command",
|
|
480
|
-
script,
|
|
481
|
-
],
|
|
482
|
-
options.env,
|
|
483
|
-
);
|
|
484
|
-
const output = result.stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
|
|
485
|
-
if (!output) {
|
|
486
|
-
return { action: options.remove ? "restore" : "route", routed: [], restored: [], skipped: [] };
|
|
487
|
-
}
|
|
488
|
-
try {
|
|
489
|
-
return JSON.parse(output);
|
|
490
|
-
} catch (error) {
|
|
491
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
492
|
-
throw new Error(
|
|
493
|
-
`codex-multi-auth-app-launcher: unexpected powershell output (${detail}): ${result.stdout.trim().slice(-512)}`,
|
|
494
|
-
);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* @param {ReturnType<typeof resolveAppLauncherPlan>} plan
|
|
500
|
-
*/
|
|
501
|
-
async function installLinuxDesktopFile(plan) {
|
|
502
|
-
await withFileOperationRetry(() => mkdir(dirname(plan.launcherPath), { recursive: true }));
|
|
503
|
-
await withFileOperationRetry(() =>
|
|
504
|
-
writeFile(plan.launcherPath, createLinuxDesktopFile(plan), "utf8"),
|
|
505
|
-
);
|
|
506
|
-
await chmod(plan.launcherPath, 0o755);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/**
|
|
510
|
-
* @param {ReturnType<typeof resolveAppLauncherPlan>} plan
|
|
511
|
-
*/
|
|
512
|
-
async function installMacAppBundle(plan) {
|
|
513
|
-
const contentsDir = join(plan.launcherPath, "Contents");
|
|
514
|
-
const macosDir = join(contentsDir, "MacOS");
|
|
515
|
-
await withFileOperationRetry(() => mkdir(macosDir, { recursive: true }));
|
|
516
|
-
await withFileOperationRetry(() =>
|
|
517
|
-
writeFile(join(contentsDir, "Info.plist"), createMacInfoPlist(plan), "utf8"),
|
|
518
|
-
);
|
|
519
|
-
const launcherScriptPath = join(macosDir, "Codex");
|
|
520
|
-
await withFileOperationRetry(() =>
|
|
521
|
-
writeFile(launcherScriptPath, createMacLauncherScript(plan), "utf8"),
|
|
522
|
-
);
|
|
523
|
-
await chmod(launcherScriptPath, 0o755);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* @param {{
|
|
528
|
-
* env?: NodeJS.ProcessEnv,
|
|
529
|
-
* platform?: NodeJS.Platform,
|
|
530
|
-
* home?: string,
|
|
531
|
-
* moduleUrl?: string,
|
|
532
|
-
* dryRun?: boolean,
|
|
533
|
-
* remove?: boolean,
|
|
534
|
-
* log?: (message: string) => void,
|
|
535
|
-
* }} [options]
|
|
536
|
-
*/
|
|
537
|
-
export async function installCodexAppLauncher(options = {}) {
|
|
538
|
-
const env = options.env ?? process.env;
|
|
539
|
-
const plan = resolveAppLauncherPlan({
|
|
540
|
-
env,
|
|
541
|
-
platform: options.platform,
|
|
542
|
-
home: options.home,
|
|
543
|
-
moduleUrl: options.moduleUrl,
|
|
544
|
-
});
|
|
545
|
-
const log = options.log ?? console.log;
|
|
546
|
-
|
|
547
|
-
if (plan.platform === "win32") {
|
|
548
|
-
const result = await installWindowsShortcut(plan, {
|
|
549
|
-
env,
|
|
550
|
-
dryRun: options.dryRun,
|
|
551
|
-
remove: options.remove,
|
|
552
|
-
});
|
|
553
|
-
const routedCount = Array.isArray(result.routed) ? result.routed.length : 0;
|
|
554
|
-
const restoredCount = Array.isArray(result.restored) ? result.restored.length : 0;
|
|
555
|
-
const packagedAppCount = Array.isArray(result.packagedApps)
|
|
556
|
-
? result.packagedApps.length
|
|
557
|
-
: 0;
|
|
558
|
-
if (options.remove) {
|
|
559
|
-
const prefix = options.dryRun ? "[dry-run] Would restore" : "Restored";
|
|
560
|
-
log(`${prefix} ${restoredCount} Codex app shortcut(s) from ${plan.backupPath}`);
|
|
561
|
-
return plan;
|
|
562
|
-
}
|
|
563
|
-
if (routedCount === 0) {
|
|
564
|
-
const prefix = options.dryRun ? "[dry-run] No" : "No";
|
|
565
|
-
log(
|
|
566
|
-
`${prefix} existing Codex app shortcuts or taskbar pins found to route through codex-multi-auth.`,
|
|
567
|
-
);
|
|
568
|
-
if (packagedAppCount > 0) {
|
|
569
|
-
log(
|
|
570
|
-
`Detected ${packagedAppCount} packaged Codex app entry; packaged app entries cannot be retargeted without a persistent background router.`,
|
|
571
|
-
);
|
|
572
|
-
}
|
|
573
|
-
return plan;
|
|
574
|
-
}
|
|
575
|
-
const prefix = options.dryRun ? "[dry-run] Would route" : "Routed";
|
|
576
|
-
log(`${prefix} ${routedCount} existing Codex app shortcut(s) through codex-multi-auth`);
|
|
577
|
-
if (options.dryRun) {
|
|
578
|
-
log(`[dry-run] Target: ${plan.commandPath} ${plan.commandArgs}`);
|
|
579
|
-
}
|
|
580
|
-
return plan;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
if (options.remove) {
|
|
584
|
-
if (options.dryRun) {
|
|
585
|
-
log(`[dry-run] Would remove ${plan.launcherPath}`);
|
|
586
|
-
return plan;
|
|
587
|
-
}
|
|
588
|
-
await withFileOperationRetry(() => rm(plan.launcherPath, { recursive: true, force: true }));
|
|
589
|
-
log(`Removed ${MANAGED_LAUNCHER_NAME} app launcher: ${plan.launcherPath}`);
|
|
590
|
-
return plan;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
if (options.dryRun) {
|
|
594
|
-
log(`[dry-run] Would install ${MANAGED_LAUNCHER_NAME} app launcher: ${plan.launcherPath}`);
|
|
595
|
-
log(`[dry-run] Target: ${plan.commandPath} ${plan.commandArgs}`);
|
|
596
|
-
return plan;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
if (plan.platform === "darwin") {
|
|
600
|
-
await installMacAppBundle(plan);
|
|
601
|
-
} else {
|
|
602
|
-
await installLinuxDesktopFile(plan);
|
|
603
|
-
}
|
|
604
|
-
log(`Installed ${MANAGED_LAUNCHER_NAME} app launcher: ${plan.launcherPath}`);
|
|
605
|
-
return plan;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
function printHelp() {
|
|
609
|
-
console.log(
|
|
610
|
-
[
|
|
611
|
-
"Usage: codex-multi-auth-app-launcher [--remove] [--dry-run]",
|
|
612
|
-
"",
|
|
613
|
-
"Routes existing user-level Codex app shortcuts through codex-multi-auth on Windows.",
|
|
614
|
-
`On other platforms, installs a user-level ${MANAGED_LAUNCHER_NAME} app launcher that runs \`codex app\` through codex-multi-auth.`,
|
|
615
|
-
"",
|
|
616
|
-
"Options:",
|
|
617
|
-
" --remove Remove the managed launcher",
|
|
618
|
-
" --dry-run Print planned changes without writing",
|
|
619
|
-
" --help Show this help",
|
|
620
|
-
"",
|
|
621
|
-
].join("\n"),
|
|
622
|
-
);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
async function main() {
|
|
626
|
-
const args = new Set(process.argv.slice(2));
|
|
627
|
-
if (args.has("--help") || args.has("-h")) {
|
|
628
|
-
printHelp();
|
|
629
|
-
return 0;
|
|
630
|
-
}
|
|
631
|
-
await installCodexAppLauncher({
|
|
632
|
-
dryRun: args.has("--dry-run"),
|
|
633
|
-
remove: args.has("--remove"),
|
|
634
|
-
});
|
|
635
|
-
return 0;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
const isDirectRun = (() => {
|
|
639
|
-
try {
|
|
640
|
-
return resolve(process.argv[1] ?? "") === fileURLToPath(import.meta.url);
|
|
641
|
-
} catch {
|
|
642
|
-
return false;
|
|
643
|
-
}
|
|
644
|
-
})();
|
|
645
|
-
|
|
646
|
-
if (isDirectRun) {
|
|
647
|
-
main().catch((error) => {
|
|
648
|
-
console.error(
|
|
649
|
-
`Codex app launcher routing failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
650
|
-
);
|
|
651
|
-
process.exit(1);
|
|
652
|
-
});
|
|
653
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// @ts-check
|
|
4
|
+
|
|
5
|
+
import { chmod, mkdir, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import process from "node:process";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import { withFileOperationRetry } from "./install-codex-auth-utils.js";
|
|
12
|
+
|
|
13
|
+
const OFFICIAL_LAUNCHER_NAME = "Codex";
|
|
14
|
+
const MANAGED_LAUNCHER_NAME = "Codex Multi Auth";
|
|
15
|
+
const WINDOWS_SHORTCUT_NAME = `${OFFICIAL_LAUNCHER_NAME}.lnk`;
|
|
16
|
+
const LINUX_DESKTOP_FILE_NAME = "codex-multi-auth.desktop";
|
|
17
|
+
const MACOS_APP_NAME = `${MANAGED_LAUNCHER_NAME}.app`;
|
|
18
|
+
const WINDOWS_BACKUP_FILE_NAME = "app-shortcuts.json";
|
|
19
|
+
const MANAGED_SHORTCUT_DESCRIPTION =
|
|
20
|
+
"Launch Codex through codex-multi-auth runtime rotation";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} value
|
|
24
|
+
*/
|
|
25
|
+
function quotePowerShellSingle(value) {
|
|
26
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} value
|
|
31
|
+
*/
|
|
32
|
+
function encodePowerShellCommand(value) {
|
|
33
|
+
return Buffer.from(value, "utf16le").toString("base64");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {boolean} value
|
|
38
|
+
*/
|
|
39
|
+
function quotePowerShellBoolean(value) {
|
|
40
|
+
return value ? "$true" : "$false";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {string[]} values
|
|
45
|
+
*/
|
|
46
|
+
function quotePowerShellArray(values) {
|
|
47
|
+
if (values.length === 0) {
|
|
48
|
+
return "@()";
|
|
49
|
+
}
|
|
50
|
+
return `@(${values.map(quotePowerShellSingle).join(", ")})`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string} value
|
|
55
|
+
*/
|
|
56
|
+
function quoteDesktopExec(value) {
|
|
57
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {string} value
|
|
62
|
+
*/
|
|
63
|
+
function quotePosixShell(value) {
|
|
64
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {string[]} values
|
|
69
|
+
*/
|
|
70
|
+
function uniqueStrings(values) {
|
|
71
|
+
return [...new Set(values.filter((value) => value.trim().length > 0))];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {NodeJS.ProcessEnv} env
|
|
76
|
+
* @param {string} home
|
|
77
|
+
*/
|
|
78
|
+
function resolveWindowsStartMenuDir(env, home) {
|
|
79
|
+
const appData = (env.APPDATA ?? "").trim() || join(home, "AppData", "Roaming");
|
|
80
|
+
return join(appData, "Microsoft", "Windows", "Start Menu", "Programs");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {NodeJS.ProcessEnv} env
|
|
85
|
+
*/
|
|
86
|
+
function resolveWindowsPowerShellPath(env) {
|
|
87
|
+
const systemRoot =
|
|
88
|
+
(env.SystemRoot ?? env.SYSTEMROOT ?? "").trim() || "C:\\Windows";
|
|
89
|
+
return join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {NodeJS.ProcessEnv} env
|
|
94
|
+
* @param {string} home
|
|
95
|
+
*/
|
|
96
|
+
function resolveWindowsTaskbarPinnedDir(env, home) {
|
|
97
|
+
const appData = (env.APPDATA ?? "").trim() || join(home, "AppData", "Roaming");
|
|
98
|
+
return join(
|
|
99
|
+
appData,
|
|
100
|
+
"Microsoft",
|
|
101
|
+
"Internet Explorer",
|
|
102
|
+
"Quick Launch",
|
|
103
|
+
"User Pinned",
|
|
104
|
+
"TaskBar",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {NodeJS.ProcessEnv} env
|
|
110
|
+
* @param {string} home
|
|
111
|
+
*/
|
|
112
|
+
function resolveWindowsDesktopDirs(env, home) {
|
|
113
|
+
const configured = (env.CODEX_MULTI_AUTH_APP_LAUNCHER_WINDOWS_DESKTOP_DIR ?? "").trim();
|
|
114
|
+
const onedriveRoots = [
|
|
115
|
+
env.OneDrive,
|
|
116
|
+
env.OneDriveConsumer,
|
|
117
|
+
env.OneDriveCommercial,
|
|
118
|
+
].filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
119
|
+
return uniqueStrings([
|
|
120
|
+
configured,
|
|
121
|
+
...onedriveRoots.map((root) => join(String(root), "Desktop")),
|
|
122
|
+
join(home, "Desktop"),
|
|
123
|
+
]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {NodeJS.ProcessEnv} env
|
|
128
|
+
* @param {string} home
|
|
129
|
+
*/
|
|
130
|
+
function resolveCodexMultiAuthDir(env, home) {
|
|
131
|
+
return (env.CODEX_MULTI_AUTH_DIR ?? "").trim() || join(home, ".codex", "multi-auth");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @param {NodeJS.ProcessEnv} env
|
|
136
|
+
* @param {string} home
|
|
137
|
+
*/
|
|
138
|
+
function resolveLinuxApplicationsDir(env, home) {
|
|
139
|
+
const dataHome = (env.XDG_DATA_HOME ?? "").trim() || join(home, ".local", "share");
|
|
140
|
+
return join(dataHome, "applications");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {NodeJS.ProcessEnv} env
|
|
145
|
+
* @param {string} home
|
|
146
|
+
*/
|
|
147
|
+
function resolveMacApplicationsDir(env, home) {
|
|
148
|
+
return (env.CODEX_MULTI_AUTH_APP_LAUNCHER_MACOS_DIR ?? "").trim() || join(home, "Applications");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {string} moduleUrl
|
|
153
|
+
*/
|
|
154
|
+
function resolveCurrentScriptPath(moduleUrl) {
|
|
155
|
+
return fileURLToPath(moduleUrl);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {{
|
|
160
|
+
* nodePath: string,
|
|
161
|
+
* codexScriptPath: string,
|
|
162
|
+
* workingDirectory: string,
|
|
163
|
+
* }} params
|
|
164
|
+
*/
|
|
165
|
+
function createWindowsLauncherCommandArgs(params) {
|
|
166
|
+
const command = [
|
|
167
|
+
"$ErrorActionPreference = 'Stop'",
|
|
168
|
+
`Set-Location -LiteralPath ${quotePowerShellSingle(params.workingDirectory)}`,
|
|
169
|
+
`& ${quotePowerShellSingle(params.nodePath)} ${quotePowerShellSingle(params.codexScriptPath)} app`,
|
|
170
|
+
].join("; ");
|
|
171
|
+
return `-NoProfile -ExecutionPolicy Bypass -EncodedCommand ${encodePowerShellCommand(command)}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @param {{
|
|
176
|
+
* env?: NodeJS.ProcessEnv,
|
|
177
|
+
* platform?: NodeJS.Platform,
|
|
178
|
+
* home?: string,
|
|
179
|
+
* moduleUrl?: string,
|
|
180
|
+
* }} [options]
|
|
181
|
+
*/
|
|
182
|
+
export function resolveAppLauncherPlan(options = {}) {
|
|
183
|
+
const env = options.env ?? process.env;
|
|
184
|
+
const platform = options.platform ?? process.platform;
|
|
185
|
+
const home = options.home ?? homedir();
|
|
186
|
+
const moduleUrl = options.moduleUrl ?? import.meta.url;
|
|
187
|
+
const scriptPath = resolveCurrentScriptPath(moduleUrl);
|
|
188
|
+
const codexScriptPath = join(dirname(scriptPath), "codex.js");
|
|
189
|
+
const nodePath = process.execPath;
|
|
190
|
+
const commandArgv = [codexScriptPath, "app"];
|
|
191
|
+
|
|
192
|
+
if (platform === "win32") {
|
|
193
|
+
const startMenuDir = resolveWindowsStartMenuDir(env, home);
|
|
194
|
+
return {
|
|
195
|
+
platform,
|
|
196
|
+
mode: "route-existing",
|
|
197
|
+
launcherPath: join(startMenuDir, WINDOWS_SHORTCUT_NAME),
|
|
198
|
+
shortcutRoots: [
|
|
199
|
+
startMenuDir,
|
|
200
|
+
resolveWindowsTaskbarPinnedDir(env, home),
|
|
201
|
+
...resolveWindowsDesktopDirs(env, home),
|
|
202
|
+
],
|
|
203
|
+
backupPath: join(resolveCodexMultiAuthDir(env, home), WINDOWS_BACKUP_FILE_NAME),
|
|
204
|
+
commandPath: resolveWindowsPowerShellPath(env),
|
|
205
|
+
commandArgs: createWindowsLauncherCommandArgs({
|
|
206
|
+
nodePath,
|
|
207
|
+
codexScriptPath,
|
|
208
|
+
workingDirectory: home,
|
|
209
|
+
}),
|
|
210
|
+
commandArgv,
|
|
211
|
+
workingDirectory: home,
|
|
212
|
+
iconPath: nodePath,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (platform === "darwin") {
|
|
217
|
+
const appPath = join(resolveMacApplicationsDir(env, home), MACOS_APP_NAME);
|
|
218
|
+
return {
|
|
219
|
+
platform,
|
|
220
|
+
mode: "create-managed",
|
|
221
|
+
launcherPath: appPath,
|
|
222
|
+
commandPath: nodePath,
|
|
223
|
+
commandArgs: `"${codexScriptPath}" app`,
|
|
224
|
+
commandArgv,
|
|
225
|
+
workingDirectory: home,
|
|
226
|
+
iconPath: nodePath,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const desktopPath = join(resolveLinuxApplicationsDir(env, home), LINUX_DESKTOP_FILE_NAME);
|
|
231
|
+
return {
|
|
232
|
+
platform,
|
|
233
|
+
mode: "create-managed",
|
|
234
|
+
launcherPath: desktopPath,
|
|
235
|
+
commandPath: nodePath,
|
|
236
|
+
commandArgs: `"${codexScriptPath}" app %F`,
|
|
237
|
+
commandArgv: [codexScriptPath, "app", "%F"],
|
|
238
|
+
workingDirectory: home,
|
|
239
|
+
iconPath: "utilities-terminal",
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @param {ReturnType<typeof resolveAppLauncherPlan>} plan
|
|
245
|
+
* @param {{ dryRun?: boolean, remove?: boolean }} [options]
|
|
246
|
+
*/
|
|
247
|
+
export function createWindowsShortcutPowerShellScript(plan, options = {}) {
|
|
248
|
+
const shortcutRoots = Array.isArray(plan.shortcutRoots) ? plan.shortcutRoots : [];
|
|
249
|
+
const backupPath = typeof plan.backupPath === "string" ? plan.backupPath : "";
|
|
250
|
+
const dryRun = options.dryRun === true;
|
|
251
|
+
const remove = options.remove === true;
|
|
252
|
+
|
|
253
|
+
if (remove) {
|
|
254
|
+
return [
|
|
255
|
+
"$ErrorActionPreference = 'Stop'",
|
|
256
|
+
`$DryRun = ${quotePowerShellBoolean(dryRun)}`,
|
|
257
|
+
`$BackupPath = ${quotePowerShellSingle(backupPath)}`,
|
|
258
|
+
"$Restored = @()",
|
|
259
|
+
"$Skipped = @()",
|
|
260
|
+
"if (Test-Path -LiteralPath $BackupPath) {",
|
|
261
|
+
" $Raw = Get-Content -LiteralPath $BackupPath -Raw -Encoding UTF8",
|
|
262
|
+
" $Backups = @($Raw | ConvertFrom-Json)",
|
|
263
|
+
" $Shell = New-Object -ComObject WScript.Shell",
|
|
264
|
+
" foreach ($Backup in $Backups) {",
|
|
265
|
+
" if ($null -eq $Backup.Path -or -not (Test-Path -LiteralPath $Backup.Path)) {",
|
|
266
|
+
" if ($null -ne $Backup.Path) { $Skipped += [string]$Backup.Path }",
|
|
267
|
+
" continue",
|
|
268
|
+
" }",
|
|
269
|
+
" if (-not $DryRun) {",
|
|
270
|
+
" $Shortcut = $Shell.CreateShortcut([string]$Backup.Path)",
|
|
271
|
+
" $Shortcut.TargetPath = [string]$Backup.TargetPath",
|
|
272
|
+
" $Shortcut.Arguments = [string]$Backup.Arguments",
|
|
273
|
+
" $Shortcut.WorkingDirectory = [string]$Backup.WorkingDirectory",
|
|
274
|
+
" $Shortcut.IconLocation = [string]$Backup.IconLocation",
|
|
275
|
+
" $Shortcut.Description = [string]$Backup.Description",
|
|
276
|
+
" $Shortcut.Save()",
|
|
277
|
+
" }",
|
|
278
|
+
" $Restored += [string]$Backup.Path",
|
|
279
|
+
" }",
|
|
280
|
+
" if (-not $DryRun) { Remove-Item -LiteralPath $BackupPath -Force -ErrorAction SilentlyContinue }",
|
|
281
|
+
"}",
|
|
282
|
+
"$Result = [ordered]@{ action = 'restore'; dryRun = $DryRun; backupPath = $BackupPath; restored = @($Restored); skipped = @($Skipped) }",
|
|
283
|
+
"$Result | ConvertTo-Json -Depth 6 -Compress",
|
|
284
|
+
].join("\r\n");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return [
|
|
288
|
+
"$ErrorActionPreference = 'Stop'",
|
|
289
|
+
`$DryRun = ${quotePowerShellBoolean(dryRun)}`,
|
|
290
|
+
`$ShortcutRoots = ${quotePowerShellArray(shortcutRoots)}`,
|
|
291
|
+
"$ShellDesktop = [Environment]::GetFolderPath('Desktop')",
|
|
292
|
+
"if (-not [string]::IsNullOrWhiteSpace($ShellDesktop)) { $ShortcutRoots = @($ShortcutRoots + $ShellDesktop) | Sort-Object -Unique }",
|
|
293
|
+
`$BackupPath = ${quotePowerShellSingle(backupPath)}`,
|
|
294
|
+
`$ShortcutName = ${quotePowerShellSingle(OFFICIAL_LAUNCHER_NAME)}`,
|
|
295
|
+
`$TargetPath = ${quotePowerShellSingle(plan.commandPath)}`,
|
|
296
|
+
`$Arguments = ${quotePowerShellSingle(plan.commandArgs)}`,
|
|
297
|
+
`$WorkingDirectory = ${quotePowerShellSingle(plan.workingDirectory)}`,
|
|
298
|
+
`$ManagedDescription = ${quotePowerShellSingle(MANAGED_SHORTCUT_DESCRIPTION)}`,
|
|
299
|
+
"$Candidates = @()",
|
|
300
|
+
"$PackagedApps = @()",
|
|
301
|
+
"foreach ($Root in $ShortcutRoots) {",
|
|
302
|
+
" if (-not (Test-Path -LiteralPath $Root)) { continue }",
|
|
303
|
+
" $Candidates += Get-ChildItem -LiteralPath $Root -Filter '*.lnk' -File -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.BaseName -ieq $ShortcutName } | ForEach-Object { $_.FullName }",
|
|
304
|
+
"}",
|
|
305
|
+
"$Candidates = @($Candidates | Sort-Object -Unique)",
|
|
306
|
+
"try {",
|
|
307
|
+
" $AppsFolder = (New-Object -ComObject Shell.Application).Namespace('shell:AppsFolder')",
|
|
308
|
+
" if ($null -ne $AppsFolder) {",
|
|
309
|
+
" $PackagedApps = @($AppsFolder.Items() | Where-Object { $_.Name -ieq $ShortcutName } | ForEach-Object { [ordered]@{ Name = [string]$_.Name; Path = [string]$_.Path } })",
|
|
310
|
+
" }",
|
|
311
|
+
"} catch { $PackagedApps = @() }",
|
|
312
|
+
"$Shell = New-Object -ComObject WScript.Shell",
|
|
313
|
+
"$ExistingBackups = @()",
|
|
314
|
+
"if (Test-Path -LiteralPath $BackupPath) {",
|
|
315
|
+
" try {",
|
|
316
|
+
" $Raw = Get-Content -LiteralPath $BackupPath -Raw -Encoding UTF8",
|
|
317
|
+
" if ($Raw.Trim().Length -gt 0) { $ExistingBackups = @($Raw | ConvertFrom-Json) }",
|
|
318
|
+
" } catch { $ExistingBackups = @() }",
|
|
319
|
+
"}",
|
|
320
|
+
"$BackupByPath = @{}",
|
|
321
|
+
"$BackupsToWrite = New-Object System.Collections.ArrayList",
|
|
322
|
+
"foreach ($Backup in $ExistingBackups) {",
|
|
323
|
+
" if ($null -eq $Backup.Path) { continue }",
|
|
324
|
+
" $BackupByPath[[string]$Backup.Path] = $Backup",
|
|
325
|
+
" [void]$BackupsToWrite.Add($Backup)",
|
|
326
|
+
"}",
|
|
327
|
+
"$Routed = @()",
|
|
328
|
+
"$Skipped = @()",
|
|
329
|
+
"foreach ($Path in $Candidates) {",
|
|
330
|
+
" $Shortcut = $Shell.CreateShortcut($Path)",
|
|
331
|
+
" $ShortcutText = (($Shortcut.TargetPath, $Shortcut.Arguments, $Shortcut.Description) -join ' ')",
|
|
332
|
+
" if ($ShortcutText -notmatch '(?i)codex') {",
|
|
333
|
+
" $Skipped += $Path",
|
|
334
|
+
" continue",
|
|
335
|
+
" }",
|
|
336
|
+
" $AlreadyManaged = (([string]$Shortcut.Description) -eq $ManagedDescription) -or ((([string]$Shortcut.TargetPath) -ieq $TargetPath) -and (([string]$Shortcut.Arguments) -ieq $Arguments))",
|
|
337
|
+
" if (-not $BackupByPath.ContainsKey($Path) -and -not $AlreadyManaged) {",
|
|
338
|
+
" $IconLocation = [string]$Shortcut.IconLocation",
|
|
339
|
+
" if ([string]::IsNullOrWhiteSpace($IconLocation)) { $IconLocation = [string]$Shortcut.TargetPath }",
|
|
340
|
+
" $Backup = [ordered]@{",
|
|
341
|
+
" Path = [string]$Path",
|
|
342
|
+
" TargetPath = [string]$Shortcut.TargetPath",
|
|
343
|
+
" Arguments = [string]$Shortcut.Arguments",
|
|
344
|
+
" WorkingDirectory = [string]$Shortcut.WorkingDirectory",
|
|
345
|
+
" IconLocation = $IconLocation",
|
|
346
|
+
" Description = [string]$Shortcut.Description",
|
|
347
|
+
" }",
|
|
348
|
+
" [void]$BackupsToWrite.Add($Backup)",
|
|
349
|
+
" $BackupByPath[$Path] = $Backup",
|
|
350
|
+
" }",
|
|
351
|
+
" if (-not $DryRun) {",
|
|
352
|
+
" $Backup = $BackupByPath[$Path]",
|
|
353
|
+
" $Shortcut.TargetPath = $TargetPath",
|
|
354
|
+
" $Shortcut.Arguments = $Arguments",
|
|
355
|
+
" $Shortcut.WorkingDirectory = $WorkingDirectory",
|
|
356
|
+
" if ($null -ne $Backup -and $null -ne $Backup.IconLocation) { $Shortcut.IconLocation = [string]$Backup.IconLocation }",
|
|
357
|
+
" $Shortcut.Description = $ManagedDescription",
|
|
358
|
+
" $Shortcut.Save()",
|
|
359
|
+
" }",
|
|
360
|
+
" $Routed += $Path",
|
|
361
|
+
"}",
|
|
362
|
+
"if (-not $DryRun -and $Routed.Count -gt 0) {",
|
|
363
|
+
" New-Item -ItemType Directory -Force -Path (Split-Path -Parent $BackupPath) | Out-Null",
|
|
364
|
+
" @($BackupsToWrite) | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $BackupPath -Encoding UTF8",
|
|
365
|
+
"}",
|
|
366
|
+
"$Result = [ordered]@{ action = 'route'; dryRun = $DryRun; backupPath = $BackupPath; candidates = @($Candidates); packagedApps = @($PackagedApps); routed = @($Routed); skipped = @($Skipped); targetPath = $TargetPath; arguments = $Arguments }",
|
|
367
|
+
"$Result | ConvertTo-Json -Depth 6 -Compress",
|
|
368
|
+
].join("\r\n");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* @param {ReturnType<typeof resolveAppLauncherPlan>} plan
|
|
373
|
+
*/
|
|
374
|
+
function createLinuxDesktopFile(plan) {
|
|
375
|
+
return [
|
|
376
|
+
"[Desktop Entry]",
|
|
377
|
+
"Type=Application",
|
|
378
|
+
`Name=${MANAGED_LAUNCHER_NAME}`,
|
|
379
|
+
"Comment=Launch Codex through codex-multi-auth runtime rotation",
|
|
380
|
+
`Exec=${quoteDesktopExec(plan.commandPath)} ${plan.commandArgs}`,
|
|
381
|
+
`Path=${plan.workingDirectory}`,
|
|
382
|
+
`Icon=${plan.iconPath}`,
|
|
383
|
+
"Terminal=false",
|
|
384
|
+
"Categories=Development;",
|
|
385
|
+
"StartupNotify=true",
|
|
386
|
+
"",
|
|
387
|
+
].join("\n");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* @param {ReturnType<typeof resolveAppLauncherPlan>} plan
|
|
392
|
+
*/
|
|
393
|
+
function createMacInfoPlist(plan) {
|
|
394
|
+
return [
|
|
395
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
396
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
397
|
+
'<plist version="1.0">',
|
|
398
|
+
"<dict>",
|
|
399
|
+
" <key>CFBundleExecutable</key>",
|
|
400
|
+
" <string>Codex</string>",
|
|
401
|
+
" <key>CFBundleIdentifier</key>",
|
|
402
|
+
" <string>com.ndycode.codex-multi-auth.launcher</string>",
|
|
403
|
+
" <key>CFBundleName</key>",
|
|
404
|
+
` <string>${MANAGED_LAUNCHER_NAME}</string>`,
|
|
405
|
+
" <key>CFBundlePackageType</key>",
|
|
406
|
+
" <string>APPL</string>",
|
|
407
|
+
"</dict>",
|
|
408
|
+
"</plist>",
|
|
409
|
+
"",
|
|
410
|
+
].join("\n");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* @param {ReturnType<typeof resolveAppLauncherPlan>} plan
|
|
415
|
+
*/
|
|
416
|
+
function createMacLauncherScript(plan) {
|
|
417
|
+
const args = Array.isArray(plan.commandArgv)
|
|
418
|
+
? plan.commandArgv.map(quotePosixShell).join(" ")
|
|
419
|
+
: plan.commandArgs;
|
|
420
|
+
return [
|
|
421
|
+
"#!/bin/sh",
|
|
422
|
+
`cd ${quotePosixShell(plan.workingDirectory)}`,
|
|
423
|
+
`exec ${quotePosixShell(plan.commandPath)} ${args}`,
|
|
424
|
+
"",
|
|
425
|
+
].join("\n");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* @param {string} command
|
|
430
|
+
* @param {string[]} args
|
|
431
|
+
* @param {NodeJS.ProcessEnv} env
|
|
432
|
+
*/
|
|
433
|
+
function runCommand(command, args, env) {
|
|
434
|
+
return new Promise((resolve, reject) => {
|
|
435
|
+
const child = spawn(command, args, {
|
|
436
|
+
env,
|
|
437
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
438
|
+
windowsHide: true,
|
|
439
|
+
});
|
|
440
|
+
let stdout = "";
|
|
441
|
+
let stderr = "";
|
|
442
|
+
child.stdout?.setEncoding("utf8");
|
|
443
|
+
child.stderr?.setEncoding("utf8");
|
|
444
|
+
child.stdout?.on("data", (chunk) => {
|
|
445
|
+
stdout += chunk;
|
|
446
|
+
});
|
|
447
|
+
child.stderr?.on("data", (chunk) => {
|
|
448
|
+
stderr += chunk;
|
|
449
|
+
});
|
|
450
|
+
child.once("error", reject);
|
|
451
|
+
child.once("close", (code) => {
|
|
452
|
+
if (code === 0) {
|
|
453
|
+
resolve({ stdout, stderr });
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
reject(new Error(`${command} exited with ${code ?? "unknown"}: ${stderr.trim()}`));
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* @param {ReturnType<typeof resolveAppLauncherPlan>} plan
|
|
463
|
+
* @param {{ env: NodeJS.ProcessEnv, dryRun?: boolean, remove?: boolean }} options
|
|
464
|
+
*/
|
|
465
|
+
async function installWindowsShortcut(plan, options) {
|
|
466
|
+
const script = createWindowsShortcutPowerShellScript(plan, {
|
|
467
|
+
dryRun: options.dryRun,
|
|
468
|
+
remove: options.remove,
|
|
469
|
+
});
|
|
470
|
+
const powershell =
|
|
471
|
+
(options.env.SystemRoot ?? options.env.SYSTEMROOT ?? "C:\\Windows") +
|
|
472
|
+
"\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
|
|
473
|
+
const result = await runCommand(
|
|
474
|
+
powershell,
|
|
475
|
+
[
|
|
476
|
+
"-NoProfile",
|
|
477
|
+
"-ExecutionPolicy",
|
|
478
|
+
"Bypass",
|
|
479
|
+
"-Command",
|
|
480
|
+
script,
|
|
481
|
+
],
|
|
482
|
+
options.env,
|
|
483
|
+
);
|
|
484
|
+
const output = result.stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
|
|
485
|
+
if (!output) {
|
|
486
|
+
return { action: options.remove ? "restore" : "route", routed: [], restored: [], skipped: [] };
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
return JSON.parse(output);
|
|
490
|
+
} catch (error) {
|
|
491
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
492
|
+
throw new Error(
|
|
493
|
+
`codex-multi-auth-app-launcher: unexpected powershell output (${detail}): ${result.stdout.trim().slice(-512)}`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* @param {ReturnType<typeof resolveAppLauncherPlan>} plan
|
|
500
|
+
*/
|
|
501
|
+
async function installLinuxDesktopFile(plan) {
|
|
502
|
+
await withFileOperationRetry(() => mkdir(dirname(plan.launcherPath), { recursive: true }));
|
|
503
|
+
await withFileOperationRetry(() =>
|
|
504
|
+
writeFile(plan.launcherPath, createLinuxDesktopFile(plan), "utf8"),
|
|
505
|
+
);
|
|
506
|
+
await chmod(plan.launcherPath, 0o755);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* @param {ReturnType<typeof resolveAppLauncherPlan>} plan
|
|
511
|
+
*/
|
|
512
|
+
async function installMacAppBundle(plan) {
|
|
513
|
+
const contentsDir = join(plan.launcherPath, "Contents");
|
|
514
|
+
const macosDir = join(contentsDir, "MacOS");
|
|
515
|
+
await withFileOperationRetry(() => mkdir(macosDir, { recursive: true }));
|
|
516
|
+
await withFileOperationRetry(() =>
|
|
517
|
+
writeFile(join(contentsDir, "Info.plist"), createMacInfoPlist(plan), "utf8"),
|
|
518
|
+
);
|
|
519
|
+
const launcherScriptPath = join(macosDir, "Codex");
|
|
520
|
+
await withFileOperationRetry(() =>
|
|
521
|
+
writeFile(launcherScriptPath, createMacLauncherScript(plan), "utf8"),
|
|
522
|
+
);
|
|
523
|
+
await chmod(launcherScriptPath, 0o755);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* @param {{
|
|
528
|
+
* env?: NodeJS.ProcessEnv,
|
|
529
|
+
* platform?: NodeJS.Platform,
|
|
530
|
+
* home?: string,
|
|
531
|
+
* moduleUrl?: string,
|
|
532
|
+
* dryRun?: boolean,
|
|
533
|
+
* remove?: boolean,
|
|
534
|
+
* log?: (message: string) => void,
|
|
535
|
+
* }} [options]
|
|
536
|
+
*/
|
|
537
|
+
export async function installCodexAppLauncher(options = {}) {
|
|
538
|
+
const env = options.env ?? process.env;
|
|
539
|
+
const plan = resolveAppLauncherPlan({
|
|
540
|
+
env,
|
|
541
|
+
platform: options.platform,
|
|
542
|
+
home: options.home,
|
|
543
|
+
moduleUrl: options.moduleUrl,
|
|
544
|
+
});
|
|
545
|
+
const log = options.log ?? console.log;
|
|
546
|
+
|
|
547
|
+
if (plan.platform === "win32") {
|
|
548
|
+
const result = await installWindowsShortcut(plan, {
|
|
549
|
+
env,
|
|
550
|
+
dryRun: options.dryRun,
|
|
551
|
+
remove: options.remove,
|
|
552
|
+
});
|
|
553
|
+
const routedCount = Array.isArray(result.routed) ? result.routed.length : 0;
|
|
554
|
+
const restoredCount = Array.isArray(result.restored) ? result.restored.length : 0;
|
|
555
|
+
const packagedAppCount = Array.isArray(result.packagedApps)
|
|
556
|
+
? result.packagedApps.length
|
|
557
|
+
: 0;
|
|
558
|
+
if (options.remove) {
|
|
559
|
+
const prefix = options.dryRun ? "[dry-run] Would restore" : "Restored";
|
|
560
|
+
log(`${prefix} ${restoredCount} Codex app shortcut(s) from ${plan.backupPath}`);
|
|
561
|
+
return plan;
|
|
562
|
+
}
|
|
563
|
+
if (routedCount === 0) {
|
|
564
|
+
const prefix = options.dryRun ? "[dry-run] No" : "No";
|
|
565
|
+
log(
|
|
566
|
+
`${prefix} existing Codex app shortcuts or taskbar pins found to route through codex-multi-auth.`,
|
|
567
|
+
);
|
|
568
|
+
if (packagedAppCount > 0) {
|
|
569
|
+
log(
|
|
570
|
+
`Detected ${packagedAppCount} packaged Codex app entry; packaged app entries cannot be retargeted without a persistent background router.`,
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
return plan;
|
|
574
|
+
}
|
|
575
|
+
const prefix = options.dryRun ? "[dry-run] Would route" : "Routed";
|
|
576
|
+
log(`${prefix} ${routedCount} existing Codex app shortcut(s) through codex-multi-auth`);
|
|
577
|
+
if (options.dryRun) {
|
|
578
|
+
log(`[dry-run] Target: ${plan.commandPath} ${plan.commandArgs}`);
|
|
579
|
+
}
|
|
580
|
+
return plan;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (options.remove) {
|
|
584
|
+
if (options.dryRun) {
|
|
585
|
+
log(`[dry-run] Would remove ${plan.launcherPath}`);
|
|
586
|
+
return plan;
|
|
587
|
+
}
|
|
588
|
+
await withFileOperationRetry(() => rm(plan.launcherPath, { recursive: true, force: true }));
|
|
589
|
+
log(`Removed ${MANAGED_LAUNCHER_NAME} app launcher: ${plan.launcherPath}`);
|
|
590
|
+
return plan;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (options.dryRun) {
|
|
594
|
+
log(`[dry-run] Would install ${MANAGED_LAUNCHER_NAME} app launcher: ${plan.launcherPath}`);
|
|
595
|
+
log(`[dry-run] Target: ${plan.commandPath} ${plan.commandArgs}`);
|
|
596
|
+
return plan;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (plan.platform === "darwin") {
|
|
600
|
+
await installMacAppBundle(plan);
|
|
601
|
+
} else {
|
|
602
|
+
await installLinuxDesktopFile(plan);
|
|
603
|
+
}
|
|
604
|
+
log(`Installed ${MANAGED_LAUNCHER_NAME} app launcher: ${plan.launcherPath}`);
|
|
605
|
+
return plan;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function printHelp() {
|
|
609
|
+
console.log(
|
|
610
|
+
[
|
|
611
|
+
"Usage: codex-multi-auth-app-launcher [--remove] [--dry-run]",
|
|
612
|
+
"",
|
|
613
|
+
"Routes existing user-level Codex app shortcuts through codex-multi-auth on Windows.",
|
|
614
|
+
`On other platforms, installs a user-level ${MANAGED_LAUNCHER_NAME} app launcher that runs \`codex app\` through codex-multi-auth.`,
|
|
615
|
+
"",
|
|
616
|
+
"Options:",
|
|
617
|
+
" --remove Remove the managed launcher",
|
|
618
|
+
" --dry-run Print planned changes without writing",
|
|
619
|
+
" --help Show this help",
|
|
620
|
+
"",
|
|
621
|
+
].join("\n"),
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async function main() {
|
|
626
|
+
const args = new Set(process.argv.slice(2));
|
|
627
|
+
if (args.has("--help") || args.has("-h")) {
|
|
628
|
+
printHelp();
|
|
629
|
+
return 0;
|
|
630
|
+
}
|
|
631
|
+
await installCodexAppLauncher({
|
|
632
|
+
dryRun: args.has("--dry-run"),
|
|
633
|
+
remove: args.has("--remove"),
|
|
634
|
+
});
|
|
635
|
+
return 0;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const isDirectRun = (() => {
|
|
639
|
+
try {
|
|
640
|
+
return resolve(process.argv[1] ?? "") === fileURLToPath(import.meta.url);
|
|
641
|
+
} catch {
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
})();
|
|
645
|
+
|
|
646
|
+
if (isDirectRun) {
|
|
647
|
+
main().catch((error) => {
|
|
648
|
+
console.error(
|
|
649
|
+
`Codex app launcher routing failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
650
|
+
);
|
|
651
|
+
process.exit(1);
|
|
652
|
+
});
|
|
653
|
+
}
|