UNPKG

@lenne.tech/cli

Version:

lenne.Tech CLI: lt

393 lines (392 loc) 18.3 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const child_process_1 = require("child_process"); const fs_1 = require("fs"); const path_1 = require("path"); const framework_detection_1 = require("../../lib/framework-detection"); // ──────────────────────────────────────────────────────────────────────────── // Helper functions (alphabetically sorted per ESLint rules) // ──────────────────────────────────────────────────────────────────────────── /** * Find the project root containing src/server/modules/ * (CLI-local version needed before scanner is loaded) */ function findProjectRoot(startPath) { let current = startPath; for (let i = 0; i < 10; i++) { if ((0, fs_1.existsSync)((0, path_1.join)(current, 'src', 'server', 'modules'))) { return current; } const parent = (0, path_1.join)(current, '..'); if (parent === current) break; current = parent; } return null; } /** * Minimal markdown fallback if scanner.generateMarkdownReport is not available * (only happens with nest-server versions that export scanPermissions but not generateMarkdownReport) */ function generateFallbackMarkdown(report, projectPath) { return JSON.stringify(Object.assign(Object.assign({}, report), { project: projectPath }), null, 2); } /** * Generate HTML report via EJS template */ function generateHtml(report, projectPath, templateFn) { return templateFn.generate({ props: { generated: report.generated, modules: report.modules, objects: report.objects, projectPath, roleEnums: report.roleEnums, stats: report.stats, warnings: report.warnings, }, target: undefined, template: 'permissions/report.html.ejs', }); } /** * Generate JSON report */ function generateJson(report, projectPath) { return JSON.stringify({ generated: report.generated, modules: report.modules, objects: report.objects, project: projectPath, roleEnums: report.roleEnums, stats: report.stats, warnings: report.warnings, }, null, 2); } /** * Try to dynamically load the permissions scanner module. * * Strategy (in order of preference): * 1. Load from project's @lenne.tech/nest-server (>= 11.17.0) — single source of truth * 2. Use CLI's bundled fallback scanner (standalone copy using ts-morph) * * Returns an object with scanPermissions + generateMarkdownReport, or null if neither is available. */ function loadScanner(projectPath) { return __awaiter(this, void 0, void 0, function* () { // Build an ordered list of candidate paths, mode-aware: // // - Vendored projects: the framework code lives in src/core/ as // project-compiled TypeScript. After `nest build` the compiled output // lands under dist/src/core/modules/permissions/. Before the build, // the .ts source sits at src/core/modules/permissions/permissions-scanner.ts // (which Node's `require()` cannot load directly). We try the dist // variant first and fall back to the bundled CLI scanner if the // project hasn't been built yet. // - npm projects: classic path under node_modules/@lenne.tech/nest-server/dist. const scannerPaths = []; if ((0, framework_detection_1.isVendoredProject)(projectPath)) { scannerPaths.push((0, path_1.join)(projectPath, 'dist', 'src', 'core', 'modules', 'permissions', 'permissions-scanner'), (0, path_1.join)(projectPath, 'dist', 'src', 'core', 'modules', 'permissions', 'permissions-scanner.js'), (0, path_1.join)(projectPath, 'dist', 'core', 'modules', 'permissions', 'permissions-scanner'), (0, path_1.join)(projectPath, 'dist', 'core', 'modules', 'permissions', 'permissions-scanner.js')); } else { scannerPaths.push((0, path_1.join)(projectPath, 'node_modules', '@lenne.tech', 'nest-server', 'dist', 'core', 'modules', 'permissions', 'permissions-scanner'), (0, path_1.join)(projectPath, 'node_modules', '@lenne.tech', 'nest-server', 'dist', 'core', 'modules', 'permissions', 'permissions-scanner.js')); } for (const scannerPath of scannerPaths) { try { const mod = require(scannerPath); if (typeof mod.scanPermissions === 'function') { return { generateMarkdownReport: typeof mod.generateMarkdownReport === 'function' ? mod.generateMarkdownReport : null, scanPermissions: mod.scanPermissions, }; } } catch (_a) { // Not available at this path } } // Fallback: Use CLI's bundled scanner. Covers both "not yet built" (vendor // mode before `nest build`) and "framework not installed" (detached clone). try { const fallback = require('../../lib/fallback-scanner'); if (typeof fallback.scanPermissions === 'function') { return { generateMarkdownReport: typeof fallback.generateMarkdownReport === 'function' ? fallback.generateMarkdownReport : null, scanPermissions: fallback.scanPermissions, }; } } catch (_b) { // Fallback not available } return null; }); } /** * Open a file in the default browser/application */ function openFile(filePath) { const platform = process.platform; const cmd = platform === 'darwin' ? 'open' : platform === 'win32' ? 'start' : 'xdg-open'; (0, child_process_1.exec)(`${cmd} "${filePath}"`); } /** * Print console summary */ function printConsoleSummary(print, report) { print.info(''); print.info('Summary:'); print.info(''); const tableData = [['Module', 'Models', 'Inputs', 'Outputs', 'Ctrl', 'Resolver', 'Warn']]; for (const mod of report.modules) { const modWarnings = report.warnings.filter((w) => w.module === mod.name).length; tableData.push([ mod.name, String(mod.models.length), String(mod.inputs.length), String(mod.outputs.length), String(mod.controllers.length), String(mod.resolvers.length), String(modWarnings), ]); } print.table(tableData); print.info(`Endpoint Coverage: ${report.stats.endpointCoverage}% | Security Coverage: ${report.stats.securityCoverage}%`); if (report.objects.length > 0) { print.info(`SubObjects: ${report.objects.length}`); } if (report.warnings.length > 0) { print.info(''); print.warning(`${report.warnings.length} Warning(s):`); for (const w of report.warnings) { const fileName = w.file.split('/').pop() || w.file; print.warning(` [${w.type}] ${w.module}/${fileName}: ${w.details}`); } } } // ──────────────────────────────────────────────────────────────────────────── // Command // ──────────────────────────────────────────────────────────────────────────── /** * Scan server permissions and generate report */ const PermissionsCommand = { alias: ['p'], description: 'Scan server permissions', hidden: false, name: 'permissions', run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () { var _a, _b, _c, _d, _e, _f, _g, _h, _j; const { config, filesystem, parameters, print, print: { error, info, spin, success }, template, } = toolbox; // Handle --help-json flag if (toolbox.tools.helpJson({ aliases: ['p'], configuration: 'commands.server.permissions.*', description: 'Scan server permissions and generate report', name: 'permissions', options: [ { description: 'Project path containing src/server/modules/', flag: '--path', required: false, type: 'string', }, { default: 'html', description: 'Report output format', flag: '--format', required: false, type: 'string', values: ['md', 'json', 'html'], }, { description: 'Output file name', flag: '--output', required: false, type: 'string' }, { default: true, description: 'Open report in default application', flag: '--open', required: false, type: 'boolean', }, { default: false, description: 'Disable opening report', flag: '--no-open', required: false, type: 'boolean', }, { default: false, description: 'Print summary to console', flag: '--console', required: false, type: 'boolean', }, { default: false, description: 'Exit with code 1 if warnings found', flag: '--fail-on-warnings', required: false, type: 'boolean', }, { default: false, description: 'Skip all interactive prompts', flag: '--noConfirm', required: false, type: 'boolean', }, ], })) { return; } info('Scan server permissions'); // Hint for non-interactive callers toolbox.tools.nonInteractiveHint('lt server permissions --path <dir> --format <md|json|html> --noConfirm'); // Load configuration const ltConfig = config.loadConfig(); const permConfig = (_b = (_a = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _a === void 0 ? void 0 : _a.server) === null || _b === void 0 ? void 0 : _b.permissions; // Determine noConfirm config.getNoConfirm({ cliValue: parameters.options.noConfirm, commandConfig: permConfig, config: ltConfig, }); // Intelligent defaults based on TTY const isTTY = process.stdin.isTTY; const defaultFormat = isTTY ? 'html' : 'md'; const defaultOpen = !!isTTY; const defaultConsole = false; const defaultFailOnWarnings = false; // Resolve parameters with config priority const format = config.getValue({ cliValue: parameters.options.format, configValue: permConfig === null || permConfig === void 0 ? void 0 : permConfig.format, defaultValue: defaultFormat, }) || defaultFormat; const shouldOpen = (_d = config.getValue({ cliValue: (_c = parameters.options.open) !== null && _c !== void 0 ? _c : (parameters.options['no-open'] === true ? false : undefined), configValue: permConfig === null || permConfig === void 0 ? void 0 : permConfig.open, defaultValue: defaultOpen, })) !== null && _d !== void 0 ? _d : defaultOpen; const shouldConsole = (_e = config.getValue({ cliValue: parameters.options.console, configValue: permConfig === null || permConfig === void 0 ? void 0 : permConfig.console, defaultValue: defaultConsole, })) !== null && _e !== void 0 ? _e : defaultConsole; const failOnWarnings = (_g = config.getValue({ cliValue: (_f = parameters.options['fail-on-warnings']) !== null && _f !== void 0 ? _f : parameters.options.failOnWarnings, configValue: permConfig === null || permConfig === void 0 ? void 0 : permConfig.failOnWarnings, defaultValue: defaultFailOnWarnings, })) !== null && _g !== void 0 ? _g : defaultFailOnWarnings; // Determine project path let projectPath = config.getValue({ cliValue: parameters.options.path, configValue: permConfig === null || permConfig === void 0 ? void 0 : permConfig.path, }); if (!projectPath) { projectPath = findProjectRoot(filesystem.cwd()); } if (!projectPath) { error('No NestJS project found. Use --path <dir> to specify the project path.'); error('Expected: src/server/modules/ directory'); if (!parameters.options.fromGluegunMenu) process.exit(1); return 'permissions: project not found'; } info(`Project: ${projectPath}`); // Output file const defaultOutputName = `permissions.${format}`; const outputFile = config.getValue({ cliValue: parameters.options.output, configValue: permConfig === null || permConfig === void 0 ? void 0 : permConfig.output, defaultValue: defaultOutputName, }) || defaultOutputName; const outputPath = (0, path_1.join)(projectPath, outputFile); // Load scanner from project's @lenne.tech/nest-server const spinner = spin('Loading permissions scanner...'); const scanner = yield loadScanner(projectPath); if (!scanner) { spinner.fail('Permissions scanner not available'); error(''); error("Neither the project's @lenne.tech/nest-server scanner (>= 11.17.0) nor the CLI fallback scanner could be loaded."); error('Please ensure ts-morph is installed: npm install ts-morph'); if (!parameters.options.fromGluegunMenu) process.exit(1); return 'permissions: scanner not available'; } // Scan permissions spinner.text = 'Scanning modules...'; let report; try { report = scanner.scanPermissions(projectPath, { log: (msg) => { spinner.text = msg; }, warn: (msg) => { print.warning(` ${msg}`); }, }); } catch (err) { spinner.fail('Permissions scan failed'); error(String(err)); if (!parameters.options.fromGluegunMenu) process.exit(1); return 'permissions: scan failed'; } spinner.succeed(`Scanned ${report.stats.totalModules} modules, ${report.stats.totalSubObjects} objects, found ${report.stats.totalWarnings} warnings`); // Generate report const reportSpinner = spin(`Generating ${format} report...`); let reportContent; if (format === 'json') { reportContent = generateJson(report, projectPath); } else if (format === 'html') { reportContent = (yield generateHtml(report, projectPath, template)) || ''; if (!reportContent) { // Fallback: generate markdown if HTML template not available reportContent = ((_h = scanner.generateMarkdownReport) === null || _h === void 0 ? void 0 : _h.call(scanner, report, projectPath)) || generateFallbackMarkdown(report, projectPath); reportSpinner.warn('HTML template not found, falling back to markdown'); } } else { reportContent = ((_j = scanner.generateMarkdownReport) === null || _j === void 0 ? void 0 : _j.call(scanner, report, projectPath)) || generateFallbackMarkdown(report, projectPath); } // Write file filesystem.write(outputPath, reportContent); reportSpinner.succeed(`Report saved to ${outputFile}`); // Console summary if (shouldConsole) { printConsoleSummary(print, report); } // Open in browser if (shouldOpen && (format === 'html' || format === 'md')) { openFile(outputPath); info(`Report opened in default application`); } // Summary line (always shown) success(`${report.stats.totalModules} modules, ${report.stats.totalSubObjects} objects, ${report.stats.totalWarnings} warnings → ${outputFile}`); // Exit code for CI if (failOnWarnings && report.warnings.length > 0) { error(`${report.warnings.length} warning(s) found (--fail-on-warnings is active)`); if (!parameters.options.fromGluegunMenu) process.exit(1); } if (!parameters.options.fromGluegunMenu) { process.exit(); } return `permissions report generated: ${outputFile}`; }), }; exports.default = PermissionsCommand;