
    i=                    l   d Z ddlm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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dZd	d
hZ ej        d          Zd4dZd4dZd5d6dZ d7dZ!d8dZ"d9dZ#d:dZ$d;d!Z%d<d=d$Z&d>d'Z'd?d)Z(d@d+Z)dAd.Z*d5dBd0Z+dCd2Z,dDd3Z-dS )Eu  Curator snapshot + rollback.

A pre-run snapshot of ``~/.hermes/skills/`` (excluding ``.curator_backups/``
itself) is taken before any mutating curator pass. Snapshots are tar.gz
files under ``~/.hermes/skills/.curator_backups/<utc-iso>/`` with a
companion ``manifest.json`` describing the snapshot (reason, time, size,
counted skill files). Rollback picks a snapshot, moves the current
``skills/`` tree aside into another snapshot so even the rollback itself
is undoable, then extracts the chosen snapshot into place.

The snapshot does NOT include:
  - ``.curator_backups/`` (would recurse)
  - ``.hub/`` (hub-installed skills — managed by the hub, not us)

It DOES include:
  - all SKILL.md files + their directories (``scripts/``, ``references/``,
    ``templates/``, ``assets/``)
  - ``.usage.json`` (usage telemetry — needed to rehydrate state cleanly)
  - ``.archive/`` (so rollback restores previously-archived skills too)
  - ``.curator_state`` (so rolling back also restores the last-run-at
    pointer — otherwise the curator would immediately re-fire on the next
    tick)
  - ``.bundled_manifest`` (so protection markers stay consistent)
    )annotationsN)datetimetimezone)Path)AnyDictListOptionalTupleget_hermes_home   .curator_backupsz.hubz/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z(-\d{2})?$returnr   c                 *    t                      dz  dz  S )Nskillsr   r        ;/home/longshao/.hermes/hermes-agent/agent/curator_backup.py_backups_dirr   :   s    x'*<<<r   c                 $    t                      dz  S )Nr   r   r   r   r   _skills_dirr   >   s    x''r   nowOptional[datetime]strc                   | t          j        t          j                  } |                     d                                          }|                    d          r
|dd         }|                    dd          dz   S )	z@UTC ISO-ish filesystem-safe timestamp: ``2026-05-01T13-05-42Z``.Nr   )microsecondz+00:00i:-Z)r   r   r   utcreplace	isoformatendswith)r   ss     r   _utc_idr&   B   sq    
{l8<(("",,..Azz( crcF99S#$$r   Dict[str, Any]c                 x   	 ddl m}   |             }n4# t          $ r'}t                              d|           i cY d }~S d }~ww xY wt          |t                    si S |                    d          pi }t          |t                    si S |                    d          pi }t          |t                    r|ni S )Nr   )load_configz,Failed to load config for curator backup: %scuratorbackup)hermes_cli.configr)   	Exceptionloggerdebug
isinstancedictget)r)   cfgecurbks        r   _load_configr7   M   s    111111kmm   CQGGG						 c4   	
'')


"Cc4   					 bBB%%-222-s    
A?AAboolc                 `    t          t                                          dd                    S )uB   Default ON — the whole point of the backup is safety by default.enabledT)r8   r7   r2   r   r   r   
is_enabledr;   ]   s$    ""9d33444r   intc                     t                      } 	 t          |                     dt                              }n# t          t
          f$ r
 t          }Y nw xY wt          d|          S )Nkeep   )r7   r<   r2   DEFAULT_KEEP	TypeError
ValueErrormax)r3   ns     r   get_keeprE   b   sd    
..C--..z"   q!99s   (9 AAbasec                ~    	 t          d |                     d          D                       S # t          $ r Y dS w xY w)Nc              3     K   | ]}d V  dS )r?   Nr   ).0_s     r   	<genexpr>z%_count_skill_files.<locals>.<genexpr>q   s"      551555555r   zSKILL.mdr   )sumrglobOSError)rF   s    r   _count_skill_filesrO   o   sR    55djj44555555   qqs   +. 
<<destreasonarchive_pathskills_countedNonec                   | j         |t          j        t          j                                                  |j         |                                j        |d}| dz                      t          j
        |dd          d           d S )N)idrQ   
created_atarchivearchive_bytesskill_filesmanifest.json   T)indent	sort_keysutf-8encoding)namer   r   r   r!   r#   statst_size
write_textjsondumps)rP   rQ   rR   rS   manifests        r   _write_manifestri   v   s     il8<00::<<$%**,,4% H 
O''
8A666 (     r   manualOptional[Path]c                r   t                      st                              d           dS t                      }|                                st                              d           dS t                      }	 |                    dd           n4# t          $ r'}t                              d||           Y d}~dS d}~ww xY wt                      }|}d}||z                                  r$| d|d	}|dz  }||z                                  $||z  }	 |                    dd
           n4# t          $ r'}t                              d||           Y d}~dS d}~ww xY w|dz  }	 t          j
        |dd          5 }	t          |                                          D ];}
|
j        t          v r|	                    t!          |
          |
j        d           <	 ddd           n# 1 swxY w Y   t#          || |t%          |                     ni# t          t          j        f$ rP}t                              d|d           	 t)          j        |d           n# t          $ r Y nw xY wY d}~dS d}~ww xY wt-          t/                                 t                              d||            |S )uY  Create a tar.gz snapshot of ``~/.hermes/skills/`` and prune old ones.

    Returns the snapshot directory path, or ``None`` if the snapshot was
    skipped (backup disabled, skills dir missing, or an IO error occurred —
    in which case we log at debug and return None so the curator never
    aborts a pass because of a backup failure).
    z4Curator backup disabled by config; skipping snapshotNu5   No ~/.hermes/skills/ directory — nothing to back upTparentsexist_okz#Failed to create backups dir %s: %sr?   r   02dFz$Failed to create snapshot dir %s: %sskills.tar.gzzw:gz   )compresslevel)arcname	recursivezCurator snapshot failed: %s)exc_infoignore_errors)r>   z!Curator snapshot created: %s (%s))r;   r.   r/   r   existsr   mkdirrN   r&   tarfileopensortediterdirrb   _EXCLUDE_TOP_LEVELaddr   ri   rO   TarErrorshutilrmtree
_prune_oldrE   info)rQ   r   backupsr4   base_idsnap_idcounterrP   rX   tfentrys              r   snapshot_skillsr      s`    << KLLLt]]F==?? LMMMtnnGdT2222   :GQGGGttttt iiGGGW
$
$
&
& ,,w,,,1 W
$
$
&
&  WD

4%
0000   ;T1EEEttttt _$G\'6;;; 	Gr 0 011 G G:!333 s5zz5:FFFFG	G 	G 	G 	G 	G 	G 	G 	G 	G 	G 	G 	G 	G 	G 	G 	fg/A&/I/IJJJJW%&   2AEEE	M$d33333 	 	 	D	ttttt HJJ
KK3WfEEEKs   8B 
CB<<CD/ /
E 9EE )H  AG+H +G//H 2G/3"H I<,I7
I! I7!
I.+I7-I..I77I<r>   	List[str]c                6   t                      }|                                sg S g }g }|                                D ]}|                                s|j                            d          r|                    |           Gt                              |j                  r|                    |j        |f           |	                    d d           g }|| d         D ]g\  }}	 t          j        |           |                    |j                   5# t          $ r&}t                              d||           Y d}~`d}~ww xY w|D ]J}	 t          j        |           # t          $ r&}t                              d||           Y d}~Cd}~ww xY w|S )zDelete regular snapshots beyond the newest *keep*. Returns deleted
    ids. Staging dirs (``.rollback-staging-*``) are implementation detail
    and pruned independently on every call..rollback-staging-c                    | d         S )Nr   r   )ts    r   <lambda>z_prune_old.<locals>.<lambda>   s
    qt r   T)keyreverseNzFailed to prune %s: %sz(Failed to clean stale staging dir %s: %s)r   ry   r~   is_dirrb   
startswithappend_ID_REmatchsortr   r   rN   r.   r/   )	r>   r   entriesstale_stagingchilddeletedrJ   pathr4   s	            r   r   r      s    nnG>> 	&(G "M"" 
0 
0||~~ 	:  !566 	   '''<<
## 	0NNEJ.///LL^^TL222G455> < <4	<M$NN49%%%% 	< 	< 	<LL14;;;;;;;;	< N N	NM$ 	N 	N 	NLLCT1MMMMMMMM	NNs0   *.D
E	#EE	E&&
F0FFsnap_dirc                    | dz  }|                                 si S 	 t          j        |                    d                    S # t          t          j        f$ r i cY S w xY w)Nr[   r_   r`   )ry   rf   loads	read_textrN   JSONDecodeError)r   mfs     r   _read_manifestr      sp    	O	#B99;; 	z",,,88999T)*   			s   'A A A List[Dict[str, Any]]c                 t   t                      } |                                 sg S g }t          |                                 d          D ]}|                                st
                              |j                  s7|dz                                  sOt          |          }|	                    d|j                   |	                    dt          |                     d|vr8|dz  }	 |                                j        |d<   n# t          $ r d|d<   Y nw xY w|                    |           |S )u   Return all restorable snapshots, newest first. Only entries with a
    real ``skills.tar.gz`` tarball are listed — transient
    ``.rollback-staging-*`` directories created mid-rollback are
    implementation detail and not shown.Tr   rq   rV   r   rY   r   )r   ry   r}   r~   r   r   r   rb   r   
setdefaultr   rc   rd   rN   r   )r   outr   r   arcs        r   list_backupsr      sG   
 nnG>> 	 "C))4888  ||~~ 	||EJ'' 	'//11 	E""
dEJ'''
fc%jj)))"$$/)C(&)hhjj&8?## ( ( (&'?###(

2Js   0DDD	backup_idOptional[str]c                \   t                      }|                                sdS | rN|| z  }|                                r3t                              |           r|dz                                  r|S dS d t          |                                d          D             }|r|d         ndS )zpReturn the path of the requested backup, or the newest one if
    *backup_id* is None. Returns None if no match.Nrq   c                    g | ]N}|                                 t                              |j                  5|d z                                  L|OS )rq   )r   r   r   rb   ry   )rI   cs     r   
<listcomp>z#_resolve_backup.<locals>.<listcomp>&  se       88:: ,,qv..454G3O3O3Q3Q	  r   Tr   r   )r   ry   r   r   r   r}   r~   )r   r   target
candidatess       r   _resolve_backupr     s     nnG>> t 9$MMOO	Y''	 /)1133	
 Mt '//++T:::  J '0:a==D0r    Tuple[bool, str, Optional[Path]]c                   t          |           }|dd| rd|  dndz   dz   dfS |dz  }|                                sdd	|j         d
dfS t                      }|                    dd           t                      }|                    dd           	 t          d|j                    n# t          $ r}dd| dfcY d}~S d}~ww xY w|dt                       z  }	 |                    dd           n# t          $ r}dd| dfcY d}~S d}~ww xY wg }	 t          |                                          D ]a}|j        t          v r||j        z  }	t          j        t          |          t          |	                     |                    ||	f           bn# t          $ r}|D ]E\  }
}		 t          j        t          |	          t          |
                     6# t          $ r Y Bw xY w	 t          j        |d           n# t          $ r Y nw xY wdd| dfcY d}~S d}~ww xY w	 t%          j        |d          5 }|                                D ]K}|j        }|                    d          sdt-          |          j        v rt%          j        d|          L	 |                    t          |          d           n2# t4          $ r% |                    t          |                     Y nw xY wddd           n# 1 swxY w Y   n# t          t$          j        f$ r}|D ]E\  }
}		 t          j        t          |	          t          |
                     6# t          $ r Y Bw xY w	 t          j        |d           n# t          $ r Y nw xY wdd| dfcY d}~S d}~ww xY w	 t          j        |d           n# t          $ r Y nw xY wt6                              d|j                   dd|j         |fS )ao  Restore ``~/.hermes/skills/`` from a snapshot.

    Strategy:
      1. Resolve the target snapshot (explicit id or newest regular).
      2. Take a safety snapshot of the CURRENT skills tree under
         ``.curator_backups/pre-rollback-<ts>/`` so the rollback itself is
         undoable.
      3. Move all current top-level entries (except ``.curator_backups``
         and ``.hub``) into a tempdir.
      4. Extract the chosen snapshot into ``~/.hermes/skills/``.
      5. On failure during 4, move the tempdir contents back (best-effort)
         and return failure.

    Returns ``(ok, message, snapshot_path)``.
    NFzno matching backup foundz	 for id '' zB (use `hermes curator rollback --list` to see available snapshots)rq   z	snapshot u$    has no skills.tar.gz — corrupted?Trm   zpre-rollback to )rQ   z%pre-rollback safety snapshot failed: r   zfailed to create staging dir: rw   z failed to stage current skills: zr:gz/z..z!refusing to extract unsafe path: data)filterz*snapshot extract failed (state restored): z"Curator rollback: restored from %szrestored from snapshot )r   ry   rb   r   rz   r   r   r-   r&   rN   listr~   r   r   mover   r   r   r{   r|   
getmembersr   r   partsr   
extractallrA   r.   r   )r   r   rX   r   r   r4   stagedmovedr   rP   origr   memberrb   s                 r   rollbackr   -  s     Y''F~'+4<'9''''">RS 
 	
 &G>> \T6;TTTVZ[[]]F
LLL---nnGMM$M...
J?&+??@@@@@ J J JBqBBDIIIIIIIJ 7GII777FCTE2222 C C C;;;TBBBBBBBC &(EE&..**++ 	( 	(Ez///EJ&DKE

CII...LL%''''	(  E E E 	 	JD$CIIs4yy1111   	M&55555 	 	 	D	=!==tDDDDDDDEO\'6** 	+b --//  {??3'' 44::3C+C+C!*DDDD   ,D+c&kk&9999 + + +c&kk*****+	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ W%& O O O 	 	JD$CIIs4yy1111   	M&55555 	 	 	D	GAGGNNNNNNNOfD11111    KK4fkBBB9FK996BBsd  B0 0
C:CCC$C< <
DDDDBF" "
H.,H)4/G$#H)$
G1.H)0G11H)5HH)
HH)H
H)#H.)H.2L A!L)$KL,K=:L<K==L L LL LL N0.N+6/M&%N+&
M30N+2M33N+7NN+
NN+N
N+%N0+N04O 
OOrD   c                d    dD ](}| dk     s|dk    r|dk    r| dd| n|  dc S | dz  } )| ddS )	N)BKBMBGBi   r   r   z.1f z Bz GBr   )rD   units     r   format_sizer     sk    '  t88tt||(,a$$$d$$$AAAA	T	====r   c                    t                      } | sdS dddddddddd	d
g}|                    dt          |d                   z             | D ]}|                    |                    dd          dd|                    dd          pdd d         dd|                    dd          ddt	          t          |                    dd                              d
           d                    |          S )NzNo curator snapshots yet.rV   z<24z  rQ   z<40r   z>6sizez>8u   ─r   ?(   rZ   rY   
)r   r   lenr2   r   r<   join)rowslinesrs      r   summarize_backupsr     sH   >>D +**FFFHFFFHFFF6FFFGE	LLU1X&''' 
 
uuT#& @ @hs##*sCRC08@ @uu]A&&-@ @ 3quu_a8899::?@ @	
 	
 	
 	
 99Ur   )r   r   )N)r   r   r   r   )r   r'   )r   r8   )r   r<   )rF   r   r   r<   )
rP   r   rQ   r   rR   r   rS   r<   r   rT   )rj   )rQ   r   r   rk   )r>   r<   r   r   )r   r   r   r'   )r   r   )r   r   r   rk   )r   r   r   r   )rD   r<   r   r   )r   r   ).__doc__
__future__r   rf   loggingosrer   r{   tempfiletimer   r   pathlibr   typingr   r   r	   r
   r   hermes_constantsr   	getLogger__name__r.   r@   r   compiler   r   r   r&   r7   r;   rE   rO   ri   r   r   r   r   r   r   r   r   r   r   r   <module>r      sq   2 # " " " " "   				 				     ' ' ' ' ' ' ' '       3 3 3 3 3 3 3 3 3 3 3 3 3 3 , , , , , ,		8	$	$ 
 )&1 
 
F	G	G= = = =( ( ( (% % % % %. . . . 5 5 5 5
         ? ? ? ? ?D" " " "R      :1 1 1 1,oC oC oC oC oCl        r   