#![cfg_attr(windows, windows_subsystem = "windows")]
#[cfg(not(target_arch = "wasm32"))]
const USAGE: &str = "usage: brep-app [FILE] [--mcp] [--mcp-port PORT] [--session-root DIR] [--plm-url URL] [--plm-token-file FILE]
brep-app --kicad-library
brep-app --install-file-associations | --uninstall-file-associations
brep-app --plm-configure URL [--plm-token-file FILE] | --plm-forget | --plm-show | --plm-check
brep-app --bake-worker [--plm-url URL] [--plm-token-file FILE] [--once | --poll SECS]
brep-app --provision-typescript | --typescript-status
FILE open this document in a tab at startup (what a file
association hands us on a double click): a part (.nbrep),
a family seed (.fbrep) or a template (.tbrep); a STEP,
IGES, STL, 3MF, OBJ or KiCad file is IMPORTED (as File >
Import does) into a new untitled tab named after it
--mcp embed the MCP server: agents drive this window over http://127.0.0.1:PORT/mcp
--mcp-instance NAME named persistent MCP state (default desktop; also used by the editor)
--mcp-state-dir DIR explicit MCP journal directory
--mcp-ephemeral disable MCP journal persistence for this launch
--mcp-port PORT the port to listen on (default 8765)
--session-root DIR where MCP sessions keep screenshots and call logs (default: the OS temp dir)
--provision-typescript
install the pinned TypeScript compiler run_script checks
TypeScript with, into the user-local prefix it discovers,
using npm (BREP_NPM or PATH); downloads, then exits
--typescript-status report that compiler (discovery and launcher probe) and exit
--kicad-library open the KiCad importer instead of the CAD window: clone
KiCad's symbol, footprint and 3D-model libraries (git), or import
parts from the KiCad installed here, saving each as a document
--install-file-associations
make THIS executable the opener for .nbrep, .fbrep and
.tbrep files, for the current user, offer it (not as the
default) for STEP, IGES, STL, 3MF and OBJ files, and print
what was written
--uninstall-file-associations
remove that association again
--plm-url URL connect this session to the PLM server at URL (overrides
BREP_PLM_URL, which overrides plm.json in the config dir)
--plm-token-file FILE
read this session's PLM API token from FILE (overrides
BREP_PLM_TOKEN, which overrides the config dir's plm-token)
--plm-configure URL write plm.json (and, with --plm-token-file, plm-token at mode
0600) into the config dir and exit: the no-prompt setup an
installer script runs. With no PLM configured the app is
the file-based app
--plm-forget remove plm.json and plm-token from the config dir and exit
--plm-show print the PLM server this launch would use, and from where,
and exit (the token is never printed)
--plm-check sign in to that server and check it serves this version of
the app, exactly as a window launch would, and exit: 0 when
it connects, 1 with the refusal when it does not
--version print `brep-app <version>` and exit
--bake-worker run as the PLM's headless bake worker, with no window:
build each queued family member or template copy, keep
it or report why it failed, one line per job. It finds
its server and token as --plm-url / --plm-token-file do
--once one pass over the queue, then exit
--poll SECS seconds between passes when the queue is empty (default 30)
-h, --help this text";
#[cfg(not(target_arch = "wasm32"))]
#[derive(Debug, PartialEq, Default)]
struct Launch {
mcp: bool,
mcp_port: Option<u16>,
mcp_instance: Option<String>,
mcp_state_dir: Option<std::path::PathBuf>,
mcp_ephemeral: bool,
session_root: Option<std::path::PathBuf>,
help: bool,
version: bool,
open: Option<std::path::PathBuf>,
associations: Option<bool>,
kicad_library: bool,
plm: brep_app::plm::config::Flags,
plm_command: Option<PlmCommand>,
}
#[cfg(not(target_arch = "wasm32"))]
#[derive(Debug, PartialEq)]
enum PlmCommand {
Configure(String),
Forget,
Show,
Check,
}
#[cfg(not(target_arch = "wasm32"))]
fn parse_args(args: impl IntoIterator<Item = String>) -> Result<Launch, String> {
let mut launch = Launch::default();
let mut args = args.into_iter();
while let Some(arg) = args.next() {
match arg.as_str() {
"--mcp" => launch.mcp = true,
"--mcp-instance" => launch.mcp_instance = Some(args.next().ok_or("--mcp-instance needs a name")?),
"--mcp-state-dir" => launch.mcp_state_dir = Some(args.next().ok_or("--mcp-state-dir needs a directory")?.into()),
"--mcp-ephemeral" => launch.mcp_ephemeral = true,
"--mcp-port" => {
let value = args.next().ok_or("--mcp-port needs a port number")?;
launch.mcp_port = Some(value.parse().map_err(|_| format!("--mcp-port: `{value}` is not a port number"))?);
}
"--session-root" => {
launch.session_root = Some(args.next().ok_or("--session-root needs a directory")?.into());
}
"--kicad-library" => launch.kicad_library = true,
"--install-file-associations" => launch.associations = Some(true),
"--uninstall-file-associations" => launch.associations = Some(false),
"--plm-url" => launch.plm.url = Some(args.next().ok_or("--plm-url needs the server's URL")?),
"--plm-token-file" => {
launch.plm.token_file = Some(args.next().ok_or("--plm-token-file needs a file")?.into());
}
"--plm-configure" => {
let url = args.next().ok_or("--plm-configure needs the server's URL")?;
set_plm_command(&mut launch, PlmCommand::Configure(url))?;
}
"--plm-forget" => set_plm_command(&mut launch, PlmCommand::Forget)?,
"--plm-show" => set_plm_command(&mut launch, PlmCommand::Show)?,
"--plm-check" => set_plm_command(&mut launch, PlmCommand::Check)?,
"-h" | "--help" => launch.help = true,
"--version" => launch.version = true,
other if other.starts_with('-') => return Err(format!("unknown argument `{other}`")),
path if launch.open.is_none() => launch.open = Some(path.into()),
extra => return Err(format!("only one document can be opened at a time (`{extra}` is the second)")),
}
}
if usize::from(launch.mcp_instance.is_some()) + usize::from(launch.mcp_state_dir.is_some()) + usize::from(launch.mcp_ephemeral) > 1 {
return Err("give only one of --mcp-instance, --mcp-state-dir, --mcp-ephemeral".into());
}
if (launch.mcp_port.is_some() || launch.session_root.is_some()) && !launch.mcp {
return Err("--mcp-port and --session-root need --mcp".into());
}
if launch.associations.is_some() && (launch.mcp || launch.open.is_some() || launch.kicad_library) {
return Err("--install-file-associations / --uninstall-file-associations do not open a window".into());
}
if launch.kicad_library && (launch.mcp || launch.open.is_some()) {
return Err("--kicad-library opens the KiCad importer, not a document or an MCP session".into());
}
if let Some(command) = &launch.plm_command {
if launch.mcp || launch.open.is_some() || launch.kicad_library || launch.associations.is_some() {
return Err("--plm-configure / --plm-forget / --plm-show / --plm-check do not open a window".into());
}
if matches!(command, PlmCommand::Configure(_)) && launch.plm.url.is_some() {
return Err("--plm-configure takes the URL itself; drop --plm-url".into());
}
if matches!(command, PlmCommand::Forget) && (launch.plm.url.is_some() || launch.plm.token_file.is_some()) {
return Err("--plm-forget takes no --plm-url or --plm-token-file".into());
}
}
Ok(launch)
}
#[cfg(not(target_arch = "wasm32"))]
fn set_plm_command(launch: &mut Launch, command: PlmCommand) -> Result<(), String> {
if launch.plm_command.is_some() {
return Err("give one of --plm-configure, --plm-forget, --plm-show, --plm-check".into());
}
launch.plm_command = Some(command);
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
fn run_plm_command(command: &PlmCommand, flags: &brep_app::plm::config::Flags) -> ! {
use brep_app::plm::config;
let dir = config::app_config_dir();
let result = match command {
PlmCommand::Configure(url) => {
let token = match &flags.token_file {
Some(path) => std::fs::read_to_string(path)
.map(Some)
.map_err(|e| format!("--plm-token-file {}: {e}", path.display())),
None => Ok(None),
};
token
.and_then(|token| config::write(&dir, url, token.as_deref()))
.and_then(|()| config::resolve(&dir, &|_| None, &config::Flags::default()))
.map(|written| config::describe(written.as_ref(), &dir))
.map(|shown| format!("wrote {}\n{shown}", dir.display()))
}
PlmCommand::Forget => config::forget(&dir).map(|removed| {
if removed.is_empty() {
format!("no PLM configured in {}: nothing to remove\n", dir.display())
} else {
removed.iter().map(|p| format!("removed {}\n", p.display())).collect()
}
}),
PlmCommand::Show => config::resolve(&dir, &|name| std::env::var(name).ok(), flags)
.map(|resolved| config::describe(resolved.as_ref(), &dir)),
PlmCommand::Check => config::resolve(&dir, &|name| std::env::var(name).ok(), flags).and_then(|resolved| {
let config = resolved.ok_or_else(|| config::describe(None, &dir).trim_end().to_string())?;
let url = config.url.clone();
brep_app::store::check_native_plm(dir.clone(), config)
.map(|()| format!("connected to {url}: signed in, and it serves brep-app {}\n", env!("CARGO_PKG_VERSION")))
.map_err(|e| format!("PLM at {url}: {e}"))
}),
};
match result {
Ok(text) => {
print!("{text}");
std::process::exit(0)
}
Err(e) => {
eprintln!("brep-app: {e}");
std::process::exit(1)
}
}
}
#[cfg(not(target_arch = "wasm32"))]
fn main() -> eframe::Result<()> {
#[cfg(feature = "mcp")]
if brep_mcp_core::cad::code::worker_entry() {
return Ok(());
}
#[cfg(windows)]
if std::env::args_os().len() > 1 {
windows_console::attach_parent();
}
let _ = brep_app::logger::try_init();
#[cfg(feature = "mcp")]
match std::env::args().nth(1).as_deref() {
Some("--provision-typescript") => std::process::exit(match brep_mcp_core::cad::code::provision_typescript() {
Ok(report) => { println!("{report}"); 0 }
Err(error) => { eprintln!("brep-app: TypeScript provisioning: {error}"); 1 }
}),
Some("--typescript-status") => std::process::exit(match brep_mcp_core::cad::code::typescript_status() {
Ok((node, tsc)) => { println!("TypeScript {} available: node {} runs {}", brep_mcp_core::cad::code::TSC_VERSION, node.display(), tsc.display()); 0 }
Err(error) => { eprintln!("brep-app: {}", error.message); 1 }
}),
_ => {}
}
if std::env::args().nth(1).as_deref() == Some("--bake-worker") {
std::process::exit(brep_app::plm::bake::worker_main(std::env::args().skip(2)));
}
let launch = match parse_args(std::env::args().skip(1)) {
Ok(l) => l,
Err(e) => {
eprintln!("brep-app: {e}\n{USAGE}");
std::process::exit(2);
}
};
if launch.help {
println!("{USAGE}");
return Ok(());
}
if launch.version {
println!("brep-app {}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
if let Some(install) = launch.associations {
let (action, steps) = if install {
("install", brep_app::file_association::install())
} else {
("uninstall", brep_app::file_association::uninstall())
};
print!("{}", brep_app::file_association::report(action, &steps));
let failed = brep_app::file_association::failures(&steps);
std::process::exit(if failed == 0 { 0 } else { 1 });
}
if let Some(command) = &launch.plm_command {
run_plm_command(command, &launch.plm);
}
let plm = brep_app::plm::config::resolve(
&brep_app::plm::config::app_config_dir(),
&|name| std::env::var(name).ok(),
&launch.plm,
);
match &plm {
Ok(None) => {}
Ok(Some(config)) => log::info!("PLM configured: {} (from the {})", config.url, config.url_source.describe()),
Err(e) => eprintln!("brep-app: PLM config: {e}; this session uses the file stores"),
}
#[cfg(feature = "mcp")]
if let Err(error) = brep_app::mcp::configure_state(&brep_mcp_core::launch_state::Policy {
instance: launch.mcp_instance.clone(),
state_dir: launch.mcp_state_dir.clone(),
ephemeral: launch.mcp_ephemeral,
}) {
eprintln!("brep-app: MCP state: {error}");
if launch.mcp { std::process::exit(1); }
}
#[cfg(feature = "mcp")]
let mcp = if launch.mcp {
let port = launch.mcp_port.unwrap_or(brep_app::mcp::DEFAULT_PORT);
let mcp = match brep_app::mcp::launch([port], launch.session_root.clone()) {
Ok(l) => l,
Err(e) => {
eprintln!("brep-app: {e}");
std::process::exit(1);
}
};
println!("{}", mcp.instructions());
Some(mcp)
} else {
None
};
#[cfg(not(feature = "mcp"))]
if launch.mcp || launch.mcp_instance.is_some() || launch.mcp_state_dir.is_some() || launch.mcp_ephemeral {
eprintln!("brep-app: this build has no MCP server (built without the `mcp` feature)");
std::process::exit(1);
}
let native_options = eframe::NativeOptions {
multisampling: 0,
..Default::default()
};
let result = brep_app::desktop_loop::run(
"brep-app",
native_options,
Box::new(move |cc, waker| {
brep_app::fonts::install(&cc.egui_ctx);
let plm_store = |store| brep_app::automation::AppOptions { store: Some(store), ..Default::default() };
#[allow(unused_mut)]
let mut app = match plm {
Ok(None) => brep_app::app::BrepApp::new(cc).map_err(|e| e.to_string())?,
Ok(Some(config)) => {
let ctx = cc.egui_ctx.clone();
brep_app::plm::native::set_wake(move || ctx.request_repaint());
let store = brep_app::store::boot_native_store(brep_app::plm::config::app_config_dir(), config);
brep_app::app::BrepApp::new_with(cc, plm_store(store)).map_err(|e| e.to_string())?
}
Err(e) => {
let store = brep_app::store::file_store_with_notice(
brep_app::plm::config::app_config_dir(),
format!("PLM config: {e}"),
);
brep_app::app::BrepApp::new_with(cc, plm_store(store)).map_err(|e| e.to_string())?
}
};
#[cfg(feature = "automation")]
app.automation().set_waker(waker);
#[cfg(not(feature = "automation"))]
let _ = waker;
if let Some(path) = launch.open.as_deref() {
app.open_path_from_command_line(path);
}
app.start_update_check(&cc.egui_ctx);
app.start_report_followup(&cc.egui_ctx);
#[cfg(feature = "mcp")]
if let Some(launch) = mcp {
let adapter = app.diagnostics().adapter_line();
brep_app::mcp::start(app.automation().clone(), adapter, launch)?;
}
Ok(app)
}),
)
.map_err(|e| eframe::Error::AppCreation(e.into()));
if !brep_app::store::flush_writes(std::time::Duration::from_secs(10)) {
eprintln!("brep-app: some settings were still being written at exit");
}
brep_app::update::relaunch_if_installed();
result
}
#[cfg(target_arch = "wasm32")]
fn main() {}
#[cfg(windows)]
mod windows_console {
use std::ffi::c_void;
const ATTACH_PARENT_PROCESS: u32 = u32::MAX; const STD_OUTPUT_HANDLE: u32 = -11i32 as u32;
const STD_ERROR_HANDLE: u32 = -12i32 as u32;
const GENERIC_READ: u32 = 0x8000_0000;
const GENERIC_WRITE: u32 = 0x4000_0000;
const FILE_SHARE_READ: u32 = 1;
const FILE_SHARE_WRITE: u32 = 2;
const OPEN_EXISTING: u32 = 3;
const INVALID_HANDLE_VALUE: *mut c_void = -1isize as *mut c_void;
#[link(name = "kernel32")]
extern "system" {
fn AttachConsole(process: u32) -> i32;
fn GetStdHandle(which: u32) -> *mut c_void;
fn SetStdHandle(which: u32, handle: *mut c_void) -> i32;
fn CreateFileW(
name: *const u16,
access: u32,
share: u32,
security: *mut c_void,
disposition: u32,
flags: u32,
template: *mut c_void,
) -> *mut c_void;
}
pub fn attach_parent() {
unsafe {
if AttachConsole(ATTACH_PARENT_PROCESS) == 0 {
return; }
let name: Vec<u16> = "CONOUT$".encode_utf16().chain(Some(0)).collect();
for which in [STD_OUTPUT_HANDLE, STD_ERROR_HANDLE] {
let current = GetStdHandle(which);
if current.is_null() || current == INVALID_HANDLE_VALUE {
let console = CreateFileW(
name.as_ptr(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null_mut(),
OPEN_EXISTING,
0,
std::ptr::null_mut(),
);
if console != INVALID_HANDLE_VALUE {
SetStdHandle(which, console);
}
}
}
}
}
}