@lenne.tech/cli 1.9.5 → 1.10.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/README.md +83 -0
- package/build/cli.js +0 -1
- package/build/commands/blocks/add.js +0 -1
- package/build/commands/blocks/blocks.js +0 -1
- package/build/commands/claude/claude.js +0 -1
- package/build/commands/claude/plugins.js +0 -1
- package/build/commands/claude/shortcuts.js +0 -1
- package/build/commands/cli/cli.js +0 -1
- package/build/commands/cli/create.js +0 -1
- package/build/commands/cli/rename.js +0 -1
- package/build/commands/completion.js +34 -2
- package/build/commands/components/add.js +0 -1
- package/build/commands/components/components.js +0 -1
- package/build/commands/config/config.js +0 -1
- package/build/commands/config/help.js +0 -1
- package/build/commands/config/init.js +0 -1
- package/build/commands/config/show.js +0 -1
- package/build/commands/config/validate.js +0 -1
- package/build/commands/deployment/create.js +0 -1
- package/build/commands/deployment/deployment.js +0 -1
- package/build/commands/directus/directus.js +0 -1
- package/build/commands/directus/docker-setup.js +35 -3
- package/build/commands/directus/remove.js +0 -1
- package/build/commands/directus/typegen.js +0 -1
- package/build/commands/docs/docs.js +0 -1
- package/build/commands/docs/open.js +34 -2
- package/build/commands/doctor.js +0 -1
- package/build/commands/frontend/angular.js +0 -1
- package/build/commands/frontend/frontend.js +0 -1
- package/build/commands/frontend/nuxt.js +0 -1
- package/build/commands/fullstack/fullstack.js +0 -1
- package/build/commands/fullstack/init.js +119 -6
- package/build/commands/fullstack/update.js +129 -0
- package/build/commands/git/clean.js +0 -1
- package/build/commands/git/clear.js +0 -1
- package/build/commands/git/create.js +0 -1
- package/build/commands/git/force-pull.js +0 -1
- package/build/commands/git/get.js +0 -1
- package/build/commands/git/git.js +0 -1
- package/build/commands/git/install-scripts.js +0 -1
- package/build/commands/git/rebase.js +0 -1
- package/build/commands/git/rename.js +0 -1
- package/build/commands/git/reset.js +0 -1
- package/build/commands/git/squash.js +0 -1
- package/build/commands/git/undo.js +0 -1
- package/build/commands/git/update.js +0 -1
- package/build/commands/history.js +0 -1
- package/build/commands/lt.js +0 -1
- package/build/commands/mongodb/collection-export.js +35 -3
- package/build/commands/mongodb/mongodb.js +0 -1
- package/build/commands/mongodb/s3-restore.js +35 -3
- package/build/commands/npm/npm.js +0 -1
- package/build/commands/npm/reinit.js +0 -1
- package/build/commands/npm/update.js +0 -1
- package/build/commands/qdrant/delete.js +0 -1
- package/build/commands/qdrant/qdrant.js +0 -1
- package/build/commands/qdrant/stats.js +0 -1
- package/build/commands/redis/redis.js +0 -1
- package/build/commands/server/add-property.js +34 -5
- package/build/commands/server/create-secret.js +34 -2
- package/build/commands/server/create.js +41 -4
- package/build/commands/server/module.js +62 -27
- package/build/commands/server/object.js +30 -7
- package/build/commands/server/permissions.js +20 -7
- package/build/commands/server/server.js +0 -1
- package/build/commands/server/set-secrets.js +0 -1
- package/build/commands/server/test.js +7 -2
- package/build/commands/starter/chrome-extension.js +0 -1
- package/build/commands/starter/starter.js +0 -1
- package/build/commands/status.js +13 -2
- package/build/commands/templates/list.js +0 -1
- package/build/commands/templates/llm.js +0 -1
- package/build/commands/templates/templates.js +0 -1
- package/build/commands/tools/crypt.js +0 -1
- package/build/commands/tools/install-scripts.js +0 -1
- package/build/commands/tools/jwt-read.js +0 -1
- package/build/commands/tools/regex.js +34 -2
- package/build/commands/tools/sha256.js +0 -1
- package/build/commands/tools/tools.js +0 -1
- package/build/commands/typescript/create.js +0 -1
- package/build/commands/typescript/playground.js +0 -1
- package/build/commands/typescript/typescript.js +0 -1
- package/build/commands/update.js +0 -1
- package/build/config/vendor-runtime-deps.json +9 -0
- package/build/extensions/api-mode.js +19 -4
- package/build/extensions/config.js +35 -3
- package/build/extensions/frontend-helper.js +0 -1
- package/build/extensions/git.js +0 -1
- package/build/extensions/history.js +0 -1
- package/build/extensions/logger.js +0 -1
- package/build/extensions/package-manager.js +0 -1
- package/build/extensions/parse-properties.js +0 -1
- package/build/extensions/server.js +1095 -6
- package/build/extensions/template.js +0 -1
- package/build/extensions/tools.js +0 -1
- package/build/extensions/typescript.js +35 -3
- package/build/interfaces/ServerProps.interface.js +0 -1
- package/build/interfaces/extended-gluegun-command.js +0 -1
- package/build/interfaces/extended-gluegun-toolbox.js +0 -1
- package/build/interfaces/lt-config.interface.js +0 -1
- package/build/lib/claude-cli.js +0 -1
- package/build/lib/fallback-scanner.js +0 -1
- package/build/lib/framework-detection.js +167 -0
- package/build/lib/json-utils.js +0 -1
- package/build/lib/marketplace.js +0 -1
- package/build/lib/nuxt-base-components.js +40 -5
- package/build/lib/plugin-utils.js +0 -1
- package/build/lib/shell-config.js +0 -1
- package/build/lib/validation.js +0 -1
- package/build/templates/nest-server-module/inputs/template-create.input.ts.ejs +1 -1
- package/build/templates/nest-server-module/inputs/template.input.ts.ejs +1 -1
- package/build/templates/nest-server-module/outputs/template-fac-result.output.ts.ejs +1 -1
- package/build/templates/nest-server-module/template.controller.ts.ejs +1 -1
- package/build/templates/nest-server-module/template.model.ts.ejs +1 -1
- package/build/templates/nest-server-module/template.module.ts.ejs +1 -1
- package/build/templates/nest-server-module/template.resolver.ts.ejs +1 -1
- package/build/templates/nest-server-module/template.service.ts.ejs +1 -1
- package/build/templates/nest-server-object/template-create.input.ts.ejs +1 -1
- package/build/templates/nest-server-object/template.input.ts.ejs +1 -1
- package/build/templates/nest-server-object/template.object.ts.ejs +1 -1
- package/build/templates/nest-server-tests/tests.e2e-spec.ts.ejs +1 -1
- package/build/templates/vendor-scripts/check-vendor-freshness.mjs +131 -0
- package/build/templates/vendor-scripts/propose-upstream-pr.ts +269 -0
- package/build/templates/vendor-scripts/sync-from-upstream.ts +250 -0
- package/docs/commands.md +13 -0
- package/package.json +22 -13
- package/tsconfig.json +4 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApiCommonErrorResponses, FilterArgs, RoleEnum, Roles } from '
|
|
1
|
+
import { ApiCommonErrorResponses, FilterArgs, RoleEnum, Roles } from '<%= props.frameworkImport %>';
|
|
2
2
|
import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
|
|
3
3
|
import { ApiOkResponse } from '@nestjs/swagger';
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Restricted, RoleEnum, equalIds<% if (props.mappings.includes('mapClasses')) { %>, mapClasses<% } %>, UnifiedField} from '
|
|
1
|
+
import { Restricted, RoleEnum, equalIds<% if (props.mappings.includes('mapClasses')) { %>, mapClasses<% } %>, UnifiedField} from '<%= props.frameworkImport %>';
|
|
2
2
|
<% if (props.isGql) { %>
|
|
3
3
|
import { ObjectType } from '@nestjs/graphql';
|
|
4
4
|
<% } %>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConfigService } from '
|
|
1
|
+
import { ConfigService } from '<%= props.frameworkImport %>';
|
|
2
2
|
import { Module, forwardRef } from '@nestjs/common';
|
|
3
3
|
import { MongooseModule } from '@nestjs/mongoose';
|
|
4
4
|
<% if ((props.controller === 'GraphQL') || (props.controller === 'Both')) { -%>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FilterArgs, GraphQLServiceOptions, RoleEnum, Roles, ServiceOptions } from '
|
|
1
|
+
import { FilterArgs, GraphQLServiceOptions, RoleEnum, Roles, ServiceOptions } from '<%= props.frameworkImport %>';
|
|
2
2
|
import { Inject } from '@nestjs/common';
|
|
3
3
|
import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql';
|
|
4
4
|
import { PubSub } from 'graphql-subscriptions';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConfigService, CrudService, ServiceOptions, assignPlain } from '
|
|
1
|
+
import { ConfigService, CrudService, ServiceOptions, assignPlain } from '<%= props.frameworkImport %>';
|
|
2
2
|
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
|
|
3
3
|
import { InjectModel } from '@nestjs/mongoose';
|
|
4
4
|
<% if (props.isGql) { %>import { PubSub } from 'graphql-subscriptions';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Restricted, RoleEnum, UnifiedField } from '
|
|
1
|
+
import { Restricted, RoleEnum, UnifiedField } from '<%= props.frameworkImport %>';
|
|
2
2
|
import { InputType } from '@nestjs/graphql';
|
|
3
3
|
import { <%= props.namePascal %>Input } from './<%= props.nameKebab %>.input';<%- props.imports %>
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CoreModel, Restricted, RoleEnum<% if (props.mappings.includes('mapClasses')) { %>, mapClasses<% } %>, UnifiedField } from '
|
|
1
|
+
import { CoreModel, Restricted, RoleEnum<% if (props.mappings.includes('mapClasses')) { %>, mapClasses<% } %>, UnifiedField } from '<%= props.frameworkImport %>';
|
|
2
2
|
import { ObjectType } from '@nestjs/graphql';
|
|
3
3
|
import { Schema as MongooseSchema, SchemaFactory } from '@nestjs/mongoose';
|
|
4
4
|
import { Document } from 'mongoose';<%- props.imports %>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RoleEnum, TestGraphQLType, TestHelper } from '
|
|
1
|
+
import { RoleEnum, TestGraphQLType, TestHelper } from '<%= props.frameworkImport %>';
|
|
2
2
|
import { Test, TestingModule } from '@nestjs/testing';
|
|
3
3
|
import { PubSub } from 'graphql-subscriptions';
|
|
4
4
|
import envConfig from '../src/config.env';
|
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
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`);
|