fix: expose user agent bins to system service (#1292)
This commit is contained in:
@@ -64,7 +64,7 @@ describe("renderSystemdUnit", () => {
|
||||
mode: "user",
|
||||
});
|
||||
expect(unit).toMatch(
|
||||
/^Environment=PATH=%h\/\.local\/bin:%h\/\.bun\/bin:\/usr\/local\/bin:\/usr\/bin:\/bin:\/opt\/homebrew\/bin:\/home\/linuxbrew\/\.linuxbrew\/bin$/m,
|
||||
/^Environment=PATH=%h\/\.local\/bin:%h\/\.bun\/bin:%h\/\.opencode\/bin:\/usr\/local\/bin:\/usr\/bin:\/bin:\/opt\/homebrew\/bin:\/home\/linuxbrew\/\.linuxbrew\/bin$/m,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -78,7 +78,7 @@ describe("renderSystemdUnit", () => {
|
||||
expect(unit).toContain("%h/.bun/bin");
|
||||
});
|
||||
|
||||
it("omits %h/.local/bin from PATH for system-mode units", () => {
|
||||
it("includes run-as user agent bin dirs in PATH for system-mode units", () => {
|
||||
const unit = renderSystemdUnit({
|
||||
launcher: NPM_LAUNCHER,
|
||||
homeDir: "/var/lib/kandev",
|
||||
@@ -86,8 +86,9 @@ describe("renderSystemdUnit", () => {
|
||||
mode: "system",
|
||||
systemUser: "alice",
|
||||
});
|
||||
expect(unit).not.toContain("%h/.local/bin");
|
||||
expect(unit).toMatch(/^Environment=PATH=\/usr\/local\/bin/m);
|
||||
expect(unit).toMatch(
|
||||
/^Environment=PATH=%h\/\.local\/bin:%h\/\.bun\/bin:%h\/\.opencode\/bin:\/usr\/local\/bin/m,
|
||||
);
|
||||
});
|
||||
|
||||
it("sets WantedBy=multi-user.target and User= for system mode", () => {
|
||||
@@ -149,7 +150,7 @@ describe("renderSystemdUnit", () => {
|
||||
mode: "user",
|
||||
});
|
||||
expect(unit).toContain(
|
||||
"Environment=PATH=/home/alice/.local/share/fnm/node-versions/v24.14.0/installation/bin:%h/.local/bin:%h/.bun/bin:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin",
|
||||
"Environment=PATH=/home/alice/.local/share/fnm/node-versions/v24.14.0/installation/bin:%h/.local/bin:%h/.bun/bin:%h/.opencode/bin:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -160,9 +161,9 @@ describe("renderSystemdUnit", () => {
|
||||
logDir: "/home/alice/.kandev/logs",
|
||||
mode: "user",
|
||||
});
|
||||
// /usr/local/bin already in SYSTEMD_USER_PATH — dirname(nodePath) must not double it.
|
||||
// /usr/local/bin already in the base service PATH — dirname(nodePath) must not double it.
|
||||
expect(unit).toContain(
|
||||
"Environment=PATH=%h/.local/bin:%h/.bun/bin:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin",
|
||||
"Environment=PATH=%h/.local/bin:%h/.bun/bin:%h/.opencode/bin:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin",
|
||||
);
|
||||
expect(unit).not.toContain("/usr/local/bin:/usr/local/bin");
|
||||
});
|
||||
@@ -176,7 +177,7 @@ describe("renderSystemdUnit", () => {
|
||||
systemUser: "alice",
|
||||
});
|
||||
expect(unit).toContain(
|
||||
"Environment=PATH=/home/alice/.local/share/fnm/node-versions/v24.14.0/installation/bin:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin",
|
||||
"Environment=PATH=/home/alice/.local/share/fnm/node-versions/v24.14.0/installation/bin:%h/.local/bin:%h/.bun/bin:%h/.opencode/bin:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -196,7 +197,7 @@ describe("renderSystemdUnit", () => {
|
||||
});
|
||||
expect(unit).not.toMatch(/^Environment=PATH=\.:/m);
|
||||
expect(unit).toContain(
|
||||
"Environment=PATH=%h/.local/bin:%h/.bun/bin:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin",
|
||||
"Environment=PATH=%h/.local/bin:%h/.bun/bin:%h/.opencode/bin:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -246,7 +247,7 @@ describe("renderSystemdUnit", () => {
|
||||
// bin. The shim's own bin dir (/home/linuxbrew/.linuxbrew/bin) is already in
|
||||
// the base path, so it is not duplicated.
|
||||
expect(unit).toContain(
|
||||
"Environment=PATH=%h/.local/bin:%h/.bun/bin:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin",
|
||||
"Environment=PATH=%h/.local/bin:%h/.bun/bin:%h/.opencode/bin:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -276,7 +277,9 @@ describe("renderSystemdUnit", () => {
|
||||
});
|
||||
// Custom prefix's bin dir is prepended so npm/npx resolve, even though it
|
||||
// isn't one of the hardcoded default prefixes.
|
||||
expect(unit).toContain("Environment=PATH=/home/me/.brew/bin:%h/.local/bin:%h/.bun/bin:");
|
||||
expect(unit).toContain(
|
||||
"Environment=PATH=/home/me/.brew/bin:%h/.local/bin:%h/.bun/bin:%h/.opencode/bin:",
|
||||
);
|
||||
expect(unit).not.toContain("/Cellar/");
|
||||
});
|
||||
});
|
||||
@@ -456,7 +459,9 @@ describe("renderLaunchdPlist", () => {
|
||||
// The whole assignment must be wrapped, not just the value.
|
||||
expect(unit).toContain('Environment="KANDEV_HOME_DIR=/home/john doe/.kandev"');
|
||||
// PATH always contains colons but no spaces — should NOT be quoted.
|
||||
expect(unit).toMatch(/^Environment=PATH=%h\/\.local\/bin:%h\/\.bun\/bin:\/usr\/local\/bin/m);
|
||||
expect(unit).toMatch(
|
||||
/^Environment=PATH=%h\/\.local\/bin:%h\/\.bun\/bin:%h\/\.opencode\/bin:\/usr\/local\/bin/m,
|
||||
);
|
||||
});
|
||||
|
||||
it("escapes backslash + double-quote in Environment= and ExecStart values", () => {
|
||||
|
||||
@@ -21,12 +21,14 @@ export type UnitInputs = {
|
||||
serviceMetadataPath?: string;
|
||||
};
|
||||
|
||||
// User-mode PATH includes ~/.local/bin and ~/.bun/bin so user-installed agent
|
||||
// CLIs (npm user prefix, pipx, fnm, Bun globals like oh-my-pi/omp, etc.) are
|
||||
// discoverable.
|
||||
// Service PATH includes common per-user agent install dirs so user-installed
|
||||
// CLIs (npm user prefix, pipx, fnm, Bun globals like oh-my-pi/omp, OpenCode's
|
||||
// installer, etc.) are discoverable. System-mode units still run as the
|
||||
// configured User=, so %h resolves to that user's home directory.
|
||||
const SYSTEMD_SYSTEM_PATH =
|
||||
"/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin";
|
||||
const SYSTEMD_USER_PATH = `%h/.local/bin:%h/.bun/bin:${SYSTEMD_SYSTEM_PATH}`;
|
||||
const SYSTEMD_AGENT_BIN_PATH = `%h/.local/bin:%h/.bun/bin:%h/.opencode/bin`;
|
||||
const SYSTEMD_SERVICE_PATH = `${SYSTEMD_AGENT_BIN_PATH}:${SYSTEMD_SYSTEM_PATH}`;
|
||||
const LAUNCHD_SYSTEM_PATH = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
|
||||
const launchdUserPath = (): string =>
|
||||
`${os.homedir()}/.local/bin:${os.homedir()}/.bun/bin:${LAUNCHD_SYSTEM_PATH}`;
|
||||
@@ -79,7 +81,7 @@ export function looksLikeManagedUnit(content: string): boolean {
|
||||
*/
|
||||
export function renderSystemdUnit(input: UnitInputs): string {
|
||||
const shimPath = input.launcher.shimPath;
|
||||
const basePath = input.mode === "system" ? SYSTEMD_SYSTEM_PATH : SYSTEMD_USER_PATH;
|
||||
const basePath = SYSTEMD_SERVICE_PATH;
|
||||
// For the shim, prepend its own bin dir (the Homebrew prefix's `bin`, where
|
||||
// node/npm/npx live) so npx-based agents resolve even when the prefix isn't
|
||||
// one of the hardcoded defaults. pathWithNodeBinDir dedupes when it already is.
|
||||
|
||||
@@ -113,7 +113,7 @@ Type=simple
|
||||
ExecStart=/usr/local/bin/node /usr/local/lib/node_modules/kandev/bin/cli.js --headless
|
||||
Environment=KANDEV_HOME_DIR=/home/alice/.kandev
|
||||
Environment=KANDEV_LOG_LEVEL=info
|
||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin
|
||||
Environment=PATH=%h/.local/bin:%h/.bun/bin:%h/.opencode/bin:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin
|
||||
Environment=KANDEV_RUNNING_AS_SERVICE=true
|
||||
Environment=KANDEV_SERVICE_MODE=user
|
||||
Environment=KANDEV_SERVICE_MANAGER=systemd
|
||||
|
||||
Reference in New Issue
Block a user