@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/README.md +5 -3
- package/build/commands/config/validate.js +2 -0
- package/build/commands/frontend/convert-mode.js +198 -0
- package/build/commands/fullstack/convert-mode.js +368 -0
- package/build/commands/fullstack/init.js +44 -2
- package/build/commands/fullstack/update.js +49 -1
- package/build/commands/server/convert-mode.js +197 -0
- package/build/commands/status.js +81 -2
- package/build/config/vendor-frontend-runtime-deps.json +4 -0
- package/build/extensions/frontend-helper.js +652 -0
- package/build/extensions/server.js +515 -68
- package/build/lib/frontend-framework-detection.js +129 -0
- package/docs/LT-ECOSYSTEM-GUIDE.md +972 -0
- package/docs/VENDOR-MODE-WORKFLOW.md +471 -0
- package/docs/commands.md +196 -0
- package/docs/lt.config.md +9 -7
- package/package.json +2 -1
- package/build/templates/vendor-scripts/check-vendor-freshness.mjs +0 -131
- package/build/templates/vendor-scripts/propose-upstream-pr.ts +0 -269
- package/build/templates/vendor-scripts/sync-from-upstream.ts +0 -250
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
|
-
/
|
|
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
|
|
68
|
-
1.
|
|
69
|
-
2.
|
|
70
|
-
3.
|
|
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 (
|
|
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 (
|
|
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.
|
|
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`);
|