@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.
@@ -0,0 +1,351 @@
1
+ "use strict";
2
+ /**
3
+ * Helpers for integrating an API (`projects/api/`) or an App
4
+ * (`projects/app/`) into a fullstack workspace. Used by `lt fullstack
5
+ * init` (full-workspace flow) and by `lt fullstack add-api` /
6
+ * `lt fullstack add-app` (incremental flow on an already-existing
7
+ * workspace that only ships one half of the stack).
8
+ *
9
+ * The functions here own the small amount of "monorepo glue" that sits
10
+ * between the framework-agnostic setup primitives in
11
+ * `extensions/server.ts` and `extensions/frontend-helper.ts`:
12
+ *
13
+ * - writing `projects/api/lt.config.json` with the resolved api/
14
+ * framework mode so that follow-up generators (lt server module
15
+ * etc.) pick it up without re-probing,
16
+ * - patching the frontend `.env` with the project-specific storage
17
+ * prefix,
18
+ * - running the experimental `bun run rename` step for the
19
+ * `--next` nest-base template,
20
+ * - running the post-install format pass on the touched sub-project,
21
+ *
22
+ * They deliberately do NOT manage workspace-level concerns
23
+ * (lt-monorepo clone, root CLAUDE.md patching, top-level pnpm install,
24
+ * git initialisation). Those stay in `commands/fullstack/init.ts`
25
+ * because they only happen once per workspace creation. add-api /
26
+ * add-app run on an already-installed workspace and only need to wire
27
+ * in the new sub-project plus run a workspace-wide install.
28
+ */
29
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
30
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
31
+ return new (P || (P = Promise))(function (resolve, reject) {
32
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
33
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
34
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
35
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
36
+ });
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.detectSubProjectContext = detectSubProjectContext;
40
+ exports.detectWorkspaceLayout = detectWorkspaceLayout;
41
+ exports.findWorkspaceRoot = findWorkspaceRoot;
42
+ exports.isNonInteractive = isNonInteractive;
43
+ exports.runExperimentalNestBaseRename = runExperimentalNestBaseRename;
44
+ exports.runStandaloneWorkspaceGate = runStandaloneWorkspaceGate;
45
+ exports.shouldProceedAsStandalone = shouldProceedAsStandalone;
46
+ exports.writeApiConfig = writeApiConfig;
47
+ /**
48
+ * Detect whether the current working directory IS a sub-project of an
49
+ * lt-monorepo workspace (i.e. cwd is `projects/api/` or `projects/app/`,
50
+ * or any nested directory thereof). Returns the workspace root + the
51
+ * sub-project kind so the caller can give a precise hint like
52
+ * "you're inside projects/api — go up to the workspace root".
53
+ *
54
+ * Returns `null` when cwd is not inside such a sub-project.
55
+ */
56
+ function detectSubProjectContext(startDir, filesystem) {
57
+ const root = findWorkspaceRoot(startDir, filesystem);
58
+ if (!root)
59
+ return null;
60
+ // If the start dir IS the root, we're not inside a sub-project.
61
+ if (root === startDir)
62
+ return null;
63
+ const apiDir = filesystem.path(root, 'projects', 'api');
64
+ const appDir = filesystem.path(root, 'projects', 'app');
65
+ // Resolve absolute prefixes for a clean startsWith compare. We use
66
+ // the gluegun-resolved paths because all callers go through it.
67
+ const startAbs = filesystem.path(startDir);
68
+ if (startAbs === apiDir || startAbs.startsWith(`${apiDir}/`)) {
69
+ return { kind: 'api', subProjectDir: apiDir, workspaceRoot: root };
70
+ }
71
+ if (startAbs === appDir || startAbs.startsWith(`${appDir}/`)) {
72
+ return { kind: 'app', subProjectDir: appDir, workspaceRoot: root };
73
+ }
74
+ return null;
75
+ }
76
+ /**
77
+ * Detect what's already present in a workspace directory. Used by the
78
+ * fullstack commands to decide whether to perform a full init, only
79
+ * add the missing sub-project, or refuse because both halves already
80
+ * exist.
81
+ */
82
+ function detectWorkspaceLayout(workspaceDir, filesystem) {
83
+ const projectsDir = `${workspaceDir}/projects`;
84
+ const apiDir = `${projectsDir}/api`;
85
+ const appDir = `${projectsDir}/app`;
86
+ // `hasWorkspaceMarker` covers pnpm-workspace.yaml, npm/yarn
87
+ // `workspaces` in package.json, and the `projects/` directory
88
+ // convention. Mirrors what `findWorkspaceRoot` walks for.
89
+ const hasWorkspace = hasWorkspaceMarker(workspaceDir, filesystem);
90
+ // For "hasApi"/"hasApp", a directory existing is not enough — empty
91
+ // or stub directories from a partially-cloned monorepo would yield
92
+ // false positives. Require at least a package.json inside.
93
+ const hasApi = filesystem.exists(`${apiDir}/package.json`) === 'file';
94
+ const hasApp = filesystem.exists(`${appDir}/package.json`) === 'file';
95
+ return { hasApi, hasApp, hasWorkspace, workspaceDir };
96
+ }
97
+ /**
98
+ * Walk up from `startDir` until a workspace marker is found or the
99
+ * filesystem root is reached. Limited to 6 levels to avoid pathological
100
+ * scans on deeply-nested CWDs (e.g. inside a temp dir hierarchy).
101
+ *
102
+ * Returns the directory that contains the marker, or `null` if none
103
+ * was found within the search budget. Used by the standalone commands
104
+ * to detect the case "user is inside `projects/api/` and ran
105
+ * `lt frontend nuxt` there".
106
+ */
107
+ function findWorkspaceRoot(startDir, filesystem, maxDepth = 6) {
108
+ // Resolve to an absolute path so the parent traversal is reliable.
109
+ // gluegun's filesystem.path is a join helper; we want the OS path.
110
+ let cur = startDir;
111
+ // First check the start dir itself.
112
+ if (hasWorkspaceMarker(cur, filesystem))
113
+ return cur;
114
+ for (let i = 0; i < maxDepth; i++) {
115
+ const parent = filesystem.path(cur, '..');
116
+ if (parent === cur)
117
+ return null;
118
+ if (hasWorkspaceMarker(parent, filesystem))
119
+ return parent;
120
+ cur = parent;
121
+ }
122
+ return null;
123
+ }
124
+ /**
125
+ * Treat the caller as "non-interactive" (KI/CI) when either
126
+ * - `--noConfirm` was passed explicitly, OR
127
+ * - stdin is not a TTY (typical for `claude < script.txt`, piped CI
128
+ * runs, or any agent that captures the CLI's stdout).
129
+ *
130
+ * The TTY check catches AI agents that call `lt …` without
131
+ * `--noConfirm` (Claude Code does this) and would otherwise hit the
132
+ * `confirm()` prompt forever. Caller can opt out via `force`.
133
+ *
134
+ * Exposed so commands can derive their `noConfirm` value once and
135
+ * pass the same boolean to `shouldProceedAsStandalone`.
136
+ */
137
+ function isNonInteractive(noConfirmFlag) {
138
+ if (noConfirmFlag)
139
+ return true;
140
+ // process.stdin may be undefined in some test runners — guard.
141
+ return Boolean(process.stdin && process.stdin.isTTY === false);
142
+ }
143
+ /**
144
+ * Run the experimental `bun run rename <projectDir>` step. Only relevant
145
+ * for the `--next` nest-base template (it ships hard-coded `nest-base`
146
+ * references in package.json, README.md, portless.yml, and
147
+ * docker-compose.yml). Failures are non-fatal — the workspace is still
148
+ * usable, and the user can re-run the rename script manually.
149
+ *
150
+ * Returns true if the step was attempted (regardless of success). The
151
+ * caller decides whether to surface a warning.
152
+ */
153
+ function runExperimentalNestBaseRename(options) {
154
+ return __awaiter(this, void 0, void 0, function* () {
155
+ const { apiDir, patching, projectDir, system } = options;
156
+ // setupServerForFullstack already patched package.json to set
157
+ // `name = projectDir`. The rename planner reads that name as the
158
+ // "old" slug, which would short-circuit the rest of the rewrites
159
+ // because they still say `nest-base`. Restore the canonical
160
+ // `name = "nest-base"` first so the planner has a coherent starting
161
+ // state across all four files.
162
+ yield patching.update(`${apiDir}/package.json`, (config) => {
163
+ config.name = 'nest-base';
164
+ return config;
165
+ });
166
+ try {
167
+ yield system.run(`cd ${apiDir} && bun run rename ${projectDir}`);
168
+ return { attempted: true };
169
+ }
170
+ catch (err) {
171
+ return { attempted: true, error: err };
172
+ }
173
+ });
174
+ }
175
+ /**
176
+ * Print + prompt + decision for a standalone scaffolding command's
177
+ * workspace gate. Centralises the ~25 lines of identical logic that
178
+ * `lt server create`, `lt frontend nuxt`, and `lt frontend angular`
179
+ * each had inline.
180
+ *
181
+ * Side effects (intentionally bundled — easier to reason about as one
182
+ * unit, and the three commands all want the same shape):
183
+ * - prints the workspace-detected note + sub-project hint
184
+ * - asks the user via `confirm()` when interactive
185
+ * - prints the refusal/abort reason via `print.error` when refused
186
+ * - calls `process.exit(1)` on refusal (unless `fromGluegunMenu`)
187
+ *
188
+ * Returns `true` when the caller may proceed, `false` when the caller
189
+ * has already been told to abort and should `return` from its `run`.
190
+ *
191
+ * The shape stays narrow on purpose — adding more knobs (e.g.
192
+ * "abort message format") would just push the duplication elsewhere.
193
+ */
194
+ function runStandaloneWorkspaceGate(options) {
195
+ return __awaiter(this, void 0, void 0, function* () {
196
+ var _a;
197
+ const { cwd, filesystem, force, fromGluegunMenu, noConfirmFlag, pieceName, print: { confirm, error, info }, projectKind, suggestion, } = options;
198
+ // Sub-project hint: if the user is inside `projects/api/` or
199
+ // `projects/app/` of a workspace, point them at the root explicitly
200
+ // — the standalone path would otherwise clone a sibling tree
201
+ // *inside* the existing sub-project, which is almost never wanted.
202
+ const subProject = detectSubProjectContext(cwd, filesystem);
203
+ if (subProject) {
204
+ info('');
205
+ info(`You appear to be inside projects/${subProject.kind}/ of a workspace at ${subProject.workspaceRoot}.`);
206
+ info(` → Run \`${suggestion}\` from the workspace root instead.`);
207
+ error(`Refusing to create a standalone ${projectKind} from inside a sub-project. cd to the workspace root first.`);
208
+ if (!fromGluegunMenu)
209
+ process.exit(1);
210
+ return false;
211
+ }
212
+ const layout = detectWorkspaceLayout(cwd, filesystem);
213
+ if (!layout.hasWorkspace) {
214
+ return true; // Plain dir → standalone is fine, no gate.
215
+ }
216
+ const alreadyHas = pieceName === 'api' ? layout.hasApi : layout.hasApp;
217
+ info('');
218
+ info('Note: current directory looks like a fullstack workspace (pnpm-workspace.yaml, package.json#workspaces, or projects/).');
219
+ if (!alreadyHas) {
220
+ info(` → To integrate the ${pieceName} into this workspace, use \`${suggestion}\`.`);
221
+ }
222
+ else {
223
+ info(` → projects/${pieceName}/ already exists in this workspace; this command would create a separate ${projectKind}.`);
224
+ }
225
+ const nonInteractive = isNonInteractive(noConfirmFlag);
226
+ const userConfirmed = nonInteractive
227
+ ? undefined
228
+ : yield confirm(`Proceed with standalone ${projectKind} creation anyway?`, false);
229
+ const decision = shouldProceedAsStandalone({
230
+ force,
231
+ nonInteractive,
232
+ projectKind,
233
+ suggestion,
234
+ userConfirmed,
235
+ });
236
+ if (!decision.proceed) {
237
+ error((_a = decision.reason) !== null && _a !== void 0 ? _a : 'Aborted.');
238
+ if (!fromGluegunMenu)
239
+ process.exit(1);
240
+ return false;
241
+ }
242
+ if (nonInteractive && force) {
243
+ info(' --force set — continuing despite workspace context.');
244
+ }
245
+ info('');
246
+ return true;
247
+ });
248
+ }
249
+ /**
250
+ * Decide whether a standalone scaffolding command (`lt server create`,
251
+ * `lt frontend nuxt`, `lt frontend angular`) should run inside a
252
+ * directory that already looks like a fullstack workspace.
253
+ *
254
+ * Three modes:
255
+ *
256
+ * - **interactive** (the user can answer a prompt) — caller asks via
257
+ * `confirm()` and passes the result as `userConfirmed`.
258
+ * - **non-interactive without force** — refuse. This is the path AI
259
+ * agents and CI scripts take by default (either `--noConfirm` was
260
+ * set, or stdin isn't a TTY). Forcing them onto the workspace-
261
+ * aware command (`add-api` / `add-app`) prevents stray side-by-
262
+ * side clones that pnpm-workspace.yaml does not pick up.
263
+ * - **non-interactive with --force** — proceed, but the caller
264
+ * should log a hint so the override is visible in CI logs.
265
+ *
266
+ * The function does NOT print or prompt. It only decides; the caller
267
+ * owns the interaction surface.
268
+ */
269
+ function shouldProceedAsStandalone(options) {
270
+ const { force, nonInteractive, projectKind, suggestion, userConfirmed } = options;
271
+ // Interactive caller already gave an explicit yes/no.
272
+ if (userConfirmed !== undefined) {
273
+ return userConfirmed
274
+ ? { proceed: true }
275
+ : { proceed: false, reason: `Aborted standalone ${projectKind} creation. Use \`${suggestion}\` instead.` };
276
+ }
277
+ // Non-interactive path: refuse unless --force is set. This is the
278
+ // AI-agent / CI default — fail loud rather than silently produce a
279
+ // stray clone that does not integrate with the workspace.
280
+ if (nonInteractive && !force) {
281
+ return {
282
+ proceed: false,
283
+ reason: `Refusing to create a standalone ${projectKind} inside an existing fullstack workspace ` +
284
+ `(non-interactive caller detected). ` +
285
+ `Use \`${suggestion}\` for the workspace-aware flow, or pass --force to override (rare).`,
286
+ };
287
+ }
288
+ // Non-interactive + force: caller knows what they want.
289
+ return { proceed: true };
290
+ }
291
+ /**
292
+ * Write `projects/api/lt.config.json` with the apiMode and frameworkMode
293
+ * baked in so follow-up generators (lt server module, addProp,
294
+ * permissions) can pick the correct controller type and detect vendor
295
+ * mode without re-probing the file tree.
296
+ *
297
+ * Idempotent: overwrites whatever was at `lt.config.json` so a re-run
298
+ * after an apiMode change reflects the new value.
299
+ */
300
+ function writeApiConfig(options) {
301
+ const { apiDir, apiMode, filesystem, frameworkMode } = options;
302
+ filesystem.write(filesystem.path(apiDir, 'lt.config.json'), {
303
+ commands: {
304
+ server: {
305
+ module: {
306
+ controller: apiMode,
307
+ },
308
+ },
309
+ },
310
+ meta: {
311
+ apiMode,
312
+ frameworkMode,
313
+ version: '1.0.0',
314
+ },
315
+ }, { jsonIndent: 2 });
316
+ }
317
+ /**
318
+ * Probe a single directory for workspace markers. Pure helper used by
319
+ * `detectWorkspaceLayout` and `findWorkspaceRoot`.
320
+ *
321
+ * Recognised markers (any one is sufficient):
322
+ * - `pnpm-workspace.yaml` — pnpm workspace
323
+ * - `package.json` with `workspaces` field — npm/yarn/bun workspaces
324
+ * - `projects/` directory — lt-monorepo convention
325
+ *
326
+ * Returns false for `node_modules`-style directories that may contain
327
+ * a stray `package.json` with `workspaces`.
328
+ */
329
+ function hasWorkspaceMarker(dir, filesystem) {
330
+ var _a;
331
+ if (filesystem.exists(`${dir}/pnpm-workspace.yaml`) === 'file')
332
+ return true;
333
+ if (filesystem.exists(`${dir}/projects`) === 'dir')
334
+ return true;
335
+ const pkgPath = `${dir}/package.json`;
336
+ if (filesystem.exists(pkgPath) !== 'file')
337
+ return false;
338
+ const pkg = filesystem.read(pkgPath, 'json');
339
+ if (!pkg)
340
+ return false;
341
+ // npm/yarn workspaces: `workspaces` is either an array of globs
342
+ // (`["packages/*"]`) or an object with a `packages` array
343
+ // (yarn classic). Both count.
344
+ const ws = pkg.workspaces;
345
+ if (Array.isArray(ws) && ws.length > 0)
346
+ return true;
347
+ if (ws && typeof ws === 'object' && Array.isArray(ws.packages)) {
348
+ return ((_a = ws.packages) !== null && _a !== void 0 ? _a : []).length > 0;
349
+ }
350
+ return false;
351
+ }
package/docs/commands.md CHANGED
@@ -67,7 +67,7 @@ lt cli rename <new-name>
67
67
 
68
68
  ### `lt server create`
69
69
 
70
- Creates a new NestJS server project.
70
+ Creates a new standalone NestJS server project in a sibling directory. For an in-workspace API, prefer [`lt fullstack add-api`](#lt-fullstack-add-api).
71
71
 
72
72
  **Usage:**
73
73
  ```bash
@@ -77,17 +77,27 @@ lt server create [name] [options]
77
77
  **Options:**
78
78
  | Option | Description |
79
79
  |--------|-------------|
80
+ | `--name <name>` | Server name (preferred over the positional argument) |
80
81
  | `--description <text>` | Project description |
81
82
  | `--author <name>` | Author name |
82
83
  | `--api-mode <Rest\|GraphQL\|Both>` | API mode (ignored with `--next`) |
83
84
  | `--framework-mode <npm\|vendor>` | Framework consumption mode (ignored with `--next`) |
85
+ | `--framework-upstream-branch <ref>` | Upstream `nest-server` branch/tag/commit to vendor (only with `--framework-mode vendor`) |
84
86
  | `--branch <branch>` / `-b` | Branch of nest-server-starter to use as template |
85
87
  | `--copy <path>` / `-c` | Copy from local template directory instead of cloning |
86
88
  | `--link <path>` | Symlink to local template directory (fastest, changes affect original) |
87
89
  | `--git` | Initialize git repository |
88
90
  | `--next` | **Experimental:** clone [`nest-base`](https://github.com/lenneTech/nest-base) (Bun + Prisma 7 + Postgres + Better-Auth) instead of `nest-server-starter`. Skips API-mode / vendor-mode / install / lt.config.json processing. |
91
+ | `--dry-run` | Print the resolved plan and exit without making any changes |
92
+ | `--force` | Override the workspace-detection abort under `--noConfirm` |
89
93
  | `--noConfirm` | Skip confirmation prompts |
90
94
 
95
+ **Workspace-awareness:** When run inside a directory that already looks like a fullstack workspace (contains `pnpm-workspace.yaml` or `projects/`), the command behaves differently per mode:
96
+
97
+ - **interactive** → asks for confirmation before creating a stray standalone clone
98
+ - **`--noConfirm` without `--force`** → **refuses** with exit code 1 and points the caller to `lt fullstack add-api`. This is the default behaviour for AI agents and CI scripts: fail loud rather than produce a stray clone that pnpm-workspace.yaml does not pick up.
99
+ - **`--noConfirm --force`** → proceeds and logs a hint so the override is visible in CI logs.
100
+
91
101
  **CLAUDE.md Patching:** If the project contains a `CLAUDE.md`, the generic API mode description is replaced with the selected mode. In single-mode projects (`Rest` or `GraphQL`), the "API Mode System" documentation section is condensed to a brief note. Skipped when `--next` is used.
92
102
 
93
103
  **Configuration:** `commands.server.create.*`, `defaults.author`, `defaults.noConfirm`
@@ -558,6 +568,82 @@ Additionally, the API's `CLAUDE.md` is patched to reflect the selected API mode
558
568
 
559
569
  **Configuration:** `commands.fullstack.*`, `defaults.noConfirm`
560
570
 
571
+ **Auto-detection in existing workspaces:** When `lt fullstack init` runs without a name argument inside a directory that already looks like a fullstack workspace (contains `pnpm-workspace.yaml` or `projects/`), it inspects the layout and dispatches to the matching incremental command:
572
+
573
+ - both `projects/api` and `projects/app` exist → refuses with a hint to use `add-api` / `add-app` directly
574
+ - only `projects/app` exists → delegates to `lt fullstack add-api` (with all original flags forwarded)
575
+ - only `projects/api` exists → delegates to `lt fullstack add-app`
576
+ - neither exists → falls through to the regular new-workspace flow
577
+
578
+ To force a brand-new workspace from inside an existing one, pass `--name <slug>`.
579
+
580
+ ---
581
+
582
+ ### `lt fullstack add-api`
583
+
584
+ Add a NestJS API (`projects/api/`) to an existing fullstack workspace that currently only contains a frontend (`projects/app/`). Mirrors every API-related flag from `lt fullstack init` so configuration stays consistent across both flows.
585
+
586
+ **Usage:**
587
+ ```bash
588
+ lt fullstack add-api [options]
589
+ ```
590
+
591
+ **Options:**
592
+ | Option | Description |
593
+ |--------|-------------|
594
+ | `--api-mode <mode>` | API mode: `Rest`, `GraphQL`, or `Both` |
595
+ | `--framework-mode <mode>` | Backend framework consumption mode: `npm` (classic) or `vendor` (core copied to `src/core/`) |
596
+ | `--framework-upstream-branch <ref>` | Upstream `nest-server` branch/tag/commit to vendor (only with `--framework-mode vendor`) |
597
+ | `--api-branch <branch>` | Branch of `nest-server-starter` to clone |
598
+ | `--api-copy <path>` | Copy API from a local template directory |
599
+ | `--api-link <path>` | Symlink API to a local template directory (fastest, changes affect original) |
600
+ | `--next` | **Experimental:** clone [`nest-base`](https://github.com/lenneTech/nest-base) (Bun + Prisma 7 + Postgres + Better-Auth) instead of `nest-server-starter`. Forces `--api-mode Rest` and `--framework-mode npm`, runs `bun run rename` post-clone, skips workspace install. |
601
+ | `--workspace-dir <path>` | Workspace root. When omitted, defaults to cwd; if cwd is not a workspace, the command walks up until it finds one (so it works from inside `projects/api/src/`). |
602
+ | `--skip-install` | Skip `pnpm install` and the post-install format pass |
603
+ | `--dry-run` | Print the resolved plan without making any changes |
604
+ | `--noConfirm` | Skip all interactive prompts |
605
+
606
+ **Refusal cases:**
607
+ - `projects/api/` already exists → suggests `lt fullstack init` in a fresh directory
608
+ - no workspace detected at the target path → asks the user to run `lt fullstack init` first
609
+
610
+ **Side effects:** writes `projects/api/lt.config.json` with the resolved `apiMode` + `frameworkMode`, hoists workspace-scoped `pnpm.overrides` from sub-projects to the root, runs `pnpm install` + `oxfmt` on the new sub-project (unless `--skip-install` is set).
611
+
612
+ **Configuration:** Reads `commands.fullstack.*` (same keys as `lt fullstack init`).
613
+
614
+ ---
615
+
616
+ ### `lt fullstack add-app`
617
+
618
+ Add a frontend app (`projects/app/`) to an existing fullstack workspace that currently only contains an API (`projects/api/`). Mirrors every frontend-related flag from `lt fullstack init`.
619
+
620
+ **Usage:**
621
+ ```bash
622
+ lt fullstack add-app [options]
623
+ ```
624
+
625
+ **Options:**
626
+ | Option | Description |
627
+ |--------|-------------|
628
+ | `--frontend <type>` | Frontend framework: `nuxt` or `angular` |
629
+ | `--frontend-framework-mode <mode>` | Frontend framework consumption mode: `npm` or `vendor` (nuxt-extensions copied to `app/core/`) |
630
+ | `--frontend-branch <branch>` | Branch of the frontend starter to clone (`ng-base-starter` or `nuxt-base-starter`) |
631
+ | `--frontend-copy <path>` | Copy frontend from a local template directory |
632
+ | `--frontend-link <path>` | Symlink frontend to a local template directory (fastest, changes affect original) |
633
+ | `--next` | Default the nuxt-base-starter ref to the `next` branch (auth `basePath` aligned with the experimental `--next` API) |
634
+ | `--workspace-dir <path>` | Workspace root. When omitted, defaults to cwd; if cwd is not a workspace, the command walks up until it finds one (so it works from inside `projects/api/src/`). |
635
+ | `--skip-install` | Skip `pnpm install` and the post-install format pass |
636
+ | `--dry-run` | Print the resolved plan without making any changes |
637
+ | `--noConfirm` | Skip all interactive prompts |
638
+
639
+ **Refusal cases:**
640
+ - `projects/app/` already exists → suggests `lt fullstack init` in a fresh directory
641
+ - no workspace detected at the target path → asks the user to run `lt fullstack init` first
642
+
643
+ **Side effects:** patches `projects/app/.env` with a project-specific `NUXT_PUBLIC_STORAGE_PREFIX`, optionally vendorizes `nuxt-extensions` into `app/core/`, hoists workspace-scoped `pnpm.overrides`, runs `pnpm install` + `oxfmt` on the new sub-project (unless `--skip-install` is set).
644
+
645
+ **Configuration:** Reads `commands.fullstack.*` (same keys as `lt fullstack init`).
646
+
561
647
  ---
562
648
 
563
649
  ### `lt fullstack convert-mode`
@@ -689,7 +775,7 @@ lt npm update
689
775
 
690
776
  ### `lt frontend angular`
691
777
 
692
- Creates a new Angular workspace using ng-base-starter.
778
+ Creates a new standalone Angular workspace using ng-base-starter. For an in-workspace app, prefer [`lt fullstack add-app --frontend angular`](#lt-fullstack-add-app).
693
779
 
694
780
  **Usage:**
695
781
  ```bash
@@ -699,21 +785,26 @@ lt frontend angular [name] [options]
699
785
  **Options:**
700
786
  | Option | Description |
701
787
  |--------|-------------|
788
+ | `--name <name>` | Workspace name (preferred over the positional argument) |
702
789
  | `--branch <branch>` / `-b` | Branch of ng-base-starter to use as template |
703
790
  | `--copy <path>` / `-c` | Copy from local template directory instead of cloning |
704
791
  | `--link <path>` | Symlink to local template directory (fastest, changes affect original) |
705
792
  | `--localize` | Enable Angular localize |
706
793
  | `--noLocalize` | Disable Angular localize |
707
794
  | `--gitLink <url>` | Git repository URL to link |
795
+ | `--dry-run` | Print the resolved plan and exit without making any changes |
796
+ | `--force` | Override the workspace-detection abort under `--noConfirm` |
708
797
  | `--noConfirm` / `-y` | Skip confirmation prompts |
709
798
 
799
+ **Workspace-awareness:** Inside a fullstack workspace the command is interactive (confirm prompt), but under `--noConfirm` it **refuses** with exit code 1 and points the caller to `lt fullstack add-app --frontend angular`. Pass `--noConfirm --force` to override (rare).
800
+
710
801
  **Configuration:** `commands.frontend.angular.*`, `defaults.noConfirm`
711
802
 
712
803
  ---
713
804
 
714
805
  ### `lt frontend nuxt`
715
806
 
716
- Creates a new Nuxt workspace using nuxt-base-starter.
807
+ Creates a new standalone Nuxt workspace using nuxt-base-starter. For an in-workspace app, prefer [`lt fullstack add-app --frontend nuxt`](#lt-fullstack-add-app).
717
808
 
718
809
  **Usage:**
719
810
  ```bash
@@ -723,16 +814,24 @@ lt frontend nuxt [options]
723
814
  **Options:**
724
815
  | Option | Description |
725
816
  |--------|-------------|
817
+ | `--name <name>` | Workspace name |
726
818
  | `--branch <branch>` / `-b` | Branch of nuxt-base-starter to use (uses git clone instead of create-nuxt-base) |
727
819
  | `--copy <path>` / `-c` | Copy from local template directory instead of cloning |
728
820
  | `--link <path>` | Symlink to local template directory (fastest, changes affect original) |
821
+ | `--frontend-framework-mode <npm\|vendor>` | Frontend framework consumption mode (`vendor` copies `nuxt-extensions` into `app/core/`) |
822
+ | `--next` | Default branch to `nuxt-base-starter#next` (auth `basePath` aligned with the experimental `--next` API) |
823
+ | `--dry-run` | Print the resolved plan and exit without making any changes |
824
+ | `--force` | Override the workspace-detection abort under `--noConfirm` |
825
+ | `--noConfirm` | Skip confirmation prompts (requires `--name`) |
729
826
 
730
827
  **Note:** For `--copy` and `--link`, specify the path to the `nuxt-base-template/` subdirectory, not the repository root:
731
828
  ```bash
732
829
  lt frontend nuxt --copy /path/to/nuxt-base-starter/nuxt-base-template
733
830
  ```
734
831
 
735
- **Configuration:** `commands.frontend.nuxt.*`
832
+ **Workspace-awareness:** Inside a fullstack workspace the command is interactive (confirm prompt), but under `--noConfirm` it **refuses** with exit code 1 and points the caller to `lt fullstack add-app --frontend nuxt`. Pass `--noConfirm --force` to override (rare).
833
+
834
+ **Configuration:** `commands.frontend.nuxt.*`, `commands.fullstack.frontendFrameworkMode` (shared with `init` / `add-app`)
736
835
 
737
836
  ---
738
837
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/cli",
3
- "version": "1.19.0",
3
+ "version": "1.20.0",
4
4
  "description": "lenne.Tech CLI: lt",
5
5
  "keywords": [
6
6
  "lenne.Tech",
@@ -31,6 +31,7 @@
31
31
  "coverage": "jest --coverage",
32
32
  "test:vendor-init": "bash scripts/test-vendor-init.sh",
33
33
  "test:frontend-vendor-init": "bash scripts/test-frontend-vendor-init.sh",
34
+ "test:incremental-fullstack": "bash scripts/test-incremental-fullstack.sh",
34
35
  "format": "prettier --write 'src/**/*.{js,ts,tsx,json}' '!src/templates/**/*'",
35
36
  "lint": "eslint './src/**/*.{ts,js,vue}'",
36
37
  "lint:fix": "eslint './src/**/*.{ts,js,vue}' --fix",