@bitkyc08/opencodex 2.52.0 → 2.53.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 (236) hide show
  1. package/gui/dist/assets/index-BBOZWGB6.css +1 -0
  2. package/gui/dist/assets/index-BlO4Yl6q.js +128 -0
  3. package/gui/dist/index.html +2 -2
  4. package/native/remote-workspace-helper/Cargo.lock +130 -0
  5. package/native/remote-workspace-helper/Cargo.toml +24 -0
  6. package/native/remote-workspace-helper/src/main.rs +49 -0
  7. package/native/remote-workspace-helper/src/protocol.rs +246 -0
  8. package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
  9. package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
  10. package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
  11. package/package.json +6 -1
  12. package/src/adapters/anthropic-image-normalize.ts +30 -2
  13. package/src/adapters/anthropic.ts +1 -1
  14. package/src/adapters/base.ts +8 -2
  15. package/src/adapters/cursor/cursor-errors.ts +12 -0
  16. package/src/adapters/cursor/thread-continuity.ts +93 -0
  17. package/src/adapters/cursor.ts +104 -73
  18. package/src/adapters/devin/cloud-direct/chat.ts +312 -23
  19. package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
  20. package/src/adapters/devin/live-models.ts +70 -3
  21. package/src/adapters/devin.ts +281 -21
  22. package/src/adapters/google-wire-compiler.ts +14 -6
  23. package/src/adapters/google.ts +22 -8
  24. package/src/adapters/kiro/adapter.ts +316 -0
  25. package/src/adapters/kiro/conversation.ts +136 -0
  26. package/src/adapters/kiro/payload.ts +432 -0
  27. package/src/adapters/kiro/reasoning.ts +56 -0
  28. package/src/adapters/kiro/stream.ts +1153 -0
  29. package/src/adapters/kiro/usage.ts +223 -0
  30. package/src/adapters/kiro/wire.ts +76 -0
  31. package/src/adapters/kiro.ts +8 -2319
  32. package/src/adapters/mimo-free.ts +1 -1
  33. package/src/adapters/openai-chat-images.ts +101 -0
  34. package/src/adapters/openai-chat.ts +201 -181
  35. package/src/adapters/openai-responses.ts +92 -224
  36. package/src/adapters/registry.ts +0 -7
  37. package/src/adapters/run-turn-queue.ts +13 -6
  38. package/src/bridge.ts +14 -15
  39. package/src/chat/inbound.ts +29 -4
  40. package/src/chat/outbound.ts +145 -107
  41. package/src/claude/desktop-profile.ts +4 -6
  42. package/src/cli/account-api.ts +14 -0
  43. package/src/cli/account-extended.ts +1 -1
  44. package/src/cli/account-history.ts +60 -0
  45. package/src/cli/account-main.ts +80 -0
  46. package/src/cli/account.ts +11 -3
  47. package/src/cli/capabilities.ts +113 -0
  48. package/src/cli/catalog.ts +109 -0
  49. package/src/cli/dispatch.ts +9 -0
  50. package/src/cli/help.ts +2 -0
  51. package/src/cli/index.ts +2 -2
  52. package/src/cli/observe.ts +28 -1
  53. package/src/cli/opencode.ts +42 -8
  54. package/src/cli/provider-runtime.ts +11 -1
  55. package/src/cli/provider.ts +22 -2
  56. package/src/cli/registry.ts +21 -0
  57. package/src/cli/remote-workspace.ts +154 -0
  58. package/src/cli/status.ts +39 -7
  59. package/src/cli/usage-report.ts +14 -2
  60. package/src/client/hub-client.ts +34 -0
  61. package/src/client/hub-state.ts +9 -1
  62. package/src/codex/account-store.ts +78 -0
  63. package/src/codex/auth-api.ts +81 -54
  64. package/src/codex/auth-context.ts +45 -16
  65. package/src/codex/catalog/effort.ts +1 -1
  66. package/src/codex/catalog/metadata.ts +3 -6
  67. package/src/codex/catalog/native-models.ts +4 -4
  68. package/src/codex/catalog/parsing.ts +2 -20
  69. package/src/codex/catalog/provider-fetch.ts +10 -1
  70. package/src/codex/catalog/remote.ts +233 -0
  71. package/src/codex/catalog/sync.ts +403 -35
  72. package/src/codex/convergence.ts +1 -1
  73. package/src/codex/history-manifest.ts +36 -0
  74. package/src/codex/history-provider.ts +32 -5
  75. package/src/codex/inject.ts +9 -0
  76. package/src/codex/main-account.ts +113 -0
  77. package/src/codex/main-device-reauth-api.ts +89 -0
  78. package/src/codex/main-device-reauth.ts +217 -0
  79. package/src/codex/native-residue.ts +9 -2
  80. package/src/codex/quota-auto-refresh.ts +3 -2
  81. package/src/codex/quota-capacity.ts +98 -0
  82. package/src/codex/quota-history.ts +160 -0
  83. package/src/codex/quota-types.ts +8 -0
  84. package/src/codex/quota.ts +118 -91
  85. package/src/codex/refresh.ts +2 -1
  86. package/src/codex/routing.ts +90 -17
  87. package/src/codex/sync.ts +33 -4
  88. package/src/combos/request.ts +19 -1
  89. package/src/config/multi-agent-surface.ts +61 -0
  90. package/src/config/provider-validation.ts +176 -0
  91. package/src/config.ts +213 -11
  92. package/src/generated/compatibility-version.json +436 -168
  93. package/src/images/loop.ts +119 -36
  94. package/src/lib/admission.ts +12 -6
  95. package/src/lib/redact.ts +7 -0
  96. package/src/lib/translator-budget.ts +4 -3
  97. package/src/lib/windows-atomic-replace.ts +1 -0
  98. package/src/lib/windows-elevation.ts +1 -1
  99. package/src/oauth/chatgpt-device.ts +62 -5
  100. package/src/oauth/devin/cli-import.ts +130 -0
  101. package/src/oauth/devin.ts +63 -8
  102. package/src/oauth/index.ts +29 -14
  103. package/src/oauth/kiro.ts +18 -6
  104. package/src/oauth/login-cli.ts +9 -1
  105. package/src/oauth/meta-muse-device.ts +464 -0
  106. package/src/oauth/meta-muse.ts +123 -32
  107. package/src/oauth/pool-kernel.ts +9 -0
  108. package/src/oauth/pool-settings-capability.ts +2 -2
  109. package/src/oauth/store.ts +57 -0
  110. package/src/oauth/types.ts +31 -0
  111. package/src/providers/derive.ts +13 -3
  112. package/src/providers/devin-cli-authmode-migration.ts +57 -35
  113. package/src/providers/devin-provider-merge-migration.ts +240 -0
  114. package/src/providers/muse-key-quota.ts +117 -0
  115. package/src/providers/muse-subscription-usage.ts +14 -2
  116. package/src/providers/openai-sidecar.ts +25 -3
  117. package/src/providers/opencode-zen-rate-limit.ts +58 -0
  118. package/src/providers/provider-id-rewrite.ts +20 -5
  119. package/src/providers/quota-types.ts +12 -0
  120. package/src/providers/quota.ts +143 -102
  121. package/src/providers/reasoning-metadata.ts +543 -0
  122. package/src/providers/registry.ts +80 -49
  123. package/src/reasoning-effort.ts +26 -2
  124. package/src/remote/hub-usage.ts +32 -0
  125. package/src/remote-control/index.ts +192 -41
  126. package/src/remote-control/workspace-activation.ts +9 -0
  127. package/src/remote-control/workspace-agent-connection.ts +366 -0
  128. package/src/remote-control/workspace-claude-runtime.ts +243 -0
  129. package/src/remote-control/workspace-codex-runtime.ts +531 -0
  130. package/src/remote-control/workspace-codex-sandbox.ts +115 -0
  131. package/src/remote-control/workspace-command-runner.ts +748 -0
  132. package/src/remote-control/workspace-coordinator.ts +231 -0
  133. package/src/remote-control/workspace-device.ts +585 -0
  134. package/src/remote-control/workspace-executable.ts +43 -0
  135. package/src/remote-control/workspace-executor.ts +397 -0
  136. package/src/remote-control/workspace-hub.ts +519 -0
  137. package/src/remote-control/workspace-pi-runtime.ts +382 -0
  138. package/src/remote-control/workspace-process.ts +129 -0
  139. package/src/remote-control/workspace-rpc.ts +304 -0
  140. package/src/remote-control/workspace-runtime.ts +60 -0
  141. package/src/remote-control/workspace-secret-store.ts +39 -0
  142. package/src/remote-control/workspace-sessions.ts +799 -0
  143. package/src/remote-control/workspace-tool-bridge.ts +192 -0
  144. package/src/responses/code-mode-helper-compat.ts +22 -3
  145. package/src/responses/hosted-tool-policy.ts +0 -1
  146. package/src/responses/muse-tool-name-alias.ts +379 -0
  147. package/src/responses/plaintext-v2-agent-messages.ts +902 -0
  148. package/src/router.ts +7 -0
  149. package/src/routing/compatibility/behavior.ts +0 -1
  150. package/src/server/audio-client.ts +64 -0
  151. package/src/server/audio-dictation.ts +91 -0
  152. package/src/server/audio-live.ts +185 -0
  153. package/src/server/audio-transcriptions.ts +183 -0
  154. package/src/server/audio-upstream.ts +153 -0
  155. package/src/server/auth-cors.ts +61 -2
  156. package/src/server/chat-completions.ts +1 -1
  157. package/src/server/chat-native-sse.ts +92 -48
  158. package/src/server/chat-native.ts +37 -15
  159. package/src/server/hub-usage.ts +57 -0
  160. package/src/server/images.ts +4 -0
  161. package/src/server/index.ts +722 -57
  162. package/src/server/lifecycle.ts +5 -6
  163. package/src/server/live-call-bindings.ts +60 -0
  164. package/src/server/live.ts +12 -1
  165. package/src/server/management/agent-settings-routes.ts +25 -4
  166. package/src/server/management/api-access.ts +37 -0
  167. package/src/server/management/api-key-usage.ts +7 -2
  168. package/src/server/management/config-routes.ts +1 -18
  169. package/src/server/management/context.ts +15 -0
  170. package/src/server/management/logs-usage-routes.ts +2 -0
  171. package/src/server/management/oauth-account-routes.ts +39 -12
  172. package/src/server/management/provider-routes.ts +125 -2
  173. package/src/server/management/remote-workspace-routes.ts +140 -0
  174. package/src/server/management/route-registry.ts +15 -0
  175. package/src/server/management/usage-aggregate-cache.ts +14 -15
  176. package/src/server/management/usage-summary-cache.ts +2 -0
  177. package/src/server/management-api.ts +23 -0
  178. package/src/server/ports.ts +17 -0
  179. package/src/server/relay-eager.ts +4 -1
  180. package/src/server/relay.ts +70 -10
  181. package/src/server/request-decompress.ts +6 -3
  182. package/src/server/responses/agent-task-recovery.ts +25 -32
  183. package/src/server/responses/codex-auth-error.ts +11 -0
  184. package/src/server/responses/codex-ws-exchange.ts +52 -3
  185. package/src/server/responses/codex-ws-wire.ts +55 -0
  186. package/src/server/responses/compact.ts +9 -1
  187. package/src/server/responses/core.ts +337 -73
  188. package/src/server/responses/encrypted-payload.ts +45 -2
  189. package/src/server/responses/ws-upstream.ts +4 -1
  190. package/src/server/responses-self-named-namespace-scrub.ts +1 -3
  191. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  192. package/src/server/search.ts +3 -0
  193. package/src/server/sse-payload-rewrite.ts +136 -51
  194. package/src/server/ws-bridge.ts +35 -1
  195. package/src/service/cli.ts +372 -0
  196. package/src/service/diagnostics.ts +340 -0
  197. package/src/service/guards.ts +303 -0
  198. package/src/service/health.ts +222 -0
  199. package/src/service/launchd.ts +853 -0
  200. package/src/service/orchestration.ts +617 -0
  201. package/src/service/repair.ts +334 -0
  202. package/src/service/state.ts +363 -0
  203. package/src/service/systemd.ts +229 -0
  204. package/src/service/windows-ops.ts +690 -0
  205. package/src/service/windows-scheduler.ts +769 -0
  206. package/src/service/windows-taskxml.ts +613 -0
  207. package/src/service.ts +22 -5550
  208. package/src/storage/cleanup/db.ts +258 -0
  209. package/src/storage/cleanup/execute.ts +358 -0
  210. package/src/storage/cleanup/paths.ts +189 -0
  211. package/src/storage/cleanup/pending.ts +140 -0
  212. package/src/storage/cleanup/preview.ts +292 -0
  213. package/src/storage/cleanup/reconcile.ts +347 -0
  214. package/src/storage/cleanup/restore.ts +932 -0
  215. package/src/storage/cleanup/satellite.ts +474 -0
  216. package/src/storage/cleanup/staging.ts +129 -0
  217. package/src/storage/cleanup/types.ts +98 -0
  218. package/src/storage/cleanup.ts +49 -3127
  219. package/src/types/accounts.ts +2 -0
  220. package/src/types/config.ts +13 -12
  221. package/src/types/provider.ts +37 -0
  222. package/src/types/request.ts +2 -0
  223. package/src/types/tools.ts +17 -5
  224. package/src/types.ts +1 -0
  225. package/src/usage/expected-prices.ts +127 -0
  226. package/src/usage/log.ts +58 -1
  227. package/src/vision/eligibility.ts +13 -2
  228. package/src/web-search/loop.ts +56 -3
  229. package/gui/dist/assets/index-CWXut3rG.js +0 -115
  230. package/gui/dist/assets/index-EdoPnm9_.css +0 -1
  231. package/src/adapters/devin-cli/acp.ts +0 -204
  232. package/src/adapters/devin-cli/adapter.ts +0 -345
  233. package/src/adapters/devin-cli/binary.ts +0 -69
  234. package/src/adapters/devin-cli/models.ts +0 -57
  235. package/src/oauth/devin-cli.ts +0 -149
  236. package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
@@ -0,0 +1,372 @@
1
+ import { existsSync, unlinkSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { restoreNativeCodexAsync } from "../codex/inject";
4
+ import { stripGrokConfig } from "../grok/inject";
5
+ import { serviceApiTokenFilePath } from "../lib/service-secrets";
6
+ import { statusWinswRaw, type WinswStatus } from "../lib/winsw";
7
+ import { withWindowsServiceMutationLock } from "../lib/windows-service-mutation-lock";
8
+ import { maybeShowStarPrompt } from "../cli/star-prompt";
9
+ import { serviceStatusReport } from "./diagnostics";
10
+ import { assertServiceEnvironmentMatchesInstall, assertServiceAuthEnvironment } from "./guards";
11
+ import { resolveServiceListenPort, reportServiceServing } from "./health";
12
+ import { platformOps, proxyStillLiveAfterStop, stopTrackedProxyForServiceCommand, installServiceSafely, installFreshWindowsSchedulerSafely, removeServiceInstallState, isServiceInstalled } from "./orchestration";
13
+ import { repairService } from "./repair";
14
+ import type { ServiceRepairVerb } from "./repair";
15
+ import { TASK, plistPath, readServiceBackend } from "./state";
16
+ import type { ServiceBackend } from "./state";
17
+ import { unitPath } from "./systemd";
18
+ import { inspectWindowsSchedulerServiceStatus, schtasksErrorDetail, probeWindowsSchedulerTask } from "./windows-scheduler";
19
+ import type { WindowsSchedulerTaskProbe } from "./windows-scheduler";
20
+ import { win32 } from "node:path";
21
+ import { serviceDiagnosticsSummary } from "./diagnostics";
22
+
23
+ /**
24
+ * `restart` is NO LONGER folded into `repair`.
25
+ *
26
+ * It used to be, and on macOS that made it a lie: `repair` now returns early when the plist
27
+ * is already current and the job is loaded from it (#4236), so `ocx service restart` of a
28
+ * healthy service restarted nothing and the operator had to run `launchctl kickstart -k` by
29
+ * hand. The two verbs share the whole repair path and diverge only in `repairService`, which
30
+ * kicks the launchd job the no-op left running. A BARE `ocx service` still maps to `repair`
31
+ * (see {@link selectServiceSubcommand}): it is an idempotent "make it current", not a
32
+ * request to bounce a healthy hub.
33
+ */
34
+ export function normalizeServiceSubcommand(sub?: string): string {
35
+ return sub ?? "install";
36
+ }
37
+
38
+ export interface ParsedServiceArgs {
39
+ sub: string;
40
+ backend: ServiceBackend | null;
41
+ invalid: string[];
42
+ }
43
+
44
+ export type ServiceInstallationState = "installed" | "absent" | "unknown";
45
+
46
+ export interface ServiceInstallationProbe {
47
+ state: ServiceInstallationState;
48
+ detail?: string;
49
+ }
50
+
51
+ export interface ServiceInstallationProbeHooks {
52
+ platform?: NodeJS.Platform;
53
+ exists?: (path: string) => boolean;
54
+ probeWindowsTask?: () => WindowsSchedulerTaskProbe;
55
+ nativeStatus?: () => WinswStatus;
56
+ }
57
+
58
+ /**
59
+ * Read only enough registration state to choose between install and repair.
60
+ * Windows must keep query failure distinct from proven absence: treating an
61
+ * unreadable scheduler/SCM as absent would send a bare command into the
62
+ * elevated registration path and recreate the original #2287 failure.
63
+ */
64
+ export function probeServiceInstallation(
65
+ hooks: ServiceInstallationProbeHooks = {},
66
+ ): ServiceInstallationProbe {
67
+ const platform = hooks.platform ?? process.platform;
68
+ const exists = hooks.exists ?? existsSync;
69
+ if (platform === "darwin") {
70
+ return { state: exists(plistPath()) ? "installed" : "absent" };
71
+ }
72
+ if (platform === "linux") {
73
+ return { state: exists(unitPath()) ? "installed" : "absent" };
74
+ }
75
+ if (platform !== "win32") return { state: "absent" };
76
+
77
+ let scheduler: WindowsSchedulerTaskProbe;
78
+ try {
79
+ scheduler = (hooks.probeWindowsTask ?? probeWindowsSchedulerTask)();
80
+ } catch (cause) {
81
+ scheduler = { status: "unknown", detail: schtasksErrorDetail(cause) };
82
+ }
83
+ let native: WinswStatus;
84
+ try {
85
+ native = (hooks.nativeStatus ?? statusWinswRaw)();
86
+ } catch {
87
+ native = "unknown";
88
+ }
89
+
90
+ if (scheduler.status === "present" || native === "started" || native === "stopped") {
91
+ return { state: "installed" };
92
+ }
93
+ if (scheduler.status === "unknown" || native === "unknown") {
94
+ const parts = [
95
+ scheduler.status === "unknown" ? `Task Scheduler: ${scheduler.detail}` : null,
96
+ native === "unknown" ? "WinSW status could not be determined" : null,
97
+ ].filter((part): part is string => Boolean(part));
98
+ return { state: "unknown", detail: parts.join("; ") };
99
+ }
100
+ return { state: "absent" };
101
+ }
102
+
103
+ /**
104
+ * A bare invocation is an idempotent "make the installed service current"
105
+ * operation. First-time setup still installs, but an existing registration must
106
+ * use the repair path so Windows avoids unconditional elevated registration; repair may
107
+ * still refresh a stale scheduler definition.
108
+ * Backend flags remain an explicit install request because they select which
109
+ * registration mechanism to create.
110
+ */
111
+ export function selectServiceSubcommand(
112
+ parsed: ParsedServiceArgs,
113
+ options: { hasExplicitSubcommand: boolean; installed: boolean },
114
+ ): string {
115
+ if (!options.hasExplicitSubcommand && parsed.backend === null && options.installed) return "repair";
116
+ return parsed.sub;
117
+ }
118
+
119
+ export type ServiceCommandPlan =
120
+ | { ok: true; parsed: ParsedServiceArgs; command: string }
121
+ | { ok: false; message: string };
122
+
123
+ export function planServiceCommand(
124
+ args: string[],
125
+ options: { platform?: NodeJS.Platform; probeInstallation?: () => ServiceInstallationProbe } = {},
126
+ ): ServiceCommandPlan {
127
+ const parsed = parseServiceArgs(args);
128
+ if (parsed.invalid.length > 0) {
129
+ return { ok: false, message: `Unknown service option: ${parsed.invalid.join(" ")}` };
130
+ }
131
+ if (parsed.backend && parsed.sub !== "install") {
132
+ return { ok: false, message: "--native/--scheduler apply to `ocx service install` only; other subcommands use the installed backend." };
133
+ }
134
+ if (parsed.backend === "native" && (options.platform ?? process.platform) !== "win32") {
135
+ return { ok: false, message: "--native (WinSW) is Windows-only." };
136
+ }
137
+
138
+ const hasExplicitSubcommand = args.some(arg => !arg.startsWith("--"));
139
+ let installed = false;
140
+ if (!hasExplicitSubcommand && parsed.backend === null) {
141
+ const probe = (options.probeInstallation ?? probeServiceInstallation)();
142
+ if (probe.state === "unknown") {
143
+ const suffix = probe.detail ? ` (${probe.detail})` : "";
144
+ return {
145
+ ok: false,
146
+ message: `Could not safely determine whether the service is installed${suffix}. Run 'ocx service status' and retry; use explicit 'ocx service install' only after confirming it is absent.`,
147
+ };
148
+ }
149
+ installed = probe.state === "installed";
150
+ }
151
+ return {
152
+ ok: true,
153
+ parsed,
154
+ command: selectServiceSubcommand(parsed, { hasExplicitSubcommand, installed }),
155
+ };
156
+ }
157
+
158
+ /**
159
+ * `ocx service [sub] [--native|--scheduler]`. The first non-flag token is the
160
+ * subcommand; backend flags are only meaningful for `install` (validated by the caller).
161
+ */
162
+ export function parseServiceArgs(args: string[]): ParsedServiceArgs {
163
+ let sub: string | undefined;
164
+ let backend: ServiceBackend | null = null;
165
+ const invalid: string[] = [];
166
+ for (const arg of args) {
167
+ if (arg === "--native") {
168
+ if (backend === "scheduler") { invalid.push("--native (conflicts with --scheduler)"); continue; }
169
+ backend = "native";
170
+ }
171
+ else if (arg === "--scheduler") {
172
+ if (backend === "native") { invalid.push("--scheduler (conflicts with --native)"); continue; }
173
+ backend = "scheduler";
174
+ }
175
+ else if (arg.startsWith("--")) invalid.push(arg);
176
+ else if (sub === undefined) sub = arg;
177
+ else invalid.push(arg);
178
+ }
179
+ return { sub: normalizeServiceSubcommand(sub), backend, invalid };
180
+ }
181
+
182
+ export async function serviceCommand(...args: (string | undefined)[]): Promise<void> {
183
+ const filteredArgs = args.filter((a): a is string => Boolean(a));
184
+ const execute = async (): Promise<void> => {
185
+ // Planning reads manager state. Repeat it only after the writer lock is held, otherwise a
186
+ // bare command can choose install from a snapshot another service command already changed.
187
+ const plan = planServiceCommand(filteredArgs);
188
+ if (!plan.ok) {
189
+ console.error(plan.message);
190
+ process.exit(1);
191
+ }
192
+ const { parsed, command } = plan;
193
+ if (command === "repair" || command === "restart") {
194
+ const verb: ServiceRepairVerb = command === "restart" ? "restart" : "repair";
195
+ assertServiceEnvironmentMatchesInstall();
196
+ assertServiceAuthEnvironment();
197
+ // A throw used to escape straight to the top level, so the one command that can
198
+ // leave a macOS hub evicted never reached its own serving check (#4236, defect 1f).
199
+ // Report the failure, then still ask whether anything is listening: on darwin the
200
+ // rollback inside installLaunchd may have brought the previous job back, and on
201
+ // Windows the preserve/restart protocol may have done the same. The operator needs
202
+ // both halves of that answer, and the exit code stays non-zero either way.
203
+ let repairError: unknown;
204
+ try {
205
+ await repairService({ verb });
206
+ } catch (error) {
207
+ repairError = error;
208
+ console.error(`❌ Service ${verb} failed: ${error instanceof Error ? error.message : String(error)}`);
209
+ process.exitCode = 1;
210
+ }
211
+ // All three platforms: a repair that reports success while nothing serves is the
212
+ // defect class this unit exists to close. Windows bakes its port into the
213
+ // scheduler wrapper or the WinSW XML, both of which installedServiceListenPort()
214
+ // now reads.
215
+ await reportServiceServing(verb === "restart" ? "restarted" : "repaired");
216
+ if (repairError !== undefined) process.exitCode = 1;
217
+ return;
218
+ }
219
+ // Non-install subcommands follow the backend recorded at install time (state v2).
220
+ const backend: ServiceBackend = parsed.backend ?? (process.platform === "win32" ? readServiceBackend() : "scheduler");
221
+ const ops = platformOps(backend);
222
+ if (!ops) {
223
+ console.error("ocx service supports macOS (launchd), Windows (Task Scheduler), and Linux (systemd).");
224
+ process.exit(1);
225
+ }
226
+ switch (command) {
227
+ case "install":
228
+ assertServiceEnvironmentMatchesInstall();
229
+ assertServiceAuthEnvironment();
230
+ // A manually started proxy can still own the configured port while the service
231
+ // registration is absent or unloaded. Stop both the registered manager and any
232
+ // tracked standalone listener before loading the freshly written service assets.
233
+ // Otherwise launchd/Task Scheduler can register successfully while its child
234
+ // restart-loops on EADDRINUSE, and the old standalone process makes the install
235
+ // verification report a false success.
236
+ try {
237
+ if (process.platform === "win32" && backend === "scheduler") {
238
+ const scheduler = probeWindowsSchedulerTask(TASK);
239
+ if (scheduler.status === "unknown") {
240
+ throw new Error(`Task Scheduler state could not be verified before install: ${scheduler.detail}`);
241
+ }
242
+ if (scheduler.status === "absent") {
243
+ await installFreshWindowsSchedulerSafely();
244
+ } else {
245
+ await installServiceSafely(backend, ops.install);
246
+ }
247
+ } else {
248
+ await installServiceSafely(backend, ops.install);
249
+ }
250
+ } catch (error) {
251
+ console.error(`❌ Service install cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
252
+ process.exitCode = 1;
253
+ break;
254
+ }
255
+ // The wrapper was written moments ago in this process, so the configured port
256
+ // and the baked one cannot have diverged yet — unlike `start`, which reads the
257
+ // installed artifact instead.
258
+ await reportServiceServing("installed", { port: resolveServiceListenPort() });
259
+ if (process.platform === "linux") console.log(" For auto-start on boot: loginctl enable-linger $USER");
260
+ // Service users never reach the `ocx start` prompt: the proxy they run is the
261
+ // supervised child, which always carries OCX_SERVICE=1. This command, though, is
262
+ // hand-typed in a real terminal, so it is the one interactive moment they get.
263
+ // Same one-time marker and same guards (TTY, gh auth, agent deferral) apply.
264
+ await maybeShowStarPrompt();
265
+ break;
266
+ case "start":
267
+ ops.start();
268
+ await reportServiceServing("started");
269
+ break;
270
+ case "stop": {
271
+ assertServiceEnvironmentMatchesInstall();
272
+ // Only stop what is actually installed. The unguarded call ran a real `launchctl unload`
273
+ // (and its Windows/Linux twins) even with nothing installed.
274
+ if (ops.status() !== null || isServiceInstalled()) {
275
+ ops.stop();
276
+ }
277
+ await stopTrackedProxyForServiceCommand();
278
+ {
279
+ // Verify rather than trust the stop command: a surviving wrapper respawns its child
280
+ // seconds later, and restoring native Codex on top of a live proxy is the failure #764
281
+ // reports as "stop reports success without stopping the proxy".
282
+ const survivor = await proxyStillLiveAfterStop();
283
+ if (survivor) {
284
+ console.error(
285
+ `❌ service stop did not take effect: a proxy is still listening on port ${survivor.port}.`
286
+ + "\nNative Codex was NOT restored, because doing so while the proxy is running leaves"
287
+ + " both pointing at each other. Check for a second service backend (`ocx service status`)"
288
+ + " or a manually started proxy, then re-run `ocx service stop`.",
289
+ );
290
+ process.exitCode = 1;
291
+ break;
292
+ }
293
+ const restore = await restoreNativeCodexAsync();
294
+ if (restore.success) console.log("✅ service stopped + native Codex restored.");
295
+ else console.error(`⚠️ service stopped, but native Codex restore FAILED: ${restore.message}\nRun \`ocx restore\` (or check $CODEX_HOME/config.toml) before using native Codex.`);
296
+ if (!restore.success) process.exitCode = 1;
297
+ // The Grok fence is the other managed config this command owns. Leaving it behind
298
+ // pointed grok at a dead endpoint while native Codex was already restored.
299
+ const grok = stripGrokConfig();
300
+ if (grok.changed) console.log(`↩️ ${grok.message}`);
301
+ else if (!grok.ok) {
302
+ // A failed strip leaves Grok aimed at a proxy this command just stopped. Exiting
303
+ // 0 tells a script the teardown finished when half of it did not.
304
+ console.error(`⚠️ ${grok.message}`);
305
+ process.exitCode = 1;
306
+ }
307
+ }
308
+ break;
309
+ }
310
+ case "status": {
311
+ if (process.platform === "win32" && backend === "scheduler") {
312
+ console.log(await inspectWindowsSchedulerServiceStatus());
313
+ } else {
314
+ // Replaces raw `ops.status()` output, which on darwin is a `launchctl list`
315
+ // line: registration reported as if it were service. serviceStatusReport
316
+ // subsumes the not-installed case and adds the serving / stale-plist split.
317
+ console.log(await serviceStatusReport());
318
+ }
319
+ console.log(`Diagnostics: ${serviceDiagnosticsSummary()}`);
320
+ break;
321
+ }
322
+ case "uninstall":
323
+ case "remove":
324
+ assertServiceEnvironmentMatchesInstall();
325
+ try { ops.stop(); } catch (err) {
326
+ console.warn(`⚠️ Service stop failed: ${err instanceof Error ? err.message : String(err)}`);
327
+ }
328
+ await stopTrackedProxyForServiceCommand();
329
+ try {
330
+ ops.uninstall();
331
+ } catch (err) {
332
+ console.error(`❌ Service uninstall failed: ${err instanceof Error ? err.message : String(err)}`);
333
+ console.error("The service may still be installed. Check with 'ocx service status' or remove manually.");
334
+ process.exit(1);
335
+ }
336
+ {
337
+ const restore = await restoreNativeCodexAsync();
338
+ if (!restore.success) {
339
+ console.error(`⚠️ native Codex restore FAILED: ${restore.message}\nRun \`ocx restore\` before using native Codex.`);
340
+ process.exitCode = 1;
341
+ }
342
+ const grok = stripGrokConfig();
343
+ if (grok.changed) console.log(`↩️ ${grok.message}`);
344
+ else if (!grok.ok) {
345
+ console.error(`⚠️ ${grok.message}`);
346
+ process.exitCode = 1;
347
+ }
348
+ }
349
+ removeServiceInstallState();
350
+ try { if (existsSync(serviceApiTokenFilePath())) unlinkSync(serviceApiTokenFilePath()); } catch { /* best-effort */ }
351
+ console.log("✅ service uninstalled.");
352
+ break;
353
+ default:
354
+ console.error("Usage: ocx service [install|repair|restart|start|stop|status|uninstall|remove] [--native|--scheduler]");
355
+ console.error(" With no subcommand, installs when absent or repairs/restarts an existing service.");
356
+ console.error(" repair: refresh the installed backend, reloading it only when the definition changed; stale Windows tasks may request admin approval.");
357
+ console.error(" restart: the same refresh, but always restarts the service — on macOS a healthy job is kickstarted in place.");
358
+ console.error(" --native (Windows only): register a real SCM service via WinSW instead of Task Scheduler.");
359
+ process.exit(1);
360
+ }
361
+ };
362
+
363
+ const preliminary = parseServiceArgs(filteredArgs);
364
+ const windowsMutation = process.platform === "win32"
365
+ && preliminary.invalid.length === 0
366
+ && preliminary.sub !== "status";
367
+ if (windowsMutation) {
368
+ await withWindowsServiceMutationLock(execute);
369
+ return;
370
+ }
371
+ await execute();
372
+ }