
    ߩ^jP                     ,   d Z ddlZddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
m
Z
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d	d
hZh dZh dZ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e&de'fdZ(dBdZ)de	j*        de+e"e'f         fdZ,de	j*        de'fdZ-dBd Z.d!Z/d"Z0d#Z1dCd$ee         defd%Z2	 	 	 	 dDd&ee'         d$ee         d'ee&         d(ee&         dee'         f
d)Z3	 	 dEd*e&d$ee         deee'ef                  fd+Z4	 dCd,e'd$ee         de"fd-Z5d.Z6d/edee&         fd0Z7	 dCd,e'd$ee         deee'ef                  fd1Z8e1fd2ed'e&de&fd3Z9e1dfd'e&d$ee         de&fd4Z:dBd5Z;ded6edee         fd7Z<d8Z=d9Z>d:Z?dCd$ee         defd;Z@d<ed'e&de&fd=ZAde?fd$ee         d'e&dee         fd>ZBd?ZCd:ZDd<ed'e&de&fd@ZEdeDfd$ee         d'e&dee         fdAZFdS )Fa  
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)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gateway.pidcron.pid>   gateway.lockr   r   processes.jsongateway_state.json>   .envstate.db	auth.jsonz
_external/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.memoryr&   r'   	Exceptionbackup_pathsloggerwarningsetr   
expanduserexistsresolveOSError
ValueErroraddappend)r&   r'   activeproviderdeclaredexcoutseenrawpresolveds              3/home/ice/.hermes/hermes-agent/hermes_cli/backup.py'_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  _EXCLUDED_DIRS.0ds     r>   
<listcomp>z(_iter_external_files.<locals>.<listcomp>   s"    FFFQa~.E.Eq.E.E.E    N)is_file
is_symlinkr4   is_diroswalkr   name_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	  LrL   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)partsrG   rR   rS   rT   rU   )r^   r`   partrR   s       r>   _should_excluderb      s    NE  ~%% >!!deAh&6&6tt=Dt}}'(( t5rL   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)rb   rN   r0   r1   r2   )rc   r^   rd   s      r>   _should_skip_backup_filerf      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.
    Nzfile:z?mode=roT)uriz"SQLite safe copy failed for %s: %s
missing_okF)
sqlite3connectstrbackupcloser)   r+   r,   unlinkr1   )rg   rh   connbackup_conn
connectionr8   s         r>   _safe_copy_dbrv      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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   ry   .1f z TBrE   )rw   units     r>   _format_sizer   "  so    '  D==-1S[[f)))4)))>P>P$>P>PPPP$rL   c           	        ' t                      }|                                s&t          d|            t          j        d           | 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          dt%                       d	           g }t'                      }t)          j        |d
          D ]\  }}}t          |          }	|	                    |          }
|
t          d          k    '|dd         }'fd|D             |dd<   t'          |          t'          |          z
  D ]'}|                    t1          |
|z                       (|D ]E}|	|z  }|                    |          }t3          |||          r.|                    ||f           Ft          j                                                    }g }g }t7                      D ]}	 |                                }|                    |           n9# t8          t:          f$ r% |                    t1          |                     Y bw xY wt=          |          D ]u}	 |                                                    |          }n# t8          t:          f$ r Y >w xY wt>          |                                 z   }|                    ||f           v|s|st          d           dS tC          |          tC          |          z   }t          d| d           d}g }tE          j#                    }tI          j%        |dtH          j&        d          5 }tO          |d          D ]\  }\  }}	 |j        dk    rtQ          j)        dd
t1          |j                            5 }t          |j*                  } ddd           n# 1 swxY w Y   tW          ||           rW|,                    | t1          |                     || -                                j.        z  }| /                    d           nq| /                    d           |                    d| d           |,                    |t1          |                     ||-                                j.        z  }n@# t`          t:          t8          f$ r&}!|                    d| d|!            Y d}!~!{d}!~!ww xY w|dz  dk    rt          d| d| d           |D ]x\  }}	 |,                    ||           ||-                                j.        z  }:# t`          t:          t8          f$ r%}!|                    d| d|!            Y d}!~!qd}!~!ww xY w	 ddd           n# 1 swxY w Y   tE          j#                    |z
  }"|-                                j.        }#t                       |rt          d|            nt          d |            t          d!|            t          d"tc          |                      t          d#tc          |#                      t          d$|"d%d&           |r/t          d'tC          |           d(t%                       d           |rLt          d)tC          |           d*           te          |          dd+         D ]}$t          d,|$            |r4t          d-           te          |          D ]}%t          d,|% d           |rrt          d.tC          |           d/           |dd+         D ]}&t          |&           tC          |          d+k    r#t          d0tC          |          d+z
   d1           |st          d2|j*                    dS dS )3z1Create a zip backup of the Hermes home directory.z*Error: Hermes home directory not found at    %Y-%m-%d-%H%M%Szhermes-backup-.zipTparentsexist_okz	Scanning z ...FrB   .Nc                 6    g | ]}|t           vs|d k    |S )r   rF   )rI   rJ   is_roots     r>   rK   zrun_backup.<locals>.<listcomp>S  s=     
 
 
&&1+>+>w+> +>+>+>rL   zNo files to back up.zBacking up 
 files ...r   w   compresslevel.dbsuffixdeletedirarcnamerk     z: SQLite safe copy failed:   /z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 )3r
   rO   printsysexitoutputr   r.   r0   r   nowstrftimehomer   lowerwith_suffixparentmkdirr   r-   rP   rQ   relative_tor3   ro   rf   r4   r?   r2   r1   r]   _EXTERNAL_PREFIXas_posixlentime	monotoniczipfileZipFileZIP_DEFLATED	enumeratetempfileNamedTemporaryFilerR   rv   writestatst_sizerr   PermissionErrorr   sorted)(argshermes_rootrd   stampfiles_to_addskipped_dirsrW   rX   rY   rZ   rel_dirorig_dirnamesremovedr[   r\   relhome_direxternal_to_addskipped_externalr@   base_resolvedrel_to_homer   
file_counttotal_byteserrorst0zfirc   r^   tmptmp_dbr8   elapsedzip_sizer<   rJ   er   s(                                          @r>   
run_backupr   +  s
   )++K H;HHIII { >$$//1199;;?? 	?LNN++,=>>E">5">">">>H''(9::9;;!=%!=!=!== &((''&(@AA O$666 

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   $%%% \""S%9%99J	
.

.
.
.///KF			B	3(<A	N	N	N )RT'0q'A'A 	7 	7#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
 "1 	 	Hg7333x}}66#Wj9   3733c33444		G) ) ) ) ) ) ) ) ) ) ) ) ) ) )V n#G}}&H 
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   )K663L,+L,'M))M=<M=5Y24V	R%V	%R))V	,R)-BV	Y2A V	Y2	W W;Y2W+Y223X&%Y2&Y"=YY2Y""Y22Y69Y6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))T )namelistr-   r   rR   r3   )r   namesmarkersfoundnbasenames         r>   _validate_backup_zipr     s    
 KKMME -,, 211GEEE    77<wIIh 

 

 8rL   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   rT   rI   r   s     r>   rK   z"_detect_prefix.<locals>.<listcomp>  s'    ===1QZZ__=Q===rL   r   c                 6    g | ]}t          |          j        S rE   )r   r`   r   s     r>   rK   z"_detect_prefix.<locals>.<listcomp>  s     ///A$q''-///rL   c                 D    h | ]}t          |          d k    |d         S )r   r   )r   rI   r<   s     r>   	<setcomp>z!_detect_prefix.<locals>.<setcomp>  s'    :::As1vvzz1Q4zzzrL   r   >   .hermeshermesr   )r   r   pop)r   r   
parts_listfirst_partsprefixs        r>   _detect_prefixr     s     >====E r 0////J ;::::K
;1""***C<2rL   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: rzError: c                 <    g | ]}|                     d           |S r   r   r   s     r>   rK   zrun_import.<locals>.<listcomp>+  s'    CCC1::c??C1CCCrL   zBackup contains z fileszTarget: zDetected archive prefix: z (will be stripped)r   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   Tr   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 rE   rE   rI   r   oks      r>   rK   zrun_import.<locals>.<listcomp>  s!    FFFUQ2FqFFFrL   c                     g | ]	\  }}||
S rE   rE   r   s      r>   rK   zrun_import.<locals>.<listcomp>  s!    JJJUQrJqJJJrL   z
  Profile aliases restored: z, 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 rE   rE   )rI   r   _s      r>   rK   zrun_import.<locals>.<listcomp>  s    ;;;A1;;;rL   z,
To re-enable gateway services for profiles:z  hermes -p z gateway installz2Done. Your Hermes configuration has been restored.)7r   r   r.   r0   rM   r   r   r   
is_zipfiler
   r   r   r   r   r   r   r/   forceinputstripr   EOFErrorKeyboardInterruptr   r   r   r   
startswithr   r   r2   r4   r   openr   readr   rR   _SECRET_FILE_NAMESrP   chmodr1   r   _IMPORT_SKIP_NAMESr   rO   hermes_cli.profilesr   r   r   r   iterdirjoinImportErrorany))r   zip_pathr   r   r  reasonr   membersr   
has_confighas_envanswerr   restoredrestored_externalskipped_runtimer   r   memberext_reltargetrg   rh   r8   r   r   r   profiles_dirrestored_profilesr   r   r   r   entryprofile_name	collisionwrappercreatedskippedgw_profilespnames)                                            r>   
run_importr,    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"   r   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   hermes_homec                 6    | pt                      }|t          z  S N)r   _QUICK_SNAPSHOTS_DIRr0  r   s     r>   _quick_snapshot_rootr5  !  s    +/++D&&&rL   labelkeepmax_file_sizec                 2   |pt                      }t          |          }dt          dt          dt          ffd}t          j        t          j                  	                    d          }| r| d|  n|}||z  }	|	
                    dd           i }
t          D ]"}||z  }|                                s|                                r |                    d	          D ]}|                                s|                    |                                          }d
d| dv sdd| dv rP |||          r]|	|z  }|j        
                    dd           	 |j        dk    rt)          ||          snt+          j        ||           |                                j        |
|<   # t2          t4          f$ r'}t6                              d||           Y d}~d}~ww xY wR|                                sh |||          rv|	|z  }|j        
                    dd           	 |j        dk    rt)          ||          snt+          j        ||           |                                j        |
|<   # t2          t4          f$ r'}t6                              d||           Y d}~d}~ww xY w|
st+          j        |	d           dS ||| t=          |
          t?          |
                                           |
d}tC          |	dz  dd          5 }tE          j#        ||d           ddd           n# 1 swxY w Y   tI          ||tJ          n|           t6          &                    d|t=          |
                     |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.
    pathrel_namer$   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)r   r   r1   r   r   r+   r,   )r:  r;  sizer8  s      r>   
_too_largez)create_quick_snapshot.<locals>._too_largeA  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%S-Tr   *z/workspaces/r   z/attachments/r   zCould not snapshot %s: %sN)ignore_errors)id	timestampr6  r   
total_sizerV   manifest.jsonr   utf-8encoding   )indentr7  z%State snapshot created: %s (%d files))'r   r5  r   ro   boolr   r   r   utcr   r   _QUICK_STATE_FILESr/   rO   rglobrM   r   r   r   r   rv   shutilcopy2r   r   r1   r   r+   r,   rmtreer   sumvaluesr  jsondump_prune_quick_snapshots_QUICK_DEFAULT_KEEPinfo)r6  r0  r7  r8  r   rootr>  tssnap_idsnap_dirmanifestr   rg   subsub_relrh   r8   metafs      `               r>   create_quick_snapshotrc  &  sc   0 +/++D%%D        , 
hl	#	#	,	,_	=	=B!&.ooeoooBGg~HNN4$N///!H! 5B 5BSjzz|| 	::<< 	 yy~~ N N{{}} //$//88:: "^^^^33.g...7X7X:c7++ (
   ===N zU**,S#66 %$% S#...(+

(:HW%%1 N N NNN#>MMMMMMMMN{{}} 	:c3 	n
555	BzU""$S#..  S#&&&HHJJ.HSMM) 	B 	B 	BNN6SAAAAAAAA	B  hd3333t (mm(//++,, D 
h(#	@	@	@ %A	$!$$$$% % % % % % % % % % % % % % % 4T\&9&9tTTTT
KK7#h--PPPNsN   #F2?2F22G*G%%G*5J2JJ>J99J>%M		MMlimitc                 >   t          |          }|                                sg S g }t          |                                d          D ]}|                                s|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)reverserE  rF  rG  Nr   )rB  r   rD  )r5  r/   r   r  rO   r  r4   rU  loadJSONDecodeErrorr1   rR   r   )rd  r0  rZ  resultsrJ   manifest_pathrb  s          r>   list_quick_snapshotsrk    st   
  ,,D;;== 	GDLLNND111  xxzz 	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   <C(C5CC	CC		C4DD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: %srE  rF  rG  Nr   rV   z#Manifest path traversal blocked: %sTr   r   r   z.snap_restorerk   r   zFailed to restore %s: %sz"Restored %d files from snapshot %s)r   r5  r+   errorr0   r   r2   rO   r/   r  rU  rg  getr   r   r   rR   rP  rQ  rr   movero   r1   r   rY  )rl  r0  r   rZ  r]  rj  rb  ra  r  r   rg   rh   r   r8   s                 r>   restore_quick_snapshotrr    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-  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-sigrG  jobs)rM   r  rU  rg  r1   rh  
isinstancedictrp  listr   )r:  rb  datart  s       r>   _count_cron_jobsry    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.
    NTr   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_countrl  )r   _CRON_JOBS_RELry  r5  r   r   rP  rQ  r1   r   r+   ro  r,   )rl  r0  r   	live_path
live_count	snap_path
snap_countr8   s           r>   restore_cron_jobs_if_emptiedr  8  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rZ  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   B   K   | ]}|                                 |V  d S r2  )rO   rH   s     r>   	<genexpr>z)_prune_quick_snapshots.<locals>.<genexpr>  s/      11qahhjj1111111rL   c                     | j         S r2  rR   )rJ   s    r>   <lambda>z(_prune_quick_snapshots.<locals>.<lambda>      af rL   Tkeyrf  Nr   zFailed to prune snapshot %s: %s)	r/   r   r  rP  rR  r1   r+   r,   rR   )rZ  r7  dirsdeletedrJ   r8   s         r>   rW  rW    s    ;;== q11DLLNN111  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.rK  )rW  r5  )r7  r0  s     r>   prune_quick_snapshotsr    s     
 ""6{"C"C$OOOOrL   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.r6  N)r6  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.)getattrrc  r   rk  r   r   )r   r6  r\  snapss       r>   run_quick_backupr    s    D'4((E#%000G 3222333$&&^3u::^^6I6K6K^^^___<7<<=====122222rL   r   c           	         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 d}	 t          j        | dt          j        d          5 }|D ]i\  }}	 |j        d	k    rt          j        d	dt!          | j                  
          5 }t          |j                  }ddd           n# 1 swxY w Y   	 t'          ||          s6t          	                    d|           d}	 |                    d            n|                    |t!          |                     |                    d           n?# |                    d           w xY w|                    |t!          |                     *# t,          t          t          f$ r'}
t                              d||
           Y d}
~
cd}
~
ww xY wddd           n# 1 swxY w Y   n[# t          $ rN}
t          	                    d|
           	 |                     d           n# t          $ r Y nw xY wY d}
~
dS d}
~
ww xY w|r*	 |                     d           n# t          $ r Y nw xY wdS | 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).
    FrB   c                 $    g | ]}|t           v|S rE   rF   rH   s     r>   rK   z*_write_full_zip_backup.<locals>.<listcomp>  s"    JJJ!>2I2I12I2I2IrL   Nz Full-zip backup: walk failed: %sr   r   r   r   r   z6Full-zip backup aborted: SQLite snapshot failed for %sTrk   r   zSkipping %s in zip backup: %sz%Full-zip backup: zip write failed: %s)rP   rQ   r   r   r2   rf   r4   r1   r+   r,   r   r   r   r   r   r   ro   r   rR   rv   rr   r   r   debug)rd   r   r   rW   rX   rY   rZ   r[   r\   r   r8   sqlite_snapshot_failedr   rc   r^   r   r   s                    r>   _write_full_zip_backupr    sE    -/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"$_XsG,@PQRRR 	VX&2  "(%//
 &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 & &$\$,!" !" !" :> 6 % #MMTM::::: HHVS]]HCCC"MMTM::::FMMTM::::3x==AAA'*=   LL!@(CPPPHHHH3	 	 	 	 	 	 	 	 	 	 	 	 	 	 	8    >DDD	OOtO,,,, 	 	 	D	ttttt  	OOtO,,,, 	 	 	D	tOs&  AB 
A B  
A-*B ,A--.B 
C&CC"I> 8	I24H%6EH%EH%EH%#-G$H%'I2)$G$H%$G<<'H%#I2%I#<II2I##I2&I> 2I66I> 9I6:I> >
KK$J;:K;
KKKKKK3 3
L ?L r   zpre-update-   c                 6    | pt                      }|t          z  S r2  )r   _PRE_UPDATE_BACKUPS_DIRr4  s     r>   _pre_update_backup_dirr    s    +/++D)))rL   
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)rM   rR   r  _PRE_UPDATE_PREFIXr   r   r   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rL   c                     | j         S r2  r  r<   s    r>   r  z+_prune_pre_update_backups.<locals>.<lambda>(  r  rL   Tr  NzFailed to prune backup %s: %s	maxr/   r   r  rr   r1   r+   r,   rR   r  r7  r   r  r<   r8   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.
    NTr   z-Could not create pre-update backup dir %s: %sr   r   rK  )r
   rO   r  r   r1   r+   r,   r   r   r   r  r  r  r0  r7  r   r  r8   r   rd   results           r>   create_pre_update_backupr  7  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  )rM   rR   r  _PRE_MIGRATION_PREFIXr   r   r   s     r>   r  z/_prune_pre_migration_backups.<locals>.<genexpr>n  st       	e 	eqIIKK	eF--.CDD	eIJIYIY]cIcIc 
IcIcIcIc	e 	erL   c                     | j         S r2  r  r  s    r>   r  z._prune_pre_migration_backups.<locals>.<lambda>p  r  rL   Tr  Nr   z+Failed to prune pre-migration backup %s: %sr  r  s         r>   _prune_pre_migration_backupsr  c  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.
    NTr   z0Could not create pre-migration backup dir %s: %sr   r   rK  )r
   rO   r  r   r1   r+   r,   r   r   r   r  r  r  r  s           r>   create_pre_migration_backupr    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  )r$   Nr2  )NNNN)r/  N)G__doc__rU  loggingrP   rP  rm   r   r   r   r   r   r   pathlibr   typingr   r   r   r	   hermes_constantsr
   r   r   	getLogger__name__r+   rG   rU   rS   r  r  r   r?   r]   rL  rb   rf   rv   intro   r   r   r   tupler   r   r,  rN  r3  rX  r5  rc  rk  rr  r|  ry  r  rW  r  r  r  r  r  _PRE_UPDATE_DEFAULT_KEEPr  r  r  r  _PRE_MIGRATION_DEFAULT_KEEPr  r  rE   rL   r>   <module>r     sc      				   



    ' ' ' ' ' ' ' '       , , , , , , , , , , , , Z Z Z Z Z Z Z Z Z Z		8	$	$.  .  <    766    3d 3 3 3 3lt T
    ,d t    2t t t PT    (t $ 4    D     p@ p@ p@ p@nW_ tSy1A    6w 3    0YD YD YD YDR > )  ' 'htn ' ' ' ' '  "&#'	E EC=E$E 3-E C=	E
 c]E E E ER "& $ 
$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    . $"&P P
P$P 	P P P P
3 
3 
3 
3"NT N N$ N N N Nj $ "  * * *$ * * * *
"$ "c "c " " " "L #'(! !$!
! d^! ! ! !P )  T      : #'+& &$&
& d^& & & & & &rL   