synapse-hub 2.2.3 → 2.4.0
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.
- package/.agents/agents/failure-analyst.md +11 -0
- package/.agents/agents/live-explorer.md +28 -0
- package/.agents/agents/orchestrator.md +20 -4
- package/.agents/agents/pom-crafter.md +12 -0
- package/.agents/agents/quality-gatekeeper.md +19 -4
- package/.agents/agents/test-runner.md +11 -0
- package/.agents/rules/communication-style.md +40 -0
- package/.agents/rules/context-engineering.md +23 -0
- package/.agents/rules/git-push-protocol.md +11 -17
- package/.agents/rules/graphify.md +14 -14
- package/.agents/rules/lessons-learned.md +3 -3
- package/.agents/rules/mandatory-playwright-live.md +29 -29
- package/.agents/rules/mobile_appium_rules.md +132 -0
- package/.agents/rules/performance_testing_rules.md +74 -0
- package/.agents/rules/skill-precedence.md +1 -1
- package/.agents/rules/systematic-debugging.md +6 -6
- package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +64 -215
- package/.agents/skills/agent-orchestration-multi-agent-optimize/examples/swarm_workload_config.json +12 -0
- package/.agents/skills/antigravity-test-sentinel/SKILL.md +3 -3
- package/.agents/skills/antigravity-test-sentinel/examples/dod_gate_policy.json +11 -0
- package/.agents/skills/api-network-mocking-interception/SKILL.md +1 -1
- package/.agents/skills/api-network-mocking-interception/examples/network_route_stub.json +9 -0
- package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +9 -9
- package/.agents/skills/api-spec-to-e2e-matrix/examples/sample_openapi.json +19 -0
- package/.agents/skills/architecture-decision-records/SKILL.md +35 -412
- package/.agents/skills/architecture-decision-records/examples/0001-record-architecture-decisions.md +12 -0
- package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +4 -1
- package/.agents/skills/auth-session-and-cookie-manager/examples/storage_state_sample.json +11 -0
- package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +80 -186
- package/.agents/skills/backend-load-and-stress-patterns/examples/k6_load_test_sample.js +13 -0
- package/.agents/skills/bats-testing-patterns/SKILL.md +1 -1
- package/.agents/skills/bats-testing-patterns/examples/cli_runner_test.bats +6 -0
- package/.agents/skills/bats-testing-patterns/resources/implementation-playbook.md +614 -614
- package/.agents/skills/clean-pom-crafter/SKILL.md +21 -3
- package/.agents/skills/clean-pom-crafter/examples/standard_test_case.groovy +28 -28
- package/.agents/skills/clean-pom-crafter/references/pom_patterns.groovy +138 -138
- package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +2 -2
- package/.agents/skills/code-refactoring-refactor-clean/examples/refactored_page_sample.groovy +9 -0
- package/.agents/skills/code-refactoring-refactor-clean/resources/implementation-playbook.md +9 -7
- package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +5 -5
- package/.agents/skills/db-state-verifier-and-seeder/examples/db_fixture_seed.json +12 -0
- package/.agents/skills/e2e-testing-patterns/SKILL.md +1 -1
- package/.agents/skills/e2e-testing-patterns/examples/playwright_sharded_config.ts +12 -0
- package/.agents/skills/e2e-testing-patterns/resources/implementation-playbook.md +522 -522
- package/.agents/skills/failure-autopsy-and-healing/SKILL.md +17 -3
- package/.agents/skills/failure-autopsy-and-healing/examples/sample_junit_failure.xml +8 -0
- package/.agents/skills/failure-autopsy-and-healing/references/healing_matrix.md +64 -64
- package/.agents/skills/failure-autopsy-and-healing/references/qa_investigation_protocol.md +56 -56
- package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +98 -98
- package/.agents/skills/file-and-report-validator/SKILL.md +1 -1
- package/.agents/skills/file-and-report-validator/examples/sample_export_schema.json +14 -0
- package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +3 -3
- package/.agents/skills/flaky-test-sentinel-and-quarantine/examples/quarantine_registry.json +11 -0
- package/.agents/skills/gherkin-to-suite/SKILL.md +103 -0
- package/.agents/skills/gherkin-to-suite/examples/checkout.feature +39 -0
- package/.agents/skills/gherkin-to-suite/examples/suite_report_sample.json +334 -0
- package/.agents/skills/github-actions-templates/SKILL.md +47 -304
- package/.agents/skills/github-actions-templates/examples/matrix_test_pipeline.yml +8 -0
- package/.agents/skills/graphify-impact-analyzer/SKILL.md +6 -6
- package/.agents/skills/graphify-impact-analyzer/examples/dependency_blast_radius.json +8 -0
- package/.agents/skills/katalon-cli-runner/SKILL.md +2 -2
- package/.agents/skills/katalon-cli-runner/examples/headless_kre_exec.sh +3 -0
- package/.agents/skills/katalon-cli-runner/references/groovy-patterns.md +209 -209
- package/.agents/skills/katalon-cli-runner/references/katalon-api.md +155 -155
- package/.agents/skills/katalon-cli-runner/references/runner_commands.md +67 -67
- package/.agents/skills/lam-script-generator/SKILL.md +42 -1
- package/.agents/skills/lam-script-generator/examples/user_story_sample.md +8 -0
- package/.agents/skills/lean-code-ladder/SKILL.md +83 -0
- package/.agents/skills/lean-code-ladder/examples/ladder_walkthrough.md +37 -0
- package/.agents/skills/meta-prompt-crafter/SKILL.md +107 -0
- package/.agents/skills/meta-prompt-crafter/examples/qa_prompt_template.md +35 -0
- package/.agents/skills/persistent-planning-files/SKILL.md +108 -0
- package/.agents/skills/persistent-planning-files/examples/task_plan_example.md +56 -0
- package/.agents/skills/playwright-live-interaction/SKILL.md +49 -1
- package/.agents/skills/playwright-live-interaction/examples/live_probe_script.js +5 -0
- package/.agents/skills/playwright-live-interaction/references/advanced_superpowers.md +160 -160
- package/.agents/skills/playwright-live-interaction/references/compact_accessibility_snapshots.md +75 -75
- package/.agents/skills/playwright-live-interaction/references/recipes.md +388 -388
- package/.agents/skills/playwright-live-interaction/references/swarm_orchestration.md +134 -134
- package/.agents/skills/playwright-webapp-testing/SKILL.md +93 -0
- package/.agents/skills/playwright-webapp-testing/examples/server_lifecycle_playwright.py +63 -0
- package/.agents/skills/rbt-manual-testing/SKILL.md +150 -0
- package/.agents/skills/rbt-manual-testing/examples/checkout_rbt_manual_suite.md +25 -0
- package/.agents/skills/security-dast-sentinel/SKILL.md +57 -135
- package/.agents/skills/security-dast-sentinel/examples/dast_scan_config.json +10 -0
- package/.agents/skills/self-healing-locator/SKILL.md +24 -10
- package/.agents/skills/self-healing-locator/examples/aom_diff_sample.json +16 -0
- package/.agents/skills/skillspector-security-guard/SKILL.md +32 -90
- package/.agents/skills/skillspector-security-guard/examples/safe_mcp_config.json +12 -0
- package/.agents/skills/surgical-coding-principles/SKILL.md +89 -0
- package/.agents/skills/surgical-coding-principles/examples/surgical_vs_scope_creep.md +46 -0
- package/.agents/skills/synthetic-test-data-generator/SKILL.md +3 -3
- package/.agents/skills/synthetic-test-data-generator/examples/idempotent_data_recipe.json +9 -0
- package/.agents/skills/systematic-debugging/SKILL.md +100 -0
- package/.agents/skills/systematic-debugging/examples/four_phase_case.md +44 -0
- package/.agents/skills/test-driven-development/SKILL.md +114 -0
- package/.agents/skills/test-driven-development/examples/red_green_refactor_qa.md +78 -0
- package/.agents/skills/token-and-context-optimizer/SKILL.md +1 -1
- package/.agents/skills/token-and-context-optimizer/examples/distilled_log_sample.txt +3 -0
- package/.agents/skills/verification-before-completion/SKILL.md +100 -0
- package/.agents/skills/verification-before-completion/examples/evidence_log_example.md +46 -0
- package/.agents/skills/visual-regression-sentinel/SKILL.md +21 -1
- package/.agents/skills/visual-regression-sentinel/examples/visual_mask_rules.json +9 -0
- package/.agents/skills/wcag-audit-patterns/SKILL.md +3 -3
- package/.agents/skills/wcag-audit-patterns/examples/wcag_compliance_checklist.json +10 -0
- package/.agents/skills/wcag-audit-patterns/resources/implementation-playbook.md +541 -541
- package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +1 -1
- package/.agents/skills/web-vitals-and-performance-profiler/examples/cwv_budget_policy.json +6 -0
- package/.agents/templates/completion_report.md +57 -57
- package/.agents/workflows/analyze_requirement_document.md +18 -0
- package/.agents/workflows/export_testcases_excel.md +32 -0
- package/.agents/workflows/generate_combinatorial_test_data.md +28 -0
- package/.agents/workflows/generate_cross_module_test_plan.md +37 -0
- package/.agents/workflows/generate_manual_testcases_rbt.md +56 -0
- package/.agents/workflows/generate_requirements_from_website.md +25 -0
- package/.agents/workflows/generate_testcases_from_requirements.md +21 -0
- package/.agents/workflows/graphify.md +10 -10
- package/.agents/workflows/verify.md +2 -2
- package/.cursor/rules/language.mdc +7 -7
- package/.cursor/rules/synapse-qa.mdc +23 -0
- package/.cursorrules +7 -0
- package/.pre-commit-hooks.yaml +14 -0
- package/.windsurfrules +7 -0
- package/AGENTS.md +54 -15
- package/CLAUDE.md +35 -12
- package/LICENSE +30 -0
- package/NOTICE +180 -0
- package/README.en.md +325 -0
- package/README.md +247 -512
- package/action.yml +51 -0
- package/benchmarks/qa_eval/README.md +71 -0
- package/benchmarks/qa_eval/build_dataset.py +677 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-06_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-07_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-08_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-09_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-10_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-21_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-22_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-23_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-24_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-25_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-01_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-02_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-03_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-04_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-05_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-16_stale_and_mutation.json +22 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-17_stale_and_mutation.json +22 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-18_stale_and_mutation.json +22 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-19_stale_and_mutation.json +22 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-20_stale_and_mutation.json +23 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-11_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-12_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-13_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-14_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-15_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/eval_runner.py +418 -0
- package/benchmarks/qa_eval/reports/AB_METHODOLOGY_REPORT.md +83 -0
- package/benchmarks/qa_eval/reports/BENCHMARK_REPORT.md +69 -0
- package/benchmarks/qa_eval/reports/eval_results.json +105 -0
- package/bin/agent-qa.js +10 -3
- package/docs/ARCHITECTURE.md +107 -0
- package/docs/CI_CD_INTEGRATION.md +94 -0
- package/docs/CLI_REFERENCE.md +157 -0
- package/docs/CONTRIBUTING.md +91 -0
- package/docs/PLUGIN_DEVELOPMENT_GUIDE.md +126 -0
- package/docs/TOKEN_QUOTA_PLAYBOOK.md +41 -0
- package/docs/adopted-ecosystem-tools.md +87 -0
- package/docs/schemas/synapse.config.schema.json +64 -0
- package/examples/cypress-ecommerce-suite/README.md +45 -0
- package/examples/cypress-ecommerce-suite/cypress/e2e/checkout.cy.js +17 -0
- package/examples/cypress-ecommerce-suite/cypress/pages/BasePage.js +29 -0
- package/examples/cypress-ecommerce-suite/cypress/pages/CartPage.js +51 -0
- package/examples/cypress-ecommerce-suite/cypress.config.js +13 -0
- package/examples/cypress-ecommerce-suite/package.json +13 -0
- package/examples/katalon-suite/Keywords/pages/BasePage.groovy +19 -0
- package/examples/katalon-suite/Keywords/pages/DashboardPage.groovy +29 -0
- package/examples/katalon-suite/Keywords/pages/LoginPage.groovy +45 -0
- package/examples/katalon-suite/Profiles/default.glbl +12 -0
- package/examples/katalon-suite/README.md +18 -0
- package/examples/katalon-suite/Scripts/Auth/TC_LoginSuccess/Script1234567890.groovy +12 -0
- package/examples/katalon-suite/Scripts/Dashboard/TC_ViewSummary/Script1234567891.groovy +15 -0
- package/examples/katalon-suite/katalon_suite.prj +21 -0
- package/examples/playwright-crm-suite/README.md +40 -0
- package/examples/playwright-crm-suite/package.json +14 -0
- package/examples/playwright-crm-suite/pages/BasePage.ts +37 -0
- package/examples/playwright-crm-suite/pages/CustomerPage.ts +57 -0
- package/examples/playwright-crm-suite/pages/LoginPage.ts +32 -0
- package/examples/playwright-crm-suite/playwright.config.ts +27 -0
- package/examples/playwright-crm-suite/tests/auth.spec.ts +22 -0
- package/examples/playwright-crm-suite/tests/customers.spec.ts +33 -0
- package/examples/plugins/sample-custom-plugin/README.md +14 -0
- package/examples/plugins/sample-custom-plugin/plugin.json +16 -0
- package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/SKILL.md +29 -0
- package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/examples/audit_fixture.json +8 -0
- package/examples/plugins/sample-custom-plugin/tools/custom_security_headers.py +125 -0
- package/examples/practices/README.md +22 -0
- package/examples/practices/requirements/crm/requirements_contracts.md +292 -0
- package/examples/practices/requirements/crm/requirements_customers.md +462 -0
- package/examples/practices/requirements/crm/requirements_login.md +202 -0
- package/examples/practices/requirements/crm/requirements_projects.md +396 -0
- package/examples/practices/requirements/crm/requirements_tasks.md +343 -0
- package/examples/practices/requirements/ecommerce/requirements_category_brand.txt +402 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-1/2026-04-03_04-28-27.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-1/KAN-1_requirement.md +52 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-19.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-57.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-4/KAN-4_requirement.md +119 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-5_overview.md +9 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-08.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-23.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-6/KAN-6_requirement.md +32 -0
- package/examples/practices/testcases/api/book_management_api_test_cases.md +144 -0
- package/examples/practices/testcases/api/user_management_api_test_cases.md +139 -0
- package/examples/practices/testcases/crm/test_cases_crm_login.md +148 -0
- package/examples/practices/testcases/crm/test_cases_customers.md +156 -0
- package/examples/pytest-api-suite/README.md +45 -0
- package/examples/pytest-api-suite/conftest.py +31 -0
- package/examples/pytest-api-suite/pages/user_api_client.py +36 -0
- package/examples/pytest-api-suite/requirements.txt +2 -0
- package/examples/pytest-api-suite/tests/test_user_api.py +44 -0
- package/examples/selenium-suite/README.md +18 -0
- package/examples/selenium-suite/pages/base_page.py +27 -0
- package/examples/selenium-suite/pages/login_page.py +29 -0
- package/examples/selenium-suite/pages/order_page.py +25 -0
- package/examples/selenium-suite/pytest.ini +6 -0
- package/examples/selenium-suite/requirements.txt +2 -0
- package/examples/selenium-suite/tests/test_login.py +10 -0
- package/examples/selenium-suite/tests/test_order.py +10 -0
- package/package.json +22 -7
- package/scripts/generate_skill_examples.py +244 -0
- package/scripts/install.ps1 +112 -0
- package/scripts/install_global_plugin.ps1 +94 -0
- package/scripts/install_global_plugin.sh +49 -0
- package/scripts/lint_skills.ps1 +57 -0
- package/scripts/package_dist.py +323 -0
- package/scripts/setup.sh +59 -0
- package/scripts/update_hub.ps1 +19 -0
- package/scripts/update_hub.sh +15 -0
- package/skills_manifest.json +209 -6
- package/synapse.config.json +30 -0
- package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +7 -7
- package/templates/PROJECT_STATE_TEMPLATE.md +1 -1
- package/templates/ai_bridges/CLAUDE.md +33 -16
- package/templates/ai_bridges/copilot-instructions.md +14 -21
- package/templates/ai_bridges/cursor_synapse_qa.mdc +17 -20
- package/templates/ai_bridges/windsurfrules +21 -21
- package/templates/ci/github-actions-synapse.yml +71 -4
- package/templates/welcome.html.tmpl +1674 -0
- package/tools/.infer_baseline.json +1102 -0
- package/tools/TOOLS_RUNBOOK.md +92 -130
- package/tools/__init__.py +26 -0
- package/tools/agent_canary.py +501 -501
- package/tools/analyze_project.py +31 -17
- package/tools/api_fuzzer.py +278 -6
- package/tools/author_integrity.py +6 -6
- package/tools/auto_heal.py +1 -1
- package/tools/browser_helpers.js +105 -7
- package/tools/bug_reporter.py +31 -1
- package/tools/bump_version.py +23 -3
- package/tools/cache_resolver.py +248 -14
- package/tools/check_env.py +55 -14
- package/tools/check_version_sync.py +153 -0
- package/tools/ci_healing_hook.py +1 -1
- package/tools/cli.py +263 -13
- package/tools/count_tokens.py +1257 -0
- package/tools/dashboard_generator.py +2638 -0
- package/tools/db_sandbox.py +198 -20
- package/tools/e2e_flow_prober.ps1 +12 -64
- package/tools/e2e_flow_prober.py +99 -0
- package/tools/e2e_flow_prober.sh +4 -0
- package/tools/export_postman_collection.py +29 -7
- package/tools/flaky_detector.py +4 -3
- package/tools/generate_allure_report.ps1 +12 -29
- package/tools/generate_allure_report.py +77 -0
- package/tools/generate_allure_report.sh +4 -0
- package/tools/generate_jmx_plan.py +299 -68
- package/tools/generate_test_reports.ps1 +12 -134
- package/tools/generate_test_reports.py +133 -0
- package/tools/generate_test_reports.sh +4 -0
- package/tools/gherkin_suite.py +758 -0
- package/tools/git_pre_commit_guard.py +12 -6
- package/tools/goldfish_review.py +366 -0
- package/tools/heal_pr_automator.py +1 -1
- package/tools/heuristic_locator_matcher.py +170 -21
- package/tools/infer_linter.py +612 -70
- package/tools/jtl_analyzer.py +1 -1
- package/tools/lint_ai_docs.py +64 -0
- package/tools/matrix_sharder.py +1 -1
- package/tools/mcp_excel_server.ps1 +12 -250
- package/tools/mcp_excel_server.py +249 -0
- package/tools/mcp_excel_server.sh +4 -0
- package/tools/mcp_server.py +368 -144
- package/tools/md_to_xlsx.py +307 -0
- package/tools/micro_mutator.py +300 -69
- package/tools/omni_transpiler.py +2 -2
- package/tools/output_formatter.py +128 -0
- package/tools/pinpoint_failure.py +88 -88
- package/tools/project_state.py +2 -2
- package/tools/repo_map.py +1 -1
- package/tools/safety_firewall.py +197 -33
- package/tools/setup_project.py +294 -30
- package/tools/skillspector.py +8 -8
- package/tools/spec_drift_sentinel.py +439 -0
- package/tools/sync_agent_rules.py +248 -0
- package/tools/telemetry_correlator.py +45 -0
- package/tools/test_impact.py +51 -12
- package/tools/tms_sync_bridge.py +21 -5
- package/tools/universal_ir.py +3 -0
- package/tools/update_sentinel.py +87 -15
- package/tools/verify_all.ps1 +13 -0
- package/tools/verify_all.py +463 -79
- package/tools/visual_diff_canvas.py +1 -0
- package/tools/visual_som_helper.py +1 -1
- package/tools/watch_mode.py +305 -0
- package/tools/welcome_page.py +291 -0
|
@@ -46,7 +46,7 @@ if hasattr(sys.stdout, 'reconfigure'):
|
|
|
46
46
|
|
|
47
47
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
48
48
|
QA_HUB_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
|
49
|
-
DEFAULT_CACHE_DB = os.path.
|
|
49
|
+
DEFAULT_CACHE_DB = os.path.expanduser("~/.agent_memory/global_cache.db")
|
|
50
50
|
|
|
51
51
|
MARK_COLORS = [
|
|
52
52
|
"#FF0055", # Vibrant Red/Pink
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026 Thế Vị (@TeeVee998). All rights reserved.
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
# Project: Synapse-Hub (https://github.com/TeeVee998/Synapse-Hub)
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
tools/watch_mode.py — Incremental Watch Mode & Real-Time Test Engineering HUD
|
|
8
|
+
----------------------------------------------------------------------------
|
|
9
|
+
Continuously monitors target repository files for code changes and triggers
|
|
10
|
+
near-instant (< 100ms) incremental verification:
|
|
11
|
+
1. UTF-8 No BOM validation
|
|
12
|
+
2. Forbidden machine absolute path scan
|
|
13
|
+
3. AST Static Defect / Fake-Pass Sentinel (Meta Infer-style)
|
|
14
|
+
4. POM & Test Impact Blast Radius Scoping
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
python3 tools/watch_mode.py [target]
|
|
18
|
+
python3 tools/watch_mode.py [target] --debounce 0.8
|
|
19
|
+
python3 tools/watch_mode.py [target] --once
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
import time
|
|
25
|
+
import glob
|
|
26
|
+
import re
|
|
27
|
+
import argparse
|
|
28
|
+
from datetime import datetime
|
|
29
|
+
from typing import Dict, Set, List, Optional, Tuple, Any
|
|
30
|
+
|
|
31
|
+
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
32
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
33
|
+
try:
|
|
34
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
35
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
40
|
+
HUB_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
|
41
|
+
|
|
42
|
+
WATCH_EXTENSIONS = (
|
|
43
|
+
".groovy", ".tc", ".ts", ".js", ".py", ".glbl", ".json", ".feature", ".md"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
IGNORED_DIRS = {
|
|
47
|
+
".git", ".gradle", "node_modules", "allure-results", "Screenshots",
|
|
48
|
+
"Reports", "reports", "TestReports", "testreports", "bin", "Libs", ".settings",
|
|
49
|
+
"graphify-out", "__pycache__", "agent_qa.egg-info", "dist", "build", ".pytest_cache",
|
|
50
|
+
"claude-ecosystem-repos"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
IGNORED_FILES = {
|
|
54
|
+
".project", ".classpath", ".DS_Store", "memory_cache.db",
|
|
55
|
+
"verify_all.py", "git_pre_commit_guard.py", "agent_canary.py", "watch_mode.py",
|
|
56
|
+
"repo_map.txt", "repo_map.json", "PROJECT_HEALTH_REPORT.md", "PROJECT_STATE.md"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
FORBIDDEN_PATH_PATTERNS = [
|
|
60
|
+
(re.compile(r'file:///[a-zA-Z]:', re.IGNORECASE), "Machine file URI (file:///...)"),
|
|
61
|
+
(re.compile(r'\b[a-zA-Z]:\\Users\\[a-zA-Z0-9_\-]+\\', re.IGNORECASE), "Windows User directory path"),
|
|
62
|
+
(re.compile(r'\b/Users/[a-zA-Z0-9_\-]+/', re.IGNORECASE), "macOS/Unix User directory path"),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
# ANSI colors
|
|
66
|
+
RESET = "\033[0m"
|
|
67
|
+
BOLD = "\033[1m"
|
|
68
|
+
GREEN = "\033[92m"
|
|
69
|
+
RED = "\033[91m"
|
|
70
|
+
YELLOW = "\033[93m"
|
|
71
|
+
CYAN = "\033[96m"
|
|
72
|
+
MAGENTA = "\033[95m"
|
|
73
|
+
DIM = "\033[2m"
|
|
74
|
+
BG_BLUE = "\033[44m"
|
|
75
|
+
BG_DARK = "\033[100m"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def check_inactivity(last_activity_time: float, threshold_sec: float) -> Tuple[bool, float]:
|
|
79
|
+
"""
|
|
80
|
+
Checks if a monitored process or run has been silent longer than the threshold.
|
|
81
|
+
Inspired by agentblazo/blazo 'no_activity' detection rule.
|
|
82
|
+
"""
|
|
83
|
+
if threshold_sec <= 0:
|
|
84
|
+
return False, 0.0
|
|
85
|
+
idle = time.time() - last_activity_time
|
|
86
|
+
return idle > threshold_sec, idle
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def format_event_badge(badge_type: str, status: str = "INFO", duration_ms: Optional[float] = None) -> str:
|
|
90
|
+
"""
|
|
91
|
+
Formats compact, colorful event badges for the Live TUI HUD.
|
|
92
|
+
Inspired by agentblazo/blazo TUI event badges.
|
|
93
|
+
"""
|
|
94
|
+
dur_str = f" {duration_ms}ms" if duration_ms is not None else ""
|
|
95
|
+
if status == "PASS":
|
|
96
|
+
color = GREEN
|
|
97
|
+
elif status == "FAIL" or status == "ERR":
|
|
98
|
+
color = RED
|
|
99
|
+
elif status == "WARN":
|
|
100
|
+
color = YELLOW
|
|
101
|
+
else:
|
|
102
|
+
color = CYAN
|
|
103
|
+
|
|
104
|
+
return f"{color}[{badge_type.upper()}{dur_str}]{RESET}"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def check_file_bom(file_path: str) -> bool:
|
|
108
|
+
"""Returns True if file has UTF-8 BOM bytes."""
|
|
109
|
+
try:
|
|
110
|
+
with open(file_path, "rb") as fp:
|
|
111
|
+
return fp.read(3) == b"\xef\xbb\xbf"
|
|
112
|
+
except Exception:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def check_forbidden_paths(file_path: str) -> List[Tuple[int, str]]:
|
|
117
|
+
"""Returns list of (line_no, description) of forbidden absolute machine paths."""
|
|
118
|
+
fname = os.path.basename(file_path)
|
|
119
|
+
if fname in IGNORED_FILES:
|
|
120
|
+
return []
|
|
121
|
+
|
|
122
|
+
violations = []
|
|
123
|
+
try:
|
|
124
|
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as fp:
|
|
125
|
+
for line_no, line in enumerate(fp, 1):
|
|
126
|
+
for pattern, desc in FORBIDDEN_PATH_PATTERNS:
|
|
127
|
+
if pattern.search(line):
|
|
128
|
+
violations.append((line_no, desc))
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
return violations
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def run_incremental_linter(file_path: str) -> Tuple[bool, str]:
|
|
135
|
+
"""Runs fast AST check on modified script."""
|
|
136
|
+
infer_script = os.path.join(SCRIPT_DIR, "infer_linter.py")
|
|
137
|
+
if not os.path.exists(infer_script):
|
|
138
|
+
return True, "infer_linter not found"
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
import subprocess
|
|
142
|
+
res = subprocess.run(
|
|
143
|
+
[sys.executable, infer_script, file_path],
|
|
144
|
+
stdout=subprocess.PIPE,
|
|
145
|
+
stderr=subprocess.PIPE,
|
|
146
|
+
text=True
|
|
147
|
+
)
|
|
148
|
+
if res.returncode == 0:
|
|
149
|
+
return True, "Clean (0 AST Defects / 0 Fake Passes)"
|
|
150
|
+
else:
|
|
151
|
+
return False, res.stdout.strip() or res.stderr.strip()
|
|
152
|
+
except Exception as e:
|
|
153
|
+
return False, str(e)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def run_incremental_audit(file_path: str, target_dir: str) -> Dict[str, Any]:
|
|
157
|
+
"""Performs fast sub-100ms audit on a single changed file."""
|
|
158
|
+
start_t = time.perf_counter()
|
|
159
|
+
rel_path = os.path.relpath(file_path, target_dir)
|
|
160
|
+
results = {
|
|
161
|
+
"file": rel_path,
|
|
162
|
+
"bom_pass": True,
|
|
163
|
+
"path_pass": True,
|
|
164
|
+
"lint_pass": True,
|
|
165
|
+
"details": [],
|
|
166
|
+
"duration_ms": 0.0
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# 1. BOM Check
|
|
170
|
+
if check_file_bom(file_path):
|
|
171
|
+
results["bom_pass"] = False
|
|
172
|
+
results["details"].append("UTF-8 BOM detected (Must be 100% UTF-8 No BOM)")
|
|
173
|
+
|
|
174
|
+
# 2. Path Portability Check
|
|
175
|
+
path_issues = check_forbidden_paths(file_path)
|
|
176
|
+
if path_issues:
|
|
177
|
+
results["path_pass"] = False
|
|
178
|
+
for lno, desc in path_issues:
|
|
179
|
+
results["details"].append(f"Line {lno}: {desc}")
|
|
180
|
+
|
|
181
|
+
# 3. Incremental AST Linter for test automation scripts (e.g. Scripts/ in target projects)
|
|
182
|
+
# Skip python unittests in hub tests/ directory
|
|
183
|
+
norm_rel = rel_path.replace("\\", "/")
|
|
184
|
+
is_target_script = norm_rel.startswith("Scripts/") or ("/Scripts/" in norm_rel)
|
|
185
|
+
if is_target_script and file_path.endswith((".groovy", ".ts", ".js", ".py")):
|
|
186
|
+
lint_ok, lint_msg = run_incremental_linter(file_path)
|
|
187
|
+
if not lint_ok:
|
|
188
|
+
results["lint_pass"] = False
|
|
189
|
+
results["details"].append(f"AST Linter: {lint_msg}")
|
|
190
|
+
|
|
191
|
+
results["duration_ms"] = round((time.perf_counter() - start_t) * 1000, 2)
|
|
192
|
+
return results
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def print_hud_banner(target_dir: str):
|
|
196
|
+
print(f"\n{BOLD}{CYAN}======================================================================{RESET}")
|
|
197
|
+
print(f"{BOLD}{CYAN} ⚡ SYNAPSE-HUB REAL-TIME WATCH MODE SENTINEL (INCREMENTAL HUD) {RESET}")
|
|
198
|
+
print(f"{BOLD}{CYAN}======================================================================{RESET}")
|
|
199
|
+
print(f"{DIM}Watching directory : {RESET}{BOLD}{target_dir}{RESET}")
|
|
200
|
+
print(f"{DIM}Monitored types : {RESET}{CYAN}{', '.join(WATCH_EXTENSIONS)}{RESET}")
|
|
201
|
+
print(f"{DIM}Trigger checks : {RESET}UTF-8 No BOM, Absolute Paths, AST Fake-Pass Sentinel")
|
|
202
|
+
print(f"{DIM}Press {RESET}{BOLD}Ctrl+C{RESET}{DIM} to pause or stop watching.\n{RESET}")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def print_audit_result(res: Dict[str, Any], change_type: str = "MOD"):
|
|
206
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
207
|
+
rel_path = res["file"]
|
|
208
|
+
duration = res["duration_ms"]
|
|
209
|
+
all_clean = res["bom_pass"] and res["path_pass"] and res["lint_pass"]
|
|
210
|
+
|
|
211
|
+
badge = format_event_badge(change_type, "PASS" if all_clean else "FAIL", duration)
|
|
212
|
+
if all_clean:
|
|
213
|
+
print(f"[{timestamp}] {badge} {GREEN}✓ PASS{RESET} {BOLD}{rel_path}{RESET}")
|
|
214
|
+
else:
|
|
215
|
+
print(f"[{timestamp}] {badge} {RED}✗ FAIL{RESET} {BOLD}{rel_path}{RESET}")
|
|
216
|
+
for d in res["details"]:
|
|
217
|
+
print(f" {RED}↳ {d}{RESET}")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def scan_file_states(target_dir: str) -> Dict[str, float]:
|
|
221
|
+
"""Scans all eligible files and records their mtime."""
|
|
222
|
+
file_states = {}
|
|
223
|
+
for root, dirs, files in os.walk(target_dir):
|
|
224
|
+
dirs[:] = [d for d in dirs if d not in IGNORED_DIRS]
|
|
225
|
+
for f in files:
|
|
226
|
+
if f.endswith(WATCH_EXTENSIONS):
|
|
227
|
+
full_path = os.path.join(root, f)
|
|
228
|
+
try:
|
|
229
|
+
file_states[full_path] = os.path.getmtime(full_path)
|
|
230
|
+
except OSError:
|
|
231
|
+
pass
|
|
232
|
+
return file_states
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def watch_directory(target_dir: str, debounce: float = 0.8, run_once: bool = False, inactivity_timeout: float = 0.0) -> int:
|
|
236
|
+
"""Main watch loop with debouncing, live TUI event badges, and inactivity watchdog."""
|
|
237
|
+
target_dir = os.path.abspath(target_dir)
|
|
238
|
+
print_hud_banner(target_dir)
|
|
239
|
+
|
|
240
|
+
known_states = scan_file_states(target_dir)
|
|
241
|
+
print(f"{GREEN}● Watchdog active:{RESET} Indexed {len(known_states)} files. Ready for file changes...\n")
|
|
242
|
+
|
|
243
|
+
if run_once:
|
|
244
|
+
print(f"{CYAN}[ONCE MODE]{RESET} Executing one-time scan across {len(known_states)} indexed files...")
|
|
245
|
+
failures = 0
|
|
246
|
+
for fpath in sorted(known_states.keys()):
|
|
247
|
+
res = run_incremental_audit(fpath, target_dir)
|
|
248
|
+
if not (res["bom_pass"] and res["path_pass"] and res["lint_pass"]):
|
|
249
|
+
print_audit_result(res, change_type="AUDIT")
|
|
250
|
+
failures += 1
|
|
251
|
+
if failures == 0:
|
|
252
|
+
print(f"\n{GREEN}✅ [ALL PASS]{RESET} 100% of {len(known_states)} files passed incremental sentinel checks.")
|
|
253
|
+
return 0
|
|
254
|
+
else:
|
|
255
|
+
print(f"\n{RED}❌ [FAIL]{RESET} {failures} files violated Quality Gates.")
|
|
256
|
+
return 1
|
|
257
|
+
|
|
258
|
+
last_activity_time = time.time()
|
|
259
|
+
inactivity_alerted = False
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
while True:
|
|
263
|
+
time.sleep(debounce)
|
|
264
|
+
current_states = scan_file_states(target_dir)
|
|
265
|
+
changed_files = []
|
|
266
|
+
|
|
267
|
+
for fpath, mtime in current_states.items():
|
|
268
|
+
if fpath not in known_states:
|
|
269
|
+
changed_files.append((fpath, "CREATE"))
|
|
270
|
+
elif mtime > known_states[fpath]:
|
|
271
|
+
changed_files.append((fpath, "MODIFY"))
|
|
272
|
+
|
|
273
|
+
known_states = current_states
|
|
274
|
+
|
|
275
|
+
if changed_files:
|
|
276
|
+
last_activity_time = time.time()
|
|
277
|
+
inactivity_alerted = False
|
|
278
|
+
for fpath, change_type in changed_files:
|
|
279
|
+
res = run_incremental_audit(fpath, target_dir)
|
|
280
|
+
print_audit_result(res, change_type=change_type)
|
|
281
|
+
elif inactivity_timeout > 0:
|
|
282
|
+
is_stuck, idle_sec = check_inactivity(last_activity_time, inactivity_timeout)
|
|
283
|
+
if is_stuck and not inactivity_alerted:
|
|
284
|
+
inactivity_alerted = True
|
|
285
|
+
badge = format_event_badge("STUCK", "WARN")
|
|
286
|
+
print(f"\n{badge} {YELLOW}No activity for {round(idle_sec, 1)}s (Inactivity threshold: {inactivity_timeout}s){RESET}\n")
|
|
287
|
+
|
|
288
|
+
except KeyboardInterrupt:
|
|
289
|
+
print(f"\n\n{YELLOW}🛑 [WATCH STOPPED]{RESET} Exiting Watch Mode Sentinel.")
|
|
290
|
+
return 0
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def main():
|
|
294
|
+
parser = argparse.ArgumentParser(description="Real-Time Incremental Watch Mode Sentinel")
|
|
295
|
+
parser.add_argument("target", nargs="?", default=".", help="Directory to monitor (default: current directory)")
|
|
296
|
+
parser.add_argument("--debounce", type=float, default=0.8, help="Debounce interval in seconds (default: 0.8)")
|
|
297
|
+
parser.add_argument("--inactivity-timeout", type=float, default=0.0, help="Inactivity timeout in seconds to trigger stuck warning (default: 0 = disabled)")
|
|
298
|
+
parser.add_argument("--once", action="store_true", help="Run incremental scan once on all files and exit")
|
|
299
|
+
args = parser.parse_args()
|
|
300
|
+
|
|
301
|
+
return watch_directory(target_dir=args.target, debounce=args.debounce, run_once=args.once, inactivity_timeout=args.inactivity_timeout)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
if __name__ == "__main__":
|
|
305
|
+
sys.exit(main())
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026 Thế Vị (@TeeVee998). All rights reserved.
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
# Project: Synapse-Hub (https://github.com/TeeVee998/Synapse-Hub)
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
tools/welcome_page.py — Interactive Visual Landing Page for Synapse Hub
|
|
8
|
+
-------------------------------------------------------------------------
|
|
9
|
+
Generates and automatically launches a modern, responsive, 100% self-contained
|
|
10
|
+
HTML5 product landing page that introduces Synapse Hub's capabilities across
|
|
11
|
+
Manual, Automation, API, and Performance testing.
|
|
12
|
+
|
|
13
|
+
Features:
|
|
14
|
+
- Default Vietnamese interface with instant 1-click English toggle (bilingual i18n).
|
|
15
|
+
- 100% air-gapped & offline-ready (Zero external CDNs or remote dependencies).
|
|
16
|
+
- Dark glassmorphism interface with responsive layout.
|
|
17
|
+
- Interactive tabs covering the 4 Core Pillars of QA Intelligence.
|
|
18
|
+
- Interactive CLI Cheatsheet with 1-click clipboard copy.
|
|
19
|
+
- Comparison matrix vs existing industry tools (ESLint, Stryker, Allure, Healenium).
|
|
20
|
+
- 6 Subagent Swarm Profiles & 9 Quality Gates DoD showcase.
|
|
21
|
+
- Automatic browser launching on setup, pull, or CLI dispatch.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
import json
|
|
27
|
+
import html as html_lib
|
|
28
|
+
import argparse
|
|
29
|
+
import webbrowser
|
|
30
|
+
from datetime import datetime
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Dict, Any, Optional
|
|
33
|
+
|
|
34
|
+
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
35
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
36
|
+
try:
|
|
37
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
38
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
43
|
+
HUB_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
|
44
|
+
FIRST_RUN_MARKER_FILENAME = ".welcome_shown"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_hub_metadata() -> Dict[str, Any]:
|
|
48
|
+
"""Retrieves version and catalog count from skills_manifest.json."""
|
|
49
|
+
manifest_path = os.path.join(HUB_ROOT, "skills_manifest.json")
|
|
50
|
+
meta = {
|
|
51
|
+
"version": "2.4.0",
|
|
52
|
+
"totalSkills": 38,
|
|
53
|
+
"author": "Thế Vị (@TeeVee998)",
|
|
54
|
+
"repo": "https://github.com/TeeVee998/Synapse-Hub"
|
|
55
|
+
}
|
|
56
|
+
if os.path.exists(manifest_path):
|
|
57
|
+
try:
|
|
58
|
+
with open(manifest_path, "r", encoding="utf-8") as f:
|
|
59
|
+
data = json.load(f)
|
|
60
|
+
meta["version"] = data.get("version", meta["version"])
|
|
61
|
+
meta["totalSkills"] = data.get("totalSkills", meta["totalSkills"])
|
|
62
|
+
except (OSError, json.JSONDecodeError):
|
|
63
|
+
pass
|
|
64
|
+
return meta
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def is_hub_repo(path_str: Optional[str]) -> bool:
|
|
68
|
+
"""Checks whether the given path corresponds to the Synapse-Hub core repository itself."""
|
|
69
|
+
if not path_str:
|
|
70
|
+
return True
|
|
71
|
+
resolved = os.path.abspath(path_str)
|
|
72
|
+
if resolved.lower() == HUB_ROOT.lower():
|
|
73
|
+
return True
|
|
74
|
+
if os.path.exists(os.path.join(resolved, "skills_manifest.json")) or os.path.isdir(os.path.join(resolved, ".agents", "skills")):
|
|
75
|
+
return True
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def generate_welcome_html(target_repo: Optional[str] = None, stack_info: Optional[Dict[str, str]] = None, default_lang: str = "vi") -> str:
|
|
80
|
+
"""Renders the standalone interactive HTML5 landing page with bilingual language switcher."""
|
|
81
|
+
meta = get_hub_metadata()
|
|
82
|
+
now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
83
|
+
|
|
84
|
+
is_external_target = bool(target_repo and not is_hub_repo(target_repo) and stack_info)
|
|
85
|
+
if is_external_target:
|
|
86
|
+
raw_target_name = (stack_info.get("name") if stack_info else "") or os.path.basename(os.path.abspath(target_repo))
|
|
87
|
+
raw_framework = (stack_info.get("framework") if stack_info else "") or "Automated QA"
|
|
88
|
+
target_name = html_lib.escape(str(raw_target_name))
|
|
89
|
+
framework_name = html_lib.escape(str(raw_framework))
|
|
90
|
+
hero_pill_content = f"""<span data-i18n="hero_pill_tag">🛡️ Trung Tâm Trí Tuệ Chất Lượng AI Doanh Nghiệp</span>
|
|
91
|
+
<span>•</span>
|
|
92
|
+
<span><span data-i18n="hero_target_label">Dự án mục tiêu</span>: <strong>{target_name}</strong> ({framework_name})</span>"""
|
|
93
|
+
else:
|
|
94
|
+
hero_pill_content = """<span data-i18n="hero_pill_tag">🛡️ Trung Tâm Trí Tuệ Chất Lượng AI Doanh Nghiệp</span>
|
|
95
|
+
<span>•</span>
|
|
96
|
+
<span data-i18n="hero_pill_status">Sẵn Sàng 100% Offline</span>"""
|
|
97
|
+
|
|
98
|
+
tmpl_candidates = [
|
|
99
|
+
os.path.join(HUB_ROOT, "templates", "welcome.html.tmpl"),
|
|
100
|
+
os.path.join(SCRIPT_DIR, "..", "templates", "welcome.html.tmpl"),
|
|
101
|
+
os.path.join(SCRIPT_DIR, "templates", "welcome.html.tmpl"),
|
|
102
|
+
]
|
|
103
|
+
template_str = None
|
|
104
|
+
for cand in tmpl_candidates:
|
|
105
|
+
if os.path.exists(cand):
|
|
106
|
+
try:
|
|
107
|
+
with open(cand, "r", encoding="utf-8") as tf:
|
|
108
|
+
template_str = tf.read()
|
|
109
|
+
break
|
|
110
|
+
except OSError:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
if not template_str:
|
|
114
|
+
return f"""<!DOCTYPE html>
|
|
115
|
+
<html><head><meta charset="UTF-8"><title>Synapse Hub</title></head>
|
|
116
|
+
<body style="font-family:sans-serif;background:#0f172a;color:#f8fafc;padding:40px;text-align:center;">
|
|
117
|
+
<h1>🛡️ Synapse Hub Enterprise QA v{meta.get('version', '2.4.0')}</h1>
|
|
118
|
+
<p>{hero_pill_content}</p>
|
|
119
|
+
<p>Total Standardized Skills: {meta.get('totalSkills', 38)}</p>
|
|
120
|
+
<p>Author: {meta.get('author', 'Thế Vị (@TeeVee998)')}</p>
|
|
121
|
+
</body></html>"""
|
|
122
|
+
|
|
123
|
+
rendered_html = (
|
|
124
|
+
template_str.replace("__DEFAULT_LANG__", default_lang)
|
|
125
|
+
.replace("__HERO_PILL_CONTENT__", hero_pill_content)
|
|
126
|
+
.replace("__NOW_STR__", now_str)
|
|
127
|
+
.replace("__VERSION__", str(meta.get("version", "2.4.0")))
|
|
128
|
+
.replace("__AUTHOR__", str(meta.get("author", "Thế Vị (@TeeVee998)")))
|
|
129
|
+
.replace("__TOTAL_SKILLS__", str(meta.get("totalSkills", 38)))
|
|
130
|
+
.replace("__REPO__", str(meta.get("repo", "https://github.com/TeeVee998/Synapse-Hub")))
|
|
131
|
+
)
|
|
132
|
+
return rendered_html
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def write_welcome_page(output_path: Optional[str] = None, target_repo: Optional[str] = None, stack_info: Optional[Dict[str, str]] = None, default_lang: str = "vi") -> str:
|
|
136
|
+
"""Generates and writes the welcome landing page to disk."""
|
|
137
|
+
if not output_path:
|
|
138
|
+
base_dir = target_repo if target_repo else HUB_ROOT
|
|
139
|
+
reports_dir = os.path.join(base_dir, "reports")
|
|
140
|
+
os.makedirs(reports_dir, exist_ok=True)
|
|
141
|
+
output_path = os.path.join(reports_dir, "WELCOME.html")
|
|
142
|
+
else:
|
|
143
|
+
output_path = os.path.abspath(output_path)
|
|
144
|
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
145
|
+
|
|
146
|
+
html_content = generate_welcome_html(target_repo=target_repo, stack_info=stack_info, default_lang=default_lang)
|
|
147
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
148
|
+
f.write(html_content)
|
|
149
|
+
|
|
150
|
+
return output_path
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_marker_path(target_repo: Optional[str] = None) -> str:
|
|
154
|
+
"""Returns the path to the .welcome_shown marker file for the given repository."""
|
|
155
|
+
base = target_repo if target_repo else HUB_ROOT
|
|
156
|
+
return os.path.join(base, "reports", FIRST_RUN_MARKER_FILENAME)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def is_first_run(target_repo: Optional[str] = None) -> bool:
|
|
160
|
+
"""
|
|
161
|
+
Checks whether this is the first install / first pull for the hub.
|
|
162
|
+
Returns True if the welcome page has not been displayed yet.
|
|
163
|
+
"""
|
|
164
|
+
marker_path = get_marker_path(target_repo)
|
|
165
|
+
if os.path.exists(marker_path):
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
global_dir = os.path.expanduser("~/.agent_memory")
|
|
169
|
+
global_marker = os.path.join(global_dir, "welcome_shown.json")
|
|
170
|
+
if os.path.exists(global_marker):
|
|
171
|
+
try:
|
|
172
|
+
with open(global_marker, "r", encoding="utf-8") as f:
|
|
173
|
+
data = json.load(f)
|
|
174
|
+
key = os.path.realpath(target_repo or HUB_ROOT)
|
|
175
|
+
if key in data:
|
|
176
|
+
return False
|
|
177
|
+
except (OSError, json.JSONDecodeError):
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def mark_first_run_completed(target_repo: Optional[str] = None):
|
|
184
|
+
"""
|
|
185
|
+
Records that the landing page has been shown so it does not open again.
|
|
186
|
+
"""
|
|
187
|
+
marker_path = get_marker_path(target_repo)
|
|
188
|
+
try:
|
|
189
|
+
os.makedirs(os.path.dirname(marker_path), exist_ok=True)
|
|
190
|
+
with open(marker_path, "w", encoding="utf-8") as f:
|
|
191
|
+
json.dump({
|
|
192
|
+
"shown_at": datetime.now().isoformat(),
|
|
193
|
+
"repo": os.path.realpath(target_repo or HUB_ROOT)
|
|
194
|
+
}, f, indent=2)
|
|
195
|
+
except OSError:
|
|
196
|
+
pass
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
global_dir = os.path.expanduser("~/.agent_memory")
|
|
200
|
+
os.makedirs(global_dir, exist_ok=True)
|
|
201
|
+
global_marker = os.path.join(global_dir, "welcome_shown.json")
|
|
202
|
+
data = {}
|
|
203
|
+
if os.path.exists(global_marker):
|
|
204
|
+
try:
|
|
205
|
+
with open(global_marker, "r", encoding="utf-8") as f:
|
|
206
|
+
data = json.load(f)
|
|
207
|
+
except (OSError, json.JSONDecodeError):
|
|
208
|
+
data = {}
|
|
209
|
+
key = os.path.realpath(target_repo or HUB_ROOT)
|
|
210
|
+
data[key] = datetime.now().isoformat()
|
|
211
|
+
with open(global_marker, "w", encoding="utf-8") as f:
|
|
212
|
+
json.dump(data, f, indent=2)
|
|
213
|
+
except OSError:
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def open_welcome_page(file_path: str) -> bool:
|
|
218
|
+
"""
|
|
219
|
+
Opens the welcome page in the user's default browser if not running in headless/CI mode or test runner.
|
|
220
|
+
Returns True if successfully launched, False otherwise.
|
|
221
|
+
"""
|
|
222
|
+
if (
|
|
223
|
+
os.environ.get("CI")
|
|
224
|
+
or os.environ.get("HEADLESS")
|
|
225
|
+
or os.environ.get("AGENT_QA_NO_BROWSER")
|
|
226
|
+
or "unittest" in sys.modules
|
|
227
|
+
or "pytest" in sys.modules
|
|
228
|
+
):
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
abs_path = os.path.abspath(file_path)
|
|
232
|
+
if not os.path.exists(abs_path):
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
file_url = f"file://{abs_path}"
|
|
236
|
+
try:
|
|
237
|
+
webbrowser.open(file_url)
|
|
238
|
+
return True
|
|
239
|
+
except Exception:
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def generate_welcome_page(target_repo: Optional[str] = None, output_path: Optional[str] = None, stack_info: Optional[Dict[str, str]] = None, open_browser: bool = True, default_lang: str = "vi") -> str:
|
|
244
|
+
"""
|
|
245
|
+
Full workflow helper: generates the landing page and opens it if requested.
|
|
246
|
+
"""
|
|
247
|
+
out_file = write_welcome_page(output_path=output_path, target_repo=target_repo, stack_info=stack_info, default_lang=default_lang)
|
|
248
|
+
if open_browser:
|
|
249
|
+
opened = open_welcome_page(out_file)
|
|
250
|
+
if opened:
|
|
251
|
+
print(f" 🚀 Đã mở Visual Landing Page trong trình duyệt của bạn.")
|
|
252
|
+
return out_file
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def main():
|
|
256
|
+
parser = argparse.ArgumentParser(description="Synapse Hub Visual Landing Page Generator (Bilingual VI/EN)")
|
|
257
|
+
parser.add_argument("target", nargs="?", default=None, help="Target repository directory (default: current directory)")
|
|
258
|
+
parser.add_argument("--output", "-o", default=None, help="Output HTML file path (default: reports/WELCOME.html)")
|
|
259
|
+
parser.add_argument("--lang", choices=["vi", "en"], default="vi", help="Default initial language (default: vi = Vietnamese)")
|
|
260
|
+
parser.add_argument("--no-browser", action="store_true", help="Do not open the browser after generating the page")
|
|
261
|
+
parser.add_argument("--open", action="store_true", help="Explicitly open the browser")
|
|
262
|
+
parser.add_argument("--first-run", action="store_true", help="Only generate and open if running for the first time (e.g. npm install or first pull)")
|
|
263
|
+
args = parser.parse_args()
|
|
264
|
+
|
|
265
|
+
target_dir = os.path.abspath(args.target) if args.target else HUB_ROOT
|
|
266
|
+
|
|
267
|
+
# If --first-run requested, check if it was already displayed
|
|
268
|
+
if args.first_run:
|
|
269
|
+
if not is_first_run(target_dir):
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
should_open = not args.no_browser if (not os.environ.get("CI")) else args.open
|
|
273
|
+
if args.first_run and (os.environ.get("CI") or os.environ.get("HEADLESS") or "unittest" in sys.modules or "pytest" in sys.modules):
|
|
274
|
+
should_open = False
|
|
275
|
+
|
|
276
|
+
print(f"🎨 Generating Synapse Hub Visual Landing Page (Language: {args.lang.upper()})...")
|
|
277
|
+
out_path = generate_welcome_page(
|
|
278
|
+
target_repo=target_dir,
|
|
279
|
+
output_path=args.output,
|
|
280
|
+
open_browser=should_open,
|
|
281
|
+
default_lang=args.lang
|
|
282
|
+
)
|
|
283
|
+
if args.first_run or should_open:
|
|
284
|
+
mark_first_run_completed(target_dir)
|
|
285
|
+
|
|
286
|
+
print(f"✅ Landing page generated successfully at:\n file://{out_path}")
|
|
287
|
+
print(f"👉 You can re-open it anytime with: npx synapse-hub welcome")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
if __name__ == "__main__":
|
|
291
|
+
main()
|