@lenne.tech/cli 1.19.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/commands/frontend/angular.js +110 -4
- package/build/commands/frontend/nuxt.js +184 -13
- package/build/commands/fullstack/add-api.js +360 -0
- package/build/commands/fullstack/add-app.js +284 -0
- package/build/commands/fullstack/init.js +46 -0
- package/build/commands/server/create.js +118 -7
- package/build/commands/status.js +42 -0
- package/build/commands/tools/ocr.js +6 -1
- package/build/lib/workspace-integration.js +351 -0
- package/docs/commands.md +103 -4
- package/package.json +2 -1
|
@@ -8,8 +8,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
11
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
15
|
const hoist_workspace_pnpm_config_1 = require("../../lib/hoist-workspace-pnpm-config");
|
|
16
|
+
const workspace_integration_1 = require("../../lib/workspace-integration");
|
|
17
|
+
const add_api_1 = __importDefault(require("./add-api"));
|
|
18
|
+
const add_app_1 = __importDefault(require("./add-app"));
|
|
13
19
|
/**
|
|
14
20
|
* Create a new fullstack workspace
|
|
15
21
|
*/
|
|
@@ -58,6 +64,46 @@ const NewCommand = {
|
|
|
58
64
|
commandConfig: (_y = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _y === void 0 ? void 0 : _y.fullstack,
|
|
59
65
|
config: ltConfig,
|
|
60
66
|
});
|
|
67
|
+
// ── Auto-detect existing workspace ─────────────────────────────────
|
|
68
|
+
//
|
|
69
|
+
// If `lt fullstack init` runs inside a directory that already looks
|
|
70
|
+
// like a workspace (pnpm-workspace.yaml or projects/) and the user
|
|
71
|
+
// didn't pass a workspace name, dispatch to the matching incremental
|
|
72
|
+
// command instead of trying to clone a new lt-monorepo on top of the
|
|
73
|
+
// existing one. Three branches:
|
|
74
|
+
//
|
|
75
|
+
// - both projects/api and projects/app exist → nothing to do.
|
|
76
|
+
// - only projects/app exists → delegate to add-api.
|
|
77
|
+
// - only projects/api exists → delegate to add-app.
|
|
78
|
+
//
|
|
79
|
+
// Users who really want to create a *new* workspace from inside an
|
|
80
|
+
// existing one bypass detection by supplying `--name <slug>` (or a
|
|
81
|
+
// positional argument); the slug then becomes the new project dir
|
|
82
|
+
// and the original detection-skipping path runs normally.
|
|
83
|
+
const noNameProvided = !cliName && !parameters.first;
|
|
84
|
+
if (noNameProvided) {
|
|
85
|
+
const cwdLayout = (0, workspace_integration_1.detectWorkspaceLayout)('.', filesystem);
|
|
86
|
+
if (cwdLayout.hasWorkspace) {
|
|
87
|
+
if (cwdLayout.hasApi && cwdLayout.hasApp) {
|
|
88
|
+
error('Workspace already has both projects/api and projects/app — nothing to add.');
|
|
89
|
+
info('Use `lt fullstack add-api --help-json` or `lt fullstack add-app --help-json` to inspect options.');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (cwdLayout.hasApp && !cwdLayout.hasApi) {
|
|
93
|
+
info('Detected existing workspace with projects/app — delegating to `lt fullstack add-api`.');
|
|
94
|
+
// gluegun's `GluegunCommand.run` is typed as
|
|
95
|
+
// `void | Promise<any>`. Cast through `unknown` so the await
|
|
96
|
+
// is statically meaningful for our async implementations.
|
|
97
|
+
return (yield add_api_1.default.run(toolbox));
|
|
98
|
+
}
|
|
99
|
+
if (cwdLayout.hasApi && !cwdLayout.hasApp) {
|
|
100
|
+
info('Detected existing workspace with projects/api — delegating to `lt fullstack add-app`.');
|
|
101
|
+
return (yield add_app_1.default.run(toolbox));
|
|
102
|
+
}
|
|
103
|
+
// Workspace dir but neither sub-project present — fall through
|
|
104
|
+
// to normal init. The user can still pass --name to override.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
61
107
|
// Get name of the workspace
|
|
62
108
|
const name = cliName ||
|
|
63
109
|
(yield helper.getInput(parameters.first, {
|
|
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const workspace_integration_1 = require("../../lib/workspace-integration");
|
|
12
13
|
/**
|
|
13
14
|
* Create a new server
|
|
14
15
|
*/
|
|
@@ -42,6 +43,19 @@ const NewCommand = {
|
|
|
42
43
|
{ description: 'Git branch to clone from', flag: '--branch', required: false, type: 'string' },
|
|
43
44
|
{ description: 'Copy from local path instead of cloning', flag: '--copy', required: false, type: 'string' },
|
|
44
45
|
{ description: 'Symlink to local path instead of cloning', flag: '--link', required: false, type: 'string' },
|
|
46
|
+
{
|
|
47
|
+
description: 'Backend framework consumption mode',
|
|
48
|
+
flag: '--framework-mode',
|
|
49
|
+
required: false,
|
|
50
|
+
type: 'string',
|
|
51
|
+
values: ['npm', 'vendor'],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
description: 'Upstream nest-server branch/tag to vendor (with --framework-mode vendor)',
|
|
55
|
+
flag: '--framework-upstream-branch',
|
|
56
|
+
required: false,
|
|
57
|
+
type: 'string',
|
|
58
|
+
},
|
|
45
59
|
{
|
|
46
60
|
default: false,
|
|
47
61
|
description: 'Use experimental nest-base template (Bun + Prisma + Postgres)',
|
|
@@ -49,6 +63,20 @@ const NewCommand = {
|
|
|
49
63
|
required: false,
|
|
50
64
|
type: 'boolean',
|
|
51
65
|
},
|
|
66
|
+
{
|
|
67
|
+
default: false,
|
|
68
|
+
description: 'Print resolved plan and exit without making any changes',
|
|
69
|
+
flag: '--dry-run',
|
|
70
|
+
required: false,
|
|
71
|
+
type: 'boolean',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
default: false,
|
|
75
|
+
description: 'Override the workspace-detection abort under --noConfirm',
|
|
76
|
+
flag: '--force',
|
|
77
|
+
required: false,
|
|
78
|
+
type: 'boolean',
|
|
79
|
+
},
|
|
52
80
|
{
|
|
53
81
|
default: false,
|
|
54
82
|
description: 'Skip all interactive prompts',
|
|
@@ -84,7 +112,11 @@ const NewCommand = {
|
|
|
84
112
|
const cliApiMode = parameters.options['api-mode'] || parameters.options.apiMode;
|
|
85
113
|
const cliFrameworkMode = parameters.options['framework-mode'];
|
|
86
114
|
const cliFrameworkUpstreamBranch = parameters.options['framework-upstream-branch'];
|
|
115
|
+
const cliDryRun = parameters.options['dry-run'];
|
|
116
|
+
const cliForce = parameters.options.force;
|
|
87
117
|
const experimental = parameters.options.next === true || parameters.options.next === 'true';
|
|
118
|
+
const dryRun = cliDryRun === true || cliDryRun === 'true';
|
|
119
|
+
const force = cliForce === true || cliForce === 'true';
|
|
88
120
|
// Determine noConfirm with priority: CLI > config > global > default (false)
|
|
89
121
|
const noConfirm = config.getNoConfirm({
|
|
90
122
|
cliValue: cliNoConfirm,
|
|
@@ -96,16 +128,41 @@ const NewCommand = {
|
|
|
96
128
|
// Info
|
|
97
129
|
info('Create a new server');
|
|
98
130
|
// Hint for non-interactive callers (e.g. Claude Code)
|
|
99
|
-
toolbox.tools.nonInteractiveHint('lt server create --name <name> --api-mode <Rest|GraphQL|Both> [--next] --noConfirm');
|
|
131
|
+
toolbox.tools.nonInteractiveHint('lt server create --name <name> --api-mode <Rest|GraphQL|Both> --framework-mode <npm|vendor> [--next] [--dry-run] --noConfirm');
|
|
100
132
|
// Check git
|
|
101
133
|
if (!(yield git.gitInstalled())) {
|
|
102
134
|
return;
|
|
103
135
|
}
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
136
|
+
// Workspace-awareness: bundled into runStandaloneWorkspaceGate so
|
|
137
|
+
// server/frontend commands share the print + prompt + decision +
|
|
138
|
+
// exit logic. Three modes:
|
|
139
|
+
// - interactive → confirm prompt
|
|
140
|
+
// - non-interactive → refuse (KI/CI default — fail loud)
|
|
141
|
+
// - non-interactive + force → proceed with a hint
|
|
142
|
+
const proceed = yield (0, workspace_integration_1.runStandaloneWorkspaceGate)({
|
|
143
|
+
cwd: '.',
|
|
144
|
+
filesystem,
|
|
145
|
+
force,
|
|
146
|
+
fromGluegunMenu: Boolean(toolbox.parameters.options.fromGluegunMenu),
|
|
147
|
+
noConfirmFlag: noConfirm,
|
|
148
|
+
pieceName: 'api',
|
|
149
|
+
print: { confirm, error, info },
|
|
150
|
+
projectKind: 'server',
|
|
151
|
+
suggestion: 'lt fullstack add-api',
|
|
108
152
|
});
|
|
153
|
+
if (!proceed)
|
|
154
|
+
return;
|
|
155
|
+
// Get name. Honour the explicit `--name <slug>` flag (declared as
|
|
156
|
+
// required in the help-json contract) before falling back to the
|
|
157
|
+
// first positional argument or interactive input. Without this, a
|
|
158
|
+
// non-interactive caller passing only `--name my-srv` is forced
|
|
159
|
+
// into the prompt because `parameters.first` is empty.
|
|
160
|
+
const cliName = parameters.options.name;
|
|
161
|
+
const name = cliName ||
|
|
162
|
+
(yield helper.getInput(parameters.first, {
|
|
163
|
+
name: 'server name',
|
|
164
|
+
showError: true,
|
|
165
|
+
}));
|
|
109
166
|
if (!name) {
|
|
110
167
|
return;
|
|
111
168
|
}
|
|
@@ -122,7 +179,10 @@ const NewCommand = {
|
|
|
122
179
|
const linkPath = cliLink || configLink;
|
|
123
180
|
// Determine branch with priority: CLI > config
|
|
124
181
|
const branch = cliBranch || configBranch;
|
|
125
|
-
// Determine description with priority: CLI > config > interactive
|
|
182
|
+
// Determine description with priority: CLI > config > interactive.
|
|
183
|
+
// Skip the interactive prompt under --noConfirm; description is
|
|
184
|
+
// optional and defaulting to the project name keeps the package.json
|
|
185
|
+
// valid for non-interactive callers.
|
|
126
186
|
let description;
|
|
127
187
|
if (cliDescription) {
|
|
128
188
|
description = cliDescription;
|
|
@@ -131,13 +191,17 @@ const NewCommand = {
|
|
|
131
191
|
description = configDescription.replace('{name}', name);
|
|
132
192
|
info(`Using description from lt.config: ${description}`);
|
|
133
193
|
}
|
|
194
|
+
else if (noConfirm) {
|
|
195
|
+
description = '';
|
|
196
|
+
}
|
|
134
197
|
else {
|
|
135
198
|
description = yield helper.getInput(parameters.second, {
|
|
136
199
|
name: 'Description',
|
|
137
200
|
showError: false,
|
|
138
201
|
});
|
|
139
202
|
}
|
|
140
|
-
// Determine author with priority: CLI > config > global > interactive
|
|
203
|
+
// Determine author with priority: CLI > config > global > interactive.
|
|
204
|
+
// Skip the prompt under --noConfirm.
|
|
141
205
|
let author;
|
|
142
206
|
if (cliAuthor) {
|
|
143
207
|
author = cliAuthor;
|
|
@@ -150,6 +214,9 @@ const NewCommand = {
|
|
|
150
214
|
author = globalAuthor;
|
|
151
215
|
info(`Using author from lt.config defaults: ${author}`);
|
|
152
216
|
}
|
|
217
|
+
else if (noConfirm) {
|
|
218
|
+
author = '';
|
|
219
|
+
}
|
|
153
220
|
else {
|
|
154
221
|
author = yield helper.getInput('', {
|
|
155
222
|
name: 'Author',
|
|
@@ -229,6 +296,50 @@ const NewCommand = {
|
|
|
229
296
|
const frameworkUpstreamBranch = typeof cliFrameworkUpstreamBranch === 'string' && cliFrameworkUpstreamBranch.length > 0
|
|
230
297
|
? cliFrameworkUpstreamBranch
|
|
231
298
|
: undefined;
|
|
299
|
+
// Dry-run: print the resolved plan and exit without any disk
|
|
300
|
+
// changes. Mirrors the dry-run surface of `lt fullstack init` /
|
|
301
|
+
// `add-api` / `add-app` so agent workflows can preview the
|
|
302
|
+
// standalone path the same way.
|
|
303
|
+
if (dryRun) {
|
|
304
|
+
info('');
|
|
305
|
+
info('Dry-run plan:');
|
|
306
|
+
info(` name: ${name}`);
|
|
307
|
+
info(` projectDir: ${projectDir}`);
|
|
308
|
+
info(` apiMode: ${apiMode}`);
|
|
309
|
+
info(` frameworkMode: ${frameworkMode}`);
|
|
310
|
+
if (frameworkUpstreamBranch) {
|
|
311
|
+
info(` frameworkUpstreamBranch: ${frameworkUpstreamBranch}`);
|
|
312
|
+
}
|
|
313
|
+
info(` branch: ${branch || '(default)'}`);
|
|
314
|
+
info(` copy: ${copyPath || '(none)'}`);
|
|
315
|
+
info(` link: ${linkPath || '(none)'}`);
|
|
316
|
+
info(` experimental (--next): ${experimental}`);
|
|
317
|
+
info(` description: ${description || '(none)'}`);
|
|
318
|
+
info(` author: ${author || '(none)'}`);
|
|
319
|
+
info('');
|
|
320
|
+
info('Would execute:');
|
|
321
|
+
if (experimental) {
|
|
322
|
+
info(` 1. clone nest-base → ./${projectDir}`);
|
|
323
|
+
info(` 2. patch package.json (name = ${projectDir})`);
|
|
324
|
+
}
|
|
325
|
+
else if (frameworkMode === 'vendor') {
|
|
326
|
+
info(` 1. clone nest-server-starter → ./${projectDir}`);
|
|
327
|
+
info(` 2. clone @lenne.tech/nest-server${frameworkUpstreamBranch ? ` (${frameworkUpstreamBranch})` : ''} → /tmp`);
|
|
328
|
+
info(` 3. vendor core/ + flatten-fix + codemod consumer imports`);
|
|
329
|
+
info(` 4. merge upstream deps`);
|
|
330
|
+
info(` 5. run processApiMode(${apiMode})`);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
info(` 1. clone nest-server-starter → ./${projectDir}`);
|
|
334
|
+
info(` 2. run processApiMode(${apiMode})`);
|
|
335
|
+
}
|
|
336
|
+
info(` N. write ./${projectDir}/lt.config.json`);
|
|
337
|
+
info('');
|
|
338
|
+
if (!toolbox.parameters.options.fromGluegunMenu) {
|
|
339
|
+
process.exit();
|
|
340
|
+
}
|
|
341
|
+
return `dry-run server create (${frameworkMode} / ${apiMode})`;
|
|
342
|
+
}
|
|
232
343
|
// Setup server using Server extension
|
|
233
344
|
const setupSpinner = spin(`Setting up server${linkPath ? ' (link)' : copyPath ? ' (copy)' : branch ? ` (branch: ${branch})` : ''}`);
|
|
234
345
|
const result = yield server.setupServer(`./${projectDir}`, {
|
package/build/commands/status.js
CHANGED
|
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
const path_1 = require("path");
|
|
13
13
|
const framework_detection_1 = require("../lib/framework-detection");
|
|
14
14
|
const frontend_framework_detection_1 = require("../lib/frontend-framework-detection");
|
|
15
|
+
const workspace_integration_1 = require("../lib/workspace-integration");
|
|
15
16
|
/**
|
|
16
17
|
* Show project status and context
|
|
17
18
|
*/
|
|
@@ -42,6 +43,18 @@ const StatusCommand = {
|
|
|
42
43
|
packageName: null,
|
|
43
44
|
packageVersion: null,
|
|
44
45
|
projectType: 'unknown',
|
|
46
|
+
// Probe layout at the workspace root if cwd is inside a
|
|
47
|
+
// sub-project; otherwise probe at cwd. Without this, status
|
|
48
|
+
// shown from `projects/api/src/` would report both halves
|
|
49
|
+
// missing because there's no `projects/` underneath cwd.
|
|
50
|
+
workspaceLayout: (() => {
|
|
51
|
+
const ctx = (0, workspace_integration_1.detectSubProjectContext)(cwd, filesystem);
|
|
52
|
+
return (0, workspace_integration_1.detectWorkspaceLayout)(ctx ? ctx.workspaceRoot : cwd, filesystem);
|
|
53
|
+
})(),
|
|
54
|
+
workspaceSubProject: (() => {
|
|
55
|
+
const ctx = (0, workspace_integration_1.detectSubProjectContext)(cwd, filesystem);
|
|
56
|
+
return ctx ? { kind: ctx.kind, root: ctx.workspaceRoot } : null;
|
|
57
|
+
})(),
|
|
45
58
|
};
|
|
46
59
|
// Check for lt.config
|
|
47
60
|
const ltConfigFiles = ['lt.config.json', 'lt.config.yaml', 'lt.config'];
|
|
@@ -192,6 +205,35 @@ const StatusCommand = {
|
|
|
192
205
|
info(` Frontend Framework: ${frontendModeLabel}`);
|
|
193
206
|
}
|
|
194
207
|
}
|
|
208
|
+
// Workspace overview — surfaces the layout that drives the
|
|
209
|
+
// `add-api` / `add-app` / standalone gate decisions. Shown
|
|
210
|
+
// whenever we're inside a workspace OR a sub-project of one,
|
|
211
|
+
// so users immediately see what's missing and where the
|
|
212
|
+
// workspace root is.
|
|
213
|
+
if (projectInfo.workspaceLayout.hasWorkspace || projectInfo.workspaceSubProject) {
|
|
214
|
+
info('');
|
|
215
|
+
info(colors.bold('Workspace:'));
|
|
216
|
+
if (projectInfo.workspaceSubProject) {
|
|
217
|
+
const { kind, root } = projectInfo.workspaceSubProject;
|
|
218
|
+
warning(` You are inside projects/${kind}/ of a workspace.`);
|
|
219
|
+
info(` Root: ${root}`);
|
|
220
|
+
info(colors.dim(` Hint: cd to the workspace root for \`lt fullstack add-${kind === 'api' ? 'app' : 'api'}\``));
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
success(' Detected (pnpm-workspace.yaml, package.json#workspaces, or projects/)');
|
|
224
|
+
}
|
|
225
|
+
const apiMark = projectInfo.workspaceLayout.hasApi ? colors.green('✓') : colors.yellow('✗');
|
|
226
|
+
const appMark = projectInfo.workspaceLayout.hasApp ? colors.green('✓') : colors.yellow('✗');
|
|
227
|
+
info(` ${apiMark} projects/api/`);
|
|
228
|
+
info(` ${appMark} projects/app/`);
|
|
229
|
+
// Suggest the next step when only one half is present.
|
|
230
|
+
if (projectInfo.workspaceLayout.hasApp && !projectInfo.workspaceLayout.hasApi) {
|
|
231
|
+
info(colors.dim(' Hint: `lt fullstack add-api` to integrate a NestJS server.'));
|
|
232
|
+
}
|
|
233
|
+
else if (projectInfo.workspaceLayout.hasApi && !projectInfo.workspaceLayout.hasApp) {
|
|
234
|
+
info(colors.dim(' Hint: `lt fullstack add-app` to integrate a Nuxt or Angular app.'));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
195
237
|
// Show monorepo subprojects if we detected any (typically at monorepo root)
|
|
196
238
|
if (projectInfo.monorepoSubprojects.length > 0) {
|
|
197
239
|
info('');
|
|
@@ -30,8 +30,13 @@ const NewCommand = {
|
|
|
30
30
|
description: 'OCR PDFs to Markdown via marker-pdf (MPS-accelerated on Apple Silicon)',
|
|
31
31
|
hidden: false,
|
|
32
32
|
name: 'ocr',
|
|
33
|
-
|
|
33
|
+
// GluegunCommand types `run` against the base `Toolbox`, but the lt CLI
|
|
34
|
+
// augments it with `helper`, `git`, etc. Cast inside so the implementation
|
|
35
|
+
// remains typed against the project-specific `ExtendedGluegunToolbox` while
|
|
36
|
+
// satisfying the upstream `(toolbox: Toolbox) => void` signature.
|
|
37
|
+
run: (rawToolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
34
38
|
var _a, _b, _c, _d, _e;
|
|
39
|
+
const toolbox = rawToolbox;
|
|
35
40
|
const { parameters, print: { error, info, spin, warning }, } = toolbox;
|
|
36
41
|
const showStatus = !!parameters.options.status;
|
|
37
42
|
const installOnly = !!parameters.options.install;
|