diff --git a/src/node/cli.ts b/src/node/cli.ts index 0946c8e04344..d91f4e6a89b3 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -97,6 +97,7 @@ export interface UserProvidedArgs extends UserProvidedCodeArgs { "abs-proxy-base-path"?: string i18n?: string "idle-timeout-seconds"?: number + "vscode-option"?: string[] /* Positional arguments. */ _?: string[] } @@ -322,6 +323,13 @@ export const options: Options> = { "Override the reconnection grace time in seconds. Clients who disconnect for longer than this duration will need to \n" + "reload the window. Defaults to 10800 (3 hours).", }, + "vscode-option": { + type: "string[]", + description: + "Pass an option straight through to the VS Code server as flag=value, or as a bare flag for a \n" + + "boolean. Repeatable; repeating the same flag builds an array. Use this to reach VS Code options \n" + + "code-server does not model itself, e.g. --vscode-option enable-sandbox --vscode-option agents=true.", + }, } export const optionDescriptions = (opts: Partial>> = options): string[] => { @@ -643,6 +651,15 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config args["reconnection-grace-time"] = process.env.CODE_SERVER_RECONNECTION_GRACE_TIME } + // Space-separated, like NODE_OPTIONS. Appended to any flags rather than + // replacing them so the two can be combined. + if (process.env.VSCODE_OPTIONS) { + args["vscode-option"] = [ + ...(args["vscode-option"] ?? []), + ...process.env.VSCODE_OPTIONS.split(/\s+/).filter((option) => option), + ] + } + if (process.env.CODE_SERVER_IDLE_TIMEOUT_SECONDS) { if (isNaN(Number(process.env.CODE_SERVER_IDLE_TIMEOUT_SECONDS))) { logger.info("CODE_SERVER_IDLE_TIMEOUT_SECONDS must be a number") @@ -909,17 +926,58 @@ export interface CodeArgs extends UserProvidedCodeArgs { log?: string[] } +/** + * Expand --vscode-option entries into VS Code server arguments. + * + * An entry is `flag=value`, or a bare `flag` meaning true. A leading `--` on + * the flag is optional, so both spellings people reach for work. Repeating a + * flag collects the values into an array, since several VS Code options take + * one. + * + * `true` and `false` become booleans rather than strings. VS Code tests these + * flags for truthiness and the string "false" is truthy, so passing it along + * verbatim would quietly do the opposite of what was asked. + */ +export const parseVscodeOptions = (entries: string[]): Record => { + const parsed: Record = {} + + for (const entry of entries) { + const [flag, rawValue] = splitOnFirstEquals(entry.replace(/^--/, "")) + if (!flag) { + throw new Error(`--vscode-option requires a flag name (got "${entry}")`) + } + + const value: string | boolean = + typeof rawValue === "undefined" || rawValue === "true" ? true : rawValue === "false" ? false : rawValue + + const existing = parsed[flag] + if (typeof existing === "undefined") { + parsed[flag] = value + } else if (Array.isArray(existing)) { + existing.push(String(value)) + } else { + parsed[flag] = [String(existing), String(value)] + } + } + + return parsed +} + /** * Convert our arguments to equivalent VS Code server arguments. * Does not add any extra arguments. */ export const toCodeArgs = async (args: DefaultedArgs): Promise => { + // The passthrough option is ours; VS Code has no idea what it is. + const { "vscode-option": vscodeOptions, ...rest } = args return { - ...args, + ...rest, /** Type casting. */ help: !!args.help, version: !!args.version, port: args.port?.toString(), log: args.log ? [args.log] : undefined, - } + // Last, so that reaching an option code-server does model still works. + ...parseVscodeOptions(vscodeOptions ?? []), + } as CodeArgs } diff --git a/test/unit/node/cli.test.ts b/test/unit/node/cli.test.ts index 85e16f38ca99..53eb066c1e70 100644 --- a/test/unit/node/cli.test.ts +++ b/test/unit/node/cli.test.ts @@ -51,6 +51,7 @@ describe("parser", () => { delete process.env.CODE_SERVER_RECONNECTION_GRACE_TIME delete process.env.VSCODE_PROXY_URI delete process.env.CS_DISABLE_PROXY + delete process.env.VSCODE_OPTIONS console.log = jest.fn() }) @@ -413,6 +414,17 @@ describe("parser", () => { }) }) + it("should use env var VSCODE_OPTIONS", async () => { + process.env.VSCODE_OPTIONS = "--enable-sandbox agents=true" + const args = parse(["--vscode-option", "verbose-logging"]) + + const defaultArgs = await setDefaults(args) + expect(defaultArgs).toEqual({ + ...defaults, + "vscode-option": ["verbose-logging", "--enable-sandbox", "agents=true"], + }) + }) + it("should use env var CS_DISABLE_GETTING_STARTED_OVERRIDE", async () => { process.env.CS_DISABLE_GETTING_STARTED_OVERRIDE = "1" const args = parse([]) @@ -1006,6 +1018,44 @@ describe("toCodeArgs", () => { _: [file], }) }) + + it("should pass through --vscode-option", async () => { + const args = parse([ + "--vscode-option", + "enable-sandbox", + "--vscode-option", + "agents=true", + "--vscode-option", + "enable-smoke-test-driver=false", + ]) + expect(await toCodeArgs(await setDefaults(args))).toStrictEqual({ + ...vscodeDefaults, + "enable-sandbox": true, + agents: true, + "enable-smoke-test-driver": false, + }) + }) + + it("should collect a repeated --vscode-option into an array", async () => { + const args = parse([ + "--vscode-option", + "locate-extension=a", + "--vscode-option", + "locate-extension=b", + "--vscode-option", + "locate-extension=c", + ]) + expect(await toCodeArgs(await setDefaults(args))).toStrictEqual({ + ...vscodeDefaults, + "locate-extension": ["a", "b", "c"], + }) + }) + + it("should error if --vscode-option has no flag", async () => { + await expect(toCodeArgs(await setDefaults(parse(["--vscode-option", "=nothing"])))).rejects.toThrow( + "--vscode-option requires a flag name", + ) + }) }) describe("optionDescriptions", () => {