feat(misc): enhance IDE integration with Nx Console auto-installation and improved logging (#31462)
## Current Behavior Currently, IDE integration setup requires manual configuration and lacks streamlined auto-installation capabilities for Nx Console. The logging system also needs improvement for better developer experience. ## Expected Behavior With these changes, the IDE integration provides: - Prompt for automatic Nx Console installation with user preferences - Enhanced native logger with proper formatting and levels - Better development documentation for IDE setup - Updated documentation links to use the new format ## Related Issue(s) <\!-- Please link the issue being fixed so it gets closed when this is merged. --> This PR implements IDE integration improvements including Nx Console auto-installation and enhanced logging capabilities.
This commit is contained in:
parent
6613dd29ea
commit
77ff63f356
@ -1,10 +1,11 @@
|
|||||||
When responding to queries about this repository:
|
When responding to queries about this repository:
|
||||||
|
|
||||||
1. Use the `nx_workspace` mcp tool for understanding the workspace architecture when appropriate
|
1. Use the `nx_workspace` mcp tool for understanding the workspace architecture when appropriate
|
||||||
2. Suggest relevant commands from the "Essential Commands" section when applicable
|
2. When working in projects, use the `nx_project` mcp tool to analyze and understand the specific project structure and dependencies
|
||||||
3. Highlight Nx's focus on monorepos and its key features like smart task execution, code generation, and project graph analysis
|
3. Suggest relevant commands from the "Essential Commands" section when applicable
|
||||||
4. Mention the plugin ecosystem and support for various frameworks when relevant
|
4. Highlight Nx's focus on monorepos and its key features like smart task execution, code generation, and project graph analysis
|
||||||
5. Emphasize the importance of running the full validation suite before committing changes
|
5. Mention the plugin ecosystem and support for various frameworks when relevant
|
||||||
|
6. Emphasize the importance of running the full validation suite before committing changes
|
||||||
|
|
||||||
Always strive to provide accurate, helpful responses that align with the best practices and workflows described in this file.
|
Always strive to provide accurate, helpful responses that align with the best practices and workflows described in this file.
|
||||||
|
|
||||||
|
|||||||
@ -3,14 +3,14 @@ import { performance } from 'perf_hooks';
|
|||||||
import { commandsObject } from '../src/command-line/nx-commands';
|
import { commandsObject } from '../src/command-line/nx-commands';
|
||||||
import { WorkspaceTypeAndRoot } from '../src/utils/find-workspace-root';
|
import { WorkspaceTypeAndRoot } from '../src/utils/find-workspace-root';
|
||||||
import { stripIndents } from '../src/utils/strip-indents';
|
import { stripIndents } from '../src/utils/strip-indents';
|
||||||
|
import { ensureNxConsoleInstalled } from '../src/utils/nx-console-prompt';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nx is being run inside a workspace.
|
* Nx is being run inside a workspace.
|
||||||
*
|
*
|
||||||
* @param workspace Relevant local workspace properties
|
* @param workspace Relevant local workspace properties
|
||||||
*/
|
*/
|
||||||
|
export async function initLocal(workspace: WorkspaceTypeAndRoot) {
|
||||||
export function initLocal(workspace: WorkspaceTypeAndRoot) {
|
|
||||||
process.env.NX_CLI_SET = 'true';
|
process.env.NX_CLI_SET = 'true';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -25,6 +25,11 @@ export function initLocal(workspace: WorkspaceTypeAndRoot) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure NxConsole is installed if the user has it configured.
|
||||||
|
try {
|
||||||
|
await ensureNxConsoleInstalled();
|
||||||
|
} catch {}
|
||||||
|
|
||||||
const command = process.argv[2];
|
const command = process.argv[2];
|
||||||
if (command === 'run' || command === 'g' || command === 'generate') {
|
if (command === 'run' || command === 'g' || command === 'generate') {
|
||||||
commandsObject.parse(process.argv.slice(2));
|
commandsObject.parse(process.argv.slice(2));
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import { performance } from 'perf_hooks';
|
|||||||
import { setupWorkspaceContext } from '../src/utils/workspace-context';
|
import { setupWorkspaceContext } from '../src/utils/workspace-context';
|
||||||
import { daemonClient } from '../src/daemon/client/client';
|
import { daemonClient } from '../src/daemon/client/client';
|
||||||
import { removeDbConnections } from '../src/utils/db-connection';
|
import { removeDbConnections } from '../src/utils/db-connection';
|
||||||
import { signalToCode } from '../src/utils/exit-codes';
|
|
||||||
|
|
||||||
// In case Nx Cloud forcibly exits while the TUI is running, ensure the terminal is restored etc.
|
// In case Nx Cloud forcibly exits while the TUI is running, ensure the terminal is restored etc.
|
||||||
process.on('exit', (...args) => {
|
process.on('exit', (...args) => {
|
||||||
@ -30,7 +29,7 @@ process.on('exit', (...args) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function main() {
|
async function main() {
|
||||||
if (
|
if (
|
||||||
process.argv[2] !== 'report' &&
|
process.argv[2] !== 'report' &&
|
||||||
process.argv[2] !== '--version' &&
|
process.argv[2] !== '--version' &&
|
||||||
@ -44,16 +43,16 @@ function main() {
|
|||||||
|
|
||||||
const workspace = findWorkspaceRoot(process.cwd());
|
const workspace = findWorkspaceRoot(process.cwd());
|
||||||
|
|
||||||
performance.mark('loading dotenv files:start');
|
|
||||||
if (workspace) {
|
if (workspace) {
|
||||||
|
performance.mark('loading dotenv files:start');
|
||||||
loadRootEnvFiles(workspace.dir);
|
loadRootEnvFiles(workspace.dir);
|
||||||
|
performance.mark('loading dotenv files:end');
|
||||||
|
performance.measure(
|
||||||
|
'loading dotenv files',
|
||||||
|
'loading dotenv files:start',
|
||||||
|
'loading dotenv files:end'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
performance.mark('loading dotenv files:end');
|
|
||||||
performance.measure(
|
|
||||||
'loading dotenv files',
|
|
||||||
'loading dotenv files:start',
|
|
||||||
'loading dotenv files:end'
|
|
||||||
);
|
|
||||||
|
|
||||||
// new is a special case because there is no local workspace to load
|
// new is a special case because there is no local workspace to load
|
||||||
if (
|
if (
|
||||||
@ -103,7 +102,7 @@ function main() {
|
|||||||
|
|
||||||
// this file is already in the local workspace
|
// this file is already in the local workspace
|
||||||
if (isLocalInstall) {
|
if (isLocalInstall) {
|
||||||
initLocal(workspace);
|
await initLocal(workspace);
|
||||||
} else {
|
} else {
|
||||||
// Nx is being run from globally installed CLI - hand off to the local
|
// Nx is being run from globally installed CLI - hand off to the local
|
||||||
warnIfUsingOutdatedGlobalInstall(GLOBAL_NX_VERSION, LOCAL_NX_VERSION);
|
warnIfUsingOutdatedGlobalInstall(GLOBAL_NX_VERSION, LOCAL_NX_VERSION);
|
||||||
@ -287,4 +286,7 @@ process.on('exit', () => {
|
|||||||
removeDbConnections();
|
removeDbConnections();
|
||||||
});
|
});
|
||||||
|
|
||||||
main();
|
main().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|||||||
51
packages/nx/src/native/CLAUDE.md
Normal file
51
packages/nx/src/native/CLAUDE.md
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Nx Native (Rust Core) Development Guide
|
||||||
|
|
||||||
|
This directory contains the core Nx functionality written in Rust, providing high-performance operations for the Nx build system.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The native module uses [napi-rs](https://napi.rs/) to create Node.js bindings for Rust code, enabling seamless integration between the TypeScript Nx codebase and performance-critical Rust implementations.
|
||||||
|
|
||||||
|
## Building the Native Module
|
||||||
|
|
||||||
|
After making changes to any Rust code in this directory, you must rebuild the nx package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nx build nx
|
||||||
|
```
|
||||||
|
|
||||||
|
This command:
|
||||||
|
|
||||||
|
- Compiles the Rust code
|
||||||
|
- Generates TypeScript bindings
|
||||||
|
- Creates the native module for your current platform
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
1. **Make Rust Changes**: Edit `.rs` files in this directory
|
||||||
|
2. **Build Native Module**: Run `nx build-native nx --configuration local`
|
||||||
|
3. **Test Changes**: Run tests to verify functionality
|
||||||
|
4. **Format Code**: Ensure Rust code follows project conventions using `cargo fmt`
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- Always rebuild after Rust changes - TypeScript won't see updates until you rebuild
|
||||||
|
- The generated TypeScript bindings are in `packages/nx/src/native/index.js` and `.d.ts`
|
||||||
|
- Performance-critical operations should be implemented here rather than in TypeScript
|
||||||
|
- Use appropriate error handling - Rust panics will crash the Node.js process
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run Rust tests with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test
|
||||||
|
```
|
||||||
|
|
||||||
|
Integration tests that verify the TypeScript/Rust boundary should be written in the TypeScript test files.
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
- **Module not found errors**: Ensure you've run the build command after changes
|
||||||
|
- **Type mismatches**: Check that the napi-rs decorators match the TypeScript expectations
|
||||||
|
- **Performance regressions**: Profile before and after changes to ensure optimizations work as expected
|
||||||
31
packages/nx/src/native/config/dir.rs
Normal file
31
packages/nx/src/native/config/dir.rs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
const NX_CONFIG_DIR_NAME: &str = ".nx";
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
const NX_CONFIG_DIR_NAME: &str = "nx";
|
||||||
|
|
||||||
|
/// Get the base configuration directory path.
|
||||||
|
///
|
||||||
|
/// - **Windows**: `%USERPROFILE%`
|
||||||
|
/// - **Unix**: `$XDG_CONFIG_HOME` or `$HOME/.config`
|
||||||
|
fn get_home_dir(home_dir: impl AsRef<Path>) -> PathBuf {
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
home_dir.as_ref().to_path_buf()
|
||||||
|
} else {
|
||||||
|
match env::var("XDG_CONFIG_HOME") {
|
||||||
|
Ok(xdg_home) => PathBuf::from(xdg_home),
|
||||||
|
Err(_) => home_dir.as_ref().join(".config"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the user configuration directory for Nx.
|
||||||
|
///
|
||||||
|
/// - **Windows**: `%USERPROFILE%\.nx`
|
||||||
|
/// - **Unix**: `$XDG_CONFIG_HOME/nx` or `$HOME/.config/nx`
|
||||||
|
pub(crate) fn get_user_config_dir(home_dir: impl AsRef<Path>) -> PathBuf {
|
||||||
|
get_home_dir(home_dir).join(NX_CONFIG_DIR_NAME)
|
||||||
|
}
|
||||||
1
packages/nx/src/native/config/mod.rs
Normal file
1
packages/nx/src/native/config/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub(crate) mod dir;
|
||||||
@ -1,9 +1,6 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
mod ipc_transport;
|
|
||||||
pub mod messaging;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum SupportedEditor {
|
pub enum SupportedEditor {
|
||||||
VSCode,
|
VSCode,
|
||||||
@ -19,7 +16,7 @@ pub fn get_current_editor() -> &'static SupportedEditor {
|
|||||||
CURRENT_EDITOR.get_or_init(|| detect_editor(HashMap::new()))
|
CURRENT_EDITOR.get_or_init(|| detect_editor(HashMap::new()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn detect_editor(mut env_map: HashMap<String, String>) -> SupportedEditor {
|
pub fn detect_editor(mut env_map: HashMap<String, String>) -> SupportedEditor {
|
||||||
let term_editor = if let Some(term) = get_env_var("TERM_PROGRAM", &mut env_map) {
|
let term_editor = if let Some(term) = get_env_var("TERM_PROGRAM", &mut env_map) {
|
||||||
let term_lower = term.to_lowercase();
|
let term_lower = term.to_lowercase();
|
||||||
match term_lower.as_str() {
|
match term_lower.as_str() {
|
||||||
198
packages/nx/src/native/ide/install.rs
Normal file
198
packages/nx/src/native/ide/install.rs
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
use crate::native::ide::detection::{SupportedEditor, get_current_editor};
|
||||||
|
use crate::native::logger::enable_logger;
|
||||||
|
use napi::Error;
|
||||||
|
use std::process::Command;
|
||||||
|
use tracing::{debug, info, trace};
|
||||||
|
|
||||||
|
const NX_CONSOLE_EXTENSION_ID: &str = "nrwl.angular-console";
|
||||||
|
|
||||||
|
fn is_nx_console_installed(command: &str) -> Result<bool, Error> {
|
||||||
|
debug!(
|
||||||
|
"Checking if Nx Console extension is installed with: {} --list-extensions",
|
||||||
|
command
|
||||||
|
);
|
||||||
|
|
||||||
|
let output = match Command::new(command).arg("--list-extensions").output() {
|
||||||
|
Ok(output) => output,
|
||||||
|
Err(e) => match e.kind() {
|
||||||
|
std::io::ErrorKind::NotFound => {
|
||||||
|
debug!(
|
||||||
|
"Command '{}' not found, cannot check extension status",
|
||||||
|
command
|
||||||
|
);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
debug!("Failed to execute command: {}", e);
|
||||||
|
return Err(Error::from_reason(format!(
|
||||||
|
"Failed to run command '{}': {}",
|
||||||
|
command, e
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
let is_installed = stdout
|
||||||
|
.lines()
|
||||||
|
.any(|line| line.trim() == NX_CONSOLE_EXTENSION_ID);
|
||||||
|
if is_installed {
|
||||||
|
debug!("Nx Console extension is installed");
|
||||||
|
} else {
|
||||||
|
debug!("Nx Console extension is not installed");
|
||||||
|
}
|
||||||
|
return Ok(is_installed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the error output for debugging
|
||||||
|
debug!("Command failed with status: {:?}", output.status);
|
||||||
|
if !stdout.is_empty() {
|
||||||
|
trace!("Command stdout: {}", stdout.trim());
|
||||||
|
}
|
||||||
|
if !stderr.is_empty() {
|
||||||
|
trace!("Command stderr: {}", stderr.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command failed, assume not installed
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_extension(command: &str) -> Result<(), Error> {
|
||||||
|
debug!(
|
||||||
|
"Attempting to install Nx Console extension with: {} --install-extension {}",
|
||||||
|
command, NX_CONSOLE_EXTENSION_ID
|
||||||
|
);
|
||||||
|
|
||||||
|
let output = match Command::new(command)
|
||||||
|
.arg("--install-extension")
|
||||||
|
.arg(NX_CONSOLE_EXTENSION_ID)
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
Ok(output) => output,
|
||||||
|
Err(e) => match e.kind() {
|
||||||
|
std::io::ErrorKind::NotFound => {
|
||||||
|
debug!(
|
||||||
|
"Command '{}' not found, skipping extension installation",
|
||||||
|
command
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
debug!("Failed to execute command: {}", e);
|
||||||
|
return Err(Error::from_reason(format!(
|
||||||
|
"Failed to run command '{}': {}",
|
||||||
|
command, e
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
if stdout.contains("already installed") {
|
||||||
|
debug!("Nx Console extension is already installed");
|
||||||
|
} else {
|
||||||
|
info!("Successfully installed Nx Console");
|
||||||
|
}
|
||||||
|
trace!("Command output: {}", stdout.trim());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for "already installed" message in stdout or stderr
|
||||||
|
let combined_output = format!("{} {}", stdout, stderr);
|
||||||
|
if combined_output.contains("already installed") {
|
||||||
|
debug!("Nx Console extension is already installed");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the error output for debugging
|
||||||
|
debug!("Command failed with status: {:?}", output.status);
|
||||||
|
if !stdout.is_empty() {
|
||||||
|
trace!("Command stdout: {}", stdout.trim());
|
||||||
|
}
|
||||||
|
if !stderr.is_empty() {
|
||||||
|
trace!("Command stderr: {}", stderr.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command failed but this is OK - we don't want to crash Nx
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn can_install_nx_console() -> bool {
|
||||||
|
enable_logger();
|
||||||
|
|
||||||
|
if let Some(command) = get_install_command() {
|
||||||
|
if let Ok(installed) = is_nx_console_installed(command) {
|
||||||
|
!installed
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_install_command() -> Option<&'static str> {
|
||||||
|
// Check if installation should be skipped
|
||||||
|
let skip_install = std::env::var("NX_SKIP_VSCODE_EXTENSION_INSTALL")
|
||||||
|
.map(|v| v == "true")
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if skip_install {
|
||||||
|
debug!("Nx Console extension installation disabled via NX_SKIP_VSCODE_EXTENSION_INSTALL");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the sophisticated editor detection from nx_console
|
||||||
|
let current_editor = get_current_editor();
|
||||||
|
debug!("Detected editor: {:?}", current_editor);
|
||||||
|
|
||||||
|
match current_editor {
|
||||||
|
SupportedEditor::VSCode => {
|
||||||
|
debug!("Installing Nx Console extension for VS Code");
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
Some("code.cmd")
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
Some("code")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SupportedEditor::Windsurf => {
|
||||||
|
debug!("Installing Nx Console extension for Windsurf");
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
Some("windsurf.cmd")
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
Some("windsurf")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editor => {
|
||||||
|
trace!(
|
||||||
|
"Unknown editor ({editor:?}) detected, skipping Nx Console extension installation"
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn install_nx_console() {
|
||||||
|
enable_logger();
|
||||||
|
|
||||||
|
if let Some(command) = get_install_command() {
|
||||||
|
// Try to install the extension
|
||||||
|
if let Err(e) = install_extension(command) {
|
||||||
|
debug!("Failed to install Nx Console extension: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
packages/nx/src/native/ide/mod.rs
Normal file
4
packages/nx/src/native/ide/mod.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
pub mod detection;
|
||||||
|
pub mod install;
|
||||||
|
pub mod nx_console;
|
||||||
|
mod preferences;
|
||||||
5
packages/nx/src/native/ide/nx_console.rs
Normal file
5
packages/nx/src/native/ide/nx_console.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
mod ipc_transport;
|
||||||
|
pub mod messaging;
|
||||||
|
|
||||||
|
// Re-export from ide/detection for backward compatibility
|
||||||
|
pub use crate::native::ide::detection::{SupportedEditor, get_current_editor};
|
||||||
@ -12,9 +12,9 @@ use jsonrpsee::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::native::{
|
use crate::native::{
|
||||||
|
ide::nx_console::ipc_transport::IpcTransport,
|
||||||
tui::{
|
tui::{
|
||||||
components::tasks_list::{TaskItem, TaskStatus},
|
components::tasks_list::{TaskItem, TaskStatus},
|
||||||
nx_console::ipc_transport::IpcTransport,
|
|
||||||
pty::PtyInstance,
|
pty::PtyInstance,
|
||||||
},
|
},
|
||||||
utils::socket_path::get_full_nx_console_socket_path,
|
utils::socket_path::get_full_nx_console_socket_path,
|
||||||
69
packages/nx/src/native/ide/preferences.rs
Normal file
69
packages/nx/src/native/ide/preferences.rs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
use crate::native::config::dir::get_user_config_dir;
|
||||||
|
use crate::native::utils::json::{JsonResult, read_json_file};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
const NX_CONSOLE_PREFERENCES_FILE_NAME: &str = "ide.json";
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct NxConsolePreferences {
|
||||||
|
auto_install_console: Option<bool>,
|
||||||
|
#[serde(skip)]
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
impl NxConsolePreferences {
|
||||||
|
#[napi(constructor)]
|
||||||
|
pub fn new(home_dir: String) -> Self {
|
||||||
|
let home_dir = PathBuf::from(home_dir);
|
||||||
|
let config_dir = get_user_config_dir(home_dir);
|
||||||
|
Self {
|
||||||
|
auto_install_console: None,
|
||||||
|
path: config_dir.join(NX_CONSOLE_PREFERENCES_FILE_NAME),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn get_auto_install_preference(&mut self) -> Option<bool> {
|
||||||
|
if let Ok(prefs) = self.load() {
|
||||||
|
prefs.auto_install_console
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn set_auto_install_preference(&mut self, auto_install: bool) {
|
||||||
|
self.auto_install_console = Some(auto_install);
|
||||||
|
if let Err(err) = self.save() {
|
||||||
|
debug!("Failed to save console preferences: {}", err);
|
||||||
|
} else {
|
||||||
|
debug!("Console preferences saved successfully.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&self) -> anyhow::Result<()> {
|
||||||
|
if let Some(parent) = self.path.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = serde_json::to_string_pretty(self)?;
|
||||||
|
|
||||||
|
fs::write(&self.path, content)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load(&self) -> JsonResult<NxConsolePreferences> {
|
||||||
|
let mut prefs: NxConsolePreferences = read_json_file(&self.path)?;
|
||||||
|
|
||||||
|
// Set the path field since it's skipped during deserialization
|
||||||
|
prefs.path = self.path.clone();
|
||||||
|
|
||||||
|
debug!("Loaded console preferences: {:?}", prefs);
|
||||||
|
Ok(prefs)
|
||||||
|
}
|
||||||
|
}
|
||||||
14
packages/nx/src/native/index.d.ts
vendored
14
packages/nx/src/native/index.d.ts
vendored
@ -73,6 +73,12 @@ export declare class NxCache {
|
|||||||
checkCacheFsInSync(): boolean
|
checkCacheFsInSync(): boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export declare class NxConsolePreferences {
|
||||||
|
constructor(homeDir: string)
|
||||||
|
getAutoInstallPreference(): boolean | null
|
||||||
|
setAutoInstallPreference(autoInstall: boolean): void
|
||||||
|
}
|
||||||
|
|
||||||
export declare class NxTaskHistory {
|
export declare class NxTaskHistory {
|
||||||
constructor(db: ExternalObject<NxDbConnection>)
|
constructor(db: ExternalObject<NxDbConnection>)
|
||||||
recordTaskRuns(taskRuns: Array<TaskRun>): void
|
recordTaskRuns(taskRuns: Array<TaskRun>): void
|
||||||
@ -148,6 +154,8 @@ export interface CachedResult {
|
|||||||
size?: number
|
size?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export declare export declare function canInstallNxConsole(): boolean
|
||||||
|
|
||||||
export declare export declare function closeDbConnection(connection: ExternalObject<NxDbConnection>): void
|
export declare export declare function closeDbConnection(connection: ExternalObject<NxDbConnection>): void
|
||||||
|
|
||||||
export declare export declare function connectToNxDb(cacheDir: string, nxVersion: string, dbName?: string | undefined | null): ExternalObject<NxDbConnection>
|
export declare export declare function connectToNxDb(cacheDir: string, nxVersion: string, dbName?: string | undefined | null): ExternalObject<NxDbConnection>
|
||||||
@ -235,11 +243,13 @@ export interface InputsInput {
|
|||||||
projects?: string | Array<string>
|
projects?: string | Array<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export declare export declare function installNxConsole(): void
|
||||||
|
|
||||||
export const IS_WASM: boolean
|
export const IS_WASM: boolean
|
||||||
|
|
||||||
export declare export declare function logError(message: string): void
|
export declare export declare function logDebug(message: string): void
|
||||||
|
|
||||||
export declare export declare function logInfo(message: string): void
|
export declare export declare function logError(message: string): void
|
||||||
|
|
||||||
/** Stripped version of the NxJson interface for use in rust */
|
/** Stripped version of the NxJson interface for use in rust */
|
||||||
export interface NxJson {
|
export interface NxJson {
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
use crate::native::logger::enable_logger;
|
use crate::native::logger::enable_logger;
|
||||||
use tracing::{error, info};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
#[napi]
|
#[napi]
|
||||||
pub fn log_info(message: String) {
|
pub fn log_debug(message: String) {
|
||||||
enable_logger();
|
enable_logger();
|
||||||
info!(message);
|
debug!(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[napi]
|
#[napi]
|
||||||
|
|||||||
@ -49,8 +49,13 @@ where
|
|||||||
Level::WARN => {
|
Level::WARN => {
|
||||||
write!(&mut writer, "\n{} {} ", ">".yellow(), "NX".bold().yellow())?;
|
write!(&mut writer, "\n{} {} ", ">".yellow(), "NX".bold().yellow())?;
|
||||||
}
|
}
|
||||||
_ => {
|
Level::INFO => {
|
||||||
write!(&mut writer, "\n{} {} ", ">".cyan(), "NX".bold().cyan())?;
|
// Match TypeScript logger format: inverse cyan "NX" prefix
|
||||||
|
write!(&mut writer, "\n{} ", " NX ".on_cyan().black().bold())?;
|
||||||
|
}
|
||||||
|
Level::ERROR => {
|
||||||
|
// Match TypeScript logger format: inverse red "ERROR" prefix
|
||||||
|
write!(&mut writer, "\n{} ", " ERROR ".on_red().white().bold())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +85,7 @@ where
|
|||||||
// Write fields on the event
|
// Write fields on the event
|
||||||
ctx.field_format().format_fields(writer.by_ref(), event)?;
|
ctx.field_format().format_fields(writer.by_ref(), event)?;
|
||||||
|
|
||||||
if !(matches!(level, Level::TRACE)) && !(matches!(level, Level::DEBUG)) {
|
if matches!(level, Level::INFO | Level::ERROR | Level::WARN) {
|
||||||
writeln!(&mut writer)?;
|
writeln!(&mut writer)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,9 +94,10 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Enable logging for the native module
|
/// Enable logging for the native module
|
||||||
/// You can set log levels and different logs by setting the `NX_NATIVE_LOGGING` environment variable
|
/// By default, info level logs are shown. You can change log levels by setting the `NX_NATIVE_LOGGING` environment variable
|
||||||
/// Examples:
|
/// Examples:
|
||||||
/// - `NX_NATIVE_LOGGING=trace|warn|debug|error|info` - enable all logs for all crates and modules
|
/// - `NX_NATIVE_LOGGING=trace|warn|debug|error|info` - enable all logs for all crates and modules
|
||||||
|
/// - `NX_NATIVE_LOGGING=off` - disable all logging
|
||||||
/// - `NX_NATIVE_LOGGING=nx=trace` - enable all logs for the `nx` (this) crate
|
/// - `NX_NATIVE_LOGGING=nx=trace` - enable all logs for the `nx` (this) crate
|
||||||
/// - `NX_NATIVE_LOGGING=nx::native::tasks::hashers::hash_project_files=trace` - enable all logs for the `hash_project_files` module
|
/// - `NX_NATIVE_LOGGING=nx::native::tasks::hashers::hash_project_files=trace` - enable all logs for the `hash_project_files` module
|
||||||
/// - `NX_NATIVE_LOGGING=[{project_name=project}]` - enable logs that contain the project in its span
|
/// - `NX_NATIVE_LOGGING=[{project_name=project}]` - enable logs that contain the project in its span
|
||||||
@ -102,7 +108,7 @@ pub(crate) fn enable_logger() {
|
|||||||
.with_writer(std::io::stdout)
|
.with_writer(std::io::stdout)
|
||||||
.event_format(NxLogFormatter)
|
.event_format(NxLogFormatter)
|
||||||
.with_filter(
|
.with_filter(
|
||||||
EnvFilter::try_from_env("NX_NATIVE_LOGGING").unwrap_or_else(|_| EnvFilter::new("OFF")),
|
EnvFilter::try_from_env("NX_NATIVE_LOGGING").unwrap_or_else(|_| EnvFilter::new("info")),
|
||||||
);
|
);
|
||||||
|
|
||||||
let registry = tracing_subscriber::registry()
|
let registry = tracing_subscriber::registry()
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod glob;
|
pub mod glob;
|
||||||
pub mod hasher;
|
pub mod hasher;
|
||||||
|
pub mod ide;
|
||||||
pub mod logger;
|
pub mod logger;
|
||||||
mod machine_id;
|
mod machine_id;
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
@ -12,6 +13,7 @@ pub mod utils;
|
|||||||
mod walker;
|
mod walker;
|
||||||
pub mod workspace;
|
pub mod workspace;
|
||||||
|
|
||||||
|
mod config;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub mod db;
|
pub mod db;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
|||||||
@ -368,6 +368,7 @@ module.exports.HashPlanner = nativeBinding.HashPlanner
|
|||||||
module.exports.HttpRemoteCache = nativeBinding.HttpRemoteCache
|
module.exports.HttpRemoteCache = nativeBinding.HttpRemoteCache
|
||||||
module.exports.ImportResult = nativeBinding.ImportResult
|
module.exports.ImportResult = nativeBinding.ImportResult
|
||||||
module.exports.NxCache = nativeBinding.NxCache
|
module.exports.NxCache = nativeBinding.NxCache
|
||||||
|
module.exports.NxConsolePreferences = nativeBinding.NxConsolePreferences
|
||||||
module.exports.NxTaskHistory = nativeBinding.NxTaskHistory
|
module.exports.NxTaskHistory = nativeBinding.NxTaskHistory
|
||||||
module.exports.RunningTasksService = nativeBinding.RunningTasksService
|
module.exports.RunningTasksService = nativeBinding.RunningTasksService
|
||||||
module.exports.RustPseudoTerminal = nativeBinding.RustPseudoTerminal
|
module.exports.RustPseudoTerminal = nativeBinding.RustPseudoTerminal
|
||||||
@ -375,6 +376,7 @@ module.exports.TaskDetails = nativeBinding.TaskDetails
|
|||||||
module.exports.TaskHasher = nativeBinding.TaskHasher
|
module.exports.TaskHasher = nativeBinding.TaskHasher
|
||||||
module.exports.Watcher = nativeBinding.Watcher
|
module.exports.Watcher = nativeBinding.Watcher
|
||||||
module.exports.WorkspaceContext = nativeBinding.WorkspaceContext
|
module.exports.WorkspaceContext = nativeBinding.WorkspaceContext
|
||||||
|
module.exports.canInstallNxConsole = nativeBinding.canInstallNxConsole
|
||||||
module.exports.closeDbConnection = nativeBinding.closeDbConnection
|
module.exports.closeDbConnection = nativeBinding.closeDbConnection
|
||||||
module.exports.connectToNxDb = nativeBinding.connectToNxDb
|
module.exports.connectToNxDb = nativeBinding.connectToNxDb
|
||||||
module.exports.copy = nativeBinding.copy
|
module.exports.copy = nativeBinding.copy
|
||||||
@ -387,9 +389,10 @@ module.exports.getFilesForOutputs = nativeBinding.getFilesForOutputs
|
|||||||
module.exports.getTransformableOutputs = nativeBinding.getTransformableOutputs
|
module.exports.getTransformableOutputs = nativeBinding.getTransformableOutputs
|
||||||
module.exports.hashArray = nativeBinding.hashArray
|
module.exports.hashArray = nativeBinding.hashArray
|
||||||
module.exports.hashFile = nativeBinding.hashFile
|
module.exports.hashFile = nativeBinding.hashFile
|
||||||
|
module.exports.installNxConsole = nativeBinding.installNxConsole
|
||||||
module.exports.IS_WASM = nativeBinding.IS_WASM
|
module.exports.IS_WASM = nativeBinding.IS_WASM
|
||||||
|
module.exports.logDebug = nativeBinding.logDebug
|
||||||
module.exports.logError = nativeBinding.logError
|
module.exports.logError = nativeBinding.logError
|
||||||
module.exports.logInfo = nativeBinding.logInfo
|
|
||||||
module.exports.parseTaskStatus = nativeBinding.parseTaskStatus
|
module.exports.parseTaskStatus = nativeBinding.parseTaskStatus
|
||||||
module.exports.remove = nativeBinding.remove
|
module.exports.remove = nativeBinding.remove
|
||||||
module.exports.restoreTerminal = nativeBinding.restoreTerminal
|
module.exports.restoreTerminal = nativeBinding.restoreTerminal
|
||||||
|
|||||||
@ -22,6 +22,7 @@ use crate::native::{
|
|||||||
tasks::types::{Task, TaskResult},
|
tasks::types::{Task, TaskResult},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::action::Action;
|
||||||
use super::components::Component;
|
use super::components::Component;
|
||||||
use super::components::countdown_popup::CountdownPopup;
|
use super::components::countdown_popup::CountdownPopup;
|
||||||
use super::components::help_popup::HelpPopup;
|
use super::components::help_popup::HelpPopup;
|
||||||
@ -37,7 +38,7 @@ use super::pty::PtyInstance;
|
|||||||
use super::theme::THEME;
|
use super::theme::THEME;
|
||||||
use super::tui;
|
use super::tui;
|
||||||
use super::utils::normalize_newlines;
|
use super::utils::normalize_newlines;
|
||||||
use super::{action::Action, nx_console::messaging::NxConsoleMessageConnection};
|
use crate::native::ide::nx_console::messaging::NxConsoleMessageConnection;
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub components: Vec<Box<dyn Component>>,
|
pub components: Vec<Box<dyn Component>>,
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
use super::{Component, Frame};
|
||||||
|
use crate::native::ide::detection::{SupportedEditor, get_current_editor};
|
||||||
|
use crate::native::tui::action::Action;
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
@ -11,9 +14,6 @@ use ratatui::{
|
|||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
use super::{Component, Frame};
|
|
||||||
use crate::native::tui::{action::Action, nx_console};
|
|
||||||
|
|
||||||
use crate::native::tui::theme::THEME;
|
use crate::native::tui::theme::THEME;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@ -164,8 +164,8 @@ impl HelpPopup {
|
|||||||
("", ""),
|
("", ""),
|
||||||
(
|
(
|
||||||
"<ctrl>+a",
|
"<ctrl>+a",
|
||||||
match nx_console::get_current_editor() {
|
match get_current_editor() {
|
||||||
nx_console::SupportedEditor::VSCode => {
|
SupportedEditor::VSCode => {
|
||||||
"Send terminal output to Copilot so that it can assist with any issues"
|
"Send terminal output to Copilot so that it can assist with any issues"
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
|||||||
@ -8,8 +8,8 @@ use tracing::debug;
|
|||||||
use crate::native::logger::enable_logger;
|
use crate::native::logger::enable_logger;
|
||||||
use crate::native::tasks::types::{Task, TaskResult};
|
use crate::native::tasks::types::{Task, TaskResult};
|
||||||
use crate::native::{
|
use crate::native::{
|
||||||
|
ide::nx_console::messaging::NxConsoleMessageConnection,
|
||||||
pseudo_terminal::pseudo_terminal::{ParserArc, WriterArc},
|
pseudo_terminal::pseudo_terminal::{ParserArc, WriterArc},
|
||||||
tui::nx_console::messaging::NxConsoleMessageConnection,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::app::App;
|
use super::app::App;
|
||||||
|
|||||||
@ -3,7 +3,6 @@ pub mod app;
|
|||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod lifecycle;
|
pub mod lifecycle;
|
||||||
pub mod nx_console;
|
|
||||||
pub mod pty;
|
pub mod pty;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
#[allow(clippy::module_inception)]
|
#[allow(clippy::module_inception)]
|
||||||
|
|||||||
22
packages/nx/src/native/utils/json.rs
Normal file
22
packages/nx/src/native/utils/json.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum JsonError {
|
||||||
|
#[error("I/O error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error("JSON parse error: {0}")]
|
||||||
|
Parse(#[from] serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type JsonResult<T> = Result<T, JsonError>;
|
||||||
|
|
||||||
|
/// Efficiently reads and parses a JSON file using buffered I/O
|
||||||
|
pub fn read_json_file<T: DeserializeOwned>(path: &Path) -> JsonResult<T> {
|
||||||
|
let file = fs::File::open(path)?;
|
||||||
|
let reader = std::io::BufReader::new(file);
|
||||||
|
let data = serde_json::from_reader(reader)?;
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
mod find_matching_projects;
|
mod find_matching_projects;
|
||||||
mod get_mod_time;
|
mod get_mod_time;
|
||||||
|
pub mod json;
|
||||||
mod normalize_trait;
|
mod normalize_trait;
|
||||||
pub mod path;
|
pub mod path;
|
||||||
pub mod socket_path;
|
pub mod socket_path;
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import {
|
|||||||
getTaskDetails,
|
getTaskDetails,
|
||||||
hashTasksThatDoNotDependOnOutputsOfOtherTasks,
|
hashTasksThatDoNotDependOnOutputsOfOtherTasks,
|
||||||
} from '../hasher/hash-task';
|
} from '../hasher/hash-task';
|
||||||
import { logError, logInfo, RunMode } from '../native';
|
import { logError, logDebug, RunMode } from '../native';
|
||||||
import {
|
import {
|
||||||
runPostTasksExecution,
|
runPostTasksExecution,
|
||||||
runPreTasksExecution,
|
runPreTasksExecution,
|
||||||
@ -222,7 +222,7 @@ async function getTerminalOutputLifeCycle(
|
|||||||
: chunk.toString()
|
: chunk.toString()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
logInfo(
|
logDebug(
|
||||||
Buffer.isBuffer(chunk)
|
Buffer.isBuffer(chunk)
|
||||||
? chunk.toString(encoding)
|
? chunk.toString(encoding)
|
||||||
: chunk.toString()
|
: chunk.toString()
|
||||||
|
|||||||
61
packages/nx/src/utils/nx-console-prompt.ts
Normal file
61
packages/nx/src/utils/nx-console-prompt.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { prompt } from 'enquirer';
|
||||||
|
import { homedir } from 'os';
|
||||||
|
import { output } from './output';
|
||||||
|
import {
|
||||||
|
installNxConsole,
|
||||||
|
canInstallNxConsole,
|
||||||
|
NxConsolePreferences,
|
||||||
|
} from '../native';
|
||||||
|
|
||||||
|
export async function ensureNxConsoleInstalled() {
|
||||||
|
const preferences = new NxConsolePreferences(homedir());
|
||||||
|
let setting = preferences.getAutoInstallPreference();
|
||||||
|
|
||||||
|
const canInstallConsole = canInstallNxConsole();
|
||||||
|
|
||||||
|
// Noop
|
||||||
|
if (!canInstallConsole) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof setting !== 'boolean') {
|
||||||
|
setting = await promptForNxConsoleInstallation();
|
||||||
|
preferences.setAutoInstallPreference(setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setting) {
|
||||||
|
installNxConsole();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompts the user whether they want to automatically install the Nx Console extension
|
||||||
|
* and persists their preference using the NxConsolePreferences struct
|
||||||
|
*/
|
||||||
|
async function promptForNxConsoleInstallation(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
output.log({
|
||||||
|
title:
|
||||||
|
"Enhance your developer experience with Nx Console, Nx's official editor extension",
|
||||||
|
bodyLines: [
|
||||||
|
'- Enable your AI assistant to do more by understanding your workspace',
|
||||||
|
'- Add IntelliSense for Nx configuration files',
|
||||||
|
'- Explore your workspace visually',
|
||||||
|
'- Generate code and execute tasks interactively',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { shouldInstallNxConsole } = await prompt<{
|
||||||
|
shouldInstallNxConsole: boolean;
|
||||||
|
}>({
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'shouldInstallNxConsole',
|
||||||
|
message: 'Install Nx Console? (you can uninstall anytime)',
|
||||||
|
initial: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return shouldInstallNxConsole;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user