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.
Files changed (79) hide show
  1. package/.codex-plugin/plugin.json +9 -9
  2. package/LICENSE +21 -21
  3. package/README.md +149 -148
  4. package/assets/codex-multi-auth-icon.svg +39 -39
  5. package/assets/readme-hero.svg +31 -31
  6. package/config/README.md +31 -31
  7. package/config/codex-legacy.json +1021 -1021
  8. package/config/codex-modern.json +347 -347
  9. package/config/minimal-codex.json +15 -15
  10. package/dist/index.js +1 -1
  11. package/dist/lib/accounts/rate-limits.js +5 -0
  12. package/dist/lib/accounts.js +5 -4
  13. package/dist/lib/auth/account-access.js +2 -2
  14. package/dist/lib/codex-cli/version.d.ts +22 -0
  15. package/dist/lib/codex-cli/version.js +78 -0
  16. package/dist/lib/codex-manager/commands/limits.d.ts +2 -0
  17. package/dist/lib/codex-manager/commands/limits.js +31 -0
  18. package/dist/lib/codex-manager/commands/status.js +1 -1
  19. package/dist/lib/codex-manager.js +2 -0
  20. package/dist/lib/constants.d.ts +2 -0
  21. package/dist/lib/constants.js +2 -0
  22. package/dist/lib/context-budget/model-context-windows.d.ts +1 -1
  23. package/dist/lib/context-budget/model-context-windows.js +4 -0
  24. package/dist/lib/context-budget-response.js +8 -8
  25. package/dist/lib/context-overflow.js +10 -10
  26. package/dist/lib/luna-reserve.d.ts +26 -0
  27. package/dist/lib/luna-reserve.js +68 -0
  28. package/dist/lib/oauth-success.html +125 -125
  29. package/dist/lib/policy/runtime-policy.js +14 -0
  30. package/dist/lib/prompts/codex-host-bridge.js +138 -138
  31. package/dist/lib/prompts/codex.js +79 -79
  32. package/dist/lib/request/helpers/model-map.d.ts +1 -1
  33. package/dist/lib/request/helpers/model-map.js +12 -0
  34. package/dist/lib/request/response-outcome.d.ts +1 -1
  35. package/dist/lib/request/response-outcome.js +4 -1
  36. package/dist/lib/runtime/account-status.js +2 -1
  37. package/dist/lib/runtime/luna-reserve-usage.d.ts +9 -0
  38. package/dist/lib/runtime/luna-reserve-usage.js +68 -0
  39. package/dist/lib/runtime/model-discovery-status.d.ts +6 -2
  40. package/dist/lib/runtime/model-discovery-status.js +13 -5
  41. package/dist/lib/runtime/reset-credit-routing.js +4 -1
  42. package/dist/lib/runtime/rotation-server-types.d.ts +2 -0
  43. package/dist/lib/runtime/runtime-observability.d.ts +2 -0
  44. package/dist/lib/runtime/runtime-observability.js +1 -0
  45. package/dist/lib/runtime-rotation-proxy.js +200 -32
  46. package/dist/lib/usage/pricing.d.ts +1 -1
  47. package/dist/lib/usage/pricing.js +4 -0
  48. package/dist/lib/usage/usage-extraction.d.ts +2 -0
  49. package/dist/lib/usage/usage-extraction.js +25 -1
  50. package/dist/lib/utils.d.ts +1 -1
  51. package/dist/lib/utils.js +1 -1
  52. package/node_modules/@codex-ai/plugin/dist/index.d.ts +2 -2
  53. package/node_modules/@codex-ai/plugin/dist/index.js +2 -2
  54. package/node_modules/@codex-ai/plugin/dist/tool.d.ts +42 -42
  55. package/node_modules/@codex-ai/plugin/dist/tool.js +29 -29
  56. package/node_modules/@codex-ai/plugin/package.json +9 -9
  57. package/node_modules/@codex-ai/sdk/dist/index.d.ts +4 -4
  58. package/node_modules/@codex-ai/sdk/dist/index.js +2 -2
  59. package/node_modules/@codex-ai/sdk/package.json +8 -8
  60. package/package.json +207 -207
  61. package/scripts/codex-app-launcher.js +653 -653
  62. package/scripts/codex-app-router.js +374 -373
  63. package/scripts/codex-bin-resolver.js +332 -332
  64. package/scripts/codex-multi-auth.js +65 -65
  65. package/scripts/codex-routing.js +62 -62
  66. package/scripts/codex.js +7006 -7005
  67. package/scripts/install-codex-auth-utils.js +213 -213
  68. package/scripts/mcodex.js +367 -367
  69. package/scripts/postinstall.js +114 -114
  70. package/scripts/preuninstall.js +265 -265
  71. package/skills/codex-auth-setup/SKILL.md +73 -73
  72. package/vendor/codex-ai-plugin/dist/index.d.ts +2 -2
  73. package/vendor/codex-ai-plugin/dist/index.js +2 -2
  74. package/vendor/codex-ai-plugin/dist/tool.d.ts +42 -42
  75. package/vendor/codex-ai-plugin/dist/tool.js +29 -29
  76. package/vendor/codex-ai-plugin/package.json +9 -9
  77. package/vendor/codex-ai-sdk/dist/index.d.ts +4 -4
  78. package/vendor/codex-ai-sdk/dist/index.js +2 -2
  79. 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
+ }