lazyplan-tui 0.7.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.
- lazyplan/__init__.py +15 -0
- lazyplan/__main__.py +4 -0
- lazyplan/cli.py +530 -0
- lazyplan/cli_parser.py +240 -0
- lazyplan/design.py +162 -0
- lazyplan/gantt.py +37 -0
- lazyplan/models.py +203 -0
- lazyplan/postgres_sync.py +121 -0
- lazyplan/render.py +175 -0
- lazyplan/repo.py +101 -0
- lazyplan/screens.py +437 -0
- lazyplan/seed.py +36 -0
- lazyplan/skills.py +415 -0
- lazyplan/store.py +1448 -0
- lazyplan/tui.py +711 -0
- lazyplan_tui-0.7.0.dist-info/METADATA +172 -0
- lazyplan_tui-0.7.0.dist-info/RECORD +21 -0
- lazyplan_tui-0.7.0.dist-info/WHEEL +5 -0
- lazyplan_tui-0.7.0.dist-info/entry_points.txt +2 -0
- lazyplan_tui-0.7.0.dist-info/licenses/LICENSE +21 -0
- lazyplan_tui-0.7.0.dist-info/top_level.txt +1 -0
lazyplan/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""lazyplan: Projects > UserStories > Tasks. TUI first, agent-injectable."""
|
|
2
|
+
|
|
3
|
+
from .models import Criterion, Link, Milestone, Project, Risk, RiskStatus, Status, Task, UserStory
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"Criterion",
|
|
7
|
+
"Link",
|
|
8
|
+
"Milestone",
|
|
9
|
+
"Project",
|
|
10
|
+
"Risk",
|
|
11
|
+
"RiskStatus",
|
|
12
|
+
"Status",
|
|
13
|
+
"Task",
|
|
14
|
+
"UserStory",
|
|
15
|
+
]
|
lazyplan/__main__.py
ADDED
lazyplan/cli.py
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
"""`lp` companion CLI. Human + agent ingress: every mutation writes SQLite;
|
|
2
|
+
the running TUI auto-refreshes. Stdlib only (no Textual import here)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
from .cli_parser import build_parser
|
|
13
|
+
from .design import (
|
|
14
|
+
EMPTY_FEED,
|
|
15
|
+
EMPTY_NO_PROJECTS,
|
|
16
|
+
HELP_TEXT,
|
|
17
|
+
msg_error,
|
|
18
|
+
msg_not_found,
|
|
19
|
+
)
|
|
20
|
+
from .gantt import bar, span
|
|
21
|
+
from .models import Project, Status, Task, UserStory
|
|
22
|
+
from .render import render_full, render_markdown, tree_lines
|
|
23
|
+
from .repo import ensure_plan_dir, find_repo_root, repo_label
|
|
24
|
+
from .seed import seed as seed_demo
|
|
25
|
+
from .skills import handle_init as _init_skills
|
|
26
|
+
from .store import Store, _ser
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _emit(args: argparse.Namespace, payload: object) -> int:
|
|
30
|
+
print(json.dumps(payload, indent=2, default=str))
|
|
31
|
+
return 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main(argv: list[str] | None = None) -> int:
|
|
35
|
+
"""Dispatch one lp invocation; exit codes are agent-readable."""
|
|
36
|
+
args = build_parser().parse_args(argv)
|
|
37
|
+
# `watch` is `feed --follow`
|
|
38
|
+
if args.cmd == "watch":
|
|
39
|
+
args.cmd = "feed"
|
|
40
|
+
args.follow = True
|
|
41
|
+
args.interval = 1.0
|
|
42
|
+
args.json = getattr(args, "json", False)
|
|
43
|
+
store = Store(args.db)
|
|
44
|
+
actor = getattr(args, "actor", "human")
|
|
45
|
+
try:
|
|
46
|
+
if args.cmd == "init":
|
|
47
|
+
plan_dir = ensure_plan_dir(store.path)
|
|
48
|
+
root = find_repo_root(os.getcwd())
|
|
49
|
+
skills_info = _init_skills(args, root)
|
|
50
|
+
return _emit(
|
|
51
|
+
args,
|
|
52
|
+
{
|
|
53
|
+
"db": str(store.path),
|
|
54
|
+
"repo": str(root) if root else None,
|
|
55
|
+
"plan_dir": str(plan_dir),
|
|
56
|
+
"scope": repo_label(store.path),
|
|
57
|
+
"counts": store.counts(),
|
|
58
|
+
"skills": skills_info,
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
if args.cmd == "info":
|
|
62
|
+
root = find_repo_root(os.getcwd())
|
|
63
|
+
plan_dir = store.path.parent
|
|
64
|
+
try:
|
|
65
|
+
contents = sorted(f.name for f in plan_dir.iterdir()) if plan_dir.is_dir() else []
|
|
66
|
+
except OSError:
|
|
67
|
+
contents = []
|
|
68
|
+
return _emit(
|
|
69
|
+
args,
|
|
70
|
+
{
|
|
71
|
+
"db": str(store.path),
|
|
72
|
+
"repo": str(root) if root else None,
|
|
73
|
+
"scope": repo_label(store.path),
|
|
74
|
+
"plan_files": contents,
|
|
75
|
+
"counts": store.counts(),
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
if args.cmd == "seed":
|
|
79
|
+
seed_demo(store)
|
|
80
|
+
return _emit(args, {"db": str(store.path), "counts": store.counts()})
|
|
81
|
+
if args.cmd == "list":
|
|
82
|
+
status = Status(args.status) if args.status else None
|
|
83
|
+
if args.json:
|
|
84
|
+
payload = [
|
|
85
|
+
{
|
|
86
|
+
"project": _ser(vars(p)),
|
|
87
|
+
"stories": [
|
|
88
|
+
{
|
|
89
|
+
"story": _ser(vars(s)),
|
|
90
|
+
"links": [vars(li) for li in store.list_links("story", s.id)],
|
|
91
|
+
"criteria": [vars(c) for c in store.list_criteria("story", s.id)],
|
|
92
|
+
"tasks": [
|
|
93
|
+
{
|
|
94
|
+
"task": _ser(vars(t)),
|
|
95
|
+
"links": [
|
|
96
|
+
vars(li) for li in store.list_links("task", t.id)
|
|
97
|
+
],
|
|
98
|
+
"criteria": [
|
|
99
|
+
vars(c) for c in store.list_criteria("task", t.id)
|
|
100
|
+
],
|
|
101
|
+
}
|
|
102
|
+
for t in ts
|
|
103
|
+
],
|
|
104
|
+
}
|
|
105
|
+
for s, ts in stories
|
|
106
|
+
],
|
|
107
|
+
}
|
|
108
|
+
for p, stories in store.tree(
|
|
109
|
+
status=status,
|
|
110
|
+
project_id=args.project,
|
|
111
|
+
owner=args.owner,
|
|
112
|
+
priority=args.priority,
|
|
113
|
+
)
|
|
114
|
+
]
|
|
115
|
+
return _emit(args, payload)
|
|
116
|
+
lines = tree_lines(store, status, args.project, args.owner, args.priority)
|
|
117
|
+
print("\n".join(lines) if lines else EMPTY_NO_PROJECTS)
|
|
118
|
+
return 0
|
|
119
|
+
if args.cmd == "gantt":
|
|
120
|
+
status = Status(args.status) if args.status else None
|
|
121
|
+
tree = store.tree(
|
|
122
|
+
status=status, project_id=args.project, owner=args.owner, priority=args.priority
|
|
123
|
+
)
|
|
124
|
+
ms_by_project: dict[int, list] = {}
|
|
125
|
+
dates = []
|
|
126
|
+
for _, stories in tree:
|
|
127
|
+
for s, tasks in stories:
|
|
128
|
+
dates += [s.start_date, s.end_date]
|
|
129
|
+
if args.tasks:
|
|
130
|
+
for t in tasks:
|
|
131
|
+
dates += [t.start_date, t.end_date]
|
|
132
|
+
if args.project is not None:
|
|
133
|
+
ms_by_project[args.project] = store.list_milestones(args.project)
|
|
134
|
+
else:
|
|
135
|
+
for p, _ in tree:
|
|
136
|
+
ms_by_project[p.id] = store.list_milestones(p.id)
|
|
137
|
+
for ms in ms_by_project.values():
|
|
138
|
+
dates += [m.date for m in ms if m.date]
|
|
139
|
+
dates = [d for d in dates if d]
|
|
140
|
+
lo, hi = span(dates, args.width)
|
|
141
|
+
print(f"{lo} .. {hi} (w={args.width})")
|
|
142
|
+
for p, stories in tree:
|
|
143
|
+
print(f"P#{p.id} {p.name}")
|
|
144
|
+
crit_ids = set(store.critical_path(p.id))
|
|
145
|
+
for m in ms_by_project.get(p.id, []):
|
|
146
|
+
print(f" ◆ {m.date or '????-??-??'} M#{m.id} {m.title}")
|
|
147
|
+
for s, tasks in stories:
|
|
148
|
+
mark = ("*" if s.id in crit_ids else " ") + (
|
|
149
|
+
"!" if store.is_blocked("story", s.id) else " "
|
|
150
|
+
)
|
|
151
|
+
print(
|
|
152
|
+
f"{mark} S#{s.id:>3} {bar(s.start_date, s.end_date, lo, hi, args.width)} {s.title} [{s.status.value}]"
|
|
153
|
+
)
|
|
154
|
+
if args.tasks:
|
|
155
|
+
for t in tasks:
|
|
156
|
+
tmark = " " + ("!" if store.is_blocked("task", t.id) else " ")
|
|
157
|
+
b = (
|
|
158
|
+
bar(t.start_date, t.end_date, lo, hi, args.width)
|
|
159
|
+
if (t.start_date and t.end_date)
|
|
160
|
+
else "(no dates)"
|
|
161
|
+
)
|
|
162
|
+
print(f"{tmark} T#{t.id:>3} {b} {t.title} [{t.status.value}]")
|
|
163
|
+
if not tree:
|
|
164
|
+
print(EMPTY_NO_PROJECTS)
|
|
165
|
+
return 0
|
|
166
|
+
if args.cmd == "help":
|
|
167
|
+
if args.json:
|
|
168
|
+
return _emit(args, {"help": HELP_TEXT})
|
|
169
|
+
print(HELP_TEXT)
|
|
170
|
+
return 0
|
|
171
|
+
if args.cmd == "add-project":
|
|
172
|
+
p = store.add_project(
|
|
173
|
+
args.name,
|
|
174
|
+
args.desc,
|
|
175
|
+
args.start,
|
|
176
|
+
args.end,
|
|
177
|
+
goal=args.goal,
|
|
178
|
+
success_metrics=args.metrics,
|
|
179
|
+
scope_in=args.scope_in,
|
|
180
|
+
scope_out=args.scope_out,
|
|
181
|
+
assumptions=args.assumptions,
|
|
182
|
+
actor=actor,
|
|
183
|
+
)
|
|
184
|
+
return _emit(args, {"project": _ser(vars(p))})
|
|
185
|
+
if args.cmd == "add-story":
|
|
186
|
+
s = store.add_story(
|
|
187
|
+
args.project,
|
|
188
|
+
args.title,
|
|
189
|
+
args.desc,
|
|
190
|
+
args.start,
|
|
191
|
+
args.end,
|
|
192
|
+
owner=args.owner,
|
|
193
|
+
priority=args.priority,
|
|
194
|
+
labels=args.labels,
|
|
195
|
+
estimate=args.estimate,
|
|
196
|
+
blocked_by=args.blocked_by,
|
|
197
|
+
actor=actor,
|
|
198
|
+
)
|
|
199
|
+
return _emit(args, {"story": _ser(vars(s))})
|
|
200
|
+
if args.cmd == "add-task":
|
|
201
|
+
t = store.add_task(
|
|
202
|
+
args.story,
|
|
203
|
+
args.title,
|
|
204
|
+
args.estimate,
|
|
205
|
+
args.start,
|
|
206
|
+
args.end,
|
|
207
|
+
description=args.desc,
|
|
208
|
+
owner=args.owner,
|
|
209
|
+
priority=args.priority,
|
|
210
|
+
labels=args.labels,
|
|
211
|
+
blocked_by=args.blocked_by,
|
|
212
|
+
actor=actor,
|
|
213
|
+
)
|
|
214
|
+
return _emit(args, {"task": _ser(vars(t))})
|
|
215
|
+
if args.cmd == "update":
|
|
216
|
+
title = args.title or args.name
|
|
217
|
+
priority = (
|
|
218
|
+
None
|
|
219
|
+
if args.priority is None
|
|
220
|
+
else ("" if args.priority == "none" else args.priority)
|
|
221
|
+
)
|
|
222
|
+
updated: Project | UserStory | Task | None = None
|
|
223
|
+
if args.kind == "project":
|
|
224
|
+
updated = store.update_project(
|
|
225
|
+
args.id,
|
|
226
|
+
name=title,
|
|
227
|
+
description=args.desc,
|
|
228
|
+
start_date=args.start,
|
|
229
|
+
end_date=args.end,
|
|
230
|
+
goal=args.goal,
|
|
231
|
+
success_metrics=args.metrics,
|
|
232
|
+
scope_in=args.scope_in,
|
|
233
|
+
scope_out=args.scope_out,
|
|
234
|
+
assumptions=args.assumptions,
|
|
235
|
+
actor=actor,
|
|
236
|
+
)
|
|
237
|
+
elif args.kind == "story":
|
|
238
|
+
updated = store.update_story(
|
|
239
|
+
args.id,
|
|
240
|
+
title=title,
|
|
241
|
+
description=args.desc,
|
|
242
|
+
start_date=args.start,
|
|
243
|
+
end_date=args.end,
|
|
244
|
+
owner=args.owner,
|
|
245
|
+
priority=priority,
|
|
246
|
+
labels=args.labels,
|
|
247
|
+
estimate=args.estimate,
|
|
248
|
+
blocked_by=args.blocked_by,
|
|
249
|
+
actor=actor,
|
|
250
|
+
)
|
|
251
|
+
else:
|
|
252
|
+
updated = store.update_task(
|
|
253
|
+
args.id,
|
|
254
|
+
title=title,
|
|
255
|
+
estimate=args.estimate,
|
|
256
|
+
start_date=args.start,
|
|
257
|
+
end_date=args.end,
|
|
258
|
+
description=args.desc,
|
|
259
|
+
owner=args.owner,
|
|
260
|
+
priority=priority,
|
|
261
|
+
labels=args.labels,
|
|
262
|
+
blocked_by=args.blocked_by,
|
|
263
|
+
actor=actor,
|
|
264
|
+
)
|
|
265
|
+
if updated is None:
|
|
266
|
+
print(msg_not_found(args.kind, args.id), file=sys.stderr)
|
|
267
|
+
return 1
|
|
268
|
+
return _emit(args, {"updated": _ser(vars(updated))})
|
|
269
|
+
if args.cmd == "show":
|
|
270
|
+
full = store.get_full(args.kind, args.id)
|
|
271
|
+
if full is None:
|
|
272
|
+
print(msg_not_found(args.kind, args.id), file=sys.stderr)
|
|
273
|
+
return 1
|
|
274
|
+
if args.json:
|
|
275
|
+
return _emit(args, full)
|
|
276
|
+
print(render_full(args.kind, full))
|
|
277
|
+
return 0
|
|
278
|
+
if args.cmd == "add-link":
|
|
279
|
+
link = store.add_link(args.kind, args.id, args.url, args.label, actor=actor)
|
|
280
|
+
return _emit(args, {"link": vars(link)})
|
|
281
|
+
if args.cmd == "links":
|
|
282
|
+
found = store.get_story(args.id) if args.kind == "story" else store.get_task(args.id)
|
|
283
|
+
if found is None:
|
|
284
|
+
print(msg_not_found(args.kind, args.id), file=sys.stderr)
|
|
285
|
+
return 1
|
|
286
|
+
return _emit(args, [vars(li) for li in store.list_links(args.kind, args.id)])
|
|
287
|
+
if args.cmd == "delete-link":
|
|
288
|
+
if not store.delete_link(args.id, actor=actor):
|
|
289
|
+
print(msg_not_found("link", args.id), file=sys.stderr)
|
|
290
|
+
return 1
|
|
291
|
+
return _emit(args, {"ok": True, "deleted": f"link #{args.id}"})
|
|
292
|
+
if args.cmd == "status":
|
|
293
|
+
if args.to == "done" and args.kind in ("story", "task"):
|
|
294
|
+
left = store.open_criteria(args.kind, args.id)
|
|
295
|
+
if left:
|
|
296
|
+
print(
|
|
297
|
+
f"warning: {len(left)} open acceptance criteria "
|
|
298
|
+
f"({', '.join(c.text[:40] for c in left[:3])}); closing anyway",
|
|
299
|
+
file=sys.stderr,
|
|
300
|
+
)
|
|
301
|
+
ok = store.set_status(args.kind, args.id, Status(args.to), actor=actor)
|
|
302
|
+
if not ok:
|
|
303
|
+
print(msg_not_found(args.kind, args.id), file=sys.stderr)
|
|
304
|
+
return 1
|
|
305
|
+
return _emit(args, {"ok": True, "kind": args.kind, "id": args.id, "to": args.to})
|
|
306
|
+
if args.cmd == "delete":
|
|
307
|
+
ok = store.delete(args.kind, args.id, actor=actor)
|
|
308
|
+
if not ok:
|
|
309
|
+
print(msg_not_found(args.kind, args.id), file=sys.stderr)
|
|
310
|
+
return 1
|
|
311
|
+
return _emit(args, {"ok": True, "deleted": f"{args.kind} #{args.id}"})
|
|
312
|
+
if args.cmd == "add-criteria":
|
|
313
|
+
c = store.add_criteria(args.kind, args.id, args.text, actor=actor)
|
|
314
|
+
return _emit(args, {"criteria": vars(c)})
|
|
315
|
+
if args.cmd == "criteria":
|
|
316
|
+
found = store.get_story(args.id) if args.kind == "story" else store.get_task(args.id)
|
|
317
|
+
if found is None:
|
|
318
|
+
print(msg_not_found(args.kind, args.id), file=sys.stderr)
|
|
319
|
+
return 1
|
|
320
|
+
return _emit(args, [vars(c) for c in store.list_criteria(args.kind, args.id)])
|
|
321
|
+
if args.cmd in ("check", "uncheck"):
|
|
322
|
+
crit = store.set_criteria(args.id, args.cmd == "check", actor=actor)
|
|
323
|
+
if crit is None:
|
|
324
|
+
print(msg_not_found("criteria", args.id), file=sys.stderr)
|
|
325
|
+
return 1
|
|
326
|
+
return _emit(args, {"criteria": vars(crit)})
|
|
327
|
+
if args.cmd == "delete-criteria":
|
|
328
|
+
if not store.delete_criteria(args.id, actor=actor):
|
|
329
|
+
print(msg_not_found("criteria", args.id), file=sys.stderr)
|
|
330
|
+
return 1
|
|
331
|
+
return _emit(args, {"ok": True, "deleted": f"criteria #{args.id}"})
|
|
332
|
+
if args.cmd == "add-risk":
|
|
333
|
+
from .models import RiskStatus as _RS
|
|
334
|
+
|
|
335
|
+
r = store.add_risk(
|
|
336
|
+
args.project, args.title, args.likelihood, args.impact, args.mitigation, actor=actor
|
|
337
|
+
)
|
|
338
|
+
d = vars(r)
|
|
339
|
+
d["status"] = r.status.value
|
|
340
|
+
return _emit(args, {"risk": d})
|
|
341
|
+
if args.cmd == "risks":
|
|
342
|
+
if store.get_project(args.project) is None:
|
|
343
|
+
print(msg_not_found("project", args.project), file=sys.stderr)
|
|
344
|
+
return 1
|
|
345
|
+
return _emit(args, [store._risk_to_dict(r) for r in store.list_risks(args.project)])
|
|
346
|
+
if args.cmd == "risk-status":
|
|
347
|
+
from .models import RiskStatus as _RS
|
|
348
|
+
|
|
349
|
+
to = _RS(args.to) if args.to else None
|
|
350
|
+
risk = store.set_risk(args.id, status=to, mitigation=args.mitigation, actor=actor)
|
|
351
|
+
if risk is None:
|
|
352
|
+
print(msg_not_found("risk", args.id), file=sys.stderr)
|
|
353
|
+
return 1
|
|
354
|
+
return _emit(args, {"risk": store._risk_to_dict(risk)})
|
|
355
|
+
if args.cmd == "add-milestone":
|
|
356
|
+
m = store.add_milestone(args.project, args.title, args.date, actor=actor)
|
|
357
|
+
return _emit(args, {"milestone": vars(m)})
|
|
358
|
+
if args.cmd == "milestones":
|
|
359
|
+
if store.get_project(args.project) is None:
|
|
360
|
+
print(msg_not_found("project", args.project), file=sys.stderr)
|
|
361
|
+
return 1
|
|
362
|
+
return _emit(args, [vars(m) for m in store.list_milestones(args.project)])
|
|
363
|
+
if args.cmd == "delete-milestone":
|
|
364
|
+
if not store.delete_milestone(args.id, actor=actor):
|
|
365
|
+
print(msg_not_found("milestone", args.id), file=sys.stderr)
|
|
366
|
+
return 1
|
|
367
|
+
return _emit(args, {"ok": True, "deleted": f"milestone #{args.id}"})
|
|
368
|
+
if args.cmd == "propose":
|
|
369
|
+
fields = {
|
|
370
|
+
"title": args.title,
|
|
371
|
+
"description": args.desc,
|
|
372
|
+
"owner": args.owner,
|
|
373
|
+
"priority": args.priority,
|
|
374
|
+
"labels": args.labels,
|
|
375
|
+
"estimate": args.estimate,
|
|
376
|
+
"start_date": args.start,
|
|
377
|
+
"end_date": args.end,
|
|
378
|
+
"blocked_by": args.blocked_by,
|
|
379
|
+
}
|
|
380
|
+
return _emit(args, store.propose(args.kind, args.parent, fields, actor=actor))
|
|
381
|
+
if args.cmd == "proposals":
|
|
382
|
+
return _emit(args, store.list_proposals(args.status))
|
|
383
|
+
if args.cmd in ("approve", "reject"):
|
|
384
|
+
try:
|
|
385
|
+
res = store.decide_proposal(args.id, args.cmd == "approve", actor=actor)
|
|
386
|
+
except ValueError as exc:
|
|
387
|
+
print(msg_error(exc), file=sys.stderr)
|
|
388
|
+
return 2
|
|
389
|
+
if res is None:
|
|
390
|
+
print(msg_not_found("proposal", args.id), file=sys.stderr)
|
|
391
|
+
return 1
|
|
392
|
+
return _emit(args, res)
|
|
393
|
+
if args.cmd == "ask":
|
|
394
|
+
return _emit(args, store.ask(actor, args.question))
|
|
395
|
+
if args.cmd == "questions":
|
|
396
|
+
return _emit(args, store.list_questions(open_only=not args.all))
|
|
397
|
+
if args.cmd == "answer":
|
|
398
|
+
res = store.answer_question(args.id, args.text, actor=actor)
|
|
399
|
+
if res is None:
|
|
400
|
+
print(msg_not_found("question", args.id), file=sys.stderr)
|
|
401
|
+
return 1
|
|
402
|
+
return _emit(args, res)
|
|
403
|
+
if args.cmd == "export":
|
|
404
|
+
data = store.export_all()
|
|
405
|
+
if args.format == "json":
|
|
406
|
+
return _emit(args, data)
|
|
407
|
+
print(render_markdown(data))
|
|
408
|
+
return 0
|
|
409
|
+
if args.cmd == "import":
|
|
410
|
+
import json as _json
|
|
411
|
+
|
|
412
|
+
with open(args.file) as fh:
|
|
413
|
+
data = _json.load(fh)
|
|
414
|
+
try:
|
|
415
|
+
counts = store.import_all(data, actor=actor)
|
|
416
|
+
except ValueError as exc:
|
|
417
|
+
print(msg_error(exc), file=sys.stderr)
|
|
418
|
+
return 2
|
|
419
|
+
return _emit(args, {"imported": counts})
|
|
420
|
+
if args.cmd == "feed":
|
|
421
|
+
seen = -1
|
|
422
|
+
while True:
|
|
423
|
+
events = store.feed(args.limit)
|
|
424
|
+
if args.json or not args.follow:
|
|
425
|
+
if args.follow and args.json:
|
|
426
|
+
fresh = [e for e in events if e["id"] > seen]
|
|
427
|
+
for e in reversed(fresh):
|
|
428
|
+
print(json.dumps(e, default=str))
|
|
429
|
+
if fresh:
|
|
430
|
+
seen = max(e["id"] for e in fresh)
|
|
431
|
+
else:
|
|
432
|
+
if args.json:
|
|
433
|
+
print(json.dumps(events, indent=2, default=str))
|
|
434
|
+
else:
|
|
435
|
+
if not events:
|
|
436
|
+
print(EMPTY_FEED)
|
|
437
|
+
for e in reversed(events):
|
|
438
|
+
print(
|
|
439
|
+
f"#{e['id']} [{e['actor']}] {e['kind']}/{e['action']}: {e['detail']}"
|
|
440
|
+
)
|
|
441
|
+
if not args.follow:
|
|
442
|
+
return 0
|
|
443
|
+
time.sleep(args.interval)
|
|
444
|
+
continue
|
|
445
|
+
# plain follow: only print new rows
|
|
446
|
+
fresh = [e for e in events if e["id"] > seen]
|
|
447
|
+
for e in reversed(fresh):
|
|
448
|
+
print(
|
|
449
|
+
f"#{e['id']} [{e['actor']}] {e['kind']}/{e['action']}: {e['detail']}",
|
|
450
|
+
flush=True,
|
|
451
|
+
)
|
|
452
|
+
if fresh:
|
|
453
|
+
seen = max(e["id"] for e in fresh)
|
|
454
|
+
time.sleep(args.interval)
|
|
455
|
+
if args.cmd == "sync":
|
|
456
|
+
import os as _os
|
|
457
|
+
|
|
458
|
+
dsn = args.dsn.strip() or _os.environ.get("LAZYPLAN_PG_DSN", "").strip()
|
|
459
|
+
if not dsn:
|
|
460
|
+
print("need --dsn or $LAZYPLAN_PG_DSN", file=sys.stderr)
|
|
461
|
+
return 2
|
|
462
|
+
from .postgres_sync import sync_sqlite_to_pg
|
|
463
|
+
|
|
464
|
+
try:
|
|
465
|
+
counts = sync_sqlite_to_pg(str(store.path), dsn)
|
|
466
|
+
except RuntimeError as exc:
|
|
467
|
+
print(msg_error(exc), file=sys.stderr)
|
|
468
|
+
return 2
|
|
469
|
+
return _emit(args, {"synced_to": dsn.split("@")[-1], "counts": counts})
|
|
470
|
+
if args.cmd == "completion":
|
|
471
|
+
cmds = [
|
|
472
|
+
"init",
|
|
473
|
+
"seed",
|
|
474
|
+
"list",
|
|
475
|
+
"gantt",
|
|
476
|
+
"add-project",
|
|
477
|
+
"add-story",
|
|
478
|
+
"add-task",
|
|
479
|
+
"update",
|
|
480
|
+
"show",
|
|
481
|
+
"add-link",
|
|
482
|
+
"links",
|
|
483
|
+
"delete-link",
|
|
484
|
+
"add-criteria",
|
|
485
|
+
"criteria",
|
|
486
|
+
"check",
|
|
487
|
+
"uncheck",
|
|
488
|
+
"delete-criteria",
|
|
489
|
+
"add-risk",
|
|
490
|
+
"risks",
|
|
491
|
+
"risk-status",
|
|
492
|
+
"add-milestone",
|
|
493
|
+
"milestones",
|
|
494
|
+
"delete-milestone",
|
|
495
|
+
"propose",
|
|
496
|
+
"proposals",
|
|
497
|
+
"approve",
|
|
498
|
+
"reject",
|
|
499
|
+
"ask",
|
|
500
|
+
"questions",
|
|
501
|
+
"answer",
|
|
502
|
+
"export",
|
|
503
|
+
"import",
|
|
504
|
+
"sync",
|
|
505
|
+
"status",
|
|
506
|
+
"delete",
|
|
507
|
+
"feed",
|
|
508
|
+
"watch",
|
|
509
|
+
"info",
|
|
510
|
+
"help",
|
|
511
|
+
"ui",
|
|
512
|
+
"completion",
|
|
513
|
+
]
|
|
514
|
+
if args.shell == "bash":
|
|
515
|
+
print(f'complete -W "{" ".join(cmds)}" lp')
|
|
516
|
+
else:
|
|
517
|
+
print(f"#compdef lp\n_arguments '1:command:({' '.join(cmds)})'")
|
|
518
|
+
return 0
|
|
519
|
+
if args.cmd == "ui":
|
|
520
|
+
from .tui import run
|
|
521
|
+
|
|
522
|
+
return run(db_path=args.db)
|
|
523
|
+
except (ValueError, KeyError) as exc:
|
|
524
|
+
print(msg_error(exc), file=sys.stderr)
|
|
525
|
+
return 2
|
|
526
|
+
raise AssertionError(args.cmd)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
if __name__ == "__main__":
|
|
530
|
+
raise SystemExit(main())
|