
    (Gjm                        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mZmZ ddlm	Z	m
Z
 ddlmZmZmZ ddlmZ ddlmZmZmZmZmZ ddlmZ  ej        e          Z e            Zed	z  Zed
z  ZdZde	fdZ de	fdZ!dee"         fdZ#dee"e"f         fdZ$de%fdZ&dee"e"f         fdZ'de	de"de"fdZ(de	deee"e	f                  fdZ)de	de	de	fdZ*de	de"fdZ+de	de	de"fdZ,de	dee"         fd Z-de	de"fd!Z.dee"ee"e"e	f         f         fd"Z/de	d#e	de"fd$Z0d%d&d'e"d(e1de2fd)Z3dQd*e1dee"         fd+Z4dQd*e1de2fd,Z5de	ddfd-Z6dQd'e"d(e1de2fd.Z7d/e"d0e"de1fd1Z8dee2         fd2Z9de	deee:         ee"         f         fd3Z;d'e"de2fd4Z<d5e1de2fd6Z=de1fd7Z>dQd8e1de2fd9Z?ed:k    rW e@d;            e5d%<          ZA eBeAd=                    d> eBeAd?                    d@eAdA          dBgZCeAdC         rneAdC         ZDdDZEdEF                    eDdeE                   ZG eBeD          eEk    reGdF eBeD          eEz
   dGz  ZGeCH                     eBeD           dHeG            eAdI         r'eCH                     eBeAdI                    dJ           eAI                    dK          r'eCH                     eBeAdK                    dL            e@dMdEF                    eC           dNeAdO          dP           dS dS )Ru  
Skills Sync -- Manifest-based seeding and updating of bundled skills.

Copies bundled skills from the repo's skills/ directory into ~/.hermes/skills/
and uses a manifest to track which skills have been synced and their origin hash.

Manifest format (v2): each line is "skill_name:origin_hash" where origin_hash
is the MD5 of the bundled skill at the time it was last synced to the user dir.
Old v1 manifests (plain names without hashes) are auto-migrated.

Update logic:
  - NEW skills (not in manifest): copied to user dir, origin hash recorded.
  - EXISTING skills (in manifest, present in user dir):
      * If user copy matches origin hash: user hasn't modified it → safe to
        update from bundled if bundled changed. New origin hash recorded.
      * If user copy differs from origin hash: user customized it → SKIP.
  - DELETED by user (in manifest, absent from user dir): respected, not re-added.
  - REMOVED from bundled (in manifest, gone from repo): cleaned from manifest.

The manifest lives at ~/.hermes/skills/.bundled_manifest.
    N)datetimetimezone)PathPurePosixPath)get_bundled_skills_dirget_hermes_homeget_optional_skills_dir)is_excluded_skill_path)DictListOptionalSetTuple)atomic_replaceskillsz.bundled_manifestz.no-bundled-skillsreturnc                  ^    t          t          t                    j        j        dz            S )zLocate the bundled skills/ directory.

    Checks HERMES_BUNDLED_SKILLS env var first (set by Nix wrapper),
    then a wheel-installed data dir, then falls back to the relative
    path from this source file.
    r   )r   r   __file__parent     7/home/rurouni/.hermes/hermes-agent/tools/skills_sync.py_get_bundled_dirr   5   s#     "$x.."7">"IJJJr   c                  ^    t          t          t                    j        j        dz            S )z/Locate the official optional-skills/ directory.optional-skills)r	   r   r   r   r   r   r   _get_optional_dirr   ?   s"    "4>>#8#?BS#STTTr   c                     	 ddl m} m} n# t          $ r t	                      cY S w xY w |             t	                      } |             D ]r}|                    d          D ]Z}t          |          r|j        }|                    |j	                   t          |d          }|r|                    |           [s|S )zIndex every skill available in external_dirs by name and frontmatter name.

    Returns a set of skill names that are already provided by external dirs.
    Used to prevent sync_skills from shadowing externally-delegated skills.
    r   )get_external_skills_dirs_external_dirs_cache_clearSKILL.md )agent.skill_utilsr   r   ImportErrorsetrglobr
   r   addname_read_skill_name)r   r   external_namesext_dirskill_md	skill_dirfrontmatter_names          r   _build_external_skill_indexr.   D   s   ZZZZZZZZZ   uu    "uuN++-- 
5 
5j11 		5 		5H%h//  Iy~.../"== 5""#3444		5 s    &&c                     t                                           si S 	 i } t                               d                                          D ]e}|                                }|sd|v rC|                    d          \  }}}|                                | |                                <   `d| |<   f| S # t          t          f$ r i cY S w xY w)z
    Read the manifest as a dict of {skill_name: origin_hash}.

    Handles both v1 (plain names) and v2 (name:hash) formats.
    v1 entries get an empty hash string which triggers migration on next sync.
    utf-8encoding:r!   )MANIFEST_FILEexists	read_text
splitlinesstrip	partitionOSErrorIOError)resultliner'   _hash_vals        r   _read_manifestr@   a   s     !! 	!++W+==HHJJ 
	" 
	"D::<<D d{{$(NN3$7$7!a'/~~'7'7tzz||$$  "tW   			s   BB4 4C
	C
c                     	 ddl m}   |             S # t          $ r t          dz  }|                                st                      cY S t                      }	 |                    d                                          D ]B}|                                }|r*|	                    d          s|
                    |           Cn# t          $ r Y nw xY w|cY S w xY w)u  Built-in skills the curator pruned — must NOT be re-seeded on sync.

    Delegates to ``tools.skill_usage`` (single source of truth) and falls back
    to reading ``~/.hermes/skills/.curator_suppressed`` directly if that import
    is unavailable in a packaged/update context.
    r   )read_suppressed_namesz.curator_suppressedr0   r1   #)tools.skill_usagerB   	Exception
SKILLS_DIRr5   r$   r6   r7   r8   
startswithr&   r:   )rB   pathnamesr=   s       r   _read_suppressed_namesrJ   |   s
   ;;;;;;$$&&&   11{{}} 	55LLL	88CCEE $ $zz|| $ 4 4 $IIdOOO$  	 	 	D	s:    6C
CA+CC
CCCCCentriesc                 @   ddl }t          j                            dd           d                    d t          |                                           D                       dz   }	 |                    t          t          j                  dd	          \  }}	 t          j
        |d
d          5 }|                    |           |                                 t          j        |                                           ddd           n# 1 swxY w Y   t          |t                     dS # t           $ r( 	 t          j        |           n# t$          $ r Y nw xY w w xY w# t&          $ r.}t(                              dt          |d           Y d}~dS d}~ww xY w)zWrite the manifest file atomically in v2 format (name:hash).

    Uses a temp file + os.replace() to avoid corruption if the process
    crashes or is interrupted mid-write.
    r   NTparentsexist_ok
c              3   *   K   | ]\  }}| d | V  dS )r3   Nr   ).0r'   r?   s      r   	<genexpr>z"_write_manifest.<locals>.<genexpr>   s4      XXndH))x))XXXXXXr   z.bundled_manifest_.tmpdirprefixsuffixwr0   r1   z&Failed to write skills manifest %s: %sexc_info)tempfiler4   r   mkdirjoinsorteditemsmkstempstrosfdopenwriteflushfsyncfilenor   BaseExceptionunlinkr:   rE   loggerdebug)rK   r\   datafdtmp_pathfes          r   _write_manifestrr      s    OOOtd;;;99XXw}}@W@WXXXXX[__D`''M())' ( 
 
H
	2sW555 %			$$$% % % % % % % % % % % % % % % 8]33333 	 	 		(####   	  ` ` `=}aZ^_________`ss   (2E% D0 2ADD0 DD0 DD0 0
E";EE"
EE"EE""E% %
F/#FFr+   fallbackc                    	 |                      dd          dd         }n# t          $ r |cY S w xY wd}|                    d          D ]}|                                }|dk    r|r nbd	}#|r\|                    d
          rG|                    dd          d                                                             d          }|r|c S |S )zORead the name field from SKILL.md YAML frontmatter, falling back to *fallback*.r0   replace)r2   errorsNi  FrP   z---Tzname:r3      z"')r6   r:   splitr8   rG   )r+   rs   contentin_frontmatterr=   strippedvalues          r   r(   r(      s   $$gi$HH$O   Nd## 
 
::<<u !N 	h11':: 	NN3**1-3355;;EBBE Os   " 11bundled_dirc                     g }|                                  s|S |                     d          D ]E}t          |          r|j        }t	          ||j                  }|                    ||f           F|S )zz
    Find all SKILL.md files in the bundled directory.
    Returns list of (skill_name, skill_directory_path) tuples.
    r    )r5   r%   r
   r   r(   r'   append)r}   r   r+   r,   
skill_names        r   _discover_bundled_skillsr      s    
 F %%j11 / /!(++ 	O	%h	??
z9-....Mr   r,   c                 @    |                      |          }t          |z  S )z
    Compute the destination path in SKILLS_DIR preserving the category structure.
    e.g., bundled/skills/mlops/axolotl -> ~/.hermes/skills/mlops/axolotl
    )relative_torF   )r,   r}   rels      r   _compute_relative_destr      s"    
 


,
,Cr   	directoryc                    t          j                    }	 t          |                     d                    D ]}|                                rq|                    |           }|                    t          |                              d                     |                    |	                                           n# t          t          f$ r Y nw xY w|                                S )zHCompute a hash of all file contents in a directory for change detection.*r0   )hashlibmd5r_   r%   is_filer   updaterb   encode
read_bytesr:   r;   	hexdigest)r   hasherfpathr   s       r   	_dir_hashr      s    []]FIOOC0011 	2 	2E}} 2''	22c#hhoog66777e..00111		2
 W   s   B*C   CCrH   basec                 @   |                      |          }|                                }t          |          }d |j        D             }|                                s|rt          d |D                       rt          d|           d                    |          S )zLReturn a normalized relative POSIX path, rejecting traversal/absolute paths.c                     g | ]}|d v|	S )>   r!   .r   rR   parts     r   
<listcomp>z*_safe_rel_install_path.<locals>.<listcomp>   s"    BBBdD	,A,AT,A,A,Ar   c              3   "   K   | ]
}|d k    V  dS )z..Nr   r   s     r   rS   z)_safe_rel_install_path.<locals>.<genexpr>   s&      -M-Mtddl-M-M-M-M-M-Mr   zUnsafe optional skill path: /)r   as_posixr   partsis_absoluteany
ValueErrorr^   )rH   r   r   posixpurer   s         r   _safe_rel_install_pathr      s    


4
 
 CLLNNEDBBdjBBBE A A#-M-Mu-M-M-M*M*M A???@@@88E??r   c                     g }t          |                     d                    D ]P}|                                r:|                    |                    |                                                      Q|S )z8List files inside a skill directory in lock-file format.r   )r_   r%   r   r   r   r   )r,   filesr   s      r   _skill_file_listr     so    E	,,-- B B==?? 	BLL**955>>@@AAALr   c                 d    	 ddl m}  ||           S # t          $ r t          |           cY S w xY w)zJReturn the same hash style the skills hub lock uses, falling back locally.r   )content_hash)tools.skills_guardr   rE   r   )r   r   s     r   _content_hashr   
  sZ    $333333|I&&& $ $ $ #####$s    //c                  Z   t                      } i }|                                 s|S t          |                     d                    D ]a}t	          |          r|j        }	 t          ||           }n# t          $ r Y 7w xY w|j        }t          ||          }|||f}|||<   |||<   b|S )a%  Return official optional skills keyed by folder name and frontmatter name.

    Values are ``(folder_name, install_path, source_dir)``. Multiple keys may
    point to the same skill so callers can accept either the folder slug used
    by the hub lock or the user-facing frontmatter name.
    r    )
r   r5   r_   r%   r
   r   r   r   r'   r(   )optional_dirindexr+   srcinstall_pathfolder_namer-   r|   s           r   _optional_skill_indexr     s     %&&L.0E   <--j99:: ( (!(++ 	o	1#|DDLL 	 	 	H	h+HkBBlC0"k"'Ls   #A44
B Bbackup_rootc                 6   |                      t                    }||z  }|j                            dd           |                                rd}|                    |j         d|                                           r6|dz  }|                    |j         d|                                           6|                    |j         d|           }t          j        t          |           t          |                     |
                                S )zLMove an existing skill directory into a restore backup, preserving rel path.TrM   rw   -)r   rF   r   r]   r5   	with_namer'   shutilmoverb   r   )rH   r   r   targetrX   s        r   _move_to_restore_backupr   1  s   


:
&
&C3F
Mt444}} =&+888899@@BB 	aKF &+888899@@BB 	!!V[";";6";";<<
KD		3v;;'''<<>>r   F)restorer'   r   c                   t                      }|sddg g g dS | dv r1t          t          |                                          d           ng }|s%|                    |           }|dd|  g g g dS |g}g }g }t          j        t          j                  	                    d	          }t          d
z  d| z  }|D ]\  }	}
}t          t          |
                    d           z  }t          |          }|                                ot          |          |k    }t          |dz  |	          }g }t                                          rt          t                              d                    D ]}t#          |          r|j        }	 |                    t                     n# t(          $ r Y Aw xY wt          ||j                  }||k    ra|j        |	k    s||	|hv r|                    |           |r|D ]9}|                                r#|                    t/          ||                     :|                                r%|s#|                    t/          ||                     |                                sF|j                            dd           t3          j        ||           |                    |	           |st7          d          }dd||||rt9          |          nddS )a3  Restore one or all official optional skills from repo source.

    ``restore=False`` only performs exact-match provenance backfill. ``restore=True``
    repairs already-mutated/reorganized skills by backing up matching active
    copies and copying the official optional source into its canonical path.
    Fz,No official optional skills directory found.)okmessagerestored
backfilled	backed_up>   r   allc                     | d         S )Nrw   r   )items    r   <lambda>z1restore_official_optional_skill.<locals>.<lambda>J  s
    47 r   keyNz#Official optional skill not found: z%Y%m%d-%H%M%Sz.restore-backupszofficial-optional-r   r    TrM   quietz(Official optional skill repair complete.r!   )r   r   r   r   r   
backup_dir)r   r_   r$   valuesgetr   nowr   utcstrftimerF   r   rx   r   r5   r(   r%   r
   r   r   r   r'   r   r   r]   r   copytree_backfill_optional_provenancerb   )r'   r   r   targetsr   r   r   	timestampr   r   r   r   destsrc_hashcanonical_oksrc_frontmattermatchesr+   	candidatecandidate_namematchr   s                         r   restore_official_optional_skillr   ?  s    "##E K(Vdfvx  HJ  K  K  	KGK|G[G[fS((.B.BCCCCacG 4>,XRV,X,Xfhxz  JL  M  M  M(HIX\**33ODDI114T4T4TTK*1 # #&\3D,"4"4S"9"9::S>>{{}}D4H)D +3+;[II  	.":#3#3J#?#?@@ . .)(33 $O	))*5555!   H!1(IN!K!K$$>[00N{TcFd4d4dNN9--- 	  R R<<>> R$$%<UK%P%PQQQ{{}} M\ M  !8{!K!KLLL;;== -!!$!>>>T***,,, 		 /T:::J= *3;c+&&&  s   F))
F65F6r   c                 <   t                      }|                                sg S t          dz  dz  }	 |                                r&t          j        |                                          ndi d}n!# t          j        t          f$ r di d}Y nw xY w|                    di           }d |	                                D             }g }d}t          |                    d                    D ]}}t          |          r|j        }		 t          |	|          }
n3# t          $ r&}t                               d	|	|           Y d
}~Wd
}~ww xY wt          t%          |
                    d           z  }|                                r|                                st+          |          t+          |	          k    r|	j        }||v s|
|v rt/          j        t2          j                                                  }dd|
 ddt9          |          |
t;          |          ddi||d
||<   |                    |
           |                    |           d}| stA          d| d           |r,|j        !                    dd           dd
l"}t          j#        |dd          dz   }|$                    tK          |j                  dd          \  }}	 tM          j'        |dd           5 }|(                    |           |)                                 tM          j*        |+                                           d
d
d
           n# 1 swxY w Y   tY          ||           n5# tZ          $ r( 	 tM          j.        |           n# t          $ r Y nw xY w w xY w|S )!a  Mark already-present official optional skills as hub-installed.

    This covers the migration case where a skill used to be bundled (or was
    manually copied into the active skills tree) and later lives under
    optional-skills/. If the active copy is byte-identical to the official
    optional source, record official hub provenance without copying or
    reinstalling anything. Modified/local skills are left alone.
    z.hubz	lock.jsonrw   )version	installedr   c                 b    h | ],}t          |t                    |                    d           -S )r   )
isinstancedictr   )rR   entrys     r   	<setcomp>z0_backfill_optional_provenance.<locals>.<setcomp>  sD       eT""		.!!  r   Fr    z/Skipping optional skill with unsafe path %s: %sNr   officialz	official/builtinr   backfilled_fromr   )
source
identifiertrust_levelscan_verdictr   r   r   metadatainstalled_at
updated_atTz  = z* (official optional provenance backfilled)rM   r      )indentensure_asciirP   z.lock_rT   rU   rY   r0   r1   )/r   r5   rF   jsonloadsr6   JSONDecodeErrorr:   
setdefaultr   r_   r%   r
   r   r   r   rk   rl   r   rx   is_dirr   r'   r   r   r   r   	isoformatr   r   r&   r   printr]   r\   dumpsra   rb   rc   rd   re   rf   rg   rh   r   ri   rj   )r   r   	lock_pathrm   r   existing_pathsr   changedr+   r   r   rq   r   	lock_namer   r\   payloadrn   ro   rp   s                       r   r   r     sZ    %&&L   	V#k1I/4=4D4D4F4Fktz)--//000XYhjLkLk '* / / /2../R00I %%''  N JG<--j99:: $P $P!(++ 	o	1#|DDLL 	 	 	LLJCQRSSSHHHH	 D,"4"4S"9"9::{{}} 	DKKMM 	T??inn,,H		!!\^%C%CL..88::	 4l44$()$//(%d++*,=>%# 
  
	) 	<((()$$$ 	PNNNNOOO td;;; 	*T!%@@@4G''I$%% ( 
 
H
	2sW555 %   			$$$% % % % % % % % % % % % % % % 8Y//// 	 	 		(####   	 s~   ?A3 3BB	D
E
$EE
M' /AM?M' MM' MM' '
N2NN
NNNNc           
      Z   t           t          z                                  r| st          d           g g dg g dg ddS t	                      }|                                sg g dg g g dg dS t
                              dd           t                      }t          |          }d |D             }t                      }t                      }g }g }g }	g }
g }d}|D ](\  }}||v r|                    |            t          ||          }t          |          }|                    d          }|                                r|                                s	 |j                            dd           t!          j        t%          |          t%          |                     t&                              d	|           n4# t*          t,          f$ r  t&                              d
|d           Y nw xY w||v r|                    |           |dz  }| st          d| d           |                                rLt          |          |k    r9t1          |           | st          d|            |                    |d           ||vr	 |                                r7|dz  }t          |          |k    r|||<   nx| st          d| d| d           n_|j                            dd           t!          j        ||           |                    |           |||<   | st          d|            ^# t*          t,          f$ r"}| st          d| d|            Y d}~d}~ww xY w|                                rz|                    |d          }t          |          }|s|||<   ||k    r|dz  }n|dz  }t9          ||          r,|
                    |           | st          d| d           $||k    r	 |                    d          }|                                rt1          |           t!          j        t%          |          t%          |                     	 t!          j        ||           |||<   |	                    |           | st          d| d           	 t1          |           n4# t*          t,          f$ r  t&                              d|d           Y nw xY wn# t*          t,          f$ r |                                r|                                rE	 t1          |           n4# t*          t,          f$ r  t&                              d|d           Y nw xY w|                                s/t!          j        t%          |          t%          |                      w xY w# t*          t,          f$ r"}| st          d| d|            Y d}~d}~ww xY w|dz  }#|dz  }*t=          t?          |                                           |z
            }|D ]}||= |!                    d          D ]}|"                    |          }t
          |z  }|                                sm	 |j                            dd           t!          j#        ||           h# t*          t,          f$ r&}t&                              d ||           Y d}~d}~ww xY wtI          |           tK          | !          }||	||
||tM          |          ||d"	S )#z
    Sync bundled skills into ~/.hermes/skills/ using the manifest.

    Returns:
        dict with keys: copied (list), updated (list), skipped (int),
                        user_modified (list), cleaned (list), total_bundled (int)
    uJ     (skipped — profile opted out of bundled skills via .no-bundled-skills)r   T)copiedupdatedskippeduser_modifiedcleanedtotal_bundledoptional_provenance_backfilledskipped_opt_out)r   r   r  r  r  
suppressedr  r  rM   c                     h | ]\  }}|S r   r   )rR   r'   r>   s      r   r   zsync_skills.<locals>.<setcomp>  s    888gdAT888r   z.bakz#Recovered orphaned skill backup: %sz*Could not recover orphaned skill backup %srZ   rw   u     ⇢ z7 (deferred to external_dirs, not written to local tree)u     ✓ removed stale shadow of Nu     ⚠ uw   : bundled version shipped but you already have a local skill by this name — yours was kept. Run `hermes skills reset z)` to replace it with the bundled version.z  + z  ! Failed to copy : r!   z  ~ z (user-modified, skipping)u     ↑ z
 (updated)zCould not remove backup %sz.Could not clear partial copy %s during restorez  ! Failed to update zDESCRIPTION.mdzCould not copy %s: %sr   )	r   r   r  r  r  r  r  r  shadowed_by_external)'HERMES_HOMENO_BUNDLED_SKILLS_MARKERr5   r   r   rF   r]   r@   r   rJ   r.   r   r   r   with_suffixr   r   r   rb   rk   infor:   r;   warning_rmtree_writablepopr   r   _is_tracked_user_modificationrl   r_   r$   keysr%   r   copy2rr   r   len)r   r}   manifestbundled_skillsbundled_namesr  external_indexr
  r   r   r  suppressed_skippedr  r   	skill_srcr   bundled_hash_orphanrq   origin_hash	user_hashbackupr  r'   desc_mdr   	dest_descr  s                               r   sync_skillsr#    s    	..6688 
 	`^___RAB.0T
 
 	
 #$$K 
RABbST.0
 
 	
 TD111H-k::N88888M'))J022N&(FGM$&G!/ a a
I ##%%j111%i== ++ ""6**>> 		DKKMM 		!!$!>>>CLL#d))444A7KKKKW%   @'!       ''
 !''
333qLG 1Z 1 1 1   {{}} /4L!@!@ &&& IG:GGHHHZ...X%%C;;== 3 qLG ,66/;,," GZ G GBLG G G   K%%dT%BBBOIt444MM*---+7HZ(  31Z11222W% C C C CA
AAaAABBBC
 [[]] F	",,z266K!$I 	 (1$,,qLGG qLG,[)DD $$Z000 IGGGGHHH {**'I!--f55F
 }} 1(000KD		3v;;777	4888/;,z222$ C!"A:"A"A"ABBB^,V4444 '1 ^ ^ ^"LL)EvX\L]]]]]^#W-    "==?? 
D#{{}} &!&$4T$:$:$:$:(/'9 !& !& !&$*NN(X(,t %3 %& %& %& %& %&!&
 $(;;== D &CKKT C C C   ) I I I  IGjGGAGGHHHI 1 qLGG S))M9::G  TNN $$%566 B B!!+..$	!! 	BB &&td&CCCWi0000W% B B B4gqAAAAAAAAB		B H%B%O%O%O" &(^,,*H 4
 
 
s   A&F>>.G/.G/B*L88M+	M&&M+A'W .AS83SS8.S41S83S44S87W 89V;2UV;.U30V;2U33AV;;W  W3W..W31Z::[1[,,[1c                     t          |                                           }t                                          }||j        vrt	          d|d|d          ddlfd}t          j        | |           dS )a  Remove a directory tree, making read-only entries writable first.

    Handles immutable package sources (Nix store, deb/rpm installs) that
    preserve read-only permissions on copied files *and* directories
    (``r-xr-xr-x``).  Removing a child requires write permission on its
    parent directory, so the retry handler makes the failing path **and its
    parent** writable before re-attempting.  See #34860, #34972.
    zrefusing to rmtree z: not strictly under u    (scope guard — see #48200)r   Nc                     t           j                            |          |fD ]-}	 t          j        |j                   # t
          $ r Y *w xY w | |           d S )N)rc   rH   dirnamechmodS_IRWXUr:   )funcr   r[   r   stats       r   	_on_errorz#_rmtree_writable.<locals>._on_error  st     wu--u5 	 	F....   Us   A
AA)onerror)r   resolverF   rN   r   r*  r   rmtree)rH   r   skills_rootr+  r*  s       @r   r  r    s    ( $ZZ!!F$$&&K &.((,& , , , , ,
 
 	
 KKK     M$	******r   c                 .   t                      }t                      }t          |          }t          |          }| |v }| |v }|s|sddd|  dddS d}|rv|sddd|  dddS t	          ||          |          }	|	                                r?	 t          |	           d	}n,# t          t          f$ r}
dd
d|	 d|
 dddcY d}
~
S d}
~
ww xY w|r|| = t          |           t          d	          }|r|r	d}d|  d}n|r	d}d|  d}nd}d|  d}d	|||dS )u;  
    Reset a bundled skill's manifest tracking so future syncs work normally.

    When a user edits a bundled skill, subsequent syncs mark it as
    ``user_modified`` and skip it forever — even if the user later copies
    the bundled version back into place, because the manifest still holds
    the *old* origin hash. This function breaks that loop.

    Args:
        name: The skill name (matches the manifest key / skill frontmatter name).
        restore: If True, also delete the user's copy in SKILLS_DIR and let
                 the next sync re-copy the current bundled version. If False
                 (default), only clear the manifest entry — the user's
                 current copy is preserved but future updates work again.

    Returns:
        dict with keys:
          - ok: bool, whether the reset succeeded
          - action: one of "manifest_cleared", "restored", "not_in_manifest",
                    "bundled_missing"
          - message: human-readable description
          - synced: dict from sync_skills() if a sync was triggered, else None
    Fnot_in_manifest'zi' is not a tracked bundled skill. Nothing to reset. (Hub-installed skills use `hermes skills uninstall`.)N)r   actionr   syncedbundled_missingur   ' has no bundled source — manifest entry preserved but cannot restore from bundled (skill was removed upstream).T	not_resetzCould not delete user copy at r	  u3   . Manifest entry preserved — nothing was changed.r   r   z
Restored 'z' from bundled source.z/' (no prior user copy, re-copied from bundled).manifest_clearedzCleared manifest entry for 'zf'. Future `hermes update` runs will re-baseline against your current copy and accept upstream changes.)r@   r   r   r   r   r5   r  r:   r;   rr   r#  )r'   r   r  r}   r  bundled_by_namein_manifest
is_bundleddeleted_user_copyr   rq   r4  r3  r   s                 r   reset_bundled_skillr<    sG   0 H"$$K-k::N>**O("K(J 	
z 	
'ID I I I 
 
 	
   		+U U U U    &od&;[II;;== 	 &&&$(!!W% 	 	 	)M M M M M M #       	  "TN!!! t$$$F 
$ 
;t;;;	 	
TtTTT#W4 W W W 	
 &WOOOs   B   C	1C>C	C	r  r  c                 ,    t          |           o|| k    S )a  Whether an on-disk skill counts as a user modification ``hermes update`` keeps.

    Shared by the sync loop (which decides what to skip) and
    ``list_user_modified_bundled_skills`` (which surfaces the names) so the two
    can never drift. A skill is a tracked modification only when it has a
    recorded origin hash (an un-baselined / v1 entry with an empty hash is not)
    and its current content hash differs from that origin.
    )bool)r  r  s     r   r  r  g  s     9k!99r   c                     t                      } | sg S t                      }g }t          |          D ]y\  }}|                     |d          }|st	          ||          }|                                sCt          |t          |                    r|                    |||d           z|	                    d            |S )u  Return the bundled skills that ``hermes update`` keeps because the user
    edited them locally.

    A skill counts as user-modified when its on-disk copy no longer matches the
    origin hash recorded in the manifest the last time it was synced — the exact
    same test the sync loop uses to decide what to skip. This is the discovery
    half of that behavior, so a user can find the names the ``~ N user-modified
    (kept)`` notice only counts.

    Returns a list (sorted by name) of dicts:
        ``{"name": str, "dest": Path, "bundled_src": Path}``
    where ``dest`` is the user's copy and ``bundled_src`` is the current stock
    copy (so callers can diff or restore).
    r!   )r'   r   bundled_srcc                     | d         S )Nr'   r   )rq   s    r   r   z3list_user_modified_bundled_skills.<locals>.<lambda>  s
    &	 r   r   )
r@   r   r   r   r   r5   r  r   r   sort)r  r}   modifiedr   r,   r  r   s          r   !list_user_modified_bundled_skillsrD  s  s     H 	"$$KH!9+!F!F  
Ill:r22  	%i=={{}} 	(iooFF 	OO#T)LL   MM))M***Or   c                     	 |                                  }n# t          $ r Y dS w xY wd|v r|dfS 	 ||                    d          fS # t          $ r |dfcY S w xY w)zRead a file once for diffing.

    Returns ``(raw_bytes, text)`` where ``text`` is ``None`` if the file is
    binary; ``(None, None)`` if it could not be read. Returning the raw bytes
    lets the caller compare binary files without re-reading them.
    NN    Nr0   )r   r:   decodeUnicodeDecodeError)rH   rm   s     r   _read_for_diffrJ    s         zz$TzT[[))))   Tzs    
%%A AAc                 H   ddl }t                      }t          t          |                    }|                    |           }|d| ddg d|  ddS t          ||          }|                                sd| ddg d|  d	| d
dS t          t          |                    }t          t          |                    }g }t          ||z            D ]}	|	|v }
|	|v }|
rt          ||	z            nd\  }}|rt          ||	z            nd\  }}|
r|r|| ||k    r|                    |	ddd           e||k    rld                    |                    |                    d          |                    d          d|	 d|	                     }|                    |	d|d           |
r|                    |	dd|	 d           |                    |	dd|	 d           t          |          }d| d|||sd|  dnd|  dt!          |           ddS )u8  Diff a user's copy of a bundled skill against the current stock version.

    Lets a user see exactly what diverged before deciding whether to keep their
    edits or ``hermes skills reset`` back to upstream.

    Returns a dict:
        ``ok`` (bool), ``name`` (str), ``found`` (bool — bundled source exists),
        ``modified`` (bool), ``message`` (str),
        ``diffs``: list of ``{"path": str, "status": str, "diff": str}`` where
        status is one of ``modified`` / ``added`` (only in user copy) /
        ``removed`` (only in bundled) / ``binary``.
    r   NFr2  zv' is not a tracked bundled skill (no stock version to diff against). Hub-installed skills use `hermes skills inspect`.)r   r'   foundrC  diffsr   TzNo local copy of 'z' found at r   rF  binaryz<binary file differs>)rH   statusdiffr!   )keependszstock/zyours/)fromfiletofilerC  addedz+ only in your copy: removedz- only in stock: z' matches the stock version.z$' differs from the stock version in z	 file(s).)difflibr   r   r   r   r   r5   r$   r   r_   rJ  r   r^   unified_diffr7   r>  r  )r'   rV  r}   r8  r@  r   
user_filesstock_filesrM  r   in_userin_stock
user_bytes	user_textstock_bytes
stock_texttextrC  s                     r   diff_bundled_skillra    s4    NNN"$$K3K@@AAO!%%d++KTD T T T

 

 
	
 "+{;;D;;== 
DDDDTDDD
 
 	
 %d++,,J&{3344KEj;.// % %#+%*1CN4#:&&&| 	
I 2:KN;,---| 	 Z  	x 	 J$6 ,,LL!$BYZZ   J&&77$$))4)88(($(77+c^^)C>>	 %   D LL#TJJKKKK 	LL9VQT9V9VWW    LL	;Ts;T;TUU    E{{H U22222TTTTs5zzTTT  r   enabledc                    t           t          z  }|                                }	 | r=t                               dd           |                    dd           | }|rdnd}n|r|                                 |}|rdnd	}n1# t          $ r$}d
d
t          |          d| d| dcY d}~S d}~ww xY wd|t          |          |dS )aj  Toggle the .no-bundled-skills opt-out marker for the active profile.

    When ``enabled`` is True, writes HERMES_HOME/.no-bundled-skills so the
    installer, ``hermes update``, and any direct sync stop seeding bundled
    skills. When False, removes the marker so seeding resumes on the next
    sync. This is the on-disk-state half of ``hermes skills opt-out`` /
    ``opt-in``; removal of already-present skills is a separate, explicit
    step (see ``remove_pristine_bundled_skills``).

    Returns:
        dict with keys: ok (bool), changed (bool), marker (str path),
                        message (str).
    TrM   zThis profile opted out of bundled-skill seeding (`hermes skills opt-out`).
Delete this file to re-enable sync on the next `hermes update`.
r0   r1   zpOpted out of bundled skills. Future install / update / sync runs will not seed bundled skills into this profile.u1   Already opted out — marker was already present.zgOpted back in. The next `hermes update` (or `hermes skills opt-in --sync`) will re-seed bundled skills.u&   Not opted out — no marker to remove.Fz#Could not update opt-out marker at r	  )r   r   markerr   N)r  r  r5   r]   
write_textrj   r:   rb   )rb  rd  existedr   r   rq   s         r   set_bundled_skills_opt_outrg    sU    33FmmooG
 	dT:::T !	     "kG I G G I	 G   G > ? ? >	   
 
 
ES[[JVJJqJJ
 
 	
 	
 	
 	
 	
 	


 7c&kkgVVVs   AB 
B1B,&B1,B1c                  D    t           t          z                                  S )z=Return True if the active profile carries the opt-out marker.)r  r  r5   r   r   r   is_bundled_skills_opt_outri  @  s    22::<<<r   dry_runc                 l   t                      }t                      }t          t          |                    }g }g }t	          |                                          D ]\  }}|                    |          }||                    |dd           6t          ||          }	|		                                s
| s||v r||= dt          |	          }
|
|k    r|                    |dd           | r|                    |           	 t          |	           n9# t          t          f$ r%}|                    |d| d           Y d}~d}~ww xY w||v r||= |                    |           | s|rt          |           | rdnd}| dt          |           d	t          |           d
}d||| |dS )a  Delete bundled skills that are present, manifest-tracked, AND unmodified.

    Safety is the whole point of this function. A skill on disk is removed
    ONLY when all of these hold:
      - it is recorded in the sync manifest (so it is genuinely a bundled
        skill, not a hub-installed or hand-written one), AND
      - it still exists in the bundled source (so we can hash-compare), AND
      - its on-disk copy is byte-identical to the manifest origin hash
        (so the user has not edited it).

    Anything user-modified, hub-installed, or locally authored is left
    untouched and reported under ``skipped``. The manifest entry for each
    removed skill is dropped so a later opt-in re-seed treats it as new.

    Args:
        dry_run: When True, compute what would be removed without deleting.

    Returns:
        dict with keys: ok (bool), removed (list[str]),
                        skipped (list[dict]) where each dict is
                        {name, reason}, dry_run (bool), message (str).
    Nz$no bundled source (removed upstream))r'   reasonzuser-modified (kept)zdelete failed: zWould removeRemoved z! pristine bundled skill(s); kept r   T)r   rU  r  rj  r   )r@   r   r   r   r_   r`   r   r   r   r5   r   r  r:   r;   rr   r  )rj  r  r}   r8  rU  r  r'   r  r   r   on_diskrq   verbr   s                 r   remove_pristine_bundled_skillsrq  E  s#   . H"$$K3K@@AAOGG#HNN$4$455  k!!$'';NND4Z[[\\\%c;77{{}} 	 #tx//TND//k!!NND4JKKLLL 	NN4   	T""""! 	 	 	NND4Ia4I4IJJKKKHHHH	 8t "w "!!!$3>>)DVVGVVs7||VVVGw7w  s   	DE*E

E__main__z1Syncing bundled skills into ~/.hermes/skills/ ...r   r   z newr   z updatedr  z
 unchangedr     z, z, +z morez user-modified (kept): r  z cleaned from manifestr  z official optional backfilledz
Done: z. r  z total bundled.)F)J__doc__r   r   loggingrc   r   r   r   pathlibr   r   hermes_constantsr   r   r	   r"   r
   typingr   r   r   r   r   utilsr   	getLogger__name__rk   r  rF   r4   r  r   r   rb   r.   r@   r$   rJ   rr   r(   r   r   r   r   r   r   r   r   r>  r   r   r   r#  r  r<  r  rD  bytesrJ  ra  rg  ri  rq  r   r<   r  r   rI   MAX_SHOWr^   shownr   r   r   r   r   <module>r     s>   ,    				  ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ] ] ] ] ] ] ] ] ] ] 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3            		8	$	$ o8#
00 0 K$ K K K KU4 U U U U
SX    :S#X    6    4`T#s(^ ` ` ` `Bt s s    *$ 4c4i8H3I    &d  $     #     T c     c    	$T 	$c 	$ 	$ 	$ 	$tCsC~)>$>?    6$ T c     CH D D D# D4 DD D D D DNZ Z Z$s) Z Z Z Zzm mt m m m m m`.+4 .+D .+ .+ .+ .+b`P `Pc `PD `PT `P `P `P `PF	:s 	:s 	:t 	: 	: 	: 	:"4: " " " "J %#(F"G    &`S `T ` ` ` `F/W /W /W /W /W /Wd=4 = = = =
C CD CT C C C CL z	E
=>>>[u%%%F3vh  &&&3vi !!+++)(((E
 o D'		%		*++3u::  733u::07777EE

BB5BBCCCi HF9-..FFFGGGzz233 fF#CDEEdddeee	E
QTYYu%%
Q
Q)@
Q
Q
QRRRRR' r   