Files
codexpi/scripts/codexpi-remote

104 lines
4.3 KiB
Plaintext
Raw Permalink Normal View History

2026-11-14 20:23:59 +01:00
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import os
import sys
from codexpi.remote_ipc import remote_channel_request
from codexpi.remote_service import RemoteError, enqueue_remote_job
def main() -> int:
parser = argparse.ArgumentParser(description="Accès fichier limité à une machine associée à CodexPi")
parser.add_argument("--machine", help="identifiant de machine ; facultatif si une seule est associée")
subparsers = parser.add_subparsers(dest="operation", required=True)
subparsers.add_parser("machines")
listing = subparsers.add_parser("list")
listing.add_argument("path", nargs="?", default="")
reading = subparsers.add_parser("read")
reading.add_argument("path")
writing = subparsers.add_parser("write")
writing.add_argument("path")
writing.add_argument("--expected", help="SHA-256 obtenu lors de la lecture, pour empêcher un écrasement concurrent")
executing = subparsers.add_parser("exec")
executing.add_argument("argv", nargs=argparse.REMAINDER)
args = parser.parse_args()
database = os.environ.get("CODEXPI_DATABASE", "/app/data/codexpi.db")
remote_channel = os.environ.get("CODEXPI_REMOTE_CHANNEL")
project_id = os.environ.get("CODEXPI_PROJECT_ID", "")
if not project_id:
print("Cette commande doit être lancée depuis une session de projet CodexPi.", file=sys.stderr)
return 2
try:
if remote_channel:
payload = None
expected = None
if args.operation == "write":
if sys.stdin.isatty():
raise RemoteError("Le contenu du fichier doit être fourni sur l’entrée standard")
payload = {"content": sys.stdin.read()}
expected = args.expected
elif args.operation == "exec":
if not args.argv:
raise RemoteError("Indiquez la commande distante après exec")
payload = {"argv": args.argv, "timeout": 120}
result = remote_channel_request(remote_channel, {
"operation": args.operation,
"path": getattr(args, "path", ""),
"payload": payload,
"expected_hash": expected,
"machine_id": args.machine,
})
if args.operation == "machines":
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0
elif args.operation == "machines":
import sqlite3
connection = sqlite3.connect(database)
connection.row_factory = sqlite3.Row
rows = connection.execute(
"SELECT id,name,folder_name,platform,last_seen FROM remote_machines WHERE project_id=? AND revoked_at IS NULL ORDER BY last_seen DESC",
(project_id,),
).fetchall()
print(json.dumps([dict(row) for row in rows], ensure_ascii=False, indent=2))
return 0
else:
payload = None
expected = None
if args.operation == "write":
if sys.stdin.isatty():
raise RemoteError("Le contenu du fichier doit être fourni sur l’entrée standard")
content = sys.stdin.read()
payload = {"content": content}
expected = args.expected
elif args.operation == "exec":
if not args.argv:
raise RemoteError("Indiquez la commande distante après exec")
payload = {"argv": args.argv, "timeout": 120}
result = enqueue_remote_job(
database,
project_id,
args.operation,
args.path,
payload,
expected,
args.machine,
)
if args.operation == "read":
sys.stdout.write(str(result.get("content", "")))
if result.get("content") and not str(result["content"]).endswith("\n"):
sys.stdout.write("\n")
print(f"[sha256:{result.get('sha256','')}]", file=sys.stderr)
else:
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0
except (RemoteError, OSError, ValueError) as exc:
print(f"Erreur distante : {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())