@bitkyc08/opencodex 2.7.35 → 2.7.36

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 (80) hide show
  1. package/README.ja.md +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +4 -2
  4. package/README.ru.md +1 -1
  5. package/README.zh-CN.md +1 -1
  6. package/bin/ocx.mjs +52 -0
  7. package/gui/dist/assets/index-BpX-hoSd.css +1 -0
  8. package/gui/dist/assets/index-ZmFopEYw.js +52 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -1
  11. package/src/adapters/cursor/cursor-errors.ts +38 -1
  12. package/src/adapters/cursor/discovery.ts +1 -0
  13. package/src/adapters/cursor/effort-map.ts +1 -0
  14. package/src/adapters/cursor/live-models.ts +22 -5
  15. package/src/adapters/cursor/live-transport.ts +82 -7
  16. package/src/adapters/cursor/transport.ts +2 -0
  17. package/src/adapters/cursor.ts +5 -2
  18. package/src/adapters/openai-responses.ts +64 -1
  19. package/src/cli/doctor.ts +10 -0
  20. package/src/cli/help.ts +10 -0
  21. package/src/cli/index.ts +88 -9
  22. package/src/cli/internal-dispatch.ts +20 -0
  23. package/src/cli/status.ts +15 -4
  24. package/src/cli/tray-proxy.ts +52 -0
  25. package/src/codex/auth-api.ts +46 -5
  26. package/src/codex/autostart-health.ts +149 -0
  27. package/src/codex/catalog/aggregation.ts +268 -0
  28. package/src/codex/catalog/bundled.ts +188 -0
  29. package/src/codex/catalog/effort.ts +263 -0
  30. package/src/codex/catalog/metadata.ts +176 -0
  31. package/src/codex/catalog/parsing.ts +399 -0
  32. package/src/codex/catalog/provider-fetch.ts +609 -0
  33. package/src/codex/catalog/sync.ts +540 -0
  34. package/src/codex/catalog.ts +11 -2426
  35. package/src/codex/inject.ts +165 -3
  36. package/src/codex/shim.ts +141 -8
  37. package/src/codex/sync.ts +17 -2
  38. package/src/config.ts +23 -0
  39. package/src/lib/errors.ts +11 -0
  40. package/src/providers/antigravity-models.ts +33 -0
  41. package/src/providers/kiro-models.ts +2 -0
  42. package/src/providers/registry.ts +2 -2
  43. package/src/responses/state.ts +69 -6
  44. package/src/server/auth-cors.ts +3 -0
  45. package/src/server/management/agent-settings-routes.ts +536 -0
  46. package/src/server/management/combo-routes.ts +210 -0
  47. package/src/server/management/config-routes.ts +302 -0
  48. package/src/server/management/context.ts +21 -0
  49. package/src/server/management/logs-usage-routes.ts +176 -0
  50. package/src/server/management/model-routes.ts +253 -0
  51. package/src/server/management/oauth-account-routes.ts +301 -0
  52. package/src/server/management/provider-routes.ts +408 -0
  53. package/src/server/management/shared.ts +186 -0
  54. package/src/server/management-api.ts +23 -1806
  55. package/src/server/responses/collaboration.ts +300 -0
  56. package/src/server/responses/compact.ts +342 -0
  57. package/src/server/responses/core.ts +1498 -0
  58. package/src/server/responses/encrypted-payload.ts +231 -0
  59. package/src/server/responses/fetch-helpers.ts +157 -0
  60. package/src/server/responses.ts +9 -2172
  61. package/src/server/startup-action-control.ts +41 -0
  62. package/src/server/startup-health-cache.ts +100 -0
  63. package/src/server/windows-tray-control.ts +41 -0
  64. package/src/service.ts +171 -19
  65. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  66. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  67. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  68. package/src/tray/assets/opencodex-tray.png +0 -0
  69. package/src/tray/windows-tray.ps1 +290 -0
  70. package/src/tray/windows.ts +628 -0
  71. package/src/types.ts +5 -0
  72. package/src/update/index.ts +43 -0
  73. package/src/update/job.ts +46 -0
  74. package/src/update/tray-update-plan.d.mts +18 -0
  75. package/src/update/tray-update-plan.mjs +38 -0
  76. package/src/usage/cost.ts +0 -0
  77. package/src/usage/expected-prices.ts +9 -2
  78. package/src/usage/summary.ts +42 -7
  79. package/gui/dist/assets/index-BunUANVE.js +0 -52
  80. package/gui/dist/assets/index-Sg-7L_oZ.css +0 -1
@@ -0,0 +1,41 @@
1
+ import { execFile } from "node:child_process";
2
+ import { join } from "node:path";
3
+ import { durableBunPath } from "../lib/bun-runtime";
4
+
5
+ export type StartupInstallAction = "install-service" | "install-shim";
6
+ let activeInstall: StartupInstallAction | null = null;
7
+
8
+ export function startupInstallArgv(action: StartupInstallAction): string[] {
9
+ return action === "install-service"
10
+ ? ["service", "install"]
11
+ : ["codex-shim", "install"];
12
+ }
13
+
14
+ /** Execute the existing fixed CLI installer outside the proxy event loop. */
15
+ export function runStartupInstallAction(action: StartupInstallAction): Promise<{ message: string }> {
16
+ if (activeInstall) return Promise.reject(new Error(`Another startup installation is already running: ${activeInstall}`));
17
+ activeInstall = action;
18
+ const bun = durableBunPath();
19
+ const cli = join(import.meta.dir, "..", "cli", "index.ts");
20
+ const operation = new Promise<{ message: string }>((resolve, reject) => {
21
+ execFile(bun, [cli, ...startupInstallArgv(action)], {
22
+ encoding: "utf8",
23
+ env: process.env,
24
+ timeout: 60_000,
25
+ windowsHide: true,
26
+ maxBuffer: 256 * 1024,
27
+ }, (error, stdout, stderr) => {
28
+ if (error) {
29
+ const detail = stderr.trim() || stdout.trim() || error.message;
30
+ reject(new Error(detail.slice(0, 2_000)));
31
+ return;
32
+ }
33
+ resolve({
34
+ message: action === "install-service"
35
+ ? "Background service installed."
36
+ : "Codex launcher shim installed.",
37
+ });
38
+ });
39
+ });
40
+ return operation.finally(() => { activeInstall = null; });
41
+ }
@@ -0,0 +1,100 @@
1
+ import { execFile } from "node:child_process";
2
+ import { join } from "node:path";
3
+ import { codexAutoStartEnabled } from "../config";
4
+ import { deriveStartupHealth, type StartupHealth } from "../codex/autostart-health";
5
+ import { getCodexRoutingKind } from "../codex/inject";
6
+ import { diagnoseCodexShim } from "../codex/shim";
7
+ import { durableBunPath } from "../lib/bun-runtime";
8
+ import type { OcxConfig } from "../types";
9
+
10
+ const CACHE_TTL_MS = 30_000;
11
+ const PROBE_TIMEOUT_MS = 5_000;
12
+ let cached: { timestamp: number; value: StartupHealth } | null = null;
13
+ let inflight: Promise<StartupHealth> | null = null;
14
+ let generation = 0;
15
+
16
+ export function markStartupHealthDiagnosticStale(value: StartupHealth): StartupHealth {
17
+ if (!value.localRoutingDependency) return { ...value, diagnosticStale: true };
18
+ return {
19
+ ...value,
20
+ status: "at-risk",
21
+ rebootSafe: false,
22
+ protection: "none",
23
+ diagnosticStale: true,
24
+ recommendedCommand: value.routingKind === "custom-local" || value.routingKind === "unknown"
25
+ ? value.commands.restoreNative
26
+ : value.commands.installService,
27
+ };
28
+ }
29
+
30
+ function conservativeFallback(config: Pick<OcxConfig, "codexAutoStart">): StartupHealth {
31
+ const shim = diagnoseCodexShim();
32
+ return deriveStartupHealth({
33
+ routingKind: getCodexRoutingKind(),
34
+ autostartEnabled: codexAutoStartEnabled(config),
35
+ serviceInstalled: false,
36
+ serviceViable: false,
37
+ serviceEnabled: false,
38
+ serviceRunning: false,
39
+ serviceStale: false,
40
+ serviceConflict: false,
41
+ serviceSupported: process.platform === "win32" || process.platform === "darwin" || process.platform === "linux",
42
+ shimInstalled: shim.installed,
43
+ shimHealthy: shim.healthy,
44
+ platform: process.platform,
45
+ diagnosticStale: true,
46
+ });
47
+ }
48
+
49
+ function runProbe(config: Pick<OcxConfig, "codexAutoStart">): Promise<StartupHealth> {
50
+ const bun = durableBunPath();
51
+ const cli = join(import.meta.dir, "..", "cli", "index.ts");
52
+ return new Promise(resolve => {
53
+ execFile(bun, [cli, "__startup-health"], {
54
+ encoding: "utf8",
55
+ env: process.env,
56
+ timeout: PROBE_TIMEOUT_MS,
57
+ windowsHide: true,
58
+ maxBuffer: 256 * 1024,
59
+ }, (error, stdout) => {
60
+ if (!error) {
61
+ const lines = stdout.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
62
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
63
+ try {
64
+ const parsed = JSON.parse(lines[index]) as StartupHealth;
65
+ if (["native", "protected", "at-risk"].includes(parsed.status) && typeof parsed.rebootSafe === "boolean") {
66
+ resolve({ ...parsed, diagnosticStale: false });
67
+ return;
68
+ }
69
+ } catch { /* scan earlier output; config repair messages may precede JSON */ }
70
+ }
71
+ }
72
+ resolve(cached ? markStartupHealthDiagnosticStale(cached.value) : conservativeFallback(config));
73
+ });
74
+ });
75
+ }
76
+
77
+ function refreshInBackground(config: Pick<OcxConfig, "codexAutoStart">): void {
78
+ if (inflight) return;
79
+ const startedGeneration = generation;
80
+ const probe = runProbe(config).then(value => {
81
+ if (startedGeneration === generation) cached = { timestamp: Date.now(), value };
82
+ return value;
83
+ });
84
+ inflight = probe.finally(() => {
85
+ if (inflight === probe || startedGeneration === generation) inflight = null;
86
+ });
87
+ }
88
+
89
+ /** Stale-while-revalidate: service-manager probes never hold open a model/UI request. */
90
+ export async function getCachedStartupHealth(config: Pick<OcxConfig, "codexAutoStart">): Promise<StartupHealth> {
91
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) return cached.value;
92
+ refreshInBackground(config);
93
+ return cached ? markStartupHealthDiagnosticStale(cached.value) : conservativeFallback(config);
94
+ }
95
+
96
+ export function invalidateStartupHealthCache(): void {
97
+ generation += 1;
98
+ cached = null;
99
+ inflight = null;
100
+ }
@@ -0,0 +1,41 @@
1
+ import { execFile } from "node:child_process";
2
+ import { join } from "node:path";
3
+ import { durableBunPath } from "../lib/bun-runtime";
4
+ import type { WindowsTrayStatus } from "../tray/windows";
5
+
6
+ export type WindowsTrayAction = "install" | "start" | "stop" | "status" | "uninstall";
7
+
8
+ /** Run blocking registry/PowerShell waits outside the proxy event loop. */
9
+ export function runWindowsTrayAction(action: WindowsTrayAction): Promise<WindowsTrayStatus> {
10
+ if (action === "status") {
11
+ return import("../tray/windows").then(({ getWindowsTrayStatusAsync }) => getWindowsTrayStatusAsync());
12
+ }
13
+ const bun = durableBunPath();
14
+ const cli = join(import.meta.dir, "..", "cli", "index.ts");
15
+ return new Promise((resolve, reject) => {
16
+ execFile(bun, [cli, "tray", action, "--json"], {
17
+ encoding: "utf8",
18
+ env: process.env,
19
+ timeout: 30_000,
20
+ windowsHide: true,
21
+ maxBuffer: 256 * 1024,
22
+ }, (error, stdout, stderr) => {
23
+ const lines = stdout.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
24
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
25
+ try {
26
+ const parsed = JSON.parse(lines[index]) as WindowsTrayStatus | { error?: unknown };
27
+ if ("supported" in parsed && typeof parsed.supported === "boolean") {
28
+ resolve(parsed);
29
+ return;
30
+ }
31
+ const failure = parsed as { error?: unknown };
32
+ if (typeof failure.error === "string") {
33
+ reject(new Error(failure.error));
34
+ return;
35
+ }
36
+ } catch { /* scan earlier lines */ }
37
+ }
38
+ reject(new Error(error?.message || stderr.trim() || "Windows tray action returned no status"));
39
+ });
40
+ });
41
+ }
package/src/service.ts CHANGED
@@ -88,7 +88,7 @@ function normalizePathForCompare(path: string): string {
88
88
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
89
89
  }
90
90
 
91
- interface ServiceInstallState {
91
+ export interface ServiceInstallState {
92
92
  version: 1 | 2;
93
93
  codexHome: string;
94
94
  opencodexHome: string;
@@ -101,6 +101,23 @@ interface ServiceInstallState {
101
101
  winswSha256?: string;
102
102
  }
103
103
 
104
+ export function parseServiceInstallState(value: unknown): ServiceInstallState | null {
105
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
106
+ const state = value as Record<string, unknown>;
107
+ if (state.version !== 1 && state.version !== 2) return null;
108
+ if (typeof state.codexHome !== "string" || state.codexHome.length === 0) return null;
109
+ if (typeof state.opencodexHome !== "string" || state.opencodexHome.length === 0) return null;
110
+ for (const key of ["bunPath", "cliPath", "winswVersion", "winswSha256"] as const) {
111
+ if (state[key] !== undefined && (typeof state[key] !== "string" || state[key].length === 0)) return null;
112
+ }
113
+ if (state.version === 1) {
114
+ if (state.backend !== undefined) return null;
115
+ } else if (state.backend !== "scheduler" && state.backend !== "native") {
116
+ return null;
117
+ }
118
+ return state as unknown as ServiceInstallState;
119
+ }
120
+
104
121
  function writeServiceInstallState(backend: ServiceBackend = "scheduler"): void {
105
122
  const { bun, cli } = cliEntry();
106
123
  const state: ServiceInstallState = {
@@ -124,8 +141,8 @@ function writeServiceInstallState(backend: ServiceBackend = "scheduler"): void {
124
141
  function readServiceInstallState(): ServiceInstallState | null {
125
142
  for (const path of serviceStatePaths()) {
126
143
  try {
127
- const parsed = JSON.parse(readFileSync(path, "utf8")) as ServiceInstallState;
128
- if (parsed.version === 1 || parsed.version === 2) return parsed;
144
+ const parsed = parseServiceInstallState(JSON.parse(readFileSync(path, "utf8")));
145
+ if (parsed) return parsed;
129
146
  } catch {
130
147
  /* try the next known state path */
131
148
  }
@@ -451,6 +468,30 @@ export function buildWindowsTaskXml(script = windowsServiceScriptPath(), launche
451
468
  `;
452
469
  }
453
470
 
471
+ function taskXmlSection(xml: string, tag: string): string {
472
+ return new RegExp(`<${tag}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${tag}>`, "i").exec(xml)?.[1] ?? "";
473
+ }
474
+
475
+ /** Validate the security/lifecycle-critical fields of the registered scheduler task. */
476
+ export function windowsTaskRegistrationHealthy(
477
+ xml: string,
478
+ wscript = windowsWscript(),
479
+ launcher = windowsLauncherVbsPath(),
480
+ ): boolean {
481
+ const trigger = taskXmlSection(xml, "LogonTrigger");
482
+ const principal = taskXmlSection(xml, "Principal");
483
+ const settings = taskXmlSection(xml, "Settings");
484
+ const action = taskXmlSection(xml, "Exec");
485
+ return /<Enabled>\s*true\s*<\/Enabled>/i.test(trigger)
486
+ && /<LogonType>\s*InteractiveToken\s*<\/LogonType>/i.test(principal)
487
+ && /<RunLevel>\s*LeastPrivilege\s*<\/RunLevel>/i.test(principal)
488
+ && /<Enabled>\s*true\s*<\/Enabled>/i.test(settings)
489
+ && /<MultipleInstancesPolicy>\s*IgnoreNew\s*<\/MultipleInstancesPolicy>/i.test(settings)
490
+ && /<ExecutionTimeLimit>\s*PT0S\s*<\/ExecutionTimeLimit>/i.test(settings)
491
+ && action.includes(`<Command>${taskXmlString(wscript)}</Command>`)
492
+ && action.includes(`<Arguments>${taskXmlString(`/b /nologo "${launcher}"`)}</Arguments>`);
493
+ }
494
+
454
495
  // ── macOS (launchd) ──
455
496
  function installLaunchd(): void {
456
497
  const dir = join(homedir(), "Library", "LaunchAgents");
@@ -561,6 +602,7 @@ async function installWindowsNative(): Promise<void> {
561
602
  function startWindows(): void { schtasks(["/run", "/tn", TASK]); }
562
603
  function stopWindows(): void { try { schtasks(["/end", "/tn", TASK]); } catch { /* not running */ } }
563
604
  function statusWindows(): string { try { return schtasks(["/query", "/tn", TASK]); } catch { return ""; } }
605
+ function statusWindowsXml(): string { try { return schtasks(["/query", "/tn", TASK, "/xml"]); } catch { return ""; } }
564
606
  function uninstallWindows(): void {
565
607
  try { schtasks(["/delete", "/tn", TASK, "/f"]); } catch { /* absent */ }
566
608
  if (existsSync(windowsServiceScriptPath())) unlinkSync(windowsServiceScriptPath());
@@ -814,31 +856,141 @@ export function uninstallServiceIfInstalled(): boolean {
814
856
 
815
857
  /** True if a background service (launchd/systemd/Task Scheduler) is installed. */
816
858
  export function isServiceInstalled(): boolean {
817
- return serviceStatusSummary().startsWith("installed");
859
+ return diagnoseService().installed;
860
+ }
861
+
862
+ export interface ServiceDiagnostic {
863
+ supported: boolean;
864
+ installed: boolean;
865
+ enabled: boolean;
866
+ running: boolean;
867
+ viable: boolean;
868
+ startable: boolean;
869
+ stale: boolean;
870
+ conflict: boolean;
871
+ backend: ServiceBackend | "launchd" | "systemd" | null;
872
+ summary: string;
873
+ }
874
+
875
+ /** Windows tray may restart a healthy-but-stopped native service; stale/conflicting installs remain blocked. */
876
+ export function serviceStartableFromTray(service: ServiceDiagnostic): boolean {
877
+ return service.startable && !service.stale && !service.conflict;
878
+ }
879
+
880
+ export interface WindowsServiceDiagnosticInputs {
881
+ schedulerInstalled: boolean;
882
+ schedulerEnabled: boolean;
883
+ schedulerAssetsHealthy: boolean;
884
+ nativeStatus: "started" | "stopped" | "nonexistent" | "unknown";
885
+ recordedBackend: ServiceBackend | null;
886
+ staleBakedPaths: boolean;
887
+ nativeRepairAssetsOnly: boolean;
888
+ diagnostics: string;
889
+ }
890
+
891
+ export function deriveWindowsServiceDiagnostic(inputs: WindowsServiceDiagnosticInputs): ServiceDiagnostic {
892
+ const nativeInstalled = inputs.nativeStatus !== "nonexistent";
893
+ const conflict = inputs.schedulerInstalled && nativeInstalled;
894
+ const backendStateMismatch = inputs.schedulerInstalled
895
+ ? inputs.recordedBackend !== "scheduler"
896
+ : nativeInstalled && inputs.recordedBackend !== "native";
897
+ const stale = inputs.staleBakedPaths
898
+ || (inputs.schedulerInstalled && !inputs.schedulerAssetsHealthy)
899
+ || backendStateMismatch
900
+ || (inputs.nativeStatus === "nonexistent" && inputs.nativeRepairAssetsOnly);
901
+ const backend = inputs.schedulerInstalled ? "scheduler" : nativeInstalled ? "native" : null;
902
+ const enabled = inputs.schedulerInstalled ? inputs.schedulerEnabled : inputs.nativeStatus === "started";
903
+ const running = nativeInstalled ? inputs.nativeStatus === "started" : inputs.schedulerInstalled && inputs.schedulerEnabled;
904
+ const viable = !conflict && !stale
905
+ && (inputs.schedulerInstalled ? inputs.schedulerEnabled && inputs.schedulerAssetsHealthy : inputs.nativeStatus === "started");
906
+ const startable = !conflict && !stale
907
+ && (inputs.schedulerInstalled
908
+ ? inputs.schedulerEnabled && inputs.schedulerAssetsHealthy
909
+ : inputs.nativeStatus === "started" || inputs.nativeStatus === "stopped");
910
+ const detail = conflict
911
+ ? "CONFLICT: Task Scheduler and native WinSW are both present — run 'ocx service uninstall' then reinstall one"
912
+ : stale
913
+ ? "stale or missing service assets — run 'ocx service install' to repair"
914
+ : inputs.schedulerInstalled
915
+ ? inputs.schedulerEnabled ? "Task Scheduler enabled" : "Task Scheduler disabled"
916
+ : nativeInstalled
917
+ ? `native (WinSW ${WINSW_VERSION}): ${inputs.nativeStatus}`
918
+ : "not installed";
919
+ const summary = backend ? `installed, ${detail} (${inputs.diagnostics})` : `not installed (${inputs.diagnostics})`;
920
+ return {
921
+ supported: true,
922
+ installed: inputs.schedulerInstalled || nativeInstalled,
923
+ enabled,
924
+ running,
925
+ viable,
926
+ startable,
927
+ stale,
928
+ conflict,
929
+ backend,
930
+ summary,
931
+ };
818
932
  }
819
933
 
820
- export function serviceStatusSummary(): string {
934
+ /**
935
+ * Fail-closed restart diagnostic. Presence alone is never enough: conflicting
936
+ * managers, stale baked paths, disabled registrations, and unknown/stopped
937
+ * native managers cannot claim that Codex will reconnect after a reboot.
938
+ */
939
+ export function diagnoseService(): ServiceDiagnostic {
821
940
  const diagnostics = serviceDiagnosticsSummary();
822
941
  if (process.platform === "darwin") {
823
- if (!existsSync(plistPath())) return `not installed (${diagnostics})`;
824
- const status = statusLaunchd();
825
- return status ? `installed (launchd; ${diagnostics})` : `installed, not loaded (${diagnostics})`;
942
+ const installed = existsSync(plistPath());
943
+ const running = installed && Boolean(statusLaunchd());
944
+ const stale = installed && bakedServicePathsDiagnostic() !== null;
945
+ const viable = installed && running && !stale;
946
+ const summary = !installed ? `not installed (${diagnostics})`
947
+ : stale ? `installed, but stale (launchd; ${diagnostics})`
948
+ : running ? `installed and loaded (launchd; ${diagnostics})`
949
+ : `installed, not loaded (launchd; ${diagnostics})`;
950
+ return { supported: true, installed, enabled: running, running, viable, startable: installed && !stale, stale, conflict: false, backend: "launchd", summary };
826
951
  }
827
952
  if (process.platform === "win32") {
828
- const scheduler = statusWindows();
829
- const native = winswStatusSummary();
830
- if (scheduler && native) return `installed (CONFLICT: Task Scheduler AND native WinSW both present — run 'ocx service uninstall' then reinstall one; ${diagnostics})`;
831
- if (native) return `installed (${native}; ${diagnostics})`;
832
- return scheduler ? `installed (Task Scheduler; ${diagnostics})` : `not installed (${diagnostics})`;
953
+ const schedulerXml = statusWindowsXml();
954
+ const schedulerInstalled = schedulerXml.length > 0;
955
+ const schedulerSettings = taskXmlSection(schedulerXml, "Settings");
956
+ const schedulerEnabled = schedulerInstalled && /<Enabled>\s*true\s*<\/Enabled>/i.test(schedulerSettings);
957
+ const schedulerAssets = [windowsServiceScriptPath(), windowsLauncherVbsPath(), windowsTaskXmlPath()].every(existsSync)
958
+ && windowsTaskRegistrationHealthy(schedulerXml);
959
+ const nativeStatus = statusWinswRaw();
960
+ const installState = readServiceInstallState();
961
+ const recordedBackend: ServiceBackend | null = !installState
962
+ ? null
963
+ : installState.backend === "native" ? "native" : "scheduler";
964
+ return deriveWindowsServiceDiagnostic({
965
+ schedulerInstalled,
966
+ schedulerEnabled,
967
+ schedulerAssetsHealthy: schedulerAssets,
968
+ nativeStatus,
969
+ recordedBackend,
970
+ staleBakedPaths: bakedServicePathsDiagnostic() !== null,
971
+ nativeRepairAssetsOnly: Boolean(winswStatusSummary()),
972
+ diagnostics,
973
+ });
833
974
  }
834
975
  if (process.platform === "linux") {
835
- if (existsSync("/.dockerenv")) return "unsupported in Docker";
836
- if (!isSystemd()) return "unsupported: systemd not found";
837
- if (!existsSync(unitPath())) return `not installed (${diagnostics})`;
838
- const status = statusSystemd();
839
- return status ? `installed (systemd user; ${diagnostics})` : `installed, not running (${diagnostics})`;
976
+ if (existsSync("/.dockerenv")) return { supported: false, installed: false, enabled: false, running: false, viable: false, startable: false, stale: false, conflict: false, backend: null, summary: "unsupported in Docker" };
977
+ if (!isSystemd()) return { supported: false, installed: false, enabled: false, running: false, viable: false, startable: false, stale: false, conflict: false, backend: null, summary: "unsupported: systemd not found" };
978
+ const installed = existsSync(unitPath());
979
+ const enabled = installed && (() => { try { return sh(`systemctl --user is-enabled ${TASK}`) === "enabled"; } catch { return false; } })();
980
+ const running = installed && (() => { try { return sh(`systemctl --user is-active ${TASK}`) === "active"; } catch { return false; } })();
981
+ const stale = installed && bakedServicePathsDiagnostic() !== null;
982
+ const viable = installed && enabled && running && !stale;
983
+ const summary = !installed ? `not installed (${diagnostics})`
984
+ : stale ? `installed, but stale (systemd user; ${diagnostics})`
985
+ : viable ? `installed, enabled and running (systemd user; ${diagnostics})`
986
+ : `installed, but ${!enabled ? "disabled" : "not running"} (systemd user; ${diagnostics})`;
987
+ return { supported: true, installed, enabled, running, viable, startable: installed && !stale, stale, conflict: false, backend: "systemd", summary };
840
988
  }
841
- return `unsupported on ${process.platform}`;
989
+ return { supported: false, installed: false, enabled: false, running: false, viable: false, startable: false, stale: false, conflict: false, backend: null, summary: `unsupported on ${process.platform}` };
990
+ }
991
+
992
+ export function serviceStatusSummary(): string {
993
+ return diagnoseService().summary;
842
994
  }
843
995
 
844
996
  export function normalizeServiceSubcommand(sub?: string): string {