refactor(cli): hide shell support internals

This commit is contained in:
Vincent Koc
2026-06-17 17:41:02 +08:00
parent 582c2d41b9
commit 120fd2f702
7 changed files with 28 additions and 39 deletions

View File

@@ -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);
});
});

View File

@@ -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",

View File

@@ -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);
});
});

View File

@@ -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",

View File

@@ -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, "'\\''");
}

View File

@@ -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 });

View File

@@ -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;
}