diff --git a/package.json b/package.json
index e82c44f9..438364df 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"dist:osx-64": "yarn dist:osx",
"dist:osx-arm64": "yarn dist:osx",
"dist:osx-dev": "yarn build && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --macos --publish never",
+ "dist:osx-arm64-dev": "yarn build && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --macos --arm64 --publish never",
"dist:win-64": "yarn build && electron-builder --win --publish never",
"dist:win-arm64": "yarn build && yarn electron-builder --arm64 --publish never",
"update_workflow_conda_lock": "cd workflow_env && rimraf *.lock && conda-lock --kind explicit -f publish_env.yaml && cd -",
diff --git a/src/main/app.ts b/src/main/app.ts
index 9babbf0e..43d1ea20 100644
--- a/src/main/app.ts
+++ b/src/main/app.ts
@@ -24,6 +24,7 @@ import {
installBundledEnvironment,
isDarkTheme,
pythonPathForEnvPath,
+ setupJlabCLICommandWithElevatedRights,
waitForDuration
} from './utils';
import { IServerFactory, JupyterServerFactory } from './server';
@@ -1201,6 +1202,29 @@ export class JupyterApplication implements IApplication, IDisposable {
return true;
}
);
+
+ this._evm.registerSyncEventHandler(
+ EventTypeMain.SetupCLICommandWithElevatedRights,
+ async event => {
+ const showSetupErrorMessage = () => {
+ dialog.showErrorBox(
+ 'CLI setup error',
+ 'Failed to setup jlab CLI command! Please see logs for details.'
+ );
+ };
+
+ try {
+ const succeeded = await setupJlabCLICommandWithElevatedRights();
+ if (!succeeded) {
+ showSetupErrorMessage();
+ }
+ return succeeded;
+ } catch (error) {
+ showSetupErrorMessage();
+ return false;
+ }
+ }
+ );
}
private _showUpdateDialog(
diff --git a/src/main/eventtypes.ts b/src/main/eventtypes.ts
index 7234ae7e..883205c4 100644
--- a/src/main/eventtypes.ts
+++ b/src/main/eventtypes.ts
@@ -81,7 +81,8 @@ export enum EventTypeMain {
SetSystemPythonPath = 'set-system-python-path',
CopySessionInfoToClipboard = 'copy-session-info-to-clipboard',
RestartSession = 'restart-session',
- SetSettings = 'set-settings'
+ SetSettings = 'set-settings',
+ SetupCLICommandWithElevatedRights = 'setup-cli-command'
}
// events sent to Renderer process
diff --git a/src/main/main.ts b/src/main/main.ts
index 95c73698..114a9cb2 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -5,16 +5,16 @@ import * as semver from 'semver';
import {
bundledEnvironmentIsInstalled,
EnvironmentInstallStatus,
- getAppDir,
getBundledPythonEnvPath,
getBundledPythonPath,
installBundledEnvironment,
isDevMode,
+ jlabCLICommandIsSetup,
+ setupJlabCommandWithUserRights,
versionWithoutSuffix,
waitForDuration,
waitForFunction
} from './utils';
-import { execSync } from 'child_process';
import { JupyterApplication } from './app';
import { ICLIArguments } from './tokens';
import { SessionConfig } from './config/sessionconfig';
@@ -130,25 +130,11 @@ function setupJLabCommand() {
return;
}
- const symlinkPath = '/usr/local/bin/jlab';
- const targetPath = `${getAppDir()}/app/jlab`;
-
- if (!fs.existsSync(targetPath)) {
+ if (jlabCLICommandIsSetup()) {
return;
}
- try {
- if (!fs.existsSync(symlinkPath)) {
- const cmd = `ln -s ${targetPath} ${symlinkPath}`;
- execSync(cmd, { shell: '/bin/bash' });
- fs.chmodSync(symlinkPath, 0o755);
- }
-
- // after a DMG install, mode resets
- fs.chmodSync(targetPath, 0o755);
- } catch (error) {
- log.error(error);
- }
+ setupJlabCommandWithUserRights();
}
function createPythonEnvsDirectory() {
diff --git a/src/main/settingsdialog/preload.ts b/src/main/settingsdialog/preload.ts
index e583dc88..6aa9973f 100644
--- a/src/main/settingsdialog/preload.ts
+++ b/src/main/settingsdialog/preload.ts
@@ -78,6 +78,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
},
setSettings: (settings: { [key: string]: any }) => {
ipcRenderer.send(EventTypeMain.SetSettings, settings);
+ },
+ setupCLICommand: () => {
+ return ipcRenderer.invoke(EventTypeMain.SetupCLICommandWithElevatedRights);
}
});
diff --git a/src/main/settingsdialog/settingsdialog.ts b/src/main/settingsdialog/settingsdialog.ts
index be374829..f92efe85 100644
--- a/src/main/settingsdialog/settingsdialog.ts
+++ b/src/main/settingsdialog/settingsdialog.ts
@@ -16,6 +16,7 @@ import {
ThemeType
} from '../config/settings';
import { IRegistry } from '../registry';
+import { jlabCLICommandIsSetup } from '../utils';
export class SettingsDialog {
constructor(options: SettingsDialog.IOptions, registry: IRegistry) {
@@ -23,7 +24,7 @@ export class SettingsDialog {
isDarkTheme: options.isDarkTheme,
title: 'Settings',
width: 700,
- height: 450,
+ height: 500,
preload: path.join(__dirname, './preload.js')
});
@@ -45,6 +46,7 @@ export class SettingsDialog {
const installUpdatesAutomaticallyEnabled = process.platform === 'darwin';
const installUpdatesAutomatically =
installUpdatesAutomaticallyEnabled && options.installUpdatesAutomatically;
+ const cliCommandIsSetup = jlabCLICommandIsSetup();
let strServerEnvVars = '';
if (Object.keys(serverEnvVars).length > 0) {
@@ -336,6 +338,19 @@ export class SettingsDialog {
+
+
@@ -376,6 +391,9 @@ export class SettingsDialog {
const autoInstallCheckbox = document.getElementById('checkbox-update-install');
const notifyOnBundledEnvUpdatesCheckbox = document.getElementById('notify-on-bundled-env-updates');
const updateBundledEnvAutomaticallyCheckbox = document.getElementById('update-bundled-env-automatically');
+ const setupCLICommandButton = document.getElementById('setup-cli-command-button');
+ const setupCLICommandLabel = document.getElementById('setup-cli-command-label');
+ let cliCommandIsSetup = <%= cliCommandIsSetup %>;
function handleAutoCheckForUpdates(el) {
updateAutoInstallCheckboxState();
@@ -398,11 +416,29 @@ export class SettingsDialog {
window.electronAPI.showLogs();
}
+ function handleSetupCLICommand(el) {
+ window.electronAPI.setupCLICommand().then(result => {
+ cliCommandIsSetup = result;
+ updateCLICommandSetupStatus();
+ });
+ }
+
+ function updateCLICommandSetupStatus() {
+ if (cliCommandIsSetup) {
+ setupCLICommandButton.style.display = 'none';
+ setupCLICommandLabel.innerHTML = 'jlab CLI command is ready to use in your system terminal!';
+ } else {
+ setupCLICommandButton.style.display = 'block';
+ setupCLICommandLabel.innerHTML = 'CLI command is not set up yet. Click to set up now. This requires elevated permissions.';
+ }
+ }
+
function onLogLevelChanged(el) {
window.electronAPI.setLogLevel(el.value);
}
document.addEventListener("DOMContentLoaded", () => {
+ updateCLICommandSetupStatus();
updateAutoInstallCheckboxState();
});
@@ -480,7 +516,8 @@ export class SettingsDialog {
serverArgs,
overrideDefaultServerArgs,
serverEnvVars: strServerEnvVars,
- ctrlWBehavior
+ ctrlWBehavior,
+ cliCommandIsSetup
});
}
diff --git a/src/main/utils.ts b/src/main/utils.ts
index 3630457c..1692d62e 100644
--- a/src/main/utils.ts
+++ b/src/main/utils.ts
@@ -10,7 +10,13 @@ import log from 'electron-log';
import { AddressInfo, createServer, Socket } from 'net';
import { app, nativeTheme } from 'electron';
import { IPythonEnvironment } from './tokens';
-import { exec, execFile, ExecFileOptions, execFileSync } from 'child_process';
+import {
+ exec,
+ execFile,
+ ExecFileOptions,
+ execFileSync,
+ execSync
+} from 'child_process';
export const DarkThemeBGColor = '#212121';
export const LightThemeBGColor = '#ffffff';
@@ -710,3 +716,117 @@ export function launchTerminalInDirectory(
exec(`gnome-terminal --working-directory="${dirPath}"${callCommands}`);
}
}
+
+export function getJlabCLICommandSymlinkPath(): string {
+ if (process.platform === 'darwin') {
+ return '/usr/local/bin/jlab';
+ }
+}
+
+export function getJlabCLICommandTargetPath(): string {
+ if (process.platform === 'darwin') {
+ return `${getAppDir()}/app/jlab`;
+ }
+}
+
+export function jlabCLICommandIsSetup(): boolean {
+ if (process.platform !== 'darwin') {
+ return true;
+ }
+
+ const symlinkPath = getJlabCLICommandSymlinkPath();
+ const targetPath = getJlabCLICommandTargetPath();
+
+ if (!fs.existsSync(symlinkPath)) {
+ return false;
+ }
+
+ const stats = fs.lstatSync(symlinkPath);
+ if (!stats.isSymbolicLink()) {
+ return false;
+ }
+
+ try {
+ fs.accessSync(targetPath, fs.constants.X_OK);
+ } catch (error) {
+ log.error('App CLI is not executable', error);
+ return false;
+ }
+
+ return fs.readlinkSync(symlinkPath) === targetPath;
+}
+
+export async function setupJlabCLICommandWithElevatedRights(): Promise<
+ boolean
+> {
+ if (process.platform !== 'darwin') {
+ return false;
+ }
+
+ const symlinkPath = getJlabCLICommandSymlinkPath();
+ const targetPath = getJlabCLICommandTargetPath();
+
+ if (!fs.existsSync(targetPath)) {
+ log.error(`Target path "${targetPath}" does not exist! `);
+ return false;
+ }
+
+ const shellCommands: string[] = [];
+ const symlinkParentDir = path.dirname(symlinkPath);
+
+ // create parent directory
+ if (!fs.existsSync(symlinkParentDir)) {
+ shellCommands.push(`mkdir -p ${symlinkParentDir}`);
+ }
+
+ // create symlink
+ shellCommands.push(`ln -f -s \\"${targetPath}\\" \\"${symlinkPath}\\"`);
+
+ // make files executable
+ shellCommands.push(`chmod 755 \\"${symlinkPath}\\"`);
+ shellCommands.push(`chmod 755 \\"${targetPath}\\"`);
+
+ const command = `do shell script "${shellCommands.join(
+ ' && '
+ )}" with administrator privileges`;
+
+ return new Promise((resolve, reject) => {
+ const cliSetupProc = exec(`osascript -e '${command}'`);
+
+ cliSetupProc.on('exit', (exitCode: number) => {
+ if (exitCode === 0) {
+ resolve(true);
+ } else {
+ log.error(`Failed to setup CLI with exit code ${exitCode}`);
+ reject();
+ }
+ });
+
+ cliSetupProc.on('error', (err: Error) => {
+ log.error(err);
+ reject();
+ });
+ });
+}
+
+export async function setupJlabCommandWithUserRights() {
+ const symlinkPath = getJlabCLICommandSymlinkPath();
+ const targetPath = getJlabCLICommandTargetPath();
+
+ if (!fs.existsSync(targetPath)) {
+ return;
+ }
+
+ try {
+ if (!fs.existsSync(symlinkPath)) {
+ const cmd = `ln -s ${targetPath} ${symlinkPath}`;
+ execSync(cmd, { shell: '/bin/bash' });
+ fs.chmodSync(symlinkPath, 0o755);
+ }
+
+ // after a DMG install, mode resets
+ fs.chmodSync(targetPath, 0o755);
+ } catch (error) {
+ log.error(error);
+ }
+}