2020-05-10 07:19:32 +02:00
|
|
|
import * as fs from "fs-extra"
|
|
|
|
import yaml from "js-yaml"
|
2020-02-06 20:11:38 +01:00
|
|
|
import * as path from "path"
|
2020-02-07 01:26:07 +01:00
|
|
|
import { field, logger, Level } from "@coder/logger"
|
2020-02-06 19:29:19 +01:00
|
|
|
import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc"
|
2020-02-06 20:11:38 +01:00
|
|
|
import { AuthType } from "./http"
|
|
|
|
import { xdgLocalDir } from "./util"
|
2020-05-10 07:19:32 +02:00
|
|
|
import xdgBasedir from "xdg-basedir"
|
2020-02-06 19:29:19 +01:00
|
|
|
|
2020-02-07 01:26:07 +01:00
|
|
|
export class Optional<T> {
|
|
|
|
public constructor(public readonly value?: T) {}
|
|
|
|
}
|
|
|
|
|
2020-02-19 18:06:32 +01:00
|
|
|
export enum LogLevel {
|
|
|
|
Trace = "trace",
|
|
|
|
Debug = "debug",
|
|
|
|
Info = "info",
|
|
|
|
Warn = "warn",
|
|
|
|
Error = "error",
|
|
|
|
}
|
|
|
|
|
2020-02-07 01:26:07 +01:00
|
|
|
export class OptionalString extends Optional<string> {}
|
|
|
|
|
2020-02-06 19:29:19 +01:00
|
|
|
export interface Args extends VsArgs {
|
2020-05-10 07:19:32 +02:00
|
|
|
readonly config?: string
|
2020-02-07 01:26:07 +01:00
|
|
|
readonly auth?: AuthType
|
|
|
|
readonly cert?: OptionalString
|
|
|
|
readonly "cert-key"?: string
|
2020-02-14 22:57:51 +01:00
|
|
|
readonly "disable-updates"?: boolean
|
2020-02-18 19:24:12 +01:00
|
|
|
readonly "disable-telemetry"?: boolean
|
2020-02-07 01:26:07 +01:00
|
|
|
readonly help?: boolean
|
|
|
|
readonly host?: string
|
|
|
|
readonly json?: boolean
|
2020-02-19 18:06:32 +01:00
|
|
|
log?: LogLevel
|
2020-02-07 01:26:07 +01:00
|
|
|
readonly open?: boolean
|
|
|
|
readonly port?: number
|
2020-04-27 15:22:52 +02:00
|
|
|
readonly "bind-addr"?: string
|
2020-02-07 01:26:07 +01:00
|
|
|
readonly socket?: string
|
|
|
|
readonly version?: boolean
|
2020-03-05 17:39:42 +01:00
|
|
|
readonly force?: boolean
|
2020-02-18 19:24:12 +01:00
|
|
|
readonly "list-extensions"?: boolean
|
|
|
|
readonly "install-extension"?: string[]
|
2020-03-13 19:17:59 +01:00
|
|
|
readonly "show-versions"?: boolean
|
2020-02-18 19:24:12 +01:00
|
|
|
readonly "uninstall-extension"?: string[]
|
2020-03-23 18:08:50 +01:00
|
|
|
readonly "proxy-domain"?: string[]
|
2020-02-21 00:36:38 +01:00
|
|
|
readonly locale?: string
|
2020-02-07 01:26:07 +01:00
|
|
|
readonly _: string[]
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Option<T> {
|
|
|
|
type: T
|
|
|
|
/**
|
|
|
|
* Short flag for the option.
|
|
|
|
*/
|
|
|
|
short?: string
|
|
|
|
/**
|
|
|
|
* Whether the option is a path and should be resolved.
|
|
|
|
*/
|
|
|
|
path?: boolean
|
|
|
|
/**
|
|
|
|
* Description of the option. Leave blank to hide the option.
|
|
|
|
*/
|
|
|
|
description?: string
|
|
|
|
}
|
|
|
|
|
|
|
|
type OptionType<T> = T extends boolean
|
|
|
|
? "boolean"
|
|
|
|
: T extends OptionalString
|
|
|
|
? typeof OptionalString
|
2020-02-19 18:06:32 +01:00
|
|
|
: T extends LogLevel
|
|
|
|
? typeof LogLevel
|
2020-02-07 01:26:07 +01:00
|
|
|
: T extends AuthType
|
|
|
|
? typeof AuthType
|
|
|
|
: T extends number
|
|
|
|
? "number"
|
|
|
|
: T extends string
|
|
|
|
? "string"
|
|
|
|
: T extends string[]
|
|
|
|
? "string[]"
|
|
|
|
: "unknown"
|
|
|
|
|
|
|
|
type Options<T> = {
|
|
|
|
[P in keyof T]: Option<OptionType<T[P]>>
|
2020-02-06 19:29:19 +01:00
|
|
|
}
|
|
|
|
|
2020-02-07 01:26:07 +01:00
|
|
|
const options: Options<Required<Args>> = {
|
|
|
|
auth: { type: AuthType, description: "The type of authentication to use." },
|
|
|
|
cert: {
|
|
|
|
type: OptionalString,
|
|
|
|
path: true,
|
|
|
|
description: "Path to certificate. Generated if no path is provided.",
|
|
|
|
},
|
|
|
|
"cert-key": { type: "string", path: true, description: "Path to certificate key when using non-generated cert." },
|
2020-02-14 22:57:51 +01:00
|
|
|
"disable-updates": { type: "boolean", description: "Disable automatic updates." },
|
2020-02-18 19:24:12 +01:00
|
|
|
"disable-telemetry": { type: "boolean", description: "Disable telemetry." },
|
2020-02-07 01:26:07 +01:00
|
|
|
help: { type: "boolean", short: "h", description: "Show this output." },
|
|
|
|
json: { type: "boolean" },
|
2020-02-19 18:06:32 +01:00
|
|
|
open: { type: "boolean", description: "Open in browser on startup. Does not work remotely." },
|
2020-04-27 15:22:52 +02:00
|
|
|
|
|
|
|
"bind-addr": { type: "string", description: "Address to bind to in host:port." },
|
|
|
|
|
2020-05-10 07:19:32 +02:00
|
|
|
config: { type: "string", description: "Path to yaml config file." },
|
|
|
|
|
2020-04-27 15:22:52 +02:00
|
|
|
// These two have been deprecated by bindAddr.
|
|
|
|
host: { type: "string", description: "" },
|
|
|
|
port: { type: "number", description: "" },
|
|
|
|
|
2020-04-29 00:29:25 +02:00
|
|
|
socket: { type: "string", path: true, description: "Path to a socket (bind-addr will be ignored)." },
|
2020-02-07 01:26:07 +01:00
|
|
|
version: { type: "boolean", short: "v", description: "Display version information." },
|
|
|
|
_: { type: "string[]" },
|
|
|
|
|
|
|
|
"user-data-dir": { type: "string", path: true, description: "Path to the user data directory." },
|
|
|
|
"extensions-dir": { type: "string", path: true, description: "Path to the extensions directory." },
|
|
|
|
"builtin-extensions-dir": { type: "string", path: true },
|
|
|
|
"extra-extensions-dir": { type: "string[]", path: true },
|
|
|
|
"extra-builtin-extensions-dir": { type: "string[]", path: true },
|
2020-03-13 19:21:46 +01:00
|
|
|
"list-extensions": { type: "boolean", description: "List installed VS Code extensions." },
|
|
|
|
force: { type: "boolean", description: "Avoid prompts when installing VS Code extensions." },
|
|
|
|
"install-extension": { type: "string[]", description: "Install or update a VS Code extension by id or vsix." },
|
|
|
|
"uninstall-extension": { type: "string[]", description: "Uninstall a VS Code extension by id." },
|
|
|
|
"show-versions": { type: "boolean", description: "Show VS Code extension versions." },
|
2020-03-23 18:08:50 +01:00
|
|
|
"proxy-domain": { type: "string[]", description: "Domain used for proxying ports." },
|
2020-02-07 01:26:07 +01:00
|
|
|
|
2020-02-20 19:38:46 +01:00
|
|
|
locale: { type: "string" },
|
2020-02-19 18:06:32 +01:00
|
|
|
log: { type: LogLevel },
|
2020-02-07 01:26:07 +01:00
|
|
|
verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." },
|
|
|
|
}
|
|
|
|
|
|
|
|
export const optionDescriptions = (): string[] => {
|
|
|
|
const entries = Object.entries(options).filter(([, v]) => !!v.description)
|
|
|
|
const widths = entries.reduce(
|
|
|
|
(prev, [k, v]) => ({
|
|
|
|
long: k.length > prev.long ? k.length : prev.long,
|
|
|
|
short: v.short && v.short.length > prev.short ? v.short.length : prev.short,
|
|
|
|
}),
|
2020-02-15 01:46:00 +01:00
|
|
|
{ short: 0, long: 0 },
|
2020-02-07 01:26:07 +01:00
|
|
|
)
|
|
|
|
return entries.map(
|
|
|
|
([k, v]) =>
|
|
|
|
`${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${v.short ? `-${v.short}` : " "} --${k}${" ".repeat(
|
2020-02-15 01:46:00 +01:00
|
|
|
widths.long - k.length,
|
|
|
|
)} ${v.description}${typeof v.type === "object" ? ` [${Object.values(v.type).join(", ")}]` : ""}`,
|
2020-02-07 01:26:07 +01:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
export const parse = (argv: string[]): Args => {
|
|
|
|
const args: Args = { _: [] }
|
|
|
|
let ended = false
|
|
|
|
|
|
|
|
for (let i = 0; i < argv.length; ++i) {
|
|
|
|
const arg = argv[i]
|
|
|
|
|
|
|
|
// -- signals the end of option parsing.
|
|
|
|
if (!ended && arg == "--") {
|
|
|
|
ended = true
|
|
|
|
continue
|
|
|
|
}
|
2020-02-06 20:11:38 +01:00
|
|
|
|
2020-02-07 01:26:07 +01:00
|
|
|
// Options start with a dash and require a value if non-boolean.
|
|
|
|
if (!ended && arg.startsWith("-")) {
|
|
|
|
let key: keyof Args | undefined
|
2020-02-19 17:54:23 +01:00
|
|
|
let value: string | undefined
|
2020-02-07 01:26:07 +01:00
|
|
|
if (arg.startsWith("--")) {
|
2020-02-19 17:54:23 +01:00
|
|
|
const split = arg.replace(/^--/, "").split("=", 2)
|
|
|
|
key = split[0] as keyof Args
|
|
|
|
value = split[1]
|
2020-02-07 01:26:07 +01:00
|
|
|
} else {
|
|
|
|
const short = arg.replace(/^-/, "")
|
|
|
|
const pair = Object.entries(options).find(([, v]) => v.short === short)
|
|
|
|
if (pair) {
|
|
|
|
key = pair[0] as keyof Args
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!key || !options[key]) {
|
|
|
|
throw new Error(`Unknown option ${arg}`)
|
|
|
|
}
|
|
|
|
|
|
|
|
const option = options[key]
|
|
|
|
if (option.type === "boolean") {
|
|
|
|
;(args[key] as boolean) = true
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2020-02-19 17:54:23 +01:00
|
|
|
// Might already have a value if it was the --long=value format.
|
|
|
|
if (typeof value === "undefined") {
|
|
|
|
// A value is only valid if it doesn't look like an option.
|
|
|
|
value = argv[i + 1] && !argv[i + 1].startsWith("-") ? argv[++i] : undefined
|
|
|
|
}
|
|
|
|
|
2020-02-07 01:26:07 +01:00
|
|
|
if (!value && option.type === OptionalString) {
|
|
|
|
;(args[key] as OptionalString) = new OptionalString(value)
|
|
|
|
continue
|
|
|
|
} else if (!value) {
|
2020-02-19 17:54:23 +01:00
|
|
|
throw new Error(`--${key} requires a value`)
|
2020-02-07 01:26:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (option.path) {
|
|
|
|
value = path.resolve(value)
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (option.type) {
|
|
|
|
case "string":
|
|
|
|
;(args[key] as string) = value
|
|
|
|
break
|
|
|
|
case "string[]":
|
|
|
|
if (!args[key]) {
|
|
|
|
;(args[key] as string[]) = []
|
|
|
|
}
|
|
|
|
;(args[key] as string[]).push(value)
|
|
|
|
break
|
|
|
|
case "number":
|
|
|
|
;(args[key] as number) = parseInt(value, 10)
|
|
|
|
if (isNaN(args[key] as number)) {
|
2020-02-19 17:54:23 +01:00
|
|
|
throw new Error(`--${key} must be a number`)
|
2020-02-07 01:26:07 +01:00
|
|
|
}
|
|
|
|
break
|
|
|
|
case OptionalString:
|
|
|
|
;(args[key] as OptionalString) = new OptionalString(value)
|
|
|
|
break
|
|
|
|
default: {
|
2020-04-28 23:39:01 +02:00
|
|
|
if (!Object.values(option.type).includes(value)) {
|
2020-02-19 17:54:23 +01:00
|
|
|
throw new Error(`--${key} valid values: [${Object.values(option.type).join(", ")}]`)
|
2020-02-07 01:26:07 +01:00
|
|
|
}
|
|
|
|
;(args[key] as string) = value
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Everything else goes into _.
|
|
|
|
args._.push(arg)
|
2020-02-06 20:11:38 +01:00
|
|
|
}
|
|
|
|
|
2020-02-07 01:26:07 +01:00
|
|
|
logger.debug("parsed command line", field("args", args))
|
|
|
|
|
2020-04-28 23:39:01 +02:00
|
|
|
// --verbose takes priority over --log and --log takes priority over the
|
|
|
|
// environment variable.
|
|
|
|
if (args.verbose) {
|
|
|
|
args.log = LogLevel.Trace
|
|
|
|
} else if (
|
|
|
|
!args.log &&
|
|
|
|
process.env.LOG_LEVEL &&
|
|
|
|
Object.values(LogLevel).includes(process.env.LOG_LEVEL as LogLevel)
|
|
|
|
) {
|
2020-02-19 18:06:32 +01:00
|
|
|
args.log = process.env.LOG_LEVEL as LogLevel
|
2020-02-06 19:29:19 +01:00
|
|
|
}
|
2020-02-07 01:26:07 +01:00
|
|
|
|
2020-04-28 23:39:01 +02:00
|
|
|
// Sync --log, --verbose, the environment variable, and logger level.
|
|
|
|
if (args.log) {
|
|
|
|
process.env.LOG_LEVEL = args.log
|
|
|
|
}
|
2020-02-07 01:26:07 +01:00
|
|
|
switch (args.log) {
|
2020-02-19 18:06:32 +01:00
|
|
|
case LogLevel.Trace:
|
2020-02-07 01:26:07 +01:00
|
|
|
logger.level = Level.Trace
|
2020-04-28 23:39:01 +02:00
|
|
|
args.verbose = true
|
2020-02-07 01:26:07 +01:00
|
|
|
break
|
2020-02-19 18:06:32 +01:00
|
|
|
case LogLevel.Debug:
|
2020-02-07 01:26:07 +01:00
|
|
|
logger.level = Level.Debug
|
|
|
|
break
|
2020-02-19 18:06:32 +01:00
|
|
|
case LogLevel.Info:
|
2020-02-07 01:26:07 +01:00
|
|
|
logger.level = Level.Info
|
|
|
|
break
|
2020-02-19 18:06:32 +01:00
|
|
|
case LogLevel.Warn:
|
2020-02-07 01:26:07 +01:00
|
|
|
logger.level = Level.Warning
|
|
|
|
break
|
2020-02-19 18:06:32 +01:00
|
|
|
case LogLevel.Error:
|
2020-02-07 01:26:07 +01:00
|
|
|
logger.level = Level.Error
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!args["user-data-dir"]) {
|
|
|
|
args["user-data-dir"] = xdgLocalDir
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!args["extensions-dir"]) {
|
|
|
|
args["extensions-dir"] = path.join(args["user-data-dir"], "extensions")
|
|
|
|
}
|
|
|
|
|
|
|
|
return args
|
2020-02-06 19:29:19 +01:00
|
|
|
}
|
2020-05-10 07:19:32 +02:00
|
|
|
|
|
|
|
// readConfigFile reads the config file specified in the config flag
|
|
|
|
// and loads it's configuration.
|
|
|
|
//
|
|
|
|
// Flags set on the CLI take priority.
|
|
|
|
//
|
|
|
|
// The config file can also be passed via $CODE_SERVER_CONFIG and defaults
|
|
|
|
// to ~/.config/code-server/config.yaml.
|
|
|
|
export async function readConfigFile(args: Args): Promise<Args> {
|
|
|
|
const configPath = getConfigPath(args)
|
|
|
|
if (configPath === undefined) {
|
|
|
|
return args
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!(await fs.pathExists(configPath))) {
|
|
|
|
await fs.outputFile(configPath, `default: hello`)
|
|
|
|
}
|
|
|
|
|
|
|
|
const configFile = await fs.readFile(configPath)
|
|
|
|
const config = yaml.safeLoad(configFile.toString(), {
|
|
|
|
filename: args.config,
|
|
|
|
})
|
|
|
|
|
|
|
|
// We convert the config file into a set of flags.
|
|
|
|
// This is a temporary measure until we add a proper CLI library.
|
|
|
|
const configFileArgv = Object.entries(config).map(([optName, opt]) => `--${optName}=${opt}`)
|
|
|
|
const configFileArgs = parse(configFileArgv)
|
|
|
|
|
|
|
|
// This prioritizes the flags set in args over the ones in the config file.
|
|
|
|
return Object.assign(configFileArgs, args)
|
|
|
|
}
|
|
|
|
|
|
|
|
function getConfigPath(args: Args): string | undefined {
|
|
|
|
if (args.config !== undefined) {
|
|
|
|
return args.config
|
|
|
|
}
|
|
|
|
if (process.env.CODE_SERVER_CONFIG !== undefined) {
|
|
|
|
return process.env.CODE_SERVER_CONFIG
|
|
|
|
}
|
|
|
|
if (xdgBasedir.config !== undefined) {
|
|
|
|
return `${xdgBasedir.config}/code-server/config.yaml`
|
|
|
|
}
|
|
|
|
return undefined
|
|
|
|
}
|