From 7c55d751ba218d0d6711806db14d67b43485b844 Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Tue, 31 Mar 2026 23:22:31 -0500 Subject: [PATCH 01/23] feat: add lis all remote templates --- .gitattributes | 1 - Cargo.toml | 2 +- README.md | 4 ---- src/cli/mod.rs | 8 ++++++-- src/commands/template.rs | 42 ++++++++++++++++++++++++++++++++-------- src/templates/mod.rs | 34 ++++++++++++++++++++++++++++++++ 6 files changed, 75 insertions(+), 16 deletions(-) diff --git a/.gitattributes b/.gitattributes index a8a7839..6313b56 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1 @@ * text=auto eol=lf -install.sh text eol=lf diff --git a/Cargo.toml b/Cargo.toml index 8b1e61a..13a6489 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ walkdir = "2.5" dirs = "6.0" # HTTP client (template downloads) -reqwest = { version = "0.13", features = ["blocking"] } +reqwest = { version = "0.13", features = ["blocking", "json"] } # Archive extraction (GitHub tarballs) flate2 = "1.1" diff --git a/README.md b/README.md index 06c9a94..ebbfffc 100644 --- a/README.md +++ b/README.md @@ -231,10 +231,6 @@ texforge fmt --check # check without modifying (CI-friendly) --- -## Roadmap - -See [texforge-spec.md](texforge-spec.md) for the complete specification and roadmap. - --- ## License diff --git a/src/cli/mod.rs b/src/cli/mod.rs index bcba729..391c982 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -43,7 +43,11 @@ enum Commands { #[derive(Subcommand)] enum TemplateAction { /// List available templates - List, + List { + /// Also show templates available in the remote registry + #[arg(long)] + all: bool, + }, /// Add a template from URL or registry Add { source: String }, /// Remove a template @@ -60,7 +64,7 @@ impl Cli { Commands::Fmt { check } => commands::fmt::execute(check), Commands::Check => commands::check::execute(), Commands::Template { action } => match action { - TemplateAction::List => commands::template::list(), + TemplateAction::List { all } => commands::template::list(all), TemplateAction::Add { source } => commands::template::add(&source), TemplateAction::Remove { name } => commands::template::remove(&name), TemplateAction::Validate { name } => commands::template::validate(&name), diff --git a/src/commands/template.rs b/src/commands/template.rs index ee0ed6f..1d6a49c 100644 --- a/src/commands/template.rs +++ b/src/commands/template.rs @@ -5,17 +5,43 @@ use anyhow::Result; use crate::templates; /// List available templates. -pub fn list() -> Result<()> { +pub fn list(all: bool) -> Result<()> { let cached = templates::list_cached()?; - if cached.is_empty() { - println!("No templates installed locally."); - println!("The 'general' template is always available (built-in)."); - } else { - println!("Installed templates:"); - for name in &cached { - println!(" - {}", name); + let installed: std::collections::HashSet<&str> = + cached.iter().map(String::as_str).collect(); + + println!("Installed:"); + println!(" - general (built-in)"); + for name in &cached { + println!(" - {}", name); + } + + if all { + print!("\nFetching remote registry..."); + match templates::list_remote() { + Ok(remote) => { + println!("\r \r"); // clear line + let available: Vec<&str> = remote + .iter() + .map(String::as_str) + .filter(|n| *n != "general" && !installed.contains(n)) + .collect(); + if available.is_empty() { + println!("Available (not installed): none"); + } else { + println!("Available (not installed):"); + for name in available { + println!(" - {}", name); + } + } + println!("\nRun 'texforge template add ' to install."); + } + Err(e) => { + println!("\nCould not reach registry: {}", e); + } } } + Ok(()) } diff --git a/src/templates/mod.rs b/src/templates/mod.rs index 409fbc1..7c10879 100644 --- a/src/templates/mod.rs +++ b/src/templates/mod.rs @@ -153,6 +153,40 @@ pub fn download(name: &str) -> Result { Ok(ResolvedTemplate { files }) } +/// List template names available in the remote registry. +pub fn list_remote() -> Result> { + let url = format!( + "https://api.github.com/repos/{}/contents", + REGISTRY_REPO + ); + + let response = reqwest::blocking::Client::new() + .get(&url) + .header("User-Agent", "texforge") + .send() + .context("Failed to connect to template registry")?; + + if !response.status().is_success() { + anyhow::bail!("Registry returned HTTP {}", response.status()); + } + + #[derive(serde::Deserialize)] + struct Entry { + name: String, + #[serde(rename = "type")] + kind: String, + } + + let entries: Vec = response.json()?; + let mut names: Vec = entries + .into_iter() + .filter(|e| e.kind == "dir") + .map(|e| e.name) + .collect(); + names.sort(); + Ok(names) +} + /// List template names available in local cache. pub fn list_cached() -> Result> { let dir = utils::templates_dir()?; From 1de4271bc812d9ccf805afe8a64f0792dc667f16 Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Tue, 31 Mar 2026 23:58:30 -0500 Subject: [PATCH 02/23] feat: add texforge init command to migrate existing LaTeX projects --- src/cli/mod.rs | 3 ++ src/commands/init.rs | 97 ++++++++++++++++++++++++++++++++++++++++++++ src/commands/mod.rs | 1 + 3 files changed, 101 insertions(+) create mode 100644 src/commands/init.rs diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 391c982..88a297b 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -15,6 +15,8 @@ pub struct Cli { #[derive(Subcommand)] enum Commands { + /// Initialize a texforge project in the current directory + Init, /// Create a new project from a template New { /// Project name @@ -59,6 +61,7 @@ enum TemplateAction { impl Cli { pub fn execute(self) -> Result<()> { match self.command { + Commands::Init => commands::init::execute(), Commands::New { name, template } => commands::new::execute(&name, template.as_deref()), Commands::Build => commands::build::execute(), Commands::Fmt { check } => commands::fmt::execute(check), diff --git a/src/commands/init.rs b/src/commands/init.rs new file mode 100644 index 0000000..eaefe93 --- /dev/null +++ b/src/commands/init.rs @@ -0,0 +1,97 @@ +//! `texforge init` command implementation. + +use std::path::Path; + +use anyhow::Result; + +/// Initialize a texforge project in the current directory. +pub fn execute() -> Result<()> { + let root = std::env::current_dir()?; + + if root.join("project.toml").exists() { + anyhow::bail!("project.toml already exists in this directory"); + } + + // Detect entry point: file with \documentclass + let entry = detect_entry(&root) + .unwrap_or_else(|| "main.tex".to_string()); + + // Detect bibliography: first .bib file found + let bib = detect_bib(&root); + + let bib_line = match &bib { + Some(b) => format!("bibliografia = \"{}\"", b), + None => "# bibliografia = \"refs.bib\"".to_string(), + }; + + let project_toml = format!( + r#"[documento] +titulo = "{}" +autor = "Author" +template = "general" + +[compilacion] +entry = "{}" +{} +"#, + root.file_name() + .and_then(|n| n.to_str()) + .unwrap_or("documento"), + entry, + bib_line, + ); + + std::fs::write(root.join("project.toml"), &project_toml)?; + + println!("✅ project.toml generated"); + println!(" entry: {}", entry); + if let Some(b) = bib { + println!(" bibliography: {}", b); + } + println!(); + println!("Edit project.toml to set titulo and autor, then run:"); + println!(" texforge build"); + + Ok(()) +} + +/// Find the .tex file that contains \documentclass. +fn detect_entry(root: &Path) -> Option { + for entry in walkdir::WalkDir::new(root) + .max_depth(2) + .into_iter() + .filter_map(|e| e.ok()) + { + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()) != Some("tex") { + continue; + } + if let Ok(content) = std::fs::read_to_string(path) { + if content.contains("\\documentclass") { + return path + .strip_prefix(root) + .ok() + .map(|p| p.to_string_lossy().to_string()); + } + } + } + None +} + +/// Find the first .bib file in the project. +fn detect_bib(root: &Path) -> Option { + for entry in walkdir::WalkDir::new(root) + .max_depth(3) + .into_iter() + .filter_map(|e| e.ok()) + { + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()) == Some("bib") { + return path + .strip_prefix(root) + .ok() + .map(|p| p.to_string_lossy().to_string()); + } + } + None +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 467bf6d..a6279fe 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -3,5 +3,6 @@ pub mod build; pub mod check; pub mod fmt; +pub mod init; pub mod new; pub mod template; From cbaa718f1ed5ef5f9f08162c1fab23adaa886fae Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Tue, 31 Mar 2026 23:58:34 -0500 Subject: [PATCH 03/23] feat: detect \lstinputlisting and \inputminted missing files in linter --- src/linter/mod.rs | 68 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/linter/mod.rs b/src/linter/mod.rs index 8520197..83fe42c 100644 --- a/src/linter/mod.rs +++ b/src/linter/mod.rs @@ -123,6 +123,30 @@ fn check_references( } } + for arg in extract_commands(&line, "lstinputlisting") { + let path = root.join(arg); + if !path.exists() { + errors.push(LintError { + file: rel.to_string(), + line: line_num, + message: format!("\\lstinputlisting{{{}}} — file not found", arg), + suggestion: None, + }); + } + } + + for args in extract_commands_two_args(&line, "inputminted") { + let path = root.join(args); + if !path.exists() { + errors.push(LintError { + file: rel.to_string(), + line: line_num, + message: format!("\\inputminted{{...}}{{{}}} — file not found", args), + suggestion: None, + }); + } + } + if bib_file.is_some() { for arg in extract_commands(&line, "cite") { for key in arg.split(',') { @@ -204,6 +228,50 @@ fn check_environments(rel: &str, content: &str, errors: &mut Vec) { } } +/// Extract the second `{arg}` from `\command{first}{second}` occurrences. +fn extract_commands_two_args<'a>(line: &'a str, cmd: &str) -> Vec<&'a str> { + let mut results = Vec::new(); + let pattern = format!("\\{}", cmd); + let mut search = line; + + while let Some(pos) = search.find(&pattern) { + let after = &search[pos + pattern.len()..]; + // Skip optional args [...] + let after = if after.starts_with('[') { + match after.find(']') { + Some(end) => &after[end + 1..], + None => break, + } + } else { + after + }; + // Skip first {arg} + let after = if after.starts_with('{') { + match after.find('}') { + Some(end) => &after[end + 1..], + None => { search = after; continue; } + } + } else { + search = after; + continue; + }; + // Extract second {arg} + if after.starts_with('{') { + if let Some(end) = after.find('}') { + let arg = after[1..end].trim(); + if !arg.is_empty() { + results.push(arg); + } + search = &after[end + 1..]; + continue; + } + } + search = after; + } + + results +} + /// Extract arguments from `\command{arg}` and `\command[opts]{arg}` occurrences in a line. fn extract_commands<'a>(line: &'a str, cmd: &str) -> Vec<&'a str> { let mut results = Vec::new(); From 4f1fce36a63c5c8f2393753f89b7c8865f640bcc Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Tue, 31 Mar 2026 23:58:37 -0500 Subject: [PATCH 04/23] feat: auto-install tectonic on first build if not found --- Cargo.toml | 3 +- src/compiler/mod.rs | 110 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 106 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 13a6489..b8e45c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,9 +33,10 @@ dirs = "6.0" # HTTP client (template downloads) reqwest = { version = "0.13", features = ["blocking", "json"] } -# Archive extraction (GitHub tarballs) +# Archive extraction (GitHub tarballs + tectonic zip on Windows) flate2 = "1.1" tar = "0.4" +zip = { version = "2", default-features = false, features = ["deflate"] } [dev-dependencies] tempfile = "3.8" diff --git a/src/compiler/mod.rs b/src/compiler/mod.rs index 6921b33..33256d5 100644 --- a/src/compiler/mod.rs +++ b/src/compiler/mod.rs @@ -96,7 +96,7 @@ fn parse_errors(raw: &str) -> Vec { errors } -/// Find the tectonic binary in PATH or known locations. +/// Find the tectonic binary in PATH or known locations, auto-installing if needed. fn find_tectonic() -> Result { // Check PATH if let Ok(output) = Command::new("which").arg("tectonic").output() { @@ -107,8 +107,9 @@ fn find_tectonic() -> Result { } // Check known locations (including texforge-managed install) + let managed = dirs::home_dir().map(|h| h.join(".texforge/bin/tectonic")); for candidate in [ - dirs::home_dir().map(|h| h.join(".texforge/bin/tectonic")), + managed.clone(), dirs::home_dir().map(|h| h.join(".cargo/bin/tectonic")), Some("/usr/local/bin/tectonic".into()), Some("/opt/homebrew/bin/tectonic".into()), @@ -121,9 +122,106 @@ fn find_tectonic() -> Result { } } - anyhow::bail!( - "Tectonic not found. Install everything with:\n\ - \n curl -fsSL https://raw.githubusercontent.com/JheisonMB/texforge/main/install.sh | sh\n\ - \nor install tectonic separately: cargo install tectonic" + // Auto-install tectonic + eprintln!("Tectonic not found. Installing automatically..."); + let dest = managed.ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?; + install_tectonic(&dest)?; + Ok(dest) +} + +/// Download and install tectonic to the given path. +fn install_tectonic(dest: &std::path::Path) -> Result<()> { + let target = current_target()?; + let version = "0.15.0"; + let (filename, is_zip) = if target.contains("windows") { + (format!("tectonic-{}-{}.zip", version, target), true) + } else { + (format!("tectonic-{}-{}.tar.gz", version, target), false) + }; + + let url = format!( + "https://github.com/tectonic-typesetting/tectonic/releases/download/tectonic%40{}/{}", + version, filename ); + + eprintln!("Downloading tectonic {}...", version); + + let response = reqwest::blocking::Client::new() + .get(&url) + .header("User-Agent", "texforge") + .send() + .context("Failed to download tectonic")?; + + if !response.status().is_success() { + anyhow::bail!( + "Failed to download tectonic: HTTP {}\nURL: {}", + response.status(), + url + ); + } + + let bytes = response.bytes()?; + + if let Some(parent) = dest.parent() { + std::fs::create_dir_all(parent)?; + } + + if is_zip { + install_from_zip(&bytes, dest)?; + } else { + install_from_targz(&bytes, dest)?; + } + + // Make executable on Unix + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(dest, std::fs::Permissions::from_mode(0o755))?; + } + + eprintln!("✅ Tectonic installed to {}", dest.display()); + Ok(()) +} + +fn install_from_targz(bytes: &[u8], dest: &std::path::Path) -> Result<()> { + let decoder = flate2::read::GzDecoder::new(bytes); + let mut archive = tar::Archive::new(decoder); + for entry in archive.entries()? { + let mut entry = entry?; + let path = entry.path()?.to_string_lossy().to_string(); + if path.ends_with("tectonic") || path == "tectonic" { + std::io::copy(&mut entry, &mut std::fs::File::create(dest)?)?; + return Ok(()); + } + } + anyhow::bail!("tectonic binary not found in archive") +} + +fn install_from_zip(bytes: &[u8], dest: &std::path::Path) -> Result<()> { + let cursor = std::io::Cursor::new(bytes); + let mut archive = zip::ZipArchive::new(cursor)?; + for i in 0..archive.len() { + let mut file = archive.by_index(i)?; + if file.name().ends_with("tectonic.exe") || file.name() == "tectonic.exe" { + std::io::copy(&mut file, &mut std::fs::File::create(dest)?)?; + return Ok(()); + } + } + anyhow::bail!("tectonic.exe not found in archive") +} + +/// Detect the current platform target triple for tectonic releases. +fn current_target() -> Result<&'static str> { + #[cfg(all(target_os = "linux", target_arch = "x86_64"))] + return Ok("x86_64-unknown-linux-musl"); + #[cfg(all(target_os = "linux", target_arch = "aarch64"))] + return Ok("aarch64-unknown-linux-musl"); + #[cfg(all(target_os = "macos", target_arch = "x86_64"))] + return Ok("x86_64-apple-darwin"); + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] + return Ok("aarch64-apple-darwin"); + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + return Ok("x86_64-pc-windows-msvc"); + #[allow(unreachable_code)] + anyhow::bail!("Unsupported platform for automatic tectonic installation") } From 58ff96626630b2f8c6128b27fe4c56178b803e7c Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Tue, 31 Mar 2026 23:58:40 -0500 Subject: [PATCH 05/23] feat: add install.ps1 for Windows and simplify install.sh (tectonic auto-managed) --- install.ps1 | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++ install.sh | 45 +++++-------------------------- 2 files changed, 83 insertions(+), 39 deletions(-) create mode 100644 install.ps1 diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..79e96fa --- /dev/null +++ b/install.ps1 @@ -0,0 +1,77 @@ +# install.ps1 — download and install texforge on Windows +# tectonic (LaTeX engine) is installed automatically on first build +# Usage: irm https://raw.githubusercontent.com/JheisonMB/texforge/main/install.ps1 | iex +# +# Options (set as env vars before running): +# $env:VERSION = "0.1.0" # pin a specific version +# $env:INSTALL_DIR = "C:\my\bin" # custom install directory + +$ErrorActionPreference = "Stop" + +$Repo = "JheisonMB/texforge" +$Binary = "texforge.exe" +$Target = "x86_64-pc-windows-msvc" +$InstallDir = if ($env:INSTALL_DIR) { $env:INSTALL_DIR } else { "$env:USERPROFILE\.local\bin" } + +function Info($label, $msg) { + Write-Host " " -NoNewline + Write-Host $label -ForegroundColor Blue -NoNewline + Write-Host " $msg" +} + +function Fail($msg) { + Write-Host " error: $msg" -ForegroundColor Red + exit 1 +} + +# --- resolve version --- +if ($env:VERSION) { + $Tag = "v$($env:VERSION)" + Info "version" "$Tag (pinned)" +} else { + $latest = Invoke-RestMethod "https://api.github.com/repos/$Repo/releases/latest" + $Tag = $latest.tag_name + if (-not $Tag) { Fail "Could not resolve latest release tag" } + Info "version" "$Tag (latest)" +} + +# --- download --- +$Archive = "texforge-$Tag-$Target.zip" +$Url = "https://github.com/$Repo/releases/download/$Tag/$Archive" +$Tmp = Join-Path $env:TEMP "texforge-install" +New-Item -ItemType Directory -Force -Path $Tmp | Out-Null + +Info "download" $Url +try { + Invoke-WebRequest -Uri $Url -OutFile "$Tmp\$Archive" -UseBasicParsing +} catch { + Fail "Download failed: $_`nURL: $Url" +} + +# --- extract --- +Expand-Archive -Path "$Tmp\$Archive" -DestinationPath $Tmp -Force +$extracted = Join-Path $Tmp $Binary +if (-not (Test-Path $extracted)) { Fail "Binary not found in archive" } + +# --- install --- +New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null +Copy-Item $extracted "$InstallDir\$Binary" -Force +Info "installed" "$InstallDir\$Binary" + +# --- ensure PATH --- +$userPath = [Environment]::GetEnvironmentVariable("PATH", "User") +if ($userPath -notlike "*$InstallDir*") { + [Environment]::SetEnvironmentVariable("PATH", "$InstallDir;$userPath", "User") + $env:PATH = "$InstallDir;$env:PATH" + Info "updated" "User PATH" +} + +# --- cleanup --- +Remove-Item $Tmp -Recurse -Force + +# --- verify --- +$ver = & "$InstallDir\$Binary" --version 2>$null +Info "done" $ver +Write-Host "" +Info "ready" "Run 'texforge new my-project' to get started!" +Info "note" "tectonic (LaTeX engine) will be installed automatically on first build" diff --git a/install.sh b/install.sh index 7b92314..cefa3f4 100755 --- a/install.sh +++ b/install.sh @@ -1,13 +1,12 @@ #!/bin/sh -# install.sh — download and install texforge + tectonic from GitHub Releases +# install.sh — download and install texforge from GitHub Releases +# tectonic (LaTeX engine) is installed automatically on first build # Usage: curl -fsSL https://raw.githubusercontent.com/JheisonMB/texforge/main/install.sh | sh set -eu REPO="JheisonMB/texforge" BINARY="texforge" -TECTONIC_VERSION="0.15.0+20251006" INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}" -TEXFORGE_DIR="$HOME/.texforge/bin" info() { printf ' \033[1;34m%s\033[0m %s\n' "$1" "$2"; } error() { printf ' \033[1;31merror:\033[0m %s\n' "$1" >&2; exit 1; } @@ -35,34 +34,7 @@ TMPDIR="$(mktemp -d)" trap 'rm -rf "$TMPDIR"' EXIT # ============================================================ -# 1. Install tectonic (LaTeX engine) -# ============================================================ - -if command -v tectonic >/dev/null 2>&1; then - info "tectonic" "already installed ($(tectonic --version 2>/dev/null || echo 'unknown version'))" -elif [ -x "$TEXFORGE_DIR/tectonic" ]; then - info "tectonic" "already installed at $TEXFORGE_DIR/tectonic" -else - info "tectonic" "installing v${TECTONIC_VERSION}..." - - TECTONIC_ARCHIVE="tectonic-${TECTONIC_VERSION}-${TARGET}.tar.gz" - TECTONIC_URL="https://github.com/tectonic-typesetting/tectonic/releases/download/continuous/${TECTONIC_ARCHIVE}" - - info "download" "$TECTONIC_URL" - HTTP_CODE=$(curl -fSL -w '%{http_code}' -o "$TMPDIR/$TECTONIC_ARCHIVE" "$TECTONIC_URL" 2>/dev/null) || true - [ "$HTTP_CODE" = "200" ] || error "Tectonic download failed (HTTP $HTTP_CODE). URL:\n $TECTONIC_URL" - - tar xzf "$TMPDIR/$TECTONIC_ARCHIVE" -C "$TMPDIR" - [ -f "$TMPDIR/tectonic" ] || error "Tectonic binary not found in archive" - - mkdir -p "$TEXFORGE_DIR" - mv "$TMPDIR/tectonic" "$TEXFORGE_DIR/tectonic" - chmod +x "$TEXFORGE_DIR/tectonic" - info "installed" "$TEXFORGE_DIR/tectonic" -fi - -# ============================================================ -# 2. Install texforge +# 1. Install texforge # ============================================================ # --- resolve latest version --- @@ -94,7 +66,7 @@ chmod +x "$INSTALL_DIR/$BINARY" info "installed" "$INSTALL_DIR/$BINARY" # ============================================================ -# 3. Ensure PATH includes both directories +# 2. Ensure PATH # ============================================================ PATHS_TO_ADD="" @@ -102,10 +74,6 @@ case ":$PATH:" in *":$INSTALL_DIR:"*) ;; *) PATHS_TO_ADD="$INSTALL_DIR" ;; esac -case ":$PATH:" in - *":$TEXFORGE_DIR:"*) ;; - *) PATHS_TO_ADD="$PATHS_TO_ADD $TEXFORGE_DIR" ;; -esac if [ -n "$PATHS_TO_ADD" ]; then for dir in $PATHS_TO_ADD; do @@ -125,11 +93,10 @@ if [ -n "$PATHS_TO_ADD" ]; then fi # ============================================================ -# 4. Verify +# 3. Verify # ============================================================ info "done" "$($INSTALL_DIR/$BINARY --version 2>/dev/null || echo "$BINARY installed")" -TECTONIC_BIN=$(command -v tectonic 2>/dev/null || echo "$TEXFORGE_DIR/tectonic") -info "tectonic" "$($TECTONIC_BIN --version 2>/dev/null || echo "installed")" echo "" info "ready" "Run 'texforge new my-project' to get started!" +info "note" "tectonic (LaTeX engine) will be installed automatically on first build" From 342f694f6ee36e0225bebdf5110aedb4914c271a Mon Sep 17 00:00:00 2001 From: Jheison Martinez Bolivar Date: Tue, 31 Mar 2026 23:58:44 -0500 Subject: [PATCH 06/23] docs: update README with init command, template list --all, and tectonic auto-install --- README.md | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ebbfffc..0c9a6c7 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,19 @@ Self-contained LaTeX to PDF compiler — one curl, zero friction. No TeX Live, n ### Quick install (recommended) +**Linux / macOS:** + ```bash curl -fsSL https://raw.githubusercontent.com/JheisonMB/texforge/main/install.sh | sh ``` -This downloads and installs both `texforge` and `tectonic` (the LaTeX engine). Nothing else needed. No Rust toolchain required. +**Windows (PowerShell):** + +```powershell +irm https://raw.githubusercontent.com/JheisonMB/texforge/main/install.ps1 | iex +``` + +This downloads and installs `texforge`. No Rust toolchain required. Tectonic (the LaTeX engine) is installed automatically on first build. You can customize the install: @@ -29,17 +37,18 @@ VERSION=0.1.0 curl -fsSL https://raw.githubusercontent.com/JheisonMB/texforge/ma INSTALL_DIR=/usr/local/bin curl -fsSL https://raw.githubusercontent.com/JheisonMB/texforge/main/install.sh | sh ``` +```powershell +# Pin a specific version (PowerShell) +$env:VERSION="0.1.0"; irm https://raw.githubusercontent.com/JheisonMB/texforge/main/install.ps1 | iex +``` + ### Via cargo ```bash cargo install texforge ``` -**Note:** This only installs texforge. You also need tectonic for compilation: - -```bash -cargo install tectonic -``` +Tectonic (the LaTeX engine) is installed automatically on first build. No extra steps needed. Available on [crates.io](https://crates.io/crates/texforge). @@ -82,11 +91,13 @@ texforge build |---|---| | `texforge new ` | Create new project from template | | `texforge new -t