From a8aee7b550e9d61dc71470a8cb84557ce05172fb Mon Sep 17 00:00:00 2001 From: Federico Jaramillo Martinez Date: Mon, 13 Jul 2026 13:10:34 +0200 Subject: [PATCH] fix(cli): use POSIX native service paths --- src/nativeServices/serviceDoctor.test.ts | 15 ++++++++++++++- src/nativeServices/serviceDoctor.ts | 6 +++--- src/nativeServices/serviceRendering.test.ts | 3 ++- src/nativeServices/serviceRendering.ts | 4 ++-- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/nativeServices/serviceDoctor.test.ts b/src/nativeServices/serviceDoctor.test.ts index 3da5e8d..49d24fc 100644 --- a/src/nativeServices/serviceDoctor.test.ts +++ b/src/nativeServices/serviceDoctor.test.ts @@ -84,7 +84,7 @@ describe("installed native-service mode and definition inspection", () => { expect(inferInstalledNativeServiceMode(new Set(["sessiond"]))).toBe("ambiguous"); }); - it.each(["systemd", "launchd"] as const)("reconstructs an exact installed development input from %s definitions", (kind) => { + it.each(["systemd", "launchd"] as const)("reconstructs POSIX development paths from %s definitions on every host", (kind) => { const plan = developmentPlan(kind); expect(inspectInstalledDevelopmentServiceInput(plan.backend, renderedDefinitions(plan))).toEqual({ ok: true, @@ -124,6 +124,19 @@ describe("installed native-service mode and definition inspection", () => { }); }); + it("interprets installed shell executable paths with POSIX semantics", () => { + const plan = developmentPlan("systemd"); + const definitions = renderedDefinitions(plan).map((definition) => ({ + ...definition, + contents: definition.contents.replace('"/bin/zsh"', '"/bin/not-zsh\\\\zsh"'), + })); + + const inspection = inspectInstalledDevelopmentServiceInput(plan.backend, definitions); + expect(inspection.ok).toBe(false); + if (inspection.ok) throw new Error("Expected the POSIX shell basename inspection to fail"); + expect(inspection.message).toContain("unsupported login shell"); + }); + it("inspects legacy systemd definitions without /usr/bin/env or quoted working directories", () => { const plan = developmentPlan("systemd"); const definitions = renderedDefinitions(plan).map((definition) => ({ diff --git a/src/nativeServices/serviceDoctor.ts b/src/nativeServices/serviceDoctor.ts index 9bc0a1e..c1043da 100644 --- a/src/nativeServices/serviceDoctor.ts +++ b/src/nativeServices/serviceDoctor.ts @@ -1,4 +1,4 @@ -import { basename, join } from "node:path"; +import { posix as posixPath } from "node:path"; import { createDevelopmentNativeServicePlan, nativeServicePrerequisiteNeedsPathAdvice, @@ -135,7 +135,7 @@ export function inspectInstalledDevelopmentServiceInput( shell: first.shell, environment: first.environment, workingDirectory: first.workingDirectory, - packageJsonPath: join(first.workingDirectory, "package.json"), + packageJsonPath: posixPath.join(first.workingDirectory, "package.json"), }; const expectedPlan = createDevelopmentNativeServicePlan(input); for (const definition of parsed.value) { @@ -436,7 +436,7 @@ function parseLaunchdDefinition( } function installedShell(executable: string): InstalledNativeServiceInspection { - const name = basename(executable).replace(/^-/, ""); + const name = posixPath.basename(executable).replace(/^-/, ""); if (name !== "bash" && name !== "zsh" && name !== "fish") { return { ok: false, message: `Installed service definition uses unsupported login shell ${executable}.` }; } diff --git a/src/nativeServices/serviceRendering.test.ts b/src/nativeServices/serviceRendering.test.ts index 617d407..5fe6567 100644 --- a/src/nativeServices/serviceRendering.test.ts +++ b/src/nativeServices/serviceRendering.test.ts @@ -70,7 +70,8 @@ describe("native service rendering", () => { expect(plist).toContain("exec npm run start:sessiond"); expect(plist).toContain("WorkingDirectory\n /checkout with space"); expect(plist).toContain("PI_WEB_CONFIG\n /home/user/config with "quote".json"); - expect(plist).toContain("/logs/sessiond.log"); + expect(plist.match(/\/logs\/sessiond\.log<\/string>/gu)).toHaveLength(2); + expect(plist).not.toContain("\\logs\\sessiond.log"); expect(plist).not.toContain("KeepAlive"); }); diff --git a/src/nativeServices/serviceRendering.ts b/src/nativeServices/serviceRendering.ts index a421f9c..401ce13 100644 --- a/src/nativeServices/serviceRendering.ts +++ b/src/nativeServices/serviceRendering.ts @@ -1,4 +1,4 @@ -import { join } from "node:path"; +import { posix as posixPath } from "node:path"; import type { NativeServiceId, NativeServicePlan, @@ -42,7 +42,7 @@ export function renderLaunchdPlist( const keepAlive = service.restart === "on-failure" ? " KeepAlive\n \n SuccessfulExit\n \n \n" : ""; - const logPath = join(logDirectory, service.manager.logName); + const logPath = posixPath.join(logDirectory, service.manager.logName); return `