#![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)
--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, 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);
}
}
}
}
}
}