
    
Bj                        U d Z ddlZddlZddlZddlZddlZddlZddlZddlm	Z	 ddl
mZmZmZ ddlmZmZ  ej"                  d      Zg dZg dZd	d
gZg dZee   ed<    eh d      Zee   ed<   dZdedefdZdefdZ eh d      Z  eh d      Z! eh d      Z"defdZ#defdZ$defdZ%defdZ&dedefdZ'deddfdZ(dedefdZ)dedefd Z*dedee   fd!Z+defd"Z,dedee   fd#Z-dedefd$Z.e	 G d% d&             Z/dede0fd'Z1dede0fd(Z2dedefd)Z3dede4fd*Z5dee/   fd+Z6	 	 	 	 	 dNded,ee   d-ed.ed/ed0edefd1Z7dOded2edee8   fd3Z9dOded4edefd5Z:dededdfd6Z;deddfd7Z<defd8Z=deddfd9Z>defd:Z?d;efd<Z@ded=edefd>ZAd?edee   fd@ZBdAedBeddfdCZCdAedeDe   fdDZEdPdEedee   defdFZFdGedHedIeddfdJZGdGedHedefdKZHdLedefdMZIy)Qu_  
Profile management for multiple isolated Hermes instances.

Each profile is a fully independent HERMES_HOME directory with its own
config.yaml, .env, memory, sessions, skills, gateway, cron, and logs.
Profiles live under ``~/.hermes/profiles/<name>/`` by default.

The "default" profile is ``~/.hermes`` itself — backward compatible,
zero migration needed.

Usage::

    hermes profile create coder          # fresh profile + bundled skills
    hermes profile create coder --clone  # also copy config, .env, SOUL.md, skills
    hermes profile create coder --clone-all  # full copy of source profile
    coder chat                           # use via wrapper alias
    hermes -p coder chat                 # or via flag
    hermes profile use coder             # set as sticky default
    hermes profile delete coder          # remove profile + alias + service
    N)	dataclass)PathPurePosixPathPureWindowsPath)ListOptionalz^[a-z0-9][a-z0-9_-]{0,63}$)	memoriessessionsskillsskinslogsplans	workspacecronhome)config.yaml.envSOUL.mdzmemories/MEMORY.mdzmemories/USER.md)gateway.pidgateway_state.jsonprocesses.json_CLONE_ALL_STRIP>   binprofilesnode_modules
.worktreeshermes-agent_CLONE_ALL_DEFAULT_EXCLUDE_ROOTz.no-bundled-skillsprofile_dirreturnc                 P    	 | t         z  j                         S # t        $ r Y yw xY w)z>Return True if the profile opted out of bundled-skill seeding.F)NO_BUNDLED_SKILLS_MARKERexistsOSError)r   s    8/home/ubuntu/.hermes/hermes-agent/hermes_cli/profiles.pyhas_bundled_skills_opt_outr&   m   s.    66>>@@ s    	%%
source_dirc                     | j                         t               j                         k(  dt        dt        t           dt        t           ffd}|S )uo  Exclude infrastructure artifacts when cloning a profile via --clone-all.

    Two categories:
      1. Root-level entries in ``_CLONE_ALL_DEFAULT_EXCLUDE_ROOT`` — known
         Hermes infrastructure directories that only the default profile
         (``~/.hermes``) ever contains.  Gated on ``source_dir`` actually
         being the default profile so a named-profile source never has its
         own data silently dropped.
      2. Universal exclusions at any depth — Python bytecode caches that
         are stale or regenerable (``__pycache__``, ``*.pyc``, ``*.pyo``)
         and runtime sockets / temp files (``*.sock``, ``*.tmp``).

    The export-side ignore (``_default_export_ignore``) uses the same
    two-tier pattern with the broader ``_DEFAULT_EXPORT_EXCLUDE_ROOT`` set
    because the export archive is a portable snapshot rather than a live
    clone.
    	directorynamesr    c                    g }|D ]c  }|dk(  s|j                  d      r|j                  |       +s.	 t        |       j                         k(  r|t        v r|j                  |       e |S # t
        t        f$ r Y yw xY w)N__pycache__)z.pycz.pyo.sock.tmp)endswithappendr   resolver   r$   
ValueError)r)   r*   ignoredentryis_default_sourcesource_resolveds       r%   _ignorez+_clone_all_copytree_ignore.<locals>._ignore   s     	E &>>"CDu% I..0OC $CC#NN51	&   ,  	s   5A..B ?B )r1   _get_default_hermes_homestrr   )r'   r7   r5   r6   s     @@r%   _clone_all_copytree_ignorer:   u   sT    $ !((*O'+C+E+M+M+OO3 tCy T#Y . N    >   state.db	auth.lock
errors.logstate.db-shmstate.db-wal.update_check.hermes_historyhermes_state.dbresponse_store.dbresponse_store.db-shmresponse_store.db-walr   r   r   	sandboxesaudio_cachecheckpointsimage_cacher   active_profiledocument_cachebrowser_screenshotsr   	auth.jsonr   r   r   r   r   >   tmprootsudotesthermesdefault>   acpmcpchatr   dumploginmodelsetuptoolsconfigdoctorhonchologoutr   statusupdategatewaypairingpluginsprofileversioninsightsr
   whatsapp	uninstallc                      t               dz  S )a  Return the directory where named profiles are stored.

    Anchored to the hermes root, NOT to the current HERMES_HOME
    (which may itself be a profile).  This ensures ``coder profile list``
    can see all profiles.

    In Docker/custom deployments where HERMES_HOME points outside
    ``~/.hermes``, profiles live under ``HERMES_HOME/profiles/`` so
    they persist on the mounted volume.
    r   r8    r;   r%   _get_profiles_rootrn      s     $%
22r;   c                      ddl m}   |        S )zReturn the default (pre-profile) HERMES_HOME path.

    In standard deployments this is ``~/.hermes``.
    In Docker/custom deployments where HERMES_HOME is outside ``~/.hermes``
    (e.g. ``/opt/data``), returns HERMES_HOME directly.
    r   get_default_hermes_root)hermes_constantsrq   rp   s    r%   r8   r8      s     9"$$r;   c                      t               dz  S )z2Return the path to the sticky active_profile file.rK   rl   rm   r;   r%   _get_active_profile_pathrt      s    #%(888r;   c                  6    t        j                         dz  dz  S )z)Return the directory for wrapper scripts.z.localr   )r   r   rm   r;   r%   _get_wrapper_dirrv      s    99;!E))r;   namec                     t        | t              st        |       } | j                         }|st        d      |j	                         dk(  ry|j                         S )u  Return the canonical profile id used on disk and in CLI ``-p`` argv.

    Named profiles are stored lowercase under ``profiles/<id>/``. The special
    alias ``default`` is matched case-insensitively (``Default`` → ``default``).
    Dashboards and tools may pass title-cased display labels; normalize before
    validation, assignment, and subprocess spawn (see issue #18498).
    zprofile name cannot be emptyrT   )
isinstancer9   stripr2   casefoldlower)rw   strippeds     r%   normalize_profile_namer~      sR     dC 4yzz|H788i'>>r;   c                     | dk(  ryt         j                  |       st        d| d      | t        v rt        d| d      y)u  Raise ``ValueError`` if *name* is not a valid profile identifier.

    Validates the input as-given — strict lowercase match. Callers that accept
    mixed-case or title-cased input from users (dashboard UI, CLI args) should
    call :func:`normalize_profile_name` first. This separation keeps validate
    honest about what the on-disk directory name must look like, while
    ingress-point normalization handles UX flexibility (see #18498).

    Also rejects names in :data:`_RESERVED_NAMES` (``hermes``, ``test``,
    ``tmp``, ``root``, ``sudo``) that would create confusing on-disk
    collisions (a ``hermes`` profile inside ``~/.hermes/``) or get refused
    at alias-creation time anyway. ``default`` is a special pass-through —
    it's a valid alias for the built-in root profile.
    rT   NzInvalid profile name z%. Must match [a-z0-9][a-z0-9_-]{0,63}zProfile name uz    is reserved — it collides with either the Hermes installation itself or a common system binary.  Pick a different name.)_PROFILE_ID_REmatchr2   _RESERVED_NAMES)rw   s    r%   validate_profile_namer     sh     y%#D8 ,) *
 	
 D8 $% &
 	
 r;   c                 P    t        |       }|dk(  r
t               S t               |z  S )z4Resolve a profile name to its HERMES_HOME directory.rT   )r~   r8   rn   rw   canons     r%   get_profile_dirr   )  s+    "4(E	'))%''r;   c                 V    t        |       }|dk(  ryt        |      j                         S )z)Check whether a profile directory exists.rT   T)r~   r   is_dirr   s     r%   profile_existsr   1  s+    "4(E	5!((**r;   c                    t        |       }|t        v rd| dS |t        v rd| dS t               }	 t	        j
                  d|gddd      }|j                  dk(  rN|j                  j                         }|t        ||z        k(  r	 ||z  j                         }d	|v ry
	 d| d| dS 	 y
# t        $ r Y w xY w# t        t        j                  f$ r Y y
w xY w)zReturn a human-readable collision message, or None if the name is safe.

    Checks: reserved names, hermes subcommands, existing binaries in PATH.
    'z' is a reserved namez$' conflicts with a hermes subcommandwhichT   )capture_outputtexttimeoutr   	hermes -pNz&' conflicts with an existing command ())r~   r   _HERMES_SUBCOMMANDSrv   
subprocessrun
returncodestdoutrz   r9   	read_text	ExceptionFileNotFoundErrorTimeoutExpired)rw   r   wrapper_dirresultexisting_pathcontents         r%   check_alias_collisionr   =  s   
 #4(E5'-..##5'=>> #$KeTa
 !"MM//1MK%$7 88*U2==?G"g-# . ugCM?RSTT "  !  z889 s6   AB< 	B- !	B< -	B96B< 8B99B< <CCc                      t        t                     } | t        j                  j	                  dd      j                  t        j                        v S )z!Check if ~/.local/bin is in PATH.PATH )r9   rv   osenvirongetsplitpathsep)r   s    r%   _is_wrapper_dir_in_pathr   _  s9    &()K"**..4::2::FFFr;   c                    t        |       }t               }	 |j                  dd       ||z  }	 |j                  d| d       |j                  |j                         j                  t        j                  z  t        j                  z  t        j                  z         |S # t        $ r}t	        d| d|        Y d}~yd}~ww xY w# t        $ r}t	        d| d|        Y d}~yd}~ww xY w)	zCreate a shell wrapper script at ~/.local/bin/<name>.

    Returns the path to the created wrapper, or None if creation failed.
    Tparentsexist_oku   ⚠ Could not create : Nz#!/bin/sh
exec hermes -p z "$@"
u    ⚠ Could not create wrapper at )r~   rv   mkdirr$   print
write_textchmodstatst_modeS_IEXECS_IXGRPS_IXOTH)rw   r   r   ewrapper_paths        r%   create_wrapper_scriptr   e  s    
 #4(E"$K$6
 &L"<UG7 KL<,,.66ETW[WcWccd  %k]"QC89  0bDEs/   B# A2C
 #	C,CC
	C.C))C.c                     t               t        |       z  }|j                         r(	 |j                         }d|v r|j	                          y	 yy# t
        $ r Y yw xY w)zARemove the wrapper script for a profile. Returns True if removed.r   TF)rv   r~   r#   r   unlinkr   )rw   r   r   s      r%   remove_wrapper_scriptr   |  sl    #%(>t(DDL	",,.Gg%##% &
   		s   $A 	AAc                       e Zd ZU dZeed<   eed<   eed<   eed<   dZe	e   ed<   dZ
e	e   ed<   d	Zeed
<   dZeed<   dZe	e   ed<   dZe	e   ed<   dZe	e   ed<   dZe	e   ed<   y)ProfileInfoz$Summary information about a profile.rw   path
is_defaultgateway_runningNrZ   providerFhas_envr   skill_count
alias_pathdistribution_namedistribution_versiondistribution_source)__name__
__module____qualname____doc__r9   __annotations__r   boolrZ   r   r   r   r   intr   r   r   r   rm   r;   r%   r   r     s    .
I
JE8C="Hhsm"GTK!%J%'+x}+*.(3-.)-#-r;   r   c                 L   | dz  }|j                         sy	 ddl}t        |dd      5 }|j                  |      xs i }ddd       t	        t
              sy|j                  d      |j                  d	      |j                  d
      fS # 1 sw Y   LxY w# t        $ r Y yw xY w)u  Return ``(name, version, source)`` from the profile's ``distribution.yaml``
    if present; ``(None, None, None)`` otherwise.

    Failures (missing file, bad YAML) are swallowed — a bad manifest should
    never break ``hermes profile list`` for an unrelated profile.
    zdistribution.yaml)NNNr   Nrutf-8encodingrw   rg   source)is_fileyamlopen	safe_loadry   dictr   r   )r   mf_pathr   fdatas        r%   _read_distribution_metar     s     //G?? '31 	+Q>>!$*D	+$%#HHVHHYHHX
 	
		+ 	+    s.   B B B 1B BB 	B#"B#c                    | dz  }|j                         sy	 ddl}t        |dd      5 }|j                  |      xs i }ddd       j	                  di       }t        |t              r|dfS t        |t              r5|j	                  d	      xs |j	                  d      |j	                  d
      fS y# 1 sw Y   uxY w# t        $ r Y yw xY w)zLRead model/provider from a profile's config.yaml. Returns (model, provider).r   )NNr   Nr   r   r   rZ   rT   r   )	r#   r   r   r   r   ry   r9   r   r   )r   config_pathr   r   cfg	model_cfgs         r%   _read_config_modelr     s    -K+sW5 	*..#)rC	*GGGR(	i%d?"i&==+Ey}}W/Ey}}U_G```	* 	*  s/   C  B4 -C  .AC  4B=9C   	CCc                 L    	 ddl m}  || dz  d      duS # t        $ r Y yw xY w)z<Check if a gateway is running for a given profile directory.r   )get_running_pidr   F)cleanup_staleN)gateway.statusr   r   )r   r   s     r%   _check_gateway_runningr     s4    2{]:%PX\\\ s    	##c                     | dz  }|j                         syd}|j                  d      D ]#  }dt        |      vsdt        |      vs|dz  }% |S )z$Count installed skills in a profile.r   r   zSKILL.mdz/.hub/z/.git/   )r   rglobr9   )r   
skills_dircountmds       r%   _count_skillsr     sa    x'JEz* 3r7"xs2w'>QJE Lr;   c                     g } t               }t               }|j                         ret        |      \  }}t	        |      \  }}}| j                  t        d|dt        |      |||dz  j                         t        |      |||             t               }|j                         rt        |j                               D ]  }	|	j                         s|	j                  }
t        j                  |
      s6t        |	      \  }}||
z  }t	        |	      \  }}}| j                  t        |
|	dt        |	      |||	dz  j                         t        |	      |j                         r|nd|||              | S )z4Return info for all profiles, including the default.rT   Tr   )rw   r   r   r   rZ   r   r   r   r   r   r   FN)rw   r   r   r   rZ   r   r   r   r   r   r   r   )rv   r8   r   r   r   r0   r   r   r#   r   rn   sortediterdirrw   r   r   )r   r   default_homerZ   r   	dist_namedist_versiondist_sourceprofiles_rootr4   rw   r   s               r%   list_profilesr     st   H"$K ,-L,\:x/F|/T,	<2<@!F*224%l3'!- +
 	 '(MM1134 	E<<>::D!''-07OE8$t+J3J53Q0I|[OOK  6u =!//1)%0)3):):)<:$"+%1$/ 	0 Or;   
clone_from	clone_allclone_configno_alias	no_skillsc                    |r|s|rt        d      t        |       }t        |       |dk(  rt        d      t        |      }|j	                         rt        d| d|       d}||s|rV|ddlm}	  |	       }n!t        |      }t        |       t        |      }|j                         st        d	|xs d
 d|       |rD|rBt        j                  ||t        |             t        D ]  }
||
z  j                  d        n|j                  dd       t         D ]  }||z  j                  dd        |t"        D ]1  }||z  }|j	                         st        j$                  |||z         3 |dz  }|j                         rt        j                  ||dz  d       t&        D ]P  }||z  }|j	                         s||z  }|j(                  j                  dd       t        j$                  ||       R |dz  }|j	                         s	 ddlm} |j/                  |d       |r	 |t2        z  j/                  dd       |S |S # t0        $ r Y ,w xY w# t4        $ r Y |S w xY w)a  Create a new profile directory.

    Parameters
    ----------
    name:
        Profile identifier (lowercase, alphanumeric, hyphens, underscores).
    clone_from:
        Source profile to clone from. If ``None`` and clone_config/clone_all
        is True, defaults to the currently active profile.
    clone_all:
        If True, do a full copytree of the source (all state).
    clone_config:
        If True, copy config files (config.yaml, .env, SOUL.md), installed
        skills, and selected profile identity files from the source profile.
    no_alias:
        If True, skip wrapper script creation.
    no_skills:
        If True, create an empty profile with no bundled skills, and write
        a marker file so ``hermes update`` skips re-seeding this profile's
        skills. Mutually exclusive with ``clone_config``/``clone_all`` (those
        explicitly copy skills from the source).

    Returns
    -------
    Path
        The newly created profile directory.
    zx--no-skills is mutually exclusive with --clone / --clone-all (cloning explicitly copies skills from the source profile).rT   uS   Cannot create a profile named 'default' — it is the built-in profile (~/.hermes).	Profile '' already exists at Nr   get_hermes_homezSource profile 'activez' does not exist at ignoreT
missing_okr   r   )dirs_exist_okr   )DEFAULT_SOUL_MDr   r   zThis profile opted out of bundled-skill seeding (`hermes profile create --no-skills`).
Delete this file to re-enable sync on the next `hermes update`.
)r2   r~   r   r   r#   FileExistsErrorrr   r  r   r   shutilcopytreer:   r   r   r   _PROFILE_DIRS_CLONE_CONFIG_FILEScopy2_CLONE_SUBDIR_FILESparenthermes_cli.default_soulr  r   r   r"   r$   )rw   r   r   r   r   r   r   r   r'   r  stalesubdirfilenamesrcsource_skillsrelpathdst	soul_pathr  s                      r%   create_profiler    s   F liJ
 	
 #4(E% 	a
 	
 "%(K	%0D[MRSS Jl8(*J/
;J!*-(4J  "#":#9"::Nzl[  Z-j9	
 & 	:E5 ((D(9	: 	$6# 	FF6!(((E	F !/ > 8+::<LLkH&<=> '1M##%{X/EUYZ / + 7*::<%/CJJ$$TD$ALLc*+ i'I	?  7 C 	33??T !	 @  ;!  		  		s$   I 3I  	II 	I-,I-quietc                 $   t        |       rg g g ddS t        t              j                  j                  j	                         }	 t        j                  t        j                  ddgi t        j                  dt        |       it        |      ddd      }|j                  dk(  rG|j                  j                         r-t        j                   |j                  j                               S |s[t#        d	|j                          |j$                  j                         r)t#        d
|j$                  j                         dd         y# t
        j&                  $ r |st#        d       Y yt(        $ r}|st#        d|        Y d}~yd}~ww xY w)u  Seed bundled skills into a profile via subprocess.

    Uses subprocess because sync_skills() caches HERMES_HOME at module level.
    Returns the sync result dict, or None on failure.

    Profiles that opted out of bundled skills (via ``hermes profile create
    --no-skills`` — which writes ``.no-bundled-skills`` to the profile root)
    are skipped and get an empty-result dict so callers can report
    "opted out" instead of "failed".
    T)copiedupdateduser_modifiedskipped_opt_outz-cziimport json; from tools.skills_sync import sync_skills; r = sync_skills(quiet=True); print(json.dumps(r))HERMES_HOME<   )envcwdr   r   r   r   u%   ⚠ Skill seeding returned exit code z  N   u!   ⚠ Skill seeding timed out (60s)u   ⚠ Skill seeding failed: )r&   r   __file__r  r1   r   r   sys
executabler   r   r9   r   r   rz   jsonloadsr   stderrr   r   )r   r  project_rootr   r   s        r%   seed_profile_skillsr0    sc    "+.#	
 	
 >((//779L^^TAB @2::?}c+.>?L!dB
 !fmm&9&9&;::fmm113449&:K:K9LMN}}""$6==..0#6789$$ 56 .qc23s&   B'E -AE  F-F5F

Fyesc                    t        |       }t        |       |dk(  rt        d      t        |      }|j	                         st        d| d      t        |      \  }}t        |      }t        |      }t        |      \  }}	}
t        d|        t        d|        |rt        d| |rd| d	nd
z          |rt        d|        |r%t        d| d|	xs d        |
rt        d|
        dg}t               |z  }|j                         }|r|j                  d| d	       t        d       |D ]  }t        d|         |rt        d       |s:t                	 t        d| d      j                         }||k7  rt        d       |S t%        ||       |rt'        |       |rt)        |      rt        d|        	 t+        j,                  |       t        d|        	 t1               }||k(  rt3        d       t        d       t        d| d       |S # t         t"        f$ r t        d       |cY S w xY w# t.        $ r}t        d| d|        Y d}~xd}~ww xY w# t.        $ r Y ew xY w) zDelete a profile, its wrapper script, and its gateway service.

    Stops the gateway if running. Disables systemd/launchd service first
    to prevent auto-restart.

    Returns the path that was removed.
    rT   zZCannot delete the default profile (~/.hermes).
To remove everything, use: hermes uninstallr  ' does not exist.z

Profile: z	Path:    z	Model:   z (r   r   z	Skills:  zDistribution: @?zInstalled from: z;All config, API keys, memories, sessions, skills, cron jobszCommand alias (z
This will permanently delete:u     • u0     ⚠ Gateway is running — it will be stopped.zType 'z' to confirm: z
Cancelled.z
Cancelled.u   ✓ Removed u   ⚠ Could not remove r   Nu#   ✓ Active profile reset to defaultz

Profile 'z
' deleted.)r~   r   r2   r   r   r   r   r   r   r   r   rv   r#   r0   inputrz   KeyboardInterruptEOFError_cleanup_gateway_service_stop_gateway_processr   r  rmtreer   get_active_profileset_active_profile)rw   r1  r   r   rZ   r   
gw_runningr   r   r   r   itemsr   has_wrapperitemconfirmr   r  s                     r%   delete_profilerC    s    #4(E% 	:
 	

 "%(K)E72C DEE )5OE8'4J,K+B;+O(I|[	Kw
 	Ik]
#$	%!r(1%5rJK	+'(yk<+>3*?@A$[M23 	FE
 $%-L%%'K|nA67	+- tfo@B 	fUG>:;AACG e, UK0 k*  'L/0:k"[M*+
#%U?y)78 
Kwj
)*I "8, 	.!	.  :%k]"QC899:  s<   #H! #I +%I* !I ?I 	I'I""I'*	I65I6c                 .   ddl }t        j                  j                  d      }	 t	        |      t        j                  d<   ddlm}m} |j                         dk(  r |       }t        j                         dz  dz  dz  | d	z  }|j                         rt        j                  d
dd|gddd       t        j                  d
dd|gddd       |j                  d       t        j                  g dddd       t        d| d       nl|j                         dk(  rY |       }|j                         rBt        j                  ddt	        |      gddd       |j                  d       t        d       ||t        j                  d<   ydt        j                  v rt        j                  d= yy# t         $ r}	t        d|	        Y d}	~	Wd}	~	ww xY w# ||t        j                  d<   w dt        j                  v rt        j                  d= w w xY w)z9Disable and remove systemd/launchd service for a profile.r   Nr$  )get_service_nameget_launchd_plist_pathLinuxz.configsystemduserz.service	systemctl--userdisableTF
   )r   checkr   stopr  )rJ  rK  zdaemon-reloadu   ✓ Service z removedDarwin	launchctlunloadu   ✓ Launchd service removedu   ⚠ Service cleanup: )platformr   r   r   r9   hermes_cli.gatewayrE  rF  systemr   r   r#   r   r   r   r   r   )
rw   r   	_platformold_homerE  rF  svc_namesvc_file
plist_pathr   s
             r%   r9  r9  0  s     zz~~m,H&*$'$4

=!O(')Hyy{Y.:VC
RZF[[H  (Ix@#'ub  (FH=#'ub 40<#'ub XJh788+/1J  " (C
O<#'ub !!T!235 (0BJJ}%bjj(

=) )  +%aS)**+ (0BJJ}%bjj(

=) )s*   EF4 4	G=GG GG <Hc                 p   ddl }| dz  }|j                         sy	 |j                         j                         }|j	                  d      rt        j                  |      ndt        |      i}t        |d         }ddlm	} ddlm
}  ||       t        d      D ],  }|j                  d	        ||      rt        d
| d        y 	  ||d       t        d| d       y# t        t        f$ r Y !w xY w# t        t         f$ r t        d       Y yt"        $ r}	t        d|	        Y d}	~	yd}	~	ww xY w)z0Stop a running gateway process via its PID file.r   Nr   {pid)terminate_pid)_pid_exists   g      ?u   ✓ Gateway stopped (PID r   T)forceu   ✓ Gateway force-stopped (PID u   ✓ Gateway already stoppedu   ⚠ Could not stop gateway: )timer#   r   rz   
startswithr,  r-  r   r   r^  r_  rangesleepr   ProcessLookupErrorr$   PermissionErrorr   )
r   _timepid_filerawr   r]  _terminate_pidr_  _r   s
             r%   r:  r:  `  s.   ]*H??2  "((*"%.."5tzz#E3s8;L$u+ 	C.s r 	AKKs#1#a89			3d+ 	/uA67 #G, 		 0 -+, 2,QC0112sN   BC9 7C9 C9 

C$ C9 $C63C9 5C66C9 9D5D5D00D5c                      t               } 	 | j                         j                         }|sy|S # t        t        t
        f$ r Y yw xY w)ztRead the sticky active profile name.

    Returns ``"default"`` if no active_profile file exists or it's empty.
    rT   )rt   r   rz   r   UnicodeDecodeErrorr$   )r   rw   s     r%   r<  r<    sK    
 $%D~~%%'17; s    / / AAc                 \   t        |       }t        |       |dk7  rt        |      st        d| d|       t	               }|j
                  j                  dd       |dk(  r|j                  d       y	|j                  d      }|j                  |dz          |j                  |       y	)
zlSet the sticky active profile.

    Writes to ``~/.hermes/active_profile``. Use ``"default"`` to clear.
    rT   r  8' does not exist. Create it with: hermes profile create Tr   r  r.   
N)r~   r   r   r   rt   r  r   r   with_suffixr   replace)rw   r   r   rO   s       r%   r=  r=    s    
 #4(E% 	."7w 55:G=
 	

 $%DKKdT2	t$ v&ut|$Dr;   c                  Z   ddl m}   |        }|j                         }t               j                         }||k(  ryt	               j                         }	 |j                  |      }|j                  }t        |      dk(  rt        j                  |d         r|d   S y# t        $ r Y yw xY w)a%  Infer the current profile name from HERMES_HOME.

    Returns ``"default"`` if HERMES_HOME is not set or points to ``~/.hermes``.
    Returns the profile name if HERMES_HOME points into ``~/.hermes/profiles/<name>``.
    Returns ``"custom"`` if HERMES_HOME is set to an unrecognized path.
    r   r  rT   r   custom)rr   r  r1   r8   rn   relative_topartslenr   r   r2   )r  hermes_homeresolveddefault_resolvedr   relrw  s          r%   get_active_profile_namer}    s     1!#K""$H/199;##&(002M""=1		u:?~33E!H=8O   s   AB 	B*)B*root_dirc                 8     dt         dt        dt        f fd}|S )zReturn an *ignore* callable for :func:`shutil.copytree`.

    At the root level it excludes everything in ``_DEFAULT_EXPORT_EXCLUDE_ROOT``.
    At all levels it excludes ``__pycache__``, sockets, and temp files.
    r)   contentsr    c                     t               }|D ]@  }|dk(  s|j                  d      r|j                  |       +|dv s0|j                  |       B t        |       k(  r|j	                  d |D               |S )Nr,   )r-   r.   >   package.jsonpackage-lock.jsonc              3   2   K   | ]  }|t         v s|  y wN)_DEFAULT_EXPORT_EXCLUDE_ROOT).0cs     r%   	<genexpr>z:_default_export_ignore.<locals>._ignore.<locals>.<genexpr>  s     T!7S2S1Ts   )setr/   addr   rb   )r)   r  r3   r4   r~  s       r%   r7   z'_default_export_ignore.<locals>._ignore  ss    u 	#E%8I)JE"??E"	# 	?h&NNThTTr;   )r9   listr  )r~  r7   s   ` r%   _default_export_ignorer    s$    3 $ 3  Nr;   output_pathc                   
 ddl }t        |       }t        |       t        |      }|j	                         st        d| d      t        |      }t        |      j                  d      j                  d      }|dk(  rl|j                         5 }t        |      dz  }t        j                  ||t        |             t        j                  |d	|d      }	t        |	      cddd       S |j                         5 }t        |      |z  }d
dh
t        j                  ||
fd       t        j                  |d	||      }	t        |	      cddd       S # 1 sw Y   sxY w# 1 sw Y   yxY w)zMExport a profile to a tar.gz archive.

    Returns the output file path.
    r   Nr  r3  z.tar.gzz.tgzrT   r  gztarrN   r   c                      t        |      z  S r  )r  )dr  _CREDENTIAL_FILESs     r%   <lambda>z export_profile.<locals>.<lambda>  s    '83x='H r;   )tempfiler~   r   r   r   r   r   r9   removesuffixTemporaryDirectoryr  r  r  make_archive)rw   r  r  r   r   outputbasetmpdirstagedr   r  s             @r%   export_profiler    s^   
 "4(E% !%(K)E72C DEE+Fv;##I.;;FCD	 ((* 	 f&\I-FOO-k:
 ((w	JF<	  	  
	$	$	& 	&f%(&1H	

 $$T7FEBF|	 		  	 	 	s   AE;AE!E!E*member_namec                 X   | j                  dd      }t        |      }t        |       }|r,|j                         s|j                         s|j                  rt        d|        |j                  D cg c]	  }|dvs| }}|rt        d |D              rt        d|        |S c c}w )z4Return safe path parts for a profile archive member.\/zUnsafe archive member path: >   r   .c              3   &   K   | ]	  }|d k(    yw)z..Nrm   )r  parts     r%   r  z3_normalize_profile_archive_parts.<locals>.<genexpr>%  s     77s   )rs  r   r   is_absolutedriver2   rw  any)r  normalized_name
posix_pathwindows_pathr  rw  s         r%    _normalize_profile_archive_partsr    s    !))$4O/J";/L !!###%7}EFF(..Hd$i2GTHEHC7777}EFFL Is   3	B'=B'archivedestinationc           	         ddl }|j                  | d      5 }|j                         D ]  }t        |j                        } |j
                  | }|j                         r|j                  dd       L|j                         st        d|j                         |j                  j                  dd       |j                  |      }|t        d|j                         |5  t        |d      5 }t        j                  ||       ddd       ddd       	 t        j                  ||j                   d	z          	 ddd       y# 1 sw Y   BxY w# 1 sw Y   FxY w# t"        $ r Y Gw xY w# 1 sw Y   yxY w)
zAExtract a profile archive without allowing path escapes or links.r   Nr:gzTr   z!Unsupported archive member type: zCannot read archive member: wbi  )tarfiler   
getmembersr  rw   joinpathisdirr   isfiler2   r  extractfiler  copyfileobjr   r   moder$   )	r  r  r  tfmemberrw  target	extractedr  s	            r%   _safe_extract_profile_archiver  *  s_   	gv	& "mmo 	F4V[[AE)[))51F||~TD9==? 7}E  MMt<v.I  #?}!MNN 3D. 3#""9c23 3u!45-	 &3 3 3 3
  1 s`   CE3&E3E	
EE3#E#>E3EEE E3#	E0,E3/E00E33E<c                    ddl }|j                  | d      5 }|j                         D ch c]:  }t        |j                        }t        |      dkD  s|j                         r|d   < }}}|sC|j                         D ch c]*  }|j                         rt        |j                        d   , }}ddd       |S c c}}w c c}w # 1 sw Y   S xY w)a  Return the archive's top-level directory names.

    Profile imports expect exactly one root directory. Inspecting the archive
    before extraction lets us stage the import safely instead of mutating a
    live profile tree first and reconciling names later.
    r   Nr  r   )r  r   r  r  rw   rx  r  )r  r  r  r  rw  top_dirss         r%   _inspect_profile_archive_rootsr  J  s     	gv	& " --/
:6;;G5zA~ !H
 
  !mmo<<> 1=a@H  O
 Os(   C?B<*C/C0C<CCarchive_pathc                 "   ddl }t        |       }|j                         st        d|       t	        |      }t        |      dk(  r|j                         nd}|xs |}|st        d      |t        d      t        |      }t        |       |dk(  rt        d      t        |      }|j                         rt        d	| d
|       t               }	|	j                  dd       |j                  d      5 }
t        |
      }t        ||       ||z  }|j!                         st        d|       |}||k7  r||z  }|j#                  |       t%        j&                  t)        |      t)        |             ddd       |S # 1 sw Y   |S xY w)zImport a profile from a tar.gz archive.

    If *name* is not given, infers it from the archive's top-level directory.
    Returns the imported profile directory.
    r   NzArchive not found: r   zpCannot determine profile name from archive. Specify it explicitly: hermes profile import <archive> --name <name>z=Profile archive must contain exactly one top-level directory.rT   u   Cannot import as 'default' — that is the built-in root profile (~/.hermes). Specify a different name: hermes profile import <archive> --name <name>r  r  Tr   hermes_profile_import_)prefixz,Profile archive root is missing or invalid: )r  r   r#   r   r  rx  popr2   r~   r   r   r  rn   r   r  r  r   renamer  mover9   )r  rw   r  r  r  archive_rootinferred_namer   r   r   r  staging_rootr  final_sources                 r%   import_profiler  c  s    < G>>"5gY ?@@-g6H%(]a%78<<>TL(LMS
 	
 K
 	
 #=1E% 	V
 	

 "%(K	%0D[MRSS&(Mt4		$	$,D	$	E 9F|%g|< </	!>|nM  !5 '%/L\*C%s;'789" #9" s   :B FFold_namenew_namenew_dirc                    d|  }d| }|dz  t               dz  t        j                         dz  dz  g}t               }|D ]V  }	 |j	                         }||v s|j                         s*|j                  |       	 t        j                  |j                  d            }	|	j                  d      }
t        |
t              r||
vr||
v rt        d| d	|        |
|   }t        |t              r$d
|vr d|v r|j!                  dd      d   n|}||d
<   |
j#                  |      |
|<   |j%                  |j&                  dz         }	 |j)                  t        j*                  |	dd      dz   d       |j-                  |       t        d| d|        Y y# t
        $ r |}Y Tw xY w# t
        t        j                  f$ r Y w xY w# t
        $ r' 	 |j/                  d       n# t
        $ r Y nw xY wY w xY w)zGRename Honcho host blocks for a renamed profile without changing peers.zhermes.zhoncho.jsonz.honchozconfig.jsonr   r   hostsu$   ⚠ Honcho host block not migrated: z already exists in aiPeerr  r   r.      F)indentensure_asciirq  Tr  u   ✓ Honcho host updated:     → N)r8   r   r   r  r1   r$   r   r  r,  r-  r   JSONDecodeErrorr   ry   r   r   r   r  rr  suffixr   dumpsrs  r   )r  r  r  old_hostnew_host
candidatesseenr   rz  rj  r  blockbarerO   s                 r%   _migrate_honcho_profile_hostr    s   
#H
#H 	- "]2		i-/J eD &E	||~H t4<<>	**T^^W^=>C  %&(%*?u8
BUVZU[\]heT"xu'<03x8>>#q)!,XD"E(O))H-ht{{V34	NN4::c!%H4OZaNbKK 	)(5
CDM&E  	H	 --. 		(  	

d
+ 	sZ   F>%F/=GF,+F,/GG	G?G,+G?,	G85G?7G88G?>G?c                    t        |       }t        |      }t        |       t        |       |dk(  rt        d      |dk(  rt        d      t        |      }t        |      }|j	                         st        d| d      |j                         rt        d| d      t        |      rt        ||       t        |       |j                  |       t        d|j                   d|j                          t        |||       t        |       t!        |      }|st#        |       t        d	|        nt        d
| d|        	 t%               |k(  rt'        |       t        d|        |S # t(        $ r Y |S w xY w)zrRename a profile: directory, wrapper script, service, active_profile.

    Returns the new profile directory.
    rT   z"Cannot rename the default profile.u.   Cannot rename to 'default' — it is reserved.r  r3  z' already exists.u   ✓ Renamed r  u   ✓ Alias updated: u   ⚠ Cannot create alias 'u   ' — u   ✓ Active profile updated: )r~   r   r2   r   r   r   r#   r  r   r9  r:  r  r   rw   r  r   r   r   r<  r=  r   )r  r  	old_canon	new_canonold_dirr  	collisions          r%   rename_profiler    s   
 'x0I&x0I)$)$I=>>IIJJi(Gi(G>>)I;6G HII~~	)4EFGG g& G4g& NN7	LeGLL>
:; !Iw? )$%i0Ii(#I;/0))F9+FG9,y)0<= N  Ns   
&E2 2	E?>E?profile_namec                     t        |       }t        |       t        |      }|dk7  r!|j                         st	        d| d|       t        |      S )zResolve a profile name to a HERMES_HOME path string.

    Called early in the CLI entry point, before any hermes modules
    are imported, to set the HERMES_HOME environment variable.
    rT   r  rp  )r~   r   r   r   r   r9   )r  r   r   s      r%   resolve_profile_envr    sc     #<0E% !%(K	+"4"4"6w 55:G=
 	

 {r;   )NFFFF)Fr  )Jr   r,  r   rer  r   r   r*  dataclassesr   pathlibr   r   r   typingr   r   compiler   r  r  r  r   r  r9   r   	frozensetr   r"   r   r&   r:   r  r   r   rn   r8   rt   rv   r~   r   r   r   r   r   r   r   r   tupler   r   r   r   r   r   r  r   r0  rC  r9  r:  r<  r=  r}  r  r  r  r  r  r  r  r  r  r  rm   r;   r%   <module>r     s  *  	 	    
 ! 8 8 !9:"    $s) , 3< = 3 3  0 D T ,4 ,f  ) *   2   
   !  3D 3%$ %9$ 9
*$ *  $
 
 
<(# ($ (+ + +  DG G  .  & . . ."   %  2D U &  	t 	 	 3tK( 3p !%B
BB B 	B
 B B 
BJ*T *$ *8D> *Z_ _4 _D _D-*3 -*T -*d -*`$2t $2 $2VC S T 2 >T 0) )3 )4 )X# $s) (4 d t @D SX 2< <HSM <T <F2E3 2E# 2E 2EQU 2Ej4S 4C 4D 4vc c r;   