@enterpriseai/cli 3.19.0 → 3.19.2
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/package.json +3 -3
- package/resources/deploy/eai-app-template/producer-pin.json +5 -5
- package/resources/deploy/eai-app-template/tests/source-unknown-deployment-evidence.test.mjs +2141 -1
- package/resources/gofer/.gofer-version +4 -357
- package/resources/gofer/agents-skills/eai/SKILL.md +365 -0
- package/resources/gofer/agents-skills/eai-update/SKILL.md +56 -0
- package/resources/gofer/agents-skills/gofer-documentation/SKILL.md +93 -0
- package/resources/gofer/bash-scripts/create-new-feature.sh +3 -1
- package/resources/gofer/bash-scripts/sync-implementation-status.sh +4 -3
- package/resources/gofer/bash-scripts/update-agent-context.sh +7 -8
- package/resources/gofer/bash-scripts/validate-artifact.sh +10 -6
- package/resources/gofer/bash-scripts/verify-object-type-routing-workspace.sh +184 -0
- package/resources/gofer/claude-commands/eai-update.md +46 -0
- package/resources/gofer/claude-commands/eai.md +355 -0
- package/resources/gofer/claude-skills/eai/SKILL.md +365 -0
- package/resources/gofer/claude-skills/eai-update/SKILL.md +56 -0
- package/resources/gofer/claude-skills/gofer-documentation/SKILL.md +93 -0
- package/resources/gofer/commands/0_gofer_start.md +440 -99
- package/resources/gofer/commands/0a_problem_validation.md +187 -27
- package/resources/gofer/commands/10_gofer_cloud.md +183 -23
- package/resources/gofer/commands/1_gofer_research.md +239 -30
- package/resources/gofer/commands/2_gofer_specify.md +239 -35
- package/resources/gofer/commands/3_gofer_plan.md +259 -43
- package/resources/gofer/commands/4_gofer_tasks.md +401 -81
- package/resources/gofer/commands/5_gofer_implement.md +375 -84
- package/resources/gofer/commands/6_gofer_validate.md +284 -53
- package/resources/gofer/commands/7_gofer_save.md +187 -26
- package/resources/gofer/commands/7a_stakeholder_comms.md +183 -23
- package/resources/gofer/commands/8_gofer_branding.md +183 -23
- package/resources/gofer/commands/9_gofer_tests.md +212 -23
- package/resources/gofer/commands/gofer_bootstrap_workspace.md +121 -8
- package/resources/gofer/commands/gofer_check_workspace.md +123 -8
- package/resources/gofer/commands/gofer_constitution.md +183 -23
- package/resources/gofer/commands/gofer_diagnose.md +183 -23
- package/resources/gofer/commands/gofer_eai_first_run.md +254 -25
- package/resources/gofer/commands/gofer_hydrate.md +183 -23
- package/resources/gofer/commands/gofer_personality.md +117 -6
- package/resources/gofer/commands/gofer_plan.md +117 -6
- package/resources/gofer/commands/gofer_side.md +117 -6
- package/resources/gofer/commands/gofer_spec_summary.md +183 -23
- package/resources/gofer/commands/gofer_tdd.md +183 -23
- package/resources/gofer/commands/gofer_vocabulary.md +185 -25
- package/resources/gofer/commands/gofer_zoom_out.md +183 -23
- package/resources/gofer/config/typesafe-learning-review.json +16 -0
- package/resources/gofer/config/typesafe-semantic-review.json +9 -0
- package/resources/gofer/copilot-instructions/gofer-cost.instructions.md +15 -6
- package/resources/gofer/copilot-instructions/typescript.instructions.md +1 -1
- package/resources/gofer/copilot-prompts/eai-update.prompt.md +58 -0
- package/resources/gofer/copilot-prompts/eai.prompt.md +367 -0
- package/resources/gofer/gemini/commands/gofer/eai-update.md +22 -0
- package/resources/gofer/gemini/commands/gofer/eai-update.toml +2 -0
- package/resources/gofer/gemini/commands/gofer/eai.md +357 -0
- package/resources/gofer/gemini/commands/gofer/eai.toml +2 -0
- package/resources/gofer/gemini/commands/gofer/manifest.json +3 -27
- package/resources/gofer/gemini/extension.json +4 -4
- package/resources/gofer/github-agents/eai.agent.md +83 -0
- package/resources/gofer/github-agents/gofer-business.agent.md +73 -4
- package/resources/gofer/github-agents/gofer-implement.agent.md +71 -2
- package/resources/gofer/github-agents/gofer-plan.agent.md +72 -2
- package/resources/gofer/github-agents/gofer-research.agent.md +72 -2
- package/resources/gofer/github-agents/gofer-validate.agent.md +72 -2
- package/resources/gofer/github-skills/eai/SKILL.md +365 -0
- package/resources/gofer/github-skills/eai-update/SKILL.md +56 -0
- package/resources/gofer/github-skills/gofer-documentation/SKILL.md +93 -0
- package/resources/gofer/grok-skills/eai/SKILL.md +267 -14
- package/resources/gofer/grok-skills/eai-update/SKILL.md +56 -0
- package/resources/gofer/hook-scripts/user-prompt-submit.mjs +58 -13
- package/resources/gofer/instruction-templates/base/agents-base.md +8 -4
- package/resources/gofer/instruction-templates/gofer/gofer-claude.md +7 -5
- package/resources/gofer/instruction-templates/gofer/gofer-copilot.md +10 -8
- package/resources/gofer/memory/constitution.md +9 -7
- package/resources/gofer/memory/gofer-model-policy.yaml +23 -1
- package/resources/gofer/memory/lessons.md +0 -31
- package/resources/gofer/node-scripts/canonical-descriptions.mjs +1 -1
- package/resources/gofer/node-scripts/check-version-alignment.mjs +4 -7
- package/resources/gofer/node-scripts/delivery-lineage-corpus.mjs +156 -0
- package/resources/gofer/node-scripts/generate-commands.mjs +1073 -265
- package/resources/gofer/node-scripts/gofer-acceptance-check.mjs +143 -0
- package/resources/gofer/node-scripts/gofer-agent-catalog.mjs +271 -0
- package/resources/gofer/node-scripts/gofer-benchmark-executor.mjs +123 -0
- package/resources/gofer/node-scripts/gofer-benchmark-sign.mjs +62 -0
- package/resources/gofer/node-scripts/gofer-benchmark-signer.mjs +89 -0
- package/resources/gofer/node-scripts/gofer-benchmark.mjs +76 -0
- package/resources/gofer/node-scripts/gofer-blocker-control.mjs +274 -0
- package/resources/gofer/node-scripts/gofer-claude-adapter.mjs +154 -0
- package/resources/gofer/node-scripts/gofer-closed-loop-audit.mjs +19 -2
- package/resources/gofer/node-scripts/gofer-delivery-check.mjs +121 -0
- package/resources/gofer/node-scripts/gofer-encrypted-key.mjs +58 -0
- package/resources/gofer/node-scripts/gofer-execution-metrics.mjs +477 -0
- package/resources/gofer/node-scripts/gofer-execution-recovery.mjs +334 -0
- package/resources/gofer/node-scripts/gofer-grok-adapter.mjs +184 -0
- package/resources/gofer/node-scripts/gofer-heldout-corpus.mjs +73 -0
- package/resources/gofer/node-scripts/gofer-heldout-snapshot.mjs +286 -0
- package/resources/gofer/node-scripts/gofer-heldout-verifier.mjs +211 -0
- package/resources/gofer/node-scripts/gofer-host-capability.mjs +335 -0
- package/resources/gofer/node-scripts/gofer-independent-reviewer.mjs +130 -0
- package/resources/gofer/node-scripts/gofer-live-routing.mjs +109 -0
- package/resources/gofer/node-scripts/gofer-local-capability-issuer.mjs +71 -0
- package/resources/gofer/node-scripts/gofer-local-isolation.mjs +175 -0
- package/resources/gofer/node-scripts/gofer-local-settings-cleanup.mjs +388 -0
- package/resources/gofer/node-scripts/gofer-loop-audit.mjs +54 -3
- package/resources/gofer/node-scripts/gofer-native-adapter.mjs +567 -0
- package/resources/gofer/node-scripts/gofer-native-benchmark-dispatch.mjs +145 -0
- package/resources/gofer/node-scripts/gofer-native-runtime.mjs +169 -0
- package/resources/gofer/node-scripts/gofer-pre-edit-check.mjs +204 -0
- package/resources/gofer/node-scripts/gofer-priority-check.mjs +214 -0
- package/resources/gofer/node-scripts/gofer-response-check.mjs +56 -0
- package/resources/gofer/node-scripts/gofer-reviewed-learning.mjs +1232 -0
- package/resources/gofer/node-scripts/gofer-run-verified-task.mjs +330 -0
- package/resources/gofer/node-scripts/gofer-runtime-ledger.mjs +289 -0
- package/resources/gofer/node-scripts/gofer-semantic-drift.mjs +584 -0
- package/resources/gofer/node-scripts/gofer-surface-update.mjs +1178 -0
- package/resources/gofer/node-scripts/gofer-trust-bootstrap.mjs +77 -0
- package/resources/gofer/node-scripts/gofer-trusted-benchmark.mjs +102 -0
- package/resources/gofer/node-scripts/gofer-trusted-evaluator.mjs +274 -0
- package/resources/gofer/node-scripts/gofer-tty-prompt.mjs +34 -0
- package/resources/gofer/node-scripts/gofer-typesafe-credentials.mjs +435 -0
- package/resources/gofer/node-scripts/gofer-ui-preview.mjs +1212 -0
- package/resources/gofer/node-scripts/gofer-verified-execution.mjs +484 -0
- package/resources/gofer/node-scripts/gofer-verifier-key-ceremony.mjs +101 -0
- package/resources/gofer/node-scripts/gofer-worktree-layout.mjs +32 -0
- package/resources/gofer/node-scripts/object-type-routing-phase-bundle.mjs +360 -0
- package/resources/gofer/node-scripts/package-agent-plugin.mjs +574 -82
- package/resources/gofer/node-scripts/parse-stage-command.mjs +1 -1
- package/resources/gofer/node-scripts/render-delivery-lineage.mjs +178 -0
- package/resources/gofer/node-scripts/sync-extension-resources.mjs +78 -2
- package/resources/gofer/node-scripts/validate-object-type-identifiers.mjs +767 -0
- package/resources/gofer/node-scripts/validate-object-type-routing-workspace.mjs +1325 -0
- package/resources/gofer/node-scripts/validate-v4-resource-contract.d.mts +44 -0
- package/resources/gofer/node-scripts/validate-v4-resource-contract.mjs +957 -0
- package/resources/gofer/node-scripts/workspace-bootstrap-lib.mjs +685 -116
- package/resources/gofer/references/agent-catalog.json +126 -0
- package/resources/gofer/references/blocker-mediation.md +156 -0
- package/resources/gofer/references/business-updates-and-goal-checks.md +109 -0
- package/resources/gofer/references/delivery-lineage.md +48 -0
- package/resources/gofer/references/mvp-capability-validation.md +102 -0
- package/resources/gofer/references/platform/README.md +1 -1
- package/resources/gofer/references/platform/eai-app-template.md +15 -13
- package/resources/gofer/references/platform/eai-auth-access.md +104 -0
- package/resources/gofer/references/platform/eai-config-driven-ui.md +5 -5
- package/resources/gofer/references/platform/eai-error-catalog.yaml +77 -19
- package/resources/gofer/references/platform/eai-repo-contract.md +61 -23
- package/resources/gofer/references/platform/eai-service-patterns.md +102 -105
- package/resources/gofer/references/platform/eai.md +44 -8
- package/resources/gofer/references/platform/vertical-template.md +10 -9
- package/resources/gofer/references/priority-outcome-protection.md +152 -0
- package/resources/gofer/references/verified-agent-execution.md +179 -0
- package/resources/gofer/schemas/gofer-learning-trace-v1.json +70 -0
- package/resources/gofer/specify-commands/0_gofer_start.md +440 -99
- package/resources/gofer/specify-commands/0a_problem_validation.md +187 -27
- package/resources/gofer/specify-commands/10_gofer_cloud.md +183 -23
- package/resources/gofer/specify-commands/1_gofer_research.md +239 -30
- package/resources/gofer/specify-commands/2_gofer_specify.md +239 -35
- package/resources/gofer/specify-commands/3_gofer_plan.md +259 -43
- package/resources/gofer/specify-commands/4_gofer_tasks.md +401 -81
- package/resources/gofer/specify-commands/5_gofer_implement.md +375 -84
- package/resources/gofer/specify-commands/6_gofer_validate.md +284 -53
- package/resources/gofer/specify-commands/7_gofer_save.md +187 -26
- package/resources/gofer/specify-commands/7a_stakeholder_comms.md +183 -23
- package/resources/gofer/specify-commands/8_gofer_branding.md +183 -23
- package/resources/gofer/specify-commands/9_gofer_tests.md +212 -23
- package/resources/gofer/specify-commands/gofer_bootstrap_workspace.md +121 -8
- package/resources/gofer/specify-commands/gofer_check_workspace.md +123 -8
- package/resources/gofer/specify-commands/gofer_constitution.md +183 -23
- package/resources/gofer/specify-commands/gofer_diagnose.md +183 -23
- package/resources/gofer/specify-commands/gofer_eai_first_run.md +254 -25
- package/resources/gofer/specify-commands/gofer_hydrate.md +183 -23
- package/resources/gofer/specify-commands/gofer_personality.md +117 -6
- package/resources/gofer/specify-commands/gofer_plan.md +117 -6
- package/resources/gofer/specify-commands/gofer_side.md +117 -6
- package/resources/gofer/specify-commands/gofer_spec_summary.md +183 -23
- package/resources/gofer/specify-commands/gofer_tdd.md +183 -23
- package/resources/gofer/specify-commands/gofer_vocabulary.md +185 -25
- package/resources/gofer/specify-commands/gofer_zoom_out.md +183 -23
- package/resources/gofer/specify-config/object-type-routing.json +24 -0
- package/resources/gofer/specify-config/typesafe-learning-review.json +16 -0
- package/resources/gofer/specify-config/typesafe-semantic-review.json +9 -0
- package/resources/gofer/system-skills/eai/SKILL.md +365 -0
- package/resources/gofer/system-skills/eai-update/SKILL.md +56 -0
- package/resources/gofer/system-skills/gofer-documentation/SKILL.md +93 -0
- package/resources/gofer/templates/build-map-template.md +64 -0
- package/resources/gofer/templates/business-scenarios-template.json +14 -0
- package/resources/gofer/templates/ciso-security-summary-template.md +3 -3
- package/resources/gofer/templates/context-bundle-template.md +4 -3
- package/resources/gofer/templates/contract-pack-template.md +8 -8
- package/resources/gofer/templates/cto-architecture-summary-template.md +3 -3
- package/resources/gofer/templates/delivery-lineage-template.json +8 -0
- package/resources/gofer/templates/eai-preflight-template.md +109 -68
- package/resources/gofer/templates/goal-ledger-template.json +1 -1
- package/resources/gofer/templates/gofer-model-policy.yaml +28 -6
- package/resources/gofer/templates/loop-contract-template.json +3 -1
- package/resources/gofer/templates/plan-template.md +8 -7
- package/resources/gofer/templates/priority-plan-template.json +29 -0
- package/resources/gofer/templates/proposal-review-template.md +6 -3
- package/resources/gofer/templates/release-capability-ledger-template.md +24 -0
- package/resources/gofer/templates/research-template.md +7 -6
- package/resources/gofer/templates/spec-summary-template.md +1 -1
- package/resources/gofer/templates/spec-template.md +45 -6
- package/resources/gofer/templates/tasks-template.md +12 -3
- package/resources/gofer/templates/ui-preview-brief-template.md +55 -5
- package/resources/gofer/templates/ui-review-log-template.md +21 -3
- package/resources/gofer/templates/ui-show-and-tell-template.md +46 -0
- package/resources/gofer/templates/visuals/capability-heatmap-template.md +2 -2
- package/resources/gofer/templates/working-backwards-prfaq-template.md +9 -8
- package/resources/linked-sources.json +4 -4
- package/resources/gofer/agents-skills/0_gofer_start/SKILL.md +0 -1287
- package/resources/gofer/agents-skills/0a_problem_validation/SKILL.md +0 -429
- package/resources/gofer/agents-skills/10_gofer_cloud/SKILL.md +0 -635
- package/resources/gofer/agents-skills/1_gofer_research/SKILL.md +0 -1159
- package/resources/gofer/agents-skills/2_gofer_specify/SKILL.md +0 -860
- package/resources/gofer/agents-skills/3_gofer_plan/SKILL.md +0 -899
- package/resources/gofer/agents-skills/4_gofer_tasks/SKILL.md +0 -760
- package/resources/gofer/agents-skills/5_gofer_implement/SKILL.md +0 -944
- package/resources/gofer/agents-skills/6_gofer_validate/SKILL.md +0 -2302
- package/resources/gofer/agents-skills/7_gofer_save/SKILL.md +0 -423
- package/resources/gofer/agents-skills/7a_stakeholder_comms/SKILL.md +0 -508
- package/resources/gofer/agents-skills/8_gofer_branding/SKILL.md +0 -315
- package/resources/gofer/agents-skills/9_gofer_tests/SKILL.md +0 -558
- package/resources/gofer/agents-skills/gofer_bootstrap_workspace/SKILL.md +0 -148
- package/resources/gofer/agents-skills/gofer_check_workspace/SKILL.md +0 -119
- package/resources/gofer/agents-skills/gofer_constitution/SKILL.md +0 -612
- package/resources/gofer/agents-skills/gofer_diagnose/SKILL.md +0 -114
- package/resources/gofer/agents-skills/gofer_eai_first_run/SKILL.md +0 -397
- package/resources/gofer/agents-skills/gofer_hydrate/SKILL.md +0 -561
- package/resources/gofer/agents-skills/gofer_personality/SKILL.md +0 -44
- package/resources/gofer/agents-skills/gofer_plan/SKILL.md +0 -40
- package/resources/gofer/agents-skills/gofer_side/SKILL.md +0 -45
- package/resources/gofer/agents-skills/gofer_spec_summary/SKILL.md +0 -112
- package/resources/gofer/agents-skills/gofer_tdd/SKILL.md +0 -112
- package/resources/gofer/agents-skills/gofer_vocabulary/SKILL.md +0 -112
- package/resources/gofer/agents-skills/gofer_zoom_out/SKILL.md +0 -112
- package/resources/gofer/claude-commands/0_gofer_start.md +0 -1282
- package/resources/gofer/claude-commands/0a_problem_validation.md +0 -424
- package/resources/gofer/claude-commands/10_gofer_cloud.md +0 -630
- package/resources/gofer/claude-commands/1_gofer_research.md +0 -1154
- package/resources/gofer/claude-commands/2_gofer_specify.md +0 -855
- package/resources/gofer/claude-commands/3_gofer_plan.md +0 -894
- package/resources/gofer/claude-commands/4_gofer_tasks.md +0 -755
- package/resources/gofer/claude-commands/5_gofer_implement.md +0 -939
- package/resources/gofer/claude-commands/6_gofer_validate.md +0 -2297
- package/resources/gofer/claude-commands/7_gofer_save.md +0 -418
- package/resources/gofer/claude-commands/7a_stakeholder_comms.md +0 -503
- package/resources/gofer/claude-commands/8_gofer_branding.md +0 -310
- package/resources/gofer/claude-commands/9_gofer_tests.md +0 -553
- package/resources/gofer/claude-commands/gofer_bootstrap_workspace.md +0 -143
- package/resources/gofer/claude-commands/gofer_check_workspace.md +0 -114
- package/resources/gofer/claude-commands/gofer_constitution.md +0 -607
- package/resources/gofer/claude-commands/gofer_diagnose.md +0 -109
- package/resources/gofer/claude-commands/gofer_eai_first_run.md +0 -392
- package/resources/gofer/claude-commands/gofer_hydrate.md +0 -556
- package/resources/gofer/claude-commands/gofer_personality.md +0 -39
- package/resources/gofer/claude-commands/gofer_plan.md +0 -35
- package/resources/gofer/claude-commands/gofer_side.md +0 -40
- package/resources/gofer/claude-commands/gofer_spec_summary.md +0 -107
- package/resources/gofer/claude-commands/gofer_tdd.md +0 -107
- package/resources/gofer/claude-commands/gofer_vocabulary.md +0 -107
- package/resources/gofer/claude-commands/gofer_zoom_out.md +0 -107
- package/resources/gofer/claude-skills/eai-gofer/SKILL.md +0 -56
- package/resources/gofer/copilot-prompts/0_gofer_start.prompt.md +0 -1308
- package/resources/gofer/copilot-prompts/0a_problem_validation.prompt.md +0 -429
- package/resources/gofer/copilot-prompts/10_gofer_cloud.prompt.md +0 -644
- package/resources/gofer/copilot-prompts/1_gofer_research.prompt.md +0 -1177
- package/resources/gofer/copilot-prompts/2_gofer_specify.prompt.md +0 -878
- package/resources/gofer/copilot-prompts/3_gofer_plan.prompt.md +0 -917
- package/resources/gofer/copilot-prompts/4_gofer_tasks.prompt.md +0 -778
- package/resources/gofer/copilot-prompts/5_gofer_implement.prompt.md +0 -962
- package/resources/gofer/copilot-prompts/6_gofer_validate.prompt.md +0 -2310
- package/resources/gofer/copilot-prompts/7_gofer_save.prompt.md +0 -433
- package/resources/gofer/copilot-prompts/7a_stakeholder_comms.prompt.md +0 -516
- package/resources/gofer/copilot-prompts/8_gofer_branding.prompt.md +0 -323
- package/resources/gofer/copilot-prompts/9_gofer_tests.prompt.md +0 -566
- package/resources/gofer/copilot-prompts/gofer_bootstrap_workspace.prompt.md +0 -161
- package/resources/gofer/copilot-prompts/gofer_check_workspace.prompt.md +0 -132
- package/resources/gofer/copilot-prompts/gofer_constitution.prompt.md +0 -621
- package/resources/gofer/copilot-prompts/gofer_diagnose.prompt.md +0 -127
- package/resources/gofer/copilot-prompts/gofer_eai_first_run.prompt.md +0 -410
- package/resources/gofer/copilot-prompts/gofer_hydrate.prompt.md +0 -571
- package/resources/gofer/copilot-prompts/gofer_personality.prompt.md +0 -57
- package/resources/gofer/copilot-prompts/gofer_plan.prompt.md +0 -53
- package/resources/gofer/copilot-prompts/gofer_side.prompt.md +0 -58
- package/resources/gofer/copilot-prompts/gofer_spec_summary.prompt.md +0 -125
- package/resources/gofer/copilot-prompts/gofer_tdd.prompt.md +0 -125
- package/resources/gofer/copilot-prompts/gofer_vocabulary.prompt.md +0 -125
- package/resources/gofer/copilot-prompts/gofer_zoom_out.prompt.md +0 -125
- package/resources/gofer/gemini/commands/gofer/0_gofer_start.md +0 -1282
- package/resources/gofer/gemini/commands/gofer/0_gofer_start.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/0a_problem_validation.md +0 -424
- package/resources/gofer/gemini/commands/gofer/0a_problem_validation.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/10_gofer_cloud.md +0 -630
- package/resources/gofer/gemini/commands/gofer/10_gofer_cloud.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/1_gofer_research.md +0 -1154
- package/resources/gofer/gemini/commands/gofer/1_gofer_research.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/2_gofer_specify.md +0 -855
- package/resources/gofer/gemini/commands/gofer/2_gofer_specify.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/3_gofer_plan.md +0 -894
- package/resources/gofer/gemini/commands/gofer/3_gofer_plan.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/4_gofer_tasks.md +0 -755
- package/resources/gofer/gemini/commands/gofer/4_gofer_tasks.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/5_gofer_implement.md +0 -939
- package/resources/gofer/gemini/commands/gofer/5_gofer_implement.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/6_gofer_validate.md +0 -2297
- package/resources/gofer/gemini/commands/gofer/6_gofer_validate.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/7_gofer_save.md +0 -418
- package/resources/gofer/gemini/commands/gofer/7_gofer_save.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/7a_stakeholder_comms.md +0 -503
- package/resources/gofer/gemini/commands/gofer/7a_stakeholder_comms.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/8_gofer_branding.md +0 -310
- package/resources/gofer/gemini/commands/gofer/8_gofer_branding.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/9_gofer_tests.md +0 -553
- package/resources/gofer/gemini/commands/gofer/9_gofer_tests.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_bootstrap_workspace.md +0 -143
- package/resources/gofer/gemini/commands/gofer/gofer_bootstrap_workspace.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_check_workspace.md +0 -114
- package/resources/gofer/gemini/commands/gofer/gofer_check_workspace.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_constitution.md +0 -607
- package/resources/gofer/gemini/commands/gofer/gofer_constitution.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_diagnose.md +0 -109
- package/resources/gofer/gemini/commands/gofer/gofer_diagnose.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_eai_first_run.md +0 -392
- package/resources/gofer/gemini/commands/gofer/gofer_eai_first_run.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_hydrate.md +0 -556
- package/resources/gofer/gemini/commands/gofer/gofer_hydrate.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_personality.md +0 -39
- package/resources/gofer/gemini/commands/gofer/gofer_personality.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_plan.md +0 -35
- package/resources/gofer/gemini/commands/gofer/gofer_plan.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_side.md +0 -40
- package/resources/gofer/gemini/commands/gofer/gofer_side.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_spec_summary.md +0 -107
- package/resources/gofer/gemini/commands/gofer/gofer_spec_summary.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_tdd.md +0 -107
- package/resources/gofer/gemini/commands/gofer/gofer_tdd.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_vocabulary.md +0 -107
- package/resources/gofer/gemini/commands/gofer/gofer_vocabulary.toml +0 -2
- package/resources/gofer/gemini/commands/gofer/gofer_zoom_out.md +0 -107
- package/resources/gofer/gemini/commands/gofer/gofer_zoom_out.toml +0 -2
- package/resources/gofer/github-skills/eai-gofer/SKILL.md +0 -56
- package/resources/gofer/system-skills/0_gofer_start/SKILL.md +0 -1287
- package/resources/gofer/system-skills/0a_problem_validation/SKILL.md +0 -429
- package/resources/gofer/system-skills/10_gofer_cloud/SKILL.md +0 -635
- package/resources/gofer/system-skills/1_gofer_research/SKILL.md +0 -1159
- package/resources/gofer/system-skills/2_gofer_specify/SKILL.md +0 -860
- package/resources/gofer/system-skills/3_gofer_plan/SKILL.md +0 -899
- package/resources/gofer/system-skills/4_gofer_tasks/SKILL.md +0 -760
- package/resources/gofer/system-skills/5_gofer_implement/SKILL.md +0 -944
- package/resources/gofer/system-skills/6_gofer_validate/SKILL.md +0 -2302
- package/resources/gofer/system-skills/7_gofer_save/SKILL.md +0 -423
- package/resources/gofer/system-skills/7a_stakeholder_comms/SKILL.md +0 -508
- package/resources/gofer/system-skills/8_gofer_branding/SKILL.md +0 -315
- package/resources/gofer/system-skills/9_gofer_tests/SKILL.md +0 -558
- package/resources/gofer/system-skills/gofer_bootstrap_workspace/SKILL.md +0 -148
- package/resources/gofer/system-skills/gofer_check_workspace/SKILL.md +0 -119
- package/resources/gofer/system-skills/gofer_constitution/SKILL.md +0 -612
- package/resources/gofer/system-skills/gofer_diagnose/SKILL.md +0 -114
- package/resources/gofer/system-skills/gofer_eai_first_run/SKILL.md +0 -397
- package/resources/gofer/system-skills/gofer_hydrate/SKILL.md +0 -561
- package/resources/gofer/system-skills/gofer_personality/SKILL.md +0 -44
- package/resources/gofer/system-skills/gofer_plan/SKILL.md +0 -40
- package/resources/gofer/system-skills/gofer_side/SKILL.md +0 -45
- package/resources/gofer/system-skills/gofer_spec_summary/SKILL.md +0 -112
- package/resources/gofer/system-skills/gofer_tdd/SKILL.md +0 -112
- package/resources/gofer/system-skills/gofer_vocabulary/SKILL.md +0 -112
- package/resources/gofer/system-skills/gofer_zoom_out/SKILL.md +0 -112
- package/resources/gofer/templates/ui-approval-template.md +0 -32
|
@@ -0,0 +1,1212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { createWriteStream } from 'node:fs';
|
|
5
|
+
import { promises as fs } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import net from 'node:net';
|
|
9
|
+
import { once } from 'node:events';
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_APP_PREVIEW_PORT = 3001;
|
|
12
|
+
export const DEFAULT_PREVIEW_PORTS = [DEFAULT_APP_PREVIEW_PORT, 3000, 5173, 4173, 6006, 8080, 8000];
|
|
13
|
+
export const PREVIEW_SCRIPT_PRIORITY = ['dev', 'start', 'preview', 'serve', 'storybook', 'docs:dev'];
|
|
14
|
+
export const BUSINESS_SCENARIO_SCRIPT_PRIORITY = [
|
|
15
|
+
'test:business-scenarios',
|
|
16
|
+
'test:e2e',
|
|
17
|
+
'test:playwright',
|
|
18
|
+
'e2e',
|
|
19
|
+
'playwright',
|
|
20
|
+
];
|
|
21
|
+
export const BUSINESS_SCENARIO_MANIFEST = 'business-scenarios.json';
|
|
22
|
+
export const UI_REVIEW_LOG_COLUMNS = [
|
|
23
|
+
'Time',
|
|
24
|
+
'Change Trigger',
|
|
25
|
+
'Command',
|
|
26
|
+
'URL',
|
|
27
|
+
'Browser Target',
|
|
28
|
+
'Screenshot',
|
|
29
|
+
'Package Lane',
|
|
30
|
+
'Coupling Status',
|
|
31
|
+
'Storybook Story IDs',
|
|
32
|
+
'Theme Override Points',
|
|
33
|
+
'Self-Review',
|
|
34
|
+
'Stakeholder Feedback',
|
|
35
|
+
'User Feedback Applied',
|
|
36
|
+
'Open Issues',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
40
|
+
|
|
41
|
+
export function detectPackageManagerFromNames(fileNames) {
|
|
42
|
+
const names = new Set(fileNames);
|
|
43
|
+
if (names.has('pnpm-lock.yaml')) return 'pnpm';
|
|
44
|
+
if (names.has('bun.lockb') || names.has('bun.lock')) return 'bun';
|
|
45
|
+
if (names.has('yarn.lock')) return 'yarn';
|
|
46
|
+
if (names.has('package-lock.json') || names.has('npm-shrinkwrap.json')) return 'npm';
|
|
47
|
+
return 'npm';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function detectPackageManager(workspaceRoot) {
|
|
51
|
+
const entries = await fs.readdir(workspaceRoot).catch(() => []);
|
|
52
|
+
return detectPackageManagerFromNames(entries);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function selectPreviewScript(scripts = {}) {
|
|
56
|
+
for (const scriptName of PREVIEW_SCRIPT_PRIORITY) {
|
|
57
|
+
if (typeof scripts[scriptName] === 'string' && scripts[scriptName].trim()) {
|
|
58
|
+
return scriptName;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function selectBusinessScenarioScript(scripts = {}) {
|
|
65
|
+
for (const scriptName of BUSINESS_SCENARIO_SCRIPT_PRIORITY) {
|
|
66
|
+
if (typeof scripts[scriptName] === 'string' && scripts[scriptName].trim()) {
|
|
67
|
+
return scriptName;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function buildPackageScriptCommand(packageManager, scriptName) {
|
|
74
|
+
if (!scriptName) return null;
|
|
75
|
+
if (packageManager === 'pnpm') return `pnpm run ${scriptName}`;
|
|
76
|
+
if (packageManager === 'yarn') return `yarn ${scriptName}`;
|
|
77
|
+
if (packageManager === 'bun') return `bun run ${scriptName}`;
|
|
78
|
+
return `npm run ${scriptName}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function fileExists(filePath) {
|
|
82
|
+
try {
|
|
83
|
+
await fs.access(filePath);
|
|
84
|
+
return true;
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function discoverRepoRunnerCommand(
|
|
91
|
+
workspaceRoot,
|
|
92
|
+
port = DEFAULT_APP_PREVIEW_PORT,
|
|
93
|
+
platform = process.platform
|
|
94
|
+
) {
|
|
95
|
+
const normalizedPort = Number(port);
|
|
96
|
+
if (!Number.isInteger(normalizedPort) || normalizedPort < 1 || normalizedPort > 65535) {
|
|
97
|
+
throw new Error(`Preview port is invalid: ${port}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (platform === 'win32') {
|
|
101
|
+
if (await fileExists(path.join(workspaceRoot, 'run.bat'))) {
|
|
102
|
+
return {
|
|
103
|
+
command: `run.bat dev ${normalizedPort}`,
|
|
104
|
+
source: 'repo-runner',
|
|
105
|
+
packageManager: null,
|
|
106
|
+
scriptName: null,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (await fileExists(path.join(workspaceRoot, 'run.ps1'))) {
|
|
110
|
+
return {
|
|
111
|
+
command: `powershell -NoProfile -ExecutionPolicy Bypass -File .\\run.ps1 dev ${normalizedPort}`,
|
|
112
|
+
source: 'repo-runner',
|
|
113
|
+
packageManager: null,
|
|
114
|
+
scriptName: null,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (await fileExists(path.join(workspaceRoot, 'run.sh'))) {
|
|
120
|
+
return {
|
|
121
|
+
command: `./run.sh dev ${normalizedPort}`,
|
|
122
|
+
source: 'repo-runner',
|
|
123
|
+
packageManager: null,
|
|
124
|
+
scriptName: null,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
command: null,
|
|
130
|
+
source: 'missing-repo-runner',
|
|
131
|
+
packageManager: null,
|
|
132
|
+
scriptName: null,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function discoverPreviewCommand(workspaceRoot, explicitCommand = null) {
|
|
137
|
+
if (explicitCommand?.trim()) {
|
|
138
|
+
return {
|
|
139
|
+
command: explicitCommand.trim(),
|
|
140
|
+
source: 'explicit',
|
|
141
|
+
packageManager: null,
|
|
142
|
+
scriptName: null,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const repoRunner = await discoverRepoRunnerCommand(workspaceRoot);
|
|
147
|
+
if (repoRunner.command) {
|
|
148
|
+
return repoRunner;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const packageJsonPath = path.join(workspaceRoot, 'package.json');
|
|
152
|
+
let packageJson;
|
|
153
|
+
try {
|
|
154
|
+
packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (error?.code === 'ENOENT') {
|
|
157
|
+
return {
|
|
158
|
+
command: null,
|
|
159
|
+
source: 'missing-package-json',
|
|
160
|
+
packageManager: null,
|
|
161
|
+
scriptName: null,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
throw new Error(`Unable to read ${packageJsonPath}: ${error.message}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const scriptName = selectPreviewScript(packageJson.scripts);
|
|
168
|
+
const packageManager = await detectPackageManager(workspaceRoot);
|
|
169
|
+
return {
|
|
170
|
+
command: buildPackageScriptCommand(packageManager, scriptName),
|
|
171
|
+
source: scriptName ? 'package-json' : 'missing-preview-script',
|
|
172
|
+
packageManager,
|
|
173
|
+
scriptName,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function isBrowserScenarioCommand(command = '') {
|
|
178
|
+
return /\b(playwright|cypress|puppeteer|webdriver|browser)\b/i.test(command);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function validateBusinessScenarioManifest(manifest) {
|
|
182
|
+
const errors = [];
|
|
183
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
184
|
+
return { valid: false, errors: ['Manifest must be a JSON object.'], scenarioCount: 0 };
|
|
185
|
+
}
|
|
186
|
+
if (manifest.schemaVersion !== '1.0') {
|
|
187
|
+
errors.push('schemaVersion must be "1.0".');
|
|
188
|
+
}
|
|
189
|
+
if (!Array.isArray(manifest.scenarios) || manifest.scenarios.length === 0) {
|
|
190
|
+
errors.push('scenarios must contain at least one business journey.');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const ids = new Set();
|
|
194
|
+
for (const [index, scenario] of (manifest.scenarios ?? []).entries()) {
|
|
195
|
+
const label = `scenarios[${index}]`;
|
|
196
|
+
if (!scenario?.id || typeof scenario.id !== 'string') {
|
|
197
|
+
errors.push(`${label}.id is required.`);
|
|
198
|
+
} else if (ids.has(scenario.id)) {
|
|
199
|
+
errors.push(`${label}.id duplicates "${scenario.id}".`);
|
|
200
|
+
} else {
|
|
201
|
+
ids.add(scenario.id);
|
|
202
|
+
}
|
|
203
|
+
if (!scenario?.userStory || typeof scenario.userStory !== 'string') {
|
|
204
|
+
errors.push(`${label}.userStory is required.`);
|
|
205
|
+
}
|
|
206
|
+
if (!Array.isArray(scenario?.screens) || scenario.screens.length === 0) {
|
|
207
|
+
errors.push(`${label}.screens must name at least one screen.`);
|
|
208
|
+
}
|
|
209
|
+
if (!Array.isArray(scenario?.testFiles) || scenario.testFiles.length === 0) {
|
|
210
|
+
errors.push(`${label}.testFiles must name at least one executable browser test.`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
valid: errors.length === 0,
|
|
216
|
+
errors,
|
|
217
|
+
scenarioCount: Array.isArray(manifest.scenarios) ? manifest.scenarios.length : 0,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function loadBusinessScenarioManifest(workspaceRoot, featureDir) {
|
|
222
|
+
if (!featureDir) {
|
|
223
|
+
return {
|
|
224
|
+
path: null,
|
|
225
|
+
exists: false,
|
|
226
|
+
valid: false,
|
|
227
|
+
errors: ['A feature directory is required for business-scenario validation.'],
|
|
228
|
+
scenarioCount: 0,
|
|
229
|
+
manifest: null,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const manifestPath = path.join(featureDir, BUSINESS_SCENARIO_MANIFEST);
|
|
234
|
+
let manifest;
|
|
235
|
+
try {
|
|
236
|
+
manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
|
|
237
|
+
} catch (error) {
|
|
238
|
+
return {
|
|
239
|
+
path: manifestPath,
|
|
240
|
+
exists: error?.code !== 'ENOENT',
|
|
241
|
+
valid: false,
|
|
242
|
+
errors: [
|
|
243
|
+
error?.code === 'ENOENT'
|
|
244
|
+
? `${BUSINESS_SCENARIO_MANIFEST} is missing.`
|
|
245
|
+
: `${BUSINESS_SCENARIO_MANIFEST} is invalid JSON: ${error.message}`,
|
|
246
|
+
],
|
|
247
|
+
scenarioCount: 0,
|
|
248
|
+
manifest: null,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const validation = validateBusinessScenarioManifest(manifest);
|
|
253
|
+
const errors = [...validation.errors];
|
|
254
|
+
const rootPrefix = `${path.resolve(workspaceRoot)}${path.sep}`;
|
|
255
|
+
for (const scenario of manifest.scenarios ?? []) {
|
|
256
|
+
for (const testFile of scenario.testFiles ?? []) {
|
|
257
|
+
const resolvedTestFile = path.resolve(workspaceRoot, testFile);
|
|
258
|
+
if (
|
|
259
|
+
resolvedTestFile !== path.resolve(workspaceRoot) &&
|
|
260
|
+
!resolvedTestFile.startsWith(rootPrefix)
|
|
261
|
+
) {
|
|
262
|
+
errors.push(`${scenario.id} test file escapes the workspace: ${testFile}`);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
await fs.access(resolvedTestFile);
|
|
267
|
+
} catch {
|
|
268
|
+
errors.push(`${scenario.id} test file is missing: ${testFile}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
path: manifestPath,
|
|
275
|
+
exists: true,
|
|
276
|
+
valid: errors.length === 0,
|
|
277
|
+
errors,
|
|
278
|
+
scenarioCount: validation.scenarioCount,
|
|
279
|
+
manifest,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export async function discoverBusinessScenarioCommand(
|
|
284
|
+
workspaceRoot,
|
|
285
|
+
explicitCommand = null,
|
|
286
|
+
manifestCommand = null
|
|
287
|
+
) {
|
|
288
|
+
const selectedExplicitCommand = explicitCommand?.trim() || manifestCommand?.trim();
|
|
289
|
+
if (selectedExplicitCommand) {
|
|
290
|
+
let browserRunner = isBrowserScenarioCommand(selectedExplicitCommand);
|
|
291
|
+
if (!browserRunner) {
|
|
292
|
+
const scriptMatch = selectedExplicitCommand.match(
|
|
293
|
+
/^(?:npm|pnpm|bun)\s+run\s+([^\s]+)|^yarn\s+([^\s]+)/
|
|
294
|
+
);
|
|
295
|
+
const scriptName = scriptMatch?.[1] ?? scriptMatch?.[2];
|
|
296
|
+
if (scriptName) {
|
|
297
|
+
try {
|
|
298
|
+
const packageJson = JSON.parse(
|
|
299
|
+
await fs.readFile(path.join(workspaceRoot, 'package.json'), 'utf8')
|
|
300
|
+
);
|
|
301
|
+
browserRunner = isBrowserScenarioCommand(packageJson.scripts?.[scriptName] ?? '');
|
|
302
|
+
} catch {
|
|
303
|
+
browserRunner = false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
command: selectedExplicitCommand,
|
|
309
|
+
source: explicitCommand?.trim() ? 'explicit' : 'manifest',
|
|
310
|
+
packageManager: null,
|
|
311
|
+
scriptName: null,
|
|
312
|
+
browserRunner,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const packageJsonPath = path.join(workspaceRoot, 'package.json');
|
|
317
|
+
let packageJson;
|
|
318
|
+
try {
|
|
319
|
+
packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
|
320
|
+
} catch (error) {
|
|
321
|
+
return {
|
|
322
|
+
command: null,
|
|
323
|
+
source: error?.code === 'ENOENT' ? 'missing-package-json' : 'invalid-package-json',
|
|
324
|
+
packageManager: null,
|
|
325
|
+
scriptName: null,
|
|
326
|
+
browserRunner: false,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const scriptName = selectBusinessScenarioScript(packageJson.scripts);
|
|
331
|
+
const packageManager = await detectPackageManager(workspaceRoot);
|
|
332
|
+
const command = buildPackageScriptCommand(packageManager, scriptName);
|
|
333
|
+
return {
|
|
334
|
+
command,
|
|
335
|
+
source: scriptName ? 'package-json' : 'missing-business-scenario-script',
|
|
336
|
+
packageManager,
|
|
337
|
+
scriptName,
|
|
338
|
+
browserRunner: isBrowserScenarioCommand(packageJson.scripts?.[scriptName] ?? ''),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function extractPortsFromCommand(command = '') {
|
|
343
|
+
const ports = new Set();
|
|
344
|
+
const patterns = [
|
|
345
|
+
/\bPORT=(\d{2,5})\b/g,
|
|
346
|
+
/(?:^|\s)--port(?:=|\s+)(\d{2,5})\b/g,
|
|
347
|
+
/(?:^|\s)-p(?:=|\s+)(\d{2,5})\b/g,
|
|
348
|
+
/(?:^|\s)(?:\.\/|\.\\)?run\.(?:sh|bat|ps1)(?:\s+\S+)?\s+(\d{2,5})\b/g,
|
|
349
|
+
/\blocalhost:(\d{2,5})\b/g,
|
|
350
|
+
/\b127\.0\.0\.1:(\d{2,5})\b/g,
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
for (const pattern of patterns) {
|
|
354
|
+
for (const match of command.matchAll(pattern)) {
|
|
355
|
+
const port = Number(match[1]);
|
|
356
|
+
if (Number.isInteger(port) && port > 0 && port <= 65535) {
|
|
357
|
+
ports.add(port);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return [...ports];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function buildCandidateUrls({ explicitUrl = null, command = null, ports = DEFAULT_PREVIEW_PORTS } = {}) {
|
|
366
|
+
if (explicitUrl?.trim()) return [sanitizePreviewUrl(explicitUrl)];
|
|
367
|
+
|
|
368
|
+
const commandPorts = extractPortsFromCommand(command ?? '');
|
|
369
|
+
// Do not mistake another app on a fallback port for this preview.
|
|
370
|
+
const orderedPorts = (commandPorts.length ? commandPorts : ports).slice(0, 1);
|
|
371
|
+
return orderedPorts.map((port) => `http://localhost:${port}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function buildOpenBrowserCommand(url, platform = process.platform) {
|
|
375
|
+
const safeUrl = sanitizePreviewUrl(url);
|
|
376
|
+
if (platform === 'darwin') return { command: 'open', args: [safeUrl] };
|
|
377
|
+
if (platform === 'win32') return { command: 'explorer.exe', args: [safeUrl] };
|
|
378
|
+
return { command: 'xdg-open', args: [safeUrl] };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function isLoopbackHostname(hostname) {
|
|
382
|
+
const normalized = hostname.toLowerCase();
|
|
383
|
+
return (
|
|
384
|
+
normalized === 'localhost' ||
|
|
385
|
+
normalized.endsWith('.localhost') ||
|
|
386
|
+
normalized === '127.0.0.1' ||
|
|
387
|
+
normalized === '::1' ||
|
|
388
|
+
normalized === '[::1]'
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function sanitizePreviewUrl(value) {
|
|
393
|
+
let parsed;
|
|
394
|
+
try {
|
|
395
|
+
parsed = new URL(String(value ?? '').trim());
|
|
396
|
+
} catch {
|
|
397
|
+
throw new Error(`Preview URL is invalid: ${value}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
401
|
+
throw new Error(`Preview URL must use http or https: ${parsed.protocol}`);
|
|
402
|
+
}
|
|
403
|
+
if (!isLoopbackHostname(parsed.hostname)) {
|
|
404
|
+
throw new Error(`Preview URL must be a local loopback URL: ${parsed.hostname}`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
parsed.username = '';
|
|
408
|
+
parsed.password = '';
|
|
409
|
+
return parsed.href;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function resolveRedirectTarget(baseUrl, location) {
|
|
413
|
+
try {
|
|
414
|
+
return new URL(location, baseUrl).href;
|
|
415
|
+
} catch {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function isSafePreviewRedirect(baseUrl, location) {
|
|
421
|
+
const target = resolveRedirectTarget(baseUrl, location);
|
|
422
|
+
if (!target) return false;
|
|
423
|
+
try {
|
|
424
|
+
sanitizePreviewUrl(target);
|
|
425
|
+
return true;
|
|
426
|
+
} catch {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function markdownCell(value) {
|
|
432
|
+
return String(value ?? '')
|
|
433
|
+
.replace(/\r\n/g, '\n')
|
|
434
|
+
.replace(/[\\|\r\n]/g, (character) => {
|
|
435
|
+
if (character === '\\') return '\\\\';
|
|
436
|
+
if (character === '|') return '\\|';
|
|
437
|
+
return '<br>';
|
|
438
|
+
})
|
|
439
|
+
.trim();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function resolveFeatureLogPaths(workspaceRoot, featureDir = null) {
|
|
443
|
+
const baseDir = featureDir
|
|
444
|
+
? path.resolve(workspaceRoot, featureDir)
|
|
445
|
+
: path.join(workspaceRoot, '.specify', 'logs', 'ui-preview');
|
|
446
|
+
return {
|
|
447
|
+
baseDir,
|
|
448
|
+
previewDir: featureDir ? path.join(baseDir, 'preview') : baseDir,
|
|
449
|
+
reviewLogPath: featureDir ? path.join(baseDir, 'ui-review-log.md') : null,
|
|
450
|
+
processLogPath: path.join(baseDir, 'preview-server.log'),
|
|
451
|
+
pidPath: path.join(baseDir, 'preview-server.pid'),
|
|
452
|
+
scenarioLogPath: path.join(baseDir, 'business-scenarios.log'),
|
|
453
|
+
scenarioReportPath: path.join(baseDir, 'business-scenario-report.json'),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export async function waitForReachableUrl(urls, timeoutMs = 45000, intervalMs = 750) {
|
|
458
|
+
const safeUrls = urls.map((url) => sanitizePreviewUrl(url));
|
|
459
|
+
const deadline = Date.now() + timeoutMs;
|
|
460
|
+
let lastError = null;
|
|
461
|
+
|
|
462
|
+
while (Date.now() <= deadline) {
|
|
463
|
+
for (let index = 0; index < safeUrls.length; index += 1) {
|
|
464
|
+
const url = safeUrls[index];
|
|
465
|
+
let timer = null;
|
|
466
|
+
try {
|
|
467
|
+
const controller = new AbortController();
|
|
468
|
+
timer = setTimeout(() => controller.abort(), Math.min(intervalMs, 5000));
|
|
469
|
+
const response = await fetch(url, {
|
|
470
|
+
method: 'GET',
|
|
471
|
+
redirect: 'manual',
|
|
472
|
+
signal: controller.signal,
|
|
473
|
+
});
|
|
474
|
+
if (response.status >= 300 && response.status < 400) {
|
|
475
|
+
const location = response.headers.get('location');
|
|
476
|
+
if (location && !isSafePreviewRedirect(url, location)) {
|
|
477
|
+
lastError = 'Unsafe redirect from local preview URL';
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (response.status >= 200 && response.status < 300) {
|
|
482
|
+
return { ok: true, urlIndex: index, status: response.status };
|
|
483
|
+
}
|
|
484
|
+
lastError = `HTTP ${response.status} from ${url}`;
|
|
485
|
+
} catch (error) {
|
|
486
|
+
lastError = `${url}: ${error.name === 'AbortError' ? 'timeout' : error.message}`;
|
|
487
|
+
} finally {
|
|
488
|
+
if (timer) clearTimeout(timer);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return { ok: false, urlIndex: null, status: null, error: lastError ?? 'No candidate URL responded' };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export async function checkPreviewPortAvailable(url) {
|
|
498
|
+
const parsed = new URL(sanitizePreviewUrl(url));
|
|
499
|
+
const port = Number(parsed.port || (parsed.protocol === 'https:' ? 443 : 80));
|
|
500
|
+
const connect = host => new Promise(resolve => {
|
|
501
|
+
const socket = net.connect({ host, port });
|
|
502
|
+
const finish = result => { socket.destroy(); resolve(result); };
|
|
503
|
+
socket.setTimeout(1000, () => finish({ ok: false, code: 'PORT_CHECK_TIMEOUT' }));
|
|
504
|
+
socket.once('connect', () => finish({ ok: false, code: 'EADDRINUSE' }));
|
|
505
|
+
socket.once('error', error => finish(['ECONNREFUSED', 'EAFNOSUPPORT', 'EADDRNOTAVAIL', 'ENETUNREACH'].includes(error.code)
|
|
506
|
+
? { ok: true } : { ok: false, code: error.code }));
|
|
507
|
+
});
|
|
508
|
+
for (const host of ['127.0.0.1', '::1']) {
|
|
509
|
+
const result = await connect(host);
|
|
510
|
+
if (!result.ok) return result;
|
|
511
|
+
}
|
|
512
|
+
const probe = host => new Promise(resolve => {
|
|
513
|
+
const server = net.createServer();
|
|
514
|
+
server.once('error', error => resolve({ ok: false, code: error.code }));
|
|
515
|
+
server.listen({ host, port, exclusive: true }, () => server.close(() => resolve({ ok: true })));
|
|
516
|
+
});
|
|
517
|
+
// Probe both families. Refuse ambiguous failures rather than launching a runner
|
|
518
|
+
// that could terminate another app. Runners must also verify ownership themselves.
|
|
519
|
+
for (const host of ['0.0.0.0', '::']) {
|
|
520
|
+
const result = await probe(host);
|
|
521
|
+
if (!result.ok && !['EAFNOSUPPORT', 'EADDRNOTAVAIL'].includes(result.code)) return result;
|
|
522
|
+
}
|
|
523
|
+
return { ok: true };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function startPreviewServer(command, workspaceRoot, logPath, pidPath) {
|
|
527
|
+
await fs.mkdir(path.dirname(logPath), { recursive: true });
|
|
528
|
+
const out = createWriteStream(logPath, { flags: 'a', mode: 0o600 });
|
|
529
|
+
await once(out, 'open');
|
|
530
|
+
out.write(`\n\n[${new Date().toISOString()}] Starting Gofer UI preview: ${command}\n`);
|
|
531
|
+
|
|
532
|
+
const child = spawn(command, {
|
|
533
|
+
cwd: workspaceRoot,
|
|
534
|
+
env: {
|
|
535
|
+
...process.env,
|
|
536
|
+
BROWSER: process.env.BROWSER ?? 'none',
|
|
537
|
+
},
|
|
538
|
+
shell: true,
|
|
539
|
+
detached: true,
|
|
540
|
+
stdio: ['ignore', out, out],
|
|
541
|
+
windowsHide: true,
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
try { await once(child, 'spawn'); }
|
|
545
|
+
finally { out.end(); }
|
|
546
|
+
child.unref();
|
|
547
|
+
await fs.writeFile(pidPath, `${child.pid}\n`, 'utf8');
|
|
548
|
+
return { pid: child.pid, logPath, pidPath };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async function runBusinessScenarioCommand(command, workspaceRoot, logPath, timeoutMs = 300000) {
|
|
552
|
+
await fs.mkdir(path.dirname(logPath), { recursive: true });
|
|
553
|
+
const out = createWriteStream(logPath, { flags: 'a', mode: 0o600 });
|
|
554
|
+
out.write(`\n\n[${new Date().toISOString()}] Running business scenarios: ${command}\n`);
|
|
555
|
+
|
|
556
|
+
return await new Promise((resolve) => {
|
|
557
|
+
const child = spawn(command, {
|
|
558
|
+
cwd: workspaceRoot,
|
|
559
|
+
env: {
|
|
560
|
+
...process.env,
|
|
561
|
+
CI: process.env.CI ?? '1',
|
|
562
|
+
},
|
|
563
|
+
shell: true,
|
|
564
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
565
|
+
windowsHide: true,
|
|
566
|
+
});
|
|
567
|
+
child.stdout.pipe(out, { end: false });
|
|
568
|
+
child.stderr.pipe(out, { end: false });
|
|
569
|
+
let settled = false;
|
|
570
|
+
let timer = null;
|
|
571
|
+
const finish = (report) => {
|
|
572
|
+
if (settled) return;
|
|
573
|
+
settled = true;
|
|
574
|
+
if (timer) clearTimeout(timer);
|
|
575
|
+
out.end();
|
|
576
|
+
resolve(report);
|
|
577
|
+
};
|
|
578
|
+
timer = setTimeout(() => {
|
|
579
|
+
child.kill('SIGTERM');
|
|
580
|
+
finish({
|
|
581
|
+
ok: false,
|
|
582
|
+
status: 'failed',
|
|
583
|
+
exitCode: null,
|
|
584
|
+
error: `Business-scenario command exceeded ${timeoutMs}ms.`,
|
|
585
|
+
logPath,
|
|
586
|
+
});
|
|
587
|
+
}, timeoutMs);
|
|
588
|
+
|
|
589
|
+
child.once('error', (error) => {
|
|
590
|
+
finish({
|
|
591
|
+
ok: false,
|
|
592
|
+
status: 'failed',
|
|
593
|
+
exitCode: null,
|
|
594
|
+
error: error.message,
|
|
595
|
+
logPath,
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
child.once('exit', (code, signal) => {
|
|
599
|
+
finish({
|
|
600
|
+
ok: code === 0,
|
|
601
|
+
status: code === 0 ? 'passed' : 'failed',
|
|
602
|
+
exitCode: code,
|
|
603
|
+
error:
|
|
604
|
+
code === 0
|
|
605
|
+
? null
|
|
606
|
+
: `Business-scenario command exited with ${
|
|
607
|
+
code === null ? `signal ${signal}` : `code ${code}`
|
|
608
|
+
}.`,
|
|
609
|
+
logPath,
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function openBrowser(url, mode) {
|
|
616
|
+
if (mode === 'none') {
|
|
617
|
+
return { attempted: false, ok: true, reason: 'open disabled' };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const { command, args } = buildOpenBrowserCommand(url);
|
|
621
|
+
try {
|
|
622
|
+
return await new Promise((resolve) => {
|
|
623
|
+
let settled = false;
|
|
624
|
+
const child = spawn(command, args, {
|
|
625
|
+
stdio: 'ignore',
|
|
626
|
+
windowsHide: true,
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
const finish = (result) => {
|
|
630
|
+
if (settled) return;
|
|
631
|
+
settled = true;
|
|
632
|
+
resolve({
|
|
633
|
+
attempted: true,
|
|
634
|
+
command: [command, ...args].join(' '),
|
|
635
|
+
...result,
|
|
636
|
+
});
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
const timer = setTimeout(() => {
|
|
640
|
+
child.unref();
|
|
641
|
+
finish({ ok: true, reason: 'browser opener still running after launch window' });
|
|
642
|
+
}, 1500);
|
|
643
|
+
|
|
644
|
+
child.once('error', (error) => {
|
|
645
|
+
clearTimeout(timer);
|
|
646
|
+
finish({ ok: false, error: error.message });
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
child.once('exit', (code, signal) => {
|
|
650
|
+
clearTimeout(timer);
|
|
651
|
+
if (code === 0) {
|
|
652
|
+
finish({ ok: true, reason: 'browser opener exited successfully' });
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
finish({
|
|
656
|
+
ok: false,
|
|
657
|
+
error: `browser opener exited with ${code === null ? `signal ${signal}` : `code ${code}`}`,
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
} catch (error) {
|
|
662
|
+
return { attempted: true, ok: false, error: error.message };
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export async function captureScreenshot(url, outputPath, engine = null) {
|
|
667
|
+
const safeUrl = sanitizePreviewUrl(url);
|
|
668
|
+
let chromium;
|
|
669
|
+
try {
|
|
670
|
+
chromium = engine ?? (await import('playwright')).chromium;
|
|
671
|
+
} catch (error) {
|
|
672
|
+
return {
|
|
673
|
+
ok: false,
|
|
674
|
+
path: null,
|
|
675
|
+
error: `Playwright is not available: ${error.message}`,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
680
|
+
let browser;
|
|
681
|
+
try {
|
|
682
|
+
browser = await chromium.launch({ headless: true });
|
|
683
|
+
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
|
684
|
+
const pageErrors = [];
|
|
685
|
+
page.on('pageerror', () => pageErrors.push('Page script failed'));
|
|
686
|
+
const response = await page.goto(safeUrl, { waitUntil: 'networkidle', timeout: 30000 });
|
|
687
|
+
sanitizePreviewUrl(page.url());
|
|
688
|
+
if (!response || !response.ok()) throw new Error('The preview page returned an error');
|
|
689
|
+
if (new URL(page.url()).origin !== new URL(safeUrl).origin) throw new Error('The preview moved to another app');
|
|
690
|
+
if (!(await page.locator('body').innerText()).trim() || pageErrors.length) throw new Error('The preview is empty or has a script error');
|
|
691
|
+
await page.screenshot({ path: outputPath, fullPage: true });
|
|
692
|
+
return { ok: true, path: outputPath };
|
|
693
|
+
} catch (error) {
|
|
694
|
+
return { ok: false, path: null, error: error.message };
|
|
695
|
+
} finally {
|
|
696
|
+
await browser?.close().catch(() => {});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export function buildReviewLogRow({
|
|
701
|
+
time = new Date().toISOString(),
|
|
702
|
+
change,
|
|
703
|
+
command,
|
|
704
|
+
url,
|
|
705
|
+
browserTarget,
|
|
706
|
+
screenshotPath,
|
|
707
|
+
packageLane = 'unspecified',
|
|
708
|
+
couplingStatus = 'pending service-fit validation',
|
|
709
|
+
storybookStoryIds = 'not recorded',
|
|
710
|
+
themeOverridePoints = 'not recorded',
|
|
711
|
+
selfReview,
|
|
712
|
+
stakeholderFeedback = 'pending',
|
|
713
|
+
changesAccepted = 'pending user feedback',
|
|
714
|
+
openIssues,
|
|
715
|
+
}) {
|
|
716
|
+
return [
|
|
717
|
+
time,
|
|
718
|
+
change,
|
|
719
|
+
command,
|
|
720
|
+
url,
|
|
721
|
+
browserTarget,
|
|
722
|
+
screenshotPath ?? 'not captured',
|
|
723
|
+
packageLane,
|
|
724
|
+
couplingStatus,
|
|
725
|
+
storybookStoryIds,
|
|
726
|
+
themeOverridePoints,
|
|
727
|
+
selfReview,
|
|
728
|
+
stakeholderFeedback,
|
|
729
|
+
changesAccepted,
|
|
730
|
+
openIssues,
|
|
731
|
+
]
|
|
732
|
+
.map(markdownCell)
|
|
733
|
+
.join(' | ');
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function buildReviewLogHeader() {
|
|
737
|
+
const separator = UI_REVIEW_LOG_COLUMNS.map(() => '---');
|
|
738
|
+
return [
|
|
739
|
+
'# UI Review Log',
|
|
740
|
+
'',
|
|
741
|
+
`| ${UI_REVIEW_LOG_COLUMNS.join(' | ')} |`,
|
|
742
|
+
`| ${separator.join(' | ')} |`,
|
|
743
|
+
].join('\n') + '\n';
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function buildPreviewSelfReview({ browser, screenshotPath, status, notes }) {
|
|
747
|
+
const browserStatus =
|
|
748
|
+
browser?.attempted === false
|
|
749
|
+
? 'Browser opening disabled by option.'
|
|
750
|
+
: browser?.ok
|
|
751
|
+
? 'Browser opener launched successfully.'
|
|
752
|
+
: `Browser opener failed: ${browser?.error ?? 'unknown error'}.`;
|
|
753
|
+
const screenshotStatus = screenshotPath
|
|
754
|
+
? `Screenshot captured at ${screenshotPath}.`
|
|
755
|
+
: `Screenshot not captured: ${notes}`;
|
|
756
|
+
return `${browserStatus} ${screenshotStatus} Status: ${status}.`;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function buildPreviewOpenIssues({ browser, screenshotPath, status, notes }) {
|
|
760
|
+
const issues = [];
|
|
761
|
+
if (browser?.attempted !== false && !browser?.ok) {
|
|
762
|
+
issues.push('Browser opener failed; use URL manually or rerun with --no-open.');
|
|
763
|
+
}
|
|
764
|
+
if (!screenshotPath) {
|
|
765
|
+
issues.push(`Screenshot missing: ${notes}`);
|
|
766
|
+
}
|
|
767
|
+
if (status === 'blocked') {
|
|
768
|
+
issues.push('Preview or required business-scenario evidence is blocked; inspect the scenario report and preview log.');
|
|
769
|
+
}
|
|
770
|
+
if (status === 'unverified') issues.push('Current preview checks are incomplete; do not claim readiness.');
|
|
771
|
+
return issues.length > 0 ? issues.join(' ') : 'none';
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function buildBrowserTargetLabel(openMode, browser) {
|
|
775
|
+
if (openMode === 'none') return 'none';
|
|
776
|
+
if (browser?.ok) return `${openMode} (${browser.reason ?? 'opened'})`;
|
|
777
|
+
return `${openMode} (failed)`;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
async function appendReviewLog({
|
|
781
|
+
reviewLogPath,
|
|
782
|
+
change,
|
|
783
|
+
command,
|
|
784
|
+
url,
|
|
785
|
+
browserTarget,
|
|
786
|
+
screenshotPath,
|
|
787
|
+
status,
|
|
788
|
+
notes,
|
|
789
|
+
browser = null,
|
|
790
|
+
packageLane,
|
|
791
|
+
couplingStatus,
|
|
792
|
+
storybookStoryIds,
|
|
793
|
+
themeOverridePoints,
|
|
794
|
+
stakeholderFeedback,
|
|
795
|
+
changesAccepted,
|
|
796
|
+
}) {
|
|
797
|
+
if (!reviewLogPath) return null;
|
|
798
|
+
|
|
799
|
+
await fs.mkdir(path.dirname(reviewLogPath), { recursive: true });
|
|
800
|
+
const handle = await fs.open(reviewLogPath, 'a+', 0o600);
|
|
801
|
+
const header = buildReviewLogHeader();
|
|
802
|
+
const row = buildReviewLogRow({
|
|
803
|
+
change,
|
|
804
|
+
command,
|
|
805
|
+
url,
|
|
806
|
+
browserTarget,
|
|
807
|
+
screenshotPath,
|
|
808
|
+
packageLane,
|
|
809
|
+
couplingStatus,
|
|
810
|
+
storybookStoryIds,
|
|
811
|
+
themeOverridePoints,
|
|
812
|
+
selfReview: buildPreviewSelfReview({ browser, screenshotPath, status, notes }),
|
|
813
|
+
stakeholderFeedback,
|
|
814
|
+
changesAccepted,
|
|
815
|
+
openIssues: buildPreviewOpenIssues({ browser, screenshotPath, status, notes }),
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
try {
|
|
819
|
+
const stats = await handle.stat();
|
|
820
|
+
if (stats.size === 0) {
|
|
821
|
+
await handle.writeFile(header, 'utf8');
|
|
822
|
+
}
|
|
823
|
+
await handle.writeFile(`| ${row} |\n`, 'utf8');
|
|
824
|
+
} finally {
|
|
825
|
+
await handle.close();
|
|
826
|
+
}
|
|
827
|
+
return reviewLogPath;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
export function parseArgs(argv) {
|
|
831
|
+
const options = {
|
|
832
|
+
workspace: process.cwd(),
|
|
833
|
+
featureDir: null,
|
|
834
|
+
command: null,
|
|
835
|
+
url: null,
|
|
836
|
+
open: 'auto',
|
|
837
|
+
screenshot: true,
|
|
838
|
+
timeoutMs: 45000,
|
|
839
|
+
scenarioTimeoutMs: 300000,
|
|
840
|
+
scenarioCommand: null,
|
|
841
|
+
scenarios: 'auto',
|
|
842
|
+
change: 'manual preview refresh',
|
|
843
|
+
json: false,
|
|
844
|
+
dryRun: false,
|
|
845
|
+
help: false,
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
849
|
+
const arg = argv[index];
|
|
850
|
+
const next = () => argv[++index];
|
|
851
|
+
|
|
852
|
+
if (arg === '--workspace') options.workspace = next();
|
|
853
|
+
else if (arg === '--feature-dir') options.featureDir = next();
|
|
854
|
+
else if (arg === '--command') options.command = next();
|
|
855
|
+
else if (arg === '--url') options.url = next();
|
|
856
|
+
else if (arg === '--open') options.open = next();
|
|
857
|
+
else if (arg === '--no-open') options.open = 'none';
|
|
858
|
+
else if (arg === '--screenshot') options.screenshot = true;
|
|
859
|
+
else if (arg === '--no-screenshot') options.screenshot = false;
|
|
860
|
+
else if (arg === '--timeout-ms') options.timeoutMs = Number(next());
|
|
861
|
+
else if (arg === '--scenario-timeout-ms') options.scenarioTimeoutMs = Number(next());
|
|
862
|
+
else if (arg === '--scenario-command') options.scenarioCommand = next();
|
|
863
|
+
else if (arg === '--require-scenarios') options.scenarios = 'required';
|
|
864
|
+
else if (arg === '--skip-scenarios') options.scenarios = 'skip';
|
|
865
|
+
else if (arg === '--change') options.change = next();
|
|
866
|
+
else if (arg === '--json') options.json = true;
|
|
867
|
+
else if (arg === '--dry-run') options.dryRun = true;
|
|
868
|
+
else if (arg === '--help' || arg === '-h') options.help = true;
|
|
869
|
+
else throw new Error(`Unknown option: ${arg}`);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
if (!['auto', 'external', 'none'].includes(options.open)) {
|
|
873
|
+
throw new Error('--open must be auto, external, or none');
|
|
874
|
+
}
|
|
875
|
+
if (!Number.isFinite(options.timeoutMs) || options.timeoutMs < 1000) {
|
|
876
|
+
throw new Error('--timeout-ms must be at least 1000');
|
|
877
|
+
}
|
|
878
|
+
if (!Number.isFinite(options.scenarioTimeoutMs) || options.scenarioTimeoutMs < 1000) {
|
|
879
|
+
throw new Error('--scenario-timeout-ms must be at least 1000');
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return options;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function helpText() {
|
|
886
|
+
return `Gofer UI Preview
|
|
887
|
+
|
|
888
|
+
Starts or reuses a local UI preview, opens it quickly, captures evidence when
|
|
889
|
+
Playwright is available, and appends ui-review-log.md for app-delivery features.
|
|
890
|
+
|
|
891
|
+
Usage:
|
|
892
|
+
node .specify/scripts/node/gofer-ui-preview.mjs [options]
|
|
893
|
+
|
|
894
|
+
Options:
|
|
895
|
+
--workspace <path> Workspace root. Defaults to cwd.
|
|
896
|
+
--feature-dir <path> Feature artifact directory, e.g. .specify/specs/my-feature.
|
|
897
|
+
--command <cmd> Explicit preview command, e.g. "./run.sh dev 3001".
|
|
898
|
+
--url <url> Existing preview URL. Skips server startup.
|
|
899
|
+
--open <auto|external|none>
|
|
900
|
+
Open URL in the host/default browser. Defaults to auto.
|
|
901
|
+
--no-open Do not open a browser.
|
|
902
|
+
--screenshot Capture screenshot evidence with Playwright. Default.
|
|
903
|
+
--no-screenshot Skip screenshot capture.
|
|
904
|
+
--timeout-ms <ms> URL readiness timeout. Defaults to 45000.
|
|
905
|
+
--scenario-command <cmd> Executable browser business-scenario command.
|
|
906
|
+
--require-scenarios Block unless a valid manifest and browser suite pass.
|
|
907
|
+
This is automatic when --feature-dir is present.
|
|
908
|
+
--skip-scenarios Explicitly skip scenario execution (early red work only).
|
|
909
|
+
--scenario-timeout-ms <ms>
|
|
910
|
+
Scenario timeout. Defaults to 300000.
|
|
911
|
+
--change <text> Change trigger recorded in ui-review-log.md.
|
|
912
|
+
--json Print machine-readable output.
|
|
913
|
+
--dry-run Detect command/URLs only; do not start/open/capture.
|
|
914
|
+
`;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
export async function runUiPreview(rawOptions) {
|
|
918
|
+
const workspaceRoot = path.resolve(rawOptions.workspace);
|
|
919
|
+
const paths = resolveFeatureLogPaths(workspaceRoot, rawOptions.featureDir);
|
|
920
|
+
const featureDir = rawOptions.featureDir
|
|
921
|
+
? path.resolve(workspaceRoot, rawOptions.featureDir)
|
|
922
|
+
: null;
|
|
923
|
+
const scenariosRequired =
|
|
924
|
+
rawOptions.scenarios !== 'skip' &&
|
|
925
|
+
(rawOptions.scenarios === 'required' || Boolean(featureDir));
|
|
926
|
+
const scenarioManifest =
|
|
927
|
+
rawOptions.scenarios === 'skip'
|
|
928
|
+
? null
|
|
929
|
+
: await loadBusinessScenarioManifest(workspaceRoot, featureDir);
|
|
930
|
+
const scenarioCommandInfo =
|
|
931
|
+
rawOptions.scenarios === 'skip'
|
|
932
|
+
? null
|
|
933
|
+
: await discoverBusinessScenarioCommand(
|
|
934
|
+
workspaceRoot,
|
|
935
|
+
rawOptions.scenarioCommand,
|
|
936
|
+
scenarioManifest?.manifest?.command
|
|
937
|
+
);
|
|
938
|
+
const commandInfo = rawOptions.url
|
|
939
|
+
? {
|
|
940
|
+
command: rawOptions.command?.trim() || null,
|
|
941
|
+
source: 'explicit-url',
|
|
942
|
+
packageManager: null,
|
|
943
|
+
scriptName: null,
|
|
944
|
+
}
|
|
945
|
+
: await discoverPreviewCommand(workspaceRoot, rawOptions.command);
|
|
946
|
+
const candidateUrls = buildCandidateUrls({
|
|
947
|
+
explicitUrl: rawOptions.url,
|
|
948
|
+
command: commandInfo.command,
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
const baseReport = {
|
|
952
|
+
status: 'planned',
|
|
953
|
+
readyToShow: false,
|
|
954
|
+
workspaceRoot,
|
|
955
|
+
featureDir,
|
|
956
|
+
command: commandInfo.command,
|
|
957
|
+
commandSource: commandInfo.source,
|
|
958
|
+
candidateUrls,
|
|
959
|
+
selectedUrl: null,
|
|
960
|
+
browser: null,
|
|
961
|
+
screenshot: null,
|
|
962
|
+
reviewLogPath: paths.reviewLogPath,
|
|
963
|
+
server: null,
|
|
964
|
+
businessScenarios: {
|
|
965
|
+
required: scenariosRequired,
|
|
966
|
+
manifest: scenarioManifest,
|
|
967
|
+
command: scenarioCommandInfo,
|
|
968
|
+
run: null,
|
|
969
|
+
reportPath: paths.scenarioReportPath,
|
|
970
|
+
},
|
|
971
|
+
nextActions: [],
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
if (rawOptions.dryRun) {
|
|
975
|
+
const scenarioReady =
|
|
976
|
+
!scenariosRequired ||
|
|
977
|
+
(scenarioManifest?.valid &&
|
|
978
|
+
scenarioCommandInfo?.command &&
|
|
979
|
+
scenarioCommandInfo?.browserRunner);
|
|
980
|
+
return {
|
|
981
|
+
...baseReport,
|
|
982
|
+
status:
|
|
983
|
+
(commandInfo.command || rawOptions.url) && scenarioReady ? 'planned' : 'blocked',
|
|
984
|
+
nextActions:
|
|
985
|
+
(commandInfo.command || rawOptions.url) && scenarioReady
|
|
986
|
+
? ['Run without --dry-run after a UI-facing change.']
|
|
987
|
+
: [
|
|
988
|
+
'Provide a preview command/URL and a valid business-scenarios.json.',
|
|
989
|
+
'Add a Playwright/Cypress/browser package script or pass --scenario-command.',
|
|
990
|
+
],
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (!rawOptions.url && !commandInfo.command) {
|
|
995
|
+
return {
|
|
996
|
+
...baseReport,
|
|
997
|
+
status: 'blocked',
|
|
998
|
+
nextActions: [
|
|
999
|
+
'Pass --command or --url, add run.sh/run.bat, or add a package.json dev/start/preview script.',
|
|
1000
|
+
'Record the chosen preview command in ui-preview-brief.md.',
|
|
1001
|
+
],
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
let server = null;
|
|
1006
|
+
if (!rawOptions.url) {
|
|
1007
|
+
const portCheck = await checkPreviewPortAvailable(candidateUrls[0]);
|
|
1008
|
+
if (!portCheck.ok) {
|
|
1009
|
+
return { ...baseReport, status: 'blocked', portCheck,
|
|
1010
|
+
nextActions: ['The chosen connection is in use or cannot be checked. No app was stopped and no runner was started.',
|
|
1011
|
+
'Verify that the process belongs to this exact app before restarting it. Leave other or unknown apps running and ask the user.',
|
|
1012
|
+
'Use --url only for an existing preview that you have confirmed belongs to this app.'] };
|
|
1013
|
+
}
|
|
1014
|
+
server = await startPreviewServer(commandInfo.command, workspaceRoot, paths.processLogPath, paths.pidPath);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const readiness = await waitForReachableUrl(candidateUrls, rawOptions.timeoutMs);
|
|
1018
|
+
if (!readiness.ok) {
|
|
1019
|
+
const report = {
|
|
1020
|
+
...baseReport,
|
|
1021
|
+
status: 'blocked',
|
|
1022
|
+
server,
|
|
1023
|
+
nextActions: [
|
|
1024
|
+
`No preview URL became reachable. Check ${paths.processLogPath}.`,
|
|
1025
|
+
'Pass --url when the app uses a non-standard host or port.',
|
|
1026
|
+
],
|
|
1027
|
+
};
|
|
1028
|
+
await appendReviewLog({
|
|
1029
|
+
reviewLogPath: paths.reviewLogPath,
|
|
1030
|
+
change: rawOptions.change,
|
|
1031
|
+
command: commandInfo.command ?? 'existing URL',
|
|
1032
|
+
url: candidateUrls.join(', '),
|
|
1033
|
+
browserTarget: rawOptions.open,
|
|
1034
|
+
screenshotPath: null,
|
|
1035
|
+
status: 'blocked',
|
|
1036
|
+
notes: 'No candidate URL became reachable before timeout.',
|
|
1037
|
+
browser: {
|
|
1038
|
+
attempted: rawOptions.open !== 'none',
|
|
1039
|
+
ok: false,
|
|
1040
|
+
error: 'preview URL not reachable',
|
|
1041
|
+
},
|
|
1042
|
+
});
|
|
1043
|
+
return report;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const selectedUrl = candidateUrls[readiness.urlIndex];
|
|
1047
|
+
const browser = await openBrowser(selectedUrl, rawOptions.open);
|
|
1048
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1049
|
+
let screenshot = { ok: false, path: null, error: 'Screenshot disabled' };
|
|
1050
|
+
if (rawOptions.screenshot) {
|
|
1051
|
+
screenshot = await captureScreenshot(
|
|
1052
|
+
selectedUrl,
|
|
1053
|
+
path.join(paths.previewDir, `ui-preview-${timestamp}.png`)
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
let scenarioRun = {
|
|
1058
|
+
ok: rawOptions.scenarios === 'skip',
|
|
1059
|
+
status: rawOptions.scenarios === 'skip' ? 'skipped' : 'not-run',
|
|
1060
|
+
exitCode: null,
|
|
1061
|
+
error: null,
|
|
1062
|
+
logPath: null,
|
|
1063
|
+
};
|
|
1064
|
+
const scenarioPreflightErrors = [];
|
|
1065
|
+
if (rawOptions.scenarios !== 'skip') {
|
|
1066
|
+
if (!scenarioManifest?.valid) {
|
|
1067
|
+
scenarioPreflightErrors.push(...(scenarioManifest?.errors ?? []));
|
|
1068
|
+
}
|
|
1069
|
+
if (!scenarioCommandInfo?.command) {
|
|
1070
|
+
scenarioPreflightErrors.push('No executable business-scenario command was found.');
|
|
1071
|
+
} else if (!scenarioCommandInfo.browserRunner) {
|
|
1072
|
+
scenarioPreflightErrors.push(
|
|
1073
|
+
'The business-scenario command does not use a recognized browser runner.'
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
if (scenarioPreflightErrors.length === 0) {
|
|
1078
|
+
scenarioRun = await runBusinessScenarioCommand(
|
|
1079
|
+
scenarioCommandInfo.command,
|
|
1080
|
+
workspaceRoot,
|
|
1081
|
+
paths.scenarioLogPath,
|
|
1082
|
+
rawOptions.scenarioTimeoutMs
|
|
1083
|
+
);
|
|
1084
|
+
} else {
|
|
1085
|
+
scenarioRun = {
|
|
1086
|
+
ok: false,
|
|
1087
|
+
status: 'blocked',
|
|
1088
|
+
exitCode: null,
|
|
1089
|
+
error: scenarioPreflightErrors.join(' '),
|
|
1090
|
+
logPath: null,
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const businessScenarios = {
|
|
1096
|
+
required: scenariosRequired,
|
|
1097
|
+
manifest: scenarioManifest,
|
|
1098
|
+
command: scenarioCommandInfo,
|
|
1099
|
+
run: scenarioRun,
|
|
1100
|
+
reportPath: paths.scenarioReportPath,
|
|
1101
|
+
};
|
|
1102
|
+
await fs.mkdir(path.dirname(paths.scenarioReportPath), { recursive: true });
|
|
1103
|
+
await fs.writeFile(
|
|
1104
|
+
paths.scenarioReportPath,
|
|
1105
|
+
`${JSON.stringify(
|
|
1106
|
+
{
|
|
1107
|
+
generatedAt: new Date().toISOString(),
|
|
1108
|
+
workspaceRoot,
|
|
1109
|
+
featureDir,
|
|
1110
|
+
selectedUrl,
|
|
1111
|
+
status: scenarioRun.status === 'passed' ? 'passed' : scenarioRun.status === 'skipped' ? 'skipped' : 'blocked',
|
|
1112
|
+
manifestPath: scenarioManifest?.path ?? null,
|
|
1113
|
+
scenarioCount: scenarioManifest?.scenarioCount ?? 0,
|
|
1114
|
+
command: scenarioCommandInfo?.command ?? null,
|
|
1115
|
+
browserRunner: scenarioCommandInfo?.browserRunner ?? false,
|
|
1116
|
+
run: scenarioRun,
|
|
1117
|
+
},
|
|
1118
|
+
null,
|
|
1119
|
+
2
|
|
1120
|
+
)}\n`,
|
|
1121
|
+
{ encoding: 'utf8', mode: 0o600 }
|
|
1122
|
+
);
|
|
1123
|
+
|
|
1124
|
+
const scenarioBlocked =
|
|
1125
|
+
scenariosRequired &&
|
|
1126
|
+
(!scenarioManifest?.valid ||
|
|
1127
|
+
!scenarioCommandInfo?.command ||
|
|
1128
|
+
!scenarioCommandInfo?.browserRunner ||
|
|
1129
|
+
!scenarioRun.ok);
|
|
1130
|
+
const readyToShow = readiness.ok && screenshot.ok && scenarioRun.status === 'passed';
|
|
1131
|
+
const previewStatus = readyToShow ? (browser.attempted && browser.ok ? 'shown' : 'verified') : 'unverified';
|
|
1132
|
+
const status = scenarioBlocked ? 'blocked' : previewStatus;
|
|
1133
|
+
|
|
1134
|
+
const reviewLogPath = await appendReviewLog({
|
|
1135
|
+
reviewLogPath: paths.reviewLogPath,
|
|
1136
|
+
change: rawOptions.change,
|
|
1137
|
+
command: commandInfo.command ?? 'existing URL',
|
|
1138
|
+
url: selectedUrl,
|
|
1139
|
+
browserTarget: buildBrowserTargetLabel(rawOptions.open, browser),
|
|
1140
|
+
screenshotPath: screenshot.path,
|
|
1141
|
+
status,
|
|
1142
|
+
notes: [
|
|
1143
|
+
screenshot.ok ? 'Preview opened and screenshot captured.' : screenshot.error,
|
|
1144
|
+
`Business scenarios: ${scenarioRun.status}.`,
|
|
1145
|
+
scenarioRun.error,
|
|
1146
|
+
]
|
|
1147
|
+
.filter(Boolean)
|
|
1148
|
+
.join(' '),
|
|
1149
|
+
browser,
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
return {
|
|
1153
|
+
...baseReport,
|
|
1154
|
+
status,
|
|
1155
|
+
readyToShow: !scenarioBlocked && readyToShow,
|
|
1156
|
+
selectedUrl,
|
|
1157
|
+
browser,
|
|
1158
|
+
screenshot,
|
|
1159
|
+
reviewLogPath,
|
|
1160
|
+
server,
|
|
1161
|
+
businessScenarios,
|
|
1162
|
+
nextActions: [
|
|
1163
|
+
readyToShow ? 'Report the checked preview URL, evidence and behaviour tested; do not claim the whole feature is complete.'
|
|
1164
|
+
: 'The preview is not verified. Explain the missing or failing checks; do not say it is ready or working.',
|
|
1165
|
+
scenarioBlocked
|
|
1166
|
+
? 'Fix the business-scenario manifest or browser suite, then rerun this helper.'
|
|
1167
|
+
: 'After the next UI-facing change, rerun this helper and browser scenarios before reporting completion.',
|
|
1168
|
+
],
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function printReport(report, json) {
|
|
1173
|
+
if (json) {
|
|
1174
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
console.log(`Gofer UI preview: ${report.status}`);
|
|
1179
|
+
if (report.command) console.log(`Command: ${report.command}`);
|
|
1180
|
+
if (report.selectedUrl) console.log(`URL: ${report.selectedUrl}`);
|
|
1181
|
+
if (report.screenshot?.path) console.log(`Screenshot: ${report.screenshot.path}`);
|
|
1182
|
+
if (report.reviewLogPath) console.log(`Review log: ${report.reviewLogPath}`);
|
|
1183
|
+
if (report.businessScenarios?.reportPath) {
|
|
1184
|
+
console.log(`Business scenarios: ${report.businessScenarios.run?.status ?? 'not-run'}`);
|
|
1185
|
+
console.log(`Scenario report: ${report.businessScenarios.reportPath}`);
|
|
1186
|
+
}
|
|
1187
|
+
if (report.server?.logPath) console.log(`Server log: ${report.server.logPath}`);
|
|
1188
|
+
for (const action of report.nextActions ?? []) {
|
|
1189
|
+
console.log(`Next: ${action}`);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
async function main() {
|
|
1194
|
+
try {
|
|
1195
|
+
const options = parseArgs(process.argv.slice(2));
|
|
1196
|
+
if (options.help) {
|
|
1197
|
+
console.log(helpText());
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const report = await runUiPreview(options);
|
|
1202
|
+
printReport(report, options.json);
|
|
1203
|
+
process.exitCode = ['blocked', 'unverified'].includes(report.status) ? 2 : 0;
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
console.error(`Gofer UI preview failed: ${error.message}`);
|
|
1206
|
+
process.exitCode = 1;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
|
|
1211
|
+
await main();
|
|
1212
|
+
}
|