From 8ee48cae1db9247cc36e5f793443b176964e090c Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Fri, 16 Feb 2024 19:11:41 -0800 Subject: [PATCH 1/3] add new CLI commands for settings appdata and logs --- src/main/cli.ts | 322 +++++++++++++++++++++++++++++++++++- src/main/config/appdata.ts | 6 +- src/main/config/settings.ts | 52 ++++-- src/main/main.ts | 4 +- 4 files changed, 361 insertions(+), 23 deletions(-) diff --git a/src/main/cli.ts b/src/main/cli.ts index 9e676f3c..9c7afd94 100644 --- a/src/main/cli.ts +++ b/src/main/cli.ts @@ -7,6 +7,7 @@ import { getBundledEnvInstallerPath, getBundledPythonEnvPath, getBundledPythonPath, + getLogFilePath, installCondaPackEnvironment, isBaseCondaEnv, isEnvInstalledByDesktopApp, @@ -16,11 +17,16 @@ import { import yargs from 'yargs/yargs'; import * as fs from 'fs'; import * as path from 'path'; -import { appData } from './config/appdata'; +import { appData, ApplicationData } from './config/appdata'; import { IEnvironmentType, IPythonEnvironment } from './tokens'; -import { SettingType, userSettings } from './config/settings'; +import { + SettingType, + UserSettings, + userSettings, + WorkspaceSettings +} from './config/settings'; import { Registry } from './registry'; -import { app } from 'electron'; +import { app, shell } from 'electron'; import { condaEnvPathForCondaExePath, getCondaChannels, @@ -188,6 +194,97 @@ export function parseCLIArgs(argv: string[]) { } } ) + .command( + 'config ', + 'Manage JupyterLab Desktop settings', + yargs => { + yargs + .positional('action', { + describe: 'Setting action', + choices: ['list', 'set', 'unset'], + default: 'list' + }) + .option('project', { + describe: 'Set config for project at current working directory', + type: 'boolean', + default: false + }) + .option('project-path', { + describe: 'Set / list config for project at specified path', + type: 'string' + }); + }, + async argv => { + console.log('Note: This is an experimental feature.'); + + const action = argv.action; + switch (action) { + case 'list': + handleConfigListCommand(argv); + break; + case 'set': + handleConfigSetCommand(argv); + break; + case 'unset': + handleConfigUnsetCommand(argv); + break; + default: + console.log('Invalid input for "config" command.'); + break; + } + } + ) + .command( + 'appdata ', + 'Manage JupyterLab Desktop app data', + yargs => { + yargs.positional('action', { + describe: 'App data action', + choices: ['list'], + default: 'list' + }); + }, + async argv => { + console.log('Note: This is an experimental feature.'); + + const action = argv.action; + switch (action) { + case 'list': + handleAppDataListCommand(argv); + break; + default: + console.log('Invalid input for "appdata" command.'); + break; + } + } + ) + .command( + 'logs ', + 'Manage JupyterLab Desktop logs', + yargs => { + yargs.positional('action', { + describe: 'Logs action', + choices: ['show', 'open'], + default: 'show' + }); + }, + async argv => { + console.log('Note: This is an experimental feature.'); + + const action = argv.action; + switch (action) { + case 'show': + handleLogsShowCommand(argv); + break; + case 'open': + handleLogsOpenCommand(argv); + break; + default: + console.log('Invalid input for "logs" command.'); + break; + } + } + ) .parseAsync(); } @@ -816,6 +913,225 @@ export async function handleEnvSetSystemPythonPathCommand(argv: any) { userSettings.save(); } +function handleConfigListCommand(argv: any) { + const listLines: string[] = []; + + let projectPath = argv.projectPath + ? path.resolve(argv.projectPath) + : process.cwd(); + + listLines.push('Project / Workspace settings'); + listLines.push('============================'); + listLines.push(`[Project path: ${projectPath}]`); + listLines.push( + `[Source file: ${WorkspaceSettings.getWorkspaceSettingsPath(projectPath)}]` + ); + listLines.push('\nSettings'); + listLines.push('========'); + + const wsSettings = new WorkspaceSettings(projectPath).settings; + const wsSettingKeys = Object.keys(wsSettings).sort(); + if (wsSettingKeys.length > 0) { + for (let key of wsSettingKeys) { + const value = wsSettings[key].value; + listLines.push(`${key}: ${JSON.stringify(value)}`); + } + } else { + listLines.push('No setting overrides found in project directory.'); + } + listLines.push('\n'); + + listLines.push('Global settings'); + listLines.push('==============='); + listLines.push(`[Source file: ${UserSettings.getUserSettingsPath()}]`); + listLines.push('\nSettings'); + listLines.push('========'); + + const settingKeys = Object.values(SettingType).sort(); + const settings = userSettings.settings; + + for (let key of settingKeys) { + const setting = settings[key]; + listLines.push( + `${key}: ${JSON.stringify(setting.value)} [${ + setting.differentThanDefault ? 'modified' : 'set to default' + }${setting.wsOverridable ? ', project overridable' : ''}]` + ); + } + + console.log(listLines.join('\n')); +} + +function handleConfigSetCommand(argv: any) { + const parseSetting = (): { key: string; value: string } => { + if (argv._.length !== 3) { + console.error(`Invalid setting. Use "set settingKey value" format.`); + return { key: undefined, value: undefined }; + } + + return { key: argv._[1], value: JSON.parse(argv._[2]) }; + }; + + let projectPath = ''; + let isProjectSetting = false; + + if (argv.project || argv.projectPath) { + projectPath = argv.projectPath + ? path.resolve(argv.projectPath) + : process.cwd(); + if ( + argv.projectPath && + !(fs.existsSync(projectPath) && fs.statSync(projectPath).isFile()) + ) { + console.error(`Invalid project path! "${projectPath}"`); + return; + } + + isProjectSetting = true; + } + + let key, value; + try { + const keyVal = parseSetting(); + key = keyVal.key; + value = keyVal.value; + } catch (error) { + console.error('Failed to parse setting!'); + return; + } + + if (!(key && value)) { + return; + } + + if (!(key in SettingType)) { + console.error(`Invalid setting key! "${key}"`); + return; + } + + if (isProjectSetting) { + const setting = userSettings.settings[key]; + if (!setting.wsOverridable) { + console.error(`Setting "${key}" is not overridable by project.`); + return; + } + + const wsSettings = new WorkspaceSettings(projectPath); + wsSettings.setValue(key as SettingType, value); + wsSettings.save(); + } else { + userSettings.setValue(key as SettingType, value); + userSettings.save(); + } + + console.log( + `${ + isProjectSetting ? 'Project' : 'Global' + } setting "${key}" set to "${value}" successfully.` + ); +} + +function handleConfigUnsetCommand(argv: any) { + const parseKey = (): string => { + if (argv._.length !== 2) { + console.error(`Invalid setting. Use "set settingKey value" format.`); + return undefined; + } + + return argv._[1]; + }; + + let projectPath = ''; + let isProjectSetting = false; + + if (argv.project || argv.projectPath) { + projectPath = argv.projectPath + ? path.resolve(argv.projectPath) + : process.cwd(); + if ( + argv.projectPath && + !(fs.existsSync(projectPath) && fs.statSync(projectPath).isFile()) + ) { + console.error(`Invalid project path! "${projectPath}"`); + return; + } + + isProjectSetting = true; + } + + let key = parseKey(); + + if (!key) { + return; + } + + if (!(key in SettingType)) { + console.error(`Invalid setting key! "${key}"`); + return; + } + + if (isProjectSetting) { + const setting = userSettings.settings[key]; + if (!setting.wsOverridable) { + console.error(`Setting "${key}" is not overridable by project.`); + return; + } + + const wsSettings = new WorkspaceSettings(projectPath); + wsSettings.unsetValue(key as SettingType); + wsSettings.save(); + } else { + userSettings.unsetValue(key as SettingType); + userSettings.save(); + } + + console.log( + `${isProjectSetting ? 'Project' : 'Global'} setting "${key}" reset to ${ + isProjectSetting ? 'global ' : '' + }default successfully.` + ); +} + +function handleAppDataListCommand(argv: any) { + const listLines: string[] = []; + + listLines.push('Application data'); + listLines.push('================'); + listLines.push(`[Source file: ${ApplicationData.getAppDataPath()}]`); + listLines.push('\nData'); + listLines.push('===='); + + const skippedKeys = new Set(['newsList']); + const appDataKeys = Object.keys(appData).sort(); + + for (let key of appDataKeys) { + if (key.startsWith('_') || skippedKeys.has(key)) { + continue; + } + const data = (appData as any)[key]; + listLines.push(`${key}: ${JSON.stringify(data)}`); + } + + console.log(listLines.join('\n')); +} + +function handleLogsShowCommand(argv: any) { + const logFilePath = getLogFilePath(); + console.log(`Log file path: ${logFilePath}`); + + if (!(fs.existsSync(logFilePath) && fs.statSync(logFilePath).isFile())) { + console.log('Log file does not exist!'); + return; + } + + const logs = fs.readFileSync(logFilePath); + console.log(logs.toString()); +} + +function handleLogsOpenCommand(argv: any) { + shell.openPath(getLogFilePath()); +} + export async function launchCLIinEnvironment( envPath: string ): Promise { diff --git a/src/main/config/appdata.ts b/src/main/config/appdata.ts index 7995225e..6e993db6 100644 --- a/src/main/config/appdata.ts +++ b/src/main/config/appdata.ts @@ -54,7 +54,7 @@ export class ApplicationData { } read() { - const appDataPath = this._getAppDataPath(); + const appDataPath = ApplicationData.getAppDataPath(); if (!fs.existsSync(appDataPath)) { return; } @@ -176,7 +176,7 @@ export class ApplicationData { } save() { - const appDataPath = this._getAppDataPath(); + const appDataPath = ApplicationData.getAppDataPath(); const appDataJSON: { [key: string]: any } = {}; if (this.pythonPath !== '') { @@ -373,7 +373,7 @@ export class ApplicationData { return this._recentSessionsChanged; } - private _getAppDataPath(): string { + static getAppDataPath(): string { const userDataDir = getUserDataDir(); return path.join(userDataDir, 'app-data.json'); } diff --git a/src/main/config/settings.ts b/src/main/config/settings.ts index 1d5a344b..1eb31bd5 100644 --- a/src/main/config/settings.ts +++ b/src/main/config/settings.ts @@ -102,13 +102,17 @@ export class Setting { } get differentThanDefault(): boolean { - return this.value !== this._defaultValue; + return JSON.stringify(this.value) !== JSON.stringify(this._defaultValue); } get wsOverridable(): boolean { return this?._options?.wsOverridable; } + setToDefault() { + this._value = this._defaultValue; + } + private _defaultValue: T; private _value: T; private _valueSet = false; @@ -163,6 +167,15 @@ export class UserSettings { } } + static getUserSettingsPath(): string { + const userDataDir = getUserDataDir(); + return path.join(userDataDir, 'settings.json'); + } + + get settings() { + return this._settings; + } + getValue(setting: SettingType) { return this._settings[setting].value; } @@ -171,8 +184,12 @@ export class UserSettings { this._settings[setting].value = value; } + unsetValue(setting: SettingType) { + this._settings[setting].setToDefault(); + } + read() { - const userSettingsPath = this._getUserSettingsPath(); + const userSettingsPath = UserSettings.getUserSettingsPath(); if (!fs.existsSync(userSettingsPath)) { return; } @@ -188,7 +205,7 @@ export class UserSettings { } save() { - const userSettingsPath = this._getUserSettingsPath(); + const userSettingsPath = UserSettings.getUserSettingsPath(); const userSettings: { [key: string]: any } = {}; for (let key in SettingType) { @@ -207,11 +224,6 @@ export class UserSettings { ); } - private _getUserSettingsPath(): string { - const userDataDir = getUserDataDir(); - return path.join(userDataDir, 'settings.json'); - } - protected _settings: { [key: string]: Setting }; } @@ -223,6 +235,10 @@ export class WorkspaceSettings extends UserSettings { this.read(); } + get settings() { + return this._wsSettings; + } + getValue(setting: SettingType) { if (setting in this._wsSettings) { return this._wsSettings[setting].value; @@ -239,10 +255,16 @@ export class WorkspaceSettings extends UserSettings { this._wsSettings[setting].value = value; } + unsetValue(setting: SettingType) { + delete this._wsSettings[setting]; + } + read() { super.read(); - const wsSettingsPath = this._getWorkspaceSettingsPath(); + const wsSettingsPath = WorkspaceSettings.getWorkspaceSettingsPath( + this._workingDirectory + ); if (!fs.existsSync(wsSettingsPath)) { return; } @@ -261,7 +283,9 @@ export class WorkspaceSettings extends UserSettings { } save() { - const wsSettingsPath = this._getWorkspaceSettingsPath(); + const wsSettingsPath = WorkspaceSettings.getWorkspaceSettingsPath( + this._workingDirectory + ); const wsSettings: { [key: string]: any } = {}; for (let key in SettingType) { @@ -299,12 +323,8 @@ export class WorkspaceSettings extends UserSettings { return false; } - private _getWorkspaceSettingsPath(): string { - return path.join( - this._workingDirectory, - '.jupyter', - 'desktop-settings.json' - ); + static getWorkspaceSettingsPath(workingDirectory: string): string { + return path.join(workingDirectory, '.jupyter', 'desktop-settings.json'); } private _workingDirectory: string; diff --git a/src/main/main.ts b/src/main/main.ts index 2a6866e9..95c73698 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -224,7 +224,9 @@ function processArgs(): Promise { parseCLIArgs(process.argv.slice(isDevMode() ? 2 : 1)).then(value => { argv = value; if ( - ['--help', '--version', 'env'].find(arg => process.argv?.includes(arg)) + ['--help', '--version', 'env', 'config', 'appdata', 'logs'].find(arg => + process.argv?.includes(arg) + ) ) { app.quit(); return; From 5db34cac46853b15b2edce73882690669158e30c Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Sat, 17 Feb 2024 16:37:23 -0800 Subject: [PATCH 2/3] fix CLI issues --- src/main/cli.ts | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/main/cli.ts b/src/main/cli.ts index 9c7afd94..b5a1284e 100644 --- a/src/main/cli.ts +++ b/src/main/cli.ts @@ -965,11 +965,25 @@ function handleConfigListCommand(argv: any) { function handleConfigSetCommand(argv: any) { const parseSetting = (): { key: string; value: string } => { if (argv._.length !== 3) { - console.error(`Invalid setting. Use "set settingKey value" format.`); + console.error(`Invalid setting. Use "set " format.`); return { key: undefined, value: undefined }; } - return { key: argv._[1], value: JSON.parse(argv._[2]) }; + let value; + + // boolean, arrays, objects + try { + value = JSON.parse(argv._[2]); + } catch (error) { + try { + // string without quotes + value = JSON.parse(`"${argv._[2]}"`); + } catch (error) { + console.error(error.message); + } + } + + return { key: argv._[1], value: value }; }; let projectPath = ''; @@ -981,7 +995,7 @@ function handleConfigSetCommand(argv: any) { : process.cwd(); if ( argv.projectPath && - !(fs.existsSync(projectPath) && fs.statSync(projectPath).isFile()) + !(fs.existsSync(projectPath) && fs.statSync(projectPath).isDirectory()) ) { console.error(`Invalid project path! "${projectPath}"`); return; @@ -1000,7 +1014,8 @@ function handleConfigSetCommand(argv: any) { return; } - if (!(key && value)) { + if (key === undefined || value === undefined) { + console.error('Failed to parse key value pair!'); return; } @@ -1034,7 +1049,7 @@ function handleConfigSetCommand(argv: any) { function handleConfigUnsetCommand(argv: any) { const parseKey = (): string => { if (argv._.length !== 2) { - console.error(`Invalid setting. Use "set settingKey value" format.`); + console.error(`Invalid setting. Use "unset " format.`); return undefined; } @@ -1050,7 +1065,7 @@ function handleConfigUnsetCommand(argv: any) { : process.cwd(); if ( argv.projectPath && - !(fs.existsSync(projectPath) && fs.statSync(projectPath).isFile()) + !(fs.existsSync(projectPath) && fs.statSync(projectPath).isDirectory()) ) { console.error(`Invalid project path! "${projectPath}"`); return; From 5aa6e49d95a0b4f53c02d03b48d181bf14829981 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Sat, 17 Feb 2024 17:48:57 -0800 Subject: [PATCH 3/3] open-file support for settings and appdata --- src/main/cli.ts | 126 ++++++++++++++++++++++++++++++------------------ 1 file changed, 79 insertions(+), 47 deletions(-) diff --git a/src/main/cli.ts b/src/main/cli.ts index b5a1284e..7052ed96 100644 --- a/src/main/cli.ts +++ b/src/main/cli.ts @@ -201,7 +201,7 @@ export function parseCLIArgs(argv: string[]) { yargs .positional('action', { describe: 'Setting action', - choices: ['list', 'set', 'unset'], + choices: ['list', 'set', 'unset', 'open-file'], default: 'list' }) .option('project', { @@ -228,6 +228,9 @@ export function parseCLIArgs(argv: string[]) { case 'unset': handleConfigUnsetCommand(argv); break; + case 'open-file': + handleConfigOpenFileCommand(argv); + break; default: console.log('Invalid input for "config" command.'); break; @@ -240,7 +243,7 @@ export function parseCLIArgs(argv: string[]) { yargs => { yargs.positional('action', { describe: 'App data action', - choices: ['list'], + choices: ['list', 'open-file'], default: 'list' }); }, @@ -252,6 +255,9 @@ export function parseCLIArgs(argv: string[]) { case 'list': handleAppDataListCommand(argv); break; + case 'open-file': + handleAppDataOpenFileCommand(argv); + break; default: console.log('Invalid input for "appdata" command.'); break; @@ -264,7 +270,7 @@ export function parseCLIArgs(argv: string[]) { yargs => { yargs.positional('action', { describe: 'Logs action', - choices: ['show', 'open'], + choices: ['show', 'open-file'], default: 'show' }); }, @@ -276,8 +282,8 @@ export function parseCLIArgs(argv: string[]) { case 'show': handleLogsShowCommand(argv); break; - case 'open': - handleLogsOpenCommand(argv); + case 'open-file': + handleLogsOpenFileCommand(argv); break; default: console.log('Invalid input for "logs" command.'); @@ -913,10 +919,28 @@ export async function handleEnvSetSystemPythonPathCommand(argv: any) { userSettings.save(); } +function getProjectPathForConfigCommand(argv: any): string | undefined { + let projectPath = undefined; + if (argv.project || argv.projectPath) { + projectPath = argv.projectPath + ? path.resolve(argv.projectPath) + : process.cwd(); + if ( + argv.projectPath && + !(fs.existsSync(projectPath) && fs.statSync(projectPath).isDirectory()) + ) { + console.error(`Invalid project path! "${projectPath}"`); + process.exit(1); + } + } + + return projectPath; +} + function handleConfigListCommand(argv: any) { const listLines: string[] = []; - let projectPath = argv.projectPath + const projectPath = argv.projectPath ? path.resolve(argv.projectPath) : process.cwd(); @@ -986,23 +1010,7 @@ function handleConfigSetCommand(argv: any) { return { key: argv._[1], value: value }; }; - let projectPath = ''; - let isProjectSetting = false; - - if (argv.project || argv.projectPath) { - projectPath = argv.projectPath - ? path.resolve(argv.projectPath) - : process.cwd(); - if ( - argv.projectPath && - !(fs.existsSync(projectPath) && fs.statSync(projectPath).isDirectory()) - ) { - console.error(`Invalid project path! "${projectPath}"`); - return; - } - - isProjectSetting = true; - } + const projectPath = getProjectPathForConfigCommand(argv); let key, value; try { @@ -1024,7 +1032,7 @@ function handleConfigSetCommand(argv: any) { return; } - if (isProjectSetting) { + if (projectPath) { const setting = userSettings.settings[key]; if (!setting.wsOverridable) { console.error(`Setting "${key}" is not overridable by project.`); @@ -1041,7 +1049,7 @@ function handleConfigSetCommand(argv: any) { console.log( `${ - isProjectSetting ? 'Project' : 'Global' + projectPath ? 'Project' : 'Global' } setting "${key}" set to "${value}" successfully.` ); } @@ -1056,23 +1064,7 @@ function handleConfigUnsetCommand(argv: any) { return argv._[1]; }; - let projectPath = ''; - let isProjectSetting = false; - - if (argv.project || argv.projectPath) { - projectPath = argv.projectPath - ? path.resolve(argv.projectPath) - : process.cwd(); - if ( - argv.projectPath && - !(fs.existsSync(projectPath) && fs.statSync(projectPath).isDirectory()) - ) { - console.error(`Invalid project path! "${projectPath}"`); - return; - } - - isProjectSetting = true; - } + const projectPath = getProjectPathForConfigCommand(argv); let key = parseKey(); @@ -1085,7 +1077,7 @@ function handleConfigUnsetCommand(argv: any) { return; } - if (isProjectSetting) { + if (projectPath) { const setting = userSettings.settings[key]; if (!setting.wsOverridable) { console.error(`Setting "${key}" is not overridable by project.`); @@ -1101,12 +1093,30 @@ function handleConfigUnsetCommand(argv: any) { } console.log( - `${isProjectSetting ? 'Project' : 'Global'} setting "${key}" reset to ${ - isProjectSetting ? 'global ' : '' + `${projectPath ? 'Project' : 'Global'} setting "${key}" reset to ${ + projectPath ? 'global ' : '' }default successfully.` ); } +function handleConfigOpenFileCommand(argv: any) { + const projectPath = getProjectPathForConfigCommand(argv); + const settingsFilePath = projectPath + ? WorkspaceSettings.getWorkspaceSettingsPath(projectPath) + : UserSettings.getUserSettingsPath(); + + console.log(`Settings file path: ${settingsFilePath}`); + + if ( + !(fs.existsSync(settingsFilePath) && fs.statSync(settingsFilePath).isFile()) + ) { + console.log('Settings file does not exist!'); + return; + } + + shell.openPath(settingsFilePath); +} + function handleAppDataListCommand(argv: any) { const listLines: string[] = []; @@ -1130,6 +1140,20 @@ function handleAppDataListCommand(argv: any) { console.log(listLines.join('\n')); } +function handleAppDataOpenFileCommand(argv: any) { + const appDataFilePath = ApplicationData.getAppDataPath(); + console.log(`App data file path: ${appDataFilePath}`); + + if ( + !(fs.existsSync(appDataFilePath) && fs.statSync(appDataFilePath).isFile()) + ) { + console.log('App data file does not exist!'); + return; + } + + shell.openPath(appDataFilePath); +} + function handleLogsShowCommand(argv: any) { const logFilePath = getLogFilePath(); console.log(`Log file path: ${logFilePath}`); @@ -1143,8 +1167,16 @@ function handleLogsShowCommand(argv: any) { console.log(logs.toString()); } -function handleLogsOpenCommand(argv: any) { - shell.openPath(getLogFilePath()); +function handleLogsOpenFileCommand(argv: any) { + const logFilePath = getLogFilePath(); + console.log(`Log file path: ${logFilePath}`); + + if (!(fs.existsSync(logFilePath) && fs.statSync(logFilePath).isFile())) { + console.log('Log file does not exist!'); + return; + } + + shell.openPath(logFilePath); } export async function launchCLIinEnvironment(