@polymorphism-tech/morph-spec 9.0.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 +8 -9
- package/README.md +38 -36
- package/bin/morph-spec.js +4 -62
- 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 +1 -1
- package/framework/MORPH.md +1 -1
- package/framework/agents.json +12 -4
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +6 -17
- 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 +11 -67
- 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/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/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 +2 -2
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- 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 +2 -2
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- 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 +34 -12
- 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/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/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/REGISTRY.json +9 -9
- package/framework/templates/docs/ui-mockups.md +4 -0
- 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 +14 -96
- 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 +2 -5
- package/src/commands/validation/validate-feature.js +5 -94
- package/src/commands/validation/verify.js +15 -98
- 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/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/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 +2 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +4 -50
- 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/dotnet-introspection.js +20 -106
- package/src/lib/mutation/subset.js +1 -4
- package/src/lib/plan/artifact-vocabulary.js +5 -25
- package/src/lib/plan/checks.js +5 -31
- package/src/lib/plan/plan-parser.js +1 -6
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +5 -23
- package/src/lib/risk-detector.js +39 -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/validators/code-budget/code-budget-validator.js +3 -43
- 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 +20 -125
- package/src/lib/validators/verify-runner.js +52 -358
- 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 +28 -275
- 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/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
|
@@ -1,44 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Quem dispara: o elo `graph-refresh-kick` (PostToolUse, depois de um comando
|
|
5
|
-
* Bash ou PowerShell com git/gh/morph-spec) e o SessionStart
|
|
6
|
-
* (`worktree-context`). Os dois chamam `kickGraphRefresh`, que só olha o disco
|
|
7
|
-
* e solta este mesmo arquivo como processo DESTACADO (`--run <raiz>`): quem
|
|
8
|
-
* dispara nunca espera o graphify, que custa ~80 s no MORPH_GHLBrain mesmo sem
|
|
9
|
-
* mudança nenhuma.
|
|
10
|
-
*
|
|
11
|
-
* Só a raiz. O processo destacado segura o diretório em que roda, e no win32
|
|
12
|
-
* um diretório em uso não se apaga: com o refresh de uma worktree correndo, o
|
|
13
|
-
* `finish` e o `worktree remove` falhavam com "Permission denied" e deixavam a
|
|
14
|
-
* worktree meio removida. Por isso o worker nasce com `cwd` no git common dir,
|
|
15
|
-
* nunca recebe o caminho de uma worktree, e o `graphify update` roda só na
|
|
16
|
-
* raiz — que nenhum `finish` remove. A worktree nasce com o grafo semeado da
|
|
17
|
-
* raiz (`seedGraphFromRoot`) e, dali em diante, é atualizada por
|
|
18
|
-
* `morph-spec graph refresh`, em primeiro plano.
|
|
19
|
-
*
|
|
20
|
-
* Por que não o hook git do próprio graphify (`graphify hook install`), medido
|
|
21
|
-
* no 0.9.50:
|
|
22
|
-
* - ele sai sem fazer nada numa worktree vinculada (compara `--git-dir` com
|
|
23
|
-
* `--git-common-dir`);
|
|
24
|
-
* - não instala `post-merge`: merge, fast-forward e `git pull` — o caminho
|
|
25
|
-
* pelo qual toda feature volta para a raiz — nunca rebuildam;
|
|
26
|
-
* - o rebuild dele só re-extrai `git diff HEAD~1 HEAD`; um commit depois de
|
|
27
|
-
* um fast-forward carimba `built_at_commit` = HEAD num grafo SEM os
|
|
28
|
-
* arquivos que o merge trouxe — "fresh" mentiroso.
|
|
29
|
-
* Aqui o critério é o da regra única (graph-freshness.js): o carimbo e o
|
|
30
|
-
* registro da última rodada contra o HEAD, e as regras do ignore por conteúdo.
|
|
31
|
-
*
|
|
32
|
-
* Um lock por REPOSITÓRIO (no git common dir) serializa o refresh em
|
|
33
|
-
* background com o `graph build|refresh` manual — cada `graphify update` usa
|
|
34
|
-
* todos os núcleos e os dois escreveriam o mesmo graph.json. O dono renova o
|
|
35
|
-
* carimbo do lock antes de cada rodada, e a checagem é refeita depois de
|
|
36
|
-
* soltá-lo, para não perder o commit que chegou enquanto ele era segurado.
|
|
37
|
-
*
|
|
38
|
-
* Fail-open em tudo: sem git, sem graphify ou sem grafo, nada acontece.
|
|
39
|
-
*
|
|
40
|
-
* @module graph-refresh
|
|
41
|
-
*/
|
|
1
|
+
// Só a raiz: o worker destacado nasce com `cwd` no git common dir, porque no win32
|
|
2
|
+
// um diretório em uso não se apaga e o `finish` de uma worktree falhava no meio.
|
|
3
|
+
// A worktree anda por `morph-spec graph refresh`, em primeiro plano.
|
|
42
4
|
|
|
43
5
|
import { spawn, spawnSync } from 'child_process';
|
|
44
6
|
import {
|
|
@@ -58,14 +20,8 @@ import {
|
|
|
58
20
|
|
|
59
21
|
export { primaryTree };
|
|
60
22
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
* `.graphify_root` guarda o caminho ABSOLUTO da raiz, e `cache/` é indexado
|
|
64
|
-
* pelo mtime da árvore de origem — medido: copiado, não poupou re-extração.
|
|
65
|
-
* O `.graphifyignore` também não vem: a worktree lê o do próprio checkout, e
|
|
66
|
-
* pôr `/.graphifyignore` no exclude do common dir escondia também o da RAIZ
|
|
67
|
-
* ainda não versionado (o `git add` dele passava a ser recusado).
|
|
68
|
-
*/
|
|
23
|
+
// Ficam fora de propósito: `.graphify_root` guarda o caminho absoluto da raiz, `cache/`
|
|
24
|
+
// é indexado pelo mtime da origem, e excluir `.graphifyignore` esconderia o da raiz.
|
|
69
25
|
const SEED_FILES = ['manifest.json', '.graphify_analysis.json', '.graphify_labels.json', 'graph.json'];
|
|
70
26
|
|
|
71
27
|
const EXCLUDE_MARKER = '# morph-spec: codebase graph inside worktrees (managed — see graph-refresh)';
|
|
@@ -77,10 +33,6 @@ const GRAPHIFY_TIMEOUT_MS = 20 * 60 * 1000;
|
|
|
77
33
|
const MAX_ROUNDS = 3;
|
|
78
34
|
const LOG_MAX_BYTES = 256 * 1024;
|
|
79
35
|
|
|
80
|
-
/**
|
|
81
|
-
* @param {string} dir
|
|
82
|
-
* @returns {boolean} `dir` está na árvore principal (o refresh automático a cobre)
|
|
83
|
-
*/
|
|
84
36
|
export function isPrimaryTree(dir) {
|
|
85
37
|
const top = dir ? workTreeTop(dir) : null;
|
|
86
38
|
const primary = primaryTree(dir);
|
|
@@ -92,7 +44,6 @@ function stateDir(dir) {
|
|
|
92
44
|
return common ? join(common, 'morph-spec') : null;
|
|
93
45
|
}
|
|
94
46
|
|
|
95
|
-
/** @param {string} dir */
|
|
96
47
|
export function refreshLockPath(dir) {
|
|
97
48
|
const d = stateDir(dir);
|
|
98
49
|
return d ? join(d, 'graph-refresh.lock') : null;
|
|
@@ -111,16 +62,8 @@ function mtimeOf(path) {
|
|
|
111
62
|
}
|
|
112
63
|
}
|
|
113
64
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
* andamento". A idade conta da última renovação: um lock de processo morto,
|
|
117
|
-
* ou sem renovação há mais que o teto, é `live: false`, e o próximo refresh o
|
|
118
|
-
* recupera.
|
|
119
|
-
*
|
|
120
|
-
* @param {string} dir
|
|
121
|
-
* @param {{now?: number, isAlive?: (pid: number) => boolean}} [deps]
|
|
122
|
-
* @returns {{pid: number, since: string, renewed?: string, live: boolean}|null}
|
|
123
|
-
*/
|
|
65
|
+
// A idade conta da última renovação: processo morto ou sem renovação além do
|
|
66
|
+
// teto é `live: false`, e o próximo refresh recupera o lock.
|
|
124
67
|
export function readRefreshLock(dir, { now = Date.now(), isAlive = pidAlive } = {}) {
|
|
125
68
|
const path = refreshLockPath(dir);
|
|
126
69
|
if (!path) return null;
|
|
@@ -133,11 +76,6 @@ export function readRefreshLock(dir, { now = Date.now(), isAlive = pidAlive } =
|
|
|
133
76
|
}
|
|
134
77
|
}
|
|
135
78
|
|
|
136
|
-
/**
|
|
137
|
-
* @param {string} dir
|
|
138
|
-
* @param {{isAlive?: (pid: number) => boolean}} [deps]
|
|
139
|
-
* @returns {string|null} o caminho do lock adquirido, ou null (ocupado ou sem git)
|
|
140
|
-
*/
|
|
141
79
|
export function acquireRefreshLock(dir, deps = {}) {
|
|
142
80
|
const path = refreshLockPath(dir);
|
|
143
81
|
if (!path) return null;
|
|
@@ -162,13 +100,8 @@ export function acquireRefreshLock(dir, deps = {}) {
|
|
|
162
100
|
return null;
|
|
163
101
|
}
|
|
164
102
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
* cada rodada do graphify: uma posse que atravessa várias rodadas passaria do
|
|
168
|
-
* teto de idade e seria roubada no meio de uma escrita.
|
|
169
|
-
*
|
|
170
|
-
* @param {string|null} path
|
|
171
|
-
*/
|
|
103
|
+
// Uma posse que atravessa várias rodadas passaria do teto de idade e seria
|
|
104
|
+
// roubada no meio de uma escrita.
|
|
172
105
|
export function renewRefreshLock(path) {
|
|
173
106
|
if (!path) return;
|
|
174
107
|
try {
|
|
@@ -178,7 +111,6 @@ export function renewRefreshLock(path) {
|
|
|
178
111
|
} catch { /* solto por outro — o release confere de novo */ }
|
|
179
112
|
}
|
|
180
113
|
|
|
181
|
-
/** Solta o lock só se ainda for deste processo. */
|
|
182
114
|
export function releaseRefreshLock(path) {
|
|
183
115
|
if (!path) return;
|
|
184
116
|
try {
|
|
@@ -199,13 +131,7 @@ function appendRefreshLog(dir, entry) {
|
|
|
199
131
|
} catch { /* log é diagnóstico, nunca bloqueia */ }
|
|
200
132
|
}
|
|
201
133
|
|
|
202
|
-
|
|
203
|
-
* `graphify` no PATH. No win32 o libuv só acha `.exe` sem shell; `.cmd`/`.bat`
|
|
204
|
-
* (o que um shim ou um dublê de teste instala) passa pelo shell.
|
|
205
|
-
*
|
|
206
|
-
* @param {NodeJS.ProcessEnv} [env]
|
|
207
|
-
* @returns {{file: string, shell: boolean}|null}
|
|
208
|
-
*/
|
|
134
|
+
// No win32 o libuv só acha `.exe` sem shell; `.cmd`/`.bat` passa pelo shell.
|
|
209
135
|
export function resolveGraphify(env = process.env) {
|
|
210
136
|
const names = process.platform === 'win32'
|
|
211
137
|
? ['graphify.exe', 'graphify.cmd', 'graphify.bat']
|
|
@@ -214,23 +140,13 @@ export function resolveGraphify(env = process.env) {
|
|
|
214
140
|
return file ? { file, shell: /\.(cmd|bat)$/i.test(file) } : null;
|
|
215
141
|
}
|
|
216
142
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
* background E do `graph build|refresh` manual: o mesmo binário resolvido e o
|
|
220
|
-
* mesmo `PYTHONHASHSEED=0` — é o que o hook do próprio graphify fixa para o
|
|
221
|
-
* clustering não trocar as comunidades de lugar a cada rodada.
|
|
222
|
-
*
|
|
223
|
-
* @param {string} tree
|
|
224
|
-
* @param {string[]} args
|
|
225
|
-
* @param {{env?: NodeJS.ProcessEnv, timeout?: number|null}} [opts] `timeout: null` = sem teto
|
|
226
|
-
* @returns {{status: number|null, output: string, unavailable?: boolean, error?: string}}
|
|
227
|
-
*/
|
|
143
|
+
// `PYTHONHASHSEED=0` impede o clustering de trocar as comunidades de lugar a cada
|
|
144
|
+
// rodada. `timeout: null` = sem teto.
|
|
228
145
|
export function runGraphify(tree, args, { env = process.env, timeout = GRAPHIFY_TIMEOUT_MS } = {}) {
|
|
229
146
|
const bin = resolveGraphify(env);
|
|
230
147
|
if (!bin) return { status: null, unavailable: true, output: '' };
|
|
231
|
-
// Nunca com cwd na árvore: o graphify lê o alvo pelo argumento
|
|
232
|
-
//
|
|
233
|
-
// prende no win32 até terminar — o finish e o worktree remove falhavam.
|
|
148
|
+
// Nunca com cwd na árvore: o graphify lê o alvo pelo argumento, e um órfão com
|
|
149
|
+
// cwd numa worktree a prende no win32 até terminar.
|
|
234
150
|
const opts = {
|
|
235
151
|
cwd: resolveGitCommonDir(tree) ?? tree,
|
|
236
152
|
env: { ...env, PYTHONHASHSEED: '0' },
|
|
@@ -247,20 +163,8 @@ export function runGraphify(tree, args, { env = process.env, timeout = GRAPHIFY_
|
|
|
247
163
|
return { status: r.status, output, ...(r.error ? { error: r.error.code } : {}) };
|
|
248
164
|
}
|
|
249
165
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
* raiz, então o grafo copiado ou já é o do HEAD dela ou fica a um
|
|
253
|
-
* `morph-spec graph refresh` de distância — nunca a uma extração do zero.
|
|
254
|
-
*
|
|
255
|
-
* Só `graphify-out/` (no exclude do common dir, senão a worktree fica suja
|
|
256
|
-
* para o `finish`). O registro da worktree copia o que se SABE do grafo da
|
|
257
|
-
* raiz: o commit em que ele está em dia e as regras com que foi extraído —
|
|
258
|
-
* nada inventado. Raiz sem registro das regras deixa a semente sem elas.
|
|
259
|
-
*
|
|
260
|
-
* @param {string} tree
|
|
261
|
-
* @param {string} root
|
|
262
|
-
* @returns {{seeded: boolean, reason?: string}}
|
|
263
|
-
*/
|
|
166
|
+
// O registro da semente copia só o que se SABE do grafo da raiz; `graphify-out/`
|
|
167
|
+
// vai para o exclude do common dir, senão a worktree fica suja para o `finish`.
|
|
264
168
|
export function seedGraphFromRoot(tree, root) {
|
|
265
169
|
if (!tree || !root || treeKey(tree) === treeKey(root)) return { seeded: false, reason: 'is-root' };
|
|
266
170
|
if (existsSync(graphFile(tree))) return { seeded: false, reason: 'present' };
|
|
@@ -272,8 +176,7 @@ export function seedGraphFromRoot(tree, root) {
|
|
|
272
176
|
for (const name of SEED_FILES) {
|
|
273
177
|
const src = join(root, GRAPH_DIR, name);
|
|
274
178
|
if (!existsSync(src)) continue;
|
|
275
|
-
//
|
|
276
|
-
// semeadura completa, e quem lê no meio nunca vê meio arquivo.
|
|
179
|
+
// graph.json por último: a presença dele é o sinal de semeadura completa.
|
|
277
180
|
const tmp = join(dst, `${name}.seed.${process.pid}`);
|
|
278
181
|
copyFileSync(src, tmp);
|
|
279
182
|
renameSync(tmp, join(dst, name));
|
|
@@ -294,13 +197,6 @@ export function seedGraphFromRoot(tree, root) {
|
|
|
294
197
|
}
|
|
295
198
|
}
|
|
296
199
|
|
|
297
|
-
/**
|
|
298
|
-
* Semeia a árvore de `dir` a partir da raiz, se ela for uma worktree sem
|
|
299
|
-
* grafo — o que o SessionStart faz por uma worktree criada antes da semeadura
|
|
300
|
-
* existir.
|
|
301
|
-
*
|
|
302
|
-
* @param {string} dir
|
|
303
|
-
*/
|
|
304
200
|
export function seedFromPrimary(dir) {
|
|
305
201
|
const tree = dir ? workTreeTop(dir) : null;
|
|
306
202
|
const root = primaryTree(dir);
|
|
@@ -308,17 +204,8 @@ export function seedFromPrimary(dir) {
|
|
|
308
204
|
return seedGraphFromRoot(tree, root);
|
|
309
205
|
}
|
|
310
206
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
* chamam). Roda `graphify update` na raiz do repositório de `dir` enquanto o
|
|
314
|
-
* grafo dela precisar (`refreshPending`), até `MAX_ROUNDS` rodadas — um
|
|
315
|
-
* commit que chega no meio pede outra. Cada rodada vai para o registro
|
|
316
|
-
* (graph-freshness.js) e para o log.
|
|
317
|
-
*
|
|
318
|
-
* @param {string} dir - qualquer diretório do repositório; o alvo é sempre a raiz
|
|
319
|
-
* @param {{env?: NodeJS.ProcessEnv, runUpdate?: (tree: string) => {status: number|null, unavailable?: boolean, output?: string}}} [deps]
|
|
320
|
-
* @returns {{ran: boolean, locked?: boolean, reason?: string, results: object[]}}
|
|
321
|
-
*/
|
|
207
|
+
// A checagem é refeita depois de soltar o lock: um commit que chegou enquanto
|
|
208
|
+
// ele era segurado pede outra rodada.
|
|
322
209
|
export function runRefresh(dir, { env = process.env, runUpdate = (tree) => runGraphify(tree, ['update', tree], { env }) } = {}) {
|
|
323
210
|
const root = primaryTree(dir);
|
|
324
211
|
if (!root) return { ran: false, reason: 'no-primary-tree', results: [] };
|
|
@@ -357,23 +244,8 @@ export function runRefresh(dir, { env = process.env, runUpdate = (tree) => runGr
|
|
|
357
244
|
return { ran: results.length > 0, results };
|
|
358
245
|
}
|
|
359
246
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
* árvore do repositório. Só olha o disco, sem subprocesso: sem grafo na raiz
|
|
363
|
-
* não há o que refrescar; grafo que não pede refresh no HEAD lido do disco não
|
|
364
|
-
* paga um processo por comando git; com o graphify registrado como ausente e
|
|
365
|
-
* ainda fora do PATH, não há quem rode. HEAD que o disco não responde (reftable,
|
|
366
|
-
* branch sem commit) fica para o worker, que confere de novo sob o lock.
|
|
367
|
-
*
|
|
368
|
-
* O worker nasce com `cwd` no git common dir e só recebe a raiz: nenhum
|
|
369
|
-
* processo destacado segura o diretório de uma worktree.
|
|
370
|
-
*
|
|
371
|
-
* `MORPH_GRAPH_AUTO_REFRESH=0` desliga (CI, máquina sem folga de CPU).
|
|
372
|
-
*
|
|
373
|
-
* @param {string} dir
|
|
374
|
-
* @param {{spawnFn?: typeof spawn, env?: NodeJS.ProcessEnv}} [deps]
|
|
375
|
-
* @returns {{kicked: boolean, reason?: string, pid?: number}}
|
|
376
|
-
*/
|
|
247
|
+
// Só olha o disco: roda a cada comando git do agente. HEAD que o disco não
|
|
248
|
+
// responde (reftable, branch sem commit) fica para o worker, sob o lock.
|
|
377
249
|
export function kickGraphRefresh(dir, { spawnFn = spawn, env = process.env } = {}) {
|
|
378
250
|
if (env.MORPH_GRAPH_AUTO_REFRESH === '0') return { kicked: false, reason: 'disabled' };
|
|
379
251
|
const root = primaryTree(dir);
|
|
@@ -95,13 +95,8 @@ function kindOfTokens(tokens) {
|
|
|
95
95
|
return null;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
* each `;`/`&&`/`||`/`|`/newline segment, so `morph-spec heavy run -- dotnet test` and `echo "dotnet build"`
|
|
101
|
-
* are not heavy: the first runs under its own slot, the second runs nothing.
|
|
102
|
-
* @param {string} command
|
|
103
|
-
* @returns {string|null}
|
|
104
|
-
*/
|
|
98
|
+
// Judges only each segment's executable: `morph-spec heavy run -- dotnet test` already holds its
|
|
99
|
+
// own slot, and `echo "dotnet build"` runs nothing.
|
|
105
100
|
export function heavyKind(command) {
|
|
106
101
|
for (const segment of splitSegments(command)) {
|
|
107
102
|
const tokens = skipWrappers(tokenize(segment.replace(/^[\s({]+/, '').replace(/[\s)}]+$/, '')));
|
|
@@ -111,11 +106,8 @@ export function heavyKind(command) {
|
|
|
111
106
|
return null;
|
|
112
107
|
}
|
|
113
108
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
* The lease has no live process to watch, so this is the only thing that frees it after a denied or
|
|
117
|
-
* interrupted call.
|
|
118
|
-
*/
|
|
109
|
+
// The lease has no live process to watch: this TTL is the only thing that frees it after a
|
|
110
|
+
// denied or interrupted call.
|
|
119
111
|
const BASH_DEFAULT_TIMEOUT_MS = 2 * 60_000;
|
|
120
112
|
const BASH_MAX_TIMEOUT_MS = 10 * 60_000;
|
|
121
113
|
const LEASE_SLACK_MS = 30_000;
|
|
@@ -1,47 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Claude Code hooks communicate via JSON to stdout.
|
|
5
|
-
* - PreToolUse: { "decision": "block"|"approve", "reason": "..." }
|
|
6
|
-
* - SessionStart/Stop/PostToolUse/etc (non-blocking context injection):
|
|
7
|
-
* { "hookSpecificOutput": { "hookEventName": "...", "additionalContext": "..." } }
|
|
8
|
-
* A top-level `additionalContext` is NOT honored by Claude Code for any
|
|
9
|
-
* event — it must be nested under `hookSpecificOutput` alongside the
|
|
10
|
-
* event name (see https://code.claude.com/docs/en/hooks.md).
|
|
11
|
-
*/
|
|
1
|
+
// A top-level `additionalContext` is NOT honored for any event: it must be nested
|
|
2
|
+
// under `hookSpecificOutput` (https://code.claude.com/docs/en/hooks.md).
|
|
12
3
|
|
|
13
|
-
/**
|
|
14
|
-
* Block a tool use with a reason.
|
|
15
|
-
* @param {string} reason - Human-readable reason for blocking
|
|
16
|
-
*/
|
|
17
4
|
export function block(reason) {
|
|
18
5
|
console.log(JSON.stringify({ decision: 'block', reason }));
|
|
19
6
|
process.exit(0);
|
|
20
7
|
}
|
|
21
8
|
|
|
22
|
-
/**
|
|
23
|
-
* Approve a tool use, optionally injecting context.
|
|
24
|
-
* @param {string} [context] - Additional context to inject
|
|
25
|
-
*/
|
|
26
9
|
export function approve(context) {
|
|
27
10
|
if (context) {
|
|
28
11
|
console.log(JSON.stringify({ decision: 'approve', additionalContext: context }));
|
|
29
12
|
}
|
|
30
|
-
// No output = implicit approve
|
|
31
13
|
process.exit(0);
|
|
32
14
|
}
|
|
33
15
|
|
|
34
|
-
/**
|
|
35
|
-
* Inject additional (non-blocking) context for the current hook event.
|
|
36
|
-
*
|
|
37
|
-
* Must be nested under `hookSpecificOutput` — Claude Code does not honor a
|
|
38
|
-
* top-level `additionalContext` for any event.
|
|
39
|
-
*
|
|
40
|
-
* @param {string} text - Context text to inject
|
|
41
|
-
* @param {string} eventName - The hook event this is responding to (e.g.
|
|
42
|
-
* 'SessionStart', 'Stop', 'PostToolUse') — required, echoed back as
|
|
43
|
-
* `hookSpecificOutput.hookEventName`.
|
|
44
|
-
*/
|
|
45
16
|
export function injectContext(text, eventName) {
|
|
46
17
|
console.log(JSON.stringify({
|
|
47
18
|
hookSpecificOutput: { hookEventName: eventName, additionalContext: text },
|
|
@@ -49,63 +20,26 @@ export function injectContext(text, eventName) {
|
|
|
49
20
|
process.exit(0);
|
|
50
21
|
}
|
|
51
22
|
|
|
52
|
-
/**
|
|
53
|
-
* Silent exit — no output, no blocking.
|
|
54
|
-
*/
|
|
55
23
|
export function pass() {
|
|
56
24
|
process.exit(0);
|
|
57
25
|
}
|
|
58
26
|
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
// Every core hook used to run inline at module scope and call block()/pass()/
|
|
62
|
-
// injectContext() above to print+exit(0) the moment it knew its verdict. That
|
|
63
|
-
// stops being safe once a hook's logic moves into an exported `run(payload)`
|
|
64
|
-
// that can ALSO be called by framework/hooks/claude-code/core/chain.js — which
|
|
65
|
-
// imports several hooks' run() into ONE process and cannot have any of them
|
|
66
|
-
// call process.exit() mid-chain (it would kill the other links). run() must
|
|
67
|
-
// instead RETURN the verdict object; chain.js (or this file's own
|
|
68
|
-
// runStandalone(), below) is what actually prints it and exits.
|
|
69
|
-
//
|
|
70
|
-
// These builders produce the exact same JSON shapes as block()/approve()/
|
|
71
|
-
// injectContext() above — keep them in sync if that shape ever changes.
|
|
72
|
-
|
|
73
|
-
/** Pure equivalent of block(reason) — returns the verdict instead of printing+exiting. */
|
|
27
|
+
// `run()` returns these instead of exiting: chain.js runs several hooks in one
|
|
28
|
+
// process. Same JSON shapes as block()/approve()/injectContext() above.
|
|
74
29
|
export function blockResult(reason) {
|
|
75
30
|
return { decision: 'block', reason };
|
|
76
31
|
}
|
|
77
32
|
|
|
78
|
-
/** Pure equivalent of approve(context) — null means "pass" (nothing to say). */
|
|
79
33
|
export function approveResult(context) {
|
|
80
34
|
return context ? { decision: 'approve', additionalContext: context } : null;
|
|
81
35
|
}
|
|
82
36
|
|
|
83
|
-
/** Pure equivalent of injectContext(text, eventName). */
|
|
84
37
|
export function contextResult(text, eventName) {
|
|
85
38
|
return { hookSpecificOutput: { hookEventName: eventName, additionalContext: text } };
|
|
86
39
|
}
|
|
87
40
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
* `await runStandalone(run);`
|
|
91
|
-
*
|
|
92
|
-
* Reads stdin once, calls the hook's `run(payload)`, prints the JSON verdict
|
|
93
|
-
* (if any) and exits 0 — reproducing exactly what block()/injectContext()/
|
|
94
|
-
* pass() used to do when a hook's logic lived inline at module scope.
|
|
95
|
-
*
|
|
96
|
-
* No-ops when MORPH_HOOK_CHAINED is set: that means core/chain.js already
|
|
97
|
-
* imported this module and will call run() itself with a payload it read
|
|
98
|
-
* once for the whole chain — this file must not ALSO read stdin or exit here
|
|
99
|
-
* (doing so would race chain.js's own stdin read and kill the chain process
|
|
100
|
-
* after the first link). chain.js sets the flag BEFORE its first dynamic
|
|
101
|
-
* import, so every hook it loads sees it already set at call time.
|
|
102
|
-
*
|
|
103
|
-
* Fail-open: any error (including a rejected run()) prints nothing and exits
|
|
104
|
-
* 0 — the same silent-pass behavior every hook's own outer try/catch used to
|
|
105
|
-
* produce on an unexpected error.
|
|
106
|
-
*
|
|
107
|
-
* @param {(payload: object) => Promise<object|null>} run
|
|
108
|
-
*/
|
|
41
|
+
// No-op under MORPH_HOOK_CHAINED: chain.js reads stdin once for the whole chain,
|
|
42
|
+
// and reading or exiting here would kill it after the first link.
|
|
109
43
|
export async function runStandalone(run) {
|
|
110
44
|
if (process.env.MORPH_HOOK_CHAINED) return;
|
|
111
45
|
|
|
@@ -123,17 +57,8 @@ export async function runStandalone(run) {
|
|
|
123
57
|
process.exit(0);
|
|
124
58
|
}
|
|
125
59
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
* `updatedInput` in place of the original `tool_input`, and the decision is
|
|
129
|
-
* an explicit `allow` (a rewrite without a decision is ignored). Use it ONLY
|
|
130
|
-
* when the hook actually changed something — an unconditional `allow` would
|
|
131
|
-
* skip the permission prompt for every call it touches.
|
|
132
|
-
*
|
|
133
|
-
* @param {object} updatedInput - the full replacement tool_input
|
|
134
|
-
* @param {string} eventName - 'PreToolUse'
|
|
135
|
-
* @param {string} [context] - optional additionalContext (what was changed)
|
|
136
|
-
*/
|
|
60
|
+
// A rewrite without a decision is ignored, so it carries `allow`: use it ONLY when
|
|
61
|
+
// the input changed, or every call it touches skips the permission prompt.
|
|
137
62
|
export function updatedInputResult(updatedInput, eventName, context) {
|
|
138
63
|
const out = { hookEventName: eventName, permissionDecision: 'allow', updatedInput };
|
|
139
64
|
if (context) out.additionalContext = context;
|
|
@@ -1,36 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Hooks are installed STANDALONE into `.morph/framework/hooks/` and may never
|
|
5
|
-
* import from `src/`; the only way for a hook to reuse a CLI command (the
|
|
6
|
-
* WorktreeCreate hook needs `worktree setup`/`provision`, WorktreeRemove needs
|
|
7
|
-
* `worktree remove --if-clean`) is to spawn the CLI as a subprocess. This
|
|
8
|
-
* module is the ONE place that knows how to find it.
|
|
9
|
-
*
|
|
10
|
-
* Resolution order:
|
|
11
|
-
* 1. `MORPH_SPEC_BIN` — an explicit command, possibly with leading args and
|
|
12
|
-
* quoted paths (`node "R:/my repo/bin/morph-spec.js"`). What tests and a
|
|
13
|
-
* framework checkout use.
|
|
14
|
-
* 2. `morph-spec` on PATH — the npm-installed bin. On win32 the shim is
|
|
15
|
-
* `morph-spec.cmd`, which Node refuses to spawn without a shell, so it is
|
|
16
|
-
* located by scanning PATH ourselves and then run through the shell with
|
|
17
|
-
* each arg quoted; on POSIX a plain `shell:false` spawn.
|
|
18
|
-
* 3. `npx --no-install morph-spec` — a project-local dependency.
|
|
19
|
-
*
|
|
20
|
-
* ENOENT (or the shell's "not recognized"/"could not determine executable")
|
|
21
|
-
* everywhere → `{ unavailable: true }`, never a throw: the caller decides what
|
|
22
|
-
* degraded behavior looks like.
|
|
23
|
-
*
|
|
24
|
-
* `json` is the LAST line of stdout that parses as a JSON object (the CLI's
|
|
25
|
-
* commands print exactly one JSON line; anything a stray console.log adds
|
|
26
|
-
* before it is skipped), or null.
|
|
27
|
-
*/
|
|
28
|
-
|
|
1
|
+
// A hook cannot import `src/`, so it reuses a CLI command by spawning the CLI; this is the one
|
|
2
|
+
// place that finds it. On win32 `morph-spec.cmd` is located on PATH by hand and run through the
|
|
3
|
+
// shell, since Node refuses to spawn a `.cmd` without one.
|
|
29
4
|
import { spawnSync } from 'child_process';
|
|
30
5
|
import { existsSync } from 'fs';
|
|
31
6
|
import { delimiter, join } from 'path';
|
|
32
7
|
|
|
33
|
-
/** Split a command string into argv, honoring single and double quotes. */
|
|
34
8
|
function splitCommand(command) {
|
|
35
9
|
const tokens = [];
|
|
36
10
|
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
@@ -39,22 +13,13 @@ function splitCommand(command) {
|
|
|
39
13
|
return tokens;
|
|
40
14
|
}
|
|
41
15
|
|
|
42
|
-
/** Quote one argument for cmd.exe (the only shell path this module uses). */
|
|
43
16
|
export function quoteForCmd(arg) {
|
|
44
17
|
if (arg === '') return '""';
|
|
45
18
|
if (!/[\s"&|<>^()]/.test(arg)) return arg;
|
|
46
19
|
return `"${arg.replace(/"/g, '""')}"`;
|
|
47
20
|
}
|
|
48
21
|
|
|
49
|
-
/**
|
|
50
|
-
* First PATH entry holding any of `files`, or null. The entries are walked in
|
|
51
|
-
* PATH order and every name is tried inside each one, the way cmd.exe applies
|
|
52
|
-
* PATHEXT: a `graphify.cmd` early in PATH wins over a `graphify.exe` later.
|
|
53
|
-
*
|
|
54
|
-
* @param {string[]} files
|
|
55
|
-
* @param {NodeJS.ProcessEnv} env
|
|
56
|
-
* @returns {string|null}
|
|
57
|
-
*/
|
|
22
|
+
/** Walks PATH in order and tries every name in each entry, the way cmd.exe applies PATHEXT. */
|
|
58
23
|
export function findFirstOnPath(files, env) {
|
|
59
24
|
const raw = env.PATH || env.Path || env.path || '';
|
|
60
25
|
for (const dir of raw.split(delimiter)) {
|
|
@@ -69,7 +34,6 @@ export function findFirstOnPath(files, env) {
|
|
|
69
34
|
|
|
70
35
|
const findOnPath = (file, env) => findFirstOnPath([file], env);
|
|
71
36
|
|
|
72
|
-
/** Last stdout line that parses as a JSON object, or null. */
|
|
73
37
|
export function lastJsonLine(stdout) {
|
|
74
38
|
const lines = String(stdout || '').split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
75
39
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -82,10 +46,6 @@ export function lastJsonLine(stdout) {
|
|
|
82
46
|
return null;
|
|
83
47
|
}
|
|
84
48
|
|
|
85
|
-
/**
|
|
86
|
-
* The candidates to try, in order. Each is `{ file, args, shell }`.
|
|
87
|
-
* @param {NodeJS.ProcessEnv} env
|
|
88
|
-
*/
|
|
89
49
|
function candidates(env) {
|
|
90
50
|
const list = [];
|
|
91
51
|
const explicit = env.MORPH_SPEC_BIN && splitCommand(env.MORPH_SPEC_BIN);
|
|
@@ -107,10 +67,8 @@ function candidates(env) {
|
|
|
107
67
|
const NOT_FOUND_RE = /not recognized as an internal or external command|could not determine executable to run|command not found|ENOENT/i;
|
|
108
68
|
|
|
109
69
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* @param {string[]} args - CLI argv after the program name
|
|
113
|
-
* @param {{ cwd?: string, timeoutMs?: number, env?: NodeJS.ProcessEnv }} [opts]
|
|
70
|
+
* A missing CLI is `{ unavailable: true }`, never a throw. `json` is the last stdout line that
|
|
71
|
+
* parses as an object.
|
|
114
72
|
* @returns {{ ok: boolean, code: number|null, stdout: string, stderr: string, json: object|null, unavailable: boolean }}
|
|
115
73
|
*/
|
|
116
74
|
export function runMorphSpec(args, { cwd, timeoutMs = 120000, env } = {}) {
|
|
@@ -134,10 +92,8 @@ export function runMorphSpec(args, { cwd, timeoutMs = 120000, env } = {}) {
|
|
|
134
92
|
}
|
|
135
93
|
const stdout = r.stdout || '';
|
|
136
94
|
const stderr = r.stderr || '';
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
// kinds are read that way — a real morph-spec failure is never mistaken
|
|
140
|
-
// for absence.
|
|
95
|
+
// Shell and npx report "not found" as a non-zero exit, never ENOENT; the explicit
|
|
96
|
+
// MORPH_SPEC_BIN is excluded so a real morph-spec failure is never read as absence.
|
|
141
97
|
if (cand.kind !== 'env' && r.status !== 0 && !stdout.trim() && NOT_FOUND_RE.test(stderr)) continue;
|
|
142
98
|
return { ok: r.status === 0, code: r.status, stdout, stderr, json: lastJsonLine(stdout), unavailable: false };
|
|
143
99
|
}
|