@lenne.tech/cli
Version:
lenne.Tech CLI: lt
395 lines (394 loc) • 17.9 kB
JavaScript
;
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 });
exports.help = void 0;
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
*/
exports.help = {
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',
},
],
};
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(exports.help)) {
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;