cmd_doctor.v

9.0 KB · raw

1//! Repository checks, remote verification and explicit maintenance.2//!3//! Remote checks only read. Cache resets are automatic; --yes permits4//! old-peek cleanup and --backup writes a consistent database copy.56module 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}