ubon
Version:
Security scanner for AI-generated apps (Cursor, Lovable, Windsurf, v0). Catches hardcoded secrets, prompt injection, hallucinated imports, Server Actions / Edge runtime mistakes, and the vibe-coded vulnerabilities traditional linters miss.
137 lines • 6.66 kB
JavaScript
;
/**
* `ubon doctor` — environment diagnostic. Mirrors `eslint --print-config`
* style: tells the user exactly what Ubon thinks about their environment so
* support requests can be debugged without back-and-forth.
*
* Outputs are intentionally cheap (no scan run): we only check that the
* binaries / optional deps Ubon depends on are reachable.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.runDoctor = runDoctor;
const fs_1 = require("fs");
const path_1 = require("path");
const child_process_1 = require("child_process");
const package_json_1 = __importDefault(require("../../package.json"));
function check(name, fn) {
return Promise.resolve()
.then(fn)
.catch((error) => ({ name, status: 'fail', detail: error?.message || String(error) }));
}
function tryExec(cmd, args = []) {
try {
// ubon-disable-next-line SEC026 argv-safe execFileSync wrapper for fixed doctor probes
return (0, child_process_1.execFileSync)(cmd, args, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
}
catch {
return null;
}
}
async function runDoctor(directory = process.cwd(), options = {}) {
const checks = [];
checks.push(check('Ubon version', async () => ({
name: 'Ubon version',
status: 'ok',
detail: `${package_json_1.default.version}`
})));
checks.push(check('Node.js', async () => {
const major = parseInt(process.versions.node.split('.')[0] ?? '0', 10);
const status = major >= 20 ? 'ok' : 'fail';
return {
name: 'Node.js',
status,
detail: `${process.versions.node}${major < 20 ? ' (Ubon v3 requires Node 20+)' : ''}`
};
}));
checks.push(check('Project package.json', async () => {
const pkgPath = (0, path_1.join)(directory, 'package.json');
if (!(0, fs_1.existsSync)(pkgPath)) {
return { name: 'Project package.json', status: 'warn', detail: 'no package.json detected — profile auto-detection limited' };
}
return { name: 'Project package.json', status: 'ok', detail: pkgPath };
}));
checks.push(check('git', async () => {
const v = tryExec('git', ['--version']);
return v
? { name: 'git', status: 'ok', detail: v }
: { name: 'git', status: 'warn', detail: 'git not on PATH — git-history scanner and --git-changed-since disabled' };
}));
checks.push(check('@modelcontextprotocol/sdk', async () => {
try {
require.resolve('@modelcontextprotocol/sdk/package.json');
return { name: '@modelcontextprotocol/sdk', status: 'ok', detail: 'installed — `ubon mcp` available' };
}
catch {
return {
name: '@modelcontextprotocol/sdk',
status: 'warn',
detail: 'not installed — install with `npm i -g @modelcontextprotocol/sdk` to enable `ubon mcp`'
};
}
}));
checks.push(check('puppeteer (deprecated)', async () => {
try {
require.resolve('puppeteer/package.json');
return {
name: 'puppeteer',
status: 'warn',
detail: 'installed — `--crawl-internal` works but is deprecated and slated for removal in v3.1'
};
}
catch {
return { name: 'puppeteer', status: 'ok', detail: 'not installed (recommended)' };
}
}));
if (options.agentHarness) {
checks.push(check('Cursor hooks', async () => {
const hooks = (0, path_1.join)(directory, '.cursor', 'hooks.json');
return (0, fs_1.existsSync)(hooks)
? { name: 'Cursor hooks', status: 'ok', detail: hooks }
: { name: 'Cursor hooks', status: 'warn', detail: 'not installed — run `ubon agent install --cursor --write`' };
}));
checks.push(check('Agent guidance', async () => {
const hasGuidance = (0, fs_1.existsSync)((0, path_1.join)(directory, 'AGENTS.md')) ||
(0, fs_1.existsSync)((0, path_1.join)(directory, 'CLAUDE.md')) ||
(0, fs_1.existsSync)((0, path_1.join)(directory, '.cursor', 'rules', 'ubon.mdc'));
return hasGuidance
? { name: 'Agent guidance', status: 'ok', detail: 'found repo-level agent instructions' }
: { name: 'Agent guidance', status: 'warn', detail: 'no AGENTS.md, CLAUDE.md, or .cursor/rules/ubon.mdc found' };
}));
checks.push(check('Pre-commit hook', async () => {
const preCommit = (0, path_1.join)(directory, '.pre-commit-config.yaml');
return (0, fs_1.existsSync)(preCommit)
? { name: 'Pre-commit hook', status: 'ok', detail: preCommit }
: { name: 'Pre-commit hook', status: 'warn', detail: 'not configured — run `ubon agent install --pre-commit --write`' };
}));
checks.push(check('Ubon cache ignore', async () => {
const gitignore = (0, path_1.join)(directory, '.gitignore');
const ignored = (0, fs_1.existsSync)(gitignore) && (0, fs_1.readFileSync)(gitignore, 'utf-8').split(/\r?\n/).includes('.ubon/');
return ignored
? { name: 'Ubon cache ignore', status: 'ok', detail: '.ubon/ is ignored' }
: { name: 'Ubon cache ignore', status: 'warn', detail: 'add .ubon/ to .gitignore for repo-local result caches' };
}));
checks.push(check('GitHub Ubon workflow', async () => {
const workflow = (0, path_1.join)(directory, '.github', 'workflows', 'ubon.yml');
return (0, fs_1.existsSync)(workflow)
? { name: 'GitHub Ubon workflow', status: 'ok', detail: workflow }
: { name: 'GitHub Ubon workflow', status: 'warn', detail: 'not configured — run `ubon agent install --github --write`' };
}));
}
const results = await Promise.all(checks);
const icon = { ok: '🪷', warn: '⚠️', fail: '❌' };
console.log('🪷 Ubon doctor');
console.log('─'.repeat(50));
for (const r of results) {
console.log(`${icon[r.status]} ${r.name.padEnd(28)} ${r.detail}`);
}
console.log('');
const fails = results.filter((r) => r.status === 'fail').length;
if (fails > 0) {
console.log(`${fails} blocker(s) — please address before running ubon.`);
process.exit(1);
}
}
//# sourceMappingURL=doctor.js.map