From 120fd2f702bd272d69abd0fd0fdbc817c23c5abe Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 17 Jun 2026 17:41:02 +0800 Subject: [PATCH] refactor(cli): hide shell support internals --- src/bootstrap/node-extra-ca-certs.test.ts | 19 +++++++++++-------- src/bootstrap/node-extra-ca-certs.ts | 2 +- src/bootstrap/node-startup-env.test.ts | 14 +++++++------- src/cli/completion-fish.test.ts | 5 ----- src/cli/completion-fish.ts | 2 +- src/cli/completion-runtime.test.ts | 10 +++------- src/cli/completion-runtime.ts | 15 +++++---------- 7 files changed, 28 insertions(+), 39 deletions(-) diff --git a/src/bootstrap/node-extra-ca-certs.test.ts b/src/bootstrap/node-extra-ca-certs.test.ts index 47a6ca9b2360..8a1e3c091062 100644 --- a/src/bootstrap/node-extra-ca-certs.test.ts +++ b/src/bootstrap/node-extra-ca-certs.test.ts @@ -2,11 +2,14 @@ import { describe, expect, it } from "vitest"; import { isNodeVersionManagerRuntime, - LINUX_CA_BUNDLE_PATHS, resolveAutoNodeExtraCaCerts, resolveLinuxSystemCaBundle, } from "./node-extra-ca-certs.js"; +const DEBIAN_CA_BUNDLE_PATH = "/etc/ssl/certs/ca-certificates.crt"; +const FEDORA_CA_BUNDLE_PATH = "/etc/pki/tls/certs/ca-bundle.crt"; +const GENERIC_CA_BUNDLE_PATH = "/etc/ssl/ca-bundle.pem"; + function allowOnly(path: string) { return (candidate: string) => { if (candidate !== path) { @@ -20,7 +23,7 @@ describe("resolveLinuxSystemCaBundle", () => { expect( resolveLinuxSystemCaBundle({ platform: "darwin", - accessSync: allowOnly(LINUX_CA_BUNDLE_PATHS[0]), + accessSync: allowOnly(DEBIAN_CA_BUNDLE_PATH), }), ).toBeUndefined(); }); @@ -29,9 +32,9 @@ describe("resolveLinuxSystemCaBundle", () => { expect( resolveLinuxSystemCaBundle({ platform: "linux", - accessSync: allowOnly(LINUX_CA_BUNDLE_PATHS[1]), + accessSync: allowOnly(FEDORA_CA_BUNDLE_PATH), }), - ).toBe(LINUX_CA_BUNDLE_PATHS[1]); + ).toBe(FEDORA_CA_BUNDLE_PATH); }); }); @@ -120,7 +123,7 @@ describe("resolveAutoNodeExtraCaCerts", () => { NODE_EXTRA_CA_CERTS: "/custom/ca.pem", }, platform: "linux", - accessSync: allowOnly(LINUX_CA_BUNDLE_PATHS[0]), + accessSync: allowOnly(DEBIAN_CA_BUNDLE_PATH), }), ).toBeUndefined(); }); @@ -131,7 +134,7 @@ describe("resolveAutoNodeExtraCaCerts", () => { env: {}, platform: "linux", execPath: "/usr/bin/node", - accessSync: allowOnly(LINUX_CA_BUNDLE_PATHS[0]), + accessSync: allowOnly(DEBIAN_CA_BUNDLE_PATH), }), ).toBeUndefined(); }); @@ -142,8 +145,8 @@ describe("resolveAutoNodeExtraCaCerts", () => { env: { NVM_DIR: "/home/test/.nvm" }, platform: "linux", execPath: "/usr/bin/node", - accessSync: allowOnly(LINUX_CA_BUNDLE_PATHS[2]), + accessSync: allowOnly(GENERIC_CA_BUNDLE_PATH), }), - ).toBe(LINUX_CA_BUNDLE_PATHS[2]); + ).toBe(GENERIC_CA_BUNDLE_PATH); }); }); diff --git a/src/bootstrap/node-extra-ca-certs.ts b/src/bootstrap/node-extra-ca-certs.ts index b3cd3aba0d97..c1b2396dd252 100644 --- a/src/bootstrap/node-extra-ca-certs.ts +++ b/src/bootstrap/node-extra-ca-certs.ts @@ -1,7 +1,7 @@ // Resolves additional CA certificate settings for Node child processes. import fs from "node:fs"; -export const LINUX_CA_BUNDLE_PATHS = [ +const LINUX_CA_BUNDLE_PATHS = [ "/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/certs/ca-bundle.crt", "/etc/ssl/ca-bundle.pem", diff --git a/src/bootstrap/node-startup-env.test.ts b/src/bootstrap/node-startup-env.test.ts index eb6925e60562..94e6a6897606 100644 --- a/src/bootstrap/node-startup-env.test.ts +++ b/src/bootstrap/node-startup-env.test.ts @@ -1,8 +1,10 @@ // Verifies startup environment merge behavior for Node subprocesses. import { describe, expect, it } from "vitest"; -import { LINUX_CA_BUNDLE_PATHS } from "./node-extra-ca-certs.js"; import { resolveNodeStartupTlsEnvironment } from "./node-startup-env.js"; +const FEDORA_CA_BUNDLE_PATH = "/etc/pki/tls/certs/ca-bundle.crt"; +const GENERIC_CA_BUNDLE_PATH = "/etc/ssl/ca-bundle.pem"; + function allowOnly(path: string) { return (candidate: string) => { if (candidate !== path) { @@ -45,10 +47,10 @@ describe("resolveNodeStartupTlsEnvironment", () => { env: { NVM_DIR: "/home/test/.nvm" }, platform: "linux", execPath: "/usr/bin/node", - accessSync: allowOnly(LINUX_CA_BUNDLE_PATHS[1]), + accessSync: allowOnly(FEDORA_CA_BUNDLE_PATH), }), ).toEqual({ - NODE_EXTRA_CA_CERTS: LINUX_CA_BUNDLE_PATHS[1], + NODE_EXTRA_CA_CERTS: FEDORA_CA_BUNDLE_PATH, NODE_USE_SYSTEM_CA: undefined, }); }); @@ -71,10 +73,8 @@ describe("resolveNodeStartupTlsEnvironment", () => { env: { NVM_DIR: "/home/test/.nvm" }, platform: "linux", execPath: "/usr/bin/node", - accessSync: allowOnly(LINUX_CA_BUNDLE_PATHS[2]), + accessSync: allowOnly(GENERIC_CA_BUNDLE_PATH), }).NODE_EXTRA_CA_CERTS; - if (value !== undefined) { - expect(LINUX_CA_BUNDLE_PATHS).toContain(value); - } + expect(value).toBe(GENERIC_CA_BUNDLE_PATH); }); }); diff --git a/src/cli/completion-fish.test.ts b/src/cli/completion-fish.test.ts index 7c61b05ca81a..6ae4f87371dc 100644 --- a/src/cli/completion-fish.test.ts +++ b/src/cli/completion-fish.test.ts @@ -3,14 +3,9 @@ import { describe, expect, it } from "vitest"; import { buildFishOptionCompletionLine, buildFishSubcommandCompletionLine, - escapeFishDescription, } from "./completion-fish.js"; describe("completion-fish helpers", () => { - it("escapes single quotes in descriptions", () => { - expect(escapeFishDescription("Bob's plugin")).toBe("Bob'\\''s plugin"); - }); - it("builds a subcommand completion line", () => { const line = buildFishSubcommandCompletionLine({ rootCmd: "openclaw", diff --git a/src/cli/completion-fish.ts b/src/cli/completion-fish.ts index 179febae09b3..a5b7954da2f8 100644 --- a/src/cli/completion-fish.ts +++ b/src/cli/completion-fish.ts @@ -1,5 +1,5 @@ // Fish completion line builders for subcommands and options. -export function escapeFishDescription(value: string): string { +function escapeFishDescription(value: string): string { return value.replace(/'/g, "'\\''"); } diff --git a/src/cli/completion-runtime.test.ts b/src/cli/completion-runtime.test.ts index 4e3adff652a9..cd19b79e3d5b 100644 --- a/src/cli/completion-runtime.test.ts +++ b/src/cli/completion-runtime.test.ts @@ -5,7 +5,6 @@ import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { formatCompletionReloadCommand, - formatCompletionSourceLine, installCompletion, resolveCompletionCachePath, resolveCompletionProfilePath, @@ -29,10 +28,7 @@ describe("completion-runtime", () => { } }); - it("formats PowerShell source and reload commands with single-quoted paths", () => { - expect( - formatCompletionSourceLine("powershell", "openclaw", "C:\\Users\\Ada\\open'claw.ps1"), - ).toBe(". 'C:\\Users\\Ada\\open''claw.ps1'"); + it("formats PowerShell reload commands with single-quoted paths", () => { expect(formatCompletionReloadCommand("powershell", "C:\\Users\\Ada\\profile.ps1")).toBe( ". 'C:\\Users\\Ada\\profile.ps1'", ); @@ -88,7 +84,7 @@ describe("completion-runtime", () => { it("installs PowerShell completion into the concrete profile path", async () => { const homeDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-completion-home-")); - const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-completion-state-")); + const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-completion-state-bob's-")); process.env.HOME = homeDir; process.env.OPENCLAW_STATE_DIR = stateDir; @@ -102,7 +98,7 @@ describe("completion-runtime", () => { const profilePath = resolveCompletionProfilePath("powershell"); const profile = await fs.readFile(profilePath, "utf-8"); - expect(profile).toBe(`# OpenClaw Completion\n. '${cachePath}'\n`); + expect(profile).toBe(`# OpenClaw Completion\n. '${cachePath.replace(/'/g, "''")}'\n`); } finally { await fs.rm(homeDir, { recursive: true, force: true }); await fs.rm(stateDir, { recursive: true, force: true }); diff --git a/src/cli/completion-runtime.ts b/src/cli/completion-runtime.ts index bcfa0dd6ea64..65e2cb66520b 100644 --- a/src/cli/completion-runtime.ts +++ b/src/cli/completion-runtime.ts @@ -82,12 +82,7 @@ function escapePowerShellSingleQuotedString(value: string): string { return value.replace(/'/g, "''"); } -/** Formats the profile line that sources the cached completion script for a shell. */ -export function formatCompletionSourceLine( - shell: CompletionShell, - _binName: string, - cachePath: string, -): string { +function formatCompletionSourceLine(shell: CompletionShell, cachePath: string): string { if (shell === "powershell") { return `. '${escapePowerShellSingleQuotedString(cachePath)}'`; } @@ -258,7 +253,7 @@ export async function installCompletion(shell: string, yes: boolean, binName = " switch (shell) { case "zsh": profilePath = resolveCompletionProfilePath("zsh"); - sourceLine = formatCompletionSourceLine("zsh", binName, cachePath); + sourceLine = formatCompletionSourceLine("zsh", cachePath); break; case "bash": profilePath = resolveCompletionProfilePath("bash"); @@ -268,15 +263,15 @@ export async function installCompletion(shell: string, yes: boolean, binName = " const home = process.env.HOME || os.homedir(); profilePath = path.join(home, ".bash_profile"); } - sourceLine = formatCompletionSourceLine("bash", binName, cachePath); + sourceLine = formatCompletionSourceLine("bash", cachePath); break; case "fish": profilePath = resolveCompletionProfilePath("fish"); - sourceLine = formatCompletionSourceLine("fish", binName, cachePath); + sourceLine = formatCompletionSourceLine("fish", cachePath); break; case "powershell": profilePath = resolveCompletionProfilePath("powershell"); - sourceLine = formatCompletionSourceLine("powershell", binName, cachePath); + sourceLine = formatCompletionSourceLine("powershell", cachePath); break; }