Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions apps/cli/src/client-env.ts
Original file line number Diff line number Diff line change
@@ -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";
4 changes: 4 additions & 0 deletions apps/cli/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
18 changes: 13 additions & 5 deletions apps/local/src/installation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions packages/core/integrations-registry/src/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
);
});

Expand Down
5 changes: 4 additions & 1 deletion packages/core/integrations-registry/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Loading