diff --git a/apps/cli/src/client-env.ts b/apps/cli/src/client-env.ts new file mode 100644 index 000000000..73dc4b0f3 --- /dev/null +++ b/apps/cli/src/client-env.ts @@ -0,0 +1,10 @@ +// Stamp the surface client for this process so the integrations User-Agent is +// always `cli` or `desktop`, never guessed. The desktop app sets +// EXECUTOR_CLIENT=desktop before spawning the CLI as its sidecar/daemon, so +// `??=` preserves that; every other invocation is the CLI itself. +// +// This MUST run before the `@executor-js/local` import graph loads: apps/local +// builds its User-Agent from EXECUTOR_CLIENT at module init (installation.ts), +// so setting the env in main.ts's body would run too late. Imported as an early +// side effect in main.ts, right after native-bindings. +process.env.EXECUTOR_CLIENT ??= "cli"; diff --git a/apps/cli/src/main.ts b/apps/cli/src/main.ts index c6e4df0e0..1ab045bf7 100644 --- a/apps/cli/src/main.ts +++ b/apps/cli/src/main.ts @@ -2,6 +2,10 @@ // before any import (e.g. `@executor-js/local` → libSQL) eagerly loads them. import "./native-bindings"; +// Stamp EXECUTOR_CLIENT=cli (unless the desktop app already set `desktop`) +// before `@executor-js/local` builds its integrations User-Agent from it. +import "./client-env"; + import { randomUUID } from "node:crypto"; import { existsSync, realpathSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; diff --git a/apps/local/src/installation.ts b/apps/local/src/installation.ts index 90f7ea50c..67fde2658 100644 --- a/apps/local/src/installation.ts +++ b/apps/local/src/installation.ts @@ -15,11 +15,19 @@ const resolveChannel = (version: string): InstallationChannel => { return "stable"; }; -// The desktop main process sets `EXECUTOR_CLIENT=desktop` when it spawns the -// sidecar so PostHog can slice desktop installs from headless apps/local -// (CLI `executor web`, `daemon run --foreground`, etc.). -const resolveClient = (): SurfaceClient => - process.env.EXECUTOR_CLIENT === "desktop" ? "desktop" : "local"; +// EXECUTOR_CLIENT identifies which product launched this headless apps/local +// server: the desktop app sets `desktop` before spawning it, and the executor +// CLI sets `cli` (apps/cli stamps it at startup, see src/client-env.ts). It is +// therefore always set to a valid surface in a real launch, so we pass it +// through rather than guess. The `cli` floor only covers direct library/test +// imports that boot the server without going through a launcher. +const SURFACE_CLIENTS = ["cli", "desktop"] as const; +const resolveClient = (): SurfaceClient => { + const client = process.env.EXECUTOR_CLIENT; + return (SURFACE_CLIENTS as readonly string[]).includes(client ?? "") + ? (client as SurfaceClient) + : "cli"; +}; export const CHANNEL: InstallationChannel = resolveChannel(LOCAL_VERSION); export const VERSION: string = LOCAL_VERSION; diff --git a/packages/core/integrations-registry/src/registry.test.ts b/packages/core/integrations-registry/src/registry.test.ts index c4ec2a420..20da742fb 100644 --- a/packages/core/integrations-registry/src/registry.test.ts +++ b/packages/core/integrations-registry/src/registry.test.ts @@ -49,8 +49,8 @@ describe("buildUserAgent", () => { expect(buildUserAgent({ channel: "stable", version: "1.2.3", client: "cli" })).toBe( "executor/stable/1.2.3/cli", ); - expect(buildUserAgent({ channel: "beta", version: "1.2.3-beta.0", client: "local" })).toBe( - "executor/beta/1.2.3-beta.0/local", + expect(buildUserAgent({ channel: "beta", version: "1.2.3-beta.0", client: "desktop" })).toBe( + "executor/beta/1.2.3-beta.0/desktop", ); }); diff --git a/packages/core/integrations-registry/src/registry.ts b/packages/core/integrations-registry/src/registry.ts index b203f5444..86957d483 100644 --- a/packages/core/integrations-registry/src/registry.ts +++ b/packages/core/integrations-registry/src/registry.ts @@ -11,7 +11,10 @@ import { NodeFileSystem } from "@effect/platform-node"; // --------------------------------------------------------------------------- export type InstallationChannel = "stable" | "beta" | "dev"; -export type SurfaceClient = "cli" | "local" | "desktop"; +// Only two surfaces: the desktop app (sets EXECUTOR_CLIENT=desktop) or the +// `executor` CLI (everything else, including the headless apps/local daemon, +// `executor mcp`, and the installed background service). +export type SurfaceClient = "cli" | "desktop"; export const DEFAULT_INTEGRATIONS_URL = "https://integrations.sh/api.json";