Compare commits
13 Commits
2.1665-vsc
...
2.1692-vsc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e14362f322 | ||
|
|
917aa48072 | ||
|
|
938c6ef829 | ||
|
|
0add01d383 | ||
|
|
2018024810 | ||
|
|
a1d6bcb8e5 | ||
|
|
727ac6483b | ||
|
|
2c15c09fc0 | ||
|
|
2ad2582cc0 | ||
|
|
cee0ac213c | ||
|
|
780a673017 | ||
|
|
af71203955 | ||
|
|
fc3acfabb2 |
@@ -33,7 +33,8 @@ RUN apt-get update && apt-get install -y \
|
||||
dumb-init \
|
||||
vim \
|
||||
curl \
|
||||
wget
|
||||
wget \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN locale-gen en_US.UTF-8
|
||||
# We cannot use update-locale because docker will not use the env variables
|
||||
|
||||
14
README.md
14
README.md
@@ -60,14 +60,14 @@ arguments when launching code-server with Docker. See
|
||||
### Build
|
||||
|
||||
See
|
||||
[VS Code prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites)
|
||||
[VS Code's prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites)
|
||||
before building.
|
||||
|
||||
```shell
|
||||
export OUT=/path/to/output/build # Optional if only building. Required if also developing.
|
||||
yarn build ${vscodeVersion} ${codeServerVersion} # See travis.yml for the VS Code version to use.
|
||||
# The code-server version can be anything you want.
|
||||
node ~/path/to/output/build/out/vs/server/main.js # You can run the built JavaScript with Node.
|
||||
node /path/to/output/build/out/vs/server/main.js # You can run the built JavaScript with Node.
|
||||
yarn binary ${vscodeVersion} ${codeServerVersion} # Or you can package it into a binary.
|
||||
```
|
||||
|
||||
@@ -135,7 +135,7 @@ data collected to improve code-server.
|
||||
### Development
|
||||
|
||||
See
|
||||
[VS Code prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites)
|
||||
[VS Code's prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites)
|
||||
before developing.
|
||||
|
||||
```shell
|
||||
@@ -155,8 +155,7 @@ yarn start
|
||||
```
|
||||
|
||||
If you run into issues about a different version of Node being used, try running
|
||||
`npm rebuild` in the VS Code directory and ignore the error at the end from
|
||||
`vscode-ripgrep`.
|
||||
`npm rebuild` in the VS Code directory.
|
||||
|
||||
### Upgrading VS Code
|
||||
|
||||
@@ -171,7 +170,6 @@ directory.
|
||||
|
||||
Our changes include:
|
||||
|
||||
- Change the remote schema to `code-server`.
|
||||
- Allow multiple extension directories (both user and built-in).
|
||||
- Modify the loader, websocket, webview, service worker, and asset requests to
|
||||
use the URL of the page as a base (and TLS if necessary for the websocket).
|
||||
@@ -190,8 +188,8 @@ Our changes include:
|
||||
|
||||
## Enterprise
|
||||
|
||||
Visit [our enterprise page](https://coder.com/enterprise) for more information
|
||||
about our enterprise offering.
|
||||
Visit [our enterprise page](https://coder.com) for more information about our
|
||||
enterprise offering.
|
||||
|
||||
## Commercialization
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
[Definition]
|
||||
|
||||
failregex = ^INFO\s+Failed login attempt\s+{\"password\":\"(\\.|[^"])*\",\"remoteAddress\":\"<HOST>\"
|
||||
failregex = ^Failed login attempt\s+{\"remoteAddress\":\"<HOST>\"
|
||||
|
||||
# Use this instead for proxies (ensure the proxy is configured to send the
|
||||
# X-Forwarded-For header).
|
||||
# failregex = ^INFO\s+Failed login attempt\s+{\"password\":\"(\\.|[^"])*\",\"xForwardedFor\":\"<HOST>\"
|
||||
# failregex = ^Failed login attempt\s+{\"xForwardedFor\":\"<HOST>\"
|
||||
|
||||
ignoreregex =
|
||||
|
||||
|
||||
@@ -30,6 +30,6 @@ accessible from the internet (use localhost or block it in your firewall).
|
||||
## Fail2Ban
|
||||
Fail2Ban allows for automatically banning and logging repeated failed
|
||||
authentication attempts for many applications through regex filters. A working
|
||||
filter for code-server can be found in `./code-server.fail2ban.conf`. Once this
|
||||
filter for code-server can be found in `./examples/fail2ban.conf`. Once this
|
||||
is installed and configured correctly, repeated failed login attempts should
|
||||
automatically be banned from connecting to your server.
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"patch:apply": "cd ../../../ && git apply ./src/vs/server/scripts/vscode.patch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@coder/nbin": "^1.2.2",
|
||||
"@coder/nbin": "^1.2.3",
|
||||
"@types/fs-extra": "^8.0.1",
|
||||
"@types/node": "^10.12.12",
|
||||
"@types/pem": "^1.9.5",
|
||||
|
||||
@@ -812,6 +812,21 @@ index 3bdfa1a79f..ded21cf9c6 100644
|
||||
|
||||
// register services that only throw errors
|
||||
function NotImplementedProxy<T>(name: ServiceIdentifier<T>): { new(): T } {
|
||||
diff --git a/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts b/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts
|
||||
index 3b5706ce76..f390ed35dc 100644
|
||||
--- a/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts
|
||||
+++ b/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts
|
||||
@@ -36,7 +36,9 @@ const nativeAddEventLister = addEventListener.bind(self);
|
||||
self.addEventLister = () => console.trace(`'addEventListener' has been blocked`);
|
||||
|
||||
self.indexedDB.open = () => console.trace(`'indexedDB.open' has been blocked`);
|
||||
-self.caches.open = () => console.trace(`'indexedDB.caches' has been blocked`);
|
||||
+if (self.caches) { // NOTE@coder: on insecure domains this exists in Firefox but not Chromium or Safari.
|
||||
+ self.caches.open = () => console.trace(`'indexedDB.caches' has been blocked`);
|
||||
+}
|
||||
|
||||
//#endregion ---
|
||||
|
||||
diff --git a/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts b/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts
|
||||
index 99394090da..4891e0fece 100644
|
||||
--- a/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Emitter } from "vs/base/common/event";
|
||||
import { URI } from "vs/base/common/uri";
|
||||
import { localize } from "vs/nls";
|
||||
import { Extensions, IConfigurationRegistry } from "vs/platform/configuration/common/configurationRegistry";
|
||||
import { registerSingleton } from "vs/platform/instantiation/common/extensions";
|
||||
import { ServiceCollection } from "vs/platform/instantiation/common/serviceCollection";
|
||||
import { ILocalizationsService } from "vs/platform/localizations/common/localizations";
|
||||
import { Registry } from "vs/platform/registry/common/platform";
|
||||
import { PersistentConnectionEventType } from "vs/platform/remote/common/remoteAgentConnection";
|
||||
import { ITelemetryService } from "vs/platform/telemetry/common/telemetry";
|
||||
import { coderApi, vscodeApi } from "vs/server/src/browser/api";
|
||||
@@ -22,6 +25,23 @@ class TelemetryService extends TelemetryChannelClient {
|
||||
}
|
||||
}
|
||||
|
||||
const TELEMETRY_SECTION_ID = "telemetry";
|
||||
|
||||
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
|
||||
"id": TELEMETRY_SECTION_ID,
|
||||
"order": 110,
|
||||
"type": "object",
|
||||
"title": localize("telemetryConfigurationTitle", "Telemetry"),
|
||||
"properties": {
|
||||
"telemetry.enableTelemetry": {
|
||||
"type": "boolean",
|
||||
"description": localize("telemetry.enableTelemetry", "Enable usage data and errors to be sent to a Microsoft online service."),
|
||||
"default": true,
|
||||
"tags": ["usesOnlineServices"]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
class NodeProxyService extends NodeProxyChannelClient implements INodeProxyService {
|
||||
private readonly _onClose = new Emitter<void>();
|
||||
public readonly onClose = this._onClose.event;
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
<link rel="manifest" href="./manifest.json">
|
||||
<link rel="apple-touch-icon" href="./static-{{COMMIT}}/out/vs/server/src/media/code-server.png" />
|
||||
<link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="./static-{{COMMIT}}/out/vs/workbench/workbench.web.api.css">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
|
||||
<!-- Prefetch to avoid waterfall -->
|
||||
<link rel="prefetch" href="./static-{{COMMIT}}/node_modules/semver-umd/lib/semver-umd.js">
|
||||
|
||||
@@ -47,7 +47,6 @@ const getArgs = (): Args => {
|
||||
case "wait":
|
||||
case "disable-gpu":
|
||||
// TODO: pretty sure these don't work but not 100%.
|
||||
case "max-memory":
|
||||
case "prof-startup":
|
||||
case "inspect-extensions":
|
||||
case "inspect-brk-extensions":
|
||||
@@ -82,8 +81,7 @@ const getArgs = (): Args => {
|
||||
return validatePaths(args);
|
||||
};
|
||||
|
||||
const startVscode = async (): Promise<void | void[]> => {
|
||||
const args = getArgs();
|
||||
const startVscode = async (args: Args): Promise<void | void[]> => {
|
||||
const extra = args["_"] || [];
|
||||
const options = {
|
||||
auth: args.auth || AuthType.Password,
|
||||
@@ -155,8 +153,7 @@ const startVscode = async (): Promise<void | void[]> => {
|
||||
}
|
||||
};
|
||||
|
||||
const startCli = (): boolean | Promise<void> => {
|
||||
const args = getArgs();
|
||||
const startCli = (args: Args): boolean | Promise<void> => {
|
||||
if (args.help) {
|
||||
const executable = `${product.applicationName}${os.platform() === "win32" ? ".exe" : ""}`;
|
||||
console.log(buildHelpMessage(product.nameLong, executable, product.codeServerVersion, OPTIONS, false));
|
||||
@@ -196,12 +193,14 @@ const startCli = (): boolean | Promise<void> => {
|
||||
export class WrapperProcess {
|
||||
private process?: cp.ChildProcess;
|
||||
private started?: Promise<void>;
|
||||
private currentVersion = product.codeServerVersion;
|
||||
|
||||
public constructor() {
|
||||
public constructor(private readonly args: Args) {
|
||||
ipcMain.onMessage(async (message) => {
|
||||
switch (message) {
|
||||
switch (message.type) {
|
||||
case "relaunch":
|
||||
logger.info("Relaunching...");
|
||||
logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`);
|
||||
this.currentVersion = message.version;
|
||||
this.started = undefined;
|
||||
if (this.process) {
|
||||
this.process.removeAllListeners();
|
||||
@@ -233,11 +232,26 @@ export class WrapperProcess {
|
||||
}
|
||||
|
||||
private spawn(): cp.ChildProcess {
|
||||
return cp.spawn(process.argv[0], process.argv.slice(1), {
|
||||
// Flags to pass along to the Node binary. We use the environment variable
|
||||
// since otherwise the code-server binary will swallow them.
|
||||
const maxMemory = this.args["max-memory"] || 2048;
|
||||
let nodeOptions = `${process.env.NODE_OPTIONS || ""} ${this.args["js-flags"] || ""}`;
|
||||
if (!/max_old_space_size=(\d+)/g.exec(nodeOptions)) {
|
||||
nodeOptions += ` --max_old_space_size=${maxMemory}`;
|
||||
}
|
||||
|
||||
// If we're using loose files then we need to specify the path. If we're in
|
||||
// the binary we need to let the binary determine the path (via nbin) since
|
||||
// it could be different between binaries which presents a problem when
|
||||
// upgrading (different version numbers or different staging directories).
|
||||
const isBinary = (global as any).NBIN_LOADED;
|
||||
return cp.spawn(process.argv[0], process.argv.slice(isBinary ? 2 : 1), {
|
||||
env: {
|
||||
...process.env,
|
||||
LAUNCH_VSCODE: "true",
|
||||
NBIN_BYPASS: undefined,
|
||||
VSCODE_PARENT_PID: process.pid.toString(),
|
||||
NODE_OPTIONS: nodeOptions,
|
||||
},
|
||||
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
||||
});
|
||||
@@ -245,11 +259,12 @@ export class WrapperProcess {
|
||||
}
|
||||
|
||||
const main = async(): Promise<boolean | void | void[]> => {
|
||||
const args = getArgs();
|
||||
if (process.env.LAUNCH_VSCODE) {
|
||||
await ipcMain.handshake();
|
||||
return startVscode();
|
||||
return startVscode(args);
|
||||
}
|
||||
return startCli() || new WrapperProcess().start();
|
||||
return startCli(args) || new WrapperProcess(args).start();
|
||||
};
|
||||
|
||||
const exit = process.exit;
|
||||
|
||||
@@ -6,7 +6,12 @@ enum ControlMessage {
|
||||
okFromChild = "ok<",
|
||||
}
|
||||
|
||||
export type Message = "relaunch";
|
||||
interface RelaunchMessage {
|
||||
type: "relaunch";
|
||||
version: string;
|
||||
}
|
||||
|
||||
export type Message = RelaunchMessage;
|
||||
|
||||
class IpcMain {
|
||||
protected readonly _onMessage = new Emitter<Message>();
|
||||
@@ -41,11 +46,15 @@ class IpcMain {
|
||||
});
|
||||
}
|
||||
|
||||
public relaunch(): void {
|
||||
public relaunch(version: string): void {
|
||||
this.send({ type: "relaunch", version });
|
||||
}
|
||||
|
||||
private send(message: Message): void {
|
||||
if (!process.send) {
|
||||
throw new Error("Not a child process with IPC enabled");
|
||||
}
|
||||
process.send("relaunch");
|
||||
process.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ import { TelemetryClient } from "vs/server/src/node/insights";
|
||||
import { getLocaleFromConfig, getNlsConfiguration } from "vs/server/src/node/nls";
|
||||
import { Protocol } from "vs/server/src/node/protocol";
|
||||
import { UpdateService } from "vs/server/src/node/update";
|
||||
import { AuthType, getMediaMime, getUriTransformer, localRequire, tmpdir } from "vs/server/src/node/util";
|
||||
import { AuthType, getMediaMime, getUriTransformer, hash, localRequire, tmpdir } from "vs/server/src/node/util";
|
||||
import { RemoteExtensionLogFileName } from "vs/workbench/services/remote/common/remoteAgentService";
|
||||
import { IWorkbenchConstructionOptions } from "vs/workbench/workbench.web.api";
|
||||
|
||||
@@ -101,6 +101,10 @@ export interface LoginPayload {
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface AuthPayload {
|
||||
key?: string[];
|
||||
}
|
||||
|
||||
export class HttpError extends Error {
|
||||
public constructor(message: string, public readonly code: number) {
|
||||
super(message);
|
||||
@@ -137,6 +141,7 @@ export abstract class Server {
|
||||
host: options.auth === "password" && options.cert ? "0.0.0.0" : "localhost",
|
||||
...options,
|
||||
basePath: options.basePath ? options.basePath.replace(/\/+$/, "") : "",
|
||||
password: options.password ? hash(options.password) : undefined,
|
||||
};
|
||||
this.protocol = this.options.cert ? "https" : "http";
|
||||
if (this.protocol === "https") {
|
||||
@@ -357,16 +362,25 @@ export abstract class Server {
|
||||
}
|
||||
|
||||
private async tryLogin(request: http.IncomingMessage): Promise<Response> {
|
||||
if (this.authenticate(request) && (request.method === "GET" || request.method === "POST")) {
|
||||
return { redirect: "/" };
|
||||
const redirect = (password: string | true) => {
|
||||
return {
|
||||
redirect: "/",
|
||||
headers: typeof password === "string"
|
||||
? { "Set-Cookie": `key=${password}; Path=${this.options.basePath || "/"}; HttpOnly; SameSite=strict` }
|
||||
: {},
|
||||
};
|
||||
};
|
||||
const providedPassword = this.authenticate(request);
|
||||
if (providedPassword && (request.method === "GET" || request.method === "POST")) {
|
||||
return redirect(providedPassword);
|
||||
}
|
||||
if (request.method === "POST") {
|
||||
const data = await this.getData<LoginPayload>(request);
|
||||
if (this.authenticate(request, data)) {
|
||||
return {
|
||||
redirect: "/",
|
||||
headers: { "Set-Cookie": `password=${data.password}` }
|
||||
};
|
||||
const password = this.authenticate(request, {
|
||||
key: typeof data.password === "string" ? [hash(data.password)] : undefined,
|
||||
});
|
||||
if (password) {
|
||||
return redirect(password);
|
||||
}
|
||||
console.error("Failed login attempt", JSON.stringify({
|
||||
xForwardedFor: request.headers["x-forwarded-for"],
|
||||
@@ -426,23 +440,33 @@ export abstract class Server {
|
||||
: Promise.resolve({} as T);
|
||||
}
|
||||
|
||||
private authenticate(request: http.IncomingMessage, payload?: LoginPayload): boolean {
|
||||
if (this.options.auth !== "password") {
|
||||
private authenticate(request: http.IncomingMessage, payload?: AuthPayload): string | boolean {
|
||||
if (this.options.auth === "none") {
|
||||
return true;
|
||||
}
|
||||
const safeCompare = localRequire<typeof import("safe-compare")>("safe-compare/index");
|
||||
if (typeof payload === "undefined") {
|
||||
payload = this.parseCookies<LoginPayload>(request);
|
||||
payload = this.parseCookies<AuthPayload>(request);
|
||||
}
|
||||
return !!this.options.password && safeCompare(payload.password || "", this.options.password);
|
||||
if (this.options.password && payload.key) {
|
||||
for (let i = 0; i < payload.key.length; ++i) {
|
||||
if (safeCompare(payload.key[i], this.options.password)) {
|
||||
return payload.key[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private parseCookies<T extends object>(request: http.IncomingMessage): T {
|
||||
const cookies: { [key: string]: string } = {};
|
||||
const cookies: { [key: string]: string[] } = {};
|
||||
if (request.headers.cookie) {
|
||||
request.headers.cookie.split(";").forEach((keyValue) => {
|
||||
const [key, value] = split(keyValue, "=");
|
||||
cookies[key] = decodeURI(value);
|
||||
if (!cookies[key]) {
|
||||
cookies[key] = [];
|
||||
}
|
||||
cookies[key].push(decodeURI(value));
|
||||
});
|
||||
}
|
||||
return cookies as T;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { IFileService } from "vs/platform/files/common/files";
|
||||
import { ILogService } from "vs/platform/log/common/log";
|
||||
import product from "vs/platform/product/common/product";
|
||||
import { asJson, IRequestService } from "vs/platform/request/common/request";
|
||||
import { AvailableForDownload, State, UpdateType } from "vs/platform/update/common/update";
|
||||
import { AvailableForDownload, State, UpdateType, StateType } from "vs/platform/update/common/update";
|
||||
import { AbstractUpdateService } from "vs/platform/update/electron-main/abstractUpdateService";
|
||||
import { ipcMain } from "vs/server/src/node/ipc";
|
||||
import { extract } from "vs/server/src/node/marketplace";
|
||||
@@ -37,6 +37,9 @@ export class UpdateService extends AbstractUpdateService {
|
||||
super(null, configurationService, environmentService, requestService, logService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the currently installed version is the latest.
|
||||
*/
|
||||
public async isLatestVersion(latest?: IUpdate | null): Promise<boolean | undefined> {
|
||||
if (!latest) {
|
||||
latest = await this.getLatestVersion();
|
||||
@@ -44,8 +47,12 @@ export class UpdateService extends AbstractUpdateService {
|
||||
if (latest) {
|
||||
const latestMajor = parseInt(latest.name);
|
||||
const currentMajor = parseInt(product.codeServerVersion);
|
||||
return !isNaN(latestMajor) && !isNaN(currentMajor) &&
|
||||
currentMajor <= latestMajor && latest.name === product.codeServerVersion;
|
||||
// If these are invalid versions we can't compare meaningfully.
|
||||
return isNaN(latestMajor) || isNaN(currentMajor) ||
|
||||
// This can happen when there is a pre-release for a new major version.
|
||||
currentMajor > latestMajor ||
|
||||
// Otherwise assume that if it's not the same then we're out of date.
|
||||
latest.name === product.codeServerVersion;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -55,14 +62,16 @@ export class UpdateService extends AbstractUpdateService {
|
||||
}
|
||||
|
||||
public async doQuitAndInstall(): Promise<void> {
|
||||
ipcMain.relaunch();
|
||||
if (this.state.type === StateType.Ready) {
|
||||
ipcMain.relaunch(this.state.update.version);
|
||||
}
|
||||
}
|
||||
|
||||
protected async doCheckForUpdates(context: any): Promise<void> {
|
||||
this.setState(State.CheckingForUpdates(context));
|
||||
try {
|
||||
const update = await this.getLatestVersion();
|
||||
if (!update || this.isLatestVersion(update)) {
|
||||
if (!update || await this.isLatestVersion(update)) {
|
||||
this.setState(State.Idle(UpdateType.Archive));
|
||||
} else {
|
||||
this.setState(State.AvailableForDownload({
|
||||
|
||||
@@ -67,6 +67,10 @@ export const generatePassword = async (length: number = 24): Promise<string> =>
|
||||
return buffer.toString("hex").substring(0, length);
|
||||
};
|
||||
|
||||
export const hash = (str: string): string => {
|
||||
return crypto.createHash("sha256").update(str).digest("hex");
|
||||
};
|
||||
|
||||
export const getMediaMime = (filePath?: string): string => {
|
||||
return filePath && (vsGetMediaMime(filePath) || (<{[index: string]: string}>{
|
||||
".css": "text/css",
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@coder/logger/-/logger-1.1.8.tgz#416a7221d84161ee35eca9cfa93ba9377639b4ee"
|
||||
integrity sha512-NJDC4rZTx0deVYqAxZtJWACq3IrVR59BjFeZebO3i7OfTZZMkkbLsGsCFMnJd5KnX6KjnvvFq4XXtwJ9yf8/YQ==
|
||||
|
||||
"@coder/nbin@^1.2.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@coder/nbin/-/nbin-1.2.2.tgz#c5f9aaa2a0e84c2a13a4cce895547efbd66730b7"
|
||||
integrity sha512-1Z6aYBRZRY1AQ2xp0jmoz+TXR8M4WaHa9FfVkOPej0KPJjYtEp18I+/6CmffDtBLxSnIai0rc+AA0VhbjCN/rg==
|
||||
"@coder/nbin@^1.2.3":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@coder/nbin/-/nbin-1.2.3.tgz#793061abc7e1f7e0a9d1b9f854fa8f4121ed4e90"
|
||||
integrity sha512-JGJhkaqCrAF9hQ8e7m29/gbbKqDrBAOJCdjNZv9LKF+67lmHUoJ2QS+eHN+KOtpO4EJeEs4/uq7LSEdT+g3t5w==
|
||||
dependencies:
|
||||
"@coder/logger" "^1.1.8"
|
||||
fs-extra "^7.0.1"
|
||||
|
||||
Reference in New Issue
Block a user