Add sandbox template image with Claude configuration and plugins
build / build (push) Canceled after 0s
build / build (push) Canceled after 0s
Carries CLAUDE.md, AGENTS.md, hooks and skills verbatim from the host, plus a manifest of the 10 marketplaces and 17 plugins to reinstall at build time. The plugin directories themselves are not committed: ~/.claude/plugins is 831 MB and sits alongside credentials and transcripts, so the image is reproduced from the manifest instead and the build needs no access to the host. The Gitea registry is behind Cloudflare, which rejects request bodies over 100 MB against a base image with a 325 MB layer, so the workflow pushes chunked through regctl rather than docker push. sbx v0.37.0 and v0.37.1 cannot consume the result: layers stacked on the base are silently dropped (docker/sbx-releases#366). The image builds and pushes correctly and is a no-op at runtime until that is fixed, so README points at 'ai:sbx setup' as the mechanism that works today.
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
---
|
||||
name: vikunja-backlog
|
||||
description: Use when you have a brainstorming, design, discussion, meeting-notes, or specification document (any format — md, txt, pdf, docx, odt) and want it turned into Vikunja backlog tasks under the "sliders, not checkboxes" feature-category planning model at tasks.mroberts.dev.
|
||||
---
|
||||
|
||||
# Vikunja Backlog From a Document
|
||||
|
||||
## Overview
|
||||
|
||||
Turn a discussion/design/brainstorming document into Vikunja tasks using the
|
||||
**"Sliders, Not Checkboxes"** framework: work is organized by **feature
|
||||
category** (the project), each item is restated as the **need** it addresses
|
||||
(the task title), the proposed solution lives in the description, and value is
|
||||
expressed as **priority** (rank within the category).
|
||||
|
||||
The mapping and the 8 categories are the contract. Read
|
||||
[reference/sliders-framework.md](reference/sliders-framework.md) before
|
||||
classifying — it defines each category, the need-vs-solution discipline, and
|
||||
the priority/label rules.
|
||||
|
||||
**Core principle:** the title is the *need*, never the solution. "Limit a
|
||||
compromised container's blast radius", not "add read-only rootfs".
|
||||
|
||||
## When to Use
|
||||
|
||||
- You have a doc (any format) describing work, decisions, or a backlog and want
|
||||
it in Vikunja.
|
||||
- Symptoms: a design spec, meeting notes, a brainstorm, a PR/backlog writeup, a
|
||||
Slack/email dump pasted into a file.
|
||||
|
||||
Not for: editing tasks one-off (use the API directly), or non-planning docs.
|
||||
|
||||
## Object Mapping (the contract)
|
||||
|
||||
| Framework concept | Vikunja object |
|
||||
|---|---|
|
||||
| Feature category | **Project** (child of the root `CareEvolution` project) |
|
||||
| Candidate item | **Task** |
|
||||
| Need | Task **title** (phrased as the goal) |
|
||||
| Solution(s) + source | Task **description** |
|
||||
| Value rank within category | Task **priority** (1–4) |
|
||||
| Urgency / returns | Label `acute` / `diminishing` |
|
||||
| Platform | Label (e.g. `Orchestrate`) |
|
||||
| Deployment / repo | Label (e.g. `Rosetta`) |
|
||||
|
||||
The same task viewed by **project** = cross-deployment value lens; filtered by
|
||||
**deployment label** = per-repo execution lens (one dataset, many views).
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Extract** the doc text:
|
||||
`scripts/extract.py <path>` (handles md/txt/pdf/docx/odt → stdout).
|
||||
2. **Analyze** the text. Pull out candidate items. For each, restate the
|
||||
**need** (what the user/admin/dev is trying to accomplish), and capture the
|
||||
proposed **solution(s)** separately. Discard pure discussion that isn't a
|
||||
candidate. See the framework reference for the need-restatement discipline.
|
||||
3. **Classify** each need into exactly one of the 8 categories. Borderline
|
||||
items: pick the category of the *primary value*, note the alternative in the
|
||||
description.
|
||||
4. **Rank** — assign a priority (4 = most acute/frequent/broad … 1 = small/late
|
||||
on the curve) reflecting value *within its category*, not build effort. Tag
|
||||
`acute` / `diminishing` per the reference rules.
|
||||
5. **Tag platform + deployment.** Infer the platform (tag) and deployment(s)
|
||||
(label) from the doc, then **confirm/override with the user** before
|
||||
creating. If a doc spans several deployments and granularity is "per
|
||||
deployment", emit one task per (need × deployment).
|
||||
6. **Propose, then create.** Present the full breakdown as a table
|
||||
(need | category | priority | labels | done?). Get the user's approval/edits.
|
||||
**Do not create anything before approval.**
|
||||
7. On approval, write the approved tasks to a JSON file and run
|
||||
`scripts/vikunja.py create --file <tasks.json>`. It idempotently ensures the
|
||||
8 category projects + labels exist, creates missing labels, skips duplicates,
|
||||
and handles the done-priority gotcha.
|
||||
8. **Report** created task IDs and the view hints above.
|
||||
|
||||
## Scripts
|
||||
|
||||
- `scripts/extract.py <path>` — extract plain text from md/txt/pdf/docx/odt.
|
||||
- `scripts/vikunja.py ensure` — idempotently create the 8 category projects +
|
||||
`acute`/`diminishing` labels under `CareEvolution`. Prints name→id maps.
|
||||
- `scripts/vikunja.py projects` / `labels` — list current ids.
|
||||
- `scripts/vikunja.py create --file <tasks.json>` — create tasks. Run `--help`
|
||||
for the JSON schema and all flags.
|
||||
|
||||
Auth: reads `VIKUNJA_API_KEY` from the environment. Base URL defaults to
|
||||
`https://tasks.mroberts.dev/api/v1` (`VIKUNJA_BASE_URL` overrides); root project
|
||||
defaults to `CareEvolution` (`VIKUNJA_ROOT_PROJECT` overrides).
|
||||
|
||||
## Tasks JSON schema
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"category": "Security, privacy & compliance",
|
||||
"title": "Limit a compromised container's runtime blast radius",
|
||||
"description": "**Need:** ...\n\n**Solution:** ...\n\n**Source:** doc.md",
|
||||
"priority": 4,
|
||||
"labels": ["Orchestrate", "Rosetta", "acute"],
|
||||
"done": false,
|
||||
"percent": 0
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
`category` must be one of the 8 exact names (see reference). `labels` are
|
||||
created if missing. `done`/`percent` optional.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
- **Title is the solution, not the need.** "Add Dependabot auto-merge" → wrong.
|
||||
"Stay patched with less manual toil" → right; Dependabot goes in the body.
|
||||
- **Priority = build effort.** No. Priority = value of the need within its
|
||||
category. A cheap rank-1 outranks an expensive rank-6.
|
||||
- **Creating before approval.** The framework's whole point is the judgment
|
||||
call on need + category + rank. Always propose first.
|
||||
- **Re-running creates duplicates.** `create` dedupes on (title + category +
|
||||
deployment labels); don't bypass it with ad-hoc API calls.
|
||||
- **Mark-done zeros priority.** The Vikunja update replaces the task; `create`
|
||||
re-sends priority on the done update. Don't hand-roll the done call.
|
||||
@@ -0,0 +1,98 @@
|
||||
# Sliders, Not Checkboxes — classification reference
|
||||
|
||||
The framework models work as a small set of **feature categories**. A backlog
|
||||
item is restated as the **need** it addresses (not the proposed solution), and
|
||||
needs are value-ranked within each category. Source: the "Sliders, Not
|
||||
Checkboxes" paper (CareEvolution).
|
||||
|
||||
## The need-restatement discipline
|
||||
|
||||
Before an item enters a category, restate it as the user/admin/developer
|
||||
**need** — what they are trying to accomplish — and treat the proposed solution
|
||||
as *one way* to address it.
|
||||
|
||||
- A doc says "build a self-serve provisioning UI." The **need** is "add/remove a
|
||||
user without filing a support ticket." The UI is one solution; SCIM or a
|
||||
declarative API are others. Title = the need; list the candidate solutions in
|
||||
the description.
|
||||
- Ranking reflects what addressing the need means for the people who hold it:
|
||||
**how often** it's hit, **how acute** the pain, **how broadly** held. Not what
|
||||
is easiest to build or most recently requested.
|
||||
- Discussion, debate, and context that isn't a candidate item → do not create a
|
||||
task. Capture only genuine candidate needs.
|
||||
|
||||
## The 8 feature categories
|
||||
|
||||
Classify each need into exactly one. Pick the category of the **primary value**;
|
||||
note a close alternative in the description.
|
||||
|
||||
| Category (use this exact name) | What it covers | Whose need |
|
||||
|---|---|---|
|
||||
| **End-user experience** | User-facing product: features, flows, learnability, polish | People using the front-end to do their own tasks |
|
||||
| **Admin & operator experience** | Configuration, governance, monitoring, operational tooling | Customer-side admins / IT staff deploying & overseeing the product |
|
||||
| **Developer experience & adoption** | API design, SDKs, docs, sandbox, time-to-first-call; community, mindshare | Developers integrating against or building on the product |
|
||||
| **Data, integrations & ecosystem** | Data quality, lineage, connectors, standards conformance (FHIR, HL7, OAuth, OpenAPI), marketplace, partners | Integrating systems and partner platforms |
|
||||
| **Performance & scale** | Latency, throughput, behavior under load | Anyone depending on the product being fast and handling their volume |
|
||||
| **Reliability & observability** | Uptime, fault tolerance, graceful degradation, recovery; logs, metrics, traces, alerting, runbooks | Customers needing it up; operators keeping it up |
|
||||
| **Security, privacy & compliance** | Authn/authz, encryption, audit logging, certifications, regulatory posture, attack-surface reduction, vulnerability/CVE management, supply-chain integrity, runtime threat detection | Security & compliance teams; regulators; users trusting the platform |
|
||||
| **Cost efficiency** | Cost per unit work — per record, per API call, per active user; compute/arch cost (e.g. ARM64/Graviton) | Internal P&L; customers indirectly via pricing |
|
||||
|
||||
### Classification hints / common overlaps
|
||||
|
||||
- **Hardening** (read-only rootfs, drop caps, minimal/chiseled base images, image
|
||||
CVE scanning, SHA-pinning CI actions, dependency patch currency, supply-chain
|
||||
inventory, runtime threat detection) → **Security, privacy & compliance**.
|
||||
- **ARM64/Graviton migration, smaller images for cost** → **Cost efficiency**
|
||||
(its perf side can be **Performance & scale** — pick the primary driver named
|
||||
in the doc).
|
||||
- **Perf vs cost** are often two sides of one coin — categorize by the value the
|
||||
doc emphasizes (latency/throughput → Performance; $/unit → Cost).
|
||||
- **A correctness/data-race bug surfaced by infra work** → **Reliability &
|
||||
observability** (it's about the service being correct/up), not Security.
|
||||
- **CI/CD integrity** (pinning actions, blocking tampering) → Security; **CI
|
||||
ergonomics/speed for devs** → Developer experience. Pick by the value.
|
||||
- **Runtime anomaly detection** (GuardDuty etc.) → Security (threat detection)
|
||||
even though it touches observability.
|
||||
|
||||
## Value rank → priority
|
||||
|
||||
Vikunja priority is 1–5. Use **1–4** for value tiers (reserve 5 for true
|
||||
DO-NOW). Rank within the category, by value of the need:
|
||||
|
||||
| Priority | Meaning |
|
||||
|---|---|
|
||||
| **4** | Rank-1 tier: most acute / frequent / broadly held; foundational. Everything else in the category is downstream of it. |
|
||||
| **3** | High value, clearly worth doing, but not the keystone. |
|
||||
| **2** | Medium: real need, smaller delta over the status quo. |
|
||||
| **1** | Small / late-on-the-curve: refinement, niche, or largely satisfied by another item. |
|
||||
|
||||
Priority reflects **value, not build effort**. A cheap rank-1 outranks an
|
||||
expensive rank-6.
|
||||
|
||||
## Urgency / returns labels
|
||||
|
||||
- **`acute`** — the need is frequent, painful, and/or broadly held *right now*.
|
||||
Apply to the rank-1/keystone needs and anything externally forced (regulatory
|
||||
deadline, audit finding).
|
||||
- **`diminishing`** — the next investment in this area has tipped into
|
||||
diminishing returns, or the need is largely satisfied by another item already
|
||||
in the plan. Apply sparingly; it's a signal to deprioritize.
|
||||
|
||||
A task can have neither. Most have neither.
|
||||
|
||||
## Platform & deployment labels
|
||||
|
||||
- **Platform** = the product/platform the work belongs to (e.g. `Orchestrate`,
|
||||
`HBNG`). One label.
|
||||
- **Deployment / repo** = the specific service or repo (e.g. `Rosetta`,
|
||||
`Hendrix`, `Insight`, `Kong`). One per task when granularity is per-deployment.
|
||||
|
||||
Categories are consistent **across** platforms and deployments (that's the point
|
||||
— cross-deployment comparability). Platform/deployment are tags, never top-level
|
||||
projects.
|
||||
|
||||
## Done / in-progress
|
||||
|
||||
- If the doc says an item is already shipped for a deployment, set `done: true`.
|
||||
- Partial progress: leave `done: false` and set `percent` (e.g. 50) — note what
|
||||
remains in the description.
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract plain text from a document for backlog analysis.
|
||||
|
||||
Supports: .md .markdown .rst .txt (read), .odt / .docx (zip + XML strip),
|
||||
.pdf (pdftotext if available, else pypdf/pdfplumber). Writes text to stdout.
|
||||
|
||||
Usage: extract.py <path>
|
||||
"""
|
||||
import sys, os, re, html, zipfile, shutil, subprocess
|
||||
|
||||
|
||||
def _strip_xml(xml: str, para_close=("</text:p>", "</text:h>", "</w:p>")) -> str:
|
||||
for tag in para_close:
|
||||
xml = xml.replace(tag, "\n")
|
||||
xml = re.sub(r"<[^>]+>", "", xml)
|
||||
xml = html.unescape(xml)
|
||||
lines = [ln.strip() for ln in xml.split("\n")]
|
||||
return "\n".join(ln for ln in lines if ln)
|
||||
|
||||
|
||||
def from_zip_xml(path: str, inner: str) -> str:
|
||||
with zipfile.ZipFile(path) as z:
|
||||
return _strip_xml(z.read(inner).decode("utf-8", "ignore"))
|
||||
|
||||
|
||||
def from_pdf(path: str) -> str:
|
||||
if shutil.which("pdftotext"):
|
||||
out = subprocess.run(["pdftotext", "-layout", path, "-"],
|
||||
capture_output=True, text=True)
|
||||
if out.returncode == 0 and out.stdout.strip():
|
||||
return out.stdout
|
||||
try:
|
||||
import pypdf
|
||||
return "\n".join(p.extract_text() or "" for p in pypdf.PdfReader(path).pages)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
import pdfplumber
|
||||
with pdfplumber.open(path) as pdf:
|
||||
return "\n".join(pg.extract_text() or "" for pg in pdf.pages)
|
||||
except Exception:
|
||||
sys.exit("PDF extraction needs `pdftotext` (poppler) or `pip install pypdf`.")
|
||||
|
||||
|
||||
def extract(path: str) -> str:
|
||||
ext = os.path.splitext(path)[1].lower()
|
||||
if ext in (".md", ".markdown", ".rst", ".txt", ".text", ""):
|
||||
with open(path, encoding="utf-8", errors="replace") as f:
|
||||
return f.read()
|
||||
if ext == ".odt":
|
||||
return from_zip_xml(path, "content.xml")
|
||||
if ext == ".docx":
|
||||
return from_zip_xml(path, "word/document.xml")
|
||||
if ext == ".pdf":
|
||||
return from_pdf(path)
|
||||
# last resort: try as text
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except UnicodeDecodeError:
|
||||
sys.exit(f"Unsupported binary format: {ext}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
sys.exit("usage: extract.py <path>")
|
||||
if not os.path.isfile(sys.argv[1]):
|
||||
sys.exit(f"no such file: {sys.argv[1]}")
|
||||
sys.stdout.write(extract(sys.argv[1]))
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Vikunja backlog helper for the sliders/feature-category model.
|
||||
|
||||
Idempotently ensures the 8 feature-category projects + framework labels exist
|
||||
under the root project, and creates tasks from an approved JSON breakdown
|
||||
(deduping, and working around the mark-done-zeros-priority quirk).
|
||||
|
||||
Auth/config (environment):
|
||||
VIKUNJA_API_KEY required — long-lived API token
|
||||
VIKUNJA_BASE_URL default https://tasks.mroberts.dev/api/v1
|
||||
VIKUNJA_ROOT_PROJECT default CareEvolution
|
||||
|
||||
Subcommands:
|
||||
ensure create missing category projects + acute/diminishing labels
|
||||
projects list category projects (name -> id)
|
||||
labels list labels (name -> id)
|
||||
create --file T.json create tasks from an approved breakdown (see --help)
|
||||
|
||||
Tasks JSON: a list of objects:
|
||||
{category, title, description, priority, labels:[...], done?, percent?}
|
||||
`category` must be one of the 8 exact names. Missing labels are created.
|
||||
Duplicates (same title + category + deployment labels) are skipped.
|
||||
"""
|
||||
import sys, os, json, argparse, urllib.request, urllib.error
|
||||
|
||||
BASE = os.environ.get("VIKUNJA_BASE_URL", "https://tasks.mroberts.dev/api/v1")
|
||||
ROOT_NAME = os.environ.get("VIKUNJA_ROOT_PROJECT", "CareEvolution")
|
||||
|
||||
CATEGORIES = [
|
||||
"End-user experience",
|
||||
"Admin & operator experience",
|
||||
"Developer experience & adoption",
|
||||
"Data, integrations & ecosystem",
|
||||
"Performance & scale",
|
||||
"Reliability & observability",
|
||||
"Security, privacy & compliance",
|
||||
"Cost efficiency",
|
||||
]
|
||||
FRAMEWORK_LABELS = ["acute", "diminishing"]
|
||||
|
||||
|
||||
def _key():
|
||||
k = os.environ.get("VIKUNJA_API_KEY")
|
||||
if not k:
|
||||
sys.exit("VIKUNJA_API_KEY not set")
|
||||
return k
|
||||
|
||||
|
||||
def call(method, path, body=None):
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
req = urllib.request.Request(
|
||||
BASE + path, data=data, method=method,
|
||||
headers={"Authorization": "Bearer " + _key(),
|
||||
"Content-Type": "application/json"})
|
||||
try:
|
||||
resp = urllib.request.urlopen(req)
|
||||
return resp.status, json.loads(resp.read() or "null")
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, e.read().decode()[:300]
|
||||
|
||||
|
||||
def get(path):
|
||||
return call("GET", path)[1]
|
||||
|
||||
|
||||
def root_id():
|
||||
for p in get("/projects") or []:
|
||||
if p["title"] == ROOT_NAME and p.get("parent_project_id") in (0, None):
|
||||
return p["id"]
|
||||
sys.exit(f"root project {ROOT_NAME!r} not found")
|
||||
|
||||
|
||||
def all_labels():
|
||||
return {l["title"]: l["id"] for l in (get("/labels") or [])}
|
||||
|
||||
|
||||
def category_projects():
|
||||
rid = root_id()
|
||||
return {p["title"]: p["id"] for p in (get("/projects") or [])
|
||||
if p.get("parent_project_id") == rid and p["title"] in CATEGORIES}
|
||||
|
||||
|
||||
def ensure_label(name, cache):
|
||||
if name in cache:
|
||||
return cache[name]
|
||||
s, l = call("PUT", "/labels", {"title": name})
|
||||
if not isinstance(l, dict):
|
||||
sys.exit(f"failed creating label {name!r}: {l}")
|
||||
cache[name] = l["id"]
|
||||
return l["id"]
|
||||
|
||||
|
||||
def cmd_ensure(_):
|
||||
rid = root_id()
|
||||
existing = category_projects()
|
||||
cats = dict(existing)
|
||||
for c in CATEGORIES:
|
||||
if c not in cats:
|
||||
s, p = call("PUT", "/projects", {"title": c, "parent_project_id": rid})
|
||||
if not isinstance(p, dict):
|
||||
sys.exit(f"failed creating project {c!r}: {p}")
|
||||
cats[c] = p["id"]
|
||||
print(f"created project {c!r} -> {p['id']}", file=sys.stderr)
|
||||
labels = all_labels()
|
||||
for n in FRAMEWORK_LABELS:
|
||||
ensure_label(n, labels)
|
||||
print(json.dumps({"root": rid, "categories": cats, "labels": labels}, indent=2))
|
||||
|
||||
|
||||
def cmd_projects(_):
|
||||
print(json.dumps(category_projects(), indent=2))
|
||||
|
||||
|
||||
def cmd_labels(_):
|
||||
print(json.dumps(all_labels(), indent=2))
|
||||
|
||||
|
||||
def _deployment_labels(labels):
|
||||
return {l for l in labels if l not in FRAMEWORK_LABELS}
|
||||
|
||||
|
||||
def cmd_create(args):
|
||||
tasks = json.load(open(args.file))
|
||||
if not isinstance(tasks, list):
|
||||
sys.exit("tasks JSON must be a list")
|
||||
rid = root_id()
|
||||
cats = category_projects()
|
||||
# ensure all referenced categories exist
|
||||
for t in tasks:
|
||||
c = t["category"]
|
||||
if c not in CATEGORIES:
|
||||
sys.exit(f"unknown category {c!r} (must be one of the 8)")
|
||||
if c not in cats:
|
||||
s, p = call("PUT", "/projects", {"title": c, "parent_project_id": rid})
|
||||
cats[c] = p["id"]
|
||||
label_cache = all_labels()
|
||||
# cache existing tasks per category for dedup
|
||||
existing = {}
|
||||
for c, pid in cats.items():
|
||||
existing[c] = get(f"/projects/{pid}/tasks") or []
|
||||
|
||||
created, skipped = [], []
|
||||
for t in tasks:
|
||||
c = t["category"]
|
||||
pid = cats[c]
|
||||
title = t["title"]
|
||||
want_dep = _deployment_labels(t.get("labels", []))
|
||||
dup = False
|
||||
for ex in existing[c]:
|
||||
if ex["title"].strip().lower() != title.strip().lower():
|
||||
continue
|
||||
ex_labels = {l["title"] for l in (ex.get("labels") or [])}
|
||||
if want_dep.issubset(ex_labels):
|
||||
dup = True
|
||||
break
|
||||
if dup:
|
||||
skipped.append((title, c, sorted(want_dep)))
|
||||
continue
|
||||
prio = int(t.get("priority", 0))
|
||||
s, task = call("PUT", f"/projects/{pid}/tasks", {
|
||||
"title": title,
|
||||
"description": (t.get("description") or "").replace("\n", "<br>"),
|
||||
"priority": prio,
|
||||
})
|
||||
if not isinstance(task, dict):
|
||||
sys.exit(f"create failed for {title!r}: {task}")
|
||||
tid = task["id"]
|
||||
for ln in t.get("labels", []):
|
||||
call("PUT", f"/tasks/{tid}/labels", {"label_id": ensure_label(ln, label_cache)})
|
||||
done = bool(t.get("done"))
|
||||
pct = int(t.get("percent", 0))
|
||||
if done or pct:
|
||||
# re-send priority: the update replaces the task and would zero it
|
||||
call("POST", f"/tasks/{tid}", {
|
||||
"done": done, "percent_done": pct / 100, "priority": prio})
|
||||
existing[c].append({"title": title,
|
||||
"labels": [{"title": x} for x in t.get("labels", [])]})
|
||||
created.append((tid, title, c, prio, sorted(want_dep), "done" if done else (f"{pct}%" if pct else "")))
|
||||
|
||||
print(f"created {len(created)} tasks, skipped {len(skipped)} duplicate(s)", file=sys.stderr)
|
||||
for r in created:
|
||||
print(f" + #{r[0]} [{r[3]}] {r[2]}: {r[1]} {r[4]} {r[5]}", file=sys.stderr)
|
||||
for r in skipped:
|
||||
print(f" = skip {r[1]}: {r[0]} {r[2]}", file=sys.stderr)
|
||||
print(json.dumps({"created": [r[0] for r in created],
|
||||
"skipped": len(skipped)}))
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
sub.add_parser("ensure", help="create missing category projects + labels")
|
||||
sub.add_parser("projects", help="list category projects")
|
||||
sub.add_parser("labels", help="list labels")
|
||||
c = sub.add_parser("create", help="create tasks from approved JSON")
|
||||
c.add_argument("--file", required=True, help="path to tasks JSON (list)")
|
||||
args = ap.parse_args()
|
||||
{"ensure": cmd_ensure, "projects": cmd_projects,
|
||||
"labels": cmd_labels, "create": cmd_create}[args.cmd](args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user