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/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")