@lenne.tech/cli 1.18.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/bin/lt +6 -2
- 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 +171 -0
- package/build/lib/marker.js +218 -0
- package/build/lib/workspace-integration.js +351 -0
- package/docs/commands.md +150 -4
- package/package.json +2 -1
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const hoist_workspace_pnpm_config_1 = require("../../lib/hoist-workspace-pnpm-config");
|
|
13
|
+
const workspace_integration_1 = require("../../lib/workspace-integration");
|
|
14
|
+
/**
|
|
15
|
+
* Add an API (`projects/api/`) to a fullstack workspace that currently
|
|
16
|
+
* only ships a frontend (`projects/app/`). Mirrors every API-related
|
|
17
|
+
* flag from `lt fullstack init` so the surface area stays in lockstep.
|
|
18
|
+
*
|
|
19
|
+
* Refuses to run if `projects/api/` already exists — use a regular
|
|
20
|
+
* `lt fullstack init` workflow on a fresh directory instead, or remove
|
|
21
|
+
* the existing API first.
|
|
22
|
+
*/
|
|
23
|
+
const NewCommand = {
|
|
24
|
+
alias: ['add-api'],
|
|
25
|
+
description: 'Add API to fullstack workspace',
|
|
26
|
+
hidden: false,
|
|
27
|
+
name: 'add-api',
|
|
28
|
+
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
29
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
30
|
+
const { config, filesystem, git, parameters, patching, print: { error, info, spin, success, warning }, prompt: { ask }, server, system, } = toolbox;
|
|
31
|
+
// Help-JSON support so AI agents can introspect the flags.
|
|
32
|
+
if (toolbox.tools.helpJson({
|
|
33
|
+
aliases: ['add-api'],
|
|
34
|
+
configuration: 'commands.fullstack.*',
|
|
35
|
+
description: 'Add a NestJS API to an existing fullstack workspace',
|
|
36
|
+
name: 'add-api',
|
|
37
|
+
options: [
|
|
38
|
+
{ description: 'API mode', flag: '--api-mode', type: 'string', values: ['Rest', 'GraphQL', 'Both'] },
|
|
39
|
+
{
|
|
40
|
+
description: 'Framework consumption mode',
|
|
41
|
+
flag: '--framework-mode',
|
|
42
|
+
type: 'string',
|
|
43
|
+
values: ['npm', 'vendor'],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
description: 'Branch/tag/commit of upstream nest-server (vendor mode)',
|
|
47
|
+
flag: '--framework-upstream-branch',
|
|
48
|
+
type: 'string',
|
|
49
|
+
},
|
|
50
|
+
{ description: 'Branch of nest-server-starter to clone', flag: '--api-branch', type: 'string' },
|
|
51
|
+
{ description: 'Path to local API template to copy from', flag: '--api-copy', type: 'string' },
|
|
52
|
+
{ description: 'Path to local API template to symlink', flag: '--api-link', type: 'string' },
|
|
53
|
+
{
|
|
54
|
+
description: 'Use experimental nest-base template (Bun + Prisma + Postgres + Better-Auth)',
|
|
55
|
+
flag: '--next',
|
|
56
|
+
type: 'boolean',
|
|
57
|
+
},
|
|
58
|
+
{ description: 'Workspace root (defaults to cwd)', flag: '--workspace-dir', type: 'string' },
|
|
59
|
+
{ description: 'Skip install / format after API integration', flag: '--skip-install', type: 'boolean' },
|
|
60
|
+
{ description: 'Print resolved plan and exit without disk changes', flag: '--dry-run', type: 'boolean' },
|
|
61
|
+
{ description: 'Skip all interactive prompts', flag: '--noConfirm', type: 'boolean' },
|
|
62
|
+
],
|
|
63
|
+
})) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const timer = system.startTimer();
|
|
67
|
+
info('Add API to fullstack workspace');
|
|
68
|
+
toolbox.tools.nonInteractiveHint('lt fullstack add-api --api-mode <Rest|GraphQL|Both> --framework-mode <npm|vendor> [--api-branch <ref>] [--next] [--dry-run] --noConfirm');
|
|
69
|
+
if (!(yield git.gitInstalled())) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const ltConfig = config.loadConfig();
|
|
73
|
+
// Parse CLI options
|
|
74
|
+
const cliApiMode = parameters.options['api-mode'] || parameters.options.apiMode;
|
|
75
|
+
const cliFrameworkMode = parameters.options['framework-mode'];
|
|
76
|
+
const cliFrameworkUpstreamBranch = parameters.options['framework-upstream-branch'];
|
|
77
|
+
const cliApiBranch = parameters.options['api-branch'];
|
|
78
|
+
const cliApiCopy = parameters.options['api-copy'];
|
|
79
|
+
const cliApiLink = parameters.options['api-link'];
|
|
80
|
+
const cliWorkspaceDir = parameters.options['workspace-dir'];
|
|
81
|
+
const cliDryRun = parameters.options['dry-run'];
|
|
82
|
+
const cliSkipInstall = parameters.options['skip-install'];
|
|
83
|
+
const experimental = parameters.options.next === true || parameters.options.next === 'true';
|
|
84
|
+
const dryRun = cliDryRun === true || cliDryRun === 'true';
|
|
85
|
+
const skipInstall = cliSkipInstall === true || cliSkipInstall === 'true';
|
|
86
|
+
const noConfirm = config.getNoConfirm({
|
|
87
|
+
cliValue: parameters.options.noConfirm,
|
|
88
|
+
commandConfig: (_a = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _a === void 0 ? void 0 : _a.fullstack,
|
|
89
|
+
config: ltConfig,
|
|
90
|
+
});
|
|
91
|
+
// Pull the same defaults from lt.config that init does, so commands
|
|
92
|
+
// share configuration without users having to maintain two blocks.
|
|
93
|
+
const configApiMode = (_c = (_b = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _b === void 0 ? void 0 : _b.fullstack) === null || _c === void 0 ? void 0 : _c.apiMode;
|
|
94
|
+
const configFrameworkMode = (_e = (_d = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _d === void 0 ? void 0 : _d.fullstack) === null || _e === void 0 ? void 0 : _e.frameworkMode;
|
|
95
|
+
const configApiBranch = (_g = (_f = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _f === void 0 ? void 0 : _f.fullstack) === null || _g === void 0 ? void 0 : _g.apiBranch;
|
|
96
|
+
const configApiCopy = (_j = (_h = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _h === void 0 ? void 0 : _h.fullstack) === null || _j === void 0 ? void 0 : _j.apiCopy;
|
|
97
|
+
const configApiLink = (_l = (_k = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _k === void 0 ? void 0 : _k.fullstack) === null || _l === void 0 ? void 0 : _l.apiLink;
|
|
98
|
+
const globalApiMode = config.getGlobalDefault(ltConfig, 'apiMode');
|
|
99
|
+
// Workspace detection. Priority:
|
|
100
|
+
// 1. explicit `--workspace-dir <path>` always wins
|
|
101
|
+
// 2. cwd if it itself is a workspace
|
|
102
|
+
// 3. nearest workspace found by walking up from cwd (catches the
|
|
103
|
+
// "user is inside projects/app/src/" case so they don't need
|
|
104
|
+
// to manually pass `--workspace-dir ../..`)
|
|
105
|
+
let workspaceDir;
|
|
106
|
+
if (cliWorkspaceDir) {
|
|
107
|
+
workspaceDir = cliWorkspaceDir;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
const cwdLayout = (0, workspace_integration_1.detectWorkspaceLayout)('.', filesystem);
|
|
111
|
+
if (cwdLayout.hasWorkspace) {
|
|
112
|
+
workspaceDir = '.';
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const upRoot = (0, workspace_integration_1.findWorkspaceRoot)('.', filesystem);
|
|
116
|
+
if (upRoot) {
|
|
117
|
+
workspaceDir = upRoot;
|
|
118
|
+
info(`Detected fullstack workspace at ${upRoot} (walked up from cwd).`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
workspaceDir = '.';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const layout = (0, workspace_integration_1.detectWorkspaceLayout)(workspaceDir, filesystem);
|
|
126
|
+
if (!layout.hasWorkspace) {
|
|
127
|
+
error(`No fullstack workspace detected at "${workspaceDir}". Expected pnpm-workspace.yaml, package.json#workspaces, or a projects/ directory. Use \`lt fullstack init\` for a fresh workspace.`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (layout.hasApi) {
|
|
131
|
+
error(`An API already exists at "${workspaceDir}/projects/api". Remove it first or use \`lt fullstack init\` in a fresh directory.`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// Resolve api mode (CLI > experimental override > config > global > interactive/default).
|
|
135
|
+
let apiMode;
|
|
136
|
+
if (experimental) {
|
|
137
|
+
apiMode = 'Rest';
|
|
138
|
+
info('Using experimental nest-base template (Bun + Prisma + Postgres + Better-Auth)');
|
|
139
|
+
}
|
|
140
|
+
else if (cliApiMode) {
|
|
141
|
+
apiMode = cliApiMode;
|
|
142
|
+
}
|
|
143
|
+
else if (configApiMode) {
|
|
144
|
+
apiMode = configApiMode;
|
|
145
|
+
info(`Using API mode from lt.config: ${apiMode}`);
|
|
146
|
+
}
|
|
147
|
+
else if (globalApiMode) {
|
|
148
|
+
apiMode = globalApiMode;
|
|
149
|
+
info(`Using API mode from lt.config defaults: ${apiMode}`);
|
|
150
|
+
}
|
|
151
|
+
else if (noConfirm) {
|
|
152
|
+
apiMode = 'Rest';
|
|
153
|
+
info('Using default API mode: REST/RPC (noConfirm mode)');
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const apiModeChoice = yield ask({
|
|
157
|
+
choices: [
|
|
158
|
+
'Rest - REST/RPC API with Swagger documentation (recommended)',
|
|
159
|
+
'GraphQL - GraphQL API with subscriptions',
|
|
160
|
+
'Both - REST and GraphQL in parallel (hybrid)',
|
|
161
|
+
],
|
|
162
|
+
initial: 0,
|
|
163
|
+
message: 'API mode?',
|
|
164
|
+
name: 'apiMode',
|
|
165
|
+
type: 'select',
|
|
166
|
+
});
|
|
167
|
+
apiMode = apiModeChoice.apiMode.split(' - ')[0];
|
|
168
|
+
}
|
|
169
|
+
// Resolve framework mode.
|
|
170
|
+
let frameworkMode;
|
|
171
|
+
if (experimental) {
|
|
172
|
+
frameworkMode = 'npm';
|
|
173
|
+
}
|
|
174
|
+
else if (cliFrameworkMode === 'npm' || cliFrameworkMode === 'vendor') {
|
|
175
|
+
frameworkMode = cliFrameworkMode;
|
|
176
|
+
}
|
|
177
|
+
else if (cliFrameworkMode) {
|
|
178
|
+
error(`Invalid --framework-mode value "${cliFrameworkMode}". Use "npm" or "vendor".`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
else if (configFrameworkMode === 'npm' || configFrameworkMode === 'vendor') {
|
|
182
|
+
frameworkMode = configFrameworkMode;
|
|
183
|
+
info(`Using framework mode from lt.config: ${frameworkMode}`);
|
|
184
|
+
}
|
|
185
|
+
else if (noConfirm) {
|
|
186
|
+
frameworkMode = 'npm';
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
const frameworkModeChoice = yield ask({
|
|
190
|
+
choices: [
|
|
191
|
+
'npm - @lenne.tech/nest-server as npm dependency (classic, stable)',
|
|
192
|
+
'vendor - framework core vendored into projects/api/src/core/ (pilot, allows local patches)',
|
|
193
|
+
],
|
|
194
|
+
initial: 0,
|
|
195
|
+
message: 'Framework consumption mode?',
|
|
196
|
+
name: 'frameworkMode',
|
|
197
|
+
type: 'select',
|
|
198
|
+
});
|
|
199
|
+
frameworkMode = frameworkModeChoice.frameworkMode.startsWith('vendor') ? 'vendor' : 'npm';
|
|
200
|
+
}
|
|
201
|
+
const frameworkUpstreamBranch = typeof cliFrameworkUpstreamBranch === 'string' && cliFrameworkUpstreamBranch.length > 0
|
|
202
|
+
? cliFrameworkUpstreamBranch
|
|
203
|
+
: undefined;
|
|
204
|
+
const apiBranch = cliApiBranch || configApiBranch;
|
|
205
|
+
const apiCopy = cliApiCopy || configApiCopy;
|
|
206
|
+
const apiLink = cliApiLink || configApiLink;
|
|
207
|
+
// Derive a project name. We use the workspace directory's basename
|
|
208
|
+
// as a reasonable default; the user can override via --name. This
|
|
209
|
+
// matches the project slug `init.ts` would have written into
|
|
210
|
+
// package.json during the original workspace creation.
|
|
211
|
+
const cliName = parameters.options.name || parameters.first;
|
|
212
|
+
let name = cliName;
|
|
213
|
+
if (!name) {
|
|
214
|
+
// Try to read from existing projects/app/package.json
|
|
215
|
+
const appPkgPath = filesystem.path(workspaceDir, 'projects', 'app', 'package.json');
|
|
216
|
+
if (filesystem.exists(appPkgPath)) {
|
|
217
|
+
const appPkg = filesystem.read(appPkgPath, 'json');
|
|
218
|
+
if (appPkg && typeof appPkg.name === 'string' && appPkg.name && appPkg.name !== 'app') {
|
|
219
|
+
name = appPkg.name;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!name) {
|
|
224
|
+
// Fall back to the directory basename so we never block on this.
|
|
225
|
+
const segments = filesystem.path(workspaceDir).split(/[\\/]/).filter(Boolean);
|
|
226
|
+
name = segments[segments.length - 1] || 'fullstack-app';
|
|
227
|
+
}
|
|
228
|
+
const projectDir = workspaceDir === '.' ? filesystem.cwd().split(/[\\/]/).filter(Boolean).pop() || name : name;
|
|
229
|
+
if (dryRun) {
|
|
230
|
+
info('');
|
|
231
|
+
info('Dry-run plan:');
|
|
232
|
+
info(` workspaceDir: ${workspaceDir}`);
|
|
233
|
+
info(` name: ${name}`);
|
|
234
|
+
info(` apiMode: ${apiMode}`);
|
|
235
|
+
info(` frameworkMode: ${frameworkMode}`);
|
|
236
|
+
if (frameworkUpstreamBranch) {
|
|
237
|
+
info(` frameworkUpstreamBranch: ${frameworkUpstreamBranch}`);
|
|
238
|
+
}
|
|
239
|
+
info(` apiBranch: ${apiBranch || '(default)'}`);
|
|
240
|
+
info(` apiCopy: ${apiCopy || '(none)'}`);
|
|
241
|
+
info(` apiLink: ${apiLink || '(none)'}`);
|
|
242
|
+
info(` experimental (--next): ${experimental}`);
|
|
243
|
+
info('');
|
|
244
|
+
info('Would execute:');
|
|
245
|
+
if (experimental) {
|
|
246
|
+
info(` 1. clone nest-base → ${workspaceDir}/projects/api`);
|
|
247
|
+
info(` 2. patch package.json + bun run rename ${projectDir}`);
|
|
248
|
+
}
|
|
249
|
+
else if (frameworkMode === 'vendor') {
|
|
250
|
+
info(` 1. clone nest-server-starter → ${workspaceDir}/projects/api`);
|
|
251
|
+
info(` 2. clone @lenne.tech/nest-server${frameworkUpstreamBranch ? ` (${frameworkUpstreamBranch})` : ''} → /tmp`);
|
|
252
|
+
info(` 3. vendor core/ + flatten-fix + codemod consumer imports`);
|
|
253
|
+
info(` 4. merge upstream deps`);
|
|
254
|
+
info(` 5. run processApiMode(${apiMode})`);
|
|
255
|
+
if (apiMode === 'Rest')
|
|
256
|
+
info(` 6. restore vendored core essentials (graphql-*)`);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
info(` 1. clone nest-server-starter → ${workspaceDir}/projects/api`);
|
|
260
|
+
info(` 2. run processApiMode(${apiMode})`);
|
|
261
|
+
}
|
|
262
|
+
info(` N. write projects/api/lt.config.json + hoist pnpm overrides`);
|
|
263
|
+
if (!skipInstall)
|
|
264
|
+
info(` M. pnpm install + format projects/api`);
|
|
265
|
+
info('');
|
|
266
|
+
return `fullstack add-api dry-run (${frameworkMode} / ${apiMode})`;
|
|
267
|
+
}
|
|
268
|
+
// Actually integrate the API.
|
|
269
|
+
const apiDest = `${workspaceDir}/projects/api`;
|
|
270
|
+
const apiSpinner = spin(`Integrate API${apiLink ? ' (link)' : apiCopy ? ' (copy)' : apiBranch ? ` (branch: ${apiBranch})` : ''}`);
|
|
271
|
+
const apiResult = yield server.setupServerForFullstack(apiDest, {
|
|
272
|
+
apiMode,
|
|
273
|
+
branch: apiBranch,
|
|
274
|
+
copyPath: apiCopy,
|
|
275
|
+
experimental,
|
|
276
|
+
frameworkMode,
|
|
277
|
+
frameworkUpstreamBranch,
|
|
278
|
+
linkPath: apiLink,
|
|
279
|
+
name,
|
|
280
|
+
projectDir,
|
|
281
|
+
});
|
|
282
|
+
if (!apiResult.success) {
|
|
283
|
+
apiSpinner.fail(`Failed to set up API: ${apiResult.path}`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
apiSpinner.succeed(`API integrated (${apiResult.method})`);
|
|
287
|
+
// For the experimental nest-base template, run the rename script so
|
|
288
|
+
// the four files that reference `nest-base` are aligned with the
|
|
289
|
+
// workspace name. Non-fatal on failure.
|
|
290
|
+
if (experimental && apiResult.method !== 'link') {
|
|
291
|
+
const renameSpinner = spin(`Rename nest-base → ${projectDir}`);
|
|
292
|
+
const renameResult = yield (0, workspace_integration_1.runExperimentalNestBaseRename)({
|
|
293
|
+
apiDir: apiDest,
|
|
294
|
+
patching,
|
|
295
|
+
projectDir,
|
|
296
|
+
system,
|
|
297
|
+
});
|
|
298
|
+
if (renameResult.error) {
|
|
299
|
+
renameSpinner.warn(`Auto-rename failed (${renameResult.error.message}). Run \`bun run rename ${projectDir}\` manually inside projects/api.`);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
renameSpinner.succeed(`Renamed nest-base → ${projectDir} in projects/api`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// Persist apiMode + frameworkMode for downstream generators.
|
|
306
|
+
if (apiResult.method !== 'link') {
|
|
307
|
+
(0, workspace_integration_1.writeApiConfig)({ apiDir: apiDest, apiMode, filesystem, frameworkMode });
|
|
308
|
+
}
|
|
309
|
+
// Hoist pnpm config — `setupServerForFullstack` may have produced a
|
|
310
|
+
// sub-project package.json with `pnpm.overrides` etc. that pnpm
|
|
311
|
+
// ignores at non-root level.
|
|
312
|
+
(0, hoist_workspace_pnpm_config_1.hoistWorkspacePnpmConfig)({
|
|
313
|
+
filesystem,
|
|
314
|
+
projectDir: workspaceDir,
|
|
315
|
+
subProjects: ['projects/api', 'projects/app'],
|
|
316
|
+
});
|
|
317
|
+
// Run install + format unless explicitly skipped (CI/agents may
|
|
318
|
+
// want to chain multiple add-* calls before installing once).
|
|
319
|
+
if (!skipInstall && !experimental) {
|
|
320
|
+
const installSpinner = spin('Install workspace packages');
|
|
321
|
+
try {
|
|
322
|
+
const detectedPm = toolbox.pm.detect(workspaceDir);
|
|
323
|
+
yield system.run(`cd ${workspaceDir} && ${toolbox.pm.install(detectedPm)}`);
|
|
324
|
+
installSpinner.succeed('Successfully installed workspace packages');
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
installSpinner.fail(`Failed to install packages: ${err.message}`);
|
|
328
|
+
warning('Run install manually after fixing the issue.');
|
|
329
|
+
}
|
|
330
|
+
// processApiMode rewrites source files, leaving whitespace
|
|
331
|
+
// artifacts that oxfmt flags in `pnpm run format:check`. Format
|
|
332
|
+
// here so the sub-project lands in a clean state.
|
|
333
|
+
if (filesystem.isDirectory(apiDest)) {
|
|
334
|
+
yield toolbox.apiMode.formatProject(apiDest);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
else if (experimental) {
|
|
338
|
+
info('Skipping workspace install — run `bun install` inside projects/api manually.');
|
|
339
|
+
}
|
|
340
|
+
info('');
|
|
341
|
+
success(`API integrated into ${workspaceDir} in ${toolbox.helper.msToMinutesAndSeconds(timer())}m.`);
|
|
342
|
+
info('');
|
|
343
|
+
info('Next:');
|
|
344
|
+
if (experimental) {
|
|
345
|
+
info(` $ cd ${workspaceDir}/projects/api && bun install`);
|
|
346
|
+
info(` Configure projects/api/.env (see .env.example)`);
|
|
347
|
+
info(` Start Postgres + run prisma generate / migrate`);
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
info(` $ cd ${workspaceDir}`);
|
|
351
|
+
info(` $ ${toolbox.pm.run('start')}`);
|
|
352
|
+
}
|
|
353
|
+
info('');
|
|
354
|
+
if (!toolbox.parameters.options.fromGluegunMenu) {
|
|
355
|
+
process.exit();
|
|
356
|
+
}
|
|
357
|
+
return `added api to workspace ${workspaceDir}`;
|
|
358
|
+
}),
|
|
359
|
+
};
|
|
360
|
+
exports.default = NewCommand;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const hoist_workspace_pnpm_config_1 = require("../../lib/hoist-workspace-pnpm-config");
|
|
13
|
+
const workspace_integration_1 = require("../../lib/workspace-integration");
|
|
14
|
+
/**
|
|
15
|
+
* Add a frontend app (`projects/app/`) to a fullstack workspace that
|
|
16
|
+
* currently only ships an API (`projects/api/`). Mirrors every
|
|
17
|
+
* frontend-related flag from `lt fullstack init` so the surface area
|
|
18
|
+
* stays in lockstep.
|
|
19
|
+
*
|
|
20
|
+
* Refuses to run if `projects/app/` already exists.
|
|
21
|
+
*/
|
|
22
|
+
const NewCommand = {
|
|
23
|
+
alias: ['add-app'],
|
|
24
|
+
description: 'Add app to fullstack workspace',
|
|
25
|
+
hidden: false,
|
|
26
|
+
name: 'add-app',
|
|
27
|
+
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
28
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
29
|
+
const { config, filesystem, frontendHelper, git, parameters, print: { error, info, spin, success, warning }, prompt: { ask }, system, } = toolbox;
|
|
30
|
+
if (toolbox.tools.helpJson({
|
|
31
|
+
aliases: ['add-app'],
|
|
32
|
+
configuration: 'commands.fullstack.*',
|
|
33
|
+
description: 'Add a frontend app to an existing fullstack workspace',
|
|
34
|
+
name: 'add-app',
|
|
35
|
+
options: [
|
|
36
|
+
{ description: 'Frontend framework', flag: '--frontend', type: 'string', values: ['nuxt', 'angular'] },
|
|
37
|
+
{
|
|
38
|
+
description: 'Frontend framework consumption mode',
|
|
39
|
+
flag: '--frontend-framework-mode',
|
|
40
|
+
type: 'string',
|
|
41
|
+
values: ['npm', 'vendor'],
|
|
42
|
+
},
|
|
43
|
+
{ description: 'Branch of the frontend starter to clone', flag: '--frontend-branch', type: 'string' },
|
|
44
|
+
{ description: 'Path to local frontend template to copy from', flag: '--frontend-copy', type: 'string' },
|
|
45
|
+
{ description: 'Path to local frontend template to symlink', flag: '--frontend-link', type: 'string' },
|
|
46
|
+
{ description: 'Use experimental nuxt-base-starter `next` branch', flag: '--next', type: 'boolean' },
|
|
47
|
+
{ description: 'Workspace root (defaults to cwd)', flag: '--workspace-dir', type: 'string' },
|
|
48
|
+
{ description: 'Skip install / format after app integration', flag: '--skip-install', type: 'boolean' },
|
|
49
|
+
{ description: 'Print resolved plan and exit without disk changes', flag: '--dry-run', type: 'boolean' },
|
|
50
|
+
{ description: 'Skip all interactive prompts', flag: '--noConfirm', type: 'boolean' },
|
|
51
|
+
],
|
|
52
|
+
})) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const timer = system.startTimer();
|
|
56
|
+
info('Add app to fullstack workspace');
|
|
57
|
+
toolbox.tools.nonInteractiveHint('lt fullstack add-app --frontend <nuxt|angular> [--frontend-branch <ref>] [--next] [--dry-run] --noConfirm');
|
|
58
|
+
if (!(yield git.gitInstalled())) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const ltConfig = config.loadConfig();
|
|
62
|
+
const cliFrontend = parameters.options.frontend;
|
|
63
|
+
const cliFrontendFrameworkMode = parameters.options['frontend-framework-mode'];
|
|
64
|
+
const cliFrontendBranch = parameters.options['frontend-branch'];
|
|
65
|
+
const cliFrontendCopy = parameters.options['frontend-copy'];
|
|
66
|
+
const cliFrontendLink = parameters.options['frontend-link'];
|
|
67
|
+
const cliWorkspaceDir = parameters.options['workspace-dir'];
|
|
68
|
+
const cliDryRun = parameters.options['dry-run'];
|
|
69
|
+
const cliSkipInstall = parameters.options['skip-install'];
|
|
70
|
+
const experimental = parameters.options.next === true || parameters.options.next === 'true';
|
|
71
|
+
const dryRun = cliDryRun === true || cliDryRun === 'true';
|
|
72
|
+
const skipInstall = cliSkipInstall === true || cliSkipInstall === 'true';
|
|
73
|
+
const noConfirm = config.getNoConfirm({
|
|
74
|
+
cliValue: parameters.options.noConfirm,
|
|
75
|
+
commandConfig: (_a = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _a === void 0 ? void 0 : _a.fullstack,
|
|
76
|
+
config: ltConfig,
|
|
77
|
+
});
|
|
78
|
+
const configFrontend = (_c = (_b = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _b === void 0 ? void 0 : _b.fullstack) === null || _c === void 0 ? void 0 : _c.frontend;
|
|
79
|
+
const configFrontendFrameworkMode = (_e = (_d = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _d === void 0 ? void 0 : _d.fullstack) === null || _e === void 0 ? void 0 : _e.frontendFrameworkMode;
|
|
80
|
+
const configFrontendBranch = (_g = (_f = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _f === void 0 ? void 0 : _f.fullstack) === null || _g === void 0 ? void 0 : _g.frontendBranch;
|
|
81
|
+
const configFrontendCopy = (_j = (_h = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _h === void 0 ? void 0 : _h.fullstack) === null || _j === void 0 ? void 0 : _j.frontendCopy;
|
|
82
|
+
const configFrontendLink = (_l = (_k = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _k === void 0 ? void 0 : _k.fullstack) === null || _l === void 0 ? void 0 : _l.frontendLink;
|
|
83
|
+
// Workspace detection — same priority as `add-api`:
|
|
84
|
+
// 1. explicit `--workspace-dir <path>` always wins
|
|
85
|
+
// 2. cwd if it itself is a workspace
|
|
86
|
+
// 3. nearest workspace by walking up from cwd (so users running
|
|
87
|
+
// from inside `projects/api/src/` don't have to pass
|
|
88
|
+
// `--workspace-dir ../..`)
|
|
89
|
+
let workspaceDir;
|
|
90
|
+
if (cliWorkspaceDir) {
|
|
91
|
+
workspaceDir = cliWorkspaceDir;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const cwdLayout = (0, workspace_integration_1.detectWorkspaceLayout)('.', filesystem);
|
|
95
|
+
if (cwdLayout.hasWorkspace) {
|
|
96
|
+
workspaceDir = '.';
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const upRoot = (0, workspace_integration_1.findWorkspaceRoot)('.', filesystem);
|
|
100
|
+
if (upRoot) {
|
|
101
|
+
workspaceDir = upRoot;
|
|
102
|
+
info(`Detected fullstack workspace at ${upRoot} (walked up from cwd).`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
workspaceDir = '.';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const layout = (0, workspace_integration_1.detectWorkspaceLayout)(workspaceDir, filesystem);
|
|
110
|
+
if (!layout.hasWorkspace) {
|
|
111
|
+
error(`No fullstack workspace detected at "${workspaceDir}". Expected pnpm-workspace.yaml, package.json#workspaces, or a projects/ directory. Use \`lt fullstack init\` for a fresh workspace.`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (layout.hasApp) {
|
|
115
|
+
error(`An app already exists at "${workspaceDir}/projects/app". Remove it first or use \`lt fullstack init\` in a fresh directory.`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Resolve frontend.
|
|
119
|
+
let frontend;
|
|
120
|
+
if (cliFrontend === 'angular' || cliFrontend === 'nuxt') {
|
|
121
|
+
frontend = cliFrontend;
|
|
122
|
+
}
|
|
123
|
+
else if (cliFrontend) {
|
|
124
|
+
error('Invalid --frontend option. Use "angular" or "nuxt".');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
else if (configFrontend === 'angular' || configFrontend === 'nuxt') {
|
|
128
|
+
frontend = configFrontend;
|
|
129
|
+
info(`Using frontend from lt.config: ${frontend}`);
|
|
130
|
+
}
|
|
131
|
+
else if (noConfirm) {
|
|
132
|
+
frontend = 'nuxt';
|
|
133
|
+
info('Using default frontend: nuxt (noConfirm mode)');
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
const choice = yield ask({
|
|
137
|
+
choices: ['angular', 'nuxt'],
|
|
138
|
+
initial: 1,
|
|
139
|
+
message: 'Which frontend framework?',
|
|
140
|
+
name: 'frontend',
|
|
141
|
+
type: 'select',
|
|
142
|
+
});
|
|
143
|
+
frontend = (choice.frontend === 'angular' ? 'angular' : 'nuxt');
|
|
144
|
+
}
|
|
145
|
+
// Resolve frontend framework mode.
|
|
146
|
+
let frontendFrameworkMode;
|
|
147
|
+
if (cliFrontendFrameworkMode === 'npm' || cliFrontendFrameworkMode === 'vendor') {
|
|
148
|
+
frontendFrameworkMode = cliFrontendFrameworkMode;
|
|
149
|
+
}
|
|
150
|
+
else if (cliFrontendFrameworkMode) {
|
|
151
|
+
error(`Invalid --frontend-framework-mode value "${cliFrontendFrameworkMode}". Use "npm" or "vendor".`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
else if (configFrontendFrameworkMode === 'npm' || configFrontendFrameworkMode === 'vendor') {
|
|
155
|
+
frontendFrameworkMode = configFrontendFrameworkMode;
|
|
156
|
+
info(`Using frontend framework mode from lt.config: ${frontendFrameworkMode}`);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
frontendFrameworkMode = 'npm';
|
|
160
|
+
}
|
|
161
|
+
// Branch / copy / link with the same `--next` default that init.ts
|
|
162
|
+
// applies: under `--next`, default the nuxt-base-starter ref to the
|
|
163
|
+
// `next` branch (auth basePath aligned with the experimental API).
|
|
164
|
+
const frontendBranch = cliFrontendBranch || configFrontendBranch || (experimental && frontend === 'nuxt' ? 'next' : undefined);
|
|
165
|
+
const frontendCopy = cliFrontendCopy || configFrontendCopy;
|
|
166
|
+
const frontendLink = cliFrontendLink || configFrontendLink;
|
|
167
|
+
// Derive a project name (kebab-case workspace slug) for env patching.
|
|
168
|
+
let projectName = parameters.options.name;
|
|
169
|
+
if (!projectName) {
|
|
170
|
+
const apiPkgPath = filesystem.path(workspaceDir, 'projects', 'api', 'package.json');
|
|
171
|
+
if (filesystem.exists(apiPkgPath)) {
|
|
172
|
+
const apiPkg = filesystem.read(apiPkgPath, 'json');
|
|
173
|
+
if (apiPkg && typeof apiPkg.name === 'string' && apiPkg.name) {
|
|
174
|
+
projectName = apiPkg.name;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!projectName) {
|
|
179
|
+
const segments = filesystem.path(workspaceDir).split(/[\\/]/).filter(Boolean);
|
|
180
|
+
projectName = segments[segments.length - 1] || 'fullstack-app';
|
|
181
|
+
}
|
|
182
|
+
if (dryRun) {
|
|
183
|
+
info('');
|
|
184
|
+
info('Dry-run plan:');
|
|
185
|
+
info(` workspaceDir: ${workspaceDir}`);
|
|
186
|
+
info(` projectName: ${projectName}`);
|
|
187
|
+
info(` frontend: ${frontend}`);
|
|
188
|
+
info(` frontendFrameworkMode: ${frontendFrameworkMode}`);
|
|
189
|
+
info(` frontendBranch: ${frontendBranch || '(default)'}`);
|
|
190
|
+
info(` frontendCopy: ${frontendCopy || '(none)'}`);
|
|
191
|
+
info(` frontendLink: ${frontendLink || '(none)'}`);
|
|
192
|
+
info(` experimental (--next): ${experimental}`);
|
|
193
|
+
info('');
|
|
194
|
+
info('Would execute:');
|
|
195
|
+
info(` 1. setup ${frontend} → ${workspaceDir}/projects/app`);
|
|
196
|
+
if (frontend === 'nuxt' && frontendFrameworkMode === 'vendor') {
|
|
197
|
+
info(` 2. clone @lenne.tech/nuxt-extensions → /tmp`);
|
|
198
|
+
info(` 3. vendor app/core/ (module.ts + runtime/)`);
|
|
199
|
+
info(` 4. rewrite nuxt.config.ts module entry`);
|
|
200
|
+
}
|
|
201
|
+
info(` N. patch projects/app/.env with NUXT_PUBLIC_STORAGE_PREFIX`);
|
|
202
|
+
if (!skipInstall)
|
|
203
|
+
info(` M. pnpm install + format projects/app`);
|
|
204
|
+
info('');
|
|
205
|
+
return `fullstack add-app dry-run (${frontend} / ${frontendFrameworkMode})`;
|
|
206
|
+
}
|
|
207
|
+
const appDest = `${workspaceDir}/projects/app`;
|
|
208
|
+
const appSpinner = spin(`Integrate ${frontend}${frontendLink ? ' (link)' : frontendCopy ? ' (copy)' : frontendBranch ? ` (branch: ${frontendBranch})` : ''}`);
|
|
209
|
+
const isNuxt = frontend === 'nuxt';
|
|
210
|
+
const result = isNuxt
|
|
211
|
+
? yield frontendHelper.setupNuxt(appDest, {
|
|
212
|
+
branch: frontendBranch,
|
|
213
|
+
copyPath: frontendCopy,
|
|
214
|
+
linkPath: frontendLink,
|
|
215
|
+
skipInstall: true,
|
|
216
|
+
})
|
|
217
|
+
: yield frontendHelper.setupAngular(appDest, {
|
|
218
|
+
branch: frontendBranch,
|
|
219
|
+
copyPath: frontendCopy,
|
|
220
|
+
linkPath: frontendLink,
|
|
221
|
+
skipGitInit: true,
|
|
222
|
+
skipHuskyRemoval: true,
|
|
223
|
+
skipInstall: true,
|
|
224
|
+
});
|
|
225
|
+
if (!result.success) {
|
|
226
|
+
appSpinner.fail(`Failed to set up ${frontend} frontend: ${result.path}`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
appSpinner.succeed(`${frontend} integrated (${result.method})`);
|
|
230
|
+
// Patch frontend .env (skip on link mode — points at user's checkout).
|
|
231
|
+
if (result.method !== 'link') {
|
|
232
|
+
frontendHelper.patchFrontendEnv(appDest, projectName);
|
|
233
|
+
}
|
|
234
|
+
// Vendor frontend if requested. Skipped on link mode for the same
|
|
235
|
+
// reason as `init.ts`.
|
|
236
|
+
if (isNuxt && frontendFrameworkMode === 'vendor' && result.method !== 'link') {
|
|
237
|
+
const vendorSpinner = spin('Converting frontend to vendor mode...');
|
|
238
|
+
try {
|
|
239
|
+
yield frontendHelper.convertAppCloneToVendored({
|
|
240
|
+
dest: appDest,
|
|
241
|
+
projectName,
|
|
242
|
+
});
|
|
243
|
+
vendorSpinner.succeed('Frontend converted to vendor mode (app/core/)');
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
vendorSpinner.fail(`Frontend vendor conversion failed: ${err.message}`);
|
|
247
|
+
warning('Continuing with npm mode for frontend.');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Hoist pnpm config (frontend templates may carry pnpm.overrides too).
|
|
251
|
+
(0, hoist_workspace_pnpm_config_1.hoistWorkspacePnpmConfig)({
|
|
252
|
+
filesystem,
|
|
253
|
+
projectDir: workspaceDir,
|
|
254
|
+
subProjects: ['projects/api', 'projects/app'],
|
|
255
|
+
});
|
|
256
|
+
if (!skipInstall) {
|
|
257
|
+
const installSpinner = spin('Install workspace packages');
|
|
258
|
+
try {
|
|
259
|
+
const detectedPm = toolbox.pm.detect(workspaceDir);
|
|
260
|
+
yield system.run(`cd ${workspaceDir} && ${toolbox.pm.install(detectedPm)}`);
|
|
261
|
+
installSpinner.succeed('Successfully installed workspace packages');
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
installSpinner.fail(`Failed to install packages: ${err.message}`);
|
|
265
|
+
warning('Run install manually after fixing the issue.');
|
|
266
|
+
}
|
|
267
|
+
if (isNuxt && filesystem.isDirectory(appDest)) {
|
|
268
|
+
yield toolbox.apiMode.formatProject(appDest);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
info('');
|
|
272
|
+
success(`App integrated into ${workspaceDir} in ${toolbox.helper.msToMinutesAndSeconds(timer())}m.`);
|
|
273
|
+
info('');
|
|
274
|
+
info('Next:');
|
|
275
|
+
info(` $ cd ${workspaceDir}`);
|
|
276
|
+
info(` $ ${toolbox.pm.run('start')}`);
|
|
277
|
+
info('');
|
|
278
|
+
if (!toolbox.parameters.options.fromGluegunMenu) {
|
|
279
|
+
process.exit();
|
|
280
|
+
}
|
|
281
|
+
return `added app to workspace ${workspaceDir}`;
|
|
282
|
+
}),
|
|
283
|
+
};
|
|
284
|
+
exports.default = NewCommand;
|