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.
Files changed (317) hide show
  1. package/.agents/agents/failure-analyst.md +11 -0
  2. package/.agents/agents/live-explorer.md +28 -0
  3. package/.agents/agents/orchestrator.md +20 -4
  4. package/.agents/agents/pom-crafter.md +12 -0
  5. package/.agents/agents/quality-gatekeeper.md +19 -4
  6. package/.agents/agents/test-runner.md +11 -0
  7. package/.agents/rules/communication-style.md +40 -0
  8. package/.agents/rules/context-engineering.md +23 -0
  9. package/.agents/rules/git-push-protocol.md +11 -17
  10. package/.agents/rules/graphify.md +14 -14
  11. package/.agents/rules/lessons-learned.md +3 -3
  12. package/.agents/rules/mandatory-playwright-live.md +29 -29
  13. package/.agents/rules/mobile_appium_rules.md +132 -0
  14. package/.agents/rules/performance_testing_rules.md +74 -0
  15. package/.agents/rules/skill-precedence.md +1 -1
  16. package/.agents/rules/systematic-debugging.md +6 -6
  17. package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +64 -215
  18. package/.agents/skills/agent-orchestration-multi-agent-optimize/examples/swarm_workload_config.json +12 -0
  19. package/.agents/skills/antigravity-test-sentinel/SKILL.md +3 -3
  20. package/.agents/skills/antigravity-test-sentinel/examples/dod_gate_policy.json +11 -0
  21. package/.agents/skills/api-network-mocking-interception/SKILL.md +1 -1
  22. package/.agents/skills/api-network-mocking-interception/examples/network_route_stub.json +9 -0
  23. package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +9 -9
  24. package/.agents/skills/api-spec-to-e2e-matrix/examples/sample_openapi.json +19 -0
  25. package/.agents/skills/architecture-decision-records/SKILL.md +35 -412
  26. package/.agents/skills/architecture-decision-records/examples/0001-record-architecture-decisions.md +12 -0
  27. package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +4 -1
  28. package/.agents/skills/auth-session-and-cookie-manager/examples/storage_state_sample.json +11 -0
  29. package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +80 -186
  30. package/.agents/skills/backend-load-and-stress-patterns/examples/k6_load_test_sample.js +13 -0
  31. package/.agents/skills/bats-testing-patterns/SKILL.md +1 -1
  32. package/.agents/skills/bats-testing-patterns/examples/cli_runner_test.bats +6 -0
  33. package/.agents/skills/bats-testing-patterns/resources/implementation-playbook.md +614 -614
  34. package/.agents/skills/clean-pom-crafter/SKILL.md +21 -3
  35. package/.agents/skills/clean-pom-crafter/examples/standard_test_case.groovy +28 -28
  36. package/.agents/skills/clean-pom-crafter/references/pom_patterns.groovy +138 -138
  37. package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +2 -2
  38. package/.agents/skills/code-refactoring-refactor-clean/examples/refactored_page_sample.groovy +9 -0
  39. package/.agents/skills/code-refactoring-refactor-clean/resources/implementation-playbook.md +9 -7
  40. package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +5 -5
  41. package/.agents/skills/db-state-verifier-and-seeder/examples/db_fixture_seed.json +12 -0
  42. package/.agents/skills/e2e-testing-patterns/SKILL.md +1 -1
  43. package/.agents/skills/e2e-testing-patterns/examples/playwright_sharded_config.ts +12 -0
  44. package/.agents/skills/e2e-testing-patterns/resources/implementation-playbook.md +522 -522
  45. package/.agents/skills/failure-autopsy-and-healing/SKILL.md +17 -3
  46. package/.agents/skills/failure-autopsy-and-healing/examples/sample_junit_failure.xml +8 -0
  47. package/.agents/skills/failure-autopsy-and-healing/references/healing_matrix.md +64 -64
  48. package/.agents/skills/failure-autopsy-and-healing/references/qa_investigation_protocol.md +56 -56
  49. package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +98 -98
  50. package/.agents/skills/file-and-report-validator/SKILL.md +1 -1
  51. package/.agents/skills/file-and-report-validator/examples/sample_export_schema.json +14 -0
  52. package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +3 -3
  53. package/.agents/skills/flaky-test-sentinel-and-quarantine/examples/quarantine_registry.json +11 -0
  54. package/.agents/skills/gherkin-to-suite/SKILL.md +103 -0
  55. package/.agents/skills/gherkin-to-suite/examples/checkout.feature +39 -0
  56. package/.agents/skills/gherkin-to-suite/examples/suite_report_sample.json +334 -0
  57. package/.agents/skills/github-actions-templates/SKILL.md +47 -304
  58. package/.agents/skills/github-actions-templates/examples/matrix_test_pipeline.yml +8 -0
  59. package/.agents/skills/graphify-impact-analyzer/SKILL.md +6 -6
  60. package/.agents/skills/graphify-impact-analyzer/examples/dependency_blast_radius.json +8 -0
  61. package/.agents/skills/katalon-cli-runner/SKILL.md +2 -2
  62. package/.agents/skills/katalon-cli-runner/examples/headless_kre_exec.sh +3 -0
  63. package/.agents/skills/katalon-cli-runner/references/groovy-patterns.md +209 -209
  64. package/.agents/skills/katalon-cli-runner/references/katalon-api.md +155 -155
  65. package/.agents/skills/katalon-cli-runner/references/runner_commands.md +67 -67
  66. package/.agents/skills/lam-script-generator/SKILL.md +42 -1
  67. package/.agents/skills/lam-script-generator/examples/user_story_sample.md +8 -0
  68. package/.agents/skills/lean-code-ladder/SKILL.md +83 -0
  69. package/.agents/skills/lean-code-ladder/examples/ladder_walkthrough.md +37 -0
  70. package/.agents/skills/meta-prompt-crafter/SKILL.md +107 -0
  71. package/.agents/skills/meta-prompt-crafter/examples/qa_prompt_template.md +35 -0
  72. package/.agents/skills/persistent-planning-files/SKILL.md +108 -0
  73. package/.agents/skills/persistent-planning-files/examples/task_plan_example.md +56 -0
  74. package/.agents/skills/playwright-live-interaction/SKILL.md +49 -1
  75. package/.agents/skills/playwright-live-interaction/examples/live_probe_script.js +5 -0
  76. package/.agents/skills/playwright-live-interaction/references/advanced_superpowers.md +160 -160
  77. package/.agents/skills/playwright-live-interaction/references/compact_accessibility_snapshots.md +75 -75
  78. package/.agents/skills/playwright-live-interaction/references/recipes.md +388 -388
  79. package/.agents/skills/playwright-live-interaction/references/swarm_orchestration.md +134 -134
  80. package/.agents/skills/playwright-webapp-testing/SKILL.md +93 -0
  81. package/.agents/skills/playwright-webapp-testing/examples/server_lifecycle_playwright.py +63 -0
  82. package/.agents/skills/rbt-manual-testing/SKILL.md +150 -0
  83. package/.agents/skills/rbt-manual-testing/examples/checkout_rbt_manual_suite.md +25 -0
  84. package/.agents/skills/security-dast-sentinel/SKILL.md +57 -135
  85. package/.agents/skills/security-dast-sentinel/examples/dast_scan_config.json +10 -0
  86. package/.agents/skills/self-healing-locator/SKILL.md +24 -10
  87. package/.agents/skills/self-healing-locator/examples/aom_diff_sample.json +16 -0
  88. package/.agents/skills/skillspector-security-guard/SKILL.md +32 -90
  89. package/.agents/skills/skillspector-security-guard/examples/safe_mcp_config.json +12 -0
  90. package/.agents/skills/surgical-coding-principles/SKILL.md +89 -0
  91. package/.agents/skills/surgical-coding-principles/examples/surgical_vs_scope_creep.md +46 -0
  92. package/.agents/skills/synthetic-test-data-generator/SKILL.md +3 -3
  93. package/.agents/skills/synthetic-test-data-generator/examples/idempotent_data_recipe.json +9 -0
  94. package/.agents/skills/systematic-debugging/SKILL.md +100 -0
  95. package/.agents/skills/systematic-debugging/examples/four_phase_case.md +44 -0
  96. package/.agents/skills/test-driven-development/SKILL.md +114 -0
  97. package/.agents/skills/test-driven-development/examples/red_green_refactor_qa.md +78 -0
  98. package/.agents/skills/token-and-context-optimizer/SKILL.md +1 -1
  99. package/.agents/skills/token-and-context-optimizer/examples/distilled_log_sample.txt +3 -0
  100. package/.agents/skills/verification-before-completion/SKILL.md +100 -0
  101. package/.agents/skills/verification-before-completion/examples/evidence_log_example.md +46 -0
  102. package/.agents/skills/visual-regression-sentinel/SKILL.md +21 -1
  103. package/.agents/skills/visual-regression-sentinel/examples/visual_mask_rules.json +9 -0
  104. package/.agents/skills/wcag-audit-patterns/SKILL.md +3 -3
  105. package/.agents/skills/wcag-audit-patterns/examples/wcag_compliance_checklist.json +10 -0
  106. package/.agents/skills/wcag-audit-patterns/resources/implementation-playbook.md +541 -541
  107. package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +1 -1
  108. package/.agents/skills/web-vitals-and-performance-profiler/examples/cwv_budget_policy.json +6 -0
  109. package/.agents/templates/completion_report.md +57 -57
  110. package/.agents/workflows/analyze_requirement_document.md +18 -0
  111. package/.agents/workflows/export_testcases_excel.md +32 -0
  112. package/.agents/workflows/generate_combinatorial_test_data.md +28 -0
  113. package/.agents/workflows/generate_cross_module_test_plan.md +37 -0
  114. package/.agents/workflows/generate_manual_testcases_rbt.md +56 -0
  115. package/.agents/workflows/generate_requirements_from_website.md +25 -0
  116. package/.agents/workflows/generate_testcases_from_requirements.md +21 -0
  117. package/.agents/workflows/graphify.md +10 -10
  118. package/.agents/workflows/verify.md +2 -2
  119. package/.cursor/rules/language.mdc +7 -7
  120. package/.cursor/rules/synapse-qa.mdc +23 -0
  121. package/.cursorrules +7 -0
  122. package/.pre-commit-hooks.yaml +14 -0
  123. package/.windsurfrules +7 -0
  124. package/AGENTS.md +54 -15
  125. package/CLAUDE.md +35 -12
  126. package/LICENSE +30 -0
  127. package/NOTICE +180 -0
  128. package/README.en.md +325 -0
  129. package/README.md +247 -512
  130. package/action.yml +51 -0
  131. package/benchmarks/qa_eval/README.md +71 -0
  132. package/benchmarks/qa_eval/build_dataset.py +677 -0
  133. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-06_api_contract_drift.json +22 -0
  134. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-07_api_contract_drift.json +22 -0
  135. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-08_api_contract_drift.json +22 -0
  136. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-09_api_contract_drift.json +22 -0
  137. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-10_api_contract_drift.json +22 -0
  138. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-21_fake_pass_tautology.json +21 -0
  139. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-22_fake_pass_tautology.json +21 -0
  140. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-23_fake_pass_tautology.json +21 -0
  141. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-24_fake_pass_tautology.json +21 -0
  142. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-25_fake_pass_tautology.json +21 -0
  143. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-01_locator_drift.json +23 -0
  144. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-02_locator_drift.json +23 -0
  145. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-03_locator_drift.json +23 -0
  146. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-04_locator_drift.json +23 -0
  147. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-05_locator_drift.json +23 -0
  148. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-16_stale_and_mutation.json +22 -0
  149. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-17_stale_and_mutation.json +22 -0
  150. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-18_stale_and_mutation.json +22 -0
  151. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-19_stale_and_mutation.json +22 -0
  152. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-20_stale_and_mutation.json +23 -0
  153. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-11_timing_and_race.json +22 -0
  154. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-12_timing_and_race.json +22 -0
  155. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-13_timing_and_race.json +22 -0
  156. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-14_timing_and_race.json +22 -0
  157. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-15_timing_and_race.json +22 -0
  158. package/benchmarks/qa_eval/eval_runner.py +418 -0
  159. package/benchmarks/qa_eval/reports/AB_METHODOLOGY_REPORT.md +83 -0
  160. package/benchmarks/qa_eval/reports/BENCHMARK_REPORT.md +69 -0
  161. package/benchmarks/qa_eval/reports/eval_results.json +105 -0
  162. package/bin/agent-qa.js +10 -3
  163. package/docs/ARCHITECTURE.md +107 -0
  164. package/docs/CI_CD_INTEGRATION.md +94 -0
  165. package/docs/CLI_REFERENCE.md +157 -0
  166. package/docs/CONTRIBUTING.md +91 -0
  167. package/docs/PLUGIN_DEVELOPMENT_GUIDE.md +126 -0
  168. package/docs/TOKEN_QUOTA_PLAYBOOK.md +41 -0
  169. package/docs/adopted-ecosystem-tools.md +87 -0
  170. package/docs/schemas/synapse.config.schema.json +64 -0
  171. package/examples/cypress-ecommerce-suite/README.md +45 -0
  172. package/examples/cypress-ecommerce-suite/cypress/e2e/checkout.cy.js +17 -0
  173. package/examples/cypress-ecommerce-suite/cypress/pages/BasePage.js +29 -0
  174. package/examples/cypress-ecommerce-suite/cypress/pages/CartPage.js +51 -0
  175. package/examples/cypress-ecommerce-suite/cypress.config.js +13 -0
  176. package/examples/cypress-ecommerce-suite/package.json +13 -0
  177. package/examples/katalon-suite/Keywords/pages/BasePage.groovy +19 -0
  178. package/examples/katalon-suite/Keywords/pages/DashboardPage.groovy +29 -0
  179. package/examples/katalon-suite/Keywords/pages/LoginPage.groovy +45 -0
  180. package/examples/katalon-suite/Profiles/default.glbl +12 -0
  181. package/examples/katalon-suite/README.md +18 -0
  182. package/examples/katalon-suite/Scripts/Auth/TC_LoginSuccess/Script1234567890.groovy +12 -0
  183. package/examples/katalon-suite/Scripts/Dashboard/TC_ViewSummary/Script1234567891.groovy +15 -0
  184. package/examples/katalon-suite/katalon_suite.prj +21 -0
  185. package/examples/playwright-crm-suite/README.md +40 -0
  186. package/examples/playwright-crm-suite/package.json +14 -0
  187. package/examples/playwright-crm-suite/pages/BasePage.ts +37 -0
  188. package/examples/playwright-crm-suite/pages/CustomerPage.ts +57 -0
  189. package/examples/playwright-crm-suite/pages/LoginPage.ts +32 -0
  190. package/examples/playwright-crm-suite/playwright.config.ts +27 -0
  191. package/examples/playwright-crm-suite/tests/auth.spec.ts +22 -0
  192. package/examples/playwright-crm-suite/tests/customers.spec.ts +33 -0
  193. package/examples/plugins/sample-custom-plugin/README.md +14 -0
  194. package/examples/plugins/sample-custom-plugin/plugin.json +16 -0
  195. package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/SKILL.md +29 -0
  196. package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/examples/audit_fixture.json +8 -0
  197. package/examples/plugins/sample-custom-plugin/tools/custom_security_headers.py +125 -0
  198. package/examples/practices/README.md +22 -0
  199. package/examples/practices/requirements/crm/requirements_contracts.md +292 -0
  200. package/examples/practices/requirements/crm/requirements_customers.md +462 -0
  201. package/examples/practices/requirements/crm/requirements_login.md +202 -0
  202. package/examples/practices/requirements/crm/requirements_projects.md +396 -0
  203. package/examples/practices/requirements/crm/requirements_tasks.md +343 -0
  204. package/examples/practices/requirements/ecommerce/requirements_category_brand.txt +402 -0
  205. package/examples/practices/requirements/jira/KAN-5/KAN-1/2026-04-03_04-28-27.png +0 -0
  206. package/examples/practices/requirements/jira/KAN-5/KAN-1/KAN-1_requirement.md +52 -0
  207. package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-19.png +0 -0
  208. package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-57.png +0 -0
  209. package/examples/practices/requirements/jira/KAN-5/KAN-4/KAN-4_requirement.md +119 -0
  210. package/examples/practices/requirements/jira/KAN-5/KAN-5_overview.md +9 -0
  211. package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-08.png +0 -0
  212. package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-23.png +0 -0
  213. package/examples/practices/requirements/jira/KAN-5/KAN-6/KAN-6_requirement.md +32 -0
  214. package/examples/practices/testcases/api/book_management_api_test_cases.md +144 -0
  215. package/examples/practices/testcases/api/user_management_api_test_cases.md +139 -0
  216. package/examples/practices/testcases/crm/test_cases_crm_login.md +148 -0
  217. package/examples/practices/testcases/crm/test_cases_customers.md +156 -0
  218. package/examples/pytest-api-suite/README.md +45 -0
  219. package/examples/pytest-api-suite/conftest.py +31 -0
  220. package/examples/pytest-api-suite/pages/user_api_client.py +36 -0
  221. package/examples/pytest-api-suite/requirements.txt +2 -0
  222. package/examples/pytest-api-suite/tests/test_user_api.py +44 -0
  223. package/examples/selenium-suite/README.md +18 -0
  224. package/examples/selenium-suite/pages/base_page.py +27 -0
  225. package/examples/selenium-suite/pages/login_page.py +29 -0
  226. package/examples/selenium-suite/pages/order_page.py +25 -0
  227. package/examples/selenium-suite/pytest.ini +6 -0
  228. package/examples/selenium-suite/requirements.txt +2 -0
  229. package/examples/selenium-suite/tests/test_login.py +10 -0
  230. package/examples/selenium-suite/tests/test_order.py +10 -0
  231. package/package.json +22 -7
  232. package/scripts/generate_skill_examples.py +244 -0
  233. package/scripts/install.ps1 +112 -0
  234. package/scripts/install_global_plugin.ps1 +94 -0
  235. package/scripts/install_global_plugin.sh +49 -0
  236. package/scripts/lint_skills.ps1 +57 -0
  237. package/scripts/package_dist.py +323 -0
  238. package/scripts/setup.sh +59 -0
  239. package/scripts/update_hub.ps1 +19 -0
  240. package/scripts/update_hub.sh +15 -0
  241. package/skills_manifest.json +209 -6
  242. package/synapse.config.json +30 -0
  243. package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +7 -7
  244. package/templates/PROJECT_STATE_TEMPLATE.md +1 -1
  245. package/templates/ai_bridges/CLAUDE.md +33 -16
  246. package/templates/ai_bridges/copilot-instructions.md +14 -21
  247. package/templates/ai_bridges/cursor_synapse_qa.mdc +17 -20
  248. package/templates/ai_bridges/windsurfrules +21 -21
  249. package/templates/ci/github-actions-synapse.yml +71 -4
  250. package/templates/welcome.html.tmpl +1674 -0
  251. package/tools/.infer_baseline.json +1102 -0
  252. package/tools/TOOLS_RUNBOOK.md +92 -130
  253. package/tools/__init__.py +26 -0
  254. package/tools/agent_canary.py +501 -501
  255. package/tools/analyze_project.py +31 -17
  256. package/tools/api_fuzzer.py +278 -6
  257. package/tools/author_integrity.py +6 -6
  258. package/tools/auto_heal.py +1 -1
  259. package/tools/browser_helpers.js +105 -7
  260. package/tools/bug_reporter.py +31 -1
  261. package/tools/bump_version.py +23 -3
  262. package/tools/cache_resolver.py +248 -14
  263. package/tools/check_env.py +55 -14
  264. package/tools/check_version_sync.py +153 -0
  265. package/tools/ci_healing_hook.py +1 -1
  266. package/tools/cli.py +263 -13
  267. package/tools/count_tokens.py +1257 -0
  268. package/tools/dashboard_generator.py +2638 -0
  269. package/tools/db_sandbox.py +198 -20
  270. package/tools/e2e_flow_prober.ps1 +12 -64
  271. package/tools/e2e_flow_prober.py +99 -0
  272. package/tools/e2e_flow_prober.sh +4 -0
  273. package/tools/export_postman_collection.py +29 -7
  274. package/tools/flaky_detector.py +4 -3
  275. package/tools/generate_allure_report.ps1 +12 -29
  276. package/tools/generate_allure_report.py +77 -0
  277. package/tools/generate_allure_report.sh +4 -0
  278. package/tools/generate_jmx_plan.py +299 -68
  279. package/tools/generate_test_reports.ps1 +12 -134
  280. package/tools/generate_test_reports.py +133 -0
  281. package/tools/generate_test_reports.sh +4 -0
  282. package/tools/gherkin_suite.py +758 -0
  283. package/tools/git_pre_commit_guard.py +12 -6
  284. package/tools/goldfish_review.py +366 -0
  285. package/tools/heal_pr_automator.py +1 -1
  286. package/tools/heuristic_locator_matcher.py +170 -21
  287. package/tools/infer_linter.py +612 -70
  288. package/tools/jtl_analyzer.py +1 -1
  289. package/tools/lint_ai_docs.py +64 -0
  290. package/tools/matrix_sharder.py +1 -1
  291. package/tools/mcp_excel_server.ps1 +12 -250
  292. package/tools/mcp_excel_server.py +249 -0
  293. package/tools/mcp_excel_server.sh +4 -0
  294. package/tools/mcp_server.py +368 -144
  295. package/tools/md_to_xlsx.py +307 -0
  296. package/tools/micro_mutator.py +300 -69
  297. package/tools/omni_transpiler.py +2 -2
  298. package/tools/output_formatter.py +128 -0
  299. package/tools/pinpoint_failure.py +88 -88
  300. package/tools/project_state.py +2 -2
  301. package/tools/repo_map.py +1 -1
  302. package/tools/safety_firewall.py +197 -33
  303. package/tools/setup_project.py +294 -30
  304. package/tools/skillspector.py +8 -8
  305. package/tools/spec_drift_sentinel.py +439 -0
  306. package/tools/sync_agent_rules.py +248 -0
  307. package/tools/telemetry_correlator.py +45 -0
  308. package/tools/test_impact.py +51 -12
  309. package/tools/tms_sync_bridge.py +21 -5
  310. package/tools/universal_ir.py +3 -0
  311. package/tools/update_sentinel.py +87 -15
  312. package/tools/verify_all.ps1 +13 -0
  313. package/tools/verify_all.py +463 -79
  314. package/tools/visual_diff_canvas.py +1 -0
  315. package/tools/visual_som_helper.py +1 -1
  316. package/tools/watch_mode.py +305 -0
  317. package/tools/welcome_page.py +291 -0
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
+ from __future__ import annotations
3
4
  """
4
5
  Enterprise Canvas Visual Diff & Regression Checker
5
6
  ===================================================
@@ -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.join(SCRIPT_DIR, "memory_cache.db")
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>&bull;</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>&bull;</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()