@lenne.tech/cli
Version:
lenne.Tech CLI: lt
358 lines (357 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 });
const path_1 = require("path");
const dev_identity_1 = require("../lib/dev-identity");
const dev_state_1 = require("../lib/dev-state");
const framework_detection_1 = require("../lib/framework-detection");
const frontend_framework_detection_1 = require("../lib/frontend-framework-detection");
const workspace_integration_1 = require("../lib/workspace-integration");
/**
* Show project status and context
*/
const StatusCommand = {
alias: ['st'],
description: 'Show project status',
hidden: false,
name: 'status',
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b, _c;
const { filesystem, print: { colors, info, success, warning }, system, } = toolbox;
const cwd = filesystem.cwd();
info('');
info(colors.bold('Project Status'));
info(colors.dim('─'.repeat(50)));
const projectInfo = {
configFiles: [],
frameworkMode: null,
frontendFrameworkMode: null,
gitBranch: null,
gitRoot: null,
hasGit: false,
hasLtConfig: false,
hasPackageJson: false,
monorepoSubprojects: [],
nodeVersion: null,
npmVersion: null,
packageName: null,
packageVersion: null,
projectType: 'unknown',
// Probe layout at the workspace root if cwd is inside a
// sub-project; otherwise probe at cwd. Without this, status
// shown from `projects/api/src/` would report both halves
// missing because there's no `projects/` underneath cwd.
workspaceLayout: (() => {
const ctx = (0, workspace_integration_1.detectSubProjectContext)(cwd, filesystem);
return (0, workspace_integration_1.detectWorkspaceLayout)(ctx ? ctx.workspaceRoot : cwd, filesystem);
})(),
workspaceSubProject: (() => {
const ctx = (0, workspace_integration_1.detectSubProjectContext)(cwd, filesystem);
return ctx ? { kind: ctx.kind, root: ctx.workspaceRoot } : null;
})(),
};
// Check for lt.config
const ltConfigFiles = ['lt.config.json', 'lt.config.yaml', 'lt.config'];
for (const configFile of ltConfigFiles) {
if (filesystem.exists((0, path_1.join)(cwd, configFile))) {
projectInfo.hasLtConfig = true;
projectInfo.configFiles.push(configFile);
}
}
// Check for package.json
const packageJsonPath = (0, path_1.join)(cwd, 'package.json');
if (filesystem.exists(packageJsonPath)) {
projectInfo.hasPackageJson = true;
try {
const packageJson = JSON.parse(filesystem.read(packageJsonPath) || '{}');
projectInfo.packageName = packageJson.name || null;
projectInfo.packageVersion = packageJson.version || null;
// Detect project type
const deps = Object.assign(Object.assign({}, packageJson.dependencies), packageJson.devDependencies);
// A project is a nest-server project if it EITHER has the npm dep
// (classic) OR has vendored the core/ directory. The frameworkMode
// field records which of the two modes this project runs in.
if (deps['@lenne.tech/nest-server'] || (0, framework_detection_1.isVendoredProject)(cwd)) {
projectInfo.projectType = 'nest-server';
projectInfo.frameworkMode = (0, framework_detection_1.detectFrameworkMode)(cwd);
}
else if (deps['@nestjs/core']) {
projectInfo.projectType = 'nestjs';
}
else if (deps['nuxt']) {
projectInfo.projectType = 'nuxt';
// Detect frontend framework mode if nuxt-extensions is present
if (deps['@lenne.tech/nuxt-extensions'] || (0, frontend_framework_detection_1.isVendoredAppProject)(cwd)) {
projectInfo.frontendFrameworkMode = (0, frontend_framework_detection_1.detectFrontendFrameworkMode)(cwd);
}
}
else if (deps['@angular/core']) {
projectInfo.projectType = 'angular';
}
else if (deps['react']) {
projectInfo.projectType = 'react';
}
else if (deps['vue']) {
projectInfo.projectType = 'vue';
}
else if (deps['typescript']) {
projectInfo.projectType = 'typescript';
}
else if (packageJson.name) {
projectInfo.projectType = 'node';
}
}
catch (_d) {
// Ignore parse errors
}
}
// Monorepo subproject detection: scan projects/api and projects/app for
// framework modes so that `lt status` at the monorepo root surfaces
// backend + frontend framework consumption modes even when the root
// itself is not a Nest/Nuxt project.
const monorepoCandidates = [
{ kind: 'backend', path: (0, path_1.join)(cwd, 'projects', 'api') },
{ kind: 'backend', path: (0, path_1.join)(cwd, 'packages', 'api') },
{ kind: 'frontend', path: (0, path_1.join)(cwd, 'projects', 'app') },
{ kind: 'frontend', path: (0, path_1.join)(cwd, 'packages', 'app') },
];
for (const candidate of monorepoCandidates) {
if (!filesystem.exists((0, path_1.join)(candidate.path, 'package.json')))
continue;
if (candidate.kind === 'backend') {
try {
const subPkg = JSON.parse(filesystem.read((0, path_1.join)(candidate.path, 'package.json')) || '{}');
const subDeps = Object.assign(Object.assign({}, subPkg.dependencies), subPkg.devDependencies);
if (subDeps['@lenne.tech/nest-server'] || (0, framework_detection_1.isVendoredProject)(candidate.path)) {
projectInfo.monorepoSubprojects.push({
frameworkMode: (0, framework_detection_1.detectFrameworkMode)(candidate.path),
kind: 'backend',
path: candidate.path,
});
}
}
catch (_e) {
// ignore
}
}
else {
try {
const subPkg = JSON.parse(filesystem.read((0, path_1.join)(candidate.path, 'package.json')) || '{}');
const subDeps = Object.assign(Object.assign({}, subPkg.dependencies), subPkg.devDependencies);
if (subDeps['@lenne.tech/nuxt-extensions'] || (0, frontend_framework_detection_1.isVendoredAppProject)(candidate.path)) {
projectInfo.monorepoSubprojects.push({
frameworkMode: (0, frontend_framework_detection_1.detectFrontendFrameworkMode)(candidate.path),
kind: 'frontend',
path: candidate.path,
});
}
}
catch (_f) {
// ignore
}
}
}
// Check for git
try {
const gitRoot = yield system.run('git rev-parse --show-toplevel 2>/dev/null');
if (gitRoot === null || gitRoot === void 0 ? void 0 : gitRoot.trim()) {
projectInfo.hasGit = true;
projectInfo.gitRoot = gitRoot.trim();
const branch = yield system.run('git rev-parse --abbrev-ref HEAD 2>/dev/null');
projectInfo.gitBranch = (branch === null || branch === void 0 ? void 0 : branch.trim()) || null;
}
}
catch (_g) {
// Not a git repository
}
// Get Node/npm versions
try {
projectInfo.nodeVersion = ((_a = (yield system.run('node --version 2>/dev/null'))) === null || _a === void 0 ? void 0 : _a.trim()) || null;
projectInfo.npmVersion = ((_b = (yield system.run('npm --version 2>/dev/null'))) === null || _b === void 0 ? void 0 : _b.trim()) || null;
}
catch (_h) {
// Ignore errors
}
// Display project info
info('');
info(colors.bold('Directory:'));
info(` ${cwd}`);
if (projectInfo.hasPackageJson) {
info('');
info(colors.bold('Package:'));
if (projectInfo.packageName) {
info(` Name: ${projectInfo.packageName}`);
}
if (projectInfo.packageVersion) {
info(` Version: ${projectInfo.packageVersion}`);
}
info(` Type: ${formatProjectType(projectInfo.projectType)}`);
if (projectInfo.frameworkMode) {
const modeLabel = projectInfo.frameworkMode === 'vendor'
? 'vendor (src/core/, VENDOR.md)'
: 'npm (@lenne.tech/nest-server dependency)';
info(` Framework: ${modeLabel}`);
}
if (projectInfo.frontendFrameworkMode) {
const frontendModeLabel = projectInfo.frontendFrameworkMode === 'vendor'
? 'vendor (app/core/, VENDOR.md)'
: 'npm (@lenne.tech/nuxt-extensions dependency)';
info(` Frontend Framework: ${frontendModeLabel}`);
}
}
// Workspace overview — surfaces the layout that drives the
// `add-api` / `add-app` / standalone gate decisions. Shown
// whenever we're inside a workspace OR a sub-project of one,
// so users immediately see what's missing and where the
// workspace root is.
if (projectInfo.workspaceLayout.hasWorkspace || projectInfo.workspaceSubProject) {
info('');
info(colors.bold('Workspace:'));
if (projectInfo.workspaceSubProject) {
const { kind, root } = projectInfo.workspaceSubProject;
warning(` You are inside projects/${kind}/ of a workspace.`);
info(` Root: ${root}`);
info(colors.dim(` Hint: cd to the workspace root for \`lt fullstack add-${kind === 'api' ? 'app' : 'api'}\``));
}
else {
success(' Detected (pnpm-workspace.yaml, package.json#workspaces, or projects/)');
}
const apiMark = projectInfo.workspaceLayout.hasApi ? colors.green('✓') : colors.yellow('✗');
const appMark = projectInfo.workspaceLayout.hasApp ? colors.green('✓') : colors.yellow('✗');
info(` ${apiMark} projects/api/`);
info(` ${appMark} projects/app/`);
// Suggest the next step when only one half is present.
if (projectInfo.workspaceLayout.hasApp && !projectInfo.workspaceLayout.hasApi) {
info(colors.dim(' Hint: `lt fullstack add-api` to integrate a NestJS server.'));
}
else if (projectInfo.workspaceLayout.hasApi && !projectInfo.workspaceLayout.hasApp) {
info(colors.dim(' Hint: `lt fullstack add-app` to integrate a Nuxt or Angular app.'));
}
}
// Local dev orchestration registry — surface URLs if registered.
// Helps users discover that `lt dev` is set up for this project, and
// gives a quick inline answer to "what URLs does this project use?".
{
const registryRoot = ((_c = projectInfo.workspaceSubProject) === null || _c === void 0 ? void 0 : _c.root) ||
(projectInfo.workspaceLayout.hasWorkspace ? projectInfo.workspaceLayout.workspaceDir : cwd);
if (registryRoot) {
const slug = (0, dev_identity_1.projectSlug)(registryRoot);
const entry = (0, dev_state_1.loadRegistry)().projects[slug];
if (entry) {
info('');
info(colors.bold('Local dev orchestration (lt dev):'));
for (const [sub, host] of Object.entries(entry.subdomains)) {
info(` ${sub.padEnd(6)} https://${host}`);
}
if (entry.dbName)
info(` db mongodb://127.0.0.1/${entry.dbName}`);
const session = (0, dev_state_1.loadSession)(registryRoot);
const apiAlive = (session === null || session === void 0 ? void 0 : session.pids.api) ? (0, dev_state_1.isPidAlive)(session.pids.api) : false;
const appAlive = (session === null || session === void 0 ? void 0 : session.pids.app) ? (0, dev_state_1.isPidAlive)(session.pids.app) : false;
if (apiAlive || appAlive) {
info(` Running: api ${apiAlive ? colors.green('●') : colors.dim('○')} app ${appAlive ? colors.green('●') : colors.dim('○')}`);
}
else {
info(colors.dim(' Hint: `lt dev up` to start API + App behind Caddy.'));
}
}
}
}
// Show monorepo subprojects if we detected any (typically at monorepo root)
if (projectInfo.monorepoSubprojects.length > 0) {
info('');
info(colors.bold('Monorepo Subprojects:'));
for (const sub of projectInfo.monorepoSubprojects) {
const relPath = sub.path.replace(`${cwd}/`, '');
if (sub.kind === 'backend') {
const label = sub.frameworkMode === 'vendor'
? 'vendor (src/core/, VENDOR.md)'
: 'npm (@lenne.tech/nest-server dependency)';
info(` Backend: ${relPath} → ${label}`);
}
else {
const label = sub.frameworkMode === 'vendor'
? 'vendor (app/core/, VENDOR.md)'
: 'npm (@lenne.tech/nuxt-extensions dependency)';
info(` Frontend: ${relPath} → ${label}`);
}
}
}
if (projectInfo.hasGit) {
info('');
info(colors.bold('Git:'));
info(` Branch: ${projectInfo.gitBranch || 'unknown'}`);
if (projectInfo.gitRoot !== cwd) {
info(` Root: ${projectInfo.gitRoot}`);
}
}
info('');
info(colors.bold('Configuration:'));
if (projectInfo.hasLtConfig) {
success(` lt.config: ${projectInfo.configFiles.join(', ')}`);
}
else {
warning(' lt.config: Not found');
info(colors.dim(' Run "lt config init" to create one'));
}
info('');
info(colors.bold('Environment:'));
if (projectInfo.nodeVersion) {
info(` Node: ${projectInfo.nodeVersion}`);
}
if (projectInfo.npmVersion) {
info(` npm: v${projectInfo.npmVersion}`);
}
const detectedPm = toolbox.pm.detect();
info(` Package Manager: ${detectedPm}`);
// Show available commands based on project type
info('');
info(colors.bold('Available Commands:'));
const commands = getAvailableCommands(projectInfo.projectType);
commands.forEach((cmd) => info(` ${colors.cyan(cmd.command)} - ${cmd.description}`));
info('');
// For tests
return `status ${projectInfo.projectType}`;
}),
};
function formatProjectType(type) {
const typeMap = {
angular: 'Angular',
'nest-server': 'lenne.tech Nest Server',
nestjs: 'NestJS',
node: 'Node.js',
nuxt: 'Nuxt',
react: 'React',
typescript: 'TypeScript',
unknown: 'Unknown',
vue: 'Vue',
};
return typeMap[type] || type;
}
function getAvailableCommands(projectType) {
const commonCommands = [
{ command: 'lt git', description: 'Git workflow commands' },
{ command: 'lt config', description: 'Configuration management' },
{ command: 'lt npm', description: 'NPM utilities' },
];
const typeCommands = {
angular: [{ command: 'lt frontend angular', description: 'Angular tools' }],
'nest-server': [
{ command: 'lt server module', description: 'Create server module' },
{ command: 'lt server object', description: 'Create object type' },
{ command: 'lt server add-property', description: 'Add property to module' },
],
nestjs: [{ command: 'lt server', description: 'NestJS server tools' }],
nuxt: [{ command: 'lt frontend nuxt', description: 'Nuxt tools' }],
};
return [...commonCommands, ...(typeCommands[projectType] || [])];
}
exports.default = StatusCommand;