// docs/desktop-coverage.md from cli/lib/desktop-coverage.ts, checked against the code: a command is "covered" only // when every method it names exists (serve.ts describeMethods(), or a #[tauri::command] of src-tauri) and the window // calls it (the method quoted in apps/desktop/src, or listed in TOOLBOX, which the More tools tab renders). // `bun tools/gen-desktop-coverage.ts` writes the page; `--check` exits 1 when it is stale. tests/desktop-coverage.test.ts // runs analyze() too. import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { COMMANDS, PLAIN } from '../cli/commands'; import { DESKTOP_COVERAGE, OWNER_STEPS, TOOLBOX } from '../cli/lib/desktop-coverage'; import { describeMethods } from '../cli/serve'; const ROOT = join(import.meta.dir, '..'); const OUT = join(ROOT, 'docs/desktop-coverage.md'); function files(dir: string, out: string[] = []): string[] { for (const f of readdirSync(dir)) { const p = join(dir, f); if (statSync(p).isDirectory()) { if (f !== 'dev' && f !== 'i18n') files(p, out); } else if (/\.(ts|tsx|rs)$/.test(f)) out.push(p); } return out; } export interface Row { command: string; status: 'covered' | 'gap' | 'not-for-desktop'; where: string; methods: string[]; owner?: string; problems: string[] } export function analyze(): { rows: Row[]; extra: string[] } { const src = files(join(ROOT, 'apps/desktop/src')).map(f => readFileSync(f, 'utf8')).join('\n'); const rust = files(join(ROOT, 'apps/desktop/src-tauri/src')).map(f => readFileSync(f, 'utf8')).join('\n'); const hostCmds = new Set([...rust.matchAll(/#\[tauri::command\][\s\S]{0,200}?fn ([a-z_]+)/g)].map(m => m[1]!)); const methods = new Set(describeMethods().map(m => m.method)); const toolboxShown = /\bTOOLBOX\b/.test(src); const inToolbox = new Set(TOOLBOX.flatMap(g => g.methods)); const reachable = (m: string): string | null => { if (m.startsWith('host:')) { const c = m.slice(5); if (!hostCmds.has(c)) return `${m}: no such tauri command`; return src.includes(`'${c}'`) ? null : `${m}: never invoked by the window`; } if (!methods.has(m)) return `${m}: no such serve method`; if (src.includes(`'${m}'`) || src.includes(`"${m}"`)) return null; if (toolboxShown && inToolbox.has(m)) return null; return `${m}: no window place calls it`; }; const all = [...Object.keys(PLAIN), ...Object.keys(COMMANDS)]; const rows: Row[] = all.map(command => { const c = DESKTOP_COVERAGE[command]; if (!c) return { command, status: 'gap', where: '', methods: [], problems: ['no entry in cli/lib/desktop-coverage.ts'] }; if ('not' in c) return { command, status: 'not-for-desktop', where: c.not, methods: [], problems: c.not.trim() ? [] : ['not-for-desktop without a reason'] }; const problems = c.methods.map(reachable).filter((p): p is string => !!p); if (!c.methods.length && !(c.owner && toolboxShown && OWNER_STEPS.length)) problems.push('no method and no owner step'); return { command, status: problems.length ? 'gap' : 'covered', where: c.where, methods: c.methods, ...(c.owner ? { owner: c.owner } : {}), problems }; }); return { rows, extra: Object.keys(DESKTOP_COVERAGE).filter(k => !all.includes(k)) }; } export function render(rows: Row[]): string { const n = (s: Row['status']): number => rows.filter(r => r.status === s).length; const cell = (s: string): string => s.replace(/\|/g, '\\|'); return [ '# Desktop coverage', '', 'Generated by `bun tools/gen-desktop-coverage.ts` from `cli/lib/desktop-coverage.ts`; do not edit by hand.', 'Every CLI command (cli/commands.ts) and the desktop place that runs it. "covered" means each method listed exists', '(serve.ts, or a host command `host:`) and the window calls it; tests/desktop-coverage.test.ts fails on a gap.', 'Owner-only parts are owner actions in the window or owner steps with their terminal command (More tools).', '', `**${rows.length} commands:** ${n('covered')} covered, ${n('gap')} gaps, ${n('not-for-desktop')} not for the desktop.`, '', '| command | status | where in the window | methods | owner step |', '|---|---|---|---|---|', ...rows.map(r => `| \`${r.command}\` | ${r.status} | ${cell(r.where || r.problems.join('; '))} | ${r.methods.map(m => `\`${m}\``).join(', ')} | ${cell(r.owner ?? '')} |`), '', ].join('\n'); } if (import.meta.main) { const { rows } = analyze(); const md = render(rows); if (process.argv.includes('--check')) { let cur = ''; try { cur = readFileSync(OUT, 'utf8'); } catch { /* missing */ } if (cur !== md) { console.error('docs/desktop-coverage.md is stale: run bun tools/gen-desktop-coverage.ts'); process.exit(1); } } else writeFileSync(OUT, md); const gaps = rows.filter(r => r.status === 'gap'); for (const g of gaps) console.log(`gap: ${g.command}: ${g.problems.join('; ')}`); console.log(`${rows.length} commands, ${rows.filter(r => r.status === 'covered').length} covered, ${gaps.length} gaps`); }