myst-cli
Version:
Command line tools for MyST
207 lines (206 loc) • 8.55 kB
JavaScript
import path from 'node:path';
import { createStore } from 'redux';
import { chalkLogger, LogLevel } from 'myst-cli-utils';
import latestVersion from 'latest-version';
import boxen from 'boxen';
import chalk from 'chalk';
import { HttpsProxyAgent } from 'https-proxy-agent';
import pLimit from 'p-limit';
import { findCurrentProjectAndLoad, findCurrentSiteAndLoad, reloadAllConfigsForCurrentSite, } from '../config.js';
import { loadPlugins } from '../plugins.js';
import { selectors } from '../store/index.js';
import { rootReducer } from '../store/reducers.js';
import version from '../version.js';
import { isWhiteLabelled } from '../utils/whiteLabelling.js';
import { KernelManager, ServerConnection, SessionManager } from '@jupyterlab/services';
import { launchJupyterServer } from 'myst-execute';
import { default as nodeFetch, Headers, Request, Response } from 'node-fetch';
// fetch polyfill for node<18
if (!globalThis.fetch) {
globalThis.fetch = nodeFetch;
globalThis.Headers = Headers;
globalThis.Request = Request;
globalThis.Response = Response;
}
const CONFIG_FILES = ['myst.yml'];
const API_URL = 'https://api.mystmd.org';
const NPM_COMMAND = 'npm i -g mystmd@latest';
const PIP_COMMAND = 'pip install -U mystmd';
const LOCALHOSTS = ['localhost', '127.0.0.1', '::1'];
function socialLink({ twitter, bsky }) {
if (bsky) {
return `Follow ${chalk.yellowBright(`@${bsky}`)} for updates!\nhttps://bsky.app/profile/${bsky}`;
}
if (twitter) {
return `Follow ${chalk.yellowBright(`@${twitter}`)} for updates!\nhttps://x.com/${twitter}`;
}
return '';
}
export function logUpdateAvailable({ current, latest, upgradeCommand, twitter, bsky, }) {
return boxen(`Update available! ${chalk.dim(`v${current}`)} ≫ ${chalk.green.bold(`v${latest}`)}\n\nRun \`${chalk.cyanBright.bold(upgradeCommand)}\` to update.\n\n${socialLink({ bsky, twitter })}`, {
padding: 1,
margin: 1,
borderColor: 'green',
borderStyle: 'round',
textAlignment: 'center',
});
}
export class Session {
get log() {
return this.$logger;
}
constructor(opts = {}) {
var _a, _b;
this._shownUpgrade = false;
this._clones = [];
this.API_URL = API_URL;
this.configFiles = CONFIG_FILES;
this.$logger = (_a = opts.logger) !== null && _a !== void 0 ? _a : chalkLogger(LogLevel.info, process.cwd());
this.doiLimiter = (_b = opts.doiLimiter) !== null && _b !== void 0 ? _b : pLimit(3);
const proxyUrl = process.env.HTTPS_PROXY;
if (proxyUrl)
this.proxyAgent = new HttpsProxyAgent(proxyUrl);
this.store = createStore(rootReducer);
// Allow the latest version to be loaded
latestVersion('mystmd')
.then((latest) => {
this._latestVersion = latest;
})
.catch(() => null);
}
showUpgradeNotice() {
if (this._shownUpgrade ||
!this._latestVersion ||
version === this._latestVersion ||
isWhiteLabelled())
return;
this.log.info(logUpdateAvailable({
current: version,
latest: this._latestVersion,
upgradeCommand: process.env.MYST_LANG === 'PYTHON' ? PIP_COMMAND : NPM_COMMAND,
bsky: 'mystmd.org',
}));
this._shownUpgrade = true;
}
async reload() {
await findCurrentProjectAndLoad(this, '.');
await findCurrentSiteAndLoad(this, '.');
if (selectors.selectCurrentSitePath(this.store.getState())) {
await reloadAllConfigsForCurrentSite(this);
}
return this;
}
async fetch(url, init) {
var _a;
const urlOnly = new URL((_a = url.url) !== null && _a !== void 0 ? _a : url);
this.log.debug(`Fetching: ${urlOnly}`);
if (this.proxyAgent && !LOCALHOSTS.includes(urlOnly.hostname)) {
if (!init)
init = {};
init = { agent: this.proxyAgent, ...init };
this.log.debug(`Using HTTPS proxy: ${this.proxyAgent.proxy}`);
}
const logData = { url: urlOnly, done: false };
setTimeout(() => {
if (!logData.done)
this.log.info(`⏳ Waiting for response from ${url}`);
}, 5000);
const resp = await nodeFetch(url, init);
logData.done = true;
return resp;
}
async loadPlugins(plugins) {
this.plugins = await loadPlugins(this, plugins);
return this.plugins;
}
sourcePath() {
var _a;
const state = this.store.getState();
const sitePath = selectors.selectCurrentSitePath(state);
const projectPath = selectors.selectCurrentProjectPath(state);
const root = (_a = sitePath !== null && sitePath !== void 0 ? sitePath : projectPath) !== null && _a !== void 0 ? _a : '.';
return path.resolve(root);
}
buildPath() {
return path.join(this.sourcePath(), '_build');
}
sitePath() {
return path.join(this.buildPath(), 'site');
}
contentPath() {
return path.join(this.sitePath(), 'content');
}
publicPath() {
return path.join(this.sitePath(), 'public');
}
async clone() {
const cloneSession = new Session({ logger: this.log, doiLimiter: this.doiLimiter });
await cloneSession.reload();
// TODO: clean this up through better state handling
cloneSession._jupyterSessionManagerPromise = this._jupyterSessionManagerPromise;
this._clones.push(cloneSession);
return cloneSession;
}
getAllWarnings(ruleId) {
const stringWarnings = [];
const warnings = [];
[this, ...this._clones].forEach((session) => {
const sessionWarnings = selectors.selectFileWarningsByRule(session.store.getState(), ruleId);
sessionWarnings.forEach((warning) => {
const stringWarning = JSON.stringify(Object.entries(warning).sort());
if (!stringWarnings.includes(stringWarning)) {
stringWarnings.push(stringWarning);
warnings.push(warning);
}
});
});
return warnings;
}
jupyterSessionManager() {
if (this._jupyterSessionManagerPromise === undefined) {
this._jupyterSessionManagerPromise = this.createJupyterSessionManager();
}
return this._jupyterSessionManagerPromise;
}
async createJupyterSessionManager() {
try {
let partialServerSettings;
// Load from environment
if (process.env.JUPYTER_BASE_URL !== undefined) {
partialServerSettings = {
baseUrl: process.env.JUPYTER_BASE_URL,
token: process.env.JUPYTER_TOKEN,
};
}
else {
// Note: To use an existing Jupyter server use `findExistingJupyterServer`, see #1716
this.log.debug(`Launching jupyter server on ${this.sourcePath()}`);
// Create and load new server
partialServerSettings = await launchJupyterServer(this.sourcePath(), this.log);
}
const serverSettings = ServerConnection.makeSettings(partialServerSettings);
const kernelManager = new KernelManager({ serverSettings });
const manager = new SessionManager({ kernelManager, serverSettings });
// Tie the lifetime of the kernelManager and (potential) spawned server to the manager
manager.disposed.connect(() => {
var _a;
kernelManager.dispose();
(_a = partialServerSettings === null || partialServerSettings === void 0 ? void 0 : partialServerSettings.dispose) === null || _a === void 0 ? void 0 : _a.call(partialServerSettings);
});
return manager;
}
catch (err) {
this.log.error('Unable to instantiate connection to Jupyter Server', err);
return undefined;
}
}
dispose() {
this._clones.forEach((session) => {
session.dispose();
});
if (this._jupyterSessionManagerPromise) {
this._jupyterSessionManagerPromise.then((manager) => { var _a; return (_a = manager === null || manager === void 0 ? void 0 : manager.dispose) === null || _a === void 0 ? void 0 : _a.call(manager); });
this._jupyterSessionManagerPromise = undefined;
}
}
}