UNPKG

@lenne.tech/cli

Version:

lenne.Tech CLI: lt

477 lines (476 loc) 23 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 }); 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;