//! Repository checks, remote verification and explicit maintenance. //! //! Remote checks only read. Cache resets are automatic; --yes permits //! old-peek cleanup and --backup writes a consistent database copy. module cli import json2 import os import time import src.fmt import src.history import src.storage pub fn cmd_doctor(raw []string) int { args := parse('doctor', raw, Options{ values: ['backup'] flags: ['json', 'yes', 'fast'] }) or { return usage_error(err.msg()) } if args.positional.len > 0 { return usage_error('doctor takes no positional arguments') } store := open_store() or { report := DoctorJson{ health: 'unhealthy' root: storage.find_root(os.getwd()) or { os.getwd() } problems: [err.msg()] checks: [DoctorCheck{'database open', 'fail', err.msg()}] scope: 'database could not be opened; further checks could not run' } if args.json() { emit_json(json2.encode(report)) } else { print_doctor(report) } return 1 } defer { store.close() } mut report := DoctorJson{ root: store.root database: os.join_path(store.root, storage.file_name) schema_version: storage.schema_version checked_at: time.now().unix() koh_version: version } working('checking database, objects and all stored history') store.db.exec('BEGIN') or { return refuse(args.json(), err.msg()) } report.check('SQLite integrity', store.integrity_check() or { [err.msg()] }) report.check('object hashes and sizes', store.check_objects() or { [err.msg()] }) report.check('save object references', store.check_save_files() or { [err.msg()] }) doctor_local(store, mut report) or { report.check('local metadata', [err.msg()]) } store.end() or { report.check('read transaction', [err.msg()]) } working('reading working files without cached hashes') doctor_working_tree(store, mut report) or { report.check('working tree', [err.msg()]) } doctor_build(store.root, mut report) if args.flag('yes') { report.peek_folders_removed = sweep_peek_folders() } if path := args.value_or_none('backup') { mut backed_up := true store.backup(path) or { report.check('backup', [err.msg()]) backed_up = false } if backed_up { report.backup = path } } report.size_bytes = os.file_size(report.database) report.wal_bytes = os.file_size('${report.database}-wal') doctor_remote_config(mut report) if args.flag('fast') { report.checks << DoctorCheck{'remote audit', 'skipped', '--fast requested; remote content not checked'} } else { working('auditing remote history and releases (read-only)') doctor_remote(store, mut report) or { report.check('remote audit', [err.msg()]) } } report.scope = 'repository data and configuration only; not application tests, backup recovery or remote write access' report.health = if report.problems.len > 0 { 'unhealthy' } else if report.warnings.len > 0 { 'warning' } else { 'healthy' } if args.json() { emit_json(json2.encode(report)) } else { print_doctor(report) } return if report.problems.len > 0 { 1 } else { 0 } } fn (mut report DoctorJson) check(name string, problems []string) { report.checks << DoctorCheck{name, if problems.len == 0 { 'pass' } else { 'fail' }, if problems.len == 0 { 'verified' } else { problems.join('; ') }} for problem in problems { if problem !in report.problems { report.problems << problem } } } fn print_doctor(report DoctorJson) { root := report.root.replace_once(os.home_dir(), '~') if report.problems.len > 0 { warn('${root} has ${fmt.plural(report.problems.len, 'problem')}') } else if report.warnings.len > 0 { ok('${root} is sound · ${fmt.plural(report.warnings.len, 'warning')}') } else { ok('${root} is healthy') } println('') if report.metadata_available { doctor_metadata(report) } else { println(' ${fmt.dim('repository metadata unavailable; see failed checks')}') } println('') passed := report.checks.filter(it.status == 'pass').map(it.name) if passed.len > 0 { ok('${fmt.plural(passed.len, 'check')} passed') for line in wrap_items(passed, 76) { println(' ${fmt.dim(line)}') } } for check in report.checks.filter(it.status != 'pass') { mark := match check.status { 'fail' { fmt.red(fmt.glyph_fail) } 'skipped' { fmt.dim('–') } else { fmt.amber('!') } } println(' ${mark} ${check.name} ${fmt.dim('· ${check.detail}')}') } for warning in report.warnings { warn(warning) } if report.peek_folders_removed > 0 { ok('removed ${report.peek_folders_removed} old peek folders') } if report.backup != '' { ok('backed up to ${report.backup}') } if report.problems.len > 0 { note('keep .koh intact before recovering history; use a known-good backup or another verified copy') } println('') note(report.scope) } fn doctor_metadata(report DoctorJson) { if report.head.id != '' { doctor_row('head', '[${report.head.id}] ${report.head.message.all_before('\n')}') doctor_row('', fmt.dim([report.head.agent, report.head.session, fmt.stamp(report.head.created_at), fmt.plural(report.head_files, 'file')].filter(it != '').join(' · '))) } else { doctor_row('head', if report.saves == 0 { 'no saves yet' } else { report.head_id }) } mut saves := ['${report.saves} saves', '${report.checkpoints} checkpoints'] if report.off_line_saves > 0 { saves << '${report.off_line_saves} off line' } if report.events > 0 { saves << fmt.plural(report.events, 'event') } if report.flagged_saves > 0 { saves << '${report.flagged_saves} flagged' } doctor_row('history', saves.join(' · ')) doctor_row('storage', '${fmt.human_bytes(i64(report.size_bytes))} + ${fmt.human_bytes(i64(report.wal_bytes))} WAL · schema ${report.schema_version}') doctor_row('', fmt.dim('${report.objects} objects · ${fmt.human_bytes(report.object_bytes)} → ${fmt.human_bytes(report.packed_bytes)} compressed · ${report.file_entries} file entries')) changes := report.changes counts := ['${changes.added.len} added', '${changes.modified.len} modified', '${changes.removed.len} removed'].filter(!it.starts_with('0 ')) mut tree := if counts.len == 0 { 'no changes' } else { counts.join(', ') } if report.cache_cleared { tree += fmt.dim(' · ${report.cached_files} cached hashes re-read') } doctor_row('tree', tree) if report.remote_target != '' { offer := if report.last_offered_head != '' { fmt.dim(' · last offer [${report.last_offered_head}]') } else { '' } doctor_row('remote', report.remote_target + offer) } if report.source != '' { doctor_row('source', '${report.source} (read-only)') } if report.project_version != '' { doctor_row('build', '${report.project_version} · ${report.build_platforms.join(', ')}') } } fn doctor_row(label string, value string) { println(' ${fmt.dim('${label:-9}')}${value}') } fn wrap_items(items []string, width int) []string { mut lines := []string{} mut line := '' for item in items { if line != '' && fmt.visible_width(line) + 3 + item.len > width { lines << line line = item } else { line = if line == '' { item } else { '${line} · ${item}' } } } if line != '' { lines << line } return lines } fn shell_quote(text string) string { return "'${text.replace("'", "'\\''")}'" } const peek_age = 86400 fn sweep_peek_folders() int { mut removed := 0 for name in os.ls(os.temp_dir()) or { return 0 } { path := os.join_path(os.temp_dir(), name) info := os.lstat(path) or { continue } if !name.starts_with(peek_prefix) || os.is_link(path) || !os.is_dir(path) || info.uid != u32(os.getuid()) || !peek_is_old(os.file_last_mod_unix(path), time.now().unix()) { continue } os.rmdir_all(path) or { continue } removed++ } return removed } // A recent peek may still be open in an editor. fn peek_is_old(modified i64, now i64) bool { return modified < now - peek_age } struct DoctorCheck { name string status string detail string } struct DoctorJson { mut: health string scope string metadata_available bool ignore_files map[string]int checks []DoctorCheck warnings []string problems []string root string database string schema_version int checked_at i64 koh_version string head_id string head history.Save head_files int objects int saves int checkpoints int line_saves int off_line_saves int events int flagged_saves int head_flagged bool file_entries i64 size_bytes u64 wal_bytes u64 object_bytes i64 packed_bytes i64 unreferenced_objects i64 restore_unfinished bool pending_strips []string changes Changes ignored_saved_files []string build_platforms []string project_version string source string remote_repo string remote_target string last_offered_head string remote_head string remote_objects int remote_saves int remote_releases int cached_files int cache_cleared bool peek_folders_removed int backup string }