@pennyfarthing/core 11.1.0 → 11.2.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/README.md +8 -8
- package/package.json +16 -14
- package/packages/core/dist/cli/utils/constants.d.ts +1 -1
- package/packages/core/dist/cli/utils/constants.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/constants.js +2 -1
- package/packages/core/dist/cli/utils/constants.js.map +1 -1
- package/packages/core/dist/consultation/dialogue-manager.d.ts +75 -0
- package/packages/core/dist/consultation/dialogue-manager.d.ts.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.js +334 -0
- package/packages/core/dist/consultation/dialogue-manager.js.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.test.d.ts +19 -0
- package/packages/core/dist/consultation/dialogue-manager.test.d.ts.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.test.js +444 -0
- package/packages/core/dist/consultation/dialogue-manager.test.js.map +1 -0
- package/packages/core/dist/server/api/git.d.ts +13 -1
- package/packages/core/dist/server/api/git.d.ts.map +1 -1
- package/packages/core/dist/server/api/git.js +53 -34
- package/packages/core/dist/server/api/git.js.map +1 -1
- package/packages/core/dist/server/otlp-receiver.d.ts +16 -11
- package/packages/core/dist/server/otlp-receiver.d.ts.map +1 -1
- package/packages/core/dist/server/otlp-receiver.js +185 -24
- package/packages/core/dist/server/otlp-receiver.js.map +1 -1
- package/packages/core/dist/server/otlp-receiver.test.d.ts +21 -0
- package/packages/core/dist/server/otlp-receiver.test.d.ts.map +1 -0
- package/packages/core/dist/server/otlp-receiver.test.js +446 -0
- package/packages/core/dist/server/otlp-receiver.test.js.map +1 -0
- package/packages/core/dist/shared/portrait-resolver.d.ts +9 -0
- package/packages/core/dist/shared/portrait-resolver.d.ts.map +1 -1
- package/packages/core/dist/shared/portrait-resolver.js +27 -0
- package/packages/core/dist/shared/portrait-resolver.js.map +1 -1
- package/packages/core/dist/shared/portrait-resolver.test.js +47 -1
- package/packages/core/dist/shared/portrait-resolver.test.js.map +1 -1
- package/packages/core/dist/shared/skill-search.test.js +2 -2
- package/packages/core/dist/shared/tandem-portrait-inventory.test.d.ts +13 -0
- package/packages/core/dist/shared/tandem-portrait-inventory.test.d.ts.map +1 -0
- package/packages/core/dist/shared/tandem-portrait-inventory.test.js +126 -0
- package/packages/core/dist/shared/tandem-portrait-inventory.test.js.map +1 -0
- package/pennyfarthing-dist/agents/dev.md +1 -1
- package/pennyfarthing-dist/agents/reviewer.md +1 -1
- package/pennyfarthing-dist/agents/sm-setup.md +1 -1
- package/pennyfarthing-dist/agents/sm.md +2 -2
- package/pennyfarthing-dist/agents/tea.md +1 -1
- package/pennyfarthing-dist/agents/testing-runner.md +2 -1
- package/pennyfarthing-dist/commands/pf-chore.md +2 -2
- package/pennyfarthing-dist/commands/pf-standalone.md +7 -2
- package/pennyfarthing-dist/guides/agent-behavior.md +1 -1
- package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +1 -1
- package/pennyfarthing-dist/guides/bikerack.md +3 -3
- package/pennyfarthing-dist/guides/hooks.md +1 -1
- package/pennyfarthing-dist/guides/worktree-mode.md +3 -3
- package/pennyfarthing-dist/guides/xml-tags.md +2 -2
- package/pennyfarthing-dist/scripts/README.md +1 -1
- package/pennyfarthing-dist/scripts/core/agent-session.sh +0 -0
- package/pennyfarthing-dist/scripts/core/check-context.sh +1 -1
- package/pennyfarthing-dist/scripts/core/dialogue-manager.sh +322 -0
- package/pennyfarthing-dist/scripts/core/phase-check-start.sh +0 -0
- package/pennyfarthing-dist/scripts/core/prime.sh +0 -0
- package/pennyfarthing-dist/scripts/cyclist/is-cyclist.sh +0 -0
- package/pennyfarthing-dist/scripts/git/README.md +24 -14
- package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +5 -266
- package/pennyfarthing-dist/scripts/git/git-status-all.sh +5 -151
- package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +6 -144
- package/pennyfarthing-dist/scripts/git/release.sh +0 -0
- package/pennyfarthing-dist/scripts/git/worktree-manager.sh +5 -496
- package/pennyfarthing-dist/scripts/health/drift-detection.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/README.md +1 -1
- package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +1 -1
- package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/context-warning.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/dispatcher-template.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +9 -11
- package/pennyfarthing-dist/scripts/hooks/post-merge.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-push.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +0 -0
- package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/session-start.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/session-stop.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +1 -1
- package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/background-tasks.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/checkpoint.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/common.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/file-lock.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/logging.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/retry.sh +0 -0
- package/pennyfarthing-dist/scripts/maintenance/migrate-theme-schema.mjs +0 -0
- package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/add-short-names.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/add_short_names.py +0 -0
- package/pennyfarthing-dist/scripts/misc/backlog.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/check-status.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/find-related-work.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +0 -0
- package/pennyfarthing-dist/scripts/misc/repo-scan.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/repo-utils.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/run-ci.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/run-timestamp.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/session-cleanup.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/statusline.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/uninstall.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +0 -0
- package/pennyfarthing-dist/scripts/portraits/generate-portraits.sh +0 -0
- package/pennyfarthing-dist/scripts/portraits/generate-tandem-portraits.sh +76 -0
- package/pennyfarthing-dist/scripts/story/create-story.sh +0 -0
- package/pennyfarthing-dist/scripts/story/size-story.sh +0 -0
- package/pennyfarthing-dist/scripts/story/story-template.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/check.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/dev-story-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/epics-and-stories-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/implementation-readiness-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/migrate-bmad-workflow.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/prd-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/project-context-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-character-voice.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-post-merge-hook.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-session-checkpoint.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-solo-command.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/ux-design-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/theme/list-themes.sh +0 -0
- package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/check.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/check.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/complete-step.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/finish-story.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +4 -221
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +5 -13
- package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +4 -123
- package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +4 -33
- package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +4 -156
- package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +4 -131
- package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +4 -249
- package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +4 -160
- package/pennyfarthing-dist/skills/pf-bc/usage.md +1 -1
- package/pennyfarthing-dist/skills/pf-jira/examples.md +5 -2
- package/pennyfarthing-dist/skills/pf-story/scripts/create-story.sh +0 -0
- package/pennyfarthing-dist/skills/pf-story/scripts/size-story.sh +0 -0
- package/pennyfarthing-dist/skills/pf-story/scripts/story-template.sh +0 -0
- package/pennyfarthing-dist/skills/pf-workflow/examples.md +27 -16
- package/pennyfarthing-dist/skills/pf-workflow/skill.md +9 -12
- package/pennyfarthing-dist/skills/pf-workflow/usage.md +33 -8
- package/pennyfarthing-dist/workflows/bdd-tandem.yaml +18 -6
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-01-analyze.md +1 -1
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-04-verify.md +1 -1
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +1 -1
- package/pennyfarthing-dist/workflows/review-tandem.yaml +65 -0
- package/pennyfarthing-dist/workflows/tdd-tandem.yaml +16 -8
- package/pennyfarthing_scripts/CLAUDE.md +26 -4
- package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/session_start_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/cli.py +3 -5
- package/pennyfarthing_scripts/bikerack/__pycache__/background_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/changed_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/debug_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/diffs_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/portrait.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/progress_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/background_panel.py +86 -5
- package/pennyfarthing_scripts/bikerack/base_panel.py +62 -0
- package/pennyfarthing_scripts/bikerack/changed_panel.py +32 -28
- package/pennyfarthing_scripts/bikerack/cli.py +10 -11
- package/pennyfarthing_scripts/bikerack/debug_panel.py +31 -1
- package/pennyfarthing_scripts/bikerack/diffs_panel.py +74 -17
- package/pennyfarthing_scripts/bikerack/git_panel.py +103 -33
- package/pennyfarthing_scripts/bikerack/launcher.py +15 -15
- package/pennyfarthing_scripts/bikerack/progress_panel.py +315 -0
- package/pennyfarthing_scripts/bikerack/sprint_panel.py +158 -26
- package/pennyfarthing_scripts/bikerack/tui.py +336 -30
- package/pennyfarthing_scripts/bikerack/ws_client.py +2 -2
- package/pennyfarthing_scripts/cli.py +37 -65
- package/pennyfarthing_scripts/consultation/__init__.py +1 -0
- package/pennyfarthing_scripts/consultation/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/consultation/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/consultation/cli.py +149 -0
- package/pennyfarthing_scripts/consultation/dialogue_manager.py +417 -0
- package/pennyfarthing_scripts/context.py +3 -3
- package/pennyfarthing_scripts/epic/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/epic/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__init__.py +12 -1
- package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/hooks_installer.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/repos.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/worktree.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/create_branches.py +3 -4
- package/pennyfarthing_scripts/git/hooks_installer.py +152 -0
- package/pennyfarthing_scripts/git/repos.py +196 -0
- package/pennyfarthing_scripts/git/status_all.py +27 -11
- package/pennyfarthing_scripts/git/worktree.py +302 -0
- package/pennyfarthing_scripts/git_group/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git_group/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git_group/cli.py +143 -40
- package/pennyfarthing_scripts/handoff/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/complete_phase.py +12 -0
- package/pennyfarthing_scripts/handoff/resolve_gate.py +5 -14
- package/pennyfarthing_scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
- package/pennyfarthing_scripts/hooks.py +3 -17
- package/pennyfarthing_scripts/pretooluse_hook.py +1 -1
- package/pennyfarthing_scripts/prime/__pycache__/heatmap.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/heatmap.py +655 -0
- package/pennyfarthing_scripts/session/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/session/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/session_start_hook.py +1 -1
- package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/loader.py +15 -1
- package/pennyfarthing_scripts/sprint/story_finish.py +14 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_handoff_cli.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_handoff_e2e.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/test_bikerack.py +51 -51
- package/pennyfarthing_scripts/tests/test_dialogue_manager.py +811 -0
- package/pennyfarthing_scripts/tests/test_handoff_cli.py +16 -11
- package/pennyfarthing_scripts/tests/test_workflow_check.py +2 -3
- package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/tandem_awareness.py +254 -0
- package/pennyfarthing_scripts/validate/cli.py +17 -5
- package/pennyfarthing_scripts/workflow/__init__.py +40 -0
- package/pennyfarthing_scripts/workflow/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/helpers.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/scale.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/state.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/cli.py +1099 -0
- package/pennyfarthing_scripts/workflow/helpers.py +241 -0
- package/pennyfarthing_scripts/{workflow.py → workflow/scale.py} +0 -104
- package/pennyfarthing_scripts/workflow/state.py +112 -0
- package/scripts/README.md +41 -0
- package/pennyfarthing-dist/skills/pf-workflow/scripts/list-workflows.sh +0 -91
- package/pennyfarthing-dist/skills/pf-workflow/scripts/resume-workflow.sh +0 -163
- package/pennyfarthing-dist/skills/pf-workflow/scripts/show-workflow.sh +0 -138
- package/pennyfarthing-dist/skills/pf-workflow/scripts/start-workflow.sh +0 -273
- package/pennyfarthing-dist/skills/pf-workflow/scripts/workflow-status.sh +0 -167
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Repos.yaml loader — Python replacement for repo-utils.sh (778 lines).
|
|
3
|
+
|
|
4
|
+
Reads .pennyfarthing/repos.yaml and provides structured access to repo
|
|
5
|
+
configuration: paths, types, branches, build/test commands, dependencies.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from pennyfarthing_scripts.git.repos import load_repos_config, get_repo_paths
|
|
9
|
+
|
|
10
|
+
config = load_repos_config()
|
|
11
|
+
for name, repo in config.items():
|
|
12
|
+
print(f"{name}: {repo.path} ({repo.default_branch})")
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import yaml
|
|
22
|
+
|
|
23
|
+
from pennyfarthing_scripts.common.config import get_project_root
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class RepoConfig:
|
|
28
|
+
"""Configuration for a single repository."""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
path: str # Relative to project root (e.g., "." or "pennyfarthing")
|
|
32
|
+
repo_type: str # "orchestrator", "framework", "api", "ui", etc.
|
|
33
|
+
default_branch: str # "main" for trunk-based, "develop" for gitflow
|
|
34
|
+
branch_strategy: str # "trunk-based" or "gitflow"
|
|
35
|
+
description: str = ""
|
|
36
|
+
language: str = "unknown"
|
|
37
|
+
test_command: str = ""
|
|
38
|
+
build_command: str = ""
|
|
39
|
+
lint_command: str = ""
|
|
40
|
+
test_filter_flag: str = ""
|
|
41
|
+
dependencies: list[str] = field(default_factory=list)
|
|
42
|
+
owns: list[str] = field(default_factory=list)
|
|
43
|
+
never_edit: list[str] = field(default_factory=list)
|
|
44
|
+
ui_layer: str = "none"
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def is_gitflow(self) -> bool:
|
|
48
|
+
return self.branch_strategy == "gitflow"
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def upstream_ref(self) -> str:
|
|
52
|
+
"""Remote ref to compare against for unpushed commits."""
|
|
53
|
+
return f"origin/{self.default_branch}"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _parse_repo_entry(name: str, data: dict[str, Any] | None) -> RepoConfig:
|
|
57
|
+
"""Parse a single repo entry from repos.yaml."""
|
|
58
|
+
if data is None:
|
|
59
|
+
data = {}
|
|
60
|
+
return RepoConfig(
|
|
61
|
+
name=name,
|
|
62
|
+
path=data.get("path", name),
|
|
63
|
+
repo_type=data.get("type", "unknown"),
|
|
64
|
+
default_branch=data.get("default_branch", "main"),
|
|
65
|
+
branch_strategy=data.get("branch_strategy", "trunk-based"),
|
|
66
|
+
description=data.get("description", ""),
|
|
67
|
+
language=data.get("language", "unknown"),
|
|
68
|
+
test_command=data.get("test_command", ""),
|
|
69
|
+
build_command=data.get("build_command", ""),
|
|
70
|
+
lint_command=data.get("lint_command", ""),
|
|
71
|
+
test_filter_flag=data.get("test_filter_flag", ""),
|
|
72
|
+
dependencies=data.get("dependencies", []) or [],
|
|
73
|
+
owns=data.get("owns", []) or [],
|
|
74
|
+
never_edit=data.get("never_edit", []) or [],
|
|
75
|
+
ui_layer=data.get("ui_layer", "none"),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def load_repos_config(project_root: Path | None = None) -> dict[str, RepoConfig]:
|
|
80
|
+
"""Load repos.yaml and return a dict of name -> RepoConfig.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
project_root: Project root directory. Auto-detected if not provided.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Ordered dict of repo name -> RepoConfig.
|
|
87
|
+
Empty dict if repos.yaml not found.
|
|
88
|
+
"""
|
|
89
|
+
if project_root is None:
|
|
90
|
+
project_root = get_project_root()
|
|
91
|
+
|
|
92
|
+
repos_path = project_root / ".pennyfarthing" / "repos.yaml"
|
|
93
|
+
if not repos_path.exists():
|
|
94
|
+
return {}
|
|
95
|
+
|
|
96
|
+
with open(repos_path) as f:
|
|
97
|
+
config = yaml.safe_load(f)
|
|
98
|
+
|
|
99
|
+
if not config or "repos" not in config:
|
|
100
|
+
return {}
|
|
101
|
+
|
|
102
|
+
repos: dict[str, RepoConfig] = {}
|
|
103
|
+
for name, data in config["repos"].items():
|
|
104
|
+
repos[name] = _parse_repo_entry(name, data)
|
|
105
|
+
|
|
106
|
+
return repos
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_repo_paths(project_root: Path | None = None) -> list[tuple[str, Path]]:
|
|
110
|
+
"""Get list of (name, absolute_path) tuples for all configured repos.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
project_root: Project root directory. Auto-detected if not provided.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
List of (repo_name, absolute_path) tuples.
|
|
117
|
+
"""
|
|
118
|
+
if project_root is None:
|
|
119
|
+
project_root = get_project_root()
|
|
120
|
+
|
|
121
|
+
repos = load_repos_config(project_root)
|
|
122
|
+
result = []
|
|
123
|
+
for name, repo in repos.items():
|
|
124
|
+
abs_path = (project_root / repo.path).resolve()
|
|
125
|
+
if abs_path.exists():
|
|
126
|
+
result.append((name, abs_path))
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_default_branch(
|
|
131
|
+
repo_name: str, project_root: Path | None = None
|
|
132
|
+
) -> str:
|
|
133
|
+
"""Get the default branch for a specific repo.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
repo_name: Name of the repo in repos.yaml.
|
|
137
|
+
project_root: Project root directory. Auto-detected if not provided.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Default branch name (e.g., "main" or "develop").
|
|
141
|
+
Falls back to "main" if repo not found.
|
|
142
|
+
"""
|
|
143
|
+
repos = load_repos_config(project_root)
|
|
144
|
+
if repo_name in repos:
|
|
145
|
+
return repos[repo_name].default_branch
|
|
146
|
+
return "main"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_repo_config(
|
|
150
|
+
repo_name: str, project_root: Path | None = None
|
|
151
|
+
) -> RepoConfig | None:
|
|
152
|
+
"""Get the full config for a specific repo.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
repo_name: Name of the repo in repos.yaml.
|
|
156
|
+
project_root: Project root directory. Auto-detected if not provided.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
RepoConfig or None if not found.
|
|
160
|
+
"""
|
|
161
|
+
repos = load_repos_config(project_root)
|
|
162
|
+
return repos.get(repo_name)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_build_order(project_root: Path | None = None) -> list[str]:
|
|
166
|
+
"""Get repos in build/dependency order.
|
|
167
|
+
|
|
168
|
+
Uses explicit build_order from repos.yaml if present,
|
|
169
|
+
otherwise returns repos in definition order.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
project_root: Project root directory. Auto-detected if not provided.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
List of repo names in build order.
|
|
176
|
+
"""
|
|
177
|
+
if project_root is None:
|
|
178
|
+
project_root = get_project_root()
|
|
179
|
+
|
|
180
|
+
repos_path = project_root / ".pennyfarthing" / "repos.yaml"
|
|
181
|
+
if not repos_path.exists():
|
|
182
|
+
return []
|
|
183
|
+
|
|
184
|
+
with open(repos_path) as f:
|
|
185
|
+
config = yaml.safe_load(f)
|
|
186
|
+
|
|
187
|
+
if not config:
|
|
188
|
+
return []
|
|
189
|
+
|
|
190
|
+
if "build_order" in config:
|
|
191
|
+
return config["build_order"]
|
|
192
|
+
|
|
193
|
+
if "repos" in config:
|
|
194
|
+
return list(config["repos"].keys())
|
|
195
|
+
|
|
196
|
+
return []
|
|
@@ -65,12 +65,15 @@ async def _run_git_command(args: list[str], cwd: Path) -> tuple[str, str, int]:
|
|
|
65
65
|
)
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
async def get_repo_status(
|
|
68
|
+
async def get_repo_status(
|
|
69
|
+
name: str, path: Path, upstream_ref: str = "origin/develop"
|
|
70
|
+
) -> RepoStatus:
|
|
69
71
|
"""Get git status for a single repository.
|
|
70
72
|
|
|
71
73
|
Args:
|
|
72
74
|
name: Display name for the repo
|
|
73
75
|
path: Path to the repository
|
|
76
|
+
upstream_ref: Remote ref to compare for unpushed commits (default: origin/develop)
|
|
74
77
|
|
|
75
78
|
Returns:
|
|
76
79
|
RepoStatus with current branch, changes, and unpushed commits
|
|
@@ -124,9 +127,9 @@ async def get_repo_status(name: str, path: Path) -> RepoStatus:
|
|
|
124
127
|
status_out, _, _ = await _run_git_command(["status", "--short"], path)
|
|
125
128
|
changes = [line for line in status_out.split("\n") if line.strip()]
|
|
126
129
|
|
|
127
|
-
# Get unpushed commits (comparing to
|
|
130
|
+
# Get unpushed commits (comparing to upstream ref)
|
|
128
131
|
unpushed_out, _, unpushed_rc = await _run_git_command(
|
|
129
|
-
["log", "
|
|
132
|
+
["log", f"{upstream_ref}..HEAD", "--oneline"], path
|
|
130
133
|
)
|
|
131
134
|
if unpushed_rc == 0 and unpushed_out:
|
|
132
135
|
unpushed_commits = [
|
|
@@ -154,11 +157,13 @@ async def get_repo_status(name: str, path: Path) -> RepoStatus:
|
|
|
154
157
|
)
|
|
155
158
|
|
|
156
159
|
|
|
157
|
-
async def get_all_repo_status(
|
|
160
|
+
async def get_all_repo_status(
|
|
161
|
+
repos: Sequence[tuple[str, Path, str] | tuple[str, Path]],
|
|
162
|
+
) -> list[RepoStatus]:
|
|
158
163
|
"""Get git status for all repos in parallel using asyncio.gather.
|
|
159
164
|
|
|
160
165
|
Args:
|
|
161
|
-
repos: Sequence of (name, path)
|
|
166
|
+
repos: Sequence of (name, path) or (name, path, upstream_ref) tuples
|
|
162
167
|
|
|
163
168
|
Returns:
|
|
164
169
|
List of RepoStatus objects in same order as input
|
|
@@ -166,7 +171,14 @@ async def get_all_repo_status(repos: Sequence[tuple[str, Path]]) -> list[RepoSta
|
|
|
166
171
|
if not repos:
|
|
167
172
|
return []
|
|
168
173
|
|
|
169
|
-
tasks = [
|
|
174
|
+
tasks = []
|
|
175
|
+
for entry in repos:
|
|
176
|
+
if len(entry) == 3:
|
|
177
|
+
name, path, upstream_ref = entry # type: ignore[misc]
|
|
178
|
+
tasks.append(get_repo_status(name, path, upstream_ref))
|
|
179
|
+
else:
|
|
180
|
+
name, path = entry # type: ignore[misc]
|
|
181
|
+
tasks.append(get_repo_status(name, path))
|
|
170
182
|
results = await asyncio.gather(*tasks, return_exceptions=False)
|
|
171
183
|
return list(results)
|
|
172
184
|
|
|
@@ -279,13 +291,17 @@ async def main(brief: bool = False) -> int:
|
|
|
279
291
|
Returns:
|
|
280
292
|
0 if all repos clean, 1 if any have changes/unpushed
|
|
281
293
|
"""
|
|
282
|
-
from pennyfarthing_scripts.
|
|
294
|
+
from pennyfarthing_scripts.git.repos import load_repos_config, get_repo_paths
|
|
295
|
+
|
|
296
|
+
repos_with_upstream: list[tuple[str, Path, str]] = []
|
|
297
|
+
repo_paths = get_repo_paths()
|
|
298
|
+
config = load_repos_config()
|
|
283
299
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
300
|
+
for name, path in repo_paths:
|
|
301
|
+
upstream = config[name].upstream_ref if name in config else "origin/develop"
|
|
302
|
+
repos_with_upstream.append((name, path, upstream))
|
|
287
303
|
|
|
288
|
-
statuses = await get_all_repo_status(
|
|
304
|
+
statuses = await get_all_repo_status(repos_with_upstream)
|
|
289
305
|
|
|
290
306
|
if brief:
|
|
291
307
|
print(format_status_brief(statuses))
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git worktree management — Python replacement for worktree-manager.sh (498 lines).
|
|
3
|
+
|
|
4
|
+
Manages git worktrees for parallel development across multiple repos.
|
|
5
|
+
|
|
6
|
+
Usage via CLI:
|
|
7
|
+
pf git worktree create <name> <branch> [--repos all|api|ui|name1,name2]
|
|
8
|
+
pf git worktree remove <name>
|
|
9
|
+
pf git worktree list
|
|
10
|
+
pf git worktree status
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import subprocess
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from pennyfarthing_scripts.common.config import get_project_root
|
|
20
|
+
from pennyfarthing_scripts.git.repos import load_repos_config
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _git(args: list[str], cwd: Path) -> tuple[str, int]:
|
|
24
|
+
"""Run a git command synchronously.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Tuple of (stdout, return_code)
|
|
28
|
+
"""
|
|
29
|
+
result = subprocess.run(
|
|
30
|
+
["git", *args],
|
|
31
|
+
cwd=cwd,
|
|
32
|
+
capture_output=True,
|
|
33
|
+
text=True,
|
|
34
|
+
)
|
|
35
|
+
return result.stdout.strip(), result.returncode
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_worktree_root(project_root: Path | None = None) -> Path:
|
|
39
|
+
"""Get worktree root directory."""
|
|
40
|
+
if project_root is None:
|
|
41
|
+
project_root = get_project_root()
|
|
42
|
+
env_root = os.environ.get("WORKTREE_ROOT")
|
|
43
|
+
if env_root:
|
|
44
|
+
return Path(env_root)
|
|
45
|
+
return project_root / "worktrees"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _filter_repos(
|
|
49
|
+
repos: dict[str, object], filter_str: str
|
|
50
|
+
) -> list[str]:
|
|
51
|
+
"""Filter repo names by type or comma-separated list."""
|
|
52
|
+
from pennyfarthing_scripts.git.repos import RepoConfig
|
|
53
|
+
|
|
54
|
+
if filter_str in ("all", "both"):
|
|
55
|
+
return list(repos.keys())
|
|
56
|
+
|
|
57
|
+
# Check if it's a type filter
|
|
58
|
+
if filter_str in ("api", "ui", "adapter", "service"):
|
|
59
|
+
return [
|
|
60
|
+
name
|
|
61
|
+
for name, cfg in repos.items()
|
|
62
|
+
if isinstance(cfg, RepoConfig) and cfg.repo_type == filter_str
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
# Comma-separated list of names
|
|
66
|
+
return [n.strip() for n in filter_str.split(",") if n.strip()]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def create_worktree(name: str, branch: str, repos_filter: str = "all") -> int:
|
|
70
|
+
"""Create worktree(s) for parallel work.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
name: Worktree name (e.g., wt-5-3a)
|
|
74
|
+
branch: Branch name (e.g., feat/5-3a-file-upload)
|
|
75
|
+
repos_filter: Which repos to target (all, api, ui, or comma-separated)
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
0 on success, 1 on error
|
|
79
|
+
"""
|
|
80
|
+
project_root = get_project_root()
|
|
81
|
+
wt_root = _get_worktree_root(project_root)
|
|
82
|
+
wt_path = wt_root / name
|
|
83
|
+
|
|
84
|
+
if wt_path.exists():
|
|
85
|
+
print(f"Error: Worktree '{name}' already exists at {wt_path}")
|
|
86
|
+
return 1
|
|
87
|
+
|
|
88
|
+
repos = load_repos_config(project_root)
|
|
89
|
+
if not repos:
|
|
90
|
+
print("Error: No repositories configured in repos.yaml")
|
|
91
|
+
return 1
|
|
92
|
+
|
|
93
|
+
target_names = _filter_repos(repos, repos_filter)
|
|
94
|
+
if not target_names:
|
|
95
|
+
print(f"No repos match filter: {repos_filter}")
|
|
96
|
+
return 1
|
|
97
|
+
|
|
98
|
+
wt_path.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
|
|
100
|
+
print(f"Creating worktree: {name}")
|
|
101
|
+
print(f" Branch: {branch}")
|
|
102
|
+
print(f" Path: {wt_path}")
|
|
103
|
+
print(f" Repos: {repos_filter}")
|
|
104
|
+
print()
|
|
105
|
+
|
|
106
|
+
created = []
|
|
107
|
+
for repo_name in target_names:
|
|
108
|
+
if repo_name not in repos:
|
|
109
|
+
print(f" SKIP {repo_name} (not in config)")
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
cfg = repos[repo_name]
|
|
113
|
+
full_path = (project_root / cfg.path).resolve()
|
|
114
|
+
|
|
115
|
+
if not full_path.exists():
|
|
116
|
+
print(f" SKIP {repo_name} (path not found: {full_path})")
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
print(f"Creating worktree for {repo_name} ({cfg.repo_type})...")
|
|
120
|
+
repo_wt = wt_path / repo_name
|
|
121
|
+
|
|
122
|
+
# Check if branch exists locally or remotely
|
|
123
|
+
_, local_rc = _git(
|
|
124
|
+
["show-ref", "--verify", "--quiet", f"refs/heads/{branch}"],
|
|
125
|
+
full_path,
|
|
126
|
+
)
|
|
127
|
+
_, remote_rc = _git(
|
|
128
|
+
["show-ref", "--verify", "--quiet", f"refs/remotes/origin/{branch}"],
|
|
129
|
+
full_path,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if local_rc == 0 or remote_rc == 0:
|
|
133
|
+
_, rc = _git(["worktree", "add", str(repo_wt), branch], full_path)
|
|
134
|
+
else:
|
|
135
|
+
# Create new branch from default branch
|
|
136
|
+
base = cfg.default_branch
|
|
137
|
+
_, base_rc = _git(
|
|
138
|
+
["show-ref", "--verify", "--quiet", f"refs/heads/{base}"],
|
|
139
|
+
full_path,
|
|
140
|
+
)
|
|
141
|
+
if base_rc != 0:
|
|
142
|
+
base = "main"
|
|
143
|
+
_, rc = _git(
|
|
144
|
+
["worktree", "add", "-b", branch, str(repo_wt), base],
|
|
145
|
+
full_path,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if rc == 0:
|
|
149
|
+
print(f" OK {repo_name}")
|
|
150
|
+
created.append(repo_name)
|
|
151
|
+
else:
|
|
152
|
+
print(f" FAIL {repo_name}")
|
|
153
|
+
|
|
154
|
+
print()
|
|
155
|
+
if created:
|
|
156
|
+
print(f"Worktree '{name}' created successfully!")
|
|
157
|
+
print()
|
|
158
|
+
print("Next steps:")
|
|
159
|
+
for repo_name in created:
|
|
160
|
+
print(f" cd {wt_path / repo_name}")
|
|
161
|
+
else:
|
|
162
|
+
print("No worktrees created.")
|
|
163
|
+
return 1
|
|
164
|
+
|
|
165
|
+
return 0
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def remove_worktree(name: str) -> int:
|
|
169
|
+
"""Remove worktree and clean up.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
name: Worktree name to remove
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
0 on success, 1 on error
|
|
176
|
+
"""
|
|
177
|
+
project_root = get_project_root()
|
|
178
|
+
wt_root = _get_worktree_root(project_root)
|
|
179
|
+
wt_path = wt_root / name
|
|
180
|
+
|
|
181
|
+
if not wt_path.exists():
|
|
182
|
+
print(f"Error: Worktree '{name}' not found at {wt_path}")
|
|
183
|
+
return 1
|
|
184
|
+
|
|
185
|
+
print(f"Removing worktree: {name}")
|
|
186
|
+
|
|
187
|
+
repos = load_repos_config(project_root)
|
|
188
|
+
for repo_name, cfg in repos.items():
|
|
189
|
+
repo_wt = wt_path / repo_name
|
|
190
|
+
if repo_wt.exists():
|
|
191
|
+
full_path = (project_root / cfg.path).resolve()
|
|
192
|
+
print(f" Removing {repo_name} worktree...")
|
|
193
|
+
_git(["worktree", "remove", str(repo_wt), "--force"], full_path)
|
|
194
|
+
|
|
195
|
+
# Clean up directory
|
|
196
|
+
import shutil
|
|
197
|
+
|
|
198
|
+
if wt_path.exists():
|
|
199
|
+
shutil.rmtree(wt_path)
|
|
200
|
+
|
|
201
|
+
# Prune worktree references
|
|
202
|
+
for repo_name, cfg in repos.items():
|
|
203
|
+
full_path = (project_root / cfg.path).resolve()
|
|
204
|
+
if full_path.exists():
|
|
205
|
+
_git(["worktree", "prune"], full_path)
|
|
206
|
+
|
|
207
|
+
print()
|
|
208
|
+
print("Note: Session file (if any) should be archived via /sm finish")
|
|
209
|
+
print()
|
|
210
|
+
print(f"Worktree '{name}' removed successfully!")
|
|
211
|
+
return 0
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def list_worktrees() -> int:
|
|
215
|
+
"""List all active worktrees.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
0 always
|
|
219
|
+
"""
|
|
220
|
+
project_root = get_project_root()
|
|
221
|
+
wt_root = _get_worktree_root(project_root)
|
|
222
|
+
repos = load_repos_config(project_root)
|
|
223
|
+
|
|
224
|
+
print("=== Active Worktrees ===")
|
|
225
|
+
print()
|
|
226
|
+
|
|
227
|
+
for repo_name, cfg in repos.items():
|
|
228
|
+
full_path = (project_root / cfg.path).resolve()
|
|
229
|
+
if full_path.exists():
|
|
230
|
+
print(f"{repo_name} ({cfg.repo_type}):")
|
|
231
|
+
output, _ = _git(["worktree", "list"], full_path)
|
|
232
|
+
if output:
|
|
233
|
+
print(output)
|
|
234
|
+
print()
|
|
235
|
+
|
|
236
|
+
print("Worktree Directory:")
|
|
237
|
+
if wt_root.exists() and any(wt_root.iterdir()):
|
|
238
|
+
for item in sorted(wt_root.iterdir()):
|
|
239
|
+
if item.is_dir():
|
|
240
|
+
print(f" {item.name}/")
|
|
241
|
+
else:
|
|
242
|
+
print(" (empty)")
|
|
243
|
+
|
|
244
|
+
return 0
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def show_worktree_status() -> int:
|
|
248
|
+
"""Show detailed worktree status.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
0 always
|
|
252
|
+
"""
|
|
253
|
+
project_root = get_project_root()
|
|
254
|
+
wt_root = _get_worktree_root(project_root)
|
|
255
|
+
repos = load_repos_config(project_root)
|
|
256
|
+
|
|
257
|
+
print("=== Worktree Status ===")
|
|
258
|
+
print()
|
|
259
|
+
|
|
260
|
+
if not wt_root.exists() or not any(wt_root.iterdir()):
|
|
261
|
+
print("No active worktrees.")
|
|
262
|
+
print()
|
|
263
|
+
print("Create one with:")
|
|
264
|
+
print(" pf git worktree create <name> <branch>")
|
|
265
|
+
return 0
|
|
266
|
+
|
|
267
|
+
for wt_dir in sorted(wt_root.iterdir()):
|
|
268
|
+
if not wt_dir.is_dir():
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
wt_name = wt_dir.name
|
|
272
|
+
print(f"{wt_name}")
|
|
273
|
+
print(f" Path: {wt_dir}")
|
|
274
|
+
|
|
275
|
+
for repo_name, cfg in repos.items():
|
|
276
|
+
repo_wt = wt_dir / repo_name
|
|
277
|
+
if repo_wt.exists():
|
|
278
|
+
branch, _ = _git(["branch", "--show-current"], repo_wt)
|
|
279
|
+
status_out, _ = _git(["status", "--short"], repo_wt)
|
|
280
|
+
count = len([l for l in status_out.split("\n") if l.strip()]) if status_out else 0
|
|
281
|
+
print(f" {repo_name} ({cfg.repo_type}): {branch} ({count} uncommitted)")
|
|
282
|
+
|
|
283
|
+
# Check for session files referencing this worktree
|
|
284
|
+
session_dir = project_root / ".session"
|
|
285
|
+
found_session = None
|
|
286
|
+
if session_dir.exists():
|
|
287
|
+
for sf in session_dir.glob("*-session.md"):
|
|
288
|
+
try:
|
|
289
|
+
content = sf.read_text()
|
|
290
|
+
if f"worktree: {wt_name}" in content:
|
|
291
|
+
found_session = sf.name
|
|
292
|
+
break
|
|
293
|
+
except OSError:
|
|
294
|
+
pass
|
|
295
|
+
|
|
296
|
+
if found_session:
|
|
297
|
+
print(f" Session: .session/{found_session}")
|
|
298
|
+
else:
|
|
299
|
+
print(" Session: (no session file references this worktree)")
|
|
300
|
+
print()
|
|
301
|
+
|
|
302
|
+
return 0
|
|
Binary file
|