@lenne.tech/cli
Version:
lenne.Tech CLI: lt
349 lines (348 loc) • 13.4 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 });
const path_1 = require("path");
/**
* Known configuration keys based on LtConfig interface
* Used for validation against unknown keys
*/
const KNOWN_KEYS = {
commands: {
blocks: {
add: { noConfirm: 'boolean' },
},
cli: {
create: { author: 'string', link: 'boolean', noConfirm: 'boolean' },
},
components: {
add: { noConfirm: 'boolean' },
},
config: {
init: { noConfirm: 'boolean' },
},
deployment: {
domain: 'string',
gitHub: 'boolean',
gitLab: 'boolean',
noConfirm: 'boolean',
prodRunner: 'string',
testRunner: 'string',
},
directus: {
dockerSetup: {
database: ['postgres', 'mysql', 'sqlite'],
name: 'string',
noConfirm: 'boolean',
port: 'number',
version: 'string',
},
remove: { noConfirm: 'boolean' },
typegen: { noConfirm: 'boolean', output: 'string', token: 'string', url: 'string' },
},
frontend: {
angular: { branch: 'string', copy: 'string', link: 'string', localize: 'boolean', noConfirm: 'boolean' },
nuxt: { branch: 'string', copy: 'string', link: 'string' },
},
fullstack: {
apiBranch: 'string',
apiCopy: 'string',
apiLink: 'string',
apiMode: ['Rest', 'GraphQL', 'Both'],
frameworkMode: ['npm', 'vendor'],
frontend: ['angular', 'nuxt'],
frontendBranch: 'string',
frontendCopy: 'string',
frontendFrameworkMode: ['npm', 'vendor'],
frontendLink: 'string',
git: 'boolean',
gitLink: 'string',
noConfirm: 'boolean',
},
git: {
baseBranch: 'string',
clean: { noConfirm: 'boolean' },
clear: { noConfirm: 'boolean' },
create: { base: 'string', noConfirm: 'boolean' },
defaultBranch: 'string',
forcePull: { noConfirm: 'boolean' },
get: { mode: ['hard'], noConfirm: 'boolean' },
noConfirm: 'boolean',
rebase: { base: 'string', noConfirm: 'boolean' },
rename: { noConfirm: 'boolean' },
reset: { noConfirm: 'boolean' },
squash: { author: 'string', base: 'string', noConfirm: 'boolean' },
undo: { noConfirm: 'boolean' },
update: { skipInstall: 'boolean' },
},
npm: {
reinit: { noConfirm: 'boolean', update: 'boolean' },
},
server: {
addProp: { skipLint: 'boolean' },
create: {
apiMode: ['Rest', 'GraphQL', 'Both'],
author: 'string',
branch: 'string',
controller: ['Rest', 'GraphQL', 'Both', 'auto'],
copy: 'string',
description: 'string',
git: 'boolean',
link: 'string',
noConfirm: 'boolean',
},
module: {
controller: ['Rest', 'GraphQL', 'Both', 'auto'],
noConfirm: 'boolean',
skipLint: 'boolean',
},
object: { skipLint: 'boolean' },
permissions: {
console: 'boolean',
failOnWarnings: 'boolean',
format: ['md', 'json', 'html'],
noConfirm: 'boolean',
open: 'boolean',
output: 'string',
path: 'string',
},
},
tools: {
crawl: {
concurrency: 'number',
depth: 'number|all',
includeImages: 'boolean',
includeSitemap: 'boolean',
maxPages: 'number',
noConfirm: 'boolean',
out: 'string',
prune: 'boolean',
renderJs: 'boolean',
selector: 'string',
timeout: 'number',
},
noConfirm: 'boolean',
},
typescript: {
create: { author: 'string', noConfirm: 'boolean', updatePackages: 'boolean' },
},
},
defaults: {
apiMode: ['Rest', 'GraphQL', 'Both'],
author: 'string',
baseBranch: 'string',
controller: ['Rest', 'GraphQL', 'Both', 'auto'],
domain: 'string',
noConfirm: 'boolean',
packageManager: ['npm', 'pnpm', 'yarn'],
skipInstall: 'boolean',
skipLint: 'boolean',
},
meta: {
// meta allows additional properties
_additionalProperties: true,
description: 'string',
name: 'string',
tags: 'array',
version: 'string',
},
};
/**
* Validate a config object against known keys
*/
function validateConfig(config, knownKeys, path = '') {
const result = { errors: [], warnings: [] };
if (typeof config !== 'object' || config === null) {
return result;
}
for (const key of Object.keys(config)) {
const currentPath = path ? `${path}.${key}` : key;
const value = config[key];
const expectedType = knownKeys[key];
// Skip $schema key (used for IDE support)
if (key === '$schema') {
continue;
}
// Check if key is known
if (expectedType === undefined) {
// Check if additional properties are allowed
if (knownKeys._additionalProperties) {
continue;
}
result.warnings.push(`Unknown key: ${currentPath}`);
continue;
}
// Validate type
if (typeof expectedType === 'string') {
// Simple type check. `'a|b'` means union (e.g. "number|all").
if (expectedType.includes('|')) {
const tokens = expectedType.split('|').map((t) => t.trim());
const ok = tokens.some((token) => {
if (token === 'string')
return typeof value === 'string';
if (token === 'number')
return typeof value === 'number';
if (token === 'boolean')
return typeof value === 'boolean';
if (token === 'array')
return Array.isArray(value);
// Everything else is treated as a string literal enum member.
return value === token;
});
if (!ok) {
result.errors.push(`${currentPath}: expected ${tokens.join(' | ')}, got ${typeof value}`);
}
}
else if (expectedType === 'string' && typeof value !== 'string') {
result.errors.push(`${currentPath}: expected string, got ${typeof value}`);
}
else if (expectedType === 'boolean' && typeof value !== 'boolean') {
result.errors.push(`${currentPath}: expected boolean, got ${typeof value}`);
}
else if (expectedType === 'number' && typeof value !== 'number') {
result.errors.push(`${currentPath}: expected number, got ${typeof value}`);
}
else if (expectedType === 'array' && !Array.isArray(value)) {
result.errors.push(`${currentPath}: expected array, got ${typeof value}`);
}
}
else if (Array.isArray(expectedType)) {
// Enum check
if (!expectedType.includes(value)) {
result.errors.push(`${currentPath}: expected one of [${expectedType.join(', ')}], got "${value}"`);
}
}
else if (typeof expectedType === 'object') {
// Nested object - recurse
if (typeof value !== 'object' || value === null) {
result.errors.push(`${currentPath}: expected object, got ${typeof value}`);
}
else {
const nested = validateConfig(value, expectedType, currentPath);
result.errors.push(...nested.errors);
result.warnings.push(...nested.warnings);
}
}
}
return result;
}
/**
* Validate lt.config file
*/
const ValidateCommand = {
alias: ['v'],
description: 'Validate config file',
hidden: false,
name: 'validate',
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
const { filesystem, print: { error, info, success, warning }, } = toolbox;
info('Validating lt.config...');
info('');
// Find config files
const cwd = filesystem.cwd();
const configFiles = ['lt.config.json', 'lt.config.yaml', 'lt.config'];
const foundFiles = [];
for (const file of configFiles) {
if (filesystem.exists((0, path_1.join)(cwd, file))) {
foundFiles.push(file);
}
}
if (foundFiles.length === 0) {
error('No lt.config file found in current directory.');
info('');
info('Run "lt config init" to create a configuration file.');
return 'config validate: no file';
}
// Warn about multiple files
if (foundFiles.length > 1) {
warning(`Multiple config files found: ${foundFiles.join(', ')}`);
warning(`Only ${foundFiles[0]} will be used (highest priority).`);
info('');
}
const configFile = foundFiles[0];
const configPath = (0, path_1.join)(cwd, configFile);
// Try to parse the config
let parsedConfig;
try {
const content = filesystem.read(configPath);
if (!content || content.trim() === '') {
error(`${configFile}: File is empty`);
return 'config validate: empty file';
}
if (configFile.endsWith('.json')) {
parsedConfig = JSON.parse(content);
}
else if (configFile.endsWith('.yaml')) {
const yaml = require('js-yaml');
parsedConfig = yaml.load(content);
}
else {
// Auto-detect
try {
parsedConfig = JSON.parse(content);
}
catch (_a) {
const yaml = require('js-yaml');
parsedConfig = yaml.load(content);
}
}
}
catch (e) {
error(`${configFile}: Parse error`);
error(` ${e.message}`);
return 'config validate: parse error';
}
success(`${configFile}: Syntax OK`);
// Validate against schema
const validation = validateConfig(parsedConfig, KNOWN_KEYS);
// Report errors
if (validation.errors.length > 0) {
info('');
error('Errors:');
for (const err of validation.errors) {
error(` - ${err}`);
}
}
// Report warnings
if (validation.warnings.length > 0) {
info('');
warning('Warnings:');
for (const warn of validation.warnings) {
warning(` - ${warn}`);
}
}
// Summary
info('');
if (validation.errors.length === 0 && validation.warnings.length === 0) {
success('Configuration is valid!');
}
else if (validation.errors.length === 0) {
success(`Configuration is valid with ${validation.warnings.length} warning(s).`);
}
else {
error(`Configuration has ${validation.errors.length} error(s) and ${validation.warnings.length} warning(s).`);
}
// Show schema hint
info('');
info('Tip: Add "$schema" to your config for IDE autocomplete:');
if (configFile.endsWith('.json')) {
info(' "$schema": "./node_modules/@lenne.tech/cli/schemas/lt.config.schema.json"');
}
else {
info(' For YAML files, use a JSON Schema-aware editor with:');
info(' # yaml-language-server: $schema=./node_modules/@lenne.tech/cli/schemas/lt.config.schema.json');
}
if (!toolbox.parameters.options.fromGluegunMenu) {
process.exit();
}
return validation.errors.length === 0 ? 'config validate: valid' : 'config validate: invalid';
}),
};
exports.default = ValidateCommand;