rosett-ai 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.ai-provenance.yml +119 -0
- data/.debride_whitelist +186 -0
- data/.fasterer.yml +29 -0
- data/.mdl_style.rb +10 -0
- data/.mdlrc +3 -0
- data/.mutant.yml +49 -0
- data/.namespace-allowlist +42 -0
- data/.reek.yml +1040 -0
- data/.rosett-ai/config.yml +3 -0
- data/.rspec +5 -0
- data/.rubocop.yml +380 -0
- data/.ruby-version +1 -0
- data/.yamllint +51 -0
- data/.yardopts +12 -0
- data/AI-DISCLOSURE.md +48 -0
- data/CHANGELOG.md +519 -0
- data/CLAUDE.md +141 -0
- data/CONTRIBUTING.md +734 -0
- data/INSTALL.md +154 -0
- data/LICENSE +674 -0
- data/LICENSE.md +675 -0
- data/QUICKSTART.md +73 -0
- data/README.md +366 -0
- data/Rakefile +200 -0
- data/SECURITY.md +114 -0
- data/bin/rai +1 -0
- data/cliff.toml +52 -0
- data/conf/adopt_redactions.yml +8 -0
- data/conf/behaviour/.gitkeep +0 -0
- data/conf/compliance/cra_rules.yml +25 -0
- data/conf/compliance/license_rules.yml +20 -0
- data/conf/design/aaif_alignment.yml +181 -0
- data/conf/design/ab_testing.yml +172 -0
- data/conf/design/accessibility.yml +84 -0
- data/conf/design/ai_authorship.yml +210 -0
- data/conf/design/ai_provenance.yml +224 -0
- data/conf/design/ai_tool_configuration.yml +207 -0
- data/conf/design/architecture.yml +139 -0
- data/conf/design/autocompletion.yml +115 -0
- data/conf/design/backward_compatibility.yml +112 -0
- data/conf/design/behaviour_composition.yml +246 -0
- data/conf/design/build_rake_extraction.yml +57 -0
- data/conf/design/ci_pipeline.yml +100 -0
- data/conf/design/claude_code_configuration.yml +157 -0
- data/conf/design/compiler.yml +128 -0
- data/conf/design/comply.yml +153 -0
- data/conf/design/content_packs.yml +84 -0
- data/conf/design/desktop_integration.yml +289 -0
- data/conf/design/distribution.yml +216 -0
- data/conf/design/doctor.yml +184 -0
- data/conf/design/documentation.yml +152 -0
- data/conf/design/engine_architecture.yml +257 -0
- data/conf/design/error_handling.yml +103 -0
- data/conf/design/feature_flags.yml +142 -0
- data/conf/design/git_hooks.yml +165 -0
- data/conf/design/gui_plugins.yml +475 -0
- data/conf/design/i18n.yml +84 -0
- data/conf/design/integration_testing.yml +56 -0
- data/conf/design/licensing_system.yml +88 -0
- data/conf/design/lifecycle_management.yml +208 -0
- data/conf/design/mcp_integration.yml +207 -0
- data/conf/design/mcp_settings.yml +126 -0
- data/conf/design/migration.yml +56 -0
- data/conf/design/monitoring_observability.yml +194 -0
- data/conf/design/namespace_cleanup.yml +145 -0
- data/conf/design/plugin_test_segregation.yml +145 -0
- data/conf/design/policy_management.yml +229 -0
- data/conf/design/project_management.yml +183 -0
- data/conf/design/rai_mcp_asset_discovery.yml +164 -0
- data/conf/design/rai_mcp_server.yml +605 -0
- data/conf/design/release_management.yml +117 -0
- data/conf/design/retrofit.yml +199 -0
- data/conf/design/retrospective_analyzer.yml +79 -0
- data/conf/design/scope_hierarchy.yml +352 -0
- data/conf/design/security.yml +115 -0
- data/conf/design/session_retrospective.yml +85 -0
- data/conf/design/smart_ui_feedback.yml +89 -0
- data/conf/design/structured_logging.yml +148 -0
- data/conf/design/styles.yml +123 -0
- data/conf/design/test_peer_review.yml +89 -0
- data/conf/design/testing.yml +136 -0
- data/conf/design/threat_model.yml +108 -0
- data/conf/design/ui_framework.yml +111 -0
- data/conf/design/usage_optimization.yml +122 -0
- data/conf/design/version_management.yml +60 -0
- data/conf/design/workflow.yml +227 -0
- data/conf/mcp/server_defaults.yml +42 -0
- data/conf/mcp/trust.yml +21 -0
- data/conf/packaging/core.yml +12 -0
- data/conf/packaging/gtk4.yml +11 -0
- data/conf/packaging/qt6.yml +11 -0
- data/conf/policy/default_deny_list.yml +197 -0
- data/conf/review/cli-command-audit.yml +857 -0
- data/conf/review/design-docs.yml +1064 -0
- data/conf/review/design-questionnaire.yml +153 -0
- data/conf/review/questionnaire.yml +146 -0
- data/conf/review/rosett-ai-core.yml +2919 -0
- data/conf/schemas/ai_config_schema.json +73 -0
- data/conf/schemas/behaviour_schema.json +132 -0
- data/conf/schemas/compliance_rule_schema.json +63 -0
- data/conf/schemas/content_pack_manifest_schema.json +51 -0
- data/conf/schemas/design_schema.json +210 -0
- data/conf/schemas/engine_manifest_schema.json +144 -0
- data/conf/schemas/lockfile_schema.json +74 -0
- data/conf/schemas/mcp_server_schema.json +48 -0
- data/conf/schemas/packaging_schema.json +70 -0
- data/conf/schemas/policy_schema.json +85 -0
- data/conf/schemas/provenance_schema.json +84 -0
- data/conf/schemas/rai_config_schema.json +56 -0
- data/conf/schemas/rai_project_schema.json +20 -0
- data/conf/schemas/scope_hierarchy_schema.json +49 -0
- data/conf/schemas/target_schema.json +67 -0
- data/conf/schemas/tooling_schema.json +65 -0
- data/conf/schemas/workflow_schema.json +112 -0
- data/conf/targets/agents_md.yml +17 -0
- data/conf/targets/claude.yml +12 -0
- data/conf/tooling/tools.yml +58 -0
- data/dist/rosett-ai-mcp.service +48 -0
- data/dist/rosett-ai-mcp.yml.default +45 -0
- data/doc/AAIF_POSITIONING.md +58 -0
- data/doc/ADOPT.md +224 -0
- data/doc/AI_PROVENANCE.md +139 -0
- data/doc/ARCHITECTURE.md +920 -0
- data/doc/BEHAVIOUR.md +409 -0
- data/doc/BUILD.md +138 -0
- data/doc/CI_CD_RECIPES.md +171 -0
- data/doc/CLAUDE_SESSIONS_MOVED.md +16 -0
- data/doc/COMMAND_ANALYSIS.md +229 -0
- data/doc/CONFIGURATION.md +281 -0
- data/doc/DESIGN_AUDIT.md +235 -0
- data/doc/DESIGN_PEER_REVIEW.md +771 -0
- data/doc/DESKTOP.md +447 -0
- data/doc/ENGINES.md +567 -0
- data/doc/ENGINE_DEVELOPMENT_GUIDE.md +417 -0
- data/doc/FEATURE_AUDIT.md +218 -0
- data/doc/IMPLEMENTATION_PLAN.md +669 -0
- data/doc/INCIDENT_REPORT_2026-02-02.md +251 -0
- data/doc/MIGRATION_GUIDE.md +88 -0
- data/doc/PACKAGING.md +232 -0
- data/doc/PROJECT_DASHBOARD.md +153 -0
- data/doc/PULP_DEPLOYMENT.md +164 -0
- data/doc/QUALITY_FIX_SUMMARY.md +110 -0
- data/doc/QUICK_START.md +162 -0
- data/doc/REEK_CONFIGURATION.md +166 -0
- data/doc/REFERENCE.md +253 -0
- data/doc/REFERENCES.md +324 -0
- data/doc/SECURITY_REVIEW_CHECKLIST.md +72 -0
- data/doc/SESSION_2026-02-28_GTK4_HARDENING.md +359 -0
- data/doc/SETUP.md +202 -0
- data/doc/TEST_PEER_REVIEW.md +152 -0
- data/doc/THREAT_MODEL.md +230 -0
- data/doc/USAGE.md +545 -0
- data/doc/USER_MANUAL.md +585 -0
- data/doc/ai_test_review_checklist.md +110 -0
- data/doc/changes/2026-02-18-packaging-fpm.md +155 -0
- data/doc/changes/2026-02-19-testing-infrastructure.md +221 -0
- data/doc/changes/2026-02-20-security-implementation.md +281 -0
- data/doc/changes/2026-02-20-styles-implementation.md +220 -0
- data/doc/changes/2026-02-21-architecture-completion.md +95 -0
- data/doc/changes/2026-02-21-architecture-ui-layer.md +253 -0
- data/doc/changes/2026-02-21-cc-config-implementation.md +108 -0
- data/doc/changes/2026-02-21-ci-pipeline-implementation.md +214 -0
- data/doc/changes/2026-02-21-compiler-multi-target-pipeline.md +241 -0
- data/doc/changes/2026-02-21-config-design-show-commands.md +61 -0
- data/doc/changes/2026-02-21-design-implementation-overview.md +455 -0
- data/doc/changes/2026-02-21-lifecycle-management.md +196 -0
- data/doc/changes/2026-02-21-path-resolver.md +128 -0
- data/doc/changes/2026-02-24-ci-tmpdir-mutant-fetch.md +45 -0
- data/doc/changes/2026-03-01-ci-bundler-strategy.md +120 -0
- data/doc/changes/2026-03-20-security-hardening-phase2.md +163 -0
- data/doc/context/SESSION-HANDOFF.md +69 -0
- data/doc/context/ai-engine-usage-trends-2026.md +80 -0
- data/doc/context/plan-pluggable-engines.md +590 -0
- data/doc/decisions/001-flog-deferred.md +32 -0
- data/doc/decisions/002-path-resolution-strategy.md +158 -0
- data/doc/decisions/003-ui-adapter-selection.md +193 -0
- data/doc/decisions/004-design-document-validation.md +179 -0
- data/doc/decisions/005-package-splitting-strategy.md +200 -0
- data/doc/decisions/006-multi-engine-architecture.md +147 -0
- data/doc/decisions/007-engine-agnostic-pivot.md +219 -0
- data/doc/decisions/008-ci-bundler-strategy.md +129 -0
- data/doc/decisions/009-core-only-v1-release.md +60 -0
- data/doc/decisions/010-engine-debian-packaging.md +66 -0
- data/doc/decisions/011-context-aware-cli.md +71 -0
- data/doc/dependency_decisions.yml +247 -0
- data/doc/issues/001-wrapper-missing-environment-variables.md +197 -0
- data/doc/issues/002-embedded-ruby-wrong-prefix.md +217 -0
- data/doc/issues/003-smoke-test-false-positive.md +127 -0
- data/doc/issues/004-market-research-design-updates.md +109 -0
- data/doc/issues/005-compile-scope-coexistence.md +161 -0
- data/doc/locales/.gitkeep +0 -0
- data/doc/man/rai.1.ronn +505 -0
- data/doc/operations/packaging.md +133 -0
- data/doc/operations/rosett-ai-release.md +65 -0
- data/doc/reference/error-catalog.md +107 -0
- data/doc/reference/rosett-ai-technical-reference.pdf +0 -0
- data/doc/reference/src/Pictures/cover.jpg +0 -0
- data/doc/reference/src/Pictures/head1.jpg +0 -0
- data/doc/reference/src/Pictures/head2.jpg +0 -0
- data/doc/reference/src/Pictures/head3.jpg +0 -0
- data/doc/reference/src/Pictures/head4.jpg +0 -0
- data/doc/reference/src/Pictures/head5.jpg +0 -0
- data/doc/reference/src/Pictures/head6.jpg +0 -0
- data/doc/reference/src/Pictures/head7.jpg +0 -0
- data/doc/reference/src/Pictures/head8.jpg +0 -0
- data/doc/reference/src/StyleInd.ist +4 -0
- data/doc/reference/src/bibliography.bib +79 -0
- data/doc/reference/src/main.tex +1288 -0
- data/doc/reference/src/structure.tex +303 -0
- data/doc/rosett-ai-bookmarks.html +301 -0
- data/kitchen.yml +46 -0
- data/lib/rosett_ai/adopter/executor_resolver.rb +77 -0
- data/lib/rosett_ai/adopter/local_analysis_collector.rb +154 -0
- data/lib/rosett_ai/adopter/rule_adopter.rb +254 -0
- data/lib/rosett_ai/ai_config/config_compiler.rb +111 -0
- data/lib/rosett_ai/ai_config/context_window.rb +55 -0
- data/lib/rosett_ai/ai_config/cost_controls.rb +44 -0
- data/lib/rosett_ai/ai_config/fallback_chain.rb +64 -0
- data/lib/rosett_ai/ai_config/model_router.rb +121 -0
- data/lib/rosett_ai/ai_config/validator.rb +45 -0
- data/lib/rosett_ai/authorship/attribution_compiler.rb +99 -0
- data/lib/rosett_ai/authorship/disclosure_policy.rb +81 -0
- data/lib/rosett_ai/authorship/review_validator.rb +39 -0
- data/lib/rosett_ai/authorship/trailer_generator.rb +88 -0
- data/lib/rosett_ai/backup/compressor.rb +180 -0
- data/lib/rosett_ai/backup/destination.rb +91 -0
- data/lib/rosett_ai/behaviour/manager.rb +156 -0
- data/lib/rosett_ai/compiler/backend.rb +86 -0
- data/lib/rosett_ai/compiler/backends/agents_md_backend.rb +80 -0
- data/lib/rosett_ai/compiler/backends/claude_backend.rb +88 -0
- data/lib/rosett_ai/compiler/backends/generic_backend.rb +15 -0
- data/lib/rosett_ai/compiler/behaviour_compiler.rb +40 -0
- data/lib/rosett_ai/compiler/capability_checker.rb +104 -0
- data/lib/rosett_ai/compiler/compilation_pipeline.rb +361 -0
- data/lib/rosett_ai/compiler/compiled_output.rb +39 -0
- data/lib/rosett_ai/compiler/locale_compiler.rb +250 -0
- data/lib/rosett_ai/compiler/target_profile.rb +112 -0
- data/lib/rosett_ai/completion/generator.rb +101 -0
- data/lib/rosett_ai/completion/shells/bash_generator.rb +126 -0
- data/lib/rosett_ai/completion/shells/fish_generator.rb +78 -0
- data/lib/rosett_ai/completion/shells/zsh_generator.rb +126 -0
- data/lib/rosett_ai/comply/checkers/cra_checker.rb +102 -0
- data/lib/rosett_ai/comply/checkers/license_checker.rb +85 -0
- data/lib/rosett_ai/comply/checkers/spdx_header_checker.rb +98 -0
- data/lib/rosett_ai/comply/reporter.rb +113 -0
- data/lib/rosett_ai/comply/runner.rb +50 -0
- data/lib/rosett_ai/composition/circular_dependency_detector.rb +56 -0
- data/lib/rosett_ai/composition/composer.rb +158 -0
- data/lib/rosett_ai/composition/composition_result.rb +64 -0
- data/lib/rosett_ai/composition/conflict_detector.rb +53 -0
- data/lib/rosett_ai/composition/lockfile.rb +103 -0
- data/lib/rosett_ai/composition/merge_strategy.rb +131 -0
- data/lib/rosett_ai/composition/priority_sorter.rb +29 -0
- data/lib/rosett_ai/composition/scope_resolver.rb +55 -0
- data/lib/rosett_ai/config/compile_result.rb +37 -0
- data/lib/rosett_ai/config/compiler.rb +13 -0
- data/lib/rosett_ai/config/domain_transformer.rb +13 -0
- data/lib/rosett_ai/config/key_map.rb +13 -0
- data/lib/rosett_ai/config/masking_secret_resolver.rb +40 -0
- data/lib/rosett_ai/config/scope_router.rb +13 -0
- data/lib/rosett_ai/config/secret_resolver.rb +125 -0
- data/lib/rosett_ai/configuration.rb +119 -0
- data/lib/rosett_ai/content/content_client.rb +60 -0
- data/lib/rosett_ai/content/pack_installer.rb +117 -0
- data/lib/rosett_ai/content/pack_manifest.rb +50 -0
- data/lib/rosett_ai/content/pack_registry.rb +68 -0
- data/lib/rosett_ai/content_packs/manager.rb +50 -0
- data/lib/rosett_ai/dbus/compositor_detector.rb +77 -0
- data/lib/rosett_ai/dbus/focus_adapters/base.rb +59 -0
- data/lib/rosett_ai/dbus/focus_adapters/gnome_adapter.rb +172 -0
- data/lib/rosett_ai/dbus/focus_adapters/hyprland_adapter.rb +77 -0
- data/lib/rosett_ai/dbus/focus_adapters/i3_adapter.rb +65 -0
- data/lib/rosett_ai/dbus/focus_adapters/kwin_adapter.rb +103 -0
- data/lib/rosett_ai/dbus/focus_adapters/x11_adapter.rb +105 -0
- data/lib/rosett_ai/dbus/focus_monitor_interface.rb +103 -0
- data/lib/rosett_ai/dbus/manager_interface.rb +213 -0
- data/lib/rosett_ai/dbus/plugin_manager_interface.rb +169 -0
- data/lib/rosett_ai/dbus/rate_limiter.rb +89 -0
- data/lib/rosett_ai/dbus/service.rb +121 -0
- data/lib/rosett_ai/dbus/status_notifier_interface.rb +79 -0
- data/lib/rosett_ai/deprecation.rb +79 -0
- data/lib/rosett_ai/desktop/dbus_client.rb +259 -0
- data/lib/rosett_ai/desktop/gtk4_app.rb +371 -0
- data/lib/rosett_ai/desktop/gtk4_preferences.rb +331 -0
- data/lib/rosett_ai/desktop/gui_logger.rb +236 -0
- data/lib/rosett_ai/doctor/check.rb +92 -0
- data/lib/rosett_ai/doctor/checks/cache_health_check.rb +50 -0
- data/lib/rosett_ai/doctor/checks/dbus_availability_check.rb +39 -0
- data/lib/rosett_ai/doctor/checks/engine_detection_check.rb +46 -0
- data/lib/rosett_ai/doctor/checks/file_permission_check.rb +44 -0
- data/lib/rosett_ai/doctor/checks/gem_dependency_check.rb +55 -0
- data/lib/rosett_ai/doctor/checks/ruby_version_check.rb +50 -0
- data/lib/rosett_ai/doctor/checks/stale_config_nncc_check.rb +57 -0
- data/lib/rosett_ai/doctor/checks/stale_home_nncc_check.rb +59 -0
- data/lib/rosett_ai/doctor.rb +81 -0
- data/lib/rosett_ai/documentation/reference_compiler.rb +122 -0
- data/lib/rosett_ai/documentation/translator.rb +62 -0
- data/lib/rosett_ai/engines/base_config_compiler.rb +203 -0
- data/lib/rosett_ai/engines/detector.rb +63 -0
- data/lib/rosett_ai/engines/registry.rb +50 -0
- data/lib/rosett_ai/error_handler.rb +139 -0
- data/lib/rosett_ai/exit_codes.rb +76 -0
- data/lib/rosett_ai/feature_flags.rb +102 -0
- data/lib/rosett_ai/formatting.rb +33 -0
- data/lib/rosett_ai/gem_consistency_checker.rb +199 -0
- data/lib/rosett_ai/git_hooks/chain_detector.rb +86 -0
- data/lib/rosett_ai/git_hooks/installer.rb +175 -0
- data/lib/rosett_ai/git_hooks/script_generator.rb +125 -0
- data/lib/rosett_ai/gitlab/validators/supplementary_gitlab_ci_yaml_validator.rb +79 -0
- data/lib/rosett_ai/i18n/locale_resolver.rb +46 -0
- data/lib/rosett_ai/i18n/utf8_checker.rb +32 -0
- data/lib/rosett_ai/init/config_file_writer.rb +24 -0
- data/lib/rosett_ai/init/directory_builder.rb +38 -0
- data/lib/rosett_ai/init/file_copier.rb +95 -0
- data/lib/rosett_ai/init/global_initializer.rb +28 -0
- data/lib/rosett_ai/init/local_initializer.rb +27 -0
- data/lib/rosett_ai/init/mcp_registrar.rb +109 -0
- data/lib/rosett_ai/init/project_initializer.rb +38 -0
- data/lib/rosett_ai/licensing/license_key.rb +139 -0
- data/lib/rosett_ai/licensing/license_store.rb +64 -0
- data/lib/rosett_ai/licensing/license_validator.rb +60 -0
- data/lib/rosett_ai/licensing/tier.rb +42 -0
- data/lib/rosett_ai/mcp/admin/auditor.rb +88 -0
- data/lib/rosett_ai/mcp/admin/health_checker.rb +81 -0
- data/lib/rosett_ai/mcp/admin/registry.rb +100 -0
- data/lib/rosett_ai/mcp/admin/schema_validator.rb +63 -0
- data/lib/rosett_ai/mcp/enforcement/.gitkeep +0 -0
- data/lib/rosett_ai/mcp/enforcement/hook_generator.rb +197 -0
- data/lib/rosett_ai/mcp/enforcement/validator.rb +215 -0
- data/lib/rosett_ai/mcp/governance.rb +160 -0
- data/lib/rosett_ai/mcp/http_security_config.rb +158 -0
- data/lib/rosett_ai/mcp/instructions.rb +266 -0
- data/lib/rosett_ai/mcp/key_hasher.rb +66 -0
- data/lib/rosett_ai/mcp/keyfile.rb +221 -0
- data/lib/rosett_ai/mcp/middleware/authentication.rb +146 -0
- data/lib/rosett_ai/mcp/middleware/content_type.rb +56 -0
- data/lib/rosett_ai/mcp/middleware/cors.rb +83 -0
- data/lib/rosett_ai/mcp/middleware/origin_validation.rb +73 -0
- data/lib/rosett_ai/mcp/middleware/rate_limit.rb +106 -0
- data/lib/rosett_ai/mcp/middleware/request_size.rb +51 -0
- data/lib/rosett_ai/mcp/plugins.rb +143 -0
- data/lib/rosett_ai/mcp/prompts/compilation_prompt.rb +40 -0
- data/lib/rosett_ai/mcp/prompts/compliance_prompt.rb +41 -0
- data/lib/rosett_ai/mcp/prompts/diagnostics_prompt.rb +41 -0
- data/lib/rosett_ai/mcp/prompts/validation_prompt.rb +41 -0
- data/lib/rosett_ai/mcp/resources/behaviour_resource.rb +127 -0
- data/lib/rosett_ai/mcp/resources/config_resource.rb +72 -0
- data/lib/rosett_ai/mcp/resources/design_resource.rb +58 -0
- data/lib/rosett_ai/mcp/resources/hooks_resource.rb +74 -0
- data/lib/rosett_ai/mcp/resources/provenance_resource.rb +51 -0
- data/lib/rosett_ai/mcp/resources/rules_resource.rb +60 -0
- data/lib/rosett_ai/mcp/resources/schema_resource.rb +72 -0
- data/lib/rosett_ai/mcp/response_helper.rb +46 -0
- data/lib/rosett_ai/mcp/security_logger.rb +60 -0
- data/lib/rosett_ai/mcp/server.rb +212 -0
- data/lib/rosett_ai/mcp/settings/server_installer.rb +112 -0
- data/lib/rosett_ai/mcp/settings/trust_manager.rb +142 -0
- data/lib/rosett_ai/mcp/tools/adopt_tool.rb +70 -0
- data/lib/rosett_ai/mcp/tools/backup_tool.rb +64 -0
- data/lib/rosett_ai/mcp/tools/behaviour_display_tool.rb +72 -0
- data/lib/rosett_ai/mcp/tools/behaviour_list_tool.rb +56 -0
- data/lib/rosett_ai/mcp/tools/behaviour_manage_tool.rb +114 -0
- data/lib/rosett_ai/mcp/tools/behaviour_show_tool.rb +62 -0
- data/lib/rosett_ai/mcp/tools/compile_status_tool.rb +122 -0
- data/lib/rosett_ai/mcp/tools/compile_tool.rb +191 -0
- data/lib/rosett_ai/mcp/tools/comply_tool.rb +79 -0
- data/lib/rosett_ai/mcp/tools/config_compile_tool.rb +71 -0
- data/lib/rosett_ai/mcp/tools/config_status_tool.rb +79 -0
- data/lib/rosett_ai/mcp/tools/content_tool.rb +78 -0
- data/lib/rosett_ai/mcp/tools/context_query_tool.rb +156 -0
- data/lib/rosett_ai/mcp/tools/design_list_tool.rb +57 -0
- data/lib/rosett_ai/mcp/tools/design_show_tool.rb +69 -0
- data/lib/rosett_ai/mcp/tools/doctor_tool.rb +62 -0
- data/lib/rosett_ai/mcp/tools/documentation_status_tool.rb +45 -0
- data/lib/rosett_ai/mcp/tools/engines_tool.rb +84 -0
- data/lib/rosett_ai/mcp/tools/hook_install_tool.rb +190 -0
- data/lib/rosett_ai/mcp/tools/hook_preview_tool.rb +173 -0
- data/lib/rosett_ai/mcp/tools/hooks_status_tool.rb +84 -0
- data/lib/rosett_ai/mcp/tools/init_tool.rb +87 -0
- data/lib/rosett_ai/mcp/tools/license_status_tool.rb +44 -0
- data/lib/rosett_ai/mcp/tools/project_tool.rb +117 -0
- data/lib/rosett_ai/mcp/tools/provenance_tool.rb +97 -0
- data/lib/rosett_ai/mcp/tools/provenance_write_tool.rb +40 -0
- data/lib/rosett_ai/mcp/tools/retrofit_tool.rb +81 -0
- data/lib/rosett_ai/mcp/tools/rule_search_tool.rb +163 -0
- data/lib/rosett_ai/mcp/tools/schema_get_tool.rb +94 -0
- data/lib/rosett_ai/mcp/tools/tooling_tool.rb +86 -0
- data/lib/rosett_ai/mcp/tools/validate_tool.rb +105 -0
- data/lib/rosett_ai/mcp/tools/workflow_execute_tool.rb +74 -0
- data/lib/rosett_ai/mcp/tools/workflow_tool.rb +78 -0
- data/lib/rosett_ai/migration/detector.rb +117 -0
- data/lib/rosett_ai/migration/nncc_config_migrator.rb +94 -0
- data/lib/rosett_ai/migration/nncc_project_migrator.rb +90 -0
- data/lib/rosett_ai/migration/xdg_migrator.rb +123 -0
- data/lib/rosett_ai/package_manager/apt.rb +108 -0
- data/lib/rosett_ai/package_manager/base.rb +68 -0
- data/lib/rosett_ai/package_manager/gem_backend.rb +90 -0
- data/lib/rosett_ai/packaging/variant_config.rb +92 -0
- data/lib/rosett_ai/path_resolver.rb +115 -0
- data/lib/rosett_ai/plugins/contract.rb +43 -0
- data/lib/rosett_ai/plugins/engine_contract.rb +60 -0
- data/lib/rosett_ai/plugins/gui_contract.rb +74 -0
- data/lib/rosett_ai/plugins/mcp_contract.rb +48 -0
- data/lib/rosett_ai/plugins/registry.rb +150 -0
- data/lib/rosett_ai/policy/auditor.rb +41 -0
- data/lib/rosett_ai/policy/deny_list.rb +71 -0
- data/lib/rosett_ai/policy/opt_out_scanner.rb +37 -0
- data/lib/rosett_ai/policy/policy_compiler.rb +84 -0
- data/lib/rosett_ai/policy/protected_files.rb +47 -0
- data/lib/rosett_ai/policy/tier_hierarchy.rb +48 -0
- data/lib/rosett_ai/policy/validator.rb +35 -0
- data/lib/rosett_ai/profiler.rb +79 -0
- data/lib/rosett_ai/project/drift_detector.rb +126 -0
- data/lib/rosett_ai/project/manager.rb +115 -0
- data/lib/rosett_ai/project/sync_manager.rb +138 -0
- data/lib/rosett_ai/project/template_applier.rb +105 -0
- data/lib/rosett_ai/project_context.rb +82 -0
- data/lib/rosett_ai/provenance/entry.rb +63 -0
- data/lib/rosett_ai/provenance/file_source.rb +32 -0
- data/lib/rosett_ai/provenance/source.rb +62 -0
- data/lib/rosett_ai/provenance/store.rb +153 -0
- data/lib/rosett_ai/provenance/tracker.rb +62 -0
- data/lib/rosett_ai/provenance/trailer_generator.rb +43 -0
- data/lib/rosett_ai/provenance/validator.rb +45 -0
- data/lib/rosett_ai/quorum/collector.rb +59 -0
- data/lib/rosett_ai/quorum/comparator.rb +81 -0
- data/lib/rosett_ai/quorum/dispatcher.rb +57 -0
- data/lib/rosett_ai/quorum/strategies/adopt.rb +56 -0
- data/lib/rosett_ai/rai_config.rb +107 -0
- data/lib/rosett_ai/retrofit/base_parser.rb +66 -0
- data/lib/rosett_ai/retrofit/engine.rb +171 -0
- data/lib/rosett_ai/retrofit/parsers/agents_md_parser.rb +50 -0
- data/lib/rosett_ai/retrofit/parsers/claude_parser.rb +69 -0
- data/lib/rosett_ai/retrofit/parsers/cursor_parser.rb +82 -0
- data/lib/rosett_ai/retrofit/round_trip_validator.rb +65 -0
- data/lib/rosett_ai/retrofit/scanner.rb +47 -0
- data/lib/rosett_ai/retrofit/secret_detector.rb +87 -0
- data/lib/rosett_ai/secrets_resolver.rb +71 -0
- data/lib/rosett_ai/smart_feedback/suggester.rb +83 -0
- data/lib/rosett_ai/smart_feedback/thor_middleware.rb +84 -0
- data/lib/rosett_ai/structured_logger.rb +110 -0
- data/lib/rosett_ai/telemetry/json_lines_writer.rb +50 -0
- data/lib/rosett_ai/telemetry/log_rotator.rb +67 -0
- data/lib/rosett_ai/telemetry/provider.rb +26 -0
- data/lib/rosett_ai/telemetry/reporter.rb +144 -0
- data/lib/rosett_ai/telemetry.rb +47 -0
- data/lib/rosett_ai/text_sanitizer.rb +62 -0
- data/lib/rosett_ai/thor/cli.rb +269 -0
- data/lib/rosett_ai/thor/tasks/adopt.rb +250 -0
- data/lib/rosett_ai/thor/tasks/backup.rb +420 -0
- data/lib/rosett_ai/thor/tasks/behaviour.rb +474 -0
- data/lib/rosett_ai/thor/tasks/build.rb +1162 -0
- data/lib/rosett_ai/thor/tasks/compile.rb +415 -0
- data/lib/rosett_ai/thor/tasks/completion.rb +123 -0
- data/lib/rosett_ai/thor/tasks/comply.rb +82 -0
- data/lib/rosett_ai/thor/tasks/config.rb +265 -0
- data/lib/rosett_ai/thor/tasks/content.rb +193 -0
- data/lib/rosett_ai/thor/tasks/dbus.rb +321 -0
- data/lib/rosett_ai/thor/tasks/design.rb +258 -0
- data/lib/rosett_ai/thor/tasks/desktop.rb +129 -0
- data/lib/rosett_ai/thor/tasks/doctor.rb +127 -0
- data/lib/rosett_ai/thor/tasks/documentation.rb +321 -0
- data/lib/rosett_ai/thor/tasks/engines.rb +167 -0
- data/lib/rosett_ai/thor/tasks/hooks.rb +219 -0
- data/lib/rosett_ai/thor/tasks/init.rb +259 -0
- data/lib/rosett_ai/thor/tasks/license.rb +120 -0
- data/lib/rosett_ai/thor/tasks/mcp.rb +535 -0
- data/lib/rosett_ai/thor/tasks/migrate.rb +121 -0
- data/lib/rosett_ai/thor/tasks/plugins.rb +157 -0
- data/lib/rosett_ai/thor/tasks/project.rb +260 -0
- data/lib/rosett_ai/thor/tasks/provenance.rb +195 -0
- data/lib/rosett_ai/thor/tasks/release.rb +314 -0
- data/lib/rosett_ai/thor/tasks/retrofit.rb +90 -0
- data/lib/rosett_ai/thor/tasks/tooling.rb +308 -0
- data/lib/rosett_ai/thor/tasks/validate.rb +108 -0
- data/lib/rosett_ai/thor/tasks/workflow.rb +196 -0
- data/lib/rosett_ai/tooling/ci_yaml_validator.rb +37 -0
- data/lib/rosett_ai/tooling/version_checker.rb +35 -0
- data/lib/rosett_ai/ui/accessible_tui.rb +61 -0
- data/lib/rosett_ai/ui/base.rb +46 -0
- data/lib/rosett_ai/ui/gtk4.rb +98 -0
- data/lib/rosett_ai/ui/kde.rb +40 -0
- data/lib/rosett_ai/ui/qt6.rb +40 -0
- data/lib/rosett_ai/ui/registry.rb +60 -0
- data/lib/rosett_ai/ui/tty_helper.rb +74 -0
- data/lib/rosett_ai/ui/tui.rb +59 -0
- data/lib/rosett_ai/validators/behaviour_validator.rb +20 -0
- data/lib/rosett_ai/validators/design_validator.rb +17 -0
- data/lib/rosett_ai/validators/schema_validator.rb +84 -0
- data/lib/rosett_ai/validators/tooling_validator.rb +17 -0
- data/lib/rosett_ai/version.rb +8 -0
- data/lib/rosett_ai/version_consistency_checker.rb +129 -0
- data/lib/rosett_ai/workflow/audit_log.rb +86 -0
- data/lib/rosett_ai/workflow/engine.rb +142 -0
- data/lib/rosett_ai/workflow/manager.rb +82 -0
- data/lib/rosett_ai/workflow/schema_validator.rb +71 -0
- data/lib/rosett_ai/workflow/step_runner.rb +61 -0
- data/lib/rosett_ai/workflow/steps/prompt_step.rb +62 -0
- data/lib/rosett_ai/workflow/steps/rai_step.rb +74 -0
- data/lib/rosett_ai/workflow/steps/shell_step.rb +53 -0
- data/lib/rosett_ai/yaml_loader.rb +78 -0
- data/lib/rosett_ai.rb +221 -0
- data/lib/rubocop/cop/rosett_ai/shell_interpolation.rb +54 -0
- data/lib/rubocop/cop/rosett_ai/unsafe_const_get.rb +60 -0
- data/lib/rubocop/cop/rosett_ai/unsafe_send.rb +50 -0
- data/lib/rubocop/cop/rosett_ai/unsafe_yaml_load.rb +40 -0
- data/lib/rubocop/rosett_ai.rb +9 -0
- data/lib/scripts/generated/docker_hub_tags.rb +126 -0
- data/locales/.gitkeep +0 -0
- data/locales/ar.yml +579 -0
- data/locales/en.yml +571 -0
- data/locales/fr.yml +567 -0
- data/packaging/build-engine-deb.sh +81 -0
- data/packaging/scripts/postinst +17 -0
- data/packaging/scripts/postrm +19 -0
- data/packaging/scripts/prerm +10 -0
- data/packaging/wrapper.sh.template +38 -0
- data/rosett-ai.gemspec +63 -0
- data/rules/.gitkeep +0 -0
- data/scripts/publish/pulp_upload.sh +123 -0
- data/settings.json +29 -0
- data/share/applications/be.neatnerds.rosettai.desktop +29 -0
- data/share/dbus-1/interfaces/be.neatnerds.rosettai.xml +103 -0
- data/share/dbus-1/services/be.neatnerds.rosettai.service +3 -0
- data/share/templates/behaviour/criticalthinking.yml +69 -0
- metadata +810 -0
|
@@ -0,0 +1,1162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-only
|
|
4
|
+
# Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
|
|
5
|
+
|
|
6
|
+
require 'thor'
|
|
7
|
+
require 'terminal-table'
|
|
8
|
+
require 'rainbow'
|
|
9
|
+
require 'tty-spinner'
|
|
10
|
+
require 'fileutils'
|
|
11
|
+
require 'pathname'
|
|
12
|
+
require 'securerandom'
|
|
13
|
+
require 'time'
|
|
14
|
+
require_relative '../../formatting'
|
|
15
|
+
|
|
16
|
+
module RosettAi
|
|
17
|
+
module Thor
|
|
18
|
+
module Tasks
|
|
19
|
+
# Mutable build state: ID, log path, timings, and failure info.
|
|
20
|
+
BuildContext = Struct.new(
|
|
21
|
+
:build_id, :build_start, :build_log,
|
|
22
|
+
:stage_timings, :completed_stages,
|
|
23
|
+
:failed_stage, :failure_reason, :summary_printed
|
|
24
|
+
) do
|
|
25
|
+
def self.create(log_dir)
|
|
26
|
+
id = SecureRandom.uuid
|
|
27
|
+
log_path = log_dir.join("build-#{id}.log")
|
|
28
|
+
FileUtils.mkdir_p(log_path.dirname)
|
|
29
|
+
ctx = new(
|
|
30
|
+
build_id: id,
|
|
31
|
+
build_start: Time.now,
|
|
32
|
+
build_log: log_path,
|
|
33
|
+
stage_timings: {},
|
|
34
|
+
completed_stages: [],
|
|
35
|
+
failed_stage: nil,
|
|
36
|
+
failure_reason: nil,
|
|
37
|
+
summary_printed: false
|
|
38
|
+
)
|
|
39
|
+
File.open(log_path, 'w', 0o644) do |f|
|
|
40
|
+
f.write("# rosett-ai build log\n# Build ID: #{id}\n" \
|
|
41
|
+
"# Started: #{ctx.build_start.iso8601}\n\n")
|
|
42
|
+
end
|
|
43
|
+
ctx
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Thor subcommand for building a .deb package using fpm + ruby-build.
|
|
48
|
+
#
|
|
49
|
+
# Produces a self-contained Debian package at /opt/rosett-ai with an
|
|
50
|
+
# embedded Ruby runtime (compiled via ruby-build), vendored gems,
|
|
51
|
+
# and maintainer scripts for proper APT lifecycle handling.
|
|
52
|
+
#
|
|
53
|
+
# Build tooling references:
|
|
54
|
+
# fpm — https://github.com/jordansissel/fpm
|
|
55
|
+
# ruby-build — https://github.com/rbenv/ruby-build
|
|
56
|
+
# dpkg-deb — https://man7.org/linux/man-pages/man1/dpkg-deb.1.html
|
|
57
|
+
# fakeroot — https://wiki.debian.org/FakeRoot
|
|
58
|
+
class BuildPackage < ::Thor
|
|
59
|
+
include RosettAi::Formatting
|
|
60
|
+
|
|
61
|
+
default_task :create
|
|
62
|
+
|
|
63
|
+
INSTALL_DIR = '/opt/rosett-ai'
|
|
64
|
+
|
|
65
|
+
# Files/directories excluded when syncing app source into the staging tree
|
|
66
|
+
APP_EXCLUDES = [
|
|
67
|
+
# Version control
|
|
68
|
+
'.git', '.gitignore',
|
|
69
|
+
# Bundler / gem build artifacts
|
|
70
|
+
'.bundle', '.binstubs', 'vendor/bundle', '*.gem',
|
|
71
|
+
'Gemfile.dev', 'Gemfile.dev.lock', 'Gemfile.integration',
|
|
72
|
+
# CI/CD
|
|
73
|
+
'.gitlab-ci*',
|
|
74
|
+
# Build & packaging
|
|
75
|
+
'coverage', 'tmp', 'pkg', 'spec', 'test', 'packaging',
|
|
76
|
+
# Dev tool configs
|
|
77
|
+
'.rspec', '.rubocop*', '.reek*',
|
|
78
|
+
'.fasterer.yml', '.gitleaks.toml', '.mutant.yml',
|
|
79
|
+
'.overcommit.yml', '.git-hooks',
|
|
80
|
+
'.mdlrc', '.mdl_style.rb', '.yamllint', '.yardopts',
|
|
81
|
+
'.project', '.rosett-ai',
|
|
82
|
+
# AI assistant state — never ship in package
|
|
83
|
+
'.claude', 'doc/claude-sessions',
|
|
84
|
+
'cliff.toml', 'kitchen.yml', 'Rakefile'
|
|
85
|
+
].freeze
|
|
86
|
+
|
|
87
|
+
# Build stages in execution order (for tracking)
|
|
88
|
+
BUILD_STAGES = [
|
|
89
|
+
:validate, :clean, :prepare, :compile_ruby,
|
|
90
|
+
:sync_app, :install_gems, :clean_build_artifacts,
|
|
91
|
+
:install_wrapper,
|
|
92
|
+
:install_config, :install_man, :install_build_info, :package
|
|
93
|
+
].freeze
|
|
94
|
+
|
|
95
|
+
# Bundler/RubyGems environment variables that must be stripped
|
|
96
|
+
# when shelling out to the embedded Ruby. Without this, a parent
|
|
97
|
+
# `bundle exec` leaks RUBYOPT=-rbundler/setup and BUNDLE_* vars
|
|
98
|
+
# into child processes, causing the embedded Ruby to try loading
|
|
99
|
+
# the project's Gemfile (which it cannot resolve).
|
|
100
|
+
BUNDLER_ENV_VARS = [
|
|
101
|
+
'BUNDLE_GEMFILE', 'BUNDLE_PATH', 'BUNDLE_BIN_PATH',
|
|
102
|
+
'BUNDLE_APP_CONFIG',
|
|
103
|
+
'BUNDLE_DEPLOYMENT', 'BUNDLE_WITHOUT', 'BUNDLE_FROZEN',
|
|
104
|
+
'BUNDLE_DISABLE_SHARED_GEMS', 'BUNDLE_IGNORE_CONFIG',
|
|
105
|
+
'BUNDLER_ORIG_BUNDLE_GEMFILE', 'BUNDLER_ORIG_BUNDLE_BIN_PATH',
|
|
106
|
+
'BUNDLER_ORIG_BUNDLER_SETUP', 'BUNDLER_ORIG_BUNDLER_VERSION',
|
|
107
|
+
'BUNDLER_ORIG_GEM_HOME', 'BUNDLER_ORIG_GEM_PATH',
|
|
108
|
+
'BUNDLER_ORIG_MANPATH', 'BUNDLER_ORIG_PATH',
|
|
109
|
+
'BUNDLER_ORIG_RB_USER_INSTALL',
|
|
110
|
+
'BUNDLER_ORIG_RUBYLIB', 'BUNDLER_ORIG_RUBYOPT',
|
|
111
|
+
'BUNDLER_VERSION',
|
|
112
|
+
'BUNDLER_SETUP', 'GEM_HOME', 'GEM_PATH',
|
|
113
|
+
'RUBYOPT', 'RUBYLIB'
|
|
114
|
+
].freeze
|
|
115
|
+
|
|
116
|
+
# Hermetic system PATH — no workstation paths allowed.
|
|
117
|
+
# Native gem compilation needs basic system tools (make, gcc, ld)
|
|
118
|
+
# but must never reference /home/*, ~/.rbenv, or other user paths.
|
|
119
|
+
HERMETIC_SYSTEM_PATH = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
|
|
120
|
+
|
|
121
|
+
desc 'create', 'Build a .deb package using fpm + ruby-build'
|
|
122
|
+
long_desc <<~LONGDESC
|
|
123
|
+
Builds a self-contained Debian package at /opt/rosett-ai with an embedded
|
|
124
|
+
Ruby runtime (compiled via ruby-build), vendored gems, wrapper script,
|
|
125
|
+
man pages, and maintainer scripts.
|
|
126
|
+
|
|
127
|
+
Requires fakeroot, dpkg-deb, rsync, ruby-build, and fpm.
|
|
128
|
+
Internal command — only available when developing Rosett-AI itself.
|
|
129
|
+
|
|
130
|
+
EXAMPLES
|
|
131
|
+
|
|
132
|
+
raictl build package
|
|
133
|
+
raictl build package --clean --verbose
|
|
134
|
+
raictl build package --variant gtk4 --architecture arm64
|
|
135
|
+
raictl build package --ruby-version 3.3.10 --output-dir /tmp/pkg
|
|
136
|
+
LONGDESC
|
|
137
|
+
method_option :verbose,
|
|
138
|
+
type: :boolean,
|
|
139
|
+
default: false,
|
|
140
|
+
desc: 'Show detailed build output'
|
|
141
|
+
method_option :clean,
|
|
142
|
+
type: :boolean,
|
|
143
|
+
default: false,
|
|
144
|
+
desc: 'Clean staging directory before building'
|
|
145
|
+
method_option :ruby_version,
|
|
146
|
+
type: :string,
|
|
147
|
+
desc: 'Ruby version to compile (default: read from .ruby-version)'
|
|
148
|
+
method_option :architecture,
|
|
149
|
+
type: :string,
|
|
150
|
+
default: nil,
|
|
151
|
+
desc: 'Target architecture (default: auto-detect via dpkg)'
|
|
152
|
+
method_option :output_dir,
|
|
153
|
+
type: :string,
|
|
154
|
+
default: 'pkg',
|
|
155
|
+
desc: 'Directory for the produced .deb file'
|
|
156
|
+
method_option :build_iteration,
|
|
157
|
+
type: :numeric,
|
|
158
|
+
default: 1,
|
|
159
|
+
desc: 'Package build iteration (release number)'
|
|
160
|
+
method_option :variant,
|
|
161
|
+
type: :string,
|
|
162
|
+
default: 'core',
|
|
163
|
+
desc: 'Package variant (core, gtk4, qt6)'
|
|
164
|
+
|
|
165
|
+
def create
|
|
166
|
+
unless RosettAi.context.rai_internal?
|
|
167
|
+
raise ::Thor::Error,
|
|
168
|
+
::I18n.t('rosett_ai.cli.build_package_internal_only')
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
init_build_context!
|
|
172
|
+
load_variant_config!
|
|
173
|
+
print_build_header
|
|
174
|
+
|
|
175
|
+
run_stage(:validate) { validate_environment! }
|
|
176
|
+
run_stage(:clean) { clean_staging! } if options[:clean]
|
|
177
|
+
run_stage(:prepare) { prepare_staging! }
|
|
178
|
+
run_stage(:compile_ruby) { compile_ruby! }
|
|
179
|
+
run_stage(:sync_app) { sync_application! }
|
|
180
|
+
run_stage(:install_gems) { install_gems! }
|
|
181
|
+
run_stage(:clean_build_artifacts) { clean_build_artifacts! }
|
|
182
|
+
run_stage(:install_wrapper) { install_wrapper! }
|
|
183
|
+
run_stage(:install_config) { install_default_config! }
|
|
184
|
+
run_stage(:install_man) { install_man_pages! }
|
|
185
|
+
run_stage(:install_build_info) { install_build_info! }
|
|
186
|
+
run_stage(:package) { build_package! }
|
|
187
|
+
|
|
188
|
+
print_build_summary(:success)
|
|
189
|
+
rescue ::Thor::Error => e
|
|
190
|
+
print_build_summary(:failed) if @ctx && !@ctx.summary_printed
|
|
191
|
+
raise e
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
private
|
|
195
|
+
|
|
196
|
+
# -- build context -----------------------------------------------------
|
|
197
|
+
|
|
198
|
+
def init_build_context!
|
|
199
|
+
@ctx = BuildContext.create(project_root.join('tmp'))
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def load_variant_config!
|
|
203
|
+
@variant_config = RosettAi::Packaging::VariantConfig.load(options[:variant])
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def log_to_file(message)
|
|
207
|
+
File.open(@ctx.build_log, 'a', 0o644) { |f| f.puts("[#{Time.now.iso8601}] #{message}") }
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def run_stage(name)
|
|
211
|
+
stage_start = Time.now
|
|
212
|
+
log_to_file("STAGE #{name}: started")
|
|
213
|
+
yield
|
|
214
|
+
elapsed = Time.now - stage_start
|
|
215
|
+
@ctx.stage_timings[name] = elapsed
|
|
216
|
+
@ctx.completed_stages << name
|
|
217
|
+
log_to_file("STAGE #{name}: completed (#{format('%.1f', elapsed)}s)")
|
|
218
|
+
rescue ::Thor::Error => e
|
|
219
|
+
elapsed = Time.now - stage_start
|
|
220
|
+
@ctx.stage_timings[name] = elapsed
|
|
221
|
+
@ctx.failed_stage = name
|
|
222
|
+
log_to_file("STAGE #{name}: FAILED after #{format('%.1f', elapsed)}s")
|
|
223
|
+
log_to_file("FAILURE: #{@ctx.failure_reason}") if @ctx.failure_reason
|
|
224
|
+
raise e
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def print_build_header
|
|
228
|
+
puts
|
|
229
|
+
puts Rainbow('raictl package build').bright
|
|
230
|
+
puts " Build ID: #{@ctx.build_id}"
|
|
231
|
+
puts " Log file: #{@ctx.build_log}"
|
|
232
|
+
puts " Variant: #{@variant_config.variant}"
|
|
233
|
+
puts " Package: #{@variant_config.name}"
|
|
234
|
+
puts " Version: #{RosettAi::VERSION}"
|
|
235
|
+
puts " Ruby version: #{ruby_version}"
|
|
236
|
+
puts " Architecture: #{architecture}"
|
|
237
|
+
puts " Output: #{project_root.join(options[:output_dir])}"
|
|
238
|
+
puts
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# -- paths -------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
def project_root
|
|
244
|
+
@project_root ||= RosettAi.root
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def staging_dir
|
|
248
|
+
@staging_dir ||= project_root.join('tmp', 'staging')
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def staging_install
|
|
252
|
+
@staging_install ||= staging_dir.join('opt', 'rosett-ai')
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def staging_embedded
|
|
256
|
+
@staging_embedded ||= staging_install.join('embedded')
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def staging_app
|
|
260
|
+
@staging_app ||= staging_install.join('app')
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def output_dir
|
|
264
|
+
@output_dir ||= project_root.join(options[:output_dir]).tap { |dir| FileUtils.mkdir_p(dir) }
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def packaging_dir
|
|
268
|
+
@packaging_dir ||= project_root.join('packaging')
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# -- validation --------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
def validate_environment!
|
|
274
|
+
check_required_commands
|
|
275
|
+
check_ronn_available!
|
|
276
|
+
check_variant_adapter!
|
|
277
|
+
log_verbose('Build environment validated')
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def check_required_commands
|
|
281
|
+
required = ['fakeroot', 'dpkg-deb', 'rsync']
|
|
282
|
+
required << 'ruby-build' unless ruby_already_compiled?
|
|
283
|
+
|
|
284
|
+
required.each do |cmd|
|
|
285
|
+
next if command_available?(cmd)
|
|
286
|
+
|
|
287
|
+
abort_build("'#{cmd}' is not installed. " \
|
|
288
|
+
'Install build dependencies: apt-get install fakeroot dpkg-dev rsync; ' \
|
|
289
|
+
'and ensure ruby-build is on PATH.')
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
return if command_available?('fpm')
|
|
293
|
+
|
|
294
|
+
abort_build("'fpm' is not installed. Run: bundle install (fpm is in the :build group)")
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def check_ronn_available!
|
|
298
|
+
ronn_source = project_root.join('doc', 'man', 'rai.1.ronn')
|
|
299
|
+
return unless ronn_source.exist?
|
|
300
|
+
|
|
301
|
+
begin
|
|
302
|
+
require 'ronn'
|
|
303
|
+
rescue LoadError
|
|
304
|
+
abort_build("'ronn-ng' gem is required to compile man pages but is not installed. " \
|
|
305
|
+
'Run: gem install ronn-ng (or add it to the :build group in Gemfile)')
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def check_variant_adapter!
|
|
310
|
+
return if @variant_config.variant == 'core'
|
|
311
|
+
|
|
312
|
+
adapter = @variant_config.ui_adapter
|
|
313
|
+
adapter_path = project_root.join('lib', 'rosett_ai', 'ui', "#{adapter}.rb")
|
|
314
|
+
return if adapter_path.exist?
|
|
315
|
+
|
|
316
|
+
abort_build(<<~MSG.chomp)
|
|
317
|
+
Adapter file lib/rosett_ai/ui/#{adapter}.rb not found.
|
|
318
|
+
The #{adapter} UI adapter has not been implemented yet.
|
|
319
|
+
See P3 (ui_framework) for implementation schedule.
|
|
320
|
+
MSG
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def command_available?(cmd)
|
|
324
|
+
ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).any? do |dir|
|
|
325
|
+
File.executable?(File.join(dir, cmd))
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# -- staging -----------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
def clean_staging!
|
|
332
|
+
spinner = build_spinner('Cleaning staging directory...')
|
|
333
|
+
FileUtils.rm_rf(staging_dir)
|
|
334
|
+
spinner.success(Rainbow(' done').green)
|
|
335
|
+
log_verbose("Removed #{staging_dir}")
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def prepare_staging!
|
|
339
|
+
log_verbose("Preparing staging tree at #{staging_dir}")
|
|
340
|
+
[
|
|
341
|
+
staging_embedded,
|
|
342
|
+
staging_app,
|
|
343
|
+
staging_install.join('bin'),
|
|
344
|
+
staging_install.join('etc')
|
|
345
|
+
].each { |dir| FileUtils.mkdir_p(dir) }
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# -- ruby-build --------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
def ruby_version
|
|
351
|
+
@ruby_version ||= options[:ruby_version] || read_ruby_version_file
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def read_ruby_version_file
|
|
355
|
+
version_file = project_root.join('.ruby-version')
|
|
356
|
+
abort_build('.ruby-version file not found and --ruby-version not given') unless version_file.exist?
|
|
357
|
+
version_file.read.strip
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def compile_ruby!
|
|
361
|
+
if ruby_already_compiled?
|
|
362
|
+
log_verbose("Ruby #{ruby_version} already compiled in staging — skipping")
|
|
363
|
+
return
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
spinner = build_spinner("Compiling Ruby #{ruby_version}...")
|
|
367
|
+
success = run_clean_command(
|
|
368
|
+
'ruby-build', ruby_version, staging_embedded.to_s
|
|
369
|
+
)
|
|
370
|
+
return spinner.success(Rainbow(' done').green) if success
|
|
371
|
+
|
|
372
|
+
spinner.error(Rainbow(' failed').red)
|
|
373
|
+
detect_ruby_build_log
|
|
374
|
+
abort_build('ruby-build failed — see build log for details.')
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def detect_ruby_build_log
|
|
378
|
+
logs = Dir.glob('/tmp/ruby-build.*.log').sort_by { |f| File.mtime(f) }
|
|
379
|
+
return if logs.empty?
|
|
380
|
+
|
|
381
|
+
latest = logs.last
|
|
382
|
+
log_to_file("ruby-build log: #{latest}")
|
|
383
|
+
tail = File.readlines(latest).last(30).join
|
|
384
|
+
log_to_file("ruby-build tail output:\n#{tail}")
|
|
385
|
+
@ctx.failure_reason = "ruby-build failed. Compiler log: #{latest}"
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def ruby_already_compiled?
|
|
389
|
+
ruby_bin = staging_embedded.join('bin', 'ruby')
|
|
390
|
+
return false unless ruby_bin.exist?
|
|
391
|
+
|
|
392
|
+
installed = IO.popen([ruby_bin.to_s, '-e', 'puts RUBY_VERSION'], &:read).strip
|
|
393
|
+
installed == ruby_version.sub(/\A(\d+\.\d+\.\d+).*/, '\1')
|
|
394
|
+
rescue StandardError
|
|
395
|
+
false
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# -- application sync --------------------------------------------------
|
|
399
|
+
|
|
400
|
+
def sync_application!
|
|
401
|
+
spinner = build_spinner('Syncing application source...')
|
|
402
|
+
excludes = APP_EXCLUDES.flat_map { |e| ['--exclude', e] }
|
|
403
|
+
success = run_command(
|
|
404
|
+
'rsync', '-a', '--delete',
|
|
405
|
+
*excludes,
|
|
406
|
+
"#{project_root}/",
|
|
407
|
+
"#{staging_app}/"
|
|
408
|
+
)
|
|
409
|
+
return spinner.success(Rainbow(' done').green) if success
|
|
410
|
+
|
|
411
|
+
spinner.error(Rainbow(' failed').red)
|
|
412
|
+
abort_build('rsync failed to sync application source.')
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# -- gem installation --------------------------------------------------
|
|
416
|
+
|
|
417
|
+
def install_gems!
|
|
418
|
+
spinner = build_spinner('Installing gems (deployment mode)...')
|
|
419
|
+
embedded_bundle = staging_embedded.join('bin', 'bundle').to_s
|
|
420
|
+
|
|
421
|
+
env = embedded_env
|
|
422
|
+
cmds = [
|
|
423
|
+
[env, embedded_bundle, 'config', 'set', '--local', 'deployment', 'true'],
|
|
424
|
+
[env, embedded_bundle, 'config', 'set', '--local', 'path', 'vendor/bundle'],
|
|
425
|
+
[env, embedded_bundle, 'config', 'set', '--local', 'without', 'development test build desktop engines'],
|
|
426
|
+
[env, embedded_bundle, 'config', 'set', '--local', 'jobs', jobs_count.to_s],
|
|
427
|
+
[env, embedded_bundle, 'install']
|
|
428
|
+
]
|
|
429
|
+
|
|
430
|
+
cmds.each do |cmd|
|
|
431
|
+
next if run_command_in(staging_app.to_s, *cmd)
|
|
432
|
+
|
|
433
|
+
spinner.error(Rainbow(' failed').red)
|
|
434
|
+
log_bundle_debug(env, embedded_bundle)
|
|
435
|
+
abort_build(<<~MSG.chomp)
|
|
436
|
+
Gem installation failed.
|
|
437
|
+
The embedded Ruby bundler could not resolve or install gems.
|
|
438
|
+
This usually means Bundler environment variables from the parent
|
|
439
|
+
process leaked into the build. Check the build log for details.
|
|
440
|
+
MSG
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
spinner.success(Rainbow(' done').green)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# -- build artifact cleanup --------------------------------------------
|
|
447
|
+
|
|
448
|
+
# Strips native-gem build debris (gem_make.out, mkmf.log, Makefile)
|
|
449
|
+
# that embeds workstation-specific paths from the staging tree.
|
|
450
|
+
# These files are not needed at runtime and their presence leaks
|
|
451
|
+
# the build host's directory layout into the .deb package.
|
|
452
|
+
def clean_build_artifacts!
|
|
453
|
+
spinner = build_spinner('Cleaning native gem build artifacts...')
|
|
454
|
+
count = remove_build_debris
|
|
455
|
+
log_to_file("clean_build_artifacts: removed #{count} files")
|
|
456
|
+
spinner.success(Rainbow(" done (#{count} files removed)").green)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def remove_build_debris
|
|
460
|
+
artifacts = collect_build_artifacts
|
|
461
|
+
artifacts.each { |f| remove_artifact(f) }
|
|
462
|
+
artifacts.size
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def collect_build_artifacts
|
|
466
|
+
Dir.glob(staging_app.join('vendor', 'bundle', '**', '{gem_make.out,mkmf.log}')) +
|
|
467
|
+
Dir.glob(staging_app.join('vendor', 'bundle', '**', 'ext', '**', 'Makefile'))
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def remove_artifact(path)
|
|
471
|
+
FileUtils.rm_f(path)
|
|
472
|
+
log_verbose("Removed build artifact: #{path}")
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def log_bundle_debug(env, bundle_bin)
|
|
476
|
+
log_to_file('--- bundle environment debug ---')
|
|
477
|
+
log_to_file(" BUNDLE_GEMFILE: #{env['BUNDLE_GEMFILE']}")
|
|
478
|
+
log_to_file(" PATH (first entry): #{env['PATH'].to_s.split(':').first}")
|
|
479
|
+
log_to_file(" bundle binary: #{bundle_bin}")
|
|
480
|
+
log_to_file(" bundle exists: #{File.exist?(bundle_bin)}")
|
|
481
|
+
gemfile = staging_app.join('Gemfile')
|
|
482
|
+
log_to_file(" Gemfile exists: #{gemfile.exist?}")
|
|
483
|
+
lockfile = staging_app.join('Gemfile.lock')
|
|
484
|
+
log_to_file(" Gemfile.lock exists: #{lockfile.exist?}")
|
|
485
|
+
|
|
486
|
+
# Log which BUNDLE_ vars are still set in the current process
|
|
487
|
+
leaked = ENV.select { |k, _| k.start_with?('BUNDLE', 'GEM_', 'RUBY') }
|
|
488
|
+
log_to_file(' Env vars in parent process (potential leaks):')
|
|
489
|
+
leaked.each { |k, v| log_to_file(" #{k}=#{v}") }
|
|
490
|
+
log_to_file('--- end bundle debug ---')
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# Returns an environment hash that:
|
|
494
|
+
# 1. Unsets all Bundler/RubyGems vars inherited from `bundle exec`
|
|
495
|
+
# 2. Sets PATH to prefer the embedded Ruby
|
|
496
|
+
# 3. Points BUNDLE_GEMFILE at the staging app's Gemfile
|
|
497
|
+
def embedded_env
|
|
498
|
+
env = clean_env.merge(
|
|
499
|
+
'PATH' => "#{staging_embedded.join('bin')}:#{HERMETIC_SYSTEM_PATH}",
|
|
500
|
+
'BUNDLE_GEMFILE' => staging_app.join('Gemfile').to_s
|
|
501
|
+
)
|
|
502
|
+
log_to_file("embedded_env: #{env.inspect}")
|
|
503
|
+
env
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# Strips all Bundler/RubyGems variables. When passed to system(),
|
|
507
|
+
# nil values cause the variable to be unset in the child process.
|
|
508
|
+
def clean_env
|
|
509
|
+
BUNDLER_ENV_VARS.to_h { |var| [var, nil] }
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def jobs_count
|
|
513
|
+
@jobs_count ||= begin
|
|
514
|
+
nproc = `nproc 2>/dev/null`.strip
|
|
515
|
+
nproc.empty? ? 4 : [nproc.to_i, 1].max
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# -- wrapper + config --------------------------------------------------
|
|
520
|
+
|
|
521
|
+
def install_wrapper!
|
|
522
|
+
template = packaging_dir.join('wrapper.sh.template').read
|
|
523
|
+
ruby_abi = query_embedded_ruby('RbConfig::CONFIG["ruby_version"]')
|
|
524
|
+
ruby_arch = query_embedded_ruby('RbConfig::CONFIG["arch"]')
|
|
525
|
+
|
|
526
|
+
content = template
|
|
527
|
+
.gsub('@@RUBY_ABI@@', ruby_abi)
|
|
528
|
+
.gsub('@@RUBY_ARCH@@', ruby_arch)
|
|
529
|
+
|
|
530
|
+
dest = staging_install.join('bin', 'raictl')
|
|
531
|
+
File.write(dest, content)
|
|
532
|
+
FileUtils.chmod(0o755, dest)
|
|
533
|
+
log_verbose("Installed wrapper script to #{dest} (ABI=#{ruby_abi}, arch=#{ruby_arch})")
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def query_embedded_ruby(expression)
|
|
537
|
+
ruby_bin = staging_embedded.join('bin', 'ruby').to_s
|
|
538
|
+
env = clean_env.merge(
|
|
539
|
+
'LD_LIBRARY_PATH' => staging_embedded.join('lib').to_s
|
|
540
|
+
)
|
|
541
|
+
result = IO.popen([env, ruby_bin, '-e', "puts #{expression}"], &:read).strip
|
|
542
|
+
abort_build("Failed to query embedded Ruby for: #{expression}") if result.empty?
|
|
543
|
+
result
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def install_default_config!
|
|
547
|
+
src = project_root.join('settings.json')
|
|
548
|
+
return unless src.exist?
|
|
549
|
+
|
|
550
|
+
# Ship default at /opt/rosett-ai/etc/ for reference
|
|
551
|
+
default_dest = staging_install.join('etc', 'settings.json.default')
|
|
552
|
+
FileUtils.cp(src, default_dest)
|
|
553
|
+
log_verbose("Installed default settings to #{default_dest}")
|
|
554
|
+
|
|
555
|
+
# Ship config at /etc/rosett-ai/ so dpkg treats it as a conffile
|
|
556
|
+
# (dpkg will prompt on upgrade if user has modified it)
|
|
557
|
+
etc_dir = staging_dir.join('etc', 'rosett-ai')
|
|
558
|
+
FileUtils.mkdir_p(etc_dir)
|
|
559
|
+
FileUtils.cp(src, etc_dir.join('settings.json'))
|
|
560
|
+
log_verbose("Installed conffile to #{etc_dir.join('settings.json')}")
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# -- man pages ---------------------------------------------------------
|
|
564
|
+
|
|
565
|
+
def install_man_pages!
|
|
566
|
+
ronn_source = project_root.join('doc', 'man', 'rai.1.ronn')
|
|
567
|
+
return log_verbose('No man page source found — skipping') unless ronn_source.exist?
|
|
568
|
+
|
|
569
|
+
man_dir = staging_dir.join('usr', 'share', 'man', 'man1')
|
|
570
|
+
FileUtils.mkdir_p(man_dir)
|
|
571
|
+
|
|
572
|
+
require 'ronn'
|
|
573
|
+
doc = Ronn::Document.new(ronn_source.to_s)
|
|
574
|
+
roff_content = doc.to_roff
|
|
575
|
+
|
|
576
|
+
man_path = man_dir.join('rai.1')
|
|
577
|
+
File.write(man_path, roff_content)
|
|
578
|
+
|
|
579
|
+
# gzip the man page for dpkg conventions
|
|
580
|
+
system('gzip', '-f', man_path.to_s)
|
|
581
|
+
log_verbose("Installed man page to #{man_path}.gz")
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# -- build info --------------------------------------------------------
|
|
585
|
+
|
|
586
|
+
def install_build_info!
|
|
587
|
+
info = build_info_content
|
|
588
|
+
dest = staging_install.join('etc', 'BUILD_INFO')
|
|
589
|
+
File.open(dest, 'w', 0o644) { |f| f.write(info) }
|
|
590
|
+
log_verbose("Installed build info to #{dest}")
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def build_info_content
|
|
594
|
+
<<~INFO
|
|
595
|
+
version=#{RosettAi::VERSION}
|
|
596
|
+
commit=#{git_commit_sha}
|
|
597
|
+
branch=#{git_branch}
|
|
598
|
+
build_date=#{Time.now.utc.iso8601}
|
|
599
|
+
build_id=#{@ctx.build_id}
|
|
600
|
+
variant=#{@variant_config.variant}
|
|
601
|
+
ruby_version=#{ruby_version}
|
|
602
|
+
architecture=#{architecture}
|
|
603
|
+
builder=#{ENV.fetch('USER', 'unknown')}
|
|
604
|
+
INFO
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
def git_commit_sha
|
|
608
|
+
IO.popen(['git', '-C', project_root.to_s, 'rev-parse', '--short', 'HEAD'], err: File::NULL, &:read).strip
|
|
609
|
+
.then { |s| s.empty? ? 'unknown' : s }
|
|
610
|
+
rescue StandardError
|
|
611
|
+
'unknown'
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def git_branch
|
|
615
|
+
IO.popen(['git', '-C', project_root.to_s, 'rev-parse', '--abbrev-ref', 'HEAD'], err: File::NULL, &:read).strip
|
|
616
|
+
.then { |s| s.empty? ? 'unknown' : s }
|
|
617
|
+
rescue StandardError
|
|
618
|
+
'unknown'
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
# -- fpm ---------------------------------------------------------------
|
|
622
|
+
|
|
623
|
+
def architecture
|
|
624
|
+
@architecture ||= options[:architecture] || detect_architecture
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def detect_architecture
|
|
628
|
+
arch = `dpkg --print-architecture 2>/dev/null`.strip
|
|
629
|
+
if arch.empty?
|
|
630
|
+
RosettAi.logger.warn('dpkg --print-architecture failed; falling back to amd64')
|
|
631
|
+
log_to_file('WARNING: dpkg --print-architecture returned empty — defaulting to amd64')
|
|
632
|
+
puts Rainbow(' Warning: could not detect architecture via dpkg; defaulting to amd64').yellow
|
|
633
|
+
'amd64'
|
|
634
|
+
else
|
|
635
|
+
arch
|
|
636
|
+
end
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
def package_filename
|
|
640
|
+
"#{@variant_config.name}_#{RosettAi::VERSION}-#{options[:build_iteration]}_#{architecture}.deb"
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
def build_package!
|
|
644
|
+
spinner = build_spinner('Building .deb package with fpm...')
|
|
645
|
+
|
|
646
|
+
fpm_args = [
|
|
647
|
+
'fpm',
|
|
648
|
+
'-s', 'dir',
|
|
649
|
+
'-t', 'deb',
|
|
650
|
+
'--name', @variant_config.name,
|
|
651
|
+
'--version', RosettAi::VERSION,
|
|
652
|
+
'--iteration', options[:build_iteration].to_s,
|
|
653
|
+
'--maintainer', 'NeatNerds <query@neatnerds.be>',
|
|
654
|
+
'--vendor', 'NeatNerds',
|
|
655
|
+
'--description', @variant_config.description,
|
|
656
|
+
'--url', 'https://gitlab.neatnerds.be/neatnerds/NeatNerds-AI/rosett-ai',
|
|
657
|
+
'--license', 'GPL-3.0-only',
|
|
658
|
+
'--architecture', architecture,
|
|
659
|
+
'--category', 'devel',
|
|
660
|
+
*fpm_depends_args,
|
|
661
|
+
'--config-files', '/etc/rosett-ai/settings.json',
|
|
662
|
+
'--directories', '/opt/rosett-ai',
|
|
663
|
+
'--after-install', packaging_dir.join('scripts', 'postinst').to_s,
|
|
664
|
+
'--before-remove', packaging_dir.join('scripts', 'prerm').to_s,
|
|
665
|
+
'--after-remove', packaging_dir.join('scripts', 'postrm').to_s,
|
|
666
|
+
'--deb-compression', 'xz',
|
|
667
|
+
'--deb-field', 'Vcs-Git: https://gitlab.neatnerds.be/neatnerds/NeatNerds-AI/rosett-ai.git',
|
|
668
|
+
'--deb-field', "Build-Commit: #{git_commit_sha}",
|
|
669
|
+
'--deb-field', "Build-Branch: #{git_branch}",
|
|
670
|
+
'--deb-field', "Build-Date: #{Time.now.utc.iso8601}",
|
|
671
|
+
'--package', output_dir.join(package_filename).to_s,
|
|
672
|
+
'--force',
|
|
673
|
+
'-C', staging_dir.to_s,
|
|
674
|
+
'.'
|
|
675
|
+
]
|
|
676
|
+
|
|
677
|
+
success = run_command(*fpm_args)
|
|
678
|
+
return spinner.success(Rainbow(' done').green) if success
|
|
679
|
+
|
|
680
|
+
spinner.error(Rainbow(' failed').red)
|
|
681
|
+
abort_build('fpm packaging failed.')
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def fpm_depends_args
|
|
685
|
+
@variant_config.fpm_depends.flat_map { |dep| ['--depends', dep] }
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
# -- build summary -----------------------------------------------------
|
|
689
|
+
|
|
690
|
+
def print_build_summary(status)
|
|
691
|
+
@ctx.summary_printed = true
|
|
692
|
+
elapsed = Time.now - @ctx.build_start
|
|
693
|
+
puts
|
|
694
|
+
|
|
695
|
+
print_stages_table(status, elapsed)
|
|
696
|
+
print_failure_detail if status == :failed
|
|
697
|
+
print_artifacts_table if status == :success
|
|
698
|
+
print_build_footer(status, elapsed)
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def print_stages_table(status, total_elapsed)
|
|
702
|
+
rows = BUILD_STAGES.map { |stage| build_stage_row(stage) }
|
|
703
|
+
rows.compact!
|
|
704
|
+
|
|
705
|
+
title_label, _, color = status_text(status)
|
|
706
|
+
table = ::Terminal::Table.new(
|
|
707
|
+
title: Rainbow("Build #{title_label}").send(color).bright,
|
|
708
|
+
headings: ['Stage', 'Status', 'Duration'],
|
|
709
|
+
rows: rows,
|
|
710
|
+
style: { border: :unicode_round }
|
|
711
|
+
)
|
|
712
|
+
puts table
|
|
713
|
+
puts " Total: #{format_duration(total_elapsed)}"
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
def build_stage_row(stage)
|
|
717
|
+
if @ctx.completed_stages.include?(stage)
|
|
718
|
+
elapsed = @ctx.stage_timings[stage]
|
|
719
|
+
[stage_label(stage), Rainbow('OK').green, format_duration(elapsed)]
|
|
720
|
+
elsif @ctx.failed_stage == stage
|
|
721
|
+
elapsed = @ctx.stage_timings[stage]
|
|
722
|
+
[stage_label(stage), Rainbow('FAILED').red, format_duration(elapsed)]
|
|
723
|
+
elsif skipped_stage?(stage)
|
|
724
|
+
nil
|
|
725
|
+
else
|
|
726
|
+
[stage_label(stage), Rainbow('--').faint, '--']
|
|
727
|
+
end
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
def skipped_stage?(stage)
|
|
731
|
+
stage == :clean && !options[:clean]
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
def stage_label(stage)
|
|
735
|
+
{
|
|
736
|
+
validate: 'Validate environment',
|
|
737
|
+
clean: 'Clean staging',
|
|
738
|
+
prepare: 'Prepare staging tree',
|
|
739
|
+
compile_ruby: "Compile Ruby #{ruby_version}",
|
|
740
|
+
sync_app: 'Sync application source',
|
|
741
|
+
install_gems: 'Install gems (deployment)',
|
|
742
|
+
install_wrapper: 'Install wrapper script',
|
|
743
|
+
install_config: 'Install default config',
|
|
744
|
+
install_man: 'Install man pages',
|
|
745
|
+
install_build_info: 'Install build info',
|
|
746
|
+
package: 'Build .deb with fpm'
|
|
747
|
+
}.fetch(stage, stage.to_s)
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
def print_failure_detail
|
|
751
|
+
puts
|
|
752
|
+
puts Rainbow('Failure Detail').red.bright
|
|
753
|
+
puts Rainbow(" Stage: #{stage_label(@ctx.failed_stage)}").red
|
|
754
|
+
puts Rainbow(" Reason: #{@ctx.failure_reason}").red if @ctx.failure_reason
|
|
755
|
+
puts
|
|
756
|
+
puts " Build log: #{@ctx.build_log}"
|
|
757
|
+
puts ' Re-run with --verbose for full command output.'
|
|
758
|
+
puts
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
def print_artifacts_table
|
|
762
|
+
packages = Dir.glob(output_dir.join('*.deb').to_s)
|
|
763
|
+
return puts Rainbow(' No .deb packages found in output directory.').yellow if packages.empty?
|
|
764
|
+
|
|
765
|
+
rows = packages.map { |pkg| [File.basename(pkg), format_size(File.size(pkg))] }
|
|
766
|
+
table = ::Terminal::Table.new(
|
|
767
|
+
title: Rainbow('Artifacts').bright,
|
|
768
|
+
headings: ['Package', 'Size'],
|
|
769
|
+
rows: rows,
|
|
770
|
+
style: { border: :unicode_round }
|
|
771
|
+
)
|
|
772
|
+
puts table
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
def print_build_footer(status, elapsed)
|
|
776
|
+
_, footer_label, color = status_text(status)
|
|
777
|
+
puts
|
|
778
|
+
puts " Build ID: #{@ctx.build_id}"
|
|
779
|
+
puts " Log file: #{@ctx.build_log}"
|
|
780
|
+
puts " Duration: #{format_duration(elapsed)}"
|
|
781
|
+
puts " Status: #{Rainbow(footer_label).send(color)}"
|
|
782
|
+
puts
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
# format_size and format_duration are provided by RosettAi::Formatting
|
|
786
|
+
|
|
787
|
+
# Returns [table_title, footer_label, color] for the given build status.
|
|
788
|
+
def status_text(status)
|
|
789
|
+
status == :success ? ['Complete', 'SUCCESS', :green] : ['Failed', 'FAILED', :red]
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
# -- helpers -----------------------------------------------------------
|
|
793
|
+
|
|
794
|
+
def build_spinner(message)
|
|
795
|
+
spinner = TTY::Spinner.new("[:spinner] #{message}", format: :dots, clear: !options[:verbose])
|
|
796
|
+
spinner.auto_spin
|
|
797
|
+
spinner
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
def run_command(*cmd)
|
|
801
|
+
if options[:verbose]
|
|
802
|
+
system(*cmd)
|
|
803
|
+
else
|
|
804
|
+
system(*cmd, [:out, :err] => File::NULL)
|
|
805
|
+
end
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
# Runs a command with a fully clean environment (no Bundler leakage).
|
|
809
|
+
# Used for ruby-build and any process that must not see the parent's bundle.
|
|
810
|
+
def run_clean_command(*cmd)
|
|
811
|
+
if options[:verbose]
|
|
812
|
+
system(clean_env, *cmd)
|
|
813
|
+
else
|
|
814
|
+
system(clean_env, *cmd, [:out, :err] => File::NULL)
|
|
815
|
+
end
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
def run_command_in(dir, env, *cmd)
|
|
819
|
+
if options[:verbose]
|
|
820
|
+
system(env, *cmd, chdir: dir)
|
|
821
|
+
else
|
|
822
|
+
system(env, *cmd, chdir: dir, [:out, :err] => File::NULL)
|
|
823
|
+
end
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
def abort_build(message)
|
|
827
|
+
@ctx.failure_reason = message
|
|
828
|
+
log_to_file("ABORT: #{message}")
|
|
829
|
+
raise ::Thor::Error, "Error: #{message}"
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
def log_verbose(message)
|
|
833
|
+
puts " #{message}" if options[:verbose]
|
|
834
|
+
end
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
# Thor subcommand for building a .deb package for an rosett-ai engine plugin.
|
|
838
|
+
#
|
|
839
|
+
# Replaces the per-engine packaging/build-deb.sh shell scripts with a
|
|
840
|
+
# unified Ruby implementation that auto-detects engine name and version
|
|
841
|
+
# from the gemspec, derives the Ruby ABI from the current runtime, and
|
|
842
|
+
# packages with fpm.
|
|
843
|
+
#
|
|
844
|
+
# Build tooling references:
|
|
845
|
+
# fpm — https://github.com/jordansissel/fpm
|
|
846
|
+
class BuildEngine < ::Thor
|
|
847
|
+
include RosettAi::Formatting
|
|
848
|
+
|
|
849
|
+
default_task :create
|
|
850
|
+
|
|
851
|
+
INSTALL_BASE = '/opt/rosett-ai/embedded/lib/ruby/gems'
|
|
852
|
+
|
|
853
|
+
desc 'create', 'Build a .deb package for an rosett-ai engine plugin'
|
|
854
|
+
long_desc <<~LONGDESC
|
|
855
|
+
Builds a .deb package for an rosett-ai engine plugin by auto-detecting
|
|
856
|
+
the engine name and version from the gemspec. The package installs
|
|
857
|
+
the gem into /opt/rosett-ai/embedded/ and depends on the Rosett-AI core package.
|
|
858
|
+
|
|
859
|
+
Requires gem and fpm. Run from the engine repo directory or use --path.
|
|
860
|
+
|
|
861
|
+
EXAMPLES
|
|
862
|
+
|
|
863
|
+
raictl build engine
|
|
864
|
+
raictl build engine --path ../rosett-ai-engine-claude --verbose
|
|
865
|
+
raictl build engine --rai-min-version 1.0.0 --output-dir /tmp/pkg
|
|
866
|
+
LONGDESC
|
|
867
|
+
method_option :verbose,
|
|
868
|
+
type: :boolean,
|
|
869
|
+
default: false,
|
|
870
|
+
desc: 'Show detailed build output'
|
|
871
|
+
method_option :path,
|
|
872
|
+
type: :string,
|
|
873
|
+
default: nil,
|
|
874
|
+
desc: 'Engine repo directory (default: current directory)'
|
|
875
|
+
method_option :output_dir,
|
|
876
|
+
type: :string,
|
|
877
|
+
default: 'pkg',
|
|
878
|
+
desc: 'Directory for the produced .deb file'
|
|
879
|
+
method_option :rai_min_version,
|
|
880
|
+
type: :string,
|
|
881
|
+
default: nil,
|
|
882
|
+
desc: 'Minimum Rosett-AI dependency version (default: current Rosett-AI version)'
|
|
883
|
+
method_option :build_iteration,
|
|
884
|
+
type: :numeric,
|
|
885
|
+
default: 1,
|
|
886
|
+
desc: 'Package build iteration (release number)'
|
|
887
|
+
|
|
888
|
+
def create
|
|
889
|
+
resolve_engine_dir!
|
|
890
|
+
detect_gemspec!
|
|
891
|
+
parse_gemspec!
|
|
892
|
+
print_engine_header
|
|
893
|
+
|
|
894
|
+
run_stage(:validate) { validate_engine_environment! }
|
|
895
|
+
run_stage(:gem_build) { build_gem! }
|
|
896
|
+
run_stage(:staging) { prepare_staging! }
|
|
897
|
+
run_stage(:gem_install) { install_gem_to_staging! }
|
|
898
|
+
run_stage(:package) { build_engine_package! }
|
|
899
|
+
|
|
900
|
+
print_engine_summary(:success)
|
|
901
|
+
rescue ::Thor::Error => e
|
|
902
|
+
print_engine_summary(:failed) unless @summary_printed
|
|
903
|
+
raise e
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
private
|
|
907
|
+
|
|
908
|
+
# -- resolution --------------------------------------------------------
|
|
909
|
+
|
|
910
|
+
def resolve_engine_dir!
|
|
911
|
+
dir = options[:path] || ENV.fetch('RAI_ORIGINAL_PWD', Dir.pwd)
|
|
912
|
+
@engine_dir = Pathname.new(dir).expand_path
|
|
913
|
+
abort_engine("Directory does not exist: #{@engine_dir}") unless @engine_dir.directory?
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
def detect_gemspec!
|
|
917
|
+
candidates = Dir.glob(@engine_dir.join('rosett-ai-engine-*.gemspec').to_s)
|
|
918
|
+
case candidates.length
|
|
919
|
+
when 0
|
|
920
|
+
abort_engine(::I18n.t('rosett_ai.cli.build_engine.no_gemspec', dir: @engine_dir))
|
|
921
|
+
when 1
|
|
922
|
+
@gemspec_path = Pathname.new(candidates.first)
|
|
923
|
+
else
|
|
924
|
+
abort_engine(::I18n.t('rosett_ai.cli.build_engine.multiple_gemspecs', dir: @engine_dir))
|
|
925
|
+
end
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
def parse_gemspec!
|
|
929
|
+
spec = Gem::Specification.load(@gemspec_path.to_s)
|
|
930
|
+
abort_engine("Failed to load gemspec: #{@gemspec_path}") unless spec
|
|
931
|
+
|
|
932
|
+
@engine_name = spec.name
|
|
933
|
+
@engine_version = spec.version.to_s
|
|
934
|
+
log_verbose("Detected engine: #{@engine_name} v#{@engine_version}")
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
# -- paths -------------------------------------------------------------
|
|
938
|
+
|
|
939
|
+
def ruby_abi
|
|
940
|
+
@ruby_abi ||= RbConfig::CONFIG['ruby_version']
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
def staging_dir
|
|
944
|
+
@staging_dir ||= @engine_dir.join('tmp', 'staging')
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
def gem_install_dir
|
|
948
|
+
@gem_install_dir ||= "#{INSTALL_BASE}/#{ruby_abi}"
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
def staging_gem_dir
|
|
952
|
+
@staging_gem_dir ||= staging_dir.join(gem_install_dir.delete_prefix('/'))
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
def output_dir
|
|
956
|
+
@output_dir ||= @engine_dir.join(options[:output_dir]).tap { |d| FileUtils.mkdir_p(d) }
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
def gem_file
|
|
960
|
+
@gem_file ||= @engine_dir.join('tmp', "#{@engine_name}-#{@engine_version}.gem")
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
def rai_min_version
|
|
964
|
+
@rai_min_version ||= options[:rai_min_version] || RosettAi::VERSION
|
|
965
|
+
end
|
|
966
|
+
|
|
967
|
+
# -- validation --------------------------------------------------------
|
|
968
|
+
|
|
969
|
+
def validate_engine_environment!
|
|
970
|
+
['gem', 'fpm'].each do |cmd|
|
|
971
|
+
next if command_available?(cmd)
|
|
972
|
+
|
|
973
|
+
abort_engine("'#{cmd}' is not installed.")
|
|
974
|
+
end
|
|
975
|
+
log_verbose('Engine build environment validated')
|
|
976
|
+
end
|
|
977
|
+
|
|
978
|
+
def command_available?(cmd)
|
|
979
|
+
ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).any? do |dir|
|
|
980
|
+
File.executable?(File.join(dir, cmd))
|
|
981
|
+
end
|
|
982
|
+
end
|
|
983
|
+
|
|
984
|
+
# -- build stages ------------------------------------------------------
|
|
985
|
+
|
|
986
|
+
def build_gem!
|
|
987
|
+
spinner = build_spinner("Building #{@engine_name}-#{@engine_version}.gem...")
|
|
988
|
+
FileUtils.mkdir_p(gem_file.dirname)
|
|
989
|
+
|
|
990
|
+
success = run_command(
|
|
991
|
+
'gem', 'build', @gemspec_path.to_s,
|
|
992
|
+
'--output', gem_file.to_s,
|
|
993
|
+
chdir: @engine_dir.to_s
|
|
994
|
+
)
|
|
995
|
+
return spinner.success(Rainbow(' done').green) if success
|
|
996
|
+
|
|
997
|
+
spinner.error(Rainbow(' failed').red)
|
|
998
|
+
abort_engine('gem build failed.')
|
|
999
|
+
end
|
|
1000
|
+
|
|
1001
|
+
def prepare_staging!
|
|
1002
|
+
FileUtils.rm_rf(staging_dir)
|
|
1003
|
+
FileUtils.mkdir_p(staging_gem_dir)
|
|
1004
|
+
log_verbose("Staging tree at #{staging_dir}")
|
|
1005
|
+
end
|
|
1006
|
+
|
|
1007
|
+
def install_gem_to_staging!
|
|
1008
|
+
spinner = build_spinner('Installing gem into staging tree...')
|
|
1009
|
+
success = run_command(
|
|
1010
|
+
'gem', 'install',
|
|
1011
|
+
'--local',
|
|
1012
|
+
'--install-dir', staging_gem_dir.to_s,
|
|
1013
|
+
'--no-document',
|
|
1014
|
+
'--ignore-dependencies',
|
|
1015
|
+
gem_file.to_s
|
|
1016
|
+
)
|
|
1017
|
+
return spinner.success(Rainbow(' done').green) if success
|
|
1018
|
+
|
|
1019
|
+
spinner.error(Rainbow(' failed').red)
|
|
1020
|
+
abort_engine('gem install into staging failed.')
|
|
1021
|
+
end
|
|
1022
|
+
|
|
1023
|
+
def build_engine_package!
|
|
1024
|
+
spinner = build_spinner('Building .deb package with fpm...')
|
|
1025
|
+
|
|
1026
|
+
fpm_args = [
|
|
1027
|
+
'fpm',
|
|
1028
|
+
'-s', 'dir',
|
|
1029
|
+
'-t', 'deb',
|
|
1030
|
+
'--name', @engine_name,
|
|
1031
|
+
'--version', @engine_version,
|
|
1032
|
+
'--iteration', options[:build_iteration].to_s,
|
|
1033
|
+
'--maintainer', 'NeatNerds <query@neatnerds.be>',
|
|
1034
|
+
'--vendor', 'NeatNerds',
|
|
1035
|
+
'--description', "rosett-ai engine plugin: #{@engine_name}",
|
|
1036
|
+
'--url', "https://gitlab.neatnerds.be/neatnerds/#{@engine_name}",
|
|
1037
|
+
'--license', 'GPL-3.0-only',
|
|
1038
|
+
'--architecture', 'all',
|
|
1039
|
+
'--category', 'devel',
|
|
1040
|
+
'--depends', "rosett-ai (>= #{rai_min_version})",
|
|
1041
|
+
'--directories', gem_install_dir,
|
|
1042
|
+
'--deb-compression', 'xz',
|
|
1043
|
+
'--deb-field', "Vcs-Git: https://gitlab.neatnerds.be/neatnerds/#{@engine_name}.git",
|
|
1044
|
+
'--deb-field', "Build-Commit: #{engine_git_commit_sha}",
|
|
1045
|
+
'--deb-field', "Build-Branch: #{engine_git_branch}",
|
|
1046
|
+
'--deb-field', "Build-Date: #{Time.now.utc.iso8601}",
|
|
1047
|
+
'--package', output_dir.join(package_filename).to_s,
|
|
1048
|
+
'--force',
|
|
1049
|
+
'-C', staging_dir.to_s,
|
|
1050
|
+
'.'
|
|
1051
|
+
]
|
|
1052
|
+
|
|
1053
|
+
success = run_command(*fpm_args)
|
|
1054
|
+
return spinner.success(Rainbow(' done').green) if success
|
|
1055
|
+
|
|
1056
|
+
spinner.error(Rainbow(' failed').red)
|
|
1057
|
+
abort_engine('fpm packaging failed.')
|
|
1058
|
+
end
|
|
1059
|
+
|
|
1060
|
+
def package_filename
|
|
1061
|
+
"#{@engine_name}_#{@engine_version}-#{options[:build_iteration]}_all.deb"
|
|
1062
|
+
end
|
|
1063
|
+
|
|
1064
|
+
# -- output ------------------------------------------------------------
|
|
1065
|
+
|
|
1066
|
+
def print_engine_header
|
|
1067
|
+
puts
|
|
1068
|
+
puts Rainbow('raictl engine build').bright
|
|
1069
|
+
puts " Engine: #{@engine_name}"
|
|
1070
|
+
puts " Version: #{@engine_version}"
|
|
1071
|
+
puts " Ruby ABI: #{ruby_abi}"
|
|
1072
|
+
puts " Source: #{@engine_dir}"
|
|
1073
|
+
puts " Output: #{output_dir}"
|
|
1074
|
+
puts " rai dep: >= #{rai_min_version}"
|
|
1075
|
+
puts
|
|
1076
|
+
end
|
|
1077
|
+
|
|
1078
|
+
def print_engine_summary(status)
|
|
1079
|
+
@summary_printed = true
|
|
1080
|
+
puts
|
|
1081
|
+
|
|
1082
|
+
if status == :success
|
|
1083
|
+
puts Rainbow('Build Complete').green.bright
|
|
1084
|
+
deb = output_dir.join(package_filename)
|
|
1085
|
+
if deb.exist?
|
|
1086
|
+
puts " Package: #{deb}"
|
|
1087
|
+
puts " Size: #{format_size(deb.size)}"
|
|
1088
|
+
end
|
|
1089
|
+
else
|
|
1090
|
+
puts Rainbow('Build Failed').red.bright
|
|
1091
|
+
puts " Reason: #{@failure_reason}" if @failure_reason
|
|
1092
|
+
end
|
|
1093
|
+
puts
|
|
1094
|
+
end
|
|
1095
|
+
|
|
1096
|
+
# -- stages tracking ---------------------------------------------------
|
|
1097
|
+
|
|
1098
|
+
def run_stage(name)
|
|
1099
|
+
log_verbose("Stage #{name}: started")
|
|
1100
|
+
yield
|
|
1101
|
+
log_verbose("Stage #{name}: completed")
|
|
1102
|
+
rescue ::Thor::Error
|
|
1103
|
+
@failed_stage = name
|
|
1104
|
+
raise
|
|
1105
|
+
end
|
|
1106
|
+
|
|
1107
|
+
# -- helpers -----------------------------------------------------------
|
|
1108
|
+
|
|
1109
|
+
def build_spinner(message)
|
|
1110
|
+
spinner = TTY::Spinner.new("[:spinner] #{message}", format: :dots, clear: !options[:verbose])
|
|
1111
|
+
spinner.auto_spin
|
|
1112
|
+
spinner
|
|
1113
|
+
end
|
|
1114
|
+
|
|
1115
|
+
def run_command(*cmd, chdir: nil)
|
|
1116
|
+
kwargs = {}
|
|
1117
|
+
kwargs[:chdir] = chdir if chdir
|
|
1118
|
+
kwargs[[:out, :err]] = File::NULL unless options[:verbose]
|
|
1119
|
+
system(*cmd, **kwargs)
|
|
1120
|
+
end
|
|
1121
|
+
|
|
1122
|
+
def engine_git_commit_sha
|
|
1123
|
+
IO.popen(['git', '-C', @engine_dir.to_s, 'rev-parse', '--short', 'HEAD'], err: File::NULL, &:read).strip
|
|
1124
|
+
.then { |s| s.empty? ? 'unknown' : s }
|
|
1125
|
+
rescue StandardError
|
|
1126
|
+
'unknown'
|
|
1127
|
+
end
|
|
1128
|
+
|
|
1129
|
+
def engine_git_branch
|
|
1130
|
+
IO.popen(['git', '-C', @engine_dir.to_s, 'rev-parse', '--abbrev-ref', 'HEAD'], err: File::NULL, &:read).strip
|
|
1131
|
+
.then { |s| s.empty? ? 'unknown' : s }
|
|
1132
|
+
rescue StandardError
|
|
1133
|
+
'unknown'
|
|
1134
|
+
end
|
|
1135
|
+
|
|
1136
|
+
def abort_engine(message)
|
|
1137
|
+
@failure_reason = message
|
|
1138
|
+
raise ::Thor::Error, "Error: #{message}"
|
|
1139
|
+
end
|
|
1140
|
+
|
|
1141
|
+
def log_verbose(message)
|
|
1142
|
+
puts " #{message}" if options[:verbose]
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
# format_size is provided by RosettAi::Formatting
|
|
1146
|
+
end
|
|
1147
|
+
|
|
1148
|
+
# Parent Thor group for build subcommands
|
|
1149
|
+
class Build < ::Thor
|
|
1150
|
+
def self.exit_on_failure?
|
|
1151
|
+
true
|
|
1152
|
+
end
|
|
1153
|
+
|
|
1154
|
+
desc 'package', 'Build a .deb package using fpm + ruby-build'
|
|
1155
|
+
subcommand 'package', BuildPackage
|
|
1156
|
+
|
|
1157
|
+
desc 'engine', 'Build a .deb package for an rosett-ai engine plugin'
|
|
1158
|
+
subcommand 'engine', BuildEngine
|
|
1159
|
+
end
|
|
1160
|
+
end
|
|
1161
|
+
end
|
|
1162
|
+
end
|