fix(cli): escape launchd service names

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-27 15:32:13 +00:00
1 parent 8a7d9c1040
commit 8925d3f38a
2 files changed
+22 -1

No files matched your search

+17
View File
@@ -130,6 +130,23 @@ describe("launchd lifecycle", () => {
expect(calls).not.toContain(`launchctl enable gui/${process.getuid?.() ?? 0}/ing.paperclip.paperclipai.team-a`);
});
it("preserves disabled state when the service name contains regex metacharacters", async () => {
const userHome = await temporaryDirectory();
const calls: string[] = [];
const serviceName = "ing.paperclip.paperclipai.team[qa]+";
const runner: CommandRunner = async (command, args) => {
calls.push([command, ...args].join(" "));
if (args[0] === "print-disabled") return { stdout: `"${serviceName}" => true`, stderr: "" };
return { stdout: "", stderr: "" };
};
const manager = new LaunchdServiceManager("team[qa]+", runner, path.join(userHome, ".paperclip"), path.join(userHome, ".local/bin/paperclipai"), userHome);
await manager.start();
expect(calls).toContain(`launchctl disable gui/${process.getuid?.() ?? 0}/${serviceName}`);
expect(calls).not.toContain(`launchctl enable gui/${process.getuid?.() ?? 0}/${serviceName}`);
});
it("disables login startup and unloads the keepalive job when stopped", async () => {
const userHome = await temporaryDirectory();
const calls: string[] = [];
+5 -1
View File
@@ -64,6 +64,10 @@ function escapeXml(value: string): string {
return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export function resolveServiceShimPath(homeDir = os.homedir()): string {
return process.env.PAPERCLIP_SHIM_PATH?.trim() || path.join(homeDir, ".local", "bin", "paperclipai");
}
@@ -258,7 +262,7 @@ export class LaunchdServiceManager implements ServiceManager {
private async isEnabled(): Promise<boolean> {
try {
const result = await this.runner("launchctl", ["print-disabled", this.domain]);
return !new RegExp(`"${this.serviceName.replaceAll(".", "\\.")}"\\s*=>\\s*true`).test(result.stdout);
return !new RegExp(`"${escapeRegExp(this.serviceName)}"\\s*=>\\s*true`).test(result.stdout);
} catch { return true; }
}