@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.
@@ -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
- // Get name
105
- const name = yield helper.getInput(parameters.first, {
106
- name: 'server name',
107
- showError: true,
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}`, {
@@ -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
- run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
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;