@lenne.tech/cli 1.12.0 → 1.14.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 +31 -0
- package/build/commands/config/validate.js +36 -2
- package/build/commands/fullstack/init.js +44 -18
- package/build/commands/server/create.js +53 -26
- package/build/commands/tools/crawl.js +307 -0
- package/build/extensions/server.js +72 -43
- package/build/lib/browser-fetcher.js +139 -0
- package/build/lib/crawler.js +661 -0
- package/docs/LT-ECOSYSTEM-GUIDE.md +1 -0
- package/docs/commands.md +57 -1
- package/docs/lt.config.md +37 -0
- package/package.json +8 -1
|
@@ -633,13 +633,16 @@ class Server {
|
|
|
633
633
|
setupServer(dest, options) {
|
|
634
634
|
return __awaiter(this, void 0, void 0, function* () {
|
|
635
635
|
const { apiMode: apiModeHelper, patching, system, template, templateHelper } = this.toolbox;
|
|
636
|
-
const { apiMode, author = '', branch, copyPath, description = '', frameworkMode = 'npm', frameworkUpstreamBranch, linkPath, name, projectDir, skipInstall = false, skipPatching = false, } = options;
|
|
636
|
+
const { apiMode, author = '', branch, copyPath, description = '', experimental = false, frameworkMode = 'npm', frameworkUpstreamBranch, linkPath, name, projectDir, skipInstall = false, skipPatching = false, } = options;
|
|
637
|
+
const repoUrl = experimental
|
|
638
|
+
? 'https://github.com/lenneTech/nest-base.git'
|
|
639
|
+
: 'https://github.com/lenneTech/nest-server-starter.git';
|
|
637
640
|
// Setup template
|
|
638
641
|
const result = yield templateHelper.setup(dest, {
|
|
639
642
|
branch,
|
|
640
643
|
copyPath,
|
|
641
644
|
linkPath,
|
|
642
|
-
repoUrl
|
|
645
|
+
repoUrl,
|
|
643
646
|
});
|
|
644
647
|
if (!result.success) {
|
|
645
648
|
return { method: result.method, path: result.path, success: false };
|
|
@@ -651,18 +654,20 @@ class Server {
|
|
|
651
654
|
// Apply patches (config.env.ts, package.json, main.ts, meta.json)
|
|
652
655
|
if (!skipPatching) {
|
|
653
656
|
try {
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
.
|
|
665
|
-
|
|
657
|
+
if (!experimental) {
|
|
658
|
+
// Generate README
|
|
659
|
+
yield template.generate({
|
|
660
|
+
props: { description, name },
|
|
661
|
+
target: `${dest}/README.md`,
|
|
662
|
+
template: 'nest-server-starter/README.md.ejs',
|
|
663
|
+
});
|
|
664
|
+
// Replace secret or private keys and update database names via AST
|
|
665
|
+
this.patchConfigEnvTs(`${dest}/src/config.env.ts`, projectDir);
|
|
666
|
+
// Update Swagger configuration in main.ts
|
|
667
|
+
yield patching.update(`${dest}/src/main.ts`, (content) => content
|
|
668
|
+
.replace(/\.setTitle\('.*?'\)/, `.setTitle('${name}')`)
|
|
669
|
+
.replace(/\.setDescription\('.*?'\)/, `.setDescription('${description || name}')`));
|
|
670
|
+
}
|
|
666
671
|
// Update package.json
|
|
667
672
|
yield patching.update(`${dest}/package.json`, (config) => {
|
|
668
673
|
config.author = author;
|
|
@@ -674,13 +679,15 @@ class Server {
|
|
|
674
679
|
config.version = '0.0.1';
|
|
675
680
|
return config;
|
|
676
681
|
});
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
682
|
+
if (!experimental) {
|
|
683
|
+
// Update meta.json if exists
|
|
684
|
+
if (this.filesystem.exists(`${dest}/src/meta`)) {
|
|
685
|
+
yield patching.update(`${dest}/src/meta`, (config) => {
|
|
686
|
+
config.name = name;
|
|
687
|
+
config.description = description;
|
|
688
|
+
return config;
|
|
689
|
+
});
|
|
690
|
+
}
|
|
684
691
|
}
|
|
685
692
|
}
|
|
686
693
|
catch (err) {
|
|
@@ -703,7 +710,7 @@ class Server {
|
|
|
703
710
|
// manifest (same dance as in setupServerForFullstack).
|
|
704
711
|
let standaloneVendorUpstreamDeps = {};
|
|
705
712
|
let standaloneVendorCoreEssentials = [];
|
|
706
|
-
if (frameworkMode === 'vendor') {
|
|
713
|
+
if (!experimental && frameworkMode === 'vendor') {
|
|
707
714
|
try {
|
|
708
715
|
const converted = yield this.convertCloneToVendored({
|
|
709
716
|
dest,
|
|
@@ -718,7 +725,7 @@ class Server {
|
|
|
718
725
|
standaloneVendorCoreEssentials = this.readApiModeGraphqlEssentials(dest);
|
|
719
726
|
}
|
|
720
727
|
// Process API mode (before install so package.json is correct)
|
|
721
|
-
if (apiMode) {
|
|
728
|
+
if (!experimental && apiMode) {
|
|
722
729
|
try {
|
|
723
730
|
yield apiModeHelper.processApiMode(dest, apiMode);
|
|
724
731
|
}
|
|
@@ -727,7 +734,7 @@ class Server {
|
|
|
727
734
|
}
|
|
728
735
|
}
|
|
729
736
|
// Restore core essentials after processApiMode stripped them (vendor + REST only).
|
|
730
|
-
if (frameworkMode === 'vendor' && apiMode === 'Rest') {
|
|
737
|
+
if (!experimental && frameworkMode === 'vendor' && apiMode === 'Rest') {
|
|
731
738
|
try {
|
|
732
739
|
this.restoreVendorCoreEssentials({
|
|
733
740
|
dest,
|
|
@@ -740,9 +747,11 @@ class Server {
|
|
|
740
747
|
}
|
|
741
748
|
}
|
|
742
749
|
// Patch CLAUDE.md with API mode info
|
|
743
|
-
|
|
750
|
+
if (!experimental) {
|
|
751
|
+
this.patchClaudeMdApiMode(dest, apiMode);
|
|
752
|
+
}
|
|
744
753
|
// Install packages
|
|
745
|
-
if (!skipInstall) {
|
|
754
|
+
if (!skipInstall && !experimental) {
|
|
746
755
|
try {
|
|
747
756
|
const { pm } = this.toolbox;
|
|
748
757
|
yield system.run(`cd "${dest}" && ${pm.install(pm.detect(dest))}`);
|
|
@@ -771,7 +780,10 @@ class Server {
|
|
|
771
780
|
setupServerForFullstack(dest, options) {
|
|
772
781
|
return __awaiter(this, void 0, void 0, function* () {
|
|
773
782
|
const { apiMode: apiModeHelper, templateHelper } = this.toolbox;
|
|
774
|
-
const { apiMode, branch, copyPath, frameworkMode = 'npm', frameworkUpstreamBranch, linkPath, name, projectDir, } = options;
|
|
783
|
+
const { apiMode, branch, copyPath, experimental = false, frameworkMode = 'npm', frameworkUpstreamBranch, linkPath, name, projectDir, } = options;
|
|
784
|
+
const repoUrl = experimental
|
|
785
|
+
? 'https://github.com/lenneTech/nest-base.git'
|
|
786
|
+
: 'https://github.com/lenneTech/nest-server-starter';
|
|
775
787
|
// Both npm and vendor mode clone nest-server-starter as the base. The
|
|
776
788
|
// starter ships the minimal consumer conventions a project needs
|
|
777
789
|
// (src/server/common/models/persistence.model.ts, src/server/modules/user/,
|
|
@@ -791,7 +803,7 @@ class Server {
|
|
|
791
803
|
branch,
|
|
792
804
|
copyPath,
|
|
793
805
|
linkPath,
|
|
794
|
-
repoUrl
|
|
806
|
+
repoUrl,
|
|
795
807
|
});
|
|
796
808
|
if (!result.success) {
|
|
797
809
|
return { method: result.method, path: result.path, success: false };
|
|
@@ -801,18 +813,33 @@ class Server {
|
|
|
801
813
|
return { method: 'link', path: result.path, success: true };
|
|
802
814
|
}
|
|
803
815
|
// Apply minimal patches for fullstack
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
816
|
+
if (!experimental) {
|
|
817
|
+
try {
|
|
818
|
+
// Write meta.json
|
|
819
|
+
this.filesystem.write(`${dest}/src/meta.json`, {
|
|
820
|
+
description: `API for ${name} app`,
|
|
821
|
+
name: `${name}-api-server`,
|
|
822
|
+
version: '0.0.0',
|
|
823
|
+
});
|
|
824
|
+
// Replace secret or private keys and update database names via AST
|
|
825
|
+
this.patchConfigEnvTs(`${dest}/src/config.env.ts`, projectDir);
|
|
826
|
+
}
|
|
827
|
+
catch (err) {
|
|
828
|
+
return { method: result.method, path: dest, success: false };
|
|
829
|
+
}
|
|
813
830
|
}
|
|
814
|
-
|
|
815
|
-
|
|
831
|
+
else {
|
|
832
|
+
try {
|
|
833
|
+
yield this.toolbox.patching.update(`${dest}/package.json`, (config) => {
|
|
834
|
+
config.name = projectDir;
|
|
835
|
+
config.description = `API for ${name} app`;
|
|
836
|
+
config.version = '0.0.0';
|
|
837
|
+
return config;
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
catch (err) {
|
|
841
|
+
return { method: result.method, path: dest, success: false };
|
|
842
|
+
}
|
|
816
843
|
}
|
|
817
844
|
// Clean up copied template artifacts
|
|
818
845
|
if (result.method === 'copy') {
|
|
@@ -833,7 +860,7 @@ class Server {
|
|
|
833
860
|
// without hard-coding package lists.
|
|
834
861
|
let vendorUpstreamDeps = {};
|
|
835
862
|
let vendorCoreEssentials = [];
|
|
836
|
-
if (frameworkMode === 'vendor') {
|
|
863
|
+
if (!experimental && frameworkMode === 'vendor') {
|
|
837
864
|
try {
|
|
838
865
|
const converted = yield this.convertCloneToVendored({
|
|
839
866
|
dest,
|
|
@@ -856,7 +883,7 @@ class Server {
|
|
|
856
883
|
vendorCoreEssentials = this.readApiModeGraphqlEssentials(dest);
|
|
857
884
|
}
|
|
858
885
|
// Process API mode (before install which happens at monorepo level)
|
|
859
|
-
if (apiMode) {
|
|
886
|
+
if (!experimental && apiMode) {
|
|
860
887
|
try {
|
|
861
888
|
yield apiModeHelper.processApiMode(dest, apiMode);
|
|
862
889
|
}
|
|
@@ -867,7 +894,7 @@ class Server {
|
|
|
867
894
|
// In vendor mode + REST, re-add the graphql essentials that
|
|
868
895
|
// processApiMode just stripped. Both and GraphQL keep all packages
|
|
869
896
|
// by construction and don't need restoration.
|
|
870
|
-
if (frameworkMode === 'vendor' && apiMode === 'Rest') {
|
|
897
|
+
if (!experimental && frameworkMode === 'vendor' && apiMode === 'Rest') {
|
|
871
898
|
try {
|
|
872
899
|
this.restoreVendorCoreEssentials({
|
|
873
900
|
dest,
|
|
@@ -881,7 +908,9 @@ class Server {
|
|
|
881
908
|
}
|
|
882
909
|
}
|
|
883
910
|
// Patch CLAUDE.md with API mode info
|
|
884
|
-
|
|
911
|
+
if (!experimental) {
|
|
912
|
+
this.patchClaudeMdApiMode(dest, apiMode);
|
|
913
|
+
}
|
|
885
914
|
return { method: result.method, path: dest, success: true };
|
|
886
915
|
});
|
|
887
916
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.createBrowserFetcher = createBrowserFetcher;
|
|
13
|
+
/**
|
|
14
|
+
* Headless-browser HTML fetcher for single-page applications.
|
|
15
|
+
*
|
|
16
|
+
* Mirrors the chrome-md content script's PageReadyDetector:
|
|
17
|
+
* waits for the network to settle, then returns the fully hydrated
|
|
18
|
+
* HTML so Defuddle can extract the real content instead of the
|
|
19
|
+
* pre-render shell.
|
|
20
|
+
*
|
|
21
|
+
* Uses `playwright-core` with a three-tier strategy:
|
|
22
|
+
* 1. System Chrome / Edge via `channel: 'chrome' | 'msedge'`.
|
|
23
|
+
* 2. Playwright's own bundled Chromium (if already installed).
|
|
24
|
+
* 3. Auto-install Playwright's Chromium (`npx playwright install
|
|
25
|
+
* chromium`) and retry — opt-in via `autoInstall`.
|
|
26
|
+
*/
|
|
27
|
+
const child_process_1 = require("child_process");
|
|
28
|
+
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (compatible; lenneTech-CLI-Crawler/1.0; +https://lenne.tech)';
|
|
29
|
+
/**
|
|
30
|
+
* Try to construct a browser fetcher. Prefers a system Chrome /
|
|
31
|
+
* Edge via Playwright channels, falls back to Playwright's bundled
|
|
32
|
+
* Chromium, and (optionally) auto-installs Chromium on demand.
|
|
33
|
+
*/
|
|
34
|
+
function createBrowserFetcher() {
|
|
35
|
+
return __awaiter(this, arguments, void 0, function* (options = {}) {
|
|
36
|
+
const log = options.onLog || (() => undefined);
|
|
37
|
+
const reasons = [];
|
|
38
|
+
const { chromium } = require('playwright-core');
|
|
39
|
+
// 1. System Chrome.
|
|
40
|
+
const chromeFetcher = yield launch(chromium, { channel: 'chrome' }, options, 'system-chrome').catch((error) => {
|
|
41
|
+
reasons.push(`channel:chrome: ${error.message}`);
|
|
42
|
+
return null;
|
|
43
|
+
});
|
|
44
|
+
if (chromeFetcher) {
|
|
45
|
+
log(`Browser engine: ${chromeFetcher.engine}`);
|
|
46
|
+
return chromeFetcher;
|
|
47
|
+
}
|
|
48
|
+
// 2. System Edge (Windows fallback, also common on macOS).
|
|
49
|
+
const edgeFetcher = yield launch(chromium, { channel: 'msedge' }, options, 'system-edge').catch((error) => {
|
|
50
|
+
reasons.push(`channel:msedge: ${error.message}`);
|
|
51
|
+
return null;
|
|
52
|
+
});
|
|
53
|
+
if (edgeFetcher) {
|
|
54
|
+
log(`Browser engine: ${edgeFetcher.engine}`);
|
|
55
|
+
return edgeFetcher;
|
|
56
|
+
}
|
|
57
|
+
// 3. Playwright's bundled Chromium.
|
|
58
|
+
const bundledFetcher = yield launch(chromium, {}, options, 'playwright-chromium').catch((error) => {
|
|
59
|
+
reasons.push(`playwright-chromium: ${error.message}`);
|
|
60
|
+
return null;
|
|
61
|
+
});
|
|
62
|
+
if (bundledFetcher) {
|
|
63
|
+
log(`Browser engine: ${bundledFetcher.engine}`);
|
|
64
|
+
return bundledFetcher;
|
|
65
|
+
}
|
|
66
|
+
// 4. Optional auto-install, then retry Playwright's chromium.
|
|
67
|
+
if (options.autoInstall) {
|
|
68
|
+
log('No browser available — installing Playwright chromium (one-time download, ~170 MB)…');
|
|
69
|
+
try {
|
|
70
|
+
yield runNpx(['playwright', 'install', 'chromium']);
|
|
71
|
+
const retry = yield launch(chromium, {}, options, 'playwright-chromium');
|
|
72
|
+
if (retry) {
|
|
73
|
+
log(`Browser engine: ${retry.engine}`);
|
|
74
|
+
return retry;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
reasons.push(`auto-install: ${error instanceof Error ? error.message : String(error)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
throw new Error([
|
|
82
|
+
'Could not start a headless browser for SPA rendering.',
|
|
83
|
+
...reasons.map((r) => ` - ${r}`),
|
|
84
|
+
'',
|
|
85
|
+
'Fix one of these:',
|
|
86
|
+
' 1. Install Google Chrome or Microsoft Edge (Playwright picks them up automatically).',
|
|
87
|
+
' 2. Install Playwright browsers manually: `npx playwright install chromium`.',
|
|
88
|
+
' 3. Re-run with --install-browser to let the CLI install them.',
|
|
89
|
+
].join('\n'));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function launch(chromium, launchOptions, options, engineLabel) {
|
|
93
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
94
|
+
const browser = yield chromium.launch(Object.assign(Object.assign({}, launchOptions), { headless: true }));
|
|
95
|
+
const context = yield browser.newContext({
|
|
96
|
+
userAgent: options.userAgent || DEFAULT_USER_AGENT,
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
close: () => __awaiter(this, void 0, void 0, function* () {
|
|
100
|
+
yield context.close();
|
|
101
|
+
yield browser.close();
|
|
102
|
+
}),
|
|
103
|
+
engine: engineLabel,
|
|
104
|
+
fetch: (url) => __awaiter(this, void 0, void 0, function* () {
|
|
105
|
+
var _a;
|
|
106
|
+
const page = yield context.newPage();
|
|
107
|
+
try {
|
|
108
|
+
yield page.goto(url, {
|
|
109
|
+
timeout: (_a = options.maxWaitMs) !== null && _a !== void 0 ? _a : 20000,
|
|
110
|
+
waitUntil: 'networkidle',
|
|
111
|
+
});
|
|
112
|
+
if (options.extraWaitMs) {
|
|
113
|
+
yield page.waitForTimeout(options.extraWaitMs);
|
|
114
|
+
}
|
|
115
|
+
return yield page.content();
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
yield page.close();
|
|
119
|
+
}
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Run an `npx` command, streaming its output to the current stdio.
|
|
126
|
+
* Resolves on exit code 0, rejects otherwise.
|
|
127
|
+
*/
|
|
128
|
+
function runNpx(args) {
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
const child = (0, child_process_1.spawn)('npx', args, { shell: false, stdio: 'inherit' });
|
|
131
|
+
child.on('error', reject);
|
|
132
|
+
child.on('exit', (code) => {
|
|
133
|
+
if (code === 0)
|
|
134
|
+
resolve();
|
|
135
|
+
else
|
|
136
|
+
reject(new Error(`npx ${args.join(' ')} exited with code ${code}`));
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|