agileflow 4.0.0-alpha.2 → 4.0.0-alpha.21
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 +51 -0
- package/content/plugins/accessibility/plugin.yaml +14 -0
- package/content/plugins/accessibility/skills/agileflow-accessibility/SKILL.md +392 -0
- package/content/plugins/accessibility/skills/agileflow-accessibility/references/aria-patterns.md +528 -0
- package/content/plugins/accessibility/skills/agileflow-accessibility/references/testing-checklist.md +457 -0
- package/content/plugins/accessibility/skills/agileflow-accessibility/references/wcag-guide.md +683 -0
- package/content/plugins/accessibility/skills/agileflow-accessibility/workflows/audit-page.md +310 -0
- package/content/plugins/accessibility/skills/agileflow-accessibility/workflows/implement-accessible-component.md +479 -0
- package/content/plugins/ads/agents/ads-audit-budget.md +185 -0
- package/content/plugins/ads/agents/ads-audit-compliance.md +171 -0
- package/content/plugins/ads/agents/ads-audit-creative.md +168 -0
- package/content/plugins/ads/agents/ads-audit-google.md +227 -0
- package/content/plugins/ads/agents/ads-audit-meta.md +184 -0
- package/content/plugins/ads/agents/ads-audit-tracking.md +205 -0
- package/content/plugins/ads/agents/ads-consensus.md +410 -0
- package/content/plugins/ads/agents/ads-generate.md +152 -0
- package/content/plugins/ads/agents/ads-performance-tracker.md +212 -0
- package/content/plugins/ads/plugin.yaml +23 -4
- package/content/plugins/ads/skills/agileflow-ads/SKILL.md +218 -0
- package/content/plugins/ads/skills/agileflow-ads/references/ad-copy-formula-guide.md +131 -0
- package/content/plugins/ads/skills/agileflow-ads/references/audience-targeting-guide.md +137 -0
- package/content/plugins/ads/skills/agileflow-ads/references/bid-strategy-guide.md +115 -0
- package/content/plugins/ads/skills/agileflow-ads/references/platform-benchmarks.md +100 -0
- package/content/plugins/ads/skills/agileflow-ads/workflows/audit.md +118 -0
- package/content/plugins/ads/skills/agileflow-ads/workflows/generate.md +84 -0
- package/content/plugins/audit/agents/a11y-analyzer-aria.md +173 -0
- package/content/plugins/audit/agents/a11y-analyzer-forms.md +173 -0
- package/content/plugins/audit/agents/a11y-analyzer-keyboard.md +183 -0
- package/content/plugins/audit/agents/a11y-analyzer-semantic.md +169 -0
- package/content/plugins/audit/agents/a11y-analyzer-visual.md +172 -0
- package/content/plugins/audit/agents/a11y-consensus.md +249 -0
- package/content/plugins/audit/agents/accessibility.md +558 -0
- package/content/plugins/audit/agents/api-quality-analyzer-conventions.md +156 -0
- package/content/plugins/audit/agents/api-quality-analyzer-docs.md +184 -0
- package/content/plugins/audit/agents/api-quality-analyzer-errors.md +191 -0
- package/content/plugins/audit/agents/api-quality-analyzer-pagination.md +179 -0
- package/content/plugins/audit/agents/api-quality-analyzer-versioning.md +150 -0
- package/content/plugins/audit/agents/api-quality-consensus.md +217 -0
- package/content/plugins/audit/agents/api-validator.md +191 -0
- package/content/plugins/audit/agents/arch-analyzer-circular.md +156 -0
- package/content/plugins/audit/agents/arch-analyzer-complexity.md +193 -0
- package/content/plugins/audit/agents/arch-analyzer-coupling.md +152 -0
- package/content/plugins/audit/agents/arch-analyzer-layering.md +160 -0
- package/content/plugins/audit/agents/arch-analyzer-patterns.md +210 -0
- package/content/plugins/audit/agents/arch-consensus.md +228 -0
- package/content/plugins/audit/agents/browser-qa.md +342 -0
- package/content/plugins/audit/agents/code-reviewer.md +298 -0
- package/content/plugins/audit/agents/completeness-analyzer-api.md +199 -0
- package/content/plugins/audit/agents/completeness-analyzer-conditional.md +211 -0
- package/content/plugins/audit/agents/completeness-analyzer-handlers.md +166 -0
- package/content/plugins/audit/agents/completeness-analyzer-imports.md +165 -0
- package/content/plugins/audit/agents/completeness-analyzer-routes.md +190 -0
- package/content/plugins/audit/agents/completeness-analyzer-state.md +196 -0
- package/content/plugins/audit/agents/completeness-analyzer-stubs.md +206 -0
- package/content/plugins/audit/agents/completeness-consensus.md +295 -0
- package/content/plugins/audit/agents/error-analyzer.md +213 -0
- package/content/plugins/audit/agents/flow-analyzer-authorization.md +182 -0
- package/content/plugins/audit/agents/flow-analyzer-discovery.md +174 -0
- package/content/plugins/audit/agents/flow-analyzer-errors.md +186 -0
- package/content/plugins/audit/agents/flow-analyzer-feedback.md +185 -0
- package/content/plugins/audit/agents/flow-analyzer-navigation.md +177 -0
- package/content/plugins/audit/agents/flow-analyzer-persistence.md +193 -0
- package/content/plugins/audit/agents/flow-analyzer-wiring.md +169 -0
- package/content/plugins/audit/agents/flow-consensus.md +237 -0
- package/content/plugins/audit/agents/legal-analyzer-a11y.md +114 -0
- package/content/plugins/audit/agents/legal-analyzer-ai.md +121 -0
- package/content/plugins/audit/agents/legal-analyzer-consumer.md +114 -0
- package/content/plugins/audit/agents/legal-analyzer-content.md +117 -0
- package/content/plugins/audit/agents/legal-analyzer-international.md +119 -0
- package/content/plugins/audit/agents/legal-analyzer-licensing.md +119 -0
- package/content/plugins/audit/agents/legal-analyzer-privacy.md +112 -0
- package/content/plugins/audit/agents/legal-analyzer-security.md +116 -0
- package/content/plugins/audit/agents/legal-analyzer-terms.md +115 -0
- package/content/plugins/audit/agents/legal-consensus.md +250 -0
- package/content/plugins/audit/agents/logic-analyzer-edge.md +179 -0
- package/content/plugins/audit/agents/logic-analyzer-flow.md +264 -0
- package/content/plugins/audit/agents/logic-analyzer-invariant.md +215 -0
- package/content/plugins/audit/agents/logic-analyzer-race.md +280 -0
- package/content/plugins/audit/agents/logic-analyzer-type.md +227 -0
- package/content/plugins/audit/agents/logic-consensus.md +259 -0
- package/content/plugins/audit/agents/perf-analyzer-assets.md +182 -0
- package/content/plugins/audit/agents/perf-analyzer-bundle.md +173 -0
- package/content/plugins/audit/agents/perf-analyzer-caching.md +170 -0
- package/content/plugins/audit/agents/perf-analyzer-compute.md +173 -0
- package/content/plugins/audit/agents/perf-analyzer-memory.md +193 -0
- package/content/plugins/audit/agents/perf-analyzer-network.md +165 -0
- package/content/plugins/audit/agents/perf-analyzer-queries.md +162 -0
- package/content/plugins/audit/agents/perf-analyzer-rendering.md +168 -0
- package/content/plugins/audit/agents/perf-consensus.md +287 -0
- package/content/plugins/audit/agents/qa.md +820 -0
- package/content/plugins/audit/agents/quality-analyzer-comments.md +159 -0
- package/content/plugins/audit/agents/quality-analyzer-duplication.md +184 -0
- package/content/plugins/audit/agents/quality-analyzer-naming.md +160 -0
- package/content/plugins/audit/agents/quality-consensus.md +241 -0
- package/content/plugins/audit/agents/schema-validator.md +473 -0
- package/content/plugins/audit/agents/security-analyzer-api.md +210 -0
- package/content/plugins/audit/agents/security-analyzer-auth.md +169 -0
- package/content/plugins/audit/agents/security-analyzer-authz.md +180 -0
- package/content/plugins/audit/agents/security-analyzer-deps.md +153 -0
- package/content/plugins/audit/agents/security-analyzer-infra.md +184 -0
- package/content/plugins/audit/agents/security-analyzer-injection.md +155 -0
- package/content/plugins/audit/agents/security-analyzer-input.md +201 -0
- package/content/plugins/audit/agents/security-analyzer-secrets.md +183 -0
- package/content/plugins/audit/agents/security-consensus.md +283 -0
- package/content/plugins/audit/agents/test-analyzer-assertions.md +188 -0
- package/content/plugins/audit/agents/test-analyzer-coverage.md +189 -0
- package/content/plugins/audit/agents/test-analyzer-fragility.md +193 -0
- package/content/plugins/audit/agents/test-analyzer-integration.md +161 -0
- package/content/plugins/audit/agents/test-analyzer-maintenance.md +180 -0
- package/content/plugins/audit/agents/test-analyzer-mocking.md +188 -0
- package/content/plugins/audit/agents/test-analyzer-patterns.md +196 -0
- package/content/plugins/audit/agents/test-analyzer-structure.md +184 -0
- package/content/plugins/audit/agents/test-consensus.md +301 -0
- package/content/plugins/audit/agents/testing.md +561 -0
- package/content/plugins/audit/agents/ui-validator.md +344 -0
- package/content/plugins/audit/plugin.yaml +186 -5
- package/content/plugins/audit/skills/agileflow-audit/SKILL.md +113 -0
- package/content/plugins/audit/skills/agileflow-audit/references/audit-depth-guide.md +151 -0
- package/content/plugins/audit/skills/agileflow-audit/references/dependency-risk-guide.md +139 -0
- package/content/plugins/audit/skills/agileflow-audit/references/owasp-top10.md +120 -0
- package/content/plugins/audit/skills/agileflow-audit/references/performance-budget-guide.md +143 -0
- package/content/plugins/audit/skills/agileflow-audit/references/wcag-criteria.md +117 -0
- package/content/plugins/audit/skills/agileflow-audit/workflows/run-audit.md +52 -0
- package/content/plugins/audit/skills/agileflow-audit/workflows/tdd.md +66 -0
- package/content/plugins/core/agents/adr-writer.md +521 -0
- package/content/plugins/core/agents/epic-planner.md +520 -0
- package/content/plugins/core/agents/mentor.md +709 -0
- package/content/plugins/core/agents/orchestrator.md +776 -0
- package/content/plugins/core/agents/team-coordinator.md +334 -0
- package/content/plugins/core/agents/team-lead.md +181 -0
- package/content/plugins/core/agents/workspace-orchestrator.md +146 -0
- package/content/plugins/core/hooks/context-loader.js +31 -4
- package/content/plugins/core/hooks/damage-control-bash.js +10 -2
- package/content/plugins/core/hooks/damage-control-edit.js +4 -1
- package/content/plugins/core/hooks/damage-control-patterns.yaml +1 -1
- package/content/plugins/core/hooks/damage-control-write.js +4 -1
- package/content/plugins/core/hooks/{pre-compact-state.js → post-compact-state.js} +25 -8
- package/content/plugins/core/hooks/preferences-injector.js +352 -0
- package/content/plugins/core/plugin.yaml +24 -28
- package/content/plugins/core/skills/agileflow-adr/SKILL.md +34 -8
- package/content/plugins/core/skills/agileflow-adr/references/madr-format-guide.md +86 -0
- package/content/plugins/core/skills/agileflow-adr/workflows/write-adr.md +57 -0
- package/content/plugins/core/skills/agileflow-babysit-mentor/SKILL.md +94 -27
- package/content/plugins/core/skills/agileflow-babysit-mentor/references/mentor-decision-guide.md +81 -0
- package/content/plugins/core/skills/agileflow-babysit-mentor/workflows/mentor-session.md +79 -0
- package/content/plugins/core/skills/agileflow-epic-planner/SKILL.md +37 -7
- package/content/plugins/core/skills/agileflow-epic-planner/references/epic-sizing-guide.md +81 -0
- package/content/plugins/core/skills/agileflow-epic-planner/workflows/plan-epic.md +55 -0
- package/content/plugins/core/skills/agileflow-status-updater/SKILL.md +36 -20
- package/content/plugins/core/skills/agileflow-status-updater/references/status-transitions.md +89 -0
- package/content/plugins/core/skills/agileflow-status-updater/workflows/update-status.md +56 -0
- package/content/plugins/core/skills/agileflow-story-writer/SKILL.md +39 -114
- package/content/plugins/core/skills/agileflow-story-writer/references/estimation-reference.md +36 -0
- package/content/plugins/core/skills/agileflow-story-writer/references/story-template.md +92 -0
- package/content/plugins/core/skills/agileflow-story-writer/workflows/write-story.md +138 -0
- package/content/plugins/council/agents/council-advocate.md +223 -0
- package/content/plugins/council/agents/council-analyst.md +278 -0
- package/content/plugins/council/agents/council-compounder.md +204 -0
- package/content/plugins/council/agents/council-contrarian.md +217 -0
- package/content/plugins/council/agents/council-moonshot.md +217 -0
- package/content/plugins/council/agents/council-optimist.md +185 -0
- package/content/plugins/council/agents/council-revenue.md +200 -0
- package/content/plugins/council/agents/council-technical.md +218 -0
- package/content/plugins/council/agents/multi-expert.md +334 -0
- package/content/plugins/council/plugin.yaml +23 -4
- package/content/plugins/council/skills/agileflow-council/SKILL.md +102 -0
- package/content/plugins/council/skills/agileflow-council/references/decision-log-template.md +109 -0
- package/content/plugins/council/skills/agileflow-council/references/perspective-guide.md +104 -0
- package/content/plugins/council/skills/agileflow-council/references/when-to-convene-guide.md +112 -0
- package/content/plugins/council/skills/agileflow-council/workflows/convene.md +73 -0
- package/content/plugins/council/skills/agileflow-council/workflows/multi-expert.md +75 -0
- package/content/plugins/database/plugin.yaml +14 -0
- package/content/plugins/database/skills/agileflow-database/SKILL.md +284 -0
- package/content/plugins/database/skills/agileflow-database/references/indexing-guide.md +313 -0
- package/content/plugins/database/skills/agileflow-database/references/migration-guide.md +328 -0
- package/content/plugins/database/skills/agileflow-database/references/schema-design-guide.md +467 -0
- package/content/plugins/database/skills/agileflow-database/workflows/design-schema.md +213 -0
- package/content/plugins/database/skills/agileflow-database/workflows/optimize-query.md +253 -0
- package/content/plugins/debugging/plugin.yaml +14 -0
- package/content/plugins/debugging/skills/agileflow-debug/SKILL.md +236 -0
- package/content/plugins/debugging/skills/agileflow-debug/references/common-patterns.md +350 -0
- package/content/plugins/debugging/skills/agileflow-debug/references/debugging-strategies.md +328 -0
- package/content/plugins/debugging/skills/agileflow-debug/workflows/debug-issue.md +187 -0
- package/content/plugins/debugging/skills/agileflow-debug/workflows/reproduce-bug.md +194 -0
- package/content/plugins/delivery/agents/ci.md +547 -0
- package/content/plugins/delivery/agents/devops.md +789 -0
- package/content/plugins/delivery/plugin.yaml +19 -0
- package/content/plugins/delivery/skills/agileflow-delivery/SKILL.md +111 -0
- package/content/plugins/delivery/skills/agileflow-delivery/references/changelog-format-guide.md +133 -0
- package/content/plugins/delivery/skills/agileflow-delivery/references/ci-pipeline-guide.md +158 -0
- package/content/plugins/delivery/skills/agileflow-delivery/references/pr-checklist-guide.md +133 -0
- package/content/plugins/delivery/skills/agileflow-delivery/references/release-checklist.md +142 -0
- package/content/plugins/delivery/skills/agileflow-delivery/workflows/changelog.md +72 -0
- package/content/plugins/delivery/skills/agileflow-delivery/workflows/deploy.md +74 -0
- package/content/plugins/delivery/skills/agileflow-delivery/workflows/pr.md +75 -0
- package/content/plugins/docs/agents/documentation.md +544 -0
- package/content/plugins/docs/agents/readme-updater.md +640 -0
- package/content/plugins/docs/plugin.yaml +19 -0
- package/content/plugins/docs/skills/agileflow-docs/SKILL.md +106 -0
- package/content/plugins/docs/skills/agileflow-docs/references/api-doc-template.md +167 -0
- package/content/plugins/docs/skills/agileflow-docs/references/doc-types-guide.md +141 -0
- package/content/plugins/docs/skills/agileflow-docs/references/readme-template.md +156 -0
- package/content/plugins/docs/skills/agileflow-docs/workflows/readme-sync.md +57 -0
- package/content/plugins/docs/skills/agileflow-docs/workflows/sync.md +64 -0
- package/content/plugins/engineering/agents/api.md +718 -0
- package/content/plugins/engineering/agents/codebase-query.md +285 -0
- package/content/plugins/engineering/agents/compliance.md +559 -0
- package/content/plugins/engineering/agents/database.md +644 -0
- package/content/plugins/engineering/agents/integrations.md +644 -0
- package/content/plugins/engineering/agents/mobile.md +552 -0
- package/content/plugins/engineering/agents/monitoring.md +585 -0
- package/content/plugins/engineering/agents/performance.md +529 -0
- package/content/plugins/engineering/agents/refactor.md +592 -0
- package/content/plugins/engineering/agents/security.md +524 -0
- package/content/plugins/engineering/agents/ui.md +1336 -0
- package/content/plugins/engineering/plugin.yaml +37 -0
- package/content/plugins/engineering/skills/agileflow-engineering/SKILL.md +127 -0
- package/content/plugins/engineering/skills/agileflow-engineering/references/code-review-guide.md +126 -0
- package/content/plugins/engineering/skills/agileflow-engineering/references/domain-routing-guide.md +89 -0
- package/content/plugins/engineering/skills/agileflow-engineering/references/refactoring-guide.md +136 -0
- package/content/plugins/engineering/skills/agileflow-engineering/workflows/diagnose.md +63 -0
- package/content/plugins/engineering/skills/agileflow-engineering/workflows/impact.md +60 -0
- package/content/plugins/ideation/agents/brainstorm-analyzer-features.md +179 -0
- package/content/plugins/ideation/agents/brainstorm-analyzer-growth.md +169 -0
- package/content/plugins/ideation/agents/brainstorm-analyzer-integration.md +181 -0
- package/content/plugins/ideation/agents/brainstorm-analyzer-market.md +150 -0
- package/content/plugins/ideation/agents/brainstorm-analyzer-ux.md +180 -0
- package/content/plugins/ideation/agents/brainstorm-consensus.md +245 -0
- package/content/plugins/ideation/agents/design.md +568 -0
- package/content/plugins/ideation/agents/product.md +582 -0
- package/content/plugins/ideation/plugin.yaml +31 -0
- package/content/plugins/ideation/skills/agileflow-ideation/SKILL.md +109 -0
- package/content/plugins/ideation/skills/agileflow-ideation/references/brainstorm-techniques.md +138 -0
- package/content/plugins/ideation/skills/agileflow-ideation/references/competitive-analysis-template.md +148 -0
- package/content/plugins/ideation/skills/agileflow-ideation/references/feature-prioritization-guide.md +147 -0
- package/content/plugins/ideation/skills/agileflow-ideation/references/user-story-patterns.md +152 -0
- package/content/plugins/ideation/skills/agileflow-ideation/workflows/features.md +65 -0
- package/content/plugins/ideation/skills/agileflow-ideation/workflows/ideate.md +54 -0
- package/content/plugins/migration/agents/datamigration.md +757 -0
- package/content/plugins/migration/plugin.yaml +17 -0
- package/content/plugins/migration/skills/agileflow-migration/SKILL.md +106 -0
- package/content/plugins/migration/skills/agileflow-migration/references/data-validation-checklist.md +154 -0
- package/content/plugins/migration/skills/agileflow-migration/references/migration-patterns.md +209 -0
- package/content/plugins/migration/skills/agileflow-migration/references/rollback-playbook.md +171 -0
- package/content/plugins/migration/skills/agileflow-migration/references/version-compatibility-matrix.md +155 -0
- package/content/plugins/migration/skills/agileflow-migration/workflows/plan.md +73 -0
- package/content/plugins/migration/skills/agileflow-migration/workflows/validate.md +71 -0
- package/content/plugins/performance/plugin.yaml +14 -0
- package/content/plugins/performance/skills/agileflow-performance/SKILL.md +224 -0
- package/content/plugins/performance/skills/agileflow-performance/references/optimization-patterns.md +554 -0
- package/content/plugins/performance/skills/agileflow-performance/references/profiling-guide.md +383 -0
- package/content/plugins/performance/skills/agileflow-performance/references/web-vitals-guide.md +360 -0
- package/content/plugins/performance/skills/agileflow-performance/workflows/improve-web-vitals.md +344 -0
- package/content/plugins/performance/skills/agileflow-performance/workflows/profile-and-fix.md +254 -0
- package/content/plugins/planning/agents/analytics.md +670 -0
- package/content/plugins/planning/agents/rlm-subcore.md +215 -0
- package/content/plugins/planning/plugin.yaml +19 -0
- package/content/plugins/planning/skills/agileflow-planning/SKILL.md +111 -0
- package/content/plugins/planning/skills/agileflow-planning/references/estimation-guide.md +114 -0
- package/content/plugins/planning/skills/agileflow-planning/references/rpi-workflow.md +119 -0
- package/content/plugins/planning/skills/agileflow-planning/references/sprint-planning-guide.md +145 -0
- package/content/plugins/planning/skills/agileflow-planning/workflows/impact.md +63 -0
- package/content/plugins/planning/skills/agileflow-planning/workflows/rpi.md +104 -0
- package/content/plugins/psychology/plugin.yaml +14 -0
- package/content/plugins/psychology/skills/agileflow-retention/SKILL.md +252 -0
- package/content/plugins/psychology/skills/agileflow-retention/references/competitor-analysis.md +240 -0
- package/content/plugins/psychology/skills/agileflow-retention/references/psychology-models.md +349 -0
- package/content/plugins/psychology/skills/agileflow-retention/references/retention-patterns.md +279 -0
- package/content/plugins/psychology/skills/agileflow-retention/workflows/design-retention-feature.md +287 -0
- package/content/plugins/psychology/skills/agileflow-retention/workflows/retention-audit.md +259 -0
- package/content/plugins/refactoring/plugin.yaml +14 -0
- package/content/plugins/refactoring/skills/agileflow-refactor/SKILL.md +235 -0
- package/content/plugins/refactoring/skills/agileflow-refactor/references/refactoring-patterns.md +405 -0
- package/content/plugins/refactoring/skills/agileflow-refactor/references/safety-checks.md +177 -0
- package/content/plugins/refactoring/skills/agileflow-refactor/workflows/extract-module.md +226 -0
- package/content/plugins/refactoring/skills/agileflow-refactor/workflows/safe-refactor.md +169 -0
- package/content/plugins/research/agents/research.md +503 -0
- package/content/plugins/research/plugin.yaml +17 -0
- package/content/plugins/research/skills/agileflow-research/SKILL.md +110 -0
- package/content/plugins/research/skills/agileflow-research/references/knowledge-decay-guide.md +121 -0
- package/content/plugins/research/skills/agileflow-research/references/research-prompt-guide.md +141 -0
- package/content/plugins/research/skills/agileflow-research/references/synthesis-template.md +154 -0
- package/content/plugins/research/skills/agileflow-research/workflows/analyze.md +60 -0
- package/content/plugins/research/skills/agileflow-research/workflows/ask.md +64 -0
- package/content/plugins/research/skills/agileflow-research/workflows/import.md +66 -0
- package/content/plugins/research/skills/agileflow-research/workflows/synthesize.md +66 -0
- package/content/plugins/reviews/plugin.yaml +14 -0
- package/content/plugins/reviews/skills/agileflow-pr-reviewer/SKILL.md +241 -0
- package/content/plugins/reviews/skills/agileflow-pr-reviewer/references/review-checklist.md +200 -0
- package/content/plugins/reviews/skills/agileflow-pr-reviewer/references/security-patterns.md +328 -0
- package/content/plugins/reviews/skills/agileflow-pr-reviewer/workflows/review-pr.md +153 -0
- package/content/plugins/reviews/skills/agileflow-pr-reviewer/workflows/security-review.md +177 -0
- package/content/plugins/seo/agents/seo-analyzer-content.md +169 -0
- package/content/plugins/seo/agents/seo-analyzer-images.md +198 -0
- package/content/plugins/seo/agents/seo-analyzer-performance.md +217 -0
- package/content/plugins/seo/agents/seo-analyzer-schema.md +184 -0
- package/content/plugins/seo/agents/seo-analyzer-sitemap.md +177 -0
- package/content/plugins/seo/agents/seo-analyzer-technical.md +151 -0
- package/content/plugins/seo/agents/seo-consensus.md +304 -0
- package/content/plugins/seo/plugin.yaml +19 -4
- package/content/plugins/seo/skills/agileflow-seo/SKILL.md +188 -0
- package/content/plugins/seo/skills/agileflow-seo/references/cwv-thresholds.md +110 -0
- package/content/plugins/seo/skills/agileflow-seo/references/eeat-framework.md +144 -0
- package/content/plugins/seo/skills/agileflow-seo/references/keyword-research-guide.md +125 -0
- package/content/plugins/seo/skills/agileflow-seo/references/schema-types.md +139 -0
- package/content/plugins/seo/skills/agileflow-seo/references/technical-seo-checklist.md +139 -0
- package/content/plugins/seo/skills/agileflow-seo/workflows/audit.md +98 -0
- package/content/plugins/seo/skills/agileflow-seo/workflows/page.md +118 -0
- package/content/plugins/testing/plugin.yaml +16 -0
- package/content/plugins/testing/skills/agileflow-test-writer/SKILL.md +260 -0
- package/content/plugins/testing/skills/agileflow-test-writer/references/coverage-targets.md +239 -0
- package/content/plugins/testing/skills/agileflow-test-writer/references/test-patterns.md +420 -0
- package/content/plugins/testing/skills/agileflow-test-writer/workflows/add-coverage.md +154 -0
- package/content/plugins/testing/skills/agileflow-test-writer/workflows/write-tests-from-ac.md +225 -0
- package/package.json +2 -2
- package/src/cli/commands/doctor.js +818 -30
- package/src/cli/commands/hook.js +17 -14
- package/src/cli/commands/launch.js +1454 -0
- package/src/cli/commands/learn.js +149 -0
- package/src/cli/commands/plugins.js +113 -0
- package/src/cli/commands/setup.js +455 -110
- package/src/cli/commands/skills.js +324 -0
- package/src/cli/commands/status.js +8 -10
- package/src/cli/commands/update.js +76 -15
- package/src/cli/index.js +90 -26
- package/src/cli/wizard/babysit-mode-picker.js +192 -0
- package/src/cli/wizard/behaviors-picker.js +208 -54
- package/src/cli/wizard/ide-picker.js +40 -28
- package/src/cli/wizard/install-scope-picker.js +57 -0
- package/src/cli/wizard/launch-alias-picker.js +50 -0
- package/src/cli/wizard/launch-cli-picker.js +129 -0
- package/src/cli/wizard/launch-tmux-picker.js +133 -0
- package/src/cli/wizard/learnings-picker.js +40 -0
- package/src/cli/wizard/plugin-picker.js +47 -16
- package/src/lib/brand.js +116 -0
- package/src/lib/errors.js +120 -0
- package/src/lib/path-check.js +39 -0
- package/src/runtime/config/defaults.js +22 -17
- package/src/runtime/config/loader.js +77 -8
- package/src/runtime/config/schema.json +43 -16
- package/src/runtime/config/writer.js +3 -1
- package/src/runtime/ide/babysit-skill.js +202 -0
- package/src/runtime/ide/capabilities.js +84 -29
- package/src/runtime/ide/claude-code-content.js +177 -0
- package/src/runtime/ide/claude-code-settings.js +67 -29
- package/src/runtime/ide/claude-code-skills.js +47 -32
- package/src/runtime/ide/codex-config.js +295 -0
- package/src/runtime/installer/install.js +252 -24
- package/src/runtime/launch/alias-installer.js +191 -0
- package/src/runtime/launch/cli-resume.js +244 -0
- package/src/runtime/launch/closed-windows.js +338 -0
- package/src/runtime/launch/defaults.js +66 -0
- package/src/runtime/launch/detect-clis.js +69 -0
- package/src/runtime/launch/doctor.js +464 -0
- package/src/runtime/launch/exec-wrapper.js +114 -0
- package/src/runtime/launch/parallel-session.js +247 -0
- package/src/runtime/launch/prefs.js +211 -0
- package/src/runtime/launch/project-prefs.js +234 -0
- package/src/runtime/launch/resolve-cli.js +56 -0
- package/src/runtime/launch/restore.js +152 -0
- package/src/runtime/launch/schema.json +75 -0
- package/src/runtime/launch/session-lifecycle.js +313 -0
- package/src/runtime/launch/session-registry.js +401 -0
- package/src/runtime/launch/spawn.js +103 -0
- package/src/runtime/launch/tabs.js +350 -0
- package/src/runtime/launch/tmux.js +764 -0
- package/src/runtime/launch/worktree.js +260 -0
- package/src/runtime/plugins/registry.js +16 -11
- package/src/runtime/plugins/validator.js +57 -43
- package/src/runtime/skills/learnings.js +308 -0
- package/content/plugins/core/hooks/babysit-mentor-injector.js +0 -55
- package/src/cli/wizard/personalization.js +0 -64
|
@@ -0,0 +1,1454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agileflow launch`.
|
|
3
|
+
*
|
|
4
|
+
* Two surfaces:
|
|
5
|
+
* - `agileflow launch setup` — always runs the prefs wizard.
|
|
6
|
+
* - `agileflow launch` — runs setup on first invocation
|
|
7
|
+
* (no prefs file); otherwise loads
|
|
8
|
+
* prefs, resolves the user's preferred
|
|
9
|
+
* AI CLI, and either wraps it in a
|
|
10
|
+
* per-cwd tmux session (if tmux is
|
|
11
|
+
* installed and enabled in prefs) or
|
|
12
|
+
* plain-spawns it as a foreground
|
|
13
|
+
* child.
|
|
14
|
+
*
|
|
15
|
+
* Errors here go through the typed-error / `fail()` plumbing in
|
|
16
|
+
* `src/lib/errors.js` so messages stay consistent with `setup` /
|
|
17
|
+
* `doctor` / etc.
|
|
18
|
+
*/
|
|
19
|
+
const fs = require("fs");
|
|
20
|
+
const path = require("path");
|
|
21
|
+
const prompts = require("@clack/prompts");
|
|
22
|
+
const pkg = require("../../../package.json");
|
|
23
|
+
const { logoBanner, questionMessage } = require("../../lib/brand.js");
|
|
24
|
+
const {
|
|
25
|
+
loadPrefs,
|
|
26
|
+
writePrefs,
|
|
27
|
+
prefsExist,
|
|
28
|
+
} = require("../../runtime/launch/prefs.js");
|
|
29
|
+
const { pickCli } = require("../wizard/launch-cli-picker.js");
|
|
30
|
+
const { pickTmux } = require("../wizard/launch-tmux-picker.js");
|
|
31
|
+
const { pickAliases } = require("../wizard/launch-alias-picker.js");
|
|
32
|
+
const { resolveCli } = require("../../runtime/launch/resolve-cli.js");
|
|
33
|
+
const { runCli } = require("../../runtime/launch/spawn.js");
|
|
34
|
+
const {
|
|
35
|
+
isInsideTmux,
|
|
36
|
+
tmuxAvailable,
|
|
37
|
+
launchInTmux,
|
|
38
|
+
listSessionsForCli,
|
|
39
|
+
killSession,
|
|
40
|
+
defaultRunner: defaultTmuxRunner,
|
|
41
|
+
} = require("../../runtime/launch/tmux.js");
|
|
42
|
+
const {
|
|
43
|
+
runParallelSpawn,
|
|
44
|
+
} = require("../../runtime/launch/parallel-session.js");
|
|
45
|
+
const { runExec } = require("../../runtime/launch/exec-wrapper.js");
|
|
46
|
+
const { runRestore } = require("../../runtime/launch/restore.js");
|
|
47
|
+
const {
|
|
48
|
+
loadRegistry,
|
|
49
|
+
pinSession,
|
|
50
|
+
} = require("../../runtime/launch/session-registry.js");
|
|
51
|
+
const {
|
|
52
|
+
listSessions,
|
|
53
|
+
killBySessionName,
|
|
54
|
+
attachByName,
|
|
55
|
+
pruneCandidates,
|
|
56
|
+
applyPrune,
|
|
57
|
+
} = require("../../runtime/launch/session-lifecycle.js");
|
|
58
|
+
const {
|
|
59
|
+
runDoctorChecks,
|
|
60
|
+
anyFailed,
|
|
61
|
+
} = require("../../runtime/launch/doctor.js");
|
|
62
|
+
const {
|
|
63
|
+
installAfAlias,
|
|
64
|
+
uninstallAfAlias,
|
|
65
|
+
resolveAgileflowBin,
|
|
66
|
+
} = require("../../runtime/launch/alias-installer.js");
|
|
67
|
+
const { loadCascadedPrefs } = require("../../runtime/launch/project-prefs.js");
|
|
68
|
+
const closedWindows = require("../../runtime/launch/closed-windows.js");
|
|
69
|
+
const {
|
|
70
|
+
AgileflowError,
|
|
71
|
+
OperationFailedError,
|
|
72
|
+
fail,
|
|
73
|
+
} = require("../../lib/errors.js");
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Pure helper for unit testing: decide which flow to enter given the
|
|
77
|
+
* subcommand name and whether a prefs file exists.
|
|
78
|
+
*
|
|
79
|
+
* @param {{ sub?: string, hasPrefs: boolean }} input
|
|
80
|
+
* @returns {'setup' | 'engine' | 'first-run-setup'}
|
|
81
|
+
*/
|
|
82
|
+
function decideFlow({ sub, hasPrefs }) {
|
|
83
|
+
if (sub === "setup") return "setup";
|
|
84
|
+
if (!hasPrefs) return "first-run-setup";
|
|
85
|
+
return "engine";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Load prefs, or `fail()` with the actionable hint that matches the
|
|
90
|
+
* inner-wizard error path. Without this, the bare-launch / first-run-tail
|
|
91
|
+
* call sites fall through to the catch-all `OperationFailedError` wrapper
|
|
92
|
+
* and the user gets a generic "re-run with DEBUG=1" message for what is
|
|
93
|
+
* really a "fix or delete the prefs file" situation.
|
|
94
|
+
*
|
|
95
|
+
* @returns {Promise<Awaited<ReturnType<typeof loadPrefs>>>}
|
|
96
|
+
*/
|
|
97
|
+
async function loadPrefsOrFail() {
|
|
98
|
+
try {
|
|
99
|
+
// Cascade: defaults ← global ~/.agileflow ← project .agileflow.
|
|
100
|
+
// Returns the same shape as loadPrefs() (prefs + source path) plus
|
|
101
|
+
// a `sources` audit trail consumed by `agileflow launch where`.
|
|
102
|
+
const cascaded = await loadCascadedPrefs();
|
|
103
|
+
// Preserve the loadPrefs() shape so existing callers that destructure
|
|
104
|
+
// { prefs } keep working. The extra `sources` field is silently
|
|
105
|
+
// ignored by destructures that don't ask for it.
|
|
106
|
+
return {
|
|
107
|
+
prefs: cascaded.prefs,
|
|
108
|
+
// `source` here reflects the HIGHEST-precedence layer that
|
|
109
|
+
// contributed, since downstream messages like "fix or delete the
|
|
110
|
+
// prefs file" need a single concrete path to point at. If the
|
|
111
|
+
// project file is present, that's the most-recently-edited file.
|
|
112
|
+
source: cascaded.sources.some((s) => s.layer === "project")
|
|
113
|
+
? "file"
|
|
114
|
+
: cascaded.sources.some((s) => s.layer === "global")
|
|
115
|
+
? "file"
|
|
116
|
+
: "defaults",
|
|
117
|
+
path:
|
|
118
|
+
(cascaded.sources.find((s) => s.layer === "project") || {}).path ||
|
|
119
|
+
(cascaded.sources.find((s) => s.layer === "global") || {}).path ||
|
|
120
|
+
"",
|
|
121
|
+
sources: cascaded.sources,
|
|
122
|
+
};
|
|
123
|
+
} catch (err) {
|
|
124
|
+
fail(
|
|
125
|
+
new OperationFailedError(err.message, {
|
|
126
|
+
suggestion:
|
|
127
|
+
"fix or delete the prefs file and re-run `agileflow launch setup`",
|
|
128
|
+
cause: err,
|
|
129
|
+
}),
|
|
130
|
+
{ command: "launch" },
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Pure helper: decide whether to offer orphan cleanup, given the prior
|
|
137
|
+
* preferred CLI, the new one, and whether tmux is available. Extracted
|
|
138
|
+
* so unit tests can cover the decision matrix without spinning up tmux.
|
|
139
|
+
*
|
|
140
|
+
* @param {{
|
|
141
|
+
* oldPreferred: string | undefined,
|
|
142
|
+
* newPreferred: string,
|
|
143
|
+
* tmuxAvailable: boolean,
|
|
144
|
+
* existingSource: 'file' | 'defaults',
|
|
145
|
+
* }} input
|
|
146
|
+
* @returns {boolean}
|
|
147
|
+
*/
|
|
148
|
+
function shouldOfferOrphanCleanup({
|
|
149
|
+
oldPreferred,
|
|
150
|
+
newPreferred,
|
|
151
|
+
tmuxAvailable: tmuxOk,
|
|
152
|
+
existingSource,
|
|
153
|
+
}) {
|
|
154
|
+
if (existingSource !== "file") return false; // first-time setup has no orphans
|
|
155
|
+
if (!tmuxOk) return false;
|
|
156
|
+
if (!oldPreferred || oldPreferred === newPreferred) return false;
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Run the interactive setup wizard. Used by both explicit `launch setup`
|
|
162
|
+
* and first-run from bare `launch`.
|
|
163
|
+
*
|
|
164
|
+
* Returns the prefs object that was just persisted so the first-run path
|
|
165
|
+
* can hand it straight to runEngine without re-reading from disk — which
|
|
166
|
+
* avoids surfacing a misleading "fix or delete the prefs file" hint in
|
|
167
|
+
* the rare case where the file becomes unreadable immediately after a
|
|
168
|
+
* successful write.
|
|
169
|
+
*
|
|
170
|
+
* @returns {Promise<import('../../runtime/launch/defaults.js').LaunchPrefs>}
|
|
171
|
+
*/
|
|
172
|
+
async function runSetup() {
|
|
173
|
+
// eslint-disable-next-line no-console
|
|
174
|
+
console.log("\n" + logoBanner(pkg.version) + "\n");
|
|
175
|
+
prompts.intro("agileflow launch — setup");
|
|
176
|
+
|
|
177
|
+
/** @type {Awaited<ReturnType<typeof loadPrefs>>} */
|
|
178
|
+
let existing;
|
|
179
|
+
try {
|
|
180
|
+
existing = await loadPrefs();
|
|
181
|
+
} catch (err) {
|
|
182
|
+
prompts.log.error(err.message);
|
|
183
|
+
prompts.cancel("Fix or delete the prefs file and re-run.");
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (existing.source === "file") {
|
|
188
|
+
prompts.log.info(
|
|
189
|
+
`Existing prefs at ${existing.path} — re-running wizard to update.`,
|
|
190
|
+
);
|
|
191
|
+
} else {
|
|
192
|
+
prompts.log.info("No prefs found — starting from defaults.");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const base = existing.prefs;
|
|
196
|
+
|
|
197
|
+
const cli = await pickCli(base);
|
|
198
|
+
const tmuxAndKeybinds = await pickTmux(base);
|
|
199
|
+
const aliases = await pickAliases(base);
|
|
200
|
+
|
|
201
|
+
const next = {
|
|
202
|
+
version: /** @type {1} */ (1),
|
|
203
|
+
cli,
|
|
204
|
+
tmux: tmuxAndKeybinds.tmux,
|
|
205
|
+
keybinds: tmuxAndKeybinds.keybinds,
|
|
206
|
+
aliases,
|
|
207
|
+
pinned: base.pinned,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Orphan cleanup: when the user switches their preferred CLI, the old
|
|
211
|
+
// `<old-cli>-<dir>` sessions stop being reachable through `launch` but
|
|
212
|
+
// keep occupying the tmux server. Offer to kill them while we have the
|
|
213
|
+
// user's attention — declining is fine, they can clean up manually later.
|
|
214
|
+
if (
|
|
215
|
+
shouldOfferOrphanCleanup({
|
|
216
|
+
oldPreferred: base.cli.preferred,
|
|
217
|
+
newPreferred: cli.preferred,
|
|
218
|
+
tmuxAvailable: tmuxAvailable(),
|
|
219
|
+
existingSource: existing.source,
|
|
220
|
+
})
|
|
221
|
+
) {
|
|
222
|
+
const runner = defaultTmuxRunner();
|
|
223
|
+
const orphans = listSessionsForCli(base.cli.preferred, runner);
|
|
224
|
+
if (orphans.length > 0) {
|
|
225
|
+
const choice = await prompts.confirm({
|
|
226
|
+
message: questionMessage(
|
|
227
|
+
`Kill ${orphans.length} stale tmux session(s) from your previous CLI (${base.cli.preferred})?`,
|
|
228
|
+
orphans.join(", "),
|
|
229
|
+
),
|
|
230
|
+
initialValue: true,
|
|
231
|
+
});
|
|
232
|
+
if (prompts.isCancel(choice)) {
|
|
233
|
+
prompts.cancel("Setup cancelled. No changes made.");
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
if (choice) {
|
|
237
|
+
let killed = 0;
|
|
238
|
+
let failed = 0;
|
|
239
|
+
for (const name of orphans) {
|
|
240
|
+
if (killSession(name, runner)) killed++;
|
|
241
|
+
else failed++;
|
|
242
|
+
}
|
|
243
|
+
if (killed > 0) {
|
|
244
|
+
prompts.log.info(`Killed ${killed} orphaned session(s).`);
|
|
245
|
+
}
|
|
246
|
+
if (failed > 0) {
|
|
247
|
+
prompts.log.warn(
|
|
248
|
+
`${failed} session(s) could not be killed — check \`tmux ls\`.`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Apply the alias side-effect BEFORE writing prefs. Writing first
|
|
256
|
+
// risks recording `aliases.af.enabled = true` to disk when the symlink
|
|
257
|
+
// failed to materialize — on next invocation we'd then lie about the
|
|
258
|
+
// alias being installed. Run the install/uninstall, reconcile the
|
|
259
|
+
// `next` object against what actually happened, then persist.
|
|
260
|
+
/** @type {string[]} */
|
|
261
|
+
const aliasSummary = [];
|
|
262
|
+
|
|
263
|
+
if (aliases.af.enabled) {
|
|
264
|
+
const aliasSpinner = prompts.spinner();
|
|
265
|
+
aliasSpinner.start("Installing `af` alias");
|
|
266
|
+
const result = await installAfAlias();
|
|
267
|
+
if (result.status === "installed" || result.status === "updated") {
|
|
268
|
+
aliasSpinner.stop(
|
|
269
|
+
`Alias ${result.status} → ${result.path} → ${result.target}`,
|
|
270
|
+
);
|
|
271
|
+
if (!result.onPath) {
|
|
272
|
+
prompts.log.warn(
|
|
273
|
+
`${path.dirname(result.path)} is not on your PATH. Add this to your shell rc:\n ` +
|
|
274
|
+
`export PATH="$HOME/.local/bin:$PATH"`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
aliasSummary.push(`af alias: ${result.status} (${result.path})`);
|
|
278
|
+
} else if (result.status === "unchanged") {
|
|
279
|
+
aliasSpinner.stop("Alias already in place");
|
|
280
|
+
aliasSummary.push(`af alias: unchanged (${result.path})`);
|
|
281
|
+
} else if (result.status === "unsupported") {
|
|
282
|
+
aliasSpinner.stop("Alias not auto-installable on this platform");
|
|
283
|
+
prompts.log.warn(result.message || "Unsupported platform");
|
|
284
|
+
aliasSummary.push("af alias: skipped (platform)");
|
|
285
|
+
// Reconcile: no symlink exists, so persist that truth.
|
|
286
|
+
next.aliases.af.enabled = false;
|
|
287
|
+
} else {
|
|
288
|
+
aliasSpinner.stop("Alias install failed");
|
|
289
|
+
prompts.log.warn(
|
|
290
|
+
`${result.message || "unknown failure"}\n ` +
|
|
291
|
+
"Re-run `agileflow launch setup` to retry, or create the symlink manually.",
|
|
292
|
+
);
|
|
293
|
+
aliasSummary.push("af alias: failed");
|
|
294
|
+
// Reconcile: install failed → no symlink exists.
|
|
295
|
+
next.aliases.af.enabled = false;
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
// User declined — clean up any previous symlink we installed.
|
|
299
|
+
const aliasSpinner = prompts.spinner();
|
|
300
|
+
aliasSpinner.start("Checking `af` alias");
|
|
301
|
+
const removal = await uninstallAfAlias();
|
|
302
|
+
if (removal.status === "removed") {
|
|
303
|
+
aliasSpinner.stop(`Alias removed → ${removal.path}`);
|
|
304
|
+
aliasSummary.push(`af alias: removed (${removal.path})`);
|
|
305
|
+
} else if (removal.status === "absent") {
|
|
306
|
+
aliasSpinner.stop("Alias not present");
|
|
307
|
+
aliasSummary.push("af alias: not present");
|
|
308
|
+
} else if (removal.status === "skipped") {
|
|
309
|
+
aliasSpinner.stop("Alias points elsewhere — left untouched");
|
|
310
|
+
prompts.log.warn(
|
|
311
|
+
removal.message ||
|
|
312
|
+
`${removal.path} points at a different binary; remove it manually to free the name.`,
|
|
313
|
+
);
|
|
314
|
+
aliasSummary.push("af alias: skipped (foreign symlink)");
|
|
315
|
+
} else if (removal.status === "failed") {
|
|
316
|
+
aliasSpinner.stop("Alias removal failed");
|
|
317
|
+
prompts.log.warn(
|
|
318
|
+
`Could not remove existing af alias at ${removal.path}: ${
|
|
319
|
+
removal.message || "unknown error"
|
|
320
|
+
}`,
|
|
321
|
+
);
|
|
322
|
+
aliasSummary.push(`af alias: removal failed (${removal.path})`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const writeSpinner = prompts.spinner();
|
|
327
|
+
writeSpinner.start("Writing launch-prefs.json");
|
|
328
|
+
/** @type {string} */
|
|
329
|
+
let file;
|
|
330
|
+
try {
|
|
331
|
+
file = await writePrefs(next);
|
|
332
|
+
} catch (err) {
|
|
333
|
+
writeSpinner.stop("Prefs write failed");
|
|
334
|
+
prompts.log.error(err.message);
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
writeSpinner.stop(`Prefs written → ${file}`);
|
|
338
|
+
|
|
339
|
+
/** @type {string[]} */
|
|
340
|
+
const summary = [
|
|
341
|
+
`preferred CLI: ${cli.preferred}`,
|
|
342
|
+
`fallback order: ${cli.fallbackOrder.join(" → ")}`,
|
|
343
|
+
tmuxAndKeybinds.tmux.enabled
|
|
344
|
+
? `tmux: on (status ${tmuxAndKeybinds.tmux.statusPosition}, keybinds ${tmuxAndKeybinds.keybinds.preset})`
|
|
345
|
+
: "tmux: off",
|
|
346
|
+
...aliasSummary,
|
|
347
|
+
];
|
|
348
|
+
|
|
349
|
+
prompts.outro(summary.join("\n"));
|
|
350
|
+
|
|
351
|
+
return next;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Resolve the AI CLI from prefs and launch it as a foreground child.
|
|
356
|
+
* Exits the parent with the child's exit code so shell pipelines see
|
|
357
|
+
* the right status.
|
|
358
|
+
*
|
|
359
|
+
* Slice 2b: when `prefs.tmux.enabled === true` AND tmux is on PATH AND
|
|
360
|
+
* we're not already inside a tmux client, wrap the CLI in a per-cwd
|
|
361
|
+
* tmux session via `launchInTmux`. Otherwise — including the
|
|
362
|
+
* "tmux=true but unavailable / nested" fallback paths — plain-spawn the
|
|
363
|
+
* CLI (slice 2a behavior).
|
|
364
|
+
*
|
|
365
|
+
* @param {import('../../runtime/launch/defaults.js').LaunchPrefs} prefs
|
|
366
|
+
* @returns {Promise<never>}
|
|
367
|
+
*/
|
|
368
|
+
async function runEngine(prefs) {
|
|
369
|
+
const { resolved, tried } = resolveCli(prefs);
|
|
370
|
+
if (!resolved) {
|
|
371
|
+
fail(
|
|
372
|
+
new OperationFailedError(
|
|
373
|
+
`no configured AI CLI is installed (tried ${tried.join(", ")})`,
|
|
374
|
+
{
|
|
375
|
+
suggestion:
|
|
376
|
+
"install one of the supported CLIs (claude, codex, cursor-agent, aider), " +
|
|
377
|
+
"or run `agileflow launch setup` to update your fallback order",
|
|
378
|
+
},
|
|
379
|
+
),
|
|
380
|
+
{ command: "launch" },
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (prefs.tmux.enabled) {
|
|
385
|
+
if (isInsideTmux()) {
|
|
386
|
+
// Nesting tmux sessions is allowed but confusing — and the v3 `af`
|
|
387
|
+
// explicitly avoided it. Plain-spawn in the current pane.
|
|
388
|
+
// eslint-disable-next-line no-console
|
|
389
|
+
console.error(
|
|
390
|
+
`agileflow launch: already inside a tmux session — launching ${resolved.bin} in the current pane.`,
|
|
391
|
+
);
|
|
392
|
+
} else if (!tmuxAvailable()) {
|
|
393
|
+
// eslint-disable-next-line no-console
|
|
394
|
+
console.error(
|
|
395
|
+
`agileflow launch: tmux is not installed — launching ${resolved.bin} directly. Install tmux to enable session management.`,
|
|
396
|
+
);
|
|
397
|
+
} else {
|
|
398
|
+
try {
|
|
399
|
+
const result = await launchInTmux({
|
|
400
|
+
bin: resolved.bin,
|
|
401
|
+
args: [],
|
|
402
|
+
statusPosition: prefs.tmux.statusPosition,
|
|
403
|
+
keybindPreset: prefs.keybinds.preset,
|
|
404
|
+
});
|
|
405
|
+
process.exit(result.exitCode);
|
|
406
|
+
} catch (err) {
|
|
407
|
+
fail(
|
|
408
|
+
new OperationFailedError(
|
|
409
|
+
`tmux launch failed: ${err && err.message ? err.message : String(err)}`,
|
|
410
|
+
{
|
|
411
|
+
suggestion:
|
|
412
|
+
"verify tmux works (`tmux new-session -d -s test && tmux kill-session -t test`), " +
|
|
413
|
+
"or disable tmux via `agileflow launch setup`",
|
|
414
|
+
cause: err,
|
|
415
|
+
},
|
|
416
|
+
),
|
|
417
|
+
{ command: "launch" },
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const result = await runCli(resolved.bin, []);
|
|
424
|
+
process.exit(result.exitCode);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* `agileflow launch new [name]` — spawn a parallel session (same-dir or
|
|
429
|
+
* worktree-backed) and switch the user's tmux client to it. Bound by
|
|
430
|
+
* default to Alt+s (no name) and Alt+n (prompts for a name) via the
|
|
431
|
+
* default keybind preset.
|
|
432
|
+
*
|
|
433
|
+
* Pre-conditions:
|
|
434
|
+
* - prefs file must exist (no auto-setup here — `new` only makes sense
|
|
435
|
+
* after the user has already configured launch)
|
|
436
|
+
* - we must be inside a tmux client (switch-client needs a current
|
|
437
|
+
* client; outside tmux there is no session to swap from)
|
|
438
|
+
* - the user's preferred CLI must be installed
|
|
439
|
+
* - tmux must be on PATH (it's a prerequisite for being "inside tmux"
|
|
440
|
+
* so this should always hold, but we guard defensively)
|
|
441
|
+
*
|
|
442
|
+
* Returns void on success (user is now inside the new session via
|
|
443
|
+
* switch-client; the parent process exits cleanly with status 0). All
|
|
444
|
+
* failure paths call `fail()` which calls `process.exit(1)`.
|
|
445
|
+
*
|
|
446
|
+
* @param {string | undefined} name - worktree name; omit for same-dir
|
|
447
|
+
* @returns {Promise<void>}
|
|
448
|
+
*/
|
|
449
|
+
async function runNew(name) {
|
|
450
|
+
if (!(await prefsExist())) {
|
|
451
|
+
fail(
|
|
452
|
+
new OperationFailedError("agileflow launch new requires prefs first", {
|
|
453
|
+
suggestion: "run `agileflow launch setup` to create launch-prefs.json",
|
|
454
|
+
}),
|
|
455
|
+
{ command: "launch" },
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (!isInsideTmux()) {
|
|
460
|
+
fail(
|
|
461
|
+
new OperationFailedError(
|
|
462
|
+
"agileflow launch new only works inside an existing tmux session",
|
|
463
|
+
{
|
|
464
|
+
suggestion:
|
|
465
|
+
"run `agileflow launch` first to start a session, then use Alt+s / Alt+n inside it",
|
|
466
|
+
},
|
|
467
|
+
),
|
|
468
|
+
{ command: "launch" },
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (!tmuxAvailable()) {
|
|
473
|
+
fail(
|
|
474
|
+
new OperationFailedError(
|
|
475
|
+
"tmux is not available — required for `launch new`",
|
|
476
|
+
{ suggestion: "install tmux and try again" },
|
|
477
|
+
),
|
|
478
|
+
{ command: "launch" },
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const { prefs } = await loadPrefsOrFail();
|
|
483
|
+
const { resolved, tried } = resolveCli(prefs);
|
|
484
|
+
if (!resolved) {
|
|
485
|
+
fail(
|
|
486
|
+
new OperationFailedError(
|
|
487
|
+
`no configured AI CLI is installed (tried ${tried.join(", ")})`,
|
|
488
|
+
{
|
|
489
|
+
suggestion:
|
|
490
|
+
"install one of the supported CLIs (claude, codex, cursor-agent, aider), " +
|
|
491
|
+
"or run `agileflow launch setup` to update your fallback order",
|
|
492
|
+
},
|
|
493
|
+
),
|
|
494
|
+
{ command: "launch" },
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
await runParallelSpawn({
|
|
500
|
+
bin: resolved.bin,
|
|
501
|
+
name,
|
|
502
|
+
prefs,
|
|
503
|
+
});
|
|
504
|
+
} catch (err) {
|
|
505
|
+
const code = err && err.code;
|
|
506
|
+
let suggestion = "re-run with DEBUG=1 for a stack trace";
|
|
507
|
+
if (code === "EWT_DIR_EXISTS") {
|
|
508
|
+
suggestion =
|
|
509
|
+
"remove the existing worktree directory or pick a different name";
|
|
510
|
+
} else if (code === "EWT_BRANCH_EXISTS") {
|
|
511
|
+
suggestion = "the branch already exists — pick a different name";
|
|
512
|
+
} else if (code === "EWT_NOT_REPO") {
|
|
513
|
+
suggestion =
|
|
514
|
+
"run from inside a git repository, or omit the name for a same-dir session";
|
|
515
|
+
} else if (code === "EWT_NO_HEAD") {
|
|
516
|
+
suggestion =
|
|
517
|
+
"check out a branch first (HEAD is detached), or pass an explicit base via prefs";
|
|
518
|
+
} else if (code === "EWT_BAD_NAME") {
|
|
519
|
+
suggestion =
|
|
520
|
+
"pick a name with letters, digits, dot, underscore, or hyphen";
|
|
521
|
+
} else if (code === "EWT_CREATE") {
|
|
522
|
+
suggestion =
|
|
523
|
+
"git worktree add failed; inspect the repo state and try again, or omit the name for a same-dir session";
|
|
524
|
+
} else if (code === "ETMUX_CREATE") {
|
|
525
|
+
suggestion =
|
|
526
|
+
"tmux session creation failed; verify tmux works (`tmux new-session -d -s test && tmux kill-session -t test`)";
|
|
527
|
+
} else if (code === "ETMUX_SWITCH") {
|
|
528
|
+
// The session is alive but switch-client didn't take. Tell the user
|
|
529
|
+
// exactly how to attach to it manually.
|
|
530
|
+
suggestion =
|
|
531
|
+
"switch-client failed but the new session is still running — attach with `tmux attach -t <session-name>` (see error message above for the name)";
|
|
532
|
+
}
|
|
533
|
+
fail(
|
|
534
|
+
new OperationFailedError(
|
|
535
|
+
`launch new failed: ${err && err.message ? err.message : String(err)}`,
|
|
536
|
+
{ suggestion, cause: err },
|
|
537
|
+
),
|
|
538
|
+
{ command: "launch" },
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
// runParallelSpawn returns normally after switch-client. We don't
|
|
542
|
+
// process.exit — the user is now inside the new session and this
|
|
543
|
+
// invocation finishes cleanly with exit code 0.
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* `agileflow launch restore` — bulk-restore every session in the
|
|
548
|
+
* registry that isn't currently alive on the tmux server. Used after a
|
|
549
|
+
* reboot (or `tmux kill-server`) to bring back the tabs the user had
|
|
550
|
+
* before. Idempotent.
|
|
551
|
+
*
|
|
552
|
+
* @returns {Promise<void>}
|
|
553
|
+
*/
|
|
554
|
+
async function runRestoreCommand() {
|
|
555
|
+
if (!(await prefsExist())) {
|
|
556
|
+
fail(
|
|
557
|
+
new OperationFailedError(
|
|
558
|
+
"agileflow launch restore requires prefs first",
|
|
559
|
+
{
|
|
560
|
+
suggestion:
|
|
561
|
+
"run `agileflow launch setup` to create launch-prefs.json",
|
|
562
|
+
},
|
|
563
|
+
),
|
|
564
|
+
{ command: "launch" },
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
if (!tmuxAvailable()) {
|
|
568
|
+
fail(
|
|
569
|
+
new OperationFailedError(
|
|
570
|
+
"tmux is not available — required for `launch restore`",
|
|
571
|
+
{ suggestion: "install tmux and try again" },
|
|
572
|
+
),
|
|
573
|
+
{ command: "launch" },
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
const { prefs } = await loadPrefsOrFail();
|
|
577
|
+
const reg = loadRegistry();
|
|
578
|
+
if (reg.sessions.length === 0) {
|
|
579
|
+
// eslint-disable-next-line no-console
|
|
580
|
+
console.error(
|
|
581
|
+
"agileflow launch: no saved sessions to restore (registry is empty).",
|
|
582
|
+
);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const result = runRestore({ prefs });
|
|
587
|
+
// eslint-disable-next-line no-console
|
|
588
|
+
console.error(
|
|
589
|
+
`agileflow launch: restored ${result.restored}, already-alive ${result.alreadyAlive}, ` +
|
|
590
|
+
`skipped ${result.skipped}, failed ${result.failed}`,
|
|
591
|
+
);
|
|
592
|
+
if (result.failed > 0 || result.skipped > 0) {
|
|
593
|
+
for (const note of result.notes) {
|
|
594
|
+
// eslint-disable-next-line no-console
|
|
595
|
+
console.error(` ${note.name}: ${note.reason}`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* `agileflow launch ls` — print a one-line-per-session table of every
|
|
602
|
+
* known session with its current state. Read-only; no prefs required.
|
|
603
|
+
*
|
|
604
|
+
* @returns {Promise<void>}
|
|
605
|
+
*/
|
|
606
|
+
async function runLs() {
|
|
607
|
+
const rows = listSessions();
|
|
608
|
+
if (rows.length === 0) {
|
|
609
|
+
// eslint-disable-next-line no-console
|
|
610
|
+
console.log("No saved sessions. Run `agileflow launch` to create one.");
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
// Pinned entries float to the top so the user sees their "always keep"
|
|
614
|
+
// sessions first. Within each group, original registry order is
|
|
615
|
+
// preserved (which is roughly creation order).
|
|
616
|
+
rows.sort(
|
|
617
|
+
(a, b) => (b.pinned === true ? 1 : 0) - (a.pinned === true ? 1 : 0),
|
|
618
|
+
);
|
|
619
|
+
// Column widths sized to the longest value, capped to keep wide cwds
|
|
620
|
+
// from blowing past the terminal. Leading column reserves a glyph for
|
|
621
|
+
// the pin marker so pinned/unpinned rows align.
|
|
622
|
+
const nameW = Math.max(4, ...rows.map((r) => r.name.length));
|
|
623
|
+
const cliW = Math.max(3, ...rows.map((r) => r.cli.length));
|
|
624
|
+
const stateW = "missing-cwd".length;
|
|
625
|
+
const pinMark = (r) => (r.pinned ? "★" : " ");
|
|
626
|
+
const fmt = (r) =>
|
|
627
|
+
`${pinMark(r)} ${r.name.padEnd(nameW)} ${r.cli.padEnd(cliW)} ${r.state.padEnd(stateW)} ${r.cwd}${
|
|
628
|
+
r.worktree && r.worktree.branch ? ` [wt ${r.worktree.branch}]` : ""
|
|
629
|
+
}`;
|
|
630
|
+
// eslint-disable-next-line no-console
|
|
631
|
+
console.log(
|
|
632
|
+
` ${"NAME".padEnd(nameW)} ${"CLI".padEnd(cliW)} ${"STATE".padEnd(stateW)} CWD`,
|
|
633
|
+
);
|
|
634
|
+
for (const r of rows) {
|
|
635
|
+
// eslint-disable-next-line no-console
|
|
636
|
+
console.log(fmt(r));
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* `agileflow launch kill <name>` — kill the tmux session if alive,
|
|
642
|
+
* forget the registry entry, and optionally remove its git worktree.
|
|
643
|
+
*
|
|
644
|
+
* @param {string | undefined} name
|
|
645
|
+
* @returns {Promise<void>}
|
|
646
|
+
*/
|
|
647
|
+
async function runKill(name) {
|
|
648
|
+
if (!name) {
|
|
649
|
+
fail(
|
|
650
|
+
new OperationFailedError(
|
|
651
|
+
"agileflow launch kill requires a session name",
|
|
652
|
+
{
|
|
653
|
+
suggestion: "run `agileflow launch ls` to see available names",
|
|
654
|
+
},
|
|
655
|
+
),
|
|
656
|
+
{ command: "launch" },
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
const reg = loadRegistry();
|
|
660
|
+
const entry = reg.sessions.find((s) => s.name === name);
|
|
661
|
+
if (!entry) {
|
|
662
|
+
fail(
|
|
663
|
+
new OperationFailedError(`no session named "${name}" in the registry`, {
|
|
664
|
+
suggestion: "run `agileflow launch ls` to see available names",
|
|
665
|
+
}),
|
|
666
|
+
{ command: "launch" },
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
// Surface the worktree question only when there's something to remove —
|
|
670
|
+
// missing worktree dirs don't need a prompt, just forget the entry.
|
|
671
|
+
let removeWorktreeFlag = false;
|
|
672
|
+
if (
|
|
673
|
+
entry.worktree &&
|
|
674
|
+
entry.worktree.path &&
|
|
675
|
+
fs.existsSync(entry.worktree.path)
|
|
676
|
+
) {
|
|
677
|
+
const choice = await prompts.confirm({
|
|
678
|
+
message: questionMessage(
|
|
679
|
+
`Also remove the worktree at ${entry.worktree.path}?`,
|
|
680
|
+
`branch: ${entry.worktree.branch} — this runs \`git worktree remove -f\` and \`git branch -D\`.`,
|
|
681
|
+
),
|
|
682
|
+
initialValue: true,
|
|
683
|
+
});
|
|
684
|
+
if (prompts.isCancel(choice)) {
|
|
685
|
+
prompts.cancel("Kill cancelled.");
|
|
686
|
+
process.exit(0);
|
|
687
|
+
}
|
|
688
|
+
removeWorktreeFlag = !!choice;
|
|
689
|
+
}
|
|
690
|
+
const result = killBySessionName({
|
|
691
|
+
name,
|
|
692
|
+
removeWorktree: removeWorktreeFlag,
|
|
693
|
+
});
|
|
694
|
+
if (!result.ok) {
|
|
695
|
+
fail(
|
|
696
|
+
new OperationFailedError(
|
|
697
|
+
`could not kill "${name}": ${result.reason || "unknown reason"}`,
|
|
698
|
+
{ suggestion: "run `agileflow launch ls` to confirm the name" },
|
|
699
|
+
),
|
|
700
|
+
{ command: "launch" },
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
/** @type {string[]} */
|
|
704
|
+
const summary = [];
|
|
705
|
+
summary.push(
|
|
706
|
+
result.wasAlive
|
|
707
|
+
? `Killed tmux session "${name}" and forgot it.`
|
|
708
|
+
: `Forgot dormant session "${name}".`,
|
|
709
|
+
);
|
|
710
|
+
if (result.worktree) {
|
|
711
|
+
if (result.worktree.removed && result.worktree.branchRemoved) {
|
|
712
|
+
summary.push(`Removed worktree + branch.`);
|
|
713
|
+
} else if (result.worktree.removed) {
|
|
714
|
+
summary.push(`Removed worktree (branch removal failed).`);
|
|
715
|
+
} else {
|
|
716
|
+
summary.push(`Worktree removal failed: ${result.worktree.stderr}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
// eslint-disable-next-line no-console
|
|
720
|
+
console.log(summary.join("\n"));
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* `agileflow launch attach <name>` — attach to a named session, lazily
|
|
725
|
+
* restoring it from the registry if the tmux server doesn't have it.
|
|
726
|
+
*
|
|
727
|
+
* @param {string | undefined} name
|
|
728
|
+
* @returns {Promise<void>}
|
|
729
|
+
*/
|
|
730
|
+
async function runAttachByName(name) {
|
|
731
|
+
if (!name) {
|
|
732
|
+
fail(
|
|
733
|
+
new OperationFailedError(
|
|
734
|
+
"agileflow launch attach requires a session name",
|
|
735
|
+
{
|
|
736
|
+
suggestion: "run `agileflow launch ls` to see available names",
|
|
737
|
+
},
|
|
738
|
+
),
|
|
739
|
+
{ command: "launch" },
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
if (!tmuxAvailable()) {
|
|
743
|
+
fail(
|
|
744
|
+
new OperationFailedError(
|
|
745
|
+
"tmux is not available — required for `launch attach`",
|
|
746
|
+
{ suggestion: "install tmux and try again" },
|
|
747
|
+
),
|
|
748
|
+
{ command: "launch" },
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
const { prefs } = await loadPrefsOrFail();
|
|
752
|
+
const result = await attachByName({
|
|
753
|
+
name,
|
|
754
|
+
prefs,
|
|
755
|
+
agileflowBin: resolveAgileflowBin(),
|
|
756
|
+
});
|
|
757
|
+
if (!result.ok) {
|
|
758
|
+
/** @type {string} */
|
|
759
|
+
let suggestion;
|
|
760
|
+
if (result.reason === "not in registry") {
|
|
761
|
+
suggestion = "run `agileflow launch ls` to see available names";
|
|
762
|
+
} else if (result.reason === "cwd missing") {
|
|
763
|
+
suggestion = `the original directory has been deleted — run \`agileflow launch kill ${name}\` to forget it`;
|
|
764
|
+
} else {
|
|
765
|
+
suggestion = "check tmux state and the registry file";
|
|
766
|
+
}
|
|
767
|
+
fail(
|
|
768
|
+
new OperationFailedError(
|
|
769
|
+
`could not attach "${name}": ${result.reason || "unknown reason"}`,
|
|
770
|
+
{ suggestion },
|
|
771
|
+
),
|
|
772
|
+
{ command: "launch" },
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
// Always exit with the attach's exit code so shell pipelines see the
|
|
776
|
+
// right status. Defensive default of 0 covers the unlikely case where
|
|
777
|
+
// the attach result doesn't carry a numeric exitCode — better to exit
|
|
778
|
+
// cleanly than leave the parent process hung in the terminal.
|
|
779
|
+
const exitCode =
|
|
780
|
+
result.attach &&
|
|
781
|
+
typeof result.attach === "object" &&
|
|
782
|
+
typeof result.attach.exitCode === "number"
|
|
783
|
+
? result.attach.exitCode
|
|
784
|
+
: 0;
|
|
785
|
+
process.exit(exitCode);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* `agileflow launch prune` — interactive cleanup of dormant entries
|
|
790
|
+
* whose original directory has been deleted, or whose worktree path no
|
|
791
|
+
* longer exists.
|
|
792
|
+
*
|
|
793
|
+
* @returns {Promise<void>}
|
|
794
|
+
*/
|
|
795
|
+
async function runPrune() {
|
|
796
|
+
const candidates = pruneCandidates();
|
|
797
|
+
if (candidates.length === 0) {
|
|
798
|
+
// eslint-disable-next-line no-console
|
|
799
|
+
console.log(
|
|
800
|
+
"Nothing to prune — every registered session still has a live cwd.",
|
|
801
|
+
);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const options = candidates.map((c) => ({
|
|
805
|
+
value: c.name,
|
|
806
|
+
label: c.name,
|
|
807
|
+
hint: `${c.cli} — ${c.reason}`,
|
|
808
|
+
}));
|
|
809
|
+
const selection = await prompts.multiselect({
|
|
810
|
+
message: questionMessage(
|
|
811
|
+
`Select session(s) to forget (${candidates.length} candidate(s))`,
|
|
812
|
+
"Forgetting drops the registry entry; worktree dirs are only removed if they still exist.",
|
|
813
|
+
),
|
|
814
|
+
options,
|
|
815
|
+
initialValues: options.map((o) => o.value),
|
|
816
|
+
required: false,
|
|
817
|
+
});
|
|
818
|
+
if (prompts.isCancel(selection)) {
|
|
819
|
+
prompts.cancel("Prune cancelled.");
|
|
820
|
+
process.exit(0);
|
|
821
|
+
}
|
|
822
|
+
if (!Array.isArray(selection) || selection.length === 0) {
|
|
823
|
+
// eslint-disable-next-line no-console
|
|
824
|
+
console.log("No sessions selected. Nothing to do.");
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
const result = applyPrune({
|
|
828
|
+
selections: selection.map((name) => ({ name })),
|
|
829
|
+
// Worktree removal is skipped here: the candidates that surface ARE
|
|
830
|
+
// the ones whose dirs are already missing OR whose cwd is missing.
|
|
831
|
+
// For "dir missing" entries there's no worktree to remove; for
|
|
832
|
+
// "cwd missing" the worktree path may still exist as a stranded
|
|
833
|
+
// directory. We surface it but don't auto-rm to avoid surprising
|
|
834
|
+
// users — they can `launch kill <name>` for targeted removal.
|
|
835
|
+
removeWorktrees: false,
|
|
836
|
+
});
|
|
837
|
+
// eslint-disable-next-line no-console
|
|
838
|
+
console.log(
|
|
839
|
+
`Forgot ${result.forgotten} session(s), removed ${result.worktreesRemoved} worktree(s).`,
|
|
840
|
+
);
|
|
841
|
+
if (result.errors.length > 0) {
|
|
842
|
+
for (const e of result.errors) {
|
|
843
|
+
// eslint-disable-next-line no-console
|
|
844
|
+
console.error(` ${e.name}: ${e.error}`);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* `agileflow launch doctor` — read-only health check. Exits 1 if any
|
|
851
|
+
* check fails (tmux missing, preferred CLI missing, registry malformed).
|
|
852
|
+
* Warnings don't fail the doctor.
|
|
853
|
+
*
|
|
854
|
+
* @returns {Promise<void>}
|
|
855
|
+
*/
|
|
856
|
+
async function runDoctor() {
|
|
857
|
+
const report = await runDoctorChecks();
|
|
858
|
+
for (const c of report.checks) {
|
|
859
|
+
const symbol = c.status === "pass" ? "✓" : c.status === "warn" ? "⚠" : "✗";
|
|
860
|
+
// eslint-disable-next-line no-console
|
|
861
|
+
console.log(`${symbol} ${c.id}: ${c.message}`);
|
|
862
|
+
if (c.fix) {
|
|
863
|
+
// eslint-disable-next-line no-console
|
|
864
|
+
console.log(` fix: ${c.fix}`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
if (anyFailed(report)) {
|
|
868
|
+
process.exit(1);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* `agileflow launch pin <name>` / `unpin <name>` — flip the pinned flag
|
|
874
|
+
* on a registry entry. Pinned entries skip `prune` and arrive pre-
|
|
875
|
+
* selected in the auto-restore picker.
|
|
876
|
+
*
|
|
877
|
+
* @param {string | undefined} name
|
|
878
|
+
* @param {boolean} pinned
|
|
879
|
+
* @returns {Promise<void>}
|
|
880
|
+
*/
|
|
881
|
+
async function runPin(name, pinned) {
|
|
882
|
+
const action = pinned ? "pin" : "unpin";
|
|
883
|
+
if (!name) {
|
|
884
|
+
fail(
|
|
885
|
+
new OperationFailedError(
|
|
886
|
+
`agileflow launch ${action} requires a session name`,
|
|
887
|
+
{ suggestion: "run `agileflow launch ls` to see available names" },
|
|
888
|
+
),
|
|
889
|
+
{ command: "launch" },
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
const ok = pinSession(name, pinned);
|
|
893
|
+
if (!ok) {
|
|
894
|
+
fail(
|
|
895
|
+
new OperationFailedError(`no session named "${name}" in the registry`, {
|
|
896
|
+
suggestion: "run `agileflow launch ls` to see available names",
|
|
897
|
+
}),
|
|
898
|
+
{ command: "launch" },
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
// eslint-disable-next-line no-console
|
|
902
|
+
console.log(`${pinned ? "Pinned" : "Unpinned"} "${name}".`);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* `agileflow launch where` — show which prefs files contributed to the
|
|
907
|
+
* effective config in this directory. Useful for debugging "why is this
|
|
908
|
+
* repo picking codex when my global says claude?" surprises.
|
|
909
|
+
*
|
|
910
|
+
* @returns {Promise<void>}
|
|
911
|
+
*/
|
|
912
|
+
async function runWhere() {
|
|
913
|
+
const cascaded = await loadCascadedPrefs();
|
|
914
|
+
// eslint-disable-next-line no-console
|
|
915
|
+
console.log(
|
|
916
|
+
"Active launch prefs in this directory (lowest → highest precedence):",
|
|
917
|
+
);
|
|
918
|
+
for (const s of cascaded.sources) {
|
|
919
|
+
const label =
|
|
920
|
+
s.layer === "defaults"
|
|
921
|
+
? "built-in defaults"
|
|
922
|
+
: s.layer === "global"
|
|
923
|
+
? `global ${s.path}`
|
|
924
|
+
: `project ${s.path}`;
|
|
925
|
+
// eslint-disable-next-line no-console
|
|
926
|
+
console.log(` ${label}`);
|
|
927
|
+
}
|
|
928
|
+
// eslint-disable-next-line no-console
|
|
929
|
+
console.log("");
|
|
930
|
+
// eslint-disable-next-line no-console
|
|
931
|
+
console.log("Effective values:");
|
|
932
|
+
// eslint-disable-next-line no-console
|
|
933
|
+
console.log(` cli.preferred ${cascaded.prefs.cli.preferred}`);
|
|
934
|
+
// eslint-disable-next-line no-console
|
|
935
|
+
console.log(
|
|
936
|
+
` cli.fallbackOrder ${cascaded.prefs.cli.fallbackOrder.join(" → ")}`,
|
|
937
|
+
);
|
|
938
|
+
// eslint-disable-next-line no-console
|
|
939
|
+
console.log(
|
|
940
|
+
` tmux ${cascaded.prefs.tmux.enabled ? "on" : "off"} (status ${cascaded.prefs.tmux.statusPosition})`,
|
|
941
|
+
);
|
|
942
|
+
// eslint-disable-next-line no-console
|
|
943
|
+
console.log(` keybinds ${cascaded.prefs.keybinds.preset}`);
|
|
944
|
+
// eslint-disable-next-line no-console
|
|
945
|
+
console.log(
|
|
946
|
+
` af alias ${cascaded.prefs.aliases.af.enabled ? "on" : "off"}`,
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Hidden subcommand wired to the `Alt+w` tab close keybind. Captures
|
|
952
|
+
* the current tmux window's name + pane cwd via `tmux display-message`,
|
|
953
|
+
* pushes onto the closed-windows log, then `kill-window`s. Designed to
|
|
954
|
+
* be called from a tmux `run-shell` action — no UI, no exceptions
|
|
955
|
+
* propagated to the user (failures only log to stderr so they appear in
|
|
956
|
+
* the tmux pane error overlay if the user surfaces it).
|
|
957
|
+
*
|
|
958
|
+
* Ordering: kill-window FIRST (with an explicit `-t session:index`
|
|
959
|
+
* target captured atomically from display-message), then push to the
|
|
960
|
+
* log only on kill success. This is the inverse of the obvious order
|
|
961
|
+
* but it avoids two real bugs caught in pre-commit audit:
|
|
962
|
+
* (a) Phantom log entry — if kill fails (permission, race) we'd
|
|
963
|
+
* otherwise leave a "closed" record pointing at a still-alive
|
|
964
|
+
* window, and Alt+T would duplicate it.
|
|
965
|
+
* (b) Wrong-window kill — a bare `kill-window` with no target acts
|
|
966
|
+
* on whatever window has focus at that instant, which can drift
|
|
967
|
+
* between display-message and the kill if the user switches
|
|
968
|
+
* windows in another pane. Explicit `-t` pins it.
|
|
969
|
+
*
|
|
970
|
+
* Trade-off: if push fails (lock contention etc.) the window is gone
|
|
971
|
+
* but Alt+T can't undo it — acceptable, since the inverse failure
|
|
972
|
+
* (logged but alive) is worse.
|
|
973
|
+
*
|
|
974
|
+
* @param {{
|
|
975
|
+
* runner?: ReturnType<typeof defaultTmuxRunner>,
|
|
976
|
+
* pushClosedImpl?: typeof closedWindows.pushClosed,
|
|
977
|
+
* }} [deps]
|
|
978
|
+
* @returns {Promise<void>}
|
|
979
|
+
*/
|
|
980
|
+
async function runInternalCloseWindow(deps = {}) {
|
|
981
|
+
const runner = deps.runner || defaultTmuxRunner();
|
|
982
|
+
const pushClosedImpl = deps.pushClosedImpl || closedWindows.pushClosed;
|
|
983
|
+
const exit = deps.exit || ((code) => process.exit(code));
|
|
984
|
+
// ASCII Unit Separator — never appears in a session/window name or
|
|
985
|
+
// filesystem path, so splitting on it is unambiguous.
|
|
986
|
+
const DELIM = "\x1f";
|
|
987
|
+
// When the tmux keybind passes session+index positionally, target
|
|
988
|
+
// that exact window. This avoids a wrong-window kill if focus shifts
|
|
989
|
+
// between Alt+w being pressed and this subprocess starting.
|
|
990
|
+
const argSession = (deps.targetSession || "").trim();
|
|
991
|
+
const argIndex = (deps.targetIndex || "").trim();
|
|
992
|
+
let probeArgs;
|
|
993
|
+
if (argSession && argIndex) {
|
|
994
|
+
probeArgs = [
|
|
995
|
+
"display-message",
|
|
996
|
+
"-p",
|
|
997
|
+
"-t",
|
|
998
|
+
`${argSession}:${argIndex}`,
|
|
999
|
+
"-F",
|
|
1000
|
+
`#S${DELIM}#I${DELIM}#W${DELIM}#{pane_current_path}`,
|
|
1001
|
+
];
|
|
1002
|
+
} else {
|
|
1003
|
+
probeArgs = [
|
|
1004
|
+
"display-message",
|
|
1005
|
+
"-p",
|
|
1006
|
+
"-F",
|
|
1007
|
+
`#S${DELIM}#I${DELIM}#W${DELIM}#{pane_current_path}`,
|
|
1008
|
+
];
|
|
1009
|
+
}
|
|
1010
|
+
const probe = runner.runSync(probeArgs);
|
|
1011
|
+
if (probe.status !== 0) {
|
|
1012
|
+
// eslint-disable-next-line no-console
|
|
1013
|
+
console.error(
|
|
1014
|
+
`agileflow launch __close-window: tmux display-message failed: ${probe.stderr || "unknown"}`,
|
|
1015
|
+
);
|
|
1016
|
+
return exit(1);
|
|
1017
|
+
}
|
|
1018
|
+
const parts = (probe.stdout || "").trimEnd().split(DELIM);
|
|
1019
|
+
if (parts.length !== 4) {
|
|
1020
|
+
// eslint-disable-next-line no-console
|
|
1021
|
+
console.error(
|
|
1022
|
+
`agileflow launch __close-window: unexpected display-message output (got ${parts.length} fields)`,
|
|
1023
|
+
);
|
|
1024
|
+
return exit(1);
|
|
1025
|
+
}
|
|
1026
|
+
const [sessionName, windowIndex, windowName, cwd] = parts;
|
|
1027
|
+
if (!sessionName || !windowIndex || !cwd) {
|
|
1028
|
+
// eslint-disable-next-line no-console
|
|
1029
|
+
console.error(
|
|
1030
|
+
"agileflow launch __close-window: missing session/index/cwd; skipping kill",
|
|
1031
|
+
);
|
|
1032
|
+
return exit(1);
|
|
1033
|
+
}
|
|
1034
|
+
// Kill first with the explicit target captured above. If this fails
|
|
1035
|
+
// we abort without touching the log — the window is still alive and
|
|
1036
|
+
// a phantom entry would mislead Alt+T into resurrecting a duplicate.
|
|
1037
|
+
const kill = runner.runSync([
|
|
1038
|
+
"kill-window",
|
|
1039
|
+
"-t",
|
|
1040
|
+
`${sessionName}:${windowIndex}`,
|
|
1041
|
+
]);
|
|
1042
|
+
if (kill.status !== 0) {
|
|
1043
|
+
// eslint-disable-next-line no-console
|
|
1044
|
+
console.error(
|
|
1045
|
+
`agileflow launch __close-window: kill-window failed: ${kill.stderr || "unknown"}`,
|
|
1046
|
+
);
|
|
1047
|
+
return exit(1);
|
|
1048
|
+
}
|
|
1049
|
+
try {
|
|
1050
|
+
pushClosedImpl({ sessionName, name: windowName || "", cwd });
|
|
1051
|
+
} catch (err) {
|
|
1052
|
+
// Window is already gone; push failure means Alt+T can't undo
|
|
1053
|
+
// this particular close. Surface for visibility but don't throw —
|
|
1054
|
+
// a tmux keybind run-shell can't usefully recover.
|
|
1055
|
+
// eslint-disable-next-line no-console
|
|
1056
|
+
console.error(
|
|
1057
|
+
`agileflow launch __close-window: log push failed (window already closed): ${err && err.message ? err.message : err}`,
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Hidden subcommand wired to the `Alt+T` restore keybind. Pops the
|
|
1064
|
+
* most recent closed entry for the current tmux session and spawns a
|
|
1065
|
+
* new window in its cwd with the original name. No-op when the log is
|
|
1066
|
+
* empty for this session — quieter than printing a "nothing to undo"
|
|
1067
|
+
* message, since the user just sees their layout unchanged.
|
|
1068
|
+
*
|
|
1069
|
+
* @param {{
|
|
1070
|
+
* runner?: ReturnType<typeof defaultTmuxRunner>,
|
|
1071
|
+
* popClosedImpl?: typeof closedWindows.popClosed,
|
|
1072
|
+
* }} [deps]
|
|
1073
|
+
* @returns {Promise<void>}
|
|
1074
|
+
*/
|
|
1075
|
+
async function runInternalRestoreWindow(deps = {}) {
|
|
1076
|
+
const runner = deps.runner || defaultTmuxRunner();
|
|
1077
|
+
const popClosedImpl = deps.popClosedImpl || closedWindows.popClosed;
|
|
1078
|
+
const pushClosedImpl = deps.pushClosedImpl || closedWindows.pushClosed;
|
|
1079
|
+
const exit = deps.exit || ((code) => process.exit(code));
|
|
1080
|
+
// Keybind passes #{session_name} so the restore targets the session
|
|
1081
|
+
// the user actually pressed Alt+T from. Fall back to display-message
|
|
1082
|
+
// for manual invocations (which only works inside tmux).
|
|
1083
|
+
let sessionName = (deps.targetSession || "").trim();
|
|
1084
|
+
if (!sessionName) {
|
|
1085
|
+
const probe = runner.runSync(["display-message", "-p", "-F", "#S"]);
|
|
1086
|
+
if (probe.status !== 0) {
|
|
1087
|
+
// eslint-disable-next-line no-console
|
|
1088
|
+
console.error(
|
|
1089
|
+
`agileflow launch __restore-window: tmux display-message failed: ${probe.stderr || "unknown"}`,
|
|
1090
|
+
);
|
|
1091
|
+
return exit(1);
|
|
1092
|
+
}
|
|
1093
|
+
sessionName = (probe.stdout || "").trim();
|
|
1094
|
+
}
|
|
1095
|
+
if (!sessionName) return exit(1);
|
|
1096
|
+
/** @type {ReturnType<typeof closedWindows.popClosed>} */
|
|
1097
|
+
let entry;
|
|
1098
|
+
try {
|
|
1099
|
+
entry = popClosedImpl(sessionName);
|
|
1100
|
+
} catch (err) {
|
|
1101
|
+
// eslint-disable-next-line no-console
|
|
1102
|
+
console.error(
|
|
1103
|
+
`agileflow launch __restore-window: log pop failed: ${err && err.message ? err.message : err}`,
|
|
1104
|
+
);
|
|
1105
|
+
return exit(1);
|
|
1106
|
+
}
|
|
1107
|
+
if (!entry) {
|
|
1108
|
+
// Empty stack — silent. The user pressed Alt+T with nothing to undo.
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
const args = ["new-window", "-t", sessionName, "-c", entry.cwd];
|
|
1112
|
+
if (entry.name) args.push("-n", entry.name);
|
|
1113
|
+
const create = runner.runSync(args);
|
|
1114
|
+
if (create.status !== 0) {
|
|
1115
|
+
// eslint-disable-next-line no-console
|
|
1116
|
+
console.error(
|
|
1117
|
+
`agileflow launch __restore-window: new-window failed: ${create.stderr || "unknown"}`,
|
|
1118
|
+
);
|
|
1119
|
+
// Re-push so the user doesn't lose their undo entry — pop already
|
|
1120
|
+
// mutated the log, so a failed new-window without re-push means
|
|
1121
|
+
// pressing Alt+T again would skip THIS entry and pop the NEXT one,
|
|
1122
|
+
// double-losing data.
|
|
1123
|
+
try {
|
|
1124
|
+
pushClosedImpl({
|
|
1125
|
+
sessionName,
|
|
1126
|
+
name: entry.name,
|
|
1127
|
+
cwd: entry.cwd,
|
|
1128
|
+
});
|
|
1129
|
+
} catch (pushErr) {
|
|
1130
|
+
// eslint-disable-next-line no-console
|
|
1131
|
+
console.error(
|
|
1132
|
+
`agileflow launch __restore-window: failed to re-push entry after new-window failure: ${pushErr && pushErr.message ? pushErr.message : pushErr}`,
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
return exit(1);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Auto-restore check on bare `agileflow launch`. Fires only when:
|
|
1141
|
+
* - tmux is available (we use sessionExists to count alive sessions)
|
|
1142
|
+
* - the registry has entries
|
|
1143
|
+
* - none of those entries' sessions are currently alive on the
|
|
1144
|
+
* server (typical post-reboot state — tmux server is brand-new)
|
|
1145
|
+
*
|
|
1146
|
+
* Prompts the user yes/no. On yes, calls runRestore and falls through
|
|
1147
|
+
* so the normal engine flow attaches to (or creates) the cwd's session
|
|
1148
|
+
* afterwards.
|
|
1149
|
+
*
|
|
1150
|
+
* @param {import("../../runtime/launch/defaults.js").LaunchPrefs} prefs
|
|
1151
|
+
* @returns {Promise<void>}
|
|
1152
|
+
*/
|
|
1153
|
+
async function maybeOfferAutoRestore(prefs) {
|
|
1154
|
+
if (!tmuxAvailable()) return;
|
|
1155
|
+
const reg = loadRegistry();
|
|
1156
|
+
if (reg.sessions.length === 0) return;
|
|
1157
|
+
|
|
1158
|
+
// If ANY registered session is alive, the server isn't fresh — skip
|
|
1159
|
+
// the bulk-restore prompt. The user is likely just launching a new
|
|
1160
|
+
// window in an already-running server.
|
|
1161
|
+
const runner = defaultTmuxRunner();
|
|
1162
|
+
const { sessionExists } = require("../../runtime/launch/tmux.js");
|
|
1163
|
+
for (const s of reg.sessions) {
|
|
1164
|
+
if (sessionExists(s.name, runner)) return;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// eslint-disable-next-line no-console
|
|
1168
|
+
console.log("\n" + logoBanner(pkg.version) + "\n");
|
|
1169
|
+
prompts.intro("agileflow launch — saved sessions detected");
|
|
1170
|
+
prompts.log.info(
|
|
1171
|
+
`Found ${reg.sessions.length} saved session(s) from before this tmux server started.`,
|
|
1172
|
+
);
|
|
1173
|
+
|
|
1174
|
+
// Multi-select picker: every session is an option, pinned entries
|
|
1175
|
+
// (and entries with worktrees — these are usually intentional Alt+n
|
|
1176
|
+
// work-in-progress) come pre-selected. Default-selecting nothing
|
|
1177
|
+
// would surprise users coming from v3 (which restored all), so when
|
|
1178
|
+
// no session is pinned we default-select everything.
|
|
1179
|
+
const sorted = reg.sessions.slice().sort((a, b) => {
|
|
1180
|
+
const ap = a.pinned === true ? 1 : 0;
|
|
1181
|
+
const bp = b.pinned === true ? 1 : 0;
|
|
1182
|
+
if (ap !== bp) return bp - ap;
|
|
1183
|
+
return 0;
|
|
1184
|
+
});
|
|
1185
|
+
// Smart toggle at the top of the picker: one entry that flips the
|
|
1186
|
+
// current selection state. If everything's selected, checking it
|
|
1187
|
+
// deselects all; if anything's unselected, checking it selects all.
|
|
1188
|
+
// Visually separated from session entries by a dash divider line
|
|
1189
|
+
// so it doesn't blend in with real options.
|
|
1190
|
+
const TOGGLE_ALL = "__toggle_all__";
|
|
1191
|
+
const DIVIDER = "__divider__";
|
|
1192
|
+
const allNames = sorted.map((s) => s.name);
|
|
1193
|
+
const sessionOptions = sorted.map((s) => ({
|
|
1194
|
+
value: s.name,
|
|
1195
|
+
label: `${s.pinned ? "* " : " "}${s.name}`,
|
|
1196
|
+
hint: `${s.cli} — ${s.cwd}${s.worktree && s.worktree.branch ? ` [wt ${s.worktree.branch}]` : ""}`,
|
|
1197
|
+
}));
|
|
1198
|
+
const options = [
|
|
1199
|
+
{
|
|
1200
|
+
value: TOGGLE_ALL,
|
|
1201
|
+
label: "[ select all / deselect all ]",
|
|
1202
|
+
hint: "toggles every session below",
|
|
1203
|
+
},
|
|
1204
|
+
{
|
|
1205
|
+
value: DIVIDER,
|
|
1206
|
+
label: "─────────────────────────────",
|
|
1207
|
+
hint: "",
|
|
1208
|
+
},
|
|
1209
|
+
...sessionOptions,
|
|
1210
|
+
];
|
|
1211
|
+
const anyPinned = sorted.some((s) => s.pinned === true);
|
|
1212
|
+
const initial = anyPinned
|
|
1213
|
+
? sorted.filter((s) => s.pinned === true).map((s) => s.name)
|
|
1214
|
+
: sorted.map((s) => s.name);
|
|
1215
|
+
|
|
1216
|
+
const selection = await prompts.multiselect({
|
|
1217
|
+
message: questionMessage(
|
|
1218
|
+
"Pick which sessions to restore",
|
|
1219
|
+
"Pinned (★) entries are pre-selected. Space toggles, Enter confirms, Ctrl+C cancels everything.",
|
|
1220
|
+
),
|
|
1221
|
+
options,
|
|
1222
|
+
initialValues: initial,
|
|
1223
|
+
required: false,
|
|
1224
|
+
});
|
|
1225
|
+
// Distinguish Ctrl+C ("cancel everything") from explicit "no selections"
|
|
1226
|
+
// ("skip restore but keep going"). The two used to be one branch and
|
|
1227
|
+
// both fell through to runEngine — surprising for users who pressed
|
|
1228
|
+
// Ctrl+C expecting nothing further to happen.
|
|
1229
|
+
if (prompts.isCancel(selection)) {
|
|
1230
|
+
prompts.cancel("Launch cancelled. No sessions restored.");
|
|
1231
|
+
process.exit(0);
|
|
1232
|
+
}
|
|
1233
|
+
/** @type {string[]} */
|
|
1234
|
+
let chosen = Array.isArray(selection) ? selection : [];
|
|
1235
|
+
// Strip the synthetic divider always (it's never a real choice).
|
|
1236
|
+
// Resolve the toggle: if the user checked it, flip the current
|
|
1237
|
+
// selection state — everything selected goes to nothing, anything
|
|
1238
|
+
// partial or empty goes to everything.
|
|
1239
|
+
const toggled = chosen.includes(TOGGLE_ALL);
|
|
1240
|
+
chosen = chosen.filter((v) => v !== TOGGLE_ALL && v !== DIVIDER);
|
|
1241
|
+
if (toggled) {
|
|
1242
|
+
const allSelected =
|
|
1243
|
+
chosen.length === allNames.length &&
|
|
1244
|
+
allNames.every((n) => chosen.includes(n));
|
|
1245
|
+
chosen = allSelected ? [] : [...allNames];
|
|
1246
|
+
}
|
|
1247
|
+
if (chosen.length === 0) {
|
|
1248
|
+
prompts.outro(
|
|
1249
|
+
"Skipped. Run `agileflow launch restore` later to bring them back.",
|
|
1250
|
+
);
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
const result = runRestore({ prefs, onlyNames: chosen });
|
|
1254
|
+
prompts.outro(
|
|
1255
|
+
`Restored ${result.restored} session(s). Continuing into the current directory's session...`,
|
|
1256
|
+
);
|
|
1257
|
+
if (result.failed > 0 || result.skipped > 0) {
|
|
1258
|
+
for (const note of result.notes) {
|
|
1259
|
+
// eslint-disable-next-line no-console
|
|
1260
|
+
console.error(` ${note.name}: ${note.reason}`);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* Commander action for `agileflow launch [sub] [name]`.
|
|
1267
|
+
*
|
|
1268
|
+
* Commander v12 invokes action with positional args first, options last:
|
|
1269
|
+
* `action((sub, name, options) => ...)` for `.command("launch [sub] [name]")`.
|
|
1270
|
+
* The `name` is only meaningful when `sub === "new"`; ignored otherwise.
|
|
1271
|
+
*
|
|
1272
|
+
* @param {string | undefined} sub
|
|
1273
|
+
* @param {string | undefined} nameArg
|
|
1274
|
+
* @param {Record<string, unknown>} [_options]
|
|
1275
|
+
*/
|
|
1276
|
+
async function launch(sub, nameArg, _options) {
|
|
1277
|
+
try {
|
|
1278
|
+
if (sub === "new") {
|
|
1279
|
+
await runNew(nameArg);
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
if (sub === "__exec") {
|
|
1283
|
+
// Hidden subcommand invoked by tmux itself when a session boots.
|
|
1284
|
+
// `nameArg` is the registry key. runExec loads the entry, spawns
|
|
1285
|
+
// the right CLI with its resume args, captures the new UUID after
|
|
1286
|
+
// exit, and process.exits with the CLI's status.
|
|
1287
|
+
if (!nameArg) {
|
|
1288
|
+
fail(
|
|
1289
|
+
new OperationFailedError(
|
|
1290
|
+
"agileflow launch __exec requires a session name",
|
|
1291
|
+
{ suggestion: "this command is invoked by tmux internally" },
|
|
1292
|
+
),
|
|
1293
|
+
{ command: "launch" },
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
await runExec(nameArg);
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
if (sub === "restore") {
|
|
1300
|
+
await runRestoreCommand();
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
if (sub === "ls") {
|
|
1304
|
+
await runLs();
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
if (sub === "kill") {
|
|
1308
|
+
await runKill(nameArg);
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
if (sub === "attach") {
|
|
1312
|
+
await runAttachByName(nameArg);
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
if (sub === "prune") {
|
|
1316
|
+
await runPrune();
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
if (sub === "doctor") {
|
|
1320
|
+
await runDoctor();
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
if (sub === "pin") {
|
|
1324
|
+
await runPin(nameArg, true);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
if (sub === "unpin") {
|
|
1328
|
+
await runPin(nameArg, false);
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
if (sub === "where") {
|
|
1332
|
+
await runWhere();
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
if (sub === "__close-window") {
|
|
1336
|
+
// Hidden subcommand invoked from tmux keybind (Alt+w). The keybind
|
|
1337
|
+
// passes session name + window index as positional args so we
|
|
1338
|
+
// target the exact tab the user pressed Alt+w on, regardless of
|
|
1339
|
+
// any focus shift during the confirmation prompt. nameArg is the
|
|
1340
|
+
// session name; we read the window index from raw argv since
|
|
1341
|
+
// commander's signature only declares two positionals.
|
|
1342
|
+
const targetSession = nameArg || "";
|
|
1343
|
+
const targetIndex = (process.argv && process.argv[5]) || "";
|
|
1344
|
+
await runInternalCloseWindow({ targetSession, targetIndex });
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
if (sub === "__restore-window") {
|
|
1348
|
+
// Hidden subcommand invoked from tmux keybind (Alt+T). Pops the
|
|
1349
|
+
// most recent closed entry for the session the user pressed
|
|
1350
|
+
// Alt+T from (passed positionally via #{session_name}) and
|
|
1351
|
+
// spawns a new window in that cwd with the original name. No-op
|
|
1352
|
+
// when the log is empty for this session.
|
|
1353
|
+
await runInternalRestoreWindow({ targetSession: nameArg || "" });
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
if (sub && sub !== "setup") {
|
|
1357
|
+
fail(
|
|
1358
|
+
new OperationFailedError(`unknown launch subcommand: ${sub}`, {
|
|
1359
|
+
suggestion:
|
|
1360
|
+
"use `agileflow launch`, `agileflow launch setup`, `agileflow launch new [name]`, `agileflow launch restore`, `agileflow launch ls`, `agileflow launch kill <name>`, `agileflow launch attach <name>`, `agileflow launch prune`, `agileflow launch doctor`, `agileflow launch pin <name>`, `agileflow launch unpin <name>`, or `agileflow launch where`",
|
|
1361
|
+
}),
|
|
1362
|
+
{ command: "launch" },
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
const hasPrefs = await prefsExist();
|
|
1367
|
+
const flow = decideFlow({ sub, hasPrefs });
|
|
1368
|
+
|
|
1369
|
+
if (flow === "setup") {
|
|
1370
|
+
await runSetup();
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
if (flow === "first-run-setup") {
|
|
1375
|
+
// Walk the user through setup, then immediately launch — they
|
|
1376
|
+
// expect `agileflow launch` to do something on first invocation,
|
|
1377
|
+
// not just configure and exit. Use the prefs returned from
|
|
1378
|
+
// runSetup directly so a failed reload can't tell the user to
|
|
1379
|
+
// "re-run setup" when they just finished doing exactly that.
|
|
1380
|
+
const prefs = await runSetup();
|
|
1381
|
+
// eslint-disable-next-line no-console
|
|
1382
|
+
console.log("");
|
|
1383
|
+
await runEngine(prefs);
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
const { prefs } = await loadPrefsOrFail();
|
|
1388
|
+
// Auto-restore prompt before engine: if tmux is up, the registry
|
|
1389
|
+
// has entries, and none of them are currently alive on the server,
|
|
1390
|
+
// ask the user whether to bulk-restore. After they choose, the
|
|
1391
|
+
// engine still runs and either attaches to (or creates) the cwd's
|
|
1392
|
+
// canonical session.
|
|
1393
|
+
await maybeOfferAutoRestore(prefs);
|
|
1394
|
+
await runEngine(prefs);
|
|
1395
|
+
} catch (err) {
|
|
1396
|
+
// Preserve typed AgileflowError subclasses (OperationFailedError,
|
|
1397
|
+
// InvalidArgumentError, MissingFileError) with their `suggestion`
|
|
1398
|
+
// intact. A `name` string compare would miss subclasses — they each
|
|
1399
|
+
// override `name` to their own class name.
|
|
1400
|
+
if (err instanceof AgileflowError) throw err;
|
|
1401
|
+
|
|
1402
|
+
// TOCTOU: the PATH probe in resolveCli passed, but the binary was
|
|
1403
|
+
// removed before spawn. Surface the same actionable hint as the
|
|
1404
|
+
// no-CLI-installed path, not the generic "re-run with DEBUG=1".
|
|
1405
|
+
if (err && err.code === "ENOENT") {
|
|
1406
|
+
fail(
|
|
1407
|
+
new OperationFailedError(
|
|
1408
|
+
`AI CLI not found at launch time: ${err.message}`,
|
|
1409
|
+
{
|
|
1410
|
+
suggestion:
|
|
1411
|
+
"install one of the supported CLIs (claude, codex, cursor-agent, aider), " +
|
|
1412
|
+
"or run `agileflow launch setup` to update your fallback order",
|
|
1413
|
+
cause: err,
|
|
1414
|
+
},
|
|
1415
|
+
),
|
|
1416
|
+
{ command: "launch" },
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// Binary exists but is not executable — distinct fix from "install".
|
|
1421
|
+
if (err && err.code === "EACCES") {
|
|
1422
|
+
fail(
|
|
1423
|
+
new OperationFailedError(`AI CLI is not executable: ${err.message}`, {
|
|
1424
|
+
suggestion:
|
|
1425
|
+
"check file permissions on the CLI binary, or re-install it",
|
|
1426
|
+
cause: err,
|
|
1427
|
+
}),
|
|
1428
|
+
{ command: "launch" },
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
fail(
|
|
1433
|
+
new OperationFailedError(
|
|
1434
|
+
`launch failed: ${err && err.message ? err.message : String(err)}`,
|
|
1435
|
+
{ suggestion: "re-run with DEBUG=1 for a stack trace", cause: err },
|
|
1436
|
+
),
|
|
1437
|
+
{ command: "launch" },
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
module.exports = launch;
|
|
1443
|
+
module.exports.decideFlow = decideFlow;
|
|
1444
|
+
module.exports.runSetup = runSetup;
|
|
1445
|
+
module.exports.runEngine = runEngine;
|
|
1446
|
+
module.exports.runNew = runNew;
|
|
1447
|
+
module.exports.runLs = runLs;
|
|
1448
|
+
module.exports.runKill = runKill;
|
|
1449
|
+
module.exports.runAttachByName = runAttachByName;
|
|
1450
|
+
module.exports.runPrune = runPrune;
|
|
1451
|
+
module.exports.runDoctor = runDoctor;
|
|
1452
|
+
module.exports.runPin = runPin;
|
|
1453
|
+
module.exports.runWhere = runWhere;
|
|
1454
|
+
module.exports.shouldOfferOrphanCleanup = shouldOfferOrphanCleanup;
|