ultimate-pi 0.18.0 → 0.19.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/.agents/skills/harness-debate-plan/SKILL.md +1 -1
- package/.agents/skills/harness-decisions/SKILL.md +2 -3
- package/.agents/skills/harness-governor/SKILL.md +6 -5
- package/.agents/skills/harness-orchestration/SKILL.md +4 -4
- package/.agents/skills/harness-review/SKILL.md +7 -7
- package/.agents/skills/harness-sentrux-setup/SKILL.md +4 -3
- package/.agents/skills/harness-steer/SKILL.md +1 -1
- package/.agents/skills/sentrux/SKILL.md +9 -9
- package/.pi/PACKAGING.md +4 -4
- package/.pi/SYSTEM.md +54 -120
- package/.pi/agents/harness/incident-recorder.md +0 -1
- package/.pi/agents/harness/planning/decompose.md +1 -3
- package/.pi/agents/harness/planning/execution-plan-author.md +0 -2
- package/.pi/agents/harness/planning/hypothesis-validator.md +0 -2
- package/.pi/agents/harness/planning/hypothesis.md +0 -2
- package/.pi/agents/harness/planning/implementation-researcher.md +0 -2
- package/.pi/agents/harness/planning/plan-adversary.md +0 -2
- package/.pi/agents/harness/planning/plan-evaluator.md +1 -3
- package/.pi/agents/harness/planning/planning-context.md +0 -2
- package/.pi/agents/harness/planning/review-integrator.md +0 -2
- package/.pi/agents/harness/planning/sprint-contract-auditor.md +0 -2
- package/.pi/agents/harness/planning/stack-researcher.md +0 -2
- package/.pi/agents/harness/{adversary.md → reviewing/adversary.md} +0 -2
- package/.pi/agents/harness/{evaluator.md → reviewing/evaluator.md} +0 -2
- package/.pi/agents/harness/{tie-breaker.md → reviewing/tie-breaker.md} +0 -2
- package/.pi/agents/harness/{executor.md → running/executor.md} +0 -2
- package/.pi/agents/harness/sentrux-bootstrap.md +0 -1
- package/.pi/agents/harness/sentrux-steward.md +0 -2
- package/.pi/agents/harness/trace-librarian.md +0 -1
- package/.pi/extensions/00-harness-project-control.ts +133 -0
- package/.pi/extensions/00-posthog-network-bootstrap.ts +1 -1
- package/.pi/extensions/agt-kill-switch.ts +57 -0
- package/.pi/extensions/agt-prompt-guard.ts +32 -0
- package/.pi/extensions/budget-guard.ts +2 -0
- package/.pi/extensions/custom-footer.ts +46 -145
- package/.pi/extensions/custom-header.ts +1 -1
- package/.pi/extensions/custom-system-prompt.ts +1 -1
- package/.pi/extensions/debate-orchestrator.ts +7 -5
- package/.pi/extensions/harness-ask-user.ts +8 -8
- package/.pi/extensions/harness-debate-tools.ts +27 -43
- package/.pi/extensions/harness-lens.ts +94 -0
- package/.pi/extensions/harness-live-widget.ts +33 -2
- package/.pi/extensions/harness-plan-approval.ts +12 -12
- package/.pi/extensions/harness-run-context.ts +1214 -852
- package/.pi/extensions/harness-subagent-governance.ts +8 -0
- package/.pi/extensions/harness-subagent-submit.ts +36 -164
- package/.pi/extensions/harness-subagents.ts +4 -4
- package/.pi/extensions/harness-telemetry.ts +3 -1
- package/.pi/extensions/harness-web-tools.ts +3 -3
- package/.pi/extensions/observation-bus.ts +2 -0
- package/.pi/extensions/policy-gate.ts +27 -5
- package/.pi/extensions/review-integrity.ts +91 -10
- package/.pi/extensions/sentrux-rules-sync.ts +3 -1
- package/.pi/extensions/subagent-governance.ts +92 -0
- package/.pi/extensions/test-diff-integrity.ts +1 -0
- package/.pi/extensions/trace-recorder.ts +3 -1
- package/.pi/extensions/{ultimate-pi-vcc.ts → vcc-compaction.ts} +1 -1
- package/.pi/harness/README.md +6 -2
- package/.pi/harness/agents.manifest.json +38 -49
- package/.pi/harness/agents.policy.yaml +275 -0
- package/.pi/harness/corpus/graphify-kb-updater.config.json +55 -0
- package/.pi/harness/docs/adrs/0006-sentrux-dual-layer.md +2 -1
- package/.pi/harness/docs/adrs/0030-inhouse-vcc-compaction.md +1 -1
- package/.pi/harness/docs/adrs/0035-plan-phase-review-gate.md +1 -1
- package/.pi/harness/docs/adrs/0044-harness-steer-loop.md +3 -2
- package/.pi/harness/docs/adrs/0045-harness-lens-minimal-contract.md +49 -0
- package/.pi/harness/docs/adrs/0045-phase-scoped-agent-directories.md +33 -0
- package/.pi/harness/docs/adrs/0046-agt-policy-engine.md +51 -0
- package/.pi/harness/docs/adrs/0047-agt-layered-security.md +39 -0
- package/.pi/harness/docs/adrs/0048-tool-call-hook-order.md +25 -0
- package/.pi/harness/docs/adrs/0049-agents-policy-manifest.md +36 -0
- package/.pi/harness/docs/adrs/README.md +6 -0
- package/.pi/harness/docs/graphify-kb-updater-runbook.md +11 -5
- package/.pi/harness/docs/practice-map.md +2 -2
- package/.pi/harness/evolution/README.md +1 -2
- package/.pi/harness/examples/agents.policy.project.yaml +19 -0
- package/.pi/harness/examples/policies/custom-deny-bash.yaml +9 -0
- package/.pi/harness/policies/bash-denylists.yaml +5 -0
- package/.pi/harness/policies/defaults.yaml +51 -0
- package/.pi/harness/policies/orchestrator.yaml +18 -0
- package/.pi/harness/policies/phases.yaml +10 -0
- package/.pi/harness/policies/roles.yaml +5 -0
- package/.pi/harness/policies/web-guard.yaml +5 -0
- package/.pi/harness/policies/workflow-sequences.yaml +9 -0
- package/.pi/harness/sentrux/architecture.manifest.json +26 -4
- package/.pi/harness/specs/harness-spawn-context.schema.json +1 -1
- package/.pi/harness/specs/observation.schema.json +2 -1
- package/.pi/lib/agents-policy.d.mts +70 -0
- package/.pi/lib/agents-policy.mjs +325 -0
- package/.pi/lib/agents-policy.ts +19 -0
- package/.pi/lib/agt/audit-run-sink.ts +52 -0
- package/.pi/lib/agt/build-evaluation-context.ts +285 -0
- package/.pi/lib/agt/config.ts +28 -0
- package/.pi/lib/agt/delegation.ts +69 -0
- package/.pi/lib/agt/evaluate-policy.ts +56 -0
- package/.pi/lib/agt/identity-registry.ts +41 -0
- package/.pi/lib/agt/index.ts +55 -0
- package/.pi/lib/agt/kill-switch-state.ts +11 -0
- package/.pi/lib/agt/legacy-evaluate.ts +101 -0
- package/.pi/lib/agt/policy-engine.ts +154 -0
- package/.pi/lib/agt/rings.ts +21 -0
- package/.pi/lib/agt/sre-hooks.ts +45 -0
- package/.pi/lib/agt/trust-run-store.ts +26 -0
- package/.pi/lib/agt/workflow-history.ts +29 -0
- package/.pi/lib/agt-governance-active.ts +14 -0
- package/.pi/lib/agt-tool-guard.ts +78 -0
- package/.pi/lib/ask-user/dialog.ts +314 -0
- package/.pi/{extensions/lib → lib}/debate-bus-core.ts +10 -10
- package/.pi/{extensions/lib → lib}/debate-bus-state.ts +1 -1
- package/.pi/{extensions/lib → lib}/extension-load-guard.ts +21 -0
- package/.pi/lib/harness-agt-tool-guard.ts +5 -0
- package/.pi/{extensions/lib → lib}/harness-artifact-gate.ts +6 -16
- package/.pi/lib/harness-debate-core-deps.ts +14 -0
- package/.pi/lib/harness-debate-workflow-deps.ts +43 -0
- package/.pi/lib/harness-lens/.gitattributes +1 -0
- package/.pi/lib/harness-lens/clients/edit-autopatch.ts +88 -0
- package/.pi/lib/harness-lens/clients/file-kinds.ts +380 -0
- package/.pi/lib/harness-lens/clients/file-time.ts +215 -0
- package/.pi/lib/harness-lens/clients/file-utils.ts +484 -0
- package/.pi/lib/harness-lens/clients/format-service.ts +276 -0
- package/.pi/lib/harness-lens/clients/formatters.ts +1000 -0
- package/.pi/lib/harness-lens/clients/git-guard.ts +31 -0
- package/.pi/lib/harness-lens/clients/indent-retarget.ts +90 -0
- package/.pi/lib/harness-lens/clients/installer/index.ts +2368 -0
- package/.pi/lib/harness-lens/clients/latency-logger.ts +80 -0
- package/.pi/lib/harness-lens/clients/lens-config.ts +43 -0
- package/.pi/lib/harness-lens/clients/lens-events.ts +164 -0
- package/.pi/lib/harness-lens/clients/lsp/aggregation.ts +91 -0
- package/.pi/lib/harness-lens/clients/lsp/client.ts +1466 -0
- package/.pi/lib/harness-lens/clients/lsp/config.ts +216 -0
- package/.pi/lib/harness-lens/clients/lsp/edits.ts +297 -0
- package/.pi/lib/harness-lens/clients/lsp/index.ts +1355 -0
- package/.pi/lib/harness-lens/clients/lsp/interactive-install.ts +424 -0
- package/.pi/lib/harness-lens/clients/lsp/language.ts +223 -0
- package/.pi/lib/harness-lens/clients/lsp/launch.ts +939 -0
- package/.pi/lib/harness-lens/clients/lsp/lsp-index.ts +11 -0
- package/.pi/lib/harness-lens/clients/lsp/path-utils.ts +12 -0
- package/.pi/lib/harness-lens/clients/lsp/server-strategies.ts +81 -0
- package/.pi/lib/harness-lens/clients/lsp/server.ts +1971 -0
- package/.pi/lib/harness-lens/clients/path-utils.ts +182 -0
- package/.pi/lib/harness-lens/clients/pipeline.ts +360 -0
- package/.pi/lib/harness-lens/clients/project-profile.ts +117 -0
- package/.pi/lib/harness-lens/clients/runtime-agent-end.ts +112 -0
- package/.pi/lib/harness-lens/clients/runtime-config.ts +33 -0
- package/.pi/lib/harness-lens/clients/runtime-coordinator.ts +186 -0
- package/.pi/lib/harness-lens/clients/runtime-tool-result.ts +171 -0
- package/.pi/lib/harness-lens/clients/safe-spawn.ts +339 -0
- package/.pi/lib/harness-lens/clients/secrets-scanner.ts +214 -0
- package/.pi/lib/harness-lens/clients/tool-policy.ts +2072 -0
- package/.pi/lib/harness-lens/clients/types.ts +59 -0
- package/.pi/lib/harness-lens/clients/widget-state.ts +283 -0
- package/.pi/lib/harness-lens/index.ts +532 -0
- package/.pi/lib/harness-lens/tools/lsp-diagnostics.ts +706 -0
- package/.pi/lib/harness-lens/tools/lsp-navigation.ts +1246 -0
- package/.pi/{extensions/lib → lib}/harness-posthog.ts +3 -0
- package/.pi/lib/harness-project-config.ts +91 -0
- package/.pi/lib/harness-run-context-responses.ts +9 -0
- package/.pi/lib/harness-run-context.ts +1 -3
- package/.pi/{extensions/lib/spawn-policy.ts → lib/harness-spawn-policy.ts} +4 -3
- package/.pi/{extensions/lib → lib}/harness-spawn-topology.ts +5 -28
- package/.pi/lib/harness-subagent-auth.ts +51 -0
- package/.pi/{extensions/lib → lib}/harness-subagent-precheck.ts +13 -10
- package/.pi/{extensions/lib → lib}/harness-subagent-submit-pipeline.ts +3 -3
- package/.pi/lib/harness-subagent-submit-register.ts +163 -0
- package/.pi/{extensions/lib → lib}/harness-subagent-submit-registry.ts +1 -55
- package/.pi/{extensions/lib → lib}/harness-subagents-bridge.ts +53 -14
- package/.pi/{extensions/lib → lib}/harness-subprocess-bootstrap.ts +1 -1
- package/.pi/lib/harness-ui-state.ts +27 -12
- package/.pi/{extensions/lib → lib}/plan-approval/create-plan.ts +2 -2
- package/.pi/{extensions/lib → lib}/plan-approval/format-plan.ts +2 -2
- package/.pi/{extensions/lib → lib}/plan-approval/plan-review.ts +162 -201
- package/.pi/{extensions/lib → lib}/plan-approval/render.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-approval/resolve-disk.ts +2 -2
- package/.pi/{extensions/lib → lib}/plan-approval/types.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-approval/validate.ts +3 -3
- package/.pi/{extensions/lib → lib}/plan-approval-readiness.ts +3 -52
- package/.pi/{extensions/lib → lib}/plan-debate-envelope.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-debate-gate.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-debate-lane.ts +1 -4
- package/.pi/{extensions/lib → lib}/plan-messenger.ts +1 -1
- package/.pi/prompts/harness-auto.md +2 -2
- package/.pi/prompts/harness-plan.md +4 -6
- package/.pi/prompts/harness-review.md +9 -9
- package/.pi/prompts/harness-run.md +7 -7
- package/.pi/prompts/harness-setup.md +42 -68
- package/.pi/prompts/harness-steer.md +2 -2
- package/.pi/scripts/README.md +3 -5
- package/.pi/scripts/generate-agents-policy-yaml.mjs +148 -0
- package/.pi/scripts/graphify-kb-updater.mjs +48 -8
- package/.pi/scripts/harness-agents-manifest.mjs +61 -4
- package/.pi/scripts/harness-agt-doctor.ts +36 -0
- package/.pi/scripts/harness-cli-verify.sh +9 -2
- package/.pi/scripts/harness-project-toggle.mjs +129 -0
- package/.pi/scripts/harness-sentrux-cli.mjs +142 -0
- package/.pi/scripts/harness-verify.mjs +113 -39
- package/.pi/scripts/harness-web-policy-guard.mjs +2 -2
- package/.pi/scripts/validate-plan-dag.mjs +65 -74
- package/.pi/scripts/vendor-pi-vcc-settings.stub.ts +2 -2
- package/.pi/scripts/vendor-sync-pi-vcc.sh +1 -1
- package/.pi/skills/architecture/broker-domain/SKILL.md +65 -0
- package/.pi/skills/architecture/cqrs/SKILL.md +63 -0
- package/.pi/skills/architecture/event-driven/SKILL.md +60 -0
- package/.pi/skills/architecture/hexagonal-ports-adapters/SKILL.md +66 -0
- package/.pi/skills/architecture/layered/SKILL.md +68 -0
- package/.pi/skills/architecture/microkernel/SKILL.md +62 -0
- package/.pi/skills/architecture/microservices/SKILL.md +64 -0
- package/.pi/skills/architecture/modular-monolith/SKILL.md +65 -0
- package/.pi/skills/architecture/orchestration-driven-soa/SKILL.md +61 -0
- package/.pi/skills/architecture/pipeline/SKILL.md +63 -0
- package/.pi/skills/architecture/service-based/SKILL.md +64 -0
- package/.pi/skills/architecture/service-mesh/SKILL.md +60 -0
- package/.pi/skills/architecture/space-based/SKILL.md +60 -0
- package/.pi/skills/ast-grep/SKILL.md +40 -321
- package/.pi/skills/delivery/debugging-discipline/SKILL.md +36 -0
- package/.pi/skills/delivery/documentation-update/SKILL.md +33 -0
- package/.pi/skills/delivery/requirements-to-implementation/SKILL.md +34 -0
- package/.pi/skills/delivery/risk-based-verification/SKILL.md +43 -0
- package/.pi/skills/delivery/tradeoff-analysis/SKILL.md +34 -0
- package/.pi/skills/engineering/api-contract-design/SKILL.md +38 -0
- package/.pi/skills/engineering/cohesion-coupling/SKILL.md +43 -0
- package/.pi/skills/engineering/complexity-control/SKILL.md +31 -0
- package/.pi/skills/engineering/defensive-programming/SKILL.md +38 -0
- package/.pi/skills/engineering/dependency-management/SKILL.md +29 -0
- package/.pi/skills/engineering/domain-modeling/SKILL.md +32 -0
- package/.pi/skills/engineering/error-handling/SKILL.md +37 -0
- package/.pi/skills/engineering/legacy-code-seams/SKILL.md +35 -0
- package/.pi/skills/engineering/naming-and-intent/SKILL.md +29 -0
- package/.pi/skills/engineering/refactoring-safe-evolution/SKILL.md +35 -0
- package/.pi/skills/engineering/routine-function-design/SKILL.md +34 -0
- package/.pi/skills/engineering/small-change-discipline/SKILL.md +35 -0
- package/.pi/skills/lsp-navigation/SKILL.md +89 -0
- package/.pi/skills/quality/code-review-self-check/SKILL.md +35 -0
- package/.pi/skills/quality/privacy-data-handling/SKILL.md +26 -0
- package/.pi/skills/quality/security-review/SKILL.md +34 -0
- package/.pi/skills/quality/test-strategy/SKILL.md +33 -0
- package/.pi/skills/quality/testability-design/SKILL.md +33 -0
- package/.pi/skills/systems/concurrency-safety/SKILL.md +32 -0
- package/.pi/skills/systems/data-modeling-migrations/SKILL.md +31 -0
- package/.pi/skills/systems/observability-instrumentation/SKILL.md +32 -0
- package/.pi/skills/systems/performance-measurement/SKILL.md +35 -0
- package/.pi/skills/systems/reliability-design/SKILL.md +32 -0
- package/.sentrux/rules.toml +20 -4
- package/AGENTS.md +5 -0
- package/CHANGELOG.md +26 -0
- package/README.md +85 -58
- package/THIRD_PARTY_NOTICES.md +12 -21
- package/package.json +15 -7
- package/vendor/pi-subagents/src/agents.ts +45 -1
- package/vendor/pi-subagents/src/subagents.ts +866 -811
- package/vendor/pi-vcc/src/core/brief.ts +68 -99
- package/vendor/pi-vcc/src/core/settings.ts +2 -2
- package/.agents/skills/caveman/SKILL.md +0 -67
- package/.pi/agents/harness/meta-optimizer.md +0 -36
- package/.pi/agents/harness/planning/scout-graphify.md +0 -39
- package/.pi/agents/harness/planning/scout-semantic.md +0 -41
- package/.pi/agents/harness/planning/scout-structure.md +0 -37
- package/.pi/extensions/lib/ask-user/dialog.ts +0 -260
- package/.pi/extensions/lib/harness-subagent-auth.ts +0 -209
- package/.pi/extensions/lib/harness-subagent-policy.ts +0 -236
- package/.pi/extensions/pi-model-router-harness.ts +0 -42
- package/.pi/harness/evolution/meta-optimizer.mjs +0 -99
- package/.pi/harness/specs/router-tuning-proposal.schema.json +0 -114
- package/.pi/model-router.example.json +0 -36
- package/.pi/prompts/harness-critic.md +0 -10
- package/.pi/prompts/harness-eval.md +0 -10
- package/.pi/prompts/harness-router-tune.md +0 -52
- package/.pi/scripts/harness-generate-model-router.mjs +0 -327
- package/.pi/scripts/harness-model-router-routing.test.mjs +0 -97
- package/.pi/scripts/harness-sync-model-router.mjs +0 -97
- package/.pi/scripts/vendor-sync-pi-model-router.sh +0 -47
- package/vendor/pi-model-router/.prettierignore +0 -4
- package/vendor/pi-model-router/.prettierrc +0 -5
- package/vendor/pi-model-router/AGENTS.md +0 -39
- package/vendor/pi-model-router/LICENSE +0 -21
- package/vendor/pi-model-router/README.md +0 -99
- package/vendor/pi-model-router/UPSTREAM_PIN.md +0 -10
- package/vendor/pi-model-router/docs/ARCHITECTURE.md +0 -54
- package/vendor/pi-model-router/extensions/commands.ts +0 -720
- package/vendor/pi-model-router/extensions/config.ts +0 -348
- package/vendor/pi-model-router/extensions/constants.ts +0 -1
- package/vendor/pi-model-router/extensions/index.ts +0 -478
- package/vendor/pi-model-router/extensions/provider.ts +0 -580
- package/vendor/pi-model-router/extensions/routing.ts +0 -564
- package/vendor/pi-model-router/extensions/state.ts +0 -52
- package/vendor/pi-model-router/extensions/types.ts +0 -95
- package/vendor/pi-model-router/extensions/ui.ts +0 -144
- package/vendor/pi-model-router/model-router.example.json +0 -48
- package/vendor/pi-model-router/package.json +0 -48
- package/vendor/pi-model-router/tsconfig.json +0 -16
- /package/.pi/{prompts → harness/docs}/planning-rubrics.md +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/fallback.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/render.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/schema.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/types.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/validate-core.mjs +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/validate.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-cocoindex-refresh.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-paths.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-spawn-budget.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-vcc-settings.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-web/run-cli.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-approval/dialog.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-approval/schema.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-eligibility.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-focus.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-id.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-lanes.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-round-status.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-write-guard.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-review-gate.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-review-integrator-rules.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-scope-guard.ts +0 -0
- /package/.pi/{extensions/lib → lib}/posthog-client.ts +0 -0
- /package/.pi/{extensions/lib → lib}/posthog-node.d.ts +0 -0
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatter Definitions for pi-lens
|
|
3
|
+
*
|
|
4
|
+
* Auto-detects formatters based on:
|
|
5
|
+
* - Config files (biome.json, .prettierrc, etc.)
|
|
6
|
+
* - Dependencies (package.json, requirements.txt, etc.)
|
|
7
|
+
* - Binary availability (which/where)
|
|
8
|
+
*
|
|
9
|
+
* Inspired by OpenCode's formatter.ts pattern
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from "node:fs/promises";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { logLatency } from "./latency-logger.js";
|
|
15
|
+
import { safeSpawn, safeSpawnAsync } from "./safe-spawn.js";
|
|
16
|
+
import {
|
|
17
|
+
getAutoInstallToolIdForFormatter,
|
|
18
|
+
getFormatterPolicyForFile,
|
|
19
|
+
getSmartDefaultFormatterName,
|
|
20
|
+
hasBiomeConfig,
|
|
21
|
+
hasBlackConfig,
|
|
22
|
+
hasClangFormatConfig,
|
|
23
|
+
hasCljfmtConfig,
|
|
24
|
+
hasCmakeFormatConfig,
|
|
25
|
+
hasGoogleJavaFormatConfig,
|
|
26
|
+
hasNearestPackageJsonDependency,
|
|
27
|
+
hasNearestPackageJsonField,
|
|
28
|
+
hasOcamlformatConfig,
|
|
29
|
+
hasOxfmtConfig,
|
|
30
|
+
hasPhpCsFixerConfig,
|
|
31
|
+
hasPrettierConfig,
|
|
32
|
+
hasRubocopConfig,
|
|
33
|
+
hasRuffConfig,
|
|
34
|
+
hasSqlfluffConfig,
|
|
35
|
+
hasStandardrbConfig,
|
|
36
|
+
hasStyluaConfig,
|
|
37
|
+
hasVitePlusConfig,
|
|
38
|
+
} from "./tool-policy.js";
|
|
39
|
+
|
|
40
|
+
const _lazyInstallAttempts = new Set<string>();
|
|
41
|
+
|
|
42
|
+
async function tryLazyInstallFormatterTool(
|
|
43
|
+
_tool: "rubocop" | "rustfmt",
|
|
44
|
+
_cwd: string,
|
|
45
|
+
): Promise<boolean> {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// --- Types ---
|
|
50
|
+
|
|
51
|
+
export interface FormatterInfo {
|
|
52
|
+
name: string;
|
|
53
|
+
command: string[]; // Command with $FILE placeholder — used as fallback
|
|
54
|
+
extensions: string[];
|
|
55
|
+
/** Detect if this formatter should be used for a project */
|
|
56
|
+
detect(cwd: string): Promise<boolean>;
|
|
57
|
+
/**
|
|
58
|
+
* Optionally resolve the full command at runtime (venv, vendor/bin, bundle exec).
|
|
59
|
+
* Return null to fall back to the static `command` field.
|
|
60
|
+
* filePath is already resolved to an absolute path.
|
|
61
|
+
*/
|
|
62
|
+
resolveCommand?(filePath: string, cwd: string): Promise<string[] | null>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface FormatterResult {
|
|
66
|
+
success: boolean;
|
|
67
|
+
changed: boolean;
|
|
68
|
+
error?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- Utility Functions ---
|
|
72
|
+
|
|
73
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
74
|
+
try {
|
|
75
|
+
await fs.access(filePath);
|
|
76
|
+
return true;
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function findUp(
|
|
83
|
+
targets: string[],
|
|
84
|
+
startDir: string,
|
|
85
|
+
stopDir: string = path.parse(startDir).root,
|
|
86
|
+
): Promise<string[]> {
|
|
87
|
+
const found: string[] = [];
|
|
88
|
+
let currentDir = startDir;
|
|
89
|
+
|
|
90
|
+
while (currentDir !== stopDir) {
|
|
91
|
+
for (const target of targets) {
|
|
92
|
+
const checkPath = path.join(currentDir, target);
|
|
93
|
+
if (await fileExists(checkPath)) {
|
|
94
|
+
found.push(checkPath);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const parent = path.dirname(currentDir);
|
|
98
|
+
if (parent === currentDir) break;
|
|
99
|
+
currentDir = parent;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return found;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function which(command: string): Promise<string | null> {
|
|
106
|
+
const result = safeSpawn(
|
|
107
|
+
process.platform === "win32" ? "where" : "which",
|
|
108
|
+
[command],
|
|
109
|
+
{ timeout: 5000 },
|
|
110
|
+
);
|
|
111
|
+
if (result.error || result.status !== 0) return null;
|
|
112
|
+
return result.stdout?.trim().split(/\r?\n/)[0] ?? null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function resolveGoFmtBinary(): Promise<string | null> {
|
|
116
|
+
const inPath = await which("gofmt");
|
|
117
|
+
if (inPath) return inPath;
|
|
118
|
+
|
|
119
|
+
const goCheck = safeSpawn("go", ["env", "GOROOT"], {
|
|
120
|
+
timeout: 5000,
|
|
121
|
+
});
|
|
122
|
+
if (goCheck.error || goCheck.status !== 0) return null;
|
|
123
|
+
|
|
124
|
+
const goroot = (goCheck.stdout ?? "").trim();
|
|
125
|
+
if (!goroot) return null;
|
|
126
|
+
|
|
127
|
+
const binary = path.join(
|
|
128
|
+
goroot,
|
|
129
|
+
"bin",
|
|
130
|
+
process.platform === "win32" ? "gofmt.exe" : "gofmt",
|
|
131
|
+
);
|
|
132
|
+
return (await fileExists(binary)) ? binary : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --- Venv / Local Binary Helpers ---
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Walk up from cwd looking for a binary in .venv or venv.
|
|
139
|
+
* Returns the absolute path if found, null otherwise.
|
|
140
|
+
*/
|
|
141
|
+
async function findInVenv(binary: string, cwd: string): Promise<string | null> {
|
|
142
|
+
const isWin = process.platform === "win32";
|
|
143
|
+
const candidates = isWin
|
|
144
|
+
? [
|
|
145
|
+
`.venv/Scripts/${binary}.exe`,
|
|
146
|
+
`venv/Scripts/${binary}.exe`,
|
|
147
|
+
`.venv/Scripts/${binary}`,
|
|
148
|
+
`venv/Scripts/${binary}`,
|
|
149
|
+
]
|
|
150
|
+
: [`.venv/bin/${binary}`, `venv/bin/${binary}`];
|
|
151
|
+
|
|
152
|
+
let dir = cwd;
|
|
153
|
+
const root = path.parse(dir).root;
|
|
154
|
+
while (dir !== root) {
|
|
155
|
+
for (const candidate of candidates) {
|
|
156
|
+
const full = path.join(dir, candidate);
|
|
157
|
+
if (await fileExists(full)) return full;
|
|
158
|
+
}
|
|
159
|
+
const parent = path.dirname(dir);
|
|
160
|
+
if (parent === dir) break;
|
|
161
|
+
dir = parent;
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check vendor/bin for PHP Composer-managed tools.
|
|
168
|
+
* Walks up from cwd to find vendor/bin/<binary>.
|
|
169
|
+
*/
|
|
170
|
+
async function findInVendorBin(
|
|
171
|
+
binary: string,
|
|
172
|
+
cwd: string,
|
|
173
|
+
): Promise<string | null> {
|
|
174
|
+
const isWin = process.platform === "win32";
|
|
175
|
+
const names = isWin ? [`${binary}.bat`, binary] : [binary];
|
|
176
|
+
let dir = cwd;
|
|
177
|
+
const root = path.parse(dir).root;
|
|
178
|
+
while (dir !== root) {
|
|
179
|
+
for (const name of names) {
|
|
180
|
+
const full = path.join(dir, "vendor", "bin", name);
|
|
181
|
+
if (await fileExists(full)) return full;
|
|
182
|
+
}
|
|
183
|
+
const parent = path.dirname(dir);
|
|
184
|
+
if (parent === dir) break;
|
|
185
|
+
dir = parent;
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Check node_modules/.bin for locally installed Node tools.
|
|
192
|
+
* Walks up from cwd to find node_modules/.bin/<binary>.
|
|
193
|
+
*/
|
|
194
|
+
async function findInNodeModules(
|
|
195
|
+
binary: string,
|
|
196
|
+
cwd: string,
|
|
197
|
+
): Promise<string | null> {
|
|
198
|
+
const isWin = process.platform === "win32";
|
|
199
|
+
let dir = cwd;
|
|
200
|
+
const root = path.parse(dir).root;
|
|
201
|
+
while (dir !== root) {
|
|
202
|
+
const candidates = isWin
|
|
203
|
+
? [
|
|
204
|
+
path.join(dir, "node_modules", ".bin", `${binary}.cmd`),
|
|
205
|
+
path.join(dir, "node_modules", ".bin", binary),
|
|
206
|
+
]
|
|
207
|
+
: [path.join(dir, "node_modules", ".bin", binary)];
|
|
208
|
+
for (const full of candidates) {
|
|
209
|
+
if (await fileExists(full)) return full;
|
|
210
|
+
}
|
|
211
|
+
const parent = path.dirname(dir);
|
|
212
|
+
if (parent === dir) break;
|
|
213
|
+
dir = parent;
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Returns true if `bundle exec <gem>` should be used:
|
|
220
|
+
* bundle binary is available AND Gemfile.lock exists in the tree.
|
|
221
|
+
*/
|
|
222
|
+
async function canUseBundleExec(cwd: string): Promise<boolean> {
|
|
223
|
+
if ((await which("bundle")) === null) return false;
|
|
224
|
+
const lockfiles = await findUp(["Gemfile.lock"], cwd);
|
|
225
|
+
return lockfiles.length > 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function resolveManagedSmartDefaultCommand(
|
|
229
|
+
formatterName: string,
|
|
230
|
+
filePath: string,
|
|
231
|
+
args: string[],
|
|
232
|
+
): Promise<string[] | null> {
|
|
233
|
+
const toolId = getAutoInstallToolIdForFormatter(formatterName);
|
|
234
|
+
if (!toolId) return null;
|
|
235
|
+
const { ensureTool } = await import("./installer/index.js");
|
|
236
|
+
const installed = await ensureTool(toolId);
|
|
237
|
+
if (!installed) return null;
|
|
238
|
+
return [installed, ...args, filePath];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function hasExplicitFormatterConfig(
|
|
242
|
+
formatterName: string,
|
|
243
|
+
cwd: string,
|
|
244
|
+
): boolean {
|
|
245
|
+
switch (formatterName) {
|
|
246
|
+
case "biome":
|
|
247
|
+
return hasBiomeConfig(cwd);
|
|
248
|
+
case "prettier":
|
|
249
|
+
return (
|
|
250
|
+
hasPrettierConfig(cwd) || hasNearestPackageJsonField(cwd, "prettier")
|
|
251
|
+
);
|
|
252
|
+
case "oxfmt":
|
|
253
|
+
return (
|
|
254
|
+
hasOxfmtConfig(cwd) ||
|
|
255
|
+
hasVitePlusConfig(cwd) ||
|
|
256
|
+
hasNearestPackageJsonDependency(cwd, "@oxc-project/oxfmt")
|
|
257
|
+
);
|
|
258
|
+
case "ruff":
|
|
259
|
+
return hasRuffConfig(cwd);
|
|
260
|
+
case "black":
|
|
261
|
+
return hasBlackConfig(cwd);
|
|
262
|
+
case "sqlfluff":
|
|
263
|
+
return hasSqlfluffConfig(cwd);
|
|
264
|
+
case "rubocop":
|
|
265
|
+
return hasRubocopConfig(cwd);
|
|
266
|
+
case "standardrb":
|
|
267
|
+
return hasStandardrbConfig(cwd);
|
|
268
|
+
case "clang-format":
|
|
269
|
+
return hasClangFormatConfig(cwd);
|
|
270
|
+
case "php-cs-fixer":
|
|
271
|
+
return hasPhpCsFixerConfig(cwd);
|
|
272
|
+
case "stylua":
|
|
273
|
+
return hasStyluaConfig(cwd);
|
|
274
|
+
case "ocamlformat":
|
|
275
|
+
return hasOcamlformatConfig(cwd);
|
|
276
|
+
case "google-java-format":
|
|
277
|
+
return hasGoogleJavaFormatConfig(cwd);
|
|
278
|
+
case "cljfmt":
|
|
279
|
+
return hasCljfmtConfig(cwd);
|
|
280
|
+
case "cmake-format":
|
|
281
|
+
return hasCmakeFormatConfig(cwd);
|
|
282
|
+
default:
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// --- Formatter Definitions ---
|
|
288
|
+
|
|
289
|
+
async function hasEditorConfig(cwd: string): Promise<boolean> {
|
|
290
|
+
try {
|
|
291
|
+
await fs.access(path.join(cwd, ".editorconfig"));
|
|
292
|
+
return true;
|
|
293
|
+
} catch {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export const biomeFormatter: FormatterInfo = {
|
|
299
|
+
name: "biome",
|
|
300
|
+
command: ["npx", "@biomejs/biome", "format", "--write", "$FILE"],
|
|
301
|
+
async resolveCommand(filePath, cwd) {
|
|
302
|
+
const editorConfigFlag = (await hasEditorConfig(cwd))
|
|
303
|
+
? ["--use-editorconfig=true"]
|
|
304
|
+
: [];
|
|
305
|
+
const local = await findInNodeModules("biome", cwd);
|
|
306
|
+
if (local)
|
|
307
|
+
return [local, "format", "--write", ...editorConfigFlag, filePath];
|
|
308
|
+
const toolId = getAutoInstallToolIdForFormatter("biome");
|
|
309
|
+
if (!toolId) return null;
|
|
310
|
+
const { ensureTool } = await import("./installer/index.js");
|
|
311
|
+
const installed = await ensureTool(toolId);
|
|
312
|
+
if (installed)
|
|
313
|
+
return [installed, "format", "--write", ...editorConfigFlag, filePath];
|
|
314
|
+
return null;
|
|
315
|
+
},
|
|
316
|
+
extensions: [
|
|
317
|
+
".js",
|
|
318
|
+
".jsx",
|
|
319
|
+
".mjs",
|
|
320
|
+
".cjs",
|
|
321
|
+
".ts",
|
|
322
|
+
".tsx",
|
|
323
|
+
".mts",
|
|
324
|
+
".cts",
|
|
325
|
+
".json",
|
|
326
|
+
".jsonc",
|
|
327
|
+
".css",
|
|
328
|
+
".scss",
|
|
329
|
+
".sass",
|
|
330
|
+
".vue",
|
|
331
|
+
".svelte",
|
|
332
|
+
".html",
|
|
333
|
+
".htm",
|
|
334
|
+
],
|
|
335
|
+
async detect(cwd: string) {
|
|
336
|
+
return (
|
|
337
|
+
hasBiomeConfig(cwd) ||
|
|
338
|
+
hasNearestPackageJsonDependency(cwd, "@biomejs/biome")
|
|
339
|
+
);
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
export const prettierFormatter: FormatterInfo = {
|
|
344
|
+
name: "prettier",
|
|
345
|
+
command: ["npx", "prettier", "--write", "$FILE"],
|
|
346
|
+
async resolveCommand(filePath, cwd) {
|
|
347
|
+
const local = await findInNodeModules("prettier", cwd);
|
|
348
|
+
if (local) return [local, "--write", filePath];
|
|
349
|
+
return resolveManagedSmartDefaultCommand("prettier", filePath, ["--write"]);
|
|
350
|
+
},
|
|
351
|
+
extensions: [
|
|
352
|
+
".js",
|
|
353
|
+
".jsx",
|
|
354
|
+
".mjs",
|
|
355
|
+
".cjs",
|
|
356
|
+
".ts",
|
|
357
|
+
".tsx",
|
|
358
|
+
".mts",
|
|
359
|
+
".cts",
|
|
360
|
+
".json",
|
|
361
|
+
".jsonc",
|
|
362
|
+
".css",
|
|
363
|
+
".scss",
|
|
364
|
+
".sass",
|
|
365
|
+
".less",
|
|
366
|
+
".vue",
|
|
367
|
+
".svelte",
|
|
368
|
+
".html",
|
|
369
|
+
".htm",
|
|
370
|
+
".md",
|
|
371
|
+
".mdx",
|
|
372
|
+
".yaml",
|
|
373
|
+
".yml",
|
|
374
|
+
".graphql",
|
|
375
|
+
".gql",
|
|
376
|
+
],
|
|
377
|
+
async detect(cwd: string) {
|
|
378
|
+
return (
|
|
379
|
+
hasPrettierConfig(cwd) ||
|
|
380
|
+
hasNearestPackageJsonDependency(cwd, "prettier") ||
|
|
381
|
+
hasNearestPackageJsonField(cwd, "prettier")
|
|
382
|
+
);
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
export const oxfmtFormatter: FormatterInfo = {
|
|
387
|
+
name: "oxfmt",
|
|
388
|
+
command: ["oxfmt", "$FILE"],
|
|
389
|
+
async resolveCommand(filePath, cwd) {
|
|
390
|
+
if (hasVitePlusConfig(cwd)) {
|
|
391
|
+
const localVp = await findInNodeModules("vp", cwd);
|
|
392
|
+
if (localVp) return [localVp, "fmt", filePath, "--write"];
|
|
393
|
+
const globalVp = await which("vp");
|
|
394
|
+
if (globalVp) return [globalVp, "fmt", filePath, "--write"];
|
|
395
|
+
}
|
|
396
|
+
const local = await findInNodeModules("oxfmt", cwd);
|
|
397
|
+
if (local) return [local, filePath];
|
|
398
|
+
const found = await which("oxfmt");
|
|
399
|
+
if (found) return [found, filePath];
|
|
400
|
+
return null;
|
|
401
|
+
},
|
|
402
|
+
extensions: [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"],
|
|
403
|
+
async detect(cwd: string) {
|
|
404
|
+
return (
|
|
405
|
+
hasOxfmtConfig(cwd) ||
|
|
406
|
+
hasVitePlusConfig(cwd) ||
|
|
407
|
+
hasNearestPackageJsonDependency(cwd, "@oxc-project/oxfmt")
|
|
408
|
+
);
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
export const ruffFormatter: FormatterInfo = {
|
|
413
|
+
name: "ruff",
|
|
414
|
+
command: ["ruff", "format", "$FILE"],
|
|
415
|
+
extensions: [".py", ".pyi"],
|
|
416
|
+
async resolveCommand(filePath, cwd) {
|
|
417
|
+
const venv = await findInVenv("ruff", cwd);
|
|
418
|
+
if (venv) return [venv, "format", filePath];
|
|
419
|
+
const toolId = getAutoInstallToolIdForFormatter("ruff");
|
|
420
|
+
if (!toolId) return null;
|
|
421
|
+
const { ensureTool } = await import("./installer/index.js");
|
|
422
|
+
const installed = await ensureTool(toolId);
|
|
423
|
+
if (installed) return [installed, "format", filePath];
|
|
424
|
+
return null;
|
|
425
|
+
},
|
|
426
|
+
async detect(cwd: string) {
|
|
427
|
+
if (hasRuffConfig(cwd)) return true;
|
|
428
|
+
// No-config fallback: if Ruff is already available, allow formatter usage.
|
|
429
|
+
// This keeps Python default behavior consistent with startup defaults.
|
|
430
|
+
const { getToolPath } = await import("./installer/index.js");
|
|
431
|
+
const installed = await getToolPath("ruff");
|
|
432
|
+
return Boolean(installed);
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
export const blackFormatter: FormatterInfo = {
|
|
437
|
+
name: "black",
|
|
438
|
+
command: ["black", "$FILE"],
|
|
439
|
+
extensions: [".py", ".pyi"],
|
|
440
|
+
async resolveCommand(filePath, cwd) {
|
|
441
|
+
const venv = await findInVenv("black", cwd);
|
|
442
|
+
if (venv) return [venv, filePath];
|
|
443
|
+
return null;
|
|
444
|
+
},
|
|
445
|
+
async detect(cwd: string) {
|
|
446
|
+
return hasBlackConfig(cwd);
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
export const sqlfluffFormatter: FormatterInfo = {
|
|
451
|
+
name: "sqlfluff",
|
|
452
|
+
command: ["sqlfluff", "fix", "--force", "$FILE"],
|
|
453
|
+
extensions: [".sql"],
|
|
454
|
+
async resolveCommand(filePath, cwd) {
|
|
455
|
+
const venv = await findInVenv("sqlfluff", cwd);
|
|
456
|
+
if (venv) return [venv, "fix", "--force", filePath];
|
|
457
|
+
return null;
|
|
458
|
+
},
|
|
459
|
+
async detect(cwd: string) {
|
|
460
|
+
return hasSqlfluffConfig(cwd);
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
export const gofmtFormatter: FormatterInfo = {
|
|
465
|
+
name: "gofmt",
|
|
466
|
+
command: ["gofmt", "-w", "$FILE"],
|
|
467
|
+
extensions: [".go"],
|
|
468
|
+
async resolveCommand(filePath, _cwd) {
|
|
469
|
+
const gofmtBinary = await resolveGoFmtBinary();
|
|
470
|
+
if (!gofmtBinary) return null;
|
|
471
|
+
return [gofmtBinary, "-w", filePath];
|
|
472
|
+
},
|
|
473
|
+
async detect(_cwd: string) {
|
|
474
|
+
return (await resolveGoFmtBinary()) !== null;
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
export const rustfmtFormatter: FormatterInfo = {
|
|
479
|
+
name: "rustfmt",
|
|
480
|
+
command: ["rustfmt", "$FILE"],
|
|
481
|
+
extensions: [".rs"],
|
|
482
|
+
async detect(cwd: string) {
|
|
483
|
+
if ((await which("rustfmt")) !== null) return true;
|
|
484
|
+
// If we're in a Rust project, attempt one lazy install of rustfmt component.
|
|
485
|
+
const rustProject = (await findUp(["Cargo.toml"], cwd)).length > 0;
|
|
486
|
+
if (!rustProject) return false;
|
|
487
|
+
if ((await which("rustup")) === null) return false;
|
|
488
|
+
// rustfmt: PATH-only; no lazy gem/rustup install from harness-lens
|
|
489
|
+
return (await which("rustfmt")) !== null;
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
export const zigFormatter: FormatterInfo = {
|
|
494
|
+
name: "zig",
|
|
495
|
+
command: ["zig", "fmt", "$FILE"],
|
|
496
|
+
extensions: [".zig", ".zon"],
|
|
497
|
+
async detect(_cwd: string) {
|
|
498
|
+
return (await which("zig")) !== null;
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
export const dartFormatter: FormatterInfo = {
|
|
503
|
+
name: "dart",
|
|
504
|
+
command: ["dart", "format", "$FILE"],
|
|
505
|
+
extensions: [".dart"],
|
|
506
|
+
async detect(_cwd: string) {
|
|
507
|
+
return (await which("dart")) !== null;
|
|
508
|
+
},
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
export const shfmtFormatter: FormatterInfo = {
|
|
512
|
+
name: "shfmt",
|
|
513
|
+
command: ["shfmt", "-w", "$FILE"],
|
|
514
|
+
extensions: [".sh", ".bash"],
|
|
515
|
+
async resolveCommand(filePath, _cwd) {
|
|
516
|
+
const inPath = await which("shfmt");
|
|
517
|
+
if (inPath) return [inPath, "-w", filePath];
|
|
518
|
+
return resolveManagedSmartDefaultCommand("shfmt", filePath, ["-w"]);
|
|
519
|
+
},
|
|
520
|
+
async detect(_cwd: string) {
|
|
521
|
+
if ((await which("shfmt")) !== null) return true;
|
|
522
|
+
const { getToolPath } = await import("./installer/index.js");
|
|
523
|
+
return Boolean(await getToolPath("shfmt"));
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
export const nixfmtFormatter: FormatterInfo = {
|
|
528
|
+
name: "nixfmt",
|
|
529
|
+
command: ["nixfmt", "$FILE"],
|
|
530
|
+
extensions: [".nix"],
|
|
531
|
+
async detect(_cwd: string) {
|
|
532
|
+
return (await which("nixfmt")) !== null;
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
export const mixFormatter: FormatterInfo = {
|
|
537
|
+
name: "mix",
|
|
538
|
+
command: ["mix", "format", "$FILE"],
|
|
539
|
+
extensions: [".ex", ".exs", ".eex", ".heex", ".leex"],
|
|
540
|
+
async detect(_cwd: string) {
|
|
541
|
+
return (await which("mix")) !== null;
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
export const ocamlformatFormatter: FormatterInfo = {
|
|
546
|
+
name: "ocamlformat",
|
|
547
|
+
command: ["ocamlformat", "-i", "$FILE"],
|
|
548
|
+
extensions: [".ml", ".mli"],
|
|
549
|
+
async detect(cwd: string) {
|
|
550
|
+
const hasBinary = (await which("ocamlformat")) !== null;
|
|
551
|
+
if (!hasBinary) return false;
|
|
552
|
+
const configs = [".ocamlformat"];
|
|
553
|
+
const found = await findUp(configs, cwd);
|
|
554
|
+
return found.length > 0;
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
export const clangFormatFormatter: FormatterInfo = {
|
|
559
|
+
name: "clang-format",
|
|
560
|
+
command: ["clang-format", "-i", "$FILE"],
|
|
561
|
+
extensions: [".c", ".cc", ".cpp", ".cxx", ".h", ".hpp", ".ino"],
|
|
562
|
+
async detect(cwd: string) {
|
|
563
|
+
const hasBinary = (await which("clang-format")) !== null;
|
|
564
|
+
if (!hasBinary) return false;
|
|
565
|
+
const configs = [".clang-format", "_clang-format"];
|
|
566
|
+
const found = await findUp(configs, cwd);
|
|
567
|
+
return found.length > 0;
|
|
568
|
+
},
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
export const ktlintFormatter: FormatterInfo = {
|
|
572
|
+
name: "ktlint",
|
|
573
|
+
command: ["ktlint", "-F", "$FILE"],
|
|
574
|
+
extensions: [".kt", ".kts"],
|
|
575
|
+
async resolveCommand(filePath, _cwd) {
|
|
576
|
+
const inPath = await which("ktlint");
|
|
577
|
+
if (inPath) return [inPath, "-F", filePath];
|
|
578
|
+
return resolveManagedSmartDefaultCommand("ktlint", filePath, ["-F"]);
|
|
579
|
+
},
|
|
580
|
+
async detect(_cwd: string) {
|
|
581
|
+
if ((await which("ktlint")) !== null) return true;
|
|
582
|
+
const { getToolPath } = await import("./installer/index.js");
|
|
583
|
+
return Boolean(await getToolPath("ktlint"));
|
|
584
|
+
},
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
export const rubocopFormatter: FormatterInfo = {
|
|
588
|
+
name: "rubocop",
|
|
589
|
+
command: ["rubocop", "-a", "--no-color", "$FILE"],
|
|
590
|
+
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
|
591
|
+
async resolveCommand(filePath, cwd) {
|
|
592
|
+
if (await canUseBundleExec(cwd))
|
|
593
|
+
return ["bundle", "exec", "rubocop", "-a", "--no-color", filePath];
|
|
594
|
+
return null;
|
|
595
|
+
},
|
|
596
|
+
async detect(cwd: string) {
|
|
597
|
+
if (!hasRubocopConfig(cwd)) return false;
|
|
598
|
+
if ((await which("rubocop")) !== null) return true;
|
|
599
|
+
// rubocop: PATH-only; no lazy gem install from harness-lens
|
|
600
|
+
return (await which("rubocop")) !== null;
|
|
601
|
+
},
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
export const standardrbFormatter: FormatterInfo = {
|
|
605
|
+
name: "standardrb",
|
|
606
|
+
command: ["standardrb", "--fix", "$FILE"],
|
|
607
|
+
extensions: [".rb", ".rake"],
|
|
608
|
+
async resolveCommand(filePath, cwd) {
|
|
609
|
+
if (await canUseBundleExec(cwd))
|
|
610
|
+
return ["bundle", "exec", "standardrb", "--fix", filePath];
|
|
611
|
+
return null;
|
|
612
|
+
},
|
|
613
|
+
async detect(cwd: string) {
|
|
614
|
+
if (!hasStandardrbConfig(cwd)) return false;
|
|
615
|
+
return (await which("standardrb")) !== null;
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
export const gleamFormatter: FormatterInfo = {
|
|
620
|
+
name: "gleam",
|
|
621
|
+
command: ["gleam", "format", "$FILE"],
|
|
622
|
+
extensions: [".gleam"],
|
|
623
|
+
async detect(cwd: string) {
|
|
624
|
+
// Present if gleam.toml exists (any Gleam project)
|
|
625
|
+
const found = await findUp(["gleam.toml"], cwd);
|
|
626
|
+
if (found.length > 0) return (await which("gleam")) !== null;
|
|
627
|
+
return false;
|
|
628
|
+
},
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
export const terraformFormatter: FormatterInfo = {
|
|
632
|
+
name: "terraform",
|
|
633
|
+
command: ["terraform", "fmt", "$FILE"],
|
|
634
|
+
extensions: [".tf", ".tfvars"],
|
|
635
|
+
async detect(_cwd: string) {
|
|
636
|
+
return (await which("terraform")) !== null;
|
|
637
|
+
},
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
export const phpCsFixerFormatter: FormatterInfo = {
|
|
641
|
+
name: "php-cs-fixer",
|
|
642
|
+
command: ["php-cs-fixer", "fix", "$FILE"],
|
|
643
|
+
extensions: [".php"],
|
|
644
|
+
async resolveCommand(filePath, cwd) {
|
|
645
|
+
const vendor = await findInVendorBin("php-cs-fixer", cwd);
|
|
646
|
+
if (vendor) return [vendor, "fix", filePath];
|
|
647
|
+
return null;
|
|
648
|
+
},
|
|
649
|
+
async detect(cwd: string) {
|
|
650
|
+
const vendorBin = await findInVendorBin("php-cs-fixer", cwd);
|
|
651
|
+
const globalBin = await which("php-cs-fixer");
|
|
652
|
+
if (!vendorBin && !globalBin) return false;
|
|
653
|
+
// Only run if project has explicit config
|
|
654
|
+
const configs = [".php-cs-fixer.php", ".php-cs-fixer.dist.php"];
|
|
655
|
+
const found = await findUp(configs, cwd);
|
|
656
|
+
return found.length > 0;
|
|
657
|
+
},
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
export const csharpierFormatter: FormatterInfo = {
|
|
661
|
+
name: "csharpier",
|
|
662
|
+
command: ["dotnet", "csharpier", "$FILE"],
|
|
663
|
+
extensions: [".cs"],
|
|
664
|
+
async detect(_cwd: string) {
|
|
665
|
+
// Check dotnet is available AND csharpier tool is installed
|
|
666
|
+
if ((await which("dotnet")) === null) return false;
|
|
667
|
+
const result = safeSpawn("dotnet", ["csharpier", "--version"], {
|
|
668
|
+
timeout: 5000,
|
|
669
|
+
});
|
|
670
|
+
return !result.error && result.status === 0;
|
|
671
|
+
},
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
export const fantomasFormatter: FormatterInfo = {
|
|
675
|
+
name: "fantomas",
|
|
676
|
+
command: ["fantomas", "$FILE"],
|
|
677
|
+
extensions: [".fs", ".fsi", ".fsx"],
|
|
678
|
+
async detect(_cwd: string) {
|
|
679
|
+
return (await which("fantomas")) !== null;
|
|
680
|
+
},
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
export const swiftformatFormatter: FormatterInfo = {
|
|
684
|
+
name: "swiftformat",
|
|
685
|
+
command: ["swiftformat", "$FILE"],
|
|
686
|
+
extensions: [".swift"],
|
|
687
|
+
async detect(_cwd: string) {
|
|
688
|
+
return (await which("swiftformat")) !== null;
|
|
689
|
+
},
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
export const styluaFormatter: FormatterInfo = {
|
|
693
|
+
name: "stylua",
|
|
694
|
+
command: ["stylua", "$FILE"],
|
|
695
|
+
extensions: [".lua"],
|
|
696
|
+
async detect(cwd: string) {
|
|
697
|
+
if ((await which("stylua")) === null) return false;
|
|
698
|
+
// Prefer explicit config but also run if binary is present in a Lua project
|
|
699
|
+
const configs = ["stylua.toml", ".stylua.toml"];
|
|
700
|
+
const found = await findUp(configs, cwd);
|
|
701
|
+
return found.length > 0;
|
|
702
|
+
},
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
export const ormoluFormatter: FormatterInfo = {
|
|
706
|
+
name: "ormolu",
|
|
707
|
+
command: ["ormolu", "--mode", "inplace", "$FILE"],
|
|
708
|
+
extensions: [".hs", ".lhs"],
|
|
709
|
+
async detect(_cwd: string) {
|
|
710
|
+
return (await which("ormolu")) !== null;
|
|
711
|
+
},
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
export const taploFormatter: FormatterInfo = {
|
|
715
|
+
name: "taplo",
|
|
716
|
+
command: ["taplo", "fmt", "$FILE"],
|
|
717
|
+
extensions: [".toml"],
|
|
718
|
+
async resolveCommand(filePath, _cwd) {
|
|
719
|
+
const inPath = await which("taplo");
|
|
720
|
+
if (inPath) return [inPath, "fmt", filePath];
|
|
721
|
+
return resolveManagedSmartDefaultCommand("taplo", filePath, ["fmt"]);
|
|
722
|
+
},
|
|
723
|
+
async detect(_cwd: string) {
|
|
724
|
+
if ((await which("taplo")) !== null) return true;
|
|
725
|
+
const { getToolPath } = await import("./installer/index.js");
|
|
726
|
+
return Boolean(await getToolPath("taplo"));
|
|
727
|
+
},
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
export const googleJavaFormatFormatter: FormatterInfo = {
|
|
731
|
+
name: "google-java-format",
|
|
732
|
+
command: ["google-java-format", "--replace", "$FILE"],
|
|
733
|
+
extensions: [".java"],
|
|
734
|
+
async detect(cwd: string) {
|
|
735
|
+
if ((await which("google-java-format")) === null) return false;
|
|
736
|
+
return hasGoogleJavaFormatConfig(cwd);
|
|
737
|
+
},
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
export const cljfmtFormatter: FormatterInfo = {
|
|
741
|
+
name: "cljfmt",
|
|
742
|
+
command: ["cljfmt", "fix", "$FILE"],
|
|
743
|
+
extensions: [".clj", ".cljc", ".cljs"],
|
|
744
|
+
async detect(cwd: string) {
|
|
745
|
+
if ((await which("cljfmt")) === null) return false;
|
|
746
|
+
return hasCljfmtConfig(cwd);
|
|
747
|
+
},
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
export const cmakeFormatFormatter: FormatterInfo = {
|
|
751
|
+
name: "cmake-format",
|
|
752
|
+
command: ["cmake-format", "-i", "$FILE"],
|
|
753
|
+
extensions: [".cmake"],
|
|
754
|
+
async detect(cwd: string) {
|
|
755
|
+
if ((await which("cmake-format")) === null) return false;
|
|
756
|
+
return hasCmakeFormatConfig(cwd);
|
|
757
|
+
},
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
export const psscriptanalyzerFormatFormatter: FormatterInfo = {
|
|
761
|
+
name: "psscriptanalyzer-format",
|
|
762
|
+
command: [
|
|
763
|
+
"pwsh",
|
|
764
|
+
"-Command",
|
|
765
|
+
"Invoke-Formatter -ScriptDefinition (Get-Content -Raw '$FILE') | Set-Content '$FILE'",
|
|
766
|
+
],
|
|
767
|
+
extensions: [".ps1", ".psm1", ".psd1"],
|
|
768
|
+
async resolveCommand(filePath, _cwd) {
|
|
769
|
+
const pwsh = (await which("pwsh")) ?? (await which("powershell"));
|
|
770
|
+
if (!pwsh) return null;
|
|
771
|
+
return [
|
|
772
|
+
pwsh,
|
|
773
|
+
"-NoProfile",
|
|
774
|
+
"-Command",
|
|
775
|
+
`$content = Get-Content -Raw '${filePath}'; $formatted = Invoke-Formatter -ScriptDefinition $content; Set-Content -Path '${filePath}' -Value $formatted`,
|
|
776
|
+
];
|
|
777
|
+
},
|
|
778
|
+
async detect(_cwd: string) {
|
|
779
|
+
const pwsh = (await which("pwsh")) ?? (await which("powershell"));
|
|
780
|
+
if (!pwsh) return false;
|
|
781
|
+
// Check PSScriptAnalyzer module is available
|
|
782
|
+
const result = safeSpawn(
|
|
783
|
+
pwsh,
|
|
784
|
+
[
|
|
785
|
+
"-NoProfile",
|
|
786
|
+
"-Command",
|
|
787
|
+
"Get-Module -ListAvailable PSScriptAnalyzer | Select-Object -First 1 -ExpandProperty Name",
|
|
788
|
+
],
|
|
789
|
+
{ timeout: 5_000 },
|
|
790
|
+
);
|
|
791
|
+
return (result.stdout ?? "").includes("PSScriptAnalyzer");
|
|
792
|
+
},
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
// --- Registry ---
|
|
796
|
+
|
|
797
|
+
const ALL_FORMATTERS: FormatterInfo[] = [
|
|
798
|
+
biomeFormatter,
|
|
799
|
+
prettierFormatter,
|
|
800
|
+
oxfmtFormatter,
|
|
801
|
+
ruffFormatter,
|
|
802
|
+
blackFormatter,
|
|
803
|
+
sqlfluffFormatter,
|
|
804
|
+
gofmtFormatter,
|
|
805
|
+
rustfmtFormatter,
|
|
806
|
+
zigFormatter,
|
|
807
|
+
dartFormatter,
|
|
808
|
+
shfmtFormatter,
|
|
809
|
+
nixfmtFormatter,
|
|
810
|
+
mixFormatter,
|
|
811
|
+
ocamlformatFormatter,
|
|
812
|
+
clangFormatFormatter,
|
|
813
|
+
ktlintFormatter,
|
|
814
|
+
terraformFormatter,
|
|
815
|
+
phpCsFixerFormatter,
|
|
816
|
+
csharpierFormatter,
|
|
817
|
+
fantomasFormatter,
|
|
818
|
+
swiftformatFormatter,
|
|
819
|
+
styluaFormatter,
|
|
820
|
+
ormoluFormatter,
|
|
821
|
+
rubocopFormatter,
|
|
822
|
+
standardrbFormatter,
|
|
823
|
+
gleamFormatter,
|
|
824
|
+
taploFormatter,
|
|
825
|
+
googleJavaFormatFormatter,
|
|
826
|
+
cljfmtFormatter,
|
|
827
|
+
cmakeFormatFormatter,
|
|
828
|
+
psscriptanalyzerFormatFormatter,
|
|
829
|
+
];
|
|
830
|
+
|
|
831
|
+
// Cache for detection results - stores array of enabled formatter names per cwd+ext
|
|
832
|
+
const detectionCache = new Map<string, Map<string, string[]>>();
|
|
833
|
+
|
|
834
|
+
// --- Public API ---
|
|
835
|
+
|
|
836
|
+
export async function getFormattersForFile(
|
|
837
|
+
filePath: string,
|
|
838
|
+
cwd: string,
|
|
839
|
+
): Promise<FormatterInfo[]> {
|
|
840
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
841
|
+
const cacheKey = `${cwd}:${ext}`;
|
|
842
|
+
|
|
843
|
+
// Check cache
|
|
844
|
+
let cached = detectionCache.get(cwd);
|
|
845
|
+
if (!cached) {
|
|
846
|
+
cached = new Map();
|
|
847
|
+
detectionCache.set(cwd, cached);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (cached.has(cacheKey)) {
|
|
851
|
+
const enabledNames = cached.get(cacheKey);
|
|
852
|
+
if (!enabledNames || enabledNames.length === 0) return [];
|
|
853
|
+
// Return cached formatters by name (preserves priority order)
|
|
854
|
+
return ALL_FORMATTERS.filter((f) => enabledNames.includes(f.name));
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Detect formatters for this extension
|
|
858
|
+
const matching = ALL_FORMATTERS.filter((f) => f.extensions.includes(ext));
|
|
859
|
+
const formatterPolicy = getFormatterPolicyForFile(filePath);
|
|
860
|
+
const smartDefaultFormatterName = getSmartDefaultFormatterName(filePath);
|
|
861
|
+
|
|
862
|
+
const candidateFormatters = formatterPolicy?.formatterNames?.length
|
|
863
|
+
? matching.filter((f) => formatterPolicy.formatterNames.includes(f.name))
|
|
864
|
+
: matching;
|
|
865
|
+
|
|
866
|
+
let selected: FormatterInfo | undefined;
|
|
867
|
+
if (formatterPolicy) {
|
|
868
|
+
const explicitlyConfigured = candidateFormatters.filter((formatter) =>
|
|
869
|
+
hasExplicitFormatterConfig(formatter.name, cwd),
|
|
870
|
+
);
|
|
871
|
+
if (explicitlyConfigured.length > 0) {
|
|
872
|
+
// A formatter with explicit project config was found — use it.
|
|
873
|
+
// Prefer the policy's defaultFormatter only if it has explicit config,
|
|
874
|
+
// otherwise pick the first explicitly-configured formatter.
|
|
875
|
+
selected = formatterPolicy.defaultFormatter
|
|
876
|
+
? (explicitlyConfigured.find(
|
|
877
|
+
(f) => f.name === formatterPolicy.defaultFormatter,
|
|
878
|
+
) ?? explicitlyConfigured[0])
|
|
879
|
+
: explicitlyConfigured[0];
|
|
880
|
+
} else if (smartDefaultFormatterName) {
|
|
881
|
+
// Reached only when explicitlyConfigured is empty, so no candidate
|
|
882
|
+
// has explicit config. Safe to activate the smart-default.
|
|
883
|
+
const smartDefaultFormatter = candidateFormatters.find(
|
|
884
|
+
(f) => f.name === smartDefaultFormatterName,
|
|
885
|
+
);
|
|
886
|
+
if (smartDefaultFormatter) {
|
|
887
|
+
const autoInstallToolId = getAutoInstallToolIdForFormatter(
|
|
888
|
+
smartDefaultFormatter.name,
|
|
889
|
+
);
|
|
890
|
+
if (autoInstallToolId || (await smartDefaultFormatter.detect(cwd))) {
|
|
891
|
+
selected = smartDefaultFormatter;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
} else {
|
|
896
|
+
for (const formatter of candidateFormatters) {
|
|
897
|
+
try {
|
|
898
|
+
if (!(await hasExplicitFormatterConfig(formatter.name, cwd))) continue;
|
|
899
|
+
if (await formatter.detect(cwd)) {
|
|
900
|
+
selected = formatter;
|
|
901
|
+
break;
|
|
902
|
+
}
|
|
903
|
+
} catch (err) {
|
|
904
|
+
console.error(`[format] Detection failed for ${formatter.name}:`, err);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const enabled = selected ? [selected] : [];
|
|
910
|
+
|
|
911
|
+
let selectionReason: string;
|
|
912
|
+
if (!selected) {
|
|
913
|
+
selectionReason = "none";
|
|
914
|
+
} else if (!formatterPolicy) {
|
|
915
|
+
selectionReason = "detect";
|
|
916
|
+
} else {
|
|
917
|
+
selectionReason = candidateFormatters.some((f) =>
|
|
918
|
+
hasExplicitFormatterConfig(f.name, cwd),
|
|
919
|
+
)
|
|
920
|
+
? "explicit-config"
|
|
921
|
+
: "smart-default";
|
|
922
|
+
}
|
|
923
|
+
logLatency({
|
|
924
|
+
type: "phase",
|
|
925
|
+
phase: "formatter_selected",
|
|
926
|
+
filePath: filePath,
|
|
927
|
+
durationMs: 0,
|
|
928
|
+
metadata: {
|
|
929
|
+
formatter: selected?.name ?? null,
|
|
930
|
+
reason: selectionReason,
|
|
931
|
+
cwd,
|
|
932
|
+
},
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
// Store the list of enabled formatter names in cache
|
|
936
|
+
const enabledNames = enabled.map((f) => f.name);
|
|
937
|
+
cached.set(cacheKey, enabledNames);
|
|
938
|
+
return enabled;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
export function clearFormatterCache(): void {
|
|
942
|
+
detectionCache.clear();
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
export function clearFormatterRuntimeState(): void {
|
|
946
|
+
detectionCache.clear();
|
|
947
|
+
_lazyInstallAttempts.clear();
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
export async function formatFile(
|
|
951
|
+
filePath: string,
|
|
952
|
+
formatter: FormatterInfo,
|
|
953
|
+
): Promise<FormatterResult> {
|
|
954
|
+
try {
|
|
955
|
+
const absolutePath = path.resolve(filePath);
|
|
956
|
+
const cwd = path.dirname(absolutePath);
|
|
957
|
+
const contentBefore = await fs.readFile(absolutePath, "utf-8");
|
|
958
|
+
|
|
959
|
+
// Resolve command: prefer local (venv/vendor/node_modules) over global
|
|
960
|
+
const resolved = formatter.resolveCommand
|
|
961
|
+
? await formatter.resolveCommand(absolutePath, cwd)
|
|
962
|
+
: null;
|
|
963
|
+
const cmd =
|
|
964
|
+
resolved ??
|
|
965
|
+
formatter.command.map((c) => c.replace("$FILE", absolutePath));
|
|
966
|
+
|
|
967
|
+
// Run formatter without blocking the event loop.
|
|
968
|
+
const result = await safeSpawnAsync(cmd[0], cmd.slice(1), {
|
|
969
|
+
timeout: 15000,
|
|
970
|
+
cwd,
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
if (result.error) {
|
|
974
|
+
return {
|
|
975
|
+
success: false,
|
|
976
|
+
changed: false,
|
|
977
|
+
error: result.error.message,
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Check if content changed
|
|
982
|
+
const contentAfter = await fs.readFile(absolutePath, "utf-8");
|
|
983
|
+
const changed = contentBefore !== contentAfter;
|
|
984
|
+
|
|
985
|
+
return {
|
|
986
|
+
success: true,
|
|
987
|
+
changed,
|
|
988
|
+
};
|
|
989
|
+
} catch (err) {
|
|
990
|
+
return {
|
|
991
|
+
success: false,
|
|
992
|
+
changed: false,
|
|
993
|
+
error: err instanceof Error ? err.message : String(err),
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
export function listAllFormatters(): string[] {
|
|
999
|
+
return ALL_FORMATTERS.map((f) => f.name);
|
|
1000
|
+
}
|