From 67294096ed02e8ed424e0851d7a68f35c38b7744 Mon Sep 17 00:00:00 2001 From: scawful Date: Tue, 30 Dec 2025 18:09:34 -0500 Subject: [PATCH] Add AFS Studio CLI helpers and versioning --- README.md | 5 + apps/studio/CMakeLists.txt | 3 +- apps/studio/README.md | 37 +++- apps/studio/src/app.cc | 15 +- apps/studio/src/core/version.h | 15 ++ apps/studio/src/data_loader.cc | 16 +- apps/studio/src/main.cc | 58 ++++-- apps/studio/src/models/state.h | 3 + apps/studio/src/ui/components/panels.cc | 36 +++- apps/studio/src/widgets/training_status.cpp | 4 +- docs/PORTING_MAP.md | 8 +- src/afs/cli.py | 197 ++++++++++++++++++++ 12 files changed, 365 insertions(+), 32 deletions(-) create mode 100644 apps/studio/src/core/version.h diff --git a/README.md b/README.md index d8df9b0..25d4f91 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,8 @@ Quickstart: - `python -m afs orchestrator list` Discovery skips directory names in `general.discovery_ignore` (default: legacy, archive, archives). + +Studio: +- `python -m afs studio build` +- `python -m afs studio run --build` +- `python -m afs studio install --prefix ~/.local` diff --git a/apps/studio/CMakeLists.txt b/apps/studio/CMakeLists.txt index 8da4e7c..6cc2b87 100644 --- a/apps/studio/CMakeLists.txt +++ b/apps/studio/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.15) -project(afs_studio LANGUAGES CXX) +project(afs_studio VERSION 0.0.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -131,6 +131,7 @@ target_include_directories(afs_studio PRIVATE target_compile_definitions(afs_studio PRIVATE IMGUI_IMPL_OPENGL_LOADER_GLAD=0 + AFS_STUDIO_VERSION="${PROJECT_VERSION}" ) if(TARGET OpenGL::OpenGL) diff --git a/apps/studio/README.md b/apps/studio/README.md index afd0ccf..9597227 100644 --- a/apps/studio/README.md +++ b/apps/studio/README.md @@ -5,15 +5,39 @@ Native C++17 visualization and training management application for AFS. ## Build ```bash -# From project root -cmake -B build -S . -DAFS_BUILD_STUDIO=ON -cmake --build build --target afs_studio +# From AFS repo root +cmake -S apps/studio -B build/studio +cmake --build build/studio --target afs_studio ``` ## Run ```bash -./build/apps/studio/afs_studio +./build/studio/afs_studio --data ~/src/training +``` + +## Install (local) + +```bash +cmake --install build/studio --prefix ~/.local +# Ensure ~/.local/bin is on PATH +``` + +## CLI helpers + +```bash +python -m afs studio build +python -m afs studio run --build +python -m afs studio install --prefix ~/.local +python -m afs studio alias +``` + +## Quick aliases + +```bash +export AFS_ROOT=~/src/trunk/lab/afs +alias afs-studio='PYTHONPATH="$AFS_ROOT/src" python -m afs studio run --build' +alias afs-studio-build='PYTHONPATH="$AFS_ROOT/src" python -m afs studio build' ``` ## Data sources @@ -23,6 +47,11 @@ cmake --build build --target afs_studio - Dataset registry: `AFS_DATASET_REGISTRY` or `${AFS_TRAINING_ROOT}/index/dataset_registry.json`. - Resource index: `AFS_RESOURCE_INDEX` or `${AFS_TRAINING_ROOT}/index/resource_index.json`. +## Flags + +- `--data` or `--data-path`: override training root +- `--version`: print version and exit + ## Features - **Dashboard**: Training metrics overview diff --git a/apps/studio/src/app.cc b/apps/studio/src/app.cc index 37599e9..d38e717 100644 --- a/apps/studio/src/app.cc +++ b/apps/studio/src/app.cc @@ -1,7 +1,8 @@ #include "app.h" -#include "core/logger.h" -#include "core/context.h" #include "core/assets.h" +#include "core/context.h" +#include "core/logger.h" +#include "core/version.h" #include "ui/panels/chat_panel.h" #include @@ -41,6 +42,9 @@ App::App(const std::string& data_path) : data_path_(data_path), loader_(data_path) { LOG_INFO("AFS Studio initialize with data path: " + data_path); + state_.studio_version = studio::core::StudioVersion(); + state_.studio_data_root = data_path_; + std::snprintf(state_.new_agent_role.data(), state_.new_agent_role.size(), "Evaluator"); std::snprintf(state_.new_mission_owner.data(), state_.new_mission_owner.size(), "Ops"); std::snprintf(state_.system_prompt.data(), state_.system_prompt.size(), @@ -69,7 +73,12 @@ App::App(const std::string& data_path) SeedDefaultState(); // Create graphics context - context_ = std::make_unique("AFS Studio", 1400, 900); + std::string title = "AFS Studio"; + if (!state_.studio_version.empty()) { + title += " "; + title += state_.studio_version; + } + context_ = std::make_unique(title, 1400, 900); if (context_->IsValid()) { fonts_ = studio::core::AssetLoader::LoadFonts(); themes::ApplyHafsTheme(); diff --git a/apps/studio/src/core/version.h b/apps/studio/src/core/version.h new file mode 100644 index 0000000..15ea747 --- /dev/null +++ b/apps/studio/src/core/version.h @@ -0,0 +1,15 @@ +#pragma once + +namespace afs { +namespace studio { +namespace core { + +#ifndef AFS_STUDIO_VERSION +#define AFS_STUDIO_VERSION "0.0.0" +#endif + +inline const char* StudioVersion() { return AFS_STUDIO_VERSION; } + +} // namespace core +} // namespace studio +} // namespace afs diff --git a/apps/studio/src/data_loader.cc b/apps/studio/src/data_loader.cc index 24ce5e5..2d2a8ce 100644 --- a/apps/studio/src/data_loader.cc +++ b/apps/studio/src/data_loader.cc @@ -41,10 +41,14 @@ std::optional ResolveHafsScawfulRoot() { auto trunk_root = ResolveTrunkRoot(); if (trunk_root) { - auto candidate = *trunk_root / "scawful" / "research" / "afs_scawful"; + auto candidate = *trunk_root / "lab" / "afs_scawful"; if (studio::core::FileSystem::Exists(candidate)) { return candidate; } + auto legacy = *trunk_root / "scawful" / "research" / "afs_scawful"; + if (studio::core::FileSystem::Exists(legacy)) { + return legacy; + } } return std::nullopt; @@ -1019,9 +1023,15 @@ void DataLoader::MountDrive(const std::string& name) { } else { auto trunk_root = ResolveTrunkRoot(); if (trunk_root) { - script_path = *trunk_root / "scawful" / "research" / "afs_scawful" / "scripts" / "mount_windows.sh"; + script_path = *trunk_root / "lab" / "afs_scawful" / "scripts" / "mount_windows.sh"; + if (!studio::core::FileSystem::Exists(script_path)) { + script_path = *trunk_root / "scawful" / "research" / "afs_scawful" / "scripts" / "mount_windows.sh"; + } } else { - script_path = studio::core::FileSystem::ResolvePath("~/src/trunk/scawful/research/afs_scawful/scripts/mount_windows.sh"); + script_path = studio::core::FileSystem::ResolvePath("~/src/trunk/lab/afs_scawful/scripts/mount_windows.sh"); + if (!studio::core::FileSystem::Exists(script_path)) { + script_path = studio::core::FileSystem::ResolvePath("~/src/trunk/scawful/research/afs_scawful/scripts/mount_windows.sh"); + } } } diff --git a/apps/studio/src/main.cc b/apps/studio/src/main.cc index f336f4d..656bb63 100644 --- a/apps/studio/src/main.cc +++ b/apps/studio/src/main.cc @@ -1,11 +1,12 @@ -/// AFS Training Data Visualization - Main Entry Point +/// AFS Studio - Main Entry Point /// -/// Usage: afs_viz [data_path] -/// data_path: Path to training data directory (default: ~/src/training if present) +/// Usage: afs_studio [--data PATH] +/// --data PATH: Path to training data directory (default: ~/src/training if present) +/// --version: Print version and exit /// /// Build: -/// cmake -B build -S src/cc -DAFS_BUILD_VIZ=ON -/// cmake --build build +/// cmake -S apps/studio -B build/studio +/// cmake --build build/studio --target afs_studio /// /// Keys: /// F5 - Refresh data @@ -17,29 +18,62 @@ #include #include "app.h" -#include "core/logger.h" #include "core/filesystem.h" +#include "core/logger.h" +#include "core/version.h" + +namespace { + +void PrintUsage() { + std::cout << "afs_studio [--data PATH]\n" + << " --data PATH Training data root (default: ~/src/training)\n" + << " --version Print version and exit\n" + << " -h, --help Show this help\n"; +} + +} // namespace int main(int argc, char* argv[]) { using afs::studio::core::FileSystem; - + // Determine data path std::string data_path_str; - if (argc > 1) { - data_path_str = argv[1]; - } else { + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + if (arg == "--help" || arg == "-h") { + PrintUsage(); + return 0; + } + if (arg == "--version" || arg == "-v") { + std::cout << "AFS Studio " << afs::studio::core::StudioVersion() << "\n"; + return 0; + } + if (arg == "--data" || arg == "--data-path") { + if (i + 1 < argc) { + data_path_str = argv[++i]; + continue; + } + std::cerr << "Missing value for --data\n"; + return 1; + } + if (data_path_str.empty() && !arg.empty() && arg[0] != '-') { + data_path_str = arg; + } + } + if (data_path_str.empty()) { const char* env_root = std::getenv("AFS_TRAINING_ROOT"); if (env_root && env_root[0] != '\0') { data_path_str = env_root; } else { auto preferred = FileSystem::ResolvePath("~/src/training"); - data_path_str = FileSystem::Exists(preferred) ? preferred.string() : "~/.context/training"; + data_path_str = FileSystem::Exists(preferred) ? preferred.string() + : "~/.context/training"; } } std::filesystem::path data_path = FileSystem::ResolvePath(data_path_str); - LOG_INFO("AFS Studio Starting..."); + LOG_INFO(std::string("AFS Studio ") + afs::studio::core::StudioVersion()); LOG_INFO("Data path: " + data_path.string()); LOG_INFO("Press F5 to refresh data"); diff --git a/apps/studio/src/models/state.h b/apps/studio/src/models/state.h index 9b4c0bc..af0ec8d 100644 --- a/apps/studio/src/models/state.h +++ b/apps/studio/src/models/state.h @@ -122,6 +122,7 @@ struct AppState { bool show_quality_trends = false; // Default off, let layout init handle it bool show_generator_efficiency = false; bool show_coverage_density = false; + bool show_about_modal = false; bool enable_viewports = true; bool enable_docking = true; bool reset_layout_on_workspace_change = false; @@ -160,6 +161,8 @@ struct AppState { bool force_reset_layout = false; bool lock_layout = false; double last_refresh_time = 0.0; + std::string studio_version; + std::string studio_data_root; // Advanced Interaction std::vector active_floaters; diff --git a/apps/studio/src/ui/components/panels.cc b/apps/studio/src/ui/components/panels.cc index 953fb6d..46eb7d7 100644 --- a/apps/studio/src/ui/components/panels.cc +++ b/apps/studio/src/ui/components/panels.cc @@ -37,16 +37,24 @@ std::filesystem::path ResolveHafsScawfulRoot() { const char* trunk_env = std::getenv("TRUNK_ROOT"); if (trunk_env && trunk_env[0] != '\0') { auto trunk_root = studio::core::FileSystem::ResolvePath(trunk_env); - auto candidate = trunk_root / "scawful" / "research" / "afs_scawful"; + auto candidate = trunk_root / "lab" / "afs_scawful"; if (studio::core::FileSystem::Exists(candidate)) { return candidate; } + auto legacy = trunk_root / "scawful" / "research" / "afs_scawful"; + if (studio::core::FileSystem::Exists(legacy)) { + return legacy; + } } - auto fallback_path = studio::core::FileSystem::ResolvePath("~/src/trunk/scawful/research/afs_scawful"); + auto fallback_path = studio::core::FileSystem::ResolvePath("~/src/trunk/lab/afs_scawful"); if (studio::core::FileSystem::Exists(fallback_path)) { return fallback_path; } + auto legacy_fallback = studio::core::FileSystem::ResolvePath("~/src/trunk/scawful/research/afs_scawful"); + if (studio::core::FileSystem::Exists(legacy_fallback)) { + return legacy_fallback; + } return {}; } @@ -1458,11 +1466,33 @@ void RenderMenuBar(AppState& state, if (show_shortcuts_window) *show_shortcuts_window = true; } ImGui::Separator(); - if (ImGui::MenuItem("About AFS Viz")) {} + if (ImGui::MenuItem("About AFS Studio")) { + state.show_about_modal = true; + } ImGui::EndMenu(); } ImGui::EndMainMenuBar(); } + + if (state.show_about_modal) { + ImGui::OpenPopup("About AFS Studio"); + } + if (ImGui::BeginPopupModal("About AFS Studio", &state.show_about_modal, + ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("AFS Studio"); + ImGui::Separator(); + ImGui::Text("Version: %s", state.studio_version.empty() + ? "(unknown)" + : state.studio_version.c_str()); + ImGui::Text("Data root: %s", state.studio_data_root.empty() + ? "(not set)" + : state.studio_data_root.c_str()); + ImGui::Spacing(); + if (ImGui::Button("Close")) { + state.show_about_modal = false; + } + ImGui::EndPopup(); + } } void RenderSidebar(AppState& state, const DataLoader& loader, ImFont* font_ui, ImFont* font_header) { diff --git a/apps/studio/src/widgets/training_status.cpp b/apps/studio/src/widgets/training_status.cpp index 460e4bd..f9d3a7e 100644 --- a/apps/studio/src/widgets/training_status.cpp +++ b/apps/studio/src/widgets/training_status.cpp @@ -108,11 +108,11 @@ bool TrainingStatusWidget::FetchHealthData() { } else { const char* trunk_root = std::getenv("TRUNK_ROOT"); if (trunk_root && trunk_root[0] != '\0') { - root = std::string(trunk_root) + "/scawful/research/afs"; + root = std::string(trunk_root) + "/lab/afs"; } else { const char* home = std::getenv("HOME"); if (home && home[0] != '\0') { - root = std::string(home) + "/src/trunk/scawful/research/afs"; + root = std::string(home) + "/src/trunk/lab/afs"; } } } diff --git a/docs/PORTING_MAP.md b/docs/PORTING_MAP.md index 870049c..e8bf73c 100644 --- a/docs/PORTING_MAP.md +++ b/docs/PORTING_MAP.md @@ -3,10 +3,10 @@ Scope: inventory of legacy features and their status in the current AFS/AFS Scawful repos. Use this as a porting checklist; verify specifics when moving code. Sources (local workspace): -- Previous core: `trunk/scawful/research/legacy/afs_legacy` -- Previous plugin: `trunk/scawful/research/legacy/afs_scawful_legacy` -- Current core: `trunk/scawful/research/afs` -- Current plugin: `trunk/scawful/research/afs_scawful` +- Previous core: `trunk/lab/legacy/afs_legacy` +- Previous plugin: `trunk/lab/legacy/afs_scawful_legacy` +- Current core: `trunk/lab/afs` +- Current plugin: `trunk/lab/afs_scawful` Status legend: Ported, Partial, Planned, Not started. diff --git a/src/afs/cli.py b/src/afs/cli.py index fc201ce..c4132f2 100644 --- a/src/afs/cli.py +++ b/src/afs/cli.py @@ -3,6 +3,8 @@ from __future__ import annotations import argparse +import os +import subprocess from pathlib import Path from typing import Iterable @@ -38,6 +40,75 @@ def _parse_mount_type(value: str) -> MountType: raise argparse.ArgumentTypeError(f"Unknown mount type: {value}") from exc +def _resolve_studio_root() -> Path: + candidates: list[Path] = [] + env_root = os.getenv("AFS_ROOT") + if env_root: + candidates.append(Path(env_root).expanduser().resolve()) + trunk_root = os.getenv("TRUNK_ROOT") + if trunk_root: + candidates.append(Path(trunk_root).expanduser().resolve() / "lab" / "afs") + candidates.append(Path(__file__).resolve().parents[2]) + + for candidate in candidates: + if (candidate / "apps" / "studio" / "CMakeLists.txt").exists(): + return candidate + + raise FileNotFoundError( + "AFS studio source not found. Set AFS_ROOT to the repo root." + ) + + +def _studio_binary_name() -> str: + return "afs_studio.exe" if os.name == "nt" else "afs_studio" + + +def _studio_build_dir(root: Path, override: str | None) -> Path: + return ( + Path(override).expanduser().resolve() + if override + else root / "build" / "studio" + ) + + +def _studio_binary_path(build_dir: Path, config: str | None) -> Path: + if config: + candidate = build_dir / config / _studio_binary_name() + if candidate.exists(): + return candidate + return build_dir / _studio_binary_name() + + +def _run_command(cmd: list[str]) -> int: + try: + subprocess.run(cmd, check=True) + except FileNotFoundError: + print(f"command not found: {cmd[0]}") + return 1 + except subprocess.CalledProcessError as exc: + return exc.returncode + return 0 + + +def _studio_build( + root: Path, + build_dir: Path, + build_type: str | None, + config: str | None, +) -> int: + src_dir = root / "apps" / "studio" + cmake_cmd = ["cmake", "-S", str(src_dir), "-B", str(build_dir)] + if build_type: + cmake_cmd.append(f"-DCMAKE_BUILD_TYPE={build_type}") + status = _run_command(cmake_cmd) + if status != 0: + return status + build_cmd = ["cmake", "--build", str(build_dir), "--target", "afs_studio"] + if config: + build_cmd.extend(["--config", config]) + return _run_command(build_cmd) + + def _load_manager(config_path: Path | None) -> AFSManager: config = load_config_model(config_path=config_path, merge_user=True) return AFSManager(config=config) @@ -197,6 +268,90 @@ def _orchestrator_plan_command(args: argparse.Namespace) -> int: return 0 +def _studio_build_command(args: argparse.Namespace) -> int: + try: + root = _resolve_studio_root() + except FileNotFoundError as exc: + print(str(exc)) + return 1 + build_dir = _studio_build_dir(root, args.build_dir) + status = _studio_build(root, build_dir, args.build_type, args.config) + if status == 0: + print(f"build_dir: {build_dir}") + return status + + +def _studio_run_command(args: argparse.Namespace) -> int: + try: + root = _resolve_studio_root() + except FileNotFoundError as exc: + print(str(exc)) + return 1 + build_dir = _studio_build_dir(root, args.build_dir) + binary = _studio_binary_path(build_dir, args.config) + if not binary.exists() and args.build: + status = _studio_build(root, build_dir, args.build_type, args.config) + if status != 0: + return status + binary = _studio_binary_path(build_dir, args.config) + if not binary.exists(): + print(f"binary not found: {binary}") + return 1 + cmd = [str(binary)] + if args.args: + cmd.extend(args.args) + return _run_command(cmd) + + +def _studio_install_command(args: argparse.Namespace) -> int: + try: + root = _resolve_studio_root() + except FileNotFoundError as exc: + print(str(exc)) + return 1 + build_dir = _studio_build_dir(root, args.build_dir) + if not build_dir.exists(): + print(f"build dir missing: {build_dir}") + return 1 + prefix = ( + Path(args.prefix).expanduser().resolve() + if args.prefix + else Path.home() / ".local" + ) + cmd = ["cmake", "--install", str(build_dir), "--prefix", str(prefix)] + if args.config: + cmd.extend(["--config", args.config]) + status = _run_command(cmd) + if status == 0: + print(f"installed: {prefix / 'bin' / _studio_binary_name()}") + return status + + +def _studio_path_command(args: argparse.Namespace) -> int: + try: + root = _resolve_studio_root() + except FileNotFoundError as exc: + print(str(exc)) + return 1 + build_dir = _studio_build_dir(root, args.build_dir) + binary = _studio_binary_path(build_dir, args.config) + print(binary) + return 0 + + +def _studio_alias_command(args: argparse.Namespace) -> int: + try: + root = _resolve_studio_root() + except FileNotFoundError as exc: + print(str(exc)) + return 1 + root_value = os.getenv("AFS_ROOT") or str(root) + print(f"export AFS_ROOT=\"{root_value}\"") + print("alias afs-studio='PYTHONPATH=\"$AFS_ROOT/src\" python -m afs studio run --build'") + print("alias afs-studio-build='PYTHONPATH=\"$AFS_ROOT/src\" python -m afs studio build'") + return 0 + + def _status_command(args: argparse.Namespace) -> int: start_dir = Path(args.start_dir).expanduser().resolve() if args.start_dir else None root = find_root(start_dir) @@ -580,6 +735,45 @@ def build_parser() -> argparse.ArgumentParser: orch_plan.add_argument("--role", help="Role to match.") orch_plan.set_defaults(func=_orchestrator_plan_command) + studio_parser = subparsers.add_parser("studio", help="AFS Studio helpers.") + studio_sub = studio_parser.add_subparsers(dest="studio_command") + + studio_build = studio_sub.add_parser("build", help="Build AFS Studio.") + studio_build.add_argument("--build-dir", help="Build directory override.") + studio_build.add_argument( + "--build-type", + default="RelWithDebInfo", + help="CMake build type (default: RelWithDebInfo).", + ) + studio_build.add_argument("--config", help="Multi-config build name.") + studio_build.set_defaults(func=_studio_build_command) + + studio_run = studio_sub.add_parser("run", help="Run AFS Studio.") + studio_run.add_argument("--build", action="store_true", help="Build if missing.") + studio_run.add_argument("--build-dir", help="Build directory override.") + studio_run.add_argument( + "--build-type", + default="RelWithDebInfo", + help="CMake build type (default: RelWithDebInfo).", + ) + studio_run.add_argument("--config", help="Multi-config build name.") + studio_run.add_argument("args", nargs=argparse.REMAINDER, help="Arguments for afs_studio.") + studio_run.set_defaults(func=_studio_run_command) + + studio_install = studio_sub.add_parser("install", help="Install AFS Studio.") + studio_install.add_argument("--prefix", help="Install prefix (default: ~/.local).") + studio_install.add_argument("--build-dir", help="Build directory override.") + studio_install.add_argument("--config", help="Multi-config build name.") + studio_install.set_defaults(func=_studio_install_command) + + studio_path = studio_sub.add_parser("path", help="Print studio binary path.") + studio_path.add_argument("--build-dir", help="Build directory override.") + studio_path.add_argument("--config", help="Multi-config build name.") + studio_path.set_defaults(func=_studio_path_command) + + studio_alias = studio_sub.add_parser("alias", help="Print alias suggestions.") + studio_alias.set_defaults(func=_studio_alias_command) + status_parser = subparsers.add_parser("status", help="Show context root status.") status_parser.add_argument("--start-dir", help="Directory to search from.") status_parser.set_defaults(func=_status_command) @@ -777,6 +971,9 @@ def main(argv: Iterable[str] | None = None) -> int: if args.command == "orchestrator" and not getattr(args, "orchestrator_command", None): parser.print_help() return 1 + if args.command == "studio" and not getattr(args, "studio_command", None): + parser.print_help() + return 1 return args.func(args)