Merge pull request #1619 from cdr/config
Add support for a YAML config file
This commit is contained in:
commit
8626bed4ef
|
@ -22,3 +22,10 @@ rules:
|
||||||
# For overloads.
|
# For overloads.
|
||||||
no-dupe-class-members: off
|
no-dupe-class-members: off
|
||||||
"@typescript-eslint/no-use-before-define": off
|
"@typescript-eslint/no-use-before-define": off
|
||||||
|
"@typescript-eslint/no-non-null-assertion": off
|
||||||
|
|
||||||
|
settings:
|
||||||
|
# Does not work with CommonJS unfortunately.
|
||||||
|
import/ignore:
|
||||||
|
- env-paths
|
||||||
|
- xdg-basedir
|
||||||
|
|
|
@ -78,6 +78,8 @@ You can disable minification by setting `MINIFY=`.
|
||||||
- Used to configure [nfpm](https://github.com/goreleaser/nfpm) to generate .deb and .rpm
|
- Used to configure [nfpm](https://github.com/goreleaser/nfpm) to generate .deb and .rpm
|
||||||
- [./build/code-server-nfpm.sh](./build/code-server-nfpm.sh)
|
- [./build/code-server-nfpm.sh](./build/code-server-nfpm.sh)
|
||||||
- Entrypoint script for code-server for .deb and .rpm
|
- Entrypoint script for code-server for .deb and .rpm
|
||||||
|
- [./build/code-server.service](./build/code-server.service)
|
||||||
|
- systemd user service packaged into the debs and rpms
|
||||||
- [./build/release-github-draft.sh](./build/release-github-draft.sh) (`yarn release:github-draft`)
|
- [./build/release-github-draft.sh](./build/release-github-draft.sh) (`yarn release:github-draft`)
|
||||||
- Uses [hub](https://github.com/github/hub) to create a draft release with a template description
|
- Uses [hub](https://github.com/github/hub) to create a draft release with a template description
|
||||||
- [./build/release-github-assets.sh](./build/release-github-assets.sh) (`yarn release:github-assets`)
|
- [./build/release-github-assets.sh](./build/release-github-assets.sh) (`yarn release:github-assets`)
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
[Unit]
|
||||||
|
Description=code-server
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=exec
|
||||||
|
ExecStart=/usr/bin/code-server
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
|
@ -13,4 +13,5 @@ license: "MIT"
|
||||||
bindir: "/usr/bin"
|
bindir: "/usr/bin"
|
||||||
files:
|
files:
|
||||||
./ci/build/code-server-nfpm.sh: /usr/bin/code-server
|
./ci/build/code-server-nfpm.sh: /usr/bin/code-server
|
||||||
|
./ci/build/code-server.service: /usr/lib/systemd/user/code-server.service
|
||||||
./release-static/**/*: "/usr/lib/code-server/"
|
./release-static/**/*: "/usr/lib/code-server/"
|
||||||
|
|
|
@ -11,16 +11,88 @@ index e73dd4d9e8..e3192b3a0d 100644
|
||||||
build/node_modules
|
build/node_modules
|
||||||
coverage/
|
coverage/
|
||||||
diff --git a/.yarnrc b/.yarnrc
|
diff --git a/.yarnrc b/.yarnrc
|
||||||
index d86b284e83..a7300dbfb9 100644
|
deleted file mode 100644
|
||||||
|
index d86b284e83..0000000000
|
||||||
--- a/.yarnrc
|
--- a/.yarnrc
|
||||||
+++ b/.yarnrc
|
+++ /dev/null
|
||||||
@@ -1,3 +1,3 @@
|
@@ -1,3 +0,0 @@
|
||||||
-disturl "https://atom.io/download/electron"
|
-disturl "https://atom.io/download/electron"
|
||||||
-target "7.2.4"
|
-target "7.2.4"
|
||||||
-runtime "electron"
|
-runtime "electron"
|
||||||
+disturl "http://nodejs.org/dist"
|
diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js
|
||||||
+target "12.16.3"
|
index f2ea1bd370..2dc6da34fb 100644
|
||||||
+runtime "node"
|
--- a/build/gulpfile.reh.js
|
||||||
|
+++ b/build/gulpfile.reh.js
|
||||||
|
@@ -52,6 +52,7 @@ gulp.task('vscode-reh-web-linux-x64-min', noop);
|
||||||
|
gulp.task('vscode-reh-web-linux-alpine-min', noop);
|
||||||
|
|
||||||
|
function getNodeVersion() {
|
||||||
|
+ return process.versions.node
|
||||||
|
const yarnrc = fs.readFileSync(path.join(REPO_ROOT, 'remote', '.yarnrc'), 'utf8');
|
||||||
|
const target = /^target "(.*)"$/m.exec(yarnrc)[1];
|
||||||
|
return target;
|
||||||
|
diff --git a/build/lib/electron.js b/build/lib/electron.js
|
||||||
|
index abf6baab41..8a1d1bad13 100644
|
||||||
|
--- a/build/lib/electron.js
|
||||||
|
+++ b/build/lib/electron.js
|
||||||
|
@@ -17,6 +17,7 @@ const root = path.dirname(path.dirname(__dirname));
|
||||||
|
const product = JSON.parse(fs.readFileSync(path.join(root, 'product.json'), 'utf8'));
|
||||||
|
const commit = util.getVersion(root);
|
||||||
|
function getElectronVersion() {
|
||||||
|
+ return process.versions.node;
|
||||||
|
const yarnrc = fs.readFileSync(path.join(root, '.yarnrc'), 'utf8');
|
||||||
|
const target = /^target "(.*)"$/m.exec(yarnrc)[1];
|
||||||
|
return target;
|
||||||
|
diff --git a/build/lib/electron.ts b/build/lib/electron.ts
|
||||||
|
index 86c7afcf31..0663b4c28d 100644
|
||||||
|
--- a/build/lib/electron.ts
|
||||||
|
+++ b/build/lib/electron.ts
|
||||||
|
@@ -20,6 +20,7 @@ const product = JSON.parse(fs.readFileSync(path.join(root, 'product.json'), 'utf
|
||||||
|
const commit = util.getVersion(root);
|
||||||
|
|
||||||
|
export function getElectronVersion(): string {
|
||||||
|
+ return process.versions.node
|
||||||
|
const yarnrc = fs.readFileSync(path.join(root, '.yarnrc'), 'utf8');
|
||||||
|
const target = /^target "(.*)"$/m.exec(yarnrc)![1];
|
||||||
|
return target;
|
||||||
|
diff --git a/build/lib/node.js b/build/lib/node.js
|
||||||
|
index 403ae3d965..738ee8cee0 100644
|
||||||
|
--- a/build/lib/node.js
|
||||||
|
+++ b/build/lib/node.js
|
||||||
|
@@ -5,11 +5,8 @@
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const path = require("path");
|
||||||
|
-const fs = require("fs");
|
||||||
|
const root = path.dirname(path.dirname(__dirname));
|
||||||
|
-const yarnrcPath = path.join(root, 'remote', '.yarnrc');
|
||||||
|
-const yarnrc = fs.readFileSync(yarnrcPath, 'utf8');
|
||||||
|
-const version = /^target\s+"([^"]+)"$/m.exec(yarnrc)[1];
|
||||||
|
+const version = process.versions.node;
|
||||||
|
const node = process.platform === 'win32' ? 'node.exe' : 'node';
|
||||||
|
const nodePath = path.join(root, '.build', 'node', `v${version}`, `${process.platform}-${process.arch}`, node);
|
||||||
|
console.log(nodePath);
|
||||||
|
diff --git a/build/lib/node.ts b/build/lib/node.ts
|
||||||
|
index 6439703446..c53dccf4dc 100644
|
||||||
|
--- a/build/lib/node.ts
|
||||||
|
+++ b/build/lib/node.ts
|
||||||
|
@@ -4,13 +4,10 @@
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import * as path from 'path';
|
||||||
|
-import * as fs from 'fs';
|
||||||
|
|
||||||
|
const root = path.dirname(path.dirname(__dirname));
|
||||||
|
-const yarnrcPath = path.join(root, 'remote', '.yarnrc');
|
||||||
|
-const yarnrc = fs.readFileSync(yarnrcPath, 'utf8');
|
||||||
|
-const version = /^target\s+"([^"]+)"$/m.exec(yarnrc)![1];
|
||||||
|
+const version = process.versions.node;
|
||||||
|
const node = process.platform === 'win32' ? 'node.exe' : 'node';
|
||||||
|
const nodePath = path.join(root, '.build', 'node', `v${version}`, `${process.platform}-${process.arch}`, node);
|
||||||
|
|
||||||
|
-console.log(nodePath);
|
||||||
|
\ No newline at end of file
|
||||||
|
+console.log(nodePath);
|
||||||
diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js
|
diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js
|
||||||
index 7a2320d828..5768890636 100644
|
index 7a2320d828..5768890636 100644
|
||||||
--- a/build/npm/postinstall.js
|
--- a/build/npm/postinstall.js
|
||||||
|
@ -166,6 +238,15 @@ index 91b3b92678..f76f2c1548 100644
|
||||||
"urlProtocol": "code-oss",
|
"urlProtocol": "code-oss",
|
||||||
"extensionAllowedProposedApi": [
|
"extensionAllowedProposedApi": [
|
||||||
"ms-vscode.vscode-js-profile-table",
|
"ms-vscode.vscode-js-profile-table",
|
||||||
|
diff --git a/remote/.yarnrc b/remote/.yarnrc
|
||||||
|
deleted file mode 100644
|
||||||
|
index 1e16cde724..0000000000
|
||||||
|
--- a/remote/.yarnrc
|
||||||
|
+++ /dev/null
|
||||||
|
@@ -1,3 +0,0 @@
|
||||||
|
-disturl "http://nodejs.org/dist"
|
||||||
|
-target "12.4.0"
|
||||||
|
-runtime "node"
|
||||||
diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts
|
diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts
|
||||||
index e4546b2cf6..9df12239fb 100644
|
index e4546b2cf6..9df12239fb 100644
|
||||||
--- a/src/vs/base/common/network.ts
|
--- a/src/vs/base/common/network.ts
|
||||||
|
|
|
@ -10,6 +10,8 @@ main() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
download_artifact npm-package ./release
|
download_artifact npm-package ./release
|
||||||
|
# https://github.com/actions/upload-artifact/issues/38
|
||||||
|
chmod +x $(grep -rl '^#!/.\+' release)
|
||||||
yarn publish --non-interactive release
|
yarn publish --non-interactive release
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -168,10 +168,10 @@ code-server crashes can be helpful.
|
||||||
### Where is the data directory?
|
### Where is the data directory?
|
||||||
|
|
||||||
If the `XDG_DATA_HOME` environment variable is set the data directory will be
|
If the `XDG_DATA_HOME` environment variable is set the data directory will be
|
||||||
`$XDG_DATA_HOME/code-server`. Otherwise the default is:
|
`$XDG_DATA_HOME/code-server`. Otherwise:
|
||||||
|
|
||||||
1. Linux: `~/.local/share/code-server`.
|
1. Unix: `~/.local/share/code-server`
|
||||||
2. Mac: `~/Library/Application\ Support/code-server`.
|
1. Windows: `%APPDATA%\Local\code-server\Data`
|
||||||
|
|
||||||
## Enterprise
|
## Enterprise
|
||||||
|
|
||||||
|
|
10
package.json
10
package.json
|
@ -33,6 +33,7 @@
|
||||||
"@types/adm-zip": "^0.4.32",
|
"@types/adm-zip": "^0.4.32",
|
||||||
"@types/fs-extra": "^8.0.1",
|
"@types/fs-extra": "^8.0.1",
|
||||||
"@types/http-proxy": "^1.17.4",
|
"@types/http-proxy": "^1.17.4",
|
||||||
|
"@types/js-yaml": "^3.12.3",
|
||||||
"@types/mocha": "^5.2.7",
|
"@types/mocha": "^5.2.7",
|
||||||
"@types/node": "^12.12.7",
|
"@types/node": "^12.12.7",
|
||||||
"@types/parcel-bundler": "^1.12.1",
|
"@types/parcel-bundler": "^1.12.1",
|
||||||
|
@ -55,8 +56,7 @@
|
||||||
"stylelint": "^13.0.0",
|
"stylelint": "^13.0.0",
|
||||||
"stylelint-config-recommended": "^3.0.0",
|
"stylelint-config-recommended": "^3.0.0",
|
||||||
"ts-node": "^8.4.1",
|
"ts-node": "^8.4.1",
|
||||||
"typescript": "3.7.2",
|
"typescript": "3.7.2"
|
||||||
"yarn": "^1.22.4"
|
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/node": "^12.12.7",
|
"@types/node": "^12.12.7",
|
||||||
|
@ -66,16 +66,20 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@coder/logger": "1.1.11",
|
"@coder/logger": "1.1.11",
|
||||||
"adm-zip": "^0.4.14",
|
"adm-zip": "^0.4.14",
|
||||||
|
"env-paths": "^2.2.0",
|
||||||
"fs-extra": "^8.1.0",
|
"fs-extra": "^8.1.0",
|
||||||
"http-proxy": "^1.18.0",
|
"http-proxy": "^1.18.0",
|
||||||
"httpolyglot": "^0.1.2",
|
"httpolyglot": "^0.1.2",
|
||||||
|
"js-yaml": "^3.13.1",
|
||||||
"limiter": "^1.1.5",
|
"limiter": "^1.1.5",
|
||||||
"pem": "^1.14.2",
|
"pem": "^1.14.2",
|
||||||
"safe-compare": "^1.1.4",
|
"safe-compare": "^1.1.4",
|
||||||
"semver": "^7.1.3",
|
"semver": "^7.1.3",
|
||||||
"tar": "^6.0.1",
|
"tar": "^6.0.1",
|
||||||
"tar-fs": "^2.0.0",
|
"tar-fs": "^2.0.0",
|
||||||
"ws": "^7.2.0"
|
"ws": "^7.2.0",
|
||||||
|
"xdg-basedir": "^4.0.0",
|
||||||
|
"yarn": "^1.22.4"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"code-server": "out/node/entry.js"
|
"code-server": "out/node/entry.js"
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
<div class="card-box">
|
<div class="card-box">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1 class="main">Welcome to code-server</h1>
|
<h1 class="main">Welcome to code-server</h1>
|
||||||
<div class="sub">Please log in below. Check code-server's logs for the generated password.</div>
|
<div class="sub">Please log in below. {{PASSWORD_MSG}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<form class="login-form" method="post">
|
<form class="login-form" method="post">
|
||||||
|
|
|
@ -2,8 +2,8 @@ import * as http from "http"
|
||||||
import * as limiter from "limiter"
|
import * as limiter from "limiter"
|
||||||
import * as querystring from "querystring"
|
import * as querystring from "querystring"
|
||||||
import { HttpCode, HttpError } from "../../common/http"
|
import { HttpCode, HttpError } from "../../common/http"
|
||||||
import { AuthType, HttpProvider, HttpResponse, Route } from "../http"
|
import { AuthType, HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
||||||
import { hash } from "../util"
|
import { hash, humanPath } from "../util"
|
||||||
|
|
||||||
interface LoginPayload {
|
interface LoginPayload {
|
||||||
password?: string
|
password?: string
|
||||||
|
@ -18,6 +18,14 @@ interface LoginPayload {
|
||||||
* Login HTTP provider.
|
* Login HTTP provider.
|
||||||
*/
|
*/
|
||||||
export class LoginHttpProvider extends HttpProvider {
|
export class LoginHttpProvider extends HttpProvider {
|
||||||
|
public constructor(
|
||||||
|
options: HttpProviderOptions,
|
||||||
|
private readonly configFile: string,
|
||||||
|
private readonly envPassword: boolean,
|
||||||
|
) {
|
||||||
|
super(options)
|
||||||
|
}
|
||||||
|
|
||||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||||
if (this.options.auth !== AuthType.Password || !this.isRoot(route)) {
|
if (this.options.auth !== AuthType.Password || !this.isRoot(route)) {
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
throw new HttpError("Not found", HttpCode.NotFound)
|
||||||
|
@ -46,6 +54,11 @@ export class LoginHttpProvider extends HttpProvider {
|
||||||
public async getRoot(route: Route, error?: Error): Promise<HttpResponse> {
|
public async getRoot(route: Route, error?: Error): Promise<HttpResponse> {
|
||||||
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/login.html")
|
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/login.html")
|
||||||
response.content = response.content.replace(/{{ERROR}}/, error ? `<div class="error">${error.message}</div>` : "")
|
response.content = response.content.replace(/{{ERROR}}/, error ? `<div class="error">${error.message}</div>` : "")
|
||||||
|
let passwordMsg = `Check the config file at ${humanPath(this.configFile)} for the password.`
|
||||||
|
if (this.envPassword) {
|
||||||
|
passwordMsg = "Password was set from $PASSWORD."
|
||||||
|
}
|
||||||
|
response.content = response.content.replace(/{{PASSWORD_MSG}}/g, passwordMsg)
|
||||||
return this.replaceTemplates(route, response)
|
return this.replaceTemplates(route, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
147
src/node/cli.ts
147
src/node/cli.ts
|
@ -1,8 +1,10 @@
|
||||||
|
import { field, Level, logger } from "@coder/logger"
|
||||||
|
import * as fs from "fs-extra"
|
||||||
|
import yaml from "js-yaml"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { field, logger, Level } from "@coder/logger"
|
|
||||||
import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc"
|
import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc"
|
||||||
import { AuthType } from "./http"
|
import { AuthType } from "./http"
|
||||||
import { xdgLocalDir } from "./util"
|
import { generatePassword, humanPath, paths } from "./util"
|
||||||
|
|
||||||
export class Optional<T> {
|
export class Optional<T> {
|
||||||
public constructor(public readonly value?: T) {}
|
public constructor(public readonly value?: T) {}
|
||||||
|
@ -19,10 +21,11 @@ export enum LogLevel {
|
||||||
export class OptionalString extends Optional<string> {}
|
export class OptionalString extends Optional<string> {}
|
||||||
|
|
||||||
export interface Args extends VsArgs {
|
export interface Args extends VsArgs {
|
||||||
|
readonly config?: string
|
||||||
readonly auth?: AuthType
|
readonly auth?: AuthType
|
||||||
|
readonly password?: string
|
||||||
readonly cert?: OptionalString
|
readonly cert?: OptionalString
|
||||||
readonly "cert-key"?: string
|
readonly "cert-key"?: string
|
||||||
readonly "disable-updates"?: boolean
|
|
||||||
readonly "disable-telemetry"?: boolean
|
readonly "disable-telemetry"?: boolean
|
||||||
readonly help?: boolean
|
readonly help?: boolean
|
||||||
readonly host?: string
|
readonly host?: string
|
||||||
|
@ -81,19 +84,30 @@ type Options<T> = {
|
||||||
|
|
||||||
const options: Options<Required<Args>> = {
|
const options: Options<Required<Args>> = {
|
||||||
auth: { type: AuthType, description: "The type of authentication to use." },
|
auth: { type: AuthType, description: "The type of authentication to use." },
|
||||||
|
password: {
|
||||||
|
type: "string",
|
||||||
|
description: "The password for password authentication (can only be passed in via $PASSWORD or the config file).",
|
||||||
|
},
|
||||||
cert: {
|
cert: {
|
||||||
type: OptionalString,
|
type: OptionalString,
|
||||||
path: true,
|
path: true,
|
||||||
description: "Path to certificate. Generated if no path is provided.",
|
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." },
|
"cert-key": { type: "string", path: true, description: "Path to certificate key when using non-generated cert." },
|
||||||
"disable-updates": { type: "boolean", description: "Disable automatic updates." },
|
|
||||||
"disable-telemetry": { type: "boolean", description: "Disable telemetry." },
|
"disable-telemetry": { type: "boolean", description: "Disable telemetry." },
|
||||||
help: { type: "boolean", short: "h", description: "Show this output." },
|
help: { type: "boolean", short: "h", description: "Show this output." },
|
||||||
json: { type: "boolean" },
|
json: { type: "boolean" },
|
||||||
open: { type: "boolean", description: "Open in browser on startup. Does not work remotely." },
|
open: { type: "boolean", description: "Open in browser on startup. Does not work remotely." },
|
||||||
|
|
||||||
"bind-addr": { type: "string", description: "Address to bind to in host:port." },
|
"bind-addr": {
|
||||||
|
type: "string",
|
||||||
|
description: "Address to bind to in host:port. You can also use $PORT to override the port.",
|
||||||
|
},
|
||||||
|
|
||||||
|
config: {
|
||||||
|
type: "string",
|
||||||
|
description: "Path to yaml config file. Every flag maps directly to a key in the config file.",
|
||||||
|
},
|
||||||
|
|
||||||
// These two have been deprecated by bindAddr.
|
// These two have been deprecated by bindAddr.
|
||||||
host: { type: "string", description: "" },
|
host: { type: "string", description: "" },
|
||||||
|
@ -137,7 +151,19 @@ export const optionDescriptions = (): string[] => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parse = (argv: string[]): Args => {
|
export const parse = (
|
||||||
|
argv: string[],
|
||||||
|
opts?: {
|
||||||
|
configFile: string
|
||||||
|
},
|
||||||
|
): Args => {
|
||||||
|
const error = (msg: string): Error => {
|
||||||
|
if (opts?.configFile) {
|
||||||
|
msg = `error reading ${opts.configFile}: ${msg}`
|
||||||
|
}
|
||||||
|
return new Error(msg)
|
||||||
|
}
|
||||||
|
|
||||||
const args: Args = { _: [] }
|
const args: Args = { _: [] }
|
||||||
let ended = false
|
let ended = false
|
||||||
|
|
||||||
|
@ -167,7 +193,11 @@ export const parse = (argv: string[]): Args => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!key || !options[key]) {
|
if (!key || !options[key]) {
|
||||||
throw new Error(`Unknown option ${arg}`)
|
throw error(`Unknown option ${arg}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "password" && !opts?.configFile) {
|
||||||
|
throw new Error("--password can only be set in the config file or passed in via $PASSWORD")
|
||||||
}
|
}
|
||||||
|
|
||||||
const option = options[key]
|
const option = options[key]
|
||||||
|
@ -186,7 +216,11 @@ export const parse = (argv: string[]): Args => {
|
||||||
;(args[key] as OptionalString) = new OptionalString(value)
|
;(args[key] as OptionalString) = new OptionalString(value)
|
||||||
continue
|
continue
|
||||||
} else if (!value) {
|
} else if (!value) {
|
||||||
throw new Error(`--${key} requires a value`)
|
throw error(`--${key} requires a value`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option.type == OptionalString && value == "false") {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (option.path) {
|
if (option.path) {
|
||||||
|
@ -206,7 +240,7 @@ export const parse = (argv: string[]): Args => {
|
||||||
case "number":
|
case "number":
|
||||||
;(args[key] as number) = parseInt(value, 10)
|
;(args[key] as number) = parseInt(value, 10)
|
||||||
if (isNaN(args[key] as number)) {
|
if (isNaN(args[key] as number)) {
|
||||||
throw new Error(`--${key} must be a number`)
|
throw error(`--${key} must be a number`)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case OptionalString:
|
case OptionalString:
|
||||||
|
@ -214,7 +248,7 @@ export const parse = (argv: string[]): Args => {
|
||||||
break
|
break
|
||||||
default: {
|
default: {
|
||||||
if (!Object.values(option.type).includes(value)) {
|
if (!Object.values(option.type).includes(value)) {
|
||||||
throw new Error(`--${key} valid values: [${Object.values(option.type).join(", ")}]`)
|
throw error(`--${key} valid values: [${Object.values(option.type).join(", ")}]`)
|
||||||
}
|
}
|
||||||
;(args[key] as string) = value
|
;(args[key] as string) = value
|
||||||
break
|
break
|
||||||
|
@ -266,7 +300,7 @@ export const parse = (argv: string[]): Args => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!args["user-data-dir"]) {
|
if (!args["user-data-dir"]) {
|
||||||
args["user-data-dir"] = xdgLocalDir
|
args["user-data-dir"] = paths.data
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!args["extensions-dir"]) {
|
if (!args["extensions-dir"]) {
|
||||||
|
@ -275,3 +309,94 @@ export const parse = (argv: string[]): Args => {
|
||||||
|
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function defaultConfigFile(): Promise<string> {
|
||||||
|
return `bind-addr: 127.0.0.1:8080
|
||||||
|
auth: password
|
||||||
|
password: ${await generatePassword()}
|
||||||
|
cert: false
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the code-server yaml config file and returns it as Args.
|
||||||
|
*
|
||||||
|
* @param configPath Read the config from configPath instead of $CODE_SERVER_CONFIG or the default.
|
||||||
|
*/
|
||||||
|
export async function readConfigFile(configPath?: string): Promise<Args> {
|
||||||
|
if (!configPath) {
|
||||||
|
configPath = process.env.CODE_SERVER_CONFIG
|
||||||
|
if (!configPath) {
|
||||||
|
configPath = path.join(paths.config, "config.yaml")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(configPath))) {
|
||||||
|
await fs.outputFile(configPath, await defaultConfigFile())
|
||||||
|
logger.info(`Wrote default config file to ${humanPath(configPath)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Using config file from ${humanPath(configPath)}`)
|
||||||
|
|
||||||
|
const configFile = await fs.readFile(configPath)
|
||||||
|
const config = yaml.safeLoad(configFile.toString(), {
|
||||||
|
filename: configPath,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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]) => {
|
||||||
|
if (opt === true) {
|
||||||
|
return `--${optName}`
|
||||||
|
}
|
||||||
|
return `--${optName}=${opt}`
|
||||||
|
})
|
||||||
|
const args = parse(configFileArgv, {
|
||||||
|
configFile: configPath,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
...args,
|
||||||
|
config: configPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBindAddr(bindAddr: string): [string, number] {
|
||||||
|
const u = new URL(`http://${bindAddr}`)
|
||||||
|
return [u.hostname, parseInt(u.port, 10)]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Addr {
|
||||||
|
host: string
|
||||||
|
port: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindAddrFromArgs(addr: Addr, args: Args): Addr {
|
||||||
|
addr = { ...addr }
|
||||||
|
if (args["bind-addr"]) {
|
||||||
|
;[addr.host, addr.port] = parseBindAddr(args["bind-addr"])
|
||||||
|
}
|
||||||
|
if (args.host) {
|
||||||
|
addr.host = args.host
|
||||||
|
}
|
||||||
|
if (args.port !== undefined) {
|
||||||
|
addr.port = args.port
|
||||||
|
}
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bindAddrFromAllSources(cliArgs: Args, configArgs: Args): [string, number] {
|
||||||
|
let addr: Addr = {
|
||||||
|
host: "localhost",
|
||||||
|
port: 8080,
|
||||||
|
}
|
||||||
|
|
||||||
|
addr = bindAddrFromArgs(addr, configArgs)
|
||||||
|
|
||||||
|
if (process.env.PORT) {
|
||||||
|
addr.port = parseInt(process.env.PORT, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr = bindAddrFromArgs(addr, cliArgs)
|
||||||
|
|
||||||
|
return [addr.host, addr.port]
|
||||||
|
}
|
||||||
|
|
|
@ -9,9 +9,9 @@ import { ProxyHttpProvider } from "./app/proxy"
|
||||||
import { StaticHttpProvider } from "./app/static"
|
import { StaticHttpProvider } from "./app/static"
|
||||||
import { UpdateHttpProvider } from "./app/update"
|
import { UpdateHttpProvider } from "./app/update"
|
||||||
import { VscodeHttpProvider } from "./app/vscode"
|
import { VscodeHttpProvider } from "./app/vscode"
|
||||||
import { Args, optionDescriptions, parse } from "./cli"
|
import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile } from "./cli"
|
||||||
import { AuthType, HttpServer, HttpServerOptions } from "./http"
|
import { AuthType, HttpServer, HttpServerOptions } from "./http"
|
||||||
import { generateCertificate, generatePassword, hash, open } from "./util"
|
import { generateCertificate, hash, open, humanPath } from "./util"
|
||||||
import { ipcMain, wrap } from "./wrapper"
|
import { ipcMain, wrap } from "./wrapper"
|
||||||
|
|
||||||
process.on("uncaughtException", (error) => {
|
process.on("uncaughtException", (error) => {
|
||||||
|
@ -31,25 +31,36 @@ try {
|
||||||
const version = pkg.version || "development"
|
const version = pkg.version || "development"
|
||||||
const commit = pkg.commit || "development"
|
const commit = pkg.commit || "development"
|
||||||
|
|
||||||
const main = async (args: Args): Promise<void> => {
|
const main = async (cliArgs: Args): Promise<void> => {
|
||||||
const auth = args.auth || AuthType.Password
|
const configArgs = await readConfigFile(cliArgs.config)
|
||||||
const originalPassword = auth === AuthType.Password && (process.env.PASSWORD || (await generatePassword()))
|
// This prioritizes the flags set in args over the ones in the config file.
|
||||||
|
let args = Object.assign(configArgs, cliArgs)
|
||||||
|
|
||||||
let host = args.host
|
if (!args.auth) {
|
||||||
let port = args.port
|
args = {
|
||||||
if (args["bind-addr"] !== undefined) {
|
...args,
|
||||||
const u = new URL(`http://${args["bind-addr"]}`)
|
auth: AuthType.Password,
|
||||||
host = u.hostname
|
|
||||||
port = parseInt(u.port, 10)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.trace(`Using extensions-dir at ${humanPath(args["extensions-dir"])}`)
|
||||||
|
logger.trace(`Using user-data-dir at ${humanPath(args["user-data-dir"])}`)
|
||||||
|
|
||||||
|
const envPassword = !!process.env.PASSWORD
|
||||||
|
const password = args.auth === AuthType.Password && (process.env.PASSWORD || args.password)
|
||||||
|
if (args.auth === AuthType.Password && !password) {
|
||||||
|
throw new Error("Please pass in a password via the config file or $PASSWORD")
|
||||||
|
}
|
||||||
|
const [host, port] = bindAddrFromAllSources(cliArgs, configArgs)
|
||||||
|
|
||||||
// Spawn the main HTTP server.
|
// Spawn the main HTTP server.
|
||||||
const options: HttpServerOptions = {
|
const options: HttpServerOptions = {
|
||||||
auth,
|
auth: args.auth,
|
||||||
commit,
|
commit,
|
||||||
host: host || (args.auth === AuthType.Password && args.cert !== undefined ? "0.0.0.0" : "localhost"),
|
host: host,
|
||||||
password: originalPassword ? hash(originalPassword) : undefined,
|
// The hash does not add any actual security but we do it for obfuscation purposes.
|
||||||
port: port !== undefined ? port : process.env.PORT ? parseInt(process.env.PORT, 10) : 8080,
|
password: password ? hash(password) : undefined,
|
||||||
|
port: port,
|
||||||
proxyDomains: args["proxy-domain"],
|
proxyDomains: args["proxy-domain"],
|
||||||
socket: args.socket,
|
socket: args.socket,
|
||||||
...(args.cert && !args.cert.value
|
...(args.cert && !args.cert.value
|
||||||
|
@ -67,9 +78,9 @@ const main = async (args: Args): Promise<void> => {
|
||||||
const httpServer = new HttpServer(options)
|
const httpServer = new HttpServer(options)
|
||||||
const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args)
|
const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args)
|
||||||
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"])
|
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"])
|
||||||
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, !args["disable-updates"])
|
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, false)
|
||||||
httpServer.registerHttpProvider("/proxy", ProxyHttpProvider)
|
httpServer.registerHttpProvider("/proxy", ProxyHttpProvider)
|
||||||
httpServer.registerHttpProvider("/login", LoginHttpProvider)
|
httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword)
|
||||||
httpServer.registerHttpProvider("/static", StaticHttpProvider)
|
httpServer.registerHttpProvider("/static", StaticHttpProvider)
|
||||||
httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update)
|
httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update)
|
||||||
|
|
||||||
|
@ -79,14 +90,13 @@ const main = async (args: Args): Promise<void> => {
|
||||||
const serverAddress = await httpServer.listen()
|
const serverAddress = await httpServer.listen()
|
||||||
logger.info(`HTTP server listening on ${serverAddress}`)
|
logger.info(`HTTP server listening on ${serverAddress}`)
|
||||||
|
|
||||||
if (auth === AuthType.Password && !process.env.PASSWORD) {
|
if (args.auth === AuthType.Password) {
|
||||||
logger.info(` - Password is ${originalPassword}`)
|
if (envPassword) {
|
||||||
logger.info(" - To use your own password set the PASSWORD environment variable")
|
logger.info(" - Using password from $PASSWORD")
|
||||||
if (!args.auth) {
|
} else {
|
||||||
logger.info(" - To disable use `--auth none`")
|
logger.info(` - Using password from ${humanPath(args.config)}`)
|
||||||
}
|
}
|
||||||
} else if (auth === AuthType.Password) {
|
logger.info(" - To disable use `--auth none`")
|
||||||
logger.info(" - Using custom password for authentication")
|
|
||||||
} else {
|
} else {
|
||||||
logger.info(" - No authentication")
|
logger.info(" - No authentication")
|
||||||
}
|
}
|
||||||
|
@ -107,8 +117,6 @@ const main = async (args: Args): Promise<void> => {
|
||||||
httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
|
httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Automatic updates are ${update.enabled ? "enabled" : "disabled"}`)
|
|
||||||
|
|
||||||
if (serverAddress && !options.socket && args.open) {
|
if (serverAddress && !options.socket && args.open) {
|
||||||
// The web socket doesn't seem to work if browsing with 0.0.0.0.
|
// The web socket doesn't seem to work if browsing with 0.0.0.0.
|
||||||
const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost")
|
const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost")
|
||||||
|
|
|
@ -14,7 +14,7 @@ import * as url from "url"
|
||||||
import { HttpCode, HttpError } from "../common/http"
|
import { HttpCode, HttpError } from "../common/http"
|
||||||
import { normalize, Options, plural, split } from "../common/util"
|
import { normalize, Options, plural, split } from "../common/util"
|
||||||
import { SocketProxyProvider } from "./socket"
|
import { SocketProxyProvider } from "./socket"
|
||||||
import { getMediaMime, xdgLocalDir } from "./util"
|
import { getMediaMime, paths } from "./util"
|
||||||
|
|
||||||
export type Cookies = { [key: string]: string[] | undefined }
|
export type Cookies = { [key: string]: string[] | undefined }
|
||||||
export type PostData = { [key: string]: string | string[] | undefined }
|
export type PostData = { [key: string]: string | string[] | undefined }
|
||||||
|
@ -473,7 +473,7 @@ export class HttpServer {
|
||||||
|
|
||||||
public constructor(private readonly options: HttpServerOptions) {
|
public constructor(private readonly options: HttpServerOptions) {
|
||||||
this.proxyDomains = new Set((options.proxyDomains || []).map((d) => d.replace(/^\*\./, "")))
|
this.proxyDomains = new Set((options.proxyDomains || []).map((d) => d.replace(/^\*\./, "")))
|
||||||
this.heart = new Heart(path.join(xdgLocalDir, "heartbeat"), async () => {
|
this.heart = new Heart(path.join(paths.data, "heartbeat"), async () => {
|
||||||
const connections = await this.getConnections()
|
const connections = await this.getConnections()
|
||||||
logger.trace(`${connections} active connection${plural(connections)}`)
|
logger.trace(`${connections} active connection${plural(connections)}`)
|
||||||
return connections !== 0
|
return connections !== 0
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as fs from "fs-extra"
|
import * as fs from "fs-extra"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { extend, xdgLocalDir } from "./util"
|
import { extend, paths } from "./util"
|
||||||
import { logger } from "@coder/logger"
|
import { logger } from "@coder/logger"
|
||||||
|
|
||||||
export type Settings = { [key: string]: Settings | string | boolean | number }
|
export type Settings = { [key: string]: Settings | string | boolean | number }
|
||||||
|
@ -60,4 +60,4 @@ export interface CoderSettings extends UpdateSettings {
|
||||||
/**
|
/**
|
||||||
* Global code-server settings file.
|
* Global code-server settings file.
|
||||||
*/
|
*/
|
||||||
export const settings = new SettingsProvider<CoderSettings>(path.join(xdgLocalDir, "coder.json"))
|
export const settings = new SettingsProvider<CoderSettings>(path.join(paths.data, "coder.json"))
|
||||||
|
|
|
@ -4,24 +4,54 @@ import * as fs from "fs-extra"
|
||||||
import * as os from "os"
|
import * as os from "os"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import * as util from "util"
|
import * as util from "util"
|
||||||
|
import envPaths from "env-paths"
|
||||||
|
import xdgBasedir from "xdg-basedir"
|
||||||
|
|
||||||
export const tmpdir = path.join(os.tmpdir(), "code-server")
|
export const tmpdir = path.join(os.tmpdir(), "code-server")
|
||||||
|
|
||||||
const getXdgDataDir = (): string => {
|
interface Paths {
|
||||||
switch (process.platform) {
|
data: string
|
||||||
case "win32":
|
config: string
|
||||||
return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), "AppData/Local"), "code-server/Data")
|
|
||||||
case "darwin":
|
|
||||||
return path.join(
|
|
||||||
process.env.XDG_DATA_HOME || path.join(os.homedir(), "Library/Application Support"),
|
|
||||||
"code-server",
|
|
||||||
)
|
|
||||||
default:
|
|
||||||
return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local/share"), "code-server")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const xdgLocalDir = getXdgDataDir()
|
export const paths = getEnvPaths()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the config and data paths for the current platform/configuration.
|
||||||
|
* On MacOS this function gets the standard XDG directories instead of using the native macOS
|
||||||
|
* ones. Most CLIs do this as in practice only GUI apps use the standard macOS directories.
|
||||||
|
*/
|
||||||
|
function getEnvPaths(): Paths {
|
||||||
|
let paths: Paths
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
paths = envPaths("code-server", {
|
||||||
|
suffix: "",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if (xdgBasedir.data === undefined || xdgBasedir.config === undefined) {
|
||||||
|
throw new Error("No home folder?")
|
||||||
|
}
|
||||||
|
paths = {
|
||||||
|
data: path.join(xdgBasedir.data, "code-server"),
|
||||||
|
config: path.join(xdgBasedir.config, "code-server"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* humanPath replaces the home directory in p with ~.
|
||||||
|
* Makes it more readable.
|
||||||
|
*
|
||||||
|
* @param p
|
||||||
|
*/
|
||||||
|
export function humanPath(p?: string): string {
|
||||||
|
if (!p) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return p.replace(os.homedir(), "~")
|
||||||
|
}
|
||||||
|
|
||||||
export const generateCertificate = async (): Promise<{ cert: string; certKey: string }> => {
|
export const generateCertificate = async (): Promise<{ cert: string; certKey: string }> => {
|
||||||
const paths = {
|
const paths = {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { logger, Level } from "@coder/logger"
|
||||||
import * as assert from "assert"
|
import * as assert from "assert"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { parse } from "../src/node/cli"
|
import { parse } from "../src/node/cli"
|
||||||
import { xdgLocalDir } from "../src/node/util"
|
import { paths } from "../src/node/util"
|
||||||
|
|
||||||
describe("cli", () => {
|
describe("cli", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -12,8 +12,8 @@ describe("cli", () => {
|
||||||
// The parser will always fill these out.
|
// The parser will always fill these out.
|
||||||
const defaults = {
|
const defaults = {
|
||||||
_: [],
|
_: [],
|
||||||
"extensions-dir": path.join(xdgLocalDir, "extensions"),
|
"extensions-dir": path.join(paths.data, "extensions"),
|
||||||
"user-data-dir": xdgLocalDir,
|
"user-data-dir": paths.data,
|
||||||
}
|
}
|
||||||
|
|
||||||
it("should set defaults", () => {
|
it("should set defaults", () => {
|
||||||
|
|
15
yarn.lock
15
yarn.lock
|
@ -931,6 +931,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/js-yaml@^3.12.3":
|
||||||
|
version "3.12.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.3.tgz#abf383c5b639d0aa8b8c4a420d6a85f703357d6c"
|
||||||
|
integrity sha512-otRe77JNNWzoVGLKw8TCspKswRoQToys4tuL6XYVBFxjgeM0RUrx7m3jkaTdxILxeGry3zM8mGYkGXMeQ02guA==
|
||||||
|
|
||||||
"@types/json-schema@^7.0.3":
|
"@types/json-schema@^7.0.3":
|
||||||
version "7.0.4"
|
version "7.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
|
||||||
|
@ -2557,6 +2562,11 @@ entities@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
|
resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
|
||||||
integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
|
integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
|
||||||
|
|
||||||
|
env-paths@^2.2.0:
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"
|
||||||
|
integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==
|
||||||
|
|
||||||
envinfo@^7.3.1:
|
envinfo@^7.3.1:
|
||||||
version "7.5.1"
|
version "7.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.5.1.tgz#93c26897225a00457c75e734d354ea9106a72236"
|
resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.5.1.tgz#93c26897225a00457c75e734d354ea9106a72236"
|
||||||
|
@ -7373,6 +7383,11 @@ ws@^7.2.0:
|
||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.5.tgz#abb1370d4626a5a9cd79d8de404aa18b3465d10d"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.5.tgz#abb1370d4626a5a9cd79d8de404aa18b3465d10d"
|
||||||
integrity sha512-C34cIU4+DB2vMyAbmEKossWq2ZQDr6QEyuuCzWrM9zfw1sGc0mYiJ0UnG9zzNykt49C2Fi34hvr2vssFQRS6EA==
|
integrity sha512-C34cIU4+DB2vMyAbmEKossWq2ZQDr6QEyuuCzWrM9zfw1sGc0mYiJ0UnG9zzNykt49C2Fi34hvr2vssFQRS6EA==
|
||||||
|
|
||||||
|
xdg-basedir@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
|
||||||
|
integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==
|
||||||
|
|
||||||
xml-name-validator@^3.0.0:
|
xml-name-validator@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
|
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
|
||||||
|
|
Loading…
Reference in New Issue