fix(cli): use POSIX native service paths

This commit is contained in:
Federico Jaramillo Martinez
2026-07-13 13:10:34 +02:00
parent 767e653028
commit a8aee7b550
4 changed files with 21 additions and 7 deletions
+14 -1
View File
@@ -84,7 +84,7 @@ describe("installed native-service mode and definition inspection", () => {
expect(inferInstalledNativeServiceMode(new Set(["sessiond"]))).toBe("ambiguous"); 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); const plan = developmentPlan(kind);
expect(inspectInstalledDevelopmentServiceInput(plan.backend, renderedDefinitions(plan))).toEqual({ expect(inspectInstalledDevelopmentServiceInput(plan.backend, renderedDefinitions(plan))).toEqual({
ok: true, 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", () => { it("inspects legacy systemd definitions without /usr/bin/env or quoted working directories", () => {
const plan = developmentPlan("systemd"); const plan = developmentPlan("systemd");
const definitions = renderedDefinitions(plan).map((definition) => ({ const definitions = renderedDefinitions(plan).map((definition) => ({
+3 -3
View File
@@ -1,4 +1,4 @@
import { basename, join } from "node:path"; import { posix as posixPath } from "node:path";
import { import {
createDevelopmentNativeServicePlan, createDevelopmentNativeServicePlan,
nativeServicePrerequisiteNeedsPathAdvice, nativeServicePrerequisiteNeedsPathAdvice,
@@ -135,7 +135,7 @@ export function inspectInstalledDevelopmentServiceInput(
shell: first.shell, shell: first.shell,
environment: first.environment, environment: first.environment,
workingDirectory: first.workingDirectory, workingDirectory: first.workingDirectory,
packageJsonPath: join(first.workingDirectory, "package.json"), packageJsonPath: posixPath.join(first.workingDirectory, "package.json"),
}; };
const expectedPlan = createDevelopmentNativeServicePlan(input); const expectedPlan = createDevelopmentNativeServicePlan(input);
for (const definition of parsed.value) { for (const definition of parsed.value) {
@@ -436,7 +436,7 @@ function parseLaunchdDefinition(
} }
function installedShell(executable: string): InstalledNativeServiceInspection<NativeServiceShell> { function installedShell(executable: string): InstalledNativeServiceInspection<NativeServiceShell> {
const name = basename(executable).replace(/^-/, ""); const name = posixPath.basename(executable).replace(/^-/, "");
if (name !== "bash" && name !== "zsh" && name !== "fish") { if (name !== "bash" && name !== "zsh" && name !== "fish") {
return { ok: false, message: `Installed service definition uses unsupported login shell ${executable}.` }; return { ok: false, message: `Installed service definition uses unsupported login shell ${executable}.` };
} }
+2 -1
View File
@@ -70,7 +70,8 @@ describe("native service rendering", () => {
expect(plist).toContain("<string>exec npm run start:sessiond</string>"); expect(plist).toContain("<string>exec npm run start:sessiond</string>");
expect(plist).toContain("<key>WorkingDirectory</key>\n <string>/checkout with space</string>"); expect(plist).toContain("<key>WorkingDirectory</key>\n <string>/checkout with space</string>");
expect(plist).toContain("<key>PI_WEB_CONFIG</key>\n <string>/home/user/config with &quot;quote&quot;.json</string>"); expect(plist).toContain("<key>PI_WEB_CONFIG</key>\n <string>/home/user/config with &quot;quote&quot;.json</string>");
expect(plist).toContain("<string>/logs/sessiond.log</string>"); expect(plist.match(/<string>\/logs\/sessiond\.log<\/string>/gu)).toHaveLength(2);
expect(plist).not.toContain("<string>\\logs\\sessiond.log</string>");
expect(plist).not.toContain("<key>KeepAlive</key>"); expect(plist).not.toContain("<key>KeepAlive</key>");
}); });
+2 -2
View File
@@ -1,4 +1,4 @@
import { join } from "node:path"; import { posix as posixPath } from "node:path";
import type { import type {
NativeServiceId, NativeServiceId,
NativeServicePlan, NativeServicePlan,
@@ -42,7 +42,7 @@ export function renderLaunchdPlist(
const keepAlive = service.restart === "on-failure" const keepAlive = service.restart === "on-failure"
? " <key>KeepAlive</key>\n <dict>\n <key>SuccessfulExit</key>\n <false/>\n </dict>\n" ? " <key>KeepAlive</key>\n <dict>\n <key>SuccessfulExit</key>\n <false/>\n </dict>\n"
: ""; : "";
const logPath = join(logDirectory, service.manager.logName); const logPath = posixPath.join(logDirectory, service.manager.logName);
return `<?xml version="1.0" encoding="UTF-8"?> return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">