@polymorphism-tech/morph-spec 9.0.0 → 9.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +8 -9
- package/README.md +38 -36
- package/bin/morph-spec.js +11 -63
- package/bin/validate.js +1 -39
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -1
- package/framework/MORPH.md +3 -3
- package/framework/agents.json +19 -11
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +22 -31
- package/framework/evals/review-checklist.md +12 -2
- package/framework/hooks/README.md +8 -44
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
- package/framework/hooks/claude-code/core/chain.js +4 -65
- package/framework/hooks/claude-code/core/comment-guard.js +2 -16
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
- package/framework/hooks/claude-code/core/gate-guard.js +7 -67
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
- package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
- package/framework/hooks/claude-code/core/heavy-release.js +2 -13
- package/framework/hooks/claude-code/core/loop-detect.js +2 -31
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +0 -4
- package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
- package/framework/hooks/claude-code/core/state-sync.js +12 -68
- package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
- package/framework/hooks/claude-code/core/validate-completion.js +11 -57
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
- package/framework/hooks/claude-code/core/worktree-context.js +10 -110
- package/framework/hooks/claude-code/core/worktree-create.js +6 -70
- package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
- package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/shared/activity-logger.js +9 -89
- package/framework/hooks/shared/agent-reap.js +2 -17
- package/framework/hooks/shared/build-check.js +6 -75
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +6 -51
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +6 -25
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +2 -15
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +1 -3
- package/framework/hooks/shared/gate-question.js +1 -7
- package/framework/hooks/shared/git-exclude.js +3 -20
- package/framework/hooks/shared/git-paths.js +8 -104
- package/framework/hooks/shared/graph-freshness.js +22 -166
- package/framework/hooks/shared/graph-refresh.js +21 -149
- package/framework/hooks/shared/heavy-command.js +4 -12
- package/framework/hooks/shared/hook-response.js +8 -83
- package/framework/hooks/shared/morph-cli.js +8 -52
- package/framework/hooks/shared/payload-utils.js +10 -132
- package/framework/hooks/shared/phase-utils.js +1 -42
- package/framework/hooks/shared/redact-secrets.js +4 -15
- package/framework/hooks/shared/reparse-scan.js +6 -66
- package/framework/hooks/shared/slice-progress.js +4 -23
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/state-reader.js +13 -183
- package/framework/hooks/shared/state-writer.js +8 -52
- package/framework/hooks/shared/stdin-reader.js +1 -11
- package/framework/hooks/shared/telemetry-logger.js +7 -67
- package/framework/hooks/shared/transcript-usage.js +7 -29
- package/framework/hooks/shared/worktree-root.js +7 -29
- package/framework/rules/csharp-standards.md +1 -1
- package/framework/rules/frontend-standards.md +1 -0
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/neon-standards.md +1 -1
- package/framework/rules/nextjs-standards.md +1 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +4 -4
- package/framework/skills/core/morph-hotfix/SKILL.md +3 -3
- package/framework/skills/core/morph-machine/SKILL.md +1 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +9 -7
- package/framework/skills/core/morph-review/SKILL.md +9 -6
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +29 -12
- package/framework/standards/STANDARDS.json +36 -14
- package/framework/standards/STANDARDS.meta.json +16 -2
- package/framework/standards/ai-agents/agent-archetypes.md +2 -2
- package/framework/standards/ai-agents/agent-session.md +1 -1
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
- package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
- package/framework/standards/ai-agents/guardrails.md +1 -1
- package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
- package/framework/standards/ai-agents/observability-patterns.md +1 -1
- package/framework/standards/ai-agents/production.md +1 -1
- package/framework/standards/ai-agents/prompt-sources.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
- package/framework/standards/ai-agents/setup.md +2 -2
- package/framework/standards/ai-agents/sweet-spot.md +2 -2
- package/framework/standards/ai-agents/tool-adapters.md +0 -2
- package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
- package/framework/standards/ai-agents/workflows.md +1 -1
- package/framework/standards/architecture/code-comments.md +1 -1
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
- package/framework/standards/backend/api/validation.md +1 -1
- package/framework/standards/backend/dotnet/testing.md +10 -4
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
- package/framework/standards/data/nosql/cache/redis.md +1 -1
- package/framework/standards/data/nosql/cosmos-db.md +3 -3
- package/framework/standards/frontend/admin-app/forms.md +1 -1
- package/framework/standards/frontend/design-system/art-direction.md +88 -0
- package/framework/standards/frontend/design-system/premium-finish.md +77 -2
- package/framework/standards/frontend/nextjs/testing.md +10 -6
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +9 -9
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/docs/plan.md +3 -1
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/context/context-refresh.js +0 -16
- package/src/commands/dashboard/dashboard.js +2 -8
- package/src/commands/e2e/e2e.js +17 -104
- package/src/commands/graph/affected.js +0 -14
- package/src/commands/graph/build.js +11 -50
- package/src/commands/graph/explain.js +0 -16
- package/src/commands/graph/status.js +1 -12
- package/src/commands/mcp/mcp-setup.js +4 -62
- package/src/commands/observability/cost.js +5 -57
- package/src/commands/observability/telemetry.js +1 -14
- package/src/commands/project/branch.js +4 -45
- package/src/commands/project/claim.js +4 -48
- package/src/commands/project/doctor.js +48 -302
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +3 -32
- package/src/commands/project/init.js +4 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/standards.js +1 -62
- package/src/commands/project/status.js +0 -53
- package/src/commands/project/update.js +31 -239
- package/src/commands/project/worktree.js +108 -882
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +14 -85
- package/src/commands/state/approve.js +15 -97
- package/src/commands/state/archive.js +12 -64
- package/src/commands/state/auto-approval.js +2 -5
- package/src/commands/state/create.js +22 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +98 -475
- package/src/commands/state/gate-check.js +22 -132
- package/src/commands/state/gate-decision.js +3 -39
- package/src/commands/state/handoff.js +3 -30
- package/src/commands/state/list.js +14 -84
- package/src/commands/state/report-check.js +8 -58
- package/src/commands/state/retype.js +4 -26
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +11 -11
- package/src/commands/validation/plan-check.js +4 -2
- package/src/commands/validation/tests-audit.js +7 -0
- package/src/commands/validation/validate-feature.js +10 -97
- package/src/commands/validation/verify.js +30 -113
- package/src/core/paths/output-schema.js +2 -101
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +0 -27
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +22 -304
- package/src/core/templates/template-registry.js +2 -97
- package/src/core/templates/template-renderer.js +13 -93
- package/src/core/templates/template-seed.js +0 -1
- package/src/core/work-types.js +1 -36
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
- package/src/dashboard/app/components/CommandPalette.jsx +0 -7
- package/src/dashboard/app/components/DetailPanel.jsx +1 -5
- package/src/dashboard/app/components/EntityLink.jsx +0 -12
- package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
- package/src/dashboard/app/components/Markdown.jsx +0 -9
- package/src/dashboard/app/components/PhaseRail.jsx +0 -11
- package/src/dashboard/app/components/SearchFilter.jsx +0 -4
- package/src/dashboard/app/components/Sidebar.jsx +0 -5
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
- package/src/dashboard/app/hooks/usePref.js +1 -5
- package/src/dashboard/app/lib/format.js +0 -2
- package/src/dashboard/app/lib/phases.js +1 -5
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
- package/src/dashboard/app/pages/Features.jsx +1 -8
- package/src/dashboard/app/pages/Guide.jsx +0 -1
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
- package/src/dashboard/app/pages/Portfolio.jsx +0 -1
- package/src/dashboard/app/pages/RulesMap.jsx +1 -16
- package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
- package/src/dashboard/app/pages/Standards.jsx +2 -17
- package/src/dashboard/scanner/activity-scanner.js +10 -69
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +0 -6
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +10 -66
- package/src/dashboard/scanner/hooks-scanner.js +5 -28
- package/src/dashboard/scanner/index.js +0 -4
- package/src/dashboard/scanner/portfolio.js +5 -29
- package/src/dashboard/scanner/project-api.js +8 -54
- package/src/dashboard/scanner/rules-scanner.js +1 -2
- package/src/dashboard/scanner/skills-scanner.js +2 -12
- package/src/dashboard/scanner/standards-scanner.js +2 -6
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/ai/ai-checks.js +7 -52
- package/src/lib/ai/ai-project-scan.js +14 -108
- package/src/lib/ai/code-audit.js +7 -63
- package/src/lib/ai/pin-drift.js +8 -51
- package/src/lib/ai/registry-audit.js +11 -90
- package/src/lib/ci/ci-verify.js +4 -19
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +14 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +4 -50
- package/src/lib/e2e/applicability.js +37 -0
- package/src/lib/e2e/compose-env.js +5 -66
- package/src/lib/e2e/stack-teardown.js +6 -40
- package/src/lib/generators/architecture-scanner.js +18 -156
- package/src/lib/generators/handoff-generator.js +9 -53
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +4 -37
- package/src/lib/git/branch-classifier.js +11 -84
- package/src/lib/git/default-branch.js +14 -169
- package/src/lib/git/evidence-staleness.js +1 -0
- package/src/lib/git/gh.js +2 -17
- package/src/lib/git/pending-prs.js +6 -37
- package/src/lib/git/pr-files.js +3 -28
- package/src/lib/git/pr-index.js +3 -34
- package/src/lib/git/tree-fingerprint.js +3 -15
- package/src/lib/git/tree-state.js +6 -33
- package/src/lib/graph/god-nodes.js +2 -22
- package/src/lib/graph/graph-health.js +3 -24
- package/src/lib/graph/graph-loader.js +5 -56
- package/src/lib/graph/graphify-runtime.js +7 -37
- package/src/lib/graph/graphifyignore.js +8 -74
- package/src/lib/graph/sanitize.js +16 -136
- package/src/lib/heavy/cleanup.js +1 -1
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +4 -27
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/adapters.js +6 -17
- package/src/lib/mutation/dotnet-introspection.js +20 -106
- package/src/lib/mutation/runner.js +23 -75
- package/src/lib/mutation/subset.js +1 -4
- package/src/lib/mutation/trx.js +0 -1
- package/src/lib/plan/artifact-vocabulary.js +5 -25
- package/src/lib/plan/checks.js +34 -33
- package/src/lib/plan/plan-parser.js +7 -8
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +11 -26
- package/src/lib/risk-detector.js +41 -173
- package/src/lib/specs/adr-index.js +6 -34
- package/src/lib/specs/frontmatter.js +2 -15
- package/src/lib/specs/gfm-table.js +1 -7
- package/src/lib/specs/revocations.js +1 -14
- package/src/lib/specs/screen-checklist.js +47 -124
- package/src/lib/stack/dotnet-build-ignores.js +2 -6
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +1 -8
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +1 -7
- package/src/lib/standards/sections.js +2 -13
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +21 -169
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -240
- package/src/lib/tasks/test-runner.js +43 -341
- package/src/lib/telemetry/metrics.js +7 -36
- package/src/lib/telemetry/telemetry-reader.js +13 -125
- package/src/lib/telemetry/verify-event.js +2 -30
- package/src/lib/tests-audit/audit.js +96 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
- package/src/lib/validators/code-budget/index.js +0 -7
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
- package/src/lib/validators/contracts/index.js +0 -6
- package/src/lib/validators/nextjs/index.js +0 -5
- package/src/lib/validators/nextjs/next-component-validator.js +4 -81
- package/src/lib/validators/packages/index.js +0 -6
- package/src/lib/validators/packages/package-validator.js +6 -56
- package/src/lib/validators/reuse/index.js +0 -8
- package/src/lib/validators/reuse/reuse-validator.js +11 -101
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +5 -63
- package/src/lib/validators/validation-runner.js +21 -158
- package/src/lib/validators/verify-runner.js +85 -376
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -136
- package/src/lib/worktree/env-allocator.js +10 -98
- package/src/lib/worktree/feature-truth.js +12 -115
- package/src/lib/worktree/finish-backup.js +32 -242
- package/src/lib/worktree/fleet-scanner.js +13 -95
- package/src/lib/worktree/ignored-triage.js +15 -91
- package/src/lib/worktree/vscode-settings.js +2 -3
- package/src/lib/worktree/worktree-audit.js +21 -213
- package/src/lib/worktree/worktree-include.js +3 -5
- package/src/scripts/global-install.js +3 -9
- package/src/scripts/install-plugin.js +1 -13
- package/src/scripts/setup-infra.js +17 -147
- package/src/utils/agents-installer.js +29 -276
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -52
- package/src/utils/file-copier.js +11 -95
- package/src/utils/hooks-installer.js +21 -298
- package/src/utils/skills-installer.js +4 -101
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -9
- package/framework/hooks/git/commit-msg/conventional-commits.sh +0 -33
- package/framework/hooks/git/pre-commit/agents.sh +0 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +0 -64
- package/framework/hooks/git/pre-push/run-tests.sh +0 -44
- package/src/commands/utils/index.js +0 -3
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/old-format.js +0 -68
- package/src/utils/agent-id-mapper.js +0 -11
package/src/lib/ci/github-ci.js
CHANGED
|
@@ -21,14 +21,8 @@ function gh(cwd, args, timeout = 15000) {
|
|
|
21
21
|
return execFileSync(bin, [...prefix, ...args], { cwd, stdio: 'pipe', timeout }).toString();
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* publish its JSON, because only that artifact becomes a stamp. A green run of any other
|
|
27
|
-
* workflow (lint, deploy) proves nothing about the suite.
|
|
28
|
-
*
|
|
29
|
-
* @param {string} root
|
|
30
|
-
* @returns {string|null} workflow file name, as `gh run list --workflow` takes it
|
|
31
|
-
*/
|
|
24
|
+
// Must call `morph-spec verify` AND publish its JSON: only that artifact becomes a stamp, and a green
|
|
25
|
+
// lint or deploy run proves nothing about the suite.
|
|
32
26
|
export function findCiWorkflow(root) {
|
|
33
27
|
const dir = join(root, CI_WORKFLOW_DIR);
|
|
34
28
|
if (!existsSync(dir)) return null;
|
|
@@ -69,15 +63,7 @@ function ghAuthText(root) {
|
|
|
69
63
|
return `${res.stdout ?? ''}${res.stderr ?? ''}`;
|
|
70
64
|
}
|
|
71
65
|
|
|
72
|
-
/**
|
|
73
|
-
* Whether CI can answer for this feature, and why not. Never throws: every "no" is a reason the
|
|
74
|
-
* caller prints before falling back to the local verify.
|
|
75
|
-
*
|
|
76
|
-
* @param {string} root - the feature's tree
|
|
77
|
-
* @param {string} featureName
|
|
78
|
-
* @param {object|null} config - `.morph/config/config.json`
|
|
79
|
-
* @returns {{ ok: true, branch: string, https: boolean, scopes: string|null } | { ok: false, reason: string }}
|
|
80
|
-
*/
|
|
66
|
+
/** Never throws: every "no" is a reason the caller prints before falling back to the local verify. */
|
|
81
67
|
export function ciAvailability(root, featureName, config) {
|
|
82
68
|
if (config?.project?.ci === false) return { ok: false, reason: 'project.ci = false no config.json' };
|
|
83
69
|
const url = originUrl(root);
|
|
@@ -93,21 +79,14 @@ export function ciAvailability(root, featureName, config) {
|
|
|
93
79
|
return { ok: true, branch, https: /^https?:/i.test(url), scopes };
|
|
94
80
|
}
|
|
95
81
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
* rejects the whole push. Unknown scopes (fine-grained token, SSH remote) are not a refusal.
|
|
99
|
-
*/
|
|
82
|
+
// Creating a workflow file over HTTPS needs the token's `workflow` scope, or GitHub rejects the whole push.
|
|
83
|
+
// Unknown scopes (fine-grained token, SSH remote) are not a refusal.
|
|
100
84
|
export function workflowPushProblem({ https, scopes }) {
|
|
101
85
|
if (!https || scopes === null) return null;
|
|
102
86
|
return /\bworkflow\b/.test(scopes) ? null : 'o token do gh não tem o escopo "workflow" (gh auth refresh -s workflow)';
|
|
103
87
|
}
|
|
104
88
|
|
|
105
|
-
/**
|
|
106
|
-
* Paths CI would not see: uncommitted or untracked files outside `.morph/`, plus the config,
|
|
107
|
-
* which names the test commands.
|
|
108
|
-
*
|
|
109
|
-
* @returns {string[]|null} null when git did not answer
|
|
110
|
-
*/
|
|
89
|
+
/** Uncommitted or untracked files outside `.morph/`, plus the config; null when git did not answer. */
|
|
111
90
|
export function uncommittedCode(root) {
|
|
112
91
|
let out;
|
|
113
92
|
try {
|
|
@@ -138,10 +117,8 @@ export function listRuns(root, branch, workflow) {
|
|
|
138
117
|
}
|
|
139
118
|
}
|
|
140
119
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
* the review agents and the archive commit only `.morph/`, and the workflow does not re-run for them.
|
|
144
|
-
*/
|
|
120
|
+
// `isCurrent` compares trees, not SHAs: the review agents and the archive commit only `.morph/`,
|
|
121
|
+
// and the workflow does not re-run for them.
|
|
145
122
|
export function pickRun(runs, isCurrent) {
|
|
146
123
|
return (runs || []).find((run) => !(run.status === 'completed' && NO_VERDICT.has(run.conclusion))
|
|
147
124
|
&& run.headSha && isCurrent(run.headSha)) ?? null;
|
|
@@ -160,12 +137,7 @@ export function downloadResult(root, runId) {
|
|
|
160
137
|
}
|
|
161
138
|
}
|
|
162
139
|
|
|
163
|
-
/**
|
|
164
|
-
* Polls until the run over the current tree completes. Never decides: the caller turns
|
|
165
|
-
* `timeout`/`no-run`/`unknown` into "no CI proof", never into red.
|
|
166
|
-
*
|
|
167
|
-
* @returns {Promise<{ state: 'completed'|'timeout'|'no-run'|'unknown', run?: object }>}
|
|
168
|
-
*/
|
|
140
|
+
/** Never decides: the caller turns `timeout`/`no-run`/`unknown` into "no CI proof", never into red. */
|
|
169
141
|
export async function waitForRun(root, { branch, workflow, isCurrent, timeoutMs, appearMs, pollMs = 15000,
|
|
170
142
|
sleep = (ms) => new Promise((r) => setTimeout(r, ms)), now = Date.now, onPending = () => {}, list = listRuns }) {
|
|
171
143
|
const start = now();
|
|
@@ -2,6 +2,7 @@ import { existsSync, readdirSync } from 'fs';
|
|
|
2
2
|
import { join, posix, relative, resolve } from 'path';
|
|
3
3
|
import { PACKAGE_LOCATIONS } from '../detectors/index.js';
|
|
4
4
|
import { CI_ARTIFACT, CI_RESULT_FILE } from './github-ci.js';
|
|
5
|
+
import { discoverDotnetSolution } from '../tasks/test-runner.js';
|
|
5
6
|
|
|
6
7
|
const LOCKFILES = [
|
|
7
8
|
['package-lock.json', 'npm ci'],
|
|
@@ -50,22 +51,24 @@ function dotnetSetup(root, config) {
|
|
|
50
51
|
];
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
// The verify build node runs `dotnet build --no-restore` (right on a warm tree): a fresh checkout restores first,
|
|
55
|
+
// against the same target the build node resolves.
|
|
56
|
+
function dotnetRestore(root, config) {
|
|
57
|
+
if (!usesDotnet(root, config)) return [];
|
|
58
|
+
const backend = config?.project?.backendPath;
|
|
59
|
+
if (typeof backend === 'string') return step('dotnet restore', backend, 'dotnet restore');
|
|
60
|
+
const { target } = discoverDotnetSolution(root);
|
|
61
|
+
return step('dotnet restore', '.', target && target !== '.' ? `dotnet restore "${target}"` : 'dotnet restore');
|
|
62
|
+
}
|
|
63
|
+
|
|
53
64
|
const step = (name, dir, run) => [
|
|
54
65
|
` - name: ${name}`,
|
|
55
66
|
...(dir === '.' ? [] : [` working-directory: ${posix.normalize(dir)}`]),
|
|
56
67
|
` run: ${run}`,
|
|
57
68
|
];
|
|
58
69
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
* pinned morph-spec, so the suite is whatever `project.tests` says, not a second definition here.
|
|
62
|
-
* `.morph/` is out of the trigger (bookkeeping commits would cancel the run over the code) except
|
|
63
|
-
* the config, which names the commands.
|
|
64
|
-
*
|
|
65
|
-
* @param {string} root
|
|
66
|
-
* @param {object|null} config
|
|
67
|
-
* @returns {string}
|
|
68
|
-
*/
|
|
70
|
+
// Calls the project's pinned morph-spec, so the suite is whatever `project.tests` says. `.morph/` is out of
|
|
71
|
+
// the trigger (bookkeeping commits would cancel the run over the code) except the config.
|
|
69
72
|
export function renderCiWorkflow(root, config) {
|
|
70
73
|
const packages = packageDirs(root, config);
|
|
71
74
|
const lines = [
|
|
@@ -102,6 +105,7 @@ export function renderCiWorkflow(root, config) {
|
|
|
102
105
|
lines.push(...step(`install (${pkg.dir})`, pkg.dir, pkg.install));
|
|
103
106
|
if (pkg.playwright) lines.push(...step(`playwright browsers (${pkg.dir})`, pkg.dir, 'npx playwright install --with-deps chromium'));
|
|
104
107
|
}
|
|
108
|
+
lines.push(...dotnetRestore(root, config));
|
|
105
109
|
lines.push(
|
|
106
110
|
' - name: morph-spec verify',
|
|
107
111
|
' shell: bash',
|
|
@@ -1,24 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Claude Config Detector
|
|
3
|
-
*
|
|
4
|
-
* Scans .claude/ settings and plugins for MCP servers, hooks, skills, and commands.
|
|
5
|
-
* Used by mcp-setup and session-start hooks.
|
|
6
|
-
*
|
|
7
|
-
* @module detectors/claude-config-detector
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
1
|
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
11
2
|
import { join } from 'path';
|
|
12
3
|
import { homedir } from 'os';
|
|
13
4
|
|
|
14
|
-
|
|
15
|
-
* Plugin ids (`name@marketplace`) that are switched ON. User settings first, then
|
|
16
|
-
* the project's; a later file overrides an earlier one, and `false` turns one off.
|
|
17
|
-
*
|
|
18
|
-
* @param {string} targetPath - Root path of the project
|
|
19
|
-
* @param {string} [home] - Home directory holding `.claude/settings.json`
|
|
20
|
-
* @returns {string[]}
|
|
21
|
-
*/
|
|
5
|
+
// User settings first, then the project's: a later file overrides, and `false` turns a plugin off.
|
|
22
6
|
export function readEnabledPlugins(targetPath, home = homedir()) {
|
|
23
7
|
const merged = new Map();
|
|
24
8
|
const files = [
|
|
@@ -32,17 +16,11 @@ export function readEnabledPlugins(targetPath, home = homedir()) {
|
|
|
32
16
|
const map = JSON.parse(readFileSync(file, 'utf8').replace(/^/, '')).enabledPlugins;
|
|
33
17
|
if (!map || typeof map !== 'object') continue;
|
|
34
18
|
for (const [id, on] of Object.entries(map)) merged.set(id, on === true);
|
|
35
|
-
} catch {
|
|
19
|
+
} catch {}
|
|
36
20
|
}
|
|
37
21
|
return [...merged].filter(([, on]) => on).map(([id]) => id);
|
|
38
22
|
}
|
|
39
23
|
|
|
40
|
-
/**
|
|
41
|
-
* Detect Claude Code configuration for a project.
|
|
42
|
-
*
|
|
43
|
-
* @param {string} targetPath - Root path of the project
|
|
44
|
-
* @returns {{ plugins: string[], enabledPlugins: string[], mcpServers: Array<{name: string, config: Object}>, customSkills: string[], customCommands: string[], hooks: string[] }}
|
|
45
|
-
*/
|
|
46
24
|
export function detectClaudeConfig(targetPath) {
|
|
47
25
|
const result = {
|
|
48
26
|
plugins: [],
|
|
@@ -58,7 +36,6 @@ export function detectClaudeConfig(targetPath) {
|
|
|
58
36
|
const mcpJsonNames = new Set();
|
|
59
37
|
|
|
60
38
|
try {
|
|
61
|
-
// ── MCP Servers from .mcp.json (primary — Claude Code reads this) ───────
|
|
62
39
|
const mcpJsonPath = join(targetPath, '.mcp.json');
|
|
63
40
|
if (existsSync(mcpJsonPath)) {
|
|
64
41
|
try {
|
|
@@ -68,10 +45,9 @@ export function detectClaudeConfig(targetPath) {
|
|
|
68
45
|
result.mcpServers.push({ name, config });
|
|
69
46
|
mcpJsonNames.add(name);
|
|
70
47
|
}
|
|
71
|
-
} catch {
|
|
48
|
+
} catch {}
|
|
72
49
|
}
|
|
73
50
|
|
|
74
|
-
// ── MCP Servers + hooks from settings (legacy + hooks/permissions) ───────
|
|
75
51
|
const settingsPaths = [
|
|
76
52
|
join(targetPath, '.claude', 'settings.json'),
|
|
77
53
|
join(targetPath, '.claude', 'settings.local.json'),
|
|
@@ -81,19 +57,18 @@ export function detectClaudeConfig(targetPath) {
|
|
|
81
57
|
if (existsSync(sp)) {
|
|
82
58
|
try {
|
|
83
59
|
const settings = JSON.parse(readFileSync(sp, 'utf8'));
|
|
84
|
-
// Legacy
|
|
60
|
+
// Legacy entries: .mcp.json wins; among settings files, the later one wins.
|
|
85
61
|
const mcps = settings.mcpServers || {};
|
|
86
62
|
for (const [name, config] of Object.entries(mcps)) {
|
|
87
|
-
if (mcpJsonNames.has(name)) continue;
|
|
63
|
+
if (mcpJsonNames.has(name)) continue;
|
|
88
64
|
const idx = result.mcpServers.findIndex(m => m.name === name);
|
|
89
65
|
if (idx >= 0) {
|
|
90
|
-
result.mcpServers[idx] = { name, config };
|
|
66
|
+
result.mcpServers[idx] = { name, config };
|
|
91
67
|
} else {
|
|
92
68
|
result.mcpServers.push({ name, config });
|
|
93
69
|
}
|
|
94
70
|
}
|
|
95
71
|
|
|
96
|
-
// Collect hooks
|
|
97
72
|
if (settings.hooks) {
|
|
98
73
|
for (const [event, eventHooks] of Object.entries(settings.hooks)) {
|
|
99
74
|
for (const group of (Array.isArray(eventHooks) ? eventHooks : [])) {
|
|
@@ -103,60 +78,43 @@ export function detectClaudeConfig(targetPath) {
|
|
|
103
78
|
}
|
|
104
79
|
}
|
|
105
80
|
}
|
|
106
|
-
} catch {
|
|
81
|
+
} catch {}
|
|
107
82
|
}
|
|
108
83
|
}
|
|
109
84
|
|
|
110
|
-
// ── Plugins from global config ──────────────────────────────────────────
|
|
111
85
|
const pluginsPath = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
112
86
|
if (existsSync(pluginsPath)) {
|
|
113
87
|
try {
|
|
114
88
|
const pluginsData = JSON.parse(readFileSync(pluginsPath, 'utf8'));
|
|
115
89
|
const plugins = pluginsData.plugins || {};
|
|
116
90
|
result.plugins = Object.keys(plugins);
|
|
117
|
-
} catch {
|
|
91
|
+
} catch {}
|
|
118
92
|
}
|
|
119
93
|
|
|
120
|
-
// ── Custom skills ───────────────────────────────────────────────────────
|
|
121
94
|
const skillsPath = join(targetPath, '.claude', 'skills');
|
|
122
95
|
if (existsSync(skillsPath)) {
|
|
123
96
|
try {
|
|
124
97
|
result.customSkills = readdirSync(skillsPath).filter(f => !f.startsWith('.'));
|
|
125
|
-
} catch {
|
|
98
|
+
} catch {}
|
|
126
99
|
}
|
|
127
100
|
|
|
128
|
-
// ── Custom commands ─────────────────────────────────────────────────────
|
|
129
101
|
const commandsPath = join(targetPath, '.claude', 'commands');
|
|
130
102
|
if (existsSync(commandsPath)) {
|
|
131
103
|
try {
|
|
132
104
|
result.customCommands = readdirSync(commandsPath).filter(f => f.endsWith('.md'));
|
|
133
|
-
} catch {
|
|
105
|
+
} catch {}
|
|
134
106
|
}
|
|
135
107
|
} catch {
|
|
136
|
-
// Fail-safe
|
|
137
108
|
}
|
|
138
109
|
|
|
139
110
|
return result;
|
|
140
111
|
}
|
|
141
112
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
*
|
|
145
|
-
* `phaseMatrix` and `currentPhase` default rather than being required: the
|
|
146
|
-
* `init` caller passed only `mcpServers`, so `phaseMatrix[currentPhase]` threw
|
|
147
|
-
* a TypeError that `init`'s outer catch swallowed as
|
|
148
|
-
* "Claude config detection skipped" — taking `saveIntegrations` and
|
|
149
|
-
* `generateSettings` down with it, since both run after this line.
|
|
150
|
-
*
|
|
151
|
-
* @param {Array<{name: string}>} mcpServers - Detected MCP server list
|
|
152
|
-
* @param {Object} [phaseMatrix] - From framework/mcp/registry.json → phaseMatrix
|
|
153
|
-
* @param {string} [currentPhase] - Phase to look up; omit for every phase
|
|
154
|
-
* @returns {{ available: string[], recommended: string[], missing: string[] }}
|
|
155
|
-
*/
|
|
113
|
+
// `phaseMatrix`/`currentPhase` default: `init` passes only `mcpServers`, and a throw here was
|
|
114
|
+
// swallowed by its outer catch, skipping the steps after it.
|
|
156
115
|
export function mapMcpsToPhases(mcpServers, phaseMatrix = {}, currentPhase = null) {
|
|
157
116
|
const configuredNames = new Set((mcpServers || []).map(m => m.name.toLowerCase()));
|
|
158
|
-
// No phase asked for → the union across every phase, deduped.
|
|
159
|
-
// caller showing "which of my MCPs this framework uses" wants.
|
|
117
|
+
// No phase asked for → the union across every phase, deduped.
|
|
160
118
|
const recommended = currentPhase
|
|
161
119
|
? (phaseMatrix[currentPhase] || [])
|
|
162
120
|
: [...new Set(Object.values(phaseMatrix).flat())];
|
|
@@ -168,19 +126,6 @@ export function mapMcpsToPhases(mcpServers, phaseMatrix = {}, currentPhase = nul
|
|
|
168
126
|
};
|
|
169
127
|
}
|
|
170
128
|
|
|
171
|
-
/**
|
|
172
|
-
* The MCP-by-phase table `init` prints: every phase that recommends at least
|
|
173
|
-
* one of the MCPs this machine actually has configured.
|
|
174
|
-
*
|
|
175
|
-
* Separate from `mapMcpsToPhases` because the caller wants a per-phase
|
|
176
|
-
* breakdown, not one phase's slice — reaching for the wrong one is what
|
|
177
|
-
* produced `Object.entries()` over `{available, recommended, missing}` and a
|
|
178
|
-
* `.map(m => m.name)` over plain strings.
|
|
179
|
-
*
|
|
180
|
-
* @param {Array<{name: string}>} mcpServers
|
|
181
|
-
* @param {Object} phaseMatrix
|
|
182
|
-
* @returns {Array<{phase: string, available: string[]}>} phases with ≥1 match
|
|
183
|
-
*/
|
|
184
129
|
export function mcpAvailabilityByPhase(mcpServers, phaseMatrix = {}) {
|
|
185
130
|
return Object.keys(phaseMatrix)
|
|
186
131
|
.map(phase => ({
|
|
@@ -190,12 +135,6 @@ export function mcpAvailabilityByPhase(mcpServers, phaseMatrix = {}) {
|
|
|
190
135
|
.filter(({ available }) => available.length > 0);
|
|
191
136
|
}
|
|
192
137
|
|
|
193
|
-
/**
|
|
194
|
-
* Format a human-readable one-liner summary of detected config.
|
|
195
|
-
*
|
|
196
|
-
* @param {{ plugins: string[], mcpServers: Array<{name: string}> }} config
|
|
197
|
-
* @returns {string}
|
|
198
|
-
*/
|
|
199
138
|
export function formatConfigSummary(config) {
|
|
200
139
|
const parts = [];
|
|
201
140
|
if (config.mcpServers.length > 0) {
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Project Detector
|
|
3
|
-
*
|
|
4
|
-
* Scans a project directory for stack, architecture, UI library, and integrations.
|
|
5
|
-
* Used by `morph-spec init`, `mcp setup` and the test-node fallback (test-nodes.js).
|
|
6
|
-
*
|
|
7
|
-
* @module detectors/index
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
1
|
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
11
2
|
import { join } from 'path';
|
|
12
3
|
|
|
@@ -14,14 +5,6 @@ import { PACKAGE_LOCATIONS } from '../../../framework/hooks/shared/frontend-loca
|
|
|
14
5
|
|
|
15
6
|
export { PACKAGE_LOCATIONS };
|
|
16
7
|
|
|
17
|
-
/**
|
|
18
|
-
* Detect project structure from filesystem evidence.
|
|
19
|
-
*
|
|
20
|
-
* @param {string} targetPath - Root path of the project
|
|
21
|
-
* @param {Object} [opts] - Options
|
|
22
|
-
* @param {boolean} [opts.generateStandards=false] - Whether to generate inferred markdown
|
|
23
|
-
* @returns {{ structure: { stack: string, architecture: string|null, uiLibrary: string|null, integrations: string[], hasVercel: boolean }, inferred: { markdown: string } }}
|
|
24
|
-
*/
|
|
25
8
|
export function detectProject(targetPath, opts = {}) {
|
|
26
9
|
const structure = {
|
|
27
10
|
stack: 'unknown',
|
|
@@ -32,8 +15,6 @@ export function detectProject(targetPath, opts = {}) {
|
|
|
32
15
|
};
|
|
33
16
|
|
|
34
17
|
try {
|
|
35
|
-
// ── Package.json analysis ──────────────────────────────────────────────
|
|
36
|
-
// Collect deps from root and common monorepo subfolders
|
|
37
18
|
const allDeps = {};
|
|
38
19
|
let rootPkg = null;
|
|
39
20
|
let scripts = [];
|
|
@@ -45,17 +26,15 @@ export function detectProject(targetPath, opts = {}) {
|
|
|
45
26
|
if (loc === '.') rootPkg = pkg;
|
|
46
27
|
Object.assign(allDeps, pkg.dependencies, pkg.devDependencies);
|
|
47
28
|
scripts.push(...Object.keys(pkg.scripts || {}));
|
|
48
|
-
// Track frontend path for config
|
|
49
29
|
if (loc !== '.' && (pkg.dependencies?.next || pkg.dependencies?.react || pkg.dependencies?.vue)) {
|
|
50
30
|
structure.frontendPath = loc;
|
|
51
31
|
}
|
|
52
|
-
} catch {
|
|
32
|
+
} catch {}
|
|
53
33
|
}
|
|
54
34
|
const depNames = Object.keys(allDeps);
|
|
55
35
|
|
|
56
36
|
if (depNames.length > 0) {
|
|
57
37
|
|
|
58
|
-
// Stack detection from deps
|
|
59
38
|
if (depNames.includes('next')) {
|
|
60
39
|
structure.stack = 'nextjs';
|
|
61
40
|
} else if (depNames.includes('react')) {
|
|
@@ -68,12 +47,10 @@ export function detectProject(targetPath, opts = {}) {
|
|
|
68
47
|
structure.stack = 'nodejs';
|
|
69
48
|
}
|
|
70
49
|
|
|
71
|
-
// UI library detection
|
|
72
50
|
if (existsSync(join(targetPath, 'components.json')) || existsSync(join(targetPath, structure.frontendPath || '.', 'components.json')) || depNames.includes('@shadcn/ui')) {
|
|
73
51
|
structure.uiLibrary = 'shadcn';
|
|
74
52
|
}
|
|
75
53
|
|
|
76
|
-
// Integrations
|
|
77
54
|
if (depNames.includes('@neondatabase/serverless') || depNames.includes('postgres') || depNames.includes('pg')) {
|
|
78
55
|
structure.integrations.push('neon');
|
|
79
56
|
}
|
|
@@ -84,13 +61,11 @@ export function detectProject(targetPath, opts = {}) {
|
|
|
84
61
|
structure.integrations.push('stripe');
|
|
85
62
|
}
|
|
86
63
|
|
|
87
|
-
// Vercel detection
|
|
88
64
|
if (depNames.includes('vercel') || scripts.includes('vercel')) {
|
|
89
65
|
structure.hasVercel = true;
|
|
90
66
|
}
|
|
91
67
|
}
|
|
92
68
|
|
|
93
|
-
// ── .csproj detection ──────────────────────────────────────────────────
|
|
94
69
|
let hasDotnet = false;
|
|
95
70
|
const csprojLocations = ['.', 'src', 'src/Api', 'src/Web', 'src/backend'];
|
|
96
71
|
|
|
@@ -105,7 +80,7 @@ export function detectProject(targetPath, opts = {}) {
|
|
|
105
80
|
if (content.includes('Npgsql')) {
|
|
106
81
|
if (!structure.integrations.includes('neon')) structure.integrations.push('neon');
|
|
107
82
|
}
|
|
108
|
-
} catch {
|
|
83
|
+
} catch {}
|
|
109
84
|
}
|
|
110
85
|
|
|
111
86
|
for (const loc of csprojLocations) {
|
|
@@ -113,41 +88,34 @@ export function detectProject(targetPath, opts = {}) {
|
|
|
113
88
|
const dir = join(targetPath, loc);
|
|
114
89
|
if (!existsSync(dir)) continue;
|
|
115
90
|
const entries = readdirSyncSafe(dir);
|
|
116
|
-
// Direct .csproj files
|
|
117
91
|
for (const e of entries.filter(f => f.endsWith('.csproj'))) {
|
|
118
92
|
checkCsproj(join(dir, e));
|
|
119
93
|
}
|
|
120
|
-
// One level deeper (e.g., src/backend/MyProject/*.csproj)
|
|
121
94
|
for (const sub of entries) {
|
|
122
95
|
const subDir = join(dir, sub);
|
|
123
96
|
for (const e of readdirSyncSafe(subDir).filter(f => f.endsWith('.csproj'))) {
|
|
124
97
|
checkCsproj(join(subDir, e));
|
|
125
98
|
}
|
|
126
99
|
}
|
|
127
|
-
} catch {
|
|
100
|
+
} catch {}
|
|
128
101
|
}
|
|
129
102
|
|
|
130
|
-
// Determine stack: monorepo (nextjs+dotnet), dotnet, or keep frontend stack
|
|
131
103
|
if (hasDotnet) {
|
|
132
104
|
if (structure.stack === 'nextjs' || structure.stack === 'react' || structure.stack === 'vue') {
|
|
133
|
-
// Monorepo: frontend + .NET backend
|
|
134
105
|
structure.stack = `${structure.stack}+dotnet`;
|
|
135
106
|
} else if (structure.stack === 'unknown') {
|
|
136
107
|
structure.stack = 'dotnet';
|
|
137
108
|
}
|
|
138
109
|
}
|
|
139
110
|
|
|
140
|
-
// ── vercel.json detection ──────────────────────────────────────────────
|
|
141
111
|
if (existsSync(join(targetPath, 'vercel.json'))) {
|
|
142
112
|
structure.hasVercel = true;
|
|
143
113
|
}
|
|
144
114
|
|
|
145
|
-
// ── tsconfig.json detection (fallback for stack) ────────────────────────
|
|
146
115
|
if (structure.stack === 'unknown' && existsSync(join(targetPath, 'tsconfig.json'))) {
|
|
147
116
|
structure.stack = 'nodejs';
|
|
148
117
|
}
|
|
149
118
|
|
|
150
|
-
// ── Architecture detection ─────────────────────────────────────────────
|
|
151
119
|
try {
|
|
152
120
|
const configPath = join(targetPath, '.morph/config/config.json');
|
|
153
121
|
if (existsSync(configPath)) {
|
|
@@ -156,16 +124,14 @@ export function detectProject(targetPath, opts = {}) {
|
|
|
156
124
|
structure.architecture = config.architecture.style;
|
|
157
125
|
}
|
|
158
126
|
}
|
|
159
|
-
} catch {
|
|
127
|
+
} catch {}
|
|
160
128
|
|
|
161
|
-
// ── Docker detection ───────────────────────────────────────────────────
|
|
162
129
|
if (existsSync(join(targetPath, 'docker-compose.yml')) ||
|
|
163
130
|
existsSync(join(targetPath, 'docker-compose.yaml')) ||
|
|
164
131
|
existsSync(join(targetPath, 'Dockerfile'))) {
|
|
165
132
|
if (!structure.integrations.includes('docker')) structure.integrations.push('docker');
|
|
166
133
|
}
|
|
167
134
|
|
|
168
|
-
// ── Azure detection ────────────────────────────────────────────────────
|
|
169
135
|
const azureSignals = [
|
|
170
136
|
'.azure',
|
|
171
137
|
'azure-pipelines.yml',
|
|
@@ -176,7 +142,6 @@ export function detectProject(targetPath, opts = {}) {
|
|
|
176
142
|
const p = join(targetPath, signal);
|
|
177
143
|
if (!existsSync(p)) return false;
|
|
178
144
|
if (signal === 'infra') {
|
|
179
|
-
// Check for .bicep files inside infra/
|
|
180
145
|
return readdirSyncSafe(p).some(f => f.endsWith('.bicep'));
|
|
181
146
|
}
|
|
182
147
|
return true;
|
|
@@ -185,7 +150,6 @@ export function detectProject(targetPath, opts = {}) {
|
|
|
185
150
|
structure.integrations.push('azure');
|
|
186
151
|
}
|
|
187
152
|
} catch {
|
|
188
|
-
// Fail-safe — return defaults
|
|
189
153
|
}
|
|
190
154
|
|
|
191
155
|
return {
|
|
@@ -196,11 +160,6 @@ export function detectProject(targetPath, opts = {}) {
|
|
|
196
160
|
};
|
|
197
161
|
}
|
|
198
162
|
|
|
199
|
-
/**
|
|
200
|
-
* Safe readdir that returns [] on error.
|
|
201
|
-
* @param {string} dir
|
|
202
|
-
* @returns {string[]}
|
|
203
|
-
*/
|
|
204
163
|
function readdirSyncSafe(dir) {
|
|
205
164
|
try {
|
|
206
165
|
return readdirSync(dir);
|
|
@@ -209,11 +168,6 @@ function readdirSyncSafe(dir) {
|
|
|
209
168
|
}
|
|
210
169
|
}
|
|
211
170
|
|
|
212
|
-
/**
|
|
213
|
-
* Generate a markdown summary from detected structure.
|
|
214
|
-
* @param {Object} structure
|
|
215
|
-
* @returns {string}
|
|
216
|
-
*/
|
|
217
171
|
function generateInferredMarkdown(structure) {
|
|
218
172
|
const lines = [`## Detected Stack: ${structure.stack}`];
|
|
219
173
|
if (structure.architecture) lines.push(`Architecture: ${structure.architecture}`);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { basename } from 'path';
|
|
2
|
+
import { readPlanChecks } from '../plan/checks.js';
|
|
3
|
+
import { changedPathsAgainst, detectDefaultBranch } from '../git/default-branch.js';
|
|
4
|
+
import { resolveComposeFile } from '../../../framework/hooks/shared/e2e-check.js';
|
|
5
|
+
import { E2E_SPEC_LOCATIONS } from '../../../framework/hooks/shared/frontend-locations.js';
|
|
6
|
+
|
|
7
|
+
export const E2E_NOT_APPLICABLE = 'not applicable: no `prova: e2e` scenario and the diff touches no UI, e2e spec or container file';
|
|
8
|
+
|
|
9
|
+
const UI_FILE = /\.(tsx|jsx|vue|svelte|astro|css|scss|sass|less|html)$/i;
|
|
10
|
+
const RUNTIME_FILE = /(^|\/)(dockerfile[^/]*|\.dockerignore|(docker-)?compose[^/]*\.ya?ml|playwright\.config\.[cm]?[jt]s)$/i;
|
|
11
|
+
const E2E_DIR = /(^|\/)e2e\//i;
|
|
12
|
+
const KNOWN_PACKAGE_DIRS = E2E_SPEC_LOCATIONS.filter((dir) => dir !== '.');
|
|
13
|
+
|
|
14
|
+
function packageDirsOf(config) {
|
|
15
|
+
const declared = [config?.project?.frontendPath, ...(config?.project?.tests ?? []).filter((n) => n?.runner !== 'dotnet').map((n) => n?.cwd)];
|
|
16
|
+
const extra = declared.filter((d) => typeof d === 'string').map((d) => d.replace(/\\/g, '/').replace(/^\.\/|\/$/g, '')).filter((d) => d && d !== '.');
|
|
17
|
+
return [...KNOWN_PACKAGE_DIRS, ...extra];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const touchesRuntime = (file, composeFile, packageDirs) => UI_FILE.test(file)
|
|
21
|
+
|| RUNTIME_FILE.test(file)
|
|
22
|
+
|| E2E_DIR.test(file)
|
|
23
|
+
|| (composeFile && basename(file) === basename(composeFile))
|
|
24
|
+
|| packageDirs.some((dir) => file === dir || file.startsWith(`${dir}/`));
|
|
25
|
+
|
|
26
|
+
/** Unknown (no git, no base, empty diff) is applicable: the e2e only stands down on evidence. */
|
|
27
|
+
export function e2eApplicability(projectPath, featureName, config) {
|
|
28
|
+
const { checks } = readPlanChecks(projectPath, featureName);
|
|
29
|
+
if (checks?.checks?.some((c) => c.proof === 'e2e')) return { applicable: true, reason: 'prova: e2e' };
|
|
30
|
+
const base = detectDefaultBranch(projectPath);
|
|
31
|
+
const changed = base ? changedPathsAgainst(base, projectPath).filter((f) => !f.startsWith('.morph/')) : [];
|
|
32
|
+
if (changed.length === 0) return { applicable: true, reason: 'diff unknown' };
|
|
33
|
+
const composeFile = resolveComposeFile(projectPath, config);
|
|
34
|
+
const packageDirs = packageDirsOf(config);
|
|
35
|
+
const hit = changed.find((f) => touchesRuntime(f, composeFile, packageDirs));
|
|
36
|
+
return hit ? { applicable: true, reason: hit } : { applicable: false, reason: E2E_NOT_APPLICABLE };
|
|
37
|
+
}
|
|
@@ -1,22 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* the credentials its auth setup needs.
|
|
4
|
-
*
|
|
5
|
-
* This lives in ONE place because it has two callers that must never disagree:
|
|
6
|
-
* `morph-spec e2e up` (manual) and the `e2e` node inside `morph-spec verify`
|
|
7
|
-
* (the one that actually runs in the loop). When only the manual path built the
|
|
8
|
-
* port block, the isolation was real in the command nobody runs and absent in
|
|
9
|
-
* the one everybody runs — two worktrees would get distinct container names via
|
|
10
|
-
* COMPOSE_PROJECT_NAME and then collide on host port 3000 anyway, surfacing as
|
|
11
|
-
* "port is already allocated" and pausing a gate over an infrastructure clash
|
|
12
|
-
* rather than a defect.
|
|
13
|
-
*
|
|
14
|
-
* It sits in src/lib rather than framework/hooks/shared because it depends on
|
|
15
|
-
* env-allocator.js: hooks are installed standalone into target projects, where
|
|
16
|
-
* `src/` does not exist, so the dependency may only point this way.
|
|
17
|
-
*
|
|
18
|
-
* @module lib/e2e/compose-env
|
|
19
|
-
*/
|
|
1
|
+
// One place for `e2e up` and the `e2e` node of `verify`, which must never disagree on ports. Lives in
|
|
2
|
+
// src/lib, not hooks/shared, because it depends on env-allocator.js and hooks run without `src/`.
|
|
20
3
|
|
|
21
4
|
import { join } from 'path';
|
|
22
5
|
|
|
@@ -25,43 +8,11 @@ import chalk from 'chalk';
|
|
|
25
8
|
import { loadEnvFile } from '../../../framework/hooks/shared/e2e-check.js';
|
|
26
9
|
import { allocateEnvBlock, portLayout, resolveFleetConfig, resolvePortAliases } from '../worktree/env-allocator.js';
|
|
27
10
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
* Port variables come from `portLayout()` in env-allocator.js — the SAME
|
|
32
|
-
* function that renders `.morph/worktree.env` — so a compose file and the
|
|
33
|
-
* worktree manifest describe the same world by construction (`PORT`,
|
|
34
|
-
* `API_PORT`/`ASPNETCORE_URLS`, `POSTGRES_PORT`, `COMPOSE_PROJECT_NAME`).
|
|
35
|
-
*
|
|
36
|
-
* `PLAYWRIGHT_BASE_URL` is derived here rather than left to the spec config —
|
|
37
|
-
* the suite has to be told where the app actually landed, otherwise a feature
|
|
38
|
-
* on port 4020 gets tested against a hardcoded localhost:3000 and fails with
|
|
39
|
-
* ECONNREFUSED that looks exactly like a broken app.
|
|
40
|
-
*
|
|
41
|
-
* The credentials env file loads AFTER the layout but cannot outrank it: a
|
|
42
|
-
* `.env.e2e` copied from a sibling worktree carries THAT worktree's block, and
|
|
43
|
-
* letting it win points this stack at the neighbour's postgres while every
|
|
44
|
-
* other surface still reports the allocated port. The protected keys are the
|
|
45
|
-
* ones actually assigned above — never a second literal list, which is exactly
|
|
46
|
-
* how an offset lands in one writer and not the other. A key that gets ignored
|
|
47
|
-
* is announced on stderr (never stdout: `morph-spec env` writes shell-eval
|
|
48
|
-
* output there), because swapping a silent override for a silent discard just
|
|
49
|
-
* moves the debugging session.
|
|
50
|
-
*
|
|
51
|
-
* Fail-open throughout: with no git dir (or a corrupt registry) the caller gets
|
|
52
|
-
* default ports, which only matters when two stacks run at once. With no layout
|
|
53
|
-
* applied nothing is filtered — there the env file is the only port source.
|
|
54
|
-
*
|
|
55
|
-
* @param {object} params
|
|
56
|
-
* @param {string} params.cwd - project root
|
|
57
|
-
* @param {object|null} [params.config] - .morph/config/config.json contents
|
|
58
|
-
* @param {string} params.featureName
|
|
59
|
-
* @param {string|null} [params.gitCommonDir]
|
|
60
|
-
* @returns {Record<string,string>}
|
|
61
|
-
*/
|
|
11
|
+
// Ports come from `portLayout()`, the same function that renders `.morph/worktree.env`. The credentials
|
|
12
|
+
// file loads after the layout but cannot outrank it: a `.env.e2e` copied from a sibling worktree carries
|
|
13
|
+
// that worktree's block. Ignored keys are announced on stderr; stdout carries `morph-spec env` output.
|
|
62
14
|
export function buildComposeEnv({ cwd, config, featureName, gitCommonDir }) {
|
|
63
15
|
const env = {};
|
|
64
|
-
/** @type {Set<string>|null} - null while no layout has been applied */
|
|
65
16
|
let layoutKeys = null;
|
|
66
17
|
|
|
67
18
|
if (gitCommonDir) {
|
|
@@ -97,18 +48,6 @@ export function buildComposeEnv({ cwd, config, featureName, gitCommonDir }) {
|
|
|
97
48
|
return env;
|
|
98
49
|
}
|
|
99
50
|
|
|
100
|
-
/**
|
|
101
|
-
* Tell the caller — `e2e up`, the `e2e` node of `verify`, `stack-teardown` and
|
|
102
|
-
* `morph-spec env` all reach this through `buildComposeEnv` — which keys of the
|
|
103
|
-
* credentials file were dropped and what won instead.
|
|
104
|
-
*
|
|
105
|
-
* stderr, always: `morph-spec env` prints `export K=v` on stdout for a
|
|
106
|
-
* shell to eval, and `verify` prints JSON there.
|
|
107
|
-
*
|
|
108
|
-
* @param {string} envFile
|
|
109
|
-
* @param {string[]} ignored
|
|
110
|
-
* @param {Record<string,string>} env - the layout that won
|
|
111
|
-
*/
|
|
112
51
|
function reportIgnoredLayoutKeys(envFile, ignored, env) {
|
|
113
52
|
const shown = ignored.map((key) => `${key}=${env[key]}`).join(', ');
|
|
114
53
|
console.warn(chalk.yellow(
|