
    PL
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defd+Z6dede7fd,Z8ddd-ded.ee         d/ee         ddfd0Z9dee/         fd1Z:	 	 	 	 	 	 dUded3ee         d4ed5ed6ed7ed.ee         defd8Z;dVded9edee7         fd:Z<dVded;edefd<Z=dededdfd=Z>deddfd>Z?defd?Z@deddfd@ZAdefdAZBdBefdCZCdedDedefdEZDdFedee         fdGZEdHedIeddfdJZFdHedeGe         fdKZHdWdLedee         defdMZIdNedOedPeddfdQZJdNedOedefdRZKdSedefdTZLdS )Xu_  
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                 ^    	 | t           z                                  S # t          $ r Y dS 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    7/home/kuhnn/.hermes/hermes-agent/hermes_cli/profiles.pyhas_bundled_skills_opt_outr'   m   sA    66>>@@@   uus    
,,
source_dirc                     |                                  t                                                       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                 4   g }|D ]}|dk    s|                     d          r|                    |           3r\	 t          |                                           k    r|t          v r|                    |           z# t
          t          f$ r Y w xY w|S )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D ' u%%%  	I..00OCC $CCC#NN5111,    D		 s   AB  BB)r2   _get_default_hermes_homestrr   )r(   r8   r6   r7   s     @@r&   _clone_all_copytree_ignorer;   u   s|    $ !((**O'+C+E+E+M+M+O+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   r9    r<   r&   _get_profiles_rootro      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_constantsrr   rq   s    r&   r9   r9      s%     988888""$$$r<   c                  $    t                      dz  S )z2Return the path to the sticky active_profile file.rL   rm   rn   r<   r&   _get_active_profile_pathru      s    #%%(888r<   c                  4    t          j                    dz  dz  S )z)Return the directory for wrapper scripts.z.localr   )r   r   rn   r<   r&   _get_wrapper_dirrw      s    9;;!E))r<   namec                     t          | t                    st          |           } |                                 }|st          d          |                                dk    rdS |                                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 emptyrU   )
isinstancer:   stripr3   casefoldlower)rx   strippeds     r&   normalize_profile_namer      sq     dC   4yyzz||H 97888i''y>>r<   c                     | dk    rdS t                               |           st          d| d          | t          v rt          d| d          dS )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.
    rU   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matchr3   _RESERVED_NAMES)rx   s    r&   validate_profile_namer     s     y%% 
*D * * *
 
 	
 &D & & &
 
 	
 r<   c                 j    t          |           }|dk    rt                      S t                      |z  S )z4Resolve a profile name to its HERMES_HOME directory.rU   )r   r9   ro   rx   canons     r&   get_profile_dirr   )  s7    "4((E	')))%''r<   c                 r    t          |           }|dk    rdS t          |                                          S )z)Check whether a profile directory exists.rU   T)r   r   is_dirr   s     r&   profile_existsr   1  s9    "4((E	t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    rg|j                                        }|t          ||z            k    r/	 ||z  
                                }d	|v rd
S n# t          $ r Y nw xY wd| d| dS n# t          t          j        f$ r Y nw xY wd
S )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_SUBCOMMANDSrw   
subprocessrun
returncodestdoutr{   r:   	read_text	ExceptionFileNotFoundErrorTimeoutExpired)rx   r   wrapper_dirresultexisting_pathcontents         r&   check_alias_collisionr   =  sZ   
 #4((E.5....###>5>>>> #$$KeTa
 
 
 !!"M//11MK%$7 8 888*U2==??G"g--#t .    DTuTTMTTTT " z89    4s6   AC
 B0 /C
 0
B=:C
 <B==C
 
C#"C#c                      t          t                                } | t          j                            dd                              t          j                  v S )z!Check if ~/.local/bin is in PATH.PATH )r:   rw   osenvirongetsplitpathsep)r   s    r&   _is_wrapper_dir_in_pathr   _  sA    &(())K"*..44::2:FFFFr<   c                 
   t          |           }t                      }	 |                    dd           n-# t          $ r }t	          d| d|            Y d}~dS d}~ww xY w||z  }	 |                    d| d           |                    |                                j        t          j	        z  t          j
        z  t          j        z             |S # t          $ r }t	          d| d|            Y d}~dS 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   rw   mkdirr$   print
write_textchmodstatst_modeS_IEXECS_IXGRPS_IXOTH)rx   r   r   ewrapper_paths        r&   create_wrapper_scriptr   e  sD   
 #4((E"$$K$6666   8k88Q88999ttttt &L KU K K KLLL<,,..6ETW[Wccddd   DDDDDEEEttttts.   7 
A!AA!*A-C 
D"C==Dc                     t                      t          |           z  }|                                r@	 |                                }d|v r|                                 dS n# t
          $ r Y nw xY wdS )zARemove the wrapper script for a profile. Returns True if removed.r   TF)rw   r   r#   r   unlinkr   )rx   r   r   s      r&   remove_wrapper_scriptr   |  s    #%%(>t(D(DDL 	",,..Gg%%##%%%t &  	 	 	D	5s   ,A# #
A0/A0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<   dZeed<   d	Zeed<   dS )ProfileInfoz$Summary information about a profile.rx   path
is_defaultgateway_runningNr[   providerFhas_envr   skill_count
alias_pathdistribution_namedistribution_versiondistribution_sourcer   descriptiondescription_auto)__name__
__module____qualname____doc__r:   __annotations__r   boolr[   r   r   r   r   intr   r   r   r   r   r   rn   r<   r&   r   r     s        ..
III
JJJE8C="Hhsm"""GTK!%J%%%'+x}+++*.(3-...)-#--- K
 #d"""""r<   r   c                    | dz  }|                                 sdS 	 ddl}t          |dd          5 }|                    |          pi }ddd           n# 1 swxY w Y   t	          |t
                    sdS |                    d          |                    d	          |                    d
          fS # t          $ r Y dS 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encodingrx   rh   source)is_fileyamlopen	safe_loadrz   dictr   r   )r   mf_pathr   fdatas        r&   _read_distribution_metar     s'    //G??   '3111 	+Q>>!$$*D	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+$%% 	$##HHVHHYHHX
 	

        s:   B7 AB7 AB7 AB7 9=B7 7
CCc                    | dz  }|                                 sdS 	 ddl}t          |dd          5 }|                    |          pi }ddd           n# 1 swxY w Y   |                    di           }t          |t                    r|dfS t          |t                    r?|                    d	          p|                    d          |                    d
          fS dS # t          $ r Y dS w xY w)zLRead model/provider from a profile's config.yaml. Returns (model, provider).r   )NNr   Nr   r   r   r[   rU   r   )	r#   r   r   r   r   rz   r:   r   r   )r   config_pathr   r   cfg	model_cfgs         r&   _read_config_modelr     sN   -K z+sW555 	*..##)rC	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	*GGGR((	i%% 	#d?"i&& 	a==++Ey}}W/E/Ey}}U_G`G```z   zzs;   C' AC' AC' A1C' AC' '
C54C5c                 V    	 ddl m}  || dz  d          duS # t          $ r Y dS 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     s[    222222{]:%PPPX\\\   uus    
((c                     | dz  }|                                 sdS d}|                    d          D ])}dt          |          vrdt          |          vr|dz  }*|S )z$Count installed skills in a profile.r   r   zSKILL.mdz/.hub/z/.git/   )r   rglobr:   )r   
skills_dircountmds       r&   _count_skillsr     sw    x'J qEz**  3r77""xs2ww'>'>QJELr<   c                     | dz  S )Nzprofile.yamlrn   r%   s    r&   _profile_yaml_pathr     s    ''r<   c                    t          |           }|                                sdddS 	 ddl}t          |dd          5 }|                    |          pi }ddd           n# 1 swxY w Y   n# t
          $ r dddcY S w xY wt          |t                    sdddS t          |	                    d	          pd          
                                t          |	                    d
d                    dS )u  Read ``<profile_dir>/profile.yaml`` and return a dict.

    Returns ``{"description": "", "description_auto": False}`` when the
    file is missing or unreadable. Never raises — a corrupt
    profile.yaml on an unrelated profile must not break
    ``hermes profile list``.
    r   Fr   r   r   Nr   r   r   r   r   )r   r   r   r   r   r   rz   r   r:   r   r{   r   )r   r   r   r   r   s        r&   read_profile_metar     s[    k**D<<>> >!u===>$g... 	+!>>!$$*D	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ > > >!u=====>dD!! >!u===488M228b99??AA *<e!D!DEE  s5   A0  A$A0 $A((A0 +A(,A0 0BBr   r   r   c                ^   |                                  st          d|            ddl}t          |           }i }|                                rl	 t          |dd          5 }|                    |          pi }ddd           n# 1 swxY w Y   t          |t                    r|}n# t          $ r i }Y nw xY w||
                                |d<   |t          |          |d<   t          |d	d          5 }|                    ||d
d
           ddd           dS # 1 swxY w Y   dS )zUpdate ``<profile_dir>/profile.yaml`` in place.

    Only the explicitly passed fields are overwritten; unspecified
    fields preserve existing values. Creates the file if missing.
    Profile directory itself must exist.
    z"profile directory does not exist: r   Nr   r   r   r   r   wF)	sort_keysdefault_flow_style)r   r   r   r   r   r   r   rz   r   r   r{   r   	safe_dump)r   r   r   r   r   existingr   loadeds           r&   write_profile_metar    s     T R[ R RSSSKKKk**DH||~~ 	dC'222 1a**0b1 1 1 1 1 1 1 1 1 1 1 1 1 1 1&$'' "! 	 	 	HHH	"-"3"3"5"5#'+,<'='=#$	dC'	*	*	* OaxeNNNO O O O O O O O O O O O O O O O O OsH   B* #B;B* BB* BB* *B98B9;D""D&)D&c                     g } t                      }t                      }|                                rt          |          \  }}t	          |          \  }}}t          |          }|                     t          d|dt          |          |||dz  	                                t          |          ||||                    dd          |                    dd                               t                      }	|	                                r3t          |	                                          D ]}
|
                                s|
j        }t                               |          s:t          |
          \  }}||z  }t	          |
          \  }}}t          |
          }|                     t          ||
dt          |
          |||
dz  	                                t          |
          |	                                r|nd	||||                    dd          |                    dd          
                     | S )z4Return info for all profiles, including the default.rU   Tr   r   r   r   F)rx   r   r   r   r[   r   r   r   r   r   r   r   r   N)rx   r   r   r   r[   r   r   r   r   r   r   r   r   r   )rw   r9   r   r   r   r   r1   r   r   r#   r   r   ro   sortediterdirrx   r   r   )r   r   default_homer[   r   	dist_namedist_versiondist_sourcemetaprofiles_rootr5   rx   r   s                r&   list_profilesr  =  sS   H"$$K ,--L ,\::x/F|/T/T,	< ..2<@@!F*2244%l33'!- +33!XX&8%@@
 
 
 	 	 	" '((M M113344 	 	E<<>> :D!''-- 077OE8$t+J3J53Q3Q0I|[$U++DOOK  6u = =!//11)%00)3):):)<)<F::$"+%1$/ HH]B77!%*<e!D!D      " Or<   F
clone_from	clone_allclone_configno_alias	no_skillsc                    |r|s|rt          d          t          |           }t          |           |dk    rt          d          t          |          }|                                rt          d| d|           d}	||s|rk|ddlm}
  |
            }	n-t          |          }t          |           t          |          }	|	                                st          d	|pd
 d|	           |rK|	rIt          j        |	|t          |	                     t          D ]}||z                      d           n|                    dd           t           D ]}||z                      dd           |	t"          D ]3}|	|z  }|                                rt          j        |||z             4|	dz  }|                                rt          j        ||dz  d           t&          D ]Q}|	|z  }|                                r6||z  }|j                            dd           t          j        ||           R|dz  }|                                s/	 ddlm} |                    |d           n# t0          $ r Y nw xY w|r1	 |t2          z                      dd           n# t4          $ r Y nw xY w|rJ|                                r6	 t9          ||                                d           n# t0          $ r Y nw xY w|S )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).rU   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`.
Fr   )r3   r   r   r   r#   FileExistsErrorrs   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$   r{   r  )rx   r  r  r  r  r  r   r   r   r(   r  stalesubdirfilenamesrcsource_skillsrelpathdst	soul_pathr  s                       r&   create_profiler.  y  sS   H  
l 
i 
J
 
 	
 #4((E%   	a
 
 	
 "%((K TR%RR[RRSSS Jl888888(**JJ/
;;J!*---(44J  "" 	#[:#9[[z[[    %+Z %+-j99	
 	
 	
 	
 & 	: 	:E5 ((D(9999	: 	$666# 	F 	FF6!(((EEEE !/ > > 8+::<< >LkH&<=== '1M##%% [{X/EUYZZZZ / + + 7*::<< +%/CJ$$TD$AAALc*** i'I 	??????  7 CCCC 	 	 	D	
  		33??T !	 @      	 	 	D	  {((** 	'--//!&    
  	 	 	D	 s6   I= =
J
	J
J0 0
J=<J=$K< <
L	L	quietc                 6   t          |           rg g g ddS t          t                    j        j                                        }	 t          j        t          j        ddgi t          j
        dt          |           it          |          ddd          }|j        dk    rD|j                                        r+t          j        |j                                                  S |sat#          d	|j                    |j                                        r1t#          d
|j                                        dd                     dS # t
          j        $ r |st#          d           Y dS t(          $ r}|st#          d|            Y d}~dS 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$  r2   r   r   sys
executabler   r   r:   r   r   r{   jsonloadsr   stderrr   r   )r   r/  project_rootr   r   s        r&   seed_profile_skillsrA    s    "+.. 
#	
 
 	
 >>(/7799L^TAB @2:?}c+.>.>??L!!dB
 
 
 !!fm&9&9&;&;!:fm1133444 	:M&:KMMNNN}""$$ :86=..00#688999t$    	75666tt    	42q22333ttttts&   BE (A#E  F0	F9FFyesc                 $   t          |           }t          |           |dk    rt          d          t          |          }|                                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|	pd            |
rt          d|
            dg}t                      |z  }|                                }|r|                    d| d	           t          d           |D ]}t          d|            |rt          d           |stt                       	 t          d| d                                          }n(# t           t"          f$ r t          d           |cY S w xY w||k    rt          d           |S t%          ||           |rt'          |           |r!t)          |          rt          d|            	 t+          j        |           t          d|            n,# t.          $ r}t          d| d|            Y d}~nd}~ww xY w	 t1                      }||k    rt3          d           t          d           n# t.          $ r Y nw xY wt          d| d           |S ) 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.
    rU   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   r3   r   r   r   r   r   r   r   r   rw   r#   r1   inputr{   KeyboardInterruptEOFError_cleanup_gateway_service_stop_gateway_processr   r  rmtreer   get_active_profileset_active_profile)rx   rB  r   r   r[   r   
gw_runningr   r  r  r  itemsr   has_wrapperitemconfirmr   r  s                     r&   delete_profilerT  9  s     #4((E%   	:
 
 	

 "%((K F DE D D DEEE )55OE8'44J,,K+B;+O+O(I|[	


   	
#k
#
#$$$ L!%!!%I%5(%5%5%5%5rJKKK )'+''((( 4@y@@<+>3@@AAA 	42[22333 	FE
 $%%-L%%''K 86|666777	
,---  otoo CABBB  		:U:::;;AACCGG!8, 	 	 	.!!!	 e, UK000  +k***  1 '' 	1///000:k"""*[**++++ : : :8k88Q8899999999:#%%U??y)))7888    

)
)
)
)***s<   <%G" ""HH&&J 
J6J11J6:2K- -
K:9K:c                 \   ddl }t          j                            d          }	 t	          |          t          j        d<   ddlm}m} |                                dk    r |            }t          j
                    dz  dz  dz  | d	z  }|                                r{t          j        d
dd|gddd           t          j        d
dd|gddd           |                    d           t          j        g dddd           t          d| d           n|                                dk    rk |            }|                                rMt          j        ddt	          |          gddd           |                    d           t          d           n)# t           $ r}	t          d|	            Y d}	~	nd}	~	ww xY w||t          j        d<   dS dt          j        v rt          j        d= dS dS # ||t          j        d<   ndt          j        v rt          j        d= w xY w)z9Disable and remove systemd/launchd service for a profile.r   Nr5  )get_service_nameget_launchd_plist_pathLinuxz.configsystemduserz.service	systemctl--userdisableTF
   )r   checkr   stopr  )r[  r\  zdaemon-reloadu   ✓ Service z removedDarwin	launchctlunloadu   ✓ Launchd service removedu   ⚠ Service cleanup: )platformr   r   r   r:   hermes_cli.gatewayrV  rW  systemr   r   r#   r   r   r   r   r   )
rx   r   	_platformold_homerV  rW  svc_namesvc_file
plist_pathr   s
             r&   rJ  rJ    s        z~~m,,H&*$'$4$4
=!OOOOOOOO((''))Hy{{Y.:VCF[F[F[[H   9 (Ix@#'ub     (FH=#'ub    4000<<<#'ub    7X7778888++//11J  "" 6 (C
OO<#'ub    !!T!2224555 + + +)a))********+ (0BJ}%%%bj((
=))) )( (0BJ}%%bj((
=)))))s0   E;F!  G< !
G+G=G< GG< </H+c                    ddl }| dz  }|                                sdS 	 |                                                                }|                    d          rt          j        |          ndt          |          i}t          |d                   }ddlm	} ddlm
}  ||           t          d          D ]8}|                    d	            ||          st          d
| d            dS 9	  ||d           n# t          t          f$ r Y nw xY wt          d| d           dS # t          t           f$ r t          d           Y dS t"          $ r}	t          d|	            Y d}	~	dS 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   r{   
startswithr=  r>  r   r   ro  rp  rangesleepr   ProcessLookupErrorr$   PermissionErrorr   )
r   _timepid_filerawr   rn  _terminate_pidrp  _r   s
             r&   rK  rK    s    ]*H?? 2  ""((**"%.."5"5Ltz#E3s88;L$u+ 	CBBBBB......s r 	 	AKK;s## 8#888999	N3d+++++"G, 	 	 	D	6666777770 - - -+,,,,,, 2 2 20Q001111111112sH   CD0 4D0 6D D0 DD0 DD0 0 E9	E9E44E9c                      t                      } 	 |                                                                 }|sdS |S # t          t          t
          f$ r Y dS w xY w)ztRead the sticky active profile name.

    Returns ``"default"`` if no active_profile file exists or it's empty.
    rU   )ru   r   r{   r   UnicodeDecodeErrorr$   )r   rx   s     r&   rM  rM    sk    
 $%%D~~%%'' 	917;   yys   (< < AAc                    t          |           }t          |           |dk    r$t          |          st          d| d|           t	                      }|j                            dd           |dk    r|                    d           d	S |                    d          }|	                    |dz              |
                    |           d	S )
zlSet the sticky active profile.

    Writes to ``~/.hermes/active_profile``. Use ``"default"`` to clear.
    rU   r  8' does not exist. Create it with: hermes profile create Tr   r  r/   
N)r   r   r   r   ru   r$  r   r   with_suffixr   replace)rx   r   r   rP   s       r&   rN  rN    s    
 #4((E%   	."7"7= = =5:= =
 
 	

 $%%DKdT222	t$$$$$ v&&ut|$$$Dr<   c                     ddl m}   |             }|                                }t                                                      }||k    rdS t	                                                      }	 |                    |          }|j        }t          |          dk    r(t          	                    |d                   r|d         S n# t          $ r Y nw xY wdS )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  rU   r   custom)rs   r  r2   r9   ro   relative_topartslenr   r   r3   )r  hermes_homeresolveddefault_resolvedr
  relr  s          r&   get_active_profile_namer    s     100000!/##K""$$H/1199;;###y&((0022M""=11	u::??~33E!H==?8O    8s   .AC 
CCroot_dirc                 >     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 ]L}|dk    s|                    d          r|                    |           3|dv r|                    |           Mt          |           k    r|                    d |D                        |S )Nr-   )r.   r/   >   package.jsonpackage-lock.jsonc              3   ,   K   | ]}|t           v |V  d S N)_DEFAULT_EXPORT_EXCLUDE_ROOT).0cs     r&   	<genexpr>z:_default_export_ignore.<locals>._ignore.<locals>.<genexpr>O  s-      TT!7S2S2S12S2S2S2STTr<   )setr0   addr   rc   )r*   r  r4   r5   r  s       r&   r8   z'_default_export_ignore.<locals>._ignoreD  s    uu 	# 	#E%%8I)J)J%E""""???E"""	??h&&NNTThTTTTTTr<   )r:   listr  )r  r8   s   ` r&   _default_export_ignorer  =  s=    3 $ 3       Nr<   output_pathc                 `  
 ddl }t          |           }t          |           t          |          }|                                st          d| d          t          |          }t          |                              d                              d          }|dk    r|	                                5 }t          |          dz  }t          j        ||t          |                     t          j        |d	|d          }	t          |	          cddd           S # 1 swxY w Y   |	                                5 }t          |          |z  }d
dh
t          j        ||
fd           t          j        |d	||          }	t          |	          cddd           S # 1 swxY w Y   dS )zMExport a profile to a tar.gz archive.

    Returns the output file path.
    r   Nr  rD  z.tar.gzz.tgzrU   r  gztarrO   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   r:   removesuffixTemporaryDirectoryr  r  r  make_archive)rx   r  r  r   r   outputbasetmpdirstagedr   r  s             @r&   export_profiler  U  sF   
 OOO"4((E%   !%((K F DE D D DEEE+Fv;;##I..;;FCCD	 ((** 	 f&\\I-FO-k::   
 (w	JJF<<	  	  	  	  	  	  	  	  	  	  	  	  	  	  	  	  
	$	$	&	& 	&f%(&1HHHH	
 	
 	
 	

 $T7FEBBF||	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	s&   8AD!!D%(D% AF##F'*F'member_namec                 p   |                      dd          }t          |          }t          |           }|r/|                                s|                                s|j        rt          d|            d |j        D             }|rt          d |D                       rt          d|            |S )z4Return safe path parts for a profile archive member.\/zUnsafe archive member path: c                     g | ]}|d v|	S )>   r   .rn   r  parts     r&   
<listcomp>z4_normalize_profile_archive_parts.<locals>.<listcomp>  s"    HHHd$i2G2GT2G2G2Gr<   c              3   "   K   | ]
}|d k    V  dS )z..Nrn   r  s     r&   r  z3_normalize_profile_archive_parts.<locals>.<genexpr>  s&      77777777r<   )r  r   r   is_absolutedriver3   r  any)r  normalized_name
posix_pathwindows_pathr  s        r&    _normalize_profile_archive_partsr    s    !))$44O//J";//L G!!##G ##%%G 	G EEEFFFHHj.HHHE GC7777777 GEEEFFFLr<   archivedestinationc           	      *   ddl }|                    | d          5 }|                                D ]H}t          |j                  } |j        | }|                                r|                    dd           M|                                st          d|j                   |j
                            dd           |                    |          }|t          d|j                   |5  t          |d          5 }t          j        ||           ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   	 t          j        ||j        d	z             9# t"          $ r Y Fw xY w	 ddd           dS # 1 swxY w Y   dS )
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  rx   joinpathisdirr   isfiler3   r$  extractfiler  copyfileobjr   r   moder$   )	r  r  r  tfmemberr  target	extractedr,  s	            r&   _safe_extract_profile_archiver    so   NNN	gv	&	& "mmoo 	 	F4V[AAE)[)51F||~~ TD999==??  EEE   Mt<<<v..I  !M!M!MNNN 3 3D.. 3#"9c2223 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3u!45555   /	                 s~   CF4D>D'	D>'D++D>.D+/D>2F>EFEF
E)'F)
E73F6E77FFFc                     ddl }|                    | d          5 }d |                                D             }|sd |                                D             }ddd           n# 1 swxY w Y   |S )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  c                     h | ]E}t          |j                  }t          |          d k    s|                                =|d         FS )r   r   )r  rx   r  r  )r  r  r  s      r&   	<setcomp>z1_inspect_profile_archive_roots.<locals>.<setcomp>  sP     
 
 
:6;GG5zzA~~~ !H ~~r<   c                 j    h | ]0}|                                 t          |j                  d          1S )r   )r  r  rx   )r  r  s     r&   r  z1_inspect_profile_archive_roots.<locals>.<setcomp>  sE       <<>>0==a@  r<   )r  r   r  )r  r  r  top_dirss       r&   _inspect_profile_archive_rootsr    s     NNN	gv	&	& "
 
--//
 
 
  	  mmoo  H               Os   ?A&&A*-A*archive_pathc                    ddl }t          |           }|                                st          d|           t	          |          }t          |          dk    r|                                nd}|p|}|st          d          |t          d          t          |          }t          |           |dk    rt          d          t          |          }|                                rt          d	| d
|           t                      }	|	                    dd           |                    d          5 }
t          |
          }t          ||           ||z  }|                                st          d|           |}||k    r||z  }|                    |           t%          j        t)          |          t)          |                     ddd           n# 1 swxY w Y   |S )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.rU   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  r  popr3   r   r   r   r  ro   r   r  r  r   renamer  mover:   )r  rx   r  r  r  archive_rootinferred_namer   r   r
  r  staging_rootr  final_sources                 r&   import_profiler    sz    OOO<  G>> A ?g ? ?@@@-g66H%(]]a%7%78<<>>>TL(LM 
S
 
 	
 K
 
 	
 #=11E%   	V
 
 	

 "%((K TR%RR[RRSSS&((Mt444		$	$,D	$	E	E 9F||%g|<<< </	!! 	M|MM   !5  '%/L\***C%%s;'7'78889 9 9 9 9 9 9 9 9 9 9 9 9 9 9" s   >BG&&G*-G*old_namenew_namenew_dirc                    d|  }d| }|dz  t                      dz  t          j                    dz  dz  g}t                      }|D ]}	 |                                }n# t
          $ r |}Y nw xY w||v s|                                sD|                    |           	 t          j	        |
                    d                    }	n# t
          t          j        f$ r Y w xY w|	                    d          }
t          |
t                    r||
vr||
v rt          d| d	|            |
|         }t          |t                    r+d
|vr'd|v r|                    dd          d         n|}||d
<   |
                    |          |
|<   |                    |j        dz             }	 |                    t          j        |	dd          dz   d           |                    |           n9# t
          $ r, 	 |                    d           n# t
          $ r Y nw xY wY w xY wt          d| d|            dS )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_asciir  Tr  u   ✓ Honcho host updated:     → N)r9   r   r   r  r2   r$   r   r  r=  r>  r   JSONDecodeErrorr   rz   r   r   r   r  r  suffixr   dumpsr  r   )r  r  r  old_hostnew_host
candidatesseenr   r  r{  r  blockbarerP   s                 r&   _migrate_honcho_profile_hostr    s   ###H###H 	- ""]2	i-/J eeD &E &E	||~~HH 	 	 	HHH	t4<<>>	*T^^W^==>>CC-. 	 	 	H	   %&& 	(%*?*?u\\\VZ\\]]]heT"" 	#xu'<'<03x8>>#q))!,,XD"E(O))H--ht{V344	NN4:c!%HHH4OZaNbbbKK 	 	 	

d
++++   H	 	C(CCCCDDDDM&E &Es[   A""A10A1#(CC%$C%/AG44
H*?HH*
H# H*"H##H*)H*c                    t          |           }t          |          }t          |           t          |           |dk    rt          d          |dk    rt          d          t          |          }t          |          }|                                st          d| d          |                                rt          d| d          t          |          rt          ||           t          |           |                    |           t          d|j         d|j                    t          |||           t          |           t!          |          }|s"t#          |           t          d	|            nt          d
| d|            	 t%                      |k    r!t'          |           t          d|            n# t(          $ r Y nw xY w|S )zrRename a profile: directory, wrapper script, service, active_profile.

    Returns the new profile directory.
    rU   z"Cannot rename the default profile.u.   Cannot rename to 'default' — it is reserved.r  rD  z' already exists.u   ✓ Renamed r  u   ✓ Alias updated: u   ⚠ Cannot create alias 'u   ' — u   ✓ Active profile updated: )r   r   r3   r   r   r   r#   r  r   rJ  rK  r  r   rx   r  r   r   r   rM  rN  r   )r  r  	old_canon	new_canonold_dirr  	collisions          r&   rename_profiler  F  s-   
 'x00I&x00I)$$$)$$$I=>>>IIJJJi((Gi((G>> J HI H H HIII~~ HF)FFFGGG g&& ' G444g&&& NN7	
:
:
:GL
:
:;;; !Iw??? )$$$%i00I Hi(((/I//0000F)FF9FFGGG9,,y)))<<<===    Ns   3G 
GGprofile_namec                     t          |           }t          |           t          |          }|dk    r)|                                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.
    rU   r  r  )r   r   r   r   r   r:   )r  r   r   s      r&   resolve_profile_envr     s     #<00E%   !%((K	+"4"4"6"6= = =5:= =
 
 	

 {r<   )NFFFFN)Fr  )Mr   r=  r   rer  r   r   r;  dataclassesr   pathlibr   r   r   typingr   r   compiler   r   r!  r#  r   r  r:   r   	frozensetr   r"   r   r'   r;   r  r   r   ro   r9   ru   rw   r   r   r   r   r   r   r   r   r   tupler   r   r   r   r   r   r   r   r  r  r.  rA  rT  rJ  rK  rM  rN  r  r  r  r  r  r  r  r  r  r  r   rn   r<   r&   <module>r     s    *  				 				       



 ! ! ! ! ! ! 8 8 8 8 8 8 8 8 8 8 ! ! ! ! ! ! ! !9::  "        $s)   , 3<) = = = 3 3 3    0 D T    ,4 , , , ,f  )y * * *     2 )     
  i ! ! !   3D 3 3 3 3%$ % % % %9$ 9 9 9 9
*$ * * * *     $
 
 
 
 
 
<(# ($ ( ( ( (+ + + + + +     DG G G G G     .     & # # # # # # # #8   %        2D U    &     	t 	 	 	 	 	4(D (T ( ( ( (4 D    8 "&'+	O O OO #O tn	O
 
O O O OJ9tK( 9 9 9 9| !%!%P P
PP P 	P
 P P #P 
P P P Pf* *T *$ *8D> * * * *Z_ _ _4 _D _ _ _ _D-*3 -*T -*d -* -* -* -*`$2t $2 $2 $2 $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 2E 2E 2Ej4S 4C 4D 4 4 4 4vc c      r<   