feat(@scripts): add account-wide env list command

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-06-07 19:31:01 -07:00
parent a49ea98efd
commit fa6eda0bbd
4 changed files with 142 additions and 0 deletions

View file

@ -53,8 +53,27 @@ claude-rc rm <name> # disable + unregister
claude-rc sync # reconcile units to the registry
claude-rc logs <name> [-f] # journal
claude-rc restart|stop|start <name>
claude-rc envs # account-wide environment list (all hosts)
```
## Account-wide view — `claude-rc-envs`
`claude-rc list`/`status` only know about *this host's* units. To see every
Remote Control environment across all hosts (exactly what the claude.ai/code
picker shows — including orphans and cloud envs), query the API:
```sh
crc status # or: crc envs / claude-rc envs / rc envs
claude-rc-envs # state, host, project, env id, dir
claude-rc-envs --json
claude-rc-envs rm <env_id> # delete an environment (e.g. an orphan)
claude-rc-envs archive <env_id>
```
Reads the OAuth token from the macOS Keychain or `~/.claude/.credentials.json`
and hits `GET https://api.anthropic.com/v1/environments`
(`anthropic-beta: environments-2025-11-01`).
Defaults (override via env in a unit drop-in):
- `CLAUDE_RC_SPAWN=worktree` — isolated git worktree per spawned session.
- `CLAUDE_RC_PERM=bypassPermissions` — spawned sessions skip permission prompts.

View file

@ -81,6 +81,11 @@ case "$cmd" in
printf '%-16s %-10s %s\n' "$n" "$(uc is-active "$TPL$n" 2>/dev/null || echo -)" "$(reg_dir "$n")"
done
;;
envs|environments)
# Account-wide environment list across ALL hosts (the claude.ai/code
# picker view) — not just this host's units.
exec claude-rc-envs "$@"
;;
status|st)
show() {
env=$(url_of "$1")

110
bin/claude-rc-envs Executable file
View file

@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""claude-rc-envs — list / inspect / remove Claude Remote Control environments.
Account-wide view (the same list the claude.ai/code environment picker shows),
across every host. Reads the OAuth token from the macOS Keychain (Claude Code)
or ~/.claude/.credentials.json.
Usage:
claude-rc-envs # table: state, host, project, dir
claude-rc-envs --json # raw JSON
claude-rc-envs rm <env_id> # delete an environment (e.g. an orphan)
claude-rc-envs archive <id> # archive instead of delete
"""
import json
import os
import platform
import subprocess
import sys
import urllib.error
import urllib.request
API = "https://api.anthropic.com/v1/environments"
BETA = "environments-2025-11-01"
def token() -> str:
if platform.system() == "Darwin":
try:
out = subprocess.run(
["security", "find-generic-password", "-s", "Claude Code-credentials", "-w"],
capture_output=True, text=True, timeout=10,
)
if out.returncode == 0 and out.stdout.strip():
return json.loads(out.stdout)["claudeAiOauth"]["accessToken"]
except Exception:
pass
path = os.path.expanduser("~/.claude/.credentials.json")
if os.path.exists(path):
with open(path) as fh:
return json.load(fh)["claudeAiOauth"]["accessToken"]
sys.exit("claude-rc-envs: no Claude credentials found (Keychain or ~/.claude/.credentials.json)")
def call(method: str, url: str) -> dict:
req = urllib.request.Request(url, method=method, headers={
"Authorization": f"Bearer {token()}",
"anthropic-beta": BETA,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
})
try:
with urllib.request.urlopen(req, timeout=30) as resp:
body = resp.read()
return json.loads(body) if body else {}
except urllib.error.HTTPError as e:
sys.exit(f"claude-rc-envs: HTTP {e.code} {e.reason}: {e.read().decode()[:200]}")
except urllib.error.URLError as e:
sys.exit(f"claude-rc-envs: network error: {e.reason}")
def fetch() -> list:
return call("GET", API).get("data", [])
def row(e: dict):
c = e.get("config", {})
bridge = c.get("type") == "bridge"
host = c.get("machine_name") or ("cloud" if not bridge else "?")
name = e.get("name", "")
project = name.split(":")[1] if bridge and name.count(":") >= 1 else name
return e.get("state", "?"), host, project, e.get("id", ""), c.get("directory", "")
def list_envs():
rows = [row(e) for e in fetch()]
if not rows:
print("(no environments)")
return
wh = max(4, *(len(r[1]) for r in rows))
wp = max(7, *(len(r[2]) for r in rows))
print(f"{'STATE':8} {'HOST':{wh}} {'PROJECT':{wp}} {'ENV ID':20} DIR")
for state, host, project, env_id, directory in rows:
print(f"{state:8} {host:{wh}} {project:{wp}} {env_id:20} {directory}")
def main():
args = sys.argv[1:]
if not args:
return list_envs()
cmd = args[0]
if cmd in ("--json", "json"):
print(json.dumps(fetch(), indent=2))
elif cmd in ("rm", "delete", "--rm"):
if len(args) < 2:
sys.exit("usage: claude-rc-envs rm <env_id>")
call("DELETE", f"{API}/{args[1]}")
print(f"removed {args[1]}")
elif cmd in ("archive", "--archive"):
if len(args) < 2:
sys.exit("usage: claude-rc-envs archive <env_id>")
call("POST", f"{API}/{args[1]}/archive")
print(f"archived {args[1]}")
elif cmd in ("ls", "list", "status", "envs"):
list_envs()
else:
sys.exit(f"claude-rc-envs: unknown command '{cmd}' (try: ls | --json | rm <id> | archive <id>)")
if __name__ == "__main__":
main()

View file

@ -42,6 +42,14 @@ rc_args=''
usage() { sed -n '2,31p' "$0" | sed 's/^# \{0,1\}//'; }
# Account-wide environment list (the claude.ai/code picker view, every host).
# crc | crc status | crc envs → list
# crc envs rm <id> | archive <id> | --json
case "${1:-}" in
''|status) exec claude-rc-envs ;;
envs|ls) shift; exec claude-rc-envs "$@" ;;
esac
while [ $# -gt 0 ]; do
case "$1" in
-h|--help) usage; exit 0 ;;