taskflow-git 0.3.0__py3-none-any.whl
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.
- taskflow/__init__.py +3 -0
- taskflow/archive.py +135 -0
- taskflow/cli.py +550 -0
- taskflow/config.py +195 -0
- taskflow/reports.py +284 -0
- taskflow/setup_cmd.py +305 -0
- taskflow/tasklib.py +451 -0
- taskflow_git-0.3.0.dist-info/METADATA +448 -0
- taskflow_git-0.3.0.dist-info/RECORD +11 -0
- taskflow_git-0.3.0.dist-info/WHEEL +4 -0
- taskflow_git-0.3.0.dist-info/entry_points.txt +2 -0
taskflow/setup_cmd.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""
|
|
2
|
+
setup_cmd.py — taskflow init and setup.
|
|
3
|
+
|
|
4
|
+
init creates a new .taskflow.yml in the current directory.
|
|
5
|
+
setup regenerates backlog file skeletons and installs git aliases.
|
|
6
|
+
|
|
7
|
+
Both write files relative to the project root — setup uses the discovered
|
|
8
|
+
root, init uses cwd since there's no root yet.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
import subprocess
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
|
|
19
|
+
from taskflow.config import CONFIG_FILE, TaskflowConfig
|
|
20
|
+
|
|
21
|
+
# starter config written by taskflow init
|
|
22
|
+
STARTER_CONFIG = """\
|
|
23
|
+
# .taskflow.yml
|
|
24
|
+
# Run `taskflow setup` after editing to regenerate your backlog.
|
|
25
|
+
# All sections except categories and phases are optional.
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# States — file paths and terminal icons for each task state.
|
|
29
|
+
# Paths are relative to this file unless absolute.
|
|
30
|
+
# Remove this section entirely to use the defaults shown here.
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
states:
|
|
34
|
+
now:
|
|
35
|
+
file: "backlog/0-now.md"
|
|
36
|
+
icon: "▶"
|
|
37
|
+
blocked:
|
|
38
|
+
file: "backlog/1-blocked.md"
|
|
39
|
+
icon: "⊘"
|
|
40
|
+
paused:
|
|
41
|
+
file: "backlog/2-paused.md"
|
|
42
|
+
icon: "⏸"
|
|
43
|
+
next:
|
|
44
|
+
file: "backlog/3-next.md"
|
|
45
|
+
icon: "◈"
|
|
46
|
+
later:
|
|
47
|
+
file: "backlog/4-later.md"
|
|
48
|
+
icon: "◇"
|
|
49
|
+
done:
|
|
50
|
+
file: "backlog/done.md"
|
|
51
|
+
icon: "✓"
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Categories — define whatever makes sense for your project.
|
|
55
|
+
# Icons are optional. Colored circles work well: 🔴 🟠 🟡 🟢 🔵 🟣 ⚫ ⚪ 🟤
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
categories:
|
|
59
|
+
- name: Engineering
|
|
60
|
+
icon: "🔵"
|
|
61
|
+
aliases: []
|
|
62
|
+
|
|
63
|
+
- name: Operations
|
|
64
|
+
icon: "🔴"
|
|
65
|
+
aliases: []
|
|
66
|
+
|
|
67
|
+
- name: Product
|
|
68
|
+
icon: "🟡"
|
|
69
|
+
aliases: []
|
|
70
|
+
|
|
71
|
+
- name: Business
|
|
72
|
+
icon: "⚫"
|
|
73
|
+
aliases: []
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
# Phases — organise 4-later.md into planning horizons.
|
|
77
|
+
# No effect on any other backlog file.
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
phases:
|
|
81
|
+
- name: "Phase 1 — Foundation"
|
|
82
|
+
description: "Core work needed to get started."
|
|
83
|
+
|
|
84
|
+
- name: "Phase 2 — Build"
|
|
85
|
+
description: "Main execution phase."
|
|
86
|
+
|
|
87
|
+
- name: "Phase 3 — Growth"
|
|
88
|
+
description: "Expansion and refinement."
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Settings
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
settings:
|
|
95
|
+
repo_name: "{repo_name}"
|
|
96
|
+
done_weeks: 4
|
|
97
|
+
weekly_plan_dir: "changelog/weekly"
|
|
98
|
+
# archive_path defaults to backlog/archive/ — uncomment to override
|
|
99
|
+
# archive_path: "backlog/archive"
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
WEEK_PLAN_BLOCK_START = "# === TASKFLOW GENERATED — do not edit below this line ==="
|
|
103
|
+
WEEK_PLAN_BLOCK_END = "# === END TASKFLOW GENERATED ==="
|
|
104
|
+
|
|
105
|
+
SIMPLE_STATE_TITLES = {
|
|
106
|
+
"now": ("Now", "Tasks actively being executed this week."),
|
|
107
|
+
"blocked": (
|
|
108
|
+
"Blocked",
|
|
109
|
+
"Tasks waiting on something external.\n\nNote what's blocking each one.",
|
|
110
|
+
),
|
|
111
|
+
"paused": (
|
|
112
|
+
"Paused",
|
|
113
|
+
"Tasks deliberately on hold.\n\nNot blocked, not forgotten — just not now.",
|
|
114
|
+
),
|
|
115
|
+
"next": ("Next", "Planned work queued for the next execution window."),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _category_heading(cat: dict) -> str:
|
|
120
|
+
icon = cat.get("icon", "")
|
|
121
|
+
name = cat["name"]
|
|
122
|
+
return f"### {icon} {name}".strip() if icon else f"### {name}"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _build_simple_file(title: str, description: str, categories: list[dict]) -> str:
|
|
126
|
+
lines = [f"# {title}", "", description.strip(), ""]
|
|
127
|
+
for cat in categories:
|
|
128
|
+
lines += ["---", "", _category_heading(cat), ""]
|
|
129
|
+
lines.append("---")
|
|
130
|
+
return "\n".join(lines) + "\n"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _build_later_file(categories: list[dict], phases: list[dict]) -> str:
|
|
134
|
+
cat_by_name = {c["name"]: c for c in categories}
|
|
135
|
+
lines = [
|
|
136
|
+
"# Later",
|
|
137
|
+
"",
|
|
138
|
+
"Longer-horizon work organised by phase.",
|
|
139
|
+
"",
|
|
140
|
+
"Promote to next when the time is right:",
|
|
141
|
+
"",
|
|
142
|
+
"```",
|
|
143
|
+
'taskflow promote "task text"',
|
|
144
|
+
"```",
|
|
145
|
+
"",
|
|
146
|
+
]
|
|
147
|
+
for phase in phases:
|
|
148
|
+
lines += ["---", "", f"## {phase['name']}"]
|
|
149
|
+
if phase.get("description"):
|
|
150
|
+
lines += ["", f"> {phase['description']}"]
|
|
151
|
+
lines.append("")
|
|
152
|
+
phase_cats = phase.get("categories") or [c["name"] for c in categories]
|
|
153
|
+
for name in phase_cats:
|
|
154
|
+
cat = cat_by_name.get(name)
|
|
155
|
+
if cat:
|
|
156
|
+
lines += [_category_heading(cat), ""]
|
|
157
|
+
lines.append("---")
|
|
158
|
+
return "\n".join(lines) + "\n"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _build_week_plan_block(categories: list[dict]) -> str:
|
|
162
|
+
alias_lines = ["CATEGORY_ALIASES = {"]
|
|
163
|
+
for cat in categories:
|
|
164
|
+
name = cat["name"]
|
|
165
|
+
for alias in cat.get("aliases", []):
|
|
166
|
+
alias_lines.append(f" {alias!r}: {name!r},")
|
|
167
|
+
alias_lines.append(f" {name!r}: {name!r},")
|
|
168
|
+
alias_lines.append("}")
|
|
169
|
+
|
|
170
|
+
order = "DEFAULT_CATEGORY_ORDER = [\n"
|
|
171
|
+
for cat in categories:
|
|
172
|
+
order += f" {cat['name']!r},\n"
|
|
173
|
+
order += "]"
|
|
174
|
+
|
|
175
|
+
return f"{WEEK_PLAN_BLOCK_START}\n" + "\n".join(alias_lines) + f"\n\n{order}\n" + f"{WEEK_PLAN_BLOCK_END}\n"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def patch_week_plan(week_plan_path: Path, categories: list[dict], dry_run: bool) -> bool:
|
|
179
|
+
"""Regenerate the TASKFLOW GENERATED block in week-plan. Returns True if changed."""
|
|
180
|
+
if not week_plan_path.exists():
|
|
181
|
+
return False
|
|
182
|
+
content = week_plan_path.read_text(encoding="utf-8")
|
|
183
|
+
new_block = _build_week_plan_block(categories)
|
|
184
|
+
|
|
185
|
+
if WEEK_PLAN_BLOCK_START in content:
|
|
186
|
+
pattern = re.compile(
|
|
187
|
+
re.escape(WEEK_PLAN_BLOCK_START) + r".*?" + re.escape(WEEK_PLAN_BLOCK_END) + r"\n?",
|
|
188
|
+
re.DOTALL,
|
|
189
|
+
)
|
|
190
|
+
new_content = pattern.sub(new_block, content)
|
|
191
|
+
else:
|
|
192
|
+
new_content = content.rstrip() + "\n\n" + new_block
|
|
193
|
+
|
|
194
|
+
if new_content == content:
|
|
195
|
+
return False
|
|
196
|
+
if not dry_run:
|
|
197
|
+
week_plan_path.write_text(new_content, encoding="utf-8")
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def install_git_aliases(root: Path) -> None:
|
|
202
|
+
"""
|
|
203
|
+
Install git aliases that just call `taskflow <subcommand>`.
|
|
204
|
+
Once taskflow is on PATH the aliases are trivial — no path resolution needed.
|
|
205
|
+
"""
|
|
206
|
+
transitions = [
|
|
207
|
+
("promote", "promote"),
|
|
208
|
+
("start", "start"),
|
|
209
|
+
("block", "block"),
|
|
210
|
+
("unblock", "unblock"),
|
|
211
|
+
("pause", "pause"),
|
|
212
|
+
("unpause", "unpause"),
|
|
213
|
+
("backlog", "backlog"),
|
|
214
|
+
("done", "done"),
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
for alias, subcmd in transitions:
|
|
219
|
+
subprocess.run(
|
|
220
|
+
["git", "config", f"alias.{alias}", f"!taskflow {subcmd}"],
|
|
221
|
+
cwd=str(root),
|
|
222
|
+
check=True,
|
|
223
|
+
capture_output=True,
|
|
224
|
+
)
|
|
225
|
+
click.echo(" git aliases installed")
|
|
226
|
+
except subprocess.CalledProcessError as e:
|
|
227
|
+
click.echo(f" warning: could not install git aliases: {e}", err=True)
|
|
228
|
+
except FileNotFoundError:
|
|
229
|
+
click.echo(" warning: git not found — skipping alias installation", err=True)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def run_setup(config: TaskflowConfig, force: bool = False, dry_run: bool = False) -> None:
|
|
233
|
+
"""
|
|
234
|
+
Regenerate backlog file skeletons from config.
|
|
235
|
+
Skips existing files unless force=True. Respects dry_run.
|
|
236
|
+
"""
|
|
237
|
+
categories = config.categories
|
|
238
|
+
phases = config.phases
|
|
239
|
+
root = config.root
|
|
240
|
+
|
|
241
|
+
click.echo(f"\ntaskflow setup — {config.repo_name}")
|
|
242
|
+
click.echo(f" {len(categories)} categories, {len(phases)} phases\n")
|
|
243
|
+
|
|
244
|
+
changed = []
|
|
245
|
+
skipped = []
|
|
246
|
+
|
|
247
|
+
for state_name, (title, description) in SIMPLE_STATE_TITLES.items():
|
|
248
|
+
path = config.state_path(state_name)
|
|
249
|
+
content = _build_simple_file(title, description, categories)
|
|
250
|
+
if not dry_run:
|
|
251
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
252
|
+
if path.exists() and not force:
|
|
253
|
+
skipped.append(str(path.relative_to(root)))
|
|
254
|
+
else:
|
|
255
|
+
if not dry_run:
|
|
256
|
+
path.write_text(content, encoding="utf-8")
|
|
257
|
+
changed.append(str(path.relative_to(root)))
|
|
258
|
+
|
|
259
|
+
later_path = config.state_path("later")
|
|
260
|
+
later_content = _build_later_file(categories, phases)
|
|
261
|
+
if not dry_run:
|
|
262
|
+
later_path.parent.mkdir(parents=True, exist_ok=True)
|
|
263
|
+
if later_path.exists() and not force:
|
|
264
|
+
skipped.append(str(later_path.relative_to(root)))
|
|
265
|
+
else:
|
|
266
|
+
if not dry_run:
|
|
267
|
+
later_path.write_text(later_content, encoding="utf-8")
|
|
268
|
+
changed.append(str(later_path.relative_to(root)))
|
|
269
|
+
|
|
270
|
+
done_path = config.state_path("done")
|
|
271
|
+
if not dry_run:
|
|
272
|
+
done_path.parent.mkdir(parents=True, exist_ok=True)
|
|
273
|
+
if not done_path.exists():
|
|
274
|
+
if not dry_run:
|
|
275
|
+
done_path.write_text(
|
|
276
|
+
"# Done\n\nCompleted tasks, appended by `taskflow done`.\n\n---\n",
|
|
277
|
+
encoding="utf-8",
|
|
278
|
+
)
|
|
279
|
+
changed.append(str(done_path.relative_to(root)))
|
|
280
|
+
|
|
281
|
+
weekly_dir = config.weekly_plan_dir
|
|
282
|
+
if not weekly_dir.exists():
|
|
283
|
+
if not dry_run:
|
|
284
|
+
weekly_dir.mkdir(parents=True, exist_ok=True)
|
|
285
|
+
(weekly_dir / ".gitkeep").touch()
|
|
286
|
+
changed.append(str(weekly_dir.relative_to(root)) + "/")
|
|
287
|
+
|
|
288
|
+
# archive dir — just make sure it exists
|
|
289
|
+
archive_dir = config.archive_path
|
|
290
|
+
if not archive_dir.exists() and not dry_run:
|
|
291
|
+
archive_dir.mkdir(parents=True, exist_ok=True)
|
|
292
|
+
|
|
293
|
+
if not dry_run:
|
|
294
|
+
install_git_aliases(root)
|
|
295
|
+
|
|
296
|
+
if changed:
|
|
297
|
+
click.echo(" created/updated:")
|
|
298
|
+
for f in changed:
|
|
299
|
+
click.echo(f" + {f}")
|
|
300
|
+
if skipped:
|
|
301
|
+
click.echo(" skipped (already exist — use --force to overwrite):")
|
|
302
|
+
for f in skipped:
|
|
303
|
+
click.echo(f" ~ {f}")
|
|
304
|
+
|
|
305
|
+
click.echo(f"\n Done. Edit {CONFIG_FILE} and re-run `taskflow setup` to regenerate.\n")
|