Add AFS Studio CLI helpers and versioning

This commit is contained in:
scawful
2025-12-30 18:09:34 -05:00
parent b28c092b3c
commit 67294096ed
12 changed files with 365 additions and 32 deletions

View File

@@ -28,3 +28,8 @@ Quickstart:
- `python -m afs orchestrator list` - `python -m afs orchestrator list`
Discovery skips directory names in `general.discovery_ignore` (default: legacy, archive, archives). 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`

View File

@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.15) 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 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -131,6 +131,7 @@ target_include_directories(afs_studio PRIVATE
target_compile_definitions(afs_studio PRIVATE target_compile_definitions(afs_studio PRIVATE
IMGUI_IMPL_OPENGL_LOADER_GLAD=0 IMGUI_IMPL_OPENGL_LOADER_GLAD=0
AFS_STUDIO_VERSION="${PROJECT_VERSION}"
) )
if(TARGET OpenGL::OpenGL) if(TARGET OpenGL::OpenGL)

View File

@@ -5,15 +5,39 @@ Native C++17 visualization and training management application for AFS.
## Build ## Build
```bash ```bash
# From project root # From AFS repo root
cmake -B build -S . -DAFS_BUILD_STUDIO=ON cmake -S apps/studio -B build/studio
cmake --build build --target afs_studio cmake --build build/studio --target afs_studio
``` ```
## Run ## Run
```bash ```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 ## 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`. - 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`. - 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 ## Features
- **Dashboard**: Training metrics overview - **Dashboard**: Training metrics overview

View File

@@ -1,7 +1,8 @@
#include "app.h" #include "app.h"
#include "core/logger.h"
#include "core/context.h"
#include "core/assets.h" #include "core/assets.h"
#include "core/context.h"
#include "core/logger.h"
#include "core/version.h"
#include "ui/panels/chat_panel.h" #include "ui/panels/chat_panel.h"
#include <algorithm> #include <algorithm>
@@ -41,6 +42,9 @@ App::App(const std::string& data_path)
: data_path_(data_path), loader_(data_path) { : data_path_(data_path), loader_(data_path) {
LOG_INFO("AFS Studio initialize with data path: " + 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_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_.new_mission_owner.data(), state_.new_mission_owner.size(), "Ops");
std::snprintf(state_.system_prompt.data(), state_.system_prompt.size(), std::snprintf(state_.system_prompt.data(), state_.system_prompt.size(),
@@ -69,7 +73,12 @@ App::App(const std::string& data_path)
SeedDefaultState(); SeedDefaultState();
// Create graphics context // Create graphics context
context_ = std::make_unique<studio::core::GraphicsContext>("AFS Studio", 1400, 900); std::string title = "AFS Studio";
if (!state_.studio_version.empty()) {
title += " ";
title += state_.studio_version;
}
context_ = std::make_unique<studio::core::GraphicsContext>(title, 1400, 900);
if (context_->IsValid()) { if (context_->IsValid()) {
fonts_ = studio::core::AssetLoader::LoadFonts(); fonts_ = studio::core::AssetLoader::LoadFonts();
themes::ApplyHafsTheme(); themes::ApplyHafsTheme();

View File

@@ -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

View File

@@ -41,10 +41,14 @@ std::optional<std::filesystem::path> ResolveHafsScawfulRoot() {
auto trunk_root = ResolveTrunkRoot(); auto trunk_root = ResolveTrunkRoot();
if (trunk_root) { if (trunk_root) {
auto candidate = *trunk_root / "scawful" / "research" / "afs_scawful"; auto candidate = *trunk_root / "lab" / "afs_scawful";
if (studio::core::FileSystem::Exists(candidate)) { if (studio::core::FileSystem::Exists(candidate)) {
return candidate; return candidate;
} }
auto legacy = *trunk_root / "scawful" / "research" / "afs_scawful";
if (studio::core::FileSystem::Exists(legacy)) {
return legacy;
}
} }
return std::nullopt; return std::nullopt;
@@ -1019,9 +1023,15 @@ void DataLoader::MountDrive(const std::string& name) {
} else { } else {
auto trunk_root = ResolveTrunkRoot(); auto trunk_root = ResolveTrunkRoot();
if (trunk_root) { 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 { } 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");
}
} }
} }

View File

@@ -1,11 +1,12 @@
/// AFS Training Data Visualization - Main Entry Point /// AFS Studio - Main Entry Point
/// ///
/// Usage: afs_viz [data_path] /// Usage: afs_studio [--data PATH]
/// data_path: Path to training data directory (default: ~/src/training if present) /// --data PATH: Path to training data directory (default: ~/src/training if present)
/// --version: Print version and exit
/// ///
/// Build: /// Build:
/// cmake -B build -S src/cc -DAFS_BUILD_VIZ=ON /// cmake -S apps/studio -B build/studio
/// cmake --build build /// cmake --build build/studio --target afs_studio
/// ///
/// Keys: /// Keys:
/// F5 - Refresh data /// F5 - Refresh data
@@ -17,29 +18,62 @@
#include <string> #include <string>
#include "app.h" #include "app.h"
#include "core/logger.h"
#include "core/filesystem.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[]) { int main(int argc, char* argv[]) {
using afs::studio::core::FileSystem; using afs::studio::core::FileSystem;
// Determine data path // Determine data path
std::string data_path_str; std::string data_path_str;
if (argc > 1) { for (int i = 1; i < argc; ++i) {
data_path_str = argv[1]; std::string arg = argv[i];
} else { 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"); const char* env_root = std::getenv("AFS_TRAINING_ROOT");
if (env_root && env_root[0] != '\0') { if (env_root && env_root[0] != '\0') {
data_path_str = env_root; data_path_str = env_root;
} else { } else {
auto preferred = FileSystem::ResolvePath("~/src/training"); 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); 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("Data path: " + data_path.string());
LOG_INFO("Press F5 to refresh data"); LOG_INFO("Press F5 to refresh data");

View File

@@ -122,6 +122,7 @@ struct AppState {
bool show_quality_trends = false; // Default off, let layout init handle it bool show_quality_trends = false; // Default off, let layout init handle it
bool show_generator_efficiency = false; bool show_generator_efficiency = false;
bool show_coverage_density = false; bool show_coverage_density = false;
bool show_about_modal = false;
bool enable_viewports = true; bool enable_viewports = true;
bool enable_docking = true; bool enable_docking = true;
bool reset_layout_on_workspace_change = false; bool reset_layout_on_workspace_change = false;
@@ -160,6 +161,8 @@ struct AppState {
bool force_reset_layout = false; bool force_reset_layout = false;
bool lock_layout = false; bool lock_layout = false;
double last_refresh_time = 0.0; double last_refresh_time = 0.0;
std::string studio_version;
std::string studio_data_root;
// Advanced Interaction // Advanced Interaction
std::vector<PlotKind> active_floaters; std::vector<PlotKind> active_floaters;

View File

@@ -37,16 +37,24 @@ std::filesystem::path ResolveHafsScawfulRoot() {
const char* trunk_env = std::getenv("TRUNK_ROOT"); const char* trunk_env = std::getenv("TRUNK_ROOT");
if (trunk_env && trunk_env[0] != '\0') { if (trunk_env && trunk_env[0] != '\0') {
auto trunk_root = studio::core::FileSystem::ResolvePath(trunk_env); 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)) { if (studio::core::FileSystem::Exists(candidate)) {
return 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)) { if (studio::core::FileSystem::Exists(fallback_path)) {
return 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 {}; return {};
} }
@@ -1458,11 +1466,33 @@ void RenderMenuBar(AppState& state,
if (show_shortcuts_window) *show_shortcuts_window = true; if (show_shortcuts_window) *show_shortcuts_window = true;
} }
ImGui::Separator(); ImGui::Separator();
if (ImGui::MenuItem("About AFS Viz")) {} if (ImGui::MenuItem("About AFS Studio")) {
state.show_about_modal = true;
}
ImGui::EndMenu(); ImGui::EndMenu();
} }
ImGui::EndMainMenuBar(); 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) { void RenderSidebar(AppState& state, const DataLoader& loader, ImFont* font_ui, ImFont* font_header) {

View File

@@ -108,11 +108,11 @@ bool TrainingStatusWidget::FetchHealthData() {
} else { } else {
const char* trunk_root = std::getenv("TRUNK_ROOT"); const char* trunk_root = std::getenv("TRUNK_ROOT");
if (trunk_root && trunk_root[0] != '\0') { if (trunk_root && trunk_root[0] != '\0') {
root = std::string(trunk_root) + "/scawful/research/afs"; root = std::string(trunk_root) + "/lab/afs";
} else { } else {
const char* home = std::getenv("HOME"); const char* home = std::getenv("HOME");
if (home && home[0] != '\0') { if (home && home[0] != '\0') {
root = std::string(home) + "/src/trunk/scawful/research/afs"; root = std::string(home) + "/src/trunk/lab/afs";
} }
} }
} }

View File

@@ -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. 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): Sources (local workspace):
- Previous core: `trunk/scawful/research/legacy/afs_legacy` - Previous core: `trunk/lab/legacy/afs_legacy`
- Previous plugin: `trunk/scawful/research/legacy/afs_scawful_legacy` - Previous plugin: `trunk/lab/legacy/afs_scawful_legacy`
- Current core: `trunk/scawful/research/afs` - Current core: `trunk/lab/afs`
- Current plugin: `trunk/scawful/research/afs_scawful` - Current plugin: `trunk/lab/afs_scawful`
Status legend: Ported, Partial, Planned, Not started. Status legend: Ported, Partial, Planned, Not started.

View File

@@ -3,6 +3,8 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import os
import subprocess
from pathlib import Path from pathlib import Path
from typing import Iterable 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 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: def _load_manager(config_path: Path | None) -> AFSManager:
config = load_config_model(config_path=config_path, merge_user=True) config = load_config_model(config_path=config_path, merge_user=True)
return AFSManager(config=config) return AFSManager(config=config)
@@ -197,6 +268,90 @@ def _orchestrator_plan_command(args: argparse.Namespace) -> int:
return 0 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: def _status_command(args: argparse.Namespace) -> int:
start_dir = Path(args.start_dir).expanduser().resolve() if args.start_dir else None start_dir = Path(args.start_dir).expanduser().resolve() if args.start_dir else None
root = find_root(start_dir) 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.add_argument("--role", help="Role to match.")
orch_plan.set_defaults(func=_orchestrator_plan_command) 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 = subparsers.add_parser("status", help="Show context root status.")
status_parser.add_argument("--start-dir", help="Directory to search from.") status_parser.add_argument("--start-dir", help="Directory to search from.")
status_parser.set_defaults(func=_status_command) 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): if args.command == "orchestrator" and not getattr(args, "orchestrator_command", None):
parser.print_help() parser.print_help()
return 1 return 1
if args.command == "studio" and not getattr(args, "studio_command", None):
parser.print_help()
return 1
return args.func(args) return args.func(args)