Projects have their own name, description and goal; the template is shown only as origin
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: sonnet Assistant-Process: 295952@bnt-lap001 Assistant-Session: e93f64ad-516c-46eb-9666-aad8d300c477
This commit is contained in:
parent
334e0d3eb6
commit
f8ce4717a8
8 changed files with 125 additions and 24 deletions
|
|
@ -78,8 +78,8 @@ def _project(a) -> int:
|
|||
print(f"{tid}\ttemplate\t-\t{d.data.get('title', '')}")
|
||||
return 0
|
||||
if a.action == "new":
|
||||
path = J.instantiate(reg, a.name, a.extra, a.environment,
|
||||
dict(kv.split("=", 1) for kv in a.set))
|
||||
path = J.instantiate(reg, a.name, a.extra, a.environment, dict(kv.split("=", 1) for kv in a.set),
|
||||
title=a.title, description=a.description, goal=a.goal)
|
||||
print(path); return 0
|
||||
d = reg.projects.get(a.name)
|
||||
if d is None:
|
||||
|
|
@ -154,6 +154,9 @@ def main(argv: list[str] | None = None) -> int:
|
|||
sp.add_argument("--environment", "-e", default="")
|
||||
sp.add_argument("--set", "-s", action="append", default=[], help="new: attribute=value")
|
||||
sp.add_argument("--operator", "-o", default="")
|
||||
sp.add_argument("--title", default=None, help="new: the project's own name (default: its id)")
|
||||
sp.add_argument("--description", default="", help="new: what the project is")
|
||||
sp.add_argument("--goal", default="", help="new: what should be true when it is done")
|
||||
sp.add_argument("--target", default=None)
|
||||
sp.add_argument("--mode", default="manual", choices=["manual", "auto-confirm"])
|
||||
sp = sub.add_parser("score", help="how often each runbook was tried and how it went")
|
||||
|
|
|
|||
|
|
@ -212,8 +212,13 @@ def start_run(reg: Registry, doc: Doc, pack_id: str, operator: str, target: str
|
|||
# ---- templates -----------------------------------------------------------------------------------------------
|
||||
|
||||
def instantiate(reg: Registry, template_id: str, project_id: str, environment: str,
|
||||
values: dict[str, str] | None = None, title: str | None = None, root: Path | None = None) -> Path:
|
||||
"""Write projects/<id>/project.yaml from a template. Required attributes may stay unset; `state()` lists them."""
|
||||
values: dict[str, str] | None = None, title: str | None = None, root: Path | None = None,
|
||||
description: str = "", goal: str = "") -> Path:
|
||||
"""Write projects/<id>/project.yaml from a template. Required attributes may stay unset; `state()` lists them.
|
||||
|
||||
The project has its own name, description and goal. Nothing of the template's own title, goal or description
|
||||
is copied: the project only records which template it came from.
|
||||
"""
|
||||
t = reg.templates.get(template_id)
|
||||
if t is None or not t.ok:
|
||||
raise ValueError(f"no valid template {template_id}")
|
||||
|
|
@ -221,7 +226,11 @@ def instantiate(reg: Registry, template_id: str, project_id: str, environment: s
|
|||
raise ValueError("project id must be lower-case letters, digits, dots and hyphens")
|
||||
if environment not in R.ENVIRONMENTS:
|
||||
raise ValueError(f"environment must be one of {', '.join(R.ENVIRONMENTS)}")
|
||||
d = {k: v for k, v in t.data.items() if k not in ("spec", "id", "title")}
|
||||
from .pack import SECRET_RE
|
||||
for label, text in (("title", title or ""), ("description", description), ("goal", goal)):
|
||||
if SECRET_RE.search(text):
|
||||
raise ValueError(f"the {label} looks like a secret; projects are never secret")
|
||||
d = {k: v for k, v in t.data.items() if k not in ("spec", "id", "title", "goal", "description")}
|
||||
attrs = normalize(d.get("attributes"))
|
||||
for k, v in (values or {}).items():
|
||||
attrs.setdefault(k, {"value": None, "requirement": None})
|
||||
|
|
@ -230,9 +239,15 @@ def instantiate(reg: Registry, template_id: str, project_id: str, environment: s
|
|||
for k, a in attrs.items():
|
||||
out_attrs[k] = a["value"] if a.get("value") is not None and not a.get("requirement") and not a.get("note") else \
|
||||
{kk: vv for kk, vv in a.items() if vv is not None}
|
||||
project = {"spec": "project/v0.1", "id": project_id, "title": title or t.data["title"],
|
||||
**{k: v for k, v in d.items() if k not in ("attributes", "environment")}, "environment": environment,
|
||||
"template": template_id}
|
||||
if len((title or project_id).strip()) < 3:
|
||||
raise ValueError("the project name needs at least 3 characters")
|
||||
project = {"spec": "project/v0.1", "id": project_id, "title": (title or project_id).strip()}
|
||||
if description.strip():
|
||||
project["description"] = description.strip()
|
||||
if goal.strip():
|
||||
project["goal"] = goal.strip()
|
||||
project.update({k: v for k, v in d.items() if k not in ("attributes", "environment")})
|
||||
project.update({"environment": environment, "template": template_id})
|
||||
if out_attrs:
|
||||
project["attributes"] = out_attrs
|
||||
home = Path(root) if root else t.path.parent.parent
|
||||
|
|
|
|||
|
|
@ -68,8 +68,10 @@ def index_sections(app) -> str:
|
|||
continue
|
||||
errs = J.validate(reg, d)
|
||||
st = pill("invalid") if errs else pill(J.state(reg, d)["state"])
|
||||
prj.append(f"<li><a href='/project/{e(pid)}'>{e(d.data['title'])}</a> {pill(d.data['environment'])} {st} "
|
||||
f"<span class=note>{e(d.data.get('goal', ''))}</span></li>")
|
||||
origin = (f" <span class=note>from template <a href='/template/{e(d.data['template'])}'>{e(d.data['template'])}</a></span>"
|
||||
if d.data.get("template") else "")
|
||||
desc = f"<br><span class=note>{e(d.data['description'])}</span>" if d.data.get("description") else ""
|
||||
prj.append(f"<li><a href='/project/{e(pid)}'>{e(d.data['title'])}</a> {pill(d.data['environment'])} {st}{origin}{desc}</li>")
|
||||
tpl = [f"<li><a href='/template/{e(tid)}'>{e(d.data['title'])}</a> "
|
||||
f"<span class=note>{e(d.data.get('goal', ''))}</span></li>" if d.ok else f"<li class=bad>{e(tid)}: invalid</li>"
|
||||
for tid, d in reg.templates.items()]
|
||||
|
|
@ -113,9 +115,11 @@ def project_page(app, pid: str, msg: str = "") -> bytes | None:
|
|||
return page(d.get("title", pid), "".join(out))
|
||||
st = J.state(reg, doc)
|
||||
out.append(f"<p>{pill(d['environment'])} {pill(st['state'])}"
|
||||
f"{' <span class=note>from template ' + e(d['template']) + '</span>' if d.get('template') else ''}</p>")
|
||||
f"{' <span class=note>from template <a href=' + chr(39) + '/template/' + e(d['template']) + chr(39) + '>' + e(d['template']) + '</a></span>' if d.get('template') else ''}</p>")
|
||||
if d.get("description"):
|
||||
out.append(f"<p>{e(d['description'])}</p>")
|
||||
if d.get("goal"):
|
||||
out.append(f"<p>{e(d['goal'])}</p>")
|
||||
out.append(f"<p><b>Goal:</b> {e(d['goal'])}</p>")
|
||||
# what is still needed
|
||||
needs: dict[str, list[str]] = {}
|
||||
for n in st["needs"]:
|
||||
|
|
@ -204,6 +208,12 @@ def template_page(app, tid: str, msg: str = "") -> bytes | None:
|
|||
envs = "".join(f"<option>{e(x)}</option>" for x in R.ENVIRONMENTS)
|
||||
out.append("<h2>Start a project from this template</h2>"
|
||||
f"<form method=post action='/template/{e(tid)}/new'>{app.hidden()}"
|
||||
"<p><label>Project name<br><input name=title class=grow style='width:100%' minlength=3 required "
|
||||
"placeholder='what you call this project'></label></p>"
|
||||
"<p><label>Description<br><textarea name=description rows=2 style='width:100%' "
|
||||
"placeholder='what this project is (optional)'></textarea></label></p>"
|
||||
"<p><label>Goal<br><input name=goal class=grow style='width:100%' "
|
||||
"placeholder='what should be true when it is done (optional)'></label></p>"
|
||||
"<p><label>Project id<br><input name=project_id pattern='[a-z0-9]+([.-][a-z0-9]+)*' required "
|
||||
"placeholder='lower-case, digits, dots, hyphens'></label></p>"
|
||||
f"<p><label>Environment<br><select name=environment>{envs}</select></label></p>"
|
||||
|
|
@ -238,7 +248,10 @@ def create_from_template(app, tid: str, form: dict) -> str:
|
|||
if SECRET_RE.search(v):
|
||||
raise ValueError(f"{name}: value looks like a secret; attributes are never secret")
|
||||
pid = form.get("project_id", "").strip()
|
||||
J.instantiate(reg, tid, pid, form.get("environment", ""), values, root=doc.path.parent.parent)
|
||||
if len(form.get("title", "").strip()) < 3:
|
||||
raise ValueError("give the project a name of at least 3 characters")
|
||||
J.instantiate(reg, tid, pid, form.get("environment", ""), values, title=form.get("title", ""),
|
||||
root=doc.path.parent.parent, description=form.get("description", ""), goal=form.get("goal", ""))
|
||||
reg = app.registry()
|
||||
created = reg.projects[pid]
|
||||
errs = J.validate(reg, created)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue