#!/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())