
    JjU                    l   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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mZ ddl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<    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( 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dfd#Z0d edefd$Z1d ede#fd%Z2d edee         fd&Z3de#fd'Z4dld ed(ee         dee         fd)Z5d ede#fd*Z6deddfd+Z7d,edee         fd-Z8d.Z9de:eef         fd/Z;e G d0 d1                      Z<dede=fd2Z>dede=fd3Z?dede#fd4Z@i ZAe:ee=eBeBeCf         f         ed5<   d6ZDd7edeBfd8ZEdedeCfd9ZFdedefd:ZGdede:fd;ZHddd<ded=ee         d>ee#         ddfd?ZIdee<         fd@ZJdAe#deeeef                  fdBZK	 	 	 	 	 	 dmd edDee         dEe#dFe#dGe#dHe#d=ee         defdIZLdndedJe#dee:         fdKZMdndJe#dee         fdLZNdMededeeC         fdNZOdMededdfdOZPdeddfdPZQdnd edQe#defdRZRd,eddfdSZSd,eddfdTZTd ededdfdUZUdeddfdVZVdefdWZWd eddfdXZXdefdYZYdZefd[ZZd ed\edefd]Z[d^edee         fd_Z\d`edaeddfdbZ]d`ede^e         fdcZ_dldded ee         defdeZ`dfedgedheddfdiZadfedgedefdjZbd,edefdkZcdS )ou_  
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OptionalTuple)is_excluded_skill_pathz^[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_ROOT>   state-snapshotsbackupsr   checkpointsstate.dbstate.db-shmstate.db-wal_CLONE_ALL_HISTORY_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    9/home/rurouni/.hermes/hermes-agent/hermes_cli/profiles.pyhas_bundled_skills_opt_outr0      sA    66>>@@@   uus    
,,
source_dirc                     |                                  t                                                       k    dt          dt          t                   dt          t                   ffd}|S )uW  Exclude infrastructure artifacts when cloning a profile via --clone-all.

    Three categories:
      1. Root-level entries in ``_CLONE_ALL_HISTORY_EXCLUDE_ROOT`` — session
         history, backups, and snapshots that belong to the SOURCE profile
         and should never carry into a fresh clone.  Applies to any source.
      2. 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.
      3. 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                 z   g }|D ]}|dk    s|                     d          r|                    |           3	 t          |                                           k    }n# t          t
          f$ r d}Y nw xY w|r?|t          v r|                    |           r|t          v r|                    |           |S )N__pycache__)z.pycz.pyo.sock.tmpF)endswithappendr   resolver-   
ValueErrorr'   r    )r3   r4   ignoredentryat_rootis_default_sourcesource_resolveds        r/   _ignorez+_clone_all_copytree_ignore.<locals>._ignore   s     	* 	*E &&>>"CDD ' u%%% y//1133FZ(        	 
  *;;;NN5)))$ *2Q)Q)QNN5)))s   %A  A65A6)r;   _get_default_hermes_homestrr   )r1   rB   r@   rA   s     @@r/   _clone_all_copytree_ignorerE      s|    * !((**O'+C+E+E+M+M+O+OO3 tCy T#Y       6 N    >   	auth.lock
errors.log.update_check.hermes_historyhermes_state.dbresponse_store.dbresponse_store.db-shmresponse_store.db-walr   r   r   	sandboxesaudio_cacher#   image_cacher   active_profiledocument_cachebrowser_screenshotsr   r$   	auth.jsonr   r   r   r%   r&   r   r   >   USER.md	AGENTS.md	CLAUDE.md	MEMORY.md	todo.json.cursorrulessystem_prompt.mdr   r   pluginsscriptsr   r   	knowledgepreferencesr   r   >   tmprootsudotesthermesdefault>   acpmcpchatr   dumploginmodelsetuptoolsconfigdoctorhonchologoutr   statusupdategatewaypairingr]   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   rC    rF   r/   _get_profiles_rootr     s     $%%
22rF   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_constantsr   r   s    r/   rC   rC     s%     988888""$$$rF   c                  $    t                      dz  S )z2Return the path to the sticky active_profile file.rR   r}   r~   rF   r/   _get_active_profile_pathr   !  s    #%%(888rF   c                  4    t          j                    dz  dz  S )z)Return the directory for wrapper scripts.z.localr   )r   r   r~   rF   r/   _get_wrapper_dirr   &  s    9;;!E))rF   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 emptyrf   )
isinstancerD   stripr<   casefoldlower)r   strippeds     r/   normalize_profile_namer   /  sq     dC   4yyzz||H 97888i''y>>rF   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.
    rf   NzInvalid profile name %. 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matchr<   _RESERVED_NAMESr   s    r/   validate_profile_namer   A  s     y%% 
*D * * *
 
 	
 &D & & &
 
 	
 rF   c                 `    t                               |           st          d| d          dS )u  Raise ``ValueError`` if *name* is not a safe wrapper-alias identifier.

    The alias is used verbatim as a filename under :func:`_get_wrapper_dir`
    (``~/.local/bin``), so it must be a single safe command name with no path
    separators or traversal segments — otherwise a value like ``../../.bashrc``
    would escape the wrapper directory and clobber arbitrary user files. We
    reuse the profile id regex, which already forbids ``/``, ``.``, and ``..``.
    zInvalid alias name r   N)r   r   r<   r   s    r/   validate_alias_namer   _  sL     %% 
*$ * * *
 
 	

 
rF   c                 j    t          |           }|dk    rt                      S t                      |z  S )z4Resolve a profile name to its HERMES_HOME directory.rf   )r   rC   r   r   canons     r/   get_profile_dirr   o  s7    "4((E	')))%''rF   c                 r    t          |           }|dk    rdS t          |                                          S )z)Check whether a profile directory exists.rf   T)r   r   is_dirr   s     r/   profile_existsr   w  s9    "4((E	t5!!((***rF   c                    t          |           }	 t          |           n&# t          $ r}t          |          cY d}~S d}~ww xY w|t          v rd| dS |t
          v rd| dS t                      }t          j        dk    }	 t          j
        |rdnd|gddd	
          }|j        dk    r|j                                                                        d         }||r| dn|z  }|t          |          k    r,	 |                                }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: alias-name validity, reserved names, hermes subcommands, existing
    binaries in PATH.
    N'z' is a reserved namez$' conflicts with a hermes subcommandwin32wherewhichT   )capture_outputtexttimeoutr   .bat	hermes -pz&' conflicts with an existing command ())r   r   r<   rD   r   _HERMES_SUBCOMMANDSr   sysplatform
subprocessrun
returncodestdoutr   
splitlines	read_text	ExceptionFileNotFoundErrorTimeoutExpired)	r   r   excwrapper_dir
is_windowsresultexisting_pathexpectedcontents	            r/   check_alias_collisionr     s    #4((EE""""   3xx.5....###>5>>>> #$$K(J"/WW7dA
 
 
 !!"M//11<<>>qAM"
&MnnnnNHH--&0022G"g--#t .    DTuTTMTTTT " z89    4sN   ! 
A?AAA9D3 >D D3 
D&#D3 %D&&D3 3EEc                      t          t                                } | t          j                            dd                              t          j                  v S )z!Check if ~/.local/bin is in PATH.PATH )rD   r   osenvirongetsplitpathsep)r   s    r/   _is_wrapper_dir_in_pathr     sA    &(())K"*..44::2:FFFFrF   targetc                 j   t          |           }|rt          |          n|}t          |           t                      }	 |                    dd           n-# t          $ r }t          d| d|            Y d}~dS d}~ww xY wt          j        dk    }|rQ|| dz  }	 |                    d| d	           |S # t          $ r }t          d
| d|            Y d}~dS d}~ww xY w||z  }	 t          j
        d          pd}|                    dt          j        |           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)u  Create a shell wrapper script at ~/.local/bin/<name>.

    The wrapper file is named after ``name`` (the alias). The profile it
    activates is ``target`` if given, otherwise ``name`` — this lets a custom
    alias name point at a differently-named profile without a post-hoc rewrite.

    On Windows, creates a ``.bat`` file instead of a POSIX shell script.
    Returns the path to the created wrapper, or None if creation failed.
    Tparentsexist_oku   ⚠ Could not create : Nr   r   z@echo off
hermes -p z %*
u    ⚠ Could not create wrapper at re   z#!/bin/sh
exec z -p z "$@"
)r   r   r   mkdirr-   printr   r   
write_textshutilr   shlexquotechmodstatst_modeS_IEXECS_IXGRPS_IXOTH)	r   r   r   rw   r   er   wrapper_path
hermes_exes	            r/   create_wrapper_scriptr     s/    #4((E06A$V,,,EG "$$K$6666   8k88Q88999ttttt (J "^^^3	##$Ng$N$N$NOOO 	 	 	H\HHQHHIII44444	 #U*	h//;8J##$du{:7N7N$d$dT[$d$d$deee|0022:T\IDLX[_[gghhh 	 	 	H\HHQHHIII44444	sI   A 
B#A>>B!B< <
C&C!!C&/BF 
F2F--F2c                    t                      }t          |           }	 t          |           n# t          $ r Y dS w xY wt          j        dk    }||z  g}|r|                    d|| dz             |D ]W}|                                rA	 |                                }d|v r|	                                  dS G# t          $ r Y Sw xY wXdS )zARemove the wrapper script for a profile. Returns True if removed.Fr   r   r   r   T)r   r   r   r<   r   r   insertr,   r   unlinkr   )r   r   r   r   
candidatesr   r   s          r/   remove_wrapper_scriptr     s&   "$$K"4((EE""""   uu(J %&J ;![e>>>9:::" 	 	   	&0022')) '')))44 *    	 5s   / 
==,B==
C
	C
c                 (   | dz  }|                                 sdS 	 ddlm}m} ddlm}m}  |t          |                     }	  |            \  }}||k     r |dd            ||           dS #  ||           w xY w# t          $ r Y dS w xY w)	u  Bring a copied profile config.yaml up to the current schema.

    Profile creation can clone a config file that predates schema tracking (no
    ``_config_version``) or that is simply older than the running Hermes. If we
    leave it untouched, the first desktop/doctor view of the new profile shows a
    scary ``v0 → latest`` warning even though we just created the profile. Scope
    the normal migration pipeline to the new profile and keep it non-interactive.
    r   Nr   )reset_hermes_home_overrideset_hermes_home_override)check_config_versionmigrate_configFT)interactivequiet)	r,   r   r   r   hermes_cli.configr   r   rD   r   )	r(   config_pathr   r   r   r   tokencurrent_ver
latest_vers	            r/   #_migrate_profile_config_if_outdatedr     s     -K YYYYYYYYJJJJJJJJ(([)9)9::	.&:&:&<&<#KZ''5====&&u-----&&u----    		s)   (B  A3 &B 3B  B 
BBprofile_namec                 ^    t                                          t          |                     S )u  Return the alias name of the wrapper that activates *profile_name*, or None.

    A wrapper created by :func:`create_wrapper_script` is a file named after the
    alias whose body invokes ``hermes -p <profile>``. When the alias name equals
    the profile name this is trivial, but a custom alias (``hermes profile alias
    <profile> --name <custom>``) produces a differently-named file — so the
    display side cannot assume ``wrapper == profile`` and must reverse-look-up.

    A custom alias (name != profile) is preferred over the profile-named wrapper
    so ``profile list``/``show`` surface the command the user actually typed.
    Results are sorted for deterministic output when several aliases match.

    For listing ALL profiles at once, prefer :func:`build_alias_map` — calling
    this per-profile re-reads every wrapper file N times (O(N*M)); on a wrapper
    dir like ``~/.local/bin`` that also holds large unrelated binaries (ffmpeg
    etc.) that meant multi-second ``list_profiles`` latency and desktop timeouts.
    )build_alias_mapr   r   )r   s    r/   find_alias_for_profiler     s'    $   !7!E!EFFFrF   i    c                  X   t                      } i }|                                 s|S t          j        dk    }d}t	          |                                           D ]N}|                                s|r|j        dk    r&|s|j        r0	 t          |ddd          5 }|	                    t                    }ddd           n# 1 swxY w Y   n# t          t          f$ r Y w xY w|                    |          }|d	k    r||t          |          z   d         }|                                r.|                    dd
          d                                         nd}	|	st#          |	          }	|r|j        n|j        }
|
|	k    r|                    |	|
           I|
||	<   P|S )a  Single-pass reverse map ``{canonical_profile -> alias_name}``.

    Scans the wrapper dir ONCE (vs. :func:`find_alias_for_profile` per profile)
    and reads only a small head slice of each candidate wrapper, skipping
    binaries. A custom alias (file name != profile) wins over the profile-named
    wrapper, matching ``find_alias_for_profile``'s preference; deterministic via
    sorted iteration.
    r   z
hermes -p r   rutf-8strict)encodingerrorsN   r   r   )r   r   r   r   sortediterdiris_filesuffixopenread_WRAPPER_READ_LIMITr-   UnicodeDecodeErrorfindlenr   r   r   stemr   
setdefault)r   r   r   prefixr>   fr   idxrestr   aliass              r/   r   r   9  s    #$$KF (JF++--.. " "}} 	 	%,&00 	el 		eS78DDD 6&&!4556 6 6 6 6 6 6 6 6 6 6 6 6 6 6+, 	 	 	H	 ll6"""99sS[[())*26**,,F

4##A&,,...B 	&u--(8

ej E>>eU++++!F5MMMs6   CC9CC			CC		CC%$C%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         ed<   dZeed<   d	Zeed<   dS )ProfileInfoz$Summary information about a profile.r   path
is_defaultgateway_runningNrl   providerFhas_envr   skill_count
alias_path
alias_namedistribution_namedistribution_versiondistribution_sourcer   descriptiondescription_auto)__name__
__module____qualname____doc__rD   __annotations__r   boolrl   r   r  r  r  intr  r  r  r  r  r  r  r~   rF   r/   r  r  n  s        ..
III
JJJE8C="Hhsm"""GTK!%J%%% !%J$$$'+x}+++*.(3-...)-#--- K
 #d"""""rF   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   Nr   r   r   r   rx   source)r   yamlr  	safe_loadr   dictr   r   )r(   mf_pathr'  r
  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%  rl   rf   r  )	r,   r'  r  r(  r   r   rD   r)  r   )r(   r   r'  r
  cfg	model_cfgs         r/   _read_config_modelr0    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                     	 ddl m}  || dz  d          	 dS n# t          $ r Y nw xY w	 ddl m}m}  || d	z            } ||| 
          duS # t          $ r Y dS w xY w)u  Check if a gateway is running for a given profile directory.

    Primary signal is the profile's ``gateway.pid`` (verified against the
    runtime lock).  That check fails closed whenever the lock isn't held by
    *this* reader — which is exactly the case for a dashboard process that is
    a separate s6 service from the gateway it's reporting on (Docker), or any
    launch-service-managed gateway that left a fresh ``gateway_state.json`` but
    no live PID file.  In those cases fall back to validating the PID recorded
    in the profile's own ``gateway_state.json`` against the live process table,
    mirroring the ``/api/status`` sidebar's liveness logic so the two surfaces
    agree.  Parameterized by ``profile_dir`` so it never mutates ``HERMES_HOME``.
    r   )get_running_pidr   F)cleanup_staleNT)get_runtime_status_running_pidread_runtime_statusr   )expected_home)gateway.statusr2  r   r4  r5  )r(   r2  r4  r5  runtimes        r/   _check_gateway_runningr9    s    222222OK-7uMMM 4    	
 	
 	
 	
 	
 	
 	
 	
 &%k4H&HII--g[QQQY]]]   uus    
))$A 
A A _SKILL_COUNT_CACHEg      >@
skills_dirc                 z   	 |                                  j        }n# t          $ r Y dS w xY w	 t          j        |           5 }|D ]L}	 |                    d          r#|                     d          j        }||k    r|}=# t          $ r Y Iw xY w	 ddd           n# 1 swxY w Y   n# t          $ r Y nw xY w|S )a\  Cheap change-signature for a skills tree.

    Max mtime of ``skills_dir`` and its immediate children (category dirs).
    Adding/removing a category bumps ``skills_dir``'s mtime; adding/removing a
    skill inside a category bumps that category dir's mtime. One ``scandir``
    (not a recursive walk) keeps this O(#categories), not O(#files).
    g        F)follow_symlinksN)r   st_mtimer-   r   scandirr   )r;  sigitr>   ms        r/   _skills_dir_signaturerC    s@   oo(   ssZ
## 	r  ||E|:: $!JJuJ==Fs77"#C   H	 	 	 	 	 	 	 	 	 	 	 	 	 	 	    Jse    
**B+ B9BB
BBBBB+ B##B+ &B#'B+ +
B87B8c                    | dz  }|                                 sdS t          |          }t          |          }t          j                    }t                              |          }|(|d         |k    r||d         z
  t          k     r|d         S d}|                    d          D ]}t          |          r|dz  }|||ft          |<   |S )zECount installed skills in a profile (cached by skills-dir signature).r   r   Nr      zSKILL.md)	r   rD   rC  timer:  r   _SKILL_COUNT_TTL_SECONDSrglobr
   )r(   r;  key	signaturenowcachedcountmds           r/   _count_skillsrO    s    x'J q
j//C%j11I
)++C##C((F1I""6!9_ 888ayEz**  !"%% 	
(#u5sLrF   c                     | dz  S )Nzprofile.yamlr~   r.   s    r/   _profile_yaml_pathrQ  +  s    ''rF   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  )rQ  r   r'  r  r(  r   r   r)  rD   r   r   r"  )r(   r  r'  r
  r+  s        r/   read_profile_metarT  /  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rS  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'  rQ  r   r  r(  r   r)  r   r   r"  	safe_dump)r(   r  r  r'  r  existingr
  loadeds           r/   write_profile_metar\  H  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                  6   g } t                      }t                      }|                                rt          |          \  }}t	          |          \  }}}t          |          }|                     t          d|dt          |          |||dz  	                                t          |          ||||                    dd          |                    dd                               t                      }	|	                                rt                      }
t          |	                                          D ]X}|                                s|j        }|dk    r&t"                              |          sAt          |          \  }}|
                    t'          |                    }|rt(          j        d	k    }||r| d
n|z  }nd}t	          |          \  }}}t          |          }|                     t          ||dt          |          |||dz  	                                t          |          |r|	                                r|nd|||||                    dd          |                    dd                               Z| S )z4Return info for all profiles, including the default.rf   Tr   r  r   r  F)r   r  r  r  rl   r  r  r  r  r  r  r  r  r   r   N)r   r  r  r  rl   r  r  r  r  r  r  r  r  r  r  )r   rC   r   r0  r,  rT  r:   r  r9  r,   rO  r   r   r   r   r   r   r   r   r   r   r   )r   r   default_homerl   r  	dist_namedist_versiondist_sourcemetaprofiles_root	alias_mapr>   r   r  r   r  s                   r/   list_profilesre  m  s   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y  !''-- 077OE8"'=d'C'CDDJ " \W4
(:,]z,?,?,?,?S]^

!
3J53Q3Q0I|[$U++DOOK  6u = =!//11)%00*4W9J9J9L9LW::SW%"+%1$/ HH]B77!%*<e!D!D      $ OrF   	multiplexc                    t                      pd}| s|t          |          fgS dt                      fg}t                      }|                                ryt          |                                          D ]W}|                                s|j        }|dk    r%t          	                    |          s@|
                    ||f           X|S )u  Return the ``(profile_name, hermes_home)`` pairs a gateway should serve.

    This is the single chokepoint for "which profiles does the inbound gateway
    handle" so later multiplexing phases never re-derive the set.

    - ``multiplex=False`` (default): returns exactly one entry for the *active*
      profile — byte-for-byte the single-profile behavior the gateway has
      always had. The name is ``"default"`` for the default profile or the
      active named profile's id.
    - ``multiplex=True``: returns the default profile plus every valid named
      profile under ``profiles/``, each paired with its own HERMES_HOME.

    Intentionally lightweight (a directory scan + name validation only): no
    per-profile config reads, gateway-running probes, or skill counts like
    :func:`list_profiles`. It runs on gateway startup and must stay cheap.

    The returned ``hermes_home`` is the path to pass to
    ``set_hermes_home_override`` when scoping a turn to that profile.
    rf   )get_active_profile_namer   rC   r   r   r   r   r   r   r   r:   )rf  activeserverc  r>   r   s         r/   profiles_to_serverk    s    ( %&&3)F 300122&/1I1K1K%L$ME&((M 	(M113344 	( 	(E<<>> :Dy  !''-- LL$''''LrF   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|	           |rL|	rJt          j        |	|dt          |	                     t          D ]}||z                      d           nB|                    dd           t           D ]}||z                      dd           |	t"          D ]o}|	|z  }|                                rT||z  }t          j        ||           |dk    r4	 t'          j        t+          |          d           _# t,          $ r Y kw xY wp|	dz  }|                                rt          j        ||dz  dd           t.          D ]Q}|	|z  }|                                r6||z  }|j                            dd           t          j        ||           R|dz  }|                                sK	 |                    dd           t'          j        t+          |          d           n# t,          $ r Y nw xY w|dz  }|                                s/	 ddlm} |                    |d           n# t8          $ r Y nw xY w|r1	 |t:          z                      dd           n# t,          $ r Y nw xY w|st=          |           |rJ|                                r6	 tA          ||                                d           n# t8          $ r Y nw xY wtC          |           |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.
    Nz--no-skills is mutually exclusive with --clone / --clone-from / --clone-all (cloning explicitly copies skills from the source profile).rf   uS   Cannot create a profile named 'default' — it is the built-in profile (~/.hermes).	Profile '' already exists at r   get_hermes_homezSource profile 'ri  z' does not exist at Tsymlinksignore
missing_okr   r     r   )rw  dirs_exist_ok# Per-profile secrets for this Hermes profile.
# API keys and tokens set here override the shell environment.
# Behavioral settings belong in config.yaml, not here.
r   r%  r   )DEFAULT_SOUL_MDzThis profile opted out of bundled-skill seeding (`hermes profile create --no-skills`).
Delete this file to re-enable sync on the next `hermes update`.
FrS  )"r<   r   r   r   r,   FileExistsErrorr   ru  r   r   r   copytreerE   r   r   r   _PROFILE_DIRS_CLONE_CONFIG_FILEScopy2r   r   rD   r-   _CLONE_SUBDIR_FILESparentr   hermes_cli.default_soulr~  r   r+   r   r   r\  _maybe_register_gateway_service)r   rl  rm  rn  ro  rp  r  r   r(   r1   ru  stalesubdirfilenamesrcdstsource_skillsrelpathenv_path	soul_pathr~  s                        r/   create_profiler    sZ   H  
j,,	,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[[    0+Z 0+-j99		
 	
 	
 	
 & 	: 	:E5 ((D(9999	: 	$666# 	F 	FF6!(((EEEE !/ ! ! 8+::<< !%0CLc***
  6))!HSXXu5555& ! ! ! D! '1M##%% j{X/EPTdhiiii / + + 7*::<< +%/CJ$$TD$AAALc*** V#H?? 
		K !	      HS]]E**** 	 	 	D	
 i'I 	??????  7 CCCC 	 	 	D	
  		33??T !	 @      	 	 	D	  9+K888
  {((** 	'--//!&    
  	 	 	D	 $E***sZ   "G((
G54G5 9K 
K'&K'L" "
L/.L/5M 
M"!M"$N2 2
N?>N?r   c                 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: )r0   r   __file__r  r;   r   r   r   
executabler   r   rD   r   r   r   jsonloadsr   stderrr   r   )r(   r   project_rootr   r   s        r/   seed_profile_skillsr    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c                    g }t                      }|                                s|S t                      dz  }t          |                                          D ]}|                                rt
                              |j                  s7|j        dk    rC|dz  }|                                r]	 |	                                rt          j        ||           n|                    dd           t          j        t          |          d           |                    |j                   # t"          $ r'}| st%          d|j         d|            Y d	}~d	}~ww xY w|S )
u  Give every named profile that predates per-profile ``.env`` files one.

    Profiles created before the dashboard/CLI started seeding a ``.env``
    (PR #44792) have none, so once the Channels/Keys endpoints became
    profile-scoped those profiles stopped inheriting the root install's
    credentials and showed everything as unconfigured. To avoid breaking
    anyone on update, copy the DEFAULT install's ``.env`` into each named
    profile that lacks one — that preserves the effective credentials those
    profiles were already running with (they previously read the root
    ``.env`` via the process environment). Users can then diverge per
    profile from there.

    Falls back to the placeholder header when the default install has no
    ``.env`` itself. Never overwrites an existing profile ``.env``.

    Returns the list of profile names that received a backfilled ``.env``.
    r   rf   r}  r   r%  r{  u%   ⚠ Could not seed .env for profile 'z': N)r   r   rC   r   r   r   r   r   r,   r   r   r  r   r   r   rD   r:   r-   r   )r   
backfilledrc  default_envr>   r  r   s          r/   backfill_profile_envsr    s   $ J&((M!! *,,v5K--//00 R R||~~ 	^%9%9%*%E%E 	:""6>?? 		R""$$ [(3333##O %	 $    HS]]E***ej)))) 	R 	R 	R RPejPPQPPQQQ	R s   7A=D55
E&?E!!E&r   c                    	 ddl }n# t          $ r g cY S w xY w	 |                                }n# t          $ r |}Y nw xY wt	          j                    h}	 |                    t	          j                                                              }|0|                    |j	                   |                                }|0n# t          $ r Y nw xY w	 |                    t	          j                              
                                }n# t          $ r d}Y nw xY wh d}d}g }	|                    g d          D ]4}
	 |
j        }|                    d          }|||v r'||                    d          |k    rC|                    d          pg }|s]d	                    |          t          j                            |d                                                   }t%          fd
|D                       p|dk    p|                    d          }|sd |D             }||z  sd}t)          |          D ]\  }}|dv r7|dz   t+          |          k     r!t-          ||dz                      | k    rd} nH@|                    d          r1t-          |                    dd          d                   | k    rd} n|se	 |
                                pi                     dd          }|r't3          |                                          |k    rd}n# t          $ r Y nw xY w|r|	                    |           # |j        |j        |j        f$ r Y &t          $ r Y 2w xY w|	S )u  PIDs of running Hermes *backends* bound to this profile.

    The ``gateway.pid`` file only tracks the messaging gateway.  A Desktop app
    spawns a headless ``serve`` (or legacy ``dashboard --no-open``) backend per
    profile that holds the profile's SQLite connection open and keeps writing
    sessions/WAL/sandbox files — the writer that makes ``rmtree`` hit
    ``ENOTEMPTY`` (and, pre-fix, resurrected the tree).  ``gateway.pid`` never
    names it, so find it by inspection: a Hermes backend subcommand
    (``serve``/``dashboard``/``gateway``) that is bound to *this* profile either
    by a ``--profile <canon>`` / ``-p <canon>`` selector or by a ``HERMES_HOME``
    that resolves to ``profile_dir``.

    Best-effort and tightly scoped: current-user processes only, backend
    subcommands only (never an interactive ``chat``/``tui``), and never this
    process or its ancestors.  Returns an empty list if ``psutil`` can't
    inspect anything.
    r   N>   rj  ru   	dashboard)zhermes_cli.mainzhermes-gatewaytui_gateway)pidr   usernamecmdliner  r  r   c              3       K   | ]}|v V  	d S Nr~   ).0markerjoineds     r/   	<genexpr>z._profile_bound_backend_pids.<locals>.<genexpr>B  s(      BBFf$BBBBBBrF   re   c                 6    h | ]}|                                 S r~   )r   )r  toks     r/   	<setcomp>z._profile_bound_backend_pids.<locals>.<setcomp>K  s     222cciikk222rF   F>   -p	--profiler   Tz
--profile==r  r   )psutilr   r;   r-   r   getpidProcessr  addr  r  process_iterinfor   joinr  basenamer   any
startswith	enumerater  r   r   r   r   r:   NoSuchProcessAccessDeniedZombieProcess)r   r(   r  resolved_dirskipr  current_userbackend_tokenshermes_markerspidsprocr  r  argvexe_name	is_hermestokensboundir  env_homer  s                        @r/   _profile_bound_backend_pidsr     s:   $   			#"**,, # # #"#
 ikk]D	,,3355 HHVZ   ]]__F      ~~bikk22;;==    766NIND##$J$J$JKK ; ;:	9D((5//C{cTkk'DHHZ,@,@L,P,P88I&&,"D  XXd^^Fw''Q006688HBBBB>BBBBB 1x'1&&x00 
   32T222F^+  E#D//  3---!a%#d))2C2C-d1q5k::eCC $ D ^^L11 -ciiQ.?.?.BCCuLL $   $ 4"99-LLH %DNN$:$:$<$<$L$L $     D
  !C   $f&96;OP 	 	 	H 	 	 	H	 Ks    0 ??A*C 
CC8D DD"M	$M	 M	BM	M	1BM	AL M	 
L-*M	,L--M		M0#	M0/M0c                 b   t          | |          }|sdS 	 ddlmm} n# t          $ r Y dS w xY w|D ]+}	  ||           # t
          t          t          f$ r Y (w xY wt          j                    dz   }t          j                    |k     rGt          fd|D                       sn+t          j
        d           t          j                    |k     G|D ]9} |          r,	  ||d           # t
          t          t          f$ r Y 5w xY w:t          d	t          |           d
           dS )a  Terminate any Desktop-spawned / stray backends bound to this profile.

    Complements ``_stop_gateway_process`` (which only knows ``gateway.pid``):
    without this, a live ``serve``/``dashboard`` backend keeps creating files
    under the profile dir while ``rmtree`` walks it, so the final ``rmdir``
    fails with ``ENOTEMPTY`` and the delete doesn't converge.  Best-effort:
    any failure is reported and swallowed so it never makes delete worse.
    Nr   )_pid_existsterminate_pidg      $@c              3   .   K   | ]} |          V  d S r  r~   )r  r  r  s     r/   r  z)_stop_profile_backends.<locals>.<genexpr>  s-      44;;s##444444rF         ?Tforceu   ✓ Stopped z profile backend process(es))r  r7  r  r  r   ProcessLookupErrorPermissionErrorr-   rF  r  sleepr   r  )r   r(   r  _terminate_pidr  deadliner  s         @r/   _stop_profile_backendsr  p  s    'uk::D OOOOOOOOO      	N3"OW= 	 	 	H	 y{{T!H
)++
 
 4444t44444 	
3 )++
 
 
   ;s 	s$/////&A   	 

@T
@
@
@AAAAAs/     
..AAA#C11D
Dc                 j   d}d}t          |          D ]}	 	 t          j        | |           n&# t          $ r t          j        | |           Y nw xY w dS # t          $ rJ}|}|                                 sY d}~ dS ||dz
  k     rt          j        d|dz   z             Y d}~d}~ww xY w||dS )a  ``shutil.rmtree`` with a short retry loop for transient races.

    Even after stopping the gateway and profile backends, a just-terminated
    process can leave in-flight writes (SQLite ``-wal``/``-shm`` checkpoints,
    sandbox temp files) that land after ``rmtree`` has walked past a directory,
    surfacing as ``ENOTEMPTY`` (POSIX) or a transient ``PermissionError``
    (Windows file lock still releasing).  A few spaced retries let those settle
    instead of failing the whole delete on a race the next attempt would win.
       N)onexc)onerrorr   g333333?)ranger   rmtree	TypeErrorr-   r,   rF  r  )r(   onexc_handlerattemptslast_excattemptr   s         r/   _rmtree_with_retryr    s    H#H?? 0 0	0Bk????? B B Bk=AAAAAABFF 	0 	0 	0H%%'' A%%
3'A+.///	0  s8   /A AAAA
B,"B'?#B''B,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%          ||           t'          |           |rt)          |           t+          ||           |r!t-          |          rt          d|            d}	 d }t/          ||           t          d|            n.# t0          $ r!}t          d| d|            |}Y d}~nd}~ww xY w	 t3                      }||k    rt5          d           t          d           n# t0          $ r Y nw xY w|t7          d| d|           |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.
    rf   zZCannot delete the default profile (~/.hermes).
To remove everything, use: hermes uninstallrr  ' 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 Nc                    ddl }t          |t                    r|d         }t          |t                    r	 t	          j        |t	          j         |          j        |j        z             n# t          $ r Y nw xY wt          j	        
                    |          }|rF	 t	          j        |t	          j         |          j        |j        z             n# t          $ r Y nw xY w | |           dS  )a  onexc/onerror handler: add +w on PermissionError so rmtree can proceed.

            Handles two cases on NixOS (and other systems with read-only
            copies from immutable stores):
            1. The path itself isn't writable (e.g. a file with mode 0444)
            2. The *parent* directory isn't writable (e.g. mode 0555)

            Compatible with both the ``onexc`` API (3.12+, receives an
            exception instance) and the ``onerror`` API (3.11-, receives
            ``sys.exc_info()`` tuple).
            r   Nr   )r   r   tupler  r   r   r   S_IWUSRr-   r  dirname)funcr  r   _statr  s        r/   _make_writablez&delete_profile.<locals>._make_writable  s    !   
 #u%% !f#// HT274==#85=#HIIII   D .. )@5=)PQQQQ"   T




s#   4A- -
A:9A:4C 
C! C!u   ⚠ Could not remove r   u#   ✓ Active profile reset to defaultz#Could not remove profile directory z

Profile 'z
' deleted.)r   r   r<   r   r   r   r0  r9  rO  r,  r   r   r,   r:   inputr   KeyboardInterruptEOFError_cleanup_gateway_service!_maybe_unregister_gateway_service_stop_gateway_processr  r   r  r   get_active_profileset_active_profileRuntimeError)r   r  r   r(   rl   r  
gw_runningr  r_  r`  ra  itemsr   has_wrapperitemconfirmremove_errorr  r   ri  s                       r/   delete_profiler    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 B@AAA  		:U:::;;AACCGG!8, 	 	 	.!!!	 e, UK000 &e,,,  +k*** 5+...  1 '' 	1///000 &*L*#	 #	 #	J 	;777*[**++++   8k88Q88999
#%%U??y)))7888    ^^^P\^^__eqq	
)
)
)
)***s<   <%G" ""HH%J- -
K7KK2L 
LLc                 X   	 ddl m}  |            dk    rdS ddl m}  |            }n# t          $ r Y dS t          $ r Y dS w xY w|                                sdS 	 |                    | d           dS # t          $ r Y dS t          $ r}t          d|            Y d}~dS d}~ww xY w)	u  Register a profile's gateway with s6 inside the container.

    No-op on host (systemd/launchd/windows) — those backends raise
    ``NotImplementedError`` on ``register_profile_gateway`` and the
    existing per-profile unit-generation paths handle lifecycle.

    Best-effort: any error (no backend detected, s6 not yet ready,
    etc.) is logged and swallowed so profile creation doesn't fail
    because the s6 supervision tree is in a weird state. The user
    can re-register manually later via the gateway start command,
    which goes through the same dispatch path.

    Port selection: each supervised profile gateway loads its own
    ``HERMES_HOME`` and binds the port resolved by ``gateway/config.py``
    from that profile's environment — ``API_SERVER_PORT`` (or
    ``platforms.api_server.extra.port`` in the profile's
    ``config.yaml``), defaulting to 8642. There is no ``[gateway] port``
    key and no Python-side allocator (PR #30136 review item I5 retired
    the SHA-256-derived range [9200, 9800) as dead code), so two
    profiles that both leave the port at its default will both try to
    bind 8642 — give each profile a distinct ``API_SERVER_PORT`` in its
    ``.env``.

    Host short-circuit: check ``detect_service_manager()`` first and
    return immediately if it isn't ``"s6"``. This keeps host
    (systemd/launchd/windows) profile creation completely silent —
    no ``get_service_manager()`` call, no exception path, no chance
    of the ``⚠ Could not register s6 gateway service`` warning ever
    rendering on a non-container machine. The earlier
    ``supports_runtime_registration()`` check still catches the case
    where detection somehow returns ``"s6"`` but the backend isn't
    actually the S6 one.
    r   detect_service_managers6Nget_service_managerF)	start_nowu+   ⚠ Could not register s6 gateway service: )	hermes_cli.service_managerr  r  r  r   supports_runtime_registrationregister_profile_gatewayr<   r   r   r  r  mgrr   s        r/   r  r  N  s9   DEEEEEE!!##t++FBBBBBB!!##       		
 ,,.. C$$\U$CCCCC    	 C C CACAABBBBBBBBBCs6   ) ) 
A	AAA6 6
B)	B)B$$B)c                 <   	 ddl m}  |            dk    rdS ddl m}  |            }n# t          $ r Y dS t          $ r Y dS w xY w|                                sdS 	 |                    |            dS # t          $ r}t          d|            Y d}~dS d}~ww xY w)u  Tear down a profile's s6 gateway service inside the container.

    No-op on host. Idempotent: absent services are silently skipped
    by ``unregister_profile_gateway``.

    Same host short-circuit as :func:`_maybe_register_gateway_service`
    — see that docstring.
    r   r  r  Nr  u-   ⚠ Could not unregister s6 gateway service: )r  r  r  r  r   r  unregister_profile_gatewayr   r  s        r/   r   r     s   	EEEEEE!!##t++FBBBBBB!!##      ,,.. E&&|44444 E E ECcCCDDDDDDDDDEs0   ) ) 
A	AAA4 4
B>BB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   Nr  )get_service_nameget_launchd_plist_pathLinuxz.configsystemduserz.service	systemctl--userdisableTF
   )r   checkr   stopry  )r   r!  zdaemon-reloadu   ✓ Service z removedDarwin	launchctlunloadu   ✓ Launchd service removedu   ⚠ Service cleanup: )r   r   r   r   rD   hermes_cli.gatewayr  r  systemr   r   r,   r   r   r   r   r   )
r   r(   	_platformold_homer  r  svc_namesvc_file
plist_pathr   s
             r/   r  r    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  "" 5 (C
OO<#'ub    !!T!2223444 + + +)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   {r  )r  )r     r  u   ✓ Gateway stopped (PID r   Tr  u   ✓ Gateway force-stopped (PID u   ✓ Gateway already stoppedu   ⚠ Could not stop gateway: )rF  r,   r   r   r  r  r  r#  r7  r  r  r  r  r   r  r-   r  r   )
r(   _timepid_filerawr+  r  r  r  _r   s
             r/   r  r    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.
    rf   )r   r   r   r   r  r-   )r  r   s     r/   r  r     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.
    rf   rr  8' does not exist. Create it with: hermes profile create Tr   ry  r8   
N)r   r   r   r   r   r  r   r   with_suffixr   replace)r   r   r  ra   s       r/   r  r    s    
 #4((E%   	."7"7= = =5:= =
 
 	

 $%%DKdT222	t$$$$$ v&&ut|$$$DrF   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   rt  rf   r   custom)r   ru  r;   rC   r   relative_topartsr  r   r   r<   )ru  hermes_homeresolveddefault_resolvedrc  relr@  s          r/   rh  rh  (  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 )u  Return an *ignore* callable for :func:`shutil.copytree`.

    Two-tier filtering:

    * **Root-level allow-list** — only entries whose name appears in
      ``_DEFAULT_EXPORT_INCLUDE_ROOT`` survive. Everything else (such as
      an unrelated ``x11-dev/`` directory in a Docker deployment where
      HERMES_HOME equals the cwd) is excluded. Blacklisting was tried
      first and proved unable to anticipate every non-Hermes file the
      user may have lying alongside HERMES_HOME (#58394).
    * **Universal exclusions at any depth** — ``__pycache__``, sockets,
      temp files; plus npm lockfiles, which may appear at the root.

    All other profile artifacts are copied through untouched.
    r3   contentsr)   c                 &   t                      }|D ]L}|dk    s|                    d          r|                    |           3|dv r|                    |           Mt          |           k    r|                    d |D                        |S )Nr6   )r7   r8   >   package.jsonpackage-lock.jsonc              3   ,   K   | ]}|t           v|V  d S r  )_DEFAULT_EXPORT_INCLUDE_ROOT)r  r>   s     r/   r  z:_default_export_ignore.<locals>._ignore.<locals>.<genexpr>d  s6        u<X/X/X/X/X/X/X rF   )setr9   r  r   rt   )r3   rG  r=   r>   rE  s       r/   rB   z'_default_export_ignore.<locals>._ignoreX  s    uu 	# 	#E%%8I)J)J%E""""???E""" 	??h&&NN  #+      rF   )rD   listrM  )rE  rB   s   ` r/   _default_export_ignorerO  G  s=    "3 $ 3      " NrF   output_pathc           	      d  
 ddl }t          |           }t          |           t          |          }|                                st          d| d          t          |          }t          |                              d                              d          }|dk    r|	                                5 }t          |          dz  }t          j        ||d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        ||d
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   Nrr  r  z.tar.gzz.tgzrf   Trv  gztarrU   r   c                 (    t          |          z  S r  )rM  )drG  _CREDENTIAL_FILESs     r/   <lambda>z export_profile.<locals>.<lambda>  s    '83x=='H rF   )tempfiler   r   r   r   r   r   rD   removesuffixTemporaryDirectoryr   r  rO  make_archive)r   rP  rW  r   r(   outputbasetmpdirstagedr   rU  s             @r/   export_profiler_  l  sL   
 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   .r~   r  parts     r/   
<listcomp>z4_normalize_profile_archive_parts.<locals>.<listcomp>  s"    HHHd$i2G2GT2G2G2GrF   c              3   "   K   | ]
}|d k    V  dS )z..Nr~   rf  s     r/   r  z3_normalize_profile_archive_parts.<locals>.<genexpr>  s&      77777777rF   )r<  r   r   is_absolutedriver<   r@  r  )r`  normalized_name
posix_pathwindows_pathr@  s        r/    _normalize_profile_archive_partsro    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LrF   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  
getmembersro  r   joinpathisdirr   isfiler<   r  extractfiler   copyfileobjr   r   moder-   )	rp  rq  ru  tfmemberr@  r   	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   Nrs  c                     h | ]E}t          |j                  }t          |          d k    s|                                =|d         FS )r   r   )ro  r   r  rx  )r  r~  r@  s      r/   r  z1_inspect_profile_archive_roots.<locals>.<setcomp>  sP     
 
 
:6;GG5zzA~~~ !H ~~rF   c                 j    h | ]0}|                                 t          |j                  d          1S )r   )rx  ro  r   )r  r~  s     r/   r  z1_inspect_profile_archive_roots.<locals>.<setcomp>  sE       <<>>0==a@  rF   )ru  r  rv  )rp  ru  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.rf   u   Cannot import as 'default' — that is the built-in root profile (~/.hermes). Specify a different name: hermes profile import <archive> --name <name>rr  rs  Tr   hermes_profile_import_)r	  z,Profile archive root is missing or invalid: )rW  r   r,   r   r  r  popr<   r   r   r   r  r   r   rY  r  r   renamer   moverD   )r  r   rW  rp  r  archive_rootinferred_namer   r(   rc  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| }|dz  t                      dz  t          j                    dz  dz  g}t                      }|D ]4}	 |                                }	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                    s||v r|n|}||vr||v rt          d	| d
|            ||         }t          |t                    r]d|vrY|                    d          r|                    dd          d         }n"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|            6dS )zGRename Honcho host blocks for a renamed profile without changing peers.hermes_zhermes.zhoncho.jsonz.honchozconfig.jsonr   r%  hostsu$   ⚠ Honcho host block not migrated: z already exists in aiPeerr6  r   re  r8   rE  F)indentensure_asciir:  Try  u   ✓ Honcho host updated:     → N)rC   r   r   rM  r;   r-   r   r  r  r  r   JSONDecodeErrorr   r   r)  r   r  r   r  r;  r   r   dumpsr<  r   )r  r  r  old_hostlegacy_old_hostnew_hostr   seenr  rB  r5  r  source_hostblockbarera   s                   r/   _migrate_honcho_profile_hostr  *  s-   ###H***O###H 	- ""]2	i-/J eeD ,H ,H	||~~HH 	 	 	HHH	t4<<>>	*T^^W^==>>CC-. 	 	 	H	   %&& 	"*e"3"3hhe##u\\\VZ\\]]]k"eT"" 	#xu'<'<%%i00 ["((a0037:k7I7I{((a0033{"E(O))K00ht{V344	NN4:c!%HHH4OZaNbbbKK 	 	 	

d
++++   H	 	F+FFHFFGGGGY,H ,Hs[   A''A65A6((CC*)C*/AH44
I*?II*
I# I*"I##I*)I*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.
    rf   z"Cannot rename the default profile.u.   Cannot rename to 'default' — it is reserved.rr  r  z' already exists.u   ✓ Renamed r  u   ✓ Alias updated: u   ⚠ Cannot create alias 'u   ' — u   ✓ Active profile updated: )r   r   r<   r   r   r   r,   r  r9  r  r  r  r   r   r  r   r   r   r  r  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c                     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.
    rf   rr  r9  )r   r   r   r   r   rD   )r   r   r(   s      r/   resolve_profile_envr    s     #<00E%   !%((K	+"4"4"6"6= = =5:= =
 
 	

 {rF   r  )NFFFFN)F)dr   r  r   rer   r   r   r   r   rF  dataclassesr   pathlibr   r   r   typingr   r   r	   agent.skill_utilsr
   compiler   r  r  r  r   rN  rD   r!  	frozensetr    r'   r+   r"  r0   rE   _DEFAULT_EXPORT_EXCLUDE_ROOTrL  r   r   r   rC   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r)  r   r  r  r,  r0  r9  r:  floatr#  rG  rC  rO  rQ  rT  r\  re  rk  r  r  r  r  r  r  r  r  r   r  r  r  r  rh  rO  r_  ro  r  rM  r  r  r  r  r  r~   rF   r/   <module>r     s
    *  				 				        



  ! ! ! ! ! ! 8 8 8 8 8 8 8 8 8 8 ( ( ( ( ( ( ( ( ( ( 4 4 4 4 4 49::  "        $s)   , 3<) = = = 3 3 3   , 3<) = = = 3 3 3     0 D T    34 3 3 3 3t  )y * * *     B  )y * * *      )     
  i ! ! !   3D 3 3 3 3%$ % % % %9$ 9 9 9 9
*$ * * * *     $
 
 
 
 
 
<
c 
d 
 
 
 
 (# ($ ( ( ( (+ + + + + +' ' ' ' ' 'TG G G G G( ( (Xc] (htn ( ( ( (V     <T d    >G G# G G G G6  .c3h . . . .j # # # # # # # #@   %        2D U    &     R ;= DeE5#$5667 < < < d u    6t     P(D (T ( ( ( (4 D    8 "&'+	O O OO #O tn	O
 
O O O OJEtK( E E E EP& &$uS$Y/?*@ & & & &V !%!%~ ~
~~ ~ 	~
 ~ ~ #~ 
~ ~ ~ ~B* *T *$ *8D> * * * *Z1 1 1$s) 1 1 1 1hms m m$s) m m m m`&B# &BD &BT &B &B &B &BRD D    <T T T4 TD T T T Tn9C# 9C$ 9C 9C 9C 9CxEC ED E E E E6-*3 -*T -*d -* -* -* -*`$2t $2 $2 $2 $2 $2VC    S T    2    >"T " " " "J+ +3 +4 + + + +\# $s)    (4 d t    @D SX    2< < <HSM <T < < < <F9H3 9H# 9H 9HQU 9H 9H 9H 9Hx4S 4C 4D 4 4 4 4vc c      rF   