Files
zkxw/.agents/skills/personal-dev/scripts/pda/updates.py
zlwu 42301730ef chore(agents): 更新个人智能体至官方 main eb4c320
固定协议 6 安装内容与来源摘要,保留试点工程配置及既有授权,为合入 V2.0 提供可复现版本。
2026-11-02 11:59:53 +08:00

127 lines
5.2 KiB
Python

"""Per-project update notifications and explicit, pinned installation."""
import json
import time
from . import remote
from .install import MANIFEST, plan, show, show_next_steps
from .source import CHANNEL, REPOSITORY, identity, revision
from .transaction import (InstallError, apply_changes, atomic_write, ensure_ready,
json_bytes, locked, read)
STATE = '.local/agent/updates/state.json'
DAY = 86400
def installed_source(root):
raw = read(root, MANIFEST)
if raw is None:
return None
value = json.loads(raw).get('source')
if (not isinstance(value, dict) or value.get('repository') != REPOSITORY
or value.get('channel') != CHANNEL or value.get('commit') is None):
return None
revision(value['commit'])
return value
def state_for(root, source):
raw = read(root, STATE)
if raw is None:
return {'version': 1, 'source': source}
value = json.loads(raw)
if not isinstance(value, dict) or value.get('version') != 1:
raise InstallError('invalid update state')
if value.get('source') != source:
return {'version': 1, 'source': source}
for key in ('checked_at', 'deferred_until'):
if key in value and (type(value[key]) not in (int, float) or value[key] < 0):
raise InstallError('invalid update timestamp')
for key in ('candidate', 'skipped'):
if value.get(key) is not None:
revision(value[key])
return value
def save(root, state):
atomic_write(root, STATE, json_bytes(state), 0o600)
def check_update(root, force=False, now=None):
now = time.time() if now is None else now
with locked(root):
ensure_ready(root)
source = installed_source(root)
if source is None:
return {'status': 'unknown_source', 'message': 'Use a reviewed clean checkout to init once; keep current rules.'}
state = state_for(root, source)
if not force and now < state.get('deferred_until', 0):
return {'status': 'deferred', 'until': state['deferred_until']}
elapsed = now - state.get('checked_at', -DAY)
if not force and 0 <= elapsed < DAY:
return {'status': 'cached', 'previous_status': state.get('status'), 'message': 'Continue with installed rules.'}
state.pop('candidate', None)
state.pop('deferred_until', None)
result = {'installed': source['commit']}
try:
target = remote.latest()
relation = remote.compare(source['commit'], target) if target != source['commit'] else {'status': 'identical', 'summary': [], 'url': ''}
status = {'ahead': 'available', 'behind': 'ahead', 'identical': 'current', 'diverged': 'diverged'}[relation['status']]
if status == 'available':
state['candidate'] = target
if state.get('skipped') == target:
status = 'skipped'
result.update(candidate=target, summary=relation['summary'], compare_url=relation['url'])
result['status'] = status
except (InstallError, OSError, ValueError, KeyError, TypeError) as exc:
result.update(status='unavailable', message='Cannot check GitHub; keep installed rules.', error=type(exc).__name__)
state.update(checked_at=now, status=result['status'])
save(root, state)
return result
def pending(root, target):
revision(target)
source = installed_source(root)
if source is None:
raise InstallError('unknown installed source; bootstrap with a reviewed installer')
state = state_for(root, source)
if state.get('candidate') != target:
raise InstallError('revision was not offered for this installed version; check-update first')
return source, state
def choose(root, choice, target, now=None):
now = time.time() if now is None else now
if choice not in ('skip', 'later'):
raise InstallError('unknown update choice')
with locked(root):
ensure_ready(root)
source, state = pending(root, target)
if choice == 'skip':
state['skipped'] = target
state.pop('deferred_until', None)
else:
state['deferred_until'] = now + DAY
save(root, state)
return {'status': 'skipped' if choice == 'skip' else 'deferred', 'candidate': target}
def update(root, target, apply=False):
source, _ = pending(root, target)
original_manifest = read(root, MANIFEST)
with remote.download(target) as package:
changes = plan(root, package, source=identity(target))
show(changes)
if not apply:
print('Preview only. Use update --revision ' + target + ' --apply after choosing this version.')
return
with locked(root):
current, _ = pending(root, target)
if (current != source or read(root, MANIFEST) != original_manifest
or plan(root, package, source=identity(target)) != changes):
raise InstallError('project changed after preview; check-update again')
if changes:
apply_changes(root, changes)
print('Updated to ' + target + '. Start a new task to load new rules. recover can restore this transaction.')
show_next_steps(root, updated=True)