@lenne.tech/cli 1.10.0 → 1.11.1

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/docs/lt.config.md CHANGED
@@ -52,7 +52,7 @@ Configuration files are searched from the **current directory up to the root** (
52
52
  ### Example: Monorepo Structure
53
53
 
54
54
  ```
55
- /home/user/
55
+ $HOME/
56
56
  ├── lt.config.json # Global defaults
57
57
  └── projects/
58
58
  └── my-monorepo/
@@ -64,10 +64,10 @@ Configuration files are searched from the **current directory up to the root** (
64
64
  └── lt.config.yaml # App-specific settings
65
65
  ```
66
66
 
67
- When running `lt server module` in `/home/user/projects/my-monorepo/projects/api/`:
68
- 1. `/home/user/lt.config.json` is loaded first
69
- 2. `/home/user/projects/my-monorepo/lt.config.json` is merged (overrides parent)
70
- 3. `/home/user/projects/my-monorepo/projects/api/lt.config.json` is merged (overrides all)
67
+ When running `lt server module` in `$HOME/projects/my-monorepo/projects/api/`:
68
+ 1. `$HOME/lt.config.json` is loaded first
69
+ 2. `$HOME/projects/my-monorepo/lt.config.json` is merged (overrides parent)
70
+ 3. `$HOME/projects/my-monorepo/projects/api/lt.config.json` is merged (overrides all)
71
71
 
72
72
  ## Configuration Structure
73
73
 
@@ -571,6 +571,8 @@ Creates a new fullstack workspace with API and frontend.
571
571
  | `commands.fullstack.frontendBranch` | `string` | - | Branch of frontend starter to use (ng-base-starter or nuxt-base-starter) |
572
572
  | `commands.fullstack.frontendCopy` | `string` | - | Path to local frontend template directory to copy instead of cloning |
573
573
  | `commands.fullstack.frontendLink` | `string` | - | Path to local frontend template directory to symlink (fastest, changes affect original) |
574
+ | `commands.fullstack.frameworkMode` | `'npm'` \| `'vendor'` | `'npm'` | Backend framework consumption mode (npm dependency vs. vendored core in `src/core/`) |
575
+ | `commands.fullstack.frontendFrameworkMode` | `'npm'` \| `'vendor'` | `'npm'` | Frontend framework consumption mode (npm dependency vs. vendored module in `app/core/`) |
574
576
  | `commands.fullstack.git` | `boolean` | - | Push initial commit to remote repository (git is always initialized with `dev` branch) |
575
577
  | `commands.fullstack.gitLink` | `string` | - | Git remote repository URL (required when `git` is true) |
576
578
 
@@ -1075,7 +1077,7 @@ meta:
1075
1077
 
1076
1078
  Set a value to `null` to remove it from parent configurations and use the default:
1077
1079
 
1078
- **Parent config (`/home/user/lt.config.json`):**
1080
+ **Parent config (`$HOME/lt.config.json`):**
1079
1081
  ```json
1080
1082
  {
1081
1083
  "commands": {
@@ -1088,7 +1090,7 @@ Set a value to `null` to remove it from parent configurations and use the defaul
1088
1090
  }
1089
1091
  ```
1090
1092
 
1091
- **Child config (`/home/user/project/lt.config.json`):**
1093
+ **Child config (`$HOME/project/lt.config.json`):**
1092
1094
  ```json
1093
1095
  {
1094
1096
  "commands": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/cli",
3
- "version": "1.10.0",
3
+ "version": "1.11.1",
4
4
  "description": "lenne.Tech CLI: lt",
5
5
  "keywords": [
6
6
  "lenne.Tech",
@@ -26,6 +26,7 @@
26
26
  "copy-templates": "npx shx cp -R ./src/templates ./build/templates && npx shx cp -R ./src/config ./build/config",
27
27
  "coverage": "jest --coverage",
28
28
  "test:vendor-init": "bash scripts/test-vendor-init.sh",
29
+ "test:frontend-vendor-init": "bash scripts/test-frontend-vendor-init.sh",
29
30
  "format": "prettier --write 'src/**/*.{js,ts,tsx,json}' '!src/templates/**/*'",
30
31
  "lint": "eslint './src/**/*.{ts,js,vue}'",
31
32
  "lint:fix": "eslint './src/**/*.{ts,js,vue}' --fix",
@@ -1,131 +0,0 @@
1
- #!/usr/bin/env node
2
- // Check whether the vendored @lenne.tech/nest-server core is up-to-date with the
3
- // latest upstream release. Non-blocking: prints a warning when outdated, but always
4
- // exits 0 so that `check` / `check:fix` pipelines continue.
5
- //
6
- // Reads:
7
- // projects/api/src/core/VENDOR.md → baseline version (e.g. "11.24.1")
8
- //
9
- // Fetches:
10
- // https://registry.npmjs.org/@lenne.tech/nest-server/latest → latest published version
11
- //
12
- // Outputs:
13
- // - Up-to-date → stdout: "✓ vendored nest-server core is up-to-date (vX.Y.Z)"
14
- // - Outdated → stderr: "⚠ vendored nest-server core is X.Y.Z, latest is A.B.C"
15
- // - Offline/err → stderr: warn + exit 0 (never fail)
16
-
17
- import { readFileSync, existsSync } from 'node:fs';
18
- import { fileURLToPath } from 'node:url';
19
- import { dirname, join } from 'node:path';
20
- import https from 'node:https';
21
-
22
- const __dirname = dirname(fileURLToPath(import.meta.url));
23
- const VENDOR_MD = join(__dirname, '..', '..', 'src', 'core', 'VENDOR.md');
24
-
25
- // ANSI color codes (no external deps)
26
- const C = {
27
- reset: '\x1b[0m',
28
- yellow: '\x1b[33m',
29
- green: '\x1b[32m',
30
- dim: '\x1b[2m',
31
- };
32
-
33
- function warnAndExit(msg) {
34
- process.stderr.write(`${C.yellow}⚠ ${msg}${C.reset}\n`);
35
- process.exit(0);
36
- }
37
-
38
- function ok(msg) {
39
- process.stdout.write(`${C.green}✓ ${msg}${C.reset}\n`);
40
- process.exit(0);
41
- }
42
-
43
- // 1. Locate VENDOR.md
44
- if (!existsSync(VENDOR_MD)) {
45
- warnAndExit(
46
- `vendor-freshness: VENDOR.md not found at ${VENDOR_MD}. ` +
47
- `Is this project vendored? Skipping check.`,
48
- );
49
- }
50
-
51
- // 2. Parse baseline version from VENDOR.md
52
- let baselineVersion;
53
- try {
54
- const content = readFileSync(VENDOR_MD, 'utf-8');
55
- // Match: "**Baseline-Version:** 11.24.1" or "Baseline-Version: 11.24.1"
56
- const match = content.match(/Baseline-Version[:*\s]+([\d.]+[\w.-]*)/);
57
- if (!match) {
58
- warnAndExit(`vendor-freshness: could not parse Baseline-Version from ${VENDOR_MD}`);
59
- }
60
- baselineVersion = match[1];
61
- } catch (err) {
62
- warnAndExit(`vendor-freshness: failed to read ${VENDOR_MD}: ${err.message}`);
63
- }
64
-
65
- // 3. Fetch latest from npm registry (offline-tolerant)
66
- function fetchLatest() {
67
- return new Promise((resolve) => {
68
- const req = https.get(
69
- 'https://registry.npmjs.org/@lenne.tech/nest-server/latest',
70
- { timeout: 5000 },
71
- (res) => {
72
- if (res.statusCode !== 200) {
73
- resolve(null);
74
- return;
75
- }
76
- let body = '';
77
- res.on('data', (chunk) => (body += chunk));
78
- res.on('end', () => {
79
- try {
80
- const json = JSON.parse(body);
81
- resolve(json.version || null);
82
- } catch {
83
- resolve(null);
84
- }
85
- });
86
- },
87
- );
88
- req.on('error', () => resolve(null));
89
- req.on('timeout', () => {
90
- req.destroy();
91
- resolve(null);
92
- });
93
- });
94
- }
95
-
96
- const latestVersion = await fetchLatest();
97
-
98
- if (!latestVersion) {
99
- warnAndExit(
100
- `vendor-freshness: could not reach npm registry. ` +
101
- `Current baseline: ${baselineVersion}. Check skipped.`,
102
- );
103
- }
104
-
105
- // 4. semver compare (simple lexical sort works for X.Y.Z)
106
- function parseSemver(v) {
107
- const parts = v.split('.').map((p) => parseInt(p, 10));
108
- return [parts[0] || 0, parts[1] || 0, parts[2] || 0];
109
- }
110
-
111
- const [bMaj, bMin, bPatch] = parseSemver(baselineVersion);
112
- const [lMaj, lMin, lPatch] = parseSemver(latestVersion);
113
-
114
- const baselineNum = bMaj * 1e6 + bMin * 1e3 + bPatch;
115
- const latestNum = lMaj * 1e6 + lMin * 1e3 + lPatch;
116
-
117
- if (baselineNum === latestNum) {
118
- ok(`vendored nest-server core is up-to-date (v${baselineVersion})`);
119
- } else if (baselineNum < latestNum) {
120
- const msg =
121
- `vendored nest-server core is v${baselineVersion}, ` +
122
- `latest upstream is v${latestVersion}\n` +
123
- `${C.dim} Run /lt-dev:backend:update-nest-server-core to sync${C.reset}`;
124
- warnAndExit(msg);
125
- } else {
126
- // baseline > latest: weird but not fatal
127
- warnAndExit(
128
- `vendored nest-server core is v${baselineVersion} (ahead of npm latest v${latestVersion}). ` +
129
- `Possibly tracking an unreleased branch.`,
130
- );
131
- }
@@ -1,269 +0,0 @@
1
- /**
2
- * Diff-generator for the `lt-dev:nest-server-core-contributor` agent.
3
- *
4
- * Analyzes local git commits that touched projects/api/src/core/ since the
5
- * vendoring baseline, emits per-commit patch files and a human-readable
6
- * candidate list. Filters out cosmetic commits (format, style, lint:fix).
7
- * Does NOT cherry-pick or open any PR — that's the contributor agent's job.
8
- *
9
- * Usage:
10
- * pnpm run vendor:propose-upstream
11
- * (or: ts-node scripts/vendor/propose-upstream-pr.ts [--since <sha>])
12
- *
13
- * Output directory: scripts/vendor/upstream-candidates/<timestamp>/
14
- * - local-commits.json: structured metadata for every commit
15
- * - local-diffs/<commit-sha>.patch: per-commit patch file
16
- * - summary.md: human-readable candidate list
17
- */
18
-
19
- import { execSync } from 'node:child_process';
20
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
21
- import { join } from 'node:path';
22
-
23
- const PROJECT_ROOT = join(__dirname, '..', '..');
24
- const VENDOR_DIR = join(PROJECT_ROOT, 'src', 'core');
25
- const VENDOR_MD = join(VENDOR_DIR, 'VENDOR.md');
26
- const OUTPUT_BASE = join(PROJECT_ROOT, 'scripts', 'vendor', 'upstream-candidates');
27
-
28
- const MONOREPO_ROOT = join(PROJECT_ROOT, '..', '..');
29
-
30
- function die(msg: string): never {
31
- process.stderr.write(`ERROR: ${msg}\n`);
32
- process.exit(1);
33
- }
34
-
35
- function sh(cmd: string, opts: { cwd?: string; allowFailure?: boolean } = {}): string {
36
- try {
37
- return execSync(cmd, {
38
- cwd: opts.cwd ?? MONOREPO_ROOT,
39
- encoding: 'utf-8',
40
- stdio: ['ignore', 'pipe', 'pipe'],
41
- });
42
- } catch (err: unknown) {
43
- if (opts.allowFailure) {
44
- const e = err as { stdout?: string };
45
- return e.stdout ?? '';
46
- }
47
- throw err;
48
- }
49
- }
50
-
51
- // Cosmetic message patterns (case-insensitive)
52
- const COSMETIC_PATTERNS = [
53
- /^chore.*format/i,
54
- /^style:/i,
55
- /^chore.*oxfmt/i,
56
- /^chore.*prettier/i,
57
- /^chore.*lint:fix/i,
58
- /^chore.*linting/i,
59
- /^chore.*apply project formatting/i,
60
- /^chore.*re-?format/i,
61
- ];
62
-
63
- interface CommitInfo {
64
- sha: string;
65
- shortSha: string;
66
- subject: string;
67
- author: string;
68
- date: string;
69
- files: string[];
70
- isCosmetic: boolean;
71
- cosmeticReason: string | null;
72
- }
73
-
74
- // 1. Parse arguments
75
- const args = process.argv.slice(2);
76
- let sinceRef: string | null = null;
77
-
78
- for (let i = 0; i < args.length; i++) {
79
- if (args[i] === '--since' && i + 1 < args.length) {
80
- sinceRef = args[++i];
81
- }
82
- }
83
-
84
- // 2. Verify vendored state
85
- if (!existsSync(VENDOR_MD)) {
86
- die(`VENDOR.md not found at ${VENDOR_MD}. Not a vendored project.`);
87
- }
88
-
89
- const vendorContent = readFileSync(VENDOR_MD, 'utf-8');
90
- const baselineVersionMatch = vendorContent.match(/Baseline-Version[:*\s]+([\d.]+[\w.-]*)/);
91
- const baselineVersion = baselineVersionMatch?.[1] ?? 'unknown';
92
-
93
- // 3. Determine starting point for git log
94
- if (!sinceRef) {
95
- // Find the commit that added VENDOR.md — that's the vendoring commit
96
- sinceRef = sh(
97
- `git log --diff-filter=A --format="%H" -- projects/api/src/core/VENDOR.md | tail -1`,
98
- ).trim();
99
- if (!sinceRef) {
100
- die(
101
- 'Could not find the commit that added VENDOR.md. Pass --since <sha> manually.',
102
- );
103
- }
104
- }
105
-
106
- // 4. Collect all commits since that ref that touched src/core/
107
- const gitLog = sh(
108
- `git log --format="%H%x09%s%x09%an%x09%aI" ${sinceRef}..HEAD -- projects/api/src/core/`,
109
- ).trim();
110
-
111
- if (!gitLog) {
112
- process.stdout.write(
113
- `No local commits found since ${sinceRef.substring(0, 8)} touching src/core/. Nothing to propose.\n`,
114
- );
115
- process.exit(0);
116
- }
117
-
118
- const commits: CommitInfo[] = gitLog
119
- .split('\n')
120
- .filter((line) => line.trim())
121
- .map((line) => {
122
- const [sha, subject, author, date] = line.split('\t');
123
- const filesOutput = sh(
124
- `git show --pretty="" --name-only ${sha} -- projects/api/src/core/`,
125
- ).trim();
126
- const files = filesOutput ? filesOutput.split('\n') : [];
127
-
128
- // Cosmetic check by message pattern
129
- let isCosmetic = false;
130
- let cosmeticReason: string | null = null;
131
- for (const pat of COSMETIC_PATTERNS) {
132
- if (pat.test(subject)) {
133
- isCosmetic = true;
134
- cosmeticReason = `commit-message matches ${pat.source}`;
135
- break;
136
- }
137
- }
138
-
139
- // Additional cosmetic check: if diff has only whitespace/formatting changes
140
- if (!isCosmetic) {
141
- const diff = sh(`git show --format="" ${sha} -- projects/api/src/core/`);
142
- // Normalize: drop all whitespace, quote style, trailing commas
143
- const normalized = diff
144
- .split('\n')
145
- .filter((l) => l.startsWith('+') || l.startsWith('-'))
146
- .filter((l) => !l.startsWith('+++') && !l.startsWith('---'))
147
- .map((l) => l.slice(1).replace(/\s+/g, '').replace(/['"`]/g, '').replace(/,$/, ''))
148
- .filter((l) => l.length > 0);
149
-
150
- // Count +/- with the same normalized content — if they cancel out, it's cosmetic
151
- const plus = normalized.filter((_, i) => diff.split('\n').filter((l) => l.startsWith('+') && !l.startsWith('+++'))[i]);
152
- // Simpler heuristic: if normalized plus == normalized minus, it's cosmetic
153
- const plusLines = diff.split('\n').filter((l) => l.startsWith('+') && !l.startsWith('+++')).map((l) => l.slice(1).replace(/\s+/g, ''));
154
- const minusLines = diff.split('\n').filter((l) => l.startsWith('-') && !l.startsWith('---')).map((l) => l.slice(1).replace(/\s+/g, ''));
155
- const plusSet = new Set(plusLines);
156
- const minusSet = new Set(minusLines);
157
- const plusOnlyCount = [...plusSet].filter((l) => !minusSet.has(l) && l.length > 0).length;
158
- const minusOnlyCount = [...minusSet].filter((l) => !plusSet.has(l) && l.length > 0).length;
159
- if (plusOnlyCount === 0 && minusOnlyCount === 0 && plusLines.length > 0) {
160
- isCosmetic = true;
161
- cosmeticReason = 'normalized diff is empty (whitespace/quotes only)';
162
- }
163
- }
164
-
165
- return {
166
- sha,
167
- shortSha: sha.substring(0, 8),
168
- subject,
169
- author,
170
- date,
171
- files,
172
- isCosmetic,
173
- cosmeticReason,
174
- };
175
- });
176
-
177
- // 5. Write output
178
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
179
- const outputDir = join(OUTPUT_BASE, timestamp);
180
- const diffsDir = join(outputDir, 'local-diffs');
181
- mkdirSync(diffsDir, { recursive: true });
182
-
183
- // Save JSON
184
- writeFileSync(
185
- join(outputDir, 'local-commits.json'),
186
- JSON.stringify(
187
- {
188
- baselineVersion,
189
- sinceRef,
190
- generated: new Date().toISOString(),
191
- total: commits.length,
192
- cosmetic: commits.filter((c) => c.isCosmetic).length,
193
- substantial: commits.filter((c) => !c.isCosmetic).length,
194
- commits,
195
- },
196
- null,
197
- 2,
198
- ),
199
- );
200
-
201
- // Save per-commit patch files (only substantial ones)
202
- for (const commit of commits.filter((c) => !c.isCosmetic)) {
203
- const patch = sh(`git show ${commit.sha} -- projects/api/src/core/`);
204
- writeFileSync(join(diffsDir, `${commit.shortSha}.patch`), patch);
205
- }
206
-
207
- // Save summary
208
- const substantialCommits = commits.filter((c) => !c.isCosmetic);
209
- const cosmeticCommits = commits.filter((c) => c.isCosmetic);
210
-
211
- const summary = `# Upstream-PR Candidates
212
-
213
- **Baseline version:** ${baselineVersion}
214
- **Since commit:** ${sinceRef.substring(0, 8)}
215
- **Generated:** ${new Date().toISOString()}
216
-
217
- ## Statistics
218
-
219
- - Total commits touching \`src/core/\`: ${commits.length}
220
- - Filtered as cosmetic: ${cosmeticCommits.length}
221
- - **Substantial (candidate pool):** ${substantialCommits.length}
222
-
223
- ## Substantial Commits (need manual categorization by the contributor agent)
224
-
225
- ${
226
- substantialCommits.length === 0
227
- ? '_No substantial local changes. Nothing to contribute._'
228
- : substantialCommits
229
- .map(
230
- (c) =>
231
- `### \`${c.shortSha}\` — ${c.subject}\n\n` +
232
- `- **Author:** ${c.author}\n` +
233
- `- **Date:** ${c.date}\n` +
234
- `- **Files:** ${c.files.length}\n` +
235
- c.files.map((f) => ` - ${f}`).join('\n') +
236
- `\n- **Patch:** \`local-diffs/${c.shortSha}.patch\`\n`,
237
- )
238
- .join('\n')
239
- }
240
-
241
- ## Filtered Cosmetic Commits
242
-
243
- ${
244
- cosmeticCommits.length === 0
245
- ? '_(none)_'
246
- : cosmeticCommits
247
- .map((c) => `- \`${c.shortSha}\` — ${c.subject} _(${c.cosmeticReason})_`)
248
- .join('\n')
249
- }
250
-
251
- ## Next Steps
252
-
253
- Run the contributor agent:
254
- \`\`\`
255
- /lt-dev:backend:contribute-nest-server-core
256
- \`\`\`
257
-
258
- It will:
259
- 1. Categorize each substantial commit (upstream-candidate / project-specific / unclear)
260
- 2. Check upstream HEAD for duplicates
261
- 3. Prepare candidate branches in a local upstream clone with reverse flatten-fix
262
- 4. Generate PR-body drafts for human review
263
- 5. Present a final list with \`gh pr create\` commands ready to run
264
- `;
265
-
266
- writeFileSync(join(outputDir, 'summary.md'), summary);
267
-
268
- process.stdout.write(`\nDone. Review:\n`);
269
- process.stdout.write(` cat ${outputDir}/summary.md\n`);