
    `gjP                        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	jT                  de+e"e'f   fdZ,de	jT                  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ZFy)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                  l   	 ddl m} m} 	  |        }|sg S 	  ||      }|g S 	 |j	                         xs g }g }t               }|D ]e  }	 t        |      j                         }	|	j                         s.	 |	j                         }
|
|v rD|j                  |
       |j                  |	       g |S # t        $ r g cY S w xY w# t        $ r d}Y w xY w# t        $ r d}Y w xY w# t        $ r#}t
        j                  d||       g cY d}~S d}~ww xY w# t        $ r Y w xY w# t        t        f$ r Y w xY w)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              D/root/.hermes/venv/lib/python3.12/site-packages/hermes_cli/backup.py'_collect_memory_provider_external_pathsr?      sf   T,. 	'/ 	((*0b
 CD 	S	$$&A xxz	yy{H t

1 JQ  	
      I6SVW	  		 $ 		st   B0 C C C# 
D5D!0B>=B>CCC C #	D,D
D
D	DD!D32D3basec                    g }| j                         r#| j                         s|j                  |        |S | j                         s|S t	        j
                  | d      D ]  \  }}}t        |      }|D cg c]  }|t        vs| c}|dd |D ][  }||z  }|j                         r|j                  t        v s|j                  j                  t              rK|j                  |       ]  |S c c}w )zYield regular files under *base* (a file or a directory), skipping
    symlinks, caches, and pyc files. *base* itself may be a file.FfollowlinksN)is_file
is_symlinkr4   is_diroswalkr   _EXCLUDED_DIRSname_EXCLUDED_NAMESendswith_EXCLUDED_SUFFIXES)	r@   filesdirpathdirnames	filenamesdpdfnamefpaths	            r>   _iter_external_filesrV      s     E||~doo/T;;=(*%(H 	 $9']"*FQa~.EqF 	 EJE!zz_,

0C0CDV0WLL	 	  L Gs   5C1C1rel_pathc                     | j                   }|D ]  }|t        vr|dk(  r	||d   k7  r y | j                  }|t        v ry|j	                  t
              ryy)zFReturn True if *rel_path* (relative to hermes root) should be skipped.r   r   TF)partsrI   rJ   rK   rL   rM   )rW   rY   partrJ   s       r>   _should_excluder[      si    NNE ~% >!deAh&6 ==D}}'(    abs_pathout_pathc                     t        |      ry| j                         ry	 | j                         |j                         k(  S # t        t        f$ r Y yw xY w)zHReturn True when a candidate file should not be written to a backup zip.TF)r[   rE   r0   r1   r2   )r]   rW   r^   s      r>   _should_skip_backup_filer`      sW    x  !X%5%5%777Z  s    A   AAsrcdstc                 V   d}d}	 t        j                  d|  dd      }t        j                  t        |            }|j                  |       	 ||fD ]  }|	 |j	                           y# t
        $ r Y %w xY w# t
        $ rp}t        j                  d| |       	 |j                  d       n# t        $ r Y nw xY wY d}~||fD ]%  }|	 |j	                          # t
        $ r Y #w xY w yd}~ww xY w# ||fD ]%  }|	 |j	                          # 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   )ra   rb   connbackup_conn
connectionr8   s         r>   _safe_copy_dbrp      sU    DKse84$?ooc#h/K  '- 	J%$$&	 !   ;S#F	JJ$J' 		&- 	J%$$&  		 '- 	J%$$&  		s   A
A> A//	A;:A;>	C7C2B21C22	B>;C2=B>>C2C: C!!	C-,C-2C77C: :
D(DD(	D#	 D("D#	#D(nbytesc                 `    dD ]$  }| dk  r|dk7  r
| dd| c S |  d| c S | dz  } & | ddS )zHuman-readable file size.)BKBMBGBi   rs   .1f z TB )rq   units     r>   _format_sizer{   "  s[    ' D=-1S[fS\4&)P$>PP$ S\r\   c           	         t               }|j                         s#t        d|        t        j                  d       | j
                  rnt        | j
                        j                         j                         }|j                         rkt        j                         j                  d      }|d| dz  }n>t        j                         j                  d      }t        j                         d| dz  }|j                  j                         dk7  r|j                  |j                  dz         }|j                   j#                  dd       t        dt%                d	       g }t'               }t)        j*                  |d
      D ]  \  }}}t        |      }	|	j-                  |      }
|
t        d      k(  }|dd }|D cg c]  }|t.        vs|dk(  r|s| c}|dd t'        |      t'        |      z
  D ]  }|j1                  t3        |
|z               ! |D ]9  }|	|z  }|j-                  |      }t5        |||      r'|j7                  ||f       ;  t        j                         j                         }g }g }t9               D ]~  }	 |j                         }|j-                  |       t?        |      D ]L  }	 |j                         j-                  |      }t@        |jC                         z   }|j7                  ||f       N  |s|st        d       ytE        |      tE        |      z   }t        d| d       d}g }tG        jH                         }tK        jL                  |dtJ        jN                  d      5 }tQ        |d      D ]7  \  }\  }} 	 |j                  dk(  rtS        jT                  dd
t3        |j                               5 }!t        |!jV                        }"ddd       tY        |"      rL|j[                  |"t3        |              ||"j]                         j^                  z  }|"ja                  d       na|"ja                  d       |j7                  d|  d       |j[                  |t3        |              ||j]                         j^                  z  }|dz  dk(  s&t        d| d| d       : |D ]6  \  }}	 |j[                  ||       ||j]                         j^                  z  }8 	 ddd       tG        jH                         |z
  }$|j]                         j^                  }%t                |rt        d|        nt        d |        t        d!|        t        d"te        |              t        d#te        |%              t        d$|$d%d&       |r#t        d'tE        |       d(t%                d       |r9t        d)tE        |       d*       tg        |      dd+ D ]  }&t        d,|&         |r*t        d-       tg        |      D ]  }t        d,| d        |rVt        d.tE        |       d/       |dd+ D ]  }'t        |'        tE        |      d+kD  rt        d0tE        |      d+z
   d1       |st        d2|jV                          yyc c}w # t:        t<        f$ r |j7                  t3        |             Y Yw xY w# t:        t<        f$ r Y ?w xY w# 1 sw Y   xY w# tb        t<        t:        f$ r"}#|j7                  d|  d|#        Y d}#~#d}#~#ww xY w# tb        t<        t:        f$ r"}#|j7                  d| d|#        Y d}#~#d}#~#ww xY w# 1 sw Y   zxY w)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   .Nr   zNo files to back up.zBacking up 
 files ...r   w   compresslevel.dbsuffixdeletedirarcnamere     z: SQLite safe copy failed:   /zBackup incomplete: zBackup complete: z  Files:       z  Original:    z  Compressed:  z  Time:        rw   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 )4r
   rF   printsysexitoutputr   r.   r0   r   nowstrftimehomer   lowerwith_suffixparentmkdirr   r-   rG   rH   relative_torI   r3   ri   r`   r4   r?   r2   r1   rV   _EXTERNAL_PREFIXas_posixlentime	monotoniczipfileZipFileZIP_DEFLATED	enumeratetempfileNamedTemporaryFilerJ   rp   writestatst_sizerl   PermissionErrorr{   sorted)(argshermes_rootr^   stampfiles_to_addskipped_dirsrO   rP   rQ   rR   rel_diris_rootorig_dirnamesrS   removedrT   rU   relhome_direxternal_to_addskipped_externalr@   base_resolvedrel_to_homer   
file_counttotal_byteserrorst0zfir]   rW   tmptmp_dbr8   elapsedzip_sizer<   es(                                           r>   
run_backupr   +  s>   )+K:;-HI {{$//199;??LLN++,=>EN5'">>H''(9:99;>%!== &(''&(@A OO$6 
I)+,D
12,.L5L(*(O .$9']..-
 T#Y& 
&1+>w 
 =)CM9 	5GS7!234	5  	.EJE##K0C'sH=-	.!.> yy{""$H.0O"$79 5	 LLNM%%h/ *$/ 	5E#mmo99(C ')=)=)??G""E7#34	55 $% \"S%99J	K
|:
./KF		B	3(<(<A	N )RT'0q'A 	7#A#(??e+
 "44$UHOO8L 0!%chh0 %Xv6X?#v{{}'<'<<668*4M&NO HHXs8}H=8==?#:#::K 3w!|1#Qzl*56;	7D "1 	Hg73x}}666	G)V nn#G}}&&H 
G#H:./!(,-	OJ<
()	OL56
78	OL23
45	OGC=
*+C01 2134A7	

 3/01 2: ;	
 ()#2. 	AD*	 )*% 	AD1+	 s6{m+;<= 	A!H	v;Js6{R/067.x}}o>? O
: G$ 	##CI.	 ( 40 0 $Wj9 8*Bse45 $Wj9 7)2cU34O) )s   Z1!Z6=[&?];:\	[<'B\	.];/9\	(];2];0] ];6)[#"[#&[98[9<\\		\?\:4];:\??];]8]3-];3]88];;^r   c                     | j                         }|syh d}t               }|D ]-  }t        |      j                  }||v s|j	                  |       / |syy)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   rJ   r3   )r   namesmarkersfoundnbasenames         r>   _validate_backup_zipr     sb    
 KKME, 2GEE  7<<wIIh	  

 r\   c                 Z   | j                         D cg c]  }|j                  d      r| }}|sy|D cg c]  }t        |      j                   }}|D ch c]  }t	        |      dkD  s|d    }}t	        |      dk(  r|j                         }|dv r|dz   S yc c}w c c}w c c}w )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).
    r   r   r}   r   >   .hermeshermes)r   rL   r   rY   r   pop)r   r   r   
parts_listr<   first_partsprefixs          r>   _detect_prefixr     s     =1QZZ_Q=E= *//A$q'--/J/ ",:As1vz1Q4:K:
;1"**C< >
 0 ;s   BBB#B(-B(c           	         t        | j                        j                         j                         }|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        |      }|j                         D cg c]  }|j                  d      r| }}t        |      }	t        d|	 d       t        d	t!                       |rt        d
|d       |dz  j#                         }
|dz  j#                         }|
s|rw| j$                  skt                t        d       t        d       t                	 t'        d      j)                         j+                         }dvrt        d       	 ddd       yt        d|	 d       |j1                  dd       g }d}d}g }t        j2                         j                         }t5        j6                         }|D ]Z  }|j9                  t:              r|t        t:              d }|s/||z  }	 |j                         j=                  |       	 |jB                  j1                  dd       |jE                  |      5 }tE        |d      5 }|jG                  |jI                                ddd       ddd       |jJ                  dv s|jL                  tN        v r	 tQ        jR                  |d       |dz  }|dz  }|d z  dk(  rt        d| d|	 d       |r |j9                  |      r|t        |      d }n|}|sEt        |      jL                  tX        v r|jA                  |       s||z  }	 |j                         j=                  |j                                	 |jB                  j1                  dd       |jE                  |      5 }tE        |d      5 }|jG                  |jI                                ddd       ddd       |jL                  tN        v rtQ        jR                  |d       |dz  }|d z  dk(  sIt        d| d|	 d       ] t5        j6                         |z
  }t                t        d!| d"|d#d$       t        d%t!                       |rt        d&| d't!                d(       |rVt        d)t        |       d*       |dd+ D ]  }t        |        t        |      d+kD  rt        d,t        |      d+z
   d-       |rbt        d.t        |       d/       t[        |      dd+ D ]  }t        d0|         t        |      d+kD  rt        d1t        |      d+z
   d-       |d2z  }g }|j]                         rZ	 dd3l/m0}m1}m2} m3}! t[        |ji                               D ]  }"|"j]                         s|"jL                  }#|"dz  j#                         s|"dz  j#                         sG ||#      }$|$r%t        d4|# d5|$        |jA                  |#d6f       v ||#      }%|jA                  |#|%duf        |r|D cg c]
  \  }}|s	| }&}}|D cg c]
  \  }}|r	| }'}}|&rt        d7d8jk                  |&              |'rt        d9d8jk                  |'               |        s*t        d: |!        d;       t        d<       t        d=       t                |d@z  j]                         st        dA       t        dB       |r5|D (cg c]  \  }}(|	 })}}(t        dC       |)D ]  }*t        dD|* dE        t        dF       ddd       yc c}w # t,        t.        f$ r$ t        d       t        j                  d       Y "w xY w# t>        $ r |jA                  d| d       Y w xY w# 1 sw Y   "xY w# 1 sw Y   'xY w# tT        $ r Y w xY w# tV        tT        f$ r"}|jA                  d| d|        Y d}~%d}~ww xY w# t>        $ r |jA                  d| d       Y Ow xY w# 1 sw Y   SxY w# 1 sw Y   XxY w# tV        tT        f$ r"}|jA                  d| d|        Y d}~Zd}~ww xY wc c}}w c c}}w # tl        $ r3 to        |ji                               rt        d>       t        d?       Y w xY wc c}(}w # 1 sw Y   yxY w)Gz(Restore a Hermes backup from a zip file.zError: File not found: r}   zError: Not a valid zip file: rzError: r   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   zImport complete: z files restored in rw   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': Fz
  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 updatez,
To re-enable gateway services for profiles:z  hermes -p z gateway installz2Done. Your Hermes configuration has been restored.)8r   r   r.   r0   rD   r   r   r   
is_zipfiler
   r   r   r   r   rL   r   r   r/   forceinputstripr   EOFErrorKeyboardInterruptr   r   r   r   
startswithr   r   r2   r4   r   openr   readr   rJ   _SECRET_FILE_NAMESrG   chmodr1   r   _IMPORT_SKIP_NAMESr   rF   hermes_cli.profilesr   r   r   r   iterdirjoinImportErrorany)+r   zip_pathr   r   okreasonr   r   membersr   
has_confighas_envanswerr   restoredrestored_externalskipped_runtimer   r   memberext_reltargetra   rb   r8   r   r   r   profiles_dirrestored_profilesr   r   r   r   entryprofile_name	collisionwrappercreatedskipped_gw_profilespnames+                                              r>   
run_importr    s0	   DLL!,,.668H'z23h'-hZ89)+K	3	' KD2)"-
FGF8$%HHQK# kkmC1::c?1CC\
 F34,./01-fZ7JKL "M199;
'//1'4::GOPQRG1288:@@B \)j!CKD KDH 	ZL
34$6%'99;&&(^^ H	>F   !12 %5!6!78!G+NN$00:8MM''t'D .Cfd1C .s		#((*-. . }}(BBfkkUgFg!HHVU3 MH%*% c>Q&Bxj*Z@A &++F3S[\* Cy~~!33&&s+ 3&F ,,[-@-@-BC
1##D4#@WWV_ *T&$-? *3IIchhj)* *;;"44HHVU+A #~"8*Aj\<=QH	>T .."R' 	!(+>wsm1MN
.0123 12 366I6K5LAO
 N3v;-/?@ACR[ a6{R
3v;#3"4E:; _!5 6 7C D o.s3 $SEl#$?#b(S%9B%>$?uEF #Z/ #S  $L$8$8$:; VE <<> #(::L!M199;UV^D[D[D]  5l CI  1,s9+NO)00,1FG"7"E)00,t@S1TUV %.?FUQ2qFGF.?JUQrqJGJ >tyy?Q>RST <TYYw=O<PQR24
+;+=*>>STUSTHI 	n,446STDE):;A1;K;AB$ >UG+;<=> 	BCWKD KD D( /0 l#< " MMBvh.F"GH
. . . .  ' ! ! (1 8MMBvhb"6778<  3%'?@A* * * *
 $W- 13%r#/001z GJ  S|++-.STQR	S <MKD KDs  =Ae!_%_)B#e!'_$4e!Be!$`.a)2a? `?	a'(a)a&
a)0A7e!(-b.cc c	1c95c.e!8Ee!Cd	
ddd 
d+d/A2d!:e!e'/e!e!$/`e!`e!`<8e!;`<<e!?a	aaa)	a&"a)%a&&a))b8be!be!b?;e!>b??e!ccccd+de!de!d8ee!e	e!!e*)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                 0    | xs
 t               }|t        z  S N)r   _QUICK_SNAPSHOTS_DIRr  r   s     r>   _quick_snapshot_rootr"  !  s    +/+D&&&r\   labelkeepmax_file_sizec                    |xs
 t               }t        |      }dt        dt        dt        ffd}t        j                  t        j                        j                  d      }| r| d|  n|}||z  }	|	j                  dd       i }
t        D ]  }||z  }|j                         s|j                         r|j                  d	      D ]  }|j                         s|j!                  |      j#                         }d
d| dv sdd| dv rD |||      rN|	|z  }|j$                  j                  dd       	 |j&                  dk(  rt)        ||      st+        j,                  ||       |j/                         j0                  |
|<    |j                         s |||      r|	|z  }|j$                  j                  dd       	 |j&                  dk(  rt)        ||      s[t+        j,                  ||       |j/                         j0                  |
|<    |
st+        j:                  |	d       y||| t=        |
      t?        |
jA                               |
d}tC        |	dz  dd      5 }tE        jF                  ||d       ddd       tI        ||tJ        n|       t6        jM                  d|t=        |
             |S # t2        t4        f$ r"}t6        j9                  d||       Y d}~.d}~ww xY w# t2        t4        f$ r"}t6        j9                  d||       Y d}~d}~ww xY w# 1 sw Y   xY w)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           
          y	 | j                         j                  }|k  ryt        d| dt	        |       dt	               d       t
        j                  d||       y# t        $ r Y yw xY w)z<True (and warn) when ``path`` exceeds the max_file_size cap.Fu     ⚠ 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(  sizer%  s      r>   
_too_largez)create_quick_snapshot.<locals>._too_largeA  s     	99;&&D = 'z 2T"#9\--H,IR	
 	G		
   		s   A& &	A21A2z%Y%m%d-%H%M%S-Tr   *z/workspaces/r   z/attachments/r   zCould not snapshot %s: %sN)ignore_errors)id	timestampr#  r   
total_sizerN   manifest.jsonr   utf-8encoding   )indentr$  z%State snapshot created: %s (%d files))'r   r"  r   ri   boolr   r   r   utcr   r   _QUICK_STATE_FILESr/   rF   rglobrD   r   r   r   r   rp   shutilcopy2r   r   r1   r   r+   r,   rmtreer   sumvaluesr   jsondump_prune_quick_snapshots_QUICK_DEFAULT_KEEPinfo)r#  r  r$  r%  r   rootr+  tssnap_idsnap_dirmanifestr   ra   subsub_relrb   r8   metafs      `               r>   create_quick_snapshotrP  &  s   0 +/+D%D   , 
hll	#	,	,_	=B!&AeWoBGg~HNN4$N/!H! 5BSjzz|::< yy~ N{{}//$/88: "q	^3AgYVW.7Xc7+(

   =N zzU*,S#6$S#.(+
(:(:HW%-N2 {{}c3n

5	BzzU"$S#.S#&HHJ..HSMg5Bn hd3 (m(//+,D 
h(#	@ %A		$!$% 4T\&9tT
KK7#h-PN[  1 NNN#>MMN( ) 	BNN6SAA	B % %sH   <K3KL+3L5L5K>K99K>L2L--L25L>limitc                    t        |      }|j                         sg S g }t        |j                         d      D ]t  }|j	                         s|dz  }|j                         r;	 t        |d      5 }|j                  t        j                  |             ddd       t        |      | k\  ss |S  |S # 1 sw Y   xY w# t        j                  t        f$ r" |j                  |j                  ddd       Y [w xY w)	z7List existing quick state snapshots, most recent first.T)reverser2  r3  r4  Nr   )r/  r   r1  )r"  r/   r   r   rF   r   r4   rB  loadJSONDecodeErrorr1   rJ   r   )rQ  r  rG  resultsrS   manifest_pathrO  s          r>   list_quick_snapshotsrX    s    
  ,D;;=	GDLLND1 xxzO+!Q-': 1aNN499Q<01 w<5 N N1 1(('2 QaffAQOPQs*   %C 2%B4C 4B=	9C  8C;:C;snapshot_idc                    |xs
 t               }t        |      }| rd| v sd| v s| dv rt        j                  d|        y|| z  }	 |j	                         j                  |j	                                |j                         sy|dz  }|j                         syt        |d	      5 }t        j                  |      }d
d
d
       d}j                  di       D ]0  }	||	z  }
	 |
j	                         j                  |j	                                ||	z  }	 |j	                         j                  |j	                                |
j                         s{|j                  j                  dd       	 |j                  dk(  rn|j                  d|j                    dz  }t#        j$                  |
|       |j'                  d       t#        j(                  t+        |      t+        |             nt#        j$                  |
|       |dz  }3 t        j1                  d||        |dkD  S # t        $ r t        j                  d|        Y yw xY w# 1 sw Y   xY w# t        $ r t        j                  d|	       Y w xY w# t        $ r t        j                  d|	       Y w xY w# t,        t.        f$ r"}t        j                  d|	|       Y d
}~d
}~ww xY w)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: %sr2  r3  r4  Nr   rN   z#Manifest path traversal blocked: %sTr   r   r   z.snap_restorere   r}   zFailed to restore %s: %sz"Restored %d files from snapshot %s)r   r"  r+   errorr0   r   r2   rF   r/   r   rB  rT  getr   r   r   rJ   r=  r>  rl   moveri   r1   r   rF  )rY  r  r   rG  rJ  rW  rO  rN  r  r   ra   rb   r   r8   s                 r>   restore_quick_snapshotr_    s    +/+D%D #,0C{VaGa.<k!H&&t||~6
 ??.M!	mg	. !yy| Hxx$  ?n	KKM%%h&6&6&89
 Sj	KKM%%dlln5
 zz|

5	?zzU"jjQsxxj#>>S#&

d
+CHc#h/S#&MH= ?D KK4hLa<e  A;O   	LL>D	  	LL>D	& ) 	?LL3S#>>	?s[   -H( &I"-I-J 1BJ&(I
	I
II=<I= J#"J#&K5KKr  r'  c                    | j                         sy	 t        | dd      5 }t        j                  |      }ddd       t        t              r/|j                  dg       }t        |t              rt        |      S dS t        |t              rt        |      S y# 1 sw Y   dxY w# t        t        j
                  f$ r Y yw xY w)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-sigr4  jobs)rD   r   rB  rT  r1   rU  
isinstancedictr]  listr   )r'  rO  datara  s       r>   _count_cron_jobsrf    s     <<>
 $k2 	 a99Q<D	  $xx#&tT2s4y<<$4y	  	 T))* s'   B& BB& B#B& &CCc                    | sy|xs
 t               }|t        z  }t        |      }|yt        |      | z  t        z  }t        |      }|sy||k\  ry	 |j                  j                  dd       t        j                  ||       t        j                  d|| ||       d|| dS # t        t        f$ r }t        j                  d|       Y d}~yd}~ww xY w)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_countrY  )r   _CRON_JOBS_RELrf  r"  r   r   r=  r>  r1   r   r+   r\  r,   )rY  r  r   	live_path
live_count	snap_path
snap_countr8   s           r>   restore_cron_jobs_if_emptiedrn  8  s    @ +/+D~%I!),J $T*[8>II!),J Ztd;Y	* NN	Y :kRR _% NPS	
 	s   3B% %C4CCrG  c                 ,   | j                         syt        d | j                         D        d d      }d}||d D ]  }	 t        j                  |       |dz  } |S # t
        $ r+}t        j                  d|j                  |       Y d}~Qd}~ww xY w)	zKRemove oldest quick snapshots beyond the keep limit. Returns count deleted.r   c              3   B   K   | ]  }|j                         s|  y wr  )rF   ).0rS   s     r>   	<genexpr>z)_prune_quick_snapshots.<locals>.<genexpr>  s     1qahhj1s   c                     | j                   S r  rJ   )rS   s    r>   <lambda>z(_prune_quick_snapshots.<locals>.<lambda>  
    aff r\   TkeyrS  Nr}   zFailed to prune snapshot %s: %s)	r/   r   r   r=  r?  r1   r+   r,   rJ   )rG  r$  dirsdeletedrS   r8   s         r>   rD  rD    s    ;;=1DLLN1D G$%[ K	KMM!qLGK N  	KNN<affcJJ	Ks   A	B(!BBc                 .    t        t        |      |       S )z6Manually prune quick snapshots. Returns count deleted.r8  )rD  r"  )r$  r  s     r>   prune_quick_snapshotsr|    s    
 ""6{"C$OOr\   c                     t        | dd      }t        |      }|rJt        d|        t               }t        dt	        |       dt                d       t        d|        yt        d	       y)
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.)getattrrP  r   rX  r   r   )r   r#  rI  snapss       r>   run_quick_backupr    sp    D'4(E#%0G(	23$&3u:,56I6K5LL]^_27)<=12r\   r   c           	         g }	 t        j                  |d      D ]l  \  }}}t        |      }|D cg c]  }|t        vs| c}|dd |D ]:  }||z  }		 |	j	                  |      }
t        |	|
|       r(|j                  |	|
f       < n 	 |syd}	 t        j                  | dt        j                  d      5 }|D ]  \  }}	 |j                  dk(  rt        j                   ddt#        | j$                        	      5 }t        |j&                        }ddd       	 t)        |      s-t        j                  d
|       d}	 |j+                  d        nM|j-                  |t#        |             |j+                  d       n|j-                  |t#        |              ddd       |r	 | j+                  d       y| S c c}w # t
        $ r Y w xY w# t        $ r }t        j                  d|       Y d}~yd}~ww xY w# 1 sw Y   xY w# j+                  d       w xY w# t.        t        t
        f$ r"}t        j1                  d||       Y d}~d}~ww xY w# 1 sw Y   xY w# t        $ rC}t        j                  d|       	 | j+                  d       n# t        $ r Y nw xY wY d}~yd}~ww xY w# t        $ r Y yw xY w)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   Nz Full-zip backup: walk failed: %sr   r   r   r   r   z6Full-zip backup aborted: SQLite snapshot failed for %sTre   r   zSkipping %s in zip backup: %sz%Full-zip backup: zip write failed: %s)rG   rH   r   rI   r   r2   r`   r4   r1   r+   r,   r   r   r   r   r   r   ri   r   rJ   rp   rl   r   r   debug)r^   r   r   rO   rP   rQ   rR   rS   rT   rU   r   r8   sqlite_snapshot_failedr   r]   rW   r   r   s                     r>   _write_full_zip_backupr    s    -/L,.GGKU,S 	2(GXygB&.J!>2I1JHQK" 
2U
++K8C ,E3A##UCL1
2	2( "$__XsG,@,@PQR 	VX&2 "(%/
 &88#(C<P 4 %)#((^F4
;#06#B &$\$,!" :> 6 % #MMTM: HHVS]HC"MMTM:3x=A/	J 	OOtO, OE K "   93?"4 4 #MMTM: (*= LL!@(CP3	 	8  >D	OOtO, 		  		s#  -G GGG G%%G 'I* 8	I:H%<HH%$H H%IH0/H%I!I* ,J9 G 	GG GG 	G?G::G?HH%H""H%%I9IIIII'#I* *	J63J1
JJ1	J)&J1(J))J11J69	KKr   zpre-update-   c                 0    | xs
 t               }|t        z  S r  )r   _PRE_UPDATE_BACKUPS_DIRr!  s     r>   _pre_update_backup_dirr    s    +/+D)))r\   
backup_dirc                 :   t        |d      }| j                         syt        d | j                         D        d d      }d}||d D ]  }	 |j	                          |dz  } |S # t
        $ r+}t        j                  d|j                  |       Y d}~Ld}~ww xY w)	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   | ]R  }|j                         r@|j                  j                  t              r!|j                  j                         d k(  r| T ywr   N)rD   rJ   r   _PRE_UPDATE_PREFIXr   r   rq  r<   s     r>   rr  z,_prune_pre_update_backups.<locals>.<genexpr>&  sG      	bqIIKAFF--.@AahhnnFVZ`F` 
 	b   AAc                     | j                   S r  rt  r<   s    r>   ru  z+_prune_pre_update_backups.<locals>.<lambda>(  rv  r\   Trw  NzFailed to prune backup %s: %s	maxr/   r   r   rl   r1   r+   r,   rJ   r  r$  r   rz  r<   r8   s         r>   _prune_pre_update_backupsr    s     tQ<D	bJ&&( 	b	G GTU^ I	IHHJqLGI N  	INN:AFFCHH	I   A&&	B/!BBc                 v   | xs
 t               }|j                         syt        |      }	 |j                  dd       t        j                         j                  d      }|t         | dz  }t        ||      }|yt        ||       |S # t        $ r!}t
        j                  d||       Y d}~yd}~ww xY w)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   r8  )r
   rF   r  r   r1   r+   r,   r   r   r   r  r  r  r  r$  r   r  r8   r   r^   results           r>   create_pre_update_backupr  7  s     :!8!:K'4J5
 LLN##$56E125'>>H#Hk:F~jt4O  F
TWX   B 	B8B33B8zpre-migration-c                 :   t        |d      }| j                         syt        d | j                         D        d d      }d}||d D ]  }	 |j	                          |dz  } |S # t
        $ r+}t        j                  d|j                  |       Y d}~Ld}~ww xY w)	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   | ]R  }|j                         r@|j                  j                  t              r!|j                  j                         d k(  r| T ywr  )rD   rJ   r   _PRE_MIGRATION_PREFIXr   r   r  s     r>   rr  z/_prune_pre_migration_backups.<locals>.<genexpr>n  sG      	eqIIKAFF--.CDIY]cIc 
 	er  c                     | j                   S r  rt  r  s    r>   ru  z._prune_pre_migration_backups.<locals>.<lambda>p  rv  r\   Trw  Nr}   z+Failed to prune pre-migration backup %s: %sr  r  s         r>   _prune_pre_migration_backupsr  c  s     tQ<D	eJ&&( 	e	G GTU^ W	WHHJqLGW N  	WNNH!&&RUVV	Wr  c                 v   | xs
 t               }|j                         syt        |      }	 |j                  dd       t        j                         j                  d      }|t         | dz  }t        ||      }|yt        ||       |S # t        $ r!}t
        j                  d||       Y d}~yd}~ww xY w)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   r8  )r
   rF   r  r   r1   r+   r,   r   r   r   r  r  r  r  s           r>   create_pre_migration_backupr    s    " :!8!:K (4J5
 LLN##$56E45eWDAAH#Hk:F~ $7O  I:WZ[r  )r$   Nr  )NNNN)r  N)G__doc__rB  loggingrG   r=  rg   r   r   r   r   r   r   pathlibr   typingr   r   r   r	   hermes_constantsr
   r   r   	getLogger__name__r+   rI   rM   rK   r   r   r   r?   rV   r9  r[   r`   rp   intri   r{   r   r   tupler   r   r  r;  r   rE  r"  rP  rX  r_  ri  rf  rn  rD  r|  r  r  r  r  _PRE_UPDATE_DEFAULT_KEEPr  r  r  r  _PRE_MIGRATION_DEFAULT_KEEPr  r  ry   r\   r>   <module>r     s     	   
    '  , , Z Z			8	$..  <  7    3d 3lt T
 ,d t 2t t t PT (t $ 4 D  p@nW__ tSy1A 6w 3 0YDR > )  'htn ' '  "&#'	EC=E$E 3-E C=	E
 c]ER "&$ 
$sCx.8 #'II$I 
I\ "4 HSM B #'ISIS$IS d38nISX 4G  S 3 . $"&P
P$P 	P
3"NT N N$ Nj $ "  * *$ *
"$ "c "c "L #'(!$!
! d^!P )  T   : #'+&$&
& d^&r\   