@polymorphism-tech/morph-spec 8.47.0 → 9.1.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 +51 -43
- package/README.md +39 -38
- package/bin/morph-spec.js +41 -113
- package/bin/validate.js +1 -147
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +41 -101
- package/framework/MORPH.md +95 -252
- package/framework/agents.json +86 -21
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +1 -1
- package/framework/commands/morph-preflight.md +2 -3
- package/framework/commands/morph-status.md +4 -4
- package/framework/docs/CLI.md +574 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- 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 +3 -17
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +15 -63
- package/framework/hooks/claude-code/core/gate-guard.js +9 -78
- 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 +36 -2
- package/framework/hooks/claude-code/core/protect-spec-files.js +25 -297
- package/framework/hooks/claude-code/core/state-sync.js +15 -85
- package/framework/hooks/claude-code/core/telemetry-log.js +15 -96
- package/framework/hooks/claude-code/core/validate-completion.js +37 -147
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +23 -0
- package/framework/hooks/claude-code/core/worktree-context.js +15 -114
- 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.py +55 -41
- package/framework/hooks/package.json +3 -0
- 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 +11 -76
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +168 -44
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +11 -43
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +59 -0
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +4 -0
- package/framework/hooks/shared/gate-question.js +12 -0
- 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 +14 -72
- 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 +99 -0
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +51 -247
- package/framework/hooks/shared/state-writer.js +9 -54
- 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/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- 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/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -50
- package/framework/skills/README.md +3 -4
- 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 +149 -222
- package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
- package/framework/skills/core/morph-machine/SKILL.md +2 -2
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +137 -299
- package/framework/skills/core/morph-review/SKILL.md +111 -136
- package/framework/skills/core/morph-standards/SKILL.md +27 -39
- package/framework/skills/core/morph-uiux/SKILL.md +94 -152
- package/framework/standards/STANDARDS.json +109 -1219
- package/framework/standards/STANDARDS.meta.json +1319 -0
- 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/evals-with-cache.md +1 -2
- 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 +3 -3
- 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 +7 -7
- 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/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/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +15 -179
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/framework/templates/dotnet/ai-kit/README.md +1 -9
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
- 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 +18 -105
- 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 +17 -111
- 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-procs.js +1 -1
- package/src/commands/project/doctor.js +53 -307
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +6 -35
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +8 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/setup-infra-cmd.js +2 -1
- package/src/commands/project/stack-scan.js +58 -0
- package/src/commands/project/standards.js +65 -72
- package/src/commands/project/status.js +28 -121
- package/src/commands/project/update.js +31 -223
- package/src/commands/project/worktree.js +109 -883
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +65 -120
- package/src/commands/state/approve.js +87 -229
- package/src/commands/state/archive.js +23 -139
- package/src/commands/state/auto-approval.js +65 -0
- package/src/commands/state/create.js +49 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +105 -494
- package/src/commands/state/gate-check.js +74 -183
- package/src/commands/state/gate-decision.js +8 -42
- package/src/commands/state/handoff.js +4 -31
- package/src/commands/state/list.js +21 -91
- package/src/commands/state/report-check.js +134 -338
- package/src/commands/state/retype.js +12 -28
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +79 -19
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +52 -216
- package/src/commands/validation/verify.js +45 -160
- package/src/core/paths/output-schema.js +9 -155
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +2 -30
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +32 -395
- 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 +21 -0
- package/src/core/work-types.js +10 -45
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -8
- package/src/dashboard/app/components/CommandPalette.jsx +4 -11
- 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 +3 -9
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -133
- package/src/dashboard/app/pages/Features.jsx +9 -16
- package/src/dashboard/app/pages/Guide.jsx +2 -3
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -7
- 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 +14 -83
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +1 -7
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +92 -113
- 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 +4 -7
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- 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 +14 -13
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +2 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +8 -59
- 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 +37 -112
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +11 -122
- 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 +2 -1
- 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 +17 -17
- 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 +2 -6
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +8 -33
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/mutation/dotnet-introspection.js +281 -0
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +47 -13
- package/src/lib/plan/artifact-vocabulary.js +40 -0
- package/src/lib/plan/checks.js +208 -0
- package/src/lib/plan/plan-parser.js +204 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +204 -0
- package/src/lib/risk-detector.js +131 -274
- package/src/lib/specs/adr-index.js +38 -85
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +31 -0
- package/src/lib/specs/gfm-table.js +11 -0
- package/src/lib/specs/revocations.js +31 -102
- package/src/lib/specs/screen-checklist.js +56 -132
- package/src/lib/stack/dotnet-build-ignores.js +43 -0
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +260 -0
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +37 -0
- package/src/lib/standards/sections.js +47 -0
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +23 -179
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -313
- package/src/lib/tasks/test-runner.js +107 -447
- package/src/lib/telemetry/metrics.js +13 -103
- package/src/lib/telemetry/telemetry-reader.js +13 -238
- package/src/lib/telemetry/verify-event.js +4 -34
- 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 +4 -26
- 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 +24 -137
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +54 -156
- package/src/lib/validators/validation-runner.js +20 -125
- package/src/lib/validators/verify-runner.js +152 -575
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -137
- 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 +31 -126
- 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 -138
- package/src/utils/agents-installer.js +92 -272
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -87
- package/src/utils/file-copier.js +32 -90
- package/src/utils/hooks-installer.js +26 -296
- package/src/utils/skills-installer.js +4 -102
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -57
- package/framework/evals/trace-schema.md +0 -67
- package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
- package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
- package/framework/hooks/git/pre-commit/specs.sh +0 -50
- package/framework/hooks/shared/tasks-validator.js +0 -111
- package/framework/rules/morph-task-tracking.md +0 -54
- package/framework/rules/morph-workflow.md +0 -244
- package/framework/schemas/tasks.schema.json +0 -79
- package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
- package/framework/skills/core/morph-implement/SKILL.md +0 -194
- package/framework/skills/core/morph-proposal/SKILL.md +0 -152
- package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
- package/framework/templates/docs/proposal.md +0 -192
- package/framework/templates/docs/spec.md +0 -57
- package/framework/templates/docs/ui-components.md +0 -52
- package/framework/templates/docs/ui-design-system.md +0 -76
- package/framework/templates/docs/ui-flows.md +0 -167
- package/framework/templates/examples/spec-examples.md +0 -91
- package/framework/templates/feature/decisions.md +0 -124
- package/framework/templates/feature/recap.md +0 -145
- package/framework/templates/tasks.template.json +0 -17
- package/src/commands/state/usecase.js +0 -143
- package/src/commands/tasks/dag.js +0 -181
- package/src/commands/utils/index.js +0 -3
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/evaluator-report.js +0 -104
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/acervo-format.js +0 -70
- package/src/lib/specs/acervo-lifecycle.js +0 -264
- package/src/lib/specs/done-criteria-check.js +0 -200
- package/src/lib/specs/usecase-check.js +0 -301
- package/src/lib/specs/usecase-promoter.js +0 -301
- package/src/lib/specs/usecase-registry.js +0 -237
- package/src/lib/tasks/dispatch-planner.js +0 -319
- package/src/lib/tasks/dotnet-test-introspection.js +0 -632
- package/src/lib/tasks/mermaid-renderer.js +0 -132
- package/src/lib/tasks/task-classifier.js +0 -30
- package/src/lib/tasks/task-parser.js +0 -209
- package/src/lib/tasks/tasks-json-parser.js +0 -296
- package/src/lib/tasks/test-scope.js +0 -597
- package/src/utils/agent-id-mapper.js +0 -11
|
@@ -1,28 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* worktree-audit.js — reconcile the three views of "which worktrees exist".
|
|
3
|
-
*
|
|
4
|
-
* Orphan worktrees accumulate silently. In the observed session,
|
|
5
|
-
* `.worktrees/verify-tables-ui` and `.worktrees/tmp-cleanup` were registered in
|
|
6
|
-
* `git worktree list` yet ABSENT from `morph-spec fleet` — fleet only reports
|
|
7
|
-
* worktrees whose branch matches `refs/heads/morph/*`, so anything else is
|
|
8
|
-
* invisible by construction and therefore never cleaned up (C2).
|
|
9
|
-
*
|
|
10
|
-
* Nothing compared the three sources that must agree:
|
|
11
|
-
*
|
|
12
|
-
* 1. git's registry — `git worktree list --porcelain`
|
|
13
|
-
* 2. the disk — directories under `worktrees/` (and legacy `.worktrees/`)
|
|
14
|
-
* 3. morph's features — `.morph/features/{f}/feature.json`
|
|
15
|
-
*
|
|
16
|
-
* Read-only and fail-open: anything unresolvable yields no issue rather than a
|
|
17
|
-
* throw. A diagnostic that invents problems is worse than one that misses some.
|
|
18
|
-
* The mutations all live in `pruneWorktrees`, and each is deliberately the
|
|
19
|
-
* safest possible: `git worktree prune` drops ADMIN RECORDS for directories
|
|
20
|
-
* that are already gone, and the empty-residue sweep removes only directories
|
|
21
|
-
* with zero entries inside — nothing that can hold work. Deleting a directory
|
|
22
|
-
* that still HOLDS something stays a human decision, the same PROVE-or-DECLINE
|
|
23
|
-
* discipline `teardownWorktree` follows.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
1
|
import { execFileSync } from 'child_process';
|
|
27
2
|
import { existsSync, readdirSync, readFileSync, rmdirSync, statSync } from 'fs';
|
|
28
3
|
import { join, resolve } from 'path';
|
|
@@ -73,7 +48,6 @@ function agentLeftoverIssue(item) {
|
|
|
73
48
|
};
|
|
74
49
|
}
|
|
75
50
|
|
|
76
|
-
/** Feature names morph knows about in the primary root. */
|
|
77
51
|
function knownFeatures(root) {
|
|
78
52
|
const names = new Set();
|
|
79
53
|
try {
|
|
@@ -86,14 +60,7 @@ function knownFeatures(root) {
|
|
|
86
60
|
return names;
|
|
87
61
|
}
|
|
88
62
|
|
|
89
|
-
|
|
90
|
-
* Is the feature's authoritative feature.json COMMITTED on its own branch?
|
|
91
|
-
*
|
|
92
|
-
* Not "does the branch exist": `git worktree add -b morph/{f}` creates the
|
|
93
|
-
* branch as a side effect, so branch-existence is true for every registered
|
|
94
|
-
* worktree and proves nothing. The blob is the real evidence — it is where the
|
|
95
|
-
* per-feature state model says the truth lives.
|
|
96
|
-
*/
|
|
63
|
+
// Branch existence proves nothing (`worktree add -b` creates it); the committed blob does.
|
|
97
64
|
function featureCommittedOnBranch(root, feature) {
|
|
98
65
|
try {
|
|
99
66
|
execFileSync('git', ['cat-file', '-e', `morph/${feature}:.morph/features/${feature}/feature.json`], {
|
|
@@ -105,29 +72,8 @@ function featureCommittedOnBranch(root, feature) {
|
|
|
105
72
|
}
|
|
106
73
|
}
|
|
107
74
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
* branch, or an OPEN PR?
|
|
111
|
-
*
|
|
112
|
-
* This is the predicate that separates "an orphan directory to clean up" from
|
|
113
|
-
* "the only copy of somebody's work". Two independent pieces of evidence, and
|
|
114
|
-
* ANY of them is enough to protect the worktree:
|
|
115
|
-
*
|
|
116
|
-
* 1. commits the default branch does not have (`git rev-list base..branch`)
|
|
117
|
-
* 2. an OPEN pull request on that branch
|
|
118
|
-
*
|
|
119
|
-
* Fail-CLOSED, and that asymmetry is the whole design: whenever the answer
|
|
120
|
-
* cannot be established — no default branch, no origin, git errored — the
|
|
121
|
-
* worktree counts as UNINTEGRATED. The cost of being wrong that way is a
|
|
122
|
-
* diagnostic that stays quiet; the cost of the opposite is a suggestion to
|
|
123
|
-
* delete work that exists nowhere else.
|
|
124
|
-
*
|
|
125
|
-
* A squash-merged branch legitimately reports commits ahead, so the tree
|
|
126
|
-
* comparison (`branch-classifier.js`) is consulted for the same reason
|
|
127
|
-
* `branch prune` needs it — the two must not disagree about "is this in".
|
|
128
|
-
*
|
|
129
|
-
* @returns {{ unintegrated: boolean, evidence: string, pr: number|null }}
|
|
130
|
-
*/
|
|
75
|
+
// Fail-CLOSED: when integration cannot be established the worktree counts as
|
|
76
|
+
// unintegrated — a quiet diagnostic beats suggesting to delete the only copy.
|
|
131
77
|
function unintegratedWork(root, branchRef, prIndex) {
|
|
132
78
|
const branch = (branchRef || '').replace(/^refs\/heads\//, '');
|
|
133
79
|
if (!branch) return { unintegrated: true, evidence: 'não foi possível resolver a branch do worktree', pr: null };
|
|
@@ -143,9 +89,7 @@ function unintegratedWork(root, branchRef, prIndex) {
|
|
|
143
89
|
if (ahead === null) return { unintegrated: true, evidence: `não foi possível comparar com ${baseRef}`, pr: null };
|
|
144
90
|
if (ahead === 0) return { unintegrated: false, evidence: `nenhum commit fora de ${baseRef}`, pr: pr?.number ?? null };
|
|
145
91
|
|
|
146
|
-
//
|
|
147
|
-
// that shape. Reuse the classifier so `doctor` and `branch prune` cannot
|
|
148
|
-
// reach opposite conclusions about the same branch.
|
|
92
|
+
// A squash-merge also leaves commits ahead; the classifier keeps `doctor` and `branch prune` agreeing.
|
|
149
93
|
const verdict = classifyBranch({ name: branch, sha: '', current: false, worktreePath: null }, { baseRef, cwd: root, pr });
|
|
150
94
|
if (verdict.kind === 'merged-ancestral' || verdict.kind === 'merged-squash') {
|
|
151
95
|
return { unintegrated: false, evidence: verdict.reason, pr: pr?.number ?? null };
|
|
@@ -153,19 +97,6 @@ function unintegratedWork(root, branchRef, prIndex) {
|
|
|
153
97
|
return { unintegrated: true, evidence: `${ahead} commit(s) fora de ${baseRef}`, pr: pr?.number ?? null };
|
|
154
98
|
}
|
|
155
99
|
|
|
156
|
-
/**
|
|
157
|
-
* Every feature's `worktree` registration in the primary root — the OTHER
|
|
158
|
-
* direction from `knownFeatures`/`dirsOnDisk`: not "does git/disk know about
|
|
159
|
-
* this worktree", but "does the ROOT's feature.json still point somewhere
|
|
160
|
-
* real". `create --worktree` writes this pointer once at birth
|
|
161
|
-
* (`{path, branch, createdAt}`) and nothing updates it afterwards except
|
|
162
|
-
* `finish`'s own teardown — so a directory removed any other way (by hand,
|
|
163
|
-
* `git worktree prune` run directly, a failed teardown that still cleared the
|
|
164
|
-
* field, an unrelated script) leaves it dangling with nothing to notice.
|
|
165
|
-
*
|
|
166
|
-
* @param {string} root
|
|
167
|
-
* @returns {Array<{ feature: string, path: string, branch: string|null }>}
|
|
168
|
-
*/
|
|
169
100
|
function featureRegistrations(root) {
|
|
170
101
|
const out = [];
|
|
171
102
|
try {
|
|
@@ -183,23 +114,15 @@ function featureRegistrations(root) {
|
|
|
183
114
|
return out;
|
|
184
115
|
}
|
|
185
116
|
|
|
186
|
-
|
|
187
|
-
* Is this directory literally empty — zero entries, dotfiles included?
|
|
188
|
-
*
|
|
189
|
-
* The whole point of the distinction: an empty residue holds nothing that can
|
|
190
|
-
* be lost, so it can be removed without a human looking at it. Anything else —
|
|
191
|
-
* including a directory we cannot read — is reported as NON-empty, because the
|
|
192
|
-
* safe answer to "might there be work in here?" is yes.
|
|
193
|
-
*/
|
|
117
|
+
// Unreadable counts as occupied: emptiness is what licenses automatic removal.
|
|
194
118
|
function isEmptyDir(path) {
|
|
195
119
|
try {
|
|
196
120
|
return readdirSync(path).length === 0;
|
|
197
121
|
} catch {
|
|
198
|
-
return false;
|
|
122
|
+
return false;
|
|
199
123
|
}
|
|
200
124
|
}
|
|
201
125
|
|
|
202
|
-
/** Directories physically present under either worktree root. */
|
|
203
126
|
function dirsOnDisk(root) {
|
|
204
127
|
const found = [];
|
|
205
128
|
const rootNorm = norm(root);
|
|
@@ -221,53 +144,8 @@ function dirsOnDisk(root) {
|
|
|
221
144
|
}
|
|
222
145
|
|
|
223
146
|
/**
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* Issue types:
|
|
227
|
-
* • registered-missing — git has a worktree whose directory is gone
|
|
228
|
-
* • non-morph-branch — a registered worktree under worktrees/ on a
|
|
229
|
-
* branch that is not `morph/*`, so `fleet`
|
|
230
|
-
* cannot see it
|
|
231
|
-
* • unregistered-dir — a directory under worktrees/ git never
|
|
232
|
-
* registered
|
|
233
|
-
* • no-feature — a `morph/{f}` worktree for a feature morph
|
|
234
|
-
* does not know AND whose branch carries
|
|
235
|
-
* nothing that is not already integrated
|
|
236
|
-
* • unintegrated-work (info) — the same shape, except the branch still holds
|
|
237
|
-
* commits the default does not, or an OPEN PR.
|
|
238
|
-
* Reported so the reader knows why no removal
|
|
239
|
-
* is suggested — this worktree may be the ONLY
|
|
240
|
-
* copy of the work
|
|
241
|
-
* • dangling-registration — a feature's ROOT feature.json still points
|
|
242
|
-
* at a `worktree.path` that no longer exists
|
|
243
|
-
* (the symmetric OTHER direction from
|
|
244
|
-
* `no-feature`: the worktree is gone, but the
|
|
245
|
-
* root's pointer to it survived)
|
|
246
|
-
* • unregistered-registration — a feature's `worktree.path` names a
|
|
247
|
-
* directory that exists but git's own
|
|
248
|
-
* registry does not know as a worktree
|
|
249
|
-
*
|
|
250
|
-
* • agent-worktree-leftover (info) — a sub-agent's `agent-<hex>` tree or
|
|
251
|
-
* `morph-task/agent-<hex>` branch left behind:
|
|
252
|
-
* `reapable` when the agent stopped and nothing
|
|
253
|
-
* is lost, otherwise the reason it is kept
|
|
254
|
-
*
|
|
255
|
-
* • docker-stack-orphan (warn) — a `morph-*` docker stack of this repository
|
|
256
|
-
* still running for a feature this tree does
|
|
257
|
-
* not know, and
|
|
258
|
-
* • docker-port-held (warn) — a port from a feature's block published by
|
|
259
|
-
* another compose project. Both come from
|
|
260
|
-
* `docker-audit.js`: the three sources above
|
|
261
|
-
* cannot see a CONTAINER, and a stack outlives
|
|
262
|
-
* every one of them
|
|
263
|
-
*
|
|
264
|
-
* @param {string} [cwd]
|
|
265
|
-
* @param {object} [opts]
|
|
266
|
-
* @param {Function} [opts.exec] - injectable executor handed to the docker
|
|
267
|
-
* audit, so the docker paths are testable with no daemon (the CI runners
|
|
268
|
-
* have none). Absent, the real `docker` CLI is used — and answers `[]` when
|
|
269
|
-
* it is not installed.
|
|
270
|
-
* @returns {Array<{ type: string, detail: string, path?: string, feature?: string, fix: string }>}
|
|
147
|
+
* Reconciles git's worktree registry, the disk, morph's features and docker.
|
|
148
|
+
* Read-only and fail-open; `exec` is the docker executor seam.
|
|
271
149
|
*/
|
|
272
150
|
export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
|
|
273
151
|
if (!isGitRepo(cwd)) return [];
|
|
@@ -275,9 +153,7 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
|
|
|
275
153
|
const rootNorm = norm(root);
|
|
276
154
|
const issues = [];
|
|
277
155
|
|
|
278
|
-
//
|
|
279
|
-
// GitHub, not authenticated), never "no PRs"; `unintegratedWork` treats the
|
|
280
|
-
// two differently on purpose.
|
|
156
|
+
// null = UNKNOWN (no gh / not GitHub / unauthenticated), never "no PRs".
|
|
281
157
|
const prIndex = buildPrIndex(root);
|
|
282
158
|
|
|
283
159
|
const containers = containerDirs(root);
|
|
@@ -288,7 +164,7 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
|
|
|
288
164
|
for (const entry of registered) {
|
|
289
165
|
const path = norm(entry.path);
|
|
290
166
|
registeredPaths.add(path);
|
|
291
|
-
if (path === rootNorm) continue;
|
|
167
|
+
if (path === rootNorm) continue;
|
|
292
168
|
|
|
293
169
|
if (!existsSync(path)) {
|
|
294
170
|
issues.push({
|
|
@@ -306,12 +182,7 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
|
|
|
306
182
|
const task = entry.branch?.match(/^refs\/heads\/morph-task\/(.+)$/)?.[1] || null;
|
|
307
183
|
|
|
308
184
|
if (!inContainer) {
|
|
309
|
-
//
|
|
310
|
-
// (a native Claude Code run before the WorktreeCreate hook, a raw
|
|
311
|
-
// `git worktree add ../x`, another skill). It has no junctions, no port
|
|
312
|
-
// block, and nothing morph runs there — the exact shape that let a
|
|
313
|
-
// root's `.claude/` be emptied. Reported as `warn` (never a failure,
|
|
314
|
-
// NEVER pruned): the work inside may be the only copy.
|
|
185
|
+
// Never pruned: the work inside a tree created outside morph may be the only copy.
|
|
315
186
|
const work = unintegratedWork(root, entry.branch, prIndex);
|
|
316
187
|
issues.push({
|
|
317
188
|
type: 'out-of-container',
|
|
@@ -327,10 +198,6 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
|
|
|
327
198
|
|
|
328
199
|
if (task) {
|
|
329
200
|
if (AGENT_TASK.test(task)) continue; // reported once, by the agent plan below
|
|
330
|
-
// `morph-task/*`: a task-isolation tree provisioned by the WorktreeCreate
|
|
331
|
-
// hook. Clean → nothing to say (the orchestrator removes it after the
|
|
332
|
-
// merge). With commits the default does not have → report, never
|
|
333
|
-
// suggest removal.
|
|
334
201
|
const work = unintegratedWork(root, entry.branch, prIndex);
|
|
335
202
|
if (work.unintegrated) {
|
|
336
203
|
issues.push({
|
|
@@ -345,11 +212,6 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
|
|
|
345
212
|
}
|
|
346
213
|
|
|
347
214
|
if (inContainer) {
|
|
348
|
-
// Same hazard as `no-feature` below, and it needs the same guard:
|
|
349
|
-
// "invisible to fleet" says nothing about whether the WORK is safe
|
|
350
|
-
// elsewhere. A worktree on a `feat/*` branch with an open PR is the
|
|
351
|
-
// most normal thing in the world, and suggesting its removal is a
|
|
352
|
-
// suggestion to destroy the only copy.
|
|
353
215
|
const work = unintegratedWork(root, entry.branch, prIndex);
|
|
354
216
|
issues.push(work.unintegrated
|
|
355
217
|
? {
|
|
@@ -365,10 +227,8 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
|
|
|
365
227
|
type: 'non-morph-branch',
|
|
366
228
|
path,
|
|
367
229
|
detail: `${path} está em ${entry.branch || 'HEAD destacado'}, não numa branch morph/* — o \`fleet\` não o enxerga (e não há trabalho pendente: ${work.evidence})`,
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
// `worktree remove` drops them first (PROVE-or-DECLINE) and takes
|
|
371
|
-
// --target precisely for worktrees like this one, with no feature.
|
|
230
|
+
// Never raw `git worktree remove`: it follows the infra junctions and
|
|
231
|
+
// empties the root's .claude/ and .morph/framework/.
|
|
372
232
|
fix: `morph-spec worktree remove --target "${path}" (se já não serve)`,
|
|
373
233
|
});
|
|
374
234
|
}
|
|
@@ -376,13 +236,8 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
|
|
|
376
236
|
}
|
|
377
237
|
|
|
378
238
|
if (!features.has(feature) && !featureCommittedOnBranch(root, feature)) {
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
// and so does a worktree whose PR is still open. In the observed session
|
|
382
|
-
// this branch recommended `worktree remove --target` for a worktree
|
|
383
|
-
// holding the ONLY copy of an open PR with 12 tasks and a 9.5 at Gate 3.
|
|
384
|
-
// So the diagnosis now depends on whether the WORK is integrated, and the
|
|
385
|
-
// suggestion to destroy appears only when it provably is.
|
|
239
|
+
// An archived feature or an open PR leaves this same shape: suggest removal
|
|
240
|
+
// only when the work is provably integrated.
|
|
386
241
|
const work = unintegratedWork(root, entry.branch, prIndex);
|
|
387
242
|
if (work.unintegrated) {
|
|
388
243
|
issues.push({
|
|
@@ -420,19 +275,12 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
|
|
|
420
275
|
detail: d.empty
|
|
421
276
|
? `${d.container}/${d.name} existe no disco, está VAZIO e o git não o registra — resíduo de um finish anterior`
|
|
422
277
|
: `${d.container}/${d.name} existe no disco mas o git não o registra — resíduo de um finish anterior`,
|
|
423
|
-
// An empty folder has nothing to lose, so it gets the automatic repair.
|
|
424
|
-
// One with content keeps the manual chore: there may be work in there,
|
|
425
|
-
// and the whole PROVE-or-DECLINE discipline exists for exactly that.
|
|
426
278
|
fix: d.empty
|
|
427
279
|
? `morph-spec doctor --worktrees --prune`
|
|
428
280
|
: `apague o diretório à mão depois de conferir que nada importante ficou nele`,
|
|
429
281
|
});
|
|
430
282
|
}
|
|
431
283
|
|
|
432
|
-
// The other direction: does the ROOT's own bookkeeping still point at a
|
|
433
|
-
// live worktree? `git worktree list`/disk only see what's PHYSICALLY
|
|
434
|
-
// there; a registration surviving past its worktree's removal is invisible
|
|
435
|
-
// to every check above.
|
|
436
284
|
for (const reg of featureRegistrations(root)) {
|
|
437
285
|
const path = norm(reg.path);
|
|
438
286
|
if (!existsSync(reg.path)) {
|
|
@@ -454,13 +302,7 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
|
|
|
454
302
|
}
|
|
455
303
|
}
|
|
456
304
|
|
|
457
|
-
//
|
|
458
|
-
// A container survives the removal of the worktree, of the branch and of the
|
|
459
|
-
// feature — none of the three reconciliations above can see it, and it keeps
|
|
460
|
-
// holding the feature's port block. The feature names come from HERE (the
|
|
461
|
-
// ones morph knows plus the ones the registered `morph/*` worktrees carry)
|
|
462
|
-
// rather than from a second scan inside docker-audit: two readings of the
|
|
463
|
-
// same fact are two chances to disagree about which stack is orphaned.
|
|
305
|
+
// Feature names are passed in so docker-audit never rescans and disagrees.
|
|
464
306
|
const treeFeatures = new Set(features);
|
|
465
307
|
for (const entry of registered) {
|
|
466
308
|
const f = entry.branch?.match(/^refs\/heads\/morph\/(.+)$/)?.[1];
|
|
@@ -468,31 +310,13 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
|
|
|
468
310
|
}
|
|
469
311
|
issues.push(...auditDockerStacks(root, { exec, features: treeFeatures }));
|
|
470
312
|
|
|
471
|
-
// What a worktree removal leaves behind in `.git/morph-spec/finish-backup/`
|
|
472
|
-
// past its retention — `warn`, pruned by `--prune` and by the next finish.
|
|
473
313
|
issues.push(...auditFinishBackups(root));
|
|
474
314
|
|
|
475
315
|
return issues;
|
|
476
316
|
}
|
|
477
317
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
* not register — the residue a finish leaves behind.
|
|
481
|
-
*
|
|
482
|
-
* "Empty" is the whole licence to act: a folder with zero entries cannot be
|
|
483
|
-
* hiding uncommitted work, so asking a human about it is pure ceremony, and
|
|
484
|
-
* the audit that only warns leaves it there forever. A directory with anything
|
|
485
|
-
* in it keeps the manual path.
|
|
486
|
-
*
|
|
487
|
-
* Two guards beyond the emptiness check:
|
|
488
|
-
* • only directories directly inside morph's own worktree containers, never
|
|
489
|
-
* the containers themselves and never a path git registers;
|
|
490
|
-
* • `rmdirSync`, not `rm -rf` — it REFUSES a non-empty directory, so a file
|
|
491
|
-
* that appears between the check and the call is a failure, not a loss.
|
|
492
|
-
*
|
|
493
|
-
* @param {string} root
|
|
494
|
-
* @returns {string[]} paths actually removed
|
|
495
|
-
*/
|
|
318
|
+
// `rmdirSync`, never `rm -rf`: a file that appears after the emptiness check
|
|
319
|
+
// makes it fail instead of being lost.
|
|
496
320
|
function removeEmptyUnregisteredDirs(root) {
|
|
497
321
|
const registeredPaths = new Set(parseWorktreeList(root).map((e) => norm(e.path)));
|
|
498
322
|
const removed = [];
|
|
@@ -507,25 +331,9 @@ function removeEmptyUnregisteredDirs(root) {
|
|
|
507
331
|
}
|
|
508
332
|
|
|
509
333
|
/**
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
* These three are the ONLY prune offered, on purpose: none can destroy
|
|
515
|
-
* anything, because everything they touch is either already absent from disk
|
|
516
|
-
* or provably empty. Removing a directory that still HOLDS something is
|
|
517
|
-
* exactly the operation that needs a human looking at it.
|
|
518
|
-
*
|
|
519
|
-
* Order is load-bearing: the empty directories go first, so a root pointer
|
|
520
|
-
* naming one of them becomes dangling in time for the registration sweep below
|
|
521
|
-
* to clear it — one command, not two runs.
|
|
522
|
-
*
|
|
523
|
-
* `clearDanglingWorktreeRegistration` re-checks the dangling predicate itself
|
|
524
|
-
* (path unset, or still exists → no-op) rather than trusting a pre-computed
|
|
525
|
-
* list, so this stays correct even called against a stale audit snapshot.
|
|
526
|
-
*
|
|
527
|
-
* @param {string} [cwd]
|
|
528
|
-
* @returns {Promise<{ pruned: boolean, error?: string, removedEmptyDirs: string[], clearedRegistrations: Array<{feature: string, cleared: boolean}> }>}
|
|
334
|
+
* Touches only what is already gone from disk or provably empty. Order matters:
|
|
335
|
+
* empty dirs go first so a root pointer to one becomes dangling and is cleared
|
|
336
|
+
* in the same run.
|
|
529
337
|
*/
|
|
530
338
|
export async function pruneWorktrees(cwd = process.cwd()) {
|
|
531
339
|
if (!isGitRepo(cwd)) return { pruned: false, error: 'not-a-git-repo', removedEmptyDirs: [], clearedRegistrations: [] };
|
|
@@ -9,11 +9,9 @@ const NESTED_GIT = /(^|\/)\.git(\/|$)/;
|
|
|
9
9
|
const gitOut = (cwd, args, input) => execFileSync('git', args, { cwd, stdio: ['pipe', 'pipe', 'ignore'], encoding: 'utf8', input });
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* @returns {string[]} relative paths copied
|
|
12
|
+
* `.worktreeinclude` parity: a WorktreeCreate hook replaces the native creation, so the native copy
|
|
13
|
+
* never runs. Copies files matching a pattern AND gitignored in the tree, when the tree lacks them;
|
|
14
|
+
* never build artifacts or `node_modules`. Returns the relative paths copied.
|
|
17
15
|
*/
|
|
18
16
|
export function copyWorktreeIncludes(worktreePath, root) {
|
|
19
17
|
const file = join(root, INCLUDE_FILE);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// src/scripts/global-install.js
|
|
2
1
|
import { join, dirname } from 'path';
|
|
3
2
|
import { fileURLToPath } from 'url';
|
|
4
3
|
import { homedir, platform } from 'os';
|
|
@@ -10,10 +9,7 @@ import { installGlobalStatusline } from '../utils/hooks-installer.js';
|
|
|
10
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
10
|
const FRAMEWORK_DIR = join(__dirname, '..', '..', 'framework');
|
|
12
11
|
|
|
13
|
-
/**
|
|
14
|
-
* Ensure the npm global bin directory is in the user's PATH on Windows.
|
|
15
|
-
* Adds it to the PowerShell $PROFILE so new terminals find `morph-spec`.
|
|
16
|
-
*/
|
|
12
|
+
/** Windows only: adds the npm global bin to the PowerShell $PROFILE so new terminals find `morph-spec`. */
|
|
17
13
|
export async function ensureNpmGlobalInPath() {
|
|
18
14
|
if (platform() !== 'win32') return;
|
|
19
15
|
|
|
@@ -21,12 +17,11 @@ export async function ensureNpmGlobalInPath() {
|
|
|
21
17
|
try {
|
|
22
18
|
npmGlobalDir = execSync('npm prefix -g', { encoding: 'utf8' }).trim();
|
|
23
19
|
} catch {
|
|
24
|
-
return;
|
|
20
|
+
return;
|
|
25
21
|
}
|
|
26
22
|
|
|
27
23
|
if (!npmGlobalDir || !existsSync(npmGlobalDir)) return;
|
|
28
24
|
|
|
29
|
-
// --- PowerShell $PROFILE ---
|
|
30
25
|
const psProfile = join(homedir(), 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1');
|
|
31
26
|
const psProfileLegacy = join(homedir(), 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1');
|
|
32
27
|
|
|
@@ -44,7 +39,7 @@ export async function ensureNpmGlobalInPath() {
|
|
|
44
39
|
let content = '';
|
|
45
40
|
if (existsSync(profilePath)) {
|
|
46
41
|
content = await readFile(profilePath, 'utf8');
|
|
47
|
-
if (content.includes(marker)) continue;
|
|
42
|
+
if (content.includes(marker)) continue;
|
|
48
43
|
} else {
|
|
49
44
|
await mkdir(dirname(profilePath), { recursive: true });
|
|
50
45
|
}
|
|
@@ -56,7 +51,6 @@ export async function ensureNpmGlobalInPath() {
|
|
|
56
51
|
}
|
|
57
52
|
}
|
|
58
53
|
|
|
59
|
-
// Run as postinstall script
|
|
60
54
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
61
55
|
const HOOKS_SRC = join(FRAMEWORK_DIR, 'hooks', 'claude-code');
|
|
62
56
|
const { readFileSync } = await import('fs');
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// src/scripts/install-plugin.js
|
|
2
1
|
import { join } from 'path';
|
|
3
2
|
import { homedir } from 'os';
|
|
4
3
|
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
@@ -10,7 +9,6 @@ const CACHE_DIR = join(PLUGINS_DIR, 'cache', 'claude-plugins-official');
|
|
|
10
9
|
const INSTALLED_FILE = join(PLUGINS_DIR, 'installed_plugins.json');
|
|
11
10
|
const GH_REPO = 'anthropics/claude-plugins-official';
|
|
12
11
|
|
|
13
|
-
/** Fetch JSON from GitHub API (no auth — public repo) */
|
|
14
12
|
async function fetchGitHub(path) {
|
|
15
13
|
return new Promise((resolve, reject) => {
|
|
16
14
|
const opts = {
|
|
@@ -29,7 +27,6 @@ async function fetchGitHub(path) {
|
|
|
29
27
|
});
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
/** Download raw file content from a URL, following one redirect */
|
|
33
30
|
async function downloadFile(rawUrl) {
|
|
34
31
|
return new Promise((resolve, reject) => {
|
|
35
32
|
const doGet = (url) => {
|
|
@@ -51,25 +48,21 @@ async function downloadFile(rawUrl) {
|
|
|
51
48
|
});
|
|
52
49
|
}
|
|
53
50
|
|
|
54
|
-
/** Read installed_plugins.json safely */
|
|
55
51
|
export function readInstalledPlugins() {
|
|
56
52
|
if (!existsSync(INSTALLED_FILE)) return { version: 2, plugins: {} };
|
|
57
53
|
try { return JSON.parse(readFileSync(INSTALLED_FILE, 'utf8')); }
|
|
58
54
|
catch { return { version: 2, plugins: {} }; }
|
|
59
55
|
}
|
|
60
56
|
|
|
61
|
-
/** Check if plugin key exists in the plugins map */
|
|
62
57
|
export function isPluginInstalled(plugins, name) {
|
|
63
58
|
return `${name}@claude-plugins-official` in plugins;
|
|
64
59
|
}
|
|
65
60
|
|
|
66
|
-
/** Build an installed_plugins.json entry */
|
|
67
61
|
export function buildInstallEntry(pluginName, installPath, gitCommitSha, version) {
|
|
68
62
|
const now = new Date().toISOString();
|
|
69
63
|
return { scope: 'user', installPath, version, installedAt: now, lastUpdated: now, gitCommitSha };
|
|
70
64
|
}
|
|
71
65
|
|
|
72
|
-
/** Resolve latest commit SHA for a plugin path in the repo */
|
|
73
66
|
export async function resolvePluginVersion(pluginName) {
|
|
74
67
|
const commits = await fetchGitHub(`/repos/${GH_REPO}/commits?path=${pluginName}&per_page=1`);
|
|
75
68
|
if (!Array.isArray(commits) || commits.length === 0) {
|
|
@@ -79,7 +72,6 @@ export async function resolvePluginVersion(pluginName) {
|
|
|
79
72
|
return { sha, shortSha: sha.slice(0, 12) };
|
|
80
73
|
}
|
|
81
74
|
|
|
82
|
-
/** Download all files for a plugin from the repo tree */
|
|
83
75
|
async function downloadPluginFiles(pluginName, sha, destDir) {
|
|
84
76
|
const tree = await fetchGitHub(`/repos/${GH_REPO}/git/trees/${sha}?recursive=1`);
|
|
85
77
|
if (!tree.tree) throw new Error('GitHub tree API returned unexpected shape');
|
|
@@ -98,11 +90,7 @@ async function downloadPluginFiles(pluginName, sha, destDir) {
|
|
|
98
90
|
}
|
|
99
91
|
}
|
|
100
92
|
|
|
101
|
-
/**
|
|
102
|
-
* Install a Claude Code plugin from GitHub.
|
|
103
|
-
* @param {string} pluginName - e.g. "context7"
|
|
104
|
-
* @returns {{ installed: boolean, version: string, alreadyInstalled: boolean }}
|
|
105
|
-
*/
|
|
93
|
+
/** @returns {{ installed: boolean, version: string, alreadyInstalled: boolean }} */
|
|
106
94
|
export async function installPlugin(pluginName) {
|
|
107
95
|
const data = readInstalledPlugins();
|
|
108
96
|
if (isPluginInstalled(data.plugins, pluginName)) {
|