Skip to content

Commit 2564190

Browse files
committed
Implement cli parser
1 parent 26f8216 commit 2564190

File tree

5 files changed

+355
-43
lines changed

5 files changed

+355
-43
lines changed

scripts/build.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ class Builder {
330330
if (server) {
331331
server.kill()
332332
}
333-
const s = cp.fork(path.join(this.rootPath, "out/node/entry.js"), process.argv.slice(2))
333+
const s = cp.fork(path.join(this.rootPath, "out/node/entry.js"), process.argv.slice(3))
334334
console.log(`[server] spawned process ${s.pid}`)
335335
s.on("exit", () => console.log(`[server] process ${s.pid} exited`))
336336
server = s

scripts/vscode.patch

+2-3
Original file line numberDiff line numberDiff line change
@@ -934,10 +934,10 @@ index 0000000000..56331ff1fc
934934
+require('../../bootstrap-amd').load('vs/server/entry');
935935
diff --git a/src/vs/server/ipc.d.ts b/src/vs/server/ipc.d.ts
936936
new file mode 100644
937-
index 0000000000..f3e358096f
937+
index 0000000000..a1047fff86
938938
--- /dev/null
939939
+++ b/src/vs/server/ipc.d.ts
940-
@@ -0,0 +1,102 @@
940+
@@ -0,0 +1,101 @@
941941
+/**
942942
+ * External interfaces for integration into code-server over IPC. No vs imports
943943
+ * should be made in this file.
@@ -984,7 +984,6 @@ index 0000000000..f3e358096f
984984
+ 'extra-builtin-extensions-dir'?: string[];
985985
+
986986
+ log?: string;
987-
+ trace?: boolean;
988987
+ verbose?: boolean;
989988
+
990989
+ _: string[];

src/node/cli.ts

+210-31
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,223 @@
11
import * as path from "path"
2-
import { logger, Level } from "@coder/logger"
2+
import { field, logger, Level } from "@coder/logger"
33
import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc"
44
import { AuthType } from "./http"
55
import { xdgLocalDir } from "./util"
66

7+
export class Optional<T> {
8+
public constructor(public readonly value?: T) {}
9+
}
10+
11+
export class OptionalString extends Optional<string> {}
12+
713
export interface Args extends VsArgs {
8-
auth?: AuthType
9-
"base-path"?: string
10-
cert?: string
11-
"cert-key"?: string
12-
format?: string
13-
host?: string
14-
json?: boolean
15-
open?: boolean
16-
port?: string
17-
socket?: string
18-
version?: boolean
19-
_: string[]
14+
readonly auth?: AuthType
15+
readonly cert?: OptionalString
16+
readonly "cert-key"?: string
17+
readonly help?: boolean
18+
readonly host?: string
19+
readonly json?: boolean
20+
readonly open?: boolean
21+
readonly port?: number
22+
readonly socket?: string
23+
readonly version?: boolean
24+
readonly _: string[]
25+
}
26+
27+
interface Option<T> {
28+
type: T
29+
/**
30+
* Short flag for the option.
31+
*/
32+
short?: string
33+
/**
34+
* Whether the option is a path and should be resolved.
35+
*/
36+
path?: boolean
37+
/**
38+
* Description of the option. Leave blank to hide the option.
39+
*/
40+
description?: string
41+
}
42+
43+
type OptionType<T> = T extends boolean
44+
? "boolean"
45+
: T extends OptionalString
46+
? typeof OptionalString
47+
: T extends AuthType
48+
? typeof AuthType
49+
: T extends number
50+
? "number"
51+
: T extends string
52+
? "string"
53+
: T extends string[]
54+
? "string[]"
55+
: "unknown"
56+
57+
type Options<T> = {
58+
[P in keyof T]: Option<OptionType<T[P]>>
2059
}
2160

22-
// TODO: Implement proper CLI parser.
23-
export const parse = (): Args => {
24-
const last = process.argv[process.argv.length - 1]
25-
const userDataDir = xdgLocalDir
26-
const verbose = process.argv.includes("--verbose")
27-
const trace = process.argv.includes("--trace")
61+
const options: Options<Required<Args>> = {
62+
auth: { type: AuthType, description: "The type of authentication to use." },
63+
cert: {
64+
type: OptionalString,
65+
path: true,
66+
description: "Path to certificate. Generated if no path is provided.",
67+
},
68+
"cert-key": { type: "string", path: true, description: "Path to certificate key when using non-generated cert." },
69+
host: { type: "string", description: "Host for the HTTP server." },
70+
help: { type: "boolean", short: "h", description: "Show this output." },
71+
json: { type: "boolean" },
72+
open: { type: "boolean", description: "Open in the browser on startup. Does not work remotely." },
73+
port: { type: "number", description: "Port for the HTTP server." },
74+
socket: { type: "string", path: true, description: "Path to a socket (host and port will be ignored)." },
75+
version: { type: "boolean", short: "v", description: "Display version information." },
76+
_: { type: "string[]" },
77+
78+
"user-data-dir": { type: "string", path: true, description: "Path to the user data directory." },
79+
"extensions-dir": { type: "string", path: true, description: "Path to the extensions directory." },
80+
"builtin-extensions-dir": { type: "string", path: true },
81+
"extra-extensions-dir": { type: "string[]", path: true },
82+
"extra-builtin-extensions-dir": { type: "string[]", path: true },
83+
84+
log: { type: "string" },
85+
verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." },
86+
}
87+
88+
export const optionDescriptions = (): string[] => {
89+
const entries = Object.entries(options).filter(([, v]) => !!v.description)
90+
const widths = entries.reduce(
91+
(prev, [k, v]) => ({
92+
long: k.length > prev.long ? k.length : prev.long,
93+
short: v.short && v.short.length > prev.short ? v.short.length : prev.short,
94+
}),
95+
{ short: 0, long: 0 }
96+
)
97+
return entries.map(
98+
([k, v]) =>
99+
`${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${v.short ? `-${v.short}` : " "} --${k}${" ".repeat(
100+
widths.long - k.length
101+
)} ${v.description}${typeof v.type === "object" ? ` [${Object.values(v.type).join(", ")}]` : ""}`
102+
)
103+
}
104+
105+
export const parse = (argv: string[]): Args => {
106+
const args: Args = { _: [] }
107+
let ended = false
108+
109+
for (let i = 0; i < argv.length; ++i) {
110+
const arg = argv[i]
111+
112+
// -- signals the end of option parsing.
113+
if (!ended && arg == "--") {
114+
ended = true
115+
continue
116+
}
28117

29-
if (verbose || trace) {
30-
process.env.LOG_LEVEL = "trace"
31-
logger.level = Level.Trace
118+
// Options start with a dash and require a value if non-boolean.
119+
if (!ended && arg.startsWith("-")) {
120+
let key: keyof Args | undefined
121+
if (arg.startsWith("--")) {
122+
key = arg.replace(/^--/, "") as keyof Args
123+
} else {
124+
const short = arg.replace(/^-/, "")
125+
const pair = Object.entries(options).find(([, v]) => v.short === short)
126+
if (pair) {
127+
key = pair[0] as keyof Args
128+
}
129+
}
130+
131+
if (!key || !options[key]) {
132+
throw new Error(`Unknown option ${arg}`)
133+
}
134+
135+
const option = options[key]
136+
if (option.type === "boolean") {
137+
;(args[key] as boolean) = true
138+
continue
139+
}
140+
141+
// A value is only valid if it doesn't look like an option.
142+
let value = argv[i + 1] && !argv[i + 1].startsWith("-") ? argv[++i] : undefined
143+
if (!value && option.type === OptionalString) {
144+
;(args[key] as OptionalString) = new OptionalString(value)
145+
continue
146+
} else if (!value) {
147+
throw new Error(`${arg} requires a value`)
148+
}
149+
150+
if (option.path) {
151+
value = path.resolve(value)
152+
}
153+
154+
switch (option.type) {
155+
case "string":
156+
;(args[key] as string) = value
157+
break
158+
case "string[]":
159+
if (!args[key]) {
160+
;(args[key] as string[]) = []
161+
}
162+
;(args[key] as string[]).push(value)
163+
break
164+
case "number":
165+
;(args[key] as number) = parseInt(value, 10)
166+
if (isNaN(args[key] as number)) {
167+
throw new Error(`${arg} must be a number`)
168+
}
169+
break
170+
case OptionalString:
171+
;(args[key] as OptionalString) = new OptionalString(value)
172+
break
173+
default: {
174+
if (!Object.values(option.type).find((v) => v === value)) {
175+
throw new Error(`${arg} valid values: [${Object.values(option.type).join(", ")}]`)
176+
}
177+
;(args[key] as string) = value
178+
break
179+
}
180+
}
181+
182+
continue
183+
}
184+
185+
// Everything else goes into _.
186+
args._.push(arg)
32187
}
33188

34-
return {
35-
"extensions-dir": path.join(userDataDir, "extensions"),
36-
"user-data-dir": userDataDir,
37-
_: last && !last.startsWith("-") ? [last] : [],
38-
json: process.argv.includes("--json"),
39-
log: process.env.LOG_LEVEL,
40-
trace,
41-
verbose,
42-
version: process.argv.includes("--version"),
189+
logger.debug("parsed command line", field("args", args))
190+
191+
if (process.env.LOG_LEVEL === "trace" || args.verbose) {
192+
args.verbose = true
193+
args.log = "trace"
43194
}
195+
196+
switch (args.log) {
197+
case "trace":
198+
logger.level = Level.Trace
199+
break
200+
case "debug":
201+
logger.level = Level.Debug
202+
break
203+
case "info":
204+
logger.level = Level.Info
205+
break
206+
case "warning":
207+
logger.level = Level.Warning
208+
break
209+
case "error":
210+
logger.level = Level.Error
211+
break
212+
}
213+
214+
if (!args["user-data-dir"]) {
215+
args["user-data-dir"] = xdgLocalDir
216+
}
217+
218+
if (!args["extensions-dir"]) {
219+
args["extensions-dir"] = path.join(args["user-data-dir"], "extensions")
220+
}
221+
222+
return args
44223
}

src/node/entry.ts

+16-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { logger } from "@coder/logger"
22
import { ApiHttpProvider } from "./api/server"
33
import { MainHttpProvider } from "./app/server"
4-
import { Args, parse } from "./cli"
4+
import { Args, optionDescriptions, parse } from "./cli"
55
import { AuthType, HttpServer } from "./http"
66
import { generateCertificate, generatePassword, hash, open } from "./util"
77
import { VscodeHttpProvider } from "./vscode/server"
@@ -21,16 +21,15 @@ const main = async (args: Args): Promise<void> => {
2121
// Spawn the main HTTP server.
2222
const options = {
2323
auth,
24-
basePath: args["base-path"],
25-
cert: args.cert,
24+
cert: args.cert ? args.cert.value : undefined,
2625
certKey: args["cert-key"],
2726
commit: commit || "development",
2827
host: args.host || (args.auth === AuthType.Password && typeof args.cert !== "undefined" ? "0.0.0.0" : "localhost"),
2928
password: originalPassword ? hash(originalPassword) : undefined,
30-
port: typeof args.port !== "undefined" ? parseInt(args.port, 10) : 8080,
29+
port: typeof args.port !== "undefined" ? args.port : 8080,
3130
socket: args.socket,
3231
}
33-
if (!options.cert && typeof options.cert !== "undefined") {
32+
if (!options.cert && args.cert) {
3433
const { cert, certKey } = await generateCertificate()
3534
options.cert = cert
3635
options.certKey = certKey
@@ -60,7 +59,7 @@ const main = async (args: Args): Promise<void> => {
6059

6160
if (httpServer.protocol === "https") {
6261
logger.info(
63-
args.cert
62+
typeof args.cert === "string"
6463
? ` - Using provided certificate${args["cert-key"] ? " and key" : ""} for HTTPS`
6564
: ` - Using generated certificate and key for HTTPS`
6665
)
@@ -76,8 +75,17 @@ const main = async (args: Args): Promise<void> => {
7675
}
7776
}
7877

79-
const args = parse()
80-
if (args.version) {
78+
const args = parse(process.argv.slice(2))
79+
if (args.help) {
80+
console.log("code-server", require("../../package.json").version)
81+
console.log("")
82+
console.log(`Usage: code-server [options] [path]`)
83+
console.log("")
84+
console.log("Options")
85+
optionDescriptions().forEach((description) => {
86+
console.log("", description)
87+
})
88+
} else if (args.version) {
8189
const version = require("../../package.json").version
8290
if (args.json) {
8391
console.log({

0 commit comments

Comments
 (0)