
    epjx                       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
Z
ddlmZ ddlmZmZ ddlmZ ddlmZmZmZmZ ddlmZmZmZ  ej        e          Zh dZd	Zh d
Zh dZ h dZ!dZ" G d de#          Z$ G d de#          Z%ed]dede&fd            Z'edefd            Z(dee         fdZ)dedee         fdZ*dede+fdZ,dededede+fd Z-d!ed"ede+fd#Z.d$d%d&d'ed(e/d)e+de+fd*Z0d+Z1d,Z2d-d-e2d.d'ed/e+d0e+d1e/de3f
d2Z4d!ed"ede+fd3Z5d4e/de6fd5Z7d^d6Z8d7eddfd8Z9d9e
j:        de;e+e6f         fd:Z<d9e
j:        de6fd;Z=d^d<Z>d=Z?d>Z@d?ZAd_dee         defd@ZB	 	 	 	 d`dAee6         dee         dBee/         dCee/         dee6         f
dDZC	 	 	 	 d`dAee6         dee         dBee/         dCee/         dee6         f
dEZD	 	 dadFe/dee         deee6ef                  fdGZE	 d_dHe6dee         de+fdIZFdJZGd'edee/         fdKZH	 d_dHe6dee         deee6ef                  fdLZIeAfdMedBe/de/fdNZJeAdfdBe/dee         de/fdOZKd^dPZLded7edee         fdQZMded7edee         fdRZNdSZOdTZPdUZQd_dee         defdVZRdWedBe/de/fdXZSdeQfdee         dBe/dee         fdYZTdZZUdUZVdWedBe/de/fd[ZWdeVfdee         dBe/dee         fd\ZXdS )ba  
Backup and import commands for hermes CLI.

`hermes backup` creates a zip archive of the entire ~/.hermes/ directory
(excluding the hermes-agent repo and transient files).

`hermes import` restores from a backup zip, overlaying onto the current
HERMES_HOME root.
    N)contextmanager)datetimetimezone)Path)AnyDictListOptional)get_default_hermes_rootget_hermes_homedisplay_hermes_home>   .git.nox.tox.venv.cache.mypy_cache.ruff_cache.pytest_cachesite-packagesvenvbackups__pycache__checkpointsnode_moduleshermes-agent)z.pycz.pyoz.db-walz.db-shmz.db-journal>   cron.pidgateway.pid.backup.lock>   gateway.lockr   r   processes.jsongateway_state.json>   .envstate.db	auth.jsonz
_external/c                       e Zd ZdZdS )BackupInProgressErrorz@Raised when another process already owns the Hermes backup slot.N)__name__
__module____qualname____doc__     7/home/thesage/.hermes/hermes-agent/hermes_cli/backup.pyr'   r'      s        JJJJr-   r'   c                       e Zd ZdS )_SQLiteSnapshotErrorN)r(   r)   r*   r,   r-   r.   r0   r0      s        Dr-   r0         ?hermes_hometimeout_secondsc              #     K   | dz  }|j                             dd           |                    d          }d}t          j                    t          d|          z   }	 t          j        dk    rdd	l}|	                                j
        dk    r)|                    d
           |                                 	 	 |                    d           |                    |                                |j        d           d}nR# t"          t$          f$ r= t          j                    |k    rt'          d          t          j        d           Y nw xY wndd	l}	 	 |                    |                                |j        |j        z             d}nR# t2          t"          f$ r= t          j                    |k    rt'          d          t          j        d           Y nw xY wd	V  |r	 t          j        dk    rHdd	l}|                    d           |                    |                                |j        d           n1dd	l}|                    |                                |j                   n# t"          t$          f$ r Y nw xY w|                                 d	S # |r	 t          j        dk    rHdd	l}|                    d           |                    |                                |j        d           n1dd	l}|                    |                                |j                   n# t"          t$          f$ r Y nw xY w|                                 w xY w)zCAcquire one cross-process backup slot for full and quick snapshots.r   Tparentsexist_okza+bFg        ntr   N       z(another Hermes backup is already runningg?)parentmkdiropentime	monotonicmaxosnamemsvcrtstatst_sizewriteflushseeklockingfilenoLK_NBLCKOSErrorPermissionErrorr'   sleepfcntlflockLOCK_EXLOCK_NBBlockingIOErrorLK_UNLCKLOCK_UNclose)r2   r3   	lock_pathhandleacquireddeadlinerC   rO   s           r.   _backup_operation_lockr[      sc      n,I4$777^^E""FH~#c?";";;H-7d??MMM~~'1,,T"""	%%KKNNNNN6==??FOQGGG#H1 % % %~''83334^___Jt$$$$$%	%
  LLL%%KK1NOOO#H'1 % % %~''83334^___Jt$$$$$%% 	 	7d??!MMMKKNNNNN6==??FOQGGGG LLLKK???_-     	7d??!MMMKKNNNNN6==??FOQGGGG LLLKK???_-   s    AJ# <AD J# AEJ# E
J# 7F J# AG!J#  G!!J# ,B	I6 6J
	J
#M'B	L10M1MMMM
final_pathc           	   #   @  K   |                      d| j         dt          j                     dt	          j                     d          }|                    d           	 |V  t          j        ||            dS # t          $ r |                    d            w xY w)zDYield a hidden sibling path and publish it only after a clean close..-.partialT
missing_okN)		with_namerB   rA   getpid	threading	get_identunlinkreplaceBaseException)r\   partial_paths     r.   _atomic_output_pathrk      s       ''KJOKKbikkKKI,?,A,AKKK L 4(((

<,,,,,   t,,,s    A; ;"Breturnc                     	 ddl m} m} n# t          $ r g cY S w xY w	  |             }n# t          $ r d}Y nw xY w|sg S 	  ||          }n# t          $ r d}Y nw xY w|g S 	 |                                pg }n5# t          $ r(}t
                              d||           g cY d}~S d}~ww xY wg }t                      }|D ]}	 t          |          	                                }	n# t          $ r Y 1w xY w|	
                                sJ	 |	                                }
n# t          t          f$ r Y sw xY w|
|v r||                    |
           |                    |	           |S )u  Return existing absolute paths the active memory provider stores
    outside HERMES_HOME, resolved from config only (no network, no init).

    Reads ``memory.provider`` from config, loads just that provider, and asks
    it for ``backup_paths()``. Returns an empty list when no external provider
    is active or the provider can't be loaded — backup must never fail because
    of a flaky plugin.
    r   )_get_active_memory_providerload_memory_providerNz0backup_paths() failed for memory provider %r: %s)plugins.memoryrn   ro   	Exceptionbackup_pathsloggerwarningsetr   
expanduserexistsresolverL   
ValueErroraddappend)rn   ro   activeproviderdeclaredexcoutseenrawpresolveds              r.   '_collect_memory_provider_external_pathsr      s   TTTTTTTTT   			,,..    	''//   	((**0b   I6SVWWW						 CD  	S		$$&&AA 	 	 	H	xxzz 		yy{{HH$ 	 	 	H	t

1Jsl    
) 88 A AA#A: :
B,B'!B,'B,!C&&
C32C3D!!D54D5basec                    g }|                                  r+|                                 s|                    |            |S |                                 s|S t	          j        | d          D ]\  }}}t          |          }d |D             |dd<   |D ]_}||z  }|                                r|j        t          v s|j        	                    t                    rJ|                    |           `|S )zYield regular files under *base* (a file or a directory), skipping
    symlinks, caches, and pyc files. *base* itself may be a file.Ffollowlinksc                 $    g | ]}|t           v|S r,   _EXCLUDED_DIRS.0ds     r.   
<listcomp>z(_iter_external_files.<locals>.<listcomp>  s"    FFFQa~.E.Eq.E.E.Er-   N)is_file
is_symlinkr{   is_dirrA   walkr   rB   _EXCLUDED_NAMESendswith_EXCLUDED_SUFFIXES)r   filesdirpathdirnames	filenamesdpfnamefpaths           r.   _iter_external_filesr     s    E||~~ doo// T;;== (*%(H(H(H 	  	 $9']]FF(FFF 	  	 EJE!! z_,,
0C0CDV0W0W,LL	  Lr-   rel_pathc                     | j         }|D ]!}|t          vr|dk    r||d         k    r dS | j        }|t          v rdS |                    t
                    rdS dS )zFReturn True if *rel_path* (relative to hermes root) should be skipped.r   r   TF)partsr   rB   r   r   r   )r   r   partrB   s       r.   _should_excluder   %  s    NE  ~%% >!!deAh&6&6tt=Dt}}'(( t5r-   abs_pathout_pathc                     t          |          rdS |                                 rdS 	 |                                 |                                k    S # t          t          f$ r Y dS w xY w)zHReturn True when a candidate file should not be written to a backup zip.TF)r   r   rx   rL   ry   )r   r   r   s      r.   _should_skip_backup_filer   >  s    x   t  t!!X%5%5%7%777Z    uus   )A A('A(srcdstc                    d}d}	 t          j        d|  dd          }t          j        t          |                    }|                    |           	 ||fD ]*}|&	 |                                 # t
          $ r Y &w xY w+dS # t
          $ r~}t                              d| |           	 |                    d           n# t          $ r Y nw xY wY d}~||fD ]*}|&	 |                                 # t
          $ r Y &w xY w+dS d}~ww xY w# ||fD ]*}|&	 |                                 # t
          $ r Y &w xY w+w xY w)	u  Copy a SQLite database safely using the backup() API.

    Handles WAL mode — produces a consistent snapshot even while
    the DB is being written to. Fail closed if a consistent snapshot cannot
    be created: copying only the live main file can omit committed WAL data.
    Nfile:?mode=roT)uriz"SQLite safe copy failed for %s: %sra   F)
sqlite3connectstrbackuprV   rq   rs   rt   rg   rL   )r   r   connbackup_conn
connectionr   s         r.   _safe_copy_dbr   R  s    DK4s444$???oc#hh//K    '- 	 	J%$$&&&&    D &	 	    ;S#FFF	JJ$J'''' 	 	 	D	uuu&- 	 	J%$$&&&&    D &	 	 '- 	 	J%$$&&&&    D &	s   AB  A55
BB
DD/CD
CDCDD #C88
DDDD 	ED21E2
D?	<E>D?	?Ed   F)probe_bytesforcepathr   r   c                   	 |                                  j        }n# t          $ r Y dS w xY w|dk    rdS ddlm}  || t          d|          |          }|sdS |                    d          rdS t          d |D                       S )a/  True when *path* looks like the #68474 zeroed-state.db signature.

    Signature: size > 0, first *probe_bytes* are all NUL (no ``SQLite format 3``
    header). Used at SessionDB open and for snapshot diagnostics so a silent
    all-zero file becomes a guided recovery instead of a generic failure.
    Fr   read_header_bytes_preopen   )lengthr   s   SQLite format 3c              3   "   K   | ]
}|d k    V  dS )r   Nr,   )r   bytes     r.   	<genexpr>z(is_zeroed_sqlite_file.<locals>.<genexpr>  s&      **Ttqy******r-   )rD   rE   rL   hermes_cli.sqlite_safe_readr   r@   
startswithall)r   r   r   sizer   heads         r.   is_zeroed_sqlite_filer   p  s    yy{{"   uuqyyuEEEEEE$$S[))  D  u)** u**T******s    
**s   SQLite format 3 l        T)check_header
run_pragma	max_bytesr   r   r   c                ~   dddd}	 |                                  }n6# t          $ r d|  |d<   |cY S t          $ r}d| |d<   |cY d}~S d}~ww xY w|j        |d<   |j        d	k     rd
|j         d|d<   |S |dk    o
|j        |k    }|rhddlm}  || t          t                              }	|	d|d<   d|d<   |S |	t          k    r*d|d<   d|	dd                                         d|d<   |S |rzd}d}
	 t          j
        d|  ddd          }
|
                    d                                           |
                    d                                           d|d<   d|j        dd|dd|d<   n# t          j        $ rA}d|d<   d| |d<   |cY d}~|
&	 |
                                 S # t          $ r Y S w xY wS d}~wt          $ rA}d|d<   d | |d<   |cY d}~|
&	 |
                                 S # t          $ r Y S w xY wS d}~ww xY w	 |
&	 |
                                 n># t          $ r Y n2w xY wn-# |
&	 |
                                 w # t          $ r Y w w xY ww xY w|rd}	 t          j
        d|  ddd          }|                    d!          }|                                }t          |          d"k    rF|d         d         d#k    r4d|d<   d$|d<   ||&	 |                                 S # t          $ r Y S w xY wS d% |D             }d&d'                    |dd(                    |d<   ||&	 |                                 S # t          $ r Y S w xY wS # t          j        $ r<}d)| |d<   |cY d}~|&	 |                                 S # t          $ r Y S w xY wS d}~wt          $ r<}d*| |d<   |cY d}~|&	 |                                 S # t          $ r Y S w xY wS d}~ww xY w# |&	 |                                 w # t          $ r Y w w xY ww xY wd|d<   |d         sd+|d<   |S ),uW  Verify that a SQLite database at *path* is intact.

    Checks, in order:
      1. File exists and has an expected minimum size.
      2. SQLite header magic bytes are present.
      3. For files at or under ``max_bytes``, a read-only
         ``PRAGMA integrity_check``. For larger files, a cheap structural
         probe (schema read) instead — see ``max_bytes``.

    Args:
        path: Path to the database file.
        check_header: When true (default), verify the SQLite header magic.
        run_pragma: When true (default), run ``PRAGMA integrity_check`` via
            a read-only connection and verify the result is ``"ok"``.
        max_bytes: Size ceiling for the full ``PRAGMA integrity_check``.
            Files larger than this fall back to the header check plus a
            cheap structural probe, because ``integrity_check`` pages
            through the ENTIRE file — minutes of silent pegged CPU on a
            multi-GB database. Defaults to
            :data:`DEFAULT_INTEGRITY_CHECK_MAX_BYTES` (2 GiB); pass ``0``
            to force the full check regardless of size.

    Returns:
        A dict with keys:
          - ``valid`` (bool): true when all requested checks passed.
          - ``message`` (str): human-readable outcome or error detail.
          - ``size`` (int | None): file size in bytes, or None if stat failed.
    F N)validmessager   znot found: r   zcannot stat: r   r   ztoo small (z% bytes) to be a valid SQLite databaser   r   )r   r   zcannot read headerz!missing SQLite header magic (got r   )r   r   Tg      ?)r   timeoutzPRAGMA schema_versionz"SELECT count(*) FROM sqlite_masterzsize ,z bytes exceeds max_bytes z?; skipped PRAGMA integrity_check (header + schema probe passed)zschema probe failed: zschema probe error: zPRAGMA integrity_checkr:   okzintegrity check passedc                 8    g | ]}t          |d                    S )r   )r   )r   rs     r.   r   z+verify_sqlite_integrity.<locals>.<listcomp>  s"    ...Ac!A$ii...r-   zintegrity check failed: z;    zcannot open database: zintegrity check error: zheader check passed)rD   FileNotFoundErrorrL   rE   r   r   len_SQLITE_HEADERhexr   r   executefetchoneDatabaseErrorrV   rq   fetchalljoin)r   r   r   r   resultstr   	oversizedr   r   prober   cursorrowserrorss                  r.   verify_sqlite_integrityr     s   F #r4@@FYY[[   0$00y   1C11y ZF6N	zC["*[[[yA8"*y"8I 
 	JIIIII((c.6I6IJJJ<#F7O 4F9M>!!#F7OHD"IMMOOHHH 9 M   
	O$:D$:$:$:cRRREMM122;;===MM>??HHJJJ"F7OP
N P PyN P P P 9 $ 	 	 	#F7O = = =F9MMMMM  KKMMMM    D !  	 	 	#F7O <s < <F9MMMMM KKMMMM    D !	   KKMMMM    D !u KKMMMM    D !  	?#94#9#9#9tSQQQD\\":;;F??$$D4yyA~~$q'!*"4"4"&w$<y! JJLLLL    D   /....F R499VBQBZ;P;P R RF9 JJLLLL    D   $ 	 	 	 > > >F9MMMMM
 JJLLLL    D  	  	 	 	 ?# ? ?F9MMMMMJJLLLL    D  		 JJLLLL    D   F7O) 21yMs   A	A	AAA;BE= <I =HGHI "F77
GGHH#H$I +H  
HHHI H2 2
H?>H?I-II-
I)&I-(I))I-6A4M, -L
LL0M, M
M('M(,O;;	N2O;O> N!!
N.-N.2O;?	O6O;	O> O%%
O21O26O;;O> >P(PP(
P$!P(#P$$P(c                 (   t          | |          sdS t          |d          }|                    d          sY	 |                    d           n# t          $ r Y nw xY wt
                              d| |                    d                     dS dS )u  Like :func:`_safe_copy_db` but verifies the destination after copy.

    Returns True only when the copy succeeded AND the destination is valid
    SQLite (header + integrity check). Verification honours the default
    size ceiling — a multi-GB destination gets the header + schema probe
    rather than a full ``PRAGMA integrity_check`` that would page through
    the whole file.
    FT)r   r   ra   z.Backup of %s failed integrity verification: %sr   )r   r   getrg   rL   rs   rt   )r   r   	integritys      r.   copy_db_and_verifyr   '  s     c"" u'===I==!! 	JJ$J'''' 	 	 	D	Gimm\eNfNfgggu4s   A 
AAnbytesc                 \    dD ]$}| dk     r|dk    r| dd| n|  d| c S | dz  } %| ddS )zHuman-readable file size.)BKBMBGBi   r   .1f z TBr,   )r   units     r.   _format_sizer   A  so    '  D==-1S[[f)))4)))>P>P$>P>PPPP$r-   c                 p   t                      }|                                s&t          d|            t          j        d           	 t          |          5  t          | |           ddd           dS # 1 swxY w Y   dS # t          $ r'}t          d|            t          d          |d}~ww xY w)z1Create a zip backup of the Hermes home directory.z*Error: Hermes home directory not found at r:   NError:    )	r   r   printsysexitr[   _run_backup_lockedr'   
SystemExit)argshermes_rootr   s      r.   
run_backupr   J  s   )++K H;HHIII%#K00 	2 	2t[111	2 	2 	2 	2 	2 	2 	2 	2 	2 	2 	2 	2 	2 	2 	2 	2 	2 	2  % % %ooomm$%s<   
B A7*B 7A;;B >A;?B 
B5"B00B5r   c           
        ) | j         r|t          | j                                                                                   }|                                r/t          j                                        d          }|d| dz  }n@t          j                                        d          }t          j                    d| dz  }|j	        
                                dk    r|                    |j	        dz             }|j                            dd           t          j                    }t                               d           t%          dt'                       d           g }t)                      }t+          j        |d	
          D ]\  }}}	t          |          }
|
                    |          }|t          d          k    )|dd         })fd|D             |dd<   t)          |          t)          |          z
  D ]'}|                    t3          ||z                       (|	D ]E}|
|z  }|                    |          }t5          |||          r.|                    ||f           Ft          j                                                    }g }g }t9                      D ]}	 |                                }|                    |           n9# t:          t<          f$ r% |                    t3          |                     Y bw xY wt?          |          D ]u}	 |                                                    |          }n# t:          t<          f$ r Y >w xY wt@          |!                                z   }|                    ||f           v|sE|sCt                               dt          j                    |z
  dz             t%          d           dS tE          |          tE          |          z   }t                               dt          j                    |z
  dz  |           t                               d|           t%          d| d           d}g }t          j                    }tG          |          5 }tI          j%        |dtH          j&        d          5 }tO          |d          D ]\  }\  }} 	 |j	        dk    rtQ          j)        dd	t3          |j                            5 }!t          |!j*                  }"ddd           n# 1 swxY w Y   tW          ||"          rW|,                    |"t3          |                      ||"-                                j.        z  }|"/                    d           nq|"/                    d           |                    d|  d           |,                    |t3          |                      ||-                                j.        z  }n@# t`          t<          t:          f$ r&}#|                    d|  d |#            Y d}#~#{d}#~#ww xY w|d!z  dk    r2t%          d| d"| d           t                               d#||           |D ]x\  }}	 |,                    ||           ||-                                j.        z  }:# t`          t<          t:          f$ r%}#|                    d| d |#            Y d}#~#qd}#~#ww xY w	 ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   t          j                    |z
  }$|-                                j.        }%t                               d$|$dz  |tE          |          |%           t%                       |rt%          d%|            nt%          d&|            t%          d'|            t%          d(tc          |                      t%          d)tc          |%                      t%          d*|$d+d,           |r/t%          d-tE          |           d.t'                       d           |rLt%          d/tE          |           d0           te          |          dd1         D ]}&t%          d2|&            |r4t%          d3           te          |          D ]}'t%          d2|' d"           |rrt%          d4tE          |           d5           |dd1         D ]}(t%          |(           tE          |          d1k    r#t%          d6tE          |          d1z
   d7           |st%          d8|j*                    dS dS )9z@Write a full backup while the cross-process backup slot is held.%Y-%m-%d-%H%M%Szhermes-backup-.zipTr5   z backup phase=scan status=startedz	Scanning z ...Fr   r^   Nc                 6    g | ]}|t           vs|d k    |S )r   r   )r   r   is_roots     r.   r   z&_run_backup_locked.<locals>.<listcomp>  s=     
 
 
&&1+>+>w+> +>+>+>r-   z/backup phase=scan status=empty duration_ms=%.1f  zNo files to back up.z;backup phase=scan status=complete duration_ms=%.1f files=%dz,backup phase=archive status=started files=%dzBacking up 
 files ...r   w   compresslevelr:   .dbsuffixdeletedirarcnamera     z: SQLite safe copy failed:   /z:backup phase=archive status=progress completed=%d total=%dzQbackup phase=archive status=complete duration_ms=%.1f files=%d errors=%d bytes=%dzBackup incomplete: zBackup complete: z  Files:       z  Original:    z  Compressed:  z  Time:        r   sz
  Included z( memory-provider file(s) stored outside z
  Skipped zD memory-provider path(s) outside your home directory (not portable):
       z
  Excluded directories:
  Warnings ( files skipped):
  ... and  morez
Restore with: hermes import )3outputr   rv   rx   r   r   nowstrftimehomer  lowerwith_suffixr;   r<   r>   r?   rs   infor   r   ru   rA   r   relative_torz   r   r   r{   r   ry   rL   r   _EXTERNAL_PREFIXas_posixr   rk   zipfileZipFileZIP_DEFLATED	enumeratetempfileNamedTemporaryFilerB   r   rF   rD   rE   rg   rM   r   sorted)*r   r   r   stampscan_startedfiles_to_addskipped_dirsr   r   r   r   rel_dirorig_dirnamesremovedr   r   relhome_direxternal_to_addskipped_externalr   base_resolvedrel_to_homer  
file_counttotal_bytesr   t0archive_pathzfir   r   tmptmp_dbr   elapsedzip_sizer   r   er  s*                                            @r.   r   r   Z  s    { >$$//1199;;?? 	?LNN++,=>>E">5">">">>H''(9::9;;!=%!=!=!== &((''&(@AA O$666 >##L
KK2333	
1)++
1
1
1222,.L55L(*(O(O(O . .$9']]..--
 T#YY& 
 
 
 

 
 
 =))CMM9 	5 	5GS7!2334444 	. 	.EJE##K00C'sH== ----	. y{{""$$H.0O"$799 5 5	 LLNNM%%h////G$ 	 	 	##CII...H	 *$// 	5 	5E#mmoo99(CC(   &)=)=)?)??G""E7#34444	5   =^,4	
 	
 	
 	$%%% \""S%9%99J
KKE			L	(D0  
 KK>
KKK	
.

.
.
.///KF			B	X	&	& 0,c7/q9 9 9 0	'0q'A'A "	 "	#A#(?e++
 "4$UHO8L8L   0!%ch0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 %Xv66 !X???#v{{}}'<<6666666&N8&N&N&NOOO HHXs8}}H===8==??#::K#Wj9   4844s44555
 3w!||5155z555666P   "1 	 	Hg7333x}}66#Wj9   3733c33444		U0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0d n#G}}&H
KK[$F   
GGG ..H..////,(,,---	
(J
(
()))	
7L55
7
7888	
4L22
4
4555	
*G
*
*
*
*+++ 
7C00 7 71337 7 7	
 	
 	

  ;3/00 ; ; ;	
 	
 	
 ())#2#. 	 	A*** )***%% 	 	A++++ 8<s6{{<<<=== 	 	A!HHHHv;;6s6{{R/666777 @>x}>>?????@ @s   1)K3LL&'MM"!M"#\*\4W>9T	W>TW>!T"BW>;\=A W>=\>X;	X6	0\6X;	;A\3Z76\7[3	[.	)\.[3	3\7\\	\
\	\\!\r@  c                     |                                  }|sdS h d}t                      }|D ]/}t          |          j        }||v r|                    |           0|sdS dS )zLCheck that a zip looks like a Hermes backup.

    Returns (ok, reason).
    )Fzzip archive is empty>   r#   r$   config.yaml)FzZzip does not appear to be a Hermes backup (no config.yaml, .env, or state databases found))Tr   )namelistru   r   rB   rz   )r@  namesmarkersfoundnbasenames         r.   _validate_backup_ziprO  &  s    
 KKMME -,, 211GEEE    77<wIIh 

 

 8r-   c                     d |                                  D             }|sdS d |D             }d |D             }t          |          dk    r|                                }|dv r|dz   S dS )zDetect if the zip has a common directory prefix wrapping all entries.

    Some tools zip as `.hermes/config.yaml` instead of `config.yaml`.
    Returns the prefix to strip (empty string if none).
    c                 <    g | ]}|                     d           |S r  r   r   rM  s     r.   r   z"_detect_prefix.<locals>.<listcomp>G  s'    ===1QZZ__=Q===r-   r   c                 6    g | ]}t          |          j        S r,   )r   r   rT  s     r.   r   z"_detect_prefix.<locals>.<listcomp>L  s     ///A$q''-///r-   c                 D    h | ]}t          |          d k    |d         S )r:   r   )r   r   r   s     r.   	<setcomp>z!_detect_prefix.<locals>.<setcomp>O  s'    :::As1vvzz1Q4zzzr-   r:   >   .hermeshermesr  )rI  r   pop)r@  rJ  
parts_listfirst_partsprefixs        r.   _detect_prefixr_  A  s     >====E r 0////J ;::::K
;1""***C<2r-   c           	         t          | j                                                                                  }|                                s&t          d|            t          j        d           t          j        |          s&t          d|            t          j        d           t                      }t          j
        |d          5 }t          |          \  }}|s&t          d|            t          j        d           t          |          }d |                                D             }t          |          }t          d| d           t          d	t                                  |rt          d
|d           |dz                                  }	|dz                                  }
|	s|
r| j        st                       t          d           t          d           t                       	 t%          d                                                                          }n:# t*          t,          f$ r& t          d           t          j        d           Y nw xY w|dvrt          d           	 ddd           dS t          d| d           |                    dd           g }d}d}g }t          j                                                    }t3          j                    }|D ]}|                    t8                    r|t          t8                    d         }|s=||z  }	 |                                                    |           n)# t<          $ r |                    d| d           Y w xY w	 |j                             dd           |!                    |          5 }tC          |d          5 }|"                    |#                                           ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   |j$        dv s|j%        tL          v r'	 tO          j(        |d           n# tR          $ r Y nw xY w|dz  }|dz  }n9# tT          tR          f$ r%}|                    d| d|            Y d}~nd}~ww xY w|d z  dk    rt          d| d!| d           |r-|                    |          r|t          |          d         }n|}|st          |          j%        tV          v r|                    |           7||z  }	 |                                                    |                                           n*# t<          $ r |                    d| d           Y w xY w	 |j                             dd           |!                    |          5 }tC          |d          5 }|"                    |#                                           ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   |j%        tL          v rtO          j(        |d           |dz  }n9# tT          tR          f$ r%}|                    d| d|            Y d}~nd}~ww xY w|d z  dk    rt          d| d!| d           t3          j                    |z
  }t                       t          d"| d#|d$d%           t          d&t                                  |r"t          d'| d(t                       d)           |rrt          d*t          |           d+           |dd,         D ]}t          |           t          |          d,k    r#t          d-t          |          d,z
   d.           |rt          d/t          |           d0           tY          |          dd,         D ]}t          d1|            t          |          d,k    r#t          d2t          |          d,z
   d.           |d3z  }g }|-                                r	 dd4l.m/}m0}m1}m2}  tY          |3                                          D ]}!|!-                                s|!j%        }"|!dz                                  s|!dz                                  sM ||"          }#|#r-t          d5|" d6|#            |                    |"d7f            ||"          }$|                    |"|$duf           |rd8 |D             }%d9 |D             }&|%r%t          d:d;4                    |%                      |&r%t          d<d;4                    |&                       |            s9t          d= |              d>           t          d?           t          d@           nO# tj          $ rB tm          |3                                          rt          dA           t          dB           Y nw xY wt                       |dCz  -                                st          dD           t          dE           |r3dF |D             }'t          dG           |'D ]}(t          dH|( dI           t          dJ           ddd           dS # 1 swxY w Y   dS )Kz(Restore a Hermes backup from a zip file.zError: File not found: r:   zError: Not a valid zip file: r   r   c                 <    g | ]}|                     d           |S rR  rS  rT  s     r.   r   zrun_import.<locals>.<listcomp>o  s'    CCC1::c??C1CCCr-   zBackup contains z fileszTarget: zDetected archive prefix: z (will be stripped)rH  r#   z;Warning: Target directory already has Hermes configuration.z=Importing will overwrite existing files with backup contents.zContinue? [y/N] z	
Aborted.>   yyeszAborted.Nz
Importing r  Tr5   r   r  z: path traversal blockedwb>   .conf.jsonr#   i  r  r  r  zImport complete: z files restored in r   r  z
  Target: z
  Restored z? memory-provider file(s) to their original location(s) outside r^   r  r  r  r  r  z
  Preserved z? runtime state file(s) (kept this machine's, not the backup's):r  z    ... and profiles)create_wrapper_scriptcheck_alias_collision_is_wrapper_dir_in_path_get_wrapper_dirz  Skipped alias 'z': Fc                     g | ]	\  }}||
S r,   r,   r   rM  r   s      r.   r   zrun_import.<locals>.<listcomp>  s!    FFFUQ2FqFFFr-   c                     g | ]	\  }}||
S r,   r,   rm  s      r.   r   zrun_import.<locals>.<listcomp>  s!    JJJUQrJqJJJr-   z
  Profile aliases restored: , z  Profile aliases skipped:  z	
  Note: z is not in your PATH.z3  Add to your shell config (~/.bashrc or ~/.zshrc):z(    export PATH="$HOME/.local/bin:$PATH"z6
  Profiles detected but aliases could not be created.z5  Run: hermes profile list  (after installing hermes)r   z?Note: The hermes-agent codebase was not included in the backup.z0  If this is a fresh install, run: hermes updatec                     g | ]\  }}|S r,   r,   )r   rM  _s      r.   r   zrun_import.<locals>.<listcomp>-  s    ;;;A1;;;r-   z,
To re-enable gateway services for profiles:z  hermes -p z gateway installz2Done. Your Hermes configuration has been restored.)7r   r(  rv   rx   r   r   r   r   
is_zipfiler   r)  rO  r_  rI  r   r   rw   r   inputstripr"  EOFErrorKeyboardInterruptr<   r!  r>   r?   r   r&  r%  ry   r{   r;   r=   rF   readr  rB   _SECRET_FILE_NAMESrA   chmodrL   rM   _IMPORT_SKIP_NAMESr.  r   hermes_cli.profilesrh  ri  rj  rk  iterdirr   ImportErrorany))r   zip_pathr   r@  r   reasonr^  membersr<  
has_confighas_envanswerr   restoredrestored_externalskipped_runtimer7  r>  memberext_reltargetr   r   r   r6  rD  rF  profiles_dirrestored_profilesrh  ri  rj  rk  entryprofile_name	collisionwrappercreatedskippedgw_profilespnames)                                            r.   
run_importr  Y  s#   DL!!,,..6688H 222333h'' 8h88999)++K	3	'	' KD2)"--
F 	$F$$%%%HQKKK##CCbkkmmCCC\\
33334440,..00111 	MKfKKKLLL "M199;;
'//11 	' 	4: 	GGGOPPPQRRRGGG12288::@@BB/0   l### \))j!!!CKD KD KD KD KD KD KD KDH 	3Z333444$666%'9;;&&((^ H	> H	>F   !122  %5!6!6!7!78 !G+NN$$00::::!   MM"Gv"G"G"GHHHH8M''t'DDD .Cfd1C1C .s		#((**---. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }(BBBfkUgFgFg!HVU3333& ! ! ! D!MH%*%%'1 8 8 8MM"6v"6"6"6"6777777778c>Q&&@x@@*@@@AAA  &++F33 S[[\\*  Cyy~!333&&s+++ 3&F  ,,[-@-@-B-BCCCC   @3@@@AAA1##D4#@@@WWV__ *T&$-?-? *3IIchhjj)))* * * * * * * * * * * * * * * * * * * * * * * * * * * * * *;"444HVU+++A#W- 1 1 1/3//#//000000001 #~""<8<<j<<<===.""R' 	M(MMwMMMMNNN2.0022333 	O 1 O O6I6K6KO O O  
  	<@3v;;@@@AAACRC[  a6{{R:3v;;#3:::;;; 	GD_!5!5 D D D   o..ss3 $ $lSll####?##b((ES%9%9B%>EEEFFF #Z/   $	S#S            $L$8$8$:$:;; V VE <<>> ! #(:L!M199;; !UV^D[D[D]D] !  5 5l C CI  VN,NN9NNOOO)00,1FGGGG"7"7"E"E)00,t@S1TUUUU$ 
JFF.?FFFGJJ.?JJJG UStyy?Q?QSSTTT SQTYYw=O=OQQRRR2244 JT+;+;+=+=TTTUUUSTTTHIII S S S|++--.. SSTTTQRRR	S 	n,4466 	FSTTTDEEE 	>;;):;;;KABBB$ > ><U<<<====BCCCWKD KD KD KD KD KD KD KD KD KD KD KD KD KD KD KD KD KDs  D0j33H54j354I,)j3+I,,j3B.j3?'M'&j3'#N
j3Nj31Q2P(P	;PPPPPQ2P""Q2%P"&Q2QQ2
Q$!Q2#Q$$Q21j32R(R#j3#R((Bj399U32j33#Vj3Vj31YX+ (X	X+XX+XX+Y+X//Y2X/3+Yj3Z0Zj3ZF5j3Fgj3A	hj3hB
j33j7:j7)r$   rH  r#   r%   cron/jobs.jsonzcron/executions.dbr"   zchannel_directory.jsonzchannel_aliases.jsonr!   z#gateway/discord_message_recovery.dbzprojects.dbzresponse_store.dbzmemory_store.dbzverification_evidence.dbz	kanban.dbzkanban/boardspairingzplatforms/pairingzfeishu_comment_pairing.jsonzstate-snapshots   c                 6    | pt                      }|t          z  S N)r   _QUICK_SNAPSHOTS_DIRr2   r!  s     r.   _quick_snapshot_rootr  e  s    +/++D&&&r-   labelkeepmax_file_sizec                     |pt                      }t          |          5  t          | |||          cddd           S # 1 swxY w Y   dS )zFCreate one atomic quick snapshot while holding the shared backup slot.)r  r2   r  r  N)r   r[   _create_quick_snapshot_locked)r  r2   r  r  r!  s        r.   create_quick_snapshotr  j  s     +/++D		%	% 
 
,'	
 
 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
s   A  AAc           	         |pt                      }t          |          }dt          dt          dt          ffd}t          j        t          j                  	                    d          }| r| d|  n|}|}	d}
||	z  
                                r#| d|
 }	|
dz  }
||	z  
                                #||	z  }|d	|	 d	t          j                     d
z  }t          j        |d           |                    dd           t                               d|	           i }g }g }t$          D ],}||z  }|
                                s|                                r|                    d          D ]}|                                s|                    |                                          }dd| dv sdd| dv rP |||          r!|j        dk    r|                    |           }||z  }|j                            dd           	 |j        dk    rwt7          ||          sf|                    |           t9          d| d           t;          |          r-t9          d| d|                                j         d            nt          j         ||           |                                j        ||<   T# tB          tD          f$ r'}t           #                    d||           Y d}~d}~ww xY w|                                s |||          r"|j        dk    r|                    |           ||z  }|j                            dd           	 |j        dk    rwt7          ||          sf|                    |           t9          d| d           t;          |          r-t9          d| d|                                j         d           nt          j         ||           |                                j        ||<   # tB          tD          f$ r'}t           #                    d||           Y d}~&d}~ww xY w|rft9          dd$                    |          z              t9          d| d            t           %                    d!d$                    |                     |s@t          j        |d           |r&t9          d"d$                    |           d#           dS |	|| tM          |          tO          |(                                          |||d$}tS          |d%z  d&d'(          5 }tU          j+        ||d)           ddd           n# 1 swxY w Y   t          j,        ||           |p|}|st[          ||t\          n|*           n|rSt9          d+d$                    |          z              t           #                    d,d$                    |                     t           #                    d-tM          |          tM          |                     t                               d.|	tM          |          tO          |(                                                     |	S )/u2  Create a quick state snapshot of critical files.

    Copies STATE_FILES to a timestamped directory under state-snapshots/.
    Auto-prunes old snapshots beyond the keep limit.

    Args:
        max_file_size: When set, individual files larger than this many bytes
            are skipped (with a printed warning) instead of copied. Used by
            the pre-update safety snapshot so a multi-GB ``state.db`` can
            never stall ``hermes update`` or silently eat disk — the small
            pairing/cron/config files the snapshot exists to protect are
            always captured. ``None`` (default) copies everything, which
            preserves manual ``/snapshot`` and ``hermes backup --quick``
            behavior.

    Returns:
        Snapshot ID (timestamp-based), or None if no files found.
    r   rel_namerl   c           
         dS 	 |                                  j        }n# t          $ r Y dS w xY w|k    rdS t          d| dt	          |           dt	                     d           t
                              d||           dS )	z<True (and warn) when ``path`` exceeds the max_file_size cap.NFu     ⚠ Snapshot: skipping z (z	 exceeds z limit)z9Quick snapshot skipped %s: %d bytes exceeds %d byte limitT)rD   rE   rL   r   r   rs   rt   )r   r  r   r  s      r.   
_too_largez1_create_quick_snapshot_locked.<locals>._too_large  s     5	99;;&DD 	 	 	55	=  5R R RT""R R-9--H-HR R R	
 	
 	
 	G		
 	
 	
 ts   ! 
//z%Y%m%d-%H%M%Sr_   r   r:   r^   r`   T)ignore_errorsFr5   z.quick snapshot phase=copy status=started id=%s*z/workspaces/r  z/attachments/r  u,     ⚠ Snapshot: SQLite safe copy FAILED for u$    — file may be locked or corruptedu     ⚠ Snapshot: z! looks ZEROED (no SQLite header; z bytes of NULs?)zCould not snapshot %s: %sNz bytes)u/     ⚠ CRITICAL: could not snapshot DB file(s): ro  u0     ⚠ If sessions disappear after update, check z and run: hermes snapshot listz/Quick snapshot failed to capture DB file(s): %su7     ⚠ Snapshot aborted: no files captured (failed DBs: r   )id	timestampr  r<  
total_sizer   
failed_dbsoversized_skippedmanifest.jsonr  utf-8encoding)indentr  u<     ⚠ Skipping snapshot prune: DB file(s) skipped for size: z/Quick snapshot skipped oversized DB file(s): %su   Skipping snapshot prune because %d DB(s) failed to capture and/or %d were oversized — preserving older snapshots as recovery sourcezAquick snapshot phase=copy status=complete id=%s files=%d bytes=%d)/r   r  r   r   boolr   r  r   utcr   rw   rA   rd   shutilrmtreer<   rs   r$  _QUICK_STATE_FILESr   rglobr   r%  r'  r  r{   r;   r   r   r   rD   rE   copy2rL   rM   rt   r   errorr   sumvaluesr=   jsondumprh   _prune_quick_snapshots_QUICK_DEFAULT_KEEP)r  r2   r  r  r!  rootr  tsbase_snap_idsnap_idr  snap_dirstaging_dirmanifestr  r  r6  r   subsub_relr   r   metaf
incompletes      `                     r.   r  r  {  s;   0 +/++D%%D        , 
hl	#	#	,	,_	=	=B&+3b??5???LGF'>
!
!
#
# !,,F,,! '>
!
!
#
#  g~H<W<<ry{{<<<<K
M+T2222dU333
KK@'JJJ!HJ
 $&! MB MBSjzz|| 	::<< )	 yy~~ $N $N{{}} //$//88:: "^^^^33.g...7X7X:c7++ zU**)00999!G+
   ===N zU**,S#66 %&--g666!!Gw !G !G !G    5S99 " %%_w %_ %_:=((**:L%_ %_ %_!" !" !" %% S#...(+

(:HW%%1 N N NNN#>MMMMMMMMN{{}} 	:c3 	zU""!((---C
555	BzU""$S#.. %%c***?s ? ? ?   -S11 Ns N N25((**2DN N N    S#&&&HHJJ.HSMM) 	B 	B 	BNN6SAAAAAAAA	B  
 	=ii
##$	
 	
 	
 	44 4 4	
 	
 	
 	=IIj!!	
 	
 	

  k6666 	9 $		* 5 59 9 9   t (mm(//++,, .	 	D 
kO+S7	C	C	C %q	$!$$$$% % % % % % % % % % % % % % % J{H%%% 00J 
t*=*=SWXXXXX 	N))-../   NNA		+,,   	 
OOS!233		
 	
 	
 KKKHHOO	   NsP   A?K2KL K;;L +A?P ,2P  Q1QQU55U9<U9limitc                    t          |          }|                                sg S g }t          |                                d          D ]}|                                r4|j                            d          s|j                            d          rL|dz  }|                                r	 t          |d          5 }|	                    t          j        |                     ddd           n# 1 swxY w Y   n:# t          j        t          f$ r! |	                    |j        d	d	d
           Y nw xY wt          |          | k    r n|S )z7List existing quick state snapshots, most recent first.T)reverser^   r`   r  r  r  Nr   )r  r<  r  )r  rw   r.  r|  r   rB   r   r   r=   r{   r  loadJSONDecodeErrorrL   r   )r  r2   r  resultsr   manifest_pathr  s          r.   list_quick_snapshotsr  ^  s   
  ,,D;;== 	GDLLNND111  xxzz 	QV..s33 	qvz7R7R 	O+!! 	QQ-'::: 1aNN49Q<<0001 1 1 1 1 1 1 1 1 1 1 1 1 1 1('2 Q Q QafAQOOPPPPPQw<<5  E ! Ns6   1D(C6*D6C:	:D=C:	>D4D98D9snapshot_idc                 t   |pt                      }t          |          }| rd| v sd| v s| dv rt                              d|            dS || z  }	 |                                                    |                                           n,# t          $ r t                              d|            Y dS w xY w|                                sdS |dz  }|                                sdS t          |d	          5 }t          j        |          }d
d
d
           n# 1 swxY w Y   d}|                    di           D ]}	||	z  }
	 |
                                                    |                                           n+# t          $ r t                              d|	           Y jw xY w||	z  }	 |                                                    |                                           n+# t          $ r t                              d|	           Y w xY w|
                                s|j                            dd           	 |j        dk    rn|j        d|j         dz  }t#          j        |
|           |                    d           t#          j        t+          |          t+          |                     nt#          j        |
|           |dz  }# t,          t.          f$ r'}t                              d|	|           Y d
}~d
}~ww xY wt                              d||            |dk    S )zRestore state from a quick snapshot.

    Overwrites current state files with the snapshot's copies.
    Returns True if at least one file was restored.
    r  \)r^   z..zInvalid snapshot_id: %sFz*Snapshot path traversal blocked for id: %sr  r  r  Nr   r   z#Manifest path traversal blocked: %sTr5   r  r^   z.snap_restorera   r:   zFailed to restore %s: %sz"Restored %d files from snapshot %s)r   r  rs   r  rx   r%  ry   r   rw   r=   r  r  r   r;   r<   r  rB   r  r  rg   mover   rL   rM   r$  )r  r2   r!  r  r  r  r  r  r  r6  r   r   rB  r   s                 r.   restore_quick_snapshotr  x  s    +/++D%%D  #,,0C0C{VaGaGa.<<<uk!H&&t||~~6666   A;OOOuu ?? u.M!! u	mg	.	.	. !y||               Hxx$$  ?  ?n	KKMM%%h&6&6&8&89999 	 	 	LL>DDDH	 Sj	KKMM%%dllnn5555 	 	 	LL>DDDH	 zz|| 	
555	?zU""j#>sx#>#>#>>S#&&&

d
+++CHHc#hh////S#&&&MHH) 	? 	? 	?LL3S#>>>>>>>>	? KK4hLLLa<sa   9B %B43B49DD!D9F  %F('F(19G++%HHBKL.LLr  c                    |                                  sdS 	 t          | dd          5 }t          j        |          }ddd           n# 1 swxY w Y   n# t          t          j        f$ r Y dS w xY wt          |t                    r<|                    dg           }t          |t                    rt          |          ndS t          |t                    rt          |          S dS )u  Return the number of cron jobs stored in ``path``.

    The canonical on-disk shape is ``{"jobs": [...]}`` (see ``cron/jobs.py``).
    A legacy bare-list shape (``[...]``) is also honoured.

    Returns:
        The job count for any *valid, readable* JSON document, or ``None`` if
        the file is missing or cannot be parsed. ``None`` means "unknown" —
        callers must not treat it as "zero jobs", because acting on an
        unreadable file could mask a real corruption the user needs to see.
    Nr   z	utf-8-sigr  jobs)r   r=   r  r  rL   r  
isinstancedictr   listr   )r   r  datar  s       r.   _count_cron_jobsr    s&    <<>> t
 $k222 	 a9Q<<D	  	  	  	  	  	  	  	  	  	  	  	  	  	  	 T)*   tt$ =xx##&tT22<s4yyy<$ 4yy4s3   A AA AA AA A10A1c                    | sdS |pt                      }|t          z  }t          |          }|dS t          |          | z  t          z  }t          |          }|sdS ||k    rdS 	 |j                            dd           t          j        ||           n:# t          t          f$ r&}t                              d|           Y d}~dS d}~ww xY wt                              d|| ||           d|| dS )uI  Safety net for silent cron-job loss across ``hermes update``.

    Config-version migrations have been observed to leave ``cron/jobs.json``
    valid-but-empty after an update, silently dropping every scheduled job
    (issue #34600). The desktop scheduler can also overwrite the file with its
    own small set of internally-tracked crons, causing partial loss (issue
    #52144).

    This compares the *current* job count against the pre-update snapshot. If
    the live file now has **fewer** jobs than the snapshot, the snapshot copy
    of ``cron/jobs.json`` is restored in place.

    The check is deliberately conservative — it only ever restores when there
    is unambiguous evidence of loss (snapshot had more jobs than live file),
    so a user who genuinely deleted jobs during/after the update is never
    second-guessed, and an unreadable live file (count ``None``) is left
    untouched so real corruption still surfaces.

    Args:
        snapshot_id: The pre-update quick-snapshot id (from
            :func:`create_quick_snapshot`).
        hermes_home: Override for the Hermes home directory (tests).

    Returns:
        ``None`` when no action was taken (the common, healthy path). On a
        successful restore, a dict ``{"restored": True, "job_count": N,
        "snapshot_id": ...}`` so the caller can warn the user.
    NTr5   z@Cron jobs were emptied during update but auto-restore failed: %su   Restored %d cron job(s) from pre-update snapshot %s (live file had %d job(s), snapshot had %d — jobs were lost during migration))r  	job_countr  )r   _CRON_JOBS_RELr  r  r;   r<   r  r  rL   rM   rs   r  rt   )r  r2   r!  	live_path
live_count	snap_path
snap_countr   s           r.   restore_cron_jobs_if_emptiedr    sJ   @  t+/++D~%I!),,J t$T**[8>II!),,J t Zttd;;;Y	****_%   NPS	
 	
 	
 ttttt	 NN	Y   :kRRRs   (1B C+CCr  c                 R   |                                  sdS t          d |                                 D             d d          }d}||d         D ]T}	 t          j        |           |dz  }# t
          $ r+}t                              d|j        |           Y d}~Md}~ww xY w|S )	zKRemove oldest quick snapshots beyond the keep limit. Returns count deleted.r   c              3      K   | ]N}|                                 |j                            d           0|j                            d          J|V  OdS )r^   r`   N)r   rB   r   r   r   s     r.   r   z)_prune_quick_snapshots.<locals>.<genexpr>:  sy       	
 	
xxzz	
 #$&"3"3C"8"8	
 BCQ[A\A\	
	
 	
 	
 	
 	
 	
r-   c                     | j         S r  rB   )r   s    r.   <lambda>z(_prune_quick_snapshots.<locals>.<lambda>?      af r-   Tkeyr  Nr:   zFailed to prune snapshot %s: %s)	rw   r.  r|  r  r  rL   rs   rt   rB   )r  r  dirsdeletedr   r   s         r.   r  r  4  s    ;;== q	
 	
\\^^	
 	
 	

   D G$%%[ K K	KM!qLGG 	K 	K 	KNN<afcJJJJJJJJ	K Ns   A//
B$9!BB$c                 >    t          t          |          |           S )z6Manually prune quick snapshots. Returns count deleted.r  )r  r  )r  r2   s     r.   prune_quick_snapshotsr  N  s     
 ""6{"C"C$OOOOr-   c                 0   t          | dd          }t          |          }|rct          d|            t                      }t          dt	          |           dt                       d           t          d|            dS t          d	           dS )
z*CLI entry point for hermes backup --quick.r  N)r  zState snapshot created: r  z snapshot(s) stored in z/state-snapshots/z"  Restore with: /snapshot restore z!No state files found to snapshot.)getattrr  r   r  r   r   )r   r  r  snapss       r.   run_quick_backupr  V  s    D'4((E#%000G 3222333$&&^3u::^^6I6K6K^^^___<7<<=====122222r-   c                     	 t          |          5  t          | |          cddd           S # 1 swxY w Y   dS # t          $ r&}t                              d|           Y d}~dS d}~ww xY w)z5Single-flight wrapper for automatic full zip backups.NzFull-zip backup skipped: %s)r[   _write_full_zip_backup_lockedr'   rs   rt   )r   r   r   s      r.   _write_full_zip_backupr  g  s    #K00 	H 	H0;GG	H 	H 	H 	H 	H 	H 	H 	H 	H 	H 	H 	H 	H 	H 	H 	H 	H 	H    4c:::ttttts-   ; .; 2; 2; 
A+A&&A+c           
         t          j                    }t                              d           g }	 t	          j        |d          D ]\  }}}t          |          }d |D             |dd<   |D ]W}||z  }		 |	                    |          }
n# t          $ r Y *w xY wt          |	|
|           r@|
                    |	|
f           Xn3# t          $ r&}t                              d|           Y d}~dS d}~ww xY w|sdS t                              dt          j                    |z
  dz  t          |                     t          j                    }	 t          |           5 }t          j        |d	t          j        d
          5 }t%          |d          D ]\  }\  }}	 |j        dk    rt)          j        ddt-          | j                            5 }t          |j                  }ddd           n# 1 swxY w Y   	 t3          ||          s7t                              d|           t5          t-          |                    |                    |t-          |                     |                    d           n?# |                    d           w xY w|                    |t-          |                     nA# t:          t          t          f$ r'}t                              d||           Y d}~fd}~ww xY w|dz  dk    r)t                              d|t          |                     	 ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   n:# t          t4          f$ r&}t                              d|           Y d}~dS d}~ww xY wt                              dt          j                    |z
  dz  t          |          |                                 j                    | S )u'  Write a full zip snapshot of ``hermes_root`` to ``out_path``.

    Uses the same exclusion rules and SQLite safe-copy as :func:`run_backup`.
    Returns the output path on success, None on failure (nothing to back up,
    or write error — caller should surface the outcome but not raise).
    z*automatic backup phase=scan status=startedFr   c                 $    g | ]}|t           v|S r,   r   r   s     r.   r   z1_write_full_zip_backup_locked.<locals>.<listcomp>  s"    JJJ!>2I2I12I2I2Ir-   Nz Full-zip backup: walk failed: %szEautomatic backup phase=scan status=complete duration_ms=%.1f files=%dr  r  r	  r
  r:   r  r  z6Full-zip backup aborted: SQLite snapshot failed for %sr  Tra   zSkipping %s in zip backup: %sr  r   zDautomatic backup phase=archive status=progress completed=%d total=%dz%Full-zip backup: zip write failed: %szQautomatic backup phase=archive status=complete duration_ms=%.1f files=%d bytes=%d)!r>   r?   rs   r$  rA   r   r   r%  ry   r   r{   rL   rt   r   rk   r(  r)  r*  r+  r  r,  r-  r   r;   rB   r   r0   rF   rg   rM   debugrD   rE   )r   r   r0  r1  r   r   r   r   r   r   r6  r   archive_startedr?  r@  indexr   r   rB  rC  s                       r.   r  r  q  s    >##L
KK<===,.L,.GKU,S,S,S 	2 	2(GXygBJJhJJJHQQQK" 
2 
2U
++K88CC!   H ,E3AA ##UCL1111
2	2     93???ttttt  t
KKO			L	(D0L   n&&O) ** "	lGO#w31=
 =
 =
 "	/8q/I/I  ++(%//
 &8#(C<P<P   4 %)#(^^F4 4 4 4 4 4 4 4 4 4 4 4 4 4 4	;#06#B#B J &$\$,!" !" !" ';3x==&I&I IHHVS]]HCCC"MMTM::::FMMTM::::3x==AAA'*=   LL!@(CPPPHHHH 3;!##KK^L))  7"	 "	 "	 "	 "	 "	 "	 "	 "	 "	 "	 "	 "	 "	 "	 "	 "	 "	 "	 "	 "	 "	 "	 "	 "	 "	 "	 "	 "	 "	F )*   >DDD ttttt KK[			O	+t3L	   Os  AC	 7BC	 
BC	 B.C	 	
C9C44C9M) ##MM!4KG6	*K6G::K=G:>KA+J-KJ	'KML	K=	7M=L	7M:MM
	
MM
	MM) M!!M) $M!%M) )N :NN r   zpre-update-r   c                 6    | pt                      }|t          z  S r  )r   _PRE_UPDATE_BACKUPS_DIRr  s     r.   _pre_update_backup_dirr    s    +/++D)))r-   
backup_dirc                 r   t          |d          }|                                 sdS t          d |                                 D             d d          }d}||d         D ]T}	 |                                 |dz  }# t
          $ r+}t                              d|j        |           Y d}~Md}~ww xY w|S )	u  Remove oldest pre-update backups beyond the keep limit.

    Returns the number of files deleted.  Only touches files matching
    ``pre-update-*.zip`` so hand-made zips dropped in the same directory
    are never touched.

    ``keep`` is floored to 1 because this helper is only called immediately
    after a fresh backup is written: deleting that backup right after the
    user paid the disk/CPU cost to create it would leave them worse off
    than no backup at all (and the wrapper in ``main.py`` would still print
    a misleading ``Saved: <path>`` line for a file that no longer exists).
    Operators who genuinely don't want a backup should set
    ``updates.pre_update_backup: off`` in config — that gates creation.
    r:   r   c              3      K   | ]V}|                                 r@|j                            t                    r!|j                                        d k    R|V  WdS r  N)r   rB   r   _PRE_UPDATE_PREFIXr  r"  rW  s     r.   r   z,_prune_pre_update_backups.<locals>.<genexpr>  st       	b 	bqIIKK	bF--.@AA	bFGhnnFVFVZ`F`F` 
F`F`F`F`	b 	br-   c                     | j         S r  r  r   s    r.   r  z+_prune_pre_update_backups.<locals>.<lambda>  r  r-   Tr  NzFailed to prune backup %s: %s	r@   rw   r.  r|  rg   rL   rs   rt   rB   r  r  r   r  r   r   s         r.   _prune_pre_update_backupsr    s    tQ<<D q	b 	bJ&&(( 	b 	b 	b	  G GTUU^ I I	IHHJJJqLGG 	I 	I 	INN:AFCHHHHHHHH	I N   %A??
B4	!B//B4c                    | pt                      }|                                sdS t          |          }	 |                    dd           n4# t          $ r'}t
                              d||           Y d}~dS d}~ww xY wt          j                    	                    d          }|t           | dz  }t          ||          }|dS t          ||           |S )u  Create a full zip backup of HERMES_HOME under ``backups/``.

    Mirrors :func:`run_backup` (same exclusion rules, same SQLite safe-copy)
    but writes to ``<HERMES_HOME>/backups/pre-update-<timestamp>.zip`` and
    auto-prunes old pre-update backups.

    Returns the path to the created zip, or ``None`` if no files were
    found or the backup could not be created.  Never raises — the caller
    (``hermes update``) should continue even if the backup fails.
    NTr5   z-Could not create pre-update backup dir %s: %sr  r  r  )r   r   r  r<   rL   rs   rt   r   r  r   r
  r  r  r2   r  r   r  r   r/  r   r   s           r.   create_pre_update_backupr    s    :!8!:!:K t'44J5555   F
TWXXXttttt LNN##$566E1>5>>>>H#Hk::F~tjt4444O   A 
B A;;B zpre-migration-c                 r   t          |d          }|                                 sdS t          d |                                 D             d d          }d}||d         D ]T}	 |                                 |dz  }# t
          $ r+}t                              d|j        |           Y d}~Md}~ww xY w|S )	zRemove oldest pre-migration backups beyond the keep limit.

    Only touches files matching ``pre-migration-*.zip`` so other backups in
    the same directory are never touched.
    r   c              3      K   | ]V}|                                 r@|j                            t                    r!|j                                        d k    R|V  WdS r	  )r   rB   r   _PRE_MIGRATION_PREFIXr  r"  rW  s     r.   r   z/_prune_pre_migration_backups.<locals>.<genexpr>9  st       	e 	eqIIKK	eF--.CDD	eIJIYIY]cIcIc 
IcIcIcIc	e 	er-   c                     | j         S r  r  r  s    r.   r  z._prune_pre_migration_backups.<locals>.<lambda>;  r  r-   Tr  Nr:   z+Failed to prune pre-migration backup %s: %sr  r  s         r.   _prune_pre_migration_backupsr  .  s    tQ<<D q	e 	eJ&&(( 	e 	e 	e	  G GTUU^ W W	WHHJJJqLGG 	W 	W 	WNNH!&RUVVVVVVVV	W Nr  c                    | pt                      }|                                sdS t          |          }	 |                    dd           n4# t          $ r'}t
                              d||           Y d}~dS d}~ww xY wt          j                    	                    d          }|t           | dz  }t          ||          }|dS t          ||           |S )ug  Create a full zip backup of HERMES_HOME under ``backups/`` before a
    ``hermes claw migrate`` apply.

    Shares implementation with :func:`create_pre_update_backup` via
    ``_write_full_zip_backup`` — same exclusions, same SQLite safe-copy,
    restorable with ``hermes import <archive>``.  Writes to
    ``<HERMES_HOME>/backups/pre-migration-<timestamp>.zip`` and auto-prunes
    old pre-migration backups.

    Returns the path to the created zip, or ``None`` if nothing was found
    to back up (fresh install) or the write failed.  Never raises — the
    caller decides whether to abort or proceed.
    NTr5   z0Could not create pre-migration backup dir %s: %sr  r  r  )r   r   r  r<   rL   rs   rt   r   r  r   r  r  r  r  s           r.   create_pre_migration_backupr  J  s   " :!8!:!:K t (44J5555   I:WZ[[[ttttt LNN##$566E4AeAAAAH#Hk::F~t $7777Or  )r1   )rl   Nr  )NNNN)r  N)Yr+   r  loggingrA   r  r   r   r,  re   r>   r(  
contextlibr   r   r   pathlibr   typingr   r   r	   r
   hermes_constantsr   r   r   	getLoggerr(   rs   r   r   r   rz  rx  r&  RuntimeErrorr'   r0   floatr[   rk   r   r   r  r   r   r   intr   r   !DEFAULT_INTEGRITY_CHECK_MAX_BYTESr  r   r   r   r   r   r   r)  tuplerO  r_  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r
  _PRE_UPDATE_DEFAULT_KEEPr  r  r  r  _PRE_MIGRATION_DEFAULT_KEEPr  r  r,   r-   r.   <module>r)     s      				   



        % % % % % % ' ' ' ' ' ' ' '       , , , , , , , , , , , , Z Z Z Z Z Z Z Z Z Z		8	$	$.  .   >    766    K K K K KL K K K	 	 	 	 	< 	 	 	 4 4 4u 4 4 4 4n D    3d 3 3 3 3lt T
    ,d t    2t t t PT    (t $ 4    > '*+ + +
+ #+26+	+ + + +@ & %, ! 6H H H
H H 	H
 H 
H H H HVD t     4     % % % % E@$ E@4 E@ E@ E@ E@XW_ tSy1A    6w 3    0YD YD YD YDR > )  ' 'htn ' ' ' ' '  "&#'	
 
C=
$
 3-
 C=	

 c]
 
 
 
$  "&#'	` `C=`$` 3-` C=	`
 c]` ` ` `H "& $ 
$sCx.   8 #'I II$I 
I I I I\ "4 HSM    B #'IS ISIS$IS d38nIS IS IS ISX 4G   S 3    6 $"&P P
P$P 	P P P P
3 
3 
3 
3"T  $    [D [t [QU [ [ [ [D $ "  * * *$ * * * *
"$ "c "c " " " "L #'(! !$!
! d^! ! ! !P )  T      : #'+& &$&
& d^& & & & & &r-   