diff --git a/.eslintignore b/.eslintignore index 2d9021afbb8..ca3b6756a1a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,6 +7,10 @@ **/extensions/html-language-features/server/lib/jquery.d.ts **/extensions/html-language-features/server/src/test/pathCompletionFixtures/** **/extensions/ipynb/notebook-out/** +# --- Start Positron --- +# Ignore OpenAPI generated files +**/extensions/kallichore-adapter/src/kcclient/** +# --- End Positron --- **/extensions/markdown-language-features/media/** **/extensions/markdown-language-features/notebook-out/** **/extensions/markdown-math/notebook-out/** diff --git a/.vscode-test.js b/.vscode-test.js index 4c720c8e76f..5252fcdd6e6 100644 --- a/.vscode-test.js +++ b/.vscode-test.js @@ -67,6 +67,11 @@ const extensions = [ workspaceFolder: 'extensions/positron-run-app/test-workspace', mocha: { timeout: 60_000 } }, + { + label: 'kallichore-adapter', + workspaceFolder: path.join(os.tmpdir(), `kallichore-adapter-${Math.floor(Math.random() * 100000)}`), + mocha: { timeout: 60_000 } + }, // --- End Positron --- { label: 'microsoft-authentication', diff --git a/build/darwin/create-universal-app.js b/build/darwin/create-universal-app.js index 3ac0e9d0de0..468f13d93cd 100644 --- a/build/darwin/create-universal-app.js +++ b/build/darwin/create-universal-app.js @@ -47,6 +47,8 @@ const stashPatterns = [ '**/pydevd/**', // Cython pre-built binaries for Python debugging // Exclusions from R language pack (positron-r) '**/ark', // Compiled R kernel and LSP + // Exclusions from Kallichore Jupyter supervisor + '**/kcserver', // Compiled Jupyter supervisor // Exclusions from Quarto '**/quarto/bin/tools/**', ]; diff --git a/build/darwin/create-universal-app.ts b/build/darwin/create-universal-app.ts index f4ee4d6bb5a..de2c099ac28 100644 --- a/build/darwin/create-universal-app.ts +++ b/build/darwin/create-universal-app.ts @@ -50,6 +50,8 @@ const stashPatterns = [ '**/pydevd/**', // Cython pre-built binaries for Python debugging // Exclusions from R language pack (positron-r) '**/ark', // Compiled R kernel and LSP + // Exclusions from Kallichore Jupyter supervisor + '**/kcserver', // Compiled Jupyter supervisor // Exclusions from Quarto '**/quarto/bin/tools/**', ]; diff --git a/build/filters.js b/build/filters.js index 451143a0e71..f743e2639b0 100644 --- a/build/filters.js +++ b/build/filters.js @@ -28,9 +28,19 @@ module.exports.all = [ '!**/node_modules/**', // --- Start Positron --- + // Excluded since it's generated code (an OpenAPI client) + '!extensions/kallichore-adapter/src/kcclient/**/*', + + // Excluded since it comes from an external source with its own hygiene + // rules '!extensions/positron-python/**/*', + + // Excluded since it comes from an external source with its own hygiene + // rules '!extensions/open-remote-ssh/**/*', - '!test/smoke/test-repo/**/*' + + // Excluded since it isn't shipping code + '!test/smoke/test-repo/**/*', // --- End Positron --- ]; diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index f85bf27382f..4e7510961ff 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -29,6 +29,7 @@ const ext = require('./lib/extensions'); // }); const compilations = [ // --- Start Positron --- + 'extensions/kallichore-adapter/tsconfig.json', 'extensions/open-remote-ssh/tsconfig.json', 'extensions/positron-code-cells/tsconfig.json', 'extensions/positron-connections/tsconfig.json', diff --git a/build/npm/dirs.js b/build/npm/dirs.js index 6e9c1f623ca..09aab2f6fd4 100644 --- a/build/npm/dirs.js +++ b/build/npm/dirs.js @@ -48,6 +48,7 @@ const dirs = [ 'extensions/json-language-features/server', // --- Start Positron --- 'extensions/jupyter-adapter', + 'extensions/kallichore-adapter', // --- End Positron --- 'extensions/markdown-language-features', 'extensions/markdown-math', diff --git a/extensions/jupyter-adapter/src/Api.ts b/extensions/jupyter-adapter/src/Api.ts index 5afd2961913..e538cb0aa24 100644 --- a/extensions/jupyter-adapter/src/Api.ts +++ b/extensions/jupyter-adapter/src/Api.ts @@ -35,8 +35,8 @@ export class JupyterAdapterApiImpl implements JupyterAdapterApi { kernel: JupyterKernelSpec, dynState: positron.LanguageRuntimeDynState, extra: JupyterKernelExtra, - ): JupyterLanguageRuntimeSession { - return new LanguageRuntimeSessionAdapter( + ): Promise { + return Promise.resolve(new LanguageRuntimeSessionAdapter( runtimeMetadata, sessionMetadata, this._context, @@ -44,7 +44,7 @@ export class JupyterAdapterApiImpl implements JupyterAdapterApi { kernel, dynState, extra - ); + )); } /** @@ -59,7 +59,7 @@ export class JupyterAdapterApiImpl implements JupyterAdapterApi { restoreSession( runtimeMetadata: positron.LanguageRuntimeMetadata, sessionMetadata: positron.RuntimeSessionMetadata - ): JupyterLanguageRuntimeSession { + ): Promise { // Get the serialized session from the workspace state. This state // contains the information we need to reconnect to the session, such as @@ -90,7 +90,7 @@ export class JupyterAdapterApiImpl implements JupyterAdapterApi { // happens later when the session is started. adapter.restoreSession(serialized.sessionState); - return adapter; + return Promise.resolve(adapter); } /** diff --git a/extensions/jupyter-adapter/src/jupyter-adapter.d.ts b/extensions/jupyter-adapter/src/jupyter-adapter.d.ts index 4ee76b2c9d2..724f5b4f95d 100644 --- a/extensions/jupyter-adapter/src/jupyter-adapter.d.ts +++ b/extensions/jupyter-adapter/src/jupyter-adapter.d.ts @@ -147,7 +147,7 @@ export interface JupyterAdapterApi extends vscode.Disposable { kernel: JupyterKernelSpec, dynState: positron.LanguageRuntimeDynState, extra?: JupyterKernelExtra | undefined, - ): JupyterLanguageRuntimeSession; + ): Promise; /** * Restore a session for a Jupyter-compatible kernel. @@ -161,7 +161,7 @@ export interface JupyterAdapterApi extends vscode.Disposable { restoreSession( runtimeMetadata: positron.LanguageRuntimeMetadata, sessionMetadata: positron.RuntimeSessionMetadata - ): JupyterLanguageRuntimeSession; + ): Promise; /** * Finds an available TCP port for a server diff --git a/extensions/kallichore-adapter/extension.webpack.config.js b/extensions/kallichore-adapter/extension.webpack.config.js new file mode 100644 index 00000000000..fb58fb348b1 --- /dev/null +++ b/extensions/kallichore-adapter/extension.webpack.config.js @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const path = require('path'); +const withDefaults = require('../shared.webpack.config'); + +module.exports = withDefaults({ + context: __dirname, + entry: { + extension: './src/extension.ts', + }, + node: { + __dirname: false + }, + externals: { + 'bufferutil': 'commonjs bufferutil', + 'utf-8-validate': 'commonjs utf-8-validate' + } +}); diff --git a/extensions/kallichore-adapter/package-lock.json b/extensions/kallichore-adapter/package-lock.json new file mode 100644 index 00000000000..ad5508100f0 --- /dev/null +++ b/extensions/kallichore-adapter/package-lock.json @@ -0,0 +1,1022 @@ +{ + "name": "kallichore-adapter", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "kallichore-adapter", + "version": "0.0.1", + "dependencies": { + "request": "^2.88.2" + }, + "devDependencies": { + "@types/decompress": "^4.2.7", + "decompress": "^4.2.1" + }, + "engines": { + "vscode": "^1.61.0" + } + }, + "node_modules/@types/decompress": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.7.tgz", + "integrity": "sha512-9z+8yjKr5Wn73Pt17/ldnmQToaFHZxK0N1GHysuk/JIPT8RIdQeoInM01wWPgypRcvb6VH1drjuFpQ4zmY437g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.5.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz", + "integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/bl/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/decompress": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", + "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2/node_modules/file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-unzip/node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "node_modules/get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/make-dir/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "engines": { + "node": "*" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/seek-bzip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^2.8.1" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-natural-number": "^4.0.1" + } + }, + "node_modules/tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/extensions/kallichore-adapter/package.json b/extensions/kallichore-adapter/package.json new file mode 100644 index 00000000000..214dc1c2a7d --- /dev/null +++ b/extensions/kallichore-adapter/package.json @@ -0,0 +1,95 @@ +{ + "name": "kallichore-adapter", + "displayName": "%displayName%", + "description": "%description%", + "publisher": "vscode", + "version": "0.0.1", + "engines": { + "vscode": "^1.61.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "onStartupFinished" + ], + "main": "./out/extension.js", + "scripts": { + "compile": "gulp compile-extension:kallichore-adapter", + "install-kallichore": "ts-node scripts/install-kallichore-server.ts", + "lint": "eslint src --ext ts", + "pretest": "yarn run compile && yarn run lint", + "postinstall": "ts-node scripts/post-install.ts", + "test": "node ./out/test/runTest.js", + "vscode:prepublish": "yarn run compile", + "watch": "gulp watch-extension:kallichore-adapter" + }, + "contributes": { + "configuration": { + "type": "object", + "title": "Kallichore Configuration", + "properties": { + "kallichoreSupervisor.enable": { + "type": "boolean", + "default": false, + "description": "%configuration.enable.description%" + }, + "kallichoreSupervisor.logLevel": { + "scope": "window", + "type": "string", + "enum": [ + "error", + "warn", + "info", + "debug", + "trace" + ], + "enumDescriptions": [ + "%configuration.logLevel.error.description%", + "%configuration.logLevel.warn.description%", + "%configuration.logLevel.info.description%", + "%configuration.logLevel.debug.description%", + "%configuration.logLevel.trace.description%" + ], + "default": "debug", + "description": "%configuration.logLevel.description%" + }, + "kallichoreSupervisor.attachOnStartup": { + "scope": "window", + "type": "boolean", + "default": false, + "description": "%configuration.attachOnStartup.description%" + }, + "kallichoreSupervisor.sleepOnStartup": { + "scope": "window", + "type": "number", + "description": "%configuration.sleepOnStartup.description%" + } + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/posit-dev/positron" + }, + "devDependencies": { + "@types/decompress": "^4.2.7", + "@types/request": "^2.48.12", + "@types/mocha": "^9.1.0", + "@types/tail": "^2.2.3", + "@types/ws": "^8.5.12", + "mocha": "^9.2.1", + "ts-node": "^10.9.1", + "decompress": "^4.2.1" + }, + "positron": { + "binaryDependencies": { + "kallichore": "0.1.8" + } + }, + "dependencies": { + "request": "^2.88.2", + "tail": "^2.2.6", + "ws": "^8.18.0" + } +} diff --git a/extensions/kallichore-adapter/package.nls.json b/extensions/kallichore-adapter/package.nls.json new file mode 100644 index 00000000000..197672e8318 --- /dev/null +++ b/extensions/kallichore-adapter/package.nls.json @@ -0,0 +1,13 @@ +{ + "displayName": "Positron Kallichore Adapter", + "description": "Runs Jupyter Kernels in the Kallichore Jupyter Kernel Supervisor", + "configuration.logLevel.error.description": "Errors only", + "configuration.logLevel.warn.description": "Errors and warnings", + "configuration.logLevel.info.description": "Informational messages", + "configuration.logLevel.debug.description": "Debug messages", + "configuration.logLevel.trace.description": "Verbose tracing messages", + "configuration.logLevel.description": "Log level for the kernel supervisor (restart Positron to apply)", + "configuration.enable.description": "Run Jupyter kernels under the Kallichore supervisor", + "configuration.attachOnStartup.description": "Run before starting up Jupyter kernel (when supported)", + "configuration.sleepOnStartup.description": "Sleep for n seconds before starting up Jupyter kernel (when supported)" +} diff --git a/extensions/kallichore-adapter/resources/.gitignore b/extensions/kallichore-adapter/resources/.gitignore new file mode 100644 index 00000000000..effc9b41658 --- /dev/null +++ b/extensions/kallichore-adapter/resources/.gitignore @@ -0,0 +1,2 @@ +# Ignore Kallichore binaries +kallichore/ diff --git a/extensions/kallichore-adapter/scripts/install-kallichore-server.ts b/extensions/kallichore-adapter/scripts/install-kallichore-server.ts new file mode 100644 index 00000000000..8ee10c18160 --- /dev/null +++ b/extensions/kallichore-adapter/scripts/install-kallichore-server.ts @@ -0,0 +1,401 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * This script is a forked copy of the `install-kernel` script from the + * positron-r extension; it is responsible for downloading a built copy of the + * Kallichore server and/or using a locally built version. + * + * In the future, we could consider some way to share this script between the + * two extensions (note that some URLs, paths, and messages are different) or + * provide a shared library for downloading and installing binaries from Github + * releases. + */ + +import decompress from 'decompress'; +import * as fs from 'fs'; +import { IncomingMessage } from 'http'; +import * as https from 'https'; +import { platform, arch } from 'os'; +import * as path from 'path'; +import { promisify } from 'util'; + + +// Promisify some filesystem functions. +const readFileAsync = promisify(fs.readFile); +const writeFileAsync = promisify(fs.writeFile); +const existsAsync = promisify(fs.exists); + +// Create a promisified version of https.get. We can't use the built-in promisify +// because the callback doesn't follow the promise convention of (error, result). +const httpsGetAsync = (opts: https.RequestOptions) => { + return new Promise((resolve, reject) => { + const req = https.get(opts, resolve); + req.once('error', reject); + }); +}; + +/** + * Gets the version of the Kallichore server specified in package.json. + * + * @returns The version of Kallichore specified in package.json, or null if it cannot be determined. + */ +async function getVersionFromPackageJson(): Promise { + try { + const packageJson = JSON.parse(await readFileAsync('package.json', 'utf-8')); + return packageJson.positron.binaryDependencies?.kallichore || null; + } catch (error) { + console.error('Error reading package.json: ', error); + return null; + } +} + +/** + * Gets the version of Kallichore installed locally by reading a `VERSION` file that's written + * by this `install-kallichore-server` script. + * + * @returns The version of Kallichore installed locally, or null if it is not installed. + */ +async function getLocalKallichoreVersion(): Promise { + const versionFile = path.join('resources', 'kallichore', 'VERSION'); + try { + const kallichoreExists = await existsAsync(versionFile); + if (!kallichoreExists) { + return null; + } + return readFileAsync(versionFile, 'utf-8'); + } catch (error) { + console.error('Error determining Kallichore version: ', error); + return null; + } +} + +/** + * Helper to execute a command and return the stdout and stderr. + * + * @param command The command to execute. + * @param stdin Optional stdin to pass to the command. + * @returns A promise that resolves with the stdout and stderr of the command. + */ +async function executeCommand(command: string, stdin?: string): + Promise<{ stdout: string; stderr: string }> { + const { exec } = require('child_process'); + return new Promise((resolve, reject) => { + const process = exec(command, (error: any, stdout: string, stderr: string) => { + if (error) { + reject(error); + } else { + resolve({ stdout, stderr }); + } + }); + if (stdin) { + process.stdin.write(stdin); + process.stdin.end(); + } + }); +} + +/** + * Downloads the specified version of Kallichore and replaces the local binary. + * + * @param version The version of Kallichore to download. + * @param githubPat A Github Personal Access Token with the appropriate rights + * to download the release. + * @param gitCredential Whether the PAT originated from the `git credential` command. + */ +async function downloadAndReplaceKallichore(version: string, + githubPat: string, + gitCredential: boolean): Promise { + + try { + const headers: Record = { + 'Accept': 'application/vnd.github.v3.raw', // eslint-disable-line + 'User-Agent': 'positron-kallichore-downloader' // eslint-disable-line + }; + // If we have a githubPat, set it for better rate limiting. + if (githubPat) { + headers.Authorization = `token ${githubPat}`; + } + const requestOptions: https.RequestOptions = { + headers, + method: 'GET', + protocol: 'https:', + hostname: 'api.github.com', + path: `/repos/posit-dev/kallichore/releases` + }; + + const response = await httpsGetAsync(requestOptions as any) as any; + + // Special handling for PATs originating from `git credential`. + if (gitCredential && response.statusCode === 200) { + // If the PAT hasn't been approved yet, do so now. This stores the credential in + // the system credential store (or whatever `git credential` uses on the system). + // Without this step, the user will be prompted for a username and password the + // next time they try to download Kallichore. + const { stdout, stderr } = + await executeCommand('git credential approve', + `protocol=https\n` + + `host=github.com\n` + + `path=/repos/posit-dev/kallichore/releases\n` + + `username=\n` + + `password=${githubPat}\n`); + console.log(stdout); + if (stderr) { + console.warn(`Unable to approve PAT. You may be prompted for a username and ` + + `password the next time you download Kallichore.`); + console.error(stderr); + } + } else if (gitCredential && response.statusCode > 400 && response.statusCode < 500) { + // This handles the case wherein we got an invalid PAT from `git credential`. In this + // case we need to clean up the PAT from the credential store, so that we don't + // continue to use it. + const { stdout, stderr } = + await executeCommand('git credential reject', + `protocol=https\n` + + `host=github.com\n` + + `path=/repos/posit-dev/kallichore/releases\n` + + `username=\n` + + `password=${githubPat}\n`); + console.log(stdout); + if (stderr) { + console.error(stderr); + throw new Error(`The stored PAT returned by 'git credential' is invalid, but\n` + + `could not be removed. Please manually remove the PAT from 'git credential'\n` + + `for the host 'github.com'`); + } + throw new Error(`The PAT returned by 'git credential' is invalid. Kallichore cannot be\n` + + `downloaded.\n\n` + + `Check to be sure that your Personal Access Token:\n` + + '- Has the `repo` scope\n' + + '- Is not expired\n' + + '- Has been authorized for the "posit-dev" organization on Github (Configure SSO)\n'); + } + + let responseBody = ''; + + response.on('data', (chunk: any) => { + responseBody += chunk; + }); + + response.on('end', async () => { + if (response.statusCode !== 200) { + throw new Error(`Failed to download Kallichore: HTTP ${response.statusCode}\n\n` + + `${responseBody}`); + } + const releases = JSON.parse(responseBody); + if (!Array.isArray(releases)) { + throw new Error(`Unexpected response from Github:\n\n` + + `${responseBody}`); + } + const release = releases.find((asset: any) => asset.tag_name === version); + if (!release) { + console.error(`Could not find Kallichore ${version} in the releases.`); + return; + } + + let os: string; + switch (platform()) { + case 'win32': os = 'windows-x64'; break; + case 'darwin': os = 'darwin-universal'; break; + case 'linux': os = (arch() === 'arm64' ? 'linux-arm64' : 'linux-x64'); break; + default: { + console.error(`Unsupported platform ${platform()}.`); + return; + } + } + + const assetName = `kallichore-${version}-${os}.zip`; + const asset = release.assets.find((asset: any) => asset.name === assetName); + if (!asset) { + console.error(`Could not find Kallichore with asset name ${assetName} in the release.`); + return; + } + console.log(`Downloading Kallichore ${version} from ${asset.url}...`); + const url = new URL(asset.url); + // Reset the Accept header to download the asset. + headers.Accept = 'application/octet-stream'; + const requestOptions: https.RequestOptions = { + headers, + method: 'GET', + protocol: url.protocol, + hostname: url.hostname, + path: url.pathname + }; + + let dlResponse = await httpsGetAsync(requestOptions) as any; + while (dlResponse.statusCode === 302) { + // Follow redirects. + dlResponse = await httpsGetAsync(dlResponse.headers.location) as any; + } + let binaryData = Buffer.alloc(0); + + dlResponse.on('data', (chunk: any) => { + binaryData = Buffer.concat([binaryData, chunk]); + }); + dlResponse.on('end', async () => { + const kallichoreDir = path.join('resources', 'kallichore'); + + // Create the resources/kallichore directory if it doesn't exist. + if (!await existsAsync(kallichoreDir)) { + await fs.promises.mkdir(kallichoreDir); + } + + console.log(`Successfully downloaded Kallichore ${version} (${binaryData.length} bytes).`); + const zipFileDest = path.join(kallichoreDir, 'kallichore.zip'); + await writeFileAsync(zipFileDest, binaryData); + + await decompress(zipFileDest, kallichoreDir).then(_files => { + console.log(`Successfully unzipped Kallichore ${version}.`); + }); + + // Clean up the zipfile. + await fs.promises.unlink(zipFileDest); + + // Write a VERSION file with the version number. + await writeFileAsync(path.join('resources', 'kallichore', 'VERSION'), version); + + }); + }); + } catch (error) { + console.error('Error downloading Kallichore:', error); + } +} + +async function main() { + const serverName = platform() === 'win32' ? 'kcserver.exe' : 'kcserver'; + + // Before we do any work, check to see if there is a locally built copy of + // Kallichore in the `kallichore / target` directory. If so, we'll assume + // that the user is a Kallichore developer and skip the download; this + // version will take precedence over any downloaded version. + const positronParent = path.dirname(path.dirname(path.dirname(path.dirname(__dirname)))); + const kallichoreFolder = path.join(positronParent, 'kallichore'); + const targetFolder = path.join(kallichoreFolder, 'target'); + const debugBinary = path.join(targetFolder, 'debug', serverName); + const releaseBinary = path.join(targetFolder, 'release', serverName); + if (fs.existsSync(debugBinary) || fs.existsSync(releaseBinary)) { + const binary = fs.existsSync(debugBinary) ? debugBinary : releaseBinary; + console.log(`Using locally built Kallichore in ${binary}.`); + + // Copy the locally built Kallichore to the resources/kallichore + // directory. It won't be read from this directory at runtime, but we + // need to put it here so that `yarn gulp vscode` will package it up + // (the packaging step doesn't look for a sideloaded Kallichore from an + // adjacent `Kallichore` directory). + fs.mkdirSync(path.join('resources', 'kallichore'), { recursive: true }); + fs.copyFileSync(binary, path.join('resources', 'kallichore', serverName)); + return; + } else { + console.log(`No locally built Kallichore found in ${path.join(positronParent, 'kallichore')}; ` + + `checking downloaded version.`); + } + + const packageJsonVersion = await getVersionFromPackageJson(); + const localKallichoreVersion = await getLocalKallichoreVersion(); + + if (!packageJsonVersion) { + console.error('Could not determine Kallichore version from package.json.'); + return; + } + + console.log(`package.json version: ${packageJsonVersion} `); + console.log(`Downloaded Kallichore version: ${localKallichoreVersion ? localKallichoreVersion : 'Not found'} `); + + if (packageJsonVersion === localKallichoreVersion) { + console.log('Versions match. No action required.'); + return; + } + + // We need a Github Personal Access Token (PAT) to download Kallichore. Because this is sensitive + // information, there are a lot of ways to set it. We try the following in order: + + // (1) The GITHUB_PAT environment variable. + // (2) The POSITRON_GITHUB_PAT environment variable. + // (3) The git config setting 'credential.https://api.github.com.token'. + // (4) The git credential store. + + // (1) Get the GITHUB_PAT from the environment. + let githubPat = process.env.GITHUB_PAT; + let gitCredential = false; + if (githubPat) { + console.log('Using Github PAT from environment variable GITHUB_PAT.'); + } else { + // (2) Try POSITRON_GITHUB_PAT (it's what the build script sets) + githubPat = process.env.POSITRON_GITHUB_PAT; + if (githubPat) { + console.log('Using Github PAT from environment variable POSITRON_GITHUB_PAT.'); + } + } + + // (3) If no GITHUB_PAT is set, try to get it from git config. This provides a + // convenient non-interactive way to set the PAT. + if (!githubPat) { + try { + const { stdout, stderr } = + await executeCommand('git config --get credential.https://api.github.com.token'); + githubPat = stdout.trim(); + if (githubPat) { + console.log(`Using Github PAT from git config setting ` + + `'credential.https://api.github.com.token'.`); + } else { + console.error(stderr); + } + } catch (error) { + // We don't care if this fails; we'll try `git credential` next. + } + } + + // (4) If no GITHUB_PAT is set, try to get it from git credential. + if (!githubPat) { + // Explain to the user what's about to happen. + console.log(`Attempting to retrieve a Github Personal Access Token from git in order\n` + + `to download Kallichore ${packageJsonVersion}. If you are prompted for a username and\n` + + `password, enter your Github username and a Personal Access Token with the\n` + + `'repo' scope. You can read about how to create a Personal Access Token here: \n` + + `\n` + + `https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens\n` + + `\n` + + `If you don't want to set up a Personal Access Token now, just press Enter twice to set \n` + + `a blank value for the password. Kallichore will not be downloaded. \n` + + `\n` + + `You can set a PAT later by running yarn again and supplying the PAT at this prompt,\n` + + `or by running 'git config credential.https://api.github.com.token YOUR_GITHUB_PAT'\n`); + const { stdout, stderr } = + await executeCommand('git credential fill', + `protocol=https\n` + + `host=github.com\n` + + `path=/repos/posit-dev/kallichore/releases\n`); + + gitCredential = true; + // Extract the `password = ` line from the output. + const passwordLine = stdout.split('\n').find( + (line: string) => line.startsWith('password=')); + if (passwordLine) { + githubPat = passwordLine.split('=')[1]; + console.log(`Using Github PAT returned from 'git credential'.`); + } else { + console.error(stderr); + } + } + + if (!githubPat) { + console.log(`No Github PAT was found. Unable to download Kallichore ${packageJsonVersion}.`); + return; + } + + await downloadAndReplaceKallichore(packageJsonVersion, githubPat, gitCredential); +} + +// Disable downloading if running inside a Github action on the public +// posit-dev/positron repository, which doesn't currently have access to the +// private Kallichore repository. +if (process.env.GITHUB_ACTIONS && process.env.GITHUB_REPOSITORY === 'posit-dev/positron') { + console.log('Skipping Kallichore download on public repository.'); +} else { + + main().catch((error) => { + console.error('An error occurred:', error); + }); +} diff --git a/extensions/kallichore-adapter/scripts/post-install.ts b/extensions/kallichore-adapter/scripts/post-install.ts new file mode 100644 index 00000000000..ef6ab277287 --- /dev/null +++ b/extensions/kallichore-adapter/scripts/post-install.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { execSync } from 'child_process'; + +// Install or update the Kallichore server binary +execSync('yarn run install-kallichore', { + stdio: 'inherit' +}); diff --git a/extensions/kallichore-adapter/scripts/regen-api.sh b/extensions/kallichore-adapter/scripts/regen-api.sh new file mode 100755 index 00000000000..f540c14df14 --- /dev/null +++ b/extensions/kallichore-adapter/scripts/regen-api.sh @@ -0,0 +1,38 @@ +# ------------------------------------------------------------ +# Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. +# Licensed under the Elastic License 2.0. See LICENSE.txt for license information. +# ------------------------------------------------------------ + +# This script regenerates the Kallichore API client in the Kallichore adapter, +# using an updated version of the Kallichore API definition. +# +# It presumes that Kallichore and Positron are both checked out in the same +# parent directory. + + +# Ensure that the openapi-generator-cli is installed +if ! command -v openapi-generator &> /dev/null +then + echo "openapi-generator-cli could not be found. Please install it with 'npm install @openapitools/openapi-generator-cli -g'" + exit +fi + +# Find the directory of this script +SCRIPTDIR=$(cd "$(dirname -- "${BASH_SOURCE[0]}")"; pwd -P) + +# Ensure that kallichore.json is where we expect it to be; it should be in a sibling directory of Positron +KALLICHORE_JSON_PATH=$(realpath "${SCRIPTDIR}/../../../../kallichore/kallichore.json") + +if [ ! -f "${KALLICHORE_JSON_PATH}" ]; then + echo "kallichore.json API definition not found" + exit +fi + +# Enter the directory of the Kallichore client source code and generate the API client +pushd "${SCRIPTDIR}/../src/kcclient" + +# Generate the API client +openapi-generator generate -i ~/git/kallichore/kallichore.json -g typescript-node + +# Return to the original directory +popd diff --git a/extensions/kallichore-adapter/src/Comm.ts b/extensions/kallichore-adapter/src/Comm.ts new file mode 100644 index 00000000000..ec61e81a94c --- /dev/null +++ b/extensions/kallichore-adapter/src/Comm.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Simple representation of a comm (communications channel) between the client + * and the kernel + */ +export class Comm { + /** + * Create a new comm representation + * + * @param id The unique ID of the comm instance @param target The comm + * @param target The comm's target name (also known as its type); can be any + * string. Positron-specific comms are listed in its `RuntimeClientType` + * enum. + */ + constructor( + public readonly id: string, + public readonly target: string) { + } +} diff --git a/extensions/kallichore-adapter/src/DapClient.ts b/extensions/kallichore-adapter/src/DapClient.ts new file mode 100644 index 00000000000..4a3bbdfa028 --- /dev/null +++ b/extensions/kallichore-adapter/src/DapClient.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as positron from 'positron'; +import { JupyterLanguageRuntimeSession } from './jupyter-adapter'; + +/** + * A Debug Adapter Protocol (DAP) client instance; handles messages from the + * kernel side of the DAP and forwards them to the debug adapter. + */ +export class DapClient { + /** Message counter; used for creating unique message IDs */ + private static _counter = 0; + + private _msgStem: string; + + constructor(readonly clientId: string, + readonly serverPort: number, + readonly debugType: string, + readonly debugName: string, + readonly session: JupyterLanguageRuntimeSession) { + + // Generate 8 random hex characters for the message stem + this._msgStem = Math.random().toString(16).slice(2, 10); + } + + handleDapMessage(msg: any) { + switch (msg.msg_type) { + // The runtime is in control of when to start a debug session. + // When this happens, we attach automatically to the runtime + // with a synthetic configuration. + case 'start_debug': { + this.session.emitJupyterLog(`Starting debug session for DAP server ${this.clientId}`); + const config = { + type: this.debugType, + name: this.debugName, + request: 'attach', + debugServer: this.serverPort, + internalConsoleOptions: 'neverOpen', + } as vscode.DebugConfiguration; + vscode.debug.startDebugging(undefined, config); + break; + } + + // If the DAP has commands to execute, such as "n", "f", or "Q", + // it sends events to let us do it from here. + case 'execute': { + this.session.execute( + msg.content.command, + this._msgStem + '-dap-' + DapClient._counter++, + positron.RuntimeCodeExecutionMode.Interactive, + positron.RuntimeErrorBehavior.Stop + ); + break; + } + + // We use the restart button as a shortcut for restarting the runtime + case 'restart': { + this.session.restart(); + break; + } + + default: { + this.session.emitJupyterLog(`Unknown DAP command: ${msg.msg_type}`); + break; + } + } + } +} diff --git a/extensions/kallichore-adapter/src/KallichoreAdapterApi.ts b/extensions/kallichore-adapter/src/KallichoreAdapterApi.ts new file mode 100644 index 00000000000..d6d0c8a7e81 --- /dev/null +++ b/extensions/kallichore-adapter/src/KallichoreAdapterApi.ts @@ -0,0 +1,439 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as positron from 'positron'; +import * as os from 'os'; +import * as path from 'path'; +import * as fs from 'fs'; +import { DefaultApi, HttpBearerAuth, HttpError, ServerStatus, Status } from './kcclient/api'; +import { findAvailablePort } from './PortFinder'; +import { KallichoreAdapterApi } from './kallichore-adapter'; +import { JupyterKernelExtra, JupyterKernelSpec, JupyterLanguageRuntimeSession } from './jupyter-adapter'; +import { KallichoreSession } from './KallichoreSession'; +import { Barrier, PromiseHandles } from './async'; + +const KALLICHORE_STATE_KEY = 'kallichore-adapter.v2'; + +/** + * The persisted state of the Kallichore server. This metadata is saved in + * workspace state storage and used to re-establish a connection to the server + * when the extension (or Positron) is reloaded. + */ +interface KallichoreServerState { + /** The port the server is listening on, e.g. 8182 */ + port: number; + + /** The full base path of the API, e.g. http://127.0.0.1:8182/ */ + base_path: string; + + /** The path to the server binary, e.g. /usr/lib/bin/kcserver. */ + server_path: string; + + /** The PID of the server process */ + server_pid: number; + + /** The bearer token used to authenticate with the server */ + bearer_token: string; +} + +export class KCApi implements KallichoreAdapterApi { + + /** The instance of the API; the API is code-generated from the Kallichore + * OpenAPI spec */ + private readonly _api: DefaultApi; + + /** A barrier that opens when the Kallichore server has successfully started; + * used to hold operations until we're online */ + private readonly _started: Barrier = new Barrier(); + + /** + * If we're currently starting, this is the promise that resolves when the + * server is online. + */ + private _starting: PromiseHandles | undefined; + + /** The currently active sessions (only the ones used by this client; does + * not track the full set of sessions on the Kallichore server) */ + private readonly _sessions: Array = []; + + /** + * Create a new Kallichore API object. + * + * @param _context The extension context + * @param _log A log output channel for the extension + */ + constructor( + private readonly _context: vscode.ExtensionContext, + private readonly _log: vscode.LogOutputChannel) { + + this._api = new DefaultApi(); + + // If the Kallichore server is enabled in the configuration, start it + // eagerly so it's warm when we start trying to create or restore sessions. + if (vscode.workspace.getConfiguration('kallichoreSupervisor').get('enabled')) { + this.ensureStarted().catch((err) => { + this._log.error(`Failed to start Kallichore server: ${err}`); + }); + } + } + + /** + * Ensures that the server has been started. If the server is already + * started, this is a no-op. If the server is starting, this waits for the + * server to start. If the server is not started, this starts the server. + * + * @returns A promise that resolves when the Kallichore server is online. + */ + async ensureStarted(): Promise { + + // If the server is already started, we're done + if (this._started.isOpen()) { + return; + } + + // If we're currently starting, just wait for that to finish + if (this._starting) { + return this._starting.promise; + } + + // Create a new starting promise and start the server + this._starting = new PromiseHandles(); + this.start().then(() => { + this._starting?.resolve(); + this._starting = undefined; + }).catch((err) => { + this._starting?.reject(err); + this._starting = undefined; + }); + return this._starting.promise; + } + + /** + * Starts a new Kallichore server. If a server is already running, it will + * attempt to reconnect to it. Returns a promise that resolves when the + * server is online. + * + * @throws An error if the server cannot be started or reconnected to. + */ + async start() { + // Check to see if there's a server already running for this workspace + const serverState = + this._context.workspaceState.get(KALLICHORE_STATE_KEY); + + // If there is, and we can reconnect to it, do so + if (serverState) { + try { + if (await this.reconnect(serverState)) { + // Successfully reconnected + return; + } else { + // Did not reconnect; start a new server. This isn't + // necessarily an error condition since we always try to + // reconnect to the server saved in the state, and it's + // normal for it to have exited if this is a new Positron + // session. + this._log.info(`Could not reconnect to Kallichore server ` + + `at ${serverState.base_path}. Starting a new server`); + } + } catch (err) { + this._log.error(`Failed to reconnect to Kallichore server ` + + ` at ${serverState.base_path}: ${err}. Starting a new server.`); + } + } + + // Get the path to the Kallichore server binary. This will throw an + // error if the server binary cannot be found. + const shellPath = this.getKallichorePath(); + + + // Get the log level from the configuration + const config = vscode.workspace.getConfiguration('kallichoreSupervisor'); + const logLevel = config.get('logLevel') ?? 'warn'; + + // Export the Positron version as an environment variable + const env = { + 'POSITRON': '1', + 'POSITRON_VERSION': positron.version, + 'RUST_LOG': logLevel, + 'POSITRON_LONG_VERSION': `${positron.version}+${positron.buildNumber}`, + 'POSITRON_MODE': vscode.env.uiKind === vscode.UIKind.Desktop ? 'desktop' : 'server', + }; + + // Create a 16 hex digit UUID for the bearer token + const bearerToken = Math.floor(Math.random() * 0x100000000).toString(16); + + // Write it to a temporary file. Kallichore will delete it after reading + // the secret. + const tokenPath = path.join(os.tmpdir(), `kallichore-${bearerToken}.token`); + fs.writeFileSync(tokenPath, bearerToken, 'utf8'); + + // Change the permissions on the file so only the current user can read it + fs.chmodSync(tokenPath, 0o600); + + // Create a bearer auth object with the token + const bearer = new HttpBearerAuth(); + bearer.accessToken = bearerToken; + + // Find a port for the server to listen on + const port = await findAvailablePort([], 10); + + // Start a timer so we can track server startup time + const startTime = Date.now(); + + // Start the server in a new terminal + this._log.info(`Starting Kallichore server ${shellPath} on port ${port}`); + const terminal = vscode.window.createTerminal({ + name: 'Kallichore', + shellPath: shellPath, + shellArgs: ['--port', port.toString(), '--token', tokenPath], + env, + message: `*** Kallichore Server (${shellPath}) ***`, + hideFromUser: false, + isTransient: false + }); + + // Wait for the terminal to start and get the PID + await terminal.processId; + + // Establish the API + this._api.basePath = `http://localhost:${port}`; + this._api.setDefaultAuthentication(bearer); + + // List the sessions to verify that the server is up. The process is + // alive for a few milliseconds before the HTTP server is ready, so we + // may need to retry a few times. + for (let retry = 0; retry < 40; retry++) { + try { + const status = await this._api.serverStatus(); + this._log.info(`Kallichore ${status.body.version} server online with ${status.body.sessions} sessions`); + break; + } catch (err) { + // ECONNREFUSED is a normal condition; the server isn't ready + // yet. Keep trying until we hit the retry limit, about 2 + // seconds from the time we got a process ID established. + if (err.code === 'ECONNREFUSED') { + if (retry < 19) { + // Wait a bit and try again + await new Promise((resolve) => setTimeout(resolve, 50)); + continue; + } else { + // Give up; it shouldn't take this long to start + this._log.error(`Kallichore server did not start after ${Date.now() - startTime}ms`); + throw new Error(`Kallichore server did not start after ${Date.now() - startTime}ms`); + } + } + this._log.error(`Failed to get session list from Kallichore; ` + + `server may not be running or may not be ready. Check the terminal for errors. ` + + `Error: ${JSON.stringify(err)}`); + throw err; + } + } + + // Open the started barrier and save the server state since we're online + this._log.debug(`Kallichore server started in ${Date.now() - startTime}ms`); + this._started.open(); + const state: KallichoreServerState = { + base_path: this._api.basePath, + port, + server_path: shellPath, + server_pid: await terminal.processId || 0, + bearer_token: bearerToken + }; + this._context.workspaceState.update(KALLICHORE_STATE_KEY, state); + } + + /** + * Attempt to reconnect to a Kallichore server that was previously running. + * + * @param serverState The state of the server to reconnect to. + * @returns True if the server was successfully reconnected, false if the + * server was not running. + * @throws An error if the server was running but could not be reconnected. + */ + async reconnect(serverState: KallichoreServerState): Promise { + // Check to see if the pid is still running + const pid = serverState.server_pid; + this._log.info(`Reconnecting to Kallichore server at ${serverState.base_path} (PID ${pid})`); + if (pid) { + try { + process.kill(pid, 0); + } catch (err) { + this._log.warn(`Kallichore server PID ${pid} is not running`); + return false; + } + } + + // Re-establish the bearer token + const bearer = new HttpBearerAuth(); + bearer.accessToken = serverState.bearer_token; + this._api.setDefaultAuthentication(bearer); + + // Reconnect and get the session list + this._api.basePath = serverState.base_path; + const status = await this._api.serverStatus(); + this._started.open(); + this._log.info(`Kallichore ${status.body.version} server reconnected with ${status.body.sessions} sessions`); + return true; + } + + /** + * Create a new session for a Jupyter-compatible kernel. + * + * @param runtimeMetadata The metadata for the associated language runtime + * @param sessionMetadata The metadata for this specific kernel session + * @param kernel The Jupyter kernel spec for the kernel to be started + * @param dynState The kernel's initial dynamic state + * @param _extra Extra functionality for the kernel + * + * @returns A promise that resolves to the new session + * @throws An error if the session cannot be created + */ + async createSession( + runtimeMetadata: positron.LanguageRuntimeMetadata, + sessionMetadata: positron.RuntimeSessionMetadata, + kernel: JupyterKernelSpec, + dynState: positron.LanguageRuntimeDynState, + _extra?: JupyterKernelExtra | undefined): Promise { + + // Ensure the server is started before trying to create the session + await this.ensureStarted(); + + // Create the session object + const session = new KallichoreSession( + sessionMetadata, runtimeMetadata, dynState, this._api, true, _extra); + + this._log.info(`Creating session: ${JSON.stringify(sessionMetadata)}`); + + // Create the session on the server + await session.create(kernel); + + // Save the session now that it has been created on the server + this._sessions.push(session); + + return session; + } + + /** + * Restores (reconnects to) an already running session on the Kallichore + * server. + * + * @param runtimeMetadata The metadata for the associated language runtime + * @param sessionMetadata The metadata for the session to be restored + * @returns The restored session + */ + async restoreSession( + runtimeMetadata: positron.LanguageRuntimeMetadata, + sessionMetadata: positron.RuntimeSessionMetadata): Promise { + + // Ensure the server is started before trying to restore the session + await this.ensureStarted(); + + return new Promise((resolve, reject) => { + this._api.getSession(sessionMetadata.sessionId).then(async (response) => { + // Make sure the session is still running; it may have exited + // while we were disconnected. + const kcSession = response.body; + if (kcSession.status === Status.Exited) { + this._log.error(`Attempt to reconnect to session ${sessionMetadata.sessionId} failed because it is no longer running`); + reject(`Session ${sessionMetadata.sessionName} (${sessionMetadata.sessionId}) is no longer running`); + return; + } + + // Create the session object + const session = new KallichoreSession(sessionMetadata, runtimeMetadata, { + continuationPrompt: kcSession.continuationPrompt, + inputPrompt: kcSession.inputPrompt, + }, this._api, false); + + // Restore the session from the server + try { + session.restore(kcSession); + } catch (err) { + this._log.error(`Failed to restore session ${sessionMetadata.sessionId}: ${JSON.stringify(err)}`); + reject(err); + } + // Save the session + this._sessions.push(session); + resolve(session); + }).catch((err) => { + if (err instanceof HttpError) { + this._log.error(`Failed to reconnect to session ${sessionMetadata.sessionId}: ${err.body.message}`); + reject(err.body.message); + } else { + this._log.error(`Failed to reconnect to session ${sessionMetadata.sessionId}: ${JSON.stringify(err)}`); + reject(err); + } + }); + }); + } + + /** + * Gets the status of the Kallichore server. + * + * @returns The server status. + */ + public async serverStatus(): Promise { + const status = await this._api.serverStatus(); + return status.body; + } + + /** + * Clean up the Kallichore server and all sessions. Note that this doesn't + * actually remove the sessions from the server; it just disconnects them + * from the API. + */ + dispose() { + // Dispose of each session + this._sessions.forEach(session => session.dispose()); + this._sessions.length = 0; + } + + findAvailablePort(excluding: Array, maxTries: number): Promise { + return findAvailablePort(excluding, maxTries); + } + + /** + * Attempts to locate a copy of the Kallichore server binary. + * + * @returns A path to the Kallichore server binary. + * @throws An error if the server binary cannot be found. + */ + getKallichorePath(): string { + + // Get the name of the server binary for the current platform + const serverBin = os.platform() === 'win32' ? 'kcserver.exe' : 'kcserver'; + + // Look for locally built Debug or Release server binaries. If both exist, we'll use + // whichever is newest. This is the location where the kernel is typically built + // by developers, who have `positron` and `kallichore` directories side-by-side. + let devBinary; + const positronParent = path.dirname(path.dirname(path.dirname(this._context.extensionPath))); + const devDebugBinary = path.join(positronParent, 'kallichore', 'target', 'debug', serverBin); + const devReleaseBinary = path.join(positronParent, 'kallichore', 'target', 'release', serverBin); + const debugModified = fs.statSync(devDebugBinary, { throwIfNoEntry: false })?.mtime; + const releaseModified = fs.statSync(devReleaseBinary, { throwIfNoEntry: false })?.mtime; + + if (debugModified) { + devBinary = (releaseModified && releaseModified > debugModified) ? devReleaseBinary : devDebugBinary; + } else if (releaseModified) { + devBinary = devReleaseBinary; + } + if (devBinary) { + this._log.info(`Loading Kallichore from disk in adjacent repository (${devBinary}). Make sure it's up-to-date.`); + return devBinary; + } + + // Now try the default (embedded) kernel. This is where the kernel is placed in + // development and release builds. + const embeddedBinary = path.join( + this._context.extensionPath, 'resources', 'kallichore', serverBin); + if (fs.existsSync(embeddedBinary)) { + return embeddedBinary; + } + + throw new Error(`Kallichore server not found (expected at ${embeddedBinary})`); + } +} diff --git a/extensions/kallichore-adapter/src/KallichoreSession.ts b/extensions/kallichore-adapter/src/KallichoreSession.ts new file mode 100644 index 00000000000..7c01a7510c4 --- /dev/null +++ b/extensions/kallichore-adapter/src/KallichoreSession.ts @@ -0,0 +1,1202 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as positron from 'positron'; +import * as os from 'os'; +import * as path from 'path'; +import * as fs from 'fs'; +import { JupyterKernelExtra, JupyterKernelSpec, JupyterLanguageRuntimeSession } from './jupyter-adapter'; +import { ActiveSession, DefaultApi, HttpError, InterruptMode, NewSession, Status } from './kcclient/api'; +import { JupyterMessage } from './jupyter/JupyterMessage'; +import { JupyterRequest } from './jupyter/JupyterRequest'; +import { KernelInfoRequest } from './jupyter/KernelInfoRequest'; +import { Barrier, PromiseHandles, withTimeout } from './async'; +import { ExecuteRequest, JupyterExecuteRequest } from './jupyter/ExecuteRequest'; +import { IsCompleteRequest, JupyterIsCompleteRequest } from './jupyter/IsCompleteRequest'; +import { CommInfoRequest } from './jupyter/CommInfoRequest'; +import { JupyterCommOpen } from './jupyter/JupyterCommOpen'; +import { CommOpenCommand } from './jupyter/CommOpenCommand'; +import { JupyterCommand } from './jupyter/JupyterCommand'; +import { CommCloseCommand } from './jupyter/CommCloseCommand'; +import { JupyterCommMsg } from './jupyter/JupyterCommMsg'; +import { RuntimeMessageEmitter } from './RuntimeMessageEmitter'; +import { CommMsgCommand } from './jupyter/CommMsgCommand'; +import { ShutdownRequest } from './jupyter/ShutdownRequest'; +import { LogStreamer } from './LogStreamer'; +import { JupyterMessageHeader } from './jupyter/JupyterMessageHeader'; +import { JupyterChannel } from './jupyter/JupyterChannel'; +import { InputReplyCommand } from './jupyter/InputReplyCommand'; +import { RpcReplyCommand } from './jupyter/RpcReplyCommand'; +import { JupyterCommRequest } from './jupyter/JupyterCommRequest'; +import { Comm } from './Comm'; +import { CommMsgRequest } from './jupyter/CommMsgRequest'; +import { DapClient } from './DapClient'; +import { SocketSession } from './ws/SocketSession'; +import { KernelOutputMessage } from './ws/KernelMessage'; + +export class KallichoreSession implements JupyterLanguageRuntimeSession { + /** + * The runtime messages emitter; consumes Jupyter messages and translates + * them to Positron language runtime messages + */ + private readonly _messages: RuntimeMessageEmitter = new RuntimeMessageEmitter(); + + /** Emitter for runtime state changes */ + private readonly _state: vscode.EventEmitter; + + /** Emitter for runtime exit events */ + private readonly _exit: vscode.EventEmitter; + + /** Barrier: opens when the session has been established on Kallichore */ + private readonly _established: Barrier = new Barrier(); + + /** Barrier: opens when the WebSocket is connected and Jupyter messages can + * be sent and received */ + private _connected: Barrier = new Barrier(); + + /** Barrier: opens when the kernel has started up and has a heartbeat */ + private _ready: Barrier = new Barrier(); + + /** Cached exit reason; used to indicate an exit is expected so we can + * distinguish between expected and unexpected exits */ + private _exitReason: positron.RuntimeExitReason = positron.RuntimeExitReason.Unknown; + + /** The WebSocket connection to the Kallichore server for this session + */ + private _socket: SocketSession | undefined; + + /** The current runtime state of this session */ + private _runtimeState: positron.RuntimeState = positron.RuntimeState.Uninitialized; + + /** A map of pending RPCs, used to pair up requests and replies */ + private _pendingRequests: Map> = new Map(); + + /** Objects that should be disposed when the session is disposed */ + private _disposables: vscode.Disposable[] = []; + + /** Whether we are currently restarting the kernel */ + private _restarting = false; + + /** The Debug Adapter Protocol client, if any */ + private _dapClient: DapClient | undefined; + + /** A map of pending comm startups */ + private _startingComms: Map> = new Map(); + + /** + * The channel to which output for this specific kernel is logged, if any + */ + private readonly _kernelChannel: vscode.OutputChannel; + + /** + * The channel to which output for this specific console is logged + */ + private readonly _consoleChannel: vscode.LogOutputChannel; + + /** + * The channel to which profile output for this specific kernel is logged, if any + */ + private _profileChannel: vscode.OutputChannel | undefined; + + /** A map of active comm channels */ + private readonly _comms: Map = new Map(); + + /** The kernel's log file, if any. */ + private _kernelLogFile: string | undefined; + + /** + * The active session on the Kallichore server. Currently, this is only + * defined for sessions that have been restored after reload or navigation. + */ + private _activeSession: ActiveSession | undefined; + + /** + * The message header for the current requests if any is active. This is + * used for input requests (e.g. from `readline()` in R) Concurrent requests + * are not supported. + */ + private _activeBackendRequestHeader: JupyterMessageHeader | null = null; + + constructor(readonly metadata: positron.RuntimeSessionMetadata, + readonly runtimeMetadata: positron.LanguageRuntimeMetadata, + readonly dynState: positron.LanguageRuntimeDynState, + private readonly _api: DefaultApi, + private _new: boolean, + private readonly _extra?: JupyterKernelExtra | undefined) { + + // Create event emitters + this._state = new vscode.EventEmitter(); + this._exit = new vscode.EventEmitter(); + + this.onDidReceiveRuntimeMessage = this._messages.event; + + this.onDidChangeRuntimeState = this._state.event; + + this.onDidEndSession = this._exit.event; + + // Establish log channels for the console and kernel we're connecting to + this._consoleChannel = vscode.window.createOutputChannel( + metadata.notebookUri ? + `${runtimeMetadata.runtimeName}: Notebook: (${path.basename(metadata.notebookUri.path)})` : + `${runtimeMetadata.runtimeName}: Console`, + { log: true }); + + this._kernelChannel = positron.window.createRawLogOutputChannel( + `${runtimeMetadata.runtimeName}: Kernel`); + } + + /** + * Create the session in on the Kallichore server. + * + * @param kernelSpec The Jupyter kernel spec to use for the session + */ + public async create(kernelSpec: JupyterKernelSpec) { + if (!this._new) { + throw new Error(`Session ${this.metadata.sessionId} already exists`); + } + + // Forward the environment variables from the kernel spec + const env = {}; + if (kernelSpec.env) { + Object.assign(env, kernelSpec.env); + } + + // Prepare the working directory; use the workspace root if available, + // otherwise the home directory + let workingDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath || os.homedir(); + + // If we have a notebook URI, use its directory as the working directory + // instead + if (this.metadata.notebookUri?.fsPath) { + workingDir = this.metadata.notebookUri.fsPath; + } + + // Form the command-line arguments to the kernel process + const tempdir = os.tmpdir(); + const sep = path.sep; + const kerneldir = fs.mkdtempSync(`${tempdir}${sep}kernel-`); + const logFile = path.join(kerneldir, 'kernel.log'); + const profileFile = path.join(kerneldir, 'kernel-profile.log'); + const args = kernelSpec.argv.map((arg, _idx) => { + + // Replace {log_file} with the log file path. Not all kernels + // have this argument. + if (arg === '{log_file}') { + fs.writeFile(logFile, '', () => { + this.streamLogFile(logFile); + }); + return logFile; + } + + // Same as `log_file` but for profiling logs + if (profileFile && arg === '{profile_file}') { + fs.writeFile(profileFile, '', () => { + this.streamProfileFile(profileFile); + }); + return profileFile; + } + + return arg; + }) as Array; + + // Default to message-based interrupts + let interruptMode = InterruptMode.Message; + + // If the kernel spec specifies an interrupt mode, use it + if (kernelSpec.interrupt_mode) { + switch (kernelSpec.interrupt_mode) { + case 'signal': + interruptMode = InterruptMode.Signal; + break; + case 'message': + interruptMode = InterruptMode.Message; + break; + } + } + + // Initialize extra functionality, if any. These settings modify the + // argument list `args` in place, so need to happen right before we send + // the arg list to the server. + const config = vscode.workspace.getConfiguration('kallichoreSupervisor'); + const attachOnStartup = config.get('attachOnStartup', false) && this._extra?.attachOnStartup; + const sleepOnStartup = config.get('sleepOnStartup', undefined) && this._extra?.sleepOnStartup; + if (attachOnStartup) { + this._extra!.attachOnStartup!.init(args); + } + if (sleepOnStartup) { + const delay = config.get('sleepOnStartup', 0); + this._extra!.sleepOnStartup!.init(args, delay); + } + + // Create the session in the underlying API + const session: NewSession = { + argv: args, + sessionId: this.metadata.sessionId, + language: kernelSpec.language, + displayName: this.metadata.sessionName, + inputPrompt: '', + continuationPrompt: '', + env, + workingDirectory: workingDir, + username: os.userInfo().username, + interruptMode + }; + await this._api.newSession(session); + this.log(`Session created: ${JSON.stringify(session)}`, vscode.LogLevel.Info); + this._established.open(); + } + + /** + * Requests that the kernel start a Language Server Protocol server, and + * connect it to the client with the given TCP address. + * + * Note: This is only useful if the kernel hasn't already started an LSP + * server. + * + * @param clientAddress The client's TCP address, e.g. '127.0.0.1:1234' + */ + async startPositronLsp(clientAddress: string) { + // Create a unique client ID for this instance + const clientId = `positron-lsp-${this.runtimeMetadata.languageId}-${this.createUniqueId()}`; + this.log(`Starting LSP server ${clientId} for ${clientAddress}`, vscode.LogLevel.Info); + + // Notify Positron that we're handling messages from this client + this._disposables.push(positron.runtime.registerClientInstance(clientId)); + + // Ask the backend to create the client + await this.createClient( + clientId, + positron.RuntimeClientType.Lsp, + { client_address: clientAddress } + ); + + // Create a promise that will resolve when the LSP starts on the server + // side. + const startPromise = new PromiseHandles(); + this._startingComms.set(clientId, startPromise); + return startPromise.promise; + } + + /** + * Requests that the kernel start a Debug Adapter Protocol server, and + * connect it to the client locally on the given TCP port. + * + * @param serverPort The port on which to bind locally. + * @param debugType Passed as `vscode.DebugConfiguration.type`. + * @param debugName Passed as `vscode.DebugConfiguration.name`. + */ + async startPositronDap( + serverPort: number, + debugType: string, + debugName: string, + ) { + // NOTE: Ideally we'd connect to any address but the + // `debugServer` property passed in the configuration below + // needs to be a port for localhost. + const serverAddress = `127.0.0.1:${serverPort}`; + + // TODO: Should we query the kernel to see if it can create a DAP + // (QueryInterface style) instead of just demanding it? + // + // The Jupyter kernel spec does not provide a way to query for + // supported comms; the only way to know is to try to create one. + + // Create a unique client ID for this instance + const clientId = `positron-dap-${this.runtimeMetadata.languageId}-${this.createUniqueId()}`; + this.log(`Starting DAP server ${clientId} for ${serverAddress}`, vscode.LogLevel.Debug); + + // Notify Positron that we're handling messages from this client + this._disposables.push(positron.runtime.registerClientInstance(clientId)); + + await this.createClient( + clientId, + positron.RuntimeClientType.Dap, + { client_address: serverAddress } + ); + + // Create the DAP client message handler + this._dapClient = new DapClient(clientId, serverPort, debugType, debugName, this); + } + + /** + * Forwards a message to the Jupyter log output channel. + * + * @param message The message to log + * @param logLevel The log level of the message + */ + emitJupyterLog(message: string, logLevel?: vscode.LogLevel): void { + this.log(message, logLevel); + } + + /** + * Reveals the output channel for this kernel. + */ + showOutput(): void { + this._kernelChannel?.show(); + } + + /** + * Calls a method on the UI comm for this kernel. + * + * @param method The method's name + * @param args Additional arguments to pass to the method + * @returns The result of the method call + */ + callMethod(method: string, ...args: Array): Promise { + const promise = new PromiseHandles; + + // Find the UI comm + const uiComm = Array.from(this._comms.values()) + .find(c => c.target === positron.RuntimeClientType.Ui); + if (!uiComm) { + throw new Error(`Cannot invoke '${method}'; no UI comm is open.`); + } + + // Create the request. This uses a JSON-RPC 2.0 format, with an + // additional `msg_type` field to indicate that this is a request type + // for the UI comm. + // + // NOTE: Currently using nested RPC messages for convenience but + // we'd like to do better + const request = { + jsonrpc: '2.0', + method: 'call_method', + params: { + method, + params: args + }, + }; + + const commMsg: JupyterCommMsg = { + comm_id: uiComm.id, + data: request + }; + + const commRequest = new CommMsgRequest(this.createUniqueId(), commMsg); + this.sendRequest(commRequest).then((reply) => { + const response = reply.data; + + // If the response is an error, throw it + if (Object.keys(response).includes('error')) { + const error = response.error; + + // Populate the error object with the name of the error code + // for conformity with code that expects an Error object. + error.name = `RPC Error ${response.error.code}`; + + promise.reject(error); + } + + // JSON-RPC specifies that the return value must have either a 'result' + // or an 'error'; make sure we got a result before we pass it back. + if (!Object.keys(response).includes('result')) { + const error: positron.RuntimeMethodError = { + code: positron.RuntimeMethodErrorCode.InternalError, + message: `Invalid response from UI comm: no 'result' field. ` + + `(response = ${JSON.stringify(response)})`, + name: `InvalidResponseError`, + data: {}, + }; + + promise.reject(error); + } + + // Otherwise, return the result + promise.resolve(response.result); + }) + .catch((err) => { + this.log(`Failed to send UI comm request: ${JSON.stringify(err)}`, vscode.LogLevel.Error); + promise.reject(err); + }); + + return promise.promise; + } + + /** + * Gets the path to the kernel's log file, if any. + * + * @returns The kernel's log file. + * @throws An error if the log file is not available. + */ + getKernelLogFile(): string { + if (!this._kernelLogFile) { + throw new Error('Kernel log file not available'); + } + return this._kernelLogFile; + } + + onDidReceiveRuntimeMessage: vscode.Event; + + onDidChangeRuntimeState: vscode.Event; + + onDidEndSession: vscode.Event; + + /** + * Requests that the kernel execute a code fragment. + * + * @param code The code to execute + * @param id An ID for the code fragment; used to identify output and errors + * that come from this code fragment. + * @param mode The execution mode + * @param errorBehavior What to do if an error occurs + */ + execute(code: string, + id: string, + mode: positron.RuntimeCodeExecutionMode, + errorBehavior: positron.RuntimeErrorBehavior): void { + + // Translate the parameters into a Jupyter execute request + const request: JupyterExecuteRequest = { + code, + silent: mode === positron.RuntimeCodeExecutionMode.Silent, + store_history: mode === positron.RuntimeCodeExecutionMode.Interactive, + user_expressions: new Map(), + allow_stdin: true, + stop_on_error: errorBehavior === positron.RuntimeErrorBehavior.Stop, + }; + + // Create and send the execute request + const execute = new ExecuteRequest(id, request); + this.sendRequest(execute).then((reply) => { + this.log(`Execution result: ${JSON.stringify(reply)}`, vscode.LogLevel.Debug); + }).catch((err) => { + // This should be exceedingly rare; it represents a failure to send + // the request to Kallichore rather than a failure to execute it + this.log(`Failed to send execution request for '${code}': ${err}`, vscode.LogLevel.Error); + }); + } + + /** + * Tests whether a code fragment is complete. + * @param code The code to test + * @returns The status of the code fragment + */ + async isCodeFragmentComplete(code: string): Promise { + // Form the Jupyter request + const request: JupyterIsCompleteRequest = { + code + }; + const isComplete = new IsCompleteRequest(request); + const reply = await this.sendRequest(isComplete); + switch (reply.status) { + case 'complete': + return positron.RuntimeCodeFragmentStatus.Complete; + case 'incomplete': + return positron.RuntimeCodeFragmentStatus.Incomplete; + case 'invalid': + return positron.RuntimeCodeFragmentStatus.Invalid; + case 'unknown': + return positron.RuntimeCodeFragmentStatus.Unknown; + } + } + + /** + * Create a new client comm. + * + * @param id The ID of the client comm; must be unique among all comms + * connected to this kernel + * @param type The type of client comm to create + * @param params The parameters to pass to the client comm + * @param metadata Additional metadata to pass to the client comm + */ + async createClient( + id: string, + type: positron.RuntimeClientType, + params: any, + metadata?: any): Promise { + + // Ensure the type of client we're being asked to create is a known type that supports + // client-initiated creation + if (type === positron.RuntimeClientType.Variables || + type === positron.RuntimeClientType.Lsp || + type === positron.RuntimeClientType.Dap || + type === positron.RuntimeClientType.Ui || + type === positron.RuntimeClientType.Help || + type === positron.RuntimeClientType.IPyWidgetControl) { + + const msg: JupyterCommOpen = { + target_name: type, // eslint-disable-line + comm_id: id, // eslint-disable-line + data: params + }; + const commOpen = new CommOpenCommand(msg, metadata); + await this.sendCommand(commOpen); + this._comms.set(id, new Comm(id, type)); + } else { + this.log(`Can't create ${type} client for ${this.runtimeMetadata.languageName} (not supported)`, vscode.LogLevel.Error); + } + } + + /** + * Get a list of open clients (comms) from the kernel. + * + * @param type The type of client to list, or undefined to list all clients + * @returns A map of client IDs to client names (targets) + */ + async listClients(type?: positron.RuntimeClientType): Promise> { + const request = new CommInfoRequest(type || ''); + const reply = await this.sendRequest(request); + const result: Record = {}; + const comms = reply.comms; + // Unwrap the comm info and add it to the result + for (const key in comms) { + if (comms.hasOwnProperty(key)) { + const target = comms[key].target_name; + result[key] = target; + // If we don't have a comm object for this comm, create one + if (!this._comms.has(key)) { + this._comms.set(key, new Comm(key, target)); + } + } + } + return result; + } + + removeClient(id: string): void { + const commOpen = new CommCloseCommand(id); + this.sendCommand(commOpen); + } + + /** + * Sends a message to an open comm. + * + * @param client_id The ID of the client comm to send the message to + * @param message_id The ID of the message to send; used to help match + * replies + * @param message The message to send + */ + sendClientMessage(client_id: string, message_id: string, message: any): void { + const msg: JupyterCommMsg = { + comm_id: client_id, + data: message + }; + const commMsg = new CommMsgCommand(message_id, msg); + this.sendCommand(commMsg).then(() => { + // Nothing to do here; the message was sent successfully + }).catch((err) => { + this.log(`Failed to send message ${JSON.stringify(message)} to ${client_id}: ${err}`, vscode.LogLevel.Error); + }); + } + + /** + * Sends a reply to an input prompt to the kernel. + * + * @param id The ID of the input request to reply to + * @param value The value to send as a reply + */ + replyToPrompt(id: string, value: string): void { + if (!this._activeBackendRequestHeader) { + this.log(`Failed to find parent for input request ${id}; sending anyway: ${value}`, vscode.LogLevel.Warning); + return; + } + const reply = new InputReplyCommand(this._activeBackendRequestHeader, value); + this.log(`Sending input reply for ${id}: ${value}`, vscode.LogLevel.Debug); + this.sendCommand(reply); + } + + /** + * Restores an existing session from the server. + * + * @param session The session to restore + */ + async restore(session: ActiveSession) { + // Re-establish the log stream by looking for the `--log` or `--logfile` + // arguments. + // + // CONSIDER: This is a convention used by the R and Python kernels but + // may not be reliable for other kernels. We could handle it more + // generically by storing this information in the session metadata. + for (const arg of ['--log', '--logfile']) { + const logFileIndex = session.argv.indexOf(arg); + if (logFileIndex > 0 && logFileIndex < session.argv.length - 1) { + const logFile = session.argv[logFileIndex + 1]; + if (fs.existsSync(logFile)) { + this.streamLogFile(logFile); + break; + } + } + } + + // Do the same for the profile file + const profileFileIndex = session.argv.indexOf('--profile'); + if (profileFileIndex > 0 && profileFileIndex < session.argv.length - 1) { + const profileFile = session.argv[profileFileIndex + 1]; + if (fs.existsSync(profileFile)) { + this.streamProfileFile(profileFile); + } + } + + // Open the established barrier so that we can start sending messages + this._activeSession = session; + this._established.open(); + } + + /** + * Starts a previously established session. + * + * This method is used both to start a new session and to reconnect to an + * existing session. + * + * @returns The kernel info for the session. + */ + async start(): Promise { + // Wait for the session to be established before connecting. This + // ensures either that we've created the session (if it's new) or that + // we've restored it (if it's not new). + await withTimeout(this._established.wait(), 2000, `Start failed: timed out waiting for session ${this.metadata.sessionId} to be established`); + + // If it's a new session, wait for it to be created before connecting + if (this._new) { + + // Wait for the session to start + try { + await this._api.startSession(this.metadata.sessionId); + } catch (err) { + if (err instanceof HttpError) { + throw new Error(err.body.message); + } else { + // Rethrow the error as-is if it's not an HTTP error + throw err; + } + } + } + + // Before connecting, check if we should attach to the session on + // startup + const config = vscode.workspace.getConfiguration('kallichoreSupervisor'); + const attachOnStartup = config.get('attachOnStartup', false) && this._extra?.attachOnStartup; + if (attachOnStartup) { + try { + await this._extra!.attachOnStartup!.attach(); + } catch (err) { + this.log(`Can't execute attach action: ${err}`, vscode.LogLevel.Error); + } + } + + // Connect to the session's websocket + await withTimeout(this.connect(), 2000, `Start failed: timed out connecting to session ${this.metadata.sessionId}`); + + if (this._new) { + // If this is a new session, wait for it to be ready before + // returning. This can take some time as it needs to wait for the + // kernel to start up. + await withTimeout(this._ready.wait(), 10000, `Start failed: timed out waiting for session ${this.metadata.sessionId} to be ready`); + } else { + if (this._activeSession?.status === Status.Busy) { + // If the session is busy, wait for it to become idle before + // connecting. This could take some time, so show a progress + // notification. + // + // CONSIDER: This could be a long wait; it would be better + // (though it'd require more orchestration) to bring the user + // back to the same experience they had before the reconnecting + // (i.e. all UI is usable but the busy indicator is shown). + vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: vscode.l10n.t('{0} is busy; waiting for it to become idle before reconnecting.', this.metadata.sessionName), + cancellable: false, + }, async () => { + await this.waitForIdle(); + }); + } else { + // Enter the ready state immediately if the session is not busy + this._ready.open(); + this._state.fire(positron.RuntimeState.Ready); + } + } + + return this.getKernelInfo(); + } + + /** + * Waits for the session to become idle before connecting. + * + * @returns A promise that resolves when the session is idle. Does not time + * out or reject. + */ + async waitForIdle(): Promise { + this.log(`Session ${this.metadata.sessionId} is busy; waiting for it to become idle before connecting.`, vscode.LogLevel.Info); + return new Promise((resolve, _reject) => { + this._state.event(async (state) => { + if (state === positron.RuntimeState.Idle) { + resolve(); + this._ready.open(); + this._state.fire(positron.RuntimeState.Ready); + } + }); + }); + } + + /** + * Connects or reconnects to the session's websocket. + * + * @returns A promise that resolves when the websocket is connected. + */ + connect(): Promise { + return new Promise((resolve, reject) => { + // Ensure websocket is closed if it's open + if (this._socket) { + this._socket.close(); + } + + // Connect to the session's websocket. The websocket URL is based on + // the base path of the API. + const uri = vscode.Uri.parse(this._api.basePath); + const wsUri = `ws://${uri.authority}/sessions/${this.metadata.sessionId}/channels`; + this.log(`Connecting to websocket: ${wsUri}`, vscode.LogLevel.Debug); + this._socket = new SocketSession(wsUri, this.metadata.sessionId); + this._disposables.push(this._socket); + + // Handle websocket events + this._socket.ws.onopen = () => { + this.log(`Connected to websocket ${wsUri}.`, vscode.LogLevel.Debug); + // Open the connected barrier so that we can start sending messages + this._connected.open(); + resolve(); + }; + + this._socket.ws.onerror = (err: any) => { + this.log(`Websocket error: ${err}`, vscode.LogLevel.Error); + if (this._connected.isOpen()) { + // If the error happened after the connection was established, + // something bad happened. Close the connected barrier and + // show an error. + this._connected = new Barrier(); + vscode.window.showErrorMessage(`Error connecting to ${this.metadata.sessionName} (${this.metadata.sessionId}): ${JSON.stringify(err)}`); + } else { + // The connection never established; reject the promise and + // let the caller handle it. + reject(err); + } + }; + + this._socket.ws.onclose = (_evt: any) => { + // When the socket is closed, reset the connected barrier and + // clear the websocket instance. + this._connected = new Barrier(); + this._socket = undefined; + }; + + // Main handler for incoming messages + this._socket.ws.onmessage = (msg: any) => { + this.log(`RECV message: ${msg.data}`, vscode.LogLevel.Trace); + try { + const data = JSON.parse(msg.data.toString()); + this.handleMessage(data); + } catch (err) { + this.log(`Could not parse message: ${err}`, vscode.LogLevel.Error); + } + }; + }); + } + + /** + * Interrupt a running kernel. + * + * @returns A promise that resolves when the kernel interrupt request has + * been sent. Note that the kernel may not be interrupted immediately. + */ + async interrupt(): Promise { + // Clear current input request if any + this._activeBackendRequestHeader = null; + + try { + await this._api.interruptSession(this.metadata.sessionId); + } catch (err) { + if (err instanceof HttpError) { + throw new Error(err.body.message); + } else { + throw err; + } + } + } + + /** + * Performs a restart of the kernel. Kallichore handles the mechanics of + * stopping the process and starting a new one; we just need to listen for + * the events and update our state. + */ + async restart(): Promise { + // Remember that we're restarting so that when the exit event arrives, + // we can label it as such + this._exitReason = positron.RuntimeExitReason.Restart; + + // Perform the restart + this._restarting = true; + try { + await this._api.restartSession(this.metadata.sessionId); + } catch (err) { + if (err instanceof HttpError) { + throw new Error(err.body.message); + } else { + throw err; + } + } + } + + /** + * Performs a shutdown of the kernel. + * + * @param exitReason The reason for the shutdown + */ + async shutdown(exitReason: positron.RuntimeExitReason): Promise { + this._exitReason = exitReason; + const restarting = exitReason === positron.RuntimeExitReason.Restart; + const shutdownRequest = new ShutdownRequest(restarting); + await this.sendRequest(shutdownRequest); + } + + /** + * Forces the kernel to quit immediately. + */ + async forceQuit(): Promise { + try { + this._exitReason = positron.RuntimeExitReason.ForcedQuit; + await this._api.killSession(this.metadata.sessionId); + } catch (err) { + this._exitReason = positron.RuntimeExitReason.Unknown; + if (err instanceof HttpError) { + throw new Error(err.body.message); + } else { + throw err; + } + } + } + + /** + * Shows the profile output for this kernel, if any. + */ + async showProfile?(): Promise { + this._profileChannel?.show(); + } + + /** + * Clean up the session. + */ + dispose() { + // Close the log streamer, the websocket, and any other disposables + this._disposables.forEach(d => d.dispose()); + this._disposables = []; + } + + /** + * Main entry point for handling messages delivered over the websocket from + * the Kallichore server. + * + * @param data The message payload + */ + handleMessage(data: any) { + if (!data.kind) { + this.log(`Kallichore session ${this.metadata.sessionId} message has no kind: ${data}`, vscode.LogLevel.Warning); + return; + } + switch (data.kind) { + case 'kernel': + this.handleKernelMessage(data); + break; + case 'jupyter': + this.handleJupyterMessage(data); + break; + } + } + + /** + * Handles kernel-level messages sent from the Kallichore server. + * + * @param data The message payload + */ + handleKernelMessage(data: any) { + if (data.hasOwnProperty('status')) { + // Check to see if the status is a valid runtime state + if (Object.values(positron.RuntimeState).includes(data.status)) { + this.onStateChange(data.status); + } else { + this.log(`Unknown state: ${data.status}`); + } + } else if (data.hasOwnProperty('output')) { + const output = data as KernelOutputMessage; + this._kernelChannel.append(output.output[1]); + } else if (data.hasOwnProperty('exited')) { + this.onExited(data.exited); + } + } + + private onStateChange(newState: positron.RuntimeState) { + // If the kernel is ready, open the ready barrier + if (newState === positron.RuntimeState.Ready) { + this.log(`Received initial heartbeat; kernel is ready.`); + this._ready.open(); + } + this.log(`State: ${this._runtimeState} => ${newState}`, vscode.LogLevel.Debug); + if (newState === positron.RuntimeState.Offline) { + // Close the connected barrier if the kernel is offline + this._connected = new Barrier(); + } + if (this._runtimeState === positron.RuntimeState.Offline && + newState !== positron.RuntimeState.Exited && + newState === positron.RuntimeState.Offline) { + // The kernel was offline but is back online; open the connected + // barrier + this.log(`The kernel is back online.`, vscode.LogLevel.Info); + this._connected.open(); + } + if (newState === positron.RuntimeState.Starting) { + this.log(`The kernel has started up after a restart.`, vscode.LogLevel.Info); + this._restarting = false; + } + this._runtimeState = newState; + this._state.fire(newState); + } + + private onExited(exitCode: number) { + if (this._restarting) { + // If we're restarting, wait for the kernel to start up again + this.log(`Kernel exited with code ${exitCode}; waiting for restart to finish.`, vscode.LogLevel.Info); + } else { + // If we aren't going to be starting up again, clean up the session + // websocket + this.log(`Kernel exited with code ${exitCode}; cleaning up.`, vscode.LogLevel.Info); + this._socket?.close(); + this._socket = undefined; + this._connected = new Barrier(); + } + + // We're no longer ready + this._ready = new Barrier(); + + // If we don't know the exit reason and there's a nonzero exit code, + // consider this exit to be due to an error. + if (this._exitReason === positron.RuntimeExitReason.Unknown && exitCode !== 0) { + this._exitReason = positron.RuntimeExitReason.Error; + } + + // Create and fire the exit event. + const event: positron.LanguageRuntimeExit = { + runtime_name: this.runtimeMetadata.runtimeName, + exit_code: exitCode, + reason: this._exitReason, + message: '' + }; + this._exit.fire(event); + + // We have now consumed the exit reason; restore it to its default + this._exitReason = positron.RuntimeExitReason.Unknown; + } + + /** + * Gets the kernel's information, using the `kernel_info` request. + * + * @returns The kernel's information + */ + async getKernelInfo(): Promise { + // Send the info request to the kernel; note that this waits for the + // kernel to be connected. + const request = new KernelInfoRequest(); + const reply = await this.sendRequest(request); + + // Translate the kernel info to a runtime info object + const info: positron.LanguageRuntimeInfo = { + banner: reply.banner, + implementation_version: reply.implementation_version, + language_version: reply.language_info.version, + }; + return info; + } + + /** + * Main entry point for handling Jupyter messages delivered over the + * websocket from the Kallichore server. + * + * @param data The message payload + */ + handleJupyterMessage(data: any) { + // Deserialize the message buffers from base64, if any + if (data.buffers) { + data.buffers = data.buffers.map((b: string) => { + return Buffer.from(b, 'base64'); + }); + } + + // Cast the data to a Jupyter message + const msg = data as JupyterMessage; + + // Check to see if the message is a reply to a request; if it is, + // resolve the associated promise and remove it from the pending + // requests map + if (msg.parent_header && msg.parent_header.msg_id) { + const request = this._pendingRequests.get(msg.parent_header.msg_id); + if (request) { + if (request.replyType === msg.header.msg_type) { + request.resolve(msg.content); + this._pendingRequests.delete(msg.parent_header.msg_id); + } + } + } + + // Special handling for stdin messages, which have reversed control flow + if (msg.channel === JupyterChannel.Stdin) { + switch (msg.header.msg_type) { + // If this is an input request, save the header so we can can + // line it up with the client's response. + case 'input_request': + this._activeBackendRequestHeader = msg.header; + break; + case 'rpc_request': { + this.onCommRequest(msg).then(() => { + this.log(`Handled comm request: ${JSON.stringify(msg.content)}`, vscode.LogLevel.Debug); + }) + .catch((err) => { + this.log(`Failed to handle comm request: ${JSON.stringify(err)}`, vscode.LogLevel.Error); + }); + break; + } + } + } + + if (msg.header.msg_type === 'comm_msg') { + const commMsg = msg.content as JupyterCommMsg; + + // If we have a DAP client active and this is a comm message intended + // for that client, forward the message. + if (this._dapClient) { + const comm = this._comms.get(commMsg.comm_id); + if (comm && comm.id === this._dapClient.clientId) { + this._dapClient.handleDapMessage(commMsg.data); + } + } + + // If this is a `server_started` message, resolve the promise that + // was created when the comm was started. + if (commMsg.data.msg_type === 'server_started') { + const startingPromise = this._startingComms.get(commMsg.comm_id); + if (startingPromise) { + startingPromise.resolve(); + this._startingComms.delete(commMsg.comm_id); + } + } + } + + // Translate the Jupyter message to a LanguageRuntimeMessage and emit it + this._messages.emitJupyter(msg); + } + + /** + * Part of a reverse request from the UI comm. These requests are fulfilled + * by Positron and the results sent back to the kernel. + * + * @param msg The message payload + */ + async onCommRequest(msg: JupyterMessage): Promise { + const request = msg.content as JupyterCommRequest; + + // Get the response from Positron + const response = await positron.methods.call(request.method, request.params); + + // Send the response back to the kernel + const reply = new RpcReplyCommand(msg.header, response); + return this.sendCommand(reply); + } + + /** + * Sends an RPC request to the kernel and waits for a response. + * + * @param request The request to send + * @returns The response from the kernel + */ + async sendRequest(request: JupyterRequest): Promise { + // Ensure we're connected before sending the request; if requests are + // sent before the connection is established, they'll fail + await this._connected.wait(); + + // Add the request to the pending requests map so we can match up the + // reply when it arrives + this._pendingRequests.set(request.msgId, request); + + // Send the request over the websocket + return request.sendRpc(this._socket!); + } + + /** + * Send a command to the kernel. Does not wait for a response. + * + * @param command The command to send + */ + async sendCommand(command: JupyterCommand): Promise { + // Ensure we're connected before sending the command + await this._connected.wait(); + + // Send the command over the websocket + return command.sendCommand(this._socket!); + } + + /** + * Begins streaming a log file to the kernel channel. + * + * @param logFile The path to the log file to stream + */ + private streamLogFile(logFile: string) { + const logStreamer = new LogStreamer(this._kernelChannel, logFile, this.runtimeMetadata.languageName); + this._disposables.push(logStreamer); + this._kernelLogFile = logFile; + logStreamer.watch(); + } + + /** + * Begins streaming a profile file to the kernel channel. + * + * @param profileFilePath The path to the profile file to stream + */ + private streamProfileFile(profileFilePath: string) { + + this._profileChannel = positron.window.createRawLogOutputChannel( + this.metadata.notebookUri ? + `Notebook: Profiler ${path.basename(this.metadata.notebookUri.path)} (${this.runtimeMetadata.runtimeName})` : + `Positron ${this.runtimeMetadata.languageName} Profiler`); + + this.log('Streaming profile file: ' + profileFilePath, vscode.LogLevel.Debug); + + const profileStreamer = new LogStreamer(this._profileChannel, profileFilePath); + this._disposables.push(profileStreamer); + + profileStreamer.watch(); + } + + /** + * Emits a message to the the log channel + * + * @param msg The message to log + */ + public log(msg: string, logLevel?: vscode.LogLevel) { + // Ensure message isn't over the maximum length + if (msg.length > 2048) { + msg = msg.substring(0, 2048) + '... (truncated)'; + } + + switch (logLevel) { + case vscode.LogLevel.Error: + this._consoleChannel.error(msg); + break; + case vscode.LogLevel.Warning: + this._consoleChannel.warn(msg); + break; + case vscode.LogLevel.Info: + this._consoleChannel.info(msg); + break; + default: + this._consoleChannel.appendLine(msg); + } + } + + /** + * Creates a short, unique ID. Use to help create unique identifiers for + * comms, messages, etc. + * + * @returns An 8-character unique ID, like `a1b2c3d4` + */ + private createUniqueId(): string { + return Math.floor(Math.random() * 0x100000000).toString(16); + } +} diff --git a/extensions/kallichore-adapter/src/LogStreamer.ts b/extensions/kallichore-adapter/src/LogStreamer.ts new file mode 100644 index 00000000000..c38041f8eb6 --- /dev/null +++ b/extensions/kallichore-adapter/src/LogStreamer.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import { Tail } from 'tail'; + +// Wrapper around Tail that flushes on `dispose()`. +// Prevents losing output on reload. + +export class LogStreamer implements vscode.Disposable { + private _tail: Tail; + private _linesCounter: number = 0; + + constructor( + private _output: vscode.OutputChannel, + private _path: string, + private _prefix?: string, + ) { + this._tail = new Tail(this._path, { fromBeginning: true, useWatchFile: true }); + + // Establish listeners for new lines in the log file + this._tail.on('line', (line) => this.appendLine(line)); + this._tail.on('error', (error) => this.appendLine(error)); + } + + public watch() { + // Initialise number of lines seen, which might not be zero as the + // kernel might have already started outputting lines, or we might be + // refreshing with an existing log file. This is used for flushing + // the tail of the log on disposal. There is a race condition here so + // this might be slightly off, causing duplicate lines in the tail of + // the log. + const lines = fs.readFileSync(this._path, 'utf8').split('\n'); + this._linesCounter = lines.length; + + // Start watching the log file. This streams output until the streamer is + // disposed. + this._tail.watch(); + } + + private appendLine(line: string) { + this._linesCounter += 1; + + if (this._prefix) { + this._output.appendLine(`[${this._prefix}] ${line}`); + } else { + this._output.appendLine(line); + } + } + + public dispose() { + this._tail.unwatch(); + + if (!fs.existsSync(this._path)) { + return; + } + + const lines = fs.readFileSync(this._path, 'utf8').split('\n'); + + // Push remaining lines in case new line events haven't had time to + // fire up before unwatching. We skip lines that we've already seen and + // flush the rest. + for (let i = this._linesCounter + 1; i < lines.length; ++i) { + this.appendLine(lines[i]); + } + } +} diff --git a/extensions/kallichore-adapter/src/PortFinder.ts b/extensions/kallichore-adapter/src/PortFinder.ts new file mode 100644 index 00000000000..2251c0c7295 --- /dev/null +++ b/extensions/kallichore-adapter/src/PortFinder.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as net from 'net'; + +/** + * Finds an available TCP port for a server + * + * @param excluding A list of ports to exclude from the search + * @param maxTries The maximum number of attempts + * @returns An available TCP port + */ +export async function findAvailablePort(excluding: Array, maxTries: number): Promise { + const portmin = 41952; + const portmax = 65536; + const nextPort = findAvailablePort; + + return new Promise((resolve, reject) => { + // Pick a random port not on the exclusion list + let candidate = 0; + do { + candidate = Math.floor(Math.random() * (portmax - portmin) + portmin); + } while (excluding.includes(candidate)); + + const test = net.createServer(); + + // If we can't bind to the port, pick another random port + test.once('error', function (err) { + // ... unless we've already tried too many times; likely there's + // a networking issue + if (maxTries < 1) { + reject(err); + } + + // Try again + resolve(nextPort(excluding, maxTries - 1)); + }); + + // If we CAN bind to the port, shutdown the server and return the + // port when it's available + test.once('listening', function () { + test.once('close', function () { + // Add the port to the exclusion list so we don't try to bind to + // it again + excluding.push(candidate); + resolve(candidate); + }); + test.close(); + }); + + // Begin attempting to listen on the candidate port. Use 'localhost' by + // name; otherwise the server attempts to listen on 0.0.0.0, which may + // be restricted by the OS. + test.listen(candidate, 'localhost'); + }); +} diff --git a/extensions/kallichore-adapter/src/RuntimeMessageEmitter.ts b/extensions/kallichore-adapter/src/RuntimeMessageEmitter.ts new file mode 100644 index 00000000000..ca169dba321 --- /dev/null +++ b/extensions/kallichore-adapter/src/RuntimeMessageEmitter.ts @@ -0,0 +1,260 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as positron from 'positron'; +import { JupyterMessage } from './jupyter/JupyterMessage'; +import { JupyterKernelStatus } from './jupyter/JupyterKernelStatus'; +import { JupyterExecuteInput } from './jupyter/JupyterExecuteInput'; +import { JupyterExecuteResult } from './jupyter/ExecuteRequest'; +import { JupyterDisplayData } from './jupyter/JupyterDisplayData'; +import { JupyterCommMsg } from './jupyter/JupyterCommMsg'; +import { JupyterCommOpen } from './jupyter/JupyterCommOpen'; +import { JupyterClearOutput } from './jupyter/JupyterClearOutput'; +import { JupyterErrorReply } from './jupyter/JupyterErrorReply'; +import { JupyterStreamOutput } from './jupyter/JupyterStreamOutput'; +import { JupyterInputRequest } from './jupyter/JupyterInputRequest'; + +/** + * An emitter for runtime messages; translates Jupyter messages into language + * runtime messages and emits them to Positron. + */ +export class RuntimeMessageEmitter { + + private readonly _emitter: vscode.EventEmitter; + + constructor() { + this._emitter = new vscode.EventEmitter(); + } + + public get event(): vscode.Event { + return this._emitter.event; + } + + /** + * Main entry point for message router; consumes a Jupyter message and emits + * a corresponding LanguageRuntimeMessage. + * + * @param msg The Jupyter message to be emitted + */ + public emitJupyter(msg: JupyterMessage): void { + switch (msg.header.msg_type) { + case 'comm_msg': + this.onCommMessage(msg, msg.content as JupyterCommMsg); + break; + case 'comm_open': + this.onCommOpen(msg, msg.content as JupyterCommOpen); + case 'display_data': + this.onDisplayData(msg, msg.content as JupyterDisplayData); + break; + case 'error': + this.onErrorResult(msg, msg.content as JupyterErrorReply); + break; + case 'execute_input': + this.onExecuteInput(msg, msg.content as JupyterExecuteInput); + break; + case 'execute_result': + this.onExecuteResult(msg, msg.content as JupyterExecuteResult); + break; + case 'input_request': + this.onInputRequest(msg, msg.content as JupyterInputRequest); + break; + case 'status': + this.onKernelStatus(msg, msg.content as JupyterKernelStatus); + break; + case 'stream': + this.onStreamOutput(msg, msg.content as JupyterStreamOutput); + } + } + + /** + * Delivers a comm_msg message from the kernel to the appropriate client instance. + * + * @param message The outer message packet + * @param msg The inner comm_msg message + */ + private onCommMessage(message: JupyterMessage, data: JupyterCommMsg): void { + this._emitter.fire({ + id: message.header.msg_id, + parent_id: message.parent_header?.msg_id, + when: message.header.date, + type: positron.LanguageRuntimeMessageType.CommData, + comm_id: data.comm_id, + data: data.data, + metadata: message.metadata, + buffers: message.buffers, + } as positron.LanguageRuntimeCommMessage); + } + + /** + * Converts a Jupyter execute_result message to a LanguageRuntimeMessage and + * emits it. + * + * @param message The message packet + * @param data The execute_result message + */ + onExecuteResult(message: JupyterMessage, data: JupyterExecuteResult) { + this._emitter.fire({ + id: message.header.msg_id, + parent_id: message.parent_header?.msg_id, + when: message.header.date, + type: positron.LanguageRuntimeMessageType.Result, + data: data.data as any, + metadata: message.metadata, + } as positron.LanguageRuntimeResult); + } + + /** + * Converts a Jupyter display_data message to a LanguageRuntimeMessage and + * emits it. + * + * @param message The message packet + * @param data The display_data message + */ + onDisplayData(message: JupyterMessage, data: JupyterDisplayData) { + this._emitter.fire({ + id: message.header.msg_id, + parent_id: message.parent_header?.msg_id, + when: message.header.date, + type: positron.LanguageRuntimeMessageType.Output, + data: data.data as any, + metadata: message.metadata, + } as positron.LanguageRuntimeOutput); + } + + /** + * Converts a Jupyter execute_input message to a LanguageRuntimeMessage and + * emits it. + * + * @param message The message packet + * @param data The execute_input message + */ + onExecuteInput(message: JupyterMessage, data: JupyterExecuteInput) { + this._emitter.fire({ + id: message.header.msg_id, + parent_id: message.parent_header?.msg_id, + when: message.header.date, + type: positron.LanguageRuntimeMessageType.Input, + code: data.code, + execution_count: data.execution_count, + metadata: message.metadata, + } as positron.LanguageRuntimeInput); + } + + /** + * Converts a Jupyter status message to a LanguageRuntimeMessage and emits + * it. + * + * @param message The message packet + * @param data The kernel status message + */ + onKernelStatus(message: JupyterMessage, data: JupyterKernelStatus) { + this._emitter.fire({ + id: message.header.msg_id, + parent_id: message.parent_header?.msg_id, + when: message.header.date, + type: positron.LanguageRuntimeMessageType.State, + state: data.execution_state, + metadata: message.metadata, + } as positron.LanguageRuntimeState); + } + + /** + * Delivers a comm_open message from the kernel to the front end. Typically + * this is used to create a front-end representation of a back-end + * object, such as an interactive plot or Jupyter widget. + * + * @param message The outer message packet + * @param data The inner comm_open message + */ + private onCommOpen(message: JupyterMessage, data: JupyterCommOpen): void { + this._emitter.fire({ + id: message.header.msg_id, + parent_id: message.parent_header?.msg_id, + when: message.header.date, + type: positron.LanguageRuntimeMessageType.CommOpen, + comm_id: data.comm_id, + target_name: data.target_name, + data: data.data, + metadata: message.metadata, + } as positron.LanguageRuntimeCommOpen); + } + + /** + * Converts a Jupyter clear_output message to a LanguageRuntimeMessage and + * emits it. + * + * @param message The message packet + * @param data The clear_output message + */ + onClearOutput(message: JupyterMessage, data: JupyterClearOutput) { + this._emitter.fire({ + id: message.header.msg_id, + parent_id: message.parent_header?.msg_id, + when: message.header.date, + type: positron.LanguageRuntimeMessageType.ClearOutput, + wait: data.wait, + metadata: message.metadata, + } as positron.LanguageRuntimeClearOutput); + } + + /** + * Converts a Jupyter error message to a LanguageRuntimeMessage and emits + * it. + * + * @param message The message packet + * @param data The error message + */ + private onErrorResult(message: JupyterMessage, data: JupyterErrorReply) { + this._emitter.fire({ + id: message.header.msg_id, + parent_id: message.parent_header?.msg_id, + when: message.header.date, + type: positron.LanguageRuntimeMessageType.Error, + name: data.ename, + message: data.evalue, + traceback: data.traceback, + metadata: message.metadata, + } as positron.LanguageRuntimeError); + } + + /** + * Converts a Jupyter stream message to a LanguageRuntimeMessage and + * emits it. + * + * @param message The message packet + * @param data The stream message + */ + private onStreamOutput(message: JupyterMessage, data: JupyterStreamOutput) { + this._emitter.fire({ + id: message.header.msg_id, + parent_id: message.parent_header?.msg_id, + when: message.header.date, + type: positron.LanguageRuntimeMessageType.Stream, + name: data.name, + text: data.text, + metadata: message.metadata, + } as positron.LanguageRuntimeStream); + } + + /** + * Handles an input_request message from the kernel. + * + * @param message The message packet + * @param req The input request + */ + private onInputRequest(message: JupyterMessage, req: JupyterInputRequest): void { + // Send the input request to the client. + this._emitter.fire({ + id: message.header.msg_id, + parent_id: message.parent_header?.msg_id, + when: message.header.date, + type: positron.LanguageRuntimeMessageType.Prompt, + prompt: req.prompt, + password: req.password, + } as positron.LanguageRuntimePrompt); + } + +} diff --git a/extensions/kallichore-adapter/src/async.ts b/extensions/kallichore-adapter/src/async.ts new file mode 100644 index 00000000000..2df931202b2 --- /dev/null +++ b/extensions/kallichore-adapter/src/async.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * PromiseHandles is a class that represents a promise that can be resolved or + * rejected externally. + */ +export class PromiseHandles { + resolve!: (value: T | Promise) => void; + + reject!: (error: unknown) => void; + + promise: Promise; + + constructor() { + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } +} + +/** + * A barrier that is initially closed and then becomes opened permanently. + * Ported from VS Code's async.ts. + */ + +export class Barrier { + private _isOpen: boolean; + private _promise: Promise; + private _completePromise!: (v: boolean) => void; + + constructor() { + this._isOpen = false; + this._promise = new Promise((c, _e) => { + this._completePromise = c; + }); + } + + isOpen(): boolean { + return this._isOpen; + } + + open(): void { + this._isOpen = true; + this._completePromise(true); + } + + wait(): Promise { + return this._promise; + } +} + +/** + * Wraps a promise in a timeout that rejects the promise if it does not resolve + * within the given time. + * + * @param promise The promise to wrap + * @param timeout The timeout interval in milliseconds + * @param message The error message to use if the promise times out + * + * @returns The wrapped promise + */ +export function withTimeout(promise: Promise, + timeout: number, + message: string): Promise { + return Promise.race([ + promise, + new Promise((_, reject) => setTimeout(() => reject(new Error(message)), timeout)) + ]); +} diff --git a/extensions/kallichore-adapter/src/extension.ts b/extensions/kallichore-adapter/src/extension.ts new file mode 100644 index 00000000000..19e3a81b27b --- /dev/null +++ b/extensions/kallichore-adapter/src/extension.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +import { KallichoreAdapterApi } from './kallichore-adapter'; +import { KCApi } from './KallichoreAdapterApi'; + +/** Singleton instance of the Kallichore API wrapper */ +export let API_INSTANCE: KCApi; + +export function activate(context: vscode.ExtensionContext): KallichoreAdapterApi { + const log = vscode.window.createOutputChannel('Kallichore Adapter', { log: true }); + log.debug('Kallichore Adapter activated'); + + // Create the singleton instance of the Kallichore API wrapper + API_INSTANCE = new KCApi(context, log); + + return API_INSTANCE; +} + +export function deactivate() { + // Dispose of the Kallichore API wrapper if it exists; this closes any open + // connections + if (API_INSTANCE) { + API_INSTANCE.dispose(); + } +} diff --git a/extensions/kallichore-adapter/src/jupyter-adapter.d.ts b/extensions/kallichore-adapter/src/jupyter-adapter.d.ts new file mode 100644 index 00000000000..724f5b4f95d --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter-adapter.d.ts @@ -0,0 +1,185 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +// eslint-disable-next-line import/no-unresolved +import * as positron from 'positron'; + +export interface JupyterSessionState { + /** The Jupyter session identifier; sent as part of every message */ + sessionId: string; + + /** The log file the kernel is writing to */ + logFile: string; + + /** The profile file the kernel is writing to */ + profileFile?: string; + + /** The connection file specifying the ZeroMQ ports, signing keys, etc. */ + connectionFile: string; + + /** The ID of the kernel's process, or 0 if the process is not running */ + processId: number; +} + +export interface JupyterSession { + readonly state: JupyterSessionState; +} + +export interface JupyterKernel { + connectToSession(session: JupyterSession): Promise; + log(msg: string): void; +} + +/** + * This set of type definitions defines the interfaces used by the Positron + * Jupyter Adapter extension. + */ + +/** + * Represents a registered Jupyter Kernel. These types are defined in the + * Jupyter documentation at: + * + * https://jupyter-client.readthedocs.io/en/stable/kernels.html#kernel-specs + */ +export interface JupyterKernelSpec { + /** Command used to start the kernel and an array of command line arguments */ + argv: Array; + + /** The kernel's display name */ + display_name: string; // eslint-disable-line + + /** The language the kernel executes */ + language: string; + + /** Interrupt mode (signal or message) */ + interrupt_mode?: 'signal' | 'message'; // eslint-disable-line + + /** Environment variables to set when starting the kernel */ + env?: NodeJS.ProcessEnv; + + /** Function that starts the kernel given a JupyterSession object. + * This is used to start the kernel if it's provided. In this case `argv` + * is ignored. + */ + startKernel?: (session: JupyterSession, kernel: JupyterKernel) => Promise; +} + +/** + * A language runtime that wraps a Jupyter kernel. + */ +export interface JupyterLanguageRuntimeSession extends positron.LanguageRuntimeSession { + /** + * Convenience method for starting the Positron LSP server, if the + * language runtime supports it. + * + * @param clientAddress The address of the client that will connect to the + * language server. + */ + startPositronLsp(clientAddress: string): Thenable; + + /** + * Convenience method for starting the Positron DAP server, if the + * language runtime supports it. + * + * @param serverPort The port on which to bind locally. + * @param debugType Passed as `vscode.DebugConfiguration.type`. + * @param debugName Passed as `vscode.DebugConfiguration.name`. + */ + startPositronDap( + serverPort: number, + debugType: string, + debugName: string, + ): Thenable; + + /** + * Method for emitting a message to the language server's Jupyter output + * channel. + * + * @param message A message to emit to the Jupyter log. + * @param logLevel Optionally, the log level of the message. + */ + emitJupyterLog(message: string, logLevel?: vscode.LogLevel): void; + + /** + * A Jupyter kernel is guaranteed to have a `showOutput()` + * method, so we declare it non-optional. + */ + showOutput(): void; + + /** + * A Jupyter kernel is guaranteed to have a `callMethod()` method; it uses + * the frontend comm to send a message to the kernel and wait for a + * response. + */ + callMethod(method: string, ...args: Array): Promise; + + /** + * Return logfile path + */ + getKernelLogFile(): string; +} + +/** + * The Jupyter Adapter API as exposed by the Jupyter Adapter extension. + */ +export interface JupyterAdapterApi extends vscode.Disposable { + + /** + * Create a session for a Jupyter-compatible kernel. + * + * @param runtimeMetadata The metadata for the language runtime to be + * wrapped by the adapter. + * @param sessionMetadata The metadata for the session to be created. + * @param kernel A Jupyter kernel spec containing the information needed to + * start the kernel. + * @param dynState The initial dynamic state of the session. + * @param extra Optional implementations for extra functionality. + * + * @returns A JupyterLanguageRuntimeSession that wraps the kernel. + */ + createSession( + runtimeMetadata: positron.LanguageRuntimeMetadata, + sessionMetadata: positron.RuntimeSessionMetadata, + kernel: JupyterKernelSpec, + dynState: positron.LanguageRuntimeDynState, + extra?: JupyterKernelExtra | undefined, + ): Promise; + + /** + * Restore a session for a Jupyter-compatible kernel. + * + * @param runtimeMetadata The metadata for the language runtime to be + * wrapped by the adapter. + * @param sessionMetadata The metadata for the session to be reconnected. + * + * @returns A JupyterLanguageRuntimeSession that wraps the kernel. + */ + restoreSession( + runtimeMetadata: positron.LanguageRuntimeMetadata, + sessionMetadata: positron.RuntimeSessionMetadata + ): Promise; + + /** + * Finds an available TCP port for a server + * + * @param excluding A list of ports to exclude from the search + * @param maxTries The maximum number of attempts + * @returns An available TCP port + */ + findAvailablePort(excluding: Array, maxTries: number): Promise; +} + +/** Specific functionality implemented by runtimes */ +export interface JupyterKernelExtra { + attachOnStartup?: { + init: (args: Array) => void; + attach: () => Promise; + }; + sleepOnStartup?: { + init: (args: Array, delay: number) => void; + }; +} diff --git a/extensions/kallichore-adapter/src/jupyter/CommCloseCommand.ts b/extensions/kallichore-adapter/src/jupyter/CommCloseCommand.ts new file mode 100644 index 00000000000..210536e286a --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/CommCloseCommand.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { JupyterChannel } from './JupyterChannel'; +import { JupyterCommand } from './JupyterCommand'; +import { JupyterCommClose } from './JupyterCommClose'; + +export class CommCloseCommand extends JupyterCommand { + /** + * Create a new command to tear down a comm + * + * @param id The ID of the comm to tear down + */ + constructor(id: string) { + super('comm_close', { + comm_id: id, + data: {} + }, JupyterChannel.Shell); + } +} diff --git a/extensions/kallichore-adapter/src/jupyter/CommInfoRequest.ts b/extensions/kallichore-adapter/src/jupyter/CommInfoRequest.ts new file mode 100644 index 00000000000..0578e251aa1 --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/CommInfoRequest.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { JupyterChannel } from './JupyterChannel'; +import { JupyterRequest } from './JupyterRequest'; + +export class CommInfoRequest extends JupyterRequest { + constructor(target: string) { + super('comm_info_request', { target_name: target }, 'comm_info_reply', JupyterChannel.Shell); + } +} + +/** + * Represents a request to list the available comms + * + * @link https://jupyter-client.readthedocs.io/en/stable/messaging.html#comm-info + */ +export interface JupyterCommInfoRequest { + /** + * Optional target name; if specified, only comms with the given target + * name will be returned. + */ + target_name: string; // eslint-disable-line +} + +/** + * Represents a single comm and its associated target name, as returned by a + * comm_info request. + */ +export interface JupyterCommTargetName { + target_name: string; +} + +/** + * Represents a list of available comms and their associated target names, as + * returned by a comm_info request. + * + * @link https://jupyter-client.readthedocs.io/en/stable/messaging.html#comm-info + */ +export interface JupyterCommInfoReply { + /** The status of the request */ + status: 'ok' | 'error'; + + /** The list of comms, as a map of comm ID to target name */ + comms: Record; +} diff --git a/extensions/kallichore-adapter/src/jupyter/CommMsgCommand.ts b/extensions/kallichore-adapter/src/jupyter/CommMsgCommand.ts new file mode 100644 index 00000000000..30a9d9150ad --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/CommMsgCommand.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { JupyterChannel } from './JupyterChannel'; +import { JupyterCommand } from './JupyterCommand'; +import { JupyterCommMsg } from './JupyterCommMsg'; + +export class CommMsgCommand extends JupyterCommand { + constructor(private readonly _id: string, payload: JupyterCommMsg) { + super('comm_msg', payload, JupyterChannel.Shell); + } + + protected override createMsgId(): string { + return this._id; + } +} diff --git a/extensions/kallichore-adapter/src/jupyter/CommMsgRequest.ts b/extensions/kallichore-adapter/src/jupyter/CommMsgRequest.ts new file mode 100644 index 00000000000..583ede29e9b --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/CommMsgRequest.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { JupyterChannel } from './JupyterChannel'; +import { JupyterCommMsg } from './JupyterCommMsg'; +import { JupyterRequest } from './JupyterRequest'; + +export class CommMsgRequest extends JupyterRequest { + constructor(private readonly _id: string, payload: JupyterCommMsg) { + super('comm_msg', payload, 'comm_msg', JupyterChannel.Shell); + } + + protected override createMsgId(): string { + return this._id; + } +} diff --git a/extensions/kallichore-adapter/src/jupyter/CommOpenCommand.ts b/extensions/kallichore-adapter/src/jupyter/CommOpenCommand.ts new file mode 100644 index 00000000000..2461cc54a8f --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/CommOpenCommand.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { JupyterChannel } from './JupyterChannel'; +import { JupyterCommand } from './JupyterCommand'; +import { JupyterCommOpen } from './JupyterCommOpen'; + +/** + * Represents a comm_open command sent to the kernel + */ +export class CommOpenCommand extends JupyterCommand { + /** + * Create a new comm_open command + * + * @param payload The payload of the command; contains initial data sent to + * the comm + * @param _metadata The metadata for the message + */ + constructor(payload: JupyterCommOpen, readonly _metadata: any) { + super('comm_open', payload, JupyterChannel.Shell); + } + + override get metadata(): any { + // If we don't have metadata, return an empty object to ensure the + // metadata field is sent + if (typeof this._metadata === 'undefined') { + return {}; + } + return this._metadata; + } +} diff --git a/extensions/kallichore-adapter/src/jupyter/ExecuteRequest.ts b/extensions/kallichore-adapter/src/jupyter/ExecuteRequest.ts new file mode 100644 index 00000000000..a48a91433b2 --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/ExecuteRequest.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { JupyterChannel } from './JupyterChannel'; +import { JupyterDisplayData } from './JupyterDisplayData'; +import { JupyterRequest } from './JupyterRequest'; + + +export class ExecuteRequest extends JupyterRequest { + constructor(readonly requestId: string, req: JupyterExecuteRequest) { + super('execute_request', req, 'execute_result', JupyterChannel.Shell); + } + protected override createMsgId(): string { + return this.requestId; + } +} + +/** + * Represents an execute_request from the Jupyter frontend to the kernel. + * + * @link https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute + */ +export interface JupyterExecuteRequest { + /** The code to be executed */ + code: string; + + /** Whether the code should be executed silently */ + silent: boolean; + + /** Whether the code should be stored in history */ + store_history: boolean; // eslint-disable-line + + /** A mapping of expressions to be evaluated after the code is executed (TODO: needs to be display_data) */ + user_expressions: Map; // eslint-disable-line + + /** Whether to allow the kernel to send stdin requests */ + allow_stdin: boolean; // eslint-disable-line + + /** Whether the kernel should stop the execution queue when an error occurs */ + stop_on_error: boolean; // eslint-disable-line +} + +/** + * Represents an execute_result from the Jupyter kernel to the front end; this + * is identical to the display_data message, with one additional field. + * + * @link https://jupyter-client.readthedocs.io/en/stable/messaging.html#id6 + */ +export interface JupyterExecuteResult extends JupyterDisplayData { + + /** Execution counter, monotonically increasing */ + execution_count: number; // eslint-disable-line +} diff --git a/extensions/kallichore-adapter/src/jupyter/InputReplyCommand.ts b/extensions/kallichore-adapter/src/jupyter/InputReplyCommand.ts new file mode 100644 index 00000000000..2cecd1f3851 --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/InputReplyCommand.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { JupyterChannel } from './JupyterChannel'; +import { JupyterCommand } from './JupyterCommand'; +import { JupyterMessageHeader } from './JupyterMessageHeader'; + + +export class InputReplyCommand extends JupyterCommand { + /** + * Construct a new input reply + * + * @param parent The parent message header, if any + * @param value The value the user entered for the input request + */ + constructor(readonly parent: JupyterMessageHeader | null, value: string) { + super('input_reply', { value }, JupyterChannel.Stdin); + } + + protected override createParentHeader(): JupyterMessageHeader | null { + return this.parent; + } +} + +/** + * Represents a input_reply sent to the kernel + * + * @link https://jupyter-client.readthedocs.io/en/stable/messaging.html#messages-on-the-stdin-router-dealer-channel + */ +export interface JupyterInputReply { + /** The value the user entered for the input request */ + value: string; +} diff --git a/extensions/kallichore-adapter/src/jupyter/IsCompleteRequest.ts b/extensions/kallichore-adapter/src/jupyter/IsCompleteRequest.ts new file mode 100644 index 00000000000..af976a3c372 --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/IsCompleteRequest.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { JupyterChannel } from './JupyterChannel'; +import { JupyterRequest } from './JupyterRequest'; + +export class IsCompleteRequest extends JupyterRequest { + constructor(req: JupyterIsCompleteRequest) { + super('is_complete_request', req, 'is_complete_reply', JupyterChannel.Shell); + } +} + +/** + * Represents a is_complete_request from the Jupyter frontend to the kernel. + * This requests tests a code fragment to see if it's complete. + * + * @link https://jupyter-client.readthedocs.io/en/stable/messaging.html#code-completeness + */ +export interface JupyterIsCompleteRequest { + /** The code to test for completeness */ + code: string; +} + + +/** + * Represents a is_complete_reply from the kernel. + * + * @link https://jupyter-client.readthedocs.io/en/stable/messaging.html#code-completeness + */ +export interface JupyterIsCompleteReply { + /** The status of the code that was tested for completeness */ + status: 'complete' | 'incomplete' | 'invalid' | 'unknown'; + + /** Characters to use to indent the next line (for 'incomplete' only) */ + indent: string; +} + diff --git a/extensions/kallichore-adapter/src/jupyter/JupyterChannel.ts b/extensions/kallichore-adapter/src/jupyter/JupyterChannel.ts new file mode 100644 index 00000000000..f3c9035a266 --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/JupyterChannel.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +export enum JupyterChannel { + Shell = 'shell', + Control = 'control', + Stdin = 'stdin', + IOPub = 'iopub', + Heartbeat = 'heartbeat' +} diff --git a/extensions/kallichore-adapter/src/jupyter/JupyterClearOutput.ts b/extensions/kallichore-adapter/src/jupyter/JupyterClearOutput.ts new file mode 100644 index 00000000000..29df884ee1c --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/JupyterClearOutput.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Represents a clear_output message from the Jupyter kernel to the front end. + * + * @link https://jupyter-client.readthedocs.io/en/latest/messaging.html#clear-output + */ +export interface JupyterClearOutput { + /** Wait to clear the output until new output is available. */ + wait: boolean; +} diff --git a/extensions/kallichore-adapter/src/jupyter/JupyterCommClose.ts b/extensions/kallichore-adapter/src/jupyter/JupyterCommClose.ts new file mode 100644 index 00000000000..33a2b80c23e --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/JupyterCommClose.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Represents a request to tear down a comm (communications channel) + * + * @link https://jupyter-client.readthedocs.io/en/stable/messaging.html#tearing-down-comms + */ +export interface JupyterCommClose { + /** The ID of the comm to tear down (as a GUID) */ + comm_id: string; // eslint-disable-line + + /** The message payload */ + data: object; +} diff --git a/extensions/kallichore-adapter/src/jupyter/JupyterCommMsg.ts b/extensions/kallichore-adapter/src/jupyter/JupyterCommMsg.ts new file mode 100644 index 00000000000..3af5530def0 --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/JupyterCommMsg.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Represents message on an open comm (communications channel) + * + * @link https://jupyter-client.readthedocs.io/en/stable/messaging.html#comm-messages + */ +export interface JupyterCommMsg { + /** The ID of the comm to send the message to (as a GUID) */ + comm_id: string; // eslint-disable-line + + /** The message payload */ + data: { [key: string]: any }; +} diff --git a/extensions/kallichore-adapter/src/jupyter/JupyterCommOpen.ts b/extensions/kallichore-adapter/src/jupyter/JupyterCommOpen.ts new file mode 100644 index 00000000000..faa9fb2c651 --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/JupyterCommOpen.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Represents a request to open a new comm (communications channel) + * + * @link https://jupyter-client.readthedocs.io/en/stable/messaging.html#opening-a-comm + */ +export interface JupyterCommOpen { + /** The ID of the comm (as a GUID) */ + comm_id: string; // eslint-disable-line + + /** The name of the comm to open */ + target_name: string; // eslint-disable-line + + /** Additional data to use to initialize the comm */ + data: object; +} diff --git a/extensions/kallichore-adapter/src/jupyter/JupyterCommRequest.ts b/extensions/kallichore-adapter/src/jupyter/JupyterCommRequest.ts new file mode 100644 index 00000000000..792b4d4ce82 --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/JupyterCommRequest.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Represents an rpc_request from the kernel. This is an StdIn extension. + * + * @link https://jupyter-client.readthedocs.io/en/stable/messaging.html#messages-on-the-stdin-router-dealer-channel + */ +export interface JupyterCommRequest { + method: string; + params: Record; +} diff --git a/extensions/kallichore-adapter/src/jupyter/JupyterCommand.ts b/extensions/kallichore-adapter/src/jupyter/JupyterCommand.ts new file mode 100644 index 00000000000..b5bb5e37996 --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/JupyterCommand.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SocketSession } from '../ws/SocketSession'; +import { JupyterChannel } from './JupyterChannel'; +import { JupyterMessageHeader } from './JupyterMessageHeader'; +import { WebSocket } from 'ws'; + +/** + * Base class for Jupyter commands; commands are messages to the kernel that do + * not expect a reply. + */ +export abstract class JupyterCommand { + private _msgId: string = ''; + + /** + * + * @param commandType The type of the command. This is the msg_type field in + * the message header. + * @param commandPayload The payload of the command. This is the content field + * in the message. + * @param channel The channel (ZeroMQ socket) to send the command on. + */ + constructor( + public readonly commandType: string, + public readonly commandPayload: T, + public readonly channel: JupyterChannel) { + } + + /** + * Creates a unique message ID. This is a random 10-character string. + * Derived classes can override this method to provide a different message + * ID. + * + * @returns + */ + protected createMsgId(): string { + return Math.random().toString(16).substring(2, 12); + } + + /** + * Returns the metadata for the message. By default, no metadata is sent; + * derived classes can override this method to provide additional metadata. + */ + protected get metadata(): any { + return {}; + } + + /** + * Creates the parent header for the message, if any. By default, no parent + * header is created; derived classes can override this method to provide a + * parent header. + */ + protected createParentHeader(): JupyterMessageHeader | null { + return null; + } + + get msgId(): string { + // If we don't have a message ID, create one + if (!this._msgId) { + this._msgId = this.createMsgId(); + } + return this._msgId; + } + + /** + * Deliver the command to the kernel via the given websocket. + * + * @param sessionId The session ID to send the command to + * @param socket + */ + public sendCommand(socket: SocketSession) { + const header: JupyterMessageHeader = { + msg_id: this.msgId, + session: socket.sessionId, + username: socket.userId, + date: new Date().toISOString(), + msg_type: this.commandType, + version: '5.3' + }; + const payload = { + header, + parent_header: this.createParentHeader(), + metadata: this.metadata, + content: this.commandPayload, + channel: this.channel, + buffers: [] + }; + const text = JSON.stringify(payload); + socket.ws.send(text); + } +} diff --git a/extensions/kallichore-adapter/src/jupyter/JupyterDisplayData.ts b/extensions/kallichore-adapter/src/jupyter/JupyterDisplayData.ts new file mode 100644 index 00000000000..b6ef597ae52 --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/JupyterDisplayData.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface JupyterDisplayDataTypes { + 'text/html'?: string; + 'text/markdown'?: string; + 'text/latex'?: string; + 'text/plain'?: string; +} + +export interface JupyterDisplayData { + data: JupyterDisplayDataTypes; +} diff --git a/extensions/kallichore-adapter/src/jupyter/JupyterErrorReply.ts b/extensions/kallichore-adapter/src/jupyter/JupyterErrorReply.ts new file mode 100644 index 00000000000..1b61c39483c --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/JupyterErrorReply.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + + +/** + * Returned by many Jupyter methods when they fail. + * + * @link https://jupyter-client.readthedocs.io/en/stable/messaging.html#execution-errors + */ +export interface JupyterErrorReply { + /** The name of the exception that caused the error, if any */ + ename: string; + + /** A description of the error, if any */ + evalue: string; + + /** A list of traceback frames for the error, if any */ + traceback: Array; +} diff --git a/extensions/kallichore-adapter/src/jupyter/JupyterExecuteInput.ts b/extensions/kallichore-adapter/src/jupyter/JupyterExecuteInput.ts new file mode 100644 index 00000000000..69fdd60cb2b --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/JupyterExecuteInput.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Represents an execute_input message on the iopub channel + * + * @link https://jupyter-client.readthedocs.io/en/stable/messaging.html#code-inputs + */ +export interface JupyterExecuteInput { + /** The code to be executed */ + code: string; + + /** The count of executions */ + execution_count: number; // eslint-disable-line +} diff --git a/extensions/kallichore-adapter/src/jupyter/JupyterHelpLink.ts b/extensions/kallichore-adapter/src/jupyter/JupyterHelpLink.ts new file mode 100644 index 00000000000..c1316c5b285 --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/JupyterHelpLink.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Represents a help link given from the Jupyter kernel + * + * @link https://jupyter-client.readthedocs.io/en/stable/messaging.html#kernel-info + */ +export interface JupyterHelpLink { + /** The name to display for the help link */ + text: string; + + /** The location (URL) of the help link */ + url: string; +} diff --git a/extensions/kallichore-adapter/src/jupyter/JupyterInputRequest.ts b/extensions/kallichore-adapter/src/jupyter/JupyterInputRequest.ts new file mode 100644 index 00000000000..8e01d3d6e79 --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/JupyterInputRequest.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Represents a input_request from the kernel + * + * @link https://jupyter-client.readthedocs.io/en/stable/messaging.html#messages-on-the-stdin-router-dealer-channel + */ +export interface JupyterInputRequest { + /** The text to show at the prompt */ + prompt: string; + + /** Whether the user is being prompted for a password */ + password: boolean; +} diff --git a/extensions/kallichore-adapter/src/jupyter/JupyterKernelStatus.ts b/extensions/kallichore-adapter/src/jupyter/JupyterKernelStatus.ts new file mode 100644 index 00000000000..29abf0d618d --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/JupyterKernelStatus.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Sent from the kernel to the front end to represent the kernel's status + * + * @link https://jupyter-client.readthedocs.io/en/stable/messaging.html#kernel-status + */ +export interface JupyterKernelStatus { + execution_state: 'busy' | 'idle' | 'starting'; // eslint-disable-line +} diff --git a/extensions/kallichore-adapter/src/jupyter/JupyterLanguageInfo.ts b/extensions/kallichore-adapter/src/jupyter/JupyterLanguageInfo.ts new file mode 100644 index 00000000000..2fe75baa553 --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/JupyterLanguageInfo.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Represents metadata associated with the language supported by the kernel. + * + * @link https://jupyter-client.readthedocs.io/en/stable/messaging.html#kernel-info + */ +export interface JupyterLanguageInfo { + /** The name of the programming language the kernel implements */ + name: string; + + /** The version of the language */ + version: string; + + /** The MIME type for script files in the language */ + mimetype: string; + + /** The file extension for script files in the language */ + file_extension: string; // eslint-disable-line + + /** Pygments lexer (for highlighting), only needed if differs from name */ + pygments_lexer: string; // eslint-disable-line + + /** Codemirror mode (for editing), only needed if differs from name */ + codemirror_mode: string; // eslint-disable-line + + /** Nbconvert exporter, if not default */ + nbconvert_exporter: string; // eslint-disable-line + + /** Posit extension */ + positron?: JupyterLanguageInfoPositron; +} + +export interface JupyterLanguageInfoPositron { + /** Initial input prompt */ + input_prompt?: string; + + /** Initial continuation prompt */ + continuation_prompt?: string; +} diff --git a/extensions/kallichore-adapter/src/jupyter/JupyterMessage.ts b/extensions/kallichore-adapter/src/jupyter/JupyterMessage.ts new file mode 100644 index 00000000000..c43d1331a5f --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/JupyterMessage.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { JupyterChannel } from './JupyterChannel'; +import { JupyterMessageHeader } from './JupyterMessageHeader'; + +/** + * Represents a message to or from the front end to Jupyter. + * + * @link https://jupyter-client.readthedocs.io/en/stable/messaging.html#a-full-message + */ +export interface JupyterMessage { + + /** The message header */ + header: JupyterMessageHeader; + + /** The parent message (the one that caused this one), if any */ + parent_header: JupyterMessageHeader; // eslint-disable-line + + /** Additional metadata, if any */ + metadata: Map; + + /** The body of the message */ + content: any; + + /** + * The channel (ZeroMQ socket) for the message. This isn't part of the + * formal Jupyter protocol; it is used to route websocket messages to/from + * the correct ZeroMQ socket. + */ + channel: JupyterChannel; + + /** Additional binary data */ + buffers: Array; +} diff --git a/extensions/kallichore-adapter/src/jupyter/JupyterMessageHeader.ts b/extensions/kallichore-adapter/src/jupyter/JupyterMessageHeader.ts new file mode 100644 index 00000000000..dab7744fa73 --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/JupyterMessageHeader.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Represents the message header for messages inbound to a Jupyter kernel. + * + * @link https://jupyter-client.readthedocs.io/en/stable/messaging.html#message-header + */ +export interface JupyterMessageHeader { + /** The message ID, must be unique per message. */ + msg_id: string; // eslint-disable-line + + /** Session ID, must be unique per session */ + session: string; + + /** Username, must be unique per user */ + username: string; + + /** Date/Time when message was created in ISO 8601 format */ + date: string; + + /** The message type (TODO: should keysof an enum) */ + msg_type: string; // eslint-disable-line + + /** The message protocol version */ + version: string; +} diff --git a/extensions/kallichore-adapter/src/jupyter/JupyterRequest.ts b/extensions/kallichore-adapter/src/jupyter/JupyterRequest.ts new file mode 100644 index 00000000000..e131c6b54c5 --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/JupyterRequest.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { WebSocket } from 'ws'; +import { JupyterChannel } from './JupyterChannel'; +import { PromiseHandles } from '../async'; +import { JupyterCommand } from './JupyterCommand'; +import { SocketSession } from '../ws/SocketSession'; + +export abstract class JupyterRequest extends JupyterCommand { + private _promise: PromiseHandles = new PromiseHandles(); + constructor( + requestType: string, + requestPayload: T, + public readonly replyType: string, + channel: JupyterChannel) { + super(requestType, requestPayload, channel); + } + + public resolve(response: U): void { + this._promise.resolve(response); + } + + public sendRpc(socket: SocketSession): Promise { + super.sendCommand(socket); + return this._promise.promise; + } +} diff --git a/extensions/kallichore-adapter/src/jupyter/JupyterShutdownReply.ts b/extensions/kallichore-adapter/src/jupyter/JupyterShutdownReply.ts new file mode 100644 index 00000000000..b9d79453e96 --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/JupyterShutdownReply.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Represents a shutdown_reply from the kernel. + * + * @link https://jupyter-client.readthedocs.io/en/stable/messaging.html#kernel-shutdown + */ +export interface JupyterShutdownReply { + /** Shutdown status */ + status: 'ok' | 'error'; + + /** Whether the shutdown precedes a restart */ + restart: boolean; +} diff --git a/extensions/kallichore-adapter/src/jupyter/JupyterShutdownRequest.ts b/extensions/kallichore-adapter/src/jupyter/JupyterShutdownRequest.ts new file mode 100644 index 00000000000..47f162633b3 --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/JupyterShutdownRequest.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Represents a shutdown_request to the kernel + * + * @link https://jupyter-client.readthedocs.io/en/stable/messaging.html#kernel-shutdown + */ +export interface JupyterShutdownRequest { + /** Whether the shutdown precedes a restart */ + restart: boolean; +} diff --git a/extensions/kallichore-adapter/src/jupyter/JupyterStreamOutput.ts b/extensions/kallichore-adapter/src/jupyter/JupyterStreamOutput.ts new file mode 100644 index 00000000000..e12833eb153 --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/JupyterStreamOutput.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface JupyterStreamOutput { + /** The stream the output belongs to, i.e. stdout/stderr */ + name: string; + + /** The text emitted from the stream */ + text: string; +} diff --git a/extensions/kallichore-adapter/src/jupyter/KernelInfoRequest.ts b/extensions/kallichore-adapter/src/jupyter/KernelInfoRequest.ts new file mode 100644 index 00000000000..2c2563af11b --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/KernelInfoRequest.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { JupyterChannel } from './JupyterChannel'; +import { JupyterHelpLink } from './JupyterHelpLink'; +import { JupyterLanguageInfo } from './JupyterLanguageInfo'; +import { JupyterRequest } from './JupyterRequest'; + +/** + * Represents an kernel_info_request to the kernel. + * + * @link https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute + */ +export class KernelInfoRequest extends JupyterRequest { + constructor() { + super('kernel_info_request', {}, 'kernel_info_reply', JupyterChannel.Shell); + } +} + +export interface KernelInfoReply { + /** Execution status */ + status: 'ok' | 'error'; + + /** Version of messaging protocol */ + protocol_version: string; // eslint-disable-line + + /** Implementation version number */ + implementation_version: string; // eslint-disable-line + + /** Information about the language the kernel supports */ + language_info: JupyterLanguageInfo; // eslint-disable-line + + /** A startup banner */ + banner: string; + + /** Whether debugging is supported */ + debugger: boolean; + + /** A list of help links */ + help_links: Array; // eslint-disable-line +} diff --git a/extensions/kallichore-adapter/src/jupyter/README.md b/extensions/kallichore-adapter/src/jupyter/README.md new file mode 100644 index 00000000000..c375ba83c42 --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/README.md @@ -0,0 +1,4 @@ +# Jupyter Types + +This folder contains type definitions for Jupyter messages and some utility +classes for adding structure to commands, requests, and replies. diff --git a/extensions/kallichore-adapter/src/jupyter/RpcReplyCommand.ts b/extensions/kallichore-adapter/src/jupyter/RpcReplyCommand.ts new file mode 100644 index 00000000000..5a354128613 --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/RpcReplyCommand.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { JupyterChannel } from './JupyterChannel'; +import { JupyterCommand } from './JupyterCommand'; +import { JupyterMessageHeader } from './JupyterMessageHeader'; + + +export class RpcReplyCommand extends JupyterCommand { + /** + * Construct a new input reply + * + * @param parent The parent message header, if any + * @param value The value the user entered for the input request + */ + constructor(readonly parent: JupyterMessageHeader | null, value: any) { + super('rpc_reply', value, JupyterChannel.Stdin); + } + + protected override createParentHeader(): JupyterMessageHeader | null { + return this.parent; + } +} diff --git a/extensions/kallichore-adapter/src/jupyter/ShutdownRequest.ts b/extensions/kallichore-adapter/src/jupyter/ShutdownRequest.ts new file mode 100644 index 00000000000..db20acfbf06 --- /dev/null +++ b/extensions/kallichore-adapter/src/jupyter/ShutdownRequest.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { JupyterChannel } from './JupyterChannel'; +import { JupyterRequest } from './JupyterRequest'; +import { JupyterShutdownReply } from './JupyterShutdownReply'; +import { JupyterShutdownRequest } from './JupyterShutdownRequest'; + +export class ShutdownRequest extends JupyterRequest { + constructor(restart: boolean) { + super('shutdown_request', { restart }, 'shutdown_reply', JupyterChannel.Control); + } +} diff --git a/extensions/kallichore-adapter/src/kallichore-adapter.d.ts b/extensions/kallichore-adapter/src/kallichore-adapter.d.ts new file mode 100644 index 00000000000..86804553d98 --- /dev/null +++ b/extensions/kallichore-adapter/src/kallichore-adapter.d.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + + +// eslint-disable-next-line import/no-unresolved +import * as positron from 'positron'; +import { JupyterAdapterApi, JupyterKernelExtra, JupyterKernelSpec, JupyterLanguageRuntimeSession } from './jupyter-adapter'; + + +/** + * The Kallichore Adapter API as exposed by the Kallichore Adapter extension. + */ +export interface KallichoreAdapterApi extends JupyterAdapterApi { + + /** + * Create a session for a Jupyter-compatible kernel. + * + * @param runtimeMetadata The metadata for the language runtime to be + * wrapped by the adapter. + * @param sessionMetadata The metadata for the session to be created. + * @param kernel A Jupyter kernel spec containing the information needed to + * start the kernel. + * @param dynState The initial dynamic state of the session. + * @param extra Optional implementations for extra functionality. + * + * @returns A JupyterLanguageRuntimeSession that wraps the kernel. + */ + createSession( + runtimeMetadata: positron.LanguageRuntimeMetadata, + sessionMetadata: positron.RuntimeSessionMetadata, + kernel: JupyterKernelSpec, + dynState: positron.LanguageRuntimeDynState, + extra?: JupyterKernelExtra | undefined, + ): Promise; + + /** + * Restore a session for a Jupyter-compatible kernel. + * + * @param runtimeMetadata The metadata for the language runtime to be + * wrapped by the adapter. + * @param sessionMetadata The metadata for the session to be reconnected. + * + * @returns A JupyterLanguageRuntimeSession that wraps the kernel. + */ + restoreSession( + runtimeMetadata: positron.LanguageRuntimeMetadata, + sessionMetadata: positron.RuntimeSessionMetadata + ): Promise; +} diff --git a/extensions/kallichore-adapter/src/kcclient/.gitignore b/extensions/kallichore-adapter/src/kcclient/.gitignore new file mode 100644 index 00000000000..149b5765472 --- /dev/null +++ b/extensions/kallichore-adapter/src/kcclient/.gitignore @@ -0,0 +1,4 @@ +wwwroot/*.js +node_modules +typings +dist diff --git a/extensions/kallichore-adapter/src/kcclient/.openapi-generator-ignore b/extensions/kallichore-adapter/src/kcclient/.openapi-generator-ignore new file mode 100644 index 00000000000..7484ee590a3 --- /dev/null +++ b/extensions/kallichore-adapter/src/kcclient/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/extensions/kallichore-adapter/src/kcclient/.openapi-generator/FILES b/extensions/kallichore-adapter/src/kcclient/.openapi-generator/FILES new file mode 100644 index 00000000000..53e8919d9d0 --- /dev/null +++ b/extensions/kallichore-adapter/src/kcclient/.openapi-generator/FILES @@ -0,0 +1,15 @@ +.gitignore +api.ts +api/apis.ts +api/defaultApi.ts +git_push.sh +model/activeSession.ts +model/executionQueue.ts +model/interruptMode.ts +model/modelError.ts +model/models.ts +model/newSession.ts +model/newSession200Response.ts +model/serverStatus.ts +model/sessionList.ts +model/status.ts diff --git a/extensions/kallichore-adapter/src/kcclient/.openapi-generator/VERSION b/extensions/kallichore-adapter/src/kcclient/.openapi-generator/VERSION new file mode 100644 index 00000000000..93c8ddab9fe --- /dev/null +++ b/extensions/kallichore-adapter/src/kcclient/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.6.0 diff --git a/extensions/kallichore-adapter/src/kcclient/README.md b/extensions/kallichore-adapter/src/kcclient/README.md new file mode 100644 index 00000000000..6351569e529 --- /dev/null +++ b/extensions/kallichore-adapter/src/kcclient/README.md @@ -0,0 +1,16 @@ +# Kallichore Client + +This folder contains the Kallichore client library. It is entirely code +generated from the Kallichore API definition using the OpenAPI generator's +`typescript-node` mode. + +Because it is code-generated, it is excluded from hygiene checks during the +build process. However, the editor may still show linting errors in the +generated code. These errors can be ignored. + +> [!NOTE] +> +> **Do not edit the files in this folder directly.** To make changes to the +> client library, edit the OpenAPI definition (in `kallichore.json` in the main +> Kallichore repository) and then run the code generation script in +> `scripts/regen-api.sh` diff --git a/extensions/kallichore-adapter/src/kcclient/api.ts b/extensions/kallichore-adapter/src/kcclient/api.ts new file mode 100644 index 00000000000..b1119f15c47 --- /dev/null +++ b/extensions/kallichore-adapter/src/kcclient/api.ts @@ -0,0 +1,3 @@ +// This is the entrypoint for the package +export * from './api/apis'; +export * from './model/models'; diff --git a/extensions/kallichore-adapter/src/kcclient/api/apis.ts b/extensions/kallichore-adapter/src/kcclient/api/apis.ts new file mode 100644 index 00000000000..93aa6621e32 --- /dev/null +++ b/extensions/kallichore-adapter/src/kcclient/api/apis.ts @@ -0,0 +1,14 @@ +export * from './defaultApi'; +import { DefaultApi } from './defaultApi'; +import * as http from 'http'; + +export class HttpError extends Error { + constructor (public response: http.IncomingMessage, public body: any, public statusCode?: number) { + super('HTTP request failed'); + this.name = 'HttpError'; + } +} + +export { RequestFile } from '../model/models'; + +export const APIS = [DefaultApi]; diff --git a/extensions/kallichore-adapter/src/kcclient/api/defaultApi.ts b/extensions/kallichore-adapter/src/kcclient/api/defaultApi.ts new file mode 100644 index 00000000000..e7e7250434d --- /dev/null +++ b/extensions/kallichore-adapter/src/kcclient/api/defaultApi.ts @@ -0,0 +1,837 @@ +/** + * Kallichore API + * Kallichore is a Jupyter kernel gateway and supervisor + * + * The version of the OpenAPI document: 1.0.0 + * Contact: info@posit.co + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import localVarRequest from 'request'; +import http from 'http'; + +/* tslint:disable:no-unused-locals */ +import { ActiveSession } from '../model/activeSession'; +import { ModelError } from '../model/modelError'; +import { NewSession } from '../model/newSession'; +import { NewSession200Response } from '../model/newSession200Response'; +import { ServerStatus } from '../model/serverStatus'; +import { SessionList } from '../model/sessionList'; + +import { ObjectSerializer, Authentication, VoidAuth, Interceptor } from '../model/models'; +import { HttpBasicAuth, HttpBearerAuth, ApiKeyAuth, OAuth } from '../model/models'; + +import { HttpError, RequestFile } from './apis'; + +let defaultBasePath = 'http://localhost'; + +// =============================================== +// This file is autogenerated - Please do not edit +// =============================================== + +export enum DefaultApiApiKeys { +} + +export class DefaultApi { + protected _basePath = defaultBasePath; + protected _defaultHeaders : any = {}; + protected _useQuerystring : boolean = false; + + protected authentications = { + 'default': new VoidAuth(), + 'bearerAuth': new HttpBearerAuth(), + } + + protected interceptors: Interceptor[] = []; + + constructor(basePath?: string); + constructor(basePathOrUsername: string, password?: string, basePath?: string) { + if (password) { + if (basePath) { + this.basePath = basePath; + } + } else { + if (basePathOrUsername) { + this.basePath = basePathOrUsername + } + } + } + + set useQuerystring(value: boolean) { + this._useQuerystring = value; + } + + set basePath(basePath: string) { + this._basePath = basePath; + } + + set defaultHeaders(defaultHeaders: any) { + this._defaultHeaders = defaultHeaders; + } + + get defaultHeaders() { + return this._defaultHeaders; + } + + get basePath() { + return this._basePath; + } + + public setDefaultAuthentication(auth: Authentication) { + this.authentications.default = auth; + } + + public setApiKey(key: DefaultApiApiKeys, value: string) { + (this.authentications as any)[DefaultApiApiKeys[key]].apiKey = value; + } + + set accessToken(accessToken: string | (() => string)) { + this.authentications.bearerAuth.accessToken = accessToken; + } + + public addInterceptor(interceptor: Interceptor) { + this.interceptors.push(interceptor); + } + + /** + * + * @summary Upgrade to a WebSocket for channel communication + * @param sessionId + */ + public async channelsWebsocket (sessionId: string, options: {headers: {[name: string]: string}} = {headers: {}}) : Promise<{ response: http.IncomingMessage; body?: any; }> { + const localVarPath = this.basePath + '/sessions/{session_id}/channels' + .replace('{' + 'session_id' + '}', encodeURIComponent(String(sessionId))); + let localVarQueryParameters: any = {}; + let localVarHeaderParams: any = (Object).assign({}, this._defaultHeaders); + const produces = ['application/json']; + // give precedence to 'application/json' + if (produces.indexOf('application/json') >= 0) { + localVarHeaderParams.Accept = 'application/json'; + } else { + localVarHeaderParams.Accept = produces.join(','); + } + let localVarFormParams: any = {}; + + // verify required parameter 'sessionId' is not null or undefined + if (sessionId === null || sessionId === undefined) { + throw new Error('Required parameter sessionId was null or undefined when calling channelsWebsocket.'); + } + + (Object).assign(localVarHeaderParams, options.headers); + + let localVarUseFormData = false; + + let localVarRequestOptions: localVarRequest.Options = { + method: 'GET', + qs: localVarQueryParameters, + headers: localVarHeaderParams, + uri: localVarPath, + useQuerystring: this._useQuerystring, + json: true, + }; + + let authenticationPromise = Promise.resolve(); + authenticationPromise = authenticationPromise.then(() => this.authentications.default.applyToRequest(localVarRequestOptions)); + + let interceptorPromise = authenticationPromise; + for (const interceptor of this.interceptors) { + interceptorPromise = interceptorPromise.then(() => interceptor(localVarRequestOptions)); + } + + return interceptorPromise.then(() => { + if (Object.keys(localVarFormParams).length) { + if (localVarUseFormData) { + (localVarRequestOptions).formData = localVarFormParams; + } else { + localVarRequestOptions.form = localVarFormParams; + } + } + return new Promise<{ response: http.IncomingMessage; body?: any; }>((resolve, reject) => { + localVarRequest(localVarRequestOptions, (error, response, body) => { + if (error) { + reject(error); + } else { + if (response.statusCode && response.statusCode >= 200 && response.statusCode <= 299) { + resolve({ response: response, body: body }); + } else { + reject(new HttpError(response, body, response.statusCode)); + } + } + }); + }); + }); + } + /** + * + * @summary Delete session + * @param sessionId + */ + public async deleteSession (sessionId: string, options: {headers: {[name: string]: string}} = {headers: {}}) : Promise<{ response: http.IncomingMessage; body: any; }> { + const localVarPath = this.basePath + '/sessions/{session_id}' + .replace('{' + 'session_id' + '}', encodeURIComponent(String(sessionId))); + let localVarQueryParameters: any = {}; + let localVarHeaderParams: any = (Object).assign({}, this._defaultHeaders); + const produces = ['application/json']; + // give precedence to 'application/json' + if (produces.indexOf('application/json') >= 0) { + localVarHeaderParams.Accept = 'application/json'; + } else { + localVarHeaderParams.Accept = produces.join(','); + } + let localVarFormParams: any = {}; + + // verify required parameter 'sessionId' is not null or undefined + if (sessionId === null || sessionId === undefined) { + throw new Error('Required parameter sessionId was null or undefined when calling deleteSession.'); + } + + (Object).assign(localVarHeaderParams, options.headers); + + let localVarUseFormData = false; + + let localVarRequestOptions: localVarRequest.Options = { + method: 'DELETE', + qs: localVarQueryParameters, + headers: localVarHeaderParams, + uri: localVarPath, + useQuerystring: this._useQuerystring, + json: true, + }; + + let authenticationPromise = Promise.resolve(); + authenticationPromise = authenticationPromise.then(() => this.authentications.default.applyToRequest(localVarRequestOptions)); + + let interceptorPromise = authenticationPromise; + for (const interceptor of this.interceptors) { + interceptorPromise = interceptorPromise.then(() => interceptor(localVarRequestOptions)); + } + + return interceptorPromise.then(() => { + if (Object.keys(localVarFormParams).length) { + if (localVarUseFormData) { + (localVarRequestOptions).formData = localVarFormParams; + } else { + localVarRequestOptions.form = localVarFormParams; + } + } + return new Promise<{ response: http.IncomingMessage; body: any; }>((resolve, reject) => { + localVarRequest(localVarRequestOptions, (error, response, body) => { + if (error) { + reject(error); + } else { + if (response.statusCode && response.statusCode >= 200 && response.statusCode <= 299) { + body = ObjectSerializer.deserialize(body, "any"); + resolve({ response: response, body: body }); + } else { + reject(new HttpError(response, body, response.statusCode)); + } + } + }); + }); + }); + } + /** + * + * @summary Get session details + * @param sessionId + */ + public async getSession (sessionId: string, options: {headers: {[name: string]: string}} = {headers: {}}) : Promise<{ response: http.IncomingMessage; body: ActiveSession; }> { + const localVarPath = this.basePath + '/sessions/{session_id}' + .replace('{' + 'session_id' + '}', encodeURIComponent(String(sessionId))); + let localVarQueryParameters: any = {}; + let localVarHeaderParams: any = (Object).assign({}, this._defaultHeaders); + const produces = ['application/json']; + // give precedence to 'application/json' + if (produces.indexOf('application/json') >= 0) { + localVarHeaderParams.Accept = 'application/json'; + } else { + localVarHeaderParams.Accept = produces.join(','); + } + let localVarFormParams: any = {}; + + // verify required parameter 'sessionId' is not null or undefined + if (sessionId === null || sessionId === undefined) { + throw new Error('Required parameter sessionId was null or undefined when calling getSession.'); + } + + (Object).assign(localVarHeaderParams, options.headers); + + let localVarUseFormData = false; + + let localVarRequestOptions: localVarRequest.Options = { + method: 'GET', + qs: localVarQueryParameters, + headers: localVarHeaderParams, + uri: localVarPath, + useQuerystring: this._useQuerystring, + json: true, + }; + + let authenticationPromise = Promise.resolve(); + authenticationPromise = authenticationPromise.then(() => this.authentications.default.applyToRequest(localVarRequestOptions)); + + let interceptorPromise = authenticationPromise; + for (const interceptor of this.interceptors) { + interceptorPromise = interceptorPromise.then(() => interceptor(localVarRequestOptions)); + } + + return interceptorPromise.then(() => { + if (Object.keys(localVarFormParams).length) { + if (localVarUseFormData) { + (localVarRequestOptions).formData = localVarFormParams; + } else { + localVarRequestOptions.form = localVarFormParams; + } + } + return new Promise<{ response: http.IncomingMessage; body: ActiveSession; }>((resolve, reject) => { + localVarRequest(localVarRequestOptions, (error, response, body) => { + if (error) { + reject(error); + } else { + if (response.statusCode && response.statusCode >= 200 && response.statusCode <= 299) { + body = ObjectSerializer.deserialize(body, "ActiveSession"); + resolve({ response: response, body: body }); + } else { + reject(new HttpError(response, body, response.statusCode)); + } + } + }); + }); + }); + } + /** + * + * @summary Interrupt session + * @param sessionId + */ + public async interruptSession (sessionId: string, options: {headers: {[name: string]: string}} = {headers: {}}) : Promise<{ response: http.IncomingMessage; body: any; }> { + const localVarPath = this.basePath + '/sessions/{session_id}/interrupt' + .replace('{' + 'session_id' + '}', encodeURIComponent(String(sessionId))); + let localVarQueryParameters: any = {}; + let localVarHeaderParams: any = (Object).assign({}, this._defaultHeaders); + const produces = ['application/json']; + // give precedence to 'application/json' + if (produces.indexOf('application/json') >= 0) { + localVarHeaderParams.Accept = 'application/json'; + } else { + localVarHeaderParams.Accept = produces.join(','); + } + let localVarFormParams: any = {}; + + // verify required parameter 'sessionId' is not null or undefined + if (sessionId === null || sessionId === undefined) { + throw new Error('Required parameter sessionId was null or undefined when calling interruptSession.'); + } + + (Object).assign(localVarHeaderParams, options.headers); + + let localVarUseFormData = false; + + let localVarRequestOptions: localVarRequest.Options = { + method: 'POST', + qs: localVarQueryParameters, + headers: localVarHeaderParams, + uri: localVarPath, + useQuerystring: this._useQuerystring, + json: true, + }; + + let authenticationPromise = Promise.resolve(); + authenticationPromise = authenticationPromise.then(() => this.authentications.default.applyToRequest(localVarRequestOptions)); + + let interceptorPromise = authenticationPromise; + for (const interceptor of this.interceptors) { + interceptorPromise = interceptorPromise.then(() => interceptor(localVarRequestOptions)); + } + + return interceptorPromise.then(() => { + if (Object.keys(localVarFormParams).length) { + if (localVarUseFormData) { + (localVarRequestOptions).formData = localVarFormParams; + } else { + localVarRequestOptions.form = localVarFormParams; + } + } + return new Promise<{ response: http.IncomingMessage; body: any; }>((resolve, reject) => { + localVarRequest(localVarRequestOptions, (error, response, body) => { + if (error) { + reject(error); + } else { + if (response.statusCode && response.statusCode >= 200 && response.statusCode <= 299) { + body = ObjectSerializer.deserialize(body, "any"); + resolve({ response: response, body: body }); + } else { + reject(new HttpError(response, body, response.statusCode)); + } + } + }); + }); + }); + } + /** + * + * @summary Force quit session + * @param sessionId + */ + public async killSession (sessionId: string, options: {headers: {[name: string]: string}} = {headers: {}}) : Promise<{ response: http.IncomingMessage; body: any; }> { + const localVarPath = this.basePath + '/sessions/{session_id}/kill' + .replace('{' + 'session_id' + '}', encodeURIComponent(String(sessionId))); + let localVarQueryParameters: any = {}; + let localVarHeaderParams: any = (Object).assign({}, this._defaultHeaders); + const produces = ['application/json']; + // give precedence to 'application/json' + if (produces.indexOf('application/json') >= 0) { + localVarHeaderParams.Accept = 'application/json'; + } else { + localVarHeaderParams.Accept = produces.join(','); + } + let localVarFormParams: any = {}; + + // verify required parameter 'sessionId' is not null or undefined + if (sessionId === null || sessionId === undefined) { + throw new Error('Required parameter sessionId was null or undefined when calling killSession.'); + } + + (Object).assign(localVarHeaderParams, options.headers); + + let localVarUseFormData = false; + + let localVarRequestOptions: localVarRequest.Options = { + method: 'POST', + qs: localVarQueryParameters, + headers: localVarHeaderParams, + uri: localVarPath, + useQuerystring: this._useQuerystring, + json: true, + }; + + let authenticationPromise = Promise.resolve(); + authenticationPromise = authenticationPromise.then(() => this.authentications.default.applyToRequest(localVarRequestOptions)); + + let interceptorPromise = authenticationPromise; + for (const interceptor of this.interceptors) { + interceptorPromise = interceptorPromise.then(() => interceptor(localVarRequestOptions)); + } + + return interceptorPromise.then(() => { + if (Object.keys(localVarFormParams).length) { + if (localVarUseFormData) { + (localVarRequestOptions).formData = localVarFormParams; + } else { + localVarRequestOptions.form = localVarFormParams; + } + } + return new Promise<{ response: http.IncomingMessage; body: any; }>((resolve, reject) => { + localVarRequest(localVarRequestOptions, (error, response, body) => { + if (error) { + reject(error); + } else { + if (response.statusCode && response.statusCode >= 200 && response.statusCode <= 299) { + body = ObjectSerializer.deserialize(body, "any"); + resolve({ response: response, body: body }); + } else { + reject(new HttpError(response, body, response.statusCode)); + } + } + }); + }); + }); + } + /** + * + * @summary List active sessions + */ + public async listSessions (options: {headers: {[name: string]: string}} = {headers: {}}) : Promise<{ response: http.IncomingMessage; body: SessionList; }> { + const localVarPath = this.basePath + '/sessions'; + let localVarQueryParameters: any = {}; + let localVarHeaderParams: any = (Object).assign({}, this._defaultHeaders); + const produces = ['application/json']; + // give precedence to 'application/json' + if (produces.indexOf('application/json') >= 0) { + localVarHeaderParams.Accept = 'application/json'; + } else { + localVarHeaderParams.Accept = produces.join(','); + } + let localVarFormParams: any = {}; + + (Object).assign(localVarHeaderParams, options.headers); + + let localVarUseFormData = false; + + let localVarRequestOptions: localVarRequest.Options = { + method: 'GET', + qs: localVarQueryParameters, + headers: localVarHeaderParams, + uri: localVarPath, + useQuerystring: this._useQuerystring, + json: true, + }; + + let authenticationPromise = Promise.resolve(); + authenticationPromise = authenticationPromise.then(() => this.authentications.default.applyToRequest(localVarRequestOptions)); + + let interceptorPromise = authenticationPromise; + for (const interceptor of this.interceptors) { + interceptorPromise = interceptorPromise.then(() => interceptor(localVarRequestOptions)); + } + + return interceptorPromise.then(() => { + if (Object.keys(localVarFormParams).length) { + if (localVarUseFormData) { + (localVarRequestOptions).formData = localVarFormParams; + } else { + localVarRequestOptions.form = localVarFormParams; + } + } + return new Promise<{ response: http.IncomingMessage; body: SessionList; }>((resolve, reject) => { + localVarRequest(localVarRequestOptions, (error, response, body) => { + if (error) { + reject(error); + } else { + if (response.statusCode && response.statusCode >= 200 && response.statusCode <= 299) { + body = ObjectSerializer.deserialize(body, "SessionList"); + resolve({ response: response, body: body }); + } else { + reject(new HttpError(response, body, response.statusCode)); + } + } + }); + }); + }); + } + /** + * + * @summary Create a new session + * @param newSession + */ + public async newSession (newSession: NewSession, options: {headers: {[name: string]: string}} = {headers: {}}) : Promise<{ response: http.IncomingMessage; body: NewSession200Response; }> { + const localVarPath = this.basePath + '/sessions'; + let localVarQueryParameters: any = {}; + let localVarHeaderParams: any = (Object).assign({}, this._defaultHeaders); + const produces = ['application/json']; + // give precedence to 'application/json' + if (produces.indexOf('application/json') >= 0) { + localVarHeaderParams.Accept = 'application/json'; + } else { + localVarHeaderParams.Accept = produces.join(','); + } + let localVarFormParams: any = {}; + + // verify required parameter 'newSession' is not null or undefined + if (newSession === null || newSession === undefined) { + throw new Error('Required parameter newSession was null or undefined when calling newSession.'); + } + + (Object).assign(localVarHeaderParams, options.headers); + + let localVarUseFormData = false; + + let localVarRequestOptions: localVarRequest.Options = { + method: 'PUT', + qs: localVarQueryParameters, + headers: localVarHeaderParams, + uri: localVarPath, + useQuerystring: this._useQuerystring, + json: true, + body: ObjectSerializer.serialize(newSession, "NewSession") + }; + + let authenticationPromise = Promise.resolve(); + authenticationPromise = authenticationPromise.then(() => this.authentications.default.applyToRequest(localVarRequestOptions)); + + let interceptorPromise = authenticationPromise; + for (const interceptor of this.interceptors) { + interceptorPromise = interceptorPromise.then(() => interceptor(localVarRequestOptions)); + } + + return interceptorPromise.then(() => { + if (Object.keys(localVarFormParams).length) { + if (localVarUseFormData) { + (localVarRequestOptions).formData = localVarFormParams; + } else { + localVarRequestOptions.form = localVarFormParams; + } + } + return new Promise<{ response: http.IncomingMessage; body: NewSession200Response; }>((resolve, reject) => { + localVarRequest(localVarRequestOptions, (error, response, body) => { + if (error) { + reject(error); + } else { + if (response.statusCode && response.statusCode >= 200 && response.statusCode <= 299) { + body = ObjectSerializer.deserialize(body, "NewSession200Response"); + resolve({ response: response, body: body }); + } else { + reject(new HttpError(response, body, response.statusCode)); + } + } + }); + }); + }); + } + /** + * + * @summary Restart a session + * @param sessionId + */ + public async restartSession (sessionId: string, options: {headers: {[name: string]: string}} = {headers: {}}) : Promise<{ response: http.IncomingMessage; body: any; }> { + const localVarPath = this.basePath + '/sessions/{session_id}/restart' + .replace('{' + 'session_id' + '}', encodeURIComponent(String(sessionId))); + let localVarQueryParameters: any = {}; + let localVarHeaderParams: any = (Object).assign({}, this._defaultHeaders); + const produces = ['application/json']; + // give precedence to 'application/json' + if (produces.indexOf('application/json') >= 0) { + localVarHeaderParams.Accept = 'application/json'; + } else { + localVarHeaderParams.Accept = produces.join(','); + } + let localVarFormParams: any = {}; + + // verify required parameter 'sessionId' is not null or undefined + if (sessionId === null || sessionId === undefined) { + throw new Error('Required parameter sessionId was null or undefined when calling restartSession.'); + } + + (Object).assign(localVarHeaderParams, options.headers); + + let localVarUseFormData = false; + + let localVarRequestOptions: localVarRequest.Options = { + method: 'POST', + qs: localVarQueryParameters, + headers: localVarHeaderParams, + uri: localVarPath, + useQuerystring: this._useQuerystring, + json: true, + }; + + let authenticationPromise = Promise.resolve(); + authenticationPromise = authenticationPromise.then(() => this.authentications.default.applyToRequest(localVarRequestOptions)); + + let interceptorPromise = authenticationPromise; + for (const interceptor of this.interceptors) { + interceptorPromise = interceptorPromise.then(() => interceptor(localVarRequestOptions)); + } + + return interceptorPromise.then(() => { + if (Object.keys(localVarFormParams).length) { + if (localVarUseFormData) { + (localVarRequestOptions).formData = localVarFormParams; + } else { + localVarRequestOptions.form = localVarFormParams; + } + } + return new Promise<{ response: http.IncomingMessage; body: any; }>((resolve, reject) => { + localVarRequest(localVarRequestOptions, (error, response, body) => { + if (error) { + reject(error); + } else { + if (response.statusCode && response.statusCode >= 200 && response.statusCode <= 299) { + body = ObjectSerializer.deserialize(body, "any"); + resolve({ response: response, body: body }); + } else { + reject(new HttpError(response, body, response.statusCode)); + } + } + }); + }); + }); + } + /** + * + * @summary Get server status and information + */ + public async serverStatus (options: {headers: {[name: string]: string}} = {headers: {}}) : Promise<{ response: http.IncomingMessage; body: ServerStatus; }> { + const localVarPath = this.basePath + '/status'; + let localVarQueryParameters: any = {}; + let localVarHeaderParams: any = (Object).assign({}, this._defaultHeaders); + const produces = ['application/json']; + // give precedence to 'application/json' + if (produces.indexOf('application/json') >= 0) { + localVarHeaderParams.Accept = 'application/json'; + } else { + localVarHeaderParams.Accept = produces.join(','); + } + let localVarFormParams: any = {}; + + (Object).assign(localVarHeaderParams, options.headers); + + let localVarUseFormData = false; + + let localVarRequestOptions: localVarRequest.Options = { + method: 'GET', + qs: localVarQueryParameters, + headers: localVarHeaderParams, + uri: localVarPath, + useQuerystring: this._useQuerystring, + json: true, + }; + + let authenticationPromise = Promise.resolve(); + authenticationPromise = authenticationPromise.then(() => this.authentications.default.applyToRequest(localVarRequestOptions)); + + let interceptorPromise = authenticationPromise; + for (const interceptor of this.interceptors) { + interceptorPromise = interceptorPromise.then(() => interceptor(localVarRequestOptions)); + } + + return interceptorPromise.then(() => { + if (Object.keys(localVarFormParams).length) { + if (localVarUseFormData) { + (localVarRequestOptions).formData = localVarFormParams; + } else { + localVarRequestOptions.form = localVarFormParams; + } + } + return new Promise<{ response: http.IncomingMessage; body: ServerStatus; }>((resolve, reject) => { + localVarRequest(localVarRequestOptions, (error, response, body) => { + if (error) { + reject(error); + } else { + if (response.statusCode && response.statusCode >= 200 && response.statusCode <= 299) { + body = ObjectSerializer.deserialize(body, "ServerStatus"); + resolve({ response: response, body: body }); + } else { + reject(new HttpError(response, body, response.statusCode)); + } + } + }); + }); + }); + } + /** + * + * @summary Shut down all sessions and the server itself + */ + public async shutdownServer (options: {headers: {[name: string]: string}} = {headers: {}}) : Promise<{ response: http.IncomingMessage; body: any; }> { + const localVarPath = this.basePath + '/shutdown'; + let localVarQueryParameters: any = {}; + let localVarHeaderParams: any = (Object).assign({}, this._defaultHeaders); + const produces = ['application/json']; + // give precedence to 'application/json' + if (produces.indexOf('application/json') >= 0) { + localVarHeaderParams.Accept = 'application/json'; + } else { + localVarHeaderParams.Accept = produces.join(','); + } + let localVarFormParams: any = {}; + + (Object).assign(localVarHeaderParams, options.headers); + + let localVarUseFormData = false; + + let localVarRequestOptions: localVarRequest.Options = { + method: 'POST', + qs: localVarQueryParameters, + headers: localVarHeaderParams, + uri: localVarPath, + useQuerystring: this._useQuerystring, + json: true, + }; + + let authenticationPromise = Promise.resolve(); + authenticationPromise = authenticationPromise.then(() => this.authentications.default.applyToRequest(localVarRequestOptions)); + + let interceptorPromise = authenticationPromise; + for (const interceptor of this.interceptors) { + interceptorPromise = interceptorPromise.then(() => interceptor(localVarRequestOptions)); + } + + return interceptorPromise.then(() => { + if (Object.keys(localVarFormParams).length) { + if (localVarUseFormData) { + (localVarRequestOptions).formData = localVarFormParams; + } else { + localVarRequestOptions.form = localVarFormParams; + } + } + return new Promise<{ response: http.IncomingMessage; body: any; }>((resolve, reject) => { + localVarRequest(localVarRequestOptions, (error, response, body) => { + if (error) { + reject(error); + } else { + if (response.statusCode && response.statusCode >= 200 && response.statusCode <= 299) { + body = ObjectSerializer.deserialize(body, "any"); + resolve({ response: response, body: body }); + } else { + reject(new HttpError(response, body, response.statusCode)); + } + } + }); + }); + }); + } + /** + * + * @summary Start a session + * @param sessionId + */ + public async startSession (sessionId: string, options: {headers: {[name: string]: string}} = {headers: {}}) : Promise<{ response: http.IncomingMessage; body: any; }> { + const localVarPath = this.basePath + '/sessions/{session_id}/start' + .replace('{' + 'session_id' + '}', encodeURIComponent(String(sessionId))); + let localVarQueryParameters: any = {}; + let localVarHeaderParams: any = (Object).assign({}, this._defaultHeaders); + const produces = ['application/json']; + // give precedence to 'application/json' + if (produces.indexOf('application/json') >= 0) { + localVarHeaderParams.Accept = 'application/json'; + } else { + localVarHeaderParams.Accept = produces.join(','); + } + let localVarFormParams: any = {}; + + // verify required parameter 'sessionId' is not null or undefined + if (sessionId === null || sessionId === undefined) { + throw new Error('Required parameter sessionId was null or undefined when calling startSession.'); + } + + (Object).assign(localVarHeaderParams, options.headers); + + let localVarUseFormData = false; + + let localVarRequestOptions: localVarRequest.Options = { + method: 'POST', + qs: localVarQueryParameters, + headers: localVarHeaderParams, + uri: localVarPath, + useQuerystring: this._useQuerystring, + json: true, + }; + + let authenticationPromise = Promise.resolve(); + authenticationPromise = authenticationPromise.then(() => this.authentications.default.applyToRequest(localVarRequestOptions)); + + let interceptorPromise = authenticationPromise; + for (const interceptor of this.interceptors) { + interceptorPromise = interceptorPromise.then(() => interceptor(localVarRequestOptions)); + } + + return interceptorPromise.then(() => { + if (Object.keys(localVarFormParams).length) { + if (localVarUseFormData) { + (localVarRequestOptions).formData = localVarFormParams; + } else { + localVarRequestOptions.form = localVarFormParams; + } + } + return new Promise<{ response: http.IncomingMessage; body: any; }>((resolve, reject) => { + localVarRequest(localVarRequestOptions, (error, response, body) => { + if (error) { + reject(error); + } else { + if (response.statusCode && response.statusCode >= 200 && response.statusCode <= 299) { + body = ObjectSerializer.deserialize(body, "any"); + resolve({ response: response, body: body }); + } else { + reject(new HttpError(response, body, response.statusCode)); + } + } + }); + }); + }); + } +} diff --git a/extensions/kallichore-adapter/src/kcclient/git_push.sh b/extensions/kallichore-adapter/src/kcclient/git_push.sh new file mode 100644 index 00000000000..f53a75d4fab --- /dev/null +++ b/extensions/kallichore-adapter/src/kcclient/git_push.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ "$git_host" = "" ]; then + git_host="github.com" + echo "[INFO] No command line input provided. Set \$git_host to $git_host" +fi + +if [ "$git_user_id" = "" ]; then + git_user_id="GIT_USER_ID" + echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" +fi + +if [ "$git_repo_id" = "" ]; then + git_repo_id="GIT_REPO_ID" + echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" +fi + +if [ "$release_note" = "" ]; then + release_note="Minor update" + echo "[INFO] No command line input provided. Set \$release_note to $release_note" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "$release_note" + +# Sets the new remote +git_remote=$(git remote) +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' diff --git a/extensions/kallichore-adapter/src/kcclient/model/activeSession.ts b/extensions/kallichore-adapter/src/kcclient/model/activeSession.ts new file mode 100644 index 00000000000..ffa7b02e5e2 --- /dev/null +++ b/extensions/kallichore-adapter/src/kcclient/model/activeSession.ts @@ -0,0 +1,156 @@ +/** + * Kallichore API + * Kallichore is a Jupyter kernel gateway and supervisor + * + * The version of the OpenAPI document: 1.0.0 + * Contact: info@posit.co + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { RequestFile } from './models'; +import { ExecutionQueue } from './executionQueue'; +import { InterruptMode } from './interruptMode'; +import { Status } from './status'; + +export class ActiveSession { + /** + * A unique identifier for the session + */ + 'sessionId': string; + /** + * The program and command-line parameters for the session + */ + 'argv': Array; + /** + * The underlying process ID of the session, if the session is running. + */ + 'processId'?: number; + /** + * The username of the user who owns the session + */ + 'username': string; + /** + * A human-readable name for the session + */ + 'displayName': string; + /** + * The interpreter language + */ + 'language': string; + 'interruptMode': InterruptMode; + /** + * The environment variables set when the session was started + */ + 'initialEnv'?: { [key: string]: string; }; + /** + * Whether the session is connected to a client + */ + 'connected': boolean; + /** + * An ISO 8601 timestamp of when the session was started + */ + 'started': Date; + /** + * The session\'s current working directory + */ + 'workingDirectory': string; + /** + * The text to use to prompt for input + */ + 'inputPrompt': string; + /** + * The text to use to prompt for input continuations + */ + 'continuationPrompt': string; + 'executionQueue': ExecutionQueue; + 'status': Status; + + static discriminator: string | undefined = undefined; + + static attributeTypeMap: Array<{name: string, baseName: string, type: string}> = [ + { + "name": "sessionId", + "baseName": "session_id", + "type": "string" + }, + { + "name": "argv", + "baseName": "argv", + "type": "Array" + }, + { + "name": "processId", + "baseName": "process_id", + "type": "number" + }, + { + "name": "username", + "baseName": "username", + "type": "string" + }, + { + "name": "displayName", + "baseName": "display_name", + "type": "string" + }, + { + "name": "language", + "baseName": "language", + "type": "string" + }, + { + "name": "interruptMode", + "baseName": "interrupt_mode", + "type": "InterruptMode" + }, + { + "name": "initialEnv", + "baseName": "initial_env", + "type": "{ [key: string]: string; }" + }, + { + "name": "connected", + "baseName": "connected", + "type": "boolean" + }, + { + "name": "started", + "baseName": "started", + "type": "Date" + }, + { + "name": "workingDirectory", + "baseName": "working_directory", + "type": "string" + }, + { + "name": "inputPrompt", + "baseName": "input_prompt", + "type": "string" + }, + { + "name": "continuationPrompt", + "baseName": "continuation_prompt", + "type": "string" + }, + { + "name": "executionQueue", + "baseName": "execution_queue", + "type": "ExecutionQueue" + }, + { + "name": "status", + "baseName": "status", + "type": "Status" + } ]; + + static getAttributeTypeMap() { + return ActiveSession.attributeTypeMap; + } +} + +export namespace ActiveSession { +} diff --git a/extensions/kallichore-adapter/src/kcclient/model/executionQueue.ts b/extensions/kallichore-adapter/src/kcclient/model/executionQueue.ts new file mode 100644 index 00000000000..512981ad019 --- /dev/null +++ b/extensions/kallichore-adapter/src/kcclient/model/executionQueue.ts @@ -0,0 +1,55 @@ +/** + * Kallichore API + * Kallichore is a Jupyter kernel gateway and supervisor + * + * The version of the OpenAPI document: 1.0.0 + * Contact: info@posit.co + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { RequestFile } from './models'; + +/** +* The execution queue for a session +*/ +export class ExecutionQueue { + /** + * The execution request currently being evaluated, if any + */ + 'active'?: object; + /** + * The number of items in the pending queue + */ + 'length': number; + /** + * The queue of pending execution requests + */ + 'pending': Array; + + static discriminator: string | undefined = undefined; + + static attributeTypeMap: Array<{name: string, baseName: string, type: string}> = [ + { + "name": "active", + "baseName": "active", + "type": "object" + }, + { + "name": "length", + "baseName": "length", + "type": "number" + }, + { + "name": "pending", + "baseName": "pending", + "type": "Array" + } ]; + + static getAttributeTypeMap() { + return ExecutionQueue.attributeTypeMap; + } +} + diff --git a/extensions/kallichore-adapter/src/kcclient/model/interruptMode.ts b/extensions/kallichore-adapter/src/kcclient/model/interruptMode.ts new file mode 100644 index 00000000000..42ab9ee2937 --- /dev/null +++ b/extensions/kallichore-adapter/src/kcclient/model/interruptMode.ts @@ -0,0 +1,21 @@ +/** + * Kallichore API + * Kallichore is a Jupyter kernel gateway and supervisor + * + * The version of the OpenAPI document: 1.0.0 + * Contact: info@posit.co + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { RequestFile } from './models'; + +/** +* The mechansim for interrupting the session +*/ +export enum InterruptMode { + Signal = 'signal', + Message = 'message' +} diff --git a/extensions/kallichore-adapter/src/kcclient/model/modelError.ts b/extensions/kallichore-adapter/src/kcclient/model/modelError.ts new file mode 100644 index 00000000000..4af736fa7b4 --- /dev/null +++ b/extensions/kallichore-adapter/src/kcclient/model/modelError.ts @@ -0,0 +1,43 @@ +/** + * Kallichore API + * Kallichore is a Jupyter kernel gateway and supervisor + * + * The version of the OpenAPI document: 1.0.0 + * Contact: info@posit.co + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { RequestFile } from './models'; + +export class ModelError { + 'code': string; + 'message': string; + 'details'?: string; + + static discriminator: string | undefined = undefined; + + static attributeTypeMap: Array<{name: string, baseName: string, type: string}> = [ + { + "name": "code", + "baseName": "code", + "type": "string" + }, + { + "name": "message", + "baseName": "message", + "type": "string" + }, + { + "name": "details", + "baseName": "details", + "type": "string" + } ]; + + static getAttributeTypeMap() { + return ModelError.attributeTypeMap; + } +} + diff --git a/extensions/kallichore-adapter/src/kcclient/model/models.ts b/extensions/kallichore-adapter/src/kcclient/model/models.ts new file mode 100644 index 00000000000..df6fb20098d --- /dev/null +++ b/extensions/kallichore-adapter/src/kcclient/model/models.ts @@ -0,0 +1,246 @@ +import localVarRequest from 'request'; + +export * from './activeSession'; +export * from './executionQueue'; +export * from './interruptMode'; +export * from './modelError'; +export * from './newSession'; +export * from './newSession200Response'; +export * from './serverStatus'; +export * from './sessionList'; +export * from './status'; + +import * as fs from 'fs'; + +export interface RequestDetailedFile { + value: Buffer; + options?: { + filename?: string; + contentType?: string; + } +} + +export type RequestFile = string | Buffer | fs.ReadStream | RequestDetailedFile; + + +import { ActiveSession } from './activeSession'; +import { ExecutionQueue } from './executionQueue'; +import { InterruptMode } from './interruptMode'; +import { ModelError } from './modelError'; +import { NewSession } from './newSession'; +import { NewSession200Response } from './newSession200Response'; +import { ServerStatus } from './serverStatus'; +import { SessionList } from './sessionList'; +import { Status } from './status'; + +/* tslint:disable:no-unused-variable */ +let primitives = [ + "string", + "boolean", + "double", + "integer", + "long", + "float", + "number", + "any" + ]; + +let enumsMap: {[index: string]: any} = { + "InterruptMode": InterruptMode, + "Status": Status, +} + +let typeMap: {[index: string]: any} = { + "ActiveSession": ActiveSession, + "ExecutionQueue": ExecutionQueue, + "ModelError": ModelError, + "NewSession": NewSession, + "NewSession200Response": NewSession200Response, + "ServerStatus": ServerStatus, + "SessionList": SessionList, +} + +export class ObjectSerializer { + public static findCorrectType(data: any, expectedType: string) { + if (data == undefined) { + return expectedType; + } else if (primitives.indexOf(expectedType.toLowerCase()) !== -1) { + return expectedType; + } else if (expectedType === "Date") { + return expectedType; + } else { + if (enumsMap[expectedType]) { + return expectedType; + } + + if (!typeMap[expectedType]) { + return expectedType; // w/e we don't know the type + } + + // Check the discriminator + let discriminatorProperty = typeMap[expectedType].discriminator; + if (discriminatorProperty == null) { + return expectedType; // the type does not have a discriminator. use it. + } else { + if (data[discriminatorProperty]) { + var discriminatorType = data[discriminatorProperty]; + if(typeMap[discriminatorType]){ + return discriminatorType; // use the type given in the discriminator + } else { + return expectedType; // discriminator did not map to a type + } + } else { + return expectedType; // discriminator was not present (or an empty string) + } + } + } + } + + public static serialize(data: any, type: string) { + if (data == undefined) { + return data; + } else if (primitives.indexOf(type.toLowerCase()) !== -1) { + return data; + } else if (type.lastIndexOf("Array<", 0) === 0) { // string.startsWith pre es6 + let subType: string = type.replace("Array<", ""); // Array => Type> + subType = subType.substring(0, subType.length - 1); // Type> => Type + let transformedData: any[] = []; + for (let index = 0; index < data.length; index++) { + let datum = data[index]; + transformedData.push(ObjectSerializer.serialize(datum, subType)); + } + return transformedData; + } else if (type === "Date") { + return data.toISOString(); + } else { + if (enumsMap[type]) { + return data; + } + if (!typeMap[type]) { // in case we dont know the type + return data; + } + + // Get the actual type of this object + type = this.findCorrectType(data, type); + + // get the map for the correct type. + let attributeTypes = typeMap[type].getAttributeTypeMap(); + let instance: {[index: string]: any} = {}; + for (let index = 0; index < attributeTypes.length; index++) { + let attributeType = attributeTypes[index]; + instance[attributeType.baseName] = ObjectSerializer.serialize(data[attributeType.name], attributeType.type); + } + return instance; + } + } + + public static deserialize(data: any, type: string) { + // polymorphism may change the actual type. + type = ObjectSerializer.findCorrectType(data, type); + if (data == undefined) { + return data; + } else if (primitives.indexOf(type.toLowerCase()) !== -1) { + return data; + } else if (type.lastIndexOf("Array<", 0) === 0) { // string.startsWith pre es6 + let subType: string = type.replace("Array<", ""); // Array => Type> + subType = subType.substring(0, subType.length - 1); // Type> => Type + let transformedData: any[] = []; + for (let index = 0; index < data.length; index++) { + let datum = data[index]; + transformedData.push(ObjectSerializer.deserialize(datum, subType)); + } + return transformedData; + } else if (type === "Date") { + return new Date(data); + } else { + if (enumsMap[type]) {// is Enum + return data; + } + + if (!typeMap[type]) { // dont know the type + return data; + } + let instance = new typeMap[type](); + let attributeTypes = typeMap[type].getAttributeTypeMap(); + for (let index = 0; index < attributeTypes.length; index++) { + let attributeType = attributeTypes[index]; + instance[attributeType.name] = ObjectSerializer.deserialize(data[attributeType.baseName], attributeType.type); + } + return instance; + } + } +} + +export interface Authentication { + /** + * Apply authentication settings to header and query params. + */ + applyToRequest(requestOptions: localVarRequest.Options): Promise | void; +} + +export class HttpBasicAuth implements Authentication { + public username: string = ''; + public password: string = ''; + + applyToRequest(requestOptions: localVarRequest.Options): void { + requestOptions.auth = { + username: this.username, password: this.password + } + } +} + +export class HttpBearerAuth implements Authentication { + public accessToken: string | (() => string) = ''; + + applyToRequest(requestOptions: localVarRequest.Options): void { + if (requestOptions && requestOptions.headers) { + const accessToken = typeof this.accessToken === 'function' + ? this.accessToken() + : this.accessToken; + requestOptions.headers["Authorization"] = "Bearer " + accessToken; + } + } +} + +export class ApiKeyAuth implements Authentication { + public apiKey: string = ''; + + constructor(private location: string, private paramName: string) { + } + + applyToRequest(requestOptions: localVarRequest.Options): void { + if (this.location == "query") { + (requestOptions.qs)[this.paramName] = this.apiKey; + } else if (this.location == "header" && requestOptions && requestOptions.headers) { + requestOptions.headers[this.paramName] = this.apiKey; + } else if (this.location == 'cookie' && requestOptions && requestOptions.headers) { + if (requestOptions.headers['Cookie']) { + requestOptions.headers['Cookie'] += '; ' + this.paramName + '=' + encodeURIComponent(this.apiKey); + } + else { + requestOptions.headers['Cookie'] = this.paramName + '=' + encodeURIComponent(this.apiKey); + } + } + } +} + +export class OAuth implements Authentication { + public accessToken: string = ''; + + applyToRequest(requestOptions: localVarRequest.Options): void { + if (requestOptions && requestOptions.headers) { + requestOptions.headers["Authorization"] = "Bearer " + this.accessToken; + } + } +} + +export class VoidAuth implements Authentication { + public username: string = ''; + public password: string = ''; + + applyToRequest(_: localVarRequest.Options): void { + // Do nothing + } +} + +export type Interceptor = (requestOptions: localVarRequest.Options) => (Promise | void); diff --git a/extensions/kallichore-adapter/src/kcclient/model/newSession.ts b/extensions/kallichore-adapter/src/kcclient/model/newSession.ts new file mode 100644 index 00000000000..565fbd30fb8 --- /dev/null +++ b/extensions/kallichore-adapter/src/kcclient/model/newSession.ts @@ -0,0 +1,115 @@ +/** + * Kallichore API + * Kallichore is a Jupyter kernel gateway and supervisor + * + * The version of the OpenAPI document: 1.0.0 + * Contact: info@posit.co + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { RequestFile } from './models'; +import { InterruptMode } from './interruptMode'; + +export class NewSession { + /** + * A unique identifier for the session + */ + 'sessionId': string; + /** + * A human-readable name for the session + */ + 'displayName': string; + /** + * The interpreter language + */ + 'language': string; + /** + * The username of the user who owns the session + */ + 'username': string; + /** + * The text to use to prompt for input + */ + 'inputPrompt': string; + /** + * The text to use to prompt for input continuations + */ + 'continuationPrompt': string; + /** + * The program and command-line parameters for the session + */ + 'argv': Array; + /** + * The working directory in which to start the session. + */ + 'workingDirectory': string; + /** + * Environment variables to set for the session + */ + 'env': { [key: string]: string; }; + 'interruptMode': InterruptMode; + + static discriminator: string | undefined = undefined; + + static attributeTypeMap: Array<{name: string, baseName: string, type: string}> = [ + { + "name": "sessionId", + "baseName": "session_id", + "type": "string" + }, + { + "name": "displayName", + "baseName": "display_name", + "type": "string" + }, + { + "name": "language", + "baseName": "language", + "type": "string" + }, + { + "name": "username", + "baseName": "username", + "type": "string" + }, + { + "name": "inputPrompt", + "baseName": "input_prompt", + "type": "string" + }, + { + "name": "continuationPrompt", + "baseName": "continuation_prompt", + "type": "string" + }, + { + "name": "argv", + "baseName": "argv", + "type": "Array" + }, + { + "name": "workingDirectory", + "baseName": "working_directory", + "type": "string" + }, + { + "name": "env", + "baseName": "env", + "type": "{ [key: string]: string; }" + }, + { + "name": "interruptMode", + "baseName": "interrupt_mode", + "type": "InterruptMode" + } ]; + + static getAttributeTypeMap() { + return NewSession.attributeTypeMap; + } +} + +export namespace NewSession { +} diff --git a/extensions/kallichore-adapter/src/kcclient/model/newSession200Response.ts b/extensions/kallichore-adapter/src/kcclient/model/newSession200Response.ts new file mode 100644 index 00000000000..8b2fac616b5 --- /dev/null +++ b/extensions/kallichore-adapter/src/kcclient/model/newSession200Response.ts @@ -0,0 +1,34 @@ +/** + * Kallichore API + * Kallichore is a Jupyter kernel gateway and supervisor + * + * The version of the OpenAPI document: 1.0.0 + * Contact: info@posit.co + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { RequestFile } from './models'; + +export class NewSession200Response { + /** + * A unique identifier for the session + */ + 'sessionId': string; + + static discriminator: string | undefined = undefined; + + static attributeTypeMap: Array<{name: string, baseName: string, type: string}> = [ + { + "name": "sessionId", + "baseName": "session_id", + "type": "string" + } ]; + + static getAttributeTypeMap() { + return NewSession200Response.attributeTypeMap; + } +} + diff --git a/extensions/kallichore-adapter/src/kcclient/model/serverStatus.ts b/extensions/kallichore-adapter/src/kcclient/model/serverStatus.ts new file mode 100644 index 00000000000..aaa6453ed6d --- /dev/null +++ b/extensions/kallichore-adapter/src/kcclient/model/serverStatus.ts @@ -0,0 +1,49 @@ +/** + * Kallichore API + * Kallichore is a Jupyter kernel gateway and supervisor + * + * The version of the OpenAPI document: 1.0.0 + * Contact: info@posit.co + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { RequestFile } from './models'; + +export class ServerStatus { + 'sessions': number; + 'active': number; + 'busy': boolean; + 'version': string; + + static discriminator: string | undefined = undefined; + + static attributeTypeMap: Array<{name: string, baseName: string, type: string}> = [ + { + "name": "sessions", + "baseName": "sessions", + "type": "number" + }, + { + "name": "active", + "baseName": "active", + "type": "number" + }, + { + "name": "busy", + "baseName": "busy", + "type": "boolean" + }, + { + "name": "version", + "baseName": "version", + "type": "string" + } ]; + + static getAttributeTypeMap() { + return ServerStatus.attributeTypeMap; + } +} + diff --git a/extensions/kallichore-adapter/src/kcclient/model/session.ts b/extensions/kallichore-adapter/src/kcclient/model/session.ts new file mode 100644 index 00000000000..5670166ff68 --- /dev/null +++ b/extensions/kallichore-adapter/src/kcclient/model/session.ts @@ -0,0 +1,79 @@ +/** + * Kallichore API + * Kallichore is a Jupyter kernel gateway and supervisor + * + * The version of the OpenAPI document: 1.0.0 + * Contact: info@posit.co + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { RequestFile } from './models'; +import { InterruptMode } from './interruptMode'; + +export class Session { + /** + * A unique identifier for the session + */ + 'sessionId': string; + /** + * The username of the user who owns the session + */ + 'username': string; + /** + * The program and command-line parameters for the session + */ + 'argv': Array; + /** + * The working directory in which to start the session. + */ + 'workingDirectory': string; + /** + * Environment variables to set for the session + */ + 'env': { [key: string]: string; }; + 'interruptMode'?: InterruptMode; + + static discriminator: string | undefined = undefined; + + static attributeTypeMap: Array<{name: string, baseName: string, type: string}> = [ + { + "name": "sessionId", + "baseName": "session_id", + "type": "string" + }, + { + "name": "username", + "baseName": "username", + "type": "string" + }, + { + "name": "argv", + "baseName": "argv", + "type": "Array" + }, + { + "name": "workingDirectory", + "baseName": "working_directory", + "type": "string" + }, + { + "name": "env", + "baseName": "env", + "type": "{ [key: string]: string; }" + }, + { + "name": "interruptMode", + "baseName": "interrupt_mode", + "type": "InterruptMode" + } ]; + + static getAttributeTypeMap() { + return Session.attributeTypeMap; + } +} + +export namespace Session { +} diff --git a/extensions/kallichore-adapter/src/kcclient/model/sessionList.ts b/extensions/kallichore-adapter/src/kcclient/model/sessionList.ts new file mode 100644 index 00000000000..b244210aeeb --- /dev/null +++ b/extensions/kallichore-adapter/src/kcclient/model/sessionList.ts @@ -0,0 +1,38 @@ +/** + * Kallichore API + * Kallichore is a Jupyter kernel gateway and supervisor + * + * The version of the OpenAPI document: 1.0.0 + * Contact: info@posit.co + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { RequestFile } from './models'; +import { ActiveSession } from './activeSession'; + +export class SessionList { + 'total': number; + 'sessions': Array; + + static discriminator: string | undefined = undefined; + + static attributeTypeMap: Array<{name: string, baseName: string, type: string}> = [ + { + "name": "total", + "baseName": "total", + "type": "number" + }, + { + "name": "sessions", + "baseName": "sessions", + "type": "Array" + } ]; + + static getAttributeTypeMap() { + return SessionList.attributeTypeMap; + } +} + diff --git a/extensions/kallichore-adapter/src/kcclient/model/sessionListSessionsInner.ts b/extensions/kallichore-adapter/src/kcclient/model/sessionListSessionsInner.ts new file mode 100644 index 00000000000..0d54f0f7f99 --- /dev/null +++ b/extensions/kallichore-adapter/src/kcclient/model/sessionListSessionsInner.ts @@ -0,0 +1,104 @@ +/** + * Kallichore API + * Kallichore is a Jupyter kernel gateway and supervisor + * + * The version of the OpenAPI document: 1.0.0 + * Contact: info@posit.co + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { RequestFile } from './models'; +import { ExecutionQueue } from './executionQueue'; +import { Status } from './status'; + +export class SessionListSessionsInner { + /** + * A unique identifier for the session + */ + 'sessionId': string; + /** + * The program and command-line parameters for the session + */ + 'argv': Array; + /** + * The underlying process ID of the session, if the session is running. + */ + 'processId'?: number; + /** + * The username of the user who owns the session + */ + 'username': string; + /** + * Whether the session is connected to a client + */ + 'connected': boolean; + /** + * An ISO 8601 timestamp of when the session was started + */ + 'started': Date; + /** + * The session\'s current working directory + */ + 'workingDirectory': string; + 'executionQueue': ExecutionQueue; + 'status': Status; + + static discriminator: string | undefined = undefined; + + static attributeTypeMap: Array<{name: string, baseName: string, type: string}> = [ + { + "name": "sessionId", + "baseName": "session_id", + "type": "string" + }, + { + "name": "argv", + "baseName": "argv", + "type": "Array" + }, + { + "name": "processId", + "baseName": "process_id", + "type": "number" + }, + { + "name": "username", + "baseName": "username", + "type": "string" + }, + { + "name": "connected", + "baseName": "connected", + "type": "boolean" + }, + { + "name": "started", + "baseName": "started", + "type": "Date" + }, + { + "name": "workingDirectory", + "baseName": "working_directory", + "type": "string" + }, + { + "name": "executionQueue", + "baseName": "execution_queue", + "type": "ExecutionQueue" + }, + { + "name": "status", + "baseName": "status", + "type": "Status" + } ]; + + static getAttributeTypeMap() { + return SessionListSessionsInner.attributeTypeMap; + } +} + +export namespace SessionListSessionsInner { +} diff --git a/extensions/kallichore-adapter/src/kcclient/model/status.ts b/extensions/kallichore-adapter/src/kcclient/model/status.ts new file mode 100644 index 00000000000..8c68f34c5a6 --- /dev/null +++ b/extensions/kallichore-adapter/src/kcclient/model/status.ts @@ -0,0 +1,26 @@ +/** + * Kallichore API + * Kallichore is a Jupyter kernel gateway and supervisor + * + * The version of the OpenAPI document: 1.0.0 + * Contact: info@posit.co + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { RequestFile } from './models'; + +/** +* The status of the session +*/ +export enum Status { + Uninitialized = 'uninitialized', + Starting = 'starting', + Ready = 'ready', + Idle = 'idle', + Busy = 'busy', + Offline = 'offline', + Exited = 'exited' +} diff --git a/extensions/kallichore-adapter/src/test/README.md b/extensions/kallichore-adapter/src/test/README.md new file mode 100644 index 00000000000..8cbc782a449 --- /dev/null +++ b/extensions/kallichore-adapter/src/test/README.md @@ -0,0 +1,5 @@ +Launch tests by running this from the repository root: + +```sh +yarn test-extension -l kallichore-adapter +``` diff --git a/extensions/kallichore-adapter/src/test/server.test.ts b/extensions/kallichore-adapter/src/test/server.test.ts new file mode 100644 index 00000000000..8d0c0a24fe4 --- /dev/null +++ b/extensions/kallichore-adapter/src/test/server.test.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { API_INSTANCE } from '../extension'; + +// Basic test to ensure the server can start and return status, and is the +// correct version as specified in the package.json. +suite('Server', () => { + test('Server starts and connects', async () => { + // Skip this test if the server is not available (see notes in `install-kallichore-server.ts`) + if (process.env.GITHUB_ACTIONS && process.env.GITHUB_REPOSITORY === 'posit-dev/positron') { + // Skip the test + return; + } + + // Start the server and connect to it + const status = await API_INSTANCE.serverStatus(); + + // Read the package.json file to get the version + const pkg = require('../../package.json'); + + // Ensure the version that the server returned is the same as the + // package.json version + assert.strictEqual(status.version, pkg.positron.binaryDependencies.kallichore); + }); +}); diff --git a/extensions/kallichore-adapter/src/ws/KernelMessage.ts b/extensions/kallichore-adapter/src/ws/KernelMessage.ts new file mode 100644 index 00000000000..c5a985d722f --- /dev/null +++ b/extensions/kallichore-adapter/src/ws/KernelMessage.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SocketMessage } from './SocketMessage'; + +/** + * Represents a status message from the kernel. + */ +export interface KernelMessageStatus extends SocketMessage { + status: string; +} + +/** + * Represents a message from the kernel containing output. + */ +export interface KernelOutputMessage extends SocketMessage { + /** + * A 2-element array containing the stream name and the output text. + */ + output: [string, string]; +} diff --git a/extensions/kallichore-adapter/src/ws/README.md b/extensions/kallichore-adapter/src/ws/README.md new file mode 100644 index 00000000000..9ccb46d3149 --- /dev/null +++ b/extensions/kallichore-adapter/src/ws/README.md @@ -0,0 +1,5 @@ +# WebSocket Messages + +This folder contains the WebSocket message definitions for the Kallichore +server. These are the messages that are sent between the server and the client +over Kallichore's WebSocket connection. diff --git a/extensions/kallichore-adapter/src/ws/SocketMessage.ts b/extensions/kallichore-adapter/src/ws/SocketMessage.ts new file mode 100644 index 00000000000..6f607086b4f --- /dev/null +++ b/extensions/kallichore-adapter/src/ws/SocketMessage.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Represents a message sent over a WebSocket connection from Kallichore and + * received by Positron. Today, Kallichore only sends two kinds of messages. + */ +export enum SocketMessageKind { + /** Jupyter messages are messages conforming to the Jupyter protocol. */ + Jupyter = 'jupyter', + + /** Kernel messages are messages sent by Kallichore to deliver kernel status + * and metadata. */ + Kernel = 'kernel', +} + +/** + * Represents a message received from a WebSocket connection. Every message sent + * from the server over the WebSocket connection will be a SocketMessage. + */ +export interface SocketMessage { + /** The kind of message */ + kind: SocketMessageKind; +} diff --git a/extensions/kallichore-adapter/src/ws/SocketSession.ts b/extensions/kallichore-adapter/src/ws/SocketSession.ts new file mode 100644 index 00000000000..b059ca8a471 --- /dev/null +++ b/extensions/kallichore-adapter/src/ws/SocketSession.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as os from 'os'; +import * as vscode from 'vscode'; +import WebSocket from 'ws'; + +/** + * Represents a session with a WebSocket client. + */ +export class SocketSession implements vscode.Disposable { + public readonly userId: string; + public readonly ws: WebSocket; + + /** + * Create a new session with a WebSocket client. + * + * @param uri The WebSocket URI to connect to + * @param sessionId The session ID to use + */ + constructor( + public readonly uri: string, + public readonly sessionId: string + ) { + // Create a new WebSocket client + this.ws = new WebSocket(uri); + + // Record the current user ID; this is attached to every message sent + // from the client + this.userId = os.userInfo().username; + } + + /** + * Close the WebSocket connection. + */ + close() { + this.ws.close(); + } + + dispose() { + this.close(); + } +} diff --git a/extensions/kallichore-adapter/tsconfig.json b/extensions/kallichore-adapter/tsconfig.json new file mode 100644 index 00000000000..16574b46d07 --- /dev/null +++ b/extensions/kallichore-adapter/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "outDir": "out", + "lib": ["ES2020"], + "sourceMap": true, + "rootDir": "src", + "strict": true, + "typeRoots": ["./node_modules/@types"], + "paths": { + "*": ["./node_modules/*"], + }, + "useUnknownInCatchVariables": false, + "noImplicitAny": false, + "esModuleInterop": true, + + /* Additional Checks */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + }, + "include": [ + "src/**/*", + "../../src/positron-dts/positron.d.ts", + "../../src/vscode-dts/vscode.d.ts", + ], +} diff --git a/extensions/kallichore-adapter/yarn.lock b/extensions/kallichore-adapter/yarn.lock new file mode 100644 index 00000000000..7bdf420261a --- /dev/null +++ b/extensions/kallichore-adapter/yarn.lock @@ -0,0 +1,1344 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@tsconfig/node10@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" + integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + +"@types/caseless@*": + version "0.12.5" + resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.5.tgz#db9468cb1b1b5a925b8f34822f1669df0c5472f5" + integrity sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg== + +"@types/decompress@^4.2.7": + version "4.2.7" + resolved "https://registry.yarnpkg.com/@types/decompress/-/decompress-4.2.7.tgz#604f69b69d519ecb74dea1ea0829f159b85e1332" + integrity sha512-9z+8yjKr5Wn73Pt17/ldnmQToaFHZxK0N1GHysuk/JIPT8RIdQeoInM01wWPgypRcvb6VH1drjuFpQ4zmY437g== + dependencies: + "@types/node" "*" + +"@types/mocha@^9.1.0": + version "9.1.1" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" + integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== + +"@types/node@*": + version "22.5.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.5.tgz#52f939dd0f65fc552a4ad0b392f3c466cc5d7a44" + integrity sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA== + dependencies: + undici-types "~6.19.2" + +"@types/request@^2.48.12": + version "2.48.12" + resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.12.tgz#0f590f615a10f87da18e9790ac94c29ec4c5ef30" + integrity sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw== + dependencies: + "@types/caseless" "*" + "@types/node" "*" + "@types/tough-cookie" "*" + form-data "^2.5.0" + +"@types/tail@^2.2.3": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@types/tail/-/tail-2.2.3.tgz#e7701ae040d0aacf2d78d7cbb71e016f304bc446" + integrity sha512-Hnf352egOlDR4nVTaGX0t/kmTNXHMdovF2C7PVDFtHTHJPFmIspOI1b86vEOxU7SfCq/dADS7ptbqgG/WGGxnA== + +"@types/tough-cookie@*": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" + integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== + +"@types/ws@^8.5.12": + version "8.5.12" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e" + integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ== + dependencies: + "@types/node" "*" + +"@ungap/promise-all-settled@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" + integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== + +acorn-walk@^8.1.1: + version "8.3.4" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" + integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== + dependencies: + acorn "^8.11.0" + +acorn@^8.11.0, acorn@^8.4.1: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + +ajv@^6.12.3: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +asn1@~0.2.3: + version "0.2.6" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== + +aws4@^1.8.0: + version "1.13.2" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.13.2.tgz#0aa167216965ac9474ccfa83892cfb6b3e1e52ef" + integrity sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== + dependencies: + tweetnacl "^0.14.3" + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +bl@^1.0.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7" + integrity sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww== + dependencies: + readable-stream "^2.3.5" + safe-buffer "^5.1.1" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +buffer-alloc-unsafe@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" + integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== + +buffer-alloc@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" + integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== + dependencies: + buffer-alloc-unsafe "^1.1.0" + buffer-fill "^1.0.0" + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== + +buffer-fill@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" + integrity sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ== + +buffer@^5.2.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^2.8.1: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== + dependencies: + assert-plus "^1.0.0" + +debug@4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1" + integrity sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ== + dependencies: + file-type "^5.2.0" + is-stream "^1.1.0" + tar-stream "^1.5.2" + +decompress-tarbz2@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz#3082a5b880ea4043816349f378b56c516be1a39b" + integrity sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A== + dependencies: + decompress-tar "^4.1.0" + file-type "^6.1.0" + is-stream "^1.1.0" + seek-bzip "^1.0.5" + unbzip2-stream "^1.0.9" + +decompress-targz@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/decompress-targz/-/decompress-targz-4.1.1.tgz#c09bc35c4d11f3de09f2d2da53e9de23e7ce1eee" + integrity sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w== + dependencies: + decompress-tar "^4.1.1" + file-type "^5.2.0" + is-stream "^1.1.0" + +decompress-unzip@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/decompress-unzip/-/decompress-unzip-4.0.1.tgz#deaaccdfd14aeaf85578f733ae8210f9b4848f69" + integrity sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw== + dependencies: + file-type "^3.8.0" + get-stream "^2.2.0" + pify "^2.3.0" + yauzl "^2.4.2" + +decompress@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/decompress/-/decompress-4.2.1.tgz#007f55cc6a62c055afa37c07eb6a4ee1b773f118" + integrity sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ== + dependencies: + decompress-tar "^4.0.0" + decompress-tarbz2 "^4.0.0" + decompress-targz "^4.0.0" + decompress-unzip "^4.0.1" + graceful-fs "^4.1.10" + make-dir "^1.0.0" + pify "^2.3.0" + strip-dirs "^2.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +diff@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +end-of-stream@^1.0.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +escalade@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-string-regexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== + +extsprintf@^1.2.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" + integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== + dependencies: + pend "~1.2.0" + +file-type@^3.8.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" + integrity sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA== + +file-type@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-5.2.0.tgz#2ddbea7c73ffe36368dfae49dc338c058c2b8ad6" + integrity sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ== + +file-type@^6.1.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-6.2.0.tgz#e50cd75d356ffed4e306dc4f5bcf52a79903a919" + integrity sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== + +form-data@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" + integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-stream@^2.2.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de" + integrity sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA== + dependencies: + object-assign "^4.0.1" + pinkie-promise "^2.0.0" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== + dependencies: + assert-plus "^1.0.0" + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +graceful-fs@^4.1.10: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q== + +har-validator@~5.1.3: + version "5.1.5" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== + dependencies: + ajv "^6.12.3" + har-schema "^2.0.0" + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ== + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-natural-number@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8" + integrity sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== + +js-yaml@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + +jsprim@^1.2.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" + integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.4.0" + verror "1.10.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +log-symbols@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +make-dir@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== + dependencies: + pify "^3.0.0" + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +minimatch@4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4" + integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^3.0.4: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +mocha@^9.2.1: + version "9.2.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.2.tgz#d70db46bdb93ca57402c809333e5a84977a88fb9" + integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g== + dependencies: + "@ungap/promise-all-settled" "1.1.2" + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.3" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" + growl "1.10.5" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "4.2.1" + ms "2.1.3" + nanoid "3.3.1" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + which "2.0.2" + workerpool "6.2.0" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" + integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +once@^1.3.0, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw== + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +psl@^1.1.28: + version "1.9.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" + integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + +punycode@^2.1.0, punycode@^2.1.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +qs@~6.5.2: + version "6.5.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" + integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +readable-stream@^2.3.0, readable-stream@^2.3.5: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +request@^2.88.2: + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +seek-bzip@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.6.tgz#35c4171f55a680916b52a07859ecf3b5857f21c4" + integrity sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ== + dependencies: + commander "^2.8.1" + +serialize-javascript@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +sshpk@^1.7.0: + version "1.18.0" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028" + integrity sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-dirs@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5" + integrity sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g== + dependencies: + is-natural-number "^4.0.1" + +strip-json-comments@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +tail@^2.2.6: + version "2.2.6" + resolved "https://registry.yarnpkg.com/tail/-/tail-2.2.6.tgz#24abd701963639b896c42496d5f416216ec0b558" + integrity sha512-IQ6G4wK/t8VBauYiGPLx+d3fA5XjSVagjWV5SIYzvEvglbQjwEcukeYI68JOPpdydjxhZ9sIgzRlSmwSpphHyw== + +tar-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" + integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A== + dependencies: + bl "^1.0.0" + buffer-alloc "^1.2.0" + end-of-stream "^1.0.0" + fs-constants "^1.0.0" + readable-stream "^2.3.0" + to-buffer "^1.1.1" + xtend "^4.0.0" + +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +to-buffer@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" + integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +ts-node@^10.9.1: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== + +unbzip2-stream@^1.0.9: + version "1.4.3" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" + integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== + dependencies: + buffer "^5.2.1" + through "^2.3.8" + +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +which@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +workerpool@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" + integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@^8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yauzl@^2.4.2: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/extensions/positron-python/src/client/jupyter-adapter.d.ts b/extensions/positron-python/src/client/jupyter-adapter.d.ts index c60889e8bd5..d82e85fc32c 100644 --- a/extensions/positron-python/src/client/jupyter-adapter.d.ts +++ b/extensions/positron-python/src/client/jupyter-adapter.d.ts @@ -8,6 +8,32 @@ import * as vscode from 'vscode'; // eslint-disable-next-line import/no-unresolved import * as positron from 'positron'; +export interface JupyterSessionState { + /** The Jupyter session identifier; sent as part of every message */ + sessionId: string; + + /** The log file the kernel is writing to */ + logFile: string; + + /** The profile file the kernel is writing to */ + profileFile?: string; + + /** The connection file specifying the ZeroMQ ports, signing keys, etc. */ + connectionFile: string; + + /** The ID of the kernel's process, or 0 if the process is not running */ + processId: number; +} + +export interface JupyterSession { + readonly state: JupyterSessionState; +} + +export interface JupyterKernel { + connectToSession(session: JupyterSession): Promise; + log(msg: string): void; +} + /** * This set of type definitions defines the interfaces used by the Positron * Jupyter Adapter extension. @@ -34,6 +60,12 @@ export interface JupyterKernelSpec { /** Environment variables to set when starting the kernel */ env?: NodeJS.ProcessEnv; + + /** Function that starts the kernel given a JupyterSession object. + * This is used to start the kernel if it's provided. In this case `argv` + * is ignored. + */ + startKernel?: (session: JupyterSession, kernel: JupyterKernel) => Promise; } /** @@ -110,7 +142,7 @@ export interface JupyterAdapterApi extends vscode.Disposable { kernel: JupyterKernelSpec, dynState: positron.LanguageRuntimeDynState, extra?: JupyterKernelExtra | undefined, - ): JupyterLanguageRuntimeSession; + ): Promise; /** * Restore a session for a Jupyter-compatible kernel. @@ -124,7 +156,7 @@ export interface JupyterAdapterApi extends vscode.Disposable { restoreSession( runtimeMetadata: positron.LanguageRuntimeMetadata, sessionMetadata: positron.RuntimeSessionMetadata, - ): JupyterLanguageRuntimeSession; + ): Promise; /** * Finds an available TCP port for a server diff --git a/extensions/positron-python/src/client/positron/manager.ts b/extensions/positron-python/src/client/positron/manager.ts index 068f628b105..85fac83afa1 100644 --- a/extensions/positron-python/src/client/positron/manager.ts +++ b/extensions/positron-python/src/client/positron/manager.ts @@ -9,6 +9,7 @@ import * as portfinder from 'portfinder'; import * as positron from 'positron'; import * as path from 'path'; import * as fs from 'fs-extra'; +import * as os from 'os'; import { Event, EventEmitter } from 'vscode'; import { inject, injectable } from 'inversify'; @@ -186,6 +187,9 @@ export class PythonRuntimeManager implements IPythonRuntimeManager { argv: args, display_name: `${runtimeMetadata.runtimeName}`, language: 'Python', + // On Windows, we need to use the 'signal' interrupt mode since 'message' is + // not supported. + interrupt_mode: os.platform() === 'win32' ? 'signal' : 'message', env, }; diff --git a/extensions/positron-python/src/client/positron/session.ts b/extensions/positron-python/src/client/positron/session.ts index 5f649ada3b0..7491c995721 100644 --- a/extensions/positron-python/src/client/positron/session.ts +++ b/extensions/positron-python/src/client/positron/session.ts @@ -426,17 +426,32 @@ export class PythonRuntimeSession implements positron.LanguageRuntimeSession, vs } private async createKernel(): Promise { - const ext = vscode.extensions.getExtension('vscode.jupyter-adapter'); - if (!ext) { - throw new Error('Jupyter Adapter extension not found'); - } - if (!ext.isActive) { - await ext.activate(); + const config = vscode.workspace.getConfiguration('kallichoreSupervisor'); + const useKallichore = config.get('enable', false); + if (useKallichore) { + // Use the Kallichore supervisor if enabled + const ext = vscode.extensions.getExtension('vscode.kallichore-adapter'); + if (!ext) { + throw new Error('Kallichore Adapter extension not found'); + } + if (!ext.isActive) { + await ext.activate(); + } + this.adapterApi = ext?.exports as JupyterAdapterApi; + } else { + // Otherwise, connect to the Jupyter kernel directly + const ext = vscode.extensions.getExtension('vscode.jupyter-adapter'); + if (!ext) { + throw new Error('Jupyter Adapter extension not found'); + } + if (!ext.isActive) { + await ext.activate(); + } + this.adapterApi = ext?.exports as JupyterAdapterApi; } - this.adapterApi = ext?.exports as JupyterAdapterApi; const kernel = this.kernelSpec ? // We have a kernel spec, so we're creating a new session - this.adapterApi.createSession( + await this.adapterApi.createSession( this.runtimeMetadata, this.metadata, this.kernelSpec, @@ -444,7 +459,7 @@ export class PythonRuntimeSession implements positron.LanguageRuntimeSession, vs createJupyterKernelExtra(), ) : // We don't have a kernel spec, so we're restoring a session - this.adapterApi.restoreSession(this.runtimeMetadata, this.metadata); + await this.adapterApi.restoreSession(this.runtimeMetadata, this.metadata); kernel.onDidChangeRuntimeState((state) => { this._stateEmitter.fire(state); diff --git a/extensions/positron-r/src/jupyter-adapter.d.ts b/extensions/positron-r/src/jupyter-adapter.d.ts index 2403c01dfc2..724f5b4f95d 100644 --- a/extensions/positron-r/src/jupyter-adapter.d.ts +++ b/extensions/positron-r/src/jupyter-adapter.d.ts @@ -8,6 +8,32 @@ import * as vscode from 'vscode'; // eslint-disable-next-line import/no-unresolved import * as positron from 'positron'; +export interface JupyterSessionState { + /** The Jupyter session identifier; sent as part of every message */ + sessionId: string; + + /** The log file the kernel is writing to */ + logFile: string; + + /** The profile file the kernel is writing to */ + profileFile?: string; + + /** The connection file specifying the ZeroMQ ports, signing keys, etc. */ + connectionFile: string; + + /** The ID of the kernel's process, or 0 if the process is not running */ + processId: number; +} + +export interface JupyterSession { + readonly state: JupyterSessionState; +} + +export interface JupyterKernel { + connectToSession(session: JupyterSession): Promise; + log(msg: string): void; +} + /** * This set of type definitions defines the interfaces used by the Positron * Jupyter Adapter extension. @@ -34,6 +60,12 @@ export interface JupyterKernelSpec { /** Environment variables to set when starting the kernel */ env?: NodeJS.ProcessEnv; + + /** Function that starts the kernel given a JupyterSession object. + * This is used to start the kernel if it's provided. In this case `argv` + * is ignored. + */ + startKernel?: (session: JupyterSession, kernel: JupyterKernel) => Promise; } /** @@ -78,11 +110,6 @@ export interface JupyterLanguageRuntimeSession extends positron.LanguageRuntimeS */ showOutput(): void; - /** - * Show profiler log if supported. - */ - showProfile?(): Thenable; - /** * A Jupyter kernel is guaranteed to have a `callMethod()` method; it uses * the frontend comm to send a message to the kernel and wait for a @@ -120,7 +147,7 @@ export interface JupyterAdapterApi extends vscode.Disposable { kernel: JupyterKernelSpec, dynState: positron.LanguageRuntimeDynState, extra?: JupyterKernelExtra | undefined, - ): JupyterLanguageRuntimeSession; + ): Promise; /** * Restore a session for a Jupyter-compatible kernel. @@ -134,7 +161,7 @@ export interface JupyterAdapterApi extends vscode.Disposable { restoreSession( runtimeMetadata: positron.LanguageRuntimeMetadata, sessionMetadata: positron.RuntimeSessionMetadata - ): JupyterLanguageRuntimeSession; + ): Promise; /** * Finds an available TCP port for a server diff --git a/extensions/positron-r/src/kallichore-adapter.d.ts b/extensions/positron-r/src/kallichore-adapter.d.ts new file mode 100644 index 00000000000..86804553d98 --- /dev/null +++ b/extensions/positron-r/src/kallichore-adapter.d.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + + +// eslint-disable-next-line import/no-unresolved +import * as positron from 'positron'; +import { JupyterAdapterApi, JupyterKernelExtra, JupyterKernelSpec, JupyterLanguageRuntimeSession } from './jupyter-adapter'; + + +/** + * The Kallichore Adapter API as exposed by the Kallichore Adapter extension. + */ +export interface KallichoreAdapterApi extends JupyterAdapterApi { + + /** + * Create a session for a Jupyter-compatible kernel. + * + * @param runtimeMetadata The metadata for the language runtime to be + * wrapped by the adapter. + * @param sessionMetadata The metadata for the session to be created. + * @param kernel A Jupyter kernel spec containing the information needed to + * start the kernel. + * @param dynState The initial dynamic state of the session. + * @param extra Optional implementations for extra functionality. + * + * @returns A JupyterLanguageRuntimeSession that wraps the kernel. + */ + createSession( + runtimeMetadata: positron.LanguageRuntimeMetadata, + sessionMetadata: positron.RuntimeSessionMetadata, + kernel: JupyterKernelSpec, + dynState: positron.LanguageRuntimeDynState, + extra?: JupyterKernelExtra | undefined, + ): Promise; + + /** + * Restore a session for a Jupyter-compatible kernel. + * + * @param runtimeMetadata The metadata for the language runtime to be + * wrapped by the adapter. + * @param sessionMetadata The metadata for the session to be reconnected. + * + * @returns A JupyterLanguageRuntimeSession that wraps the kernel. + */ + restoreSession( + runtimeMetadata: positron.LanguageRuntimeMetadata, + sessionMetadata: positron.RuntimeSessionMetadata + ): Promise; +} diff --git a/extensions/positron-r/src/session.ts b/extensions/positron-r/src/session.ts index b61267a5ce7..c9f57cc2cbe 100644 --- a/extensions/positron-r/src/session.ts +++ b/extensions/positron-r/src/session.ts @@ -469,19 +469,34 @@ export class RSession implements positron.LanguageRuntimeSession, vscode.Disposa } private async createKernel(): Promise { - const ext = vscode.extensions.getExtension('vscode.jupyter-adapter'); - if (!ext) { - throw new Error('Jupyter Adapter extension not found'); - } - if (!ext.isActive) { - await ext.activate(); + const config = vscode.workspace.getConfiguration('kallichoreSupervisor'); + const useKallichore = config.get('enable', false); + if (useKallichore) { + // Use the Kallichore supervisor if enabled + const ext = vscode.extensions.getExtension('vscode.kallichore-adapter'); + if (!ext) { + throw new Error('Kallichore Adapter extension not found'); + } + if (!ext.isActive) { + await ext.activate(); + } + this.adapterApi = ext?.exports as JupyterAdapterApi; + } else { + // Otherwise, connect to the Jupyter kernel directly + const ext = vscode.extensions.getExtension('vscode.jupyter-adapter'); + if (!ext) { + throw new Error('Jupyter Adapter extension not found'); + } + if (!ext.isActive) { + await ext.activate(); + } + this.adapterApi = ext?.exports as JupyterAdapterApi; } - this.adapterApi = ext?.exports as JupyterAdapterApi; // Create the Jupyter session const kernel = this.kernelSpec ? // We have a kernel spec, so create a new session - this.adapterApi.createSession( + await this.adapterApi.createSession( this.runtimeMetadata, this.metadata, this.kernelSpec, @@ -489,7 +504,7 @@ export class RSession implements positron.LanguageRuntimeSession, vscode.Disposa this.extra) : // We don't have a kernel spec, so restore (reconnect) the session - this.adapterApi.restoreSession( + await this.adapterApi.restoreSession( this.runtimeMetadata, this.metadata); diff --git a/extensions/positron-reticulate/src/jupyter-adapter.d.ts b/extensions/positron-reticulate/src/jupyter-adapter.d.ts index 58a1fd01442..724f5b4f95d 100644 --- a/extensions/positron-reticulate/src/jupyter-adapter.d.ts +++ b/extensions/positron-reticulate/src/jupyter-adapter.d.ts @@ -65,7 +65,7 @@ export interface JupyterKernelSpec { * This is used to start the kernel if it's provided. In this case `argv` * is ignored. */ - startKernel?: (session: JupyterSession, self: any) => Promise; + startKernel?: (session: JupyterSession, kernel: JupyterKernel) => Promise; } /** @@ -147,7 +147,7 @@ export interface JupyterAdapterApi extends vscode.Disposable { kernel: JupyterKernelSpec, dynState: positron.LanguageRuntimeDynState, extra?: JupyterKernelExtra | undefined, - ): JupyterLanguageRuntimeSession; + ): Promise; /** * Restore a session for a Jupyter-compatible kernel. @@ -161,7 +161,7 @@ export interface JupyterAdapterApi extends vscode.Disposable { restoreSession( runtimeMetadata: positron.LanguageRuntimeMetadata, sessionMetadata: positron.RuntimeSessionMetadata - ): JupyterLanguageRuntimeSession; + ): Promise; /** * Finds an available TCP port for a server diff --git a/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts b/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts index 5b33adb3ea7..8e77583864a 100644 --- a/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts +++ b/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts @@ -967,6 +967,13 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession this.waitForReconnect(session); break; + case RuntimeState.Starting: + this._onWillStartRuntimeEmitter.fire({ + session, + isNew: true + } satisfies IRuntimeSessionWillStartEvent); + break; + case RuntimeState.Exited: // Remove the runtime from the set of starting or running runtimes. this._startingConsolesByLanguageId.delete(session.runtimeMetadata.languageId); @@ -1000,10 +1007,10 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession })); this._register(session.onDidEndSession(async exit => { - // If the runtime is restarting and has just exited, let Positron know that it's - // about to start again. Note that we need to do this on the next tick since we - // need to ensure all the event handlers for the state change we - // are currently processing have been called (i.e. everyone knows it has exited) + // Note that we need to do this on the next tick since we need to + // ensure all the event handlers for the state change we are + // currently processing have been called (i.e. everyone knows it has + // exited) setTimeout(() => { const sessionInfo = this._activeSessionsBySessionId.get(session.sessionId); if (!sessionInfo) { @@ -1011,14 +1018,6 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession `Session ${formatLanguageRuntimeSession(session)} is not active.`); return; } - if (sessionInfo.state === RuntimeState.Exited && - exit.reason === RuntimeExitReason.Restart) { - const evt: IRuntimeSessionWillStartEvent = { - session, - isNew: true - }; - this._onWillStartRuntimeEmitter.fire(evt); - } // If a workspace session ended because the extension host was // disconnected, remember it so we can attempt to reconnect it