cli.ts 7.6 KB
Newer Older
1
import * as path from "path"
A
Asher 已提交
2
import { field, logger, Level } from "@coder/logger"
3
import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc"
4 5
import { AuthType } from "./http"
import { xdgLocalDir } from "./util"
6

A
Asher 已提交
7 8 9 10
export class Optional<T> {
  public constructor(public readonly value?: T) {}
}

A
Asher 已提交
11 12 13 14 15 16 17 18
export enum LogLevel {
  Trace = "trace",
  Debug = "debug",
  Info = "info",
  Warn = "warn",
  Error = "error",
}

A
Asher 已提交
19 20
export class OptionalString extends Optional<string> {}

21
export interface Args extends VsArgs {
A
Asher 已提交
22 23 24
  readonly auth?: AuthType
  readonly cert?: OptionalString
  readonly "cert-key"?: string
A
Asher 已提交
25
  readonly "disable-updates"?: boolean
A
Asher 已提交
26
  readonly "disable-telemetry"?: boolean
A
Asher 已提交
27 28 29
  readonly help?: boolean
  readonly host?: string
  readonly json?: boolean
A
Asher 已提交
30
  log?: LogLevel
A
Asher 已提交
31 32 33 34
  readonly open?: boolean
  readonly port?: number
  readonly socket?: string
  readonly version?: boolean
A
Asher 已提交
35 36 37
  readonly "list-extensions"?: boolean
  readonly "install-extension"?: string[]
  readonly "uninstall-extension"?: string[]
A
Asher 已提交
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
  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
A
Asher 已提交
61 62
  : T extends LogLevel
  ? typeof LogLevel
A
Asher 已提交
63 64 65 66 67 68 69 70 71 72 73 74
  : 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]>>
75 76
}

A
Asher 已提交
77 78 79 80 81 82 83 84
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." },
A
Asher 已提交
85
  "disable-updates": { type: "boolean", description: "Disable automatic updates." },
A
Asher 已提交
86
  "disable-telemetry": { type: "boolean", description: "Disable telemetry." },
A
Asher 已提交
87 88 89
  host: { type: "string", description: "Host for the HTTP server." },
  help: { type: "boolean", short: "h", description: "Show this output." },
  json: { type: "boolean" },
A
Asher 已提交
90
  open: { type: "boolean", description: "Open in browser on startup. Does not work remotely." },
A
Asher 已提交
91 92 93 94 95 96 97 98 99 100
  port: { type: "number", description: "Port for the HTTP server." },
  socket: { type: "string", path: true, description: "Path to a socket (host and port will be ignored)." },
  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 },
A
Asher 已提交
101 102 103
  "list-extensions": { type: "boolean" },
  "install-extension": { type: "string[]" },
  "uninstall-extension": { type: "string[]" },
A
Asher 已提交
104

A
Asher 已提交
105
  log: { type: LogLevel },
A
Asher 已提交
106 107 108 109 110 111 112 113 114 115
  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,
    }),
A
Anmol Sethi 已提交
116
    { short: 0, long: 0 },
A
Asher 已提交
117 118 119 120
  )
  return entries.map(
    ([k, v]) =>
      `${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${v.short ? `-${v.short}` : " "} --${k}${" ".repeat(
A
Anmol Sethi 已提交
121 122
        widths.long - k.length,
      )} ${v.description}${typeof v.type === "object" ? ` [${Object.values(v.type).join(", ")}]` : ""}`,
A
Asher 已提交
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
  )
}

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
    }
138

A
Asher 已提交
139 140 141
    // Options start with a dash and require a value if non-boolean.
    if (!ended && arg.startsWith("-")) {
      let key: keyof Args | undefined
142
      let value: string | undefined
A
Asher 已提交
143
      if (arg.startsWith("--")) {
144 145 146
        const split = arg.replace(/^--/, "").split("=", 2)
        key = split[0] as keyof Args
        value = split[1]
A
Asher 已提交
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
      } 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
      }

165 166 167 168 169 170
      // 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
      }

A
Asher 已提交
171 172 173 174
      if (!value && option.type === OptionalString) {
        ;(args[key] as OptionalString) = new OptionalString(value)
        continue
      } else if (!value) {
175
        throw new Error(`--${key} requires a value`)
A
Asher 已提交
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
      }

      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)) {
195
            throw new Error(`--${key} must be a number`)
A
Asher 已提交
196 197 198 199 200 201 202
          }
          break
        case OptionalString:
          ;(args[key] as OptionalString) = new OptionalString(value)
          break
        default: {
          if (!Object.values(option.type).find((v) => v === value)) {
203
            throw new Error(`--${key} valid values: [${Object.values(option.type).join(", ")}]`)
A
Asher 已提交
204 205 206 207 208 209 210 211 212 213 214
          }
          ;(args[key] as string) = value
          break
        }
      }

      continue
    }

    // Everything else goes into _.
    args._.push(arg)
215 216
  }

A
Asher 已提交
217 218
  logger.debug("parsed command line", field("args", args))

A
Asher 已提交
219 220
  // Ensure the environment variable and the flag are synced up. The flag takes
  // priority over the environment variable.
A
Asher 已提交
221 222
  if (args.log === LogLevel.Trace || process.env.LOG_LEVEL === LogLevel.Trace || args.verbose) {
    args.log = process.env.LOG_LEVEL = LogLevel.Trace
A
Asher 已提交
223
    args.verbose = true
A
Asher 已提交
224
  } else if (!args.log && process.env.LOG_LEVEL) {
A
Asher 已提交
225
    args.log = process.env.LOG_LEVEL as LogLevel
A
Asher 已提交
226 227
  } else if (args.log) {
    process.env.LOG_LEVEL = args.log
228
  }
A
Asher 已提交
229 230

  switch (args.log) {
A
Asher 已提交
231
    case LogLevel.Trace:
A
Asher 已提交
232 233
      logger.level = Level.Trace
      break
A
Asher 已提交
234
    case LogLevel.Debug:
A
Asher 已提交
235 236
      logger.level = Level.Debug
      break
A
Asher 已提交
237
    case LogLevel.Info:
A
Asher 已提交
238 239
      logger.level = Level.Info
      break
A
Asher 已提交
240
    case LogLevel.Warn:
A
Asher 已提交
241 242
      logger.level = Level.Warning
      break
A
Asher 已提交
243
    case LogLevel.Error:
A
Asher 已提交
244 245 246 247 248 249 250 251 252 253 254 255 256
      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
257
}