123456module cli78import json29import os10import time11import src.fmt12import src.history13import src.storage1415pub fn cmd_doctor(raw []string) int {16 args := parse('doctor', raw, Options{17 values: ['backup']18 flags: ['json', 'yes', 'fast']19 }) or { return usage_error(err.msg()) }20 if args.positional.len > 0 {21 return usage_error('doctor takes no positional arguments')22 }23 store := open_store() or {24 report := DoctorJson{25 health: 'unhealthy'26 root: storage.find_root(os.getwd()) or { os.getwd() }27 problems: [err.msg()]28 checks: [DoctorCheck{'database open', 'fail', err.msg()}]29 scope: 'database could not be opened; further checks could not run'30 }31 if args.json() { emit_json(json2.encode(report)) } else { print_doctor(report) }32 return 133 }34 defer { store.close() }35 mut report := DoctorJson{36 root: store.root37 database: os.join_path(store.root, storage.file_name)38 schema_version: storage.schema_version39 checked_at: time.now().unix()40 koh_version: version41 }42 working('checking database, objects and all stored history')43 store.db.exec('BEGIN') or { return refuse(args.json(), err.msg()) }44 report.check('SQLite integrity', store.integrity_check() or { [err.msg()] })45 report.check('object hashes and sizes', store.check_objects() or { [err.msg()] })46 report.check('save object references', store.check_save_files() or { [err.msg()] })47 doctor_local(store, mut report) or { report.check('local metadata', [err.msg()]) }48 store.end() or { report.check('read transaction', [err.msg()]) }49 working('reading working files without cached hashes')50 doctor_working_tree(store, mut report) or { report.check('working tree', [err.msg()]) }51 doctor_build(store.root, mut report)52 if args.flag('yes') {53 report.peek_folders_removed = sweep_peek_folders()54 }55 if path := args.value_or_none('backup') {56 mut backed_up := true57 store.backup(path) or {58 report.check('backup', [err.msg()])59 backed_up = false60 }61 if backed_up {62 report.backup = path63 }64 }65 report.size_bytes = os.file_size(report.database)66 report.wal_bytes = os.file_size('${report.database}-wal')67 doctor_remote_config(mut report)68 if args.flag('fast') {69 report.checks << DoctorCheck{'remote audit', 'skipped', '--fast requested; remote content not checked'}70 } else {71 working('auditing remote history and releases (read-only)')72 doctor_remote(store, mut report) or { report.check('remote audit', [err.msg()]) }73 }74 report.scope = 'repository data and configuration only; not application tests, backup recovery or remote write access'75 report.health = if report.problems.len > 0 {76 'unhealthy'77 } else if report.warnings.len > 0 { 'warning' } else { 'healthy' }78 if args.json() { emit_json(json2.encode(report)) } else { print_doctor(report) }79 return if report.problems.len > 0 { 1 } else { 0 }80}8182fn (mut report DoctorJson) check(name string, problems []string) {83 report.checks << DoctorCheck{name, if problems.len == 0 { 'pass' } else { 'fail' }, if problems.len == 0 {84 'verified'85 } else {86 problems.join('; ')87 }}88 for problem in problems {89 if problem !in report.problems { report.problems << problem }90 }91}9293fn print_doctor(report DoctorJson) {94 root := report.root.replace_once(os.home_dir(), '~')95 if report.problems.len > 0 {96 warn('${root} has ${fmt.plural(report.problems.len, 'problem')}')97 } else if report.warnings.len > 0 {98 ok('${root} is sound · ${fmt.plural(report.warnings.len, 'warning')}')99 } else {100 ok('${root} is healthy')101 }102 println('')103 if report.metadata_available {104 doctor_metadata(report)105 } else {106 println(' ${fmt.dim('repository metadata unavailable; see failed checks')}')107 }108 println('')109 passed := report.checks.filter(it.status == 'pass').map(it.name)110 if passed.len > 0 {111 ok('${fmt.plural(passed.len, 'check')} passed')112 for line in wrap_items(passed, 76) {113 println(' ${fmt.dim(line)}')114 }115 }116 for check in report.checks.filter(it.status != 'pass') {117 mark := match check.status {118 'fail' { fmt.red(fmt.glyph_fail) }119 'skipped' { fmt.dim('–') }120 else { fmt.amber('!') }121 }122 println(' ${mark} ${check.name} ${fmt.dim('· ${check.detail}')}')123 }124 for warning in report.warnings {125 warn(warning)126 }127 if report.peek_folders_removed > 0 {128 ok('removed ${report.peek_folders_removed} old peek folders')129 }130 if report.backup != '' { ok('backed up to ${report.backup}') }131 if report.problems.len > 0 {132 note('keep .koh intact before recovering history; use a known-good backup or another verified copy')133 }134 println('')135 note(report.scope)136}137138fn doctor_metadata(report DoctorJson) {139 if report.head.id != '' {140 doctor_row('head', '[${report.head.id}] ${report.head.message.all_before('\n')}')141 doctor_row('', fmt.dim([report.head.agent, report.head.session,142 fmt.stamp(report.head.created_at), fmt.plural(report.head_files, 'file')].filter(it != '').join(' · ')))143 } else {144 doctor_row('head', if report.saves == 0 { 'no saves yet' } else { report.head_id })145 }146 mut saves := ['${report.saves} saves', '${report.checkpoints} checkpoints']147 if report.off_line_saves > 0 { saves << '${report.off_line_saves} off line' }148 if report.events > 0 { saves << fmt.plural(report.events, 'event') }149 if report.flagged_saves > 0 { saves << '${report.flagged_saves} flagged' }150 doctor_row('history', saves.join(' · '))151 doctor_row('storage', '${fmt.human_bytes(i64(report.size_bytes))} + ${fmt.human_bytes(i64(report.wal_bytes))} WAL · schema ${report.schema_version}')152 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'))153 changes := report.changes154 counts := ['${changes.added.len} added', '${changes.modified.len} modified',155 '${changes.removed.len} removed'].filter(!it.starts_with('0 '))156 mut tree := if counts.len == 0 { 'no changes' } else { counts.join(', ') }157 if report.cache_cleared {158 tree += fmt.dim(' · ${report.cached_files} cached hashes re-read')159 }160 doctor_row('tree', tree)161 if report.remote_target != '' {162 offer := if report.last_offered_head != '' {163 fmt.dim(' · last offer [${report.last_offered_head}]')164 } else {165 ''166 }167 doctor_row('remote', report.remote_target + offer)168 }169 if report.source != '' { doctor_row('source', '${report.source} (read-only)') }170 if report.project_version != '' {171 doctor_row('build', '${report.project_version} · ${report.build_platforms.join(', ')}')172 }173}174175fn doctor_row(label string, value string) {176 println(' ${fmt.dim('${label:-9}')}${value}')177}178179fn wrap_items(items []string, width int) []string {180 mut lines := []string{}181 mut line := ''182 for item in items {183 if line != '' && fmt.visible_width(line) + 3 + item.len > width {184 lines << line185 line = item186 } else {187 line = if line == '' { item } else { '${line} · ${item}' }188 }189 }190 if line != '' {191 lines << line192 }193 return lines194}195196fn shell_quote(text string) string {197 return "'${text.replace("'", "'\\''")}'"198}199200const peek_age = 86400201202fn sweep_peek_folders() int {203 mut removed := 0204 for name in os.ls(os.temp_dir()) or { return 0 } {205 path := os.join_path(os.temp_dir(), name)206 info := os.lstat(path) or { continue }207 if !name.starts_with(peek_prefix) || os.is_link(path) || !os.is_dir(path)208 || info.uid != u32(os.getuid()) || !peek_is_old(os.file_last_mod_unix(path), time.now().unix()) {209 continue210 }211 os.rmdir_all(path) or { continue }212 removed++213 }214 return removed215}216217// A recent peek may still be open in an editor.218fn peek_is_old(modified i64, now i64) bool {219 return modified < now - peek_age220}221222struct DoctorCheck {223 name string224 status string225 detail string226}227228struct DoctorJson {229mut:230 health string231 scope string232 metadata_available bool233 ignore_files map[string]int234 checks []DoctorCheck235 warnings []string236 problems []string237 root string238 database string239 schema_version int240 checked_at i64241 koh_version string242 head_id string243 head history.Save244 head_files int245 objects int246 saves int247 checkpoints int248 line_saves int249 off_line_saves int250 events int251 flagged_saves int252 head_flagged bool253 file_entries i64254 size_bytes u64255 wal_bytes u64256 object_bytes i64257 packed_bytes i64258 unreferenced_objects i64259 restore_unfinished bool260 pending_strips []string261 changes Changes262 ignored_saved_files []string263 build_platforms []string264 project_version string265 source string266 remote_repo string267 remote_target string268 last_offered_head string269 remote_head string270 remote_objects int271 remote_saves int272 remote_releases int273 cached_files int274 cache_cleared bool275 peek_folders_removed int276 backup string277}