office: --new creates the Paperclip project, --remote adopts a folder on another machine

This commit is contained in:
Sam
2026-11-12 10:32:03 +11:00
parent 08a4b4e822
commit 9631cdda48
10 changed files with 780 additions and 20 deletions

View File

@@ -2,12 +2,19 @@
"""Open a folder as a project: adopt it, remember it, build its pages.
bin/office <folder> [--project <id|urlKey|name>] [--session NAME] [--dry]
bin/office . # the folder you are in
bin/office . # the folder you are in
bin/office <folder> --new "Name" # create the Paperclip project too
bin/office --remote <ssh-host> <folder> --project "Name" # the folder is on another machine
Adopting writes one file, <folder>/.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.
Adopting writes <folder>/.harness/ (the project file, check.sh, NOTES.md) and
nothing else. An existing file is never overwritten. With --new, Paperclip gets
a project for the folder, with the per-task worktree policy set; without it,
--project adopts a project that already exists.
A session is created on the daemon of the machine that runs this command, and
the panes run on that machine. A program cannot move your client to the new
session: TUIOS refuses that from a pane. Switch with TUIOS's session switcher,
or `tuios attach --host <machine> <session>`.
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
@@ -16,6 +23,7 @@ producing a second copy of the same project.
import json
import os
import shlex
import subprocess
import sys
@@ -27,21 +35,34 @@ import tuios # noqa: E402
def parse(argv):
opts = {"folder": ".", "project": "", "session": "", "dry": "--dry" in argv,
"host": "", "no_switch": "--no-switch" in argv, "new": ""}
for i, a in enumerate(argv):
if a == "--new" and i + 1 < len(argv):
opts["new"] = argv[i + 1]
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]
"""Read the flags, then take what is left as the folder.
A flag's value is not a folder. The first version of this treated the value
of --remote as the folder, which is exactly the kind of bug that only shows
up when someone uses the flag.
"""
opts = {"folder": ".", "project": "", "session": "", "dry": False,
"host": "", "no_switch": False, "new": "", "remote": ""}
positional = []
i = 0
while i < len(argv):
a = argv[i]
if a in ("--project", "--session", "--host", "--new", "--remote"):
if i + 1 < len(argv):
opts[a[2:]] = argv[i + 1]
i += 2
continue
i += 1
continue
if a == "--dry":
opts["dry"] = True
elif a == "--no-switch":
opts["no_switch"] = True
elif not a.startswith("-"):
positional.append(a)
i += 1
if positional:
opts["folder"] = positional[0]
return opts
@@ -145,6 +166,34 @@ exit 0
return written
def run_on_remote(host, folder, opts):
"""Adopt a folder that lives on another machine, from where you are sitting.
The session has to be made by the daemon that will hold the panes, and the
panes have to run on the machine that owns the folder. So the work is done
over ssh on that machine, and only the last step — showing the session — is
the client's own business.
"""
here = os.path.dirname(os.path.abspath(__file__))
inner = [os.path.join(here, "office"), folder, "--no-switch"]
for flag, value in (("--project", opts["project"]), ("--session", opts["session"]), ("--new", opts["new"])):
if value:
inner += [flag, value]
if opts["dry"]:
inner.append("--dry")
cmd = " ".join(shlex.quote(a) for a in inner)
h.say("on %s: %s" % (host, cmd))
out = subprocess.run(["ssh", "-o", "StrictHostKeyChecking=accept-new", host, cmd],
capture_output=True, text=True, timeout=600)
sys.stdout.write(out.stdout)
if out.returncode != 0:
raise h.Refused(3, "the other machine refused: %s" % (out.stderr or out.stdout).strip()[:300])
h.say("")
h.say("switch to it: tuios attach --host %s %s" % (host, opts["session"] or os.path.basename(folder)))
return 0
def main():
opts = parse(sys.argv[1:])
folder = os.path.abspath(os.path.expanduser(opts["folder"]))
@@ -168,6 +217,9 @@ def main():
# so one command takes a person from a folder to a working layout. TUIOS has
# the verb for exactly this; asking the person to run `tuios new` first was
# the step everyone forgot.
if opts["remote"]:
return run_on_remote(opts["remote"], folder, opts)
if opts["dry"]:
h.say("would create a Paperclip project %r for %s" % (opts["new"] or opts["project"], folder))
h.say("would write %s" % os.path.join(folder, ".harness", "harness.toml"))