#!/usr/bin/env python3 """Open a folder as a project: adopt it, remember it, build its pages. bin/office [--project ] [--session NAME] [--dry] bin/office . # the folder you are in Adopting writes one file, /.harness/harness.toml, and nothing else. An existing folder is never overwritten: what is already there stays, and only the missing file is written. Paperclip's project record is read, never created — bin/office adopts a project that exists, it does not invent one. The machine is remembered, so the flag is stated once. Running the same command on a machine that is not the recorded one is refused rather than silently producing a second copy of the same project. """ import os import sys HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(HERE, "lib")) import harness as h # noqa: E402 import project as pj # noqa: E402 import tuios # noqa: E402 def parse(argv): opts = {"folder": ".", "project": "", "session": "", "dry": "--dry" in argv, "host": ""} args = [a for a in argv if not a.startswith("--")] if args: opts["folder"] = args[0] for i, a in enumerate(argv): if a == "--project" and i + 1 < len(argv): opts["project"] = argv[i + 1] if a == "--session" and i + 1 < len(argv): opts["session"] = argv[i + 1] if a == "--host" and i + 1 < len(argv): opts["host"] = argv[i + 1] return opts def scaffold(folder, name): """Write the files this project keeps, and only the ones that are missing. Anything already there is left exactly as it is: adopting a folder must never overwrite a person's own notes or scripts. """ written = [] harness = os.path.join(folder, ".harness") os.makedirs(harness, exist_ok=True) check = os.path.join(harness, "check.sh") if not os.path.exists(check): with open(check, "w") as f: f.write("""#!/bin/sh # This project's own check, run by bin/merge before and after it puts a task's # work on the base branch. Exit 0 means "the project still passes". # # It does not decide that a task is finished: an agent sets a task's status, and # the review decides whether the change is good. This is the regression check, # the same thing a CI pipeline would run. # # Leave it as it is while the project has no tests. bin/merge will say there is # no check and let the review decide. Replace the body when tests exist: # # exec go test ./... # exec make test echo "this project has no check yet" exit 0 """) os.chmod(check, 0o755) written.append(check) notes = os.path.join(harness, "NOTES.md") if not os.path.exists(notes): with open(notes, "w") as f: f.write("# %s\n\nWhat this project is, what finished looks like, and anything a person\n" "picking it up next week needs to know. Notes only: tasks live in Paperclip.\n" % name) written.append(notes) return written def main(): opts = parse(sys.argv[1:]) folder = os.path.abspath(os.path.expanduser(opts["folder"])) if not os.path.isdir(folder): raise h.Refused(2, "no folder at %s" % folder) host = opts["host"] or pj.this_machine() # An adopted project already knows its session. Only a new one needs a # default, and a default that names a session nobody made is worse than an # error, so the session is checked before anything is written. existing = {} if pj.exists(folder): existing = pj.load(folder) session = ( opts["session"] or existing.get("session") or os.path.basename(folder) ) tuios.require_session(session) if opts["host"] and opts["host"] not in (pj.this_machine(), pj.this_machine().split(".")[0]): # Only a note, because the panes are built by the machine that owns them. h.say("host: %s (build the pages there)" % host) ref = opts["project"] if not ref and pj.exists(folder): ref = pj.load(folder)["paperclip"]["project"] if not ref: raise h.Refused( 2, "which Paperclip project is %s? pass --project " % folder, ) proj = pj.resolve_project(ref) h.say("project: %s (%s)" % (proj["name"], proj["id"])) h.say("folder: %s" % folder) h.say("session: %s" % session) h.say("machine: %s" % host) tasks = pj.active_tasks(proj["id"]) watching = next((t["identifier"] for t in tasks if t["status"] not in ("done", "cancelled")), tasks[0]["identifier"] if tasks else "") cfg = { "session": session, "paperclip": {"company": h.COMPANY, "project": proj["id"]}, "machine": {"host": host, "folder": folder}, "watching": watching, } if opts["dry"]: h.say("would write %s" % pj.path(folder)) h.say("would build pages: shell, tree, reply, log, new") return 0 pj.save(folder, cfg) h.say("wrote %s" % pj.path(folder)) pj.register(cfg, proj["name"]) h.say("recorded in %s" % pj.REGISTRY) written = scaffold(folder, proj["name"]) for path in written: h.say("wrote %s" % path) # The pages are built by this machine, from this folder, in this session. code = os.spawnv( os.P_WAIT, sys.executable, [sys.executable, os.path.join(HERE, "board"), "--folder", folder], ) return code if __name__ == "__main__": h.main_guard(main)