12345678module cli910import json211import os12import time13import src.fmt14import src.history15import src.storage1617pub fn cmd_doctor(raw []string) int {18 args := parse('doctor', raw, Options{19 values: ['backup']20 flags: ['json', 'yes', 'offline']21 }) or { return usage_error(err.msg()) }22 if args.positional.len > 0 {23 return usage_error('doctor takes no positional arguments')24 }25 store := open_store() or {26 report := DoctorJson{27 health: 'unhealthy'28 root: storage.find_root(os.getwd()) or { os.getwd() }29 problems: [err.msg()]30 checks: [DoctorCheck{'database open', 'fail', err.msg()}]31 scope: 'database could not be opened; further checks could not run'32 }33 if args.json() { emit_json(json2.encode(report)) } else { print_doctor(report) }34 return 135 }36 defer { store.close() }37 mut report := DoctorJson{38 root: store.root39 database: os.join_path(store.root, storage.file_name)40 schema_version: storage.schema_version41 checked_at: time.now().unix()42 koh_version: version43 }44 working('checking database, objects and all stored history')45 store.db.exec('BEGIN') or { return refuse(args.json(), err.msg()) }46 report.check('SQLite integrity', store.integrity_check() or { [err.msg()] })47 report.check('object hashes and sizes', store.check_objects() or { [err.msg()] })48 report.check('save object references', store.check_save_files() or { [err.msg()] })49 doctor_local(store, mut report) or { report.check('local metadata', [err.msg()]) }50 store.end() or { report.check('read transaction', [err.msg()]) }51 working('reading working files without cached hashes')52 doctor_working_tree(store, mut report) or { report.check('working tree', [err.msg()]) }53 doctor_build(store.root, mut report)54 if args.flag('yes') {55 report.peek_folders_removed = sweep_peek_folders()56 }57 if path := args.value_or_none('backup') {58 mut backed_up := true59 store.backup(path) or {60 report.check('backup', [err.msg()])61 backed_up = false62 }63 if backed_up {64 report.backup = path65 }66 }67 report.size_bytes = os.file_size(report.database)68 report.wal_bytes = os.file_size('${report.database}-wal')69 doctor_remote_config(mut report)70 if !args.flag('offline') {71 working('auditing remote history and releases (read-only)')72 doctor_remote(store, mut report) or { report.check('remote audit', [err.msg()]) }73 } else {74 report.checks << DoctorCheck{'remote audit', 'skipped', '--offline requested; remote content not checked'}75 }76 report.scope = 'repository data and configuration; application tests, backup recovery and remote write permissions are not verified'77 report.health = if report.problems.len > 0 {78 'unhealthy'79 } else if report.warnings.len > 0 { 'warning' } else { 'healthy' }80 if args.json() { emit_json(json2.encode(report)) } else { print_doctor(report) }81 return if report.problems.len > 0 { 1 } else { 0 }82}8384fn (mut report DoctorJson) check(name string, problems []string) {85 report.checks << DoctorCheck{name, if problems.len == 0 { 'pass' } else { 'fail' }, if problems.len == 0 {86 'verified'87 } else {88 problems.join('; ')89 }}90 for problem in problems {91 if problem !in report.problems { report.problems << problem }92 }93}9495fn print_doctor(report DoctorJson) {96 if report.problems.len == 0 {97 ok('${report.root} is sound · local health: ${report.health}')98 } else {99 warn('${report.root} has ${fmt.plural(report.problems.len, 'problem')}')100 }101 note('database: ${fmt.human_bytes(i64(report.size_bytes))} + ${fmt.human_bytes(i64(report.wal_bytes))} WAL · schema ${report.schema_version}')102 if report.metadata_available {103 if report.head.id != '' {104 note('head [${report.head.id}] ${report.head.message.all_before('\n')} · ${fmt.stamp(report.head.created_at)}')105 note('${report.head_files} files at head · agent: ${report.head.agent} · session: ${report.head.session}')106 } else {107 note('head: ${if report.saves == 0 { 'no saves yet' } else { report.head_id }}')108 }109 note('${report.saves} stored saves · ${report.line_saves} on line · ${report.off_line_saves} off line · ${report.checkpoints} checkpoints')110 note('${report.objects} objects · ${fmt.human_bytes(report.object_bytes)} content · ${fmt.human_bytes(report.packed_bytes)} compressed')111 note('${report.events} events · ${report.flagged_saves} flagged saves · ${report.file_entries} file entries')112 } else {113 note('repository metadata unavailable; see failed checks')114 }115 for check in report.checks {116 println(' ${check.status}: ${check.name} · ${check.detail}')117 }118 for warning in report.warnings {119 warn(warning)120 }121 if report.remote_target != '' { note('remote: ${report.remote_target}') }122 if report.source != '' { note('read-only source: ${report.source}') }123 if report.last_offered_head != '' {124 note('last successful offer: [${report.last_offered_head}] (stored value)')125 }126 if report.project_version != '' {127 note('project ${report.project_version} · ${report.build_platforms.join(', ')}')128 }129 if report.cache_cleared {130 note('cleared ${report.cached_files} cached hashes; working files read again')131 }132 if report.peek_folders_removed > 0 {133 note('removed ${report.peek_folders_removed} old peek folders')134 }135 if report.backup != '' { ok('backed up to ${report.backup}') }136 if report.problems.len > 0 {137 note('keep .koh intact before recovering history; use a known-good backup or another verified copy')138 }139 note('scope: ${report.scope}')140}141142fn shell_quote(text string) string {143 return "'${text.replace("'", "'\\''")}'"144}145146147const peek_age = 86400148149150151fn sweep_peek_folders() int {152 mut removed := 0153 for name in os.ls(os.temp_dir()) or { return 0 } {154 path := os.join_path(os.temp_dir(), name)155 info := os.lstat(path) or { continue }156 if !name.starts_with(peek_prefix) || os.is_link(path) || !os.is_dir(path)157 || info.uid != u32(os.getuid()) || !peek_is_old(os.file_last_mod_unix(path), time.now().unix()) {158 continue159 }160 os.rmdir_all(path) or { continue }161 removed++162 }163 return removed164}165166167168fn peek_is_old(modified i64, now i64) bool {169 return modified < now - peek_age170}171172struct DoctorCheck {173 name string174 status string175 detail string176}177178struct DoctorJson {179mut:180 health string181 scope string182 metadata_available bool183 ignore_files map[string]int184 checks []DoctorCheck185 warnings []string186 problems []string187 root string188 database string189 schema_version int190 checked_at i64191 koh_version string192 head_id string193 head history.Save194 head_files int195 objects int196 saves int197 checkpoints int198 line_saves int199 off_line_saves int200 events int201 flagged_saves int202 head_flagged bool203 file_entries i64204 size_bytes u64205 wal_bytes u64206 object_bytes i64207 packed_bytes i64208 unreferenced_objects i64209 restore_unfinished bool210 pending_strips []string211 changes Changes212 ignored_saved_files []string213 build_platforms []string214 project_version string215 source string216 remote_repo string217 remote_target string218 last_offered_head string219 remote_head string220 remote_objects int221 remote_saves int222 remote_releases int223 cached_files int224 cache_cleared bool225 peek_folders_removed int226 backup string227}