@lenne.tech/cli
Version:
lenne.Tech CLI: lt
477 lines (476 loc) • 23 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 workspace_integration_1 = require("../../lib/workspace-integration");
/**
* Create a new server
*/
exports.help = {
aliases: ['c'],
configuration: 'commands.server.create.*',
description: 'Create new server',
name: 'create',
options: [
{ description: 'Server name', flag: '--name', required: true, type: 'string' },
{
description: 'API mode',
flag: '--api-mode',
required: false,
type: 'string',
values: ['Rest', 'GraphQL', 'Both'],
},
{ description: 'Project description', flag: '--description', required: false, type: 'string' },
{ description: 'Project author', flag: '--author', required: false, type: 'string' },
{ description: 'Initialize git repository', flag: '--git', required: false, type: 'boolean' },
{ description: 'Git branch to clone from', flag: '--branch', required: false, type: 'string' },
{ description: 'Copy from local path instead of cloning', flag: '--copy', required: false, type: 'string' },
{ description: 'Symlink to local path instead of cloning', flag: '--link', required: false, type: 'string' },
{
description: 'Backend framework consumption mode',
flag: '--framework-mode',
required: false,
type: 'string',
values: ['npm', 'vendor'],
},
{
description: 'Upstream nest-server branch/tag to vendor (with --framework-mode vendor)',
flag: '--framework-upstream-branch',
required: false,
type: 'string',
},
{
default: false,
description: 'Use experimental nest-base template (Bun + Prisma + Postgres)',
flag: '--next',
required: false,
type: 'boolean',
},
{
default: false,
description: 'Print resolved plan and exit without making any changes',
flag: '--dry-run',
required: false,
type: 'boolean',
},
{
default: false,
description: 'Override the workspace-detection abort under --noConfirm',
flag: '--force',
required: false,
type: 'boolean',
},
{
default: false,
description: 'Skip all interactive prompts',
flag: '--noConfirm',
required: false,
type: 'boolean',
},
],
};
const NewCommand = {
alias: ['c'],
description: 'Create new server',
hidden: false,
name: 'create',
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2;
// Retrieve the tools we need
const { config, filesystem, git, helper, meta, parameters, print: { error, info, spin, success }, prompt: { ask, confirm }, server, strings: { kebabCase }, system, } = toolbox;
// Handle --help-json flag
if (toolbox.tools.helpJson(exports.help)) {
return;
}
// Load configuration
const ltConfig = config.loadConfig();
const configGit = (_c = (_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.create) === null || _c === void 0 ? void 0 : _c.git;
const configAuthor = (_f = (_e = (_d = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _d === void 0 ? void 0 : _d.server) === null || _e === void 0 ? void 0 : _e.create) === null || _f === void 0 ? void 0 : _f.author;
const configDescription = (_j = (_h = (_g = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _g === void 0 ? void 0 : _g.server) === null || _h === void 0 ? void 0 : _h.create) === null || _j === void 0 ? void 0 : _j.description;
const configBranch = (_m = (_l = (_k = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _k === void 0 ? void 0 : _k.server) === null || _l === void 0 ? void 0 : _l.create) === null || _m === void 0 ? void 0 : _m.branch;
const configCopy = (_q = (_p = (_o = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _o === void 0 ? void 0 : _o.server) === null || _p === void 0 ? void 0 : _p.create) === null || _q === void 0 ? void 0 : _q.copy;
const configLink = (_t = (_s = (_r = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _r === void 0 ? void 0 : _r.server) === null || _s === void 0 ? void 0 : _s.create) === null || _t === void 0 ? void 0 : _t.link;
const configApiMode = (_w = (_v = (_u = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _u === void 0 ? void 0 : _u.server) === null || _v === void 0 ? void 0 : _v.create) === null || _w === void 0 ? void 0 : _w.apiMode;
const configFrameworkMode = (_z = (_y = (_x = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _x === void 0 ? void 0 : _x.server) === null || _y === void 0 ? void 0 : _y.create) === null || _z === void 0 ? void 0 : _z.frameworkMode;
// Load global defaults
const globalAuthor = config.getGlobalDefault(ltConfig, 'author');
const globalApiMode = config.getGlobalDefault(ltConfig, 'apiMode');
// Parse CLI arguments
const cliGit = parameters.options.git;
const cliAuthor = parameters.options.author;
const cliDescription = parameters.options.description;
const cliNoConfirm = parameters.options.noConfirm;
const cliBranch = parameters.options.branch || parameters.options.b;
const cliCopy = parameters.options.copy || parameters.options.c;
const cliLink = parameters.options.link;
const cliApiMode = parameters.options['api-mode'] || parameters.options.apiMode;
const cliFrameworkMode = parameters.options['framework-mode'];
const cliFrameworkUpstreamBranch = parameters.options['framework-upstream-branch'];
const cliDryRun = parameters.options['dry-run'];
const cliForce = parameters.options.force;
const experimental = parameters.options.next === true || parameters.options.next === 'true';
const dryRun = cliDryRun === true || cliDryRun === 'true';
const force = cliForce === true || cliForce === 'true';
// Determine noConfirm with priority: CLI > config > global > default (false)
const noConfirm = config.getNoConfirm({
cliValue: cliNoConfirm,
commandConfig: (_1 = (_0 = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _0 === void 0 ? void 0 : _0.server) === null || _1 === void 0 ? void 0 : _1.create,
config: ltConfig,
});
// Start timer
const timer = system.startTimer();
// Info
info('Create a new server');
// Hint for non-interactive callers (e.g. Claude Code)
toolbox.tools.nonInteractiveHint('lt server create --name <name> --api-mode <Rest|GraphQL|Both> --framework-mode <npm|vendor> [--next] [--dry-run] --noConfirm');
// Check git
if (!(yield git.gitInstalled())) {
return;
}
// Workspace-awareness: bundled into runStandaloneWorkspaceGate so
// server/frontend commands share the print + prompt + decision +
// exit logic. Three modes:
// - interactive → confirm prompt
// - non-interactive → refuse (KI/CI default — fail loud)
// - non-interactive + force → proceed with a hint
const proceed = yield (0, workspace_integration_1.runStandaloneWorkspaceGate)({
cwd: '.',
filesystem,
force,
fromGluegunMenu: Boolean(toolbox.parameters.options.fromGluegunMenu),
noConfirmFlag: noConfirm,
pieceName: 'api',
print: { confirm, error, info },
projectKind: 'server',
suggestion: 'lt fullstack add-api',
});
if (!proceed)
return;
// Get name. Honour the explicit `--name <slug>` flag (declared as
// required in the help-json contract) before falling back to the
// first positional argument or interactive input. Without this, a
// non-interactive caller passing only `--name my-srv` is forced
// into the prompt because `parameters.first` is empty.
const cliName = parameters.options.name;
const name = cliName ||
(yield helper.getInput(parameters.first, {
name: 'server name',
showError: true,
}));
if (!name) {
return;
}
// Set project directory
const projectDir = kebabCase(name);
// Check if directory already exists
if (filesystem.exists(projectDir)) {
info('');
error(`There's already a folder named "${projectDir}" here.`);
return;
}
// Determine copy/link paths with priority: CLI > config
const copyPath = cliCopy || configCopy;
const linkPath = cliLink || configLink;
// Determine branch with priority: CLI > config
const branch = cliBranch || configBranch;
// Determine description with priority: CLI > config > interactive.
// Skip the interactive prompt under --noConfirm; description is
// optional and defaulting to the project name keeps the package.json
// valid for non-interactive callers.
let description;
if (cliDescription) {
description = cliDescription;
}
else if (configDescription) {
description = configDescription.replace('{name}', name);
info(`Using description from lt.config: ${description}`);
}
else if (noConfirm) {
description = '';
}
else {
description = yield helper.getInput(parameters.second, {
name: 'Description',
showError: false,
});
}
// Determine author with priority: CLI > config > global > interactive.
// Skip the prompt under --noConfirm.
let author;
if (cliAuthor) {
author = cliAuthor;
}
else if (configAuthor) {
author = configAuthor;
info(`Using author from lt.config commands.server.create: ${author}`);
}
else if (globalAuthor) {
author = globalAuthor;
info(`Using author from lt.config defaults: ${author}`);
}
else if (noConfirm) {
author = '';
}
else {
author = yield helper.getInput('', {
name: 'Author',
showError: false,
});
}
// Determine API mode with priority: CLI > config > global > interactive (default: Rest)
let apiMode;
if (experimental) {
apiMode = 'Rest';
info('Using experimental nest-base template (Bun + Prisma + Postgres + Better-Auth)');
}
else if (cliApiMode) {
apiMode = cliApiMode;
}
else if (configApiMode) {
apiMode = configApiMode;
info(`Using API mode from lt.config commands.server.create: ${apiMode}`);
}
else if (globalApiMode) {
apiMode = globalApiMode;
info(`Using API mode from lt.config defaults: ${apiMode}`);
}
else if (noConfirm) {
apiMode = 'Rest';
info('Using default API mode: REST/RPC');
}
else {
const apiModeChoice = yield ask([
{
choices: [
'Rest - REST/RPC API with Swagger documentation (recommended)',
'GraphQL - GraphQL API with subscriptions',
'Both - REST/RPC and GraphQL in parallel (hybrid)',
],
initial: 0,
message: 'API mode?',
name: 'apiMode',
type: 'select',
},
]);
apiMode = apiModeChoice.apiMode.split(' - ')[0];
}
// Determine framework consumption mode — same resolution cascade as
// lt fullstack init: CLI flag > lt.config > interactive (default npm).
let frameworkMode;
if (experimental) {
frameworkMode = 'npm';
}
else if (cliFrameworkMode === 'npm' || cliFrameworkMode === 'vendor') {
frameworkMode = cliFrameworkMode;
}
else if (cliFrameworkMode) {
error(`Invalid --framework-mode value "${cliFrameworkMode}". Use "npm" or "vendor".`);
return;
}
else if (configFrameworkMode === 'npm' || configFrameworkMode === 'vendor') {
frameworkMode = configFrameworkMode;
info(`Using framework mode from lt.config: ${frameworkMode}`);
}
else if (noConfirm) {
frameworkMode = 'npm';
}
else {
const frameworkModeChoice = yield ask({
choices: [
'npm - @lenne.tech/nest-server as npm dependency (classic, stable)',
'vendor - framework core vendored into src/core/ (pilot, allows local patches)',
],
initial: 0,
message: 'Framework consumption mode?',
name: 'frameworkMode',
type: 'select',
});
frameworkMode = frameworkModeChoice.frameworkMode.startsWith('vendor') ? 'vendor' : 'npm';
}
const frameworkUpstreamBranch = typeof cliFrameworkUpstreamBranch === 'string' && cliFrameworkUpstreamBranch.length > 0
? cliFrameworkUpstreamBranch
: undefined;
// Dry-run: print the resolved plan and exit without any disk
// changes. Mirrors the dry-run surface of `lt fullstack init` /
// `add-api` / `add-app` so agent workflows can preview the
// standalone path the same way.
if (dryRun) {
info('');
info('Dry-run plan:');
info(` name: ${name}`);
info(` projectDir: ${projectDir}`);
info(` apiMode: ${apiMode}`);
info(` frameworkMode: ${frameworkMode}`);
if (frameworkUpstreamBranch) {
info(` frameworkUpstreamBranch: ${frameworkUpstreamBranch}`);
}
info(` branch: ${branch || '(default)'}`);
info(` copy: ${copyPath || '(none)'}`);
info(` link: ${linkPath || '(none)'}`);
info(` experimental (--next): ${experimental}`);
info(` description: ${description || '(none)'}`);
info(` author: ${author || '(none)'}`);
info('');
info('Would execute:');
if (experimental) {
info(` 1. clone nest-base → ./${projectDir}`);
info(` 2. patch package.json (name = ${projectDir})`);
}
else if (frameworkMode === 'vendor') {
info(` 1. clone nest-server-starter → ./${projectDir}`);
info(` 2. clone @lenne.tech/nest-server${frameworkUpstreamBranch ? ` (${frameworkUpstreamBranch})` : ''} → /tmp`);
info(` 3. vendor core/ + flatten-fix + codemod consumer imports`);
info(` 4. merge upstream deps`);
info(` 5. run processApiMode(${apiMode})`);
}
else {
info(` 1. clone nest-server-starter → ./${projectDir}`);
info(` 2. run processApiMode(${apiMode})`);
}
info(` N. write ./${projectDir}/lt.config.json`);
info('');
if (!toolbox.parameters.options.fromGluegunMenu) {
process.exit();
}
return `dry-run server create (${frameworkMode} / ${apiMode})`;
}
// Setup server using Server extension
const setupSpinner = spin(`Setting up server${linkPath ? ' (link)' : copyPath ? ' (copy)' : branch ? ` (branch: ${branch})` : ''}`);
const result = yield server.setupServer(`./${projectDir}`, {
apiMode,
author,
branch,
copyPath,
description,
experimental,
frameworkMode,
frameworkUpstreamBranch,
linkPath,
name,
projectDir,
});
if (!result.success) {
setupSpinner.fail(`Failed to set up server: ${result.path}`);
return;
}
setupSpinner.succeed(`Server template set up (${result.method})`);
// For symlinks, skip all post-setup steps
if (result.method === 'link') {
info('');
success(`Created symlink ${projectDir} -> ${result.path}`);
info('');
info('Note: This is a symlink - changes will affect the original template!');
info('');
info('Next:');
info(` Go to project directory: cd ${projectDir}`);
info(` Start server: ${toolbox.pm.run('start')}`);
info('');
if (!toolbox.parameters.options.fromGluegunMenu) {
process.exit();
}
return `created server symlink ${name}`;
}
// For the experimental nest-base template, flip .claude/upstream.json
// from the template-self default to the downstream shape so
// `/upstream-pr` can contribute core fixes back to nest-base. The
// standalone clone lands directly at `projectDir`. Non-fatal when the
// file is absent (older templates).
if (experimental) {
const upstreamResult = (0, workspace_integration_1.reconfigureUpstreamForDownstream)({
apiDir: projectDir,
filesystem,
upstreamBranch: branch,
});
if (upstreamResult.updated) {
info('Configured .claude/upstream.json for downstream contributions');
}
}
// Git initialization (after npm install which is done in setupServer).
// When cwd is not inside a repo, `git rev-parse` exits 128 — treat as false.
if (git) {
const inGit = (_2 = (yield system.run('git rev-parse --is-inside-work-tree 2>/dev/null || echo false'))) === null || _2 === void 0 ? void 0 : _2.trim();
if (inGit !== 'true') {
// Determine initGit with priority: CLI > config > interactive
let initializeGit;
if (cliGit !== undefined) {
initializeGit = cliGit === true || cliGit === 'true';
}
else if (configGit !== undefined) {
initializeGit = configGit;
if (initializeGit) {
info('Using git initialization setting from lt.config: enabled');
}
}
else if (noConfirm) {
initializeGit = false; // Default to false when noConfirm (avoid unexpected side effects)
}
else {
initializeGit = yield confirm('Initialize git?', true);
}
if (initializeGit) {
const initGitSpinner = spin('Initialize git');
yield system.run(`cd ${projectDir} && git init && git add . && git commit -am "Init via lenne.Tech CLI ${meta.version()}"`);
initGitSpinner.succeed('Git initialized');
}
}
}
// Derive controller type from API mode and save project config
const controllerType = apiMode;
if (!experimental) {
// Create lt.config.json
const projectConfig = {
commands: {
server: {
module: {
controller: controllerType,
},
},
},
meta: {
apiMode,
version: '1.0.0',
},
};
const configPath = filesystem.path(projectDir, 'lt.config.json');
filesystem.write(configPath, projectConfig, { jsonIndent: 2 });
info('');
success(`Configuration saved to ${projectDir}/lt.config.json`);
info(` API mode: ${apiMode}`);
info(` Default controller type: ${controllerType}`);
}
// We're done, so show what to do next
info('');
success(`Generated ${name} server with lenne.Tech CLI ${meta.version()} in ${helper.msToMinutesAndSeconds(timer())}m.`);
info('');
info('Next:');
if (experimental) {
info(` Go to project directory: cd ${projectDir}`);
info(' Install dependencies: bun install');
info(' Configure .env (see .env.example)');
info(' Start Postgres + run prisma generate / migrate');
info(' Start server: bun run dev');
}
else {
info(' Start database server (e.g. MongoDB)');
info(` Check config: ${projectDir}/src/config.env.ts`);
info(` Go to project directory: cd ${projectDir}`);
info(` Run tests: ${toolbox.pm.run('test:e2e')}`);
info(` Start server: ${toolbox.pm.run('start')}`);
}
info('');
if (!toolbox.parameters.options.fromGluegunMenu) {
process.exit();
}
// For tests
return `created server ${name}`;
}),
};
exports.default = NewCommand;