#!/usr/bin/env node /** * Manifest scanner CLI (v5 N2) — produce a ranked list of every token source * loaded for a given repo path. Built on top of readActiveConfig so the source * inventory is identical to whats-active; this CLI flattens and ranks them. * * Output JSON shape: * { * meta: { repoPath, generatedAt, durationMs }, * sources: [ * { kind: 'claude-md'|'plugin'|'skill'|'mcp-server'|'hook', * name: string, source: string, estimated_tokens: number }, * ... * ], * total: * } * * Usage: * node manifest.mjs [path] [--json] [--output-file ] * * Exit codes: 0=ok, 3=unrecoverable error. * Zero external dependencies. */ import { resolve } from 'node:path'; import { writeFile, stat } from 'node:fs/promises'; import { readActiveConfig } from './lib/active-config-reader.mjs'; /** * Flatten an activeConfig snapshot into a single ranked array of sources. */ export function buildManifest(activeConfig) { const sources = []; for (const f of activeConfig.claudeMd?.files || []) { const tokens = estimateClaudeMdEntryTokens(f, activeConfig); sources.push({ kind: 'claude-md', name: f.path, source: f.scope, estimated_tokens: tokens, }); } for (const p of activeConfig.plugins || []) { sources.push({ kind: 'plugin', name: p.name, source: p.path, estimated_tokens: p.estimatedTokens || 0, }); } for (const s of activeConfig.skills || []) { sources.push({ kind: 'skill', name: s.name, source: s.pluginName ? `plugin:${s.pluginName}` : s.source || 'user', estimated_tokens: s.estimatedTokens || 0, }); } for (const m of activeConfig.mcpServers || []) { if (m && m.enabled === false) continue; sources.push({ kind: 'mcp-server', name: m.name, source: m.source || 'unknown', estimated_tokens: m.estimatedTokens || 0, }); } for (const h of activeConfig.hooks || []) { sources.push({ kind: 'hook', name: `${h.event}${h.matcher ? `:${h.matcher}` : ''}`, source: h.source || h.sourcePath || 'unknown', estimated_tokens: h.estimatedTokens || 0, }); } sources.sort((a, b) => b.estimated_tokens - a.estimated_tokens); const total = sources.reduce((s, x) => s + (x.estimated_tokens || 0), 0); return { sources, total }; } /** * Distribute the cascade-level estimated tokens across the individual files * proportional to their byte size. claudeMd.estimatedTokens is computed for * the cascade as a whole, but for ranking we want per-file figures. */ function estimateClaudeMdEntryTokens(file, activeConfig) { const totalBytes = activeConfig.claudeMd?.totalBytes || 0; const totalTokens = activeConfig.claudeMd?.estimatedTokens || 0; if (totalBytes === 0 || totalTokens === 0) return 0; const share = (file.bytes || 0) / totalBytes; return Math.round(totalTokens * share); } async function main() { const args = process.argv.slice(2); let targetPath = '.'; let outputFile = null; let jsonMode = false; // --raw is accepted for CLI surface consistency but is a no-op here: // manifest produces a token-source inventory, not findings. let rawMode = false; for (let i = 0; i < args.length; i++) { if (args[i] === '--json') jsonMode = true; else if (args[i] === '--raw') rawMode = true; else if (args[i] === '--output-file' && args[i + 1]) outputFile = args[++i]; else if (!args[i].startsWith('-')) targetPath = args[i]; } const absPath = resolve(targetPath); try { const s = await stat(absPath); if (!s.isDirectory()) { process.stderr.write(`Error: ${absPath} is not a directory\n`); process.exit(3); } } catch { process.stderr.write(`Error: path does not exist: ${absPath}\n`); process.exit(3); } const start = Date.now(); const activeConfig = await readActiveConfig(absPath, { verbose: true }); const manifest = buildManifest(activeConfig); const output = { meta: { tool: 'config-audit:manifest', repoPath: absPath, generatedAt: new Date().toISOString(), durationMs: Date.now() - start, }, sources: manifest.sources, total: manifest.total, }; const json = JSON.stringify(output, null, 2); if (outputFile) { await writeFile(outputFile, json, 'utf-8'); } if (jsonMode || rawMode || !outputFile) { process.stdout.write(json + '\n'); } } const isDirectRun = process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname); if (isDirectRun) { main().catch(err => { process.stderr.write(`Fatal: ${err.message}\n`); process.exit(3); }); }