#!/usr/bin/env bare
import process from 'process'
import fs from 'fs'
import { resolve } from 'path'
import { scan } from './lib/scanner.js'
const args = process.argv.slice(2)
const command = args[0]
const pretty = args.includes('--pretty') || args.includes('-p')
if (!command || command === '--help' || command === '-h') {
printUsage()
process.exit(0)
}
// Commands that take a module name as args[1]
const MODULE_COMMANDS = ['module', 'exports', 'deps', 'file', 'imports']
const positionalArgs = args.filter(a => !a.startsWith('-') && a !== command)
const hasModuleArg = MODULE_COMMANDS.includes(command)
const dir = resolve(hasModuleArg ? (positionalArgs[1] || '.') : (positionalArgs[0] || '.'))
const opts = {}
if (args.includes('--depth')) {
const idx = args.indexOf('--depth')
opts.maxDepth = parseInt(args[idx + 1], 10) || 2
}
// --ignore "pattern1,pattern2" — extra ignore patterns (in addition to .codetreeignore
// and the built-in skip list). Commas separate patterns; use gitignore-style syntax.
if (args.includes('--ignore')) {
const idx = args.indexOf('--ignore')
const raw = args[idx + 1] || ''
opts.ignore = raw.split(',').map(s => s.trim()).filter(Boolean)
}
// --shallow — parse only the entry file per module. Drops AST analysis of the
// rest of the project; keeps module names, deps, and top-level exports.
// For multi-project recursive scans this is 10-100x faster.
if (args.includes('--shallow')) {
opts.shallow = true
}
// --no-watch — disable fs.watch in serve mode. Useful for huge trees that
// exceed the inotify watch limit (ENOSPC). Server still serves the snapshot.
if (args.includes('--no-watch')) {
opts.watch = false
}
// Cross-filesystem dep crawling (Phase 0b). CLI flags override .codetree.json.
// --auto-crawl enables, --no-auto-crawl disables, --sibling-dirs adds explicit
// extra scan roots (comma-separated).
const cli = {}
if (args.includes('--auto-crawl')) cli.autoCrawl = true
if (args.includes('--no-auto-crawl')) cli.autoCrawl = false
if (args.includes('--sibling-dirs')) {
const idx = args.indexOf('--sibling-dirs')
const raw = args[idx + 1] || ''
cli.siblingDirs = raw.split(',').map(s => s.trim()).filter(Boolean)
}
if (Object.keys(cli).length > 0) opts.cli = cli
// Suggest-fixes mode (Phase 0c).
// --suggest Print proposed patches, no writes.
// --apply Apply high-confidence fixes.
// --check Exit 1 if staleness/validator thresholds breach.
// --confidence TIER 'high' | 'medium' | 'low' (default 'high').
// --dry-run With --apply: show what would change, don't write.
// --format json|text Output format (default text).
// --threshold N Override maxStalenessScore in --check.
// --target PATH Restrict suggestions to a single spec file.
const suggestMode = args.includes('--suggest')
const applyMode = args.includes('--apply')
const checkMode = args.includes('--check')
const dryRun = args.includes('--dry-run')
let confidence = 'high'
if (args.includes('--confidence')) {
const idx = args.indexOf('--confidence')
const v = (args[idx + 1] || '').toLowerCase()
if (['high', 'medium', 'low'].includes(v)) confidence = v
}
let format = 'text'
if (args.includes('--format')) {
const idx = args.indexOf('--format')
const v = (args[idx + 1] || '').toLowerCase()
if (['text', 'json'].includes(v)) format = v
}
let cliThreshold = null
if (args.includes('--threshold')) {
const idx = args.indexOf('--threshold')
const raw = args[idx + 1]
const n = parseFloat(raw)
if (!isNaN(n)) cliThreshold = n
}
let targetFile = null
if (args.includes('--target')) {
const idx = args.indexOf('--target')
targetFile = args[idx + 1] || null
}
async function main () {
switch (command) {
case 'scan': return cmdScan()
case 'module': return cmdModule()
case 'file': return cmdFile()
case 'exports': return cmdExports()
case 'deps': return cmdDeps()
case 'imports': return cmdImports()
case 'docs': return cmdDocs()
case 'categories': return cmdCategories()
case 'disjuncts': return cmdDisjuncts()
case 'misfits': return cmdMisfits()
case 'derive': return cmdDerive()
case 'summary': return cmdSummary()
case 'serve': return cmdServe()
default:
console.error(`Unknown command: ${command}`)
printUsage()
process.exit(1)
}
}
async function cmdScan () {
const tree = await scan(dir, opts)
output(tree)
}
async function cmdModule () {
const name = args[1]
if (!name) {
console.error('Usage: codetree module <name> [dir]')
process.exit(1)
}
const tree = await scan(dir, opts)
const mod = tree.modules[name]
if (!mod) {
console.error(`Module not found: ${name}`)
console.error(`Available: ${tree.system.modules.join(', ')}`)
process.exit(1)
}
output(mod)
}
async function cmdFile () {
const filePath = args[1]
if (!filePath) {
console.error('Usage: codetree file <path> [dir]')
process.exit(1)
}
const tree = await scan(dir, opts)
for (const mod of Object.values(tree.modules)) {
if (mod.files[filePath]) {
output(mod.files[filePath])
return
}
}
console.error(`File not found: ${filePath}`)
process.exit(1)
}
async function cmdExports () {
const name = args[1]
if (!name) {
console.error('Usage: codetree exports <module> [dir]')
process.exit(1)
}
const tree = await scan(dir, opts)
const mod = tree.modules[name]
if (!mod) {
console.error(`Module not found: ${name}`)
process.exit(1)
}
if (pretty) {
console.log(`${mod.name}@${mod.version} public API:`)
for (const name of mod.publicAPI) {
console.log(` ${name}`)
}
console.log()
console.log('File exports:')
for (const [file, data] of Object.entries(mod.files)) {
if (data.exports.length === 0) continue
console.log(` ${file}:`)
for (const exp of data.exports) {
const type = exp.type ? ` (${exp.type})` : ''
const from = exp.from ? ` from ${exp.from}` : ''
console.log(` ${exp.name}${type}${from}`)
}
}
} else {
output({
module: mod.name,
publicAPI: mod.publicAPI,
fileExports: Object.fromEntries(
Object.entries(mod.files)
.filter(([_, f]) => f.exports.length > 0)
.map(([k, f]) => [k, f.exports])
)
})
}
}
async function cmdDeps () {
const name = args[1]
if (!name) {
console.error('Usage: codetree deps <module> [dir]')
process.exit(1)
}
const tree = await scan(dir, opts)
const mod = tree.modules[name]
if (!mod) {
console.error(`Module not found: ${name}`)
process.exit(1)
}
if (pretty) {
console.log(`${mod.name} dependencies:`)
for (const [dep, ver] of Object.entries(mod.dependencies)) {
const isLocal = tree.modules[dep] ? ' (local)' : ''
console.log(` ${dep}: ${ver}${isLocal}`)
}
console.log()
// Reverse deps
const reverseDeps = []
for (const [otherName, otherMod] of Object.entries(tree.modules)) {
if (otherMod.dependencies[name]) {
reverseDeps.push(otherName)
}
}
if (reverseDeps.length > 0) {
console.log(`Used by:`)
for (const rd of reverseDeps) {
console.log(` ${rd}`)
}
}
} else {
const reverseDeps = []
for (const [otherName, otherMod] of Object.entries(tree.modules)) {
if (otherMod.dependencies[name]) reverseDeps.push(otherName)
}
output({
module: mod.name,
dependencies: mod.dependencies,
usedBy: reverseDeps
})
}
}
async function cmdImports () {
const name = args[1]
if (!name) {
console.error('Usage: codetree imports <module> [dir]')
process.exit(1)
}
const tree = await scan(dir, opts)
const mod = tree.modules[name]
if (!mod) {
console.error(`Module not found: ${name}`)
console.error(`Available: ${tree.system.modules.join(', ')}`)
process.exit(1)
}
if (!mod.importGraph) {
console.error('No import graph data (no local imports found)')
process.exit(1)
}
if (pretty) {
const graph = mod.importGraph
console.log(`${mod.name} — internal architecture\n`)
// Directory dependency matrix
const dirs = Object.keys(graph.directories).sort()
if (dirs.length > 0) {
console.log('Directory dependency matrix:')
for (const srcDir of dirs) {
const targets = graph.directoryMatrix[srcDir]
if (!targets) continue
const pairs = Object.entries(targets)
.filter(([tgt]) => tgt !== srcDir)
.sort((a, b) => b[1] - a[1])
if (pairs.length === 0) continue
const targetStr = pairs.map(([tgt, count]) => `${tgt} (${count})`).join(', ')
console.log(` ${srcDir} -> ${targetStr}`)
}
console.log()
// Directory stats
console.log('Directory stats:')
for (const d of dirs) {
const s = graph.directories[d]
console.log(` ${d}: ${s.files} files, fan-in ${s.incomingEdges}, fan-out ${s.outgoingEdges}, internal ${s.internalEdges}`)
}
console.log()
}
// Sub-modules
if (mod.subModules && Object.keys(mod.subModules).length > 0) {
console.log('Discovered sub-modules:')
const sorted = Object.entries(mod.subModules).sort((a, b) => b[1].fanIn - a[1].fanIn)
for (const [dir, sm] of sorted) {
const idx = sm.indexFile ? ' [index.js]' : ''
const reexp = sm.hasReExports ? ' [re-exports]' : ''
console.log(` ${dir}${idx}${reexp}`)
console.log(` ${sm.files.length} files, fan-in ${sm.fanIn}, fan-out ${sm.fanOut}, cohesion ${sm.cohesion}`)
if (sm.publicExports.length > 0) {
console.log(` exports: ${sm.publicExports.join(', ')}`)
}
}
}
} else {
output({
module: mod.name,
importGraph: mod.importGraph,
subModules: mod.subModules
})
}
}
async function cmdDocs () {
const tree = await scan(dir, opts)
// --suggest / --apply / --check routes to the suggest-fixes engine (Phase 0c).
if (suggestMode || applyMode || checkMode) {
await runSuggestOrApplyOrCheck(tree)
return
}
if (!tree.docCruft) {
console.error('No documentation found')
process.exit(1)
}
const cruft = tree.docCruft
if (pretty) {
console.log(`Documentation cruft report\n`)
console.log(`Total: ${cruft.summary.totalDocs} docs, ${cruft.summary.totalLines} lines`)
console.log(`Fresh (score <= 0.3): ${cruft.summary.freshDocs}`)
console.log(`Stale (score > 0.5): ${cruft.summary.staleDocs}`)
console.log(`References: ${cruft.summary.validReferences} valid, ${cruft.summary.missingReferences} missing`)
console.log()
for (const doc of cruft.docs) {
if (doc.references.total === 0 && doc.staleness.score < 0.3) continue
const icon = doc.staleness.score > 0.5 ? 'STALE' : doc.staleness.score > 0.3 ? 'WARN ' : 'FRESH'
console.log(`${icon} ${doc.path} (${doc.lines} lines, modified ${doc.lastModified}, score ${doc.staleness.score})`)
if (doc.references.missing > 0) {
const missing = doc.references.details.filter(d => d.status === 'missing')
for (const m of missing.slice(0, 5)) {
console.log(` missing: ${m.ref} (${m.type}, line ${m.line})`)
}
if (missing.length > 5) {
console.log(` ... and ${missing.length - 5} more`)
}
}
}
} else {
output(cruft)
}
}
async function runSuggestOrApplyOrCheck (tree) {
const { suggestFixes, formatSuggest, formatSuggestJson, applyFixes } =
await import('./lib/suggest-fixes.js')
const { parseSpecs } = await import('./lib/doc-parser.js')
// Locate specDir (same logic scanner uses for validation)
const specDir = tree.docs && tree.docs.specDir
if (!specDir) {
console.error('No specs found in ' + dir + ' (expected spec/ or docs/spec/)')
process.exit(1)
}
const specs = parseSpecs(specDir)
const result = suggestFixes(tree, specs, {
confidenceFloor: confidence,
targetFile
})
// --check: threshold gate
if (checkMode) {
const config = tree.config || {}
const thresholds = config.thresholds || {}
const staleLimit = cliThreshold != null ? cliThreshold : (thresholds.maxStalenessScore != null ? thresholds.maxStalenessScore : 0.3)
const maxValidatorFailures = thresholds.maxValidatorFailures != null ? thresholds.maxValidatorFailures : 0
const maxMissingRatio = thresholds.maxMissingRatio != null ? thresholds.maxMissingRatio : 0.1
let fails = 0
const cruft = tree.docCruft
const validation = tree.validation
if (cruft) {
for (const doc of cruft.docs) {
if (doc.staleness.score > staleLimit) {
console.error('stale: ' + doc.path + ' score=' + doc.staleness.score + ' > ' + staleLimit)
fails++
}
if (doc.staleness.missingRatio > maxMissingRatio) {
console.error('missing-ratio: ' + doc.path + ' missingRatio=' + doc.staleness.missingRatio + ' > ' + maxMissingRatio)
fails++
}
}
}
if (validation && validation.summary && validation.summary.fail > maxValidatorFailures) {
console.error('validator: ' + validation.summary.fail + ' failures > ' + maxValidatorFailures)
fails++
}
if (fails > 0) {
console.error('\ncodetree --check FAILED (' + fails + ' threshold breaches)')
process.exit(1)
}
console.log('codetree --check PASSED')
console.log(' ' + (cruft ? cruft.docs.length : 0) + ' docs scanned')
console.log(' ' + (validation && validation.summary ? validation.summary.fail : 0) + ' validator failures (max: ' + maxValidatorFailures + ')')
console.log(' stale-threshold: ' + staleLimit + ', missing-ratio threshold: ' + maxMissingRatio)
return
}
// --apply: write fixes
if (applyMode) {
const applyResult = applyFixes(result, tree, { dryRun })
if (format === 'json') {
console.log(JSON.stringify(applyResult, null, 2))
} else {
console.log((dryRun ? '[dry-run] ' : '') + 'applied ' + applyResult.applied + ' fix' +
(applyResult.applied === 1 ? '' : 'es') + ' across ' + applyResult.files.length + ' file' +
(applyResult.files.length === 1 ? '' : 's'))
for (const f of applyResult.files) {
console.log(' ' + f.file + ' (' + f.changes + ' change' + (f.changes === 1 ? '' : 's') + ')')
}
const remaining = result.fixes.length - applyResult.applied
if (remaining > 0) {
console.log('\n' + remaining + ' fix' + (remaining === 1 ? '' : 'es') +
' deferred — run --suggest to see manual-review items.')
}
}
return
}
// --suggest: print ALL proposals (the --confidence flag affects `result.applicable`
// for --apply purposes; the printed suggestion view should show every tier so the
// operator can see what MANUAL REVIEW items are queued up).
if (format === 'json') {
console.log(formatSuggestJson(result))
} else {
console.log(formatSuggest(result, { confidenceFloor: 'low', appliedFloor: confidence }))
}
}
async function cmdCategories () {
const tree = await scan(dir, opts)
// Build the same category mapping the visualization uses — see render.js initGraph().
// Start with SYSTEM.md categories (if any), then fall back to shared-dep clustering.
const categories = tree.categories || {}
const moduleToCategory = {}
for (const [cat, mods] of Object.entries(categories)) {
for (const m of mods) moduleToCategory[m] = cat
}
const shared = tree.system.sharedDeps || {}
const depToMostShared = {}
for (const [dep, users] of Object.entries(shared)) {
for (const u of users) {
const prev = depToMostShared[u]
if (!prev || users.length > shared[prev].length) depToMostShared[u] = dep
}
}
for (const name of tree.system.modules) {
if (moduleToCategory[name]) continue
const topDep = depToMostShared[name]
moduleToCategory[name] = topDep ? (topDep + ' users') : 'Other'
}
// Invert for display: category → [modules]
const byCategory = {}
for (const [name, cat] of Object.entries(moduleToCategory)) {
if (!byCategory[cat]) byCategory[cat] = []
byCategory[cat].push(name)
}
if (pretty) {
console.log(`Module categories for ${tree.root}\n`)
const sorted = Object.entries(byCategory).sort((a, b) => b[1].length - a[1].length)
for (const [cat, names] of sorted) {
console.log(` ${cat} (${names.length})`)
for (const n of names.sort()) console.log(` ${n}`)
}
console.log()
const specNames = new Set()
for (const mods of Object.values(categories)) for (const m of mods) specNames.add(m)
const total = Object.keys(moduleToCategory).length
const fromSpec = [...Object.keys(moduleToCategory)].filter(m => specNames.has(m)).length
const autoDerived = total - fromSpec
console.log(`Source: ${fromSpec} from SYSTEM.md, ${autoDerived} auto-derived from shared dependencies`)
} else {
output({
source: Object.keys(categories).length > 0 ? 'mixed' : 'auto',
fromSystemMd: categories,
assignments: moduleToCategory,
byCategory
})
}
}
// One line per germ: its connector set. Diffable — a commit that changes a
// disjunct changed the architecture; one that doesn't, didn't.
async function cmdDisjuncts () {
const { buildSections } = await import('./lib/sections.js')
const tree = await scan(dir, opts)
const index = buildSections(tree)
const wantScale = args.includes('--scale') ? args[args.indexOf('--scale') + 1] : 'file'
const sections = index.sections.filter(s => s.scale === wantScale)
if (format === 'json') {
output(sections.map(s => ({ id: s.id, germ: s.germ, disjunct: index.disjunct(s) })))
return
}
for (const s of sections) console.log(index.disjunct(s))
}
// The connectors that found no partner. This is the whole point of the model:
// a dead export and a broken import are the same question asked twice.
async function cmdMisfits () {
const { buildSections } = await import('./lib/sections.js')
const tree = await scan(dir, opts)
const index = buildSections(tree)
const m = index.misfits()
if (format === 'json') {
output({
dead: m.dead.map(d => ({ germ: d.germ, type: d.type })),
unresolved: m.unresolved.map(u => ({ germ: u.germ, type: u.type, specifier: u.specifier })),
leak: m.leak,
unused: m.unused
})
return
}
console.log('DEAD exports (' + m.dead.length + ') — exported, nothing imports it')
for (const d of m.dead) console.log(' ' + d.germ + ' +' + d.type)
console.log('\nUNRESOLVED imports (' + m.unresolved.length + ') — imported, nothing supplies it')
for (const u of m.unresolved) {
console.log(' ' + u.germ + ' -' + u.type + ' (' + u.specifier + ')')
}
console.log('\nLEAKED deps (' + m.leak.length + ') — used in code, absent from package.json')
for (const l of m.leak) console.log(' ' + l.module + ' ' + l.specifier + ' (' + l.germ + ')')
console.log('\nUNUSED deps (' + m.unused.length + ') — in package.json, never imported')
for (const u of m.unused) console.log(' ' + u.module + ' ' + u.specifier)
const total = m.dead.length + m.unresolved.length + m.leak.length + m.unused.length
console.log('\n' + index.sections.length + ' sections, ' + total + ' misfits')
}
// The expression floor: one file's operations, wires, zones and exits.
// Spec: docs/EXPRESSION-FLOOR.md. This exists so the MODEL is verifiable before
// any renderer does — if the decomposition is wrong, it is wrong here first.
async function cmdDerive () {
const { deriveFile } = await import('./lib/derive.js')
const target = positionalArgs[0]
if (!target) {
console.error('derive needs a file: codetree derive <file.js>')
process.exit(1)
}
const path = resolve(target)
let source
try {
source = fs.readFileSync(path, 'utf-8')
} catch (err) {
console.error('cannot read ' + path + ': ' + (err.message || err))
process.exit(1)
}
const model = deriveFile(source, path)
if (format === 'json') {
output(model)
return
}
if (model.error) {
console.log(path + ' — PARSE ERROR: ' + model.error)
process.exit(1)
}
const want = (name) => {
const flags = ['--nodes', '--zones', '--exits', '--names', '--functions', '--symbols']
const asked = flags.filter(f => args.includes(f))
return asked.length === 0 || asked.includes('--' + name)
}
const rel = path.split('/').slice(-2).join('/')
console.log(rel + ' — ' + model.nodes.length + ' nodes · ' + model.zones.length +
' zones · ' + model.exits.length + ' exits · ' + model.names.length +
' names · ' + model.functions.length + ' functions' +
(model.language ? ' [' + model.language + ']' : ''))
// A partial model looks complete. Say so loudly, at the top (spec E7).
if (model.partial) {
console.log(' PARTIAL — ' + model.partialReason +
'; structure is missing, do not trust this model')
}
const zoneById = new Map(model.zones.map(z => [z.id, z]))
const line = (r) => String(r.startLine).padStart(4)
const clip = (text, n) => {
const flat = text.replace(/\s+/g, ' ')
return flat.length > n ? flat.slice(0, n - 1) + '\u2026' : flat
}
const landing = (l) => {
if (!l) return '?'
if (l.on === 'zone-entry' || l.on === 'zone-exit' || l.on === 'catch-entry') {
const z = zoneById.get(l.id)
return l.on + ' ' + (z ? z.shape + '@' + z.range.startLine : '?')
}
return l.on
}
// The file level: what a node would be if you opened this file (spec L1-L2).
if (want('symbols') && model.symbols && model.symbols.length > 0) {
console.log('\nFILE-LEVEL SYMBOLS')
for (const sym of model.symbols) {
// A const symbol has no interior of its own; a class has members; anything
// with a body reports what the container query returns for it.
const inside = sym.fn
? model.nodes.filter(n => n.container === sym.fn).length + ' ops, ' +
model.zones.filter(z => z.container === sym.fn).length + ' zones, ' +
model.names.filter(n => n.container === sym.fn).length + ' wires'
: (sym.members.length > 0 ? sym.members.length + ' members' : '')
console.log(' ' + line(sym.range) + ' ' + sym.kind.padEnd(13) +
sym.name.padEnd(26) + inside)
}
}
if (want('functions') && model.functions.length > 0) {
console.log('\nFUNCTIONS')
for (const fn of model.functions) {
console.log(' ' + line(fn.range) + ' ' + (fn.parent ? ' ' : '') +
fn.name + '(' + fn.params.join(', ') + ')' +
(fn.async ? ' async' : '') + (fn.parent ? ' (nested)' : '') +
' lines ' + fn.range.startLine + '-' + fn.range.endLine)
}
}
if (want('zones') && model.zones.length > 0) {
console.log('\nZONES')
for (const z of model.zones) {
const indent = ' '.repeat(z.depth)
const bits = []
if (z.item) bits.push('item=' + z.item)
if (z.carried.length > 0) bits.push('carried=' + z.carried.join(','))
if (z.bands) bits.push('bands=' + z.bands.length)
if (z.exits.length > 0) bits.push('exits=' + z.exits.length)
console.log(' ' + line(z.range) + ' ' + indent + z.shape.padEnd(13) +
(z.condition ? clip(z.condition.text, 34) : '').padEnd(35) +
'L' + z.range.startLine + '-' + z.range.endLine +
(bits.length > 0 ? ' ' + bits.join(' ') : ''))
}
}
if (want('names') && model.names.length > 0) {
console.log('\nNAMED WIRES')
for (const n of model.names) {
console.log(' ' + line(n.range) + ' ' + n.name.padEnd(22) + 'from ' + n.from)
}
}
if (want('nodes') && model.nodes.length > 0) {
console.log('\nOPERATIONS')
for (const n of model.nodes) {
const ins = n.inputs.map(i => {
if (i.literal) return i.name
if (i.source === 'node') return '<' + i.name + '>'
return i.name + '[' + i.source + ']'
})
console.log(' ' + line(n.range) + ' ' + n.label.padEnd(20) +
(n.writes ? '-> ' + n.writes + ' ' : '') +
(ins.length > 0 ? '<- ' + clip(ins.join(', '), 70) : ''))
}
}
if (want('exits') && model.exits.length > 0) {
console.log('\nEXITS')
for (const e of model.exits) {
console.log(' ' + line(e.range) + ' ' + e.kind.padEnd(10) + '-> ' + landing(e.lands))
}
}
}
async function cmdSummary () {
const tree = await scan(dir, opts)
console.log(`Scanned: ${tree.root}`)
console.log(`Modules: ${tree.system.modules.length}`)
console.log()
for (const name of tree.system.modules) {
const mod = tree.modules[name]
const fileCount = Object.keys(mod.files).length
const exportCount = mod.publicAPI.length
const deps = Object.keys(mod.dependencies).length
let classCount = 0
let methodCount = 0
for (const file of Object.values(mod.files)) {
if (file.classes) {
classCount += file.classes.length
for (const cls of file.classes) {
methodCount += cls.methods.length + cls.getters.length
}
}
}
console.log(` ${name}@${mod.version}`)
console.log(` ${fileCount} files, ${exportCount} exports, ${classCount} classes, ${methodCount} members, ${deps} deps`)
}
console.log()
const sharedCount = Object.keys(tree.system.sharedDeps || {}).length
console.log(`Shared dependencies: ${sharedCount}`)
}
async function cmdServe () {
// Defer to server module
const { startServer } = await import('./lib/server.js')
const tree = await scan(dir, opts)
const port = parseInt(args.find(a => /^\d{4,5}$/.test(a)) || '9078', 10)
const prefixIdx = args.indexOf('--prefix')
const prefix = prefixIdx >= 0 ? args[prefixIdx + 1] : ''
startServer(tree, { port, prefix, ignore: opts.ignore, graph: tree.__graph, watch: opts.watch })
}
function output (data) {
console.log(JSON.stringify(data, null, pretty ? 2 : undefined))
}
function printUsage () {
console.log(`codetree — structural code scanner
Usage:
codetree scan [dir] Full scan, outputs JSON tree
codetree module <name> [dir] Show one module's structure
codetree file <path> [dir] Show one file's structure
codetree exports <module> [dir] List public API surface
codetree deps <module> [dir] Show dependency chain
codetree imports <module> [dir] Show internal import graph and sub-modules
codetree docs [dir] Documentation cruft report
codetree categories [dir] Show module categorization (SYSTEM.md + auto)
codetree disjuncts [dir] One line per germ: its connector set
codetree misfits [dir] Dead exports, unresolved imports, leaked deps
codetree derive <file> Expression floor: operations, wires, zones, exits
codetree summary [dir] Human-readable overview
codetree serve [dir] [port] Start HTTP visualization
Options:
--pretty, -p Pretty-print JSON / human-readable output
--depth <n> Max directory depth for project discovery (default: 2)
--ignore <patterns> Extra ignore patterns, comma-separated, gitignore-style
--shallow Only parse each module's entry file (no cross-file analysis).
10-100x faster for multi-project/recursive scans.
--auto-crawl Enable cross-filesystem dep crawling (follows package.json
file:../foo deps + walks up 2 ancestors for sibling deps)
--no-auto-crawl Disable auto-crawl (overrides .codetree.json setting)
--sibling-dirs a,b Explicit extra scan roots, comma-separated, resolved
relative to the scan root
Docs subcommands (use with "codetree docs <dir>"):
--suggest Print proposed patches (no writes)
--apply Write high-confidence fixes
--check Exit 1 if staleness/validator thresholds breach
--confidence TIER high | medium | low (default: high)
--dry-run With --apply: show what would change without writing
--format TYPE text | json (default: text)
--threshold N Override maxStalenessScore for --check (e.g. 0.3)
--target PATH Restrict to one spec file (relative to project root)
Ignore sources (merged, in this order):
built-in defaults (node_modules, .git, vendor, .pear-git, .claude, build, dist)
.codetreeignore file at scan root
any directory containing a .codetree-private marker
--ignore CLI flag (this takes effect last)
Examples:
codetree scan . Scan current directory
codetree summary ../hyper-fleeky Summarize all modules
codetree exports hyper-orgs -p Show hyper-orgs public API
codetree imports pear-git -p Show pear-git internal architecture
codetree docs ../pear-git -p Find stale documentation
codetree derive lib/derive.js Show one file's expression-floor model
codetree serve . 9078 Serve visualization on port 9078`)
}
main().catch(err => {
console.error(err.message || err)
process.exit(1)
})