@lenne.tech/cli 1.28.0 → 1.30.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/cli.js +7 -1
- package/build/commands/frontend/angular.js +48 -46
- package/build/commands/frontend/convert-mode.js +41 -39
- package/build/commands/frontend/nuxt.js +49 -47
- package/build/commands/fullstack/add-api.js +34 -32
- package/build/commands/fullstack/add-app.js +25 -23
- package/build/commands/fullstack/convert-mode.js +85 -65
- package/build/commands/fullstack/init.js +12 -0
- package/build/commands/fullstack/update.js +24 -0
- package/build/commands/server/add-property.js +42 -40
- package/build/commands/server/convert-mode.js +41 -39
- package/build/commands/server/create.js +65 -63
- package/build/commands/server/module.js +56 -54
- package/build/commands/server/object.js +42 -40
- package/build/commands/server/permissions.js +60 -58
- package/build/commands/tools/crawl.js +92 -90
- package/build/extensions/frontend-helper.js +8 -37
- package/build/extensions/server.js +8 -38
- package/build/extensions/tools.js +10 -1
- package/build/lib/command-help.js +209 -0
- package/build/lib/vendor-claude-md.js +227 -0
- package/docs/commands.md +39 -0
- package/package.json +1 -1
|
@@ -47,6 +47,7 @@ const crypto = __importStar(require("crypto"));
|
|
|
47
47
|
const path_1 = require("path");
|
|
48
48
|
const ts = __importStar(require("typescript"));
|
|
49
49
|
const markdown_table_1 = require("../lib/markdown-table");
|
|
50
|
+
const vendor_claude_md_1 = require("../lib/vendor-claude-md");
|
|
50
51
|
/**
|
|
51
52
|
* Server helper functions
|
|
52
53
|
*/
|
|
@@ -1585,36 +1586,9 @@ class Server {
|
|
|
1585
1586
|
const apiClaudeMdPath = `${dest}/CLAUDE.md`;
|
|
1586
1587
|
if (filesystem.exists(apiClaudeMdPath)) {
|
|
1587
1588
|
const existing = filesystem.read(apiClaudeMdPath) || '';
|
|
1588
|
-
const
|
|
1589
|
-
if (
|
|
1590
|
-
|
|
1591
|
-
marker,
|
|
1592
|
-
'',
|
|
1593
|
-
'# Vendor-Mode Notice',
|
|
1594
|
-
'',
|
|
1595
|
-
'This api project runs in **vendor mode**: the `@lenne.tech/nest-server`',
|
|
1596
|
-
'core/ tree has been copied directly into `src/core/` as first-class',
|
|
1597
|
-
'project code. There is **no** `@lenne.tech/nest-server` npm dependency.',
|
|
1598
|
-
'',
|
|
1599
|
-
'- **Read framework code from `src/core/**`** — not from `node_modules/`.',
|
|
1600
|
-
'- **Generated imports use relative paths** to `src/core`, e.g.',
|
|
1601
|
-
" `import { CrudService } from '../../../core';`",
|
|
1602
|
-
' The exact depth depends on the file location. `lt server module`',
|
|
1603
|
-
' computes it automatically.',
|
|
1604
|
-
'- **Baseline + patch log** live in `src/core/VENDOR.md`. Log any',
|
|
1605
|
-
' substantial local change there so the `nest-server-core-updater`',
|
|
1606
|
-
' agent can classify it at sync time.',
|
|
1607
|
-
'- **Update flow:** run `/lt-dev:backend:update-nest-server-core` (the',
|
|
1608
|
-
' agent clones upstream, computes a delta, and presents a review).',
|
|
1609
|
-
'- **Contribute back:** run `/lt-dev:backend:contribute-nest-server-core`',
|
|
1610
|
-
' to propose local fixes as upstream PRs.',
|
|
1611
|
-
'- **Freshness check:** `pnpm run check:vendor-freshness` warns (non-',
|
|
1612
|
-
' blockingly) when upstream has a newer release than the baseline.',
|
|
1613
|
-
'',
|
|
1614
|
-
'---',
|
|
1615
|
-
'',
|
|
1616
|
-
].join('\n');
|
|
1617
|
-
filesystem.write(apiClaudeMdPath, vendorBlock + existing);
|
|
1589
|
+
const patched = (0, vendor_claude_md_1.insertVendorBlockIfMissing)(existing, vendor_claude_md_1.BACKEND_VENDOR_MARKER, (0, vendor_claude_md_1.buildBackendVendorBlock)());
|
|
1590
|
+
if (patched !== existing) {
|
|
1591
|
+
filesystem.write(apiClaudeMdPath, patched);
|
|
1618
1592
|
}
|
|
1619
1593
|
}
|
|
1620
1594
|
// ── 9c. Merge nest-server CLAUDE.md sections into project CLAUDE.md ──
|
|
@@ -2307,14 +2281,10 @@ class Server {
|
|
|
2307
2281
|
// ── 6. Clean CLAUDE.md vendor marker ────────────────────────────────
|
|
2308
2282
|
const claudeMdPath = `${dest}/CLAUDE.md`;
|
|
2309
2283
|
if (filesystem.exists(claudeMdPath)) {
|
|
2310
|
-
|
|
2311
|
-
const
|
|
2312
|
-
if (content
|
|
2313
|
-
|
|
2314
|
-
content = content.replace(new RegExp(`${marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?---\\s*\\n?`, ''), '');
|
|
2315
|
-
// Remove leading whitespace/newlines
|
|
2316
|
-
content = content.replace(/^\n+/, '');
|
|
2317
|
-
filesystem.write(claudeMdPath, content);
|
|
2284
|
+
const content = filesystem.read(claudeMdPath) || '';
|
|
2285
|
+
const cleaned = (0, vendor_claude_md_1.removeVendorBlock)(content, vendor_claude_md_1.BACKEND_VENDOR_MARKER);
|
|
2286
|
+
if (cleaned !== content) {
|
|
2287
|
+
filesystem.write(claudeMdPath, cleaned);
|
|
2318
2288
|
}
|
|
2319
2289
|
}
|
|
2320
2290
|
// ── 7. Restore tsconfig excludes ────────────────────────────────────
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Tools = void 0;
|
|
4
4
|
const fs_1 = require("fs");
|
|
5
|
+
const command_help_1 = require("../lib/command-help");
|
|
5
6
|
const singleComment = Symbol('singleComment');
|
|
6
7
|
const multiComment = Symbol('multiComment');
|
|
7
8
|
const stripWithoutWhitespace = () => '';
|
|
@@ -27,6 +28,14 @@ class Tools {
|
|
|
27
28
|
* Check if --help-json flag is set; if so, print the command definition as JSON and return true.
|
|
28
29
|
* Commands should call this early and return immediately when it returns true.
|
|
29
30
|
*
|
|
31
|
+
* In normal CLI usage the global `installHelpInterceptor` (see
|
|
32
|
+
* `src/lib/command-help.ts`) handles `--help-json` for ALL commands before
|
|
33
|
+
* their `run()` ever fires — this method is therefore mostly a no-op in
|
|
34
|
+
* production. It is still kept as a public escape hatch for tests that
|
|
35
|
+
* invoke a command's `run()` directly without the interceptor, and for
|
|
36
|
+
* legacy callers — output goes through the same `buildHelpJson` /
|
|
37
|
+
* `emitHelpJson` pair as the global path, so the schema cannot drift.
|
|
38
|
+
*
|
|
30
39
|
* @param definition - The command's help definition (name, description, options, etc.)
|
|
31
40
|
* @returns true if --help-json was handled (caller should return), false otherwise
|
|
32
41
|
*/
|
|
@@ -35,7 +44,7 @@ class Tools {
|
|
|
35
44
|
if (!parameters.options['help-json'] && !parameters.options.helpJson) {
|
|
36
45
|
return false;
|
|
37
46
|
}
|
|
38
|
-
|
|
47
|
+
(0, command_help_1.emitHelpJson)((0, command_help_1.buildHelpJson)({ description: definition.description, name: definition.name }, definition));
|
|
39
48
|
return true;
|
|
40
49
|
}
|
|
41
50
|
/**
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Generic `--help` / `-h` support for **every** lt command.
|
|
4
|
+
*
|
|
5
|
+
* Problem this solves: gluegun's built-in `.help()` only handles the top-level
|
|
6
|
+
* `lt --help` (the command list). For a subcommand, `lt fullstack convert-mode
|
|
7
|
+
* --help` simply *runs* the command — so a user who only wanted to read about a
|
|
8
|
+
* command accidentally triggers it. {@link installHelpInterceptor} wraps every
|
|
9
|
+
* loaded command so that, when help is requested, the command prints rich help
|
|
10
|
+
* and returns **without executing**.
|
|
11
|
+
*
|
|
12
|
+
* Two levels of detail:
|
|
13
|
+
* 1. Generic (always available) — usage, aliases and description from the
|
|
14
|
+
* command metadata gluegun already has.
|
|
15
|
+
* 2. Rich (opt-in) — a command module may `export const help: CommandHelp`
|
|
16
|
+
* describing options, features, examples and configuration. The interceptor
|
|
17
|
+
* loads it from `command.file` without running the command.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.installHelpInterceptor = installHelpInterceptor;
|
|
21
|
+
exports.isHelpJsonRequested = isHelpJsonRequested;
|
|
22
|
+
exports.isHelpRequested = isHelpRequested;
|
|
23
|
+
exports.loadCommandHelp = loadCommandHelp;
|
|
24
|
+
exports.renderCommandHelp = renderCommandHelp;
|
|
25
|
+
exports.buildHelpJson = buildHelpJson;
|
|
26
|
+
exports.emitHelpJson = emitHelpJson;
|
|
27
|
+
/**
|
|
28
|
+
* Wrap every command's `run` so that `--help` / `-h` and `--help-json` print
|
|
29
|
+
* help and return without executing. Call once after `build().create()`,
|
|
30
|
+
* before `cli.run()`.
|
|
31
|
+
*
|
|
32
|
+
* Idempotent per command (guarded by `__helpWrapped`), so a command is never
|
|
33
|
+
* double-wrapped if this runs more than once in the same process (e.g. tests).
|
|
34
|
+
*/
|
|
35
|
+
function installHelpInterceptor(commands, defaultCommand) {
|
|
36
|
+
for (const command of commands || []) {
|
|
37
|
+
if (typeof command.run !== 'function' || command.__helpWrapped) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
// Leave the top-level/default command and gluegun's preloaded builtins
|
|
41
|
+
// (`help`, `version` — they have `file === null`) to gluegun's own
|
|
42
|
+
// `.help()`, so that `lt --help` keeps printing the brand banner + full
|
|
43
|
+
// command list. Real file-backed subcommands (incl. `lt config help`) are
|
|
44
|
+
// still wrapped.
|
|
45
|
+
if (command === defaultCommand || !command.file || !(command.commandPath && command.commandPath.length)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const originalRun = command.run;
|
|
49
|
+
command.run = (toolbox) => {
|
|
50
|
+
if (isHelpJsonRequested(toolbox === null || toolbox === void 0 ? void 0 : toolbox.parameters)) {
|
|
51
|
+
emitHelpJson(buildHelpJson(command, loadCommandHelp(command.file)));
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
if ((toolbox === null || toolbox === void 0 ? void 0 : toolbox.print) && isHelpRequested(toolbox.parameters)) {
|
|
55
|
+
renderCommandHelp(toolbox.print, command, loadCommandHelp(command.file));
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return originalRun(toolbox);
|
|
59
|
+
};
|
|
60
|
+
command.__helpWrapped = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** True when the invocation asks for machine-readable help (`--help-json`). */
|
|
64
|
+
function isHelpJsonRequested(parameters) {
|
|
65
|
+
const options = (parameters === null || parameters === void 0 ? void 0 : parameters.options) || {};
|
|
66
|
+
return options['help-json'] === true || options.helpJson === true;
|
|
67
|
+
}
|
|
68
|
+
/** True when the invocation asks for human-readable help (`--help` or `-h`). */
|
|
69
|
+
function isHelpRequested(parameters) {
|
|
70
|
+
const options = (parameters === null || parameters === void 0 ? void 0 : parameters.options) || {};
|
|
71
|
+
if (options['help-json'] === true || options.helpJson === true) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return options.help === true || options.h === true;
|
|
75
|
+
}
|
|
76
|
+
/** Best-effort load of a command's rich `help` export from its file. */
|
|
77
|
+
function loadCommandHelp(file) {
|
|
78
|
+
if (!file) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const mod = require(file);
|
|
83
|
+
const help = (mod && (mod.help || (mod.default && mod.default.help)));
|
|
84
|
+
return help && typeof help === 'object' ? help : undefined;
|
|
85
|
+
}
|
|
86
|
+
catch (_a) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Render human-readable help for a command to `print`. Uses the rich `help`
|
|
92
|
+
* definition when available, otherwise a useful generic fallback. Never runs
|
|
93
|
+
* the command.
|
|
94
|
+
*/
|
|
95
|
+
function renderCommandHelp(print, command, help) {
|
|
96
|
+
var _a, _b, _c;
|
|
97
|
+
const { bold, cyan, dim, yellow } = print.colors;
|
|
98
|
+
const usage = usagePath(command);
|
|
99
|
+
const description = (help === null || help === void 0 ? void 0 : help.description) || command.description || '(no description)';
|
|
100
|
+
print.info('');
|
|
101
|
+
print.info(`${bold(usage)} — ${description}`);
|
|
102
|
+
const aliases = aliasList(command, help);
|
|
103
|
+
if (aliases.length) {
|
|
104
|
+
print.info(dim(`Aliases: ${aliases.join(', ')}`));
|
|
105
|
+
}
|
|
106
|
+
if ((_a = help === null || help === void 0 ? void 0 : help.features) === null || _a === void 0 ? void 0 : _a.length) {
|
|
107
|
+
print.info('');
|
|
108
|
+
print.info(bold('What it does:'));
|
|
109
|
+
for (const feature of help.features) {
|
|
110
|
+
print.info(` • ${feature}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
print.info('');
|
|
114
|
+
print.info(bold('Usage:'));
|
|
115
|
+
print.info(` ${usage} [options]`);
|
|
116
|
+
if ((_b = help === null || help === void 0 ? void 0 : help.examples) === null || _b === void 0 ? void 0 : _b.length) {
|
|
117
|
+
print.info('');
|
|
118
|
+
print.info(bold('Examples:'));
|
|
119
|
+
for (const example of help.examples) {
|
|
120
|
+
print.info(` ${cyan(example.startsWith('lt ') ? example : `lt ${example}`)}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
print.info('');
|
|
124
|
+
print.info(bold('Options:'));
|
|
125
|
+
const options = [...((help === null || help === void 0 ? void 0 : help.options) || [])];
|
|
126
|
+
for (const option of options) {
|
|
127
|
+
const meta = [];
|
|
128
|
+
if (option.required) {
|
|
129
|
+
meta.push('required');
|
|
130
|
+
}
|
|
131
|
+
if ((_c = option.values) === null || _c === void 0 ? void 0 : _c.length) {
|
|
132
|
+
meta.push(option.values.join('|'));
|
|
133
|
+
}
|
|
134
|
+
else if (option.type) {
|
|
135
|
+
meta.push(option.type);
|
|
136
|
+
}
|
|
137
|
+
if (option.default !== undefined) {
|
|
138
|
+
meta.push(`default: ${String(option.default)}`);
|
|
139
|
+
}
|
|
140
|
+
const metaText = meta.length ? dim(` (${meta.join(', ')})`) : '';
|
|
141
|
+
print.info(` ${option.flag.padEnd(28)} ${option.description}${metaText}`);
|
|
142
|
+
}
|
|
143
|
+
// Always-present global flags
|
|
144
|
+
print.info(` ${'--help, -h'.padEnd(28)} Show this help and exit (does not run the command)`);
|
|
145
|
+
print.info(` ${'--help-json'.padEnd(28)} Machine-readable help as JSON (does not run the command)`);
|
|
146
|
+
if (!options.some((o) => o.flag === '--noConfirm')) {
|
|
147
|
+
print.info(` ${'--noConfirm'.padEnd(28)} Skip confirmation prompts (where supported)`);
|
|
148
|
+
}
|
|
149
|
+
if (help === null || help === void 0 ? void 0 : help.configuration) {
|
|
150
|
+
print.info('');
|
|
151
|
+
print.info(bold('Configuration (lt.config.json / lt.config.yaml):'));
|
|
152
|
+
for (const line of help.configuration.split('\n')) {
|
|
153
|
+
print.info(` ${line}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!help) {
|
|
157
|
+
print.info('');
|
|
158
|
+
print.info(dim('Tip: see docs/commands.md and docs/lt.config.md for full reference.'));
|
|
159
|
+
}
|
|
160
|
+
print.info('');
|
|
161
|
+
print.info(yellow('This is help output — the command was NOT executed.'));
|
|
162
|
+
print.info('');
|
|
163
|
+
}
|
|
164
|
+
function aliasList(command, help) {
|
|
165
|
+
return (help === null || help === void 0 ? void 0 : help.aliases) || command.aliases || command.alias || [];
|
|
166
|
+
}
|
|
167
|
+
/** Resolve the user-facing invocation, e.g. `lt fullstack convert-mode`. */
|
|
168
|
+
function usagePath(command) {
|
|
169
|
+
const path = command.commandPath && command.commandPath.length ? command.commandPath : [command.name || ''];
|
|
170
|
+
return `lt ${path.filter(Boolean).join(' ')}`.trim();
|
|
171
|
+
}
|
|
172
|
+
const GLOBAL_HELP_FLAGS = [
|
|
173
|
+
{ description: 'Show human-readable help; the command is NOT executed.', flag: '--help', type: 'boolean' },
|
|
174
|
+
{ description: 'Alias for --help.', flag: '-h', type: 'boolean' },
|
|
175
|
+
{
|
|
176
|
+
description: 'Print this JSON description on stdout; the command is NOT executed.',
|
|
177
|
+
flag: '--help-json',
|
|
178
|
+
type: 'boolean',
|
|
179
|
+
},
|
|
180
|
+
];
|
|
181
|
+
/**
|
|
182
|
+
* Build the JSON shape returned by `--help-json` for a command, merging the
|
|
183
|
+
* gluegun-known metadata (name, commandPath, description, aliases) with the
|
|
184
|
+
* command's optional rich `CommandHelp` export.
|
|
185
|
+
*/
|
|
186
|
+
function buildHelpJson(command, help) {
|
|
187
|
+
const aliases = aliasList(command, help);
|
|
188
|
+
return {
|
|
189
|
+
aliases,
|
|
190
|
+
command: usagePath(command),
|
|
191
|
+
configuration: help === null || help === void 0 ? void 0 : help.configuration,
|
|
192
|
+
description: (help === null || help === void 0 ? void 0 : help.description) || command.description || '',
|
|
193
|
+
examples: help === null || help === void 0 ? void 0 : help.examples,
|
|
194
|
+
features: help === null || help === void 0 ? void 0 : help.features,
|
|
195
|
+
globalFlags: GLOBAL_HELP_FLAGS,
|
|
196
|
+
name: (help === null || help === void 0 ? void 0 : help.name) || command.name || '',
|
|
197
|
+
options: (help === null || help === void 0 ? void 0 : help.options) || [],
|
|
198
|
+
richHelp: Boolean(help),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Emit a help-json payload to stdout as a single pretty-printed JSON document.
|
|
203
|
+
* Kept tiny + side-effect-only so it can be stubbed in tests via a captured
|
|
204
|
+
* `console.log`.
|
|
205
|
+
*/
|
|
206
|
+
function emitHelpJson(payload) {
|
|
207
|
+
// eslint-disable-next-line no-console
|
|
208
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
209
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Single source of truth for the "Vendor-Mode Notice" blocks that the CLI
|
|
4
|
+
* writes into project CLAUDE.md files.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists: when a project runs in vendor mode, Claude Code (and humans)
|
|
7
|
+
* must know — from the project itself, without the lt-dev plugin installed —
|
|
8
|
+
* that the framework lives in a vendored `core/` tree and which command syncs
|
|
9
|
+
* it (`update`) vs. ports local fixes back (`contribute`). The plugin hooks are
|
|
10
|
+
* a proactive safety net, but they only fire when the plugin is installed; the
|
|
11
|
+
* CLAUDE.md block is the plugin-independent channel.
|
|
12
|
+
*
|
|
13
|
+
* Three blocks are generated:
|
|
14
|
+
* - Backend → `projects/api/CLAUDE.md` (marker {@link BACKEND_VENDOR_MARKER})
|
|
15
|
+
* - Frontend → `projects/app/CLAUDE.md` (marker {@link FRONTEND_VENDOR_MARKER})
|
|
16
|
+
* - Root → workspace `CLAUDE.md` (marker {@link ROOT_VENDOR_MARKER})
|
|
17
|
+
*
|
|
18
|
+
* The root block is new: Claude often reads the monorepo root CLAUDE.md first,
|
|
19
|
+
* so it needs a short pointer to the per-subproject vendor docs.
|
|
20
|
+
*
|
|
21
|
+
* Each block starts with its HTML marker comment and ends with a `---`
|
|
22
|
+
* horizontal rule, so {@link upsertVendorBlock} / {@link removeVendorBlock} can
|
|
23
|
+
* find and replace exactly the generated region without touching hand-written
|
|
24
|
+
* content below it.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.ROOT_VENDOR_MARKER = exports.FRONTEND_VENDOR_MARKER = exports.BACKEND_VENDOR_MARKER = void 0;
|
|
28
|
+
exports.buildBackendVendorBlock = buildBackendVendorBlock;
|
|
29
|
+
exports.buildFrontendVendorBlock = buildFrontendVendorBlock;
|
|
30
|
+
exports.buildRootVendorBlock = buildRootVendorBlock;
|
|
31
|
+
exports.hasVendorBlock = hasVendorBlock;
|
|
32
|
+
exports.healVendorClaudeMd = healVendorClaudeMd;
|
|
33
|
+
exports.insertVendorBlockIfMissing = insertVendorBlockIfMissing;
|
|
34
|
+
exports.removeVendorBlock = removeVendorBlock;
|
|
35
|
+
exports.upsertVendorBlock = upsertVendorBlock;
|
|
36
|
+
exports.BACKEND_VENDOR_MARKER = '<!-- lt-vendor-marker -->';
|
|
37
|
+
exports.FRONTEND_VENDOR_MARKER = '<!-- lt-vendor-marker-frontend -->';
|
|
38
|
+
exports.ROOT_VENDOR_MARKER = '<!-- lt-vendor-marker-root -->';
|
|
39
|
+
/**
|
|
40
|
+
* Vendor-mode notice for the backend api project (`projects/api/CLAUDE.md`).
|
|
41
|
+
*/
|
|
42
|
+
function buildBackendVendorBlock() {
|
|
43
|
+
return block([
|
|
44
|
+
exports.BACKEND_VENDOR_MARKER,
|
|
45
|
+
'',
|
|
46
|
+
'# Vendor-Mode Notice',
|
|
47
|
+
'',
|
|
48
|
+
'This api project runs in **vendor mode**: the `@lenne.tech/nest-server`',
|
|
49
|
+
'core/ tree has been copied directly into `src/core/` as first-class',
|
|
50
|
+
'project code. There is **no** `@lenne.tech/nest-server` npm dependency.',
|
|
51
|
+
'',
|
|
52
|
+
'- **Read framework code from `src/core/**`** — not from `node_modules/`.',
|
|
53
|
+
'- **Generated imports use relative paths** to `src/core`, e.g.',
|
|
54
|
+
" `import { CrudService } from '../../../core';`",
|
|
55
|
+
' The exact depth depends on the file location. `lt server module`',
|
|
56
|
+
' computes it automatically.',
|
|
57
|
+
'- **Baseline + patch log** live in `src/core/VENDOR.md`. Log any',
|
|
58
|
+
' substantial local change there so the `nest-server-core-updater`',
|
|
59
|
+
' agent can classify it at sync time.',
|
|
60
|
+
'- **Update flow:** run `/lt-dev:backend:update-nest-server-core` (the',
|
|
61
|
+
' agent clones upstream, computes a delta, and presents a review). The',
|
|
62
|
+
' update also raises npm packages to at least the upstream baseline',
|
|
63
|
+
' (via `/lt-dev:maintenance:maintain`).',
|
|
64
|
+
'- **Contribute back:** run `/lt-dev:backend:contribute-nest-server-core`',
|
|
65
|
+
' to propose local fixes as upstream PRs.',
|
|
66
|
+
'- **Freshness check:** `pnpm run check:vendor-freshness` warns (non-',
|
|
67
|
+
' blockingly) when upstream has a newer release than the baseline.',
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Vendor-mode notice for the frontend app project (`projects/app/CLAUDE.md`).
|
|
72
|
+
*/
|
|
73
|
+
function buildFrontendVendorBlock() {
|
|
74
|
+
return block([
|
|
75
|
+
exports.FRONTEND_VENDOR_MARKER,
|
|
76
|
+
'',
|
|
77
|
+
'# Vendor-Mode Notice (Frontend)',
|
|
78
|
+
'',
|
|
79
|
+
'This frontend project runs in **vendor mode**: the `@lenne.tech/nuxt-extensions`',
|
|
80
|
+
'module has been copied directly into `app/core/` as first-class',
|
|
81
|
+
'project code. There is **no** `@lenne.tech/nuxt-extensions` npm dependency.',
|
|
82
|
+
'',
|
|
83
|
+
'- **Read framework code from `app/core/**`** — not from `node_modules/`.',
|
|
84
|
+
"- **nuxt.config.ts** references `'./app/core/module'` instead of",
|
|
85
|
+
" `'@lenne.tech/nuxt-extensions'`.",
|
|
86
|
+
'- **Baseline + patch log** live in `app/core/VENDOR.md`. Log any',
|
|
87
|
+
' substantial local change there so the `nuxt-extensions-core-updater`',
|
|
88
|
+
' agent can classify it at sync time.',
|
|
89
|
+
'- **Update flow:** run `/lt-dev:frontend:update-nuxt-extensions-core`. The',
|
|
90
|
+
' update also raises npm packages to at least the upstream baseline',
|
|
91
|
+
' (via `/lt-dev:maintenance:maintain`).',
|
|
92
|
+
'- **Contribute back:** run `/lt-dev:frontend:contribute-nuxt-extensions-core`.',
|
|
93
|
+
'- **Freshness check:** `pnpm run check:vendor-freshness` warns when',
|
|
94
|
+
' upstream has a newer release than the baseline.',
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Vendor-mode notice for the monorepo root (`<workspace>/CLAUDE.md`).
|
|
99
|
+
*
|
|
100
|
+
* Tailored to which halves are vendored so Claude sees only the relevant
|
|
101
|
+
* commands. At least one of `backend` / `frontend` must be true; otherwise the
|
|
102
|
+
* caller should {@link removeVendorBlock} instead of writing an empty notice.
|
|
103
|
+
*/
|
|
104
|
+
function buildRootVendorBlock(opts) {
|
|
105
|
+
const { backend, frontend } = opts;
|
|
106
|
+
const lines = [
|
|
107
|
+
exports.ROOT_VENDOR_MARKER,
|
|
108
|
+
'',
|
|
109
|
+
'# Vendor-Mode Notice (Monorepo)',
|
|
110
|
+
'',
|
|
111
|
+
'This workspace runs at least one framework in **vendor mode** — the',
|
|
112
|
+
'framework source is vendored directly into the project tree instead of',
|
|
113
|
+
'being an npm dependency. Read framework code from the vendored `core/`',
|
|
114
|
+
'trees, not from `node_modules/`.',
|
|
115
|
+
'',
|
|
116
|
+
'**Vendored frameworks:**',
|
|
117
|
+
];
|
|
118
|
+
if (backend) {
|
|
119
|
+
lines.push('- **Backend** (`@lenne.tech/nest-server`): `projects/api/src/core/` —', ' details in `projects/api/CLAUDE.md` and `projects/api/src/core/VENDOR.md`.');
|
|
120
|
+
}
|
|
121
|
+
if (frontend) {
|
|
122
|
+
lines.push('- **Frontend** (`@lenne.tech/nuxt-extensions`): `projects/app/app/core/` —', ' details in `projects/app/CLAUDE.md` and `projects/app/app/core/VENDOR.md`.');
|
|
123
|
+
}
|
|
124
|
+
lines.push('', '**Update** (sync from upstream; also raises npm packages to at least the', 'upstream baseline via `/lt-dev:maintenance:maintain`):');
|
|
125
|
+
if (backend) {
|
|
126
|
+
lines.push('- Backend: `/lt-dev:backend:update-nest-server-core`');
|
|
127
|
+
}
|
|
128
|
+
if (frontend) {
|
|
129
|
+
lines.push('- Frontend: `/lt-dev:frontend:update-nuxt-extensions-core`');
|
|
130
|
+
}
|
|
131
|
+
lines.push('', '**Contribute back** generally-useful core fixes as upstream PRs:');
|
|
132
|
+
if (backend) {
|
|
133
|
+
lines.push('- Backend: `/lt-dev:backend:contribute-nest-server-core`');
|
|
134
|
+
}
|
|
135
|
+
if (frontend) {
|
|
136
|
+
lines.push('- Frontend: `/lt-dev:frontend:contribute-nuxt-extensions-core`');
|
|
137
|
+
}
|
|
138
|
+
lines.push('', 'Project-specific code never goes into a `core/` tree — see each', "subproject's VENDOR.md Modification Policy.");
|
|
139
|
+
return block(lines);
|
|
140
|
+
}
|
|
141
|
+
/** True when `content` already contains the given vendor marker. */
|
|
142
|
+
function hasVendorBlock(content, marker) {
|
|
143
|
+
return content.includes(marker);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Bring every CLAUDE.md in a workspace in line with its current vendor state:
|
|
147
|
+
* upsert the matching notice block where a framework is vendored, remove it
|
|
148
|
+
* where it is not. Idempotent — running it on an already-correct workspace
|
|
149
|
+
* changes nothing.
|
|
150
|
+
*
|
|
151
|
+
* This is what makes `lt fullstack update` able to *heal* pre-existing or
|
|
152
|
+
* drifted vendor projects (e.g. ones scaffolded before the root notice existed,
|
|
153
|
+
* or whose notice fell out of date).
|
|
154
|
+
*
|
|
155
|
+
* @returns the list of CLAUDE.md paths that were actually modified.
|
|
156
|
+
*/
|
|
157
|
+
function healVendorClaudeMd(fs, state) {
|
|
158
|
+
const changed = [];
|
|
159
|
+
const apply = (path, marker, desiredBlock) => {
|
|
160
|
+
if (!fs.exists(path)) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const content = fs.read(path) || '';
|
|
164
|
+
const next = desiredBlock ? upsertVendorBlock(content, marker, desiredBlock) : removeVendorBlock(content, marker);
|
|
165
|
+
if (next !== content) {
|
|
166
|
+
fs.write(path, next);
|
|
167
|
+
changed.push(path);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
if (state.apiDir) {
|
|
171
|
+
apply(joinPath(state.apiDir, 'CLAUDE.md'), exports.BACKEND_VENDOR_MARKER, state.backendVendor ? buildBackendVendorBlock() : null);
|
|
172
|
+
}
|
|
173
|
+
if (state.appDir) {
|
|
174
|
+
apply(joinPath(state.appDir, 'CLAUDE.md'), exports.FRONTEND_VENDOR_MARKER, state.frontendVendor ? buildFrontendVendorBlock() : null);
|
|
175
|
+
}
|
|
176
|
+
if (state.workspaceRoot) {
|
|
177
|
+
const anyVendor = state.backendVendor || state.frontendVendor;
|
|
178
|
+
apply(joinPath(state.workspaceRoot, 'CLAUDE.md'), exports.ROOT_VENDOR_MARKER, anyVendor ? buildRootVendorBlock({ backend: state.backendVendor, frontend: state.frontendVendor }) : null);
|
|
179
|
+
}
|
|
180
|
+
return changed;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Insert the block at the very top of the file **only when it is missing**.
|
|
184
|
+
* Used during conversion so a hand-customized existing block is never clobbered.
|
|
185
|
+
*/
|
|
186
|
+
function insertVendorBlockIfMissing(content, marker, newBlock) {
|
|
187
|
+
if (content.includes(marker)) {
|
|
188
|
+
return content;
|
|
189
|
+
}
|
|
190
|
+
return newBlock + content;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Remove the generated block (marker through the first `---`) and trim leading
|
|
194
|
+
* blank lines. Used when converting a project back to npm mode.
|
|
195
|
+
*/
|
|
196
|
+
function removeVendorBlock(content, marker) {
|
|
197
|
+
if (!content.includes(marker)) {
|
|
198
|
+
return content;
|
|
199
|
+
}
|
|
200
|
+
return content.replace(blockRegex(marker), '').replace(/^\n+/, '');
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Insert the block if missing, or replace the existing generated region with the
|
|
204
|
+
* current canonical block (idempotent self-heal). Used by `lt fullstack update`
|
|
205
|
+
* to bring pre-existing / drifted projects up to the current notice.
|
|
206
|
+
*/
|
|
207
|
+
function upsertVendorBlock(content, marker, newBlock) {
|
|
208
|
+
if (!content.includes(marker)) {
|
|
209
|
+
return newBlock + content;
|
|
210
|
+
}
|
|
211
|
+
return content.replace(blockRegex(marker), newBlock);
|
|
212
|
+
}
|
|
213
|
+
/** Join block lines into the canonical `marker … --- ` shape (ends with `---\n`). */
|
|
214
|
+
function block(lines) {
|
|
215
|
+
return [...lines, '', '---', ''].join('\n');
|
|
216
|
+
}
|
|
217
|
+
/** Build the regex that matches an existing block from its marker to the first `---`. */
|
|
218
|
+
function blockRegex(marker) {
|
|
219
|
+
return new RegExp(`${escapeRegExp(marker)}[\\s\\S]*?---\\s*\\n?`);
|
|
220
|
+
}
|
|
221
|
+
/** Escape a string for safe use inside a RegExp. */
|
|
222
|
+
function escapeRegExp(value) {
|
|
223
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
224
|
+
}
|
|
225
|
+
function joinPath(dir, file) {
|
|
226
|
+
return dir.endsWith('/') ? `${dir}${file}` : `${dir}/${file}`;
|
|
227
|
+
}
|
package/docs/commands.md
CHANGED
|
@@ -10,6 +10,7 @@ This document provides a comprehensive reference for all `lt` CLI commands. For
|
|
|
10
10
|
|
|
11
11
|
## Table of Contents
|
|
12
12
|
|
|
13
|
+
- [Global Flags](#global-flags)
|
|
13
14
|
- [CLI Commands](#cli-commands)
|
|
14
15
|
- [Server Commands](#server-commands)
|
|
15
16
|
- [Local Development Commands](#local-development-commands)
|
|
@@ -32,6 +33,44 @@ This document provides a comprehensive reference for all `lt` CLI commands. For
|
|
|
32
33
|
|
|
33
34
|
---
|
|
34
35
|
|
|
36
|
+
## Global Flags
|
|
37
|
+
|
|
38
|
+
These flags work on **every** `lt` subcommand. They are intercepted before the command's `run()` fires, so the command is never executed when one of them is set.
|
|
39
|
+
|
|
40
|
+
| Flag | Description |
|
|
41
|
+
|--------|-------------|
|
|
42
|
+
| `--help`, `-h` | Print human-readable help (usage, aliases, options, examples). |
|
|
43
|
+
| `--help-json` | Print the same help as a single JSON document on stdout. Stable contract — see shape below. Intended for AI agents and tooling that want to discover a command's surface programmatically. |
|
|
44
|
+
| `--noConfirm` | Skip interactive confirmations (where supported by the command). |
|
|
45
|
+
|
|
46
|
+
**`--help-json` payload shape** (`HelpJsonShape` in [src/lib/command-help.ts](../src/lib/command-help.ts)):
|
|
47
|
+
|
|
48
|
+
```jsonc
|
|
49
|
+
{
|
|
50
|
+
"aliases": ["c"],
|
|
51
|
+
"command": "lt server create",
|
|
52
|
+
"configuration": "commands.server.create.*",
|
|
53
|
+
"description": "Create new server",
|
|
54
|
+
"examples": ["server create --name Foo --noConfirm"],
|
|
55
|
+
"features": ["..."],
|
|
56
|
+
"globalFlags": [
|
|
57
|
+
{ "flag": "--help", "type": "boolean", "description": "..." },
|
|
58
|
+
{ "flag": "-h", "type": "boolean", "description": "..." },
|
|
59
|
+
{ "flag": "--help-json", "type": "boolean", "description": "..." }
|
|
60
|
+
],
|
|
61
|
+
"name": "create",
|
|
62
|
+
"options": [
|
|
63
|
+
{ "flag": "--name", "type": "string", "required": true, "description": "Server name" }
|
|
64
|
+
],
|
|
65
|
+
"richHelp": true
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- `richHelp: true` means the command exported a typed `CommandHelp` — `options`, `features`, `examples` and `configuration` are authoritative.
|
|
70
|
+
- `richHelp: false` means only gluegun metadata was available — `options` is the empty array, but `globalFlags` is still guaranteed.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
35
74
|
## CLI Commands
|
|
36
75
|
|
|
37
76
|
### `lt cli create`
|