devflow-kit 1.8.2 → 2.0.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/CHANGELOG.md +191 -6
- package/LICENSE +1 -1
- package/README.md +139 -219
- package/dist/cli/agents-view/index.js +9 -0
- package/dist/cli/agents-view/render.js +310 -0
- package/dist/cli/agents-view/state.js +423 -0
- package/dist/cli/agents-view/terminal.js +179 -0
- package/dist/cli/commands/agents.js +585 -0
- package/dist/cli/commands/ambient.js +231 -0
- package/dist/cli/commands/capture.js +133 -0
- package/dist/cli/commands/compliance-prompts.js +174 -0
- package/dist/cli/commands/compliance.js +337 -0
- package/dist/cli/commands/context.js +66 -0
- package/dist/cli/commands/debug.js +122 -0
- package/dist/cli/commands/flags.js +119 -0
- package/dist/cli/commands/hud.js +229 -0
- package/dist/cli/commands/init-seed.js +226 -0
- package/dist/cli/commands/init.js +1786 -0
- package/dist/cli/commands/knowledge/index.js +25 -0
- package/dist/cli/commands/knowledge/list.js +115 -0
- package/dist/cli/commands/knowledge/toggle.js +84 -0
- package/dist/cli/commands/learning.js +311 -0
- package/dist/cli/commands/legacy-hooks.js +40 -0
- package/dist/cli/commands/memory.js +349 -0
- package/dist/cli/commands/proxy.js +1435 -0
- package/dist/cli/commands/rules.js +334 -0
- package/dist/cli/commands/safe-delete.js +140 -0
- package/dist/cli/commands/security.js +179 -0
- package/dist/cli/commands/skills.js +140 -0
- package/dist/cli/commands/uninstall.js +1214 -0
- package/dist/cli.js +35 -10
- package/dist/commands/bug-analysis.md +400 -0
- package/dist/commands/code-review.md +403 -0
- package/dist/commands/debug.md +282 -0
- package/dist/commands/dynamic-build.md +972 -0
- package/dist/commands/dynamic-plan.md +383 -0
- package/dist/commands/dynamic-profile.md +206 -0
- package/dist/commands/dynamic-tickets.md +583 -0
- package/dist/commands/explore.md +208 -0
- package/dist/commands/implement.md +590 -0
- package/dist/commands/plan.md +570 -0
- package/dist/commands/release.md +206 -0
- package/dist/commands/research.md +212 -0
- package/dist/commands/resolve.md +731 -0
- package/dist/commands/self-review.md +251 -0
- package/dist/core/agent-frontmatter.js +209 -0
- package/dist/core/agent-models.js +556 -0
- package/dist/core/agent-state.js +44 -0
- package/dist/core/assets.js +39 -0
- package/dist/core/cache.js +205 -0
- package/dist/core/codex-auth-inspect.js +128 -0
- package/dist/core/compliance-compose.js +458 -0
- package/dist/core/compliance.js +165 -0
- package/dist/core/external-models.js +76 -0
- package/dist/core/feature-config.js +125 -0
- package/dist/core/flags.js +309 -0
- package/dist/core/fs-atomic.js +74 -0
- package/dist/core/git.js +61 -0
- package/dist/core/learning-queue-cleanup.js +92 -0
- package/dist/core/learning-tuning-config.js +78 -0
- package/dist/core/manifest.js +171 -0
- package/dist/core/migrations.js +330 -0
- package/dist/core/model-discovery.js +655 -0
- package/dist/core/observation-io.js +50 -0
- package/dist/core/observations.js +109 -0
- package/dist/core/orphan-sweep.js +92 -0
- package/dist/core/paths.js +48 -0
- package/dist/core/plugins.js +525 -0
- package/dist/core/project-paths.js +159 -0
- package/dist/core/proxy-log.js +165 -0
- package/dist/core/proxy-state.js +305 -0
- package/dist/core/safe-delete-install.js +191 -0
- package/dist/core/teammate-mode-cleanup.js +33 -0
- package/dist/hud/colors.js +13 -2
- package/dist/hud/components/config-counts.js +2 -2
- package/dist/hud/components/context-usage.js +2 -2
- package/dist/hud/components/learning-counts.js +92 -0
- package/dist/hud/components/session-cost.js +10 -2
- package/dist/hud/components/usage-quota.js +43 -8
- package/dist/hud/components/version-badge.js +17 -7
- package/dist/hud/config.js +4 -2
- package/dist/hud/cost-history.js +281 -0
- package/dist/hud/git.js +112 -71
- package/dist/hud/index.js +47 -4
- package/dist/hud/render.js +8 -20
- package/dist/hud/transcript.js +5 -6
- package/dist/targets/claude-code/claude-paths.js +106 -0
- package/dist/targets/claude-code/compliance-install.js +305 -0
- package/dist/targets/claude-code/hooks.js +9 -0
- package/dist/targets/claude-code/installer.js +458 -0
- package/dist/targets/claude-code/legacy.js +239 -0
- package/dist/targets/claude-code/post-install.js +971 -0
- package/package.json +23 -17
- package/src/assets/agents/code.md +244 -0
- package/src/assets/agents/design.md +113 -0
- package/src/assets/agents/diagnose.md +210 -0
- package/src/assets/agents/evaluate.md +117 -0
- package/src/assets/agents/git.md +938 -0
- package/src/assets/agents/knowledge.md +89 -0
- package/src/assets/agents/learning.md +203 -0
- package/src/assets/agents/research.md +134 -0
- package/src/assets/agents/review.md +220 -0
- package/src/assets/agents/scrutinize.md +95 -0
- package/src/assets/agents/simplify.md +108 -0
- package/src/assets/agents/skim.md +136 -0
- package/src/assets/agents/synthesize.md +402 -0
- package/src/assets/agents/test.md +155 -0
- package/src/assets/agents/triage.md +141 -0
- package/src/assets/agents/validate.md +103 -0
- package/src/assets/commands/_partials/_compliance.mds +5 -0
- package/src/assets/commands/_partials/_decisions.mds +20 -0
- package/src/assets/commands/_partials/_engine.mds +244 -0
- package/src/assets/commands/_partials/_factory.mds +216 -0
- package/src/assets/commands/_partials/_knowledge.mds +95 -0
- package/src/assets/commands/_partials/_plan_contract.mds +51 -0
- package/src/assets/commands/_partials/_preamble.mds +82 -0
- package/src/assets/commands/_partials/_publication.mds +7 -0
- package/src/assets/commands/_partials/_roster.mds +31 -0
- package/src/assets/commands/_partials/_ticket_template.mds +59 -0
- package/src/assets/commands/_partials/_wave.mds +123 -0
- package/src/assets/commands/bug-analysis.mds +353 -0
- package/src/assets/commands/code-review.mds +355 -0
- package/src/assets/commands/debug.mds +224 -0
- package/src/assets/commands/dynamic-build.mds +534 -0
- package/src/assets/commands/dynamic-plan.mds +244 -0
- package/src/assets/commands/dynamic-profile.mds +132 -0
- package/src/assets/commands/dynamic-tickets.mds +226 -0
- package/src/assets/commands/explore.mds +150 -0
- package/src/assets/commands/implement.mds +496 -0
- package/src/assets/commands/plan.mds +523 -0
- package/src/assets/commands/release.md +206 -0
- package/src/assets/commands/research.mds +164 -0
- package/src/assets/commands/resolve.mds +636 -0
- package/src/assets/commands/self-review.mds +156 -0
- package/src/assets/rules/accessibility.md +11 -0
- package/src/assets/rules/compliance.md +15 -0
- package/src/assets/rules/engineering.md +13 -0
- package/src/assets/rules/go.md +12 -0
- package/src/assets/rules/java.md +12 -0
- package/src/assets/rules/python.md +12 -0
- package/src/assets/rules/quality.md +14 -0
- package/src/assets/rules/react.md +11 -0
- package/src/assets/rules/reliability.md +12 -0
- package/src/assets/rules/rust.md +12 -0
- package/src/assets/rules/security.md +12 -0
- package/src/assets/rules/typescript.md +12 -0
- package/src/assets/rules/ui-design.md +11 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +21 -0
- package/src/assets/scripts/hooks/background-memory-update +460 -0
- package/src/assets/scripts/hooks/capture-prompt +87 -0
- package/src/assets/scripts/hooks/capture-question +153 -0
- package/src/assets/scripts/hooks/capture-turn +105 -0
- package/src/assets/scripts/hooks/debug-trace +75 -0
- package/src/assets/scripts/hooks/decisions-usage-scan.cjs +131 -0
- package/src/assets/scripts/hooks/ensure-devflow-init +39 -0
- package/src/assets/scripts/hooks/ensure-proxy +294 -0
- package/src/assets/scripts/hooks/ensure-root-gitignore +116 -0
- package/src/assets/scripts/hooks/get-mtime +23 -0
- package/src/assets/scripts/hooks/git-marker +16 -0
- package/src/assets/scripts/hooks/hook-bootstrap +25 -0
- package/src/assets/scripts/hooks/hook-log-init +41 -0
- package/src/assets/scripts/hooks/json-helper.cjs +725 -0
- package/src/assets/scripts/hooks/json-parse +243 -0
- package/src/assets/scripts/hooks/learning-lock +44 -0
- package/src/assets/scripts/hooks/lib/decisions-format.cjs +301 -0
- package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +106 -0
- package/src/assets/scripts/hooks/lib/project-paths.cjs +230 -0
- package/src/assets/scripts/hooks/lib/render-decisions.cjs +398 -0
- package/src/assets/scripts/hooks/lib/safe-path.cjs +29 -0
- package/src/assets/scripts/hooks/log-paths +24 -0
- package/src/assets/scripts/hooks/memory-worker +101 -0
- package/src/assets/scripts/hooks/pre-compact-memory +126 -0
- package/src/assets/scripts/hooks/preamble +77 -0
- package/src/assets/scripts/hooks/queue-append +120 -0
- package/src/assets/scripts/hooks/resolve-project-root +37 -0
- package/src/assets/scripts/hooks/run-hook +31 -0
- package/src/assets/scripts/hooks/session-start-context +180 -0
- package/src/assets/scripts/hooks/session-start-memory +318 -0
- package/src/assets/scripts/hooks/session-start-orchestrator +54 -0
- package/src/assets/scripts/hud.sh +5 -0
- package/src/assets/scripts/redact-secrets.cjs +410 -0
- package/src/assets/skills/accessibility/SKILL.md +145 -0
- package/src/assets/skills/accessibility/references/patterns.md +398 -0
- package/src/assets/skills/accessibility/references/sources.md +26 -0
- package/src/assets/skills/accessibility/references/violations.md +358 -0
- package/src/assets/skills/apply-decisions/SKILL.md +100 -0
- package/src/assets/skills/apply-feature-knowledge/SKILL.md +69 -0
- package/src/assets/skills/architecture/SKILL.md +138 -0
- package/src/assets/skills/architecture/references/patterns.md +378 -0
- package/src/assets/skills/architecture/references/sources.md +41 -0
- package/src/assets/skills/architecture/references/violations.md +314 -0
- package/src/assets/skills/boundary-validation/SKILL.md +119 -0
- package/src/assets/skills/boundary-validation/references/detection.md +283 -0
- package/src/assets/skills/boundary-validation/references/patterns.md +283 -0
- package/src/assets/skills/boundary-validation/references/sources.md +41 -0
- package/src/assets/skills/boundary-validation/references/violations.md +211 -0
- package/src/assets/skills/complexity/SKILL.md +150 -0
- package/src/assets/skills/compliance/SKILL.md +121 -0
- package/src/assets/skills/compliance/frameworks/gdpr/fragment.md +11 -0
- package/src/assets/skills/compliance/frameworks/gdpr/reference.md +50 -0
- package/src/assets/skills/compliance/frameworks/hipaa/fragment.md +11 -0
- package/src/assets/skills/compliance/frameworks/hipaa/reference.md +44 -0
- package/src/assets/skills/compliance/frameworks/iso-27001/fragment.md +10 -0
- package/src/assets/skills/compliance/frameworks/iso-27001/reference.md +45 -0
- package/src/assets/skills/compliance/frameworks/pci-dss/fragment.md +11 -0
- package/src/assets/skills/compliance/frameworks/pci-dss/reference.md +55 -0
- package/src/assets/skills/compliance/frameworks/soc2/fragment.md +10 -0
- package/src/assets/skills/compliance/frameworks/soc2/reference.md +53 -0
- package/src/assets/skills/compliance/frameworks/sox/fragment.md +10 -0
- package/src/assets/skills/compliance/frameworks/sox/reference.md +67 -0
- package/src/assets/skills/compliance/references/detection.md +108 -0
- package/src/assets/skills/compliance/references/sources.md +28 -0
- package/src/assets/skills/consistency/SKILL.md +140 -0
- package/src/assets/skills/database/SKILL.md +134 -0
- package/src/assets/skills/dependencies/SKILL.md +141 -0
- package/src/assets/skills/dependency-research/SKILL.md +133 -0
- package/src/assets/skills/dependency-research/references/evaluation-criteria.md +101 -0
- package/src/assets/skills/design-review/SKILL.md +155 -0
- package/src/assets/skills/design-review/references/anti-patterns.md +207 -0
- package/src/assets/skills/docs-framework/SKILL.md +190 -0
- package/src/assets/skills/docs-framework/references/patterns.md +381 -0
- package/src/assets/skills/docs-framework/references/violations.md +221 -0
- package/src/assets/skills/documentation/SKILL.md +126 -0
- package/src/assets/skills/feature-knowledge/SKILL.md +341 -0
- package/src/assets/skills/gap-analysis/SKILL.md +151 -0
- package/src/assets/skills/gap-analysis/references/check-layers.md +125 -0
- package/src/assets/skills/git/SKILL.md +287 -0
- package/src/assets/skills/git/references/github-api.md +666 -0
- package/src/assets/skills/git/references/patterns.md +324 -0
- package/src/assets/skills/git/references/sources.md +16 -0
- package/src/assets/skills/git/references/violations.md +128 -0
- package/src/assets/skills/go/SKILL.md +147 -0
- package/src/assets/skills/go/references/concurrency.md +335 -0
- package/src/assets/skills/go/references/patterns.md +278 -0
- package/src/assets/skills/go/references/sources.md +41 -0
- package/src/assets/skills/go/references/violations.md +262 -0
- package/src/assets/skills/java/SKILL.md +148 -0
- package/src/assets/skills/java/references/modern-java.md +335 -0
- package/src/assets/skills/java/references/patterns.md +311 -0
- package/src/assets/skills/java/references/sources.md +44 -0
- package/src/assets/skills/java/references/violations.md +257 -0
- package/src/assets/skills/patterns/SKILL.md +162 -0
- package/src/assets/skills/performance/SKILL.md +135 -0
- package/src/assets/skills/performance/references/patterns.md +516 -0
- package/src/assets/skills/performance/references/sources.md +41 -0
- package/src/assets/skills/performance/references/violations.md +417 -0
- package/src/assets/skills/python/SKILL.md +156 -0
- package/src/assets/skills/python/references/async.md +255 -0
- package/src/assets/skills/python/references/patterns.md +280 -0
- package/src/assets/skills/python/references/sources.md +41 -0
- package/src/assets/skills/python/references/violations.md +272 -0
- package/src/assets/skills/qa/SKILL.md +136 -0
- package/src/assets/skills/qa/references/browser-testing.md +97 -0
- package/src/assets/skills/qa/references/patterns.md +118 -0
- package/src/assets/skills/qa/references/scenario-templates.md +102 -0
- package/src/assets/skills/qa/references/sources.md +30 -0
- package/src/assets/skills/qa/references/violations.md +98 -0
- package/src/assets/skills/quality-gates/SKILL.md +152 -0
- package/src/assets/skills/quality-gates/references/stub-detection.md +135 -0
- package/src/assets/skills/react/SKILL.md +143 -0
- package/src/assets/skills/react/references/error-handling.md +179 -0
- package/src/assets/skills/react/references/forms.md +214 -0
- package/src/assets/skills/react/references/hooks.md +193 -0
- package/src/assets/skills/react/references/patterns.md +1336 -0
- package/src/assets/skills/react/references/sources.md +35 -0
- package/src/assets/skills/react/references/violations.md +570 -0
- package/src/assets/skills/regression/SKILL.md +146 -0
- package/src/assets/skills/reliability/SKILL.md +153 -0
- package/src/assets/skills/reliability/references/detection.md +66 -0
- package/src/assets/skills/reliability/references/patterns.md +151 -0
- package/src/assets/skills/reliability/references/sources.md +27 -0
- package/src/assets/skills/reliability/references/violations.md +125 -0
- package/src/assets/skills/research-codebase/SKILL.md +135 -0
- package/src/assets/skills/research-competitor/SKILL.md +145 -0
- package/src/assets/skills/research-external/SKILL.md +141 -0
- package/src/assets/skills/research-market/SKILL.md +145 -0
- package/src/assets/skills/research-technology/SKILL.md +180 -0
- package/src/assets/skills/review-methodology/SKILL.md +121 -0
- package/src/assets/skills/review-methodology/references/patterns.md +186 -0
- package/src/assets/skills/review-methodology/references/report-template.md +142 -0
- package/src/assets/skills/review-methodology/references/violations.md +125 -0
- package/src/assets/skills/rust/SKILL.md +148 -0
- package/src/assets/skills/rust/references/ownership.md +278 -0
- package/src/assets/skills/rust/references/patterns.md +247 -0
- package/src/assets/skills/rust/references/sources.md +46 -0
- package/src/assets/skills/rust/references/violations.md +252 -0
- package/src/assets/skills/security/SKILL.md +121 -0
- package/src/assets/skills/security/references/patterns.md +271 -0
- package/src/assets/skills/security/references/sources.md +41 -0
- package/src/assets/skills/security/references/violations.md +241 -0
- package/src/assets/skills/software-design/SKILL.md +143 -0
- package/src/assets/skills/software-design/references/checklist.md +259 -0
- package/src/assets/skills/software-design/references/code-smell-violations.md +189 -0
- package/src/assets/skills/software-design/references/patterns.md +359 -0
- package/src/assets/skills/software-design/references/sources.md +41 -0
- package/src/assets/skills/software-design/references/violations.md +341 -0
- package/src/assets/skills/test-driven-development/SKILL.md +197 -0
- package/src/assets/skills/testing/SKILL.md +139 -0
- package/src/assets/skills/testing/references/patterns.md +346 -0
- package/src/assets/skills/testing/references/sources.md +53 -0
- package/src/assets/skills/testing/references/violations.md +322 -0
- package/src/assets/skills/typescript/SKILL.md +150 -0
- package/src/assets/skills/typescript/references/async.md +333 -0
- package/src/assets/skills/typescript/references/patterns.md +440 -0
- package/src/assets/skills/typescript/references/sources.md +41 -0
- package/src/assets/skills/typescript/references/type-guards.md +333 -0
- package/src/assets/skills/typescript/references/utility-types.md +255 -0
- package/src/assets/skills/typescript/references/violations.md +285 -0
- package/src/assets/skills/ui-design/SKILL.md +144 -0
- package/src/assets/skills/ui-design/references/patterns.md +341 -0
- package/src/assets/skills/ui-design/references/sources.md +41 -0
- package/src/assets/skills/ui-design/references/violations.md +312 -0
- package/src/assets/skills/worktree-support/SKILL.md +92 -0
- package/src/targets/claude-code/templates/claudeignore.template +188 -0
- package/src/targets/claude-code/templates/settings.json +86 -0
- package/dist/cli.d.ts +0 -3
- package/dist/commands/ambient.d.ts +0 -18
- package/dist/commands/ambient.js +0 -137
- package/dist/commands/hud.d.ts +0 -22
- package/dist/commands/hud.js +0 -180
- package/dist/commands/init.d.ts +0 -16
- package/dist/commands/init.js +0 -738
- package/dist/commands/list.d.ts +0 -24
- package/dist/commands/list.js +0 -89
- package/dist/commands/memory.d.ts +0 -22
- package/dist/commands/memory.js +0 -175
- package/dist/commands/skills.d.ts +0 -11
- package/dist/commands/skills.js +0 -116
- package/dist/commands/uninstall.d.ts +0 -22
- package/dist/commands/uninstall.js +0 -558
- package/dist/hud/cache.d.ts +0 -14
- package/dist/hud/cache.js +0 -54
- package/dist/hud/colors.d.ts +0 -23
- package/dist/hud/components/config-counts.d.ts +0 -8
- package/dist/hud/components/context-usage.d.ts +0 -8
- package/dist/hud/components/diff-stats.d.ts +0 -3
- package/dist/hud/components/directory.d.ts +0 -3
- package/dist/hud/components/git-ahead-behind.d.ts +0 -3
- package/dist/hud/components/git-branch.d.ts +0 -3
- package/dist/hud/components/model.d.ts +0 -3
- package/dist/hud/components/release-info.d.ts +0 -3
- package/dist/hud/components/session-cost.d.ts +0 -3
- package/dist/hud/components/session-duration.d.ts +0 -3
- package/dist/hud/components/todo-progress.d.ts +0 -3
- package/dist/hud/components/usage-quota.d.ts +0 -3
- package/dist/hud/components/version-badge.d.ts +0 -3
- package/dist/hud/components/worktree-count.d.ts +0 -3
- package/dist/hud/config.d.ts +0 -10
- package/dist/hud/credentials.d.ts +0 -17
- package/dist/hud/credentials.js +0 -99
- package/dist/hud/git.d.ts +0 -7
- package/dist/hud/index.d.ts +0 -2
- package/dist/hud/render.d.ts +0 -7
- package/dist/hud/stdin.d.ts +0 -6
- package/dist/hud/transcript.d.ts +0 -7
- package/dist/hud/types.d.ts +0 -116
- package/dist/hud/usage-api.d.ts +0 -7
- package/dist/hud/usage-api.js +0 -84
- package/dist/plugins.d.ts +0 -46
- package/dist/plugins.js +0 -233
- package/dist/utils/cli.d.ts +0 -5
- package/dist/utils/cli.js +0 -14
- package/dist/utils/git.d.ts +0 -11
- package/dist/utils/git.js +0 -36
- package/dist/utils/installer.d.ts +0 -41
- package/dist/utils/installer.js +0 -195
- package/dist/utils/manifest.d.ts +0 -46
- package/dist/utils/manifest.js +0 -100
- package/dist/utils/paths.d.ts +0 -42
- package/dist/utils/paths.js +0 -106
- package/dist/utils/post-install.d.ts +0 -87
- package/dist/utils/post-install.js +0 -546
- package/dist/utils/safe-delete-install.d.ts +0 -29
- package/dist/utils/safe-delete-install.js +0 -191
- package/dist/utils/safe-delete.d.ts +0 -12
- package/plugins/devflow-accessibility/.claude-plugin/plugin.json +0 -21
- package/plugins/devflow-accessibility/skills/accessibility/SKILL.md +0 -229
- package/plugins/devflow-accessibility/skills/accessibility/references/patterns.md +0 -670
- package/plugins/devflow-accessibility/skills/accessibility/references/violations.md +0 -419
- package/plugins/devflow-ambient/.claude-plugin/plugin.json +0 -33
- package/plugins/devflow-ambient/README.md +0 -68
- package/plugins/devflow-ambient/agents/coder.md +0 -135
- package/plugins/devflow-ambient/agents/reviewer.md +0 -165
- package/plugins/devflow-ambient/agents/scrutinizer.md +0 -82
- package/plugins/devflow-ambient/agents/shepherd.md +0 -108
- package/plugins/devflow-ambient/agents/simplifier.md +0 -102
- package/plugins/devflow-ambient/agents/skimmer.md +0 -143
- package/plugins/devflow-ambient/agents/validator.md +0 -86
- package/plugins/devflow-ambient/skills/ambient-router/SKILL.md +0 -145
- package/plugins/devflow-ambient/skills/ambient-router/references/skill-catalog.md +0 -74
- package/plugins/devflow-ambient/skills/debug-orchestration/SKILL.md +0 -69
- package/plugins/devflow-ambient/skills/implementation-orchestration/SKILL.md +0 -92
- package/plugins/devflow-ambient/skills/plan-orchestration/SKILL.md +0 -71
- package/plugins/devflow-audit-claude/.claude-plugin/plugin.json +0 -19
- package/plugins/devflow-audit-claude/README.md +0 -46
- package/plugins/devflow-audit-claude/agents/claude-md-auditor.md +0 -134
- package/plugins/devflow-audit-claude/commands/audit-claude.md +0 -89
- package/plugins/devflow-code-review/.claude-plugin/plugin.json +0 -38
- package/plugins/devflow-code-review/README.md +0 -73
- package/plugins/devflow-code-review/agents/git.md +0 -277
- package/plugins/devflow-code-review/agents/reviewer.md +0 -165
- package/plugins/devflow-code-review/agents/synthesizer.md +0 -211
- package/plugins/devflow-code-review/commands/code-review-teams.md +0 -285
- package/plugins/devflow-code-review/commands/code-review.md +0 -163
- package/plugins/devflow-code-review/skills/agent-teams/SKILL.md +0 -124
- package/plugins/devflow-code-review/skills/agent-teams/references/cleanup.md +0 -104
- package/plugins/devflow-code-review/skills/agent-teams/references/communication.md +0 -122
- package/plugins/devflow-code-review/skills/agent-teams/references/team-patterns.md +0 -217
- package/plugins/devflow-code-review/skills/architecture-patterns/SKILL.md +0 -153
- package/plugins/devflow-code-review/skills/architecture-patterns/references/patterns.md +0 -873
- package/plugins/devflow-code-review/skills/architecture-patterns/references/violations.md +0 -575
- package/plugins/devflow-code-review/skills/complexity-patterns/SKILL.md +0 -143
- package/plugins/devflow-code-review/skills/consistency-patterns/SKILL.md +0 -140
- package/plugins/devflow-code-review/skills/database-patterns/SKILL.md +0 -134
- package/plugins/devflow-code-review/skills/dependencies-patterns/SKILL.md +0 -141
- package/plugins/devflow-code-review/skills/documentation-patterns/SKILL.md +0 -125
- package/plugins/devflow-code-review/skills/knowledge-persistence/SKILL.md +0 -128
- package/plugins/devflow-code-review/skills/knowledge-persistence/references/examples.md +0 -44
- package/plugins/devflow-code-review/skills/performance-patterns/SKILL.md +0 -154
- package/plugins/devflow-code-review/skills/performance-patterns/references/patterns.md +0 -503
- package/plugins/devflow-code-review/skills/performance-patterns/references/violations.md +0 -354
- package/plugins/devflow-code-review/skills/regression-patterns/SKILL.md +0 -146
- package/plugins/devflow-code-review/skills/review-methodology/SKILL.md +0 -119
- package/plugins/devflow-code-review/skills/review-methodology/references/patterns.md +0 -186
- package/plugins/devflow-code-review/skills/review-methodology/references/report-template.md +0 -142
- package/plugins/devflow-code-review/skills/review-methodology/references/violations.md +0 -125
- package/plugins/devflow-code-review/skills/security-patterns/SKILL.md +0 -156
- package/plugins/devflow-code-review/skills/security-patterns/references/patterns.md +0 -507
- package/plugins/devflow-code-review/skills/security-patterns/references/violations.md +0 -237
- package/plugins/devflow-code-review/skills/test-patterns/SKILL.md +0 -183
- package/plugins/devflow-code-review/skills/test-patterns/references/patterns.md +0 -220
- package/plugins/devflow-code-review/skills/test-patterns/references/violations.md +0 -221
- package/plugins/devflow-core-skills/.claude-plugin/plugin.json +0 -31
- package/plugins/devflow-core-skills/README.md +0 -50
- package/plugins/devflow-core-skills/skills/core-patterns/SKILL.md +0 -162
- package/plugins/devflow-core-skills/skills/core-patterns/references/checklist.md +0 -276
- package/plugins/devflow-core-skills/skills/core-patterns/references/code-smell-violations.md +0 -144
- package/plugins/devflow-core-skills/skills/core-patterns/references/patterns.md +0 -576
- package/plugins/devflow-core-skills/skills/core-patterns/references/violations.md +0 -369
- package/plugins/devflow-core-skills/skills/docs-framework/SKILL.md +0 -143
- package/plugins/devflow-core-skills/skills/docs-framework/references/patterns.md +0 -346
- package/plugins/devflow-core-skills/skills/docs-framework/references/violations.md +0 -221
- package/plugins/devflow-core-skills/skills/git-safety/SKILL.md +0 -122
- package/plugins/devflow-core-skills/skills/git-safety/references/patterns.md +0 -289
- package/plugins/devflow-core-skills/skills/git-safety/references/violations.md +0 -18
- package/plugins/devflow-core-skills/skills/git-workflow/SKILL.md +0 -158
- package/plugins/devflow-core-skills/skills/git-workflow/references/commit-patterns.md +0 -115
- package/plugins/devflow-core-skills/skills/git-workflow/references/commit-violations.md +0 -77
- package/plugins/devflow-core-skills/skills/git-workflow/references/pr-patterns.md +0 -127
- package/plugins/devflow-core-skills/skills/git-workflow/references/pr-violations.md +0 -96
- package/plugins/devflow-core-skills/skills/github-patterns/SKILL.md +0 -153
- package/plugins/devflow-core-skills/skills/github-patterns/references/patterns.md +0 -572
- package/plugins/devflow-core-skills/skills/github-patterns/references/violations.md +0 -298
- package/plugins/devflow-core-skills/skills/input-validation/SKILL.md +0 -148
- package/plugins/devflow-core-skills/skills/input-validation/references/detection.md +0 -283
- package/plugins/devflow-core-skills/skills/input-validation/references/patterns.md +0 -361
- package/plugins/devflow-core-skills/skills/input-validation/references/violations.md +0 -224
- package/plugins/devflow-core-skills/skills/search-first/SKILL.md +0 -133
- package/plugins/devflow-core-skills/skills/search-first/references/evaluation-criteria.md +0 -101
- package/plugins/devflow-core-skills/skills/test-driven-development/SKILL.md +0 -137
- package/plugins/devflow-core-skills/skills/test-patterns/SKILL.md +0 -183
- package/plugins/devflow-core-skills/skills/test-patterns/references/detection.md +0 -149
- package/plugins/devflow-core-skills/skills/test-patterns/references/patterns.md +0 -220
- package/plugins/devflow-core-skills/skills/test-patterns/references/report-template.md +0 -108
- package/plugins/devflow-core-skills/skills/test-patterns/references/violations.md +0 -221
- package/plugins/devflow-debug/.claude-plugin/plugin.json +0 -26
- package/plugins/devflow-debug/README.md +0 -65
- package/plugins/devflow-debug/agents/git.md +0 -277
- package/plugins/devflow-debug/agents/synthesizer.md +0 -211
- package/plugins/devflow-debug/commands/debug-teams.md +0 -245
- package/plugins/devflow-debug/commands/debug.md +0 -174
- package/plugins/devflow-debug/skills/agent-teams/SKILL.md +0 -124
- package/plugins/devflow-debug/skills/agent-teams/references/cleanup.md +0 -104
- package/plugins/devflow-debug/skills/agent-teams/references/communication.md +0 -122
- package/plugins/devflow-debug/skills/agent-teams/references/team-patterns.md +0 -217
- package/plugins/devflow-debug/skills/git-safety/SKILL.md +0 -122
- package/plugins/devflow-debug/skills/git-safety/references/detection.md +0 -290
- package/plugins/devflow-debug/skills/git-safety/references/patterns.md +0 -289
- package/plugins/devflow-debug/skills/git-safety/references/violations.md +0 -18
- package/plugins/devflow-debug/skills/knowledge-persistence/SKILL.md +0 -128
- package/plugins/devflow-debug/skills/knowledge-persistence/references/examples.md +0 -44
- package/plugins/devflow-frontend-design/.claude-plugin/plugin.json +0 -21
- package/plugins/devflow-frontend-design/skills/frontend-design/SKILL.md +0 -254
- package/plugins/devflow-frontend-design/skills/frontend-design/references/patterns.md +0 -511
- package/plugins/devflow-frontend-design/skills/frontend-design/references/violations.md +0 -453
- package/plugins/devflow-go/.claude-plugin/plugin.json +0 -21
- package/plugins/devflow-go/skills/go/SKILL.md +0 -187
- package/plugins/devflow-go/skills/go/references/concurrency.md +0 -312
- package/plugins/devflow-go/skills/go/references/patterns.md +0 -232
- package/plugins/devflow-go/skills/go/references/violations.md +0 -205
- package/plugins/devflow-implement/.claude-plugin/plugin.json +0 -35
- package/plugins/devflow-implement/README.md +0 -71
- package/plugins/devflow-implement/agents/coder.md +0 -135
- package/plugins/devflow-implement/agents/git.md +0 -277
- package/plugins/devflow-implement/agents/scrutinizer.md +0 -82
- package/plugins/devflow-implement/agents/shepherd.md +0 -108
- package/plugins/devflow-implement/agents/simplifier.md +0 -102
- package/plugins/devflow-implement/agents/skimmer.md +0 -143
- package/plugins/devflow-implement/agents/synthesizer.md +0 -211
- package/plugins/devflow-implement/agents/validator.md +0 -86
- package/plugins/devflow-implement/commands/implement-teams.md +0 -620
- package/plugins/devflow-implement/commands/implement.md +0 -430
- package/plugins/devflow-implement/skills/agent-teams/SKILL.md +0 -124
- package/plugins/devflow-implement/skills/agent-teams/references/cleanup.md +0 -104
- package/plugins/devflow-implement/skills/agent-teams/references/communication.md +0 -122
- package/plugins/devflow-implement/skills/agent-teams/references/team-patterns.md +0 -217
- package/plugins/devflow-implement/skills/implementation-patterns/SKILL.md +0 -162
- package/plugins/devflow-implement/skills/knowledge-persistence/SKILL.md +0 -128
- package/plugins/devflow-implement/skills/knowledge-persistence/references/examples.md +0 -44
- package/plugins/devflow-implement/skills/self-review/SKILL.md +0 -149
- package/plugins/devflow-implement/skills/self-review/references/stub-detection.md +0 -135
- package/plugins/devflow-java/.claude-plugin/plugin.json +0 -21
- package/plugins/devflow-java/skills/java/SKILL.md +0 -183
- package/plugins/devflow-java/skills/java/references/modern-java.md +0 -270
- package/plugins/devflow-java/skills/java/references/patterns.md +0 -235
- package/plugins/devflow-java/skills/java/references/violations.md +0 -213
- package/plugins/devflow-python/.claude-plugin/plugin.json +0 -21
- package/plugins/devflow-python/skills/python/SKILL.md +0 -188
- package/plugins/devflow-python/skills/python/references/async.md +0 -220
- package/plugins/devflow-python/skills/python/references/patterns.md +0 -226
- package/plugins/devflow-python/skills/python/references/violations.md +0 -204
- package/plugins/devflow-react/.claude-plugin/plugin.json +0 -21
- package/plugins/devflow-react/skills/react/SKILL.md +0 -276
- package/plugins/devflow-react/skills/react/references/patterns.md +0 -1331
- package/plugins/devflow-react/skills/react/references/violations.md +0 -565
- package/plugins/devflow-resolve/.claude-plugin/plugin.json +0 -29
- package/plugins/devflow-resolve/README.md +0 -65
- package/plugins/devflow-resolve/agents/git.md +0 -277
- package/plugins/devflow-resolve/agents/resolver.md +0 -131
- package/plugins/devflow-resolve/agents/simplifier.md +0 -102
- package/plugins/devflow-resolve/commands/resolve-teams.md +0 -307
- package/plugins/devflow-resolve/commands/resolve.md +0 -246
- package/plugins/devflow-resolve/skills/agent-teams/SKILL.md +0 -124
- package/plugins/devflow-resolve/skills/agent-teams/references/cleanup.md +0 -104
- package/plugins/devflow-resolve/skills/agent-teams/references/communication.md +0 -122
- package/plugins/devflow-resolve/skills/agent-teams/references/team-patterns.md +0 -217
- package/plugins/devflow-resolve/skills/implementation-patterns/SKILL.md +0 -162
- package/plugins/devflow-resolve/skills/implementation-patterns/references/patterns.md +0 -1063
- package/plugins/devflow-resolve/skills/implementation-patterns/references/violations.md +0 -483
- package/plugins/devflow-resolve/skills/knowledge-persistence/SKILL.md +0 -128
- package/plugins/devflow-resolve/skills/knowledge-persistence/references/examples.md +0 -44
- package/plugins/devflow-resolve/skills/security-patterns/SKILL.md +0 -156
- package/plugins/devflow-resolve/skills/security-patterns/references/detection.md +0 -287
- package/plugins/devflow-resolve/skills/security-patterns/references/patterns.md +0 -507
- package/plugins/devflow-resolve/skills/security-patterns/references/violations.md +0 -237
- package/plugins/devflow-rust/.claude-plugin/plugin.json +0 -21
- package/plugins/devflow-rust/skills/rust/SKILL.md +0 -193
- package/plugins/devflow-rust/skills/rust/references/ownership.md +0 -242
- package/plugins/devflow-rust/skills/rust/references/patterns.md +0 -210
- package/plugins/devflow-rust/skills/rust/references/violations.md +0 -191
- package/plugins/devflow-self-review/.claude-plugin/plugin.json +0 -26
- package/plugins/devflow-self-review/README.md +0 -38
- package/plugins/devflow-self-review/agents/scrutinizer.md +0 -82
- package/plugins/devflow-self-review/agents/simplifier.md +0 -102
- package/plugins/devflow-self-review/agents/validator.md +0 -86
- package/plugins/devflow-self-review/commands/self-review.md +0 -132
- package/plugins/devflow-self-review/skills/core-patterns/SKILL.md +0 -162
- package/plugins/devflow-self-review/skills/core-patterns/references/checklist.md +0 -276
- package/plugins/devflow-self-review/skills/core-patterns/references/code-smell-violations.md +0 -144
- package/plugins/devflow-self-review/skills/core-patterns/references/detection.md +0 -303
- package/plugins/devflow-self-review/skills/core-patterns/references/patterns.md +0 -576
- package/plugins/devflow-self-review/skills/core-patterns/references/violations.md +0 -369
- package/plugins/devflow-self-review/skills/self-review/SKILL.md +0 -149
- package/plugins/devflow-self-review/skills/self-review/references/patterns.md +0 -405
- package/plugins/devflow-self-review/skills/self-review/references/report-template.md +0 -253
- package/plugins/devflow-self-review/skills/self-review/references/stub-detection.md +0 -135
- package/plugins/devflow-self-review/skills/self-review/references/violations.md +0 -308
- package/plugins/devflow-specify/.claude-plugin/plugin.json +0 -25
- package/plugins/devflow-specify/README.md +0 -46
- package/plugins/devflow-specify/agents/skimmer.md +0 -143
- package/plugins/devflow-specify/agents/synthesizer.md +0 -211
- package/plugins/devflow-specify/commands/specify-teams.md +0 -321
- package/plugins/devflow-specify/commands/specify.md +0 -186
- package/plugins/devflow-specify/skills/agent-teams/SKILL.md +0 -124
- package/plugins/devflow-specify/skills/agent-teams/references/cleanup.md +0 -104
- package/plugins/devflow-specify/skills/agent-teams/references/communication.md +0 -122
- package/plugins/devflow-specify/skills/agent-teams/references/team-patterns.md +0 -217
- package/plugins/devflow-typescript/.claude-plugin/plugin.json +0 -21
- package/plugins/devflow-typescript/skills/typescript/SKILL.md +0 -176
- package/plugins/devflow-typescript/skills/typescript/references/patterns.md +0 -1105
- package/plugins/devflow-typescript/skills/typescript/references/violations.md +0 -433
- package/scripts/hooks/ambient-prompt +0 -50
- package/scripts/hooks/background-memory-update +0 -257
- package/scripts/hooks/ensure-memory-gitignore +0 -17
- package/scripts/hooks/pre-compact-memory +0 -87
- package/scripts/hooks/run-hook +0 -23
- package/scripts/hooks/session-start-memory +0 -165
- package/scripts/hooks/stop-update-memory +0 -85
- package/scripts/hud/cache.d.ts +0 -14
- package/scripts/hud/cache.d.ts.map +0 -1
- package/scripts/hud/cache.js +0 -54
- package/scripts/hud/cache.js.map +0 -1
- package/scripts/hud/colors.d.ts +0 -23
- package/scripts/hud/colors.d.ts.map +0 -1
- package/scripts/hud/colors.js +0 -62
- package/scripts/hud/colors.js.map +0 -1
- package/scripts/hud/components/config-counts.d.ts +0 -8
- package/scripts/hud/components/config-counts.d.ts.map +0 -1
- package/scripts/hud/components/config-counts.js +0 -91
- package/scripts/hud/components/config-counts.js.map +0 -1
- package/scripts/hud/components/context-usage.d.ts +0 -8
- package/scripts/hud/components/context-usage.d.ts.map +0 -1
- package/scripts/hud/components/context-usage.js +0 -49
- package/scripts/hud/components/context-usage.js.map +0 -1
- package/scripts/hud/components/diff-stats.d.ts +0 -3
- package/scripts/hud/components/diff-stats.d.ts.map +0 -1
- package/scripts/hud/components/diff-stats.js +0 -36
- package/scripts/hud/components/diff-stats.js.map +0 -1
- package/scripts/hud/components/directory.d.ts +0 -3
- package/scripts/hud/components/directory.d.ts.map +0 -1
- package/scripts/hud/components/directory.js +0 -10
- package/scripts/hud/components/directory.js.map +0 -1
- package/scripts/hud/components/git-ahead-behind.d.ts +0 -3
- package/scripts/hud/components/git-ahead-behind.d.ts.map +0 -1
- package/scripts/hud/components/git-ahead-behind.js +0 -16
- package/scripts/hud/components/git-ahead-behind.js.map +0 -1
- package/scripts/hud/components/git-branch.d.ts +0 -3
- package/scripts/hud/components/git-branch.d.ts.map +0 -1
- package/scripts/hud/components/git-branch.js +0 -14
- package/scripts/hud/components/git-branch.js.map +0 -1
- package/scripts/hud/components/model.d.ts +0 -3
- package/scripts/hud/components/model.d.ts.map +0 -1
- package/scripts/hud/components/model.js +0 -21
- package/scripts/hud/components/model.js.map +0 -1
- package/scripts/hud/components/release-info.d.ts +0 -3
- package/scripts/hud/components/release-info.d.ts.map +0 -1
- package/scripts/hud/components/release-info.js +0 -9
- package/scripts/hud/components/release-info.js.map +0 -1
- package/scripts/hud/components/session-cost.d.ts +0 -3
- package/scripts/hud/components/session-cost.d.ts.map +0 -1
- package/scripts/hud/components/session-cost.js +0 -9
- package/scripts/hud/components/session-cost.js.map +0 -1
- package/scripts/hud/components/session-duration.d.ts +0 -3
- package/scripts/hud/components/session-duration.d.ts.map +0 -1
- package/scripts/hud/components/session-duration.js +0 -18
- package/scripts/hud/components/session-duration.js.map +0 -1
- package/scripts/hud/components/todo-progress.d.ts +0 -3
- package/scripts/hud/components/todo-progress.d.ts.map +0 -1
- package/scripts/hud/components/todo-progress.js +0 -11
- package/scripts/hud/components/todo-progress.js.map +0 -1
- package/scripts/hud/components/usage-quota.d.ts +0 -3
- package/scripts/hud/components/usage-quota.d.ts.map +0 -1
- package/scripts/hud/components/usage-quota.js +0 -45
- package/scripts/hud/components/usage-quota.js.map +0 -1
- package/scripts/hud/components/version-badge.d.ts +0 -3
- package/scripts/hud/components/version-badge.d.ts.map +0 -1
- package/scripts/hud/components/version-badge.js +0 -80
- package/scripts/hud/components/version-badge.js.map +0 -1
- package/scripts/hud/components/worktree-count.d.ts +0 -3
- package/scripts/hud/components/worktree-count.d.ts.map +0 -1
- package/scripts/hud/components/worktree-count.js +0 -8
- package/scripts/hud/components/worktree-count.js.map +0 -1
- package/scripts/hud/config.d.ts +0 -10
- package/scripts/hud/config.d.ts.map +0 -1
- package/scripts/hud/config.js +0 -55
- package/scripts/hud/config.js.map +0 -1
- package/scripts/hud/credentials.d.ts +0 -17
- package/scripts/hud/credentials.d.ts.map +0 -1
- package/scripts/hud/credentials.js +0 -99
- package/scripts/hud/credentials.js.map +0 -1
- package/scripts/hud/git.d.ts +0 -7
- package/scripts/hud/git.d.ts.map +0 -1
- package/scripts/hud/git.js +0 -153
- package/scripts/hud/git.js.map +0 -1
- package/scripts/hud/index.d.ts +0 -2
- package/scripts/hud/index.d.ts.map +0 -1
- package/scripts/hud/index.js +0 -81
- package/scripts/hud/index.js.map +0 -1
- package/scripts/hud/render.d.ts +0 -7
- package/scripts/hud/render.d.ts.map +0 -1
- package/scripts/hud/render.js +0 -111
- package/scripts/hud/render.js.map +0 -1
- package/scripts/hud/stdin.d.ts +0 -6
- package/scripts/hud/stdin.d.ts.map +0 -1
- package/scripts/hud/stdin.js +0 -25
- package/scripts/hud/stdin.js.map +0 -1
- package/scripts/hud/transcript.d.ts +0 -7
- package/scripts/hud/transcript.d.ts.map +0 -1
- package/scripts/hud/transcript.js +0 -130
- package/scripts/hud/transcript.js.map +0 -1
- package/scripts/hud/types.d.ts +0 -116
- package/scripts/hud/types.d.ts.map +0 -1
- package/scripts/hud/types.js +0 -2
- package/scripts/hud/types.js.map +0 -1
- package/scripts/hud/usage-api.d.ts +0 -7
- package/scripts/hud/usage-api.d.ts.map +0 -1
- package/scripts/hud/usage-api.js +0 -84
- package/scripts/hud/usage-api.js.map +0 -1
- package/scripts/hud.sh +0 -5
- package/shared/agents/coder.md +0 -135
- package/shared/agents/git.md +0 -277
- package/shared/agents/resolver.md +0 -131
- package/shared/agents/reviewer.md +0 -165
- package/shared/agents/scrutinizer.md +0 -82
- package/shared/agents/shepherd.md +0 -108
- package/shared/agents/simplifier.md +0 -102
- package/shared/agents/skimmer.md +0 -143
- package/shared/agents/synthesizer.md +0 -211
- package/shared/agents/validator.md +0 -86
- package/shared/skills/accessibility/SKILL.md +0 -229
- package/shared/skills/accessibility/references/detection.md +0 -171
- package/shared/skills/accessibility/references/patterns.md +0 -670
- package/shared/skills/accessibility/references/violations.md +0 -419
- package/shared/skills/agent-teams/SKILL.md +0 -124
- package/shared/skills/agent-teams/references/cleanup.md +0 -104
- package/shared/skills/agent-teams/references/communication.md +0 -122
- package/shared/skills/agent-teams/references/team-patterns.md +0 -217
- package/shared/skills/ambient-router/SKILL.md +0 -145
- package/shared/skills/ambient-router/references/skill-catalog.md +0 -74
- package/shared/skills/architecture-patterns/SKILL.md +0 -153
- package/shared/skills/architecture-patterns/references/detection.md +0 -337
- package/shared/skills/architecture-patterns/references/patterns.md +0 -873
- package/shared/skills/architecture-patterns/references/violations.md +0 -575
- package/shared/skills/complexity-patterns/SKILL.md +0 -143
- package/shared/skills/complexity-patterns/references/detection.md +0 -264
- package/shared/skills/complexity-patterns/references/patterns.md +0 -487
- package/shared/skills/complexity-patterns/references/violations.md +0 -361
- package/shared/skills/consistency-patterns/SKILL.md +0 -140
- package/shared/skills/consistency-patterns/references/detection.md +0 -207
- package/shared/skills/consistency-patterns/references/patterns.md +0 -202
- package/shared/skills/consistency-patterns/references/violations.md +0 -213
- package/shared/skills/core-patterns/SKILL.md +0 -162
- package/shared/skills/core-patterns/references/checklist.md +0 -276
- package/shared/skills/core-patterns/references/code-smell-violations.md +0 -144
- package/shared/skills/core-patterns/references/detection.md +0 -303
- package/shared/skills/core-patterns/references/patterns.md +0 -576
- package/shared/skills/core-patterns/references/violations.md +0 -369
- package/shared/skills/database-patterns/SKILL.md +0 -134
- package/shared/skills/database-patterns/references/detection.md +0 -208
- package/shared/skills/database-patterns/references/patterns.md +0 -394
- package/shared/skills/database-patterns/references/violations.md +0 -332
- package/shared/skills/debug-orchestration/SKILL.md +0 -69
- package/shared/skills/dependencies-patterns/SKILL.md +0 -141
- package/shared/skills/dependencies-patterns/references/detection.md +0 -181
- package/shared/skills/dependencies-patterns/references/patterns.md +0 -225
- package/shared/skills/dependencies-patterns/references/violations.md +0 -247
- package/shared/skills/docs-framework/SKILL.md +0 -143
- package/shared/skills/docs-framework/references/patterns.md +0 -346
- package/shared/skills/docs-framework/references/violations.md +0 -221
- package/shared/skills/documentation-patterns/SKILL.md +0 -125
- package/shared/skills/documentation-patterns/references/detection.md +0 -190
- package/shared/skills/documentation-patterns/references/patterns.md +0 -189
- package/shared/skills/documentation-patterns/references/violations.md +0 -163
- package/shared/skills/frontend-design/SKILL.md +0 -254
- package/shared/skills/frontend-design/references/detection.md +0 -184
- package/shared/skills/frontend-design/references/patterns.md +0 -511
- package/shared/skills/frontend-design/references/violations.md +0 -453
- package/shared/skills/git-safety/SKILL.md +0 -122
- package/shared/skills/git-safety/references/detection.md +0 -290
- package/shared/skills/git-safety/references/patterns.md +0 -289
- package/shared/skills/git-safety/references/violations.md +0 -18
- package/shared/skills/git-workflow/SKILL.md +0 -158
- package/shared/skills/git-workflow/references/commit-patterns.md +0 -115
- package/shared/skills/git-workflow/references/commit-violations.md +0 -77
- package/shared/skills/git-workflow/references/pr-patterns.md +0 -127
- package/shared/skills/git-workflow/references/pr-violations.md +0 -96
- package/shared/skills/github-patterns/SKILL.md +0 -153
- package/shared/skills/github-patterns/references/patterns.md +0 -572
- package/shared/skills/github-patterns/references/violations.md +0 -298
- package/shared/skills/go/SKILL.md +0 -187
- package/shared/skills/go/references/concurrency.md +0 -312
- package/shared/skills/go/references/detection.md +0 -129
- package/shared/skills/go/references/patterns.md +0 -232
- package/shared/skills/go/references/violations.md +0 -205
- package/shared/skills/implementation-orchestration/SKILL.md +0 -92
- package/shared/skills/implementation-patterns/SKILL.md +0 -162
- package/shared/skills/implementation-patterns/references/patterns.md +0 -1063
- package/shared/skills/implementation-patterns/references/violations.md +0 -483
- package/shared/skills/input-validation/SKILL.md +0 -148
- package/shared/skills/input-validation/references/detection.md +0 -283
- package/shared/skills/input-validation/references/patterns.md +0 -361
- package/shared/skills/input-validation/references/violations.md +0 -224
- package/shared/skills/java/SKILL.md +0 -183
- package/shared/skills/java/references/detection.md +0 -120
- package/shared/skills/java/references/modern-java.md +0 -270
- package/shared/skills/java/references/patterns.md +0 -235
- package/shared/skills/java/references/violations.md +0 -213
- package/shared/skills/knowledge-persistence/SKILL.md +0 -128
- package/shared/skills/knowledge-persistence/references/examples.md +0 -44
- package/shared/skills/performance-patterns/SKILL.md +0 -154
- package/shared/skills/performance-patterns/references/detection.md +0 -351
- package/shared/skills/performance-patterns/references/patterns.md +0 -503
- package/shared/skills/performance-patterns/references/violations.md +0 -354
- package/shared/skills/plan-orchestration/SKILL.md +0 -71
- package/shared/skills/python/SKILL.md +0 -188
- package/shared/skills/python/references/async.md +0 -220
- package/shared/skills/python/references/detection.md +0 -128
- package/shared/skills/python/references/patterns.md +0 -226
- package/shared/skills/python/references/violations.md +0 -204
- package/shared/skills/react/SKILL.md +0 -276
- package/shared/skills/react/references/patterns.md +0 -1331
- package/shared/skills/react/references/violations.md +0 -565
- package/shared/skills/regression-patterns/SKILL.md +0 -146
- package/shared/skills/regression-patterns/references/detection.md +0 -237
- package/shared/skills/regression-patterns/references/patterns.md +0 -226
- package/shared/skills/regression-patterns/references/violations.md +0 -225
- package/shared/skills/review-methodology/SKILL.md +0 -119
- package/shared/skills/review-methodology/references/patterns.md +0 -186
- package/shared/skills/review-methodology/references/report-template.md +0 -142
- package/shared/skills/review-methodology/references/violations.md +0 -125
- package/shared/skills/rust/SKILL.md +0 -193
- package/shared/skills/rust/references/detection.md +0 -131
- package/shared/skills/rust/references/ownership.md +0 -242
- package/shared/skills/rust/references/patterns.md +0 -210
- package/shared/skills/rust/references/violations.md +0 -191
- package/shared/skills/search-first/SKILL.md +0 -133
- package/shared/skills/search-first/references/evaluation-criteria.md +0 -101
- package/shared/skills/security-patterns/SKILL.md +0 -156
- package/shared/skills/security-patterns/references/detection.md +0 -287
- package/shared/skills/security-patterns/references/patterns.md +0 -507
- package/shared/skills/security-patterns/references/violations.md +0 -237
- package/shared/skills/self-review/SKILL.md +0 -149
- package/shared/skills/self-review/references/patterns.md +0 -405
- package/shared/skills/self-review/references/report-template.md +0 -253
- package/shared/skills/self-review/references/stub-detection.md +0 -135
- package/shared/skills/self-review/references/violations.md +0 -308
- package/shared/skills/test-driven-development/SKILL.md +0 -137
- package/shared/skills/test-driven-development/references/rationalization-prevention.md +0 -111
- package/shared/skills/test-patterns/SKILL.md +0 -183
- package/shared/skills/test-patterns/references/detection.md +0 -149
- package/shared/skills/test-patterns/references/patterns.md +0 -220
- package/shared/skills/test-patterns/references/report-template.md +0 -108
- package/shared/skills/test-patterns/references/violations.md +0 -221
- package/shared/skills/typescript/SKILL.md +0 -176
- package/shared/skills/typescript/references/patterns.md +0 -1105
- package/shared/skills/typescript/references/violations.md +0 -433
- package/src/templates/claudeignore.template +0 -188
- package/src/templates/settings.json +0 -59
- /package/dist/{utils → core}/safe-delete.js +0 -0
- /package/{plugins/devflow-accessibility → src/assets}/skills/accessibility/references/detection.md +0 -0
- /package/{plugins/devflow-code-review/skills/architecture-patterns → src/assets/skills/architecture}/references/detection.md +0 -0
- /package/{plugins/devflow-code-review/skills/complexity-patterns → src/assets/skills/complexity}/references/detection.md +0 -0
- /package/{plugins/devflow-code-review/skills/complexity-patterns → src/assets/skills/complexity}/references/patterns.md +0 -0
- /package/{plugins/devflow-code-review/skills/complexity-patterns → src/assets/skills/complexity}/references/violations.md +0 -0
- /package/{plugins/devflow-code-review/skills/consistency-patterns → src/assets/skills/consistency}/references/detection.md +0 -0
- /package/{plugins/devflow-code-review/skills/consistency-patterns → src/assets/skills/consistency}/references/patterns.md +0 -0
- /package/{plugins/devflow-code-review/skills/consistency-patterns → src/assets/skills/consistency}/references/violations.md +0 -0
- /package/{plugins/devflow-code-review/skills/database-patterns → src/assets/skills/database}/references/detection.md +0 -0
- /package/{plugins/devflow-code-review/skills/database-patterns → src/assets/skills/database}/references/patterns.md +0 -0
- /package/{plugins/devflow-code-review/skills/database-patterns → src/assets/skills/database}/references/violations.md +0 -0
- /package/{plugins/devflow-code-review/skills/dependencies-patterns → src/assets/skills/dependencies}/references/detection.md +0 -0
- /package/{plugins/devflow-code-review/skills/dependencies-patterns → src/assets/skills/dependencies}/references/patterns.md +0 -0
- /package/{plugins/devflow-code-review/skills/dependencies-patterns → src/assets/skills/dependencies}/references/violations.md +0 -0
- /package/{plugins/devflow-code-review/skills/documentation-patterns → src/assets/skills/documentation}/references/detection.md +0 -0
- /package/{plugins/devflow-code-review/skills/documentation-patterns → src/assets/skills/documentation}/references/patterns.md +0 -0
- /package/{plugins/devflow-code-review/skills/documentation-patterns → src/assets/skills/documentation}/references/violations.md +0 -0
- /package/{plugins/devflow-core-skills/skills/git-safety → src/assets/skills/git}/references/detection.md +0 -0
- /package/{plugins/devflow-go → src/assets}/skills/go/references/detection.md +0 -0
- /package/{plugins/devflow-java → src/assets}/skills/java/references/detection.md +0 -0
- /package/{plugins/devflow-implement/skills/implementation-patterns → src/assets/skills/patterns}/references/patterns.md +0 -0
- /package/{plugins/devflow-implement/skills/implementation-patterns → src/assets/skills/patterns}/references/violations.md +0 -0
- /package/{plugins/devflow-code-review/skills/performance-patterns → src/assets/skills/performance}/references/detection.md +0 -0
- /package/{plugins/devflow-python → src/assets}/skills/python/references/detection.md +0 -0
- /package/{plugins/devflow-implement/skills/self-review → src/assets/skills/quality-gates}/references/patterns.md +0 -0
- /package/{plugins/devflow-implement/skills/self-review → src/assets/skills/quality-gates}/references/report-template.md +0 -0
- /package/{plugins/devflow-implement/skills/self-review → src/assets/skills/quality-gates}/references/violations.md +0 -0
- /package/{plugins/devflow-code-review/skills/regression-patterns → src/assets/skills/regression}/references/detection.md +0 -0
- /package/{plugins/devflow-code-review/skills/regression-patterns → src/assets/skills/regression}/references/patterns.md +0 -0
- /package/{plugins/devflow-code-review/skills/regression-patterns → src/assets/skills/regression}/references/violations.md +0 -0
- /package/{plugins/devflow-rust → src/assets}/skills/rust/references/detection.md +0 -0
- /package/{plugins/devflow-code-review/skills/security-patterns → src/assets/skills/security}/references/detection.md +0 -0
- /package/{plugins/devflow-core-skills/skills/core-patterns → src/assets/skills/software-design}/references/detection.md +0 -0
- /package/{plugins/devflow-core-skills → src/assets}/skills/test-driven-development/references/rationalization-prevention.md +0 -0
- /package/{plugins/devflow-code-review/skills/test-patterns → src/assets/skills/testing}/references/detection.md +0 -0
- /package/{plugins/devflow-code-review/skills/test-patterns → src/assets/skills/testing}/references/report-template.md +0 -0
- /package/{plugins/devflow-frontend-design/skills/frontend-design → src/assets/skills/ui-design}/references/detection.md +0 -0
- /package/src/{templates → targets/claude-code/templates}/managed-settings.json +0 -0
|
@@ -0,0 +1,1435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* devflow proxy — Enable, disable, and check status of external model routing
|
|
3
|
+
* (GPT models via your OpenAI/Codex subscription).
|
|
4
|
+
*
|
|
5
|
+
* applies ADR-013: CLI-layer module; all core logic lives in src/core/proxy-state.ts
|
|
6
|
+
* and src/core/agent-models.ts.
|
|
7
|
+
* avoids PF-014: never process.exit() inside a finally-guarded scope; use return
|
|
8
|
+
* from the async action handler for all early-exit paths.
|
|
9
|
+
* avoids PF-001: hook output strings use fixed templates; port number interpolation
|
|
10
|
+
* is acceptable (digit-validated integer, not user-controlled content).
|
|
11
|
+
*
|
|
12
|
+
* Branding note: "subswitch" must NEVER appear in user-visible strings. Internal
|
|
13
|
+
* identifiers (SUBSWITCH_CONFIG env var, health body checks) are fine.
|
|
14
|
+
*/
|
|
15
|
+
import { Command } from 'commander';
|
|
16
|
+
import { promises as fs, readFileSync } from 'fs';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import * as net from 'net';
|
|
20
|
+
import * as http from 'http';
|
|
21
|
+
import * as https from 'https';
|
|
22
|
+
import { spawn as cpSpawn } from 'child_process';
|
|
23
|
+
import * as p from '@clack/prompts';
|
|
24
|
+
import color from 'picocolors';
|
|
25
|
+
import { readProxyState, writeProxyState, buildProxyState, buildRoutingConfigJson, proxyBaseUrl, resolveProxyBin, DEFAULT_PROXY_PORT, } from '../../core/proxy-state.js';
|
|
26
|
+
import { syncManifestFeature, readManifest } from '../../core/manifest.js';
|
|
27
|
+
import { writeFileAtomicExclusive } from '../../core/fs-atomic.js';
|
|
28
|
+
import { scrubChildEnv, openProxyLog, rotateProxyLogIfLarge } from '../../core/proxy-log.js';
|
|
29
|
+
import { inspectCodexAuth, classifyCodexAuthReadError } from '../../core/codex-auth-inspect.js';
|
|
30
|
+
import { stripAnsi } from '../../hud/colors.js';
|
|
31
|
+
import { reapplyAgentMapping, revertExternalAgents, countExternalMappedAgents, readAgentMapping, } from '../../core/agent-models.js';
|
|
32
|
+
import { getExternalModelsCached, discoverExternalModels, } from '../../core/model-discovery.js';
|
|
33
|
+
import { modelCacheDir } from '../../core/cache.js';
|
|
34
|
+
import { getClaudeDirectory, getDevFlowDirectory, getHomeDirectory, } from '../../targets/claude-code/claude-paths.js';
|
|
35
|
+
function Ok(value) {
|
|
36
|
+
return { ok: true, value };
|
|
37
|
+
}
|
|
38
|
+
function Err(error) {
|
|
39
|
+
return { ok: false, error };
|
|
40
|
+
}
|
|
41
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
42
|
+
/** Marker used to identify ensure-proxy hook entries. */
|
|
43
|
+
const PROXY_HOOK_MARKER = 'ensure-proxy';
|
|
44
|
+
/** Pattern matching our relay's ANTHROPIC_BASE_URL value. */
|
|
45
|
+
const OUR_BASE_URL_PATTERN = /^http:\/\/127\.0\.0\.1:\d+$/;
|
|
46
|
+
/** Timeout for individual TCP connect probes and HTTP health checks (ms). */
|
|
47
|
+
const PROBE_TIMEOUT_MS = 2000;
|
|
48
|
+
/**
|
|
49
|
+
* Maximum length of the authMode field rendered by formatCodexAuthLine.
|
|
50
|
+
*
|
|
51
|
+
* Known values are 'chatgpt', 'apikey', 'unknown' (≤7 chars). A generous cap
|
|
52
|
+
* of 32 accommodates any future mode strings while bounding hostile input from
|
|
53
|
+
* the third-party ~/.codex/auth.json file. Matches the screenshot/bug-report
|
|
54
|
+
* hygiene rationale applied to accountSuffix (6-char cap).
|
|
55
|
+
*/
|
|
56
|
+
const AUTH_MODE_MAX_LEN = 32;
|
|
57
|
+
/**
|
|
58
|
+
* Maximum response body accepted by realHttpGet before aborting (64 KB).
|
|
59
|
+
* Prevents unbounded memory growth on slow-trickle or misconfigured endpoints
|
|
60
|
+
* (e.g. an SSE stream on the same port that --disable probes).
|
|
61
|
+
*/
|
|
62
|
+
const BODY_CAP_BYTES = 64 * 1024;
|
|
63
|
+
/** Timeout for the relay doctor subprocess (ms). */
|
|
64
|
+
const DOCTOR_TIMEOUT_MS = 10_000;
|
|
65
|
+
/**
|
|
66
|
+
* Maximum number of relay port probes during the CLI spawn wait (5s at 100ms each).
|
|
67
|
+
*
|
|
68
|
+
* The ensure-proxy hook uses a larger budget (80×0.1s = 8s) — this difference is
|
|
69
|
+
* intentional:
|
|
70
|
+
* - CLI (--enable): the user is waiting at an interactive terminal. 5s is the
|
|
71
|
+
* maximum comfortable wait; a cold relay start typically completes in <2s.
|
|
72
|
+
* - Hook (ensure-proxy): the hook fires inside a 15-second platform timeout. The
|
|
73
|
+
* relay may be starting from a cold OS state (first session after reboot) where
|
|
74
|
+
* 5s is too short. 8s gives a wider revival window while leaving 7s of headroom
|
|
75
|
+
* before the platform kills the hook.
|
|
76
|
+
*/
|
|
77
|
+
const RELAY_SPAWN_MAX_PROBES = 50;
|
|
78
|
+
/** Interval between relay port probes during the CLI spawn wait (ms). */
|
|
79
|
+
const RELAY_SPAWN_PROBE_INTERVAL_MS = 100;
|
|
80
|
+
/** TCP connect timeout for each relay port probe during the CLI spawn wait (ms). */
|
|
81
|
+
const RELAY_SPAWN_PER_PROBE_TIMEOUT_MS = 500;
|
|
82
|
+
// ─── Version helper ───────────────────────────────────────────────────────────
|
|
83
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
84
|
+
const __dirname = path.dirname(__filename);
|
|
85
|
+
let _cachedVersion;
|
|
86
|
+
function getDevflowVersion() {
|
|
87
|
+
if (_cachedVersion !== undefined)
|
|
88
|
+
return _cachedVersion;
|
|
89
|
+
try {
|
|
90
|
+
// dist/cli/commands/ → ../../.. → repo root
|
|
91
|
+
const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', '..', '..', 'package.json'), 'utf-8'));
|
|
92
|
+
_cachedVersion = typeof pkg.version === 'string' ? pkg.version : null;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
_cachedVersion = null;
|
|
96
|
+
}
|
|
97
|
+
return _cachedVersion;
|
|
98
|
+
}
|
|
99
|
+
// ─── Internal object helpers (used for single-pass atomic settings write) ────
|
|
100
|
+
/**
|
|
101
|
+
* Mutate a parsed Settings object in place: set ANTHROPIC_BASE_URL to our relay.
|
|
102
|
+
* Returns true when the object was changed (used to detect if a write is needed).
|
|
103
|
+
*/
|
|
104
|
+
function _applyProxyEnvToObject(settings, port) {
|
|
105
|
+
const s = settings;
|
|
106
|
+
s.env = s.env ?? {};
|
|
107
|
+
const env = s.env;
|
|
108
|
+
const newUrl = proxyBaseUrl(port);
|
|
109
|
+
if (env.ANTHROPIC_BASE_URL === newUrl)
|
|
110
|
+
return false;
|
|
111
|
+
env.ANTHROPIC_BASE_URL = newUrl;
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Mutate a parsed Settings object in place: remove ANTHROPIC_BASE_URL only when
|
|
116
|
+
* its value exactly matches our relay on the given managed port.
|
|
117
|
+
*
|
|
118
|
+
* Scoped to `managedPort` so a user's own localhost gateway (LiteLLM,
|
|
119
|
+
* local Ollama proxy, etc.) on ANY other port is never clobbered.
|
|
120
|
+
* The caller is responsible for passing the port Devflow currently owns:
|
|
121
|
+
* - disable path → proxy.json.port (or DEFAULT_PROXY_PORT)
|
|
122
|
+
* - init path → proxy.json.port after the preflight block
|
|
123
|
+
* - enable path → the new port being applied (followed immediately by _applyProxyEnvToObject)
|
|
124
|
+
* - uninstall → proxy.json.port (or DEFAULT_PROXY_PORT)
|
|
125
|
+
*
|
|
126
|
+
* Returns true when the object was changed.
|
|
127
|
+
*/
|
|
128
|
+
function _stripProxyEnvFromObject(settings, managedPort) {
|
|
129
|
+
const s = settings;
|
|
130
|
+
const env = s.env;
|
|
131
|
+
if (typeof env?.ANTHROPIC_BASE_URL !== 'string')
|
|
132
|
+
return false;
|
|
133
|
+
if (env.ANTHROPIC_BASE_URL !== proxyBaseUrl(managedPort))
|
|
134
|
+
return false;
|
|
135
|
+
delete env.ANTHROPIC_BASE_URL;
|
|
136
|
+
if (Object.keys(env).length === 0)
|
|
137
|
+
delete s.env;
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
/** Internal: add ensure-proxy hook to one event. Returns true when added. */
|
|
141
|
+
function _ensureProxyHook(settings, eventName, hookCmd) {
|
|
142
|
+
const existing = settings.hooks?.[eventName];
|
|
143
|
+
if (existing?.some((m) => m.hooks.some((h) => h.command.includes(PROXY_HOOK_MARKER)))) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
settings.hooks ??= {};
|
|
147
|
+
settings.hooks[eventName] ??= [];
|
|
148
|
+
const entry = {
|
|
149
|
+
hooks: [{ type: 'command', command: hookCmd, timeout: 15 }],
|
|
150
|
+
};
|
|
151
|
+
settings.hooks[eventName].push(entry);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
/** Internal: remove ensure-proxy hooks from one event. Returns true when removed. */
|
|
155
|
+
function _filterProxyHooks(settings, eventName) {
|
|
156
|
+
if (!settings.hooks?.[eventName])
|
|
157
|
+
return false;
|
|
158
|
+
const before = settings.hooks[eventName].length;
|
|
159
|
+
settings.hooks[eventName] = settings.hooks[eventName].filter((m) => !m.hooks.some((h) => h.command.includes(PROXY_HOOK_MARKER)));
|
|
160
|
+
if (settings.hooks[eventName].length === before)
|
|
161
|
+
return false;
|
|
162
|
+
if (settings.hooks[eventName].length === 0)
|
|
163
|
+
delete settings.hooks[eventName];
|
|
164
|
+
if (Object.keys(settings.hooks).length === 0)
|
|
165
|
+
delete settings.hooks;
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
// ─── Pure env functions (exported for testing and cross-module reuse) ─────────
|
|
169
|
+
/**
|
|
170
|
+
* Apply ANTHROPIC_BASE_URL=http://127.0.0.1:<port> to settings JSON.
|
|
171
|
+
* Returns new serialized settings string. Does not mutate input.
|
|
172
|
+
* Idempotent — calling twice with the same port produces the same result.
|
|
173
|
+
*/
|
|
174
|
+
export function applyProxyEnv(settingsJson, port) {
|
|
175
|
+
const settings = JSON.parse(settingsJson);
|
|
176
|
+
_applyProxyEnvToObject(settings, port);
|
|
177
|
+
return JSON.stringify(settings, null, 2) + '\n';
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Remove ANTHROPIC_BASE_URL from settings JSON, but ONLY when its value exactly
|
|
181
|
+
* matches our relay on `managedPort`.
|
|
182
|
+
*
|
|
183
|
+
* `managedPort` scopes the strip to the port Devflow owns — a user's own
|
|
184
|
+
* localhost gateway on any other port is never touched. Pass `proxy.json.port`
|
|
185
|
+
* (or `DEFAULT_PROXY_PORT` when the file is absent) at every call site.
|
|
186
|
+
*
|
|
187
|
+
* Returns new serialized settings string. Does not mutate input.
|
|
188
|
+
* Cleans up an emptied env object.
|
|
189
|
+
*/
|
|
190
|
+
export function stripProxyEnv(settingsJson, managedPort) {
|
|
191
|
+
const settings = JSON.parse(settingsJson);
|
|
192
|
+
_stripProxyEnvFromObject(settings, managedPort);
|
|
193
|
+
return JSON.stringify(settings, null, 2) + '\n';
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Read the proxy env state from a settings JSON string.
|
|
197
|
+
*
|
|
198
|
+
* Returns:
|
|
199
|
+
* 'ours' — ANTHROPIC_BASE_URL = our relay on the given port
|
|
200
|
+
* 'ours-other-port'— ANTHROPIC_BASE_URL = our relay on a different port
|
|
201
|
+
* 'foreign' — ANTHROPIC_BASE_URL set but not our relay
|
|
202
|
+
* 'absent' — ANTHROPIC_BASE_URL not set
|
|
203
|
+
*/
|
|
204
|
+
export function readProxyEnvState(settingsJson, port) {
|
|
205
|
+
const settings = JSON.parse(settingsJson);
|
|
206
|
+
const s = settings;
|
|
207
|
+
const env = s.env;
|
|
208
|
+
const url = env?.ANTHROPIC_BASE_URL;
|
|
209
|
+
if (typeof url !== 'string')
|
|
210
|
+
return 'absent';
|
|
211
|
+
if (url === proxyBaseUrl(port))
|
|
212
|
+
return 'ours';
|
|
213
|
+
if (OUR_BASE_URL_PATTERN.test(url))
|
|
214
|
+
return 'ours-other-port';
|
|
215
|
+
return 'foreign';
|
|
216
|
+
}
|
|
217
|
+
// ─── Pure hook helpers ──────────────────────────────────────────────────────────
|
|
218
|
+
/**
|
|
219
|
+
* Add ensure-proxy hooks to BOTH SessionStart and UserPromptSubmit events.
|
|
220
|
+
* Idempotent — skips events that already have the hook.
|
|
221
|
+
* Repairs partial state (one event present, other missing) by adding the missing one.
|
|
222
|
+
* Mutates settings in place. Returns true when any hook was added.
|
|
223
|
+
*/
|
|
224
|
+
export function addProxyHooks(settings, devflowDir) {
|
|
225
|
+
const hookCmd = path.join(devflowDir, 'scripts', 'hooks', 'run-hook') + ' ' + PROXY_HOOK_MARKER;
|
|
226
|
+
const addedSession = _ensureProxyHook(settings, 'SessionStart', hookCmd);
|
|
227
|
+
const addedPrompt = _ensureProxyHook(settings, 'UserPromptSubmit', hookCmd);
|
|
228
|
+
return addedSession || addedPrompt;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Remove ensure-proxy hooks from all events.
|
|
232
|
+
* Idempotent — no-op when hooks are not present.
|
|
233
|
+
* Preserves other hooks. Cleans empty arrays/objects.
|
|
234
|
+
* Mutates settings in place. Returns true when any hook was removed.
|
|
235
|
+
*/
|
|
236
|
+
export function removeProxyHooks(settings) {
|
|
237
|
+
const removedSession = _filterProxyHooks(settings, 'SessionStart');
|
|
238
|
+
const removedPrompt = _filterProxyHooks(settings, 'UserPromptSubmit');
|
|
239
|
+
return removedSession || removedPrompt;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Apply the disable settings-pass: remove proxy hooks AND strip ANTHROPIC_BASE_URL.
|
|
243
|
+
*
|
|
244
|
+
* Both operations always run unconditionally — never short-circuited with ||.
|
|
245
|
+
* A full enabled state (hooks present AND ANTHROPIC_BASE_URL set) requires
|
|
246
|
+
* both calls to be evaluated; short-circuiting left ANTHROPIC_BASE_URL in
|
|
247
|
+
* settings when hooks were present, keeping new sessions pointed at a disabled
|
|
248
|
+
* relay.
|
|
249
|
+
*
|
|
250
|
+
* `managedPort` scopes the URL strip to the port Devflow owns — pass
|
|
251
|
+
* `proxy.json.port` (or `DEFAULT_PROXY_PORT`) at the call site.
|
|
252
|
+
*
|
|
253
|
+
* Mutates settings in place. Returns true when any change was made.
|
|
254
|
+
*/
|
|
255
|
+
export function applyDisableToSettings(settings, managedPort) {
|
|
256
|
+
const removedHooks = removeProxyHooks(settings);
|
|
257
|
+
const strippedEnv = _stripProxyEnvFromObject(settings, managedPort);
|
|
258
|
+
return removedHooks || strippedEnv;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Check whether the ensure-proxy hook is registered on at least one event.
|
|
262
|
+
* Returns true if present on either SessionStart or UserPromptSubmit.
|
|
263
|
+
* Partial state (one event present, other missing) returns true —
|
|
264
|
+
* addProxyHooks will repair it on next enable.
|
|
265
|
+
*/
|
|
266
|
+
export function hasProxyHooks(input) {
|
|
267
|
+
const settings = typeof input === 'string' ? JSON.parse(input) : input;
|
|
268
|
+
const check = (eventName) => settings.hooks?.[eventName]?.some((m) => m.hooks.some((h) => h.command.includes(PROXY_HOOK_MARKER))) === true;
|
|
269
|
+
return check('SessionStart') || check('UserPromptSubmit');
|
|
270
|
+
}
|
|
271
|
+
// ─── Health-check identity helper ────────────────────────────────────────────
|
|
272
|
+
/**
|
|
273
|
+
* Return true when the raw health-check response body identifies our relay.
|
|
274
|
+
*
|
|
275
|
+
* The internal check `parsed['name'] === 'subswitch'` is correct here — 'subswitch'
|
|
276
|
+
* is the internal package name (fine in code, not in user-facing output — see branding
|
|
277
|
+
* note at the top of this file). Returns false on JSON parse failure, empty body, or a
|
|
278
|
+
* mismatched name field.
|
|
279
|
+
*
|
|
280
|
+
* Note: the ensure-proxy shell hook has its own inline copy of this check — it is a
|
|
281
|
+
* different language and cannot share the TypeScript module.
|
|
282
|
+
*/
|
|
283
|
+
export function isOurRelayBody(body) {
|
|
284
|
+
try {
|
|
285
|
+
const parsed = JSON.parse(body);
|
|
286
|
+
return parsed['name'] === 'subswitch';
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Run preflight checks before enabling the Devflow proxy.
|
|
294
|
+
*
|
|
295
|
+
* Checks (in order):
|
|
296
|
+
* ① Routing runtime bin resolvable.
|
|
297
|
+
* ② ~/.codex/auth.json exists.
|
|
298
|
+
* ③ Port probe: free → OK; accepting → health check → adopt or fail.
|
|
299
|
+
* ④ settings.json parseable; ANTHROPIC_BASE_URL not pointing elsewhere; API key warn.
|
|
300
|
+
*
|
|
301
|
+
* Doctor is deliberately excluded from preflight: the relay's doctor subcommand
|
|
302
|
+
* probes the relay port — a not-yet-started relay makes that probe fail (exit 1). A
|
|
303
|
+
* pre-spawn gate is therefore always unsatisfiable on a cold path. Doctor now runs in
|
|
304
|
+
* runPostSpawnVerification, after the relay is confirmed up. See D-EFR-2.
|
|
305
|
+
*
|
|
306
|
+
* Returns Ok(PreflightResult) on success, Err(message) on any check failure.
|
|
307
|
+
*/
|
|
308
|
+
export async function runProxyPreflight(port, codexAuthPath, configPath, logPath, deps) {
|
|
309
|
+
// ① Routing runtime bin
|
|
310
|
+
const binResult = await deps.resolveProxyBin();
|
|
311
|
+
if (!binResult.ok)
|
|
312
|
+
return Err(binResult.error);
|
|
313
|
+
const { binPath, npxWarning } = binResult.value;
|
|
314
|
+
// ② Codex auth
|
|
315
|
+
const codexAuthExists = await deps.fileExists(codexAuthPath);
|
|
316
|
+
if (!codexAuthExists) {
|
|
317
|
+
return Err('Sign in to the Codex CLI first (codex login)');
|
|
318
|
+
}
|
|
319
|
+
// ③ Port probe
|
|
320
|
+
const portAccepting = await deps.tcpConnectable(port, PROBE_TIMEOUT_MS);
|
|
321
|
+
if (portAccepting) {
|
|
322
|
+
// Port is up — check health identity
|
|
323
|
+
const healthResult = await deps.httpGet(`${proxyBaseUrl(port)}/__subswitch/health`, PROBE_TIMEOUT_MS);
|
|
324
|
+
if (healthResult.ok && isOurRelayBody(healthResult.value)) {
|
|
325
|
+
return Ok({ binPath, npxWarning, adopted: true });
|
|
326
|
+
}
|
|
327
|
+
// Port accepting but health timed out, failed, or not our relay
|
|
328
|
+
return Err(`port ${port} is in use by another application — pick a different port with \`devflow proxy --enable --port <n>\``);
|
|
329
|
+
}
|
|
330
|
+
// Port refused — free to proceed
|
|
331
|
+
// ④ Settings.json check
|
|
332
|
+
let settingsJson;
|
|
333
|
+
try {
|
|
334
|
+
settingsJson = await deps.readSettingsJson();
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return Err('Could not read settings.json — check file permissions');
|
|
338
|
+
}
|
|
339
|
+
let parsedSettings;
|
|
340
|
+
try {
|
|
341
|
+
parsedSettings = JSON.parse(settingsJson);
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
return Err('settings.json is malformed — fix it before enabling the proxy');
|
|
345
|
+
}
|
|
346
|
+
const envState = readProxyEnvState(settingsJson, port);
|
|
347
|
+
if (envState === 'foreign') {
|
|
348
|
+
return Err('An existing ANTHROPIC_BASE_URL in settings.json points to a different gateway — Devflow will not overwrite it');
|
|
349
|
+
}
|
|
350
|
+
// API key warning (non-fatal)
|
|
351
|
+
const envBlock = parsedSettings.env;
|
|
352
|
+
if (typeof envBlock === 'object' &&
|
|
353
|
+
envBlock !== null &&
|
|
354
|
+
!Array.isArray(envBlock) &&
|
|
355
|
+
typeof envBlock.ANTHROPIC_API_KEY === 'string') {
|
|
356
|
+
deps.onWarn?.('ANTHROPIC_API_KEY is set in settings.json — requests will use that key through the local relay');
|
|
357
|
+
}
|
|
358
|
+
return Ok({ binPath, npxWarning, adopted: false });
|
|
359
|
+
}
|
|
360
|
+
// ─── Production dependency implementations ────────────────────────────────────
|
|
361
|
+
/** Production TCP connect implementation. */
|
|
362
|
+
async function realTcpConnectable(port, timeoutMs) {
|
|
363
|
+
return new Promise((resolve) => {
|
|
364
|
+
const socket = net.createConnection({ host: '127.0.0.1', port, timeout: timeoutMs });
|
|
365
|
+
socket.on('connect', () => {
|
|
366
|
+
socket.destroy();
|
|
367
|
+
resolve(true);
|
|
368
|
+
});
|
|
369
|
+
socket.on('error', () => {
|
|
370
|
+
socket.destroy();
|
|
371
|
+
resolve(false);
|
|
372
|
+
});
|
|
373
|
+
socket.on('timeout', () => {
|
|
374
|
+
socket.destroy();
|
|
375
|
+
resolve(false);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Production HTTP/HTTPS GET implementation — selects module from URL scheme.
|
|
381
|
+
*
|
|
382
|
+
* Three resource bounds:
|
|
383
|
+
* • Wall-clock total deadline: fires when 'end' never arrives (SSE / trickle server).
|
|
384
|
+
* `--disable` probes precisely the port Devflow no longer owns, so any local
|
|
385
|
+
* service that trickles bytes could pin the CLI forever without this bound.
|
|
386
|
+
* • 64 KB body cap: aborts before accumulating unbounded memory.
|
|
387
|
+
* • res.on('error') listener: response-stream errors are caught, not ignored.
|
|
388
|
+
*
|
|
389
|
+
* Exported for reliability tests.
|
|
390
|
+
*/
|
|
391
|
+
export async function realHttpGet(url, timeoutMs) {
|
|
392
|
+
const mod = url.startsWith('https://') ? https : http;
|
|
393
|
+
return new Promise((resolve) => {
|
|
394
|
+
let settled = false;
|
|
395
|
+
const settle = (r) => {
|
|
396
|
+
if (settled)
|
|
397
|
+
return;
|
|
398
|
+
settled = true;
|
|
399
|
+
resolve(r);
|
|
400
|
+
};
|
|
401
|
+
// Wall-clock deadline — independent of the per-chunk socket idle timeout below.
|
|
402
|
+
const wallClock = setTimeout(() => {
|
|
403
|
+
req.destroy();
|
|
404
|
+
settle(Err('deadline exceeded'));
|
|
405
|
+
}, timeoutMs);
|
|
406
|
+
wallClock.unref();
|
|
407
|
+
const req = mod.get(url, { timeout: timeoutMs }, (res) => {
|
|
408
|
+
let bodyBytes = 0;
|
|
409
|
+
let body = '';
|
|
410
|
+
res.on('data', (chunk) => {
|
|
411
|
+
bodyBytes += chunk.length;
|
|
412
|
+
if (bodyBytes > BODY_CAP_BYTES) {
|
|
413
|
+
res.destroy();
|
|
414
|
+
req.destroy();
|
|
415
|
+
clearTimeout(wallClock);
|
|
416
|
+
settle(Err('response too large'));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
body += chunk.toString();
|
|
420
|
+
});
|
|
421
|
+
res.on('end', () => {
|
|
422
|
+
clearTimeout(wallClock);
|
|
423
|
+
settle(Ok(body));
|
|
424
|
+
});
|
|
425
|
+
res.on('error', (err) => {
|
|
426
|
+
clearTimeout(wallClock);
|
|
427
|
+
settle(Err(err.message));
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
req.on('error', (err) => {
|
|
431
|
+
clearTimeout(wallClock);
|
|
432
|
+
settle(Err(err.message));
|
|
433
|
+
});
|
|
434
|
+
req.on('timeout', () => {
|
|
435
|
+
req.destroy();
|
|
436
|
+
clearTimeout(wallClock);
|
|
437
|
+
settle(Err('timeout'));
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
/** Production doctor subprocess implementation. */
|
|
442
|
+
async function realSpawnDoctor(binPath, env, timeoutMs, logFile) {
|
|
443
|
+
// openProxyLog: 0700 parent dir + 0600 file creation + best-effort chmod for
|
|
444
|
+
// pre-existing wider modes (SEC-2). Non-fatal on chmod failure per PF-009.
|
|
445
|
+
const logFd = await openProxyLog(logFile);
|
|
446
|
+
try {
|
|
447
|
+
return await new Promise((resolve) => {
|
|
448
|
+
const proc = cpSpawn(process.execPath, [binPath, 'doctor'], {
|
|
449
|
+
env,
|
|
450
|
+
stdio: ['ignore', logFd.fd, logFd.fd],
|
|
451
|
+
});
|
|
452
|
+
let resolved = false;
|
|
453
|
+
const timer = setTimeout(() => {
|
|
454
|
+
if (!resolved) {
|
|
455
|
+
resolved = true;
|
|
456
|
+
proc.kill(); // SIGTERM — ask the process to terminate gracefully
|
|
457
|
+
// A SIGTERM-trapping child keeps the event loop alive (no unref on
|
|
458
|
+
// proc here, since we are awaiting the promise). Schedule a SIGKILL escalation
|
|
459
|
+
// after a short grace period. The escalation timer is unref()'d so it never
|
|
460
|
+
// prevents the CLI from exiting on its own if the process exits first.
|
|
461
|
+
const sigkillTimer = setTimeout(() => {
|
|
462
|
+
try {
|
|
463
|
+
proc.kill('SIGKILL');
|
|
464
|
+
}
|
|
465
|
+
catch { /* already dead — ignore */ }
|
|
466
|
+
}, 2000);
|
|
467
|
+
sigkillTimer.unref();
|
|
468
|
+
resolve(1);
|
|
469
|
+
}
|
|
470
|
+
}, timeoutMs);
|
|
471
|
+
proc.on('close', (code) => {
|
|
472
|
+
if (!resolved) {
|
|
473
|
+
resolved = true;
|
|
474
|
+
clearTimeout(timer);
|
|
475
|
+
resolve(code ?? 1);
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
// OS-level spawn failure (EMFILE, ENOMEM, EAGAIN) must be handled — an
|
|
479
|
+
// unhandled 'error' event becomes an uncaught exception. Resolve(1) so the
|
|
480
|
+
// finally block closes logFd and callers get a clean failure path.
|
|
481
|
+
proc.on('error', () => {
|
|
482
|
+
if (!resolved) {
|
|
483
|
+
resolved = true;
|
|
484
|
+
clearTimeout(timer);
|
|
485
|
+
resolve(1);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
finally {
|
|
491
|
+
await logFd.close();
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Build the real (production) ProxyPreflightDeps from the given options.
|
|
496
|
+
* Centralises the three private implementations so both runEnable and init.ts
|
|
497
|
+
* can consume them without byte-identical inline copies.
|
|
498
|
+
*
|
|
499
|
+
* applies ADR-013: pure configuration factory; I/O implementations factored once.
|
|
500
|
+
*/
|
|
501
|
+
export function buildRealPreflightDeps(opts) {
|
|
502
|
+
const { settingsPath, onWarn, swallowSettingsReadError = false } = opts;
|
|
503
|
+
return {
|
|
504
|
+
resolveProxyBin,
|
|
505
|
+
fileExists: async (filePath) => {
|
|
506
|
+
try {
|
|
507
|
+
await fs.access(filePath);
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
tcpConnectable: realTcpConnectable,
|
|
515
|
+
httpGet: realHttpGet,
|
|
516
|
+
readSettingsJson: swallowSettingsReadError
|
|
517
|
+
? async () => { try {
|
|
518
|
+
return await fs.readFile(settingsPath, 'utf-8');
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
return '{}';
|
|
522
|
+
} }
|
|
523
|
+
: () => fs.readFile(settingsPath, 'utf-8'),
|
|
524
|
+
spawnDoctor: realSpawnDoctor,
|
|
525
|
+
onWarn,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Spawn the relay (unless adopted) and wait up to 50×100ms for TCP accept.
|
|
530
|
+
*
|
|
531
|
+
* When `adopted` is true, the relay is already running — skip spawn entirely.
|
|
532
|
+
*
|
|
533
|
+
* Returns `{ ok: true }` when the port accepts connections.
|
|
534
|
+
* Returns `{ ok: false }` when:
|
|
535
|
+
* - relay never accepted after 50 probes (caller should rollback proxy.json)
|
|
536
|
+
* - relay process died before the port came up
|
|
537
|
+
* - OS-level spawn error (EMFILE, ENOMEM, EAGAIN) — always handled via the
|
|
538
|
+
* injected onError callback, never an uncaught exception
|
|
539
|
+
*
|
|
540
|
+
* avoids PF-014: no process.exit() — returns Result; caller decides error handling.
|
|
541
|
+
*/
|
|
542
|
+
export async function spawnRelayAndWaitForPort(port, binPath, configPath, logPath, pidPath, adopted, deps) {
|
|
543
|
+
if (adopted) {
|
|
544
|
+
// Port already hosting our relay — skip spawn entirely, proceed to settings pass.
|
|
545
|
+
return { ok: true };
|
|
546
|
+
}
|
|
547
|
+
const logHandle = await deps.openLog(logPath);
|
|
548
|
+
let spawnError;
|
|
549
|
+
// SEC-2: allowlist env for the relay — only PATH/HOME/TMPDIR/LANG/LC_ALL are
|
|
550
|
+
// inherited; SUBSWITCH_CONFIG is the only process-specific addition here.
|
|
551
|
+
const env = { ...scrubChildEnv(), SUBSWITCH_CONFIG: configPath };
|
|
552
|
+
const { pid } = deps.spawnProcess({
|
|
553
|
+
execPath: process.execPath,
|
|
554
|
+
args: [binPath, 'serve'],
|
|
555
|
+
env,
|
|
556
|
+
stdioFd: logHandle.fd,
|
|
557
|
+
// Captured here; breaks the wait loop on the next iteration
|
|
558
|
+
onError: (err) => { spawnError = err; },
|
|
559
|
+
});
|
|
560
|
+
// Parent closes its copy; the spawned child retains the fd through the OS
|
|
561
|
+
await logHandle.close();
|
|
562
|
+
if (pid !== undefined) {
|
|
563
|
+
// Non-fatal: best-effort pid record for devflow proxy --status
|
|
564
|
+
await deps.writePid(pidPath, pid);
|
|
565
|
+
}
|
|
566
|
+
// Bounded wait: ≤RELAY_SPAWN_MAX_PROBES×100ms (5s max) for TCP accept
|
|
567
|
+
let portUp = false;
|
|
568
|
+
for (let i = 0; i < RELAY_SPAWN_MAX_PROBES; i++) {
|
|
569
|
+
await deps.sleep(RELAY_SPAWN_PROBE_INTERVAL_MS);
|
|
570
|
+
if (spawnError !== undefined) {
|
|
571
|
+
// OS-level error fired — no point waiting; relay will never start
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
if (pid !== undefined && !deps.isProcessAlive(pid)) {
|
|
575
|
+
// Process died before port came up — check for EADDRINUSE race
|
|
576
|
+
// (another session may have started and already owns the port)
|
|
577
|
+
if (await deps.tcpConnectable(port, RELAY_SPAWN_PER_PROBE_TIMEOUT_MS)) {
|
|
578
|
+
portUp = true;
|
|
579
|
+
}
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
if (await deps.tcpConnectable(port, RELAY_SPAWN_PER_PROBE_TIMEOUT_MS)) {
|
|
583
|
+
portUp = true;
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (!portUp) {
|
|
588
|
+
return { ok: false, reason: 'relay-not-started' };
|
|
589
|
+
}
|
|
590
|
+
return { ok: true, spawnedPid: pid };
|
|
591
|
+
}
|
|
592
|
+
/** Build the real (production) SpawnAndWaitDeps. Not exported — internal to runEnable. */
|
|
593
|
+
function buildRealSpawnAndWaitDeps() {
|
|
594
|
+
return {
|
|
595
|
+
openLog: async (logPath) => {
|
|
596
|
+
// openProxyLog: 0700 parent dir + 0600 file creation + best-effort chmod (SEC-2).
|
|
597
|
+
const handle = await openProxyLog(logPath);
|
|
598
|
+
return { fd: handle.fd, close: () => handle.close() };
|
|
599
|
+
},
|
|
600
|
+
spawnProcess: ({ execPath, args, env, stdioFd, onError }) => {
|
|
601
|
+
const proc = cpSpawn(execPath, args, {
|
|
602
|
+
detached: true,
|
|
603
|
+
stdio: ['ignore', stdioFd, stdioFd],
|
|
604
|
+
env,
|
|
605
|
+
});
|
|
606
|
+
// Attach error handler before unref so OS-level failures are caught
|
|
607
|
+
proc.on('error', onError);
|
|
608
|
+
proc.unref();
|
|
609
|
+
return { pid: proc.pid };
|
|
610
|
+
},
|
|
611
|
+
writePid: async (pidPath, pid) => {
|
|
612
|
+
// Non-fatal: failure is inconvenient (--status loses pid) but not blocking
|
|
613
|
+
try {
|
|
614
|
+
await fs.writeFile(pidPath, String(pid), 'utf-8');
|
|
615
|
+
}
|
|
616
|
+
catch { /* non-fatal */ }
|
|
617
|
+
},
|
|
618
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
619
|
+
isProcessAlive: (pid) => {
|
|
620
|
+
try {
|
|
621
|
+
process.kill(pid, 0);
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
tcpConnectable: realTcpConnectable,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Run `node <binPath> doctor` against the live relay and handle failure.
|
|
633
|
+
*
|
|
634
|
+
* @D-EFR-2 Doctor gates post-spawn, not pre-spawn: the relay's doctor subcommand
|
|
635
|
+
* probes the relay port to confirm it is running — a not-yet-started relay makes
|
|
636
|
+
* that probe fail (exit 1), so a pre-spawn gate is always unsatisfiable on a cold
|
|
637
|
+
* path (the port is down by definition before spawn). Moving the gate post-spawn
|
|
638
|
+
* means doctor runs against an already-live relay, turning "running: NO" into
|
|
639
|
+
* "running: YES" on the healthy path, validating codex auth and TLS reachability.
|
|
640
|
+
*
|
|
641
|
+
* Kill-on-rollback is restricted to self-spawned relays (spawnedPid defined): an
|
|
642
|
+
* adopted relay may be serving other live Claude Code sessions, so we must never
|
|
643
|
+
* kill it. A just-spawned relay predates the settings pass, so no session is
|
|
644
|
+
* routing through it — SIGTERM is safe (with a SIGKILL escalation in the caller).
|
|
645
|
+
*
|
|
646
|
+
* @param spawnedPid Pid of the relay we spawned; undefined when the relay was adopted.
|
|
647
|
+
* @returns Ok(undefined) when doctor exits 0; Err(message) on failure. On failure the
|
|
648
|
+
* rollback (writeDisabledProxyState) and conditional kill are applied before
|
|
649
|
+
* returning — the caller does not need to do additional rollback.
|
|
650
|
+
*/
|
|
651
|
+
export async function runPostSpawnVerification(binPath, configPath, logPath, spawnedPid, deps) {
|
|
652
|
+
// SEC-2: allowlist env for doctor — matches the relay spawn composition.
|
|
653
|
+
const doctorEnv = { ...scrubChildEnv(), SUBSWITCH_CONFIG: configPath };
|
|
654
|
+
const doctorExit = await deps.spawnDoctor(binPath, doctorEnv, DOCTOR_TIMEOUT_MS, logPath);
|
|
655
|
+
if (doctorExit === 0)
|
|
656
|
+
return Ok(undefined);
|
|
657
|
+
// Doctor failed — rollback proxy state and conditionally kill the relay.
|
|
658
|
+
// Both are best-effort: write failure is secondary; kill failure means process already exited.
|
|
659
|
+
await deps.writeDisabledProxyState();
|
|
660
|
+
if (spawnedPid !== undefined) {
|
|
661
|
+
deps.killProcess(spawnedPid);
|
|
662
|
+
}
|
|
663
|
+
return Err(`Routing verification failed — see ${logPath}`);
|
|
664
|
+
}
|
|
665
|
+
// ─── Atomic settings mutation for enable ─────────────────────────────────────
|
|
666
|
+
/**
|
|
667
|
+
* Perform the single atomic settings.json pass for enable:
|
|
668
|
+
* strip old hooks + env, then apply new hooks + env in one write.
|
|
669
|
+
*
|
|
670
|
+
* The writeFileAtomicExclusive call is guarded — ENOSPC/EACCES returns Err
|
|
671
|
+
* instead of crashing with an unhandled rejection.
|
|
672
|
+
*
|
|
673
|
+
* Returns Ok(undefined) on success, Err(reason) on hard failure.
|
|
674
|
+
*/
|
|
675
|
+
async function applyEnableSettingsPass(settingsPath, devflowDir, port) {
|
|
676
|
+
let settingsContent;
|
|
677
|
+
try {
|
|
678
|
+
settingsContent = await fs.readFile(settingsPath, 'utf-8');
|
|
679
|
+
}
|
|
680
|
+
catch {
|
|
681
|
+
// Missing settings.json is fine — start from an empty object
|
|
682
|
+
settingsContent = '{}';
|
|
683
|
+
}
|
|
684
|
+
let parsedSettings;
|
|
685
|
+
try {
|
|
686
|
+
parsedSettings = JSON.parse(settingsContent);
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
return Err('settings.json is malformed — fix it before enabling the proxy');
|
|
690
|
+
}
|
|
691
|
+
// Atomic 4-call settings mutation: strip stale entries, then apply fresh ones.
|
|
692
|
+
// Strip is scoped to `port` (the new port being applied). The apply
|
|
693
|
+
// call below always overwrites ANTHROPIC_BASE_URL regardless, so the strip
|
|
694
|
+
// here primarily removes any exact-port match before the write-set cycle.
|
|
695
|
+
removeProxyHooks(parsedSettings);
|
|
696
|
+
_stripProxyEnvFromObject(parsedSettings, port);
|
|
697
|
+
addProxyHooks(parsedSettings, devflowDir);
|
|
698
|
+
_applyProxyEnvToObject(parsedSettings, port);
|
|
699
|
+
try {
|
|
700
|
+
await writeFileAtomicExclusive(settingsPath, JSON.stringify(parsedSettings, null, 2) + '\n');
|
|
701
|
+
}
|
|
702
|
+
catch (err) {
|
|
703
|
+
return Err(`Could not write settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
704
|
+
}
|
|
705
|
+
return Ok(undefined);
|
|
706
|
+
}
|
|
707
|
+
// ─── PID-to-port identity binding ─────────────────────────────────────────────
|
|
708
|
+
/**
|
|
709
|
+
* Resolve the PID of the process currently listening on the given TCP port.
|
|
710
|
+
*
|
|
711
|
+
* @D-EFR-5 PID-port identity gate: signal 0 proves a PID exists; a health
|
|
712
|
+
* check proves some process on the port is our relay. Neither alone proves
|
|
713
|
+
* the pid-file PID *is* the listening process. A recycled PID can fool both
|
|
714
|
+
* checks simultaneously. The fix: use lsof to resolve the listening PID
|
|
715
|
+
* directly and require it to equal the pid-file PID before any signal.
|
|
716
|
+
*
|
|
717
|
+
* lsof is the canonical tool on darwin and is commonly available on linux.
|
|
718
|
+
* On unsupported platforms or when lsof is absent/restricted, returns null
|
|
719
|
+
* so the caller fails safe (identity-unconfirmed) rather than throwing.
|
|
720
|
+
*
|
|
721
|
+
* On darwin and linux: `lsof -nP -iTCP:<port> -sTCP:LISTEN -t`
|
|
722
|
+
* -n no host-name resolution -P no port-name resolution
|
|
723
|
+
* -sTCP:LISTEN only LISTEN-state sockets -t PIDs only
|
|
724
|
+
*
|
|
725
|
+
* avoids PF-001: fixed argument array — port is validated as a safe integer
|
|
726
|
+
* by the caller before this function is invoked; it is never interpolated
|
|
727
|
+
* into a shell string.
|
|
728
|
+
*
|
|
729
|
+
* Returns null when:
|
|
730
|
+
* • Platform is not darwin or linux
|
|
731
|
+
* • lsof is not on PATH or returns a non-zero exit code (EPERM, etc.)
|
|
732
|
+
* • Zero processes are listening on the port
|
|
733
|
+
* • More than one PID is returned (ambiguous — fail safe)
|
|
734
|
+
* • The single returned value is not a valid positive integer
|
|
735
|
+
*/
|
|
736
|
+
async function realResolveListeningPid(port) {
|
|
737
|
+
if (process.platform !== 'darwin' && process.platform !== 'linux') {
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
return new Promise((resolve) => {
|
|
741
|
+
// SEC: fixed argument list — port is a validated integer (1-65535).
|
|
742
|
+
const proc = cpSpawn('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t'], {
|
|
743
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
744
|
+
});
|
|
745
|
+
let stdout = '';
|
|
746
|
+
proc.stdout.on('data', (chunk) => {
|
|
747
|
+
stdout += chunk.toString();
|
|
748
|
+
});
|
|
749
|
+
// Spawn failure (ENOENT = lsof absent, EPERM = restricted) → degrade to null
|
|
750
|
+
proc.on('error', () => resolve(null));
|
|
751
|
+
proc.on('close', () => {
|
|
752
|
+
const lines = stdout.trim().split('\n').filter(Boolean);
|
|
753
|
+
// Zero or more-than-one result: ambiguous → fail safe
|
|
754
|
+
if (lines.length !== 1) {
|
|
755
|
+
resolve(null);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
const parsed = parseInt(lines[0].trim(), 10);
|
|
759
|
+
resolve(Number.isInteger(parsed) && parsed > 0 ? parsed : null);
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Read and parse the relay PID file. Returns null on ENOENT, parse failure, or
|
|
765
|
+
* invalid value — callers treat null as "no pid available".
|
|
766
|
+
*/
|
|
767
|
+
async function readPidFile(pidPath) {
|
|
768
|
+
try {
|
|
769
|
+
const pidStr = await fs.readFile(pidPath, 'utf-8');
|
|
770
|
+
const pid = parseInt(pidStr.trim(), 10);
|
|
771
|
+
return !isNaN(pid) && pid > 0 ? pid : null;
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Terminate the relay process recorded in the pid file, with identity confirmation.
|
|
779
|
+
*
|
|
780
|
+
* Protocol:
|
|
781
|
+
* 1. Read the pid file — skip silently if absent.
|
|
782
|
+
* 2. Check liveness (signal 0).
|
|
783
|
+
* 3. Confirm identity — three signals, ALL must pass before signalling:
|
|
784
|
+
* a. TCP probe: port is accepting connections.
|
|
785
|
+
* b. Health check: the process on the port is our relay (`isOurRelayBody`).
|
|
786
|
+
* c. PID-port binding (D-EFR-5): lsof confirms the pid-file PID is the
|
|
787
|
+
* process listening on the port. If lsof is unavailable or ambiguous,
|
|
788
|
+
* fail safe — return 'identity-unconfirmed' without signalling.
|
|
789
|
+
* 4. SIGTERM → poll for death every 250 ms for up to 2 s (8 checks).
|
|
790
|
+
* 5. SIGKILL → poll for death every 250 ms for up to 2 s (8 checks).
|
|
791
|
+
* 6. Remove the pid file when the process is gone.
|
|
792
|
+
* 7. Best-effort rmdir of .proxy-spawn.lock when stale.
|
|
793
|
+
*
|
|
794
|
+
* applies D-EFR-5: signal 0 and health check are independent facts; lsof
|
|
795
|
+
* binds the pid-file PID to the port owner before any signal is sent.
|
|
796
|
+
* avoids PF-009: all sub-operations are non-fatal; a kill failure never aborts disable.
|
|
797
|
+
* avoids PF-014: never process.exit() inside a finally-guarded scope.
|
|
798
|
+
*
|
|
799
|
+
* @param deps Optional injectable dependencies (default: real lsof implementation).
|
|
800
|
+
* Provide a stub in tests to exercise every branch without a real binary.
|
|
801
|
+
*
|
|
802
|
+
* @returns 'killed' — relay stopped successfully.
|
|
803
|
+
* 'not-running' — pid file absent or process already dead.
|
|
804
|
+
* 'identity-unconfirmed' — pid alive but identity not positively established
|
|
805
|
+
* (health or lsof mismatch); no signal sent.
|
|
806
|
+
* 'kill-failed' — SIGTERM + SIGKILL sent but process still alive after grace.
|
|
807
|
+
*/
|
|
808
|
+
export async function terminateRelay(pidPath, spawnLockPath, port, deps = { resolveListeningPid: realResolveListeningPid }) {
|
|
809
|
+
// Step 1: read pid file
|
|
810
|
+
const pid = await readPidFile(pidPath);
|
|
811
|
+
if (pid === null)
|
|
812
|
+
return 'not-running';
|
|
813
|
+
// Step 2: check liveness
|
|
814
|
+
try {
|
|
815
|
+
process.kill(pid, 0);
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
// Process already dead
|
|
819
|
+
try {
|
|
820
|
+
await fs.unlink(pidPath);
|
|
821
|
+
}
|
|
822
|
+
catch { /* best-effort */ }
|
|
823
|
+
return 'not-running';
|
|
824
|
+
}
|
|
825
|
+
// Step 3: confirm identity — all three signals must pass (D-EFR-5)
|
|
826
|
+
//
|
|
827
|
+
// Signal (a): TCP probe — confirms the port is accepting connections at all.
|
|
828
|
+
const portUp = await realTcpConnectable(port, PROBE_TIMEOUT_MS);
|
|
829
|
+
let identityConfirmed = false;
|
|
830
|
+
if (portUp) {
|
|
831
|
+
// Signal (b): health check — confirms the process on the port is our relay.
|
|
832
|
+
const healthResult = await realHttpGet(`${proxyBaseUrl(port)}/__subswitch/health`, PROBE_TIMEOUT_MS);
|
|
833
|
+
const healthOk = healthResult.ok && isOurRelayBody(healthResult.value);
|
|
834
|
+
if (healthOk) {
|
|
835
|
+
// Signal (c): PID-port binding — lsof confirms the pid-file PID owns the port.
|
|
836
|
+
// If lsof is absent, returns null → null !== pid → identityConfirmed stays false.
|
|
837
|
+
// Failing safe is strictly better than signalling the wrong process.
|
|
838
|
+
const listeningPid = await deps.resolveListeningPid(port);
|
|
839
|
+
identityConfirmed = listeningPid === pid;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
if (!identityConfirmed) {
|
|
843
|
+
return 'identity-unconfirmed';
|
|
844
|
+
}
|
|
845
|
+
// Bounded poll helper: check if PID is alive every 250 ms, up to maxChecks iterations.
|
|
846
|
+
// Returns true when the process is gone within the polling window.
|
|
847
|
+
const pollDead = async (maxChecks) => {
|
|
848
|
+
for (let i = 0; i < maxChecks; i++) {
|
|
849
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
850
|
+
try {
|
|
851
|
+
process.kill(pid, 0);
|
|
852
|
+
}
|
|
853
|
+
catch {
|
|
854
|
+
return true;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return false;
|
|
858
|
+
};
|
|
859
|
+
// Step 4: SIGTERM → bounded 2 s wait (8 × 250 ms)
|
|
860
|
+
try {
|
|
861
|
+
process.kill(pid, 'SIGTERM');
|
|
862
|
+
}
|
|
863
|
+
catch { /* already exited — race */ }
|
|
864
|
+
const deadAfterTerm = await pollDead(8);
|
|
865
|
+
if (!deadAfterTerm) {
|
|
866
|
+
// Step 5: SIGKILL → bounded 2 s wait (8 × 250 ms)
|
|
867
|
+
try {
|
|
868
|
+
process.kill(pid, 'SIGKILL');
|
|
869
|
+
}
|
|
870
|
+
catch { /* already exited — race */ }
|
|
871
|
+
const deadAfterKill = await pollDead(8);
|
|
872
|
+
if (!deadAfterKill) {
|
|
873
|
+
return 'kill-failed';
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
// Step 6: remove pid file
|
|
877
|
+
try {
|
|
878
|
+
await fs.unlink(pidPath);
|
|
879
|
+
}
|
|
880
|
+
catch { /* best-effort */ }
|
|
881
|
+
// Step 7: best-effort rmdir of spawn lock when stale
|
|
882
|
+
try {
|
|
883
|
+
await fs.rmdir(spawnLockPath);
|
|
884
|
+
}
|
|
885
|
+
catch { /* held by another session or absent — ignore */ }
|
|
886
|
+
return 'killed';
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Probe relay process state via TCP connect + health identity check.
|
|
890
|
+
* Always probes — does not short-circuit when the feature is disabled.
|
|
891
|
+
* This ensures `--status` surfaces the divergence case where the feature is
|
|
892
|
+
* disabled but the relay process is still listening (the relay is not killed on
|
|
893
|
+
* `--disable` for live-session continuity; it may outlive the disable operation).
|
|
894
|
+
*/
|
|
895
|
+
async function resolveProcessState(port) {
|
|
896
|
+
const portUp = await realTcpConnectable(port, PROBE_TIMEOUT_MS);
|
|
897
|
+
if (!portUp)
|
|
898
|
+
return 'down';
|
|
899
|
+
const healthResult = await realHttpGet(`${proxyBaseUrl(port)}/__subswitch/health`, PROBE_TIMEOUT_MS);
|
|
900
|
+
// Uses shared isOurRelayBody helper (same logic as runProxyPreflight)
|
|
901
|
+
if (healthResult.ok && isOurRelayBody(healthResult.value)) {
|
|
902
|
+
return 'running-ours';
|
|
903
|
+
}
|
|
904
|
+
// Port accepting but health timed out, failed, or not our relay
|
|
905
|
+
return 'port-squatted';
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Pure: compute the process log line from resolved state.
|
|
909
|
+
* Returns null when no line should be emitted (dead pid + non-down processState).
|
|
910
|
+
*/
|
|
911
|
+
function formatProcessLine(processState, pidAlive, pidFromFile, port) {
|
|
912
|
+
if (pidFromFile !== null) {
|
|
913
|
+
if (pidAlive) {
|
|
914
|
+
if (processState === 'running-ours') {
|
|
915
|
+
return {
|
|
916
|
+
level: 'info',
|
|
917
|
+
msg: `Process: ${color.green('running')} (pid ${pidFromFile}) — stop manually with: kill ${pidFromFile}`,
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
else if (processState === 'port-squatted') {
|
|
921
|
+
return {
|
|
922
|
+
level: 'warn',
|
|
923
|
+
msg: `Process: ${color.yellow('port squatted by another app')} (pid ${pidFromFile} alive but port ${port} is not our relay)`,
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
return {
|
|
928
|
+
level: 'info',
|
|
929
|
+
msg: `Process: ${color.yellow('pid alive but port not responding')} (pid ${pidFromFile})`,
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
// Pid dead
|
|
934
|
+
if (processState === 'down') {
|
|
935
|
+
return {
|
|
936
|
+
level: 'info',
|
|
937
|
+
msg: `Process: ${color.dim('down')} (last pid ${pidFromFile}, no longer running)`,
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
return null; // pid dead but port squatted/running — unusual, no line
|
|
941
|
+
}
|
|
942
|
+
// No pid file
|
|
943
|
+
if (processState === 'running-ours') {
|
|
944
|
+
return { level: 'info', msg: `Process: ${color.green('running')} (no pid file)` };
|
|
945
|
+
}
|
|
946
|
+
else if (processState === 'port-squatted') {
|
|
947
|
+
return {
|
|
948
|
+
level: 'warn',
|
|
949
|
+
msg: `Process: ${color.yellow('port squatted')} — port ${port} is in use by another application`,
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
else {
|
|
953
|
+
return { level: 'info', msg: `Process: ${color.dim('down')}` };
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
// ─── Status formatters ────────────────────────────────────────────────────────
|
|
957
|
+
/**
|
|
958
|
+
* Pure formatter for the external models status line shown by `devflow proxy --status`.
|
|
959
|
+
*
|
|
960
|
+
* Extracted to enable isolated testing without clack I/O. (AC-F6)
|
|
961
|
+
*/
|
|
962
|
+
export function formatExternalModelsLine(catalog, logPath) {
|
|
963
|
+
if (catalog.known) {
|
|
964
|
+
return `External models: ${color.cyan(catalog.selectableNames.join(', '))}`;
|
|
965
|
+
}
|
|
966
|
+
return `External models: ${color.dim('unavailable')} — see ${logPath}`;
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Pure formatter for the Codex auth status line shown by `devflow proxy --status`.
|
|
970
|
+
*
|
|
971
|
+
* Returns `{level, msg}` — level encodes the warn-on-unreadable rule so callers
|
|
972
|
+
* need not re-derive it from `state.kind`. `unreadable` is the only state the user
|
|
973
|
+
* must act on (the relay rejects requests at that point while a bare file-existence
|
|
974
|
+
* check still reported success). Expiry is informational: the relay refreshes the
|
|
975
|
+
* access token on its own, so an expired stamp on a healthy install is normal.
|
|
976
|
+
*
|
|
977
|
+
* @param now Injected for deterministic tests; pass `new Date()` in production.
|
|
978
|
+
*/
|
|
979
|
+
export function formatCodexAuthLine(state, authPath, now) {
|
|
980
|
+
switch (state.kind) {
|
|
981
|
+
case 'absent':
|
|
982
|
+
return { level: 'info', msg: `Codex auth: ${color.dim('absent')} — run codex login` };
|
|
983
|
+
case 'unreadable':
|
|
984
|
+
return {
|
|
985
|
+
level: 'warn',
|
|
986
|
+
msg: `Codex auth: ${color.red(`unreadable (${state.reason})`)} at ${authPath} — run codex login`,
|
|
987
|
+
};
|
|
988
|
+
case 'present': {
|
|
989
|
+
// authMode comes from the third-party ~/.codex/auth.json; strip
|
|
990
|
+
// ANSI escapes and cap length before embedding in terminal output.
|
|
991
|
+
const safeAuthMode = stripAnsi(state.authMode).slice(0, AUTH_MODE_MAX_LEN);
|
|
992
|
+
const identity = `${safeAuthMode}, ${state.accountSuffix}`;
|
|
993
|
+
const present = `Codex auth: ${color.green('present')} (${identity})`;
|
|
994
|
+
if (state.expiresAt === undefined)
|
|
995
|
+
return { level: 'info', msg: present };
|
|
996
|
+
const stamp = state.expiresAt.toISOString().slice(0, 10);
|
|
997
|
+
const msg = state.expiresAt.getTime() <= now.getTime()
|
|
998
|
+
? `${present} — access token expired ${stamp}, refreshes on next request`
|
|
999
|
+
: `${present} — access token valid through ${stamp}`;
|
|
1000
|
+
return { level: 'info', msg };
|
|
1001
|
+
}
|
|
1002
|
+
default: {
|
|
1003
|
+
const exhaustive = state;
|
|
1004
|
+
void exhaustive;
|
|
1005
|
+
return { level: 'info', msg: '' };
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
// ─── Port resolution ──────────────────────────────────────────────────────────
|
|
1010
|
+
/**
|
|
1011
|
+
* Resolve the effective port for enable.
|
|
1012
|
+
*
|
|
1013
|
+
* When portOption is undefined (--port flag not provided by the user), falls back to
|
|
1014
|
+
* priorPort from proxy.json. This is the remembered-port path: a user who enabled on
|
|
1015
|
+
* port 5000, disabled, then re-enables without --port correctly reuses 5000.
|
|
1016
|
+
*
|
|
1017
|
+
* @param portOption Commander --port value; undefined when flag not provided
|
|
1018
|
+
* @param priorPort Last-used port from proxy.json (or DEFAULT_PROXY_PORT)
|
|
1019
|
+
*/
|
|
1020
|
+
export function resolvePort(portOption, priorPort) {
|
|
1021
|
+
if (portOption === undefined)
|
|
1022
|
+
return Ok(priorPort);
|
|
1023
|
+
const parsed = parseInt(portOption, 10);
|
|
1024
|
+
if (isNaN(parsed) || parsed < 1 || parsed > 65535) {
|
|
1025
|
+
return Err(`Invalid port: ${portOption}`);
|
|
1026
|
+
}
|
|
1027
|
+
return Ok(parsed);
|
|
1028
|
+
}
|
|
1029
|
+
export const proxyCommand = new Command('proxy')
|
|
1030
|
+
.description('Enable or disable external model routing (GPT models via your OpenAI/Codex subscription)')
|
|
1031
|
+
.option('--enable', 'Enable external model routing via the Devflow proxy')
|
|
1032
|
+
.option('--disable', 'Disable external model routing')
|
|
1033
|
+
.option('--status', 'Show proxy status')
|
|
1034
|
+
.option('--port <n>', 'Port for the local relay (default: remembered or 4141)')
|
|
1035
|
+
.action(async (options) => {
|
|
1036
|
+
// No flag → show status
|
|
1037
|
+
const hasFlag = options.enable || options.disable || options.status;
|
|
1038
|
+
if (!hasFlag) {
|
|
1039
|
+
await runStatus();
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
if (options.status) {
|
|
1043
|
+
await runStatus();
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
if (options.enable) {
|
|
1047
|
+
await runEnable(options.port);
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
if (options.disable) {
|
|
1051
|
+
await runDisable();
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
// ─── Status ───────────────────────────────────────────────────────────────────
|
|
1056
|
+
async function runStatus() {
|
|
1057
|
+
const devflowDir = getDevFlowDirectory();
|
|
1058
|
+
const claudeDir = getClaudeDirectory();
|
|
1059
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
1060
|
+
const home = getHomeDirectory();
|
|
1061
|
+
const logPath = path.join(devflowDir, 'logs', 'proxy.log');
|
|
1062
|
+
const pidPath = path.join(devflowDir, 'proxy.pid');
|
|
1063
|
+
const codexAuthPath = path.join(home, '.codex', 'auth.json');
|
|
1064
|
+
p.intro(color.bgBlue(color.white(' Devflow Proxy Status ')));
|
|
1065
|
+
// Feature state: manifest + proxy.json
|
|
1066
|
+
const manifest = await readManifest(devflowDir);
|
|
1067
|
+
const proxyStateResult = await readProxyState(devflowDir);
|
|
1068
|
+
const proxyState = proxyStateResult.ok ? proxyStateResult.value : null;
|
|
1069
|
+
const manifestEnabled = manifest?.features.proxy ?? false;
|
|
1070
|
+
const stateEnabled = proxyState?.enabled ?? false;
|
|
1071
|
+
if (manifestEnabled !== stateEnabled) {
|
|
1072
|
+
p.log.warn(`Feature state drift: manifest says ${manifestEnabled ? color.green('enabled') : color.dim('disabled')}, ` +
|
|
1073
|
+
`proxy.json says ${stateEnabled ? color.green('enabled') : color.dim('disabled')} — run devflow proxy --enable or --disable to repair`);
|
|
1074
|
+
}
|
|
1075
|
+
const featureEnabled = manifestEnabled && stateEnabled;
|
|
1076
|
+
p.log.info(`Feature: ${featureEnabled ? color.green('enabled') : color.dim('disabled')}` +
|
|
1077
|
+
(proxyState?.port ? ` (port ${proxyState.port})` : ''));
|
|
1078
|
+
// Process state — always probe regardless of featureEnabled so that the
|
|
1079
|
+
// divergence case (feature disabled but relay still running) is visible.
|
|
1080
|
+
const port = proxyState?.port ?? DEFAULT_PROXY_PORT;
|
|
1081
|
+
const processState = await resolveProcessState(port);
|
|
1082
|
+
// Divergence: feature is disabled but the relay is still responding.
|
|
1083
|
+
// This is the expected state immediately after `--disable` — the relay
|
|
1084
|
+
// keeps serving any live Claude Code sessions whose env captured the URL.
|
|
1085
|
+
if (!stateEnabled && processState === 'running-ours') {
|
|
1086
|
+
p.log.warn(`Relay is still running on port ${port} even though external model routing is disabled. ` +
|
|
1087
|
+
`Live Claude Code sessions whose env captured the relay URL continue routing through it. ` +
|
|
1088
|
+
`Run ${color.cyan('devflow proxy --disable')} again to stop the relay, ` +
|
|
1089
|
+
`or restart Claude Code to clear the routed session.`);
|
|
1090
|
+
}
|
|
1091
|
+
const pidFromFile = await readPidFile(pidPath);
|
|
1092
|
+
let pidAlive = false;
|
|
1093
|
+
if (pidFromFile !== null) {
|
|
1094
|
+
try {
|
|
1095
|
+
process.kill(pidFromFile, 0);
|
|
1096
|
+
pidAlive = true;
|
|
1097
|
+
}
|
|
1098
|
+
catch { /* dead */ }
|
|
1099
|
+
}
|
|
1100
|
+
const processLine = formatProcessLine(processState, pidAlive, pidFromFile, port);
|
|
1101
|
+
if (processLine !== null) {
|
|
1102
|
+
if (processLine.level === 'warn') {
|
|
1103
|
+
p.log.warn(processLine.msg);
|
|
1104
|
+
}
|
|
1105
|
+
else {
|
|
1106
|
+
p.log.info(processLine.msg);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
// Env state
|
|
1110
|
+
let envState = 'absent';
|
|
1111
|
+
try {
|
|
1112
|
+
const settingsJson = await fs.readFile(settingsPath, 'utf-8');
|
|
1113
|
+
envState = readProxyEnvState(settingsJson, port);
|
|
1114
|
+
}
|
|
1115
|
+
catch { /* settings missing */ }
|
|
1116
|
+
const envStateLabels = {
|
|
1117
|
+
'ours': color.green('set (our relay)'),
|
|
1118
|
+
'ours-other-port': color.yellow('set (our relay, different port)'),
|
|
1119
|
+
'foreign': color.red('set (foreign gateway — Devflow will not overwrite)'),
|
|
1120
|
+
'absent': color.dim('not set'),
|
|
1121
|
+
};
|
|
1122
|
+
p.log.info(`ANTHROPIC_BASE_URL: ${envStateLabels[envState]}`);
|
|
1123
|
+
// Codex auth — inspect contents, not just existence. A file the relay would
|
|
1124
|
+
// reject (bad JSON, no account id) 401s every request while an existence check
|
|
1125
|
+
// still reports "present"; that gap is what this check closes.
|
|
1126
|
+
let codexAuth;
|
|
1127
|
+
try {
|
|
1128
|
+
codexAuth = inspectCodexAuth(await fs.readFile(codexAuthPath, 'utf-8'));
|
|
1129
|
+
}
|
|
1130
|
+
catch (err) {
|
|
1131
|
+
codexAuth = classifyCodexAuthReadError(err);
|
|
1132
|
+
}
|
|
1133
|
+
const codexAuthLine = formatCodexAuthLine(codexAuth, codexAuthPath, new Date());
|
|
1134
|
+
if (codexAuthLine.level === 'warn') {
|
|
1135
|
+
p.log.warn(codexAuthLine.msg);
|
|
1136
|
+
}
|
|
1137
|
+
else {
|
|
1138
|
+
p.log.info(codexAuthLine.msg);
|
|
1139
|
+
}
|
|
1140
|
+
// Agent mapping count
|
|
1141
|
+
const mappingResult = await readAgentMapping(devflowDir);
|
|
1142
|
+
if (mappingResult.ok) {
|
|
1143
|
+
const count = countExternalMappedAgents(mappingResult.value);
|
|
1144
|
+
if (count > 0) {
|
|
1145
|
+
p.log.info(`External-mapped agents: ${color.cyan(String(count))}`);
|
|
1146
|
+
}
|
|
1147
|
+
else {
|
|
1148
|
+
p.log.info('External-mapped agents: none — use devflow agents to configure');
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
// External models registry (cache-only, zero spawns — avoids multi-second silent pause in --status)
|
|
1152
|
+
const cacheDir = modelCacheDir(devflowDir); // authoritative path from cache.ts (avoids PF-013)
|
|
1153
|
+
const catalog = getExternalModelsCached(cacheDir);
|
|
1154
|
+
p.log.info(formatExternalModelsLine(catalog, logPath));
|
|
1155
|
+
// Log path
|
|
1156
|
+
p.log.info(`Proxy log: ${color.dim(logPath)}`);
|
|
1157
|
+
if (featureEnabled && processState === 'running-ours') {
|
|
1158
|
+
p.note(`${color.cyan('devflow proxy --disable')} Disable external model routing\n` +
|
|
1159
|
+
`${color.cyan('devflow proxy --status')} Refresh status`, 'Management');
|
|
1160
|
+
}
|
|
1161
|
+
else {
|
|
1162
|
+
p.note(`${color.cyan('devflow proxy --enable')} Enable external model routing\n` +
|
|
1163
|
+
`${color.cyan('devflow proxy --status')} Refresh status`, 'Management');
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
// ─── Enable ───────────────────────────────────────────────────────────────────
|
|
1167
|
+
async function runEnable(portOption) {
|
|
1168
|
+
const devflowDir = getDevFlowDirectory();
|
|
1169
|
+
const claudeDir = getClaudeDirectory();
|
|
1170
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
1171
|
+
const installDir = path.join(claudeDir, 'agents', 'devflow');
|
|
1172
|
+
const home = getHomeDirectory();
|
|
1173
|
+
const codexAuthPath = path.join(home, '.codex', 'auth.json');
|
|
1174
|
+
const configPath = path.join(devflowDir, 'proxy-routing.json');
|
|
1175
|
+
const logPath = path.join(devflowDir, 'logs', 'proxy.log');
|
|
1176
|
+
const pidPath = path.join(devflowDir, 'proxy.pid');
|
|
1177
|
+
const cacheDir = modelCacheDir(devflowDir); // authoritative path from cache.ts (avoids PF-013)
|
|
1178
|
+
// Step 1: Read prior proxy.json (remembered port); --port flag overrides
|
|
1179
|
+
const priorStateResult = await readProxyState(devflowDir);
|
|
1180
|
+
const priorPort = priorStateResult.ok ? priorStateResult.value.port : DEFAULT_PROXY_PORT;
|
|
1181
|
+
const portResult = resolvePort(portOption, priorPort);
|
|
1182
|
+
if (!portResult.ok) {
|
|
1183
|
+
p.log.error(portResult.error);
|
|
1184
|
+
process.exitCode = 1;
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
const port = portResult.value;
|
|
1188
|
+
const s = p.spinner();
|
|
1189
|
+
s.start('Running preflight checks...');
|
|
1190
|
+
// Step 2: Write routing config
|
|
1191
|
+
await fs.mkdir(devflowDir, { recursive: true });
|
|
1192
|
+
// SEC-2: mode 0o700 for the logs directory (new directories only; pre-existing
|
|
1193
|
+
// dirs are unaffected by mkdir). openProxyLog handles individual file mode.
|
|
1194
|
+
await fs.mkdir(path.join(devflowDir, 'logs'), { recursive: true, mode: 0o700 });
|
|
1195
|
+
// SEC-2 pre-spawn rotation: rotate proxy.log before the relay opens an fd on it.
|
|
1196
|
+
// MUST run before spawnRelayAndWaitForPort (Step 5) — a rename while the relay
|
|
1197
|
+
// holds an append fd would orphan that fd. At this point no relay is running.
|
|
1198
|
+
await rotateProxyLogIfLarge(logPath);
|
|
1199
|
+
// Read existing routing config to preserve user-added anthropic/limits/logLevel/providers
|
|
1200
|
+
// blocks. A missing or malformed file falls back to clean defaults inside
|
|
1201
|
+
// buildRoutingConfigJson (non-fatal; avoids PF-009).
|
|
1202
|
+
let existingRoutingContent;
|
|
1203
|
+
try {
|
|
1204
|
+
existingRoutingContent = await fs.readFile(configPath, 'utf-8');
|
|
1205
|
+
}
|
|
1206
|
+
catch { /* ENOENT or EACCES — fall through to clean defaults */ }
|
|
1207
|
+
try {
|
|
1208
|
+
await fs.writeFile(configPath, buildRoutingConfigJson(port, existingRoutingContent), 'utf-8');
|
|
1209
|
+
}
|
|
1210
|
+
catch (err) {
|
|
1211
|
+
s.stop(color.red('Failed to write routing config'));
|
|
1212
|
+
p.log.error(`Could not write routing config: ${err instanceof Error ? err.message : String(err)}`);
|
|
1213
|
+
process.exitCode = 1;
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
// Step 3: Preflight checks (①–④: bin, codex auth, port probe, settings)
|
|
1217
|
+
// preflightDeps is saved so spawnDoctor can be reused in step 6 (post-spawn verification).
|
|
1218
|
+
const preflightDeps = buildRealPreflightDeps({
|
|
1219
|
+
settingsPath,
|
|
1220
|
+
// runEnable propagates settings read errors (init.ts swallows — see swallowSettingsReadError)
|
|
1221
|
+
swallowSettingsReadError: false,
|
|
1222
|
+
onWarn: (msg) => { s.stop(''); p.log.warn(msg); s.start(''); },
|
|
1223
|
+
});
|
|
1224
|
+
const preflightResult = await runProxyPreflight(port, codexAuthPath, configPath, logPath, preflightDeps);
|
|
1225
|
+
if (!preflightResult.ok) {
|
|
1226
|
+
s.stop(color.red('Preflight failed'));
|
|
1227
|
+
p.log.error(preflightResult.error);
|
|
1228
|
+
process.exitCode = 1;
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
const { binPath, npxWarning, adopted } = preflightResult.value;
|
|
1232
|
+
// Shared best-effort rollback for steps 5–7: write proxy.json enabled:false so a
|
|
1233
|
+
// subsequent --enable attempt does not believe the relay is running. A write failure
|
|
1234
|
+
// here is always secondary to the primary failure — never surfaced to the user.
|
|
1235
|
+
// All captured variables (binPath, configPath, port, devflowDir) are resolved by
|
|
1236
|
+
// this point.
|
|
1237
|
+
const rollbackProxyState = async () => {
|
|
1238
|
+
await writeProxyState(devflowDir, buildProxyState({
|
|
1239
|
+
enabled: false,
|
|
1240
|
+
port,
|
|
1241
|
+
binPath,
|
|
1242
|
+
configPath,
|
|
1243
|
+
devflowVersion: getDevflowVersion(),
|
|
1244
|
+
}));
|
|
1245
|
+
};
|
|
1246
|
+
s.message('Writing proxy state...');
|
|
1247
|
+
// Step 4: Write proxy.json enabled:true
|
|
1248
|
+
const newState = buildProxyState({
|
|
1249
|
+
enabled: true,
|
|
1250
|
+
port,
|
|
1251
|
+
binPath,
|
|
1252
|
+
configPath,
|
|
1253
|
+
devflowVersion: getDevflowVersion(),
|
|
1254
|
+
});
|
|
1255
|
+
const writeStateResult = await writeProxyState(devflowDir, newState);
|
|
1256
|
+
if (!writeStateResult.ok) {
|
|
1257
|
+
s.stop(color.red('Failed to write proxy state'));
|
|
1258
|
+
p.log.error(writeStateResult.error);
|
|
1259
|
+
process.exitCode = 1;
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
// Step 5: Spawn relay and wait for port
|
|
1263
|
+
if (!adopted) {
|
|
1264
|
+
s.message('Starting relay...');
|
|
1265
|
+
}
|
|
1266
|
+
const spawnResult = await spawnRelayAndWaitForPort(port, binPath, configPath, logPath, pidPath, adopted, buildRealSpawnAndWaitDeps());
|
|
1267
|
+
if (!spawnResult.ok) {
|
|
1268
|
+
await rollbackProxyState();
|
|
1269
|
+
s.stop(color.red('Relay failed to start'));
|
|
1270
|
+
p.log.error(`Proxy failed to start — check ${logPath}`);
|
|
1271
|
+
process.exitCode = 1;
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
const { spawnedPid } = spawnResult;
|
|
1275
|
+
// Step 6: Post-spawn doctor verification — runs against the live relay (see D-EFR-2).
|
|
1276
|
+
// Relay is confirmed up before this runs, so "running: YES" is the expected outcome.
|
|
1277
|
+
s.message('Verifying routing connection...');
|
|
1278
|
+
const verifyResult = await runPostSpawnVerification(binPath, configPath, logPath, spawnedPid, {
|
|
1279
|
+
spawnDoctor: preflightDeps.spawnDoctor,
|
|
1280
|
+
// Best-effort SIGTERM + SIGKILL escalation after 2s grace period.
|
|
1281
|
+
// Only called when we spawned the relay (spawnedPid defined) — see D-EFR-2.
|
|
1282
|
+
killProcess: (pid) => {
|
|
1283
|
+
try {
|
|
1284
|
+
process.kill(pid, 'SIGTERM');
|
|
1285
|
+
}
|
|
1286
|
+
catch { /* already exited — ignore */ }
|
|
1287
|
+
const sigkillTimer = setTimeout(() => {
|
|
1288
|
+
try {
|
|
1289
|
+
process.kill(pid, 'SIGKILL');
|
|
1290
|
+
}
|
|
1291
|
+
catch { /* already exited — ignore */ }
|
|
1292
|
+
}, 2000);
|
|
1293
|
+
sigkillTimer.unref();
|
|
1294
|
+
},
|
|
1295
|
+
writeDisabledProxyState: rollbackProxyState,
|
|
1296
|
+
});
|
|
1297
|
+
if (!verifyResult.ok) {
|
|
1298
|
+
// Rollback and conditional kill already applied inside runPostSpawnVerification
|
|
1299
|
+
s.stop(color.red('Routing verification failed'));
|
|
1300
|
+
p.log.error(verifyResult.error);
|
|
1301
|
+
process.exitCode = 1;
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
s.message('Updating settings...');
|
|
1305
|
+
// Step 7: Atomic settings mutation
|
|
1306
|
+
const settingsResult = await applyEnableSettingsPass(settingsPath, devflowDir, port);
|
|
1307
|
+
if (!settingsResult.ok) {
|
|
1308
|
+
await rollbackProxyState();
|
|
1309
|
+
s.stop(color.red('Cannot update settings'));
|
|
1310
|
+
p.log.error(settingsResult.error);
|
|
1311
|
+
process.exitCode = 1;
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
// Step 8: Sync manifest
|
|
1315
|
+
await syncManifestFeature(devflowDir, 'proxy', true);
|
|
1316
|
+
// Step 9: Reapply agent mapping
|
|
1317
|
+
const reapplyResult = await reapplyAgentMapping({
|
|
1318
|
+
proxyEnabled: true,
|
|
1319
|
+
installDir,
|
|
1320
|
+
devflowDir,
|
|
1321
|
+
});
|
|
1322
|
+
const mappedCount = reapplyResult.updated.length;
|
|
1323
|
+
// Warm the model cache UNDER the spinner so the wait is visible and
|
|
1324
|
+
// attributed. Strictly non-fatal — a cache failure must never affect the
|
|
1325
|
+
// enable result. The promise is awaited so Node can exit cleanly after
|
|
1326
|
+
// s.stop, instead of being held open ~200-600ms.
|
|
1327
|
+
s.message('Warming model cache...');
|
|
1328
|
+
await discoverExternalModels(cacheDir, logPath).catch(() => { });
|
|
1329
|
+
s.stop(color.green('External model routing enabled'));
|
|
1330
|
+
if (adopted) {
|
|
1331
|
+
p.log.info(`Relay already running on port ${port} — adopted`);
|
|
1332
|
+
}
|
|
1333
|
+
else {
|
|
1334
|
+
p.log.success(`Relay started on port ${port}`);
|
|
1335
|
+
}
|
|
1336
|
+
if (npxWarning) {
|
|
1337
|
+
p.log.warn('Relay binary resolved from npx cache — may not persist across reboots; run devflow init to reinstall');
|
|
1338
|
+
}
|
|
1339
|
+
if (mappedCount > 0) {
|
|
1340
|
+
p.log.info(`Restored external model mapping for ${mappedCount} agent(s)`);
|
|
1341
|
+
}
|
|
1342
|
+
p.log.info(color.dim('Takes effect in new Claude Code sessions. Use devflow agents to configure per-agent models.'));
|
|
1343
|
+
}
|
|
1344
|
+
// ─── Disable ──────────────────────────────────────────────────────────────────
|
|
1345
|
+
async function runDisable() {
|
|
1346
|
+
const devflowDir = getDevFlowDirectory();
|
|
1347
|
+
const claudeDir = getClaudeDirectory();
|
|
1348
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
1349
|
+
const installDir = path.join(claudeDir, 'agents', 'devflow');
|
|
1350
|
+
const pidPath = path.join(devflowDir, 'proxy.pid');
|
|
1351
|
+
// Read prior state FIRST (before settings pass) to determine managed port.
|
|
1352
|
+
// applyDisableToSettings strips ANTHROPIC_BASE_URL only when the URL
|
|
1353
|
+
// port matches the port Devflow manages — callers must supply it. Reading
|
|
1354
|
+
// proxy.json here also consolidates state for Step 2 below.
|
|
1355
|
+
const priorStateResult = await readProxyState(devflowDir);
|
|
1356
|
+
const priorState = priorStateResult.ok ? priorStateResult.value : null;
|
|
1357
|
+
const managedPort = priorState?.port ?? DEFAULT_PROXY_PORT;
|
|
1358
|
+
// Step 1: Settings pass (removeProxyHooks + stripProxyEnv, single atomic write)
|
|
1359
|
+
let settingsContent;
|
|
1360
|
+
try {
|
|
1361
|
+
settingsContent = await fs.readFile(settingsPath, 'utf-8');
|
|
1362
|
+
}
|
|
1363
|
+
catch {
|
|
1364
|
+
settingsContent = '{}';
|
|
1365
|
+
}
|
|
1366
|
+
let parsedSettings;
|
|
1367
|
+
try {
|
|
1368
|
+
parsedSettings = JSON.parse(settingsContent);
|
|
1369
|
+
}
|
|
1370
|
+
catch {
|
|
1371
|
+
p.log.error('settings.json is malformed — fix it before disabling the proxy');
|
|
1372
|
+
process.exitCode = 1;
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
const changed = applyDisableToSettings(parsedSettings, managedPort);
|
|
1376
|
+
if (changed) {
|
|
1377
|
+
// Guard ENOSPC/EACCES — unhandled rejection leaves proxy in partial state
|
|
1378
|
+
try {
|
|
1379
|
+
await writeFileAtomicExclusive(settingsPath, JSON.stringify(parsedSettings, null, 2) + '\n');
|
|
1380
|
+
}
|
|
1381
|
+
catch (err) {
|
|
1382
|
+
p.log.error(`Could not write settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
1383
|
+
process.exitCode = 1;
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
// Step 2: Write proxy.json enabled:false (keep port/binPath/configPath)
|
|
1388
|
+
const disabledState = buildProxyState({
|
|
1389
|
+
enabled: false,
|
|
1390
|
+
port: priorState?.port ?? DEFAULT_PROXY_PORT,
|
|
1391
|
+
binPath: priorState?.binPath ?? null,
|
|
1392
|
+
configPath: priorState?.configPath ?? null,
|
|
1393
|
+
devflowVersion: getDevflowVersion(),
|
|
1394
|
+
});
|
|
1395
|
+
const writeDisabledResult = await writeProxyState(devflowDir, disabledState);
|
|
1396
|
+
if (!writeDisabledResult.ok) {
|
|
1397
|
+
p.log.error(`Could not write proxy state: ${writeDisabledResult.error}`);
|
|
1398
|
+
process.exitCode = 1;
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
// Step 3: Sync manifest
|
|
1402
|
+
await syncManifestFeature(devflowDir, 'proxy', false);
|
|
1403
|
+
// Step 4: Revert external agents to shipped defaults
|
|
1404
|
+
await revertExternalAgents({ installDir, devflowDir });
|
|
1405
|
+
p.log.success('External model routing disabled');
|
|
1406
|
+
// Step 5: Terminate the relay process.
|
|
1407
|
+
// Identity is confirmed via TCP + health before signalling — a recycled PID
|
|
1408
|
+
// must never be killed. avoids PF-009: all sub-operations are non-fatal.
|
|
1409
|
+
const spawnLockPath = path.join(devflowDir, '.proxy-spawn.lock');
|
|
1410
|
+
const terminateResult = await terminateRelay(pidPath, spawnLockPath, managedPort);
|
|
1411
|
+
switch (terminateResult) {
|
|
1412
|
+
case 'killed':
|
|
1413
|
+
p.log.info('Relay process stopped.');
|
|
1414
|
+
break;
|
|
1415
|
+
case 'not-running':
|
|
1416
|
+
p.log.info(color.dim('Relay process was not running.'));
|
|
1417
|
+
break;
|
|
1418
|
+
case 'identity-unconfirmed':
|
|
1419
|
+
p.log.warn(`PID from proxy.pid is alive but the relay identity could not be confirmed ` +
|
|
1420
|
+
`(port ${managedPort} is not responding as our relay). ` +
|
|
1421
|
+
`Inspect the process before stopping it manually.`);
|
|
1422
|
+
break;
|
|
1423
|
+
case 'kill-failed':
|
|
1424
|
+
p.log.warn(`Relay process (pid recorded in proxy.pid) did not stop after SIGTERM + SIGKILL. ` +
|
|
1425
|
+
`It may still be running — check with: kill -0 <pid>`);
|
|
1426
|
+
break;
|
|
1427
|
+
}
|
|
1428
|
+
// Always-print warning: already-running Claude Code sessions captured
|
|
1429
|
+
// ANTHROPIC_BASE_URL in their process env before the disable. The env var
|
|
1430
|
+
// is now stripped from settings.json so NEW sessions will not route, but
|
|
1431
|
+
// any session that was open during enable continues routing until restarted.
|
|
1432
|
+
p.log.warn('Already-running Claude Code sessions remain routed through the relay until they are restarted. ' +
|
|
1433
|
+
'Restart Claude Code to fully stop routing in those sessions.');
|
|
1434
|
+
}
|
|
1435
|
+
//# sourceMappingURL=proxy.js.map
|